From 5ec32f33da98831bdfc8a531d23bcb5b256e94c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=BD=B1233?= <1440196015@qq.com> Date: Mon, 20 Jul 2026 21:57:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(trace):=20Trace=20Viewer=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=89=20runId=20=E5=88=86=E7=BB=84=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=EF=BC=8C=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89=E8=BD=AE?= =?UTF-8?q?=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 v0.3.8 引入的"覆盖"问题:sendMessage 清空 traceSteps 导致 前一条消息的 trace 被永久丢失。 方案 A+ — 既不叠加也不覆盖,按 runId 分组展示所有轮次: - agent-store.ts: 回退 sendMessage 的 traceSteps: [] 清空(保留历史) 保留 tokenUsage 重置(当前 run 统计应该重置) - TraceViewer.tsx: 重写渲染逻辑 - 新增 groupByRun 工具函数,按 runId 分组 + 计算轮次序号 - 每个 run 独立 section,带"第 N 轮"标题 + 状态徽标(进行中/已完成)+ 步数 - 当前 run 高亮(primary 边框 + 浅色背景 + MessageSquare 图标) - 历史 run 弱化(divider 边框 + 透明背景 + 0.85 透明度) - 顶部统计"共 N 轮" - isCurrent 判定改为"当前 run 的最后一个 step"(之前是"全局最后一个") useAgentStream 的 runId 隔离逻辑(v0.3.8 已加)无需改动, 正好支撑多 run 共存设计:isSameIteration 校验 runId,跨 run 孤立 TERMINATED 跳过不创建 step。 冒烟测试场景: - 多轮对话 → 每轮独立显示,不再叠加 - 历史 trace 不丢失 → 切换会话回来仍可见所有轮次 - 当前 run 高亮 → 视觉聚焦"正在进行的活动" --- src/components/trace/TraceViewer.tsx | 134 ++++++++++++++++++++++++--- src/stores/agent-store.ts | 6 +- 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/src/components/trace/TraceViewer.tsx b/src/components/trace/TraceViewer.tsx index 72604ac..f0ff1ce 100644 --- a/src/components/trace/TraceViewer.tsx +++ b/src/components/trace/TraceViewer.tsx @@ -1,17 +1,70 @@ /** * TraceViewer — ReAct 迭代追踪时间轴 * + * 方案 A+: 按 runId 分组渲染,每轮 AI 回复一个独立 section + * - 所有历史 run 的 steps 都可见 + * - 每个 run 视觉分离(带标题"第 N 轮"+ 状态徽标) + * - 当前 run 高亮显示 + * * flex:1 撑满详情面板剩余空间,内容超出时内部滚动。 */ -import { Box, Typography, Stack } from '@mui/material'; -import { Activity } from 'lucide-react'; +import { Box, Typography, Stack, Chip } from '@mui/material'; +import { Activity, MessageSquare } from 'lucide-react'; +import { useMemo } from 'react'; import { useAgentStore } from '@renderer/stores/agent-store'; +import type { TraceStep as TraceStepType } from '@renderer/stores/agent-store'; import { TraceStep } from './TraceStep'; +interface RunGroup { + runId: string; + index: number; // 1-based 轮次序号 + steps: TraceStepType[]; + isCurrent: boolean; // 是否为当前正在进行的 run + isCompleted: boolean; // 是否已完成(最后一个 step 有 completedAt) +} + +function groupByRun(traceSteps: TraceStepType[], currentRunId: string | null): RunGroup[] { + const runOrder: string[] = []; + const runMap = new Map(); + + for (const step of traceSteps) { + const rid = step.runId; + if (!rid) continue; + if (!runMap.has(rid)) { + runMap.set(rid, []); + runOrder.push(rid); + } + runMap.get(rid)!.push(step); + } + + // 当前 runId 优先;若为空(idle)则取最后一个 run 作为"当前展示" + const effectiveCurrentRunId = currentRunId ?? (runOrder.length > 0 ? runOrder[runOrder.length - 1] : null); + + return runOrder.map((rid, i) => { + const steps = runMap.get(rid)!; + const lastStep = steps[steps.length - 1]; + return { + runId: rid, + index: i + 1, + steps, + isCurrent: rid === effectiveCurrentRunId, + isCompleted: Boolean(lastStep?.completedAt), + }; + }); +} + export function TraceViewer(): React.JSX.Element { - const traceSteps = useAgentStore((s) => s.traceSteps); + const allTraceSteps = useAgentStore((s) => s.traceSteps); const agentStatus = useAgentStore((s) => s.agentStatus); + const currentRunId = useAgentStore((s) => s.currentRunId); + + const groups = useMemo( + () => groupByRun(allTraceSteps, currentRunId), + [allTraceSteps, currentRunId], + ); + + const isEmpty = groups.length === 0; return ( @@ -20,20 +73,79 @@ export function TraceViewer(): React.JSX.Element { Trace Viewer + {!isEmpty && ( + + 共 {groups.length} 轮 + + )} - - {traceSteps.length === 0 ? ( + + {isEmpty ? ( 等待 Agent 活动... ) : ( - traceSteps.map((step, i) => ( - + groups.map((group) => ( + + + + + 第 {group.index} 轮 + + {group.isCurrent && agentStatus !== 'idle' && ( + + )} + {group.isCompleted && !group.isCurrent && ( + + )} + + {group.steps.length} 步 + + + + + {group.steps.map((step, i) => ( + + ))} + + )) )} diff --git a/src/stores/agent-store.ts b/src/stores/agent-store.ts index 5fb6719..a1ed7bc 100644 --- a/src/stores/agent-store.ts +++ b/src/stores/agent-store.ts @@ -267,9 +267,9 @@ export const useAgentStore = create((set, get) => ({ isStreaming: true, currentRunId: null, // 将在首个 streamEvent 中由 runId 设置 currentIteration: 0, - // 重置 trace 状态:防止新消息在上一条消息的 trace 上面叠加渲染 - // 前一条消息的 trace 已在 done 事件中通过 saveTraceData() 持久化到 DB - traceSteps: [], + // 方案 A: 不清空 traceSteps,避免前一条消息的 trace 被永久覆盖 + // TraceViewer 按 runId 过滤显示,只展示当前 run 的 steps + // 历史 trace 仍在 DB 中,切换会话回来可恢复 tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, }));