@autopilot-agent/cli 0.1.0 → 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.
@@ -2,13 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { TUI_ACCENT_COLOR } from "../design-tokens.js";
4
4
  import { getTuiToolLabel } from "./format.js";
5
- /** 根据待审批工具收窄菜单;ask 下命令不提供可持久化授权。 */
5
+ /** 根据待审批工具收窄菜单;旧记录缺少冻结范围时保持最保守的单次/拒绝。 */
6
6
  export function createTuiApprovalOptions(pendingApproval) {
7
7
  if (pendingApproval.allowedDecisions) {
8
+ // run_command 的工作区级授权展示为"记住此命令"(精确命令约束,与 Web 审批卡同一文案);
9
+ // todo_write 的单次批准即任务文件集合批准,与 Web 审批卡同一文案。
8
10
  const labels = {
9
- allow_once: "允许本次",
11
+ allow_once: pendingApproval.toolName === "todo_write"
12
+ ? pendingApproval.constraint === "task-ext-files:v1" ? "批准本任务外部文件修改" : "批准本任务文件修改"
13
+ : "允许本次",
10
14
  allow_session: "本次会话允许",
11
- allow_workspace: "此工作区允许",
15
+ allow_workspace: pendingApproval.toolName === "run_command" ? "记住此命令" : "此工作区允许",
12
16
  deny: "拒绝",
13
17
  };
14
18
  return pendingApproval.allowedDecisions.map(function createFrozenOption(decision) { return { decision, label: labels[decision] }; });
@@ -30,7 +34,7 @@ export function createTuiApprovalOptions(pendingApproval) {
30
34
  export function TuiApprovalPrompt(props) {
31
35
  const options = createTuiApprovalOptions(props.pendingApproval);
32
36
  const selectedIndex = Math.max(0, Math.min(props.selectedIndex, options.length - 1));
33
- return (_jsxs(Box, { borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u9700\u8981\u4F60\u7684\u786E\u8BA4" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", props.pendingApproval.display?.operation ?? getTuiToolLabel(props.pendingApproval.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", props.pendingApproval.reason] }), props.pendingApproval.display?.resource ? _jsxs(Text, { children: ["\u5F71\u54CD\uFF1A", props.pendingApproval.display.resource] }) : null, options.map(function renderApprovalOption(option, index) {
37
+ return (_jsxs(Box, { borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u9700\u8981\u4F60\u7684\u786E\u8BA4" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", props.pendingApproval.display?.operation ?? getTuiToolLabel(props.pendingApproval.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", props.pendingApproval.reason] }), props.pendingApproval.display?.resource ? _jsxs(Text, { children: ["\u5F71\u54CD\uFF1A", props.pendingApproval.display.resource] }) : null, props.pendingApproval.display?.cwd ? _jsxs(Text, { children: ["\u5DE5\u4F5C\u76EE\u5F55\uFF1A", props.pendingApproval.display.cwd] }) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution === true ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u547D\u4EE4\u58F0\u660E\u4E86\u5DE5\u4F5C\u533A\u5916\u76EE\u6807\uFF0C\u4F46\u5BBF\u4E3B\u8FDB\u7A0B\u4E0D\u53D7\u8BE5\u6587\u4EF6\u8303\u56F4\u7EA6\u675F\uFF0C\u53EF\u80FD\u540C\u65F6\u5F71\u54CD\u5176\u4ED6\u6587\u4EF6\uFF1B\u6279\u51C6\u4E0D\u6784\u6210\u7CBE\u786E\u8303\u56F4\u4FDD\u8BC1" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution !== true && props.pendingApproval.display?.unknownScope ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u4FEE\u6539\u8303\u56F4\u65E0\u6CD5\u7CBE\u786E\u9650\u5B9A\uFF1A\u8BE5\u547D\u4EE4\u672A\u58F0\u660E\u7CBE\u786E\u5916\u90E8\u76EE\u6807\uFF0C\u8BF7\u5BA1\u67E5\u5B8C\u6574\u547D\u4EE4\u540E\u518D\u51B3\u5B9A" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution !== true && !props.pendingApproval.display?.unknownScope ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u76EE\u6807\u4F4D\u4E8E\u5DE5\u4F5C\u533A\u4E4B\u5916\uFF0C\u6388\u6743\u4EC5\u8986\u76D6\u4E0A\u8FF0\u7CBE\u786E\u76EE\u6807" })) : null, props.pendingApproval.display?.hostExecution === true ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Windows \u5BBF\u4E3B\u6267\u884C\uFF1A\u672C\u6B21\u547D\u4EE4\u5C06\u5728\u5F53\u524D\u7528\u6237\u6743\u9650\u4E0B\u76F4\u63A5\u8FD0\u884C\uFF0C\u672A\u542F\u7528\u6C99\u7BB1\u9694\u79BB\uFF1B\u6279\u51C6\u4EC5\u5BF9\u672C\u6B21\u8C03\u7528\u6709\u6548\uFF0C\u62D2\u7EDD\u5219\u4E0D\u6267\u884C" })) : null, props.pendingApproval.toolName === "run_command" && options.some(function hasWorkspaceOption(option) { return option.decision === "allow_workspace"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u9009\u62E9\u201C\u8BB0\u4F4F\u6B64\u547D\u4EE4\u201D\u4F1A\u8BB0\u4F4F\u672C\u6761\u547D\u4EE4\uFF08\u7A0B\u5E8F\u3001\u53C2\u6570\u4E0E\u5DE5\u4F5C\u76EE\u5F55\u5B8C\u5168\u4E00\u81F4\uFF09\uFF0C\u6B64\u540E\u5728\u672C\u5DE5\u4F5C\u533A\u81EA\u52A8\u653E\u884C\uFF1B\u53C2\u6570\u6216\u76EE\u5F55\u4E0D\u540C\u7684\u547D\u4EE4\u4ECD\u4F1A\u91CD\u65B0\u8BE2\u95EE" })) : null, props.pendingApproval.constraint === "task-ext-files:v1" ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u76EE\u6807\u4F4D\u4E8E\u5DE5\u4F5C\u533A\u4E4B\u5916\uFF0C\u4E0E\u5DE5\u4F5C\u533A\u6587\u4EF6\u96C6\u5408\u5206\u5F00\u5355\u72EC\u6279\u51C6\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u6E05\u5355\u5185\u7CBE\u786E\u6587\u4EF6\uFF0C\u4E0D\u542B\u7236\u76EE\u5F55\u6216\u9012\u5F52\u6743\u9650" })) : null, props.pendingApproval.toolName === "todo_write" && props.pendingApproval.constraint !== "task-ext-files:v1" && options.some(function hasOnceOption(option) { return option.decision === "allow_once"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u6279\u51C6\u540E\uFF0C\u672C\u4EFB\u52A1\u5185\u5BF9\u6E05\u5355\u5185\u6587\u4EF6\u7684\u5BF9\u5E94\u4FEE\u6539\u5C06\u81EA\u52A8\u653E\u884C\uFF1B\u6E05\u5355\u5916\u7684\u65B0\u6587\u4EF6\u4ECD\u4F1A\u9010\u6B21\u8BE2\u95EE\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u5F53\u524D\u4EFB\u52A1" })) : null, props.pendingApproval.constraint === "task-ext-files:v1" && options.some(function hasOnceOption(option) { return option.decision === "allow_once"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u6279\u51C6\u540E\uFF0C\u672C\u4EFB\u52A1\u5185\u5BF9\u6E05\u5355\u5185\u7CBE\u786E\u6587\u4EF6\u7684\u5BF9\u5E94\u4FEE\u6539\u5C06\u81EA\u52A8\u653E\u884C\uFF1B\u6E05\u5355\u5916\u76EE\u6807\u4ECD\u9010\u6B21\u8BE2\u95EE\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u5F53\u524D\u4EFB\u52A1\uFF0C\u65B0\u76EE\u5F55\u6216\u65B0\u6587\u4EF6\u4E0D\u4F1A\u56E0\u6B64\u83B7\u5F97\u6388\u6743" })) : null, props.pendingApproval.constraint === "no-sandbox-exec:v1" ? (_jsx(Text, { color: props.useColor ? "yellow" : undefined, children: "\u6C99\u7BB1\u542F\u52A8\u5931\u8D25\uFF1A\u672C\u6B21\u5C06\u65E0\u6C99\u7BB1\u9694\u79BB\u76F4\u63A5\u6267\u884C\u4E0A\u8FF0\u547D\u4EE4\uFF1B\u6279\u51C6\u4EC5\u5BF9\u672C\u6B21\u8C03\u7528\u6709\u6548\uFF0C\u62D2\u7EDD\u5219\u4EFB\u52A1\u505C\u6B62" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.toolName !== "run_command" && options.some(function hasWorkspaceOption(option) { return option.decision === "allow_workspace"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u9009\u62E9\u201C\u6B64\u5DE5\u4F5C\u533A\u5141\u8BB8\u201D\u5C06\u628A\u8BE5\u89C4\u5219\u5199\u5165\u5DE5\u4F5C\u533A .autopilot/settings.local.json" })) : null, options.map(function renderApprovalOption(option, index) {
34
38
  const selected = index === selectedIndex;
35
39
  return _jsxs(Text, { inverse: selected && props.useColor, children: [selected ? "›" : " ", " ", option.label, selected && props.screenReader ? "(已选择)" : ""] }, option.decision);
36
40
  }), _jsx(Text, { dimColor: props.useColor, children: "\u2191/\u2193 \u9009\u62E9 \u00B7 Enter \u786E\u8BA4" })] }));
