From d1eaa8f725b140347b68b612d753a41fbce926e9 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Thu, 26 Mar 2026 19:55:28 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BC=9A=E8=AF=9Dagent?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E3=80=81=E5=88=9D=E5=A7=8B=E5=8C=96=E3=80=81?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=85=8D=E7=BD=AE=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/acp/connection.rs | 5 +- src/components/chat/chat-input.tsx | 4 +- src/components/chat/conversation-shell.tsx | 3 + .../conversation-detail-panel.tsx | 3 + src/contexts/acp-connections-context.tsx | 92 ++++++++++++++++++- src/hooks/use-connection-lifecycle.ts | 26 ++++-- src/hooks/use-connection.ts | 9 +- src/i18n/messages/ar.json | 4 +- src/i18n/messages/de.json | 4 +- src/i18n/messages/en.json | 4 +- src/i18n/messages/es.json | 4 +- src/i18n/messages/fr.json | 4 +- src/i18n/messages/ja.json | 4 +- src/i18n/messages/ko.json | 4 +- src/i18n/messages/pt.json | 4 +- src/i18n/messages/zh-CN.json | 4 +- src/i18n/messages/zh-TW.json | 4 +- 17 files changed, 154 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index 4616ba1..7155ac1 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -643,7 +643,10 @@ async fn run_connection( }, ); - // Emit connected status + // Emit connected status early so the frontend can show cached + // selectors and enable sending while the session initialises. + // Prompts sent before run_conversation_loop are buffered in + // the cmd_rx channel and processed as soon as the loop starts. crate::web::event_bridge::emit_event( &handle, "acp://event", diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx index f143fa4..0fb8015 100644 --- a/src/components/chat/chat-input.tsx +++ b/src/components/chat/chat-input.tsx @@ -25,6 +25,7 @@ interface ChatInputProps { configOptions?: SessionConfigOptionInfo[] modeLoading?: boolean configOptionsLoading?: boolean + selectorsLoading?: boolean selectedModeId?: string | null onModeChange?: (modeId: string) => void onConfigOptionChange?: (configId: string, valueId: string) => void @@ -57,6 +58,7 @@ export function ChatInput({ configOptions, modeLoading = false, configOptionsLoading = false, + selectorsLoading = false, selectedModeId, onModeChange, onConfigOptionChange, @@ -101,7 +103,7 @@ export function ChatInput({ promptCapabilities={promptCapabilities} onFocus={onFocus} defaultPath={defaultPath} - disabled={!isConnected && !isPrompting} + disabled={(!isConnected && !isPrompting) || selectorsLoading} isPrompting={isPrompting} onCancel={onCancel} modes={modes} diff --git a/src/components/chat/conversation-shell.tsx b/src/components/chat/conversation-shell.tsx index d8383a8..a4b3aec 100644 --- a/src/components/chat/conversation-shell.tsx +++ b/src/components/chat/conversation-shell.tsx @@ -34,6 +34,7 @@ interface ConversationShellProps { configOptions?: SessionConfigOptionInfo[] modeLoading?: boolean configOptionsLoading?: boolean + selectorsLoading?: boolean selectedModeId?: string | null onModeChange?: (modeId: string) => void onConfigOptionChange?: (configId: string, valueId: string) => void @@ -73,6 +74,7 @@ export function ConversationShell({ configOptions, modeLoading = false, configOptionsLoading = false, + selectorsLoading = false, selectedModeId, onModeChange, onConfigOptionChange, @@ -117,6 +119,7 @@ export function ConversationShell({ configOptions={configOptions} modeLoading={modeLoading} configOptionsLoading={configOptionsLoading} + selectorsLoading={selectorsLoading} selectedModeId={selectedModeId} onModeChange={onModeChange} onConfigOptionChange={onConfigOptionChange} diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index 983dab4..3de62ec 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -258,6 +258,7 @@ const ConversationTabView = memo(function ConversationTabView({ conn, modeLoading, configOptionsLoading, + selectorsLoading, autoConnectError, handleFocus, handleSend: lifecycleSend, @@ -850,6 +851,7 @@ const ConversationTabView = memo(function ConversationTabView({ configOptions={connectionConfigOptions} modeLoading={modeLoading} configOptionsLoading={configOptionsLoading} + selectorsLoading={selectorsLoading} selectedModeId={selectedModeId} onModeChange={setModeId} onConfigOptionChange={handleSetConfigOption} @@ -920,6 +922,7 @@ const ConversationTabView = memo(function ConversationTabView({ configOptions={connectionConfigOptions} modeLoading={modeLoading} configOptionsLoading={configOptionsLoading} + selectorsLoading={selectorsLoading} selectedModeId={selectedModeId} onModeChange={setModeId} onConfigOptionChange={handleSetConfigOption} diff --git a/src/contexts/acp-connections-context.tsx b/src/contexts/acp-connections-context.tsx index 7a7b2e6..8c17d4d 100644 --- a/src/contexts/acp-connections-context.tsx +++ b/src/contexts/acp-connections-context.tsx @@ -187,6 +187,12 @@ type Action = supported: boolean } | { type: "MODE_CHANGED"; contextKey: string; modeId: string } + | { + type: "CONFIG_OPTION_CHANGED" + contextKey: string + configId: string + valueId: string + } | { type: "PLAN_UPDATE" contextKey: string @@ -213,6 +219,21 @@ const MAX_LIVE_TOOL_RAW_OUTPUT_CHARS = 200_000 const MAX_BUFFERED_UNMAPPED_EVENTS_PER_CONNECTION = 64 const MAX_BUFFERED_UNMAPPED_CONNECTIONS = 128 +// Per-agentType cache for selectors (modes / configOptions). +// Populated when real data arrives from the backend. +// Used as UI-layer fallback when the connection hasn't received real data yet. +const selectorsCache = new Map< + string, + { + modes: SessionModeStateInfo | null + configOptions: SessionConfigOptionInfo[] | null + } +>() + +export function getCachedSelectors(agentType: string) { + return selectorsCache.get(agentType) ?? null +} + function clampLiveRawOutput(output: string | null): string | null { if (typeof output !== "string") return output if (output.length <= MAX_LIVE_TOOL_RAW_OUTPUT_CHARS) return output @@ -923,6 +944,33 @@ function connectionsReducer( return next } + case "CONFIG_OPTION_CHANGED": { + const conn = state.get(action.contextKey) + if (!conn) return state + const options = + conn.configOptions ?? + selectorsCache.get(conn.agentType)?.configOptions ?? + null + if (!options) return state + const idx = options.findIndex((o) => o.id === action.configId) + if (idx === -1) return state + const opt = options[idx] + if ( + opt.kind.type !== "select" || + opt.kind.current_value === action.valueId + ) { + return state + } + const updated = [...options] + updated[idx] = { + ...opt, + kind: { ...opt.kind, current_value: action.valueId }, + } + const next = new Map(state) + next.set(action.contextKey, { ...conn, configOptions: updated }) + return next + } + case "PLAN_UPDATE": { const conn = state.get(action.contextKey) if (!conn?.liveMessage) return state @@ -1472,29 +1520,59 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { sessionId: e.session_id, }) break - case "session_modes": + case "session_modes": { flushStreamingQueue() + const modeConn = storeRef.current.connections.get(contextKey) dispatch({ type: "SESSION_MODES", contextKey, modes: e.modes, }) + if (modeConn) { + const entry = selectorsCache.get(modeConn.agentType) ?? { + modes: null, + configOptions: null, + } + entry.modes = e.modes + selectorsCache.set(modeConn.agentType, entry) + } break - case "session_config_options": + } + case "session_config_options": { flushStreamingQueue() + const cfgConn = storeRef.current.connections.get(contextKey) dispatch({ type: "SESSION_CONFIG_OPTIONS", contextKey, configOptions: e.config_options, }) + if (cfgConn) { + const entry = selectorsCache.get(cfgConn.agentType) ?? { + modes: null, + configOptions: null, + } + entry.configOptions = e.config_options + selectorsCache.set(cfgConn.agentType, entry) + } break - case "selectors_ready": + } + case "selectors_ready": { flushStreamingQueue() dispatch({ type: "SELECTORS_READY", contextKey, }) + // Cache for agent types that may not emit session_modes / + // session_config_options at all (no selectors). + const rdyConn = storeRef.current.connections.get(contextKey) + if (rdyConn && !selectorsCache.has(rdyConn.agentType)) { + selectorsCache.set(rdyConn.agentType, { + modes: rdyConn.modes, + configOptions: rdyConn.configOptions, + }) + } break + } case "prompt_capabilities": flushStreamingQueue() dispatch({ @@ -1886,10 +1964,16 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { async (contextKey: string, configId: string, valueId: string) => { const conn = storeRef.current.connections.get(contextKey) if (!conn) return + dispatch({ + type: "CONFIG_OPTION_CHANGED", + contextKey, + configId, + valueId, + }) lastActivityRef.current.set(contextKey, Date.now()) await acpSetConfigOption(conn.connectionId, configId, valueId) }, - [] + [dispatch] ) const cancel = useCallback(async (contextKey: string) => { diff --git a/src/hooks/use-connection-lifecycle.ts b/src/hooks/use-connection-lifecycle.ts index e98d168..cdf982f 100644 --- a/src/hooks/use-connection-lifecycle.ts +++ b/src/hooks/use-connection-lifecycle.ts @@ -19,6 +19,7 @@ export interface UseConnectionLifecycleReturn { conn: UseConnectionReturn modeLoading: boolean configOptionsLoading: boolean + selectorsLoading: boolean autoConnectError: string | null handleFocus: () => void handleSend: (draft: PromptDraft, modeId?: string | null) => void @@ -65,13 +66,14 @@ export function useConnectionLifecycle({ configOptions, } = conn const isInteractiveStatus = status === "connected" || status === "prompting" - const effectiveSelectorsReady = - selectorsReady || modes !== null || configOptions !== null + const hasSelectorsData = modes !== null || configOptions !== null + const effectiveSelectorsReady = selectorsReady || hasSelectorsData const selectorTaskIdRef = useRef(null) const selectorTaskTimeoutRef = useRef | null>( null ) const selectorTaskSuppressedRef = useRef(false) + // Visual-only loading indicators for selector chips const modeLoading = status === "connecting" || status === "downloading" || @@ -80,6 +82,9 @@ export function useConnectionLifecycle({ status === "connecting" || status === "downloading" || (isInteractiveStatus && !effectiveSelectorsReady) + // Gate for send button: block until the backend session is fully + // initialized (selectorsReady from the real backend event, not cache). + const selectorsLoading = isInteractiveStatus && !selectorsReady const [lastAutoConnectError, setLastAutoConnectError] = useState<{ contextKey: string agentType: AgentType @@ -220,20 +225,22 @@ export function useConnectionLifecycle({ return } - const hasSelectorLoading = !effectiveSelectorsReady - if (!hasSelectorLoading) { + // Use the real backend selectorsReady (not effectiveSelectorsReady + // which includes cache) so the task shows during session creation + // even when cached selectors are available. + if (selectorsReady) { clearSelectorTask() return } if (!selectorTaskIdRef.current) { - const id = `acp-selectors-${Date.now()}` + const id = `acp-session-init-${Date.now()}` selectorTaskIdRef.current = id const agent = AGENT_LABELS[agentType] addTask( id, - t("tasks.loadingSelectorsTitle", { agent }), - t("tasks.loadingSelectorsDescription") + t("tasks.initSessionTitle", { agent }), + t("tasks.initSessionDescription") ) updateTask(id, { status: "running" }) } @@ -247,9 +254,7 @@ export function useConnectionLifecycle({ } }, [ status, - effectiveSelectorsReady, - modes, - configOptions, + selectorsReady, agentType, addTask, updateTask, @@ -363,6 +368,7 @@ export function useConnectionLifecycle({ conn, modeLoading, configOptionsLoading, + selectorsLoading, autoConnectError, handleFocus, handleSend, diff --git a/src/hooks/use-connection.ts b/src/hooks/use-connection.ts index 18c8f39..2f6d6cc 100644 --- a/src/hooks/use-connection.ts +++ b/src/hooks/use-connection.ts @@ -4,6 +4,7 @@ import { useCallback, useMemo, useSyncExternalStore } from "react" import { useAcpActions, useConnectionStore, + getCachedSelectors, type ConnectionState, type ConnectOptions, type LiveMessage, @@ -80,8 +81,12 @@ export function useConnection(contextKey: string): UseConnectionReturn { const supportsFork = connection?.supportsFork ?? false const selectorsReady = connection?.selectorsReady ?? false const sessionId = connection?.sessionId ?? null - const modes = connection?.modes ?? null - const configOptions = connection?.configOptions ?? null + const cached = connection?.agentType + ? getCachedSelectors(connection.agentType) + : null + const modes = connection?.modes ?? cached?.modes ?? null + const configOptions = + connection?.configOptions ?? cached?.configOptions ?? null const availableCommands = connection?.availableCommands ?? null const liveMessage = connection?.liveMessage ?? null const pendingPermission = connection?.pendingPermission ?? null diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 888b751..b9544c1 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1347,7 +1347,9 @@ "connectingTitle": "جارٍ الاتصال بـ {agent}", "connectingDescription": "جارٍ إنشاء الاتصال", "loadingSelectorsTitle": "جارٍ تحميل محددات {agent}", - "loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة" + "loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة", + "initSessionTitle": "جارٍ تهيئة جلسة {agent}", + "initSessionDescription": "جارٍ إنشاء الجلسة وتحميل الإعدادات" }, "errors": { "connectionFailed": "فشل الاتصال" diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 206b40d..b747c2a 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1347,7 +1347,9 @@ "connectingTitle": "Verbinde mit {agent}", "connectingDescription": "Verbindung wird hergestellt", "loadingSelectorsTitle": "{agent}-Selektoren werden geladen", - "loadingSelectorsDescription": "Modus- und Sitzungsoptionen werden abgerufen" + "loadingSelectorsDescription": "Modus- und Sitzungsoptionen werden abgerufen", + "initSessionTitle": "Initializing {agent} session", + "initSessionDescription": "Creating session and loading configuration" }, "errors": { "connectionFailed": "Verbindung fehlgeschlagen" diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index afb1054..a7e75a7 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1347,7 +1347,9 @@ "connectingTitle": "Connecting to {agent}", "connectingDescription": "Establishing connection", "loadingSelectorsTitle": "Loading {agent} selectors", - "loadingSelectorsDescription": "Fetching mode and session config options" + "loadingSelectorsDescription": "Fetching mode and session config options", + "initSessionTitle": "Initializing {agent} session", + "initSessionDescription": "Creating session and loading configuration" }, "errors": { "connectionFailed": "Connection failed" diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 8c0626b..2b94332 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1347,7 +1347,9 @@ "connectingTitle": "Conectando con {agent}", "connectingDescription": "Estableciendo conexión", "loadingSelectorsTitle": "Cargando selectores de {agent}", - "loadingSelectorsDescription": "Obteniendo opciones de modo y configuración de sesión" + "loadingSelectorsDescription": "Obteniendo opciones de modo y configuración de sesión", + "initSessionTitle": "Initializing {agent} session", + "initSessionDescription": "Creating session and loading configuration" }, "errors": { "connectionFailed": "Conexión fallida" diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 35ce3b2..f0dc3f2 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1347,7 +1347,9 @@ "connectingTitle": "Connexion à {agent}", "connectingDescription": "Établissement de la connexion", "loadingSelectorsTitle": "Chargement des sélecteurs de {agent}", - "loadingSelectorsDescription": "Récupération des options de mode et de configuration de session" + "loadingSelectorsDescription": "Récupération des options de mode et de configuration de session", + "initSessionTitle": "Initializing {agent} session", + "initSessionDescription": "Creating session and loading configuration" }, "errors": { "connectionFailed": "Échec de la connexion" diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index cd55905..4e06c93 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1347,7 +1347,9 @@ "connectingTitle": "{agent} に接続中", "connectingDescription": "接続を確立しています", "loadingSelectorsTitle": "{agent} のセレクターを読み込み中", - "loadingSelectorsDescription": "モードとセッション設定オプションを取得しています" + "loadingSelectorsDescription": "モードとセッション設定オプションを取得しています", + "initSessionTitle": "{agent} セッションを初期化中", + "initSessionDescription": "セッションを作成し設定を読み込んでいます" }, "errors": { "connectionFailed": "接続に失敗しました" diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 3494a2b..e7ac248 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1347,7 +1347,9 @@ "connectingTitle": "{agent}에 연결 중", "connectingDescription": "연결을 설정하는 중", "loadingSelectorsTitle": "{agent} 선택자 불러오는 중", - "loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중" + "loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중", + "initSessionTitle": "{agent} 세션 초기화 중", + "initSessionDescription": "세션 생성 및 설정 불러오는 중" }, "errors": { "connectionFailed": "연결 실패" diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 0b7d998..8d414df 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1347,7 +1347,9 @@ "connectingTitle": "Conectando a {agent}", "connectingDescription": "Estabelecendo conexão", "loadingSelectorsTitle": "Carregando seletores de {agent}", - "loadingSelectorsDescription": "Buscando opções de modo e configuração de sessão" + "loadingSelectorsDescription": "Buscando opções de modo e configuração de sessão", + "initSessionTitle": "Initializing {agent} session", + "initSessionDescription": "Creating session and loading configuration" }, "errors": { "connectionFailed": "Falha na conexão" diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index a7021ef..091c83d 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1347,7 +1347,9 @@ "connectingTitle": "正在连接 {agent}", "connectingDescription": "正在建立连接", "loadingSelectorsTitle": "正在加载 {agent} 选择项", - "loadingSelectorsDescription": "正在获取模式和会话配置选项" + "loadingSelectorsDescription": "正在获取模式和会话配置选项", + "initSessionTitle": "正在初始化 {agent} 会话", + "initSessionDescription": "正在创建会话并加载配置" }, "errors": { "connectionFailed": "连接失败" diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index a2e40a1..8e69190 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1347,7 +1347,9 @@ "connectingTitle": "正在連線 {agent}", "connectingDescription": "正在建立連線", "loadingSelectorsTitle": "正在載入 {agent} 選擇項", - "loadingSelectorsDescription": "正在取得模式與會話設定選項" + "loadingSelectorsDescription": "正在取得模式與會話設定選項", + "initSessionTitle": "正在初始化 {agent} 會話", + "initSessionDescription": "正在建立會話並載入設定" }, "errors": { "connectionFailed": "連線失敗"