@autopilot-agent/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +52 -2
  2. package/dist/activity-animation.d.ts +59 -0
  3. package/dist/activity-animation.js +148 -0
  4. package/dist/animation-clock.d.ts +22 -0
  5. package/dist/animation-clock.js +76 -0
  6. package/dist/composition-root.d.ts +105 -0
  7. package/dist/composition-root.js +600 -0
  8. package/dist/context-report.d.ts +20 -0
  9. package/dist/context-report.js +173 -0
  10. package/dist/controller.d.ts +79 -0
  11. package/dist/controller.js +741 -0
  12. package/dist/design-tokens.d.ts +38 -0
  13. package/dist/design-tokens.js +38 -0
  14. package/dist/error-diagnostics.d.ts +8 -0
  15. package/dist/error-diagnostics.js +62 -0
  16. package/dist/event-projector.d.ts +26 -0
  17. package/dist/event-projector.js +335 -0
  18. package/dist/file-mention-context.d.ts +34 -0
  19. package/dist/file-mention-context.js +173 -0
  20. package/dist/index.d.ts +19 -0
  21. package/dist/index.js +14 -0
  22. package/dist/main.d.ts +40 -0
  23. package/dist/main.js +543 -0
  24. package/dist/parser.d.ts +17 -0
  25. package/dist/parser.js +400 -0
  26. package/dist/renderer.d.ts +9 -0
  27. package/dist/renderer.js +62 -0
  28. package/dist/scroll-viewport.d.ts +22 -0
  29. package/dist/scroll-viewport.js +62 -0
  30. package/dist/termination-format.d.ts +4 -0
  31. package/dist/termination-format.js +18 -0
  32. package/dist/timeline-lines.d.ts +38 -0
  33. package/dist/timeline-lines.js +104 -0
  34. package/dist/tui/app.d.ts +30 -0
  35. package/dist/tui/app.js +413 -0
  36. package/dist/tui/approval.d.ts +19 -0
  37. package/dist/tui/approval.js +41 -0
  38. package/dist/tui/brand/brand-header.d.ts +20 -0
  39. package/dist/tui/brand/brand-header.js +22 -0
  40. package/dist/tui/brand/intro.d.ts +12 -0
  41. package/dist/tui/brand/intro.js +88 -0
  42. package/dist/tui/brand/pixel.d.ts +8 -0
  43. package/dist/tui/brand/pixel.js +50 -0
  44. package/dist/tui/constants.d.ts +56 -0
  45. package/dist/tui/constants.js +57 -0
  46. package/dist/tui/context.d.ts +11 -0
  47. package/dist/tui/context.js +12 -0
  48. package/dist/tui/diff-stats.d.ts +8 -0
  49. package/dist/tui/diff-stats.js +7 -0
  50. package/dist/tui/format.d.ts +75 -0
  51. package/dist/tui/format.js +178 -0
  52. package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
  53. package/dist/tui/hooks/use-file-mention-index.js +59 -0
  54. package/dist/tui/hooks/use-pickers.d.ts +47 -0
  55. package/dist/tui/hooks/use-pickers.js +288 -0
  56. package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
  57. package/dist/tui/hooks/use-prompt-input.js +258 -0
  58. package/dist/tui/hooks/use-status-view.d.ts +8 -0
  59. package/dist/tui/hooks/use-status-view.js +13 -0
  60. package/dist/tui/hooks/use-viewport.d.ts +22 -0
  61. package/dist/tui/hooks/use-viewport.js +117 -0
  62. package/dist/tui/input/file-mention.d.ts +33 -0
  63. package/dist/tui/input/file-mention.js +112 -0
  64. package/dist/tui/input/graphemes.d.ts +5 -0
  65. package/dist/tui/input/graphemes.js +10 -0
  66. package/dist/tui/input/keys.d.ts +16 -0
  67. package/dist/tui/input/keys.js +58 -0
  68. package/dist/tui/input/prompt-layout.d.ts +27 -0
  69. package/dist/tui/input/prompt-layout.js +84 -0
  70. package/dist/tui/input-area.d.ts +53 -0
  71. package/dist/tui/input-area.js +89 -0
  72. package/dist/tui/jobs.d.ts +10 -0
  73. package/dist/tui/jobs.js +24 -0
  74. package/dist/tui/lifecycle.d.ts +23 -0
  75. package/dist/tui/lifecycle.js +114 -0
  76. package/dist/tui/markdown/diff.d.ts +8 -0
  77. package/dist/tui/markdown/diff.js +19 -0
  78. package/dist/tui/markdown/inline.d.ts +4 -0
  79. package/dist/tui/markdown/inline.js +32 -0
  80. package/dist/tui/markdown/markdown.d.ts +17 -0
  81. package/dist/tui/markdown/markdown.js +117 -0
  82. package/dist/tui/markdown/table.d.ts +19 -0
  83. package/dist/tui/markdown/table.js +74 -0
  84. package/dist/tui/performance-diagnostics.d.ts +7 -0
  85. package/dist/tui/performance-diagnostics.js +57 -0
  86. package/dist/tui/pickers/effort.d.ts +10 -0
  87. package/dist/tui/pickers/effort.js +12 -0
  88. package/dist/tui/pickers/file-mention.d.ts +11 -0
  89. package/dist/tui/pickers/file-mention.js +22 -0
  90. package/dist/tui/pickers/model.d.ts +10 -0
  91. package/dist/tui/pickers/model.js +12 -0
  92. package/dist/tui/pickers/permission.d.ts +10 -0
  93. package/dist/tui/pickers/permission.js +13 -0
  94. package/dist/tui/pickers/skill.d.ts +13 -0
  95. package/dist/tui/pickers/skill.js +25 -0
  96. package/dist/tui/pickers/slash.d.ts +10 -0
  97. package/dist/tui/pickers/slash.js +11 -0
  98. package/dist/tui/prompt-input.d.ts +11 -0
  99. package/dist/tui/prompt-input.js +73 -0
  100. package/dist/tui/render-mode.d.ts +20 -0
  101. package/dist/tui/render-mode.js +31 -0
  102. package/dist/tui/run.d.ts +6 -0
  103. package/dist/tui/run.js +237 -0
  104. package/dist/tui/status-line.d.ts +16 -0
  105. package/dist/tui/status-line.js +37 -0
  106. package/dist/tui/store.d.ts +4 -0
  107. package/dist/tui/store.js +380 -0
  108. package/dist/tui/timeline/live.d.ts +16 -0
  109. package/dist/tui/timeline/live.js +153 -0
  110. package/dist/tui/timeline/presentation-timeline.d.ts +15 -0
  111. package/dist/tui/timeline/presentation-timeline.js +128 -0
  112. package/dist/tui/timeline/progress.d.ts +12 -0
  113. package/dist/tui/timeline/progress.js +43 -0
  114. package/dist/tui/timeline/static-timeline.d.ts +13 -0
  115. package/dist/tui/timeline/static-timeline.js +131 -0
  116. package/dist/tui/timeline/tool-tree.d.ts +7 -0
  117. package/dist/tui/timeline/tool-tree.js +25 -0
  118. package/dist/tui/timeline/turn-items.d.ts +12 -0
  119. package/dist/tui/timeline/turn-items.js +28 -0
  120. package/dist/tui/timeline/turn.d.ts +19 -0
  121. package/dist/tui/timeline/turn.js +21 -0
  122. package/dist/tui/todo-list.d.ts +50 -0
  123. package/dist/tui/todo-list.js +120 -0
  124. package/dist/tui/transcript.d.ts +24 -0
  125. package/dist/tui/transcript.js +34 -0
  126. package/dist/tui/trust-dialog.d.ts +51 -0
  127. package/dist/tui/trust-dialog.js +127 -0
  128. package/dist/tui/types.d.ts +189 -0
  129. package/dist/tui/types.js +5 -0
  130. package/dist/tui/viewport/selection.d.ts +27 -0
  131. package/dist/tui/viewport/selection.js +79 -0
  132. package/dist/tui/viewport/sources.d.ts +29 -0
  133. package/dist/tui/viewport/sources.js +592 -0
  134. package/dist/tui/viewport/viewport.d.ts +12 -0
  135. package/dist/tui/viewport/viewport.js +91 -0
  136. package/dist/tui-input-history.d.ts +23 -0
  137. package/dist/tui-input-history.js +42 -0
  138. package/dist/tui-input-router.d.ts +24 -0
  139. package/dist/tui-input-router.js +115 -0
  140. package/dist/tui.d.ts +15 -0
  141. package/dist/tui.js +13 -0
  142. package/dist/types.d.ts +467 -0
  143. package/dist/types.js +7 -0
  144. package/package.json +58 -4
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from "ink";
3
+ import stringWidth from "string-width";
4
+ import { selectTimeline } from "@autopilot-agent/presentation";
5
+ import { TUI_ACCENT_COLOR } from "../design-tokens.js";
6
+ import { splitTuiInputGraphemes } from "./input/graphemes.js";
7
+ /** 展开态按终端高度最多显示的任务行数;超出部分以隐藏计数提示。 */
8
+ const TUI_TODO_EXPANDED_ROW_LIMIT = 10;
9
+ /** 展开清单里每行文字可用的最大列数下限,避免极窄终端下行宽为负。 */
10
+ const TUI_TODO_MIN_ROW_WIDTH = 12;
11
+ /** 取得 Todo 状态的中文文字标签,供 screen reader 与 NO_COLOR 场景朗读。 */
12
+ export function getTuiTodoStatusLabel(status) {
13
+ if (status === "in_progress")
14
+ return "进行中";
15
+ if (status === "completed")
16
+ return "已完成";
17
+ return "待处理";
18
+ }
19
+ /** 从共享 Presentation 读取当前 Turn 任务链的 in_progress activeForm,作为活动状态的稳定文案。 */
20
+ export function getTuiTodoActiveForm(presentation, turnId) {
21
+ if (!presentation)
22
+ return undefined;
23
+ const todoItem = selectTimeline(presentation).find(function findTodoItem(item) {
24
+ return item.kind === "todo" && "turnId" in item && item.turnId === turnId;
25
+ });
26
+ if (todoItem?.kind !== "todo")
27
+ return undefined;
28
+ return todoItem.todos.find(function findActiveTodo(todo) { return todo.status === "in_progress"; })?.activeForm;
29
+ }
30
+ /** 取得 Todo 状态的纯文本 glyph,颜色不是唯一状态载体。 */
31
+ export function getTuiTodoStatusGlyph(status) {
32
+ if (status === "in_progress")
33
+ return "■";
34
+ if (status === "completed")
35
+ return "✓";
36
+ return "□";
37
+ }
38
+ /** 按显示宽度做 grapheme 安全截断,中英文与组合 emoji 不会截出半字。 */
39
+ export function truncateTuiTodoText(text, maximumWidth) {
40
+ if (stringWidth(text) <= maximumWidth)
41
+ return text;
42
+ const graphemes = splitTuiInputGraphemes(text);
43
+ let width = 0;
44
+ let output = "";
45
+ for (const grapheme of graphemes) {
46
+ const nextWidth = width + stringWidth(grapheme);
47
+ if (nextWidth > maximumWidth - 1)
48
+ break;
49
+ width = nextWidth;
50
+ output += grapheme;
51
+ }
52
+ return `${output}…`;
53
+ }
54
+ /** 按优先级(in_progress → 最近完成 → pending)在行数上限内挑选可见任务,保持原清单顺序展示。 */
55
+ export function orderTuiTodoItems(todos, limit = TUI_TODO_EXPANDED_ROW_LIMIT) {
56
+ if (todos.length <= limit)
57
+ return { shown: [...todos], hidden: 0 };
58
+ // 超限时为 hidden 摘要预留一行,保证展开态总行数不会超过 limit。
59
+ const taskLimit = Math.max(1, limit - 1);
60
+ const inProgress = todos.filter(function isInProgress(todo) { return todo.status === "in_progress"; });
61
+ const recentCompletedFirst = [...todos].reverse().filter(function isCompleted(todo) { return todo.status === "completed"; });
62
+ const pending = todos.filter(function isPending(todo) { return todo.status === "pending"; });
63
+ const keep = new Set();
64
+ for (const candidate of [...inProgress, ...recentCompletedFirst, ...pending]) {
65
+ if (keep.size >= taskLimit)
66
+ break;
67
+ keep.add(candidate);
68
+ }
69
+ const shown = todos.filter(function isKept(todo) { return keep.has(todo); });
70
+ return { shown, hidden: todos.length - shown.length };
71
+ }
72
+ /** 生成 Todo 视图的共享展示数据;紧凑态一行,展开态为 header + 任务行 + 隐藏计数。 */
73
+ export function getTuiTodoDisplay(item, options) {
74
+ if (item.todos.length === 0)
75
+ return undefined;
76
+ const total = item.todos.length;
77
+ const completed = item.counts.completed;
78
+ const activeTodo = item.todos.find(function findActiveTodo(todo) { return todo.status === "in_progress"; });
79
+ if (!options.expanded) {
80
+ const headerWidth = Math.max(TUI_TODO_MIN_ROW_WIDTH, options.columns - 2);
81
+ if (!activeTodo)
82
+ return { header: `Todo ${completed}/${total}`, rows: [], hidden: 0 };
83
+ // activeForm 的可用列数需扣除 'Todo x/y · ' 前缀,保证紧凑行整体不超出终端宽度。
84
+ const prefix = `Todo ${completed}/${total} · `;
85
+ const activeFormWidth = headerWidth - stringWidth(prefix);
86
+ return {
87
+ header: `${prefix}${activeFormWidth > 0 ? truncateTuiTodoText(activeTodo.activeForm, activeFormWidth) : ""}`,
88
+ activeForm: activeTodo.activeForm,
89
+ rows: [],
90
+ hidden: 0,
91
+ };
92
+ }
93
+ const { shown, hidden } = orderTuiTodoItems(item.todos, options.rowLimit ?? TUI_TODO_EXPANDED_ROW_LIMIT);
94
+ const rowWidth = Math.max(TUI_TODO_MIN_ROW_WIDTH, options.columns - 4);
95
+ const rows = shown.map(function createDisplayRow(todo) {
96
+ return { status: todo.status, text: truncateTuiTodoText(todo.content, rowWidth) };
97
+ });
98
+ return {
99
+ header: `Todo ${completed}/${total}`,
100
+ activeForm: activeTodo?.activeForm,
101
+ rows: hidden > 0 ? [...rows, { status: "pending", text: `${hidden} hidden`, hidden: true }] : rows,
102
+ hidden,
103
+ };
104
+ }
105
+ /** 渲染 Todo 任务链的 Ink 视图;screen reader 时每行前缀中文状态文字。 */
106
+ export function TuiTodoList(props) {
107
+ const display = getTuiTodoDisplay(props.item, { expanded: props.expanded, screenReader: props.screenReader, columns: props.columns, rowLimit: props.rowLimit });
108
+ if (!display)
109
+ return null;
110
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: display.header }), display.rows.map(function renderTodoRow(row, index) {
111
+ if (row.hidden)
112
+ return _jsxs(Text, { dimColor: props.useColor, children: ["+", row.text] }, `todo-row-${index}`);
113
+ const label = props.screenReader || !props.useColor ? `[${getTuiTodoStatusLabel(row.status)}] ` : `${getTuiTodoStatusGlyph(row.status)} `;
114
+ if (row.status === "completed")
115
+ return _jsxs(Text, { dimColor: props.useColor, strikethrough: true, children: [label, row.text] }, `todo-row-${index}`);
116
+ if (row.status === "in_progress")
117
+ return _jsxs(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: [label, row.text] }, `todo-row-${index}`);
118
+ return _jsxs(Text, { children: [label, row.text] }, `todo-row-${index}`);
119
+ }), props.showToggleHint ? _jsxs(Text, { dimColor: props.useColor, children: ["Ctrl+O ", props.expanded ? "收起" : "展开"] }) : null] }));
120
+ }
@@ -0,0 +1,24 @@
1
+ /** transcript 节点渲染:用户 prompt、通知、完整历史 Turn 与动态等待输入。 */
2
+ import { type ReactNode } from "react";
3
+ import type { CliTuiPendingInput, CliTuiTranscriptItem } from "./types.js";
4
+ /** 渲染一个用户 prompt;当前 Turn 会暂留在动态区,避免历史区固化时机丢失。@文件 片段用强调色加粗。 */
5
+ export declare function TuiPromptLine(props: {
6
+ content: string;
7
+ useColor: boolean;
8
+ columns: number;
9
+ keyValue?: string;
10
+ }): ReactNode;
11
+ /** 渲染已经完成的 prompt、notice 或完整 Turn。 */
12
+ export declare function TuiTranscriptItem(props: {
13
+ item: CliTuiTranscriptItem;
14
+ useColor: boolean;
15
+ columns: number;
16
+ screenReader?: boolean;
17
+ }): ReactNode;
18
+ /** 历史节点保持引用稳定;回答流动时只重新计算当前 Turn。 */
19
+ export declare const MemoizedTuiTranscriptItem: import("react").MemoExoticComponent<typeof TuiTranscriptItem>;
20
+ /** queue 在等待区显示原文;steer 正文已按提交位置留在所属轮。 */
21
+ export declare function TuiPendingInputItem(props: {
22
+ item: CliTuiPendingInput;
23
+ useColor: boolean;
24
+ }): ReactNode;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** transcript 节点渲染:用户 prompt、通知、完整历史 Turn 与动态等待输入。 */
3
+ import { memo } from "react";
4
+ import { Box, Text } from "ink";
5
+ import { TUI_ACCENT_COLOR, TUI_PROMPT_BACKGROUND_COLOR, TUI_WAITING_BACKGROUND_COLOR } from "../design-tokens.js";
6
+ import { splitTuiFileMentionSegments } from "./input/file-mention.js";
7
+ import { renderTimeline } from "./timeline/static-timeline.js";
8
+ /** 渲染一个用户 prompt;当前 Turn 会暂留在动态区,避免历史区固化时机丢失。@文件 片段用强调色加粗。 */
9
+ export function TuiPromptLine(props) {
10
+ const segments = splitTuiFileMentionSegments(props.content);
11
+ return (_jsx(Box, { width: props.columns, paddingX: 1, backgroundColor: props.useColor ? TUI_PROMPT_BACKGROUND_COLOR : undefined, children: _jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: segments.map(function renderPromptSegment(segment, index) {
12
+ if (!segment.isMention)
13
+ return _jsx(Text, { children: segment.text }, index);
14
+ return _jsx(Text, { bold: props.useColor, children: segment.text }, index);
15
+ }) }) }, props.keyValue));
16
+ }
17
+ /** 渲染已经完成的 prompt、notice 或完整 Turn。 */
18
+ export function TuiTranscriptItem(props) {
19
+ const item = props.item;
20
+ if (item.kind === "prompt") {
21
+ return _jsx(TuiPromptLine, { content: item.content, useColor: props.useColor, columns: props.columns, keyValue: item.key }, item.key);
22
+ }
23
+ if (item.kind === "notice") {
24
+ return _jsxs(Text, { dimColor: props.useColor, children: ["info: ", item.message] }, item.key);
25
+ }
26
+ return (_jsx(Box, { flexDirection: "column", children: renderTimeline(item.events, props.useColor, item, Math.max(1, props.columns - 2), props.screenReader ?? false) }, item.key));
27
+ }
28
+ /** 历史节点保持引用稳定;回答流动时只重新计算当前 Turn。 */
29
+ export const MemoizedTuiTranscriptItem = memo(TuiTranscriptItem);
30
+ /** queue 在等待区显示原文;steer 正文已按提交位置留在所属轮。 */
31
+ export function TuiPendingInputItem(props) {
32
+ const label = props.item.delivery === "queue" ? "[排队]" : "[引导]";
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
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * CLI 首次 Trust 确认对话框。
3
+ *
4
+ * 只在信任建立前渲染 canonical 工作区与 Yes/No 选择;不读取任何工作区配置、
5
+ * 不初始化 Provider,也不创建 Runtime/Project/Session。授权只通过用户显式确认。
6
+ */
7
+ import { type ReactNode } from "react";
8
+ import { type Key } from "ink";
9
+ /** Trust 对话框固定选项;Yes 是唯一授权动作。 */
10
+ export declare const TRUST_DIALOG_OPTIONS: readonly [{
11
+ readonly value: "grant";
12
+ readonly label: "是,我信任此工作区";
13
+ }, {
14
+ readonly value: "decline";
15
+ readonly label: "否,退出";
16
+ }];
17
+ /** 将按键归一为 Trust 对话框的焦点移动或确认/取消动作。 */
18
+ export declare function resolveTrustDialogNavigation(input: string, key: Key, current: number): {
19
+ next: number;
20
+ action?: "confirm" | "cancel";
21
+ };
22
+ /** 渲染首次 Trust 确认卡;直角单线、warning 语义,路径只在交互 TTY 显示。 */
23
+ export declare function TuiTrustDialog(props: {
24
+ workspaceRoot: string;
25
+ selectedIndex: number;
26
+ useColor: boolean;
27
+ screenReader: boolean;
28
+ }): ReactNode;
29
+ /** Trust 对话框的信号来源;与 main.ts 的 CliSignalSource 结构兼容。 */
30
+ export interface CliTrustDialogSignalSource {
31
+ on(signal: "SIGINT" | "SIGTERM", listener: () => void): void;
32
+ removeListener(signal: "SIGINT" | "SIGTERM", listener: () => void): void;
33
+ }
34
+ /** 首次 Trust 对话框的运行选项。 */
35
+ export interface CliTrustDialogOptions {
36
+ workspaceRoot: string;
37
+ streams: {
38
+ stdout: {
39
+ write(chunk: string): void;
40
+ isTTY?: boolean;
41
+ columns?: number;
42
+ };
43
+ stdin: NodeJS.ReadStream;
44
+ };
45
+ env?: NodeJS.ProcessEnv;
46
+ signalSource?: CliTrustDialogSignalSource;
47
+ }
48
+ /** 首次 Trust 决策;granted 表示用户显式授权。 */
49
+ export type CliTrustDecision = "granted" | "declined";
50
+ /** 在交互 TTY 渲染首次 Trust 对话框并等待用户决定。 */
51
+ export declare function runCliTrustDialog(options: CliTrustDialogOptions): Promise<CliTrustDecision>;
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * CLI 首次 Trust 确认对话框。
4
+ *
5
+ * 只在信任建立前渲染 canonical 工作区与 Yes/No 选择;不读取任何工作区配置、
6
+ * 不初始化 Provider,也不创建 Runtime/Project/Session。授权只通过用户显式确认。
7
+ */
8
+ import { useRef, useState } from "react";
9
+ import { Box, Text, render, useInput } from "ink";
10
+ import { TUI_ACCENT_COLOR } from "../design-tokens.js";
11
+ import { parseTuiControlAction } from "./input/keys.js";
12
+ import { createRoutedTuiStdin, createTuiTerminalLifecycle } from "./lifecycle.js";
13
+ import { resolveNoColor, resolveScreenReader } from "./render-mode.js";
14
+ /** Trust 对话框固定选项;Yes 是唯一授权动作。 */
15
+ export const TRUST_DIALOG_OPTIONS = [
16
+ { value: "grant", label: "是,我信任此工作区" },
17
+ { value: "decline", label: "否,退出" },
18
+ ];
19
+ /** 将按键归一为 Trust 对话框的焦点移动或确认/取消动作。 */
20
+ export function resolveTrustDialogNavigation(input, key, current) {
21
+ const control = parseTuiControlAction(input, key);
22
+ if (control === "interrupt" || control === "cancel")
23
+ return { next: current, action: "cancel" };
24
+ if (key.upArrow)
25
+ return { next: Math.max(0, current - 1) };
26
+ if (key.downArrow)
27
+ return { next: Math.min(TRUST_DIALOG_OPTIONS.length - 1, current + 1) };
28
+ if (key.return)
29
+ return { next: current, action: "confirm" };
30
+ return { next: current };
31
+ }
32
+ /** 渲染首次 Trust 确认卡;直角单线、warning 语义,路径只在交互 TTY 显示。 */
33
+ export function TuiTrustDialog(props) {
34
+ const selectedIndex = Math.max(0, Math.min(props.selectedIndex, TRUST_DIALOG_OPTIONS.length - 1));
35
+ 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: "\u8BBF\u95EE\u5DE5\u4F5C\u533A" }), _jsx(Text, { children: props.workspaceRoot }), _jsx(Text, { children: "\u4EC5\u5F53\u8FD9\u662F\u4F60\u521B\u5EFA\u6216\u4FE1\u4EFB\u7684\u9879\u76EE\u65F6\u7EE7\u7EED\u3002" }), _jsx(Text, { children: "Agent \u5C06\u80FD\u591F\u5728\u6B64\u5DE5\u4F5C\u533A\u5185\u8BFB\u53D6\u3001\u7F16\u8F91\u548C\u6267\u884C\u6587\u4EF6\u3002" }), TRUST_DIALOG_OPTIONS.map(function renderTrustOption(option, index) {
36
+ const selected = index === selectedIndex;
37
+ return _jsxs(Text, { inverse: selected && props.useColor, children: [selected ? "›" : " ", " ", option.label, selected && props.screenReader ? "(已选择)" : ""] }, option.value);
38
+ }), _jsx(Text, { dimColor: props.useColor, children: "\u2191/\u2193 \u9009\u62E9 \u00B7 Enter \u786E\u8BA4 \u00B7 Esc/Ctrl+C \u9000\u51FA" })] }));
39
+ }
40
+ /** 在交互 TTY 渲染首次 Trust 对话框并等待用户决定。 */
41
+ export function runCliTrustDialog(options) {
42
+ return new Promise(function runTrustDialog(resolve) {
43
+ const env = options.env ?? process.env;
44
+ const noColor = resolveNoColor(env);
45
+ const screenReader = resolveScreenReader(env);
46
+ const routedStdin = createRoutedTuiStdin(options.streams.stdin);
47
+ const terminalLifecycle = createTuiTerminalLifecycle(options.streams.stdout, "inline");
48
+ let decision = "declined";
49
+ let settled = false;
50
+ let instance;
51
+ /** 把真实 TTY 的按键数据原样转发给 Ink 专用输入流;Trust 对话框不参与鼠标路由。 */
52
+ function forwardTrustInput(chunk) {
53
+ routedStdin.write(chunk);
54
+ }
55
+ /** stdin 到达 EOF 时结束 Ink 输入流。 */
56
+ function finishTrustInput() {
57
+ routedStdin.end();
58
+ }
59
+ /** 移除真实 stdin 转发监听并结束 Ink 输入流,所有退出路径只清理一次。 */
60
+ function detachTrustInput() {
61
+ options.streams.stdin.removeListener("data", forwardTrustInput);
62
+ options.streams.stdin.removeListener("end", finishTrustInput);
63
+ routedStdin.end();
64
+ }
65
+ options.streams.stdin.on("data", forwardTrustInput);
66
+ options.streams.stdin.once("end", finishTrustInput);
67
+ /** 记录最终决定并请求 Ink 卸载;终端恢复在 waitUntilExit 后统一执行。 */
68
+ function settle(next) {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ decision = next;
73
+ // Inline 模式下 unmount 会保留最后一帧;先清理,避免 Trust 卡片残留在后续 TUI 上方。
74
+ instance?.clear();
75
+ instance?.unmount();
76
+ }
77
+ function handleSigint() { settle("declined"); }
78
+ function handleSigterm() { settle("declined"); }
79
+ options.signalSource?.on("SIGINT", handleSigint);
80
+ options.signalSource?.on("SIGTERM", handleSigterm);
81
+ /** 对话框主体:焦点与决策只在本组件内完成,不接触 Runtime。 */
82
+ function TrustDialogApp() {
83
+ const [selectedIndex, setSelectedIndex] = useState(0);
84
+ const selectedIndexRef = useRef(0);
85
+ function updateSelectedIndex(next) {
86
+ selectedIndexRef.current = next;
87
+ setSelectedIndex(next);
88
+ }
89
+ useInput(function handleTrustInput(input, key) {
90
+ const navigation = resolveTrustDialogNavigation(input, key, selectedIndexRef.current);
91
+ if (navigation.action === "confirm")
92
+ settle(selectedIndexRef.current === 0 ? "granted" : "declined");
93
+ else if (navigation.action === "cancel")
94
+ settle("declined");
95
+ else if (navigation.next !== selectedIndexRef.current)
96
+ updateSelectedIndex(navigation.next);
97
+ });
98
+ return _jsx(TuiTrustDialog, { workspaceRoot: options.workspaceRoot, selectedIndex: selectedIndex, useColor: !noColor && !screenReader, screenReader: screenReader });
99
+ }
100
+ try {
101
+ terminalLifecycle.enter();
102
+ instance = render(_jsx(TrustDialogApp, {}), {
103
+ stdout: options.streams.stdout,
104
+ stdin: routedStdin,
105
+ exitOnCtrlC: false,
106
+ patchConsole: true,
107
+ incrementalRendering: false,
108
+ isScreenReaderEnabled: screenReader,
109
+ });
110
+ }
111
+ catch {
112
+ terminalLifecycle.restore();
113
+ detachTrustInput();
114
+ options.signalSource?.removeListener("SIGINT", handleSigint);
115
+ options.signalSource?.removeListener("SIGTERM", handleSigterm);
116
+ resolve("declined");
117
+ return;
118
+ }
119
+ void instance.waitUntilExit().then(function finishTrustDialog() {
120
+ terminalLifecycle.restore();
121
+ detachTrustInput();
122
+ options.signalSource?.removeListener("SIGINT", handleSigint);
123
+ options.signalSource?.removeListener("SIGTERM", handleSigterm);
124
+ resolve(decision);
125
+ });
126
+ });
127
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * 仅 TUI 层自有类型。Controller / 投影 / 事件等宿主协议类型统一从
3
+ * ../types.js 与 ../controller.js 复用,不在这里重复声明领域协议。
4
+ */
5
+ import type { CliActivityState } from "../activity-animation.js";
6
+ import type { AgentPresentationState } from "@autopilot-agent/presentation";
7
+ import type { ContextUsageView } from "@autopilot-agent/core";
8
+ import type { CliController, CliControllerResult } from "../controller.js";
9
+ import type { TuiSgrMouseEvent } from "../tui-input-router.js";
10
+ import type { CliAction, CliExitCode, CliProjection, CliPublicEvent, CliQueuedInputStarted, CliTurnStatus } from "../types.js";
11
+ /** TUI 中可固化的 transcript 节点。 */
12
+ export type CliTuiTranscriptItem = {
13
+ kind: "prompt";
14
+ key: string;
15
+ content: string;
16
+ } | {
17
+ kind: "notice";
18
+ key: string;
19
+ message: string;
20
+ } | {
21
+ kind: "turn";
22
+ key: string;
23
+ turnId: string;
24
+ events: CliTuiTimelineEvent[];
25
+ presentation?: AgentPresentationState;
26
+ status: CliTurnStatus;
27
+ thoughtDurationMs?: number;
28
+ totalDurationMs?: number;
29
+ };
30
+ /** TUI 本地补充的时间线节点;不回流 Controller、Runtime 或持久化事件流。 */
31
+ export type CliTuiTimelineEvent = CliPublicEvent | {
32
+ type: "tui_input_consumed";
33
+ seq: number;
34
+ turnId: string;
35
+ inputId: string;
36
+ content: string;
37
+ delivery: "steer" | "queue";
38
+ consumed?: boolean;
39
+ cancelled?: boolean;
40
+ position?: CliTuiInputPosition;
41
+ } | {
42
+ type: "tui_approval_submitted";
43
+ seq: number;
44
+ turnId: string;
45
+ approvalId: string;
46
+ decision: Extract<CliAction, {
47
+ type: "approve";
48
+ }>["decision"];
49
+ };
50
+ /** 动态区中尚未被 Core 消费的引导或排队输入。 */
51
+ export interface CliTuiPendingInput {
52
+ key: string;
53
+ content: string;
54
+ inputId: string;
55
+ delivery: "steer" | "queue";
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
+ }
67
+ /** Turn 的本地展示计时;只用于 TUI 文案,不进入 Runtime 或持久化事件。 */
68
+ export interface CliTuiTurnMetrics {
69
+ startedAt: number;
70
+ firstAnswerAt?: number;
71
+ completedAt?: number;
72
+ }
73
+ /** 当前 Turn 的用户输入;动态区保留它,避免历史区固化时机导致 prompt 消失。 */
74
+ export interface CliTuiActivePrompt {
75
+ key: string;
76
+ content: string;
77
+ turnId?: string;
78
+ }
79
+ /** TUI 的不可变快照;快照不包含 Runtime 私有字段。 */
80
+ export interface CliTuiSnapshot {
81
+ transcript: CliTuiTranscriptItem[];
82
+ activePrompt?: CliTuiActivePrompt;
83
+ pendingInputs: CliTuiPendingInput[];
84
+ liveTurnId?: string;
85
+ liveEvents: CliTuiTimelineEvent[];
86
+ /** 当前 Turn 的共享 Runtime Presentation 状态,供 inline 与备用屏使用同一 selector。 */
87
+ livePresentation?: AgentPresentationState;
88
+ liveMetrics?: CliTuiTurnMetrics;
89
+ hasLiveApprovalEvent: boolean;
90
+ activity: CliActivityState;
91
+ status: CliTurnStatus;
92
+ recoverable: boolean;
93
+ pendingApproval?: CliProjection["pendingApproval"];
94
+ activeSkills: Array<{
95
+ skillId: string;
96
+ contentHash: string;
97
+ }>;
98
+ /** 最近一次 context_usage 的安全 DTO;只驱动状态栏刷新,不进入时间线。 */
99
+ contextUsage?: ContextUsageView | null;
100
+ cancellationRequested: boolean;
101
+ requestedExitCode?: CliExitCode;
102
+ }
103
+ /** TUI Store 只负责把安全事件转成 React 可订阅快照。 */
104
+ export interface CliTuiStore {
105
+ getSnapshot(): CliTuiSnapshot;
106
+ subscribe(listener: () => void): () => void;
107
+ recordPrompt(content: string, displayAsActive?: boolean): string | undefined;
108
+ captureInputPosition(): CliTuiInputPosition;
109
+ addPendingPrompt(content: string, input: {
110
+ inputId: string;
111
+ delivery: "steer" | "queue";
112
+ }, position?: CliTuiInputPosition): void;
113
+ markQueuedInputStarted(input: CliQueuedInputStarted): void;
114
+ recordApprovalSubmission(decision: Extract<CliAction, {
115
+ type: "approve";
116
+ }>["decision"]): void;
117
+ addNotice(message: string): void;
118
+ acceptEvent(event: CliPublicEvent, projection: CliProjection): void;
119
+ syncProjection(projection: CliProjection): void;
120
+ setCancellationRequested(requested: boolean): void;
121
+ requestExit(code: CliExitCode): void;
122
+ }
123
+ /** TUI 的启动操作;resume 不把完整 checkpoint 传入 UI。 */
124
+ export type CliTuiInitialOperation = {
125
+ type: "start";
126
+ content: string;
127
+ } | {
128
+ type: "resume";
129
+ };
130
+ /** Ink TUI 的运行参数。 */
131
+ export interface RunCliTuiOptions {
132
+ controller: CliController;
133
+ /** 主机已知的只读会话标识;仅用于备用屏退出后的安全摘要。 */
134
+ sessionId?: string;
135
+ /** canonical 工作区根;仅用于 @ 文件候选索引预热。 */
136
+ workspaceRoot?: string;
137
+ streams: {
138
+ stdout: TuiOutputStream;
139
+ stderr: {
140
+ write(chunk: string): void;
141
+ };
142
+ stdin: NodeJS.ReadStream;
143
+ };
144
+ signalSource: {
145
+ on(signal: "SIGINT" | "SIGTERM", listener: () => void): void;
146
+ removeListener(signal: "SIGINT" | "SIGTERM", listener: () => void): void;
147
+ };
148
+ forceExit?(code: 130): void;
149
+ initial?: CliTuiInitialOperation;
150
+ closeWhenIdle: boolean;
151
+ env?: NodeJS.ProcessEnv;
152
+ }
153
+ /** TUI 读取终端尺寸所需的最小宿主流接口;resize 监听保持可选以兼容测试 double。 */
154
+ export interface TuiTerminalSizeSource {
155
+ columns?: number;
156
+ rows?: number;
157
+ on?(event: "resize", listener: () => void): void;
158
+ off?(event: "resize", listener: () => void): void;
159
+ }
160
+ /** TUI 的最小输出流接口;Ink 渲染代理与真实 stdout 均实现此形状。 */
161
+ export type TuiOutputStream = TuiTerminalSizeSource & {
162
+ write(chunk: string): void;
163
+ isTTY?: boolean;
164
+ };
165
+ /** TUI 运行结果;退出码仍由宿主 Main 统一解释。 */
166
+ export interface RunCliTuiResult {
167
+ result: CliControllerResult;
168
+ exitCode: CliExitCode;
169
+ }
170
+ /** 退出备用屏后输出的最小安全会话摘要;不携带 Runtime 私有状态或存储位置。 */
171
+ export interface CliTuiRecap {
172
+ sessionId: string;
173
+ completedTurnCount: number;
174
+ finalStatus: CliTurnStatus;
175
+ recoverable: boolean;
176
+ }
177
+ /** TUI 启动期一次性解析的终端渲染模式。 */
178
+ export type TuiRenderMode = "inline" | "alternate-screen";
179
+ /** 可幂等恢复终端控制序列的最小生命周期对象。 */
180
+ export interface TuiTerminalLifecycle {
181
+ enter(): void;
182
+ reassert(): void;
183
+ restore(): void;
184
+ }
185
+ /** 备用屏向应用视口转交的鼠标事件入口;只保存一个当前处理器。 */
186
+ export interface TuiMouseInputSink {
187
+ setHandler(handler: ((event: TuiSgrMouseEvent, wheelNotches: number) => void) | undefined): void;
188
+ dispatch(event: TuiSgrMouseEvent, wheelNotches?: number): void;
189
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 仅 TUI 层自有类型。Controller / 投影 / 事件等宿主协议类型统一从
3
+ * ../types.js 与 ../controller.js 复用,不在这里重复声明领域协议。
4
+ */
5
+ export {};
@@ -0,0 +1,27 @@
1
+ /** 备用屏文字划选纯函数:来源坐标定位、跨行选区提取与平台剪贴板写入。 */
2
+ import type { TimelineVisualLine } from "../../timeline-lines.js";
3
+ /** 备用屏文字划选端点;始终关联纯文本时间线的来源坐标。 */
4
+ export interface TuiTextSelectionPoint {
5
+ itemId: string;
6
+ sourceOffset: number;
7
+ }
8
+ /** 备用屏文字划选状态;锚点和焦点可跨多个视觉行。 */
9
+ export interface TuiTextSelection {
10
+ anchor?: TuiTextSelectionPoint;
11
+ focus?: TuiTextSelectionPoint;
12
+ }
13
+ /** 将终端列坐标映射到视觉行的完整 grapheme 边界,避免截断中文和 emoji。 */
14
+ export declare function resolveTuiVisualLineOffset(line: TimelineVisualLine, column: number): number;
15
+ /** 在当前视觉行中定位选区端点,以便随 resize 后的换行重新找到来源坐标。 */
16
+ export declare function findTuiSelectionLineIndex(lines: TimelineVisualLine[], point: TuiTextSelectionPoint): number;
17
+ /** 从纯文本视觉行中提取跨行选区,供 Windows 剪贴板或 OSC 52 使用。 */
18
+ export declare function getTuiSelectedText(lines: TimelineVisualLine[], selection: TuiTextSelection): string;
19
+ /** 返回当前视觉行被选中的本地字符范围,用于仅反显实际拖拽到的文本。 */
20
+ export declare function getTuiVisualLineSelectionRange(lines: TimelineVisualLine[], selection: TuiTextSelection, lineIndex: number): {
21
+ start: number;
22
+ end: number;
23
+ } | undefined;
24
+ /** 将当前选区交给平台剪贴板;Windows 不经 shell 调用 clip,其它平台使用有界 OSC 52。 */
25
+ export declare function copyTuiSelectedText(text: string, stdout: {
26
+ write(chunk: string): void;
27
+ }): void;
@@ -0,0 +1,79 @@
1
+ /** 备用屏文字划选纯函数:来源坐标定位、跨行选区提取与平台剪贴板写入。 */
2
+ import { spawn } from "node:child_process";
3
+ import stringWidth from "string-width";
4
+ import { TUI_OSC52_MAX_BYTES } from "../constants.js";
5
+ /** 将终端列坐标映射到视觉行的完整 grapheme 边界,避免截断中文和 emoji。 */
6
+ export function resolveTuiVisualLineOffset(line, column) {
7
+ const targetColumn = Math.max(0, column);
8
+ let displayColumn = 0;
9
+ let offset = 0;
10
+ for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(line.text)) {
11
+ const nextColumn = displayColumn + stringWidth(segment.segment);
12
+ if (targetColumn < nextColumn)
13
+ return line.sourceStart + offset;
14
+ displayColumn = nextColumn;
15
+ offset += segment.segment.length;
16
+ }
17
+ return line.sourceStart + line.text.length;
18
+ }
19
+ /** 在当前视觉行中定位选区端点,以便随 resize 后的换行重新找到来源坐标。 */
20
+ export function findTuiSelectionLineIndex(lines, point) {
21
+ return lines.findIndex(function findSelectionLine(line) {
22
+ return line.itemId === point.itemId && point.sourceOffset >= line.sourceStart && point.sourceOffset <= line.sourceEnd;
23
+ });
24
+ }
25
+ /** 从纯文本视觉行中提取跨行选区,供 Windows 剪贴板或 OSC 52 使用。 */
26
+ export function getTuiSelectedText(lines, selection) {
27
+ if (!selection.anchor || !selection.focus)
28
+ return "";
29
+ const anchorIndex = findTuiSelectionLineIndex(lines, selection.anchor);
30
+ const focusIndex = findTuiSelectionLineIndex(lines, selection.focus);
31
+ if (anchorIndex < 0 || focusIndex < 0)
32
+ return "";
33
+ const [startIndex, endIndex, startPoint, endPoint] = anchorIndex < focusIndex || anchorIndex === focusIndex && selection.anchor.sourceOffset <= selection.focus.sourceOffset
34
+ ? [anchorIndex, focusIndex, selection.anchor, selection.focus]
35
+ : [focusIndex, anchorIndex, selection.focus, selection.anchor];
36
+ const fragments = [];
37
+ for (let index = startIndex; index <= endIndex; index += 1) {
38
+ const line = lines[index];
39
+ if (!line)
40
+ continue;
41
+ const start = index === startIndex ? Math.max(0, startPoint.sourceOffset - line.sourceStart) : 0;
42
+ const end = index === endIndex ? Math.max(start, endPoint.sourceOffset - line.sourceStart) : line.text.length;
43
+ fragments.push(line.text.slice(start, end));
44
+ }
45
+ return fragments.join("\n");
46
+ }
47
+ /** 返回当前视觉行被选中的本地字符范围,用于仅反显实际拖拽到的文本。 */
48
+ export function getTuiVisualLineSelectionRange(lines, selection, lineIndex) {
49
+ if (!selection.anchor || !selection.focus)
50
+ return undefined;
51
+ const anchorIndex = findTuiSelectionLineIndex(lines, selection.anchor);
52
+ const focusIndex = findTuiSelectionLineIndex(lines, selection.focus);
53
+ if (anchorIndex < 0 || focusIndex < 0)
54
+ return undefined;
55
+ const [startIndex, endIndex, startPoint, endPoint] = anchorIndex < focusIndex || anchorIndex === focusIndex && selection.anchor.sourceOffset <= selection.focus.sourceOffset
56
+ ? [anchorIndex, focusIndex, selection.anchor, selection.focus]
57
+ : [focusIndex, anchorIndex, selection.focus, selection.anchor];
58
+ if (lineIndex < startIndex || lineIndex > endIndex)
59
+ return undefined;
60
+ const line = lines[lineIndex];
61
+ if (!line)
62
+ return undefined;
63
+ const start = lineIndex === startIndex ? Math.max(0, startPoint.sourceOffset - line.sourceStart) : 0;
64
+ const end = lineIndex === endIndex ? Math.max(start, endPoint.sourceOffset - line.sourceStart) : line.text.length;
65
+ return start < end ? { start, end } : undefined;
66
+ }
67
+ /** 将当前选区交给平台剪贴板;Windows 不经 shell 调用 clip,其它平台使用有界 OSC 52。 */
68
+ export function copyTuiSelectedText(text, stdout) {
69
+ if (!text)
70
+ return;
71
+ if (process.platform === "win32") {
72
+ const clipboard = spawn("clip", [], { stdio: "pipe", windowsHide: true });
73
+ clipboard.on("error", function ignoreClipboardFailure() { });
74
+ clipboard.stdin.end(text);
75
+ return;
76
+ }
77
+ const bytes = Buffer.from(text);
78
+ stdout.write(`\u001b]52;c;${bytes.subarray(0, TUI_OSC52_MAX_BYTES).toString("base64")}\u0007`);
79
+ }