@@ -3,7 +3,7 @@
3
3
  * 代理展示名。颜色语义 token 集中在 design-tokens.ts,这里不重复定义。
4
4
  */
5
5
  /** Autopilot 品牌版本号。 */
6
- export declare const AUTOPILOT_VERSION = "0.1.0";
6
+ export declare const AUTOPILOT_VERSION = "0.2.0";
7
7
  /** 品牌像素字母的完整单词。 */
8
8
  export declare const AUTOPILOT_BRAND_WORD = "AUTOPILOT";
9
9
  /** 开场动画总时长(毫秒)。 */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import stringWidth from "string-width";
6
6
  /** Autopilot 品牌版本号。 */
7
- export const AUTOPILOT_VERSION = "0.1.0";
7
+ export const AUTOPILOT_VERSION = "0.2.0";
8
8
  /** 品牌像素字母的完整单词。 */
9
9
  export const AUTOPILOT_BRAND_WORD = "AUTOPILOT";
10
10
  /** 开场动画总时长(毫秒)。 */
@@ -3,6 +3,7 @@
3
3
  * 已消费输入文案和终端尺寸读取。本文件不依赖任何 React 组件。
4
4
  */
5
5
  import { formatAgentTermination } from "../termination-format.js";
6
+ import { isSandboxUnavailableErrorCode } from "@autopilot-agent/presentation";
6
7
  /** 判断一个状态是否已经离开可继续执行的活动阶段;waiting_user_input 是可恢复等待,不算终态。 */
