@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,58 @@
|
|
|
1
|
+
/** TUI 按键解析纯函数:归一化控制动作、删除方向、换行/粘贴/focus/滚动判断。 */
|
|
2
|
+
import { splitTuiInputGraphemes } from "./graphemes.js";
|
|
3
|
+
/** 将 Ink 键对象和终端控制字符归一为 TUI 控制动作,兼容真实 TTY 与测试 stdin。 */
|
|
4
|
+
export function parseTuiControlAction(value, key) {
|
|
5
|
+
if (value === "\u0004" || (key.ctrl && value === "d"))
|
|
6
|
+
return "eof";
|
|
7
|
+
if ((key.ctrl && (value === "c" || value === "\u0003")) || value === "\u0003")
|
|
8
|
+
return "interrupt";
|
|
9
|
+
if (key.escape || value === "\u001b")
|
|
10
|
+
return "cancel";
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** 区分 Backspace 与前向 Delete,优先采用终端原始控制字符兼容 macOS Terminal。 */
|
|
14
|
+
export function resolveTuiDeleteDirection(value, key, rawSequence = "") {
|
|
15
|
+
if (key.backspace || rawSequence === "\b" || rawSequence === "\u007f" || value === "\b" || value === "\u007f")
|
|
16
|
+
return "backspace";
|
|
17
|
+
if (/^\u001b\[3(?:;\d+)?[~$^]$/u.test(rawSequence))
|
|
18
|
+
return "delete";
|
|
19
|
+
if (key.delete)
|
|
20
|
+
return "delete";
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** 识别 Claude Code/终端常见的 Shift+Enter 编码;普通回车仍由 key.return 单独处理。 */
|
|
24
|
+
export function isTuiMultilineEnter(value, key, rawSequence = "") {
|
|
25
|
+
if (key.return && (key.shift || key.meta))
|
|
26
|
+
return true;
|
|
27
|
+
const sequencePattern = /(?:\u001b)?\[(?:13;2(?::\d+)?(?:;[\d:]+)?u|13;2~|27;2;13~)$/u;
|
|
28
|
+
if (value && sequencePattern.test(value))
|
|
29
|
+
return true;
|
|
30
|
+
// Ink 的测试 stdin 与 readable 事件先后顺序不同;只有当前事件确实是空输入的特殊键时才读取原始序列,避免复用上一枚按键。
|
|
31
|
+
return value.length === 0 && key.shift && sequencePattern.test(rawSequence);
|
|
32
|
+
}
|
|
33
|
+
/** 识别 bracketed paste 边界;Ink 会去掉 CSI 序列的首个 ESC 后再交给 useInput。 */
|
|
34
|
+
export function resolveTuiBracketedPasteBoundary(value) {
|
|
35
|
+
if (value === "[200~" || value === "\u001b[200~")
|
|
36
|
+
return "start";
|
|
37
|
+
if (value === "[201~" || value === "\u001b[201~")
|
|
38
|
+
return "end";
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/** 判断终端 focus in/out 报告,避免控制序列被误插入 prompt。 */
|
|
42
|
+
export function isTuiTerminalFocusSequence(value) {
|
|
43
|
+
return value === "[I" || value === "[O" || value === "\u001b[I" || value === "\u001b[O";
|
|
44
|
+
}
|
|
45
|
+
/** 判断按键是否属于备用屏滚动兜底,避免与普通 Home/End 输入语义混淆。 */
|
|
46
|
+
export function isTuiViewportScrollKey(key) {
|
|
47
|
+
return key.pageUp || key.pageDown || Boolean(key.ctrl && (key.home || key.end));
|
|
48
|
+
}
|
|
49
|
+
/** 判断光标前的反斜线是否是未被另一枚反斜线转义的续行标记。 */
|
|
50
|
+
export function hasTuiUnescapedContinuation(input, cursorIndex) {
|
|
51
|
+
const characters = splitTuiInputGraphemes(input);
|
|
52
|
+
if (cursorIndex <= 0 || characters[cursorIndex - 1] !== "\\")
|
|
53
|
+
return false;
|
|
54
|
+
let slashCount = 0;
|
|
55
|
+
for (let index = cursorIndex - 1; index >= 0 && characters[index] === "\\"; index -= 1)
|
|
56
|
+
slashCount += 1;
|
|
57
|
+
return slashCount % 2 === 1;
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** TUI prompt 视觉布局纯函数:测量视觉行、映射光标行列、生成布局。 */
|
|
2
|
+
/** 一条 prompt 视觉行及其源文本映射。 */
|
|
3
|
+
export interface TuiPromptVisualLine {
|
|
4
|
+
text: string;
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
width: number;
|
|
8
|
+
}
|
|
9
|
+
/** prompt 的完整视觉布局;输入状态仍只保存逻辑 grapheme cursorIndex。 */
|
|
10
|
+
export interface TuiPromptLayout {
|
|
11
|
+
source: string;
|
|
12
|
+
lines: TuiPromptVisualLine[];
|
|
13
|
+
cursorOffset: number;
|
|
14
|
+
cursorLine: number;
|
|
15
|
+
cursorColumn: number;
|
|
16
|
+
}
|
|
17
|
+
/** 按 Ink 的 hard wrap 规则测量 prompt 的视觉行,并保留每行对应的源文本 offset。 */
|
|
18
|
+
export declare function measureTuiPromptVisualLines(source: string, columns: number): TuiPromptVisualLine[];
|
|
19
|
+
/** 将源文本 offset 映射为视觉行列;精确区分软换行和显式换行。 */
|
|
20
|
+
export declare function getTuiPromptCursorPosition(lines: TuiPromptVisualLine[], source: string, cursorOffset: number): {
|
|
21
|
+
line: number;
|
|
22
|
+
column: number;
|
|
23
|
+
};
|
|
24
|
+
/** 将视觉行列映射回最近的 grapheme 边界,避免把中文或组合 emoji 拆开。 */
|
|
25
|
+
export declare function resolveTuiPromptCursorIndex(input: string, layout: TuiPromptLayout, targetLine: number, targetColumn: number): number;
|
|
26
|
+
/** 生成 prompt 的视觉布局;输入状态仍只保存逻辑 grapheme cursorIndex。 */
|
|
27
|
+
export declare function createTuiPromptLayout(input: string, cursorIndex: number, columns: number): TuiPromptLayout;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** TUI prompt 视觉布局纯函数:测量视觉行、映射光标行列、生成布局。 */
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
import { TUI_CURSOR_WIDTH, TUI_INPUT_SEGMENTER, TUI_PROMPT_PREFIX } from "../constants.js";
|
|
5
|
+
import { splitTuiInputGraphemes } from "./graphemes.js";
|
|
6
|
+
/** 按 Ink 的 hard wrap 规则测量 prompt 的视觉行,并保留每行对应的源文本 offset。 */
|
|
7
|
+
export function measureTuiPromptVisualLines(source, columns) {
|
|
8
|
+
const wrapWidth = Math.max(1, columns - TUI_CURSOR_WIDTH);
|
|
9
|
+
const wrapped = wrapAnsi(source, wrapWidth, { hard: true, trim: false });
|
|
10
|
+
const wrappedLines = wrapped.split("\n");
|
|
11
|
+
const lines = [];
|
|
12
|
+
let searchOffset = 0;
|
|
13
|
+
for (const line of wrappedLines) {
|
|
14
|
+
let start = line.length > 0 ? source.indexOf(line, searchOffset) : searchOffset;
|
|
15
|
+
if (start < 0)
|
|
16
|
+
start = searchOffset;
|
|
17
|
+
const end = start + line.length;
|
|
18
|
+
lines.push({ text: line, start, end, width: stringWidth(line) });
|
|
19
|
+
searchOffset = end;
|
|
20
|
+
if (source[searchOffset] === "\n")
|
|
21
|
+
searchOffset += 1;
|
|
22
|
+
}
|
|
23
|
+
return lines.length > 0 ? lines : [{ text: "", start: 0, end: 0, width: 0 }];
|
|
24
|
+
}
|
|
25
|
+
/** 将源文本 offset 映射为视觉行列;精确区分软换行和显式换行。 */
|
|
26
|
+
export function getTuiPromptCursorPosition(lines, source, cursorOffset) {
|
|
27
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
28
|
+
const line = lines[index];
|
|
29
|
+
if (!line || cursorOffset < line.start || cursorOffset > line.end)
|
|
30
|
+
continue;
|
|
31
|
+
// 软换行后的边界属于下一行;显式换行前的边界仍属于当前行。
|
|
32
|
+
if (cursorOffset === line.end && lines[index + 1]?.start === line.end && source[line.end] !== "\n")
|
|
33
|
+
continue;
|
|
34
|
+
const localOffset = Math.max(0, Math.min(line.text.length, cursorOffset - line.start));
|
|
35
|
+
return { line: index, column: stringWidth(line.text.slice(0, localOffset)) };
|
|
36
|
+
}
|
|
37
|
+
const lastLine = lines[lines.length - 1] ?? { text: "", start: 0, end: 0, width: 0 };
|
|
38
|
+
return { line: Math.max(0, lines.length - 1), column: lastLine.width };
|
|
39
|
+
}
|
|
40
|
+
/** 将视觉行列映射回最近的 grapheme 边界,避免把中文或组合 emoji 拆开。 */
|
|
41
|
+
export function resolveTuiPromptCursorIndex(input, layout, targetLine, targetColumn) {
|
|
42
|
+
const line = layout.lines[targetLine];
|
|
43
|
+
if (!line)
|
|
44
|
+
return splitTuiInputGraphemes(input).length;
|
|
45
|
+
const minimumOffset = line.start === 0 ? TUI_PROMPT_PREFIX.length : line.start;
|
|
46
|
+
const minimumLocalOffset = Math.max(0, minimumOffset - line.start);
|
|
47
|
+
let bestOffset = Math.max(line.start, minimumOffset);
|
|
48
|
+
let bestColumn = stringWidth(line.text.slice(0, minimumLocalOffset));
|
|
49
|
+
function consider(localOffset) {
|
|
50
|
+
if (localOffset < minimumLocalOffset || localOffset > line.text.length)
|
|
51
|
+
return;
|
|
52
|
+
const column = stringWidth(line.text.slice(0, localOffset));
|
|
53
|
+
if (column <= targetColumn && column >= bestColumn) {
|
|
54
|
+
bestColumn = column;
|
|
55
|
+
bestOffset = line.start + localOffset;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
consider(minimumLocalOffset);
|
|
59
|
+
for (const segment of TUI_INPUT_SEGMENTER.segment(line.text)) {
|
|
60
|
+
consider(segment.index);
|
|
61
|
+
consider(segment.index + segment.segment.length);
|
|
62
|
+
}
|
|
63
|
+
const inputOffset = Math.max(0, Math.min(input.length, bestOffset - TUI_PROMPT_PREFIX.length));
|
|
64
|
+
let cursorIndex = 0;
|
|
65
|
+
let sourceOffset = 0;
|
|
66
|
+
for (const segment of TUI_INPUT_SEGMENTER.segment(input)) {
|
|
67
|
+
if (sourceOffset + segment.segment.length > inputOffset)
|
|
68
|
+
break;
|
|
69
|
+
sourceOffset += segment.segment.length;
|
|
70
|
+
cursorIndex += 1;
|
|
71
|
+
}
|
|
72
|
+
return cursorIndex;
|
|
73
|
+
}
|
|
74
|
+
/** 生成 prompt 的视觉布局;输入状态仍只保存逻辑 grapheme cursorIndex。 */
|
|
75
|
+
export function createTuiPromptLayout(input, cursorIndex, columns) {
|
|
76
|
+
const characters = splitTuiInputGraphemes(input);
|
|
77
|
+
const cursor = Math.max(0, Math.min(cursorIndex, characters.length));
|
|
78
|
+
const inputOffset = characters.slice(0, cursor).join("").length;
|
|
79
|
+
const source = TUI_PROMPT_PREFIX + input;
|
|
80
|
+
const lines = measureTuiPromptVisualLines(source, columns);
|
|
81
|
+
const cursorOffset = TUI_PROMPT_PREFIX.length + inputOffset;
|
|
82
|
+
const position = getTuiPromptCursorPosition(lines, source, cursorOffset);
|
|
83
|
+
return { source, lines, cursorOffset, cursorLine: position.line, cursorColumn: position.column };
|
|
84
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** TUI 输入子树:隔离 prompt、光标与 picker 状态,避免普通按键重建完整时间线。 */
|
|
2
|
+
import { type Key } from "ink";
|
|
3
|
+
import type { CliAction, CliSlashCommand } from "../types.js";
|
|
4
|
+
import { usePickers } from "./hooks/use-pickers.js";
|
|
5
|
+
import type { FileMentionIndex } from "./hooks/use-file-mention-index.js";
|
|
6
|
+
import type { CliTuiStore } from "./types.js";
|
|
7
|
+
/** 输入区与父级之间共享的稳定 ref;普通按键不需要把输入正文写回父级。 */
|
|
8
|
+
export interface TuiInputDerivedState {
|
|
9
|
+
slashCommandSuggestions: CliSlashCommand[];
|
|
10
|
+
fileMentionSuggestions: string[];
|
|
11
|
+
handleTuiViewportKey: (key: Key) => boolean;
|
|
12
|
+
}
|
|
13
|
+
/** React ref 形状;输入子树与父级共享同一份派生值容器。 */
|
|
14
|
+
export type TuiInputDerivedRef = {
|
|
15
|
+
current: TuiInputDerivedState;
|
|
16
|
+
};
|
|
17
|
+
/** 输入区布局高度;只有跨视觉行或 picker 状态变化时才需要通知父级。 */
|
|
18
|
+
export interface TuiInputLayout {
|
|
19
|
+
promptHeight: number;
|
|
20
|
+
pickerHeight: number;
|
|
21
|
+
}
|
|
22
|
+
type TuiDispatchAction = (action: CliAction, prompt?: string, preserveSkillPickerQuery?: boolean) => Promise<void>;
|
|
23
|
+
/** 将 prompt 状态、输入分发和 picker 展示隔离到独立子树。 */
|
|
24
|
+
export declare const TuiInputArea: import("react").NamedExoticComponent<{
|
|
25
|
+
store: CliTuiStore;
|
|
26
|
+
columns: number;
|
|
27
|
+
busy: {
|
|
28
|
+
current: boolean;
|
|
29
|
+
};
|
|
30
|
+
keepOpenAfterCancel: {
|
|
31
|
+
current: boolean;
|
|
32
|
+
};
|
|
33
|
+
lastSubmittedPromptRef: {
|
|
34
|
+
current: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
rawInputSequenceRef: {
|
|
37
|
+
current: string;
|
|
38
|
+
};
|
|
39
|
+
dispatchAction: TuiDispatchAction;
|
|
40
|
+
onEof: () => void;
|
|
41
|
+
onRequestInterrupt: () => void;
|
|
42
|
+
onRequestCancel: () => void;
|
|
43
|
+
isRawModeSupported: boolean;
|
|
44
|
+
isInputActive: boolean;
|
|
45
|
+
showInput: boolean;
|
|
46
|
+
useColor: boolean;
|
|
47
|
+
screenReader: boolean;
|
|
48
|
+
pickers: ReturnType<typeof usePickers>;
|
|
49
|
+
derivedRef: TuiInputDerivedRef;
|
|
50
|
+
fileMentionIndex?: FileMentionIndex;
|
|
51
|
+
onLayoutChange?(layout: TuiInputLayout): void;
|
|
52
|
+
}>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** TUI 输入子树:隔离 prompt、光标与 picker 状态,避免普通按键重建完整时间线。 */
|
|
3
|
+
import { memo, useEffect, useLayoutEffect } from "react";
|
|
4
|
+
import { useInput } from "ink";
|
|
5
|
+
import { getSlashCommandSuggestions } from "../parser.js";
|
|
6
|
+
import { usePromptInput } from "./hooks/use-prompt-input.js";
|
|
7
|
+
import { extractFileMentionToken, filterFileMentionCandidates } from "./input/file-mention.js";
|
|
8
|
+
import { createTuiPromptLayout } from "./input/prompt-layout.js";
|
|
9
|
+
import { recordTuiPerformanceDiagnostic } from "./performance-diagnostics.js";
|
|
10
|
+
import { TuiEffortPicker } from "./pickers/effort.js";
|
|
11
|
+
import { TuiFileMentionMenu, TUI_FILE_MENTION_VISIBLE_ROWS } from "./pickers/file-mention.js";
|
|
12
|
+
import { TuiModelPicker } from "./pickers/model.js";
|
|
13
|
+
import { TuiPermissionPicker } from "./pickers/permission.js";
|
|
14
|
+
import { TuiSkillPicker } from "./pickers/skill.js";
|
|
15
|
+
import { TuiSlashCommandMenu } from "./pickers/slash.js";
|
|
16
|
+
import { TuiPromptInput } from "./prompt-input.js";
|
|
17
|
+
/** 根据当前 picker 估算固定区高度,供备用屏计算可用视口高度。 */
|
|
18
|
+
function getTuiPickerHeight(input) {
|
|
19
|
+
if (input.permissions)
|
|
20
|
+
return input.permissions.length + 6;
|
|
21
|
+
if (input.models)
|
|
22
|
+
return input.models.length + 5;
|
|
23
|
+
if (input.efforts)
|
|
24
|
+
return input.efforts.length + 5;
|
|
25
|
+
if (input.skills)
|
|
26
|
+
return Math.min(8, input.skills.length) + 8;
|
|
27
|
+
if (input.slashCommands.length > 0)
|
|
28
|
+
return input.slashCommands.length + 5;
|
|
29
|
+
if (input.fileMentionCandidates.length > 0)
|
|
30
|
+
return Math.min(TUI_FILE_MENTION_VISIBLE_ROWS, input.fileMentionCandidates.length) + 5;
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
/** 将 prompt 状态、输入分发和 picker 展示隔离到独立子树。 */
|
|
34
|
+
export const TuiInputArea = memo(function TuiInputArea(props) {
|
|
35
|
+
const pickers = props.pickers;
|
|
36
|
+
const { input, cursorIndex, handleInput } = usePromptInput({
|
|
37
|
+
store: props.store,
|
|
38
|
+
columns: props.columns,
|
|
39
|
+
busy: props.busy,
|
|
40
|
+
keepOpenAfterCancel: props.keepOpenAfterCancel,
|
|
41
|
+
lastSubmittedPromptRef: props.lastSubmittedPromptRef,
|
|
42
|
+
rawInputSequenceRef: props.rawInputSequenceRef,
|
|
43
|
+
dispatchAction: props.dispatchAction,
|
|
44
|
+
onEof: props.onEof,
|
|
45
|
+
onRequestInterrupt: props.onRequestInterrupt,
|
|
46
|
+
onRequestCancel: props.onRequestCancel,
|
|
47
|
+
isPickerActive: pickers.isPickerActive,
|
|
48
|
+
dispatchPickerInput: pickers.dispatchPickerInput,
|
|
49
|
+
approvalPickerIndexRef: pickers.approvalPickerIndexRef,
|
|
50
|
+
updateApprovalPickerIndex: pickers.updateApprovalPickerIndex,
|
|
51
|
+
slashCommandIndexRef: pickers.slashCommandIndexRef,
|
|
52
|
+
updateSlashCommandIndex: pickers.updateSlashCommandIndex,
|
|
53
|
+
fileMentionIndexRef: pickers.fileMentionIndexRef,
|
|
54
|
+
updateFileMentionIndex: pickers.updateFileMentionIndex,
|
|
55
|
+
setFileMentionDismissedInput: pickers.setFileMentionDismissedInput,
|
|
56
|
+
derivedRef: props.derivedRef,
|
|
57
|
+
});
|
|
58
|
+
const slashCommandSuggestions = getSlashCommandSuggestions(input);
|
|
59
|
+
props.derivedRef.current.slashCommandSuggestions = slashCommandSuggestions;
|
|
60
|
+
// @ 菜单派生:索引就绪且未被 Esc 关闭时,按当前 token 的四档评分过滤候选池。
|
|
61
|
+
const fileMentionToken = props.fileMentionIndex?.ready ? extractFileMentionToken(input, cursorIndex) : null;
|
|
62
|
+
const fileMentionSuggestions = fileMentionToken && pickers.fileMentionDismissedInput !== input
|
|
63
|
+
? filterFileMentionCandidates(props.fileMentionIndex?.entries ?? [], fileMentionToken.query)
|
|
64
|
+
: [];
|
|
65
|
+
props.derivedRef.current.fileMentionSuggestions = fileMentionSuggestions;
|
|
66
|
+
const promptHeight = props.showInput ? createTuiPromptLayout(input, cursorIndex, Math.max(1, props.columns - 4)).lines.length + 3 : 0;
|
|
67
|
+
const pickerHeight = props.showInput
|
|
68
|
+
? getTuiPickerHeight({ permissions: pickers.permissionPickerOptions, models: pickers.modelPickerOptions, efforts: pickers.effortPickerOptions, skills: pickers.skillPickerSkills, slashCommands: slashCommandSuggestions, fileMentionCandidates: fileMentionSuggestions })
|
|
69
|
+
: 0;
|
|
70
|
+
useLayoutEffect(function notifyInputLayoutChange() {
|
|
71
|
+
props.onLayoutChange?.({ promptHeight, pickerHeight });
|
|
72
|
+
}, [pickerHeight, promptHeight, props.onLayoutChange]);
|
|
73
|
+
useLayoutEffect(function recordInputFrameCommit() {
|
|
74
|
+
if (props.showInput) {
|
|
75
|
+
// 诊断只记录输入帧提交时刻,不记录 prompt 正文。
|
|
76
|
+
recordTuiPerformanceDiagnostic("input_frame_committed", "prompt");
|
|
77
|
+
}
|
|
78
|
+
}, [input, cursorIndex, props.showInput]);
|
|
79
|
+
useEffect(function resetSlashCommandSelection() {
|
|
80
|
+
pickers.updateSlashCommandIndex(0);
|
|
81
|
+
}, [slashCommandSuggestions.map(function getCommandName(command) { return command.name; }).join("\u0000")]);
|
|
82
|
+
useEffect(function resetFileMentionSelection() {
|
|
83
|
+
pickers.updateFileMentionIndex(0);
|
|
84
|
+
}, [fileMentionSuggestions.join("\u0000")]);
|
|
85
|
+
useInput(handleInput, { isActive: props.isRawModeSupported && props.isInputActive });
|
|
86
|
+
if (!props.showInput)
|
|
87
|
+
return null;
|
|
88
|
+
return _jsxs(_Fragment, { children: [_jsx(TuiPromptInput, { input: input, cursorIndex: cursorIndex, columns: props.columns, useColor: props.useColor, screenReader: props.screenReader, busy: props.busy.current }), pickers.permissionPickerOptions ? (_jsx(TuiPermissionPicker, { options: pickers.permissionPickerOptions, selectedIndex: pickers.permissionPickerIndex, useColor: props.useColor, screenReader: props.screenReader })) : pickers.modelPickerOptions ? (_jsx(TuiModelPicker, { options: pickers.modelPickerOptions, selectedIndex: pickers.modelPickerIndex, useColor: props.useColor, screenReader: props.screenReader })) : pickers.effortPickerOptions ? (_jsx(TuiEffortPicker, { options: pickers.effortPickerOptions, selectedIndex: pickers.effortPickerIndex, useColor: props.useColor, screenReader: props.screenReader })) : pickers.skillPickerSkills ? (_jsx(TuiSkillPicker, { skills: pickers.skillPickerSkills, query: pickers.skillPickerQuery, selectedIndex: pickers.skillPickerIndex, useColor: props.useColor, screenReader: props.screenReader })) : slashCommandSuggestions.length > 0 ? (_jsx(TuiSlashCommandMenu, { commands: slashCommandSuggestions, selectedIndex: pickers.slashCommandIndex, useColor: props.useColor, screenReader: props.screenReader })) : fileMentionSuggestions.length > 0 ? (_jsx(TuiFileMentionMenu, { candidates: fileMentionSuggestions, selectedIndex: pickers.fileMentionIndex, useColor: props.useColor, screenReader: props.screenReader })) : null] });
|
|
89
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 当前 CLI Session 的后台任务列表、详情与有限安全日志视图。 */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { CliJobView } from "../types.js";
|
|
4
|
+
/** 渲染当前 Job 管理视图;按键由 app.tsx 统一分发,避免创建第二套输入器。 */
|
|
5
|
+
export declare function TuiJobs(props: {
|
|
6
|
+
view: CliJobView;
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
screenReader: boolean;
|
|
10
|
+
}): ReactNode;
|
package/dist/tui/jobs.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../design-tokens.js";
|
|
4
|
+
/** 将毫秒时间戳格式化为稳定的本地可读时间。 */
|
|
5
|
+
function formatJobTime(value) {
|
|
6
|
+
return value === undefined ? "-" : new Date(value).toLocaleString();
|
|
7
|
+
}
|
|
8
|
+
/** 渲染单路安全日志,并明确标注缺口、重置和截断事实。 */
|
|
9
|
+
function TuiJobOutput(props) {
|
|
10
|
+
const flags = [props.stream.gap ? "gap" : "", props.stream.resetRequired ? "reset" : "", props.stream.truncated ? "truncated" : ""].filter(Boolean).join(", ");
|
|
11
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [props.label, flags ? ` (${flags})` : ""] }), _jsx(Text, { children: props.stream.text || "(empty)" })] });
|
|
12
|
+
}
|
|
13
|
+
/** 渲染当前 Job 管理视图;按键由 app.tsx 统一分发,避免创建第二套输入器。 */
|
|
14
|
+
export function TuiJobs(props) {
|
|
15
|
+
if (props.view.mode === "list") {
|
|
16
|
+
const jobs = props.view.list.jobs;
|
|
17
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, "aria-role": "list", children: [_jsx(Text, { bold: true, children: "Background jobs" }), jobs.length === 0 ? _jsx(Text, { children: "No jobs in this session." }) : jobs.map(function renderJob(job, index) {
|
|
18
|
+
const selected = index === props.selectedIndex;
|
|
19
|
+
return _jsxs(Text, { bold: selected, color: selected && props.useColor ? TUI_ACCENT_COLOR : undefined, children: [selected ? "› " : " ", job.jobId, " \u00B7 ", job.status, " \u00B7 ", job.controlState, job.canStop ? " · stoppable" : ""] }, job.jobId);
|
|
20
|
+
}), _jsxs(Text, { dimColor: props.useColor, children: ["\u2191\u2193 select \u00B7 Enter details \u00B7 o output \u00B7 s stop \u00B7 r refresh \u00B7 Esc close", props.screenReader ? " · job list" : ""] })] });
|
|
21
|
+
}
|
|
22
|
+
const job = props.view.job;
|
|
23
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, "aria-role": "list", children: [_jsx(Text, { bold: true, children: "Background job" }), _jsxs(Text, { children: ["ID: ", job.jobId] }), _jsxs(Text, { children: ["Status: ", job.status, " \u00B7 Control: ", job.controlState, " \u00B7 Stop: ", job.canStop ? "available" : "unavailable"] }), _jsxs(Text, { children: ["Started: ", formatJobTime(job.startedAt), " \u00B7 Finished: ", formatJobTime(job.finishedAt), " \u00B7 Exit: ", job.exitCode ?? "-"] }), _jsxs(Text, { children: ["Output: stdout ", job.stdoutBytes, " B \u00B7 stderr ", job.stderrBytes, " B", job.outputTruncated ? " · truncated" : ""] }), props.view.output ? _jsxs(_Fragment, { children: [_jsx(TuiJobOutput, { label: "stdout", stream: props.view.output.stdout }), _jsx(TuiJobOutput, { label: "stderr", stream: props.view.output.stderr })] }) : null, _jsxs(Text, { dimColor: props.useColor, children: ["o output \u00B7 s stop \u00B7 r refresh \u00B7 Esc back", props.screenReader ? " · job details" : ""] })] });
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** TUI 终端生命周期:鼠标事件入口、Ink stdin 包装与幂等的控制序列生命周期。 */
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import type { TuiMouseInputSink, TuiRenderMode, TuiTerminalLifecycle } from "./types.js";
|
|
4
|
+
import type { TuiOutputStream } from "./types.js";
|
|
5
|
+
/** 创建跨 DPI resize 时使用的 Ink 输出代理与尺寸提交函数。 */
|
|
6
|
+
export declare function createTuiRenderStdout(source: TuiOutputStream, initialSize: {
|
|
7
|
+
columns: number;
|
|
8
|
+
rows: number;
|
|
9
|
+
}): {
|
|
10
|
+
stdout: TuiOutputStream;
|
|
11
|
+
setSize(size: {
|
|
12
|
+
columns: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
}): void;
|
|
15
|
+
};
|
|
16
|
+
/** 创建可在 Ink 挂载前后安全使用的鼠标事件入口。 */
|
|
17
|
+
export declare function createTuiMouseInputSink(): TuiMouseInputSink;
|
|
18
|
+
/** 把原始 TTY 包装为 Ink 专用输入流,并转发 raw-mode 与 ref 生命周期以保持 Ink 挂载语义。 */
|
|
19
|
+
export declare function createRoutedTuiStdin(input: NodeJS.ReadStream): PassThrough;
|
|
20
|
+
/** 创建幂等的终端控制序列生命周期,保证所有退出路径只恢复一次。 */
|
|
21
|
+
export declare function createTuiTerminalLifecycle(stdout: {
|
|
22
|
+
write(chunk: string): void;
|
|
23
|
+
}, mode: TuiRenderMode): TuiTerminalLifecycle;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** TUI 终端生命周期:鼠标事件入口、Ink stdin 包装与幂等的控制序列生命周期。 */
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { TUI_ALTERNATE_SCREEN_DISABLE, TUI_ALTERNATE_SCREEN_ENABLE, TUI_BRACKETED_PASTE_DISABLE, TUI_BRACKETED_PASTE_ENABLE, TUI_INLINE_RESIZE_CLEAR, TUI_MOUSE_TRACKING_DISABLE, TUI_MOUSE_TRACKING_ENABLE, TUI_TERMINAL_FOCUS_DISABLE, TUI_TERMINAL_FOCUS_ENABLE, } from "./constants.js";
|
|
4
|
+
/** Ink 使用的稳定尺寸输出代理;它转发写入并在新布局提交后通知 resize。 */
|
|
5
|
+
class StableTuiOutputStream {
|
|
6
|
+
source;
|
|
7
|
+
size;
|
|
8
|
+
resizeListeners = new Set();
|
|
9
|
+
constructor(source, initialSize) {
|
|
10
|
+
this.source = source;
|
|
11
|
+
this.size = initialSize;
|
|
12
|
+
}
|
|
13
|
+
get columns() { return this.size.columns; }
|
|
14
|
+
get rows() { return this.size.rows; }
|
|
15
|
+
get isTTY() { return this.source.isTTY; }
|
|
16
|
+
/** 将 Ink 的所有输出原样写入真实终端。 */
|
|
17
|
+
write(chunk, ...args) {
|
|
18
|
+
const write = this.source.write;
|
|
19
|
+
const result = write.call(this.source, chunk, ...args);
|
|
20
|
+
return result !== false;
|
|
21
|
+
}
|
|
22
|
+
/** 保存 Ink 的 resize 监听,等待应用布局提交后再触发。 */
|
|
23
|
+
on(event, listener) {
|
|
24
|
+
if (event === "resize")
|
|
25
|
+
this.resizeListeners.add(listener);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/** 移除 Ink 的 resize 监听,满足 Ink 卸载时的监听清理。 */
|
|
29
|
+
off(event, listener) {
|
|
30
|
+
if (event === "resize")
|
|
31
|
+
this.resizeListeners.delete(listener);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** 更新 Ink 后续布局使用的稳定尺寸,并在缩窄时清空旧物理回流行及 scrollback。 */
|
|
35
|
+
setSize(size) {
|
|
36
|
+
const changed = size.columns !== this.size.columns || size.rows !== this.size.rows;
|
|
37
|
+
const widthDecreased = size.columns < this.size.columns;
|
|
38
|
+
this.size = size;
|
|
39
|
+
if (!changed)
|
|
40
|
+
return;
|
|
41
|
+
if (widthDecreased)
|
|
42
|
+
this.source.write(TUI_INLINE_RESIZE_CLEAR);
|
|
43
|
+
for (const listener of this.resizeListeners)
|
|
44
|
+
listener();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 创建跨 DPI resize 时使用的 Ink 输出代理与尺寸提交函数。 */
|
|
48
|
+
export function createTuiRenderStdout(source, initialSize) {
|
|
49
|
+
const stable = new StableTuiOutputStream(source, initialSize);
|
|
50
|
+
return {
|
|
51
|
+
stdout: stable,
|
|
52
|
+
setSize(size) { stable.setSize(size); },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** 创建可在 Ink 挂载前后安全使用的鼠标事件入口。 */
|
|
56
|
+
export function createTuiMouseInputSink() {
|
|
57
|
+
let handler;
|
|
58
|
+
return {
|
|
59
|
+
setHandler(nextHandler) {
|
|
60
|
+
handler = nextHandler;
|
|
61
|
+
},
|
|
62
|
+
dispatch(event, wheelNotches = 1) {
|
|
63
|
+
handler?.(event, wheelNotches);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 把原始 TTY 包装为 Ink 专用输入流,并转发 raw-mode 与 ref 生命周期以保持 Ink 挂载语义。 */
|
|
68
|
+
export function createRoutedTuiStdin(input) {
|
|
69
|
+
const routed = new PassThrough();
|
|
70
|
+
const lifecycleInput = input;
|
|
71
|
+
Object.defineProperty(routed, "isTTY", { value: input.isTTY === true });
|
|
72
|
+
if (typeof input.setRawMode === "function") {
|
|
73
|
+
Object.defineProperty(routed, "setRawMode", { value: input.setRawMode.bind(input) });
|
|
74
|
+
}
|
|
75
|
+
if (typeof lifecycleInput.ref === "function") {
|
|
76
|
+
Object.defineProperty(routed, "ref", { value: lifecycleInput.ref.bind(input) });
|
|
77
|
+
}
|
|
78
|
+
if (typeof lifecycleInput.unref === "function") {
|
|
79
|
+
Object.defineProperty(routed, "unref", { value: lifecycleInput.unref.bind(input) });
|
|
80
|
+
}
|
|
81
|
+
return routed;
|
|
82
|
+
}
|
|
83
|
+
/** 创建幂等的终端控制序列生命周期,保证所有退出路径只恢复一次。 */
|
|
84
|
+
export function createTuiTerminalLifecycle(stdout, mode) {
|
|
85
|
+
let entered = false;
|
|
86
|
+
let restored = false;
|
|
87
|
+
/** 按渲染模式进入终端能力;内联路径保留既有 paste/focus 输入语义。 */
|
|
88
|
+
function enter() {
|
|
89
|
+
if (entered)
|
|
90
|
+
return;
|
|
91
|
+
entered = true;
|
|
92
|
+
if (mode === "alternate-screen")
|
|
93
|
+
stdout.write(TUI_ALTERNATE_SCREEN_ENABLE + TUI_MOUSE_TRACKING_ENABLE + TUI_BRACKETED_PASTE_ENABLE + TUI_TERMINAL_FOCUS_ENABLE);
|
|
94
|
+
else
|
|
95
|
+
stdout.write(TUI_BRACKETED_PASTE_ENABLE + TUI_TERMINAL_FOCUS_ENABLE);
|
|
96
|
+
}
|
|
97
|
+
/** 在 Ink 完成输入初始化或终端重新聚焦后幂等重申鼠标模式,避免初始化顺序清掉备用屏捕获。 */
|
|
98
|
+
function reassert() {
|
|
99
|
+
if (!entered || restored || mode !== "alternate-screen")
|
|
100
|
+
return;
|
|
101
|
+
stdout.write(TUI_MOUSE_TRACKING_ENABLE);
|
|
102
|
+
}
|
|
103
|
+
/** 逆序恢复终端能力;备用屏路径同步关闭鼠标并返回主屏。 */
|
|
104
|
+
function restore() {
|
|
105
|
+
if (restored)
|
|
106
|
+
return;
|
|
107
|
+
restored = true;
|
|
108
|
+
if (mode === "alternate-screen")
|
|
109
|
+
stdout.write(TUI_MOUSE_TRACKING_DISABLE + TUI_BRACKETED_PASTE_DISABLE + TUI_TERMINAL_FOCUS_DISABLE + TUI_ALTERNATE_SCREEN_DISABLE);
|
|
110
|
+
else
|
|
111
|
+
stdout.write(TUI_BRACKETED_PASTE_DISABLE + TUI_TERMINAL_FOCUS_DISABLE);
|
|
112
|
+
}
|
|
113
|
+
return { enter, reassert, restore };
|
|
114
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** unified diff 的逐行着色渲染;保持与 Web 的 edit_file 展示语义一致。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/** 将 Core 提供的最终 unified diff 逐行着色,保持与 Web 的 edit_file 展示语义一致。 */
|
|
4
|
+
export declare function TuiDiffBlock(props: {
|
|
5
|
+
diff: string;
|
|
6
|
+
keyPrefix: string;
|
|
7
|
+
useColor: boolean;
|
|
8
|
+
}): ReactNode;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR, TUI_DIFF_ADDED_COLOR, TUI_DIFF_REMOVED_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 将 Core 提供的最终 unified diff 逐行着色,保持与 Web 的 edit_file 展示语义一致。 */
|
|
5
|
+
export function TuiDiffBlock(props) {
|
|
6
|
+
return (_jsxs(Box, { marginLeft: 2, marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "diff" }), props.diff.split("\n").map(function renderDiffLine(line, index) {
|
|
7
|
+
const isAdded = line.startsWith("+") && !line.startsWith("+++");
|
|
8
|
+
const isRemoved = line.startsWith("-") && !line.startsWith("---");
|
|
9
|
+
const isMeta = line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@");
|
|
10
|
+
const color = isAdded
|
|
11
|
+
? TUI_DIFF_ADDED_COLOR
|
|
12
|
+
: isRemoved
|
|
13
|
+
? TUI_DIFF_REMOVED_COLOR
|
|
14
|
+
: isMeta
|
|
15
|
+
? TUI_ACCENT_COLOR
|
|
16
|
+
: undefined;
|
|
17
|
+
return _jsx(Text, { color: props.useColor ? color : undefined, dimColor: props.useColor && isMeta, children: line || " " }, props.keyPrefix + ":line:" + String(index));
|
|
18
|
+
})] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
/** 将常用行内 Markdown 转为 Ink Text 节点;不支持的语法保留原文,确保流式半截内容不丢失。 */
|
|
5
|
+
export function renderMarkdownInline(content, keyPrefix, useColor) {
|
|
6
|
+
const nodes = [];
|
|
7
|
+
const tokenPattern = /(`[^`\n]*`)|(\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\))|(\*\*[^*\n]+\*\*|__[^_\n]+__)|(\*[^*\n]+\*|_[^_\n]+_)/g;
|
|
8
|
+
let offset = 0;
|
|
9
|
+
let tokenIndex = 0;
|
|
10
|
+
for (const match of content.matchAll(tokenPattern)) {
|
|
11
|
+
const index = match.index ?? 0;
|
|
12
|
+
if (index > offset)
|
|
13
|
+
nodes.push(content.slice(offset, index));
|
|
14
|
+
const key = keyPrefix + ":inline:" + String(tokenIndex++);
|
|
15
|
+
if (match[1]) {
|
|
16
|
+
nodes.push(_jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: match[1].slice(1, -1) }, key));
|
|
17
|
+
}
|
|
18
|
+
else if (match[2]) {
|
|
19
|
+
nodes.push(_jsxs(Text, { underline: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: [match[3], " (", match[4], ")"] }, key));
|
|
20
|
+
}
|
|
21
|
+
else if (match[5]) {
|
|
22
|
+
nodes.push(_jsx(Text, { bold: true, children: match[5].slice(2, -2) }, key));
|
|
23
|
+
}
|
|
24
|
+
else if (match[6]) {
|
|
25
|
+
nodes.push(_jsx(Text, { italic: true, children: match[6].slice(1, -1) }, key));
|
|
26
|
+
}
|
|
27
|
+
offset = index + match[0].length;
|
|
28
|
+
}
|
|
29
|
+
if (offset < content.length)
|
|
30
|
+
nodes.push(content.slice(offset));
|
|
31
|
+
return nodes;
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** CLI 所需的安全 Markdown 子集渲染:段落、标题、代码块、引用、列表与对齐表格。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/** 渲染已经按当前终端宽度适配列宽的对齐 Markdown 表格。 */
|
|
4
|
+
export declare function TuiMarkdownAlignedTable(props: {
|
|
5
|
+
header: string[];
|
|
6
|
+
rows: string[][];
|
|
7
|
+
keyPrefix: string;
|
|
8
|
+
useColor: boolean;
|
|
9
|
+
maximumWidth: number;
|
|
10
|
+
}): ReactNode;
|
|
11
|
+
/** 渲染 CLI 所需的安全 Markdown 子集,不解析或执行 HTML。 */
|
|
12
|
+
export declare function TuiMarkdown(props: {
|
|
13
|
+
content: string;
|
|
14
|
+
keyPrefix: string;
|
|
15
|
+
useColor: boolean;
|
|
16
|
+
maximumWidth: number;
|
|
17
|
+
}): ReactNode;
|