fix(web): split rectification process talk from the spoken reply

Keep provider thinking off, classify CoT as 思考, and stop remounted sessions from firing a second opening.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-23 10:14:35 +08:00
co-authored by Cursor
parent f699c35f56
commit f03a2706b4
13 changed files with 376 additions and 32 deletions
@@ -83,7 +83,9 @@ test("birth-time rectification entry mounts the V9 case-ref chat", () => {
test("persisted rectification turns hydrate after the async Case refresh", () => {
assert.match(chat, /function messagesFromTurns\(initialTurns:/);
assert.match(chat, /useState<RenderMessage\[\]>\(\(\) => messagesFromTurns\(initialTurns\)\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(chat, /finalizeRectificationSpokenAndThinking/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.length > 0 \? "ready" : "loading"\}`\}/);
assert.doesNotMatch(page, /rectificationTurns\.at\(-1\)\?\.id/);
assert.match(page, /methods: Array\.isArray\(\(turn\.receipt as \{ methods\?: unknown \}\)\.methods\)/);
assert.match(component, /methods\?: readonly string\[\]/);
});
@@ -92,11 +94,14 @@ test("opening is server-owned: shouldStartOpening drives the first turn, never c
assert.doesNotMatch(chat, /initialMessages\.length > 0 \|\| openingStarted/);
assert.doesNotMatch(chat, /agenticOpeningInstruction|用户刚进入生时校正会话/);
assert.match(chat, /shouldStartOpening/);
assert.match(chat, /if \(initialTurns\.length > 0\) \{/);
assert.match(chat, /if \(readonly \|\| openingStarted\.current \|\| !shouldStartOpening\) return/);
assert.match(chat, /onOpeningConsumed\?\.\(\)/);
assert.match(chat, /void send\("opening", ""\)/);
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only"\]\)/);
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
});
test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
@@ -186,9 +191,11 @@ test("agent tool calls never end silently; the runner owns completion and failur
test("persisted turns survive remounts; duplicate openings are suppressed by the server", () => {
assert.match(chat, /initialTurns/);
assert.match(chat, /const openingStarted = useRef\(false\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(chat, /if \(initialTurns\.length > 0\) \{/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.length > 0 \? "ready" : "loading"\}`\}/);
assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
});
test("the agent route verifies the exact Case/Session binding before any turn", () => {
@@ -229,6 +236,7 @@ test("usage completes or releases without hiding settlement failures", () => {
assert.match(run, /usage_settlement_failed/);
assert.match(run, /thinking: "disabled"/);
assert.match(run, /toPublicThinkingDelta/);
assert.match(run, /splitRectificationSpokenAndThinking/);
assert.match(route, /featureKey: "rectification"/);
assert.match(route, /rectification:case:\$\{caseId\}/);
});
@@ -301,7 +309,7 @@ test("rectification keeps receipts for the varga sentence and shows live tool pr
assert.doesNotMatch(chat, /reasoning-delta|chain-of-thought/);
assert.match(chat, /event.type === "thinking.delta"/);
assert.match(activityStatus, /className="message-thinking"/);
assert.match(activityStatus, /思考过程/);
assert.match(activityStatus, />思考</);
assert.match(activityStatus, /userOpen \?\? !hasAnswer/);
assert.match(styles, /\.message-thinking-body/);
});