7
8
  export function isTerminalStatus(status) {
8
9
  return status !== "idle" && status !== "running" && status !== "waiting_approval" && status !== "waiting_user_input";
@@ -130,8 +131,9 @@ export function getTuiToolFailureMessage(status, errorCode, detail) {
130
131
  return "需要确认";
131
132
  if (status === "question_required")
132
133
  return "请在支持结构化回答的 Web 中处理";
134
+ // 2.5:沙箱不可用类拒绝是环境阻塞而非用户拒绝,文案必须区分。
133
135
  if (status === "denied")
134
- return "已拒绝";
136
+ return isSandboxUnavailableErrorCode(errorCode) ? "沙箱不可用,命令未启动" : "已拒绝";
135
137
  if (status === "cancelled")
136
138
  return "已取消";
137
139
  if (status === "timeout")
@@ -153,7 +155,7 @@ export function getTuiToolFailureMessage(status, errorCode, detail) {
153
155
  /** 将已由 Controller 成功投递的输入统一为可读的插入提示。 */
154
156
  export function formatTuiInputConsumed(event) {
155
157
  const label = event.delivery === "queue" ? "[排队]" : "[引导]";
156
- return `${label} you: ${event.content} · 已插入当前 turn`;
158
+ return `${label} 已插入当前 turn`;
157
159
  }
158
160
  /** 将 Core 权限模式转换为状态栏可读的中文标签。 */
159
161
  export function getTuiPermissionModeLabel(mode) {
package/dist/tui/store.js CHANGED
@@ -3,6 +3,7 @@ import { createInitialCliActivityState, reduceCliActivityState, settleCliActivit
3
3
  import { TUI_EVENT_PUBLISH_INTERVAL_MS } from "./constants.js";
4
4
  import { isTerminalStatus } from "./format.js";
5
5
  import { recordTuiPerformanceDiagnostic } from "./performance-diagnostics.js";
6
+ import { captureTuiInputPosition } from "./timeline/turn-items.js";
6
7
  /** 创建 TUI 初始快照。 */
7
8
  function createInitialTuiSnapshot(now = Date.now()) {
8
9
  return {
@@ -25,6 +26,10 @@ export function createCliTuiStore() {
25
26
  let promptIndex = 0;
26
27
  let scheduledNotify;
27
28
  const settledInputIds = new Set();
29
+ const consumedPositions = new Map();
30
+ const cancelledTurns = new Set();
31
+ let submissionOrder = 0;
32
+ const submittedInputIds = new Set();
28
33
  const submittedApprovalIds = new Set();
29
34
  const listeners = new Set();
30
35
  const eventsByTurn = new Map();
@@ -56,18 +61,40 @@ export function createCliTuiStore() {
56
61
  listener();
57
62
  recordTuiPerformanceDiagnostic("tui_store_published", "snapshot", performance.now() - startedAt);
58
63
  }
59
- /** 追加一条不会再被等待态原地修改的普通 prompt 历史。 */
60
- function appendPrompt(content) {
61
- const trimmed = content.trim();
62
- if (!trimmed)
64
+ /** 更新所属轮的宿主输入事实;晚到回调同时刷新历史快照,不挪到当前另一轮。 */
65
+ function putInput(content, input, position, consumed) {
66
+ const turnId = position.turnId;
67
+ if (!turnId)
63
68
  return;
64
- publish({
65
- ...snapshot,
66
- transcript: [
67
- ...snapshot.transcript,
68
- { kind: "prompt", key: "prompt:" + String(promptIndex++), content: trimmed },
69
- ],
70
- });
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; }) };
71
98
  }
72
99
  function finalizeTurn(turnId, status) {
73
100
  if (finalizedTurns.has(turnId))
@@ -116,7 +143,7 @@ export function createCliTuiStore() {
116
143
  const trimmed = content.trim();
117
144
  if (!trimmed)
118
145
  return undefined;
119
- // 运行中的 steer/queue 先进入 pendingInputs;真正消费时由时间线事件放回新位置。
146
+ // 运行中的 steer/queue 由已确认的 inputId 单独记录,避免发送前伪造成功展示。
120
147
  if (!displayAsActive)
121
148
  return undefined;
122
149
  const key = "prompt:" + String(promptIndex++);
@@ -131,12 +158,20 @@ export function createCliTuiStore() {
131
158
  });
132
159
  return key;
133
160
  },
134
- /** 将已由 Controller 成功投递的输入放入动态等待区;若已消费则直接固化为普通历史。 */
135
- addPendingPrompt(content, input) {
136
- const trimmed = content.trim();
137
- if (!trimmed)
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))
138
166
  return;
139
- if (settledInputIds.has(input.inputId)) {
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);
140
175
  return;
141
176
  }
142
177
  publish({
@@ -145,7 +180,7 @@ export function createCliTuiStore() {
145
180
  ...snapshot.pendingInputs,
146
181
  {
147
182
  key: "pending:" + String(promptIndex++),
148
- content: trimmed,
183
+ content,
149
184
  inputId: input.inputId,
150
185
  delivery: input.delivery,
151
186
  },
@@ -157,28 +192,17 @@ export function createCliTuiStore() {
157
192
  if (settledInputIds.has(input.inputId))
158
193
  return;
159
194
  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);
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);
177
201
  publish({
178
202
  ...snapshot,
179
203
  pendingInputs: snapshot.pendingInputs.filter(function keepUnstartedInput(item) { return item.inputId !== input.inputId; }),
180
204
  liveTurnId: snapshot.liveTurnId === input.turnId ? input.turnId : snapshot.liveTurnId,
181
- liveEvents: snapshot.liveTurnId === input.turnId ? nextEvents : snapshot.liveEvents,
205
+ liveEvents: snapshot.liveEvents,
182
206
  });
183
207
  },
184
208
  /** 用户选择审批范围后立即将等待卡收束为已提交结果;Runtime 后续事件仍是最终执行依据。 */
@@ -268,18 +292,21 @@ export function createCliTuiStore() {
268
292
  };
269
293
  if (event.type === "input_consumed") {
270
294
  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;
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;
283
310
  }
284
311
  }
285
312
  snapshot = nextSnapshot;
@@ -295,6 +322,8 @@ export function createCliTuiStore() {
295
322
  }
296
323
  },
297
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; }));
298
327
  const turnIsFinalized = projection.turnId ? finalizedTurns.has(projection.turnId) : false;
299
328
  const isNewTurn = projection.turnId !== undefined && projection.turnId !== snapshot.liveTurnId;
300
329
  const activePrompt = snapshot.activePrompt && snapshot.activePrompt.turnId === undefined && projection.turnId
