fix: 前端子智能体视图消息显示与刷新修复

protocol.ts: ExecutionCompleted/WsError 类型增加 subagent_task_id 字段。

useChat.ts 核心修复:

- getSubagentTaskId 识别 execution_completed/error 消息类型

- task_messages_loaded 处理增加 task_id 校验,避免快速切换视图时状态污染

- 主视图 execution_completed/error case 防护,子智能体事件不操作主视图

- 新增 appendToSubAgentLayerMessage,非栈顶子智能体消息遍历栈匹配层而非丢弃

- appendToSubAgentViewMessage 对 tool_call/tool_result/tool_pending 按 id 去重

- execution_completed/error 路由到匹配层更新 status

- exitSubAgentView/navigateToSubAgentLevel 返回 load_task_messages 命令重新拉取

Spec: fix-subagent-message-refresh-on-exit (Task 3-8)
This commit is contained in:
oudecheng 2026-07-06 18:11:37 +08:00
parent b4b7c5a208
commit fc050c5074
2 changed files with 144 additions and 9 deletions

View File

@ -30,6 +30,7 @@ import type {
StreamDelta, StreamDelta,
StreamEnd, StreamEnd,
ExecutionCompleted, ExecutionCompleted,
WsError,
WsInbound, WsInbound,
} from '../types/protocol' } from '../types/protocol'
@ -87,8 +88,8 @@ interface UseChatReturn {
// 子智能体导航方法 // 子智能体导航方法
enterSubAgentView: (taskId: string, description: string, subagentType?: string) => Command enterSubAgentView: (taskId: string, description: string, subagentType?: string) => Command
exitSubAgentView: () => void exitSubAgentView: () => Command | null
navigateToSubAgentLevel: (index: number) => void navigateToSubAgentLevel: (index: number) => Command | null
// 记忆状态 // 记忆状态
memories: MemorySummary[] memories: MemorySummary[]
@ -216,6 +217,9 @@ export function useChat(): UseChatReturn {
if (message.type === 'stream_delta' || message.type === 'stream_end') { if (message.type === 'stream_delta' || message.type === 'stream_end') {
return (message as StreamDelta | StreamEnd).subagent_task_id 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 return undefined
} }
@ -337,6 +341,36 @@ export function useChat(): UseChatReturn {
} }
// stream_end: no-op, assistant_response will replace // stream_end: no-op, assistant_response will replace
if (message.type === 'stream_end') return 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 // Other messages: assistant_response replaces streamed message by ID
const chatMsg = serverMessageToChatMessage(message) const chatMsg = serverMessageToChatMessage(message)
if (chatMsg) { if (chatMsg) {
@ -352,6 +386,10 @@ export function useChat(): UseChatReturn {
newStack[newStack.length - 1] = { ...top, messages: updated } newStack[newStack.length - 1] = { ...top, messages: updated }
return newStack 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] const newStack = [...prev]
newStack[newStack.length - 1] = { ...top, messages: [...top.messages, chatMsg] } 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, // 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 // so that created_by_message_id (backend UUID) can match DOM data-message-id
const applyUserMessageId = useCallback((userMessageId: string) => { const applyUserMessageId = useCallback((userMessageId: string) => {
@ -404,6 +517,8 @@ export function useChat(): UseChatReturn {
setSubAgentStack((prev) => { setSubAgentStack((prev) => {
if (prev.length === 0) return prev if (prev.length === 0) return prev
const top = prev[prev.length - 1] const top = prev[prev.length - 1]
// 校验 task_id 匹配,避免快速切换视图时 A 的状态写到 B
if (msg.task_id !== top.taskId) return prev
const newStack = [...prev] const newStack = [...prev]
newStack[newStack.length - 1] = { newStack[newStack.length - 1] = {
...top, ...top,
@ -501,8 +616,9 @@ export function useChat(): UseChatReturn {
} }
return return
} }
// 丢弃其他子智能体的消息,避免 fall through 到主消息处理 // 非栈顶子智能体消息:遍历栈其余层查找匹配 taskId命中则追加到对应层未命中也丢弃
if (msgSubagentTaskId) { if (msgSubagentTaskId) {
appendToSubAgentLayerMessage(msgSubagentTaskId, message)
return return
} }
} }
@ -672,6 +788,8 @@ export function useChat(): UseChatReturn {
case 'execution_completed': { case 'execution_completed': {
// 智能体执行完全结束(不再有后续工具调用或 LLM 迭代) // 智能体执行完全结束(不再有后续工具调用或 LLM 迭代)
const msg = message as ExecutionCompleted const msg = message as ExecutionCompleted
// 子智能体的完成事件不操作主视图(由子智能体视图分支处理或丢弃)
if (getSubagentTaskId(message)) return
// 按 topic_id 隔离:只处理当前话题的完成信号 // 按 topic_id 隔离:只处理当前话题的完成信号
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
setIsLoading(false) setIsLoading(false)
@ -792,6 +910,8 @@ export function useChat(): UseChatReturn {
} }
case 'error': { case 'error': {
// 子智能体的错误事件不操作主视图(由子智能体视图分支处理或丢弃)
if (getSubagentTaskId(message)) return
setMessages((prev) => [ setMessages((prev) => [
...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) => { setSubAgentStack((prev) => {
if (prev.length <= 1) { if (prev.length <= 1) {
subAgentViewRef.current = null subAgentViewRef.current = null
return [] return []
} }
const newStack = prev.slice(0, -1) const newStack = prev.slice(0, -1)
subAgentViewRef.current = newStack[newStack.length - 1] const newTop = newStack[newStack.length - 1]
return newStack 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) => { setSubAgentStack((prev) => {
if (index < 0) { if (index < 0) {
// -1 means go back to main session (clear all) // -1 means go back to main session (clear all)
@ -1010,9 +1138,14 @@ export function useChat(): UseChatReturn {
} }
if (index >= prev.length) return prev if (index >= prev.length) return prev
const newStack = prev.slice(0, index + 1) const newStack = prev.slice(0, index + 1)
subAgentViewRef.current = newStack.length > 0 ? newStack[newStack.length - 1] : null const newTop = newStack[newStack.length - 1]
return newStack 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
}, []) }, [])
// 记忆方法 // 记忆方法

View File

@ -95,6 +95,7 @@ export interface WsError {
code: string code: string
message: string message: string
timestamp?: number timestamp?: number
subagent_task_id?: string
} }
export interface TaskStarted { export interface TaskStarted {
@ -282,6 +283,7 @@ export interface ExecutionCompleted {
type: 'execution_completed' type: 'execution_completed'
topic_id?: string topic_id?: string
timestamp?: number timestamp?: number
subagent_task_id?: string
} }
export type WsOutbound = export type WsOutbound =