A completed tool left only checkmarks under 正在分析, so the run looked stuck. The composer and scoring-card 先这样 buttons were unused early exits. Co-authored-by: Cursor <cursoragent@cursor.com>
100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
import type { AgentActivityTraceItem } from "./agent-activity-trace.ts";
|
|
import type { AgentActivityView } from "./chat-message-view.ts";
|
|
import type { ConsultationTimelineKind, ConsultationTimelineRow } from "./consultation-run-timeline.ts";
|
|
import { RECTIFICATION_ANALYZING_LIVE_LABEL } from "./rectification-activity-labels.ts";
|
|
import type { CompletedActivityReceiptView } from "./rectification-activity-receipt.ts";
|
|
import { PUBLIC_RECTIFICATION_METHOD_LABELS } from "./rectification-varga-sentence.ts";
|
|
|
|
export const RECTIFICATION_TIMELINE_SOURCE_LIMIT = 8;
|
|
export const RECTIFICATION_TIMELINE_LIVE_ID = "activity-live";
|
|
const FAILED_SUFFIX = "未完成";
|
|
|
|
export type RectificationTimelineInput = Readonly<{
|
|
trace: readonly AgentActivityTraceItem[] | undefined;
|
|
receipt: CompletedActivityReceiptView | undefined;
|
|
activity: AgentActivityView | undefined;
|
|
settled: boolean;
|
|
}>;
|
|
|
|
function methodChips(receipt: CompletedActivityReceiptView | undefined): string[] {
|
|
if (!receipt) return [];
|
|
const seen = new Set<string>();
|
|
const chips: string[] = [];
|
|
for (const method of receipt.methods) {
|
|
const label = PUBLIC_RECTIFICATION_METHOD_LABELS[method];
|
|
if (!label || seen.has(label)) continue;
|
|
seen.add(label);
|
|
chips.push(label);
|
|
if (chips.length >= RECTIFICATION_TIMELINE_SOURCE_LIMIT) break;
|
|
}
|
|
return chips;
|
|
}
|
|
|
|
/** Progress labels are the only ones that name an action still under way. */
|
|
function isProgressLabel(label: string): boolean {
|
|
return /^正在/.test(label.trim());
|
|
}
|
|
|
|
function liveRowKind(phase: AgentActivityView["phase"] | undefined, label: string): ConsultationTimelineKind {
|
|
if (phase === "answer-composition") return "write";
|
|
if (label === RECTIFICATION_ANALYZING_LIVE_LABEL) return "think";
|
|
return "calculate";
|
|
}
|
|
|
|
/**
|
|
* Project the rectification agent's tool trace and receipt onto the same
|
|
* timeline rows the consultation surface renders, so both sessions share one
|
|
* step list, one live marker and one settled summary. Thinking text never
|
|
* reaches this surface: the public stream drops it on the server.
|
|
*/
|
|
export function rectificationTimelineRows(input: RectificationTimelineInput): ConsultationTimelineRow[] {
|
|
const failedTool = input.receipt?.failedTool;
|
|
const rows: ConsultationTimelineRow[] = (input.trace ?? []).map((item) => {
|
|
if (item.kind === "think") {
|
|
return {
|
|
id: item.id,
|
|
kind: "think",
|
|
status: item.status,
|
|
label: item.label,
|
|
...(item.text?.trim() ? { thinkingText: item.text } : {}),
|
|
};
|
|
}
|
|
const failed = item.status === "done" && failedTool !== undefined && item.tool === failedTool;
|
|
return {
|
|
id: item.id,
|
|
kind: "calculate",
|
|
status: item.status,
|
|
label: failed ? `${item.label}${FAILED_SUFFIX}` : item.label,
|
|
};
|
|
});
|
|
|
|
const chips = methodChips(input.receipt);
|
|
if (chips.length > 0) {
|
|
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
const row = rows[index];
|
|
if (!row || row.kind !== "calculate" || row.status !== "done" || row.label.endsWith(FAILED_SUFFIX)) continue;
|
|
rows[index] = { ...row, sources: chips };
|
|
break;
|
|
}
|
|
}
|
|
|
|
const hasLiveRow = rows.some((row) => row.status === "live");
|
|
const label = input.activity?.label ?? "";
|
|
if (!input.settled && !hasLiveRow && isProgressLabel(label)) {
|
|
rows.push({
|
|
id: RECTIFICATION_TIMELINE_LIVE_ID,
|
|
kind: liveRowKind(input.activity?.phase, label),
|
|
status: "live",
|
|
label,
|
|
});
|
|
} else if (!input.settled && !hasLiveRow && rows.length > 0) {
|
|
rows.push({
|
|
id: RECTIFICATION_TIMELINE_LIVE_ID,
|
|
kind: "think",
|
|
status: "live",
|
|
label: RECTIFICATION_ANALYZING_LIVE_LABEL,
|
|
});
|
|
}
|
|
return rows;
|
|
}
|