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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user