fix(web): keep rectification replies as model text, hide thinking

Give the interview a hidden reasoning channel so planning leaves the spoken reply, publish terminal text-delta as-is, and stop regex or Case templates from replacing the model.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-25 16:34:25 +08:00
co-authored by Cursor
parent 12ac185a47
commit 5e34dd69ce
9 changed files with 175 additions and 54 deletions
@@ -52,10 +52,7 @@ import {
stableChoiceActionKey,
type ChoiceOptionId,
} from "@/lib/rectification-agentic/v9/choice-action";
import {
finalizeRectificationSpokenAndThinking,
settleRectificationSpokenAndThinking,
} from "@/lib/rectification-agentic/v9/spoken-answer";
import { finalizeRectificationSpokenAndThinking } from "@/lib/rectification-agentic/v9/spoken-answer";
import {
CHOICE_STOP_MESSAGE,
choiceCardUserMessage,
@@ -229,11 +226,9 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag
if (failed && !raw) return [];
if (isIncompleteRunBanner(raw)) return [];
const split = raw ? finalizeRectificationSpokenAndThinking(raw) : { thinking: "", spoken: raw };
const thinkingText = split.thinking.trim() || undefined;
return [{
role: "assistant",
text: split.spoken,
...(thinkingText ? { thinkingText } : {}),
renderKey: key,
state: turn.status === "completed" || failed ? "settled" : "thinking",
completedReceipt: completedReceiptFromPersisted(turn.receipt),
@@ -449,7 +444,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setDraft("");
let raw = "";
let thinkingRaw = "";
let activityTrace: readonly AgentActivityTraceItem[] = emptyActivityTrace();
let activityReceiptState = createRectificationActivityReceiptState();
let completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
@@ -528,14 +522,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (event.type === "answer.delta" && typeof event.text === "string") {
raw += event.text;
activityTrace = freezeLiveThink(activityTrace);
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
text: raw,
activityTrace,
state: settled.spoken ? "streaming" : "thinking",
state: raw.trim() ? "streaming" : "thinking",
activity: nextActivityView(message.activity, {
phase: "answer-composition",
label: "正在组织回答…",
@@ -555,7 +547,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: message));
} else if (event.type === "attempt.reset") {
raw = "";
thinkingRaw = "";
activityTrace = emptyActivityTrace();
activityReceiptState = createRectificationActivityReceiptState();
completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
@@ -639,8 +630,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
}
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = completed && !streamFailed ? parseAgentReply(settled.spoken) : { text: "", title: undefined };
const parsed = completed && !streamFailed ? parseAgentReply(raw) : { text: "", title: undefined };
const succeeded = completed && !streamFailed && Boolean(parsed.text);
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
@@ -648,7 +638,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
return [{
...message,
text: parsed.text,
thinkingText: settled.thinking.trim() || undefined,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
@@ -657,11 +646,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
activity: undefined,
}];
}
if (streamFailed || hasActivityReceipt(completedReceipt) || settled.spoken.trim()) {
if (streamFailed || hasActivityReceipt(completedReceipt) || raw.trim()) {
return [{
...message,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
text: raw,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
@@ -671,7 +659,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
return [];
}));
if (!succeeded && settled.spoken.trim()) {
if (!succeeded && raw.trim()) {
setError((current) => current || "回答未完成,已保留现有内容;本次不会扣点。");
} else if (!succeeded && runFailedMessage) {
setError((current) => current || runFailedMessage);
@@ -691,14 +679,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
? caught.name === "AbortError"
: caught instanceof Error && caught.name === "AbortError";
if (aborted) {
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
if (settled.spoken.trim() || settled.thinking.trim() || hasActivityReceipt(completedReceipt)) {
if (raw.trim() || hasActivityReceipt(completedReceipt)) {
return [{
...message,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
text: raw,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,