@autopilot-agent/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -2
- package/dist/activity-animation.d.ts +59 -0
- package/dist/activity-animation.js +148 -0
- package/dist/animation-clock.d.ts +22 -0
- package/dist/animation-clock.js +76 -0
- package/dist/composition-root.d.ts +105 -0
- package/dist/composition-root.js +600 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +173 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +741 -0
- package/dist/design-tokens.d.ts +38 -0
- package/dist/design-tokens.js +38 -0
- package/dist/error-diagnostics.d.ts +8 -0
- package/dist/error-diagnostics.js +62 -0
- package/dist/event-projector.d.ts +26 -0
- package/dist/event-projector.js +335 -0
- package/dist/file-mention-context.d.ts +34 -0
- package/dist/file-mention-context.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +40 -0
- package/dist/main.js +543 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.js +400 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +62 -0
- package/dist/scroll-viewport.d.ts +22 -0
- package/dist/scroll-viewport.js +62 -0
- package/dist/termination-format.d.ts +4 -0
- package/dist/termination-format.js +18 -0
- package/dist/timeline-lines.d.ts +38 -0
- package/dist/timeline-lines.js +104 -0
- package/dist/tui/app.d.ts +30 -0
- package/dist/tui/app.js +413 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +41 -0
- package/dist/tui/brand/brand-header.d.ts +20 -0
- package/dist/tui/brand/brand-header.js +22 -0
- package/dist/tui/brand/intro.d.ts +12 -0
- package/dist/tui/brand/intro.js +88 -0
- package/dist/tui/brand/pixel.d.ts +8 -0
- package/dist/tui/brand/pixel.js +50 -0
- package/dist/tui/constants.d.ts +56 -0
- package/dist/tui/constants.js +57 -0
- package/dist/tui/context.d.ts +11 -0
- package/dist/tui/context.js +12 -0
- package/dist/tui/diff-stats.d.ts +8 -0
- package/dist/tui/diff-stats.js +7 -0
- package/dist/tui/format.d.ts +75 -0
- package/dist/tui/format.js +178 -0
- package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
- package/dist/tui/hooks/use-file-mention-index.js +59 -0
- package/dist/tui/hooks/use-pickers.d.ts +47 -0
- package/dist/tui/hooks/use-pickers.js +288 -0
- package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
- package/dist/tui/hooks/use-prompt-input.js +258 -0
- package/dist/tui/hooks/use-status-view.d.ts +8 -0
- package/dist/tui/hooks/use-status-view.js +13 -0
- package/dist/tui/hooks/use-viewport.d.ts +22 -0
- package/dist/tui/hooks/use-viewport.js +117 -0
- package/dist/tui/input/file-mention.d.ts +33 -0
- package/dist/tui/input/file-mention.js +112 -0
- package/dist/tui/input/graphemes.d.ts +5 -0
- package/dist/tui/input/graphemes.js +10 -0
- package/dist/tui/input/keys.d.ts +16 -0
- package/dist/tui/input/keys.js +58 -0
- package/dist/tui/input/prompt-layout.d.ts +27 -0
- package/dist/tui/input/prompt-layout.js +84 -0
- package/dist/tui/input-area.d.ts +53 -0
- package/dist/tui/input-area.js +89 -0
- package/dist/tui/jobs.d.ts +10 -0
- package/dist/tui/jobs.js +24 -0
- package/dist/tui/lifecycle.d.ts +23 -0
- package/dist/tui/lifecycle.js +114 -0
- package/dist/tui/markdown/diff.d.ts +8 -0
- package/dist/tui/markdown/diff.js +19 -0
- package/dist/tui/markdown/inline.d.ts +4 -0
- package/dist/tui/markdown/inline.js +32 -0
- package/dist/tui/markdown/markdown.d.ts +17 -0
- package/dist/tui/markdown/markdown.js +117 -0
- package/dist/tui/markdown/table.d.ts +19 -0
- package/dist/tui/markdown/table.js +74 -0
- package/dist/tui/performance-diagnostics.d.ts +7 -0
- package/dist/tui/performance-diagnostics.js +57 -0
- package/dist/tui/pickers/effort.d.ts +10 -0
- package/dist/tui/pickers/effort.js +12 -0
- package/dist/tui/pickers/file-mention.d.ts +11 -0
- package/dist/tui/pickers/file-mention.js +22 -0
- package/dist/tui/pickers/model.d.ts +10 -0
- package/dist/tui/pickers/model.js +12 -0
- package/dist/tui/pickers/permission.d.ts +10 -0
- package/dist/tui/pickers/permission.js +13 -0
- package/dist/tui/pickers/skill.d.ts +13 -0
- package/dist/tui/pickers/skill.js +25 -0
- package/dist/tui/pickers/slash.d.ts +10 -0
- package/dist/tui/pickers/slash.js +11 -0
- package/dist/tui/prompt-input.d.ts +11 -0
- package/dist/tui/prompt-input.js +73 -0
- package/dist/tui/render-mode.d.ts +20 -0
- package/dist/tui/render-mode.js +31 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +237 -0
- package/dist/tui/status-line.d.ts +16 -0
- package/dist/tui/status-line.js +37 -0
- package/dist/tui/store.d.ts +4 -0
- package/dist/tui/store.js +380 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +153 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +15 -0
- package/dist/tui/timeline/presentation-timeline.js +128 -0
- package/dist/tui/timeline/progress.d.ts +12 -0
- package/dist/tui/timeline/progress.js +43 -0
- package/dist/tui/timeline/static-timeline.d.ts +13 -0
- package/dist/tui/timeline/static-timeline.js +131 -0
- package/dist/tui/timeline/tool-tree.d.ts +7 -0
- package/dist/tui/timeline/tool-tree.js +25 -0
- package/dist/tui/timeline/turn-items.d.ts +12 -0
- package/dist/tui/timeline/turn-items.js +28 -0
- package/dist/tui/timeline/turn.d.ts +19 -0
- package/dist/tui/timeline/turn.js +21 -0
- package/dist/tui/todo-list.d.ts +50 -0
- package/dist/tui/todo-list.js +120 -0
- package/dist/tui/transcript.d.ts +24 -0
- package/dist/tui/transcript.js +34 -0
- package/dist/tui/trust-dialog.d.ts +51 -0
- package/dist/tui/trust-dialog.js +127 -0
- package/dist/tui/types.d.ts +189 -0
- package/dist/tui/types.js +5 -0
- package/dist/tui/viewport/selection.d.ts +27 -0
- package/dist/tui/viewport/selection.js +79 -0
- package/dist/tui/viewport/sources.d.ts +29 -0
- package/dist/tui/viewport/sources.js +592 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +91 -0
- package/dist/tui-input-history.d.ts +23 -0
- package/dist/tui-input-history.js +42 -0
- package/dist/tui-input-router.d.ts +24 -0
- package/dist/tui-input-router.js +115 -0
- package/dist/tui.d.ts +15 -0
- package/dist/tui.js +13 -0
- package/dist/types.d.ts +467 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** 备用屏时间线来源:把安全事件转为可裁剪的纯文本视觉行来源。 */
|
|
2
|
+
import type { TimelineLineSource } from "../../timeline-lines.js";
|
|
3
|
+
import type { CliStatusView } from "../../types.js";
|
|
4
|
+
import type { CliTuiSnapshot } from "../types.js";
|
|
5
|
+
/** 将 Markdown 表格预先限制在备用屏内容列内,避免后续视口硬换行打断边框。 */
|
|
6
|
+
export declare function formatTuiViewportMarkdown(content: string, maximumWidth: number): string;
|
|
7
|
+
/** 将历史版本的 unified diff 直角卡片转换为备用屏可裁剪的文字源,并保留边框与逐行语义颜色。 */
|
|
8
|
+
export declare function createTuiViewportDiffSource(itemId: string, diff: string, contentWidth: number): TimelineLineSource;
|
|
9
|
+
/** 将已完成与当前动态时间线统一变为可裁剪的视觉行来源。 */
|
|
10
|
+
export declare function createTuiViewportSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean, todoRowLimit?: number): TimelineLineSource[];
|
|
11
|
+
/** 备用屏 source 缓存计数,区分历史命中与 live tail 重建。 */
|
|
12
|
+
export interface TuiViewportSourceCacheStats {
|
|
13
|
+
brandBuilds: number;
|
|
14
|
+
historyBuilds: number;
|
|
15
|
+
historyHits: number;
|
|
16
|
+
liveBuilds: number;
|
|
17
|
+
liveHits: number;
|
|
18
|
+
pendingBuilds: number;
|
|
19
|
+
pendingHits: number;
|
|
20
|
+
widthInvalidations: number;
|
|
21
|
+
}
|
|
22
|
+
/** 按 transcript item 与 live tail 增量生成备用屏 source 的内部接口。 */
|
|
23
|
+
export interface TuiViewportSourceCache {
|
|
24
|
+
getSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean, todoRowLimit?: number): TimelineLineSource[];
|
|
25
|
+
getStats(): TuiViewportSourceCacheStats;
|
|
26
|
+
clear(): void;
|
|
27
|
+
}
|
|
28
|
+
/** 创建备用屏 source 增量缓存;历史 item 不变时只复用其 source,live tail 单独更新。 */
|
|
29
|
+
export declare function createTuiViewportSourceCache(): TuiViewportSourceCache;
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/** 备用屏时间线来源:把安全事件转为可裁剪的纯文本视觉行来源。 */
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
import { WEB_SEARCH_DATA_FLOW_NOTICE, getPresentationCopy, getToolPresentationProfile, isSandboxUnavailableErrorCode, selectToolCalls } from "@autopilot-agent/presentation";
|
|
5
|
+
import { createAutopilotStaticWordRows } from "../brand/pixel.js";
|
|
6
|
+
import { AUTOPILOT_VERSION, TUI_AGENT_LABEL } from "../constants.js";
|
|
7
|
+
import { cleanTuiToolSummary, formatTuiDiffStats, formatTuiDuration, formatTuiInputConsumed, formatTuiToolLine, getTuiDiffStats, getTuiStatusLabel, getTuiToolLabel, getTuiTurnFailureReason, getTuiTerminationSummary, } from "../format.js";
|
|
8
|
+
import { getTuiMarkdownTableColumnWidths, isLooseMarkdownTable, isMarkdownTableDivider, normalizeMarkdownTableCells, splitMarkdownTableRow, } from "../markdown/table.js";
|
|
9
|
+
import { getTuiTurnItems, getTuiTurnTodo } from "../timeline/turn-items.js";
|
|
10
|
+
import { getTuiTodoDisplay, getTuiTodoStatusLabel } from "../todo-list.js";
|
|
11
|
+
/** 将对用户安全公开的时间线事件转换为备用屏内的纯文本行。 */
|
|
12
|
+
function formatTuiViewportEvent(event) {
|
|
13
|
+
if (event.type === "answer_delta")
|
|
14
|
+
return undefined;
|
|
15
|
+
if (event.type === "tui_input_consumed")
|
|
16
|
+
return formatTuiInputConsumed(event);
|
|
17
|
+
if (event.type === "tui_approval_submitted")
|
|
18
|
+
return `Approval submitted · ${event.decision}`;
|
|
19
|
+
if (event.type === "todo_updated")
|
|
20
|
+
return undefined;
|
|
21
|
+
if (event.type === "turn_started" || event.type === "reasoning_hidden" || event.type === "answer_reset" || event.type === "input_joined" || event.type === "input_consumed" || event.type === "active_skills")
|
|
22
|
+
return undefined;
|
|
23
|
+
if (event.type === "reasoning_summary")
|
|
24
|
+
return `Reasoning · ${event.summary}`;
|
|
25
|
+
if (event.type === "tool_started") {
|
|
26
|
+
const detail = cleanTuiToolSummary(event.toolName, event.summary).trim();
|
|
27
|
+
return `├─ [${getTuiToolLabel(event.toolName)}]${detail ? ` [${detail}]` : ""}`;
|
|
28
|
+
}
|
|
29
|
+
if (event.type === "tool_result") {
|
|
30
|
+
const diffStats = formatTuiDiffStats(event.diff ? { text: event.diff } : undefined);
|
|
31
|
+
return `└─ ${formatTuiToolLine(event.toolName, event.summary, event.status, event.errorCode)}${diffStats ? ` ${diffStats}` : ""}`;
|
|
32
|
+
}
|
|
33
|
+
if (event.type === "approval_required")
|
|
34
|
+
return `Approval required · ${event.toolName}: ${event.reason}`;
|
|
35
|
+
if (event.type === "input_queued")
|
|
36
|
+
return "Additional instruction queued; waiting for the current step";
|
|
37
|
+
if (event.type === "model_recovery")
|
|
38
|
+
return `Recovering · ${event.reason} (${event.attempt}/${event.maxAttempts})`;
|
|
39
|
+
if (event.type === "context_overflow")
|
|
40
|
+
return "Context limit reached; organizing before continuing";
|
|
41
|
+
if (event.type === "context_compact_started")
|
|
42
|
+
return "Context compact started";
|
|
43
|
+
if (event.type === "context_compacted")
|
|
44
|
+
return `Context compacted: ${event.preTokens} -> ${event.postTokens}`;
|
|
45
|
+
if (event.type === "context_compact_failed")
|
|
46
|
+
return "Context compact failed";
|
|
47
|
+
if (event.type === "context_compact_circuit_open")
|
|
48
|
+
return "Context compact circuit open";
|
|
49
|
+
if (event.type === "status")
|
|
50
|
+
return `Status · ${getTuiStatusLabel(event.status)}${event.code ? ` (${event.code})` : ""}${event.status === "failed" && event.reason ? ` · 原因:${event.reason}` : ""}`;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** 将 Markdown 表格预先限制在备用屏内容列内,避免后续视口硬换行打断边框。 */
|
|
54
|
+
export function formatTuiViewportMarkdown(content, maximumWidth) {
|
|
55
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
56
|
+
const output = [];
|
|
57
|
+
let index = 0;
|
|
58
|
+
function padDisplayWidth(value, width) {
|
|
59
|
+
return value + " ".repeat(Math.max(0, width - stringWidth(value)));
|
|
60
|
+
}
|
|
61
|
+
function fitTableColumnWidths(widths) {
|
|
62
|
+
const minimumWidth = 3;
|
|
63
|
+
const availableCellWidth = Math.max(widths.length, maximumWidth - widths.length * 3 - 1);
|
|
64
|
+
const fitted = widths.map(function keepMinimumWidth(width) { return Math.max(minimumWidth, width); });
|
|
65
|
+
let excess = fitted.reduce(function sumWidths(total, width) { return total + width; }, 0) - availableCellWidth;
|
|
66
|
+
while (excess > 0) {
|
|
67
|
+
const widestIndex = fitted.reduce(function findWidest(currentIndex, width, index) {
|
|
68
|
+
return width > (fitted[currentIndex] ?? 0) ? index : currentIndex;
|
|
69
|
+
}, 0);
|
|
70
|
+
if ((fitted[widestIndex] ?? minimumWidth) <= minimumWidth)
|
|
71
|
+
break;
|
|
72
|
+
fitted[widestIndex] = (fitted[widestIndex] ?? minimumWidth) - 1;
|
|
73
|
+
excess -= 1;
|
|
74
|
+
}
|
|
75
|
+
return fitted;
|
|
76
|
+
}
|
|
77
|
+
function createTableRows(cells, widths) {
|
|
78
|
+
const wrappedCells = cells.map(function wrapCell(cell, columnIndex) {
|
|
79
|
+
const width = widths[columnIndex] ?? 3;
|
|
80
|
+
const wrapped = wrapAnsi(cell, width, { hard: true, trim: false, wordWrap: true }).split("\n");
|
|
81
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
82
|
+
});
|
|
83
|
+
const height = Math.max(...wrappedCells.map(function getCellHeight(cellLines) { return cellLines.length; }));
|
|
84
|
+
return Array.from({ length: height }, function createTableLine(_value, lineIndex) {
|
|
85
|
+
return `│ ${wrappedCells.map(function formatCell(cellLines, columnIndex) { return padDisplayWidth(cellLines[lineIndex] ?? "", widths[columnIndex] ?? 3); }).join(" │ ")} │`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
while (index < lines.length) {
|
|
89
|
+
const line = lines[index] ?? "";
|
|
90
|
+
const header = splitMarkdownTableRow(line);
|
|
91
|
+
const strictDivider = line.includes("|") && isMarkdownTableDivider(lines[index + 1] ?? "");
|
|
92
|
+
const looseRows = isLooseMarkdownTable(lines, index);
|
|
93
|
+
if (strictDivider || looseRows) {
|
|
94
|
+
const rows = [];
|
|
95
|
+
let rowIndex = index + (strictDivider ? 2 : 1);
|
|
96
|
+
while (rowIndex < lines.length) {
|
|
97
|
+
const row = normalizeMarkdownTableCells(lines[rowIndex] ?? "", header.length);
|
|
98
|
+
if (!row)
|
|
99
|
+
break;
|
|
100
|
+
rows.push(row);
|
|
101
|
+
rowIndex += 1;
|
|
102
|
+
}
|
|
103
|
+
if (rows.length > 0) {
|
|
104
|
+
const widths = fitTableColumnWidths(getTuiMarkdownTableColumnWidths([header, ...rows]));
|
|
105
|
+
const border = function createBorder(left, join, right) {
|
|
106
|
+
return left + widths.map(function createColumn(width) { return "─".repeat(width + 2); }).join(join) + right;
|
|
107
|
+
};
|
|
108
|
+
output.push(border("┌", "┬", "┐"), ...createTableRows(header, widths), border("├", "┼", "┤"));
|
|
109
|
+
for (const row of rows)
|
|
110
|
+
output.push(...createTableRows(row, widths));
|
|
111
|
+
output.push(border("└", "┴", "┘"));
|
|
112
|
+
index = rowIndex;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
output.push(line);
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
return output.join("\n");
|
|
120
|
+
}
|
|
121
|
+
/** 为单色语义行生成覆盖全文的样式范围。 */
|
|
122
|
+
function createTuiViewportTextSpans(text, style) {
|
|
123
|
+
return style && text ? [{ start: 0, end: text.length, style }] : undefined;
|
|
124
|
+
}
|
|
125
|
+
/** 在备用屏纯文本中定位 edit diff 统计,并为正负数字附加既有语义色。 */
|
|
126
|
+
function createTuiViewportDiffStatsSpans(text, stats) {
|
|
127
|
+
if (!stats)
|
|
128
|
+
return [];
|
|
129
|
+
const statistics = `(+${stats.added}, -${stats.removed})`;
|
|
130
|
+
const start = text.lastIndexOf(statistics);
|
|
131
|
+
if (start < 0)
|
|
132
|
+
return [];
|
|
133
|
+
const addedStart = start + 1;
|
|
134
|
+
const removedStart = start + statistics.indexOf("-");
|
|
135
|
+
return [
|
|
136
|
+
{ start: addedStart, end: addedStart + String(stats.added).length + 1, style: "diff-added" },
|
|
137
|
+
{ start: removedStart, end: removedStart + String(stats.removed).length + 1, style: "diff-removed" },
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
/** 将历史版本的 unified diff 直角卡片转换为备用屏可裁剪的文字源,并保留边框与逐行语义颜色。 */
|
|
141
|
+
export function createTuiViewportDiffSource(itemId, diff, contentWidth) {
|
|
142
|
+
const maximumInnerWidth = Math.max(1, contentWidth - 4);
|
|
143
|
+
const chunks = diff.replace(/\r\n?/g, "\n").split("\n").flatMap(function wrapDiffLine(line) {
|
|
144
|
+
const wrapped = wrapAnsi(line || " ", maximumInnerWidth, { hard: true, trim: false, wordWrap: true }).split("\n");
|
|
145
|
+
return wrapped.length > 0 ? wrapped : [" "];
|
|
146
|
+
});
|
|
147
|
+
const innerWidth = Math.min(maximumInnerWidth, Math.max(1, stringWidth("diff"), ...chunks.map(function getChunkWidth(line) { return stringWidth(line); })));
|
|
148
|
+
const output = [];
|
|
149
|
+
const spans = [];
|
|
150
|
+
let sourceOffset = 0;
|
|
151
|
+
/** 追加一行备用屏文本,并把相对样式范围换算为整段源文本偏移。 */
|
|
152
|
+
function appendLine(line, lineSpans = []) {
|
|
153
|
+
output.push(line);
|
|
154
|
+
for (const span of lineSpans ?? [])
|
|
155
|
+
spans.push({ ...span, start: span.start + sourceOffset, end: span.end + sourceOffset });
|
|
156
|
+
sourceOffset += line.length + 1;
|
|
157
|
+
}
|
|
158
|
+
/** 按终端显示宽度补齐 diff 卡片内层,避免右边框错位。 */
|
|
159
|
+
function padDiffLine(line) {
|
|
160
|
+
return line + " ".repeat(Math.max(0, innerWidth - stringWidth(line)));
|
|
161
|
+
}
|
|
162
|
+
/** 追加橙色直角边框行,复用历史 TuiDiffBlock 的 single-border 语义。 */
|
|
163
|
+
function appendBorder() {
|
|
164
|
+
const border = `┌${"─".repeat(innerWidth + 2)}┐`;
|
|
165
|
+
appendLine(border, [{ start: 0, end: border.length, style: "diff-border" }]);
|
|
166
|
+
}
|
|
167
|
+
/** 追加 diff 卡片内容行,只给真实 diff 文本着色并保留边框颜色。 */
|
|
168
|
+
function appendDiffContent(line, style) {
|
|
169
|
+
const content = padDiffLine(line);
|
|
170
|
+
const rendered = `│ ${content} │`;
|
|
171
|
+
const lineSpans = [
|
|
172
|
+
{ start: 0, end: 2, style: "diff-border" },
|
|
173
|
+
{ start: rendered.length - 2, end: rendered.length, style: "diff-border" },
|
|
174
|
+
];
|
|
175
|
+
if (style && line)
|
|
176
|
+
lineSpans.push({ start: 2, end: 2 + line.length, style });
|
|
177
|
+
appendLine(rendered, lineSpans);
|
|
178
|
+
}
|
|
179
|
+
appendBorder();
|
|
180
|
+
appendDiffContent("diff", "accent");
|
|
181
|
+
for (const line of chunks) {
|
|
182
|
+
const isAdded = line.startsWith("+") && !line.startsWith("+++");
|
|
183
|
+
const isRemoved = line.startsWith("-") && !line.startsWith("---");
|
|
184
|
+
const isMeta = line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@");
|
|
185
|
+
appendDiffContent(line, isAdded ? "diff-added" : isRemoved ? "diff-removed" : isMeta ? "diff-meta" : undefined);
|
|
186
|
+
}
|
|
187
|
+
const border = `└${"─".repeat(innerWidth + 2)}┘`;
|
|
188
|
+
appendLine(border, [{ start: 0, end: border.length, style: "diff-border" }]);
|
|
189
|
+
return { itemId, text: output.join("\n"), spans };
|
|
190
|
+
}
|
|
191
|
+
/** 为备用屏构造与原 Header 相同间距和品牌 token 的静态品牌行。 */
|
|
192
|
+
function createTuiViewportBrandSource(statusView) {
|
|
193
|
+
const word = createAutopilotStaticWordRows().join("\n");
|
|
194
|
+
const model = statusView?.model ?? "runtime default";
|
|
195
|
+
const effort = statusView?.effort ?? "runtime default";
|
|
196
|
+
const detailPrefix = `${word}\n\n `;
|
|
197
|
+
const versionLabel = "Autopilot";
|
|
198
|
+
const version = `v${AUTOPILOT_VERSION}`;
|
|
199
|
+
const modelLine = `Model ${model}`;
|
|
200
|
+
const thinkingLine = `Thinking ${effort}`;
|
|
201
|
+
const text = `${detailPrefix}${versionLabel} ${version}\n ${modelLine}\n ${thinkingLine}\n ${WEB_SEARCH_DATA_FLOW_NOTICE}`;
|
|
202
|
+
const detailStart = detailPrefix.length;
|
|
203
|
+
const modelStart = detailStart + versionLabel.length + 1 + version.length + 3;
|
|
204
|
+
const thinkingStart = modelStart + modelLine.length + 3;
|
|
205
|
+
return {
|
|
206
|
+
itemId: "brand",
|
|
207
|
+
text,
|
|
208
|
+
spans: [
|
|
209
|
+
{ start: 0, end: word.length, style: "brand-blue" },
|
|
210
|
+
{ start: detailStart, end: detailStart + versionLabel.length, style: "brand-blue" },
|
|
211
|
+
{ start: detailStart + versionLabel.length + 1, end: detailStart + versionLabel.length + 1 + version.length, style: "brand-ink" },
|
|
212
|
+
{ start: modelStart, end: modelStart + modelLine.length, style: "brand-muted" },
|
|
213
|
+
{ start: thinkingStart, end: thinkingStart + thinkingLine.length, style: "brand-muted" },
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** 将共享 Presentation selector 转成备用屏文本源,与 inline TUI 复用同一事实和顺序。 */
|
|
218
|
+
function createTuiPresentationViewportSources(presentation, turnId, itemId, contentWidth, toolGroupsExpanded, events, live, rowLimit = 10) {
|
|
219
|
+
const sources = [];
|
|
220
|
+
// 构建 callId 索引供展开分组取回组内调用。
|
|
221
|
+
const callIndex = new Map(selectToolCalls(presentation, turnId).map(function indexToolCall(call) { return [call.callId, call]; }));
|
|
222
|
+
let index = 0;
|
|
223
|
+
for (const item of getTuiTurnItems(presentation, turnId, events)) {
|
|
224
|
+
if ("type" in item) {
|
|
225
|
+
sources.push({ itemId: `${itemId}:input:${item.inputId}`, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") });
|
|
226
|
+
sources.push({ itemId: `${itemId}:input:${item.inputId}:state`, text: item.cancelled ? "[引导] 已取消" : item.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(item) });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (item.kind !== "capability_notice" && (!("turnId" in item) || item.turnId !== turnId))
|
|
230
|
+
continue;
|
|
231
|
+
const sourceId = `${itemId}:presentation:${String(index++)}`;
|
|
232
|
+
if (item.kind === "todo") {
|
|
233
|
+
sources.push(...createTuiTodoSources(item, sourceId, contentWidth, live ? toolGroupsExpanded : true, live, rowLimit));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (item.kind === "thinking")
|
|
237
|
+
sources.push({ itemId: sourceId, text: `✢ ${item.summary}`, spans: createTuiViewportTextSpans(`✢ ${item.summary}`, "muted") });
|
|
238
|
+
else if (item.kind === "tool_group") {
|
|
239
|
+
const text = `• Exploring codebase · ${item.toolCallIds.length} operations`;
|
|
240
|
+
const numberStart = "• Exploring codebase · ".length;
|
|
241
|
+
sources.push({ itemId: sourceId, text, spans: [{ start: numberStart, end: numberStart + String(item.toolCallIds.length).length, style: "accent" }, ...createTuiViewportTextSpans(text, "muted") ?? []] });
|
|
242
|
+
// 分组默认收起;Ctrl+O 详情态展开组内各调用,缩进两格并保留徽标 chip 语义。
|
|
243
|
+
if (!toolGroupsExpanded)
|
|
244
|
+
continue;
|
|
245
|
+
for (const callId of item.toolCallIds) {
|
|
246
|
+
const child = callIndex.get(callId);
|
|
247
|
+
if (!child)
|
|
248
|
+
continue;
|
|
249
|
+
const childTitle = getPresentationCopy(child.titleKey);
|
|
250
|
+
const childDetail = [child.target, child.resultSummary].filter(Boolean).join(" · ");
|
|
251
|
+
const childText = ` ├─ [${childTitle}]${childDetail ? ` · ${childDetail}` : ""}`;
|
|
252
|
+
const childBadgeStart = 5;
|
|
253
|
+
sources.push({ itemId: `${sourceId}:${callId}`, text: childText, spans: [{ start: childBadgeStart, end: childBadgeStart + childTitle.length + 2, style: "chip-active" }] });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (item.kind === "tool_call") {
|
|
257
|
+
const jobProgramLabel = item.job?.action === "start" ? item.job.programLabel?.toUpperCase() : undefined;
|
|
258
|
+
const diffStats = getTuiDiffStats(item.diff);
|
|
259
|
+
const diffStatsText = formatTuiDiffStats(item.diff);
|
|
260
|
+
const detail = `${[jobProgramLabel ? undefined : item.target, item.resultSummary].filter(Boolean).join(" · ")}${diffStatsText ? ` ${diffStatsText}` : ""}`;
|
|
261
|
+
const detailLine = jobProgramLabel ? `\n └─ ${jobProgramLabel}${detail ? ` · ${detail}` : ""}` : detail ? `\n └─ ${detail}` : "";
|
|
262
|
+
const jobHint = item.job?.action === "start" && (item.job.observedStatus === "starting" || item.job.observedStatus === "running") ? "\n /jobs 查看后台任务" : "";
|
|
263
|
+
const title = getPresentationCopy(item.state === "failed" ? getToolPresentationProfile(item.toolName).completedTitleKey : item.titleKey);
|
|
264
|
+
// 2.5:沙箱不可用类拒绝是环境阻塞而非用户拒绝,文案必须区分。
|
|
265
|
+
const stateText = item.state === "denied" ? (isSandboxUnavailableErrorCode(item.errorCode) ? "沙箱不可用,命令未启动" : "已拒绝") : item.state === "cancelled" ? "已取消" : item.state === "timeout" ? "已超时" : undefined;
|
|
266
|
+
const badgeText = `[${title}]${stateText ? ` · ${stateText}` : ""}`;
|
|
267
|
+
const text = `${item.state === "failed" ? "×" : "•"} ${badgeText}${detailLine}${item.errorCode ? `\n └─ ${item.errorCode}` : ""}${jobHint}`;
|
|
268
|
+
const programStart = jobProgramLabel ? text.indexOf(jobProgramLabel) : -1;
|
|
269
|
+
const hintStart = jobHint ? text.length - jobHint.trimStart().length : -1;
|
|
270
|
+
const badgeStart = 2;
|
|
271
|
+
const badgeEnd = badgeStart + badgeText.length;
|
|
272
|
+
const spans = createTuiViewportDiffStatsSpans(text, diffStats);
|
|
273
|
+
if (programStart >= 0 && jobProgramLabel)
|
|
274
|
+
spans.push({ start: programStart, end: programStart + jobProgramLabel.length, style: "accent" });
|
|
275
|
+
if (item.state === "running" || item.state === "awaiting_approval") {
|
|
276
|
+
spans.push({ start: badgeStart, end: badgeEnd, style: "chip-active" });
|
|
277
|
+
spans.push({ start: 0, end: text.length, style: "muted" });
|
|
278
|
+
}
|
|
279
|
+
else if (item.state === "completed") {
|
|
280
|
+
spans.push({ start: badgeStart, end: badgeEnd, style: "chip-active" });
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
spans.push({ start: badgeStart, end: badgeEnd, style: "chip-error" });
|
|
284
|
+
}
|
|
285
|
+
if (hintStart >= 0)
|
|
286
|
+
spans.push({ start: hintStart, end: text.length, style: "muted" });
|
|
287
|
+
sources.push({ itemId: sourceId, text, spans });
|
|
288
|
+
if (item.diff?.text)
|
|
289
|
+
sources.push(createTuiViewportDiffSource(`${sourceId}:diff`, item.diff.text, contentWidth));
|
|
290
|
+
}
|
|
291
|
+
else if (item.kind === "assistant_answer") {
|
|
292
|
+
if (item.channel === "process") {
|
|
293
|
+
sources.push({ itemId: sourceId, text: formatTuiViewportMarkdown(item.content, contentWidth) });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const label = TUI_AGENT_LABEL;
|
|
297
|
+
const text = `• ${label}\n${formatTuiViewportMarkdown(item.content, contentWidth)}`;
|
|
298
|
+
sources.push({ itemId: sourceId, text, spans: [{ start: 0, end: `• ${label}`.length, style: "accent" }] });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (item.kind === "approval") {
|
|
302
|
+
const text = `⚠ ${item.display?.actionPreview ?? item.display?.operation ?? "需要确认"}${item.state === "resolved" ? " · 已处理" : ""}`;
|
|
303
|
+
sources.push({ itemId: sourceId, text, spans: createTuiViewportTextSpans(text, "accent") });
|
|
304
|
+
}
|
|
305
|
+
else if (item.kind === "input_notice") {
|
|
306
|
+
const text = `${item.delivery === "steer" ? "[引导] " : item.delivery === "queue" ? "[排队] " : ""}Input ${item.state}`;
|
|
307
|
+
sources.push({ itemId: sourceId, text, spans: createTuiViewportTextSpans(text, "muted") });
|
|
308
|
+
}
|
|
309
|
+
else if (item.kind === "context_notice")
|
|
310
|
+
sources.push({ itemId: sourceId, text: item.state === "compacted" ? "Context organized" : `Context ${item.state}` });
|
|
311
|
+
else if (item.kind === "undo")
|
|
312
|
+
sources.push({ itemId: sourceId, text: `Undo ${item.state}` });
|
|
313
|
+
else if (item.kind === "turn_summary")
|
|
314
|
+
sources.push({ itemId: sourceId, text: `${item.state === "completed" ? "✓" : "×"} ${getPresentationCopy(`turn.${item.state}`)} · ${item.toolCount} tools`, spans: createTuiViewportTextSpans(`${item.state === "completed" ? "✓" : "×"} ${getPresentationCopy(`turn.${item.state}`)} · ${item.toolCount} tools`, "muted") });
|
|
315
|
+
}
|
|
316
|
+
return sources;
|
|
317
|
+
}
|
|
318
|
+
/** Todo 行复用既有紧凑/展开数据;历史不输出失效快捷键。 */
|
|
319
|
+
function createTuiTodoSources(item, itemId, contentWidth, expanded, live, rowLimit = 10) {
|
|
320
|
+
const display = getTuiTodoDisplay(item, { expanded, screenReader: false, columns: contentWidth + 1, rowLimit });
|
|
321
|
+
if (!display)
|
|
322
|
+
return [];
|
|
323
|
+
const text = [display.header, ...display.rows.map(function row(row) { return row.hidden ? `+${row.text}` : `[${getTuiTodoStatusLabel(row.status)}] ${row.text}`; }), ...(live ? [`Ctrl+O ${expanded ? "收起" : "展开"}`] : [])].join("\n");
|
|
324
|
+
return [{ itemId, text }];
|
|
325
|
+
}
|
|
326
|
+
/** 将一个历史或 live Turn 的安全事件转换为可缓存的备用屏 source 列表。 */
|
|
327
|
+
function createTuiViewportEventSources(itemId, events, contentWidth, timing, toolGroupsExpanded = false) {
|
|
328
|
+
const sources = [];
|
|
329
|
+
if (timing?.presentation && timing.turnId && timing.presentation.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === timing.turnId); })) {
|
|
330
|
+
sources.push(...createTuiPresentationViewportSources(timing.presentation, timing.turnId, itemId, contentWidth, toolGroupsExpanded, events, timing.status === "running" || timing.status === "waiting_approval", timing.todoRowLimit));
|
|
331
|
+
const failureReason = getTuiTurnFailureReason(events);
|
|
332
|
+
const terminationSummary = getTuiTerminationSummary(events, timing?.status);
|
|
333
|
+
if (terminationSummary) {
|
|
334
|
+
const text = `终态:${terminationSummary}`;
|
|
335
|
+
sources.push({ itemId: `${itemId}:termination`, text, spans: createTuiViewportTextSpans(text, "accent") });
|
|
336
|
+
}
|
|
337
|
+
else if (failureReason) {
|
|
338
|
+
const text = `原因:${failureReason}`;
|
|
339
|
+
sources.push({ itemId: `${itemId}:failure-reason`, text, spans: createTuiViewportTextSpans(text, "accent") });
|
|
340
|
+
}
|
|
341
|
+
if (timing.totalDurationMs !== undefined) {
|
|
342
|
+
const text = `* Cogitated for ${formatTuiDuration(timing.totalDurationMs)}`;
|
|
343
|
+
sources.push({ itemId: `${itemId}:total-duration`, text, spans: createTuiViewportTextSpans(text, "muted") });
|
|
344
|
+
}
|
|
345
|
+
return sources;
|
|
346
|
+
}
|
|
347
|
+
let answer = "";
|
|
348
|
+
let answerCount = 0;
|
|
349
|
+
let lastAnswerSource;
|
|
350
|
+
let progressSourceIndex;
|
|
351
|
+
/** 将当前连续 answer 分片收束为一个可裁剪 source。 */
|
|
352
|
+
function flushAnswer() {
|
|
353
|
+
if (!answer)
|
|
354
|
+
return;
|
|
355
|
+
let thoughtIndex;
|
|
356
|
+
if (answerCount === 0 && timing?.thoughtDurationMs !== undefined) {
|
|
357
|
+
const text = `Thought for ${formatTuiDuration(timing.thoughtDurationMs)}`;
|
|
358
|
+
sources.push({ itemId: `${itemId}:thought`, text, spans: createTuiViewportTextSpans(text, "muted") });
|
|
359
|
+
thoughtIndex = sources.length - 1;
|
|
360
|
+
}
|
|
361
|
+
const answerText = `• ${TUI_AGENT_LABEL}\n${formatTuiViewportMarkdown(answer, contentWidth)}`;
|
|
362
|
+
sources.push({ itemId: `${itemId}:answer:${String(answerCount++)}`, text: answerText, spans: [{ start: 0, end: `• ${TUI_AGENT_LABEL}`.length, style: "accent" }] });
|
|
363
|
+
lastAnswerSource = { answerIndex: sources.length - 1, thoughtIndex };
|
|
364
|
+
answer = "";
|
|
365
|
+
}
|
|
366
|
+
/** 收到 Core reset/discard 后撤销当前采样的暂定回答及其思考计时。 */
|
|
367
|
+
function discardAnswer() {
|
|
368
|
+
answer = "";
|
|
369
|
+
if (!lastAnswerSource)
|
|
370
|
+
return;
|
|
371
|
+
sources.splice(lastAnswerSource.answerIndex, 1);
|
|
372
|
+
if (lastAnswerSource.thoughtIndex !== undefined)
|
|
373
|
+
sources.splice(lastAnswerSource.thoughtIndex, 1);
|
|
374
|
+
answerCount = Math.max(0, answerCount - 1);
|
|
375
|
+
lastAnswerSource = undefined;
|
|
376
|
+
}
|
|
377
|
+
for (const event of events) {
|
|
378
|
+
if (event.type === "answer_delta") {
|
|
379
|
+
answer += event.delta;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (event.type === "answer_reset") {
|
|
383
|
+
discardAnswer();
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (event.type === "tool_progress") {
|
|
387
|
+
const text = "正在执行:" + event.tail;
|
|
388
|
+
const spans = createTuiViewportTextSpans(text, "muted");
|
|
389
|
+
if (progressSourceIndex === undefined) {
|
|
390
|
+
sources.push({ itemId: itemId + ":progress", text, spans });
|
|
391
|
+
progressSourceIndex = sources.length - 1;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
sources[progressSourceIndex] = { itemId: itemId + ":progress", text, spans };
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
flushAnswer();
|
|
399
|
+
if (event.type === "tui_input_consumed") {
|
|
400
|
+
sources.push({ itemId: `${itemId}:input:${event.inputId}`, text: event.content, spans: createTuiViewportTextSpans(event.content, "accent") });
|
|
401
|
+
sources.push({ itemId: `${itemId}:input:${event.inputId}:state`, text: event.cancelled ? "[引导] 已取消" : event.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(event) });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (event.type === "status")
|
|
405
|
+
continue;
|
|
406
|
+
const text = formatTuiViewportEvent(event);
|
|
407
|
+
const style = event.type === "tool_started" || event.type === "reasoning_summary" ? "muted" : undefined;
|
|
408
|
+
if (text) {
|
|
409
|
+
const spans = event.type === "tool_result"
|
|
410
|
+
? createTuiViewportDiffStatsSpans(text, getTuiDiffStats(event.diff ? { text: event.diff } : undefined))
|
|
411
|
+
: createTuiViewportTextSpans(text, style);
|
|
412
|
+
sources.push({ itemId: `${itemId}:${event.seq}`, text, spans });
|
|
413
|
+
}
|
|
414
|
+
if (event.type === "tool_result" && event.diff)
|
|
415
|
+
sources.push(createTuiViewportDiffSource(`${itemId}:${event.seq}:diff`, event.diff, contentWidth));
|
|
416
|
+
// 终态到达后移除覆盖式进度行,避免与结果行重复。
|
|
417
|
+
if (event.type === "tool_result" && progressSourceIndex !== undefined) {
|
|
418
|
+
sources.splice(progressSourceIndex, 1);
|
|
419
|
+
progressSourceIndex = undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
flushAnswer();
|
|
423
|
+
const todo = getTuiTurnTodo(events, timing?.turnId ?? events[0]?.turnId ?? "");
|
|
424
|
+
const live = timing?.status === "running" || timing?.status === "waiting_approval";
|
|
425
|
+
if (todo)
|
|
426
|
+
sources.push(...createTuiTodoSources(todo, `${itemId}:todo`, contentWidth, live ? toolGroupsExpanded : true, live, timing?.todoRowLimit));
|
|
427
|
+
const terminationSummary = getTuiTerminationSummary(events, timing?.status);
|
|
428
|
+
if (terminationSummary)
|
|
429
|
+
sources.push({ itemId: `${itemId}:termination`, text: `终态:${terminationSummary}` });
|
|
430
|
+
else if (getTuiTurnFailureReason(events))
|
|
431
|
+
sources.push({ itemId: `${itemId}:failure-reason`, text: `原因:${getTuiTurnFailureReason(events)}` });
|
|
432
|
+
if (timing?.totalDurationMs !== undefined) {
|
|
433
|
+
const text = `* Cogitated for ${formatTuiDuration(timing.totalDurationMs)}`;
|
|
434
|
+
sources.push({ itemId: `${itemId}:total-duration`, text, spans: createTuiViewportTextSpans(text, "muted") });
|
|
435
|
+
}
|
|
436
|
+
return sources;
|
|
437
|
+
}
|
|
438
|
+
/** 将已完成与当前动态时间线统一变为可裁剪的视觉行来源。 */
|
|
439
|
+
export function createTuiViewportSources(snapshot, statusView, approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false, todoRowLimit = 10) {
|
|
440
|
+
const sources = [];
|
|
441
|
+
sources.push(createTuiViewportBrandSource(statusView));
|
|
442
|
+
function appendEventGroup(itemId, events, timing, expandGroups = false) {
|
|
443
|
+
sources.push(...createTuiViewportEventSources(itemId, events, contentWidth, timing, expandGroups));
|
|
444
|
+
}
|
|
445
|
+
let renderedActivePrompt = false;
|
|
446
|
+
for (const item of snapshot.transcript) {
|
|
447
|
+
if (item.kind === "prompt") {
|
|
448
|
+
sources.push({ itemId: item.key, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") });
|
|
449
|
+
if (item.key === snapshot.activePrompt?.key)
|
|
450
|
+
renderedActivePrompt = true;
|
|
451
|
+
}
|
|
452
|
+
else if (item.kind === "turn" && item.turnId !== snapshot.liveTurnId) {
|
|
453
|
+
appendEventGroup(item.key, item.events, item);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (snapshot.activePrompt && !renderedActivePrompt)
|
|
457
|
+
sources.push({ itemId: snapshot.activePrompt.key, text: snapshot.activePrompt.content, spans: createTuiViewportTextSpans(snapshot.activePrompt.content, "accent") });
|
|
458
|
+
const liveCompletedAt = snapshot.liveMetrics?.completedAt;
|
|
459
|
+
appendEventGroup(snapshot.liveTurnId ?? "live", snapshot.liveEvents, {
|
|
460
|
+
todoRowLimit,
|
|
461
|
+
thoughtDurationMs: snapshot.liveMetrics?.firstAnswerAt === undefined ? undefined : Math.max(0, snapshot.liveMetrics?.firstAnswerAt - snapshot.liveMetrics.startedAt),
|
|
462
|
+
totalDurationMs: liveCompletedAt === undefined ? undefined : Math.max(0, liveCompletedAt - snapshot.liveMetrics.startedAt),
|
|
463
|
+
presentation: snapshot.livePresentation,
|
|
464
|
+
turnId: snapshot.liveTurnId,
|
|
465
|
+
status: snapshot.status,
|
|
466
|
+
}, toolGroupsExpanded);
|
|
467
|
+
for (const item of snapshot.pendingInputs.filter(function queue(item) { return item.delivery === "queue"; }))
|
|
468
|
+
sources.push({ itemId: item.key, text: `${item.delivery === "queue" ? item.content + "\n[排队]" : "[引导]"} 等待当前步骤结束` });
|
|
469
|
+
return sources;
|
|
470
|
+
}
|
|
471
|
+
/** 创建备用屏 source 增量缓存;历史 item 不变时只复用其 source,live tail 单独更新。 */
|
|
472
|
+
export function createTuiViewportSourceCache() {
|
|
473
|
+
const history = new Map();
|
|
474
|
+
const pending = new Map();
|
|
475
|
+
let activePrompt;
|
|
476
|
+
let brand;
|
|
477
|
+
let live;
|
|
478
|
+
let width;
|
|
479
|
+
const stats = { brandBuilds: 0, historyBuilds: 0, historyHits: 0, liveBuilds: 0, liveHits: 0, pendingBuilds: 0, pendingHits: 0, widthInvalidations: 0 };
|
|
480
|
+
/** 终端宽度变化时清除依赖内容宽度的 source,但保留计数供测试审计。 */
|
|
481
|
+
function invalidateWidth(nextWidth) {
|
|
482
|
+
if (width !== undefined && width !== nextWidth) {
|
|
483
|
+
history.clear();
|
|
484
|
+
pending.clear();
|
|
485
|
+
activePrompt = undefined;
|
|
486
|
+
live = undefined;
|
|
487
|
+
brand = undefined;
|
|
488
|
+
stats.widthInvalidations += 1;
|
|
489
|
+
}
|
|
490
|
+
width = nextWidth;
|
|
491
|
+
}
|
|
492
|
+
/** 获取按 item 引用与宽度缓存的历史 source。 */
|
|
493
|
+
function getHistorySources(item, contentWidth, decorativeGrid) {
|
|
494
|
+
const previous = history.get(item.key);
|
|
495
|
+
if (previous && previous.item === item && previous.width === contentWidth && previous.decorativeGrid === decorativeGrid) {
|
|
496
|
+
stats.historyHits += 1;
|
|
497
|
+
return previous.sources;
|
|
498
|
+
}
|
|
499
|
+
const sources = item.kind === "prompt"
|
|
500
|
+
? [{ itemId: item.key, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") }]
|
|
501
|
+
: item.kind === "turn"
|
|
502
|
+
? createTuiViewportEventSources(item.key, item.events, contentWidth, item)
|
|
503
|
+
: [];
|
|
504
|
+
history.set(item.key, { item, width: contentWidth, decorativeGrid, sources });
|
|
505
|
+
stats.historyBuilds += 1;
|
|
506
|
+
return sources;
|
|
507
|
+
}
|
|
508
|
+
/** 获取未出现在 transcript 中的当前 prompt source。 */
|
|
509
|
+
function getActivePromptSource(prompt) {
|
|
510
|
+
if (activePrompt && activePrompt.key === prompt.key && activePrompt.content === prompt.content)
|
|
511
|
+
return activePrompt.sources;
|
|
512
|
+
const sources = [{ itemId: prompt.key, text: prompt.content, spans: createTuiViewportTextSpans(prompt.content, "accent") }];
|
|
513
|
+
activePrompt = { key: prompt.key, content: prompt.content, sources };
|
|
514
|
+
return sources;
|
|
515
|
+
}
|
|
516
|
+
/** 获取按 pending input key 缓存的等待输入 source。 */
|
|
517
|
+
function getPendingSource(item) {
|
|
518
|
+
const previous = pending.get(item.key);
|
|
519
|
+
if (previous && previous.content === item.content && previous.delivery === item.delivery) {
|
|
520
|
+
stats.pendingHits += 1;
|
|
521
|
+
return previous.sources;
|
|
522
|
+
}
|
|
523
|
+
const sources = [{ itemId: item.key, text: `${item.delivery === "queue" ? item.content + "\n[排队]" : "[引导]"} 等待当前步骤结束` }];
|
|
524
|
+
pending.set(item.key, { key: item.key, content: item.content, delivery: item.delivery, sources });
|
|
525
|
+
stats.pendingBuilds += 1;
|
|
526
|
+
return sources;
|
|
527
|
+
}
|
|
528
|
+
/** 获取当前 live tail source;历史 transcript 不参与本次重建。 */
|
|
529
|
+
function getLiveSources(snapshot, contentWidth, toolGroupsExpanded, todoRowLimit) {
|
|
530
|
+
const liveTurnId = snapshot.liveTurnId;
|
|
531
|
+
const metrics = snapshot.liveMetrics;
|
|
532
|
+
if (live && live.turnId === liveTurnId && live.events === snapshot.liveEvents && live.presentation === snapshot.livePresentation && live.metrics === metrics && live.width === contentWidth && live.toolGroupsExpanded === toolGroupsExpanded && live.todoRowLimit === todoRowLimit) {
|
|
533
|
+
stats.liveHits += 1;
|
|
534
|
+
return live.sources;
|
|
535
|
+
}
|
|
536
|
+
const liveCompletedAt = metrics?.completedAt;
|
|
537
|
+
const timing = {
|
|
538
|
+
todoRowLimit,
|
|
539
|
+
thoughtDurationMs: metrics?.firstAnswerAt === undefined ? undefined : Math.max(0, metrics?.firstAnswerAt - metrics.startedAt),
|
|
540
|
+
totalDurationMs: liveCompletedAt === undefined ? undefined : Math.max(0, liveCompletedAt - metrics.startedAt),
|
|
541
|
+
presentation: snapshot.livePresentation,
|
|
542
|
+
turnId: liveTurnId,
|
|
543
|
+
status: snapshot.status,
|
|
544
|
+
};
|
|
545
|
+
const sources = createTuiViewportEventSources(liveTurnId ?? "live", snapshot.liveEvents, contentWidth, timing, toolGroupsExpanded);
|
|
546
|
+
live = { turnId: liveTurnId, events: snapshot.liveEvents, presentation: snapshot.livePresentation, metrics, width: contentWidth, toolGroupsExpanded, todoRowLimit, sources };
|
|
547
|
+
stats.liveBuilds += 1;
|
|
548
|
+
return sources;
|
|
549
|
+
}
|
|
550
|
+
/** 返回按稳定历史、live tail 和 pending 输入拼接的当前 source 顺序。 */
|
|
551
|
+
function getSources(snapshot, statusView, _approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false, todoRowLimit = 10) {
|
|
552
|
+
invalidateWidth(contentWidth);
|
|
553
|
+
if (!brand || brand.statusView !== statusView) {
|
|
554
|
+
brand = { statusView, source: createTuiViewportBrandSource(statusView) };
|
|
555
|
+
stats.brandBuilds += 1;
|
|
556
|
+
}
|
|
557
|
+
const sources = [brand.source];
|
|
558
|
+
let renderedActivePrompt = false;
|
|
559
|
+
for (const item of snapshot.transcript) {
|
|
560
|
+
if (item.kind === "prompt") {
|
|
561
|
+
sources.push(...getHistorySources(item, contentWidth, decorativeGrid));
|
|
562
|
+
if (item.key === snapshot.activePrompt?.key)
|
|
563
|
+
renderedActivePrompt = true;
|
|
564
|
+
}
|
|
565
|
+
else if (item.kind === "turn" && item.turnId !== snapshot.liveTurnId) {
|
|
566
|
+
sources.push(...getHistorySources(item, contentWidth, decorativeGrid));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (snapshot.activePrompt && !renderedActivePrompt)
|
|
570
|
+
sources.push(...getActivePromptSource(snapshot.activePrompt));
|
|
571
|
+
sources.push(...getLiveSources(snapshot, contentWidth, toolGroupsExpanded, todoRowLimit));
|
|
572
|
+
for (const item of snapshot.pendingInputs.filter(function queue(item) { return item.delivery === "queue"; }))
|
|
573
|
+
sources.push(...getPendingSource(item));
|
|
574
|
+
return sources;
|
|
575
|
+
}
|
|
576
|
+
/** 返回当前缓存统计快照,不暴露内部 Map 或 source 数组。 */
|
|
577
|
+
function getStats() {
|
|
578
|
+
return { ...stats };
|
|
579
|
+
}
|
|
580
|
+
/** 清空全部 source 缓存并重置统计,供生命周期切换和测试隔离使用。 */
|
|
581
|
+
function clear() {
|
|
582
|
+
history.clear();
|
|
583
|
+
pending.clear();
|
|
584
|
+
activePrompt = undefined;
|
|
585
|
+
brand = undefined;
|
|
586
|
+
live = undefined;
|
|
587
|
+
width = undefined;
|
|
588
|
+
for (const key of Object.keys(stats))
|
|
589
|
+
stats[key] = 0;
|
|
590
|
+
}
|
|
591
|
+
return { getSources, getStats, clear };
|
|
592
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** 备用屏视口:按应用层视口裁剪时间线并渲染自绘滚动条。 */
|
|
2
|
+
import type { ScrollViewportState } from "../../scroll-viewport.js";
|
|
3
|
+
import type { TimelineVisualLine } from "../../timeline-lines.js";
|
|
4
|
+
import { type TuiTextSelection } from "./selection.js";
|
|
5
|
+
/** 在备用屏中按应用层视口裁剪时间线,并把最右一列留给自绘滚动条。 */
|
|
6
|
+
export declare const TuiViewportTimeline: import("react").NamedExoticComponent<{
|
|
7
|
+
lines: TimelineVisualLine[];
|
|
8
|
+
state: ScrollViewportState;
|
|
9
|
+
columns: number;
|
|
10
|
+
useColor: boolean;
|
|
11
|
+
selection: TuiTextSelection;
|
|
12
|
+
}>;
|