feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { ResponsesChatTransport, createResponseRequest } from "@/features/agent";
|
||||
import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { env } from "@/shared/config/env";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
import { findOwnedWorkbenchProject } from "../api/project-client";
|
||||
import type { InvestigationDiscussion } from "../api/investigation-report";
|
||||
import {
|
||||
FrontendActionExecutor,
|
||||
agentTaskDiscussionContextSchema,
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
agentBootstrapKey,
|
||||
agentSessionsKey,
|
||||
createAgentApiClient,
|
||||
useAgentMessageInbox,
|
||||
fetchAgentBootstrap,
|
||||
fetchAgentSessions,
|
||||
resolveAgentModelSelection,
|
||||
hasPendingAgentInput,
|
||||
isUiEnvelopeAllowed,
|
||||
parseUiEnvelopePayload,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
type AgentConnectionStatus,
|
||||
type AgentEventTriggerContext,
|
||||
type AgentModelOption,
|
||||
type AgentInputDelivery,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionRequest,
|
||||
type AgentQuestionRequest,
|
||||
@@ -68,6 +71,8 @@ export function useWorkbenchAgent({
|
||||
const mobileCollapseTimerRef = useRef<number | null>(null);
|
||||
const sessionStreamAbortRef = useRef<AbortController | null>(null);
|
||||
const projectIdPromiseRef = useRef<Promise<string> | null>(null);
|
||||
const [conversationScope, setConversationScope] = useState<"personal" | "project">("project");
|
||||
const conversationScopeRef = useRef<"personal" | "project">("project");
|
||||
const getProjectId = useCallback(() => {
|
||||
projectIdPromiseRef.current ??= findOwnedWorkbenchProject(getAccessToken).then(
|
||||
({ project }) => project.project_id,
|
||||
@@ -78,7 +83,15 @@ export function useWorkbenchAgent({
|
||||
);
|
||||
return projectIdPromiseRef.current;
|
||||
}, [getAccessToken]);
|
||||
const clientRef = useRef(createAgentApiClient(undefined, { getAccessToken, getProjectId }));
|
||||
const getConversationProjectId = useCallback(() =>
|
||||
conversationScopeRef.current === "personal" ? Promise.resolve(undefined) : getProjectId(), [getProjectId]);
|
||||
const clientRef = useRef(createAgentApiClient(undefined, { getAccessToken, getProjectId: getConversationProjectId }));
|
||||
const scopeClient = useMemo(() => createAgentApiClient(undefined, {
|
||||
getAccessToken,
|
||||
getProjectId: conversationScope === "project" ? getProjectId : undefined
|
||||
}), [conversationScope, getAccessToken, getProjectId]);
|
||||
const generatePromptSuggestions = useCallback((...args: Parameters<typeof clientRef.current.generatePromptSuggestions>) =>
|
||||
clientRef.current.generatePromptSuggestions(...args), []);
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const approvalModeRef = useRef<AgentApprovalMode>("request");
|
||||
const registryRef = useRef<UIRegistry | null>(null);
|
||||
@@ -92,7 +105,7 @@ export function useWorkbenchAgent({
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const [sessionPurpose, setSessionPurpose] = useState<AgentSessionPurpose>("ordinary_chat");
|
||||
const [sessionTaskId, setSessionTaskId] = useState<string | null>(null);
|
||||
const taskDiscussionRef = useRef<{ sessionId: string; context: AgentTaskDiscussionContext } | null>(null);
|
||||
const [taskDiscussion, setTaskDiscussion] = useState<{ sessionId: string; context: AgentTaskDiscussionContext; reportGeneration?: number | null } | null>(null);
|
||||
const [registry, setRegistry] = useState<UIRegistry | null>(null);
|
||||
const [sessionTitle, setSessionTitle] = useState("新对话");
|
||||
const [statusLabel, setStatusLabel] = useState("Agent 后端连接中");
|
||||
@@ -101,11 +114,17 @@ export function useWorkbenchAgent({
|
||||
const [resumedStreaming, setResumedStreaming] = useState(false);
|
||||
const [modelOptions, setModelOptions] = useState<AgentModelOption[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
const [inputDelivery, setInputDelivery] = useState<AgentInputDelivery>("steer");
|
||||
const selectedModelRef = useRef("");
|
||||
useEffect(() => { selectedModelRef.current = selectedModel; }, [selectedModel]);
|
||||
const [promptDraft, setPromptDraft] = useState("");
|
||||
const [promptFocusRequest, setPromptFocusRequest] = useState(0);
|
||||
const [approvalMode, setApprovalModeState] = useState<AgentApprovalMode>("request");
|
||||
const [streamRenderState, setStreamRenderState] = useState<AgentStreamRenderState>({});
|
||||
const [permissionOverrides, setPermissionOverrides] = useState<Record<string, PermissionOverride>>({});
|
||||
const [questionOverrides, setQuestionOverrides] = useState<Record<string, QuestionOverride>>({});
|
||||
const agentMessageInbox = useAgentMessageInbox(clientRef.current, sessionPurpose === "ordinary_chat" && conversationScope === "project" ? sessionId : null);
|
||||
const refreshAgentMessageInbox = agentMessageInbox.refresh;
|
||||
|
||||
useEffect(() => {
|
||||
sessionIdRef.current = sessionId;
|
||||
@@ -154,6 +173,19 @@ export function useWorkbenchAgent({
|
||||
[frontendActionExecutor]
|
||||
);
|
||||
|
||||
const credentialRequestsRef = useRef(new Set<string>());
|
||||
const handleCredentialRefresh = useCallback((data: unknown) => {
|
||||
const requestId = getDataString(data, "request_id");
|
||||
const activeSessionId = getDataString(data, "session_id") ?? sessionIdRef.current;
|
||||
if (!requestId || !activeSessionId || activeSessionId !== sessionIdRef.current) return;
|
||||
if (credentialRequestsRef.current.has(requestId)) return;
|
||||
credentialRequestsRef.current.add(requestId);
|
||||
void clientRef.current.refreshCredential(activeSessionId, requestId).catch(() => {
|
||||
credentialRequestsRef.current.delete(requestId);
|
||||
setStatusLabel("会话凭据续期失败,请重新登录后重试");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const claimEnvelope = useCallback((payload: UIEnvelopePayload, activeSessionId: string) => {
|
||||
if (payload.session_id !== activeSessionId) return false;
|
||||
const key = `${activeSessionId}:${payload.envelope_id}`;
|
||||
@@ -183,6 +215,11 @@ export function useWorkbenchAgent({
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (part.type === "data-agent_message") { refreshAgentMessageInbox(); return; }
|
||||
if (part.type === "data-user_input" && part.data.status === "delivered") {
|
||||
chatRef.current.setMessages((current) => applySessionStreamEvent(current, { type: "user_input", data: part.data }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "data-progress") {
|
||||
const title = getDataString(part.data, "title");
|
||||
@@ -191,6 +228,7 @@ export function useWorkbenchAgent({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (part.type === "data-credential_refresh_required") { handleCredentialRefresh(part.data); return; }
|
||||
if (part.type === "data-frontend_action") { void handleFrontendAction(part.data); return; }
|
||||
|
||||
if (part.type === "data-session_title") {
|
||||
@@ -225,37 +263,37 @@ export function useWorkbenchAgent({
|
||||
});
|
||||
});
|
||||
},
|
||||
[applySessionId, claimEnvelope, handleFrontendAction, onUiEnvelope]
|
||||
[refreshAgentMessageInbox, applySessionId, claimEnvelope, handleCredentialRefresh, handleFrontendAction, onUiEnvelope]
|
||||
);
|
||||
|
||||
const transport = useMemo(
|
||||
() =>
|
||||
new DefaultChatTransport<AgentUiMessage>({
|
||||
api: `${env.TJWATER_AGENT_API_BASE_URL.replace(/\/$/, "")}/api/v1/agent/chat/stream`,
|
||||
new ResponsesChatTransport<AgentUiMessage>({
|
||||
api: `${env.TJWATER_AGENT_URL.replace(/\/$/, "")}/api/v1/responses`,
|
||||
headers: async (): Promise<Record<string, string>> => {
|
||||
const accessToken = await getAccessToken?.();
|
||||
const projectId = await getProjectId();
|
||||
const projectId = await getConversationProjectId();
|
||||
return {
|
||||
...(projectId ? { "X-Project-Id": projectId } : {}),
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
|
||||
};
|
||||
},
|
||||
prepareSendMessagesRequest({ id, messages, body, trigger, messageId }) {
|
||||
return {
|
||||
body: {
|
||||
...body,
|
||||
id,
|
||||
messages,
|
||||
trigger,
|
||||
messageId,
|
||||
session_id: readBodyString(body, "session_id") ?? sessionIdRef.current ?? undefined,
|
||||
approval_mode: readBodyString(body, "approval_mode") ?? approvalModeRef.current,
|
||||
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1"] : []
|
||||
}
|
||||
};
|
||||
async prepareSendMessagesRequest({ messages, body }) {
|
||||
const catalog = await scopeClient.getModels();
|
||||
const model = resolveAgentModelSelection(catalog, readBodyString(body, "model") ?? selectedModelRef.current);
|
||||
setModelOptions(catalog.models);
|
||||
setSelectedModel(model);
|
||||
selectedModelRef.current = model;
|
||||
if (!model) throw new Error("Agent 后端当前没有可用模型,请检查模型配置。");
|
||||
return { body: createResponseRequest(messages, {
|
||||
conversation: readBodyString(body, "session_id") ?? sessionIdRef.current ?? undefined,
|
||||
model, approvalMode: readBodyString(body, "approval_mode") ?? approvalModeRef.current,
|
||||
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1"] : [],
|
||||
triggerContext: body?.trigger_context, taskContext: body?.task_context
|
||||
}) };
|
||||
}
|
||||
}),
|
||||
[getAccessToken, getProjectId]
|
||||
[getAccessToken, getConversationProjectId, scopeClient]
|
||||
);
|
||||
|
||||
const chat = useChat<AgentUiMessage>({
|
||||
@@ -287,7 +325,7 @@ export function useWorkbenchAgent({
|
||||
const {
|
||||
data: backendConnection,
|
||||
error: backendConnectionError
|
||||
} = useSWRImmutable(agentBootstrapKey, () => fetchAgentBootstrap(clientRef.current), {
|
||||
} = useSWR([...agentBootstrapKey, getAccessToken, conversationScope], () => fetchAgentBootstrap(scopeClient), {
|
||||
shouldRetryOnError: false
|
||||
});
|
||||
const {
|
||||
@@ -295,7 +333,7 @@ export function useWorkbenchAgent({
|
||||
isLoading: sessionHistoryLoading,
|
||||
isValidating: sessionHistoryValidating,
|
||||
mutate: mutateSessionHistory
|
||||
} = useSWR(agentSessionsKey, () => fetchAgentSessions(clientRef.current), {
|
||||
} = useSWR([...agentSessionsKey, getAccessToken, conversationScope], () => fetchAgentSessions(scopeClient), {
|
||||
revalidateOnFocus: false,
|
||||
shouldRetryOnError: false
|
||||
});
|
||||
@@ -339,7 +377,7 @@ export function useWorkbenchAgent({
|
||||
frontendActionEnabledRef.current = Boolean(backendConnection.frontendActionRegistry);
|
||||
setRegistry(backendConnection.registry);
|
||||
setModelOptions(backendConnection.models);
|
||||
setSelectedModel((current) => current || backendConnection.defaultModel || backendConnection.models[0]?.id || "");
|
||||
setSelectedModel((current) => resolveAgentModelSelection(backendConnection, current));
|
||||
if (chatRef.current.status === "ready") {
|
||||
setConnectionStatus("online");
|
||||
setStatusLabel("准备就绪");
|
||||
@@ -402,6 +440,20 @@ export function useWorkbenchAgent({
|
||||
}, [clearMobileCollapseTimer]);
|
||||
|
||||
const streaming = chat.status === "submitted" || chat.status === "streaming" || resumedStreaming;
|
||||
const { data: pendingInputs = [], mutate: refreshPendingInputs } = useSWR(
|
||||
sessionId ? ["agent", "response-inputs", getAccessToken, conversationScope, sessionId] : null,
|
||||
() => clientRef.current.listResponseInputs(sessionId!),
|
||||
{ refreshInterval: streaming ? 1000 : 0, shouldRetryOnError: false }
|
||||
);
|
||||
const updateInputDelivery = useCallback(async (id: string, delivery: AgentInputDelivery) => {
|
||||
if (!sessionIdRef.current) return;
|
||||
try {
|
||||
const updated = await clientRef.current.updateResponseInput(sessionIdRef.current, id, delivery);
|
||||
await refreshPendingInputs((current = []) => current.map((input) => input.id === id ? updated : input), { revalidate: false });
|
||||
} catch {
|
||||
showMapNotice({ tone: "warning", message: "消息可能已开始处理,无法修改发送方式。" });
|
||||
} finally { void refreshPendingInputs().catch(() => {}); }
|
||||
}, [refreshPendingInputs]);
|
||||
const messages = useMemo(
|
||||
() => toAgentChatMessages(chat.messages, permissionOverrides, questionOverrides),
|
||||
[chat.messages, permissionOverrides, questionOverrides]
|
||||
@@ -446,6 +498,8 @@ export function useWorkbenchAgent({
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "credential_refresh_required") { handleCredentialRefresh(event.data); return; }
|
||||
if (event.type === "agent_message") { refreshAgentMessageInbox(); return; }
|
||||
if (event.type === "session_title") {
|
||||
const title = getDataString(event.data, "title");
|
||||
if (title) {
|
||||
@@ -497,7 +551,7 @@ export function useWorkbenchAgent({
|
||||
|
||||
chatRef.current.setMessages((current) => applySessionStreamEvent(current, event));
|
||||
},
|
||||
[claimEnvelope, handleFrontendAction, onUiEnvelope, refreshSessionHistory]
|
||||
[refreshAgentMessageInbox, claimEnvelope, handleCredentialRefresh, handleFrontendAction, onUiEnvelope, refreshSessionHistory]
|
||||
);
|
||||
|
||||
const startSessionStreamSubscription = useCallback(
|
||||
@@ -557,6 +611,7 @@ export function useWorkbenchAgent({
|
||||
}
|
||||
|
||||
applySessionId(loadedSession.sessionId);
|
||||
setTaskDiscussion(null);
|
||||
setSessionPurpose(loadedSession.purpose);
|
||||
setSessionTaskId(loadedSession.taskId ?? null);
|
||||
setSessionTitle(loadedSession.title);
|
||||
@@ -570,6 +625,7 @@ export function useWorkbenchAgent({
|
||||
startSessionStreamSubscription(loadedSession.sessionId);
|
||||
}
|
||||
await refreshSessionHistory();
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMapNotice({
|
||||
id: "agent-history-status",
|
||||
@@ -582,20 +638,34 @@ export function useWorkbenchAgent({
|
||||
[applySessionId, chat, refreshSessionHistory, startSessionStreamSubscription, stopSessionStreamSubscription, streaming]
|
||||
);
|
||||
|
||||
const openTaskCollaboration = useCallback(async (taskId: string, context?: AgentTaskDiscussionContext) => {
|
||||
const openTaskCollaboration = useCallback(async (taskId: string, discussion?: InvestigationDiscussion) => {
|
||||
const normalizedTaskId = taskId.trim();
|
||||
if (!normalizedTaskId) {
|
||||
throw new Error("工况任务 ID 不能为空。");
|
||||
}
|
||||
if (streaming) {
|
||||
throw new Error("Agent 正在处理上一条指令,完成后再打开任务协作。");
|
||||
}
|
||||
const normalizedContext = agentTaskDiscussionContextSchema.parse(discussion?.context ?? {});
|
||||
conversationScopeRef.current = "project";
|
||||
setConversationScope("project");
|
||||
const collaboration = await clientRef.current.createTaskCollaborationSession(normalizedTaskId);
|
||||
const collaborationSessionId = collaboration.session_id || collaboration.id;
|
||||
if (!collaborationSessionId) {
|
||||
throw new Error("Agent 未返回任务协作会话。");
|
||||
}
|
||||
await loadHistorySession(collaborationSessionId);
|
||||
taskDiscussionRef.current = { sessionId: collaborationSessionId, context: agentTaskDiscussionContextSchema.parse(context ?? {}) };
|
||||
const loaded = await loadHistorySession(collaborationSessionId);
|
||||
if (!loaded || sessionIdRef.current !== collaborationSessionId) {
|
||||
throw new Error("任务协作会话未能打开,请重试。");
|
||||
}
|
||||
setTaskDiscussion({ sessionId: collaborationSessionId, context: normalizedContext, reportGeneration: discussion?.reportGeneration });
|
||||
const draft = discussion?.prompt;
|
||||
if (draft?.trim()) {
|
||||
setPromptDraft((current) => current.trim() ? `${current}\n\n${draft.trim()}` : draft.trim());
|
||||
}
|
||||
setPromptFocusRequest((current) => current + 1);
|
||||
return collaborationSessionId;
|
||||
}, [loadHistorySession]);
|
||||
}, [loadHistorySession, streaming]);
|
||||
|
||||
const renameHistorySession = useCallback(
|
||||
async (nextSessionId: string, title: string) => {
|
||||
@@ -644,6 +714,7 @@ export function useWorkbenchAgent({
|
||||
setSessionTitle("新对话");
|
||||
setSessionPurpose("ordinary_chat");
|
||||
setSessionTaskId(null);
|
||||
setTaskDiscussion(null);
|
||||
setStatusLabel(nextStatusLabel);
|
||||
setConnectionStatus("online");
|
||||
setPermissionOverrides({});
|
||||
@@ -697,6 +768,13 @@ export function useWorkbenchAgent({
|
||||
resetDraftSession();
|
||||
}, [chat, resetDraftSession, stopSessionStreamSubscription]);
|
||||
|
||||
const changeConversationScope = useCallback(async (scope: "personal" | "project") => {
|
||||
if (scope === conversationScopeRef.current || streaming) return;
|
||||
await startNewSession();
|
||||
conversationScopeRef.current = scope;
|
||||
setConversationScope(scope);
|
||||
}, [startNewSession, streaming]);
|
||||
|
||||
const submitPrompt = useCallback(
|
||||
async (prompt: string) => {
|
||||
const normalizedPrompt = prompt.trim();
|
||||
@@ -705,7 +783,19 @@ export function useWorkbenchAgent({
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
showMapNotice({ tone: "warning", message: "Agent 正在处理上一条指令。" });
|
||||
try {
|
||||
if (!sessionIdRef.current) throw new Error("Agent 正在建立会话,请稍后追加消息。");
|
||||
const receipt = await clientRef.current.submitResponseInput(sessionIdRef.current, normalizedPrompt, inputDelivery, `msg_${crypto.randomUUID().replaceAll("-", "")}`);
|
||||
// A failed list refresh must not turn an accepted message into a
|
||||
// failed send and restore a draft that the user might submit twice.
|
||||
void refreshPendingInputs((current = []) => receipt.status === "pending"
|
||||
? [...current.filter((input) => input.id !== receipt.id), receipt]
|
||||
: current.filter((input) => input.id !== receipt.id), { revalidate: false })
|
||||
.then(() => refreshPendingInputs()).catch(() => {});
|
||||
} catch (error) {
|
||||
showMapNotice({ tone: "error", title: "追加消息失败", message: error instanceof Error ? error.message : "请稍后重试。" });
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -718,12 +808,12 @@ export function useWorkbenchAgent({
|
||||
session_id: sessionIdRef.current ?? undefined,
|
||||
model: selectedModel || undefined,
|
||||
approval_mode: approvalMode,
|
||||
task_context: taskDiscussionRef.current?.sessionId === sessionIdRef.current ? taskDiscussionRef.current.context : undefined
|
||||
task_context: taskDiscussion?.sessionId === sessionIdRef.current ? taskDiscussion.context : undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
[approvalMode, chat, selectedModel, streaming]
|
||||
[approvalMode, chat, inputDelivery, refreshPendingInputs, selectedModel, streaming, taskDiscussion]
|
||||
);
|
||||
|
||||
const triggerEvent = useCallback(
|
||||
@@ -795,10 +885,10 @@ export function useWorkbenchAgent({
|
||||
stopSessionStreamSubscription();
|
||||
const activeSessionId = sessionIdRef.current;
|
||||
if (activeSessionId) {
|
||||
void clientRef.current.abort(activeSessionId).catch(() => undefined);
|
||||
void clientRef.current.abort(activeSessionId).then(() => refreshPendingInputs()).catch(() => undefined);
|
||||
}
|
||||
markLastAssistantStreamDone(chatRef.current.messages, setStreamRenderState);
|
||||
}, [chat, stopSessionStreamSubscription]);
|
||||
}, [chat, refreshPendingInputs, stopSessionStreamSubscription]);
|
||||
|
||||
const replyPermission = useCallback(async (permission: AgentPermissionRequest, reply: AgentPermissionReply) => {
|
||||
setPermissionOverrides((current) => ({
|
||||
@@ -907,6 +997,11 @@ export function useWorkbenchAgent({
|
||||
|
||||
return {
|
||||
apiClient: clientRef.current,
|
||||
inputDelivery,
|
||||
setInputDelivery,
|
||||
pendingInputs,
|
||||
updateInputDelivery,
|
||||
agentMessageInbox,
|
||||
approvalMode,
|
||||
collapsePanel,
|
||||
expandPanel,
|
||||
@@ -917,6 +1012,8 @@ export function useWorkbenchAgent({
|
||||
openMobilePanel,
|
||||
closeMobilePanel,
|
||||
connectionStatus,
|
||||
conversationScope,
|
||||
changeConversationScope,
|
||||
panelCollapsing,
|
||||
panelOpen,
|
||||
personaState,
|
||||
@@ -927,15 +1024,19 @@ export function useWorkbenchAgent({
|
||||
renameHistorySession,
|
||||
deleteHistorySession,
|
||||
sessionHistory,
|
||||
generatePromptSuggestions,
|
||||
sessionHistoryLoading: sessionHistoryLoading || sessionHistoryValidating,
|
||||
sessionId,
|
||||
sessionPurpose,
|
||||
sessionTaskId,
|
||||
taskDiscussionContext: taskDiscussion?.sessionId === sessionId ? taskDiscussion.context : undefined,
|
||||
taskDiscussionReportGeneration: taskDiscussion?.sessionId === sessionId ? taskDiscussion.reportGeneration : undefined,
|
||||
sessionTitle,
|
||||
statusLabel,
|
||||
streamRenderState,
|
||||
selectedModel,
|
||||
promptDraft,
|
||||
promptFocusRequest,
|
||||
setApprovalMode,
|
||||
setPromptDraft,
|
||||
setSelectedModel,
|
||||
|
||||
Reference in New Issue
Block a user