@@ -317,6 +346,7 @@ export function createCliTuiStore() {
317
346
  liveTurnId: keepLiveTurn ? projection.turnId : undefined,
318
347
  liveEvents: keepLiveTurn ? [...(eventsByTurn.get(projection.turnId) ?? [])] : [],
319
348
  livePresentation: keepLiveTurn ? projection.presentation : undefined,
349
+ liveMetrics: keepLiveTurn && projection.turnId ? metricsByTurn.get(projection.turnId) : undefined,
320
350
  hasLiveApprovalEvent: Boolean(keepLiveTurn && !isNewTurn && snapshot.hasLiveApprovalEvent),
321
351
  activity,
322
352
  status: projection.status,
@@ -335,6 +365,12 @@ export function createCliTuiStore() {
335
365
  setCancellationRequested(requested) {
336
366
  if (snapshot.cancellationRequested === requested)
337
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
+ }
338
374
  publish({ ...snapshot, cancellationRequested: requested });
339
375
  },
340
376
  requestExit(code) {
@@ -6,6 +6,7 @@ import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
6
6
  import { TUI_AGENT_LABEL } from "../constants.js";
7
7
  import { formatTuiDuration, formatTuiInputConsumed, getTuiApprovalDecisionLabel, isTerminalStatus } from "../format.js";
8
8
  import { TuiMarkdown } from "../markdown/markdown.js";
9
+ import { TuiPromptLine } from "../transcript.js";
9
10
  import { renderTuiToolTreeEvent } from "./tool-tree.js";
10
11
  /** 为增量时间线生成稳定键,避免 answer delta 到达时重建历史节点。 */
11
12
  function getCliTuiTimelineEventKey(event) {
@@ -17,6 +18,9 @@ function getCliTuiTimelineEventKey(event) {
17
18
  function isCliTuiTimelineAppendOnly(cache, events) {
18
19
  if (cache.eventCount === 0)
19
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;
20
24
  return events.length >= cache.eventCount && events[cache.eventCount - 1] === cache.lastEvent;
21
25
  }
22
26
  /** 应用一条时间线事件,只修改新增尾部或当前回答块。 */
@@ -50,7 +54,7 @@ function appendCliTuiTimelineEvent(cache, event) {
50
54
  }
51
55
  /** 首次或 Turn 切换时完整归约时间线;后续更新走尾部增量路径。 */
52
56
  function createCliTuiTimelineCache(events) {
53
- const cache = { eventCount: 0, blocks: [] };
57
+ const cache = { eventCount: 0, blocks: [], hostInputs: events.filter(function host(event) { return event.type === "tui_input_consumed"; }) };
54
58
  for (const event of events)
55
59
  appendCliTuiTimelineEvent(cache, event);
56
60
  cache.eventCount = events.length;
@@ -80,7 +84,7 @@ function useCliTuiTimelineBlocks(events) {
80
84
  function TuiLiveEventBlock(props) {
81
85
  const event = props.event;
82
86
  if (event.type === "tui_input_consumed") {
83
- return _jsx(Text, { dimColor: props.useColor, children: formatTuiInputConsumed(event) });
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) })] });
84
88
  }
85
89
  if (event.type === "tui_approval_submitted") {
86
90
  return _jsxs(Text, { dimColor: props.useColor, children: ["Approval submitted \u00B7 ", getTuiApprovalDecisionLabel(event.decision)] });
@@ -107,8 +111,6 @@ function TuiLiveEventBlock(props) {
107
111
  return _jsx(Text, { children: "Context organization failed" });
108
112
  if (event.type === "context_compact_circuit_open")
109
113
  return _jsx(Text, { children: "Context organization temporarily unavailable" });
110
- if (event.type === "status" && event.status === "failed" && event.reason)
111
- return _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] });
112
114
  if (event.type === "status")
113
115
  return null;
114
116
  return null;
@@ -132,7 +134,7 @@ function renderTuiLiveTimelineBlock(block, useColor, renderMarkdown, maximumWidt
132
134
  return null;
133
135
  if (block.event.type === "answer_delta")
134
136
  return null;
135
- return _jsx(MemoizedTuiLiveEventBlock, { event: block.event, useColor: useColor });
137
+ return _jsx(MemoizedTuiLiveEventBlock, { event: block.event, useColor: useColor, maximumWidth: maximumWidth });
136
138
  }
137
139
  /**
138
140
  * Ink 在动态区超过一屏后会清空终端并从顶部重绘;把已经确定的块交给
@@ -1,6 +1,7 @@
1
1
  /** 基于共享 Presentation selector 的 CLI 时间线;不读取旧工具 summary、detail 或审批 preview。 */
2
2
  import { type AgentPresentationState } from "@autopilot-agent/presentation";
3
3
  import { type ReactNode } from "react";
4
+ import type { CliTuiTimelineEvent } from "../types.js";
4
5
  /** 详情视图参数:expanded 驱动 Todo 卡展开,toolGroupsExpanded 驱动工具分组展开;screen reader 恒展开全部明细。 */
