重构会话agent连接、初始化、加载配置流程

This commit is contained in:
xintaofei
2026-03-26 19:55:28 +08:00
parent 484cb3557a
commit d1eaa8f725
17 changed files with 154 additions and 28 deletions

View File

@@ -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",

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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) => {

View File

@@ -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<string | null>(null)
const selectorTaskTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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,

View File

@@ -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

View File

@@ -1347,7 +1347,9 @@
"connectingTitle": "جارٍ الاتصال بـ {agent}",
"connectingDescription": "جارٍ إنشاء الاتصال",
"loadingSelectorsTitle": "جارٍ تحميل محددات {agent}",
"loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة"
"loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة",
"initSessionTitle": "جارٍ تهيئة جلسة {agent}",
"initSessionDescription": "جارٍ إنشاء الجلسة وتحميل الإعدادات"
},
"errors": {
"connectionFailed": "فشل الاتصال"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -1347,7 +1347,9 @@
"connectingTitle": "{agent} に接続中",
"connectingDescription": "接続を確立しています",
"loadingSelectorsTitle": "{agent} のセレクターを読み込み中",
"loadingSelectorsDescription": "モードとセッション設定オプションを取得しています"
"loadingSelectorsDescription": "モードとセッション設定オプションを取得しています",
"initSessionTitle": "{agent} セッションを初期化中",
"initSessionDescription": "セッションを作成し設定を読み込んでいます"
},
"errors": {
"connectionFailed": "接続に失敗しました"

View File

@@ -1347,7 +1347,9 @@
"connectingTitle": "{agent}에 연결 중",
"connectingDescription": "연결을 설정하는 중",
"loadingSelectorsTitle": "{agent} 선택자 불러오는 중",
"loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중"
"loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중",
"initSessionTitle": "{agent} 세션 초기화 중",
"initSessionDescription": "세션 생성 및 설정 불러오는 중"
},
"errors": {
"connectionFailed": "연결 실패"

View File

@@ -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"

View File

@@ -1347,7 +1347,9 @@
"connectingTitle": "正在连接 {agent}",
"connectingDescription": "正在建立连接",
"loadingSelectorsTitle": "正在加载 {agent} 选择项",
"loadingSelectorsDescription": "正在获取模式和会话配置选项"
"loadingSelectorsDescription": "正在获取模式和会话配置选项",
"initSessionTitle": "正在初始化 {agent} 会话",
"initSessionDescription": "正在创建会话并加载配置"
},
"errors": {
"connectionFailed": "连接失败"

View File

@@ -1347,7 +1347,9 @@
"connectingTitle": "正在連線 {agent}",
"connectingDescription": "正在建立連線",
"loadingSelectorsTitle": "正在載入 {agent} 選擇項",
"loadingSelectorsDescription": "正在取得模式與會話設定選項"
"loadingSelectorsDescription": "正在取得模式與會話設定選項",
"initSessionTitle": "正在初始化 {agent} 會話",
"initSessionDescription": "正在建立會話並載入設定"
},
"errors": {
"connectionFailed": "連線失敗"