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:
parent
b4b7c5a208
commit
fc050c5074
@ -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
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 记忆方法
|
// 记忆方法
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user