5
6
  type TuiDetailView = {
6
7
  expanded: boolean;
@@ -10,5 +11,5 @@ type TuiDetailView = {
10
11
  toolGroupsExpanded?: boolean;
11
12
  };
12
13
  /** 渲染指定 Turn 的共享 Presentation 时间线;无匹配节点时由调用方回退兼容旧事件。 */
13
- export declare function renderTuiPresentationTimeline(presentation: AgentPresentationState, turnId: string, useColor: boolean, maximumWidth: number, detailView: TuiDetailView): ReactNode[];
14
+ export declare function renderTuiPresentationTimeline(presentation: AgentPresentationState, turnId: string, useColor: boolean, maximumWidth: number, detailView: TuiDetailView, events?: CliTuiTimelineEvent[]): ReactNode[];
14
15
  export {};
@@ -1,14 +1,16 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** 基于共享 Presentation selector 的 CLI 时间线;不读取旧工具 summary、detail 或审批 preview。 */
3
- import { getPresentationCopy, selectTimeline, selectToolCalls } from "@autopilot-agent/presentation";
3
+ import { getPresentationCopy, getToolPresentationProfile, isSandboxUnavailableErrorCode, selectToolCalls } from "@autopilot-agent/presentation";
4
4
  import { Box, Text } from "ink";
5
- import { TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_TOOL_ERROR_COLOR } from "../../design-tokens.js";
5
+ import { TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_TOOL_ERROR_BG_COLOR, TUI_TOOL_ERROR_TEXT_COLOR } from "../../design-tokens.js";
6
6
  import { TUI_AGENT_LABEL } from "../constants.js";
7
7
  import { TuiDiffStats } from "../diff-stats.js";
8
8
  import { getTuiDiffStats } from "../format.js";
9
9
  import { TuiDiffBlock } from "../markdown/diff.js";
10
10
  import { TuiMarkdown } from "../markdown/markdown.js";
11
+ import { TuiPromptLine } from "../transcript.js";
11
12
  import { TuiTodoList } from "../todo-list.js";
13
+ import { getTuiTurnItems } from "./turn-items.js";
12
14
  /** 取得 ToolCall 的纯文本状态标志,不依赖颜色表达执行结果。 */
13
15
  function getToolStateGlyph(state) {
14
16
  if (state === "completed")
@@ -21,6 +23,16 @@ function getToolStateGlyph(state) {
21
23
  return "×";
22
24
  return "✢";
23
25
  }
26
+ /** 取得拒绝、取消与超时文字;执行失败由叉号和错误详情表达。沙箱不可用类拒绝是环境阻塞而非用户拒绝,文案必须区分。 */
27
+ function getToolStateText(state, errorCode) {
28
+ if (state === "denied")
29
+ return isSandboxUnavailableErrorCode(errorCode) ? "沙箱不可用,命令未启动" : "已拒绝";
30
+ if (state === "cancelled")
31
+ return "已取消";
32
+ if (state === "timeout")
33
+ return "已超时";
34
+ return undefined;
35
+ }
24
36
  /** 判断工具是否处于失败类终态;失败步骤文字使用错误红并保留文字标签。 */
25
37
  function isFailedToolState(state) {
26
38
  return state === "failed" || state === "denied" || state === "cancelled" || state === "timeout";
@@ -42,6 +54,8 @@ function getWebToolDetailText(item) {
42
54
  }
43
55
  /** 将一个共享 selector 节点渲染为 Ink 视图,Tool 节点的 React key 始终绑定 callId。 */
44
56
  function renderPresentationItem(item, useColor, maximumWidth, detailView, toolCallIndex) {
57
+ if (item.kind === "todo")
58
+ return _jsx(Box, { flexDirection: "column", children: _jsx(TuiTodoList, { item: item, expanded: detailView.expanded || detailView.screenReader, useColor: useColor, screenReader: detailView.screenReader, columns: maximumWidth + 2, rowLimit: detailView.rowLimit, showToggleHint: detailView.showToggleHint }) }, item.id);
45
59
  if (item.kind === "thinking")
46
60
  return _jsxs(Text, { dimColor: useColor, children: ["\u2722 ", item.summary] }, item.id);
47
61
  if (item.kind === "tool_group") {
@@ -55,25 +69,28 @@ function renderPresentationItem(item, useColor, maximumWidth, detailView, toolCa
55
69
  return (_jsxs(Box, { flexDirection: "column", children: [summary, children.map(function renderGroupChild(child) { return _jsx(Box, { paddingLeft: 2, children: renderPresentationItem(child, useColor, maximumWidth, detailView, toolCallIndex) }, child.callId); })] }, item.id));
56
70
  }
57
71
  if (item.kind === "tool_call") {
58
- const title = getPresentationCopy(item.titleKey);
72
+ const title = getPresentationCopy(item.state === "failed" ? getToolPresentationProfile(item.toolName).completedTitleKey : item.titleKey);
59
73
  const jobProgramLabel = item.job?.action === "start" ? item.job.programLabel?.toUpperCase() : undefined;
60
74
  const diffStats = getTuiDiffStats(item.diff);
61
75
  const detail = getWebToolDetailText(item) ?? [jobProgramLabel ? undefined : item.target, item.resultSummary].filter(Boolean).join(" · ");
62
76
  const glyph = getToolStateGlyph(item.state);
63
- // 徽标 chip 两态:活动与完成步骤都持久使用强调背景 + 深色粗体,失败类终态整行用错误红。
77
+ // 失败沿用完成态工具名称,仅徽标使用错误红;前缀符号独立显示。
78
+ const stateText = getToolStateText(item.state, item.errorCode);
64
79
  const head = isFailedToolState(item.state)
65
- ? _jsxs(Text, { color: useColor ? TUI_TOOL_ERROR_COLOR : undefined, children: [glyph, " [", title, "]"] })
80
+ ? _jsxs(Text, { children: [_jsxs(Text, { color: useColor ? TUI_TOOL_ERROR_BG_COLOR : undefined, children: [glyph, " "] }), _jsxs(Text, { backgroundColor: useColor ? TUI_TOOL_ERROR_BG_COLOR : undefined, color: useColor ? TUI_TOOL_ERROR_TEXT_COLOR : undefined, bold: useColor || undefined, children: ["[", title, "]", stateText ? ` · ${stateText}` : ""] })] })
66
81
  : _jsxs(Text, { children: [_jsxs(Text, { dimColor: useColor, children: [glyph, " "] }), _jsxs(Text, { backgroundColor: useColor ? TUI_ACCENT_BG_COLOR : undefined, color: useColor ? TUI_CHIP_TEXT_COLOR : undefined, bold: useColor || undefined, children: ["[", title, "]"] })] });
67
- return _jsxs(Box, { flexDirection: "column", children: [head, jobProgramLabel ? _jsxs(Text, { children: [" \u2514\u2500 ", _jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: jobProgramLabel }), detail ? _jsxs(Text, { dimColor: useColor, children: [" \u00B7 ", detail] }) : null] }) : detail || diffStats ? _jsxs(Text, { children: [" \u2514\u2500 ", detail ? _jsxs(Text, { dimColor: useColor, children: [detail, " "] }) : null, diffStats ? _jsx(TuiDiffStats, { ...diffStats, useColor: useColor }) : null] }) : null, item.errorCode ? _jsxs(Text, { dimColor: useColor, children: [" \u2514\u2500 ", item.errorCode] }) : null, item.job?.action === "start" && (item.job.observedStatus === "starting" || item.job.observedStatus === "running") ? _jsx(Text, { dimColor: useColor, children: " /jobs \u67E5\u770B\u540E\u53F0\u4EFB\u52A1" }) : null, item.diff?.text ? _jsx(TuiDiffBlock, { diff: item.diff.text, keyPrefix: item.callId, useColor: useColor }) : null] }, item.callId);
82
+ return _jsxs(Box, { flexDirection: "column", children: [head, jobProgramLabel ? _jsxs(Text, { children: [" \u2514\u2500 ", _jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: jobProgramLabel }), detail ? _jsxs(Text, { dimColor: useColor, children: [" \u00B7 ", detail] }) : null] }) : detail || diffStats ? _jsxs(Text, { children: [" \u2514\u2500 ", detail ? _jsxs(Text, { dimColor: useColor, children: [detail, " "] }) : null, diffStats ? _jsx(TuiDiffStats, { ...diffStats, useColor: useColor }) : null] }) : null, item.errorCode ? _jsxs(Text, { dimColor: useColor, children: [" \u2514\u2500 ", item.errorCode] }) : null, item.executionMode === "unisolated" ? _jsx(Text, { color: useColor ? TUI_TOOL_ERROR_TEXT_COLOR : undefined, children: " \u2514\u2500 \u672A\u9694\u79BB\uFF1A\u5BBF\u4E3B\u76F4\u63A5\u6267\u884C" }) : null, item.diagnostics?.length ? _jsxs(Text, { dimColor: useColor, children: [" \u2514\u2500 \u8BCA\u65AD\uFF1A", item.diagnostics.join(";")] }) : null, item.job?.action === "start" && (item.job.observedStatus === "starting" || item.job.observedStatus === "running") ? _jsx(Text, { dimColor: useColor, children: " /jobs \u67E5\u770B\u540E\u53F0\u4EFB\u52A1" }) : null, item.diff?.text ? _jsx(TuiDiffBlock, { diff: item.diff.text, keyPrefix: item.callId, useColor: useColor }) : null] }, item.callId);
68
83
  }
69
84
  if (item.kind === "approval") {
70
85
  const detail = item.display?.actionPreview ?? item.display?.operation ?? "需要确认";
71
- return _jsxs(Text, { dimColor: useColor, children: ["\u26A0 ", detail, item.state === "resolved" ? " · 已处理" : ""] }, item.id);
86
+ // 历史行与待审卡共用 constraint 判定,普通批准与无沙箱批准不得混淆。
87
+ const noSandbox = item.constraint === "no-sandbox-exec:v1" ? " · 无沙箱直接执行" : "";
88
+ // Talk-Plan1022 S3:宿主执行审批事实与 no-sandbox 降级卡分开呈现,措辞不宣称已执行。
89
+ const hostExecution = item.display?.executionMode === "unisolated" ? " · 将在当前用户权限下运行(未启用沙箱隔离)" : "";
90
+ return _jsxs(Text, { dimColor: useColor, children: ["\u26A0 ", detail, noSandbox, hostExecution, item.state === "resolved" ? " · 已处理" : ""] }, item.id);
72
91
  }
73
92
  if (item.kind === "user_question")
74
93
  return _jsx(Text, { dimColor: useColor, children: "\u8BF7\u5728\u652F\u6301\u7ED3\u6784\u5316\u56DE\u7B54\u7684 Web \u4E2D\u5904\u7406" }, item.id);
75
- if (item.kind === "todo")
76
- return _jsx(TuiTodoList, { item: item, expanded: detailView.expanded, useColor: useColor, screenReader: detailView.screenReader, columns: maximumWidth, rowLimit: detailView.rowLimit, showToggleHint: detailView.showToggleHint }, item.id);
77
94
  if (item.kind === "assistant_answer") {
78
95
  if (item.channel === "process")
79
96
  return _jsx(TuiMarkdown, { content: item.content, keyPrefix: item.id, useColor: useColor, maximumWidth: maximumWidth }, item.id);
@@ -87,7 +104,7 @@ function renderPresentationItem(item, useColor, maximumWidth, detailView, toolCa
87
104
  return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: useColor, children: [item.state === "completed" ? "✓" : "⚠", " ", title] }), item.restoredPaths.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" restored: ", item.restoredPaths.join(", ")] }) : null, item.compensatedPaths.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" compensated: ", item.compensatedPaths.join(", ")] }) : null, item.conflicts.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" conflicts (not overwritten): ", item.conflicts.join(", ")] }) : null] }, item.id);
