diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 43b3c4d..703dbe6 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -30,6 +30,7 @@ import type { StreamDelta, StreamEnd, ExecutionCompleted, + WsError, WsInbound, } from '../types/protocol' @@ -87,8 +88,8 @@ interface UseChatReturn { // 子智能体导航方法 enterSubAgentView: (taskId: string, description: string, subagentType?: string) => Command - exitSubAgentView: () => void - navigateToSubAgentLevel: (index: number) => void + exitSubAgentView: () => Command | null + navigateToSubAgentLevel: (index: number) => Command | null // 记忆状态 memories: MemorySummary[] @@ -216,6 +217,9 @@ export function useChat(): UseChatReturn { if (message.type === 'stream_delta' || message.type === 'stream_end') { return (message as StreamDelta | StreamEnd).subagent_task_id } + if (message.type === 'execution_completed' || message.type === 'error') { + return (message as ExecutionCompleted | WsError).subagent_task_id + } return undefined } @@ -337,6 +341,36 @@ export function useChat(): UseChatReturn { } // stream_end: no-op, assistant_response will replace if (message.type === 'stream_end') return + // execution_completed: 更新栈顶 status 为 completed + if (message.type === 'execution_completed') { + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, status: 'completed' } + return newStack + }) + return + } + // error: 更新栈顶 status 为 error,并追加错误消息 + if (message.type === 'error') { + const errMsg = message as WsError + const errorChatMsg: ChatMessage = { + id: generateMessageId(), + role: 'assistant', + content: `Error: ${errMsg.message}`, + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), + type: 'message', + } + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, status: 'error', messages: [...top.messages, errorChatMsg] } + return newStack + }) + return + } // Other messages: assistant_response replaces streamed message by ID const chatMsg = serverMessageToChatMessage(message) if (chatMsg) { @@ -352,6 +386,10 @@ export function useChat(): UseChatReturn { newStack[newStack.length - 1] = { ...top, messages: updated } return newStack } + } else if (message.type === 'tool_call' || message.type === 'tool_result' || message.type === 'tool_pending') { + // 按 id 去重,避免 load_task_messages 并发调用导致重复 + const exists = top.messages.some(m => m.id === chatMsg.id) + if (exists) return prev } const newStack = [...prev] newStack[newStack.length - 1] = { ...top, messages: [...top.messages, chatMsg] } @@ -360,6 +398,81 @@ export function useChat(): UseChatReturn { } } + // 追加消息到栈中非栈顶的匹配层(按 taskId 匹配),在 setSubAgentStack updater 内部完成所有判断 + const appendToSubAgentLayerMessage = (taskId: string, message: WsOutbound) => { + setSubAgentStack((prev) => { + const idx = prev.findIndex(v => v.taskId === taskId) + if (idx < 0) return prev + const layer = prev[idx] + + // execution_completed: 更新该层 status 为 completed + if (message.type === 'execution_completed') { + const newStack = [...prev] + newStack[idx] = { ...layer, status: 'completed' } + return newStack + } + // error: 更新该层 status 为 error,并追加错误消息 + if (message.type === 'error') { + const errMsg = message as WsError + const errorChatMsg: ChatMessage = { + id: generateMessageId(), + role: 'assistant', + content: `Error: ${errMsg.message}`, + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), + type: 'message', + } + const newStack = [...prev] + newStack[idx] = { ...layer, status: 'error', messages: [...layer.messages, errorChatMsg] } + return newStack + } + // stream_delta: accumulate into existing message by ID, or create new + if (message.type === 'stream_delta') { + const msg = message as StreamDelta + const existingIdx = layer.messages.findIndex(m => m.id === msg.id && m.type === 'message') + if (existingIdx >= 0) { + const updated = [...layer.messages] + const existing = updated[existingIdx] + updated[existingIdx] = { + ...existing, + content: existing.content + msg.delta, + reasoningContent: msg.reasoning_delta + ? (existing.reasoningContent || '') + msg.reasoning_delta + : existing.reasoningContent, + } + const newStack = [...prev] + newStack[idx] = { ...layer, messages: updated } + return newStack + } + const chatMsg = serverMessageToChatMessage(message) + if (!chatMsg) return prev + const newStack = [...prev] + newStack[idx] = { ...layer, messages: [...layer.messages, chatMsg] } + return newStack + } + // stream_end: no-op + if (message.type === 'stream_end') return prev + // 其他消息:assistant_response 按 id 替换,tool_call/tool_result/tool_pending 按 id 去重 + const chatMsg = serverMessageToChatMessage(message) + if (!chatMsg) return prev + if (message.type === 'assistant_response') { + const existingIdx = layer.messages.findIndex(m => m.id === chatMsg.id && m.type === 'message') + if (existingIdx >= 0) { + const updated = [...layer.messages] + updated[existingIdx] = chatMsg + const newStack = [...prev] + newStack[idx] = { ...layer, messages: updated } + return newStack + } + } else if (message.type === 'tool_call' || message.type === 'tool_result' || message.type === 'tool_pending') { + const exists = layer.messages.some(m => m.id === chatMsg.id) + if (exists) return prev + } + const newStack = [...prev] + newStack[idx] = { ...layer, messages: [...layer.messages, chatMsg] } + return newStack + }) + } + // Sync backend user message ID to the last local user message, // so that created_by_message_id (backend UUID) can match DOM data-message-id const applyUserMessageId = useCallback((userMessageId: string) => { @@ -404,6 +517,8 @@ export function useChat(): UseChatReturn { setSubAgentStack((prev) => { if (prev.length === 0) return prev const top = prev[prev.length - 1] + // 校验 task_id 匹配,避免快速切换视图时 A 的状态写到 B + if (msg.task_id !== top.taskId) return prev const newStack = [...prev] newStack[newStack.length - 1] = { ...top, @@ -501,8 +616,9 @@ export function useChat(): UseChatReturn { } return } - // 丢弃其他子智能体的消息,避免 fall through 到主消息处理 + // 非栈顶子智能体消息:遍历栈其余层查找匹配 taskId,命中则追加到对应层;未命中也丢弃 if (msgSubagentTaskId) { + appendToSubAgentLayerMessage(msgSubagentTaskId, message) return } } @@ -672,6 +788,8 @@ export function useChat(): UseChatReturn { case 'execution_completed': { // 智能体执行完全结束(不再有后续工具调用或 LLM 迭代) const msg = message as ExecutionCompleted + // 子智能体的完成事件不操作主视图(由子智能体视图分支处理或丢弃) + if (getSubagentTaskId(message)) return // 按 topic_id 隔离:只处理当前话题的完成信号 if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return setIsLoading(false) @@ -792,6 +910,8 @@ export function useChat(): UseChatReturn { } case 'error': { + // 子智能体的错误事件不操作主视图(由子智能体视图分支处理或丢弃) + if (getSubagentTaskId(message)) return setMessages((prev) => [ ...prev, { @@ -989,19 +1109,27 @@ export function useChat(): UseChatReturn { } }, []) - const exitSubAgentView = useCallback(() => { + const exitSubAgentView = useCallback((): Command | null => { + let command: Command | null = null setSubAgentStack((prev) => { if (prev.length <= 1) { subAgentViewRef.current = null return [] } const newStack = prev.slice(0, -1) - subAgentViewRef.current = newStack[newStack.length - 1] - return newStack + const newTop = newStack[newStack.length - 1] + subAgentViewRef.current = newTop + // 清空目标层 messages + status 置 loading,并准备重新拉取命令 + const clearedStack = [...newStack] + clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' } + command = { type: 'load_task_messages', task_id: newTop.taskId } + return clearedStack }) + return command }, []) - const navigateToSubAgentLevel = useCallback((index: number) => { + const navigateToSubAgentLevel = useCallback((index: number): Command | null => { + let command: Command | null = null setSubAgentStack((prev) => { if (index < 0) { // -1 means go back to main session (clear all) @@ -1010,9 +1138,14 @@ export function useChat(): UseChatReturn { } if (index >= prev.length) return prev const newStack = prev.slice(0, index + 1) - subAgentViewRef.current = newStack.length > 0 ? newStack[newStack.length - 1] : null - return newStack + const newTop = newStack[newStack.length - 1] + subAgentViewRef.current = newTop + const clearedStack = [...newStack] + clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' } + command = { type: 'load_task_messages', task_id: newTop.taskId } + return clearedStack }) + return command }, []) // 记忆方法 diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 6dece06..1822ea5 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -95,6 +95,7 @@ export interface WsError { code: string message: string timestamp?: number + subagent_task_id?: string } export interface TaskStarted { @@ -282,6 +283,7 @@ export interface ExecutionCompleted { type: 'execution_completed' topic_id?: string timestamp?: number + subagent_task_id?: string } export type WsOutbound =