diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md
index 82674b8b..53b2a828 100644
--- a/docs/BUG_HISTORY.md
+++ b/docs/BUG_HISTORY.md
@@ -5451,6 +5451,18 @@
- 复发自:BUG-354(关 thinking 后过程进正文)、BUG-357(用正则从正文里切两栏)
- 修复版本:`04463e9a`
+## BUG-360 | 生时纠正后段思考被写进第一段「思考」
-
-
+- 状态:resolved
+- 首次发现:2026-08-23
+- 最近更新:2026-08-23
+- 影响面:生时纠正对话、`AgentActivityStatus`、流式 `thinking.delta` 与 `tool.activity`
+- 用户现象:界面先列出「读取校正记录」「正在整理多条事件证据…」,再给一整块「思考」。工具之后的思维链被追加进第一段思考,而不是出现在「正在整理…」下面。
+- 触发条件:纠正回合先出现 `thinking.delta`,再 `tool.activity`,之后继续 `thinking.delta`。
+- 根因:`AgentActivityStatus` 先渲染 `completedTrail` 和当前活动,再渲染整段 `thinkingText`。流式处理把所有 `thinking.delta` 拼进同一个 `thinkingRaw`,无法按事件顺序把后段思考放到工具行之后。
+- 修复:纠正流按事件维护 `activityTrace`。工具开始会结束当前思考行;工具之后的 `thinking.delta` 开新的思考行。有轨迹时不再把拼接后的 `thinkingText` 单独渲在工具列表下面。
+- 验证:`frontend/tests/agent-activity-trace.test.ts`、`frontend/tests/chat-stream-layout.test.ts`、`frontend/tests/consultation-run-timeline.test.ts`
+- 防复发:纠正流不得把全部 `thinking.delta` 拼成一块再画在活动列表下方。工具后的思考必须是新的 trace 行。咨询时间线同样不得把后段 `thinking.delta` upsert 回第一条思考。
+- 相关记录:BUG-095、BUG-357、BUG-359
+- 复发自:BUG-095(步骤轨迹只证明工具发生过,没有与思维链按时间交错)
+- 修复版本:待本次提交
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 485b8e73..d3f52931 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -271,6 +271,13 @@ button:disabled { cursor: default; opacity: .45; }
gap: var(--space-2);
min-height: 24px;
}
+.agent-thinking-step.is-think:has(.message-thinking) {
+ display: block;
+ min-height: 0;
+}
+.agent-thinking-step.is-think .message-thinking {
+ margin-bottom: 0;
+}
.agent-thinking-marker {
width: 20px;
height: 20px;
diff --git a/frontend/src/components/agent-activity-status.tsx b/frontend/src/components/agent-activity-status.tsx
index 0f56bb3a..ef548185 100644
--- a/frontend/src/components/agent-activity-status.tsx
+++ b/frontend/src/components/agent-activity-status.tsx
@@ -7,6 +7,7 @@ import type { OrbState } from "thinking-orbs";
import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
import { activityCompletedSteps, activityElapsedLabel } from "@/lib/chat-message-view";
+import type { AgentActivityTraceItem } from "@/lib/agent-activity-trace";
const labels = {
working: "正在处理任务…",
@@ -71,6 +72,7 @@ export function AgentActivityStatus({
startedAt,
completedTrail,
thinkingText,
+ activityTrace,
hasAnswer = false,
showLive = true,
}: Readonly<{
@@ -79,11 +81,29 @@ export function AgentActivityStatus({
startedAt?: number;
completedTrail?: string;
thinkingText?: string;
+ activityTrace?: readonly AgentActivityTraceItem[];
hasAnswer?: boolean;
showLive?: boolean;
}>) {
const completedSteps = activityCompletedSteps(completedTrail);
const live = showLive && !hasAnswer;
+ const trace = activityTrace ?? [];
+ if (trace.length > 0) {
+ return (
+
+
+ {trace.map((item) => (
+
+ ))}
+
+
+ );
+ }
if (!live && completedSteps.length === 0 && !thinkingText?.trim()) return null;
return (
@@ -115,3 +135,45 @@ export function AgentActivityStatus({
);
}
+
+function TraceStep({
+ item,
+ state,
+ hasAnswer,
+}: Readonly<{
+ item: AgentActivityTraceItem;
+ state: AgentActivityState;
+ hasAnswer: boolean;
+}>) {
+ if (item.kind === "think") {
+ return (
+
+ {item.text?.trim() ? (
+
+ ) : (
+ <>
+
+ {item.status === "live" ? : }
+
+ {item.label}
+ >
+ )}
+
+ );
+ }
+ return (
+
+
+ {item.status === "live" ? : }
+
+ {item.status === "live" ? (
+
+ {item.label}
+ {item.startedAt ? : null}
+
+ ) : (
+ {item.label}
+ )}
+
+ );
+}
diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx
index 191cf184..fec5ae32 100644
--- a/frontend/src/components/chat-message-row.tsx
+++ b/frontend/src/components/chat-message-row.tsx
@@ -64,9 +64,10 @@ export function ChatMessageRow({
const hasAnswer = Boolean(message.text.trim());
const consultTimeline = message.timeline;
const thinkingSections = message.thinkingSections ?? [];
+ const hasTrace = (message.activityTrace?.length ?? 0) > 0;
const showReport = consultTimeline === undefined && thinkingSections.length > 0;
const showLiveActivity = showActivity && !showReport && consultTimeline === undefined;
- const showThinkingPanel = showLiveActivity || (!showReport && consultTimeline === undefined && Boolean(message.thinkingText?.trim()));
+ const showThinkingPanel = showLiveActivity || (!showReport && consultTimeline === undefined && (Boolean(message.thinkingText?.trim()) || hasTrace));
const showSpokenAnswer = !showReport && Boolean(message.text);
const stackedThinkingAndAnswer = showThinkingPanel && showSpokenAnswer;
@@ -101,7 +102,8 @@ export function ChatMessageRow({
label={activityLabel}
startedAt={message.activity?.startedAt}
completedTrail={message.activity?.completedTrail}
- thinkingText={message.thinkingText}
+ thinkingText={hasTrace ? undefined : message.thinkingText}
+ activityTrace={message.activityTrace}
hasAnswer={hasAnswer}
showLive={showLiveActivity}
/>
diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx
index 79809a3c..23e684c8 100644
--- a/frontend/src/components/rectification-agentic-chat.tsx
+++ b/frontend/src/components/rectification-agentic-chat.tsx
@@ -6,6 +6,16 @@ import { createPortal } from "react-dom";
import { parseAgentReply } from "@/lib/agent-reply";
import { nextActivityView, type ChatMessage, type ChatMessageView } from "@/lib/chat-message-view";
import {
+ appendActivityTraceThinking,
+ completeActivityTrace,
+ completeActivityTraceStep,
+ emptyActivityTrace,
+ freezeLiveThink,
+ startActivityTraceStep,
+ type AgentActivityTraceItem,
+} from "@/lib/agent-activity-trace";
+import {
+ RECTIFICATION_TOOL_DONE_LABELS,
RECTIFICATION_TOOL_PROGRESS_LABELS,
rectificationCompletedTrail,
rectificationToolActivityPhase,
@@ -151,6 +161,7 @@ type RenderMessage = ChatMessageView & {
completedReceipt?: CompletedActivityReceiptView;
failed?: boolean;
turnId?: string;
+ activityTrace?: readonly AgentActivityTraceItem[];
};
function turnOfferedSelection(message: RenderMessage): boolean {
@@ -383,7 +394,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...(action === "message"
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
: []),
- { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", activity: {
+ { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", activityTrace: emptyActivityTrace(), activity: {
phase: "evidence-validation",
label: "正在处理…",
startedAt: Date.now(),
@@ -393,6 +404,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
let raw = "";
let thinkingRaw = "";
+ let activityTrace: readonly AgentActivityTraceItem[] = emptyActivityTrace();
let activityReceiptState = createRectificationActivityReceiptState();
let completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
let completedTurnId: string | undefined;
@@ -464,6 +476,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (typeof event.type !== "string") continue;
if (event.type === "answer.delta" && typeof event.text === "string") {
raw += event.text;
+ activityTrace = freezeLiveThink(activityTrace);
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = parseAgentReply(settled.spoken);
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
@@ -471,6 +484,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
+ activityTrace,
state: parsed.text ? "streaming" : "thinking",
activity: nextActivityView(message.activity, {
phase: "answer-composition",
@@ -480,6 +494,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: message));
} else if (event.type === "thinking.delta" && typeof event.text === "string") {
thinkingRaw += event.text;
+ activityTrace = appendActivityTraceThinking(activityTrace, event.text);
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = parseAgentReply(settled.spoken);
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
@@ -487,12 +502,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
+ activityTrace,
state: parsed.text ? "streaming" : "thinking",
}
: message));
} else if (event.type === "attempt.reset") {
raw = "";
thinkingRaw = "";
+ activityTrace = emptyActivityTrace();
activityReceiptState = createRectificationActivityReceiptState();
completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
completedTurnId = undefined;
@@ -501,6 +518,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: "",
thinkingText: undefined,
+ activityTrace,
state: "thinking",
completedReceipt: undefined,
failed: false,
@@ -523,9 +541,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const tool = isPublicRectificationTool(event.tool) ? event.tool : null;
if (!tool) continue;
if (event.status === "started") {
+ activityTrace = startActivityTraceStep(
+ activityTrace,
+ tool,
+ RECTIFICATION_TOOL_PROGRESS_LABELS[tool],
+ );
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
+ activityTrace,
activity: nextActivityView(message.activity, {
phase: rectificationToolActivityPhase(tool),
label: RECTIFICATION_TOOL_PROGRESS_LABELS[tool],
@@ -536,6 +560,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
continue;
}
if (event.status !== "completed" && event.status !== "failed") continue;
+ activityTrace = completeActivityTraceStep(
+ activityTrace,
+ tool,
+ RECTIFICATION_TOOL_DONE_LABELS[tool],
+ );
activityReceiptState = reduceRectificationActivityReceipt(activityReceiptState, {
tool,
status: event.status,
@@ -544,6 +573,18 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: [],
});
completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
+ setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
+ ? {
+ ...message,
+ activityTrace,
+ completedReceipt,
+ activity: nextActivityView(message.activity, {
+ phase: rectificationToolActivityPhase(tool),
+ label: RECTIFICATION_TOOL_DONE_LABELS[tool],
+ completedTrail: rectificationCompletedTrail(activityReceiptState.completedSteps),
+ }),
+ }
+ : message));
}
}
}
@@ -558,6 +599,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
+ activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
failed: false,
@@ -570,6 +612,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
+ activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
failed: true,
@@ -607,6 +650,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
+ activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
failed: false,
diff --git a/frontend/src/lib/agent-activity-trace.ts b/frontend/src/lib/agent-activity-trace.ts
new file mode 100644
index 00000000..83139b52
--- /dev/null
+++ b/frontend/src/lib/agent-activity-trace.ts
@@ -0,0 +1,107 @@
+export type AgentActivityTraceKind = "activity" | "think";
+export type AgentActivityTraceStatus = "live" | "done";
+
+export type AgentActivityTraceItem = Readonly<{
+ id: string;
+ kind: AgentActivityTraceKind;
+ status: AgentActivityTraceStatus;
+ label: string;
+ text?: string;
+ startedAt?: number;
+ tool?: string;
+}>;
+
+const THINKING_TEXT_LIMIT = 8_000;
+
+export function emptyActivityTrace(): readonly AgentActivityTraceItem[] {
+ return [];
+}
+
+export function appendActivityTraceThinking(
+ rows: readonly AgentActivityTraceItem[],
+ text: string,
+): readonly AgentActivityTraceItem[] {
+ if (!text) return rows;
+ const last = rows.at(-1);
+ if (last?.kind === "think" && last.status === "live") {
+ return rows.map((row, index) => (
+ index === rows.length - 1
+ ? { ...row, text: `${row.text ?? ""}${text}`.slice(0, THINKING_TEXT_LIMIT) }
+ : row
+ ));
+ }
+ const frozen = freezeLiveThink(rows);
+ const thinkCount = frozen.filter((row) => row.kind === "think").length;
+ return [
+ ...frozen,
+ {
+ id: `think-${thinkCount + 1}`,
+ kind: "think",
+ status: "live",
+ label: "思考",
+ text: text.slice(0, THINKING_TEXT_LIMIT),
+ },
+ ];
+}
+
+export function startActivityTraceStep(
+ rows: readonly AgentActivityTraceItem[],
+ tool: string,
+ label: string,
+ startedAt = Date.now(),
+): readonly AgentActivityTraceItem[] {
+ const frozen = freezeLiveThink(rows);
+ const last = frozen.at(-1);
+ if (last?.kind === "activity" && last.status === "live" && last.tool === tool) {
+ return frozen;
+ }
+ const closed = frozen.map((row) => (
+ row.kind === "activity" && row.status === "live" ? { ...row, status: "done" as const } : row
+ ));
+ const toolCount = closed.filter((row) => row.kind === "activity" && row.tool === tool).length;
+ return [
+ ...closed,
+ {
+ id: `activity-${tool}-${toolCount + 1}`,
+ kind: "activity",
+ status: "live",
+ label,
+ startedAt,
+ tool,
+ },
+ ];
+}
+
+export function completeActivityTraceStep(
+ rows: readonly AgentActivityTraceItem[],
+ tool: string,
+ label?: string,
+): readonly AgentActivityTraceItem[] {
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
+ const row = rows[index];
+ if (row?.kind === "activity" && row.tool === tool && row.status === "live") {
+ return rows.map((item, current) => (
+ current === index
+ ? { ...item, status: "done" as const, label: label ?? item.label }
+ : item
+ ));
+ }
+ }
+ return rows;
+}
+
+export function freezeLiveThink(
+ rows: readonly AgentActivityTraceItem[],
+): readonly AgentActivityTraceItem[] {
+ return rows.map((row) => (
+ row.kind === "think" && row.status === "live" ? { ...row, status: "done" as const } : row
+ ));
+}
+
+export function completeActivityTrace(
+ rows: readonly AgentActivityTraceItem[],
+): readonly AgentActivityTraceItem[] {
+ return freezeLiveThink(rows).map((row) => (
+ row.status === "live" ? { ...row, status: "done" as const } : row
+ ));
+}
diff --git a/frontend/src/lib/chat-message-view.ts b/frontend/src/lib/chat-message-view.ts
index 5efdbdd1..c97943af 100644
--- a/frontend/src/lib/chat-message-view.ts
+++ b/frontend/src/lib/chat-message-view.ts
@@ -1,3 +1,4 @@
+import type { AgentActivityTraceItem } from "./agent-activity-trace.ts";
import type { AgentExecutionReceipt, PublicActivityPhase, WorkflowReceipt } from "./consultation-agent-events.ts";
import type { PublicThinkingSection } from "./consultation-thinking-plan.ts";
import {
@@ -66,6 +67,7 @@ export type ChatMessageView = ChatMessage & {
readonly renderKey: string;
readonly state: "settled" | "streaming" | "thinking";
readonly activity?: AgentActivityView;
+ readonly activityTrace?: readonly AgentActivityTraceItem[];
readonly timeline?: readonly ConsultationTimelineRow[];
};
diff --git a/frontend/src/lib/consultation-run-timeline.ts b/frontend/src/lib/consultation-run-timeline.ts
index 7798ea35..689c4547 100644
--- a/frontend/src/lib/consultation-run-timeline.ts
+++ b/frontend/src/lib/consultation-run-timeline.ts
@@ -34,7 +34,6 @@ export type ConsultationTimelineState = Readonly<{
const METHOD_ID = "method";
const CALCULATE_ID = "calculate";
-const OPEN_THINK_ID = "think-open";
export function emptyConsultationTimeline(): ConsultationTimelineState {
return { rows: [], answer: "" };
@@ -97,7 +96,7 @@ export function reduceConsultationTimeline(
steps: event.steps,
};
const withCalc = enrichCalculate(completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL), section);
- const withoutOpen = completeRow(withCalc, OPEN_THINK_ID);
+ const withoutOpen = completeLiveThink(withCalc);
return upsertRow(withoutOpen, {
id: `think-${section.id}`,
kind: "think",
@@ -106,15 +105,19 @@ export function reduceConsultationTimeline(
});
}
if (event.type === "thinking.delta") {
- const think = lastRow(state.rows, (row) => row.kind === "think" && row.status === "live")
- ?? {
- id: OPEN_THINK_ID,
- kind: "think" as const,
- status: "live" as const,
- label: "正在分析…",
- };
- const thinkingText = `${think.thinkingText ?? ""}${event.text}`.slice(0, 4_000);
- return upsertRow(state, { ...think, status: "live", thinkingText });
+ const think = lastRow(state.rows, (row) => row.kind === "think" && row.status === "live");
+ if (think) {
+ const thinkingText = `${think.thinkingText ?? ""}${event.text}`.slice(0, 4_000);
+ return upsertRow(state, { ...think, status: "live", thinkingText });
+ }
+ const thinkCount = state.rows.filter((row) => row.kind === "think").length;
+ return upsertRow(state, {
+ id: `think-open-${thinkCount + 1}`,
+ kind: "think",
+ status: "live",
+ label: "正在分析…",
+ thinkingText: event.text.slice(0, 4_000),
+ });
}
if (event.type === "answer.delta") {
const next = { ...completeLiveThink(state), answer: `${state.answer}${event.text}` };
diff --git a/frontend/tests/agent-activity-trace.test.ts b/frontend/tests/agent-activity-trace.test.ts
new file mode 100644
index 00000000..1bab9147
--- /dev/null
+++ b/frontend/tests/agent-activity-trace.test.ts
@@ -0,0 +1,57 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ appendActivityTraceThinking,
+ completeActivityTrace,
+ completeActivityTraceStep,
+ emptyActivityTrace,
+ startActivityTraceStep,
+} from "../src/lib/agent-activity-trace.ts";
+
+test("thinking after a tool starts a new think row instead of filling the first", () => {
+ let rows = appendActivityTraceThinking(emptyActivityTrace(), "先确认用户说的是入职当天。");
+ rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 1_000);
+ rows = appendActivityTraceThinking(rows, "再把 2024-04-07 写成经历证据。");
+
+ assert.deepEqual(rows.map((row) => row.kind), ["think", "activity", "think"]);
+ assert.equal(rows[0]?.status, "done");
+ assert.equal(rows[0]?.text, "先确认用户说的是入职当天。");
+ assert.equal(rows[1]?.label, "正在整理多条事件证据…");
+ assert.equal(rows[1]?.status, "live");
+ assert.equal(rows[2]?.status, "live");
+ assert.equal(rows[2]?.text, "再把 2024-04-07 写成经历证据。");
+ assert.notEqual(rows[0]?.id, rows[2]?.id);
+});
+
+test("completing a tool then thinking starts a new think under the done activity", () => {
+ let rows = appendActivityTraceThinking(emptyActivityTrace(), "先读取校正记录。");
+ rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 1_000);
+ rows = completeActivityTraceStep(rows, "rectification-record-evidence-batch", "整理事件证据");
+ rows = appendActivityTraceThinking(rows, "用户说转做开发就是 2024-04-07。");
+
+ assert.deepEqual(rows.map((row) => `${row.kind}:${row.status}`), [
+ "think:done",
+ "activity:done",
+ "think:live",
+ ]);
+ assert.equal(rows[1]?.label, "整理事件证据");
+ assert.equal(rows[2]?.text, "用户说转做开发就是 2024-04-07。");
+});
+
+test("the same live tool is not duplicated, and a later tool follows the second think", () => {
+ let rows = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 10);
+ rows = startActivityTraceStep(rows, "rectification-read-case", "正在读取校正记录…", 20);
+ assert.equal(rows.length, 1);
+ assert.equal(rows[0]?.startedAt, 10);
+
+ rows = completeActivityTraceStep(rows, "rectification-read-case", "读取校正记录");
+ rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 30);
+ assert.deepEqual(rows.map((row) => `${row.kind}:${row.label}`), [
+ "activity:读取校正记录",
+ "activity:正在整理多条事件证据…",
+ ]);
+
+ const settled = completeActivityTrace(rows);
+ assert.ok(settled.every((row) => row.status === "done"));
+});
diff --git a/frontend/tests/chat-stream-layout.test.ts b/frontend/tests/chat-stream-layout.test.ts
index 959290c2..385494a2 100644
--- a/frontend/tests/chat-stream-layout.test.ts
+++ b/frontend/tests/chat-stream-layout.test.ts
@@ -101,8 +101,12 @@ test("shows honest agent activity states before and during streamed text", () =>
assert.match(pageSource, /streamedThinking/);
assert.match(messageRowSource, /showLiveActivity/);
assert.match(messageRowSource, /ConsultationThinkingReport/);
- assert.match(messageRowSource, /thinkingText=\{message\.thinkingText\}/);
- assert.match(messageRowSource, /showLiveActivity \|\| \(!showReport && consultTimeline === undefined && Boolean\(message\.thinkingText/);
+ assert.match(messageRowSource, /thinkingText=\{hasTrace \? undefined : message\.thinkingText\}/);
+ assert.match(messageRowSource, /activityTrace=\{message\.activityTrace\}/);
+ assert.match(messageRowSource, /showLiveActivity \|\| \(!showReport && consultTimeline === undefined && \(Boolean\(message\.thinkingText/);
+ assert.match(activitySource, /activityTrace \?\? \[\]/);
+ assert.match(activitySource, /function TraceStep/);
+ assert.match(globalStyles, /\.agent-thinking-step\.is-think:has\(\.message-thinking\)/);
assert.match(reportSource, /caption="思考"/);
assert.match(reportSource, /reasoning=\{thinkingText\}/);
assert.match(reportSource, /aria-label="分析"/);
@@ -179,6 +183,10 @@ test("ordinary consultation replies reuse the shared Agent action bar", () => {
assert.match(pageSource, /function regenerateLatestAnswer\(renderKey: string\)/);
assert.match(pageSource, /messages: session\.messages\.slice\(0, -1\)/);
assert.match(pageSource, /restoreOnFailure: session/);
+ assert.match(rectificationChat, /appendActivityTraceThinking/);
+ assert.match(rectificationChat, /startActivityTraceStep/);
+ assert.match(rectificationChat, /completeActivityTraceStep/);
+ assert.match(rectificationChat, /activityTrace = appendActivityTraceThinking\(activityTrace, event\.text\)/);
assert.match(rectificationChat, / {
+ const state = reduceConsultationTimelineEvents([
+ { type: "thinking.delta", text: "先确认用户说的是入职当天。" },
+ {
+ type: "tool.started",
+ callId: "tool-1",
+ tool: "run-jyotish-consultation",
+ label: "正在计算本命盘…",
+ },
+ { type: "thinking.delta", text: "再按宫位写。" },
+ ]);
+
+ assert.deepEqual(state.rows.map((row) => row.kind), ["think", "calculate", "think"]);
+ assert.equal(state.rows[0]?.status, "done");
+ assert.equal(state.rows[0]?.thinkingText, "先确认用户说的是入职当天。");
+ assert.equal(state.rows[2]?.status, "live");
+ assert.equal(state.rows[2]?.thinkingText, "再按宫位写。");
+ assert.notEqual(state.rows[0]?.id, state.rows[2]?.id);
+});
+
test("idle chat without a natal calculation has no calculate row", () => {
const [section] = generalConsultationThinkingPlan();
assert.ok(section);
diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts
index ac5e1d82..71566593 100644
--- a/frontend/tests/rectification-agentic-entry.test.ts
+++ b/frontend/tests/rectification-agentic-entry.test.ts
@@ -313,6 +313,10 @@ test("rectification keeps receipts for the varga sentence and shows live tool pr
assert.match(chat, /回答未完成,已保留现有内容;本次不会扣点/);
assert.doesNotMatch(chat, /reasoning-delta|chain-of-thought/);
assert.match(chat, /event.type === "thinking.delta"/);
+ assert.match(chat, /appendActivityTraceThinking/);
+ assert.match(chat, /startActivityTraceStep/);
+ assert.match(chat, /completeActivityTraceStep/);
+ assert.match(activityStatus, /activityTrace/);
assert.match(activityStatus, /className="message-thinking"/);
assert.match(activityStatus, />思考);
assert.match(activityStatus, /userOpen \?\? !hasAnswer/);