From daf973a7dc52d3a357e452fb21a918e75d237bc9 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 13 Aug 2026 22:12:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=AD=90=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=98=BE=E7=A4=BA=E4=B8=8E=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MessageBubble: - taskStatusConfig 补全 running/completed/cancelled/interrupted 状态配色, 修复未知状态访问 undefined.borderColor 导致的白屏崩溃 - 添加安全回退到 failed 状态 main.tsx: - 添加 ErrorBoundary 捕获 React 渲染错误,防止白屏并提供错误详情与恢复入口 useMessages: - 收到子代理 execution_completed 事件后,更新主视图中 task tool result 占位消息的 status 字段(running->completed/failed/cancelled),消除永久黄色转圈 protocol.ts: - ExecutionCompleted 接口添加 subagent_status 和 subagent_summary 字段 --- web/src/components/Chat/MessageBubble.tsx | 48 +++++++++++++++++++---- web/src/hooks/chat/useMessages.ts | 33 ++++++++++++++++ web/src/main.tsx | 43 +++++++++++++++++++- web/src/types/protocol.ts | 13 +++++- 4 files changed, 127 insertions(+), 10 deletions(-) diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 2771919..b08c5f4 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -34,13 +34,14 @@ function StatusIcon({ status, size = 14, }: { - status: 'calling' | 'result' | 'pending' | 'success' | 'failed' | 'timeout'; + status: string; size?: number; }) { const iconClass = `transition-all duration-300`; switch (status) { case 'calling': + case 'running': return ( ); case 'pending': + case 'interrupted': + case 'cancelled': return ( | null)?.prompt as string) || ''; // task tool 专用的状态配色 - const taskStatusConfig = { + // 支持的状态: + // - running: 异步子代理刚 spawn,占位结果 + // - success/completed: 子代理执行成功 + // - failed: 子代理执行失败 + // - timeout: 子代理执行超时 + // - cancelled: 子代理被用户取消 + // - interrupted: 子代理因服务器重启被中断 + const taskStatusConfig: Record = { + running: { + dot: 'bg-amber-400 animate-pulse', + borderColor: 'border-amber-500/40', + iconColor: 'text-amber-400', + }, success: { dot: 'bg-emerald-400', borderColor: 'border-emerald-500/40', iconColor: 'text-emerald-400', }, + completed: { + dot: 'bg-emerald-400', + borderColor: 'border-emerald-500/40', + iconColor: 'text-emerald-400', + }, failed: { dot: 'bg-red-400', borderColor: 'border-red-500/40', iconColor: 'text-red-400' }, timeout: { dot: 'bg-amber-400', borderColor: 'border-amber-500/40', iconColor: 'text-amber-400', }, - } as const; + cancelled: { + dot: 'bg-zinc-400', + borderColor: 'border-zinc-500/40', + iconColor: 'text-zinc-400', + }, + interrupted: { + dot: 'bg-orange-400', + borderColor: 'border-orange-500/40', + iconColor: 'text-orange-400', + }, + }; + + // 安全获取 task 状态配色,未知状态回退到默认(避免 undefined.borderColor 崩溃) + const taskStyle = taskResult ? (taskStatusConfig[taskResult.status] ?? taskStatusConfig.failed) : null; return (
@@ -481,14 +515,14 @@ export const MessageBubble = memo(function MessageBubble({
setToolExpanded(!toolExpanded)} className={`cursor-pointer rounded-xl border bg-[var(--bg-tertiary)]/60 w-full transition-all duration-500 hover:bg-[var(--bg-tertiary)]/80 group ${ - taskResult ? taskStatusConfig[taskResult.status].borderColor : statusConfig.fullBorder + taskStyle ? taskStyle.borderColor : statusConfig.fullBorder }`} > {/* Header row */}
@@ -496,9 +530,7 @@ export const MessageBubble = memo(function MessageBubble({ {taskResult ? ( diff --git a/web/src/hooks/chat/useMessages.ts b/web/src/hooks/chat/useMessages.ts index 3eea0e5..b0268f6 100644 --- a/web/src/hooks/chat/useMessages.ts +++ b/web/src/hooks/chat/useMessages.ts @@ -314,6 +314,39 @@ export function useMessages(options: UseMessagesOptions): UseMessagesReturn { if (getSubagentTaskId(message)) { // 子代理执行完成:bump 统一 trigger,App.tsx 根据 subAgentView 分派 load_task_messages bumpTopicRefreshTrigger(); + + // 更新主视图中 task tool result 占位消息的状态 + // task 工具返回时 status='running'(黄色转圈),子代理完成后需更新为最终状态 + if (msg.subagent_task_id && msg.subagent_status) { + const taskId = msg.subagent_task_id; + const newStatus = msg.subagent_status; + const newSummary = msg.subagent_summary; + setMessages((prev) => { + let changed = false; + const updated = prev.map((m) => { + if (m.type !== 'tool_result' || m.toolName !== 'task') return m; + if (!m.content) return m; + // content 是 TaskToolResult JSON(可能带 loop_detector 前缀) + const jsonStart = m.content.indexOf('{'); + if (jsonStart < 0) return m; + try { + const parsed = JSON.parse(m.content.slice(jsonStart)); + if (parsed.task_id !== taskId) return m; + if (parsed.status === newStatus) return m; + parsed.status = newStatus; + if (newSummary !== undefined) parsed.summary = newSummary; + const newJson = JSON.stringify(parsed); + changed = true; + const prefix = jsonStart > 0 ? m.content.slice(0, jsonStart) : ''; + return { ...m, content: prefix + newJson }; + } catch { + return m; + } + }); + return changed ? updated : prev; + }); + } + return true; } // 按 topic_id 移除处理状态,不论当前选中哪个话题。 diff --git a/web/src/main.tsx b/web/src/main.tsx index 9aa52ff..4cd4baa 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -3,8 +3,49 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { hasError: boolean; error: Error | null } +> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return ( +
+

React Render Error

+

{this.state.error?.name}: {this.state.error?.message}

+
{this.state.error?.stack}
+
+

Try clearing browser cache and localStorage, then refresh.

+ +
+ ); + } + return this.props.children; + } +} + ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 5780495..1c9c97e 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -303,6 +303,9 @@ export interface ExecutionCompleted { topic_id?: string; timestamp?: number; subagent_task_id?: string; + /** 子代理最终状态(completed/failed/timeout/cancelled/interrupted) */ + subagent_status?: string; + subagent_summary?: string; } export type WsOutbound = @@ -504,7 +507,15 @@ export interface ChatMessage { /** task 工具返回的 JSON 结构 */ export interface TaskToolResult { - status: 'success' | 'failed' | 'timeout'; + // status 值由后端 TaskToolResult.status 决定,包括: + // - running: 异步子代理刚 spawn 的占位结果 + // - success: 子代理执行成功(旧) + // - completed: 子代理执行成功(新,与 SubagentStatus 对齐) + // - failed: 子代理执行失败 + // - timeout: 子代理执行超时 + // - cancelled: 子代理被用户取消 + // - interrupted: 子代理因服务器重启被中断 + status: string; summary: string; output: string; task_id: string;