88
105
  }
89
106
  if (item.kind === "input_notice")
90
- return _jsxs(Text, { dimColor: useColor, children: ["Input ", item.state, item.content ? ` · ${item.content}` : ""] }, item.id);
107
+ return _jsxs(Text, { dimColor: useColor, children: [item.delivery === "steer" ? "[引导] " : item.delivery === "queue" ? "[排队] " : "", "Input ", item.state] }, item.id);
91
108
  if (item.kind === "turn_summary")
92
109
  return _jsxs(Text, { dimColor: useColor, children: [getTurnStateGlyph(item.state), " ", getPresentationCopy(`turn.${item.state}`), " \u00B7 ", item.toolCount, " tools"] }, item.id);
93
110
  if (item.kind === "job") {
@@ -100,10 +117,12 @@ function renderPresentationItem(item, useColor, maximumWidth, detailView, toolCa
100
117
  return null;
101
118
  }
102
119
  /** 渲染指定 Turn 的共享 Presentation 时间线;无匹配节点时由调用方回退兼容旧事件。 */
103
- export function renderTuiPresentationTimeline(presentation, turnId, useColor, maximumWidth, detailView) {
120
+ export function renderTuiPresentationTimeline(presentation, turnId, useColor, maximumWidth, detailView, events = []) {
104
121
  // 构建 callId 索引供展开分组取回组内调用;分组只按 callId 引用,不携带子节点事实。
105
122
  const toolCallIndex = new Map(selectToolCalls(presentation, turnId).map(function indexToolCall(call) { return [call.callId, call]; }));
106
- return selectTimeline(presentation)
107
- .filter(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === turnId); })
108
- .map(function renderSharedTimelineItem(item) { return renderPresentationItem(item, useColor, maximumWidth, detailView, toolCallIndex); });
123
+ return getTuiTurnItems(presentation, turnId, events).map(function renderSharedTimelineItem(item) {
124
+ if ("type" in item)
125
+ return _jsxs(Box, { flexDirection: "column", children: [_jsx(TuiPromptLine, { content: item.content, useColor: useColor, columns: maximumWidth + 2 }), _jsxs(Text, { dimColor: useColor, children: [item.delivery === "queue" ? "[排队]" : "[引导]", " ", item.cancelled ? "已取消" : item.consumed === false ? "等待当前步骤结束" : "已插入当前 turn"] })] }, `input:${item.inputId}`);
126
+ return renderPresentationItem(item, useColor, maximumWidth, detailView, toolCallIndex);
127
+ });
109
128
  }
@@ -1,16 +1,19 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
3
+ import { TUI_ACCENT_COLOR, TUI_TOOL_ERROR_TEXT_COLOR } from "../../design-tokens.js";
4
4
  import { TUI_AGENT_LABEL } from "../constants.js";
5
5
  import { formatTuiDuration, formatTuiInputConsumed, getTuiApprovalDecisionLabel, getTuiToolLabel, getTuiTerminationSummary, getTuiTurnFailureReason } from "../format.js";
6
6
  import { TuiMarkdown } from "../markdown/markdown.js";
7
7
  import { renderTuiToolTreeEvent } from "./tool-tree.js";
8
8
  import { renderTuiPresentationTimeline } from "./presentation-timeline.js";
9
+ import { TuiPromptLine } from "../transcript.js";
10
+ import { TuiTodoList } from "../todo-list.js";
11
+ import { getTuiTurnTodo } from "./turn-items.js";
9
12
  /** 将安全事件聚合为可读的时间线行,连续 answer_delta 只显示为一个回答块。 */
10
13
  export function renderTimeline(events, useColor, timing, maximumWidth = 80, screenReader = false) {
11
14
  // 历史区在 Turn 终态只固化一次最终快照;读屏模式继续输出中文状态,不能退化为 glyph-only。
12
15
  if (timing?.presentation && timing.turnId && timing.presentation.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === timing.turnId); })) {
13
- const nodes = renderTuiPresentationTimeline(timing.presentation, timing.turnId, useColor, maximumWidth, { expanded: true, screenReader });
16
+ const nodes = renderTuiPresentationTimeline(timing.presentation, timing.turnId, useColor, maximumWidth, { expanded: true, screenReader }, events);
14
17
  const failureReason = getTuiTurnFailureReason(events);
15
18
  const terminationSummary = getTuiTerminationSummary(events, timing?.status);
16
19
  if (terminationSummary)
@@ -56,7 +59,7 @@ export function renderTimeline(events, useColor, timing, maximumWidth = 80, scre
56
59
  }
57
60
  flushAnswer();
58
61
  if (event.type === "tui_input_consumed") {
59
- nodes.push(_jsx(Text, { dimColor: useColor, children: formatTuiInputConsumed(event) }, String(event.seq) + ":input:" + event.inputId));
62
+ nodes.push(_jsxs(Box, { flexDirection: "column", children: [_jsx(TuiPromptLine, { content: event.content, useColor: useColor, columns: maximumWidth + 2 }), _jsx(Text, { dimColor: useColor, children: event.cancelled ? "[引导] 已取消" : event.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(event) })] }, "input:" + event.inputId));
60
63
  }
61
64
  else if (event.type === "tui_approval_submitted") {
62
65
  nodes.push(_jsxs(Text, { dimColor: useColor, children: ["Approval submitted \u00B7 ", getTuiApprovalDecisionLabel(event.decision)] }, String(event.seq) + ":approval:" + event.approvalId));
@@ -76,7 +79,7 @@ export function renderTimeline(events, useColor, timing, maximumWidth = 80, scre
76
79
  else if (event.type === "approval_required") {
77
80
  if (submittedApprovalIds.has(event.approvalId))
78
81
  continue;
79
- nodes.push(_jsxs(Box, { borderStyle: "single", borderColor: useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: "Approval required" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", getTuiToolLabel(event.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] }), _jsx(Text, { dimColor: useColor, children: "[y] \u672C\u6B21 [s] \u672C\u6B21\u4F1A\u8BDD [w] \u6B64\u5DE5\u4F5C\u533A [n] \u62D2\u7EDD" })] }, String(event.seq)));
82
+ nodes.push(_jsxs(Box, { borderStyle: "single", borderColor: useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: "Approval required" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", getTuiToolLabel(event.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] }), event.display?.executionMode === "unisolated" && _jsx(Text, { color: useColor ? TUI_TOOL_ERROR_TEXT_COLOR : undefined, children: "\u5C06\u5728\u5F53\u524D\u7528\u6237\u6743\u9650\u4E0B\u8FD0\u884C\uFF08\u672A\u542F\u7528\u6C99\u7BB1\u9694\u79BB\uFF09" }), _jsx(Text, { dimColor: useColor, children: "[y] \u672C\u6B21 [s] \u672C\u6B21\u4F1A\u8BDD [w] \u6B64\u5DE5\u4F5C\u533A [n] \u62D2\u7EDD" })] }, String(event.seq)));
80
83
  }
81
84
  else if (event.type === "input_queued") {
82
85
  nodes.push(_jsx(Text, { dimColor: useColor, children: "Additional instruction queued; waiting for the current step" }, String(event.seq)));
@@ -110,11 +113,18 @@ export function renderTimeline(events, useColor, timing, maximumWidth = 80, scre
110
113
  continue;
111
114
  }
112
115
  else if (event.type === "status") {
113
- if (event.status === "failed" && event.reason)
114
- nodes.push(_jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] }, String(event.seq)));
116
+ continue;
115
117
  }
116
118
  }
117
119
  flushAnswer();
120
+ const todo = getTuiTurnTodo(events, timing?.turnId ?? events[0]?.turnId ?? "");
121
+ if (todo)
122
+ nodes.push(_jsx(Box, { flexDirection: "column", children: _jsx(TuiTodoList, { item: todo, expanded: true, useColor: useColor, screenReader: screenReader, columns: maximumWidth + 2 }) }, "turn:todo"));
123
+ const terminationSummary = getTuiTerminationSummary(events, timing?.status);
124
+ if (terminationSummary)
125
+ nodes.push(_jsxs(Text, { children: ["\u7EC8\u6001\uFF1A", terminationSummary] }, "turn:termination"));
126
+ else if (getTuiTurnFailureReason(events))
127
+ nodes.push(_jsxs(Text, { children: ["\u539F\u56E0\uFF1A", getTuiTurnFailureReason(events)] }, "turn:failure-reason"));
118
128
  if (timing?.totalDurationMs !== undefined)
119
129
  nodes.push(_jsxs(Text, { dimColor: useColor, children: ["* Cogitated for ", formatTuiDuration(timing.totalDurationMs)] }, "turn:total-duration"));
120
130
  return nodes.filter(function isVisibleTimelineNode(node) { return node !== undefined; });
@@ -0,0 +1,12 @@
1
+ /** 合并既有 Presentation 与宿主输入位置,并读取每轮最后一次安全 Todo。 */
2
+ import { type AgentPresentationState, type TimelineItem, type TodoSnapshotItem } from "@autopilot-agent/presentation";
3
+ import type { CliTuiInputPosition, CliTuiTimelineEvent } from "../types.js";
4
+ export type TuiHostInput = Extract<CliTuiTimelineEvent, {
5
+ type: "tui_input_consumed";
6
+ }>;
7
+ /** 保存当前可见事实的稳定边界,连续回答以字符位置保留输入前后的正文。 */
8
+ export declare function captureTuiInputPosition(turnId: string | undefined, events: CliTuiTimelineEvent[], presentation?: AgentPresentationState): CliTuiInputPosition;
9
+ /** 只采用该轮自身的最后一次 todo_updated;显式空列表保持清空语义。 */
10
+ export declare function getTuiTurnTodo(events: CliTuiTimelineEvent[], turnId: string): TodoSnapshotItem | undefined;
11
+ /** 按提交边界切分共享节点,再复用 selector 分组;宿主正文不会被后来的分组或回答吞到前面。 */
12
+ export declare function getTuiTurnItems(presentation: AgentPresentationState, turnId: string, events: CliTuiTimelineEvent[]): Array<TimelineItem | TuiHostInput>;
@@ -0,0 +1,28 @@
1
+ /** 合并既有 Presentation 与宿主输入位置,并读取每轮最后一次安全 Todo。 */
2
+ import { capturePresentationInputPosition, reducePresentationInput, selectTurnTimeline } from "@autopilot-agent/presentation";
3
+ /** 保存当前可见事实的稳定边界,连续回答以字符位置保留输入前后的正文。 */
4
+ export function captureTuiInputPosition(turnId, events, presentation) {
5
+ return { turnId, eventCount: events.filter(function publicEvent(event) { return event.type !== "tui_input_consumed"; }).length, items: presentation && turnId ? capturePresentationInputPosition(presentation, turnId).items : [] };
6
+ }
7
+ /** 只采用该轮自身的最后一次 todo_updated;显式空列表保持清空语义。 */
8
+ export function getTuiTurnTodo(events, turnId) {
9
+ for (let index = events.length - 1; index >= 0; index -= 1) {
10
+ const event = events[index];
11
+ if (event?.type === "todo_updated" && event.turnId === turnId)
12
+ return { id: `todo:${turnId}`, kind: "todo", turnId, todos: event.todos, counts: event.counts };
13
+ }
14
+ return undefined;
15
+ }
16
+ /** 按提交边界切分共享节点,再复用 selector 分组;宿主正文不会被后来的分组或回答吞到前面。 */
17
+ export function getTuiTurnItems(presentation, turnId, events) {
18
+ const inputs = events.filter(function host(event) { return event.type === "tui_input_consumed"; });
19
+ let state = presentation;
20
+ for (const input of inputs) {
21
+ state = reducePresentationInput(state, { kind: "host_event", event: { kind: "input", input: { turnId: input.turnId, inputId: input.inputId, delivery: input.delivery, content: input.content, position: input.position ?? { items: [] }, cancelled: input.cancelled, state: input.consumed === false ? "queued" : "consumed" } } });
22
+ }
23
+ const todo = getTuiTurnTodo(events, turnId);
24
+ // CLI 只采用本轮 events;不把恢复或 Session latest 误当作本轮更新。
25
+ state = { ...state, todosByTurn: { ...state.todosByTurn, [turnId]: todo ?? { id: `todo:${turnId}`, kind: "todo", turnId, todos: [], counts: { completed: 0, pending: 0, inProgress: 0 } } } };
26
+ const inputById = new Map(inputs.map(function pair(input) { return [input.inputId, input]; }));
27
+ return selectTurnTimeline(state, turnId).map(function host(item) { return item.kind === "input_notice" ? inputById.get(item.inputId) ?? item : item; });
28
+ }