@autopilot-agent/cli 0.0.0-stage → 0.1.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 +180 -2
- package/dist/activity-animation.d.ts +57 -0
- package/dist/activity-animation.js +145 -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 +588 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +174 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +681 -0
- package/dist/design-tokens.d.ts +34 -0
- package/dist/design-tokens.js +34 -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 +333 -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 +391 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +48 -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 +17 -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 +410 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +37 -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 +176 -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 +344 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +151 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +14 -0
- package/dist/tui/timeline/presentation-timeline.js +109 -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 +121 -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.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 +175 -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 +585 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +87 -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 +454 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
package/dist/tui/run.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** TUI 入口编排:启动 Ink,统一处理信号、退出码和动态渲染生命周期。 */
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { createTuiInputRouter } from "../tui-input-router.js";
|
|
5
|
+
import { CliTuiApp } from "./app.js";
|
|
6
|
+
import { getTuiStatusLabel, isActiveTuiStatus, isTerminalStatus, readTuiTerminalSize } from "./format.js";
|
|
7
|
+
import { createRoutedTuiStdin, createTuiMouseInputSink, createTuiRenderStdout, createTuiTerminalLifecycle } from "./lifecycle.js";
|
|
8
|
+
import { getTuiRenderModeFallbackNotice, resolveNoColor, resolveReducedMotion, resolveScreenReader, resolveTuiRenderMode } from "./render-mode.js";
|
|
9
|
+
import { createCliTuiStore } from "./store.js";
|
|
10
|
+
/** 将已知的安全会话字段格式化为主屏 recap;输出保持纯文本,兼容 NO_COLOR。 */
|
|
11
|
+
export function formatCliTuiRecap(recap) {
|
|
12
|
+
const canResume = recap.recoverable
|
|
13
|
+
&& (recap.finalStatus === "waiting_approval" || recap.finalStatus === "interrupted_recoverable" || recap.finalStatus === "limit_reached");
|
|
14
|
+
const resumeHint = canResume ? ` · Resume: ap resume --session ${recap.sessionId}` : "";
|
|
15
|
+
return `Autopilot recap · Session: ${recap.sessionId} · Completed turns: ${recap.completedTurnCount} · Final status: ${getTuiStatusLabel(recap.finalStatus)}${resumeHint}\n`;
|
|
16
|
+
}
|
|
17
|
+
/** 启动 Ink,统一处理信号、退出码和动态渲染生命周期。 */
|
|
18
|
+
export async function runCliTui(options) {
|
|
19
|
+
const env = options.env ?? process.env;
|
|
20
|
+
const store = createCliTuiStore();
|
|
21
|
+
const noColor = resolveNoColor(env);
|
|
22
|
+
const screenReader = resolveScreenReader(env);
|
|
23
|
+
const renderMode = resolveTuiRenderMode(env);
|
|
24
|
+
const terminalSizeSource = options.streams.stdout;
|
|
25
|
+
const renderStdout = createTuiRenderStdout(terminalSizeSource, readTuiTerminalSize(terminalSizeSource));
|
|
26
|
+
const fallbackNotice = getTuiRenderModeFallbackNotice({ env, platform: process.platform });
|
|
27
|
+
if (fallbackNotice)
|
|
28
|
+
options.streams.stderr.write(fallbackNotice);
|
|
29
|
+
const terminalLifecycle = createTuiTerminalLifecycle(options.streams.stdout, renderMode);
|
|
30
|
+
const mouseInput = createTuiMouseInputSink();
|
|
31
|
+
const routedStdin = createRoutedTuiStdin(options.streams.stdin);
|
|
32
|
+
const inputRouter = createTuiInputRouter();
|
|
33
|
+
const reducedMotion = resolveReducedMotion(env);
|
|
34
|
+
// Ink 的 auto 预检没有覆盖 Windows Terminal;WT 1.24 支持 Kitty keyboard protocol,必须主动启用才能区分 Shift+Enter。
|
|
35
|
+
const kittyKeyboardMode = env.WT_SESSION ? "enabled" : "auto";
|
|
36
|
+
let requestedExitCode;
|
|
37
|
+
let lastResult;
|
|
38
|
+
let instance;
|
|
39
|
+
let lastInterruptAt = 0;
|
|
40
|
+
let pendingWheelNotches = 0;
|
|
41
|
+
let pendingWheelEvent;
|
|
42
|
+
let mouseFlushTimer;
|
|
43
|
+
/** 把同一 16ms 窗口内的滚轮 notch 合并为一次视口状态更新。 */
|
|
44
|
+
function flushMouseWheel() {
|
|
45
|
+
mouseFlushTimer = undefined;
|
|
46
|
+
const event = pendingWheelEvent;
|
|
47
|
+
const wheelNotches = Math.abs(pendingWheelNotches);
|
|
48
|
+
pendingWheelEvent = undefined;
|
|
49
|
+
pendingWheelNotches = 0;
|
|
50
|
+
if (event && wheelNotches > 0)
|
|
51
|
+
mouseInput.dispatch(event, wheelNotches);
|
|
52
|
+
}
|
|
53
|
+
/** 记录无法解析的 SGR 鼠标输入,而不把控制字节泄漏给 Ink prompt。 */
|
|
54
|
+
function recordMouseDiagnostics(diagnostics) {
|
|
55
|
+
for (const diagnostic of diagnostics)
|
|
56
|
+
store.addNotice(`terminal mouse input ignored (${diagnostic})`);
|
|
57
|
+
}
|
|
58
|
+
/** 单点消费真实 stdin:鼠标交给视口,其余字节按原顺序交给 Ink。 */
|
|
59
|
+
function routeRawTuiInput(chunk) {
|
|
60
|
+
const routed = inputRouter.push(String(chunk));
|
|
61
|
+
recordMouseDiagnostics(routed.diagnostics);
|
|
62
|
+
if (routed.passthrough.includes("\u001b[I"))
|
|
63
|
+
terminalLifecycle.reassert();
|
|
64
|
+
if (routed.passthrough)
|
|
65
|
+
routedStdin.write(routed.passthrough);
|
|
66
|
+
for (const event of routed.mouseEvents) {
|
|
67
|
+
if (event.kind !== "wheel_up" && event.kind !== "wheel_down") {
|
|
68
|
+
mouseInput.dispatch(event);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const delta = event.kind === "wheel_up" ? 1 : -1;
|
|
72
|
+
if (pendingWheelNotches !== 0 && Math.sign(pendingWheelNotches) !== delta)
|
|
73
|
+
flushMouseWheel();
|
|
74
|
+
pendingWheelNotches += delta;
|
|
75
|
+
pendingWheelEvent = event;
|
|
76
|
+
if (!mouseFlushTimer)
|
|
77
|
+
mouseFlushTimer = setTimeout(flushMouseWheel, 16);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 输入结束时丢弃残缺 SGR 前缀并关闭 Ink 专用输入流。 */
|
|
81
|
+
function finishRawTuiInput() {
|
|
82
|
+
recordMouseDiagnostics(inputRouter.finish().diagnostics);
|
|
83
|
+
flushMouseWheel();
|
|
84
|
+
routedStdin.end();
|
|
85
|
+
}
|
|
86
|
+
options.streams.stdin.on("data", routeRawTuiInput);
|
|
87
|
+
options.streams.stdin.once("end", finishRawTuiInput);
|
|
88
|
+
function finish(code) {
|
|
89
|
+
requestedExitCode = requestedExitCode ?? code;
|
|
90
|
+
instance?.unmount();
|
|
91
|
+
}
|
|
92
|
+
/** 在 TUI 终端恢复后尽力输出统一安全摘要;输出失败不能掩盖原本的退出或异常。 */
|
|
93
|
+
function writeTuiRecap() {
|
|
94
|
+
if (!options.sessionId)
|
|
95
|
+
return;
|
|
96
|
+
const snapshot = store.getSnapshot();
|
|
97
|
+
const completedTurnCount = snapshot.transcript.filter(function countCompletedTurn(item) {
|
|
98
|
+
return item.kind === "turn" && item.status === "completed";
|
|
99
|
+
}).length;
|
|
100
|
+
try {
|
|
101
|
+
options.streams.stdout.write(`\n${formatCliTuiRecap({
|
|
102
|
+
sessionId: options.sessionId,
|
|
103
|
+
completedTurnCount,
|
|
104
|
+
finalStatus: snapshot.status,
|
|
105
|
+
recoverable: snapshot.recoverable,
|
|
106
|
+
})}`);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// 终端已断开时只保证先完成 restore,recap 为尽力输出。
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function handleResult(result) {
|
|
113
|
+
lastResult = result;
|
|
114
|
+
store.syncProjection(result.projection);
|
|
115
|
+
// 专用视图结果由 app 层打开覆盖视图;禁止复用 notice 路径把报告行截成零散 info。
|
|
116
|
+
if (result.skillOptions || result.permissionOptions || result.modelOptions || result.effortOptions || result.jobView || result.contextView)
|
|
117
|
+
return;
|
|
118
|
+
const lines = result.lines.filter(function keepNotice(line) {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
// queue 的投递/启动由 pending prompt 和新 Turn 时间线表达,不再重复显示为两行 info。
|
|
121
|
+
return trimmed.length > 0 && !trimmed.startsWith("input queued:") && !trimmed.startsWith("queued input started:");
|
|
122
|
+
});
|
|
123
|
+
for (const line of lines)
|
|
124
|
+
store.addNotice(line);
|
|
125
|
+
if (lines.length === 0 && result.message)
|
|
126
|
+
store.addNotice(result.message);
|
|
127
|
+
}
|
|
128
|
+
/** 向 Runtime 请求当前活动 Turn 取消,但保留 Ink TUI 等待终态并回到 prompt。 */
|
|
129
|
+
function requestCancellation() {
|
|
130
|
+
const current = store.getSnapshot();
|
|
131
|
+
if (!isActiveTuiStatus(current.status) || current.cancellationRequested)
|
|
132
|
+
return;
|
|
133
|
+
store.setCancellationRequested(true);
|
|
134
|
+
void options.controller.dispatch({ type: "cancel_turn" }).then(handleResult).catch(function handleCancelFailure() {
|
|
135
|
+
store.setCancellationRequested(false);
|
|
136
|
+
store.addNotice("turn cancellation failed");
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** 处理 TUI 内 Ctrl+C:首次取消当前 Turn,取消收口前第二次强制退出。 */
|
|
140
|
+
function requestInterrupt() {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const current = store.getSnapshot();
|
|
143
|
+
if (isActiveTuiStatus(current.status)) {
|
|
144
|
+
if (current.cancellationRequested) {
|
|
145
|
+
if (now - lastInterruptAt < 100)
|
|
146
|
+
return;
|
|
147
|
+
options.streams.stderr.write("forced exit before cancellation completed; recovery status unknown\n");
|
|
148
|
+
finish(130);
|
|
149
|
+
terminalLifecycle.restore();
|
|
150
|
+
options.forceExit?.(130);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
lastInterruptAt = now;
|
|
154
|
+
requestCancellation();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
finish(130);
|
|
158
|
+
}
|
|
159
|
+
/** 处理 Esc:只取消活动 Turn,不把 Esc 解释为退出进程。 */
|
|
160
|
+
function requestCancel() {
|
|
161
|
+
requestCancellation();
|
|
162
|
+
}
|
|
163
|
+
/** 处理宿主 SIGINT/SIGTERM:取消后以相应进程退出码收口。 */
|
|
164
|
+
function requestSignalStop(code) {
|
|
165
|
+
requestedExitCode = requestedExitCode ?? code;
|
|
166
|
+
store.requestExit(code);
|
|
167
|
+
if (isActiveTuiStatus(store.getSnapshot().status)) {
|
|
168
|
+
requestCancellation();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
finish(code);
|
|
172
|
+
}
|
|
173
|
+
function handleEof() {
|
|
174
|
+
requestedExitCode = requestedExitCode ?? 0;
|
|
175
|
+
store.requestExit(0);
|
|
176
|
+
const current = store.getSnapshot();
|
|
177
|
+
if (current.status === "running" || current.status === "waiting_approval") {
|
|
178
|
+
void options.controller.dispatch({ type: "cancel_turn" }).then(handleResult).catch(function ignoreEofCancelFailure() {
|
|
179
|
+
finish(0);
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
finish(0);
|
|
184
|
+
}
|
|
185
|
+
function handleSigint() { requestInterrupt(); }
|
|
186
|
+
function handleSigterm() { requestSignalStop(143); }
|
|
187
|
+
options.signalSource.on("SIGINT", handleSigint);
|
|
188
|
+
options.signalSource.on("SIGTERM", handleSigterm);
|
|
189
|
+
const listener = function receiveEvent(event, projection) {
|
|
190
|
+
store.acceptEvent(event, projection);
|
|
191
|
+
if (event.type === "status" && isTerminalStatus(event.status) && requestedExitCode !== undefined)
|
|
192
|
+
finish(requestedExitCode);
|
|
193
|
+
};
|
|
194
|
+
options.controller.setEventListener?.(listener);
|
|
195
|
+
options.controller.setQueuedInputListener?.(function receiveQueuedInputStarted(input) {
|
|
196
|
+
store.markQueuedInputStarted(input);
|
|
197
|
+
});
|
|
198
|
+
const node = (_jsx(CliTuiApp, { store: store, controller: options.controller, initial: options.initial, closeWhenIdle: options.closeWhenIdle, workspaceRoot: options.workspaceRoot, useColor: !noColor && !screenReader, screenReader: screenReader, renderMode: renderMode, reducedMotion: reducedMotion, mouseInput: mouseInput, onResult: handleResult, onRequestInterrupt: requestInterrupt, onRequestCancel: requestCancel, onEof: handleEof, onAppExit: finish, terminalSizeSource: terminalSizeSource, onTerminalSizeChange: renderStdout.setSize }));
|
|
199
|
+
try {
|
|
200
|
+
terminalLifecycle.enter();
|
|
201
|
+
instance = render(node, {
|
|
202
|
+
stdout: renderStdout.stdout,
|
|
203
|
+
stderr: options.streams.stderr,
|
|
204
|
+
stdin: routedStdin,
|
|
205
|
+
exitOnCtrlC: false,
|
|
206
|
+
patchConsole: true,
|
|
207
|
+
// 输入框和动态时间线会改变行高;Windows Terminal 下增量渲染会残留旧帧,
|
|
208
|
+
// 表现为逐字符重复或内容重叠。使用完整帧保证当前交互区域正确。
|
|
209
|
+
incrementalRendering: false,
|
|
210
|
+
isScreenReaderEnabled: screenReader,
|
|
211
|
+
kittyKeyboard: { mode: kittyKeyboardMode },
|
|
212
|
+
});
|
|
213
|
+
// CC 在 AlternateScreen 挂载后再次声明鼠标模式;Ink 初始化完成后重申一次,避免真实终端只保留键盘模式。
|
|
214
|
+
terminalLifecycle.reassert();
|
|
215
|
+
await instance.waitUntilExit();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
if (mouseFlushTimer)
|
|
219
|
+
clearTimeout(mouseFlushTimer);
|
|
220
|
+
options.streams.stdin.removeListener("data", routeRawTuiInput);
|
|
221
|
+
options.streams.stdin.removeListener("end", finishRawTuiInput);
|
|
222
|
+
mouseInput.setHandler(undefined);
|
|
223
|
+
routedStdin.end();
|
|
224
|
+
terminalLifecycle.restore();
|
|
225
|
+
writeTuiRecap();
|
|
226
|
+
options.signalSource.removeListener("SIGINT", handleSigint);
|
|
227
|
+
options.signalSource.removeListener("SIGTERM", handleSigterm);
|
|
228
|
+
}
|
|
229
|
+
const projection = options.controller.getProjection();
|
|
230
|
+
const result = lastResult ?? {
|
|
231
|
+
kind: "exit",
|
|
232
|
+
exitCode: requestedExitCode ?? 0,
|
|
233
|
+
lines: [],
|
|
234
|
+
projection,
|
|
235
|
+
};
|
|
236
|
+
return { result, exitCode: requestedExitCode ?? result.exitCode };
|
|
237
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 状态栏与信息行:消费安全状态视图,渲染状态、模型、权限、context 与 workspace。 */
|
|
2
|
+
import type { CliStatusView } from "../types.js";
|
|
3
|
+
import type { CliTuiSnapshot } from "./types.js";
|
|
4
|
+
/** 渲染当前状态栏;配置行只消费安全状态视图,不读取 Runtime 或 checkpoint。 */
|
|
5
|
+
export declare const TuiStatusLine: import("react").NamedExoticComponent<{
|
|
6
|
+
snapshot: CliTuiSnapshot;
|
|
7
|
+
statusView?: CliStatusView;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
selectionCharacters?: number;
|
|
11
|
+
}>;
|
|
12
|
+
/** 将最近的控制台信息固定在输入框上方,避免混入会话时间线或占用回答区域。 */
|
|
13
|
+
export declare const TuiInfoLines: import("react").NamedExoticComponent<{
|
|
14
|
+
messages: string[];
|
|
15
|
+
useColor: boolean;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** 状态栏与信息行:消费安全状态视图,渲染状态、模型、权限、context 与 workspace。 */
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { formatTuiContextStatus } from "../context-report.js";
|
|
6
|
+
import { CLI_TUI_COLORS, TUI_ACCENT_COLOR } from "../design-tokens.js";
|
|
7
|
+
import { getTuiPermissionModeLabel, getTuiStatusLabel } from "./format.js";
|
|
8
|
+
/** 渲染当前状态栏;配置行只消费安全状态视图,不读取 Runtime 或 checkpoint。 */
|
|
9
|
+
export const TuiStatusLine = memo(function TuiStatusLine(props) {
|
|
10
|
+
const suffix = `${props.screenReader ? " · screen reader" : ""}${props.selectionCharacters ? ` · ${props.selectionCharacters} chars selected` : ""}`;
|
|
11
|
+
const runningHint = props.snapshot.status === "running" ? " · Esc interrupt · Enter 引导 · /queue <prompt> 排队" : "";
|
|
12
|
+
let statusLabel = getTuiStatusLabel(props.snapshot.status);
|
|
13
|
+
if (props.snapshot.cancellationRequested)
|
|
14
|
+
statusLabel = "Stopping · press Ctrl+C again to exit";
|
|
15
|
+
const statusColor = props.snapshot.status === "idle" ? undefined : props.useColor ? TUI_ACCENT_COLOR : undefined;
|
|
16
|
+
const view = props.statusView;
|
|
17
|
+
const nextPermissionLabel = view?.nextPermissionMode;
|
|
18
|
+
const permissionLabel = view ? getTuiPermissionModeLabel(view.permissionMode) : undefined;
|
|
19
|
+
const nextPermissionModeLabel = nextPermissionLabel ? getTuiPermissionModeLabel(nextPermissionLabel) : undefined;
|
|
20
|
+
if (!view) {
|
|
21
|
+
const hint = props.snapshot.status === "idle" ? " · /exit to quit" : runningHint;
|
|
22
|
+
return _jsxs(Text, { bold: props.snapshot.status === "waiting_approval", color: statusColor, children: [statusLabel, suffix, hint] });
|
|
23
|
+
}
|
|
24
|
+
const permissionColor = props.useColor && view.permissionMode === "full_access" ? CLI_TUI_COLORS.permissionFullAccess : undefined;
|
|
25
|
+
const nextPermissionColor = props.useColor && nextPermissionLabel === "full_access" ? CLI_TUI_COLORS.permissionFullAccess : undefined;
|
|
26
|
+
// 无数据/无 Session 统一显示 unknown,不猜测窗口或输出伪 0%。
|
|
27
|
+
const contextLabel = formatTuiContextStatus(view.contextUsage ?? null);
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: props.snapshot.status === "waiting_approval", color: statusColor, children: [statusLabel, suffix, runningHint] }), _jsxs(Text, { dimColor: props.useColor, children: ["model: ", view.model, " \u00B7 effort: ", view.effort, " \u00B7 ", _jsxs(Text, { color: permissionColor, bold: true, dimColor: false, children: ["permission: ", permissionLabel, " (", view.permissionMode, ")"] }), view.nextModel ? _jsxs(_Fragment, { children: [" \u00B7 next model: ", view.nextModel] }) : null, view.nextEffort ? _jsxs(_Fragment, { children: [" \u00B7 next effort: ", view.nextEffort] }) : null, nextPermissionLabel ? _jsxs(_Fragment, { children: [" \u00B7 next permission: ", _jsxs(Text, { color: nextPermissionColor, bold: true, dimColor: false, children: [nextPermissionModeLabel, " (", nextPermissionLabel, ")"] })] }) : null] }), _jsxs(Text, { dimColor: props.useColor, children: ["context: ", contextLabel] }), _jsxs(Text, { dimColor: props.useColor, children: ["workspace: ", view.workspaceRoot ?? "unavailable"] })] }));
|
|
29
|
+
});
|
|
30
|
+
/** 将最近的控制台信息固定在输入框上方,避免混入会话时间线或占用回答区域。 */
|
|
31
|
+
export const TuiInfoLines = memo(function TuiInfoLines(props) {
|
|
32
|
+
if (props.messages.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: props.messages.map(function renderInfo(message, index) {
|
|
35
|
+
return _jsxs(Text, { color: props.useColor ? CLI_TUI_COLORS.copilotMuted : undefined, children: ["info: ", message] }, `${index}:${message}`);
|
|
36
|
+
}) }));
|
|
37
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
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
|
+
/** 创建 TUI 初始快照。 */
|
|
7
|
+
function createInitialTuiSnapshot(now = Date.now()) {
|
|
8
|
+
return {
|
|
9
|
+
transcript: [],
|
|
10
|
+
pendingInputs: [],
|
|
11
|
+
liveEvents: [],
|
|
12
|
+
hasLiveApprovalEvent: false,
|
|
13
|
+
activity: createInitialCliActivityState(now),
|
|
14
|
+
status: "idle",
|
|
15
|
+
recoverable: false,
|
|
16
|
+
activeSkills: [],
|
|
17
|
+
contextUsage: undefined,
|
|
18
|
+
cancellationRequested: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** 创建 Ink TUI 的外部 Store。 */
|
|
22
|
+
export function createCliTuiStore() {
|
|
23
|
+
let snapshot = createInitialTuiSnapshot();
|
|
24
|
+
let noticeIndex = 0;
|
|
25
|
+
let promptIndex = 0;
|
|
26
|
+
let scheduledNotify;
|
|
27
|
+
const settledInputIds = new Set();
|
|
28
|
+
const submittedApprovalIds = new Set();
|
|
29
|
+
const listeners = new Set();
|
|
30
|
+
const eventsByTurn = new Map();
|
|
31
|
+
const presentationByTurn = new Map();
|
|
32
|
+
const metricsByTurn = new Map();
|
|
33
|
+
const finalizedTurns = new Set();
|
|
34
|
+
function publish(nextSnapshot) {
|
|
35
|
+
flushScheduledNotify();
|
|
36
|
+
snapshot = nextSnapshot;
|
|
37
|
+
notifyListeners();
|
|
38
|
+
}
|
|
39
|
+
function flushScheduledNotify() {
|
|
40
|
+
if (scheduledNotify === undefined)
|
|
41
|
+
return;
|
|
42
|
+
clearTimeout(scheduledNotify);
|
|
43
|
+
scheduledNotify = undefined;
|
|
44
|
+
}
|
|
45
|
+
function scheduleNotify() {
|
|
46
|
+
if (scheduledNotify !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
scheduledNotify = setTimeout(function publishAnswerBatch() {
|
|
49
|
+
scheduledNotify = undefined;
|
|
50
|
+
notifyListeners();
|
|
51
|
+
}, TUI_EVENT_PUBLISH_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
function notifyListeners() {
|
|
54
|
+
const startedAt = performance.now();
|
|
55
|
+
for (const listener of listeners)
|
|
56
|
+
listener();
|
|
57
|
+
recordTuiPerformanceDiagnostic("tui_store_published", "snapshot", performance.now() - startedAt);
|
|
58
|
+
}
|
|
59
|
+
/** 追加一条不会再被等待态原地修改的普通 prompt 历史。 */
|
|
60
|
+
function appendPrompt(content) {
|
|
61
|
+
const trimmed = content.trim();
|
|
62
|
+
if (!trimmed)
|
|
63
|
+
return;
|
|
64
|
+
publish({
|
|
65
|
+
...snapshot,
|
|
66
|
+
transcript: [
|
|
67
|
+
...snapshot.transcript,
|
|
68
|
+
{ kind: "prompt", key: "prompt:" + String(promptIndex++), content: trimmed },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function finalizeTurn(turnId, status) {
|
|
73
|
+
if (finalizedTurns.has(turnId))
|
|
74
|
+
return;
|
|
75
|
+
const events = eventsByTurn.get(turnId) ?? [];
|
|
76
|
+
if (events.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
finalizedTurns.add(turnId);
|
|
79
|
+
const metrics = metricsByTurn.get(turnId);
|
|
80
|
+
const thoughtDurationMs = metrics?.firstAnswerAt === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: Math.max(0, metrics.firstAnswerAt - metrics.startedAt);
|
|
83
|
+
const totalDurationMs = metrics?.completedAt === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: Math.max(0, metrics.completedAt - metrics.startedAt);
|
|
86
|
+
// 终态先保留当前 live Turn,让这一帧同时接住最后的 answer/status block;
|
|
87
|
+
// 立即清空会让外层历史区过滤掉同一 Turn,导致最后一段回答不可见。
|
|
88
|
+
snapshot = {
|
|
89
|
+
...snapshot,
|
|
90
|
+
transcript: [
|
|
91
|
+
...snapshot.transcript,
|
|
92
|
+
{
|
|
93
|
+
kind: "turn",
|
|
94
|
+
key: "turn:" + turnId + ":" + String(events[events.length - 1]?.seq ?? 0),
|
|
95
|
+
turnId,
|
|
96
|
+
events: [...events],
|
|
97
|
+
presentation: presentationByTurn.get(turnId),
|
|
98
|
+
status,
|
|
99
|
+
thoughtDurationMs,
|
|
100
|
+
totalDurationMs,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
getSnapshot() {
|
|
107
|
+
return snapshot;
|
|
108
|
+
},
|
|
109
|
+
subscribe(listener) {
|
|
110
|
+
listeners.add(listener);
|
|
111
|
+
return function unsubscribe() {
|
|
112
|
+
listeners.delete(listener);
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
recordPrompt(content, displayAsActive = true) {
|
|
116
|
+
const trimmed = content.trim();
|
|
117
|
+
if (!trimmed)
|
|
118
|
+
return undefined;
|
|
119
|
+
// 运行中的 steer/queue 先进入 pendingInputs;真正消费时由时间线事件放回新位置。
|
|
120
|
+
if (!displayAsActive)
|
|
121
|
+
return undefined;
|
|
122
|
+
const key = "prompt:" + String(promptIndex++);
|
|
123
|
+
publish({
|
|
124
|
+
...snapshot,
|
|
125
|
+
// info 是下一次 Turn 之前的临时反馈;新 prompt 被接收后必须立即消费,不能留在新 Turn 的输入区上方。
|
|
126
|
+
transcript: [
|
|
127
|
+
...snapshot.transcript.filter(function omitConsumedNotice(item) { return item.kind !== "notice"; }),
|
|
128
|
+
{ kind: "prompt", key, content: trimmed },
|
|
129
|
+
],
|
|
130
|
+
activePrompt: displayAsActive ? { key, content: trimmed } : snapshot.activePrompt,
|
|
131
|
+
});
|
|
132
|
+
return key;
|
|
133
|
+
},
|
|
134
|
+
/** 将已由 Controller 成功投递的输入放入动态等待区;若已消费则直接固化为普通历史。 */
|
|
135
|
+
addPendingPrompt(content, input) {
|
|
136
|
+
const trimmed = content.trim();
|
|
137
|
+
if (!trimmed)
|
|
138
|
+
return;
|
|
139
|
+
if (settledInputIds.has(input.inputId)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
publish({
|
|
143
|
+
...snapshot,
|
|
144
|
+
pendingInputs: [
|
|
145
|
+
...snapshot.pendingInputs,
|
|
146
|
+
{
|
|
147
|
+
key: "pending:" + String(promptIndex++),
|
|
148
|
+
content: trimmed,
|
|
149
|
+
inputId: input.inputId,
|
|
150
|
+
delivery: input.delivery,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
/** 在自动 queue Turn 开始前,将 prompt 放进目标 Turn 的事件序列并清除等待态。 */
|
|
156
|
+
markQueuedInputStarted(input) {
|
|
157
|
+
if (settledInputIds.has(input.inputId))
|
|
158
|
+
return;
|
|
159
|
+
settledInputIds.add(input.inputId);
|
|
160
|
+
const currentEvents = eventsByTurn.get(input.turnId) ?? [];
|
|
161
|
+
if (currentEvents.some(function hasConsumedPrompt(event) {
|
|
162
|
+
return event.type === "tui_input_consumed" && event.inputId === input.inputId;
|
|
163
|
+
}))
|
|
164
|
+
return;
|
|
165
|
+
const nextEvents = [
|
|
166
|
+
...currentEvents,
|
|
167
|
+
{
|
|
168
|
+
type: "tui_input_consumed",
|
|
169
|
+
seq: 0,
|
|
170
|
+
turnId: input.turnId,
|
|
171
|
+
inputId: input.inputId,
|
|
172
|
+
content: input.content,
|
|
173
|
+
delivery: input.delivery,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
eventsByTurn.set(input.turnId, nextEvents);
|
|
177
|
+
publish({
|
|
178
|
+
...snapshot,
|
|
179
|
+
pendingInputs: snapshot.pendingInputs.filter(function keepUnstartedInput(item) { return item.inputId !== input.inputId; }),
|
|
180
|
+
liveTurnId: snapshot.liveTurnId === input.turnId ? input.turnId : snapshot.liveTurnId,
|
|
181
|
+
liveEvents: snapshot.liveTurnId === input.turnId ? nextEvents : snapshot.liveEvents,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
/** 用户选择审批范围后立即将等待卡收束为已提交结果;Runtime 后续事件仍是最终执行依据。 */
|
|
185
|
+
recordApprovalSubmission(decision) {
|
|
186
|
+
const approval = snapshot.pendingApproval;
|
|
187
|
+
const turnId = snapshot.liveTurnId;
|
|
188
|
+
if (!approval || !turnId)
|
|
189
|
+
return;
|
|
190
|
+
submittedApprovalIds.add(approval.approvalId);
|
|
191
|
+
const currentEvents = eventsByTurn.get(turnId) ?? [];
|
|
192
|
+
if (currentEvents.some(function hasSubmission(event) {
|
|
193
|
+
return event.type === "tui_approval_submitted" && event.approvalId === approval.approvalId;
|
|
194
|
+
}))
|
|
195
|
+
return;
|
|
196
|
+
const nextEvents = [
|
|
197
|
+
...currentEvents,
|
|
198
|
+
{ type: "tui_approval_submitted", seq: Number.MAX_SAFE_INTEGER, turnId, approvalId: approval.approvalId, decision },
|
|
199
|
+
];
|
|
200
|
+
eventsByTurn.set(turnId, nextEvents);
|
|
201
|
+
publish({ ...snapshot, liveEvents: snapshot.liveTurnId === turnId ? nextEvents : snapshot.liveEvents, pendingApproval: undefined });
|
|
202
|
+
},
|
|
203
|
+
addNotice(message) {
|
|
204
|
+
const trimmed = message.trim();
|
|
205
|
+
if (!trimmed)
|
|
206
|
+
return;
|
|
207
|
+
publish({
|
|
208
|
+
...snapshot,
|
|
209
|
+
transcript: [
|
|
210
|
+
...snapshot.transcript,
|
|
211
|
+
{ kind: "notice", key: "notice:" + String(noticeIndex++), message: trimmed },
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
acceptEvent(event, projection) {
|
|
216
|
+
// 用量变化只刷新动态状态栏事实,不进入 transcript 时间线或 scrollback。
|
|
217
|
+
if (event.type === "context_usage") {
|
|
218
|
+
if (snapshot.contextUsage === event.usage)
|
|
219
|
+
return;
|
|
220
|
+
publish({ ...snapshot, contextUsage: event.usage });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Esc 后只等待 Runtime 的终态;取消请求之后已经在途的回答/工具尾部事件不再进入可见时间线。
|
|
224
|
+
if (snapshot.cancellationRequested && !(event.type === "status" && isTerminalStatus(event.status)))
|
|
225
|
+
return;
|
|
226
|
+
const currentEvents = eventsByTurn.get(event.turnId) ?? [];
|
|
227
|
+
if (currentEvents.some(function hasSequence(item) { return item.type === event.type && item.seq === event.seq; }))
|
|
228
|
+
return;
|
|
229
|
+
const nextEvents = [...currentEvents, event];
|
|
230
|
+
eventsByTurn.set(event.turnId, nextEvents);
|
|
231
|
+
presentationByTurn.set(event.turnId, projection.presentation);
|
|
232
|
+
const isNewTurn = snapshot.liveTurnId !== event.turnId;
|
|
233
|
+
const activePrompt = snapshot.activePrompt && snapshot.activePrompt.turnId === undefined
|
|
234
|
+
? { ...snapshot.activePrompt, turnId: event.turnId }
|
|
235
|
+
: snapshot.activePrompt;
|
|
236
|
+
const eventTime = Date.now();
|
|
237
|
+
const previousMetrics = metricsByTurn.get(event.turnId);
|
|
238
|
+
const nextMetrics = {
|
|
239
|
+
startedAt: event.type === "turn_started" ? eventTime : previousMetrics?.startedAt ?? eventTime,
|
|
240
|
+
firstAnswerAt: previousMetrics?.firstAnswerAt,
|
|
241
|
+
completedAt: previousMetrics?.completedAt,
|
|
242
|
+
};
|
|
243
|
+
if (event.type === "answer_delta" && nextMetrics.firstAnswerAt === undefined)
|
|
244
|
+
nextMetrics.firstAnswerAt = eventTime;
|
|
245
|
+
// Core 的 reset/discard 表示当前采样的暂定文字不能展示;下一次真正回答重新计时。
|
|
246
|
+
if (event.type === "answer_reset")
|
|
247
|
+
nextMetrics.firstAnswerAt = undefined;
|
|
248
|
+
if (event.type === "status" && isTerminalStatus(event.status))
|
|
249
|
+
nextMetrics.completedAt = eventTime;
|
|
250
|
+
metricsByTurn.set(event.turnId, nextMetrics);
|
|
251
|
+
const nextActivity = settleCliActivityState(reduceCliActivityState(snapshot.activity, event, eventTime), projection.status, eventTime);
|
|
252
|
+
const nextSnapshot = {
|
|
253
|
+
...snapshot,
|
|
254
|
+
activePrompt,
|
|
255
|
+
liveTurnId: event.turnId,
|
|
256
|
+
liveEvents: nextEvents,
|
|
257
|
+
livePresentation: projection.presentation,
|
|
258
|
+
liveMetrics: { ...nextMetrics },
|
|
259
|
+
hasLiveApprovalEvent: isNewTurn ? event.type === "approval_required" : snapshot.hasLiveApprovalEvent || event.type === "approval_required",
|
|
260
|
+
activity: nextActivity,
|
|
261
|
+
status: projection.status,
|
|
262
|
+
recoverable: projection.recoverable,
|
|
263
|
+
pendingApproval: projection.pendingApproval && !submittedApprovalIds.has(projection.pendingApproval.approvalId)
|
|
264
|
+
? { ...projection.pendingApproval }
|
|
265
|
+
: undefined,
|
|
266
|
+
activeSkills: projection.activeSkills.map(function copySkill(skill) { return { ...skill }; }),
|
|
267
|
+
cancellationRequested: event.type === "status" && isTerminalStatus(event.status) ? false : snapshot.cancellationRequested,
|
|
268
|
+
};
|
|
269
|
+
if (event.type === "input_consumed") {
|
|
270
|
+
settledInputIds.add(event.inputId);
|
|
271
|
+
const pendingInput = nextSnapshot.pendingInputs.find(function findPendingInput(item) { return item.inputId === event.inputId; });
|
|
272
|
+
if (pendingInput) {
|
|
273
|
+
nextSnapshot.pendingInputs = nextSnapshot.pendingInputs.filter(function keepPendingInput(item) { return item.inputId !== event.inputId; });
|
|
274
|
+
nextEvents.push({
|
|
275
|
+
type: "tui_input_consumed",
|
|
276
|
+
seq: event.seq,
|
|
277
|
+
turnId: event.turnId,
|
|
278
|
+
inputId: pendingInput.inputId,
|
|
279
|
+
content: pendingInput.content,
|
|
280
|
+
delivery: pendingInput.delivery,
|
|
281
|
+
});
|
|
282
|
+
nextSnapshot.liveEvents = nextEvents;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
snapshot = nextSnapshot;
|
|
286
|
+
if (event.type === "status" && isTerminalStatus(event.status)) {
|
|
287
|
+
finalizeTurn(event.turnId, event.status);
|
|
288
|
+
}
|
|
289
|
+
if (event.type === "answer_delta") {
|
|
290
|
+
scheduleNotify();
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
flushScheduledNotify();
|
|
294
|
+
notifyListeners();
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
syncProjection(projection) {
|
|
298
|
+
const turnIsFinalized = projection.turnId ? finalizedTurns.has(projection.turnId) : false;
|
|
299
|
+
const isNewTurn = projection.turnId !== undefined && projection.turnId !== snapshot.liveTurnId;
|
|
300
|
+
const activePrompt = snapshot.activePrompt && snapshot.activePrompt.turnId === undefined && projection.turnId
|
|
301
|
+
? { ...snapshot.activePrompt, turnId: projection.turnId }
|
|
302
|
+
: snapshot.activePrompt;
|
|
303
|
+
const keepFinalizedLiveTurn = Boolean(projection.turnId && turnIsFinalized && projection.turnId === snapshot.liveTurnId);
|
|
304
|
+
const keepLiveTurn = Boolean(projection.turnId && (!turnIsFinalized || keepFinalizedLiveTurn));
|
|
305
|
+
if (projection.turnId)
|
|
306
|
+
presentationByTurn.set(projection.turnId, projection.presentation);
|
|
307
|
+
let activity = isNewTurn ? createInitialCliActivityState(Date.now()) : snapshot.activity;
|
|
308
|
+
if (projection.pendingApproval || projection.status === "waiting_approval") {
|
|
309
|
+
activity = reduceCliActivityState(activity, { type: "approval_required" }, Date.now());
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
activity = settleCliActivityState(activity, projection.status, Date.now());
|
|
313
|
+
}
|
|
314
|
+
snapshot = {
|
|
315
|
+
...snapshot,
|
|
316
|
+
activePrompt,
|
|
317
|
+
liveTurnId: keepLiveTurn ? projection.turnId : undefined,
|
|
318
|
+
liveEvents: keepLiveTurn ? [...(eventsByTurn.get(projection.turnId) ?? [])] : [],
|
|
319
|
+
livePresentation: keepLiveTurn ? projection.presentation : undefined,
|
|
320
|
+
hasLiveApprovalEvent: Boolean(keepLiveTurn && !isNewTurn && snapshot.hasLiveApprovalEvent),
|
|
321
|
+
activity,
|
|
322
|
+
status: projection.status,
|
|
323
|
+
recoverable: projection.recoverable,
|
|
324
|
+
pendingApproval: projection.pendingApproval && !submittedApprovalIds.has(projection.pendingApproval.approvalId)
|
|
325
|
+
? { ...projection.pendingApproval }
|
|
326
|
+
: undefined,
|
|
327
|
+
activeSkills: projection.activeSkills.map(function copySkill(skill) { return { ...skill }; }),
|
|
328
|
+
cancellationRequested: isTerminalStatus(projection.status) ? false : snapshot.cancellationRequested,
|
|
329
|
+
};
|
|
330
|
+
if (projection.turnId && isTerminalStatus(projection.status)) {
|
|
331
|
+
finalizeTurn(projection.turnId, projection.status);
|
|
332
|
+
}
|
|
333
|
+
notifyListeners();
|
|
334
|
+
},
|
|
335
|
+
setCancellationRequested(requested) {
|
|
336
|
+
if (snapshot.cancellationRequested === requested)
|
|
337
|
+
return;
|
|
338
|
+
publish({ ...snapshot, cancellationRequested: requested });
|
|
339
|
+
},
|
|
340
|
+
requestExit(code) {
|
|
341
|
+
publish({ ...snapshot, requestedExitCode: code });
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
@@ -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
|
+
}>;
|