@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,380 @@
|
|
|
1
|
+
/** TUI 外部 Store:把安全事件归约为 React 可订阅快照,不读取 Core Store 或 checkpoint。 */
|
|
2
|
+
import { createInitialCliActivityState, reduceCliActivityState, settleCliActivityState, } from "../activity-animation.js";
|
|
3
|
+
import { TUI_EVENT_PUBLISH_INTERVAL_MS } from "./constants.js";
|
|
4
|
+
import { isTerminalStatus } from "./format.js";
|
|
5
|
+
import { recordTuiPerformanceDiagnostic } from "./performance-diagnostics.js";
|
|
6
|
+
import { captureTuiInputPosition } from "./timeline/turn-items.js";
|
|
7
|
+
/** 创建 TUI 初始快照。 */
|
|
8
|
+
function createInitialTuiSnapshot(now = Date.now()) {
|
|
9
|
+
return {
|
|
10
|
+
transcript: [],
|
|
11
|
+
pendingInputs: [],
|
|
12
|
+
liveEvents: [],
|
|
13
|
+
hasLiveApprovalEvent: false,
|
|
14
|
+
activity: createInitialCliActivityState(now),
|
|
15
|
+
status: "idle",
|
|
16
|
+
recoverable: false,
|
|
17
|
+
activeSkills: [],
|
|
18
|
+
contextUsage: undefined,
|
|
19
|
+
cancellationRequested: false,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** 创建 Ink TUI 的外部 Store。 */
|
|
23
|
+
export function createCliTuiStore() {
|
|
24
|
+
let snapshot = createInitialTuiSnapshot();
|
|
25
|
+
let noticeIndex = 0;
|
|
26
|
+
let promptIndex = 0;
|
|
27
|
+
let scheduledNotify;
|
|
28
|
+
const settledInputIds = new Set();
|
|
29
|
+
const consumedPositions = new Map();
|
|
30
|
+
const cancelledTurns = new Set();
|
|
31
|
+
let submissionOrder = 0;
|
|
32
|
+
const submittedInputIds = new Set();
|
|
33
|
+
const submittedApprovalIds = new Set();
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
const eventsByTurn = new Map();
|
|
36
|
+
const presentationByTurn = new Map();
|
|
37
|
+
const metricsByTurn = new Map();
|
|
38
|
+
const finalizedTurns = new Set();
|
|
39
|
+
function publish(nextSnapshot) {
|
|
40
|
+
flushScheduledNotify();
|
|
41
|
+
snapshot = nextSnapshot;
|
|
42
|
+
notifyListeners();
|
|
43
|
+
}
|
|
44
|
+
function flushScheduledNotify() {
|
|
45
|
+
if (scheduledNotify === undefined)
|
|
46
|
+
return;
|
|
47
|
+
clearTimeout(scheduledNotify);
|
|
48
|
+
scheduledNotify = undefined;
|
|
49
|
+
}
|
|
50
|
+
function scheduleNotify() {
|
|
51
|
+
if (scheduledNotify !== undefined)
|
|
52
|
+
return;
|
|
53
|
+
scheduledNotify = setTimeout(function publishAnswerBatch() {
|
|
54
|
+
scheduledNotify = undefined;
|
|
55
|
+
notifyListeners();
|
|
56
|
+
}, TUI_EVENT_PUBLISH_INTERVAL_MS);
|
|
57
|
+
}
|
|
58
|
+
function notifyListeners() {
|
|
59
|
+
const startedAt = performance.now();
|
|
60
|
+
for (const listener of listeners)
|
|
61
|
+
listener();
|
|
62
|
+
recordTuiPerformanceDiagnostic("tui_store_published", "snapshot", performance.now() - startedAt);
|
|
63
|
+
}
|
|
64
|
+
/** 更新所属轮的宿主输入事实;晚到回调同时刷新历史快照,不挪到当前另一轮。 */
|
|
65
|
+
function putInput(content, input, position, consumed) {
|
|
66
|
+
const turnId = position.turnId;
|
|
67
|
+
if (!turnId)
|
|
68
|
+
return;
|
|
69
|
+
const events = [...(eventsByTurn.get(turnId) ?? [])];
|
|
70
|
+
const priorIndex = events.findIndex(function same(event) { return event.type === "tui_input_consumed" && event.inputId === input.inputId; });
|
|
71
|
+
const prior = priorIndex >= 0 ? events[priorIndex] : undefined;
|
|
72
|
+
const event = { type: "tui_input_consumed", seq: 0, turnId, ...input, content, consumed, cancelled: !consumed && cancelledTurns.has(turnId), position: prior?.type === "tui_input_consumed" ? prior.position : position };
|
|
73
|
+
if (priorIndex >= 0)
|
|
74
|
+
events[priorIndex] = event;
|
|
75
|
+
else {
|
|
76
|
+
let publicCount = 0;
|
|
77
|
+
let insertAt = events.length;
|
|
78
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
79
|
+
const current = events[index];
|
|
80
|
+
if (current.type === "tui_input_consumed") {
|
|
81
|
+
if (publicCount >= position.eventCount && (current.position?.order ?? 0) > (position.order ?? 0)) {
|
|
82
|
+
insertAt = index;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (publicCount >= position.eventCount) {
|
|
88
|
+
insertAt = index;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
publicCount += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
events.splice(insertAt, 0, event);
|
|
95
|
+
}
|
|
96
|
+
eventsByTurn.set(turnId, events);
|
|
97
|
+
snapshot = { ...snapshot, liveEvents: snapshot.liveTurnId === turnId ? events : snapshot.liveEvents, transcript: snapshot.transcript.map(function update(item) { return item.kind === "turn" && item.turnId === turnId ? { ...item, events } : item; }) };
|
|
98
|
+
}
|
|
99
|
+
function finalizeTurn(turnId, status) {
|
|
100
|
+
if (finalizedTurns.has(turnId))
|
|
101
|
+
return;
|
|
102
|
+
const events = eventsByTurn.get(turnId) ?? [];
|
|
103
|
+
if (events.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
finalizedTurns.add(turnId);
|
|
106
|
+
const metrics = metricsByTurn.get(turnId);
|
|
107
|
+
const thoughtDurationMs = metrics?.firstAnswerAt === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: Math.max(0, metrics.firstAnswerAt - metrics.startedAt);
|
|
110
|
+
const totalDurationMs = metrics?.completedAt === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: Math.max(0, metrics.completedAt - metrics.startedAt);
|
|
113
|
+
// 终态先保留当前 live Turn,让这一帧同时接住最后的 answer/status block;
|
|
114
|
+
// 立即清空会让外层历史区过滤掉同一 Turn,导致最后一段回答不可见。
|
|
115
|
+
snapshot = {
|
|
116
|
+
...snapshot,
|
|
117
|
+
transcript: [
|
|
118
|
+
...snapshot.transcript,
|
|
119
|
+
{
|
|
120
|
+
kind: "turn",
|
|
121
|
+
key: "turn:" + turnId + ":" + String(events[events.length - 1]?.seq ?? 0),
|
|
122
|
+
turnId,
|
|
123
|
+
events: [...events],
|
|
124
|
+
presentation: presentationByTurn.get(turnId),
|
|
125
|
+
status,
|
|
126
|
+
thoughtDurationMs,
|
|
127
|
+
totalDurationMs,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
getSnapshot() {
|
|
134
|
+
return snapshot;
|
|
135
|
+
},
|
|
136
|
+
subscribe(listener) {
|
|
137
|
+
listeners.add(listener);
|
|
138
|
+
return function unsubscribe() {
|
|
139
|
+
listeners.delete(listener);
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
recordPrompt(content, displayAsActive = true) {
|
|
143
|
+
const trimmed = content.trim();
|
|
144
|
+
if (!trimmed)
|
|
145
|
+
return undefined;
|
|
146
|
+
// 运行中的 steer/queue 由已确认的 inputId 单独记录,避免发送前伪造成功展示。
|
|
147
|
+
if (!displayAsActive)
|
|
148
|
+
return undefined;
|
|
149
|
+
const key = "prompt:" + String(promptIndex++);
|
|
150
|
+
publish({
|
|
151
|
+
...snapshot,
|
|
152
|
+
// info 是下一次 Turn 之前的临时反馈;新 prompt 被接收后必须立即消费,不能留在新 Turn 的输入区上方。
|
|
153
|
+
transcript: [
|
|
154
|
+
...snapshot.transcript.filter(function omitConsumedNotice(item) { return item.kind !== "notice"; }),
|
|
155
|
+
{ kind: "prompt", key, content: trimmed },
|
|
156
|
+
],
|
|
157
|
+
activePrompt: displayAsActive ? { key, content: trimmed } : snapshot.activePrompt,
|
|
158
|
+
});
|
|
159
|
+
return key;
|
|
160
|
+
},
|
|
161
|
+
/** 在异步投递之前捕获真实提交位置。 */
|
|
162
|
+
captureInputPosition() { return { ...captureTuiInputPosition(snapshot.liveTurnId, snapshot.liveEvents, snapshot.livePresentation), order: submissionOrder++ }; },
|
|
163
|
+
/** 成功投递后才外显原文,消费与回调的先后次序不改变所属轮。 */
|
|
164
|
+
addPendingPrompt(content, input, position = captureTuiInputPosition(snapshot.liveTurnId, snapshot.liveEvents, snapshot.livePresentation)) {
|
|
165
|
+
if (!content.trim() || submittedInputIds.has(input.inputId))
|
|
166
|
+
return;
|
|
167
|
+
submittedInputIds.add(input.inputId);
|
|
168
|
+
const consumed = settledInputIds.has(input.inputId);
|
|
169
|
+
const settledPosition = consumedPositions.get(input.inputId);
|
|
170
|
+
const target = input.delivery === "steer" && position.turnId && (!settledPosition || settledPosition.turnId === position.turnId) ? position : settledPosition;
|
|
171
|
+
if (target)
|
|
172
|
+
putInput(content, input, target, consumed);
|
|
173
|
+
if (consumed || (position.turnId && cancelledTurns.has(position.turnId))) {
|
|
174
|
+
publish(snapshot);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
publish({
|
|
178
|
+
...snapshot,
|
|
179
|
+
pendingInputs: [
|
|
180
|
+
...snapshot.pendingInputs,
|
|
181
|
+
{
|
|
182
|
+
key: "pending:" + String(promptIndex++),
|
|
183
|
+
content,
|
|
184
|
+
inputId: input.inputId,
|
|
185
|
+
delivery: input.delivery,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
/** 在自动 queue Turn 开始前,将 prompt 放进目标 Turn 的事件序列并清除等待态。 */
|
|
191
|
+
markQueuedInputStarted(input) {
|
|
192
|
+
if (settledInputIds.has(input.inputId))
|
|
193
|
+
return;
|
|
194
|
+
settledInputIds.add(input.inputId);
|
|
195
|
+
const position = { turnId: input.turnId, eventCount: 0, items: [] };
|
|
196
|
+
consumedPositions.set(input.inputId, position);
|
|
197
|
+
const pending = snapshot.pendingInputs.find(function same(item) { return item.inputId === input.inputId; });
|
|
198
|
+
// 领取回调的 content 可能带附件展开,只在原文已经确认时输出正文。
|
|
199
|
+
if (pending)
|
|
200
|
+
putInput(pending.content, input, position, true);
|
|
201
|
+
publish({
|
|
202
|
+
...snapshot,
|
|
203
|
+
pendingInputs: snapshot.pendingInputs.filter(function keepUnstartedInput(item) { return item.inputId !== input.inputId; }),
|
|
204
|
+
liveTurnId: snapshot.liveTurnId === input.turnId ? input.turnId : snapshot.liveTurnId,
|
|
205
|
+
liveEvents: snapshot.liveEvents,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
/** 用户选择审批范围后立即将等待卡收束为已提交结果;Runtime 后续事件仍是最终执行依据。 */
|
|
209
|
+
recordApprovalSubmission(decision) {
|
|
210
|
+
const approval = snapshot.pendingApproval;
|
|
211
|
+
const turnId = snapshot.liveTurnId;
|
|
212
|
+
if (!approval || !turnId)
|
|
213
|
+
return;
|
|
214
|
+
submittedApprovalIds.add(approval.approvalId);
|
|
215
|
+
const currentEvents = eventsByTurn.get(turnId) ?? [];
|
|
216
|
+
if (currentEvents.some(function hasSubmission(event) {
|
|
217
|
+
return event.type === "tui_approval_submitted" && event.approvalId === approval.approvalId;
|
|
218
|
+
}))
|
|
219
|
+
return;
|
|
220
|
+
const nextEvents = [
|
|
221
|
+
...currentEvents,
|
|
222
|
+
{ type: "tui_approval_submitted", seq: Number.MAX_SAFE_INTEGER, turnId, approvalId: approval.approvalId, decision },
|
|
223
|
+
];
|
|
224
|
+
eventsByTurn.set(turnId, nextEvents);
|
|
225
|
+
publish({ ...snapshot, liveEvents: snapshot.liveTurnId === turnId ? nextEvents : snapshot.liveEvents, pendingApproval: undefined });
|
|
226
|
+
},
|
|
227
|
+
addNotice(message) {
|
|
228
|
+
const trimmed = message.trim();
|
|
229
|
+
if (!trimmed)
|
|
230
|
+
return;
|
|
231
|
+
publish({
|
|
232
|
+
...snapshot,
|
|
233
|
+
transcript: [
|
|
234
|
+
...snapshot.transcript,
|
|
235
|
+
{ kind: "notice", key: "notice:" + String(noticeIndex++), message: trimmed },
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
acceptEvent(event, projection) {
|
|
240
|
+
// 用量变化只刷新动态状态栏事实,不进入 transcript 时间线或 scrollback。
|
|
241
|
+
if (event.type === "context_usage") {
|
|
242
|
+
if (snapshot.contextUsage === event.usage)
|
|
243
|
+
return;
|
|
244
|
+
publish({ ...snapshot, contextUsage: event.usage });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Esc 后只等待 Runtime 的终态;取消请求之后已经在途的回答/工具尾部事件不再进入可见时间线。
|
|
248
|
+
if (snapshot.cancellationRequested && !(event.type === "status" && isTerminalStatus(event.status)))
|
|
249
|
+
return;
|
|
250
|
+
const currentEvents = eventsByTurn.get(event.turnId) ?? [];
|
|
251
|
+
if (currentEvents.some(function hasSequence(item) { return item.type === event.type && item.seq === event.seq; }))
|
|
252
|
+
return;
|
|
253
|
+
const nextEvents = [...currentEvents, event];
|
|
254
|
+
eventsByTurn.set(event.turnId, nextEvents);
|
|
255
|
+
presentationByTurn.set(event.turnId, projection.presentation);
|
|
256
|
+
const isNewTurn = snapshot.liveTurnId !== event.turnId;
|
|
257
|
+
const activePrompt = snapshot.activePrompt && snapshot.activePrompt.turnId === undefined
|
|
258
|
+
? { ...snapshot.activePrompt, turnId: event.turnId }
|
|
259
|
+
: snapshot.activePrompt;
|
|
260
|
+
const eventTime = Date.now();
|
|
261
|
+
const previousMetrics = metricsByTurn.get(event.turnId);
|
|
262
|
+
const nextMetrics = {
|
|
263
|
+
startedAt: event.type === "turn_started" ? eventTime : previousMetrics?.startedAt ?? eventTime,
|
|
264
|
+
firstAnswerAt: previousMetrics?.firstAnswerAt,
|
|
265
|
+
completedAt: previousMetrics?.completedAt,
|
|
266
|
+
};
|
|
267
|
+
if (event.type === "answer_delta" && nextMetrics.firstAnswerAt === undefined)
|
|
268
|
+
nextMetrics.firstAnswerAt = eventTime;
|
|
269
|
+
// Core 的 reset/discard 表示当前采样的暂定文字不能展示;下一次真正回答重新计时。
|
|
270
|
+
if (event.type === "answer_reset")
|
|
271
|
+
nextMetrics.firstAnswerAt = undefined;
|
|
272
|
+
if (event.type === "status" && isTerminalStatus(event.status))
|
|
273
|
+
nextMetrics.completedAt = eventTime;
|
|
274
|
+
metricsByTurn.set(event.turnId, nextMetrics);
|
|
275
|
+
const nextActivity = settleCliActivityState(reduceCliActivityState(snapshot.activity, event, eventTime), projection.status, eventTime);
|
|
276
|
+
const nextSnapshot = {
|
|
277
|
+
...snapshot,
|
|
278
|
+
activePrompt,
|
|
279
|
+
liveTurnId: event.turnId,
|
|
280
|
+
liveEvents: nextEvents,
|
|
281
|
+
livePresentation: projection.presentation,
|
|
282
|
+
liveMetrics: { ...nextMetrics },
|
|
283
|
+
hasLiveApprovalEvent: isNewTurn ? event.type === "approval_required" : snapshot.hasLiveApprovalEvent || event.type === "approval_required",
|
|
284
|
+
activity: nextActivity,
|
|
285
|
+
status: projection.status,
|
|
286
|
+
recoverable: projection.recoverable,
|
|
287
|
+
pendingApproval: projection.pendingApproval && !submittedApprovalIds.has(projection.pendingApproval.approvalId)
|
|
288
|
+
? { ...projection.pendingApproval }
|
|
289
|
+
: undefined,
|
|
290
|
+
activeSkills: projection.activeSkills.map(function copySkill(skill) { return { ...skill }; }),
|
|
291
|
+
cancellationRequested: event.type === "status" && isTerminalStatus(event.status) ? false : snapshot.cancellationRequested,
|
|
292
|
+
};
|
|
293
|
+
if (event.type === "input_consumed") {
|
|
294
|
+
settledInputIds.add(event.inputId);
|
|
295
|
+
if (!consumedPositions.has(event.inputId))
|
|
296
|
+
consumedPositions.set(event.inputId, captureTuiInputPosition(event.turnId, currentEvents, snapshot.livePresentation));
|
|
297
|
+
const pending = nextSnapshot.pendingInputs.find(function same(item) { return item.inputId === event.inputId; });
|
|
298
|
+
nextSnapshot.pendingInputs = nextSnapshot.pendingInputs.filter(function keepPendingInput(item) { return item.inputId !== event.inputId; });
|
|
299
|
+
const host = nextEvents.find(function same(item) { return item.type === "tui_input_consumed" && item.inputId === event.inputId; });
|
|
300
|
+
if (host?.type === "tui_input_consumed")
|
|
301
|
+
nextSnapshot.liveEvents = nextEvents.map(function consume(item) { return item === host ? { ...host, consumed: true } : item; });
|
|
302
|
+
eventsByTurn.set(event.turnId, nextSnapshot.liveEvents);
|
|
303
|
+
if (!host && pending) {
|
|
304
|
+
snapshot = nextSnapshot;
|
|
305
|
+
const position = pending.delivery === "queue" ? { turnId: event.turnId, eventCount: 0, items: [] } : consumedPositions.get(event.inputId);
|
|
306
|
+
consumedPositions.set(event.inputId, position);
|
|
307
|
+
putInput(pending.content, pending, position, true);
|
|
308
|
+
nextSnapshot.liveEvents = snapshot.liveEvents;
|
|
309
|
+
nextSnapshot.transcript = snapshot.transcript;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
snapshot = nextSnapshot;
|
|
313
|
+
if (event.type === "status" && isTerminalStatus(event.status)) {
|
|
314
|
+
finalizeTurn(event.turnId, event.status);
|
|
315
|
+
}
|
|
316
|
+
if (event.type === "answer_delta") {
|
|
317
|
+
scheduleNotify();
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
flushScheduledNotify();
|
|
321
|
+
notifyListeners();
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
syncProjection(projection) {
|
|
325
|
+
if (projection.turnId && !eventsByTurn.has(projection.turnId) && projection.events.length > 0)
|
|
326
|
+
eventsByTurn.set(projection.turnId, projection.events.filter(function belongs(event) { return event.turnId === projection.turnId; }));
|
|
327
|
+
const turnIsFinalized = projection.turnId ? finalizedTurns.has(projection.turnId) : false;
|
|
328
|
+
const isNewTurn = projection.turnId !== undefined && projection.turnId !== snapshot.liveTurnId;
|
|
329
|
+
const activePrompt = snapshot.activePrompt && snapshot.activePrompt.turnId === undefined && projection.turnId
|
|
330
|
+
? { ...snapshot.activePrompt, turnId: projection.turnId }
|
|
331
|
+
: snapshot.activePrompt;
|
|
332
|
+
const keepFinalizedLiveTurn = Boolean(projection.turnId && turnIsFinalized && projection.turnId === snapshot.liveTurnId);
|
|
333
|
+
const keepLiveTurn = Boolean(projection.turnId && (!turnIsFinalized || keepFinalizedLiveTurn));
|
|
334
|
+
if (projection.turnId)
|
|
335
|
+
presentationByTurn.set(projection.turnId, projection.presentation);
|
|
336
|
+
let activity = isNewTurn ? createInitialCliActivityState(Date.now()) : snapshot.activity;
|
|
337
|
+
if (projection.pendingApproval || projection.status === "waiting_approval") {
|
|
338
|
+
activity = reduceCliActivityState(activity, { type: "approval_required" }, Date.now());
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
activity = settleCliActivityState(activity, projection.status, Date.now());
|
|
342
|
+
}
|
|
343
|
+
snapshot = {
|
|
344
|
+
...snapshot,
|
|
345
|
+
activePrompt,
|
|
346
|
+
liveTurnId: keepLiveTurn ? projection.turnId : undefined,
|
|
347
|
+
liveEvents: keepLiveTurn ? [...(eventsByTurn.get(projection.turnId) ?? [])] : [],
|
|
348
|
+
livePresentation: keepLiveTurn ? projection.presentation : undefined,
|
|
349
|
+
liveMetrics: keepLiveTurn && projection.turnId ? metricsByTurn.get(projection.turnId) : undefined,
|
|
350
|
+
hasLiveApprovalEvent: Boolean(keepLiveTurn && !isNewTurn && snapshot.hasLiveApprovalEvent),
|
|
351
|
+
activity,
|
|
352
|
+
status: projection.status,
|
|
353
|
+
recoverable: projection.recoverable,
|
|
354
|
+
pendingApproval: projection.pendingApproval && !submittedApprovalIds.has(projection.pendingApproval.approvalId)
|
|
355
|
+
? { ...projection.pendingApproval }
|
|
356
|
+
: undefined,
|
|
357
|
+
activeSkills: projection.activeSkills.map(function copySkill(skill) { return { ...skill }; }),
|
|
358
|
+
cancellationRequested: isTerminalStatus(projection.status) ? false : snapshot.cancellationRequested,
|
|
359
|
+
};
|
|
360
|
+
if (projection.turnId && isTerminalStatus(projection.status)) {
|
|
361
|
+
finalizeTurn(projection.turnId, projection.status);
|
|
362
|
+
}
|
|
363
|
+
notifyListeners();
|
|
364
|
+
},
|
|
365
|
+
setCancellationRequested(requested) {
|
|
366
|
+
if (snapshot.cancellationRequested === requested)
|
|
367
|
+
return;
|
|
368
|
+
if (requested && snapshot.liveTurnId) {
|
|
369
|
+
cancelledTurns.add(snapshot.liveTurnId);
|
|
370
|
+
const events = snapshot.liveEvents.map(function cancel(event) { return event.type === "tui_input_consumed" && event.consumed === false ? { ...event, cancelled: true } : event; });
|
|
371
|
+
eventsByTurn.set(snapshot.liveTurnId, events);
|
|
372
|
+
snapshot = { ...snapshot, liveEvents: events, pendingInputs: [] };
|
|
373
|
+
}
|
|
374
|
+
publish({ ...snapshot, cancellationRequested: requested });
|
|
375
|
+
},
|
|
376
|
+
requestExit(code) {
|
|
377
|
+
publish({ ...snapshot, requestedExitCode: code });
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 动态时间线的增量缓存与渲染:只更新尾部事件或当前回答块,避免重扫全部事件。 */
|
|
2
|
+
import type { CliTurnStatus } from "../../types.js";
|
|
3
|
+
import type { CliTuiTimelineEvent, CliTuiTurnMetrics } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Ink 在动态区超过一屏后会清空终端并从顶部重绘;把已经确定的块交给
|
|
6
|
+
* 历史树后,动态区最多只保留当前回答或未决审批,避免活动期间
|
|
7
|
+
* 的每个事件都把 Windows Terminal 的原生滚动视口拉回顶部。
|
|
8
|
+
*/
|
|
9
|
+
export declare const TuiLiveTimeline: import("react").NamedExoticComponent<{
|
|
10
|
+
turnId: string;
|
|
11
|
+
events: CliTuiTimelineEvent[];
|
|
12
|
+
status: CliTurnStatus;
|
|
13
|
+
useColor: boolean;
|
|
14
|
+
maximumWidth: number;
|
|
15
|
+
metrics?: CliTuiTurnMetrics;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/** 动态时间线的增量缓存与渲染:只更新尾部事件或当前回答块,避免重扫全部事件。 */
|
|
3
|
+
import { memo, useRef } from "react";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
6
|
+
import { TUI_AGENT_LABEL } from "../constants.js";
|
|
7
|
+
import { formatTuiDuration, formatTuiInputConsumed, getTuiApprovalDecisionLabel, isTerminalStatus } from "../format.js";
|
|
8
|
+
import { TuiMarkdown } from "../markdown/markdown.js";
|
|
9
|
+
import { TuiPromptLine } from "../transcript.js";
|
|
10
|
+
import { renderTuiToolTreeEvent } from "./tool-tree.js";
|
|
11
|
+
/** 为增量时间线生成稳定键,避免 answer delta 到达时重建历史节点。 */
|
|
12
|
+
function getCliTuiTimelineEventKey(event) {
|
|
13
|
+
const inputId = "inputId" in event ? event.inputId : "";
|
|
14
|
+
const approvalId = "approvalId" in event ? event.approvalId : "";
|
|
15
|
+
return `${event.type}:${event.seq}:${event.turnId}:${inputId}:${approvalId}`;
|
|
16
|
+
}
|
|
17
|
+
/** 判断新事件列表是否只是同一 Turn 的尾部追加。 */
|
|
18
|
+
function isCliTuiTimelineAppendOnly(cache, events) {
|
|
19
|
+
if (cache.eventCount === 0)
|
|
20
|
+
return true;
|
|
21
|
+
const hostInputs = events.filter(function host(event) { return event.type === "tui_input_consumed"; });
|
|
22
|
+
if (hostInputs.length !== cache.hostInputs.length || hostInputs.some(function changed(event, index) { return event !== cache.hostInputs[index]; }))
|
|
23
|
+
return false;
|
|
24
|
+
return events.length >= cache.eventCount && events[cache.eventCount - 1] === cache.lastEvent;
|
|
25
|
+
}
|
|
26
|
+
/** 应用一条时间线事件,只修改新增尾部或当前回答块。 */
|
|
27
|
+
function appendCliTuiTimelineEvent(cache, event) {
|
|
28
|
+
if (event.type === "answer_delta") {
|
|
29
|
+
if (!event.delta)
|
|
30
|
+
return;
|
|
31
|
+
const lastBlock = cache.blocks[cache.blocks.length - 1];
|
|
32
|
+
if (lastBlock?.kind === "answer") {
|
|
33
|
+
lastBlock.answer += event.delta;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
cache.blocks.push({ kind: "answer", key: "answer:" + String(event.seq), answer: event.delta });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (event.type === "answer_reset") {
|
|
40
|
+
for (let index = cache.blocks.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (cache.blocks[index]?.kind === "answer") {
|
|
42
|
+
cache.blocks.splice(index, 1);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (event.type === "tui_approval_submitted") {
|
|
48
|
+
for (const block of cache.blocks) {
|
|
49
|
+
if (block.kind === "event" && block.event.type === "approval_required" && block.event.approvalId === event.approvalId)
|
|
50
|
+
block.hidden = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
cache.blocks.push({ kind: "event", key: getCliTuiTimelineEventKey(event), event });
|
|
54
|
+
}
|
|
55
|
+
/** 首次或 Turn 切换时完整归约时间线;后续更新走尾部增量路径。 */
|
|
56
|
+
function createCliTuiTimelineCache(events) {
|
|
57
|
+
const cache = { eventCount: 0, blocks: [], hostInputs: events.filter(function host(event) { return event.type === "tui_input_consumed"; }) };
|
|
58
|
+
for (const event of events)
|
|
59
|
+
appendCliTuiTimelineEvent(cache, event);
|
|
60
|
+
cache.eventCount = events.length;
|
|
61
|
+
cache.lastEvent = events[events.length - 1];
|
|
62
|
+
return cache;
|
|
63
|
+
}
|
|
64
|
+
/** 为当前 live Turn 复用已归约的时间线块,避免每个输入字符重扫全部事件。 */
|
|
65
|
+
function useCliTuiTimelineBlocks(events) {
|
|
66
|
+
const cacheRef = useRef(undefined);
|
|
67
|
+
let cache = cacheRef.current;
|
|
68
|
+
if (!cache || !isCliTuiTimelineAppendOnly(cache, events)) {
|
|
69
|
+
cache = createCliTuiTimelineCache(events);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (let index = cache.eventCount; index < events.length; index += 1) {
|
|
73
|
+
const event = events[index];
|
|
74
|
+
if (event)
|
|
75
|
+
appendCliTuiTimelineEvent(cache, event);
|
|
76
|
+
}
|
|
77
|
+
cache.eventCount = events.length;
|
|
78
|
+
cache.lastEvent = events[events.length - 1];
|
|
79
|
+
}
|
|
80
|
+
cacheRef.current = cache;
|
|
81
|
+
return cache.blocks;
|
|
82
|
+
}
|
|
83
|
+
/** 渲染一个不再随连续 answer delta 改变的安全事件节点。 */
|
|
84
|
+
function TuiLiveEventBlock(props) {
|
|
85
|
+
const event = props.event;
|
|
86
|
+
if (event.type === "tui_input_consumed") {
|
|
87
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(TuiPromptLine, { content: event.content, useColor: props.useColor, columns: props.maximumWidth + 2 }), _jsx(Text, { dimColor: props.useColor, children: event.cancelled ? "[引导] 已取消" : event.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(event) })] });
|
|
88
|
+
}
|
|
89
|
+
if (event.type === "tui_approval_submitted") {
|
|
90
|
+
return _jsxs(Text, { dimColor: props.useColor, children: ["Approval submitted \u00B7 ", getTuiApprovalDecisionLabel(event.decision)] });
|
|
91
|
+
}
|
|
92
|
+
if (event.type === "turn_started" || event.type === "reasoning_summary" || event.type === "reasoning_hidden" || event.type === "answer_reset" || event.type === "input_joined" || event.type === "input_consumed" || event.type === "active_skills")
|
|
93
|
+
return null;
|
|
94
|
+
if (event.type === "tool_started" || event.type === "tool_result")
|
|
95
|
+
return renderTuiToolTreeEvent(event, props.useColor);
|
|
96
|
+
if (event.type === "approval_required")
|
|
97
|
+
return null;
|
|
98
|
+
if (event.type === "input_queued")
|
|
99
|
+
return _jsx(Text, { dimColor: props.useColor, children: "Additional instruction queued; waiting for the current step" });
|
|
100
|
+
if (event.type === "todo_updated")
|
|
101
|
+
return null;
|
|
102
|
+
if (event.type === "model_recovery")
|
|
103
|
+
return _jsxs(Text, { dimColor: props.useColor, children: ["Recovering \u00B7 ", event.reason] });
|
|
104
|
+
if (event.type === "context_overflow")
|
|
105
|
+
return _jsx(Text, { children: "Context limit reached; organizing before continuing" });
|
|
106
|
+
if (event.type === "context_compact_started")
|
|
107
|
+
return _jsx(Text, { dimColor: props.useColor, children: "Organizing context" });
|
|
108
|
+
if (event.type === "context_compacted")
|
|
109
|
+
return _jsx(Text, { children: "Context organized" });
|
|
110
|
+
if (event.type === "context_compact_failed")
|
|
111
|
+
return _jsx(Text, { children: "Context organization failed" });
|
|
112
|
+
if (event.type === "context_compact_circuit_open")
|
|
113
|
+
return _jsx(Text, { children: "Context organization temporarily unavailable" });
|
|
114
|
+
if (event.type === "status")
|
|
115
|
+
return null;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/** 对历史事件节点启用 memo,使新增 answer delta 不重新执行工具/diff/审批渲染。 */
|
|
119
|
+
const MemoizedTuiLiveEventBlock = memo(TuiLiveEventBlock);
|
|
120
|
+
/** 渲染当前累计回答块;只有最后一个连续回答块会随 delta 更新。 */
|
|
121
|
+
function TuiLiveAnswerBlock(props) {
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: ["\u2022 ", TUI_AGENT_LABEL] }), props.renderMarkdown
|
|
123
|
+
? _jsx(TuiMarkdown, { content: props.answer, keyPrefix: props.keyPrefix, useColor: props.useColor, maximumWidth: props.maximumWidth })
|
|
124
|
+
: _jsx(Text, { wrap: "wrap", children: props.answer })] }));
|
|
125
|
+
}
|
|
126
|
+
/** 对回答块启用 memo,保持长时间线中已经完成的 Markdown 节点不变。 */
|
|
127
|
+
const MemoizedTuiLiveAnswerBlock = memo(TuiLiveAnswerBlock);
|
|
128
|
+
/** 增量渲染当前 Turn 的时间线块;完整 transcript 由历史树使用 renderTimeline。 */
|
|
129
|
+
function renderTuiLiveTimelineBlock(block, useColor, renderMarkdown, maximumWidth) {
|
|
130
|
+
if (block.kind === "answer") {
|
|
131
|
+
return _jsx(MemoizedTuiLiveAnswerBlock, { answer: block.answer, keyPrefix: block.key, useColor: useColor, renderMarkdown: renderMarkdown, maximumWidth: maximumWidth });
|
|
132
|
+
}
|
|
133
|
+
if (block.hidden)
|
|
134
|
+
return null;
|
|
135
|
+
if (block.event.type === "answer_delta")
|
|
136
|
+
return null;
|
|
137
|
+
return _jsx(MemoizedTuiLiveEventBlock, { event: block.event, useColor: useColor, maximumWidth: maximumWidth });
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Ink 在动态区超过一屏后会清空终端并从顶部重绘;把已经确定的块交给
|
|
141
|
+
* 历史树后,动态区最多只保留当前回答或未决审批,避免活动期间
|
|
142
|
+
* 的每个事件都把 Windows Terminal 的原生滚动视口拉回顶部。
|
|
143
|
+
*/
|
|
144
|
+
export const TuiLiveTimeline = memo(function TuiLiveTimeline(props) {
|
|
145
|
+
const blocks = useCliTuiTimelineBlocks(props.events);
|
|
146
|
+
const firstAnswerKey = blocks.find(function findFirstAnswer(block) { return block.kind === "answer"; })?.key;
|
|
147
|
+
function renderBlock(block) {
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", children: [block.key === firstAnswerKey && props.metrics?.firstAnswerAt !== undefined ? _jsxs(Text, { dimColor: props.useColor, children: ["Thought for ", formatTuiDuration(Math.max(0, props.metrics.firstAnswerAt - props.metrics.startedAt))] }) : null, renderTuiLiveTimelineBlock(block, props.useColor, isTerminalStatus(props.status), props.maximumWidth)] }, block.key));
|
|
149
|
+
}
|
|
150
|
+
return (_jsx(_Fragment, { children: blocks.map(function renderDynamicTimelineBlock(block) {
|
|
151
|
+
return renderBlock(block);
|
|
152
|
+
}) }));
|
|
153
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 基于共享 Presentation selector 的 CLI 时间线;不读取旧工具 summary、detail 或审批 preview。 */
|
|
2
|
+
import { type AgentPresentationState } from "@autopilot-agent/presentation";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import type { CliTuiTimelineEvent } from "../types.js";
|
|
5
|
+
/** 详情视图参数:expanded 驱动 Todo 卡展开,toolGroupsExpanded 驱动工具分组展开;screen reader 恒展开全部明细。 */
|
|
6
|
+
type TuiDetailView = {
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
screenReader: boolean;
|
|
9
|
+
rowLimit?: number;
|
|
10
|
+
showToggleHint?: boolean;
|
|
11
|
+
toolGroupsExpanded?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** 渲染指定 Turn 的共享 Presentation 时间线;无匹配节点时由调用方回退兼容旧事件。 */
|
|
14
|
+
export declare function renderTuiPresentationTimeline(presentation: AgentPresentationState, turnId: string, useColor: boolean, maximumWidth: number, detailView: TuiDetailView, events?: CliTuiTimelineEvent[]): ReactNode[];
|
|
15
|
+
export {};
|