@autopilot-agent/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -2
- package/dist/activity-animation.d.ts +59 -0
- package/dist/activity-animation.js +148 -0
- package/dist/animation-clock.d.ts +22 -0
- package/dist/animation-clock.js +76 -0
- package/dist/composition-root.d.ts +105 -0
- package/dist/composition-root.js +600 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +173 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +741 -0
- package/dist/design-tokens.d.ts +38 -0
- package/dist/design-tokens.js +38 -0
- package/dist/error-diagnostics.d.ts +8 -0
- package/dist/error-diagnostics.js +62 -0
- package/dist/event-projector.d.ts +26 -0
- package/dist/event-projector.js +335 -0
- package/dist/file-mention-context.d.ts +34 -0
- package/dist/file-mention-context.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +40 -0
- package/dist/main.js +543 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.js +400 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +62 -0
- package/dist/scroll-viewport.d.ts +22 -0
- package/dist/scroll-viewport.js +62 -0
- package/dist/termination-format.d.ts +4 -0
- package/dist/termination-format.js +18 -0
- package/dist/timeline-lines.d.ts +38 -0
- package/dist/timeline-lines.js +104 -0
- package/dist/tui/app.d.ts +30 -0
- package/dist/tui/app.js +413 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +41 -0
- package/dist/tui/brand/brand-header.d.ts +20 -0
- package/dist/tui/brand/brand-header.js +22 -0
- package/dist/tui/brand/intro.d.ts +12 -0
- package/dist/tui/brand/intro.js +88 -0
- package/dist/tui/brand/pixel.d.ts +8 -0
- package/dist/tui/brand/pixel.js +50 -0
- package/dist/tui/constants.d.ts +56 -0
- package/dist/tui/constants.js +57 -0
- package/dist/tui/context.d.ts +11 -0
- package/dist/tui/context.js +12 -0
- package/dist/tui/diff-stats.d.ts +8 -0
- package/dist/tui/diff-stats.js +7 -0
- package/dist/tui/format.d.ts +75 -0
- package/dist/tui/format.js +178 -0
- package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
- package/dist/tui/hooks/use-file-mention-index.js +59 -0
- package/dist/tui/hooks/use-pickers.d.ts +47 -0
- package/dist/tui/hooks/use-pickers.js +288 -0
- package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
- package/dist/tui/hooks/use-prompt-input.js +258 -0
- package/dist/tui/hooks/use-status-view.d.ts +8 -0
- package/dist/tui/hooks/use-status-view.js +13 -0
- package/dist/tui/hooks/use-viewport.d.ts +22 -0
- package/dist/tui/hooks/use-viewport.js +117 -0
- package/dist/tui/input/file-mention.d.ts +33 -0
- package/dist/tui/input/file-mention.js +112 -0
- package/dist/tui/input/graphemes.d.ts +5 -0
- package/dist/tui/input/graphemes.js +10 -0
- package/dist/tui/input/keys.d.ts +16 -0
- package/dist/tui/input/keys.js +58 -0
- package/dist/tui/input/prompt-layout.d.ts +27 -0
- package/dist/tui/input/prompt-layout.js +84 -0
- package/dist/tui/input-area.d.ts +53 -0
- package/dist/tui/input-area.js +89 -0
- package/dist/tui/jobs.d.ts +10 -0
- package/dist/tui/jobs.js +24 -0
- package/dist/tui/lifecycle.d.ts +23 -0
- package/dist/tui/lifecycle.js +114 -0
- package/dist/tui/markdown/diff.d.ts +8 -0
- package/dist/tui/markdown/diff.js +19 -0
- package/dist/tui/markdown/inline.d.ts +4 -0
- package/dist/tui/markdown/inline.js +32 -0
- package/dist/tui/markdown/markdown.d.ts +17 -0
- package/dist/tui/markdown/markdown.js +117 -0
- package/dist/tui/markdown/table.d.ts +19 -0
- package/dist/tui/markdown/table.js +74 -0
- package/dist/tui/performance-diagnostics.d.ts +7 -0
- package/dist/tui/performance-diagnostics.js +57 -0
- package/dist/tui/pickers/effort.d.ts +10 -0
- package/dist/tui/pickers/effort.js +12 -0
- package/dist/tui/pickers/file-mention.d.ts +11 -0
- package/dist/tui/pickers/file-mention.js +22 -0
- package/dist/tui/pickers/model.d.ts +10 -0
- package/dist/tui/pickers/model.js +12 -0
- package/dist/tui/pickers/permission.d.ts +10 -0
- package/dist/tui/pickers/permission.js +13 -0
- package/dist/tui/pickers/skill.d.ts +13 -0
- package/dist/tui/pickers/skill.js +25 -0
- package/dist/tui/pickers/slash.d.ts +10 -0
- package/dist/tui/pickers/slash.js +11 -0
- package/dist/tui/prompt-input.d.ts +11 -0
- package/dist/tui/prompt-input.js +73 -0
- package/dist/tui/render-mode.d.ts +20 -0
- package/dist/tui/render-mode.js +31 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +237 -0
- package/dist/tui/status-line.d.ts +16 -0
- package/dist/tui/status-line.js +37 -0
- package/dist/tui/store.d.ts +4 -0
- package/dist/tui/store.js +380 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +153 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +15 -0
- package/dist/tui/timeline/presentation-timeline.js +128 -0
- package/dist/tui/timeline/progress.d.ts +12 -0
- package/dist/tui/timeline/progress.js +43 -0
- package/dist/tui/timeline/static-timeline.d.ts +13 -0
- package/dist/tui/timeline/static-timeline.js +131 -0
- package/dist/tui/timeline/tool-tree.d.ts +7 -0
- package/dist/tui/timeline/tool-tree.js +25 -0
- package/dist/tui/timeline/turn-items.d.ts +12 -0
- package/dist/tui/timeline/turn-items.js +28 -0
- package/dist/tui/timeline/turn.d.ts +19 -0
- package/dist/tui/timeline/turn.js +21 -0
- package/dist/tui/todo-list.d.ts +50 -0
- package/dist/tui/todo-list.js +120 -0
- package/dist/tui/transcript.d.ts +24 -0
- package/dist/tui/transcript.js +34 -0
- package/dist/tui/trust-dialog.d.ts +51 -0
- package/dist/tui/trust-dialog.js +127 -0
- package/dist/tui/types.d.ts +189 -0
- package/dist/tui/types.js +5 -0
- package/dist/tui/viewport/selection.d.ts +27 -0
- package/dist/tui/viewport/selection.js +79 -0
- package/dist/tui/viewport/sources.d.ts +29 -0
- package/dist/tui/viewport/sources.js +592 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +91 -0
- package/dist/tui-input-history.d.ts +23 -0
- package/dist/tui-input-history.js +42 -0
- package/dist/tui-input-router.d.ts +24 -0
- package/dist/tui-input-router.js +115 -0
- package/dist/tui.d.ts +15 -0
- package/dist/tui.js +13 -0
- package/dist/types.d.ts +467 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
5
|
+
import { renderMarkdownInline } from "./inline.js";
|
|
6
|
+
import { fitTuiMarkdownTableColumnWidths, getTuiMarkdownTableColumnWidths, hasMarkdownTableCells, isLooseMarkdownTable, isMarkdownBlockStart, isMarkdownTableDivider, normalizeMarkdownTableCells, splitMarkdownTableRow, wrapTuiMarkdownTableCell, } from "./table.js";
|
|
7
|
+
/** 渲染已经按当前终端宽度适配列宽的对齐 Markdown 表格。 */
|
|
8
|
+
export function TuiMarkdownAlignedTable(props) {
|
|
9
|
+
const widths = fitTuiMarkdownTableColumnWidths(getTuiMarkdownTableColumnWidths([props.header, ...props.rows]), props.maximumWidth);
|
|
10
|
+
const makeBorder = function makeBorder(left, join, right) {
|
|
11
|
+
return left + widths.map(function makeColumnBorder(width) { return "─".repeat(width + 2); }).join(join) + right;
|
|
12
|
+
};
|
|
13
|
+
const renderRow = function renderRow(cells, keyPrefix, header = false) {
|
|
14
|
+
const wrappedCells = cells.map(function wrapCell(cell, index) { return wrapTuiMarkdownTableCell(cell, widths[index] ?? 3); });
|
|
15
|
+
const rowHeight = Math.max(1, ...wrappedCells.map(function getCellHeight(cellLines) { return cellLines.length; }));
|
|
16
|
+
return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: rowHeight }, function renderLine(_unused, lineIndex) {
|
|
17
|
+
return _jsxs(Box, { children: [_jsx(Text, { children: "\u2502" }), wrappedCells.map(function renderCell(cellLines, index) {
|
|
18
|
+
const cellLine = cellLines[lineIndex] ?? "";
|
|
19
|
+
const padding = " ".repeat(Math.max(0, (widths[index] ?? 3) - stringWidth(cellLine)));
|
|
20
|
+
return _jsx(Box, { width: (widths[index] ?? 3) + 2, paddingX: 1, children: _jsx(Text, { bold: header, children: renderMarkdownInline(cellLine + padding, keyPrefix + ":cell:" + String(index) + ":line:" + String(lineIndex), props.useColor) }) }, keyPrefix + ":cell:" + String(index));
|
|
21
|
+
}), _jsx(Text, { children: "\u2502" })] }, keyPrefix + ":line:" + String(lineIndex));
|
|
22
|
+
}) }, keyPrefix));
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: makeBorder("┌", "┬", "┐") }), renderRow(props.header, props.keyPrefix + ":header", true), _jsx(Text, { children: makeBorder("├", "┼", "┤") }), props.rows.map(function renderTableRow(row, index) { return renderRow(row, props.keyPrefix + ":row:" + String(index)); }), _jsx(Text, { children: makeBorder("└", "┴", "┘") })] }));
|
|
25
|
+
}
|
|
26
|
+
/** 渲染 CLI 所需的安全 Markdown 子集,不解析或执行 HTML。 */
|
|
27
|
+
export function TuiMarkdown(props) {
|
|
28
|
+
const nodes = [];
|
|
29
|
+
const lines = props.content.replace(/\r\n?/g, "\n").split("\n");
|
|
30
|
+
let index = 0;
|
|
31
|
+
let blockIndex = 0;
|
|
32
|
+
while (index < lines.length) {
|
|
33
|
+
const line = lines[index] ?? "";
|
|
34
|
+
if (!line.trim()) {
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const key = props.keyPrefix + ":block:" + String(blockIndex++);
|
|
39
|
+
const fence = line.match(/^\s*(`{3,}|~{3,})\s*([^\s]*)/);
|
|
40
|
+
if (fence) {
|
|
41
|
+
const marker = fence[1];
|
|
42
|
+
const closePattern = new RegExp("^\\s*" + marker[0] + "{" + String(marker.length) + ",}\\s*$");
|
|
43
|
+
const codeLines = [];
|
|
44
|
+
index += 1;
|
|
45
|
+
while (index < lines.length && !closePattern.test(lines[index] ?? "")) {
|
|
46
|
+
codeLines.push(lines[index] ?? "");
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
if (index < lines.length)
|
|
50
|
+
index += 1;
|
|
51
|
+
nodes.push(_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, children: [_jsxs(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: ["code \u00B7 ", fence[2] || "text"] }), _jsx(Text, { wrap: "wrap", children: codeLines.join("\n") || " " })] }, key));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
55
|
+
if (heading) {
|
|
56
|
+
nodes.push(_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, wrap: "wrap", children: renderMarkdownInline(heading[2], key, props.useColor) }, key));
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (/^\s*(?:---+|\*\*\*+|___+)\s*$/.test(line)) {
|
|
61
|
+
nodes.push(_jsx(Text, { dimColor: props.useColor, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }, key));
|
|
62
|
+
index += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const tableHeader = splitMarkdownTableRow(line);
|
|
66
|
+
const hasStrictTableDivider = line.includes("|") && isMarkdownTableDivider(lines[index + 1] ?? "");
|
|
67
|
+
const hasLooseTableRows = isLooseMarkdownTable(lines, index);
|
|
68
|
+
if (hasStrictTableDivider || hasLooseTableRows) {
|
|
69
|
+
const tableRows = [];
|
|
70
|
+
let rowIndex = index + (hasStrictTableDivider ? 2 : 1);
|
|
71
|
+
while (rowIndex < lines.length && hasMarkdownTableCells(lines[rowIndex] ?? "", tableHeader.length)) {
|
|
72
|
+
const rowCells = normalizeMarkdownTableCells(lines[rowIndex] ?? "", tableHeader.length);
|
|
73
|
+
if (!rowCells)
|
|
74
|
+
break;
|
|
75
|
+
tableRows.push(rowCells);
|
|
76
|
+
rowIndex += 1;
|
|
77
|
+
}
|
|
78
|
+
if (tableRows.length > 0) {
|
|
79
|
+
nodes.push(_jsx(TuiMarkdownAlignedTable, { header: tableHeader, rows: tableRows, keyPrefix: key, useColor: props.useColor, maximumWidth: props.maximumWidth }, key));
|
|
80
|
+
index = rowIndex;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (hasStrictTableDivider) {
|
|
84
|
+
nodes.push(_jsxs(Text, { wrap: "wrap", children: [line, "\n", lines[index + 1] ?? ""] }, key));
|
|
85
|
+
index += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
90
|
+
if (quote) {
|
|
91
|
+
const quoteLines = [];
|
|
92
|
+
while (index < lines.length) {
|
|
93
|
+
const nextQuote = (lines[index] ?? "").match(/^\s*>\s?(.*)$/);
|
|
94
|
+
if (!nextQuote)
|
|
95
|
+
break;
|
|
96
|
+
quoteLines.push(nextQuote[1]);
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
nodes.push(_jsx(Box, { borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, children: _jsx(Text, { wrap: "wrap", children: renderMarkdownInline(quoteLines.join(" "), key, props.useColor) }) }, key));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const list = line.match(/^(\s*)((?:[-+*])|(?:\d+[.)]))\s+(.+)$/);
|
|
103
|
+
if (list) {
|
|
104
|
+
nodes.push(_jsxs(Text, { wrap: "wrap", children: [list[1], list[2], " ", renderMarkdownInline(list[3], key, props.useColor)] }, key));
|
|
105
|
+
index += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const paragraph = [line];
|
|
109
|
+
index += 1;
|
|
110
|
+
while (index < lines.length && (lines[index] ?? "").trim() && !isMarkdownBlockStart(lines, index)) {
|
|
111
|
+
paragraph.push(lines[index] ?? "");
|
|
112
|
+
index += 1;
|
|
113
|
+
}
|
|
114
|
+
nodes.push(_jsx(Text, { wrap: "wrap", children: renderMarkdownInline(paragraph.join(" "), key, props.useColor) }, key));
|
|
115
|
+
}
|
|
116
|
+
return _jsx(Box, { flexDirection: "column", children: nodes });
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Markdown 表格纯函数:拆分、识别分隔行、归一化列数与列宽适配,不涉及渲染。 */
|
|
2
|
+
/** 将一行 Markdown 表格拆成可在终端逐格显示的安全文本。 */
|
|
3
|
+
export declare function splitMarkdownTableRow(line: string): string[];
|
|
4
|
+
/** 判断一行是否是 GitHub 风格 Markdown 表格的分隔行。 */
|
|
5
|
+
export declare function isMarkdownTableDivider(line: string): boolean;
|
|
6
|
+
/** 将模型行归一为表头列数;命令内容多出的 shell 管道合并进最后一列。 */
|
|
7
|
+
export declare function normalizeMarkdownTableCells(line: string, cellCount: number): string[] | undefined;
|
|
8
|
+
/** 判断一行是否包含至少指定列数的竖线分列表格数据。 */
|
|
9
|
+
export declare function hasMarkdownTableCells(line: string, cellCount: number): boolean;
|
|
10
|
+
/** 判断模型漏掉分隔行时,表头和首行数据是否仍可安全识别为宽松表格。 */
|
|
11
|
+
export declare function isLooseMarkdownTable(lines: string[], index: number): boolean;
|
|
12
|
+
/** 判断一行是否会开始新的 Markdown 块,避免把相邻段落错误合并。 */
|
|
13
|
+
export declare function isMarkdownBlockStart(lines: string[], index: number): boolean;
|
|
14
|
+
/** 计算 Markdown 表格每列的最大显示宽度。 */
|
|
15
|
+
export declare function getTuiMarkdownTableColumnWidths(rows: string[][]): number[];
|
|
16
|
+
/** 将表格列压缩到当前终端宽度,保留每列至少三个可读字符。 */
|
|
17
|
+
export declare function fitTuiMarkdownTableColumnWidths(widths: number[], maximumWidth: number): number[];
|
|
18
|
+
/** 将单元格文本按指定宽度硬换行,返回逐行显示文本。 */
|
|
19
|
+
export declare function wrapTuiMarkdownTableCell(value: string, width: number): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Markdown 表格纯函数:拆分、识别分隔行、归一化列数与列宽适配,不涉及渲染。 */
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
/** 将一行 Markdown 表格拆成可在终端逐格显示的安全文本。 */
|
|
5
|
+
export function splitMarkdownTableRow(line) {
|
|
6
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
7
|
+
return trimmed.split("|").map(function trimCell(cell) { return cell.trim(); });
|
|
8
|
+
}
|
|
9
|
+
/** 判断一行是否是 GitHub 风格 Markdown 表格的分隔行。 */
|
|
10
|
+
export function isMarkdownTableDivider(line) {
|
|
11
|
+
const cells = splitMarkdownTableRow(line);
|
|
12
|
+
return cells.length > 0 && cells.every(function isDivider(cell) { return /^:?-{3,}:?$/.test(cell); });
|
|
13
|
+
}
|
|
14
|
+
/** 将模型行归一为表头列数;命令内容多出的 shell 管道合并进最后一列。 */
|
|
15
|
+
export function normalizeMarkdownTableCells(line, cellCount) {
|
|
16
|
+
if (!line.includes("|"))
|
|
17
|
+
return undefined;
|
|
18
|
+
const cells = splitMarkdownTableRow(line);
|
|
19
|
+
if (cells.length < cellCount)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (cells.length === cellCount)
|
|
22
|
+
return cells;
|
|
23
|
+
return [...cells.slice(0, cellCount - 1), cells.slice(cellCount - 1).join(" | ")];
|
|
24
|
+
}
|
|
25
|
+
/** 判断一行是否包含至少指定列数的竖线分列表格数据。 */
|
|
26
|
+
export function hasMarkdownTableCells(line, cellCount) {
|
|
27
|
+
return normalizeMarkdownTableCells(line, cellCount) !== undefined;
|
|
28
|
+
}
|
|
29
|
+
/** 判断模型漏掉分隔行时,表头和首行数据是否仍可安全识别为宽松表格。 */
|
|
30
|
+
export function isLooseMarkdownTable(lines, index) {
|
|
31
|
+
const header = lines[index] ?? "";
|
|
32
|
+
const trimmedHeader = header.trim();
|
|
33
|
+
if (!/^\|.+/.test(trimmedHeader))
|
|
34
|
+
return false;
|
|
35
|
+
const cells = splitMarkdownTableRow(header);
|
|
36
|
+
return cells.length >= 2 && hasMarkdownTableCells(lines[index + 1] ?? "", cells.length);
|
|
37
|
+
}
|
|
38
|
+
/** 判断一行是否会开始新的 Markdown 块,避免把相邻段落错误合并。 */
|
|
39
|
+
export function isMarkdownBlockStart(lines, index) {
|
|
40
|
+
const line = lines[index] ?? "";
|
|
41
|
+
return /^\s*(`{3,}|~{3,})/.test(line)
|
|
42
|
+
|| /^#{1,6}\s+/.test(line)
|
|
43
|
+
|| /^\s*>\s?/.test(line)
|
|
44
|
+
|| /^\s*(?:[-+*]|\d+[.)])\s+/.test(line)
|
|
45
|
+
|| /^\s*(?:---+|\*\*\*+|___+)\s*$/.test(line)
|
|
46
|
+
|| (line.includes("|") && (isMarkdownTableDivider(lines[index + 1] ?? "") || isLooseMarkdownTable(lines, index)));
|
|
47
|
+
}
|
|
48
|
+
/** 计算 Markdown 表格每列的最大显示宽度。 */
|
|
49
|
+
export function getTuiMarkdownTableColumnWidths(rows) {
|
|
50
|
+
return rows[0].map(function getColumnWidth(_cell, columnIndex) {
|
|
51
|
+
return Math.max(3, ...rows.map(function getCellWidth(row) { return stringWidth(row[columnIndex] ?? ""); }));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** 将表格列压缩到当前终端宽度,保留每列至少三个可读字符。 */
|
|
55
|
+
export function fitTuiMarkdownTableColumnWidths(widths, maximumWidth) {
|
|
56
|
+
const fitted = widths.map(function fitColumn(width) { return Math.max(3, width); });
|
|
57
|
+
const availableWidth = Math.max(fitted.length * 3, maximumWidth - fitted.length * 3 - 1);
|
|
58
|
+
while (fitted.reduce(function sumWidths(total, width) { return total + width; }, 0) > availableWidth) {
|
|
59
|
+
let widestIndex = 0;
|
|
60
|
+
for (let index = 1; index < fitted.length; index += 1) {
|
|
61
|
+
if ((fitted[index] ?? 0) > (fitted[widestIndex] ?? 0))
|
|
62
|
+
widestIndex = index;
|
|
63
|
+
}
|
|
64
|
+
if ((fitted[widestIndex] ?? 3) <= 3)
|
|
65
|
+
break;
|
|
66
|
+
fitted[widestIndex] = Math.max(3, (fitted[widestIndex] ?? 3) - 1);
|
|
67
|
+
}
|
|
68
|
+
return fitted;
|
|
69
|
+
}
|
|
70
|
+
/** 将单元格文本按指定宽度硬换行,返回逐行显示文本。 */
|
|
71
|
+
export function wrapTuiMarkdownTableCell(value, width) {
|
|
72
|
+
const wrapped = wrapAnsi(value, Math.max(1, width), { hard: true, trim: false, wordWrap: true }).split("\n");
|
|
73
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
74
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 性能诊断事件:只缓冲渲染与输入时刻、耗时和事件类型,退出时一次性写入显式文件。
|
|
3
|
+
*/
|
|
4
|
+
/** 记录一个不含用户内容的 CLI 性能诊断点。 */
|
|
5
|
+
export declare function recordTuiPerformanceDiagnostic(point: string, eventType: string, durationMs?: number): void;
|
|
6
|
+
/** 只把稳定类别与白名单错误码写入显式诊断缓冲,不保存异常正文或调用内容。 */
|
|
7
|
+
export declare function recordTuiRuntimeErrorDiagnostic(error: unknown): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 性能诊断事件:只缓冲渲染与输入时刻、耗时和事件类型,退出时一次性写入显式文件。
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { classifyCliError } from "../error-diagnostics.js";
|
|
7
|
+
const PERFORMANCE_DIAGNOSTICS_KEY = Symbol.for("autopilot.performanceDiagnostics");
|
|
8
|
+
let exitHookInstalled = false;
|
|
9
|
+
/** 返回与 Core 诊断点共享的进程内缓冲。 */
|
|
10
|
+
function getPerformanceDiagnosticBuffer() {
|
|
11
|
+
const target = globalThis;
|
|
12
|
+
target[PERFORMANCE_DIAGNOSTICS_KEY] ??= [];
|
|
13
|
+
return target[PERFORMANCE_DIAGNOSTICS_KEY];
|
|
14
|
+
}
|
|
15
|
+
/** 在进程退出时一次性写入 JSONL,避免逐事件文件 I/O 污染被测热路径。 */
|
|
16
|
+
function ensureExitFlush() {
|
|
17
|
+
if (exitHookInstalled)
|
|
18
|
+
return;
|
|
19
|
+
exitHookInstalled = true;
|
|
20
|
+
process.once("exit", function flushPerformanceDiagnostics() {
|
|
21
|
+
const outputFile = process.env.AUTOPILOT_PERF_DIAGNOSTICS_FILE;
|
|
22
|
+
if (!outputFile)
|
|
23
|
+
return;
|
|
24
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
25
|
+
const records = getPerformanceDiagnosticBuffer();
|
|
26
|
+
fs.writeFileSync(outputFile, records.map(function serialize(record) { return JSON.stringify(record); }).join("\n") + "\n", "utf8");
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** 记录一个不含用户内容的 CLI 性能诊断点。 */
|
|
30
|
+
export function recordTuiPerformanceDiagnostic(point, eventType, durationMs) {
|
|
31
|
+
if (process.env.AUTOPILOT_PERF_DIAGNOSTICS !== "1")
|
|
32
|
+
return;
|
|
33
|
+
ensureExitFlush();
|
|
34
|
+
const record = {
|
|
35
|
+
version: 1,
|
|
36
|
+
point,
|
|
37
|
+
eventType,
|
|
38
|
+
monotonicMs: Number(performance.now().toFixed(3)),
|
|
39
|
+
...(durationMs === undefined ? {} : { durationMs: Number(durationMs.toFixed(3)) }),
|
|
40
|
+
};
|
|
41
|
+
getPerformanceDiagnosticBuffer().push(record);
|
|
42
|
+
}
|
|
43
|
+
/** 只把稳定类别与白名单错误码写入显式诊断缓冲,不保存异常正文或调用内容。 */
|
|
44
|
+
export function recordTuiRuntimeErrorDiagnostic(error) {
|
|
45
|
+
if (process.env.AUTOPILOT_PERF_DIAGNOSTICS !== "1")
|
|
46
|
+
return;
|
|
47
|
+
ensureExitFlush();
|
|
48
|
+
const classification = classifyCliError(error);
|
|
49
|
+
getPerformanceDiagnosticBuffer().push({
|
|
50
|
+
version: 1,
|
|
51
|
+
point: "tui_runtime_error",
|
|
52
|
+
eventType: "dispatch",
|
|
53
|
+
monotonicMs: Number(performance.now().toFixed(3)),
|
|
54
|
+
errorCategory: classification.category,
|
|
55
|
+
errorCode: classification.code,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 推理强度 picker;目录完全由 Controller 的 capability 投影提供。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliEffortOption } from "../../types.js";
|
|
4
|
+
/** 渲染当前模型支持的 effort picker;Pro 不会出现 Low。 */
|
|
5
|
+
export declare function TuiEffortPicker(props: {
|
|
6
|
+
options: CliEffortOption[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 渲染当前模型支持的 effort picker;Pro 不会出现 Low。 */
|
|
5
|
+
export function TuiEffortPicker(props) {
|
|
6
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, props.options.length - 1)));
|
|
7
|
+
const selectedOption = props.options[selectedIndex];
|
|
8
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Thinking effort" }), _jsx(Text, { dimColor: props.useColor, children: "\u2191\u2193 select \u00B7 Space on/off \u00B7 Enter save \u00B7 Esc cancel" }), props.options.map(function renderEffort(option, index) {
|
|
9
|
+
const focused = index === selectedIndex;
|
|
10
|
+
return _jsxs(Text, { bold: focused, color: focused && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [focused ? "› " : " ", "[", option.selected ? "on" : "off", "] ", option.effort] }, option.effort);
|
|
11
|
+
}), _jsxs(Text, { dimColor: props.useColor, children: [selectedOption?.selected ? "已选择;Enter 保存为下一个 Turn 默认" : "Space 选择此推理强度", props.screenReader ? " · effort selector" : ""] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `@` 触发的文件候选菜单:codex 风格两列布局、≤5 行滚动窗口与固定键位提示行。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/** @ 菜单一次可见的最大候选行数。 */
|
|
4
|
+
export declare const TUI_FILE_MENTION_VISIBLE_ROWS = 5;
|
|
5
|
+
/** 渲染 @ 文件候选菜单;目录候选以尾随 `/` 标记并在类型列显示 dir,选中行用 `› ` 标记。 */
|
|
6
|
+
export declare function TuiFileMentionMenu(props: {
|
|
7
|
+
candidates: string[];
|
|
8
|
+
selectedIndex: number;
|
|
9
|
+
useColor: boolean;
|
|
10
|
+
screenReader: boolean;
|
|
11
|
+
}): ReactNode;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** @ 菜单一次可见的最大候选行数。 */
|
|
5
|
+
export const TUI_FILE_MENTION_VISIBLE_ROWS = 5;
|
|
6
|
+
/** 计算 ≤5 行滚动窗口的起点,保证选中项始终可见。 */
|
|
7
|
+
function resolveWindowStart(selectedIndex, candidateCount) {
|
|
8
|
+
if (candidateCount <= TUI_FILE_MENTION_VISIBLE_ROWS)
|
|
9
|
+
return 0;
|
|
10
|
+
return Math.max(0, Math.min(selectedIndex - TUI_FILE_MENTION_VISIBLE_ROWS + 1, candidateCount - TUI_FILE_MENTION_VISIBLE_ROWS));
|
|
11
|
+
}
|
|
12
|
+
/** 渲染 @ 文件候选菜单;目录候选以尾随 `/` 标记并在类型列显示 dir,选中行用 `› ` 标记。 */
|
|
13
|
+
export function TuiFileMentionMenu(props) {
|
|
14
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, props.candidates.length - 1)));
|
|
15
|
+
const windowStart = resolveWindowStart(selectedIndex, props.candidates.length);
|
|
16
|
+
const visible = props.candidates.slice(windowStart, windowStart + TUI_FILE_MENTION_VISIBLE_ROWS);
|
|
17
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Files" }), _jsx(Text, { dimColor: props.useColor, children: "enter/tab insert \u00B7 esc close \u00B7 \u2191/\u2193 select" }), visible.map(function renderCandidate(candidate, offset) {
|
|
18
|
+
const absoluteIndex = windowStart + offset;
|
|
19
|
+
const selected = absoluteIndex === selectedIndex;
|
|
20
|
+
return (_jsxs(Text, { bold: selected, color: selected && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [selected ? "› " : " ", candidate, " ", _jsx(Text, { dimColor: props.useColor, children: candidate.endsWith("/") ? "dir" : "file" })] }, candidate));
|
|
21
|
+
}), _jsxs(Text, { dimColor: props.useColor, children: [selectedIndex + 1, "/", props.candidates.length, " files", props.screenReader ? " · file selector" : ""] })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 模型 picker;目录完全由 Controller 的 settings-filtered capability 投影提供。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliModelOption } from "../../types.js";
|
|
4
|
+
/** 渲染模型 picker;目录完全由 Controller 的 settings-filtered capability 投影提供。 */
|
|
5
|
+
export declare function TuiModelPicker(props: {
|
|
6
|
+
options: CliModelOption[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 渲染模型 picker;目录完全由 Controller 的 settings-filtered capability 投影提供。 */
|
|
5
|
+
export function TuiModelPicker(props) {
|
|
6
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, props.options.length - 1)));
|
|
7
|
+
const selectedOption = props.options[selectedIndex];
|
|
8
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Models" }), _jsx(Text, { dimColor: props.useColor, children: "\u2191\u2193 select \u00B7 Space on/off \u00B7 Enter save \u00B7 Esc cancel" }), props.options.map(function renderModel(option, index) {
|
|
9
|
+
const focused = index === selectedIndex;
|
|
10
|
+
return _jsxs(Text, { bold: focused, color: focused && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [focused ? "› " : " ", "[", option.selected ? "on" : "off", "] ", option.provider, "/", option.modelId] }, `${option.provider}/${option.modelId}`);
|
|
11
|
+
}), _jsxs(Text, { dimColor: props.useColor, children: [selectedOption?.selected ? "已选择;Enter 保存为下一个 Turn 默认" : "Space 选择此模型", props.screenReader ? " · model selector" : ""] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 三态权限 picker;只展示 Core 允许的模式和安全说明。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliPermissionOption } from "../../types.js";
|
|
4
|
+
/** 渲染三态权限 picker;只展示 Core 允许的模式和安全说明。 */
|
|
5
|
+
export declare function TuiPermissionPicker(props: {
|
|
6
|
+
options: CliPermissionOption[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { CLI_TUI_COLORS, TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 渲染三态权限 picker;只展示 Core 允许的模式和安全说明。 */
|
|
5
|
+
export function TuiPermissionPicker(props) {
|
|
6
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, props.options.length - 1)));
|
|
7
|
+
const selectedOption = props.options[selectedIndex];
|
|
8
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Permissions" }), _jsx(Text, { dimColor: props.useColor, children: "\u2191\u2193 select \u00B7 Space on/off \u00B7 Enter save \u00B7 Esc cancel" }), props.options.map(function renderPermission(option, index) {
|
|
9
|
+
const focused = index === selectedIndex;
|
|
10
|
+
const checkboxColor = option.mode === "full_access" && option.selected && props.useColor ? CLI_TUI_COLORS.permissionFullAccess : undefined;
|
|
11
|
+
return _jsxs(Text, { bold: focused, color: focused && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [focused ? "› " : " ", _jsxs(Text, { color: checkboxColor, dimColor: false, children: ["[", option.selected ? "on" : "off", "]"] }), " ", option.label, " (", option.mode, ")"] }, option.mode);
|
|
12
|
+
}), selectedOption ? _jsxs(Text, { dimColor: props.useColor, children: [" ", selectedOption.description] }) : null, _jsxs(Text, { dimColor: props.useColor, children: [selectedOption?.selected ? "已选择;Enter 保存为下一个 Turn 默认" : "Space 选择此权限模式", props.screenReader ? " · permission selector" : ""] })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Skill 选择器:按展示名、canonical 名称和说明筛选,方向键/空格/搜索操作。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliSkillSummary } from "../../types.js";
|
|
4
|
+
/** 以展示名、canonical 名称和说明筛选 TUI 内已脱敏的 Skill 摘要。 */
|
|
5
|
+
export declare function filterTuiSkills(skills: CliSkillSummary[], query: string): CliSkillSummary[];
|
|
6
|
+
/** 渲染可用方向键、空格开关和搜索操作的 Skill 选择器。 */
|
|
7
|
+
export declare function TuiSkillPicker(props: {
|
|
8
|
+
skills: CliSkillSummary[];
|
|
9
|
+
query: string;
|
|
10
|
+
selectedIndex: number;
|
|
11
|
+
useColor: boolean;
|
|
12
|
+
screenReader: boolean;
|
|
13
|
+
}): ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR, TUI_SKILL_OFF_COLOR, TUI_SKILL_ON_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 以展示名、canonical 名称和说明筛选 TUI 内已脱敏的 Skill 摘要。 */
|
|
5
|
+
export function filterTuiSkills(skills, query) {
|
|
6
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
7
|
+
if (!normalizedQuery)
|
|
8
|
+
return skills;
|
|
9
|
+
return skills.filter(function matchesSkill(skill) {
|
|
10
|
+
return `${skill.displayName}\n${skill.qualifiedName}\n${skill.description}`.toLocaleLowerCase().includes(normalizedQuery);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** 渲染可用方向键、空格开关和搜索操作的 Skill 选择器。 */
|
|
14
|
+
export function TuiSkillPicker(props) {
|
|
15
|
+
const filteredSkills = filterTuiSkills(props.skills, props.query);
|
|
16
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, filteredSkills.length - 1)));
|
|
17
|
+
const firstVisibleIndex = Math.max(0, Math.min(selectedIndex - 3, Math.max(0, filteredSkills.length - 8)));
|
|
18
|
+
const visibleSkills = filteredSkills.slice(firstVisibleIndex, firstVisibleIndex + 8);
|
|
19
|
+
const selectedSkill = filteredSkills[selectedIndex];
|
|
20
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Skills" }), _jsx(Text, { dimColor: props.useColor, children: "\u2191\u2193 select \u00B7 Space on/off \u00B7 Enter save \u00B7 type or / to search \u00B7 Esc cancel" }), _jsxs(Box, { borderStyle: "single", marginTop: 1, paddingX: 1, "aria-role": "textbox", children: [_jsxs(Text, { children: ["Search: ", props.query] }), _jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u258F" })] }), visibleSkills.length === 0 ? _jsx(Text, { dimColor: props.useColor, children: "no matching skills" }) : visibleSkills.map(function renderSkill(skill, visibleIndex) {
|
|
21
|
+
const index = firstVisibleIndex + visibleIndex;
|
|
22
|
+
const selected = index === selectedIndex;
|
|
23
|
+
return _jsxs(Text, { bold: selected, color: selected && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [selected ? "› " : " ", _jsxs(Text, { color: props.useColor ? skill.selected ? TUI_SKILL_ON_COLOR : TUI_SKILL_OFF_COLOR : undefined, children: ["[", skill.selected ? "on" : "off", "]"] }), " ", skill.displayName, " \u00B7 ", skill.qualifiedName] }, skill.qualifiedName);
|
|
24
|
+
}), selectedSkill?.description ? _jsxs(Text, { dimColor: props.useColor, children: [" ", selectedSkill.description] }) : null, _jsxs(Text, { dimColor: props.useColor, children: [filteredSkills.length === 0 ? "0 skills" : `${selectedIndex + 1}/${filteredSkills.length} skills`, props.screenReader ? " · skill selector" : ""] })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** `/` 触发的完整 slash 命令目录;用户只需选择,不必记住命令名。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliSlashCommand } from "../../types.js";
|
|
4
|
+
/** 渲染 `/` 触发的完整 slash 命令目录;用户只需选择,不必记住命令名。 */
|
|
5
|
+
export declare function TuiSlashCommandMenu(props: {
|
|
6
|
+
commands: CliSlashCommand[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 渲染 `/` 触发的完整 slash 命令目录;用户只需选择,不必记住命令名。 */
|
|
5
|
+
export function TuiSlashCommandMenu(props) {
|
|
6
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, Math.max(0, props.commands.length - 1)));
|
|
7
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", "aria-role": "listbox", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Commands" }), _jsx(Text, { dimColor: props.useColor, children: "\u2191\u2193 select \u00B7 Tab complete \u00B7 Enter choose \u00B7 Esc close" }), props.commands.map(function renderSlashCommand(command, index) {
|
|
8
|
+
const selected = index === selectedIndex;
|
|
9
|
+
return (_jsxs(Text, { bold: selected, color: selected && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [selected ? "› " : " ", command.usage, " ", _jsx(Text, { dimColor: props.useColor, children: command.description })] }, command.name));
|
|
10
|
+
}), _jsxs(Text, { dimColor: props.useColor, children: [selectedIndex + 1, "/", props.commands.length, " commands", props.screenReader ? " · command selector" : ""] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 带可视光标的多行输入;prompt 按 @mention 分段上色,所有文本放进同一个 Text 避免兄弟节点分别换行。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/** 渲染带可视光标的多行输入;所有 prompt 文本放进同一个 Text,避免兄弟节点分别换行。 */
|
|
4
|
+
export declare function TuiPromptInput(props: {
|
|
5
|
+
input: string;
|
|
6
|
+
cursorIndex: number;
|
|
7
|
+
columns: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
busy: boolean;
|
|
11
|
+
}): ReactNode;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../design-tokens.js";
|
|
4
|
+
import { TUI_CURSOR_CHARACTER, TUI_PROMPT_PREFIX } from "./constants.js";
|
|
5
|
+
import { splitTuiFileMentionSegments } from "./input/file-mention.js";
|
|
6
|
+
import { createTuiPromptLayout } from "./input/prompt-layout.js";
|
|
7
|
+
/** 渲染单个 mention/普通片段;mention 片段用强调色加粗,普通片段保持默认样式。 */
|
|
8
|
+
function renderSegment(segment, key, useColor) {
|
|
9
|
+
if (segment.isMention && useColor) {
|
|
10
|
+
return _jsx(Text, { color: TUI_ACCENT_COLOR, bold: true, children: segment.text }, key);
|
|
11
|
+
}
|
|
12
|
+
return _jsx(Text, { children: segment.text }, key);
|
|
13
|
+
}
|
|
14
|
+
/** 把一行文本按 mention 分段渲染,并将可视光标插入所属段的正确位置。 */
|
|
15
|
+
function renderLineWithCursor(lineText, cursorOffset, useColor, lineKey) {
|
|
16
|
+
const nodes = [];
|
|
17
|
+
const segments = splitTuiFileMentionSegments(lineText);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
let cursorRendered = cursorOffset < 0;
|
|
20
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
21
|
+
const segment = segments[index];
|
|
22
|
+
const start = offset;
|
|
23
|
+
const end = offset + segment.text.length;
|
|
24
|
+
offset = end;
|
|
25
|
+
if (cursorRendered || cursorOffset < start || cursorOffset > end) {
|
|
26
|
+
nodes.push(renderSegment(segment, `seg-${lineKey}-${index}`, useColor));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// 光标落在本段内:拆成 前/光标/后 三段,光标保持既有强调色。
|
|
30
|
+
const local = cursorOffset - start;
|
|
31
|
+
const color = segment.isMention && useColor ? TUI_ACCENT_COLOR : undefined;
|
|
32
|
+
const before = segment.text.slice(0, local);
|
|
33
|
+
const after = segment.text.slice(local);
|
|
34
|
+
if (before)
|
|
35
|
+
nodes.push(_jsx(Text, { color: color, bold: segment.isMention && useColor, children: before }, `seg-${lineKey}-${index}-b`));
|
|
36
|
+
nodes.push(_jsx(Text, { color: useColor ? TUI_ACCENT_COLOR : undefined, children: TUI_CURSOR_CHARACTER }, `prompt-cursor-${lineKey}`));
|
|
37
|
+
if (after)
|
|
38
|
+
nodes.push(_jsx(Text, { color: color, bold: segment.isMention && useColor, children: after }, `seg-${lineKey}-${index}-a`));
|
|
39
|
+
cursorRendered = true;
|
|
40
|
+
}
|
|
41
|
+
if (!cursorRendered) {
|
|
42
|
+
// 空行或段切分为空时仍需渲染光标,保持占位行为不变。
|
|
43
|
+
nodes.push(_jsx(Text, { color: useColor ? TUI_ACCENT_COLOR : undefined, children: TUI_CURSOR_CHARACTER }, `prompt-cursor-${lineKey}`));
|
|
44
|
+
}
|
|
45
|
+
return nodes;
|
|
46
|
+
}
|
|
47
|
+
/** 渲染带可视光标的多行输入;所有 prompt 文本放进同一个 Text,避免兄弟节点分别换行。 */
|
|
48
|
+
export function TuiPromptInput(props) {
|
|
49
|
+
const layout = createTuiPromptLayout(props.input, props.cursorIndex, Math.max(1, props.columns - 4));
|
|
50
|
+
const nodes = [];
|
|
51
|
+
const placeholder = props.screenReader && !props.input ? "prompt" : "";
|
|
52
|
+
for (let index = 0; index < layout.lines.length; index += 1) {
|
|
53
|
+
const line = layout.lines[index];
|
|
54
|
+
if (!line)
|
|
55
|
+
continue;
|
|
56
|
+
if (index > 0)
|
|
57
|
+
nodes.push("\n");
|
|
58
|
+
const isCursorLine = index === layout.cursorLine;
|
|
59
|
+
let lineText = line.text;
|
|
60
|
+
let cursorOffset = isCursorLine
|
|
61
|
+
? Math.max(0, Math.min(line.text.length, layout.cursorOffset - line.start))
|
|
62
|
+
: -1;
|
|
63
|
+
if (index === 0 && lineText.startsWith(TUI_PROMPT_PREFIX)) {
|
|
64
|
+
nodes.push(_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: TUI_PROMPT_PREFIX }, `prompt-prefix-${index}`));
|
|
65
|
+
lineText = lineText.slice(TUI_PROMPT_PREFIX.length);
|
|
66
|
+
cursorOffset = Math.max(0, cursorOffset - TUI_PROMPT_PREFIX.length);
|
|
67
|
+
}
|
|
68
|
+
nodes.push(...renderLineWithCursor(lineText, cursorOffset, props.useColor, index));
|
|
69
|
+
if (isCursorLine && placeholder)
|
|
70
|
+
nodes.push(placeholder);
|
|
71
|
+
}
|
|
72
|
+
return (_jsx(Box, { "aria-role": "textbox", "aria-state": { busy: props.busy, multiline: true }, width: props.columns, marginTop: 1, borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, children: _jsx(Text, { wrap: "wrap", children: nodes }) }));
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** TUI 启动期一次性解析的渲染模式纯函数:颜色/读屏/降级/渲染模式策略。 */
|
|
2
|
+
import type { TuiRenderMode } from "./types.js";
|
|
3
|
+
/** 判断当前环境是否要求关闭 ANSI 颜色。 */
|
|
4
|
+
export declare function resolveNoColor(env: NodeJS.ProcessEnv): boolean;
|
|
5
|
+
/** 解析 Ink screen-reader 开关,并保留 Ink 官方环境变量兼容性。 */
|
|
6
|
+
export declare function resolveScreenReader(env: NodeJS.ProcessEnv): boolean;
|
|
7
|
+
/** R0 冻结的最终模式策略;平台由调用方注入,避免测试伪造全局 process.platform。 */
|
|
8
|
+
export declare function resolveTuiRenderModePolicy(input: {
|
|
9
|
+
env: NodeJS.ProcessEnv;
|
|
10
|
+
platform: NodeJS.Platform | string;
|
|
11
|
+
}): TuiRenderMode;
|
|
12
|
+
/** R2 的 win32 FULLSCREEN 回落提示;返回值由启动副作用层写入 stderr。 */
|
|
13
|
+
export declare function getTuiRenderModeFallbackNotice(input: {
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
platform: NodeJS.Platform | string;
|
|
16
|
+
}): string | undefined;
|
|
17
|
+
/** 仅在 TUI 启动期决定内联或备用屏;宿主 TTY/Plain 门禁仍由 main.ts 保持。 */
|
|
18
|
+
export declare function resolveTuiRenderMode(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform | string): TuiRenderMode;
|
|
19
|
+
/** 解析终端 reduced-motion 开关;非标准环境变量只作为 CLI 宿主的本地降级提示。 */
|
|
20
|
+
export declare function resolveReducedMotion(env: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** TUI 启动期一次性解析的渲染模式纯函数:颜色/读屏/降级/渲染模式策略。 */
|
|
2
|
+
/** 判断当前环境是否要求关闭 ANSI 颜色。 */
|
|
3
|
+
export function resolveNoColor(env) {
|
|
4
|
+
return env.NO_COLOR !== undefined && env.NO_COLOR !== "";
|
|
5
|
+
}
|
|
6
|
+
/** 解析 Ink screen-reader 开关,并保留 Ink 官方环境变量兼容性。 */
|
|
7
|
+
export function resolveScreenReader(env) {
|
|
8
|
+
return env.INK_SCREEN_READER === "true" || env.SCREEN_READER === "1" || env.SCREEN_READER === "true";
|
|
9
|
+
}
|
|
10
|
+
/** R0 冻结的最终模式策略;平台由调用方注入,避免测试伪造全局 process.platform。 */
|
|
11
|
+
export function resolveTuiRenderModePolicy(input) {
|
|
12
|
+
if (resolveScreenReader(input.env) || input.env.AUTOPILOT_INLINE_TUI === "1")
|
|
13
|
+
return "inline";
|
|
14
|
+
if (input.env.AUTOPILOT_TUI_FULLSCREEN === "1" && input.platform !== "win32")
|
|
15
|
+
return "alternate-screen";
|
|
16
|
+
return "inline";
|
|
17
|
+
}
|
|
18
|
+
/** R2 的 win32 FULLSCREEN 回落提示;返回值由启动副作用层写入 stderr。 */
|
|
19
|
+
export function getTuiRenderModeFallbackNotice(input) {
|
|
20
|
+
if (input.platform !== "win32" || input.env.AUTOPILOT_TUI_FULLSCREEN !== "1")
|
|
21
|
+
return undefined;
|
|
22
|
+
return "AUTOPILOT_TUI_FULLSCREEN is unsupported on win32; using inline mode. Remove AUTOPILOT_TUI_FULLSCREEN to cancel this request.\n";
|
|
23
|
+
}
|
|
24
|
+
/** 仅在 TUI 启动期决定内联或备用屏;宿主 TTY/Plain 门禁仍由 main.ts 保持。 */
|
|
25
|
+
export function resolveTuiRenderMode(env, platform = process.platform) {
|
|
26
|
+
return resolveTuiRenderModePolicy({ env, platform });
|
|
27
|
+
}
|
|
28
|
+
/** 解析终端 reduced-motion 开关;非标准环境变量只作为 CLI 宿主的本地降级提示。 */
|
|
29
|
+
export function resolveReducedMotion(env) {
|
|
30
|
+
return env.REDUCED_MOTION === "1" || env.REDUCED_MOTION === "true" || env.TERM_REDUCED_MOTION === "1" || env.TERM_REDUCED_MOTION === "true";
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** TUI 入口编排:启动 Ink,统一处理信号、退出码和动态渲染生命周期。 */
|
|
2
|
+
import type { CliTuiRecap, RunCliTuiOptions, RunCliTuiResult } from "./types.js";
|
|
3
|
+
/** 将已知的安全会话字段格式化为主屏 recap;输出保持纯文本,兼容 NO_COLOR。 */
|
|
4
|
+
export declare function formatCliTuiRecap(recap: CliTuiRecap): string;
|
|
5
|
+
/** 启动 Ink,统一处理信号、退出码和动态渲染生命周期。 */
|
|
6
|
+
export declare function runCliTui(options: RunCliTuiOptions): Promise<RunCliTuiResult>;
|