fix(web): keep later thinking below rectification tool steps

BUG-360: stream thinking and tool activity as an ordered trace so later CoT opens under 正在整理 instead of filling the first 思考 block.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-23 17:02:06 +08:00
parent f18eb195d9
commit 6b225a288c
12 changed files with 346 additions and 18 deletions
+14 -2
View File
@@ -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(步骤轨迹只证明工具发生过,没有与思维链按时间交错)
- 修复版本:待本次提交
+7
View File
@@ -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;
@@ -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 (
<div className="agent-thinking-panel agent-activity-status">
<ol className="agent-thinking-timeline">
{trace.map((item) => (
<TraceStep
key={item.id}
item={item}
state={state}
hasAnswer={hasAnswer}
/>
))}
</ol>
</div>
);
}
if (!live && completedSteps.length === 0 && !thinkingText?.trim()) return null;
return (
@@ -115,3 +135,45 @@ export function AgentActivityStatus({
</div>
);
}
function TraceStep({
item,
state,
hasAnswer,
}: Readonly<{
item: AgentActivityTraceItem;
state: AgentActivityState;
hasAnswer: boolean;
}>) {
if (item.kind === "think") {
return (
<li className={`agent-thinking-step is-think is-${item.status}`}>
{item.text?.trim() ? (
<MessageThinkingTrace text={item.text} hasAnswer={hasAnswer || item.status === "done"} />
) : (
<>
<span className={`agent-thinking-marker${item.status === "live" ? " is-live-marker" : ""}`} aria-hidden="true">
{item.status === "live" ? <ThinkingOrb aria-hidden="true" state={state} size={20} /> : <Check />}
</span>
<span>{item.label}</span>
</>
)}
</li>
);
}
return (
<li className={`agent-thinking-step is-${item.status}`}>
<span className={`agent-thinking-marker${item.status === "live" ? " is-live-marker" : ""}`} aria-hidden="true">
{item.status === "live" ? <ThinkingOrb aria-hidden="true" state={state} size={20} /> : <Check />}
</span>
{item.status === "live" ? (
<span className="agent-activity-status__live" role="status">
<span key={item.label} className="agent-activity-status__text">{item.label}</span>
{item.startedAt ? <ActivityElapsed key={item.startedAt} startedAt={item.startedAt} /> : null}
</span>
) : (
<span>{item.label}</span>
)}
</li>
);
}
+4 -2
View File
@@ -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}
/>
@@ -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,
+107
View File
@@ -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
));
}
+2
View File
@@ -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[];
};
+14 -11
View File
@@ -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}` };
@@ -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"));
});
+10 -2
View File
@@ -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, /<ChatMessageActions/);
assert.match(globalStyles, /\.message-actions \{/);
assert.doesNotMatch(globalStyles, /\.rectification-message-actions \{/);
@@ -45,6 +45,26 @@ test("career and wealth events append method, calculate 1/2, think, then the fir
assert.match(state.rows[3]?.label ?? "", /正在写统一参数与原始结构/);
});
test("thinking after a tool starts a new think row instead of reopening the first", () => {
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);
@@ -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/);