@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.
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** 渲染当前 Turn;它会随安全事件更新,完成后整体进入历史树。 */
3
3
  import { memo } from "react";
4
4
  import { Box, Text } from "ink";
@@ -6,8 +6,8 @@ import { TuiApprovalPrompt } from "../approval.js";
6
6
  import { formatTuiDuration, getTuiTerminationSummary, getTuiTurnFailureReason } from "../format.js";
7
7
  import { TuiLiveTimeline } from "./live.js";
8
8
  import { renderTuiPresentationTimeline } from "./presentation-timeline.js";
9
- import { TuiLiveProgressSummary } from "./progress.js";
10
- import { getTuiTodoActiveForm } from "../todo-list.js";
9
+ import { TuiTodoList } from "../todo-list.js";
10
+ import { getTuiTurnTodo } from "./turn-items.js";
11
11
  /** 渲染当前 Turn;memo 保证输入光标更新不会触发 live timeline 组件执行。 */
12
12
  export const TuiLiveTurn = memo(function TuiLiveTurn(props) {
13
13
  if (!props.turnId || props.events.length === 0)
@@ -15,7 +15,7 @@ export const TuiLiveTurn = memo(function TuiLiveTurn(props) {
15
15
  const hasPresentation = props.presentation?.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === props.turnId); }) ?? false;
16
16
  const failureReason = getTuiTurnFailureReason(props.events);
17
17
  const terminationSummary = getTuiTerminationSummary(props.events, props.status);
18
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TuiLiveProgressSummary, { events: props.events, status: props.status, metrics: props.metrics, useColor: props.useColor, activeTodoForm: getTuiTodoActiveForm(props.presentation, props.turnId) }), props.presentation && hasPresentation
19
- ? renderTuiPresentationTimeline(props.presentation, props.turnId, props.useColor, props.maximumWidth, { expanded: props.todoExpanded, screenReader: props.screenReader, rowLimit: props.todoRowLimit, showToggleHint: true, toolGroupsExpanded: props.todoExpanded })
20
- : _jsx(TuiLiveTimeline, { turnId: props.turnId, events: props.events, status: props.status, metrics: props.metrics, useColor: props.useColor, maximumWidth: props.maximumWidth }), terminationSummary ? _jsxs(Text, { children: ["\u7EC8\u6001\uFF1A", terminationSummary] }) : hasPresentation && failureReason ? _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", failureReason] }) : null, props.pendingApproval ? _jsx(TuiApprovalPrompt, { pendingApproval: props.pendingApproval, selectedIndex: props.approvalSelectedIndex, useColor: props.useColor, screenReader: props.screenReader }) : null, props.metrics?.completedAt !== undefined ? _jsxs(Text, { dimColor: props.useColor, children: ["* Cogitated for ", formatTuiDuration(Math.max(0, props.metrics.completedAt - props.metrics.startedAt))] }) : null] }));
18
+ return (_jsxs(Box, { flexDirection: "column", children: [props.presentation && hasPresentation
19
+ ? renderTuiPresentationTimeline(props.presentation, props.turnId, props.useColor, props.maximumWidth, { expanded: props.todoExpanded, screenReader: props.screenReader, rowLimit: props.todoRowLimit, showToggleHint: true, toolGroupsExpanded: props.todoExpanded }, props.events)
20
+ : _jsxs(_Fragment, { children: [_jsx(TuiLiveTimeline, { turnId: props.turnId, events: props.events, status: props.status, metrics: props.metrics, useColor: props.useColor, maximumWidth: props.maximumWidth }), getTuiTurnTodo(props.events, props.turnId) ? _jsx(TuiTodoList, { item: getTuiTurnTodo(props.events, props.turnId), expanded: props.todoExpanded || props.screenReader, screenReader: props.screenReader, useColor: props.useColor, columns: props.maximumWidth + 2, rowLimit: props.todoRowLimit, showToggleHint: true }) : null] }), terminationSummary ? _jsxs(Text, { children: ["\u7EC8\u6001\uFF1A", terminationSummary] }) : hasPresentation && failureReason ? _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", failureReason] }) : null, props.pendingApproval ? _jsx(TuiApprovalPrompt, { pendingApproval: props.pendingApproval, selectedIndex: props.approvalSelectedIndex, useColor: props.useColor, screenReader: props.screenReader }) : null, props.metrics?.completedAt !== undefined ? _jsxs(Text, { dimColor: props.useColor, children: ["* Cogitated for ", formatTuiDuration(Math.max(0, props.metrics.completedAt - props.metrics.startedAt))] }) : null] }));
21
21
  });
@@ -17,7 +17,7 @@ export declare function TuiTranscriptItem(props: {
17
17
  }): ReactNode;
18
18
  /** 历史节点保持引用稳定;回答流动时只重新计算当前 Turn。 */
19
19
  export declare const MemoizedTuiTranscriptItem: import("react").MemoExoticComponent<typeof TuiTranscriptItem>;
20
- /** 渲染动态区中尚未被 Core 消费的引导或排队 prompt。 */
20
+ /** queue 在等待区显示原文;steer 正文已按提交位置留在所属轮。 */
21
21
  export declare function TuiPendingInputItem(props: {
22
22
  item: CliTuiPendingInput;
23
23
  useColor: boolean;
@@ -27,8 +27,8 @@ export function TuiTranscriptItem(props) {
27
27
  }
28
28
  /** 历史节点保持引用稳定;回答流动时只重新计算当前 Turn。 */
29
29
  export const MemoizedTuiTranscriptItem = memo(TuiTranscriptItem);
30
- /** 渲染动态区中尚未被 Core 消费的引导或排队 prompt。 */
30
+ /** queue 在等待区显示原文;steer 正文已按提交位置留在所属轮。 */
31
31
  export function TuiPendingInputItem(props) {
32
32
  const label = props.item.delivery === "queue" ? "[排队]" : "[引导]";
33
- return (_jsx(Box, { backgroundColor: props.useColor ? TUI_WAITING_BACKGROUND_COLOR : undefined, children: _jsxs(Text, { dimColor: props.useColor, children: ["\u21B3 ", label, " you: ", props.item.content] }) }, props.item.key));
33
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: props.useColor ? TUI_WAITING_BACKGROUND_COLOR : undefined, children: [props.item.delivery === "queue" ? _jsx(Text, { children: props.item.content }) : null, _jsxs(Text, { dimColor: props.useColor, children: ["\u21B3 ", label, " \u7B49\u5F85\u5F53\u524D\u6B65\u9AA4\u7ED3\u675F"] })] }, props.item.key));
34
34
  }
@@ -35,6 +35,9 @@ export type CliTuiTimelineEvent = CliPublicEvent | {
35
35
  inputId: string;
36
36
  content: string;
37
37
  delivery: "steer" | "queue";
38
+ consumed?: boolean;
39
+ cancelled?: boolean;
40
+ position?: CliTuiInputPosition;
38
41
  } | {
39
42
  type: "tui_approval_submitted";
40
43
  seq: number;
@@ -51,6 +54,16 @@ export interface CliTuiPendingInput {
51
54
  inputId: string;
52
55
  delivery: "steer" | "queue";
53
56
  }
57
+ /** 提交瞬间的本地位置;只引用安全展示节点,不保存附件展开或私有消息。 */
58
+ export interface CliTuiInputPosition {
59
+ turnId?: string;
60
+ eventCount: number;
61
+ order?: number;
62
+ items: Array<{
63
+ id: string;
64
+ answerLength?: number;
65
+ }>;
66
+ }
54
67
  /** Turn 的本地展示计时;只用于 TUI 文案,不进入 Runtime 或持久化事件。 */
55
68
  export interface CliTuiTurnMetrics {
56
69
  startedAt: number;
@@ -92,10 +105,11 @@ export interface CliTuiStore {
92
105
  getSnapshot(): CliTuiSnapshot;
93
106
  subscribe(listener: () => void): () => void;
94
107
  recordPrompt(content: string, displayAsActive?: boolean): string | undefined;
108
+ captureInputPosition(): CliTuiInputPosition;
95
109
  addPendingPrompt(content: string, input: {
96
110
  inputId: string;
97
111
  delivery: "steer" | "queue";
98
- }): void;
112
+ }, position?: CliTuiInputPosition): void;
99
113
  markQueuedInputStarted(input: CliQueuedInputStarted): void;
100
114
  recordApprovalSubmission(decision: Extract<CliAction, {
101
115
  type: "approve";
@@ -7,7 +7,7 @@ export declare function formatTuiViewportMarkdown(content: string, maximumWidth:
7
7
  /** 将历史版本的 unified diff 直角卡片转换为备用屏可裁剪的文字源,并保留边框与逐行语义颜色。 */
8
8
  export declare function createTuiViewportDiffSource(itemId: string, diff: string, contentWidth: number): TimelineLineSource;
9
9
  /** 将已完成与当前动态时间线统一变为可裁剪的视觉行来源。 */
10
- export declare function createTuiViewportSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean): TimelineLineSource[];
10
+ export declare function createTuiViewportSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean, todoRowLimit?: number): TimelineLineSource[];
11
11
  /** 备用屏 source 缓存计数,区分历史命中与 live tail 重建。 */
12
12
  export interface TuiViewportSourceCacheStats {
13
13
  brandBuilds: number;
@@ -21,7 +21,7 @@ export interface TuiViewportSourceCacheStats {
21
21
  }
22
22
  /** 按 transcript item 与 live tail 增量生成备用屏 source 的内部接口。 */
23
23
  export interface TuiViewportSourceCache {
24
- getSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean): TimelineLineSource[];
24
+ getSources(snapshot: CliTuiSnapshot, statusView: CliStatusView | undefined, approvalSelectedIndex: number, contentWidth: number, toolGroupsExpanded?: boolean, decorativeGrid?: boolean, todoRowLimit?: number): TimelineLineSource[];
25
25
  getStats(): TuiViewportSourceCacheStats;
26
26
  clear(): void;
27
27
  }
@@ -1,12 +1,13 @@
1
1
  /** 备用屏时间线来源:把安全事件转为可裁剪的纯文本视觉行来源。 */
2
2
  import stringWidth from "string-width";
3
3
  import wrapAnsi from "wrap-ansi";
4
- import { WEB_SEARCH_DATA_FLOW_NOTICE, getPresentationCopy, selectTimeline, selectToolCalls } from "@autopilot-agent/presentation";
4
+ import { WEB_SEARCH_DATA_FLOW_NOTICE, getPresentationCopy, getToolPresentationProfile, isSandboxUnavailableErrorCode, selectToolCalls } from "@autopilot-agent/presentation";
5
5
  import { createAutopilotStaticWordRows } from "../brand/pixel.js";
6
6
  import { AUTOPILOT_VERSION, TUI_AGENT_LABEL } from "../constants.js";
7
- import { getTuiTodoDisplay, getTuiTodoStatusGlyph } from "../todo-list.js";
8
7
  import { cleanTuiToolSummary, formatTuiDiffStats, formatTuiDuration, formatTuiInputConsumed, formatTuiToolLine, getTuiDiffStats, getTuiStatusLabel, getTuiToolLabel, getTuiTurnFailureReason, getTuiTerminationSummary, } from "../format.js";
9
8
  import { getTuiMarkdownTableColumnWidths, isLooseMarkdownTable, isMarkdownTableDivider, normalizeMarkdownTableCells, splitMarkdownTableRow, } from "../markdown/table.js";
9
+ import { getTuiTurnItems, getTuiTurnTodo } from "../timeline/turn-items.js";
10
+ import { getTuiTodoDisplay, getTuiTodoStatusLabel } from "../todo-list.js";
10
11
  /** 将对用户安全公开的时间线事件转换为备用屏内的纯文本行。 */
11
12
  function formatTuiViewportEvent(event) {
12
13
  if (event.type === "answer_delta")
@@ -213,46 +214,26 @@ function createTuiViewportBrandSource(statusView) {
213
214
  ],
214
215
  };
215
216
  }
216
- /** 将 Todo 任务链转换为备用屏文本源;备用屏空间充足,始终以展开形态显示最终清单。 */
217
- function createTuiTodoViewportSource(itemId, item, contentWidth) {
218
- const display = getTuiTodoDisplay(item, { expanded: true, screenReader: false, columns: contentWidth });
219
- if (!display)
220
- return undefined;
221
- const output = [];
222
- const spans = [];
223
- let sourceOffset = 0;
224
- /** 追加一行 Todo 文本并按状态记录相对样式范围。 */
225
- function appendTodoLine(line, style) {
226
- output.push(line);
227
- if (style && line)
228
- spans.push({ start: sourceOffset, end: sourceOffset + line.length, style });
229
- sourceOffset += line.length + 1;
230
- }
231
- appendTodoLine(display.header, undefined);
232
- for (const row of display.rows) {
233
- if (row.hidden)
234
- appendTodoLine(`+${row.text}`, "muted");
235
- else
236
- appendTodoLine(`${getTuiTodoStatusGlyph(row.status)} ${row.text}`, row.status === "in_progress" ? "accent" : row.status === "completed" ? "muted" : undefined);
237
- }
238
- return { itemId, text: output.join("\n"), spans };
239
- }
240
217
  /** 将共享 Presentation selector 转成备用屏文本源,与 inline TUI 复用同一事实和顺序。 */
241
- function createTuiPresentationViewportSources(presentation, turnId, itemId, contentWidth, toolGroupsExpanded) {
218
+ function createTuiPresentationViewportSources(presentation, turnId, itemId, contentWidth, toolGroupsExpanded, events, live, rowLimit = 10) {
242
219
  const sources = [];
243
220
  // 构建 callId 索引供展开分组取回组内调用。
244
221
  const callIndex = new Map(selectToolCalls(presentation, turnId).map(function indexToolCall(call) { return [call.callId, call]; }));
245
222
  let index = 0;
246
- for (const item of selectTimeline(presentation)) {
223
+ for (const item of getTuiTurnItems(presentation, turnId, events)) {
224
+ if ("type" in item) {
225
+ sources.push({ itemId: `${itemId}:input:${item.inputId}`, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") });
226
+ sources.push({ itemId: `${itemId}:input:${item.inputId}:state`, text: item.cancelled ? "[引导] 已取消" : item.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(item) });
227
+ continue;
228
+ }
247
229
  if (item.kind !== "capability_notice" && (!("turnId" in item) || item.turnId !== turnId))
248
230
  continue;
249
231
  const sourceId = `${itemId}:presentation:${String(index++)}`;
250
232
  if (item.kind === "todo") {
251
- const todoSource = createTuiTodoViewportSource(sourceId, item, contentWidth);
252
- if (todoSource)
253
- sources.push(todoSource);
233
+ sources.push(...createTuiTodoSources(item, sourceId, contentWidth, live ? toolGroupsExpanded : true, live, rowLimit));
234
+ continue;
254
235
  }
255
- else if (item.kind === "thinking")
236
+ if (item.kind === "thinking")
256
237
  sources.push({ itemId: sourceId, text: `✢ ${item.summary}`, spans: createTuiViewportTextSpans(`✢ ${item.summary}`, "muted") });
257
238
  else if (item.kind === "tool_group") {
258
239
  const text = `• Exploring codebase · ${item.toolCallIds.length} operations`;
@@ -279,13 +260,15 @@ function createTuiPresentationViewportSources(presentation, turnId, itemId, cont
279
260
  const detail = `${[jobProgramLabel ? undefined : item.target, item.resultSummary].filter(Boolean).join(" · ")}${diffStatsText ? ` ${diffStatsText}` : ""}`;
280
261
  const detailLine = jobProgramLabel ? `\n └─ ${jobProgramLabel}${detail ? ` · ${detail}` : ""}` : detail ? `\n └─ ${detail}` : "";
281
262
  const jobHint = item.job?.action === "start" && (item.job.observedStatus === "starting" || item.job.observedStatus === "running") ? "\n /jobs 查看后台任务" : "";
282
- const title = getPresentationCopy(item.titleKey);
283
- const text = `• [${title}]${detailLine}${item.errorCode ? `\n └─ ${item.errorCode}` : ""}${jobHint}`;
263
+ const title = getPresentationCopy(item.state === "failed" ? getToolPresentationProfile(item.toolName).completedTitleKey : item.titleKey);
264
+ // 2.5:沙箱不可用类拒绝是环境阻塞而非用户拒绝,文案必须区分。
265
+ const stateText = item.state === "denied" ? (isSandboxUnavailableErrorCode(item.errorCode) ? "沙箱不可用,命令未启动" : "已拒绝") : item.state === "cancelled" ? "已取消" : item.state === "timeout" ? "已超时" : undefined;
266
+ const badgeText = `[${title}]${stateText ? ` · ${stateText}` : ""}`;
267
+ const text = `${item.state === "failed" ? "×" : "•"} ${badgeText}${detailLine}${item.errorCode ? `\n └─ ${item.errorCode}` : ""}${jobHint}`;
284
268
  const programStart = jobProgramLabel ? text.indexOf(jobProgramLabel) : -1;
285
269
  const hintStart = jobHint ? text.length - jobHint.trimStart().length : -1;
286
270
  const badgeStart = 2;
287
- const badgeEnd = badgeStart + title.length + 2;
288
- const firstLineEnd = text.indexOf("\n") < 0 ? text.length : text.indexOf("\n");
271
+ const badgeEnd = badgeStart + badgeText.length;
289
272
  const spans = createTuiViewportDiffStatsSpans(text, diffStats);
290
273
  if (programStart >= 0 && jobProgramLabel)
291
274
  spans.push({ start: programStart, end: programStart + jobProgramLabel.length, style: "accent" });
@@ -297,7 +280,7 @@ function createTuiPresentationViewportSources(presentation, turnId, itemId, cont
297
280
  spans.push({ start: badgeStart, end: badgeEnd, style: "chip-active" });
298
281
  }
299
282
  else {
300
- spans.push({ start: 0, end: firstLineEnd, style: "error" });
283
+ spans.push({ start: badgeStart, end: badgeEnd, style: "chip-error" });
301
284
  }
302
285
  if (hintStart >= 0)
303
286
  spans.push({ start: hintStart, end: text.length, style: "muted" });
@@ -320,7 +303,7 @@ function createTuiPresentationViewportSources(presentation, turnId, itemId, cont
320
303
  sources.push({ itemId: sourceId, text, spans: createTuiViewportTextSpans(text, "accent") });
321
304
  }
322
305
  else if (item.kind === "input_notice") {
323
- const text = `Input ${item.state}${item.content ? ` · ${item.content}` : ""}`;
306
+ const text = `${item.delivery === "steer" ? "[引导] " : item.delivery === "queue" ? "[排队] " : ""}Input ${item.state}`;
324
307
  sources.push({ itemId: sourceId, text, spans: createTuiViewportTextSpans(text, "muted") });
325
308
  }
326
309
  else if (item.kind === "context_notice")
@@ -332,11 +315,19 @@ function createTuiPresentationViewportSources(presentation, turnId, itemId, cont
332
315
  }
333
316
  return sources;
334
317
  }
318
+ /** Todo 行复用既有紧凑/展开数据;历史不输出失效快捷键。 */
319
+ function createTuiTodoSources(item, itemId, contentWidth, expanded, live, rowLimit = 10) {
320
+ const display = getTuiTodoDisplay(item, { expanded, screenReader: false, columns: contentWidth + 1, rowLimit });
321
+ if (!display)
322
+ return [];
323
+ const text = [display.header, ...display.rows.map(function row(row) { return row.hidden ? `+${row.text}` : `[${getTuiTodoStatusLabel(row.status)}] ${row.text}`; }), ...(live ? [`Ctrl+O ${expanded ? "收起" : "展开"}`] : [])].join("\n");
324
+ return [{ itemId, text }];
325
+ }
335
326
  /** 将一个历史或 live Turn 的安全事件转换为可缓存的备用屏 source 列表。 */
336
327
  function createTuiViewportEventSources(itemId, events, contentWidth, timing, toolGroupsExpanded = false) {
337
328
  const sources = [];
338
329
  if (timing?.presentation && timing.turnId && timing.presentation.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === timing.turnId); })) {
339
- sources.push(...createTuiPresentationViewportSources(timing.presentation, timing.turnId, itemId, contentWidth, toolGroupsExpanded));
330
+ sources.push(...createTuiPresentationViewportSources(timing.presentation, timing.turnId, itemId, contentWidth, toolGroupsExpanded, events, timing.status === "running" || timing.status === "waiting_approval", timing.todoRowLimit));
340
331
  const failureReason = getTuiTurnFailureReason(events);
341
332
  const terminationSummary = getTuiTerminationSummary(events, timing?.status);
342
333
  if (terminationSummary) {
@@ -405,8 +396,15 @@ function createTuiViewportEventSources(itemId, events, contentWidth, timing, too
405
396
  continue;
406
397
  }
407
398
  flushAnswer();
399
+ if (event.type === "tui_input_consumed") {
400
+ sources.push({ itemId: `${itemId}:input:${event.inputId}`, text: event.content, spans: createTuiViewportTextSpans(event.content, "accent") });
401
+ sources.push({ itemId: `${itemId}:input:${event.inputId}:state`, text: event.cancelled ? "[引导] 已取消" : event.consumed === false ? "[引导] 等待当前步骤结束" : formatTuiInputConsumed(event) });
402
+ continue;
403
+ }
404
+ if (event.type === "status")
405
+ continue;
408
406
  const text = formatTuiViewportEvent(event);
409
- const style = event.type === "tool_started" || event.type === "reasoning_summary" ? "muted" : event.type === "status" ? "accent" : undefined;
407
+ const style = event.type === "tool_started" || event.type === "reasoning_summary" ? "muted" : undefined;
410
408
  if (text) {
411
409
  const spans = event.type === "tool_result"
412
410
  ? createTuiViewportDiffStatsSpans(text, getTuiDiffStats(event.diff ? { text: event.diff } : undefined))
@@ -422,6 +420,15 @@ function createTuiViewportEventSources(itemId, events, contentWidth, timing, too
422
420
  }
423
421
  }
424
422
  flushAnswer();
423
+ const todo = getTuiTurnTodo(events, timing?.turnId ?? events[0]?.turnId ?? "");
424
+ const live = timing?.status === "running" || timing?.status === "waiting_approval";
425
+ if (todo)
426
+ sources.push(...createTuiTodoSources(todo, `${itemId}:todo`, contentWidth, live ? toolGroupsExpanded : true, live, timing?.todoRowLimit));
427
+ const terminationSummary = getTuiTerminationSummary(events, timing?.status);
428
+ if (terminationSummary)
429
+ sources.push({ itemId: `${itemId}:termination`, text: `终态:${terminationSummary}` });
430
+ else if (getTuiTurnFailureReason(events))
431
+ sources.push({ itemId: `${itemId}:failure-reason`, text: `原因:${getTuiTurnFailureReason(events)}` });
425
432
  if (timing?.totalDurationMs !== undefined) {
426
433
  const text = `* Cogitated for ${formatTuiDuration(timing.totalDurationMs)}`;
427
434
  sources.push({ itemId: `${itemId}:total-duration`, text, spans: createTuiViewportTextSpans(text, "muted") });
@@ -429,7 +436,7 @@ function createTuiViewportEventSources(itemId, events, contentWidth, timing, too
429
436
  return sources;
430
437
  }
431
438
  /** 将已完成与当前动态时间线统一变为可裁剪的视觉行来源。 */
432
- export function createTuiViewportSources(snapshot, statusView, approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false) {
439
+ export function createTuiViewportSources(snapshot, statusView, approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false, todoRowLimit = 10) {
433
440
  const sources = [];
434
441
  sources.push(createTuiViewportBrandSource(statusView));
435
442
  function appendEventGroup(itemId, events, timing, expandGroups = false) {
@@ -438,7 +445,7 @@ export function createTuiViewportSources(snapshot, statusView, approvalSelectedI
438
445
  let renderedActivePrompt = false;
439
446
  for (const item of snapshot.transcript) {
440
447
  if (item.kind === "prompt") {
441
- sources.push({ itemId: item.key, text: item.content });
448
+ sources.push({ itemId: item.key, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") });
442
449
  if (item.key === snapshot.activePrompt?.key)
443
450
  renderedActivePrompt = true;
444
451
  }
@@ -447,17 +454,18 @@ export function createTuiViewportSources(snapshot, statusView, approvalSelectedI
447
454
  }
448
455
  }
449
456
  if (snapshot.activePrompt && !renderedActivePrompt)
450
- sources.push({ itemId: snapshot.activePrompt.key, text: snapshot.activePrompt.content });
457
+ sources.push({ itemId: snapshot.activePrompt.key, text: snapshot.activePrompt.content, spans: createTuiViewportTextSpans(snapshot.activePrompt.content, "accent") });
451
458
  const liveCompletedAt = snapshot.liveMetrics?.completedAt;
452
- appendEventGroup(snapshot.liveTurnId ?? "live", snapshot.liveEvents, snapshot.liveMetrics && {
453
- thoughtDurationMs: snapshot.liveMetrics.firstAnswerAt === undefined ? undefined : Math.max(0, snapshot.liveMetrics.firstAnswerAt - snapshot.liveMetrics.startedAt),
459
+ appendEventGroup(snapshot.liveTurnId ?? "live", snapshot.liveEvents, {
460
+ todoRowLimit,
461
+ thoughtDurationMs: snapshot.liveMetrics?.firstAnswerAt === undefined ? undefined : Math.max(0, snapshot.liveMetrics?.firstAnswerAt - snapshot.liveMetrics.startedAt),
454
462
  totalDurationMs: liveCompletedAt === undefined ? undefined : Math.max(0, liveCompletedAt - snapshot.liveMetrics.startedAt),
455
463
  presentation: snapshot.livePresentation,
456
464
  turnId: snapshot.liveTurnId,
457
465
  status: snapshot.status,
458
466
  }, toolGroupsExpanded);
459
- for (const item of snapshot.pendingInputs)
460
- sources.push({ itemId: item.key, text: `${item.delivery === "queue" ? "[queued]" : "[guidance]"} you: ${item.content}` });
467
+ for (const item of snapshot.pendingInputs.filter(function queue(item) { return item.delivery === "queue"; }))
468
+ sources.push({ itemId: item.key, text: `${item.delivery === "queue" ? item.content + "\n[排队]" : "[引导]"} 等待当前步骤结束` });
461
469
  return sources;
462
470
  }
463
471
  /** 创建备用屏 source 增量缓存;历史 item 不变时只复用其 source,live tail 单独更新。 */
@@ -489,7 +497,7 @@ export function createTuiViewportSourceCache() {
489
497
  return previous.sources;
490
498
  }
491
499
  const sources = item.kind === "prompt"
492
- ? [{ itemId: item.key, text: item.content }]
500
+ ? [{ itemId: item.key, text: item.content, spans: createTuiViewportTextSpans(item.content, "accent") }]
493
501
  : item.kind === "turn"
494
502
  ? createTuiViewportEventSources(item.key, item.events, contentWidth, item)
495
503
  : [];
@@ -501,7 +509,7 @@ export function createTuiViewportSourceCache() {
501
509
  function getActivePromptSource(prompt) {
502
510
  if (activePrompt && activePrompt.key === prompt.key && activePrompt.content === prompt.content)
503
511
  return activePrompt.sources;
504
- const sources = [{ itemId: prompt.key, text: prompt.content }];
512
+ const sources = [{ itemId: prompt.key, text: prompt.content, spans: createTuiViewportTextSpans(prompt.content, "accent") }];
505
513
  activePrompt = { key: prompt.key, content: prompt.content, sources };
506
514
  return sources;
507
515
  }
@@ -512,36 +520,35 @@ export function createTuiViewportSourceCache() {
512
520
  stats.pendingHits += 1;
513
521
  return previous.sources;
514
522
  }
515
- const sources = [{ itemId: item.key, text: `${item.delivery === "queue" ? "[queued]" : "[guidance]"} you: ${item.content}` }];
523
+ const sources = [{ itemId: item.key, text: `${item.delivery === "queue" ? item.content + "\n[排队]" : "[引导]"} 等待当前步骤结束` }];
516
524
  pending.set(item.key, { key: item.key, content: item.content, delivery: item.delivery, sources });
517
525
  stats.pendingBuilds += 1;
518
526
  return sources;
519
527
  }
520
528
  /** 获取当前 live tail source;历史 transcript 不参与本次重建。 */
521
- function getLiveSources(snapshot, contentWidth, toolGroupsExpanded) {
529
+ function getLiveSources(snapshot, contentWidth, toolGroupsExpanded, todoRowLimit) {
522
530
  const liveTurnId = snapshot.liveTurnId;
523
531
  const metrics = snapshot.liveMetrics;
524
- if (live && live.turnId === liveTurnId && live.events === snapshot.liveEvents && live.presentation === snapshot.livePresentation && live.metrics === metrics && live.width === contentWidth && live.toolGroupsExpanded === toolGroupsExpanded) {
532
+ if (live && live.turnId === liveTurnId && live.events === snapshot.liveEvents && live.presentation === snapshot.livePresentation && live.metrics === metrics && live.width === contentWidth && live.toolGroupsExpanded === toolGroupsExpanded && live.todoRowLimit === todoRowLimit) {
525
533
  stats.liveHits += 1;
526
534
  return live.sources;
527
535
  }
528
536
  const liveCompletedAt = metrics?.completedAt;
529
- const timing = metrics
530
- ? {
531
- thoughtDurationMs: metrics.firstAnswerAt === undefined ? undefined : Math.max(0, metrics.firstAnswerAt - metrics.startedAt),
532
- totalDurationMs: liveCompletedAt === undefined ? undefined : Math.max(0, liveCompletedAt - metrics.startedAt),
533
- presentation: snapshot.livePresentation,
534
- turnId: liveTurnId,
535
- status: snapshot.status,
536
- }
537
- : undefined;
537
+ const timing = {
538
+ todoRowLimit,
539
+ thoughtDurationMs: metrics?.firstAnswerAt === undefined ? undefined : Math.max(0, metrics?.firstAnswerAt - metrics.startedAt),
540
+ totalDurationMs: liveCompletedAt === undefined ? undefined : Math.max(0, liveCompletedAt - metrics.startedAt),
541
+ presentation: snapshot.livePresentation,
542
+ turnId: liveTurnId,
543
+ status: snapshot.status,
544
+ };
538
545
  const sources = createTuiViewportEventSources(liveTurnId ?? "live", snapshot.liveEvents, contentWidth, timing, toolGroupsExpanded);
539
- live = { turnId: liveTurnId, events: snapshot.liveEvents, presentation: snapshot.livePresentation, metrics, width: contentWidth, toolGroupsExpanded, sources };
546
+ live = { turnId: liveTurnId, events: snapshot.liveEvents, presentation: snapshot.livePresentation, metrics, width: contentWidth, toolGroupsExpanded, todoRowLimit, sources };
540
547
  stats.liveBuilds += 1;
541
548
  return sources;
542
549
  }
543
550
  /** 返回按稳定历史、live tail 和 pending 输入拼接的当前 source 顺序。 */
544
- function getSources(snapshot, statusView, _approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false) {
551
+ function getSources(snapshot, statusView, _approvalSelectedIndex, contentWidth, toolGroupsExpanded = false, decorativeGrid = false, todoRowLimit = 10) {
545
552
  invalidateWidth(contentWidth);
546
553
  if (!brand || brand.statusView !== statusView) {
547
554
  brand = { statusView, source: createTuiViewportBrandSource(statusView) };
@@ -561,8 +568,8 @@ export function createTuiViewportSourceCache() {
561
568
  }
562
569
  if (snapshot.activePrompt && !renderedActivePrompt)
563
570
  sources.push(...getActivePromptSource(snapshot.activePrompt));
564
- sources.push(...getLiveSources(snapshot, contentWidth, toolGroupsExpanded));
565
- for (const item of snapshot.pendingInputs)
571
+ sources.push(...getLiveSources(snapshot, contentWidth, toolGroupsExpanded, todoRowLimit));
572
+ for (const item of snapshot.pendingInputs.filter(function queue(item) { return item.delivery === "queue"; }))
566
573
  sources.push(...getPendingSource(item));
567
574
  return sources;
568
575
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** 备用屏视口:按应用层视口裁剪时间线并渲染自绘滚动条。 */
3
3
  import { memo } from "react";
4
4
  import { Box, Text } from "ink";
5
- import { CLI_TUI_COLORS, TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_DIFF_ADDED_COLOR, TUI_DIFF_REMOVED_COLOR, TUI_TOOL_ERROR_COLOR } from "../../design-tokens.js";
5
+ import { CLI_TUI_COLORS, TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_DIFF_ADDED_COLOR, TUI_DIFF_REMOVED_COLOR, TUI_TOOL_ERROR_BG_COLOR, TUI_TOOL_ERROR_COLOR, TUI_TOOL_ERROR_TEXT_COLOR } from "../../design-tokens.js";
6
6
  import { getTuiVisualLineSelectionRange } from "./selection.js";
7
7
  /** 将备用屏时间线的品牌样式映射回 Design.md 规定的 ANSI token。 */
8
8
  function getTuiViewportSpanColor(style, useColor) {
@@ -28,6 +28,8 @@ function getTuiViewportSpanColor(style, useColor) {
28
28
  return TUI_TOOL_ERROR_COLOR;
29
29
  if (style === "chip-active")
30
30
  return TUI_CHIP_TEXT_COLOR;
31
+ if (style === "chip-error")
32
+ return TUI_TOOL_ERROR_TEXT_COLOR;
31
33
  return undefined;
32
34
  }
33
35
  /** 取得备用屏 span 的背景与粗体强调;活动工具徽标 chip 使用强调背景 + 深色粗体文字。 */
@@ -36,6 +38,8 @@ function getTuiViewportSpanEmphasis(style, useColor) {
36
38
  return {};
37
39
  if (style === "chip-active")
38
40
  return { backgroundColor: TUI_ACCENT_BG_COLOR, bold: true };
41
+ if (style === "chip-error")
42
+ return { backgroundColor: TUI_TOOL_ERROR_BG_COLOR, bold: true };
39
43
  return {};
40
44
  }
41
45
  /** 在视口的已换行文本中保留来源样式,并与鼠标划选反显组合。 */
package/dist/types.d.ts CHANGED
@@ -117,6 +117,15 @@ export type CliAction =
117
117
  } | {
118
118
  type: "set_permission_mode";
119
119
  mode: CliPermissionMode;
120
+ }
121
+ /** 查看当前 Session/Workspace 可见的持久授权(Talk-Plan1013 Stage 4)。 */
122
+ | {
123
+ type: "list_permission_grants";
124
+ }
125
+ /** 按 toolName+constraint 精确撤销一条持久授权;constraint 由用户从列表复制,不做模糊匹配。 */
126
+ | {
127
+ type: "revoke_permission_grant";
128
+ constraint: string;
120
129
  } | {
121
130
  type: "open_model_picker";
122
131
  } | {
@@ -348,6 +357,8 @@ export type CliPublicEvent = {
348
357
  toolName: string;
349
358
  reason: string;
350
359
  risk?: string;
360
+ /** 冻结审批约束;Plain/JSONL 与历史行据此区分普通批准与无沙箱批准。 */
361
+ constraint?: string;
351
362
  allowedDecisions?: Array<Extract<CliAction, {
352
363
  type: "approve";
353
364
  }>["decision"]>;
@@ -436,6 +447,8 @@ export interface CliProjection {
436
447
  toolName: string;
437
448
  reason: string;
438
449
  risk?: string;
450
+ /** 冻结审批约束;沙箱降级卡(no-sandbox-exec:v1)据此展示无隔离警告。 */
451
+ constraint?: string;
439
452
  allowedDecisions?: Array<Extract<CliAction, {
440
453
  type: "approve";
441
454
  }>["decision"]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autopilot-agent/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -41,9 +41,9 @@
41
41
  "test:watch": "vitest --config vitest.config.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@autopilot-agent/adapters": "0.1.0",
45
- "@autopilot-agent/core": "0.1.0",
46
- "@autopilot-agent/presentation": "0.1.0",
44
+ "@autopilot-agent/adapters": "0.2.0",
45
+ "@autopilot-agent/core": "0.2.0",
46
+ "@autopilot-agent/presentation": "0.2.0",
47
47
  "ink": "6.8.0",
48
48
  "react": "19.2.4",
49
49
  "string-width": "^8.1.1",