重构会话agent连接、初始化、加载配置流程
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1347,7 +1347,9 @@
|
||||
"connectingTitle": "جارٍ الاتصال بـ {agent}",
|
||||
"connectingDescription": "جارٍ إنشاء الاتصال",
|
||||
"loadingSelectorsTitle": "جارٍ تحميل محددات {agent}",
|
||||
"loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة"
|
||||
"loadingSelectorsDescription": "جارٍ جلب خيارات الوضع وإعدادات الجلسة",
|
||||
"initSessionTitle": "جارٍ تهيئة جلسة {agent}",
|
||||
"initSessionDescription": "جارٍ إنشاء الجلسة وتحميل الإعدادات"
|
||||
},
|
||||
"errors": {
|
||||
"connectionFailed": "فشل الاتصال"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1347,7 +1347,9 @@
|
||||
"connectingTitle": "{agent} に接続中",
|
||||
"connectingDescription": "接続を確立しています",
|
||||
"loadingSelectorsTitle": "{agent} のセレクターを読み込み中",
|
||||
"loadingSelectorsDescription": "モードとセッション設定オプションを取得しています"
|
||||
"loadingSelectorsDescription": "モードとセッション設定オプションを取得しています",
|
||||
"initSessionTitle": "{agent} セッションを初期化中",
|
||||
"initSessionDescription": "セッションを作成し設定を読み込んでいます"
|
||||
},
|
||||
"errors": {
|
||||
"connectionFailed": "接続に失敗しました"
|
||||
|
||||
@@ -1347,7 +1347,9 @@
|
||||
"connectingTitle": "{agent}에 연결 중",
|
||||
"connectingDescription": "연결을 설정하는 중",
|
||||
"loadingSelectorsTitle": "{agent} 선택자 불러오는 중",
|
||||
"loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중"
|
||||
"loadingSelectorsDescription": "모드 및 세션 구성 옵션을 가져오는 중",
|
||||
"initSessionTitle": "{agent} 세션 초기화 중",
|
||||
"initSessionDescription": "세션 생성 및 설정 불러오는 중"
|
||||
},
|
||||
"errors": {
|
||||
"connectionFailed": "연결 실패"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1347,7 +1347,9 @@
|
||||
"connectingTitle": "正在连接 {agent}",
|
||||
"connectingDescription": "正在建立连接",
|
||||
"loadingSelectorsTitle": "正在加载 {agent} 选择项",
|
||||
"loadingSelectorsDescription": "正在获取模式和会话配置选项"
|
||||
"loadingSelectorsDescription": "正在获取模式和会话配置选项",
|
||||
"initSessionTitle": "正在初始化 {agent} 会话",
|
||||
"initSessionDescription": "正在创建会话并加载配置"
|
||||
},
|
||||
"errors": {
|
||||
"connectionFailed": "连接失败"
|
||||
|
||||
@@ -1347,7 +1347,9 @@
|
||||
"connectingTitle": "正在連線 {agent}",
|
||||
"connectingDescription": "正在建立連線",
|
||||
"loadingSelectorsTitle": "正在載入 {agent} 選擇項",
|
||||
"loadingSelectorsDescription": "正在取得模式與會話設定選項"
|
||||
"loadingSelectorsDescription": "正在取得模式與會話設定選項",
|
||||
"initSessionTitle": "正在初始化 {agent} 會話",
|
||||
"initSessionDescription": "正在建立會話並載入設定"
|
||||
},
|
||||
"errors": {
|
||||
"connectionFailed": "連線失敗"
|
||||
|
||||
Reference in New Issue
Block a user