@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,104 @@
|
|
|
1
|
+
/** TUI 时间线视觉行模型;统一文字换行、样式片段与源文本坐标,暂不接入渲染。 */
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
/** 将一个样式范围裁剪到当前视觉行,保留行内坐标。 */
|
|
5
|
+
function clipTimelineSpans(spans, sourceStart, sourceEnd) {
|
|
6
|
+
return spans.flatMap(function clipSpan(span) {
|
|
7
|
+
const start = Math.max(span.start, sourceStart);
|
|
8
|
+
const end = Math.min(span.end, sourceEnd);
|
|
9
|
+
return start < end ? [{ ...span, start: start - sourceStart, end: end - sourceStart }] : [];
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** 找到包含给定偏移的完整 Unicode grapheme,避免换行截断组合 emoji。 */
|
|
13
|
+
function getTimelineGraphemeEnd(source, offset) {
|
|
14
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
15
|
+
for (const segment of segmenter.segment(source)) {
|
|
16
|
+
const end = segment.index + segment.segment.length;
|
|
17
|
+
if (segment.index < offset && offset < end)
|
|
18
|
+
return end;
|
|
19
|
+
}
|
|
20
|
+
return offset;
|
|
21
|
+
}
|
|
22
|
+
/** 复用现有 wrap-ansi 规则生成视觉行,同时把被截断的 grapheme 收拢到同一行。 */
|
|
23
|
+
function wrapTimelineSource(source, columns) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
let offset = 0;
|
|
26
|
+
while (offset < source.length) {
|
|
27
|
+
const newline = source.indexOf("\n", offset);
|
|
28
|
+
const sourceEnd = newline < 0 ? source.length : newline;
|
|
29
|
+
const remaining = source.slice(offset, sourceEnd);
|
|
30
|
+
if (remaining.length === 0) {
|
|
31
|
+
lines.push({ text: "", sourceStart: offset, sourceEnd: offset });
|
|
32
|
+
offset = newline < 0 ? source.length : newline + 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const candidate = wrapAnsi(remaining, columns, { hard: true, trim: false, wordWrap: true }).split("\n")[0] ?? remaining;
|
|
36
|
+
const candidateEnd = getTimelineGraphemeEnd(source, offset + candidate.length);
|
|
37
|
+
const visualEnd = Math.min(sourceEnd, Math.max(offset + 1, candidateEnd));
|
|
38
|
+
lines.push({ text: source.slice(offset, visualEnd), sourceStart: offset, sourceEnd: visualEnd });
|
|
39
|
+
offset = visualEnd;
|
|
40
|
+
if (offset === sourceEnd && newline >= 0)
|
|
41
|
+
offset += 1;
|
|
42
|
+
}
|
|
43
|
+
if (source.endsWith("\n"))
|
|
44
|
+
lines.push({ text: "", sourceStart: source.length, sourceEnd: source.length });
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
/** 将安全时间线文本按当前终端列宽转换为统一的视觉行。 */
|
|
48
|
+
export function createTimelineVisualLines(items, columns) {
|
|
49
|
+
const width = Math.max(1, columns);
|
|
50
|
+
return items.flatMap(function createItemLines(item) { return createTimelineVisualLinesForItem(item, width); });
|
|
51
|
+
}
|
|
52
|
+
/** 为单个 source 生成视觉行;缓存与全量实现共用完全相同的换行规则。 */
|
|
53
|
+
function createTimelineVisualLinesForItem(item, width) {
|
|
54
|
+
const spans = item.spans ?? [];
|
|
55
|
+
return wrapTimelineSource(item.text, width).map(function createVisualLine(line) {
|
|
56
|
+
return {
|
|
57
|
+
itemId: item.itemId,
|
|
58
|
+
text: line.text,
|
|
59
|
+
displayWidth: stringWidth(line.text),
|
|
60
|
+
sourceStart: line.sourceStart,
|
|
61
|
+
sourceEnd: line.sourceEnd,
|
|
62
|
+
spans: clipTimelineSpans(spans, line.sourceStart, line.sourceEnd),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** 将 source 的样式范围压成轻量指纹,避免同一文本因新对象重复换行。 */
|
|
67
|
+
function getTimelineSpanFingerprint(spans) {
|
|
68
|
+
return (spans ?? []).map(function fingerprintSpan(span) { return `${span.start}:${span.end}:${span.style}`; }).join("|");
|
|
69
|
+
}
|
|
70
|
+
/** 创建按 itemId、文本、spans 与终端宽度复用的视觉行缓存。 */
|
|
71
|
+
export function createTimelineVisualLineCache() {
|
|
72
|
+
const entries = new Map();
|
|
73
|
+
const stats = { builds: 0, hits: 0, widthInvalidations: 0 };
|
|
74
|
+
/** 按当前 source 顺序拼出视觉行,同时只重算发生变化的 item。 */
|
|
75
|
+
function getLines(items, columns) {
|
|
76
|
+
const width = Math.max(1, columns);
|
|
77
|
+
return items.flatMap(function getCachedItemLines(item) {
|
|
78
|
+
const spans = getTimelineSpanFingerprint(item.spans);
|
|
79
|
+
const previous = entries.get(item.itemId);
|
|
80
|
+
if (previous && previous.text === item.text && previous.spans === spans && previous.width === width) {
|
|
81
|
+
stats.hits += 1;
|
|
82
|
+
return previous.lines;
|
|
83
|
+
}
|
|
84
|
+
if (previous && previous.width !== width)
|
|
85
|
+
stats.widthInvalidations += 1;
|
|
86
|
+
const lines = createTimelineVisualLinesForItem(item, width);
|
|
87
|
+
entries.set(item.itemId, { text: item.text, spans, width, lines });
|
|
88
|
+
stats.builds += 1;
|
|
89
|
+
return lines;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** 返回当前缓存计数快照,调用方不得修改内部状态。 */
|
|
93
|
+
function getStats() {
|
|
94
|
+
return { ...stats };
|
|
95
|
+
}
|
|
96
|
+
/** 清空视觉行缓存并重置计数,供终端生命周期切换或测试隔离使用。 */
|
|
97
|
+
function clear() {
|
|
98
|
+
entries.clear();
|
|
99
|
+
stats.builds = 0;
|
|
100
|
+
stats.hits = 0;
|
|
101
|
+
stats.widthInvalidations = 0;
|
|
102
|
+
}
|
|
103
|
+
return { getLines, getStats, clear };
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** 瘦身后的 Ink 应用组件:编排 store 订阅、dispatchAction、动画时钟与渲染。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliController, CliControllerResult } from "../controller.js";
|
|
4
|
+
import type { CliExitCode } from "../types.js";
|
|
5
|
+
import type { CliTuiInitialOperation, CliTuiStore, TuiTerminalSizeSource, TuiMouseInputSink, TuiRenderMode } from "./types.js";
|
|
6
|
+
/** Ink 应用组件;useInput 只把用户动作交给 Controller Action。 */
|
|
7
|
+
export declare function CliTuiApp(props: {
|
|
8
|
+
store: CliTuiStore;
|
|
9
|
+
controller: CliController;
|
|
10
|
+
initial?: CliTuiInitialOperation;
|
|
11
|
+
closeWhenIdle: boolean;
|
|
12
|
+
/** canonical 工作区根;缺省时 @ 文件候选菜单不激活。 */
|
|
13
|
+
workspaceRoot?: string;
|
|
14
|
+
useColor: boolean;
|
|
15
|
+
screenReader: boolean;
|
|
16
|
+
renderMode?: TuiRenderMode;
|
|
17
|
+
reducedMotion?: boolean;
|
|
18
|
+
mouseInput?: TuiMouseInputSink;
|
|
19
|
+
onCopyText?(text: string): void;
|
|
20
|
+
onResult(result: CliControllerResult): void;
|
|
21
|
+
onRequestInterrupt(): void;
|
|
22
|
+
onRequestCancel(): void;
|
|
23
|
+
onEof(): void;
|
|
24
|
+
onAppExit(code: CliExitCode): void;
|
|
25
|
+
terminalSizeSource?: TuiTerminalSizeSource;
|
|
26
|
+
onTerminalSizeChange?(size: {
|
|
27
|
+
columns: number;
|
|
28
|
+
rows: number;
|
|
29
|
+
}): void;
|
|
30
|
+
}): ReactNode;
|
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** 瘦身后的 Ink 应用组件:编排 store 订阅、dispatchAction、动画时钟与渲染。 */
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Box, useInput, useStdin, useStdout } from "ink";
|
|
5
|
+
import { TuiActivityLine } from "../activity-animation.js";
|
|
6
|
+
import { createTuiAnimationClock } from "../animation-clock.js";
|
|
7
|
+
import { resolveFileMentionContext } from "../file-mention-context.js";
|
|
8
|
+
import { createTimelineVisualLineCache } from "../timeline-lines.js";
|
|
9
|
+
import { TuiApprovalPrompt, createTuiApprovalOptions } from "./approval.js";
|
|
10
|
+
import { TuiBrandHeader, TUI_BRAND_HISTORY_ITEM } from "./brand/brand-header.js";
|
|
11
|
+
import { TuiContextReport } from "./context.js";
|
|
12
|
+
import { isActiveTuiStatus, isTerminalStatus, readTuiTerminalSize } from "./format.js";
|
|
13
|
+
import { recordTuiRuntimeErrorDiagnostic } from "./performance-diagnostics.js";
|
|
14
|
+
import { useFileMentionIndex } from "./hooks/use-file-mention-index.js";
|
|
15
|
+
import { usePickers } from "./hooks/use-pickers.js";
|
|
16
|
+
import { useStatusView } from "./hooks/use-status-view.js";
|
|
17
|
+
import { useViewport } from "./hooks/use-viewport.js";
|
|
18
|
+
import { TuiInputArea } from "./input-area.js";
|
|
19
|
+
import { TuiInfoLines, TuiStatusLine } from "./status-line.js";
|
|
20
|
+
import { TuiJobs } from "./jobs.js";
|
|
21
|
+
import { TuiLiveTurn } from "./timeline/turn.js";
|
|
22
|
+
import { getTuiTurnTodo } from "./timeline/turn-items.js";
|
|
23
|
+
import { MemoizedTuiTranscriptItem, TuiPendingInputItem, TuiPromptLine } from "./transcript.js";
|
|
24
|
+
import { getTuiSelectedText } from "./viewport/selection.js";
|
|
25
|
+
import { createTuiViewportSourceCache } from "./viewport/sources.js";
|
|
26
|
+
import { TuiViewportTimeline } from "./viewport/viewport.js";
|
|
27
|
+
const EMPTY_VIEWPORT_SOURCES = [];
|
|
28
|
+
const EMPTY_VIEWPORT_LINES = [];
|
|
29
|
+
/** Ink 应用组件;useInput 只把用户动作交给 Controller Action。 */
|
|
30
|
+
export function CliTuiApp(props) {
|
|
31
|
+
const { isRawModeSupported, stdin } = useStdin();
|
|
32
|
+
const { stdout } = useStdout();
|
|
33
|
+
const terminalSizeSource = props.terminalSizeSource ?? stdout;
|
|
34
|
+
const [snapshot, setSnapshot] = useState(props.store.getSnapshot());
|
|
35
|
+
const [terminalFocused, setTerminalFocused] = useState(true);
|
|
36
|
+
/** Claude Code 式 Ctrl+O 切换动态区 Todo 清单的紧凑/展开形态,只影响本地视图。 */
|
|
37
|
+
const [todoExpanded, setTodoExpanded] = useState(false);
|
|
38
|
+
const [jobView, setJobView] = useState(undefined);
|
|
39
|
+
const [jobListCache, setJobListCache] = useState(undefined);
|
|
40
|
+
const [jobSelectedIndex, setJobSelectedIndex] = useState(0);
|
|
41
|
+
const jobViewRef = useRef(undefined);
|
|
42
|
+
const jobRequestGeneration = useRef(0);
|
|
43
|
+
/** /context 覆盖视图;打开时取代时间线渲染,Esc 关闭后回到正常聊天。 */
|
|
44
|
+
const [contextView, setContextView] = useState(undefined);
|
|
45
|
+
const rawInputSequenceRef = useRef("");
|
|
46
|
+
const busy = useRef(false);
|
|
47
|
+
const keepOpenAfterCancel = useRef(false);
|
|
48
|
+
const lastSubmittedPromptRef = useRef(undefined);
|
|
49
|
+
const initialStarted = useRef(false);
|
|
50
|
+
const lastResult = useRef(undefined);
|
|
51
|
+
const animationClockRef = useRef(undefined);
|
|
52
|
+
if (!animationClockRef.current)
|
|
53
|
+
animationClockRef.current = createTuiAnimationClock();
|
|
54
|
+
const animationClock = animationClockRef.current;
|
|
55
|
+
/** 防抖后的终端尺寸;Ink 与应用布局共同使用同一份稳定快照。 */
|
|
56
|
+
const [terminalSize, setTerminalSize] = useState(function createTerminalSize() {
|
|
57
|
+
return readTuiTerminalSize(terminalSizeSource);
|
|
58
|
+
});
|
|
59
|
+
const isViewportMode = props.renderMode === "alternate-screen" && !props.screenReader;
|
|
60
|
+
const columns = terminalSize.columns;
|
|
61
|
+
const rows = terminalSize.rows;
|
|
62
|
+
const { statusView, refreshStatusView } = useStatusView(props.controller);
|
|
63
|
+
/** 输入子树通过 ref 读取 slash 与 @ 建议,父级只注入视口按键处理器。 */
|
|
64
|
+
const derivedRef = useRef({
|
|
65
|
+
slashCommandSuggestions: [],
|
|
66
|
+
fileMentionSuggestions: [],
|
|
67
|
+
handleTuiViewportKey: function noViewportKey() { return false; },
|
|
68
|
+
});
|
|
69
|
+
const pickers = usePickers({ dispatchAction, rawInputSequenceRef });
|
|
70
|
+
const fileMentionIndex = useFileMentionIndex(props.workspaceRoot);
|
|
71
|
+
const [inputLayout, setInputLayout] = useState({
|
|
72
|
+
promptHeight: 4,
|
|
73
|
+
pickerHeight: 0,
|
|
74
|
+
});
|
|
75
|
+
const handleInputLayoutChange = useCallback(function handleInputLayoutChange(next) {
|
|
76
|
+
setInputLayout(function updateInputLayout(previous) {
|
|
77
|
+
return previous.promptHeight === next.promptHeight && previous.pickerHeight === next.pickerHeight ? previous : next;
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
const infoMessages = snapshot.transcript
|
|
81
|
+
.filter(function isNotice(item) { return item.kind === "notice"; })
|
|
82
|
+
.slice(-4)
|
|
83
|
+
.map(function getNoticeMessage(item) { return item.message; });
|
|
84
|
+
const approvalHeight = isViewportMode && snapshot.pendingApproval ? createTuiApprovalOptions(snapshot.pendingApproval).length + 6 : 0;
|
|
85
|
+
// Todo 展开态最多占 10 行;矮终端为输入框、状态栏和活动文案至少保留 6 行。
|
|
86
|
+
const todoRowLimit = Math.max(2, Math.min(10, rows - 6));
|
|
87
|
+
const activeTodoForm = getTuiTurnTodo(snapshot.liveEvents, snapshot.liveTurnId ?? "")?.todos.find(function active(todo) { return todo.status === "in_progress"; })?.activeForm;
|
|
88
|
+
const fixedHeight = inputLayout.promptHeight + inputLayout.pickerHeight + approvalHeight + infoMessages.length + 1 + (statusView ? 5 : 2);
|
|
89
|
+
const viewportHeight = Math.max(1, rows - fixedHeight);
|
|
90
|
+
const viewportSourceCacheRef = useRef(undefined);
|
|
91
|
+
if (!viewportSourceCacheRef.current)
|
|
92
|
+
viewportSourceCacheRef.current = createTuiViewportSourceCache();
|
|
93
|
+
const viewportLineCacheRef = useRef(undefined);
|
|
94
|
+
if (!viewportLineCacheRef.current)
|
|
95
|
+
viewportLineCacheRef.current = createTimelineVisualLineCache();
|
|
96
|
+
const viewportContentWidth = Math.max(1, columns - 1);
|
|
97
|
+
const viewportSources = useMemo(function getCachedViewportSources() {
|
|
98
|
+
return isViewportMode
|
|
99
|
+
? viewportSourceCacheRef.current.getSources(snapshot, statusView, pickers.approvalPickerIndex, viewportContentWidth, todoExpanded, props.useColor, todoRowLimit)
|
|
100
|
+
: EMPTY_VIEWPORT_SOURCES;
|
|
101
|
+
}, [isViewportMode, pickers.approvalPickerIndex, snapshot, statusView, viewportContentWidth, todoExpanded, props.useColor, todoRowLimit]);
|
|
102
|
+
const viewportLines = useMemo(function getCachedViewportLines() {
|
|
103
|
+
return isViewportMode
|
|
104
|
+
? viewportLineCacheRef.current.getLines(viewportSources, viewportContentWidth)
|
|
105
|
+
: EMPTY_VIEWPORT_LINES;
|
|
106
|
+
}, [isViewportMode, viewportContentWidth, viewportSources]);
|
|
107
|
+
const { viewportState, selection, handleTuiMouseEvent, handleTuiViewportKey } = useViewport({
|
|
108
|
+
isViewportMode,
|
|
109
|
+
columns,
|
|
110
|
+
viewportLines,
|
|
111
|
+
viewportHeight,
|
|
112
|
+
stdout,
|
|
113
|
+
onCopyText: props.onCopyText,
|
|
114
|
+
});
|
|
115
|
+
derivedRef.current.handleTuiViewportKey = handleTuiViewportKey;
|
|
116
|
+
const selectedText = isViewportMode ? getTuiSelectedText(viewportLines, selection) : "";
|
|
117
|
+
const animationActive = isRawModeSupported && stdout.isTTY !== false && terminalFocused;
|
|
118
|
+
/** 把用户动作交给 Controller;结束后刷新状态栏并按投影结果打开对应 picker。 */
|
|
119
|
+
async function dispatchAction(action, prompt, preserveSkillPickerQuery = false) {
|
|
120
|
+
const allowWhileRunning = (action.type === "submit_prompt" || action.type === "open_permissions" || action.type === "set_permission_mode" || action.type === "open_model_picker" || action.type === "set_model" || action.type === "open_effort_picker" || action.type === "set_effort" || action.type === "list_jobs" || action.type === "show_job" || action.type === "read_job_output" || action.type === "stop_job") && isActiveTuiStatus(props.store.getSnapshot().status);
|
|
121
|
+
if (busy.current && !allowWhileRunning)
|
|
122
|
+
return;
|
|
123
|
+
const isJobAction = action.type === "list_jobs" || action.type === "show_job" || action.type === "read_job_output" || action.type === "stop_job";
|
|
124
|
+
const jobGeneration = isJobAction ? ++jobRequestGeneration.current : undefined;
|
|
125
|
+
const inputPosition = prompt && allowWhileRunning ? props.store.captureInputPosition() : undefined;
|
|
126
|
+
if (prompt) {
|
|
127
|
+
lastSubmittedPromptRef.current = prompt.trim();
|
|
128
|
+
props.store.recordPrompt(prompt, !allowWhileRunning);
|
|
129
|
+
}
|
|
130
|
+
if (action.type === "approve")
|
|
131
|
+
props.store.recordApprovalSubmission(action.decision);
|
|
132
|
+
if (!allowWhileRunning)
|
|
133
|
+
busy.current = true;
|
|
134
|
+
// D2/D5:仅交互 TUI 在发送前把 @ 引用的工作区文件内容以 <system-reminder> 附加块并入 action.content;
|
|
135
|
+
// start/steer/queue/initial 四条 TUI 提交路径都经过此处,时间线与 Esc 恢复仍使用原文 prompt。
|
|
136
|
+
let dispatchedAction = action;
|
|
137
|
+
if (action.type === "submit_prompt" && prompt && props.workspaceRoot) {
|
|
138
|
+
const resolved = await resolveFileMentionContext(action.content, props.workspaceRoot);
|
|
139
|
+
for (const message of resolved.notices)
|
|
140
|
+
props.store.addNotice(message);
|
|
141
|
+
if (resolved.content !== action.content)
|
|
142
|
+
dispatchedAction = { ...action, content: resolved.content };
|
|
143
|
+
}
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await props.controller.dispatch(dispatchedAction);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
recordTuiRuntimeErrorDiagnostic(error);
|
|
150
|
+
result = {
|
|
151
|
+
kind: "error",
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
lines: [],
|
|
154
|
+
message: "CLI_RUNTIME_ERROR",
|
|
155
|
+
projection: props.controller.getProjection(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (!allowWhileRunning)
|
|
160
|
+
busy.current = false;
|
|
161
|
+
}
|
|
162
|
+
lastResult.current = result;
|
|
163
|
+
props.onResult(result);
|
|
164
|
+
if (result.jobView && jobGeneration === jobRequestGeneration.current) {
|
|
165
|
+
jobViewRef.current = result.jobView;
|
|
166
|
+
setJobView(result.jobView);
|
|
167
|
+
if (result.jobView.mode === "list") {
|
|
168
|
+
const nextList = result.jobView;
|
|
169
|
+
setJobListCache(nextList);
|
|
170
|
+
setJobSelectedIndex(function clampJobSelection(previous) { return Math.min(previous, Math.max(0, nextList.list.jobs.length - 1)); });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const updated = result.jobView.job;
|
|
174
|
+
setJobListCache(function updateCachedSummary(previous) {
|
|
175
|
+
if (!previous)
|
|
176
|
+
return previous;
|
|
177
|
+
return { ...previous, list: { ...previous.list, jobs: previous.list.jobs.map(function replaceJob(job) { return job.jobId === updated.jobId && (updated.revision ?? 0) >= (job.revision ?? 0) ? updated : job; }) } };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (result.contextView)
|
|
182
|
+
setContextView(result.contextView);
|
|
183
|
+
if (prompt && result.submittedInput)
|
|
184
|
+
props.store.addPendingPrompt(prompt, result.submittedInput, inputPosition);
|
|
185
|
+
await refreshStatusView();
|
|
186
|
+
if (result.skillOptions) {
|
|
187
|
+
pickers.setSkillPickerSkills(result.skillOptions);
|
|
188
|
+
if (!preserveSkillPickerQuery)
|
|
189
|
+
pickers.updateSkillPickerQuery("");
|
|
190
|
+
}
|
|
191
|
+
if (result.permissionOptions) {
|
|
192
|
+
pickers.permissionPickerOptionsRef.current = result.permissionOptions;
|
|
193
|
+
pickers.setPermissionPickerOptions(result.permissionOptions);
|
|
194
|
+
pickers.updatePermissionPickerIndex(Math.max(0, result.permissionOptions.findIndex(function findSelected(option) { return option.selected; })));
|
|
195
|
+
}
|
|
196
|
+
if (result.modelOptions) {
|
|
197
|
+
pickers.modelPickerOptionsRef.current = result.modelOptions;
|
|
198
|
+
pickers.setModelPickerOptions(result.modelOptions);
|
|
199
|
+
pickers.updateModelPickerIndex(Math.max(0, result.modelOptions.findIndex(function findSelected(option) { return option.selected; })));
|
|
200
|
+
}
|
|
201
|
+
if (result.effortOptions) {
|
|
202
|
+
pickers.effortPickerOptionsRef.current = result.effortOptions;
|
|
203
|
+
pickers.setEffortPickerOptions(result.effortOptions);
|
|
204
|
+
pickers.updateEffortPickerIndex(Math.max(0, result.effortOptions.findIndex(function findSelected(option) { return option.selected; })));
|
|
205
|
+
}
|
|
206
|
+
if (result.kind === "exit")
|
|
207
|
+
props.onAppExit(result.exitCode);
|
|
208
|
+
}
|
|
209
|
+
/** 统一接管终端 resize;inline 防抖,备用屏即时提交,布局提交后由 Ink 清理旧帧。 */
|
|
210
|
+
useEffect(function subscribeTerminalResize() {
|
|
211
|
+
const onResize = terminalSizeSource.on;
|
|
212
|
+
const offResize = terminalSizeSource.off;
|
|
213
|
+
if (!onResize || !offResize)
|
|
214
|
+
return;
|
|
215
|
+
let timer;
|
|
216
|
+
function commitTerminalSize() {
|
|
217
|
+
const nextSize = readTuiTerminalSize(terminalSizeSource);
|
|
218
|
+
timer = undefined;
|
|
219
|
+
setTerminalSize(nextSize);
|
|
220
|
+
}
|
|
221
|
+
function handleTerminalResize() {
|
|
222
|
+
if (isViewportMode) {
|
|
223
|
+
commitTerminalSize();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (timer)
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
timer = setTimeout(commitTerminalSize, 200);
|
|
229
|
+
}
|
|
230
|
+
onResize.call(terminalSizeSource, "resize", handleTerminalResize);
|
|
231
|
+
return function cleanupTerminalResize() {
|
|
232
|
+
if (timer)
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
offResize.call(terminalSizeSource, "resize", handleTerminalResize);
|
|
235
|
+
};
|
|
236
|
+
}, [isViewportMode, terminalSizeSource]);
|
|
237
|
+
useLayoutEffect(function syncTerminalSizeToInk() {
|
|
238
|
+
props.onTerminalSizeChange?.(terminalSize);
|
|
239
|
+
}, [terminalSize, props.onTerminalSizeChange]);
|
|
240
|
+
useEffect(function bindTuiMouseInput() {
|
|
241
|
+
props.mouseInput?.setHandler(handleTuiMouseEvent);
|
|
242
|
+
return function unbindTuiMouseInput() {
|
|
243
|
+
props.mouseInput?.setHandler(undefined);
|
|
244
|
+
};
|
|
245
|
+
}, [props.mouseInput, isViewportMode, columns, viewportHeight, viewportLines.length]);
|
|
246
|
+
useEffect(function resetApprovalSelection() {
|
|
247
|
+
pickers.updateApprovalPickerIndex(0);
|
|
248
|
+
}, [snapshot.pendingApproval?.approvalId]);
|
|
249
|
+
useEffect(function disposeTuiAnimationClock() {
|
|
250
|
+
return function disposeAnimationClock() {
|
|
251
|
+
animationClock.dispose();
|
|
252
|
+
};
|
|
253
|
+
}, [animationClock]);
|
|
254
|
+
useEffect(function subscribeTuiStore() {
|
|
255
|
+
return props.store.subscribe(function updateTuiSnapshot() {
|
|
256
|
+
setSnapshot(props.store.getSnapshot());
|
|
257
|
+
});
|
|
258
|
+
}, [props.store]);
|
|
259
|
+
useEffect(function refreshStatusOnTurnChange() {
|
|
260
|
+
void refreshStatusView();
|
|
261
|
+
}, [props.controller, snapshot.status, snapshot.liveTurnId]);
|
|
262
|
+
// 用量事件到达时重新查询状态栏,让 context 行在 Turn 运行中也能反映三态变化。
|
|
263
|
+
useEffect(function refreshStatusOnContextUsage() {
|
|
264
|
+
void refreshStatusView();
|
|
265
|
+
}, [props.controller, snapshot.contextUsage]);
|
|
266
|
+
useEffect(function subscribeStdinEnd() {
|
|
267
|
+
function handleStdinEnd() {
|
|
268
|
+
props.onEof();
|
|
269
|
+
}
|
|
270
|
+
stdin.on("end", handleStdinEnd);
|
|
271
|
+
return function unsubscribeStdinEnd() {
|
|
272
|
+
stdin.removeListener("end", handleStdinEnd);
|
|
273
|
+
};
|
|
274
|
+
}, [stdin, props.onEof]);
|
|
275
|
+
useEffect(function captureRawInputSequence() {
|
|
276
|
+
/** 在 Ink 解析前缓存原始按键序列,用于区分 macOS Backspace 与前向 Delete。 */
|
|
277
|
+
function capture(chunk) {
|
|
278
|
+
rawInputSequenceRef.current = String(chunk);
|
|
279
|
+
}
|
|
280
|
+
stdin.prependListener("data", capture);
|
|
281
|
+
return function unsubscribeRawInputSequence() {
|
|
282
|
+
stdin.removeListener("data", capture);
|
|
283
|
+
};
|
|
284
|
+
}, [stdin]);
|
|
285
|
+
useEffect(function captureTerminalFocus() {
|
|
286
|
+
/** 读取终端 focus in/out 报告,让失焦时共享动画时钟停止。 */
|
|
287
|
+
function captureFocus(chunk) {
|
|
288
|
+
const value = String(chunk);
|
|
289
|
+
if (value.includes("\u001b[O"))
|
|
290
|
+
setTerminalFocused(false);
|
|
291
|
+
if (value.includes("\u001b[I"))
|
|
292
|
+
setTerminalFocused(true);
|
|
293
|
+
}
|
|
294
|
+
stdin.on("data", captureFocus);
|
|
295
|
+
return function unsubscribeTerminalFocus() {
|
|
296
|
+
stdin.removeListener("data", captureFocus);
|
|
297
|
+
};
|
|
298
|
+
}, [stdin]);
|
|
299
|
+
useEffect(function runInitialOperation() {
|
|
300
|
+
if (initialStarted.current || !props.initial)
|
|
301
|
+
return;
|
|
302
|
+
initialStarted.current = true;
|
|
303
|
+
if (props.initial.type === "start") {
|
|
304
|
+
void dispatchAction({ type: "submit_prompt", content: props.initial.content }, props.initial.content);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
void (async function resumeInitialTurn() {
|
|
308
|
+
if (busy.current)
|
|
309
|
+
return;
|
|
310
|
+
busy.current = true;
|
|
311
|
+
let result;
|
|
312
|
+
try {
|
|
313
|
+
result = await props.controller.resume();
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
result = {
|
|
317
|
+
kind: "error",
|
|
318
|
+
exitCode: 1,
|
|
319
|
+
lines: [],
|
|
320
|
+
message: "CLI_RUNTIME_ERROR",
|
|
321
|
+
projection: props.controller.getProjection(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
busy.current = false;
|
|
326
|
+
}
|
|
327
|
+
lastResult.current = result;
|
|
328
|
+
props.onResult(result);
|
|
329
|
+
await refreshStatusView();
|
|
330
|
+
})();
|
|
331
|
+
}
|
|
332
|
+
}, [props.controller, props.initial]);
|
|
333
|
+
useEffect(function closeCompletedOneShot() {
|
|
334
|
+
if (!props.closeWhenIdle || !initialStarted.current || busy.current)
|
|
335
|
+
return;
|
|
336
|
+
if (keepOpenAfterCancel.current)
|
|
337
|
+
return;
|
|
338
|
+
if (snapshot.status !== "running" && snapshot.status !== "waiting_approval") {
|
|
339
|
+
props.onAppExit(snapshot.requestedExitCode ?? lastResult.current?.exitCode ?? 0);
|
|
340
|
+
}
|
|
341
|
+
}, [props.closeWhenIdle, snapshot.status, snapshot.requestedExitCode]);
|
|
342
|
+
/** 在 Job 视图内复用 Ink 输入路由;Esc 只返回视图,不取消 Turn 或退出进程。 */
|
|
343
|
+
useInput(function handleJobInput(value, key) {
|
|
344
|
+
const current = jobViewRef.current;
|
|
345
|
+
if (!current)
|
|
346
|
+
return;
|
|
347
|
+
if (key.escape || value === "\u001b") {
|
|
348
|
+
jobRequestGeneration.current += 1;
|
|
349
|
+
const next = current.mode === "detail" ? jobListCache : undefined;
|
|
350
|
+
jobViewRef.current = next;
|
|
351
|
+
setJobView(next);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (current.mode === "list") {
|
|
355
|
+
if (key.upArrow) {
|
|
356
|
+
setJobSelectedIndex(function selectPrevious(index) { return Math.max(0, index - 1); });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (key.downArrow) {
|
|
360
|
+
setJobSelectedIndex(function selectNext(index) { return Math.min(Math.max(0, current.list.jobs.length - 1), index + 1); });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const selected = current.list.jobs[jobSelectedIndex];
|
|
364
|
+
if (key.return && selected)
|
|
365
|
+
void dispatchAction({ type: "show_job", jobId: selected.jobId });
|
|
366
|
+
else if (value === "o" && selected)
|
|
367
|
+
void dispatchAction({ type: "read_job_output", jobId: selected.jobId });
|
|
368
|
+
else if (value === "s" && selected?.canStop)
|
|
369
|
+
void dispatchAction({ type: "stop_job", jobId: selected.jobId });
|
|
370
|
+
else if (value === "r")
|
|
371
|
+
void dispatchAction({ type: "list_jobs" });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (value === "o")
|
|
375
|
+
void dispatchAction({ type: "read_job_output", jobId: current.job.jobId });
|
|
376
|
+
else if (value === "s" && current.job.canStop)
|
|
377
|
+
void dispatchAction({ type: "stop_job", jobId: current.job.jobId });
|
|
378
|
+
else if (value === "r")
|
|
379
|
+
void dispatchAction({ type: current.output ? "read_job_output" : "show_job", jobId: current.job.jobId });
|
|
380
|
+
}, { isActive: isRawModeSupported && jobView !== undefined });
|
|
381
|
+
/** 在 Context 覆盖视图内复用 Ink 输入路由;Esc 只关闭视图回到聊天,不取消 Turn 或退出进程。 */
|
|
382
|
+
useInput(function handleContextInput(value, key) {
|
|
383
|
+
if (contextView === undefined)
|
|
384
|
+
return;
|
|
385
|
+
if (key.escape || value === "\u001b")
|
|
386
|
+
setContextView(undefined);
|
|
387
|
+
}, { isActive: isRawModeSupported && contextView !== undefined });
|
|
388
|
+
// Ctrl+O 只切换 Todo 展开视图;picker 与审批等待打开时让位,避免按键语义冲突。
|
|
389
|
+
useInput(function handleTodoExpandInput(input, key) {
|
|
390
|
+
if (key.ctrl && input === "o")
|
|
391
|
+
setTodoExpanded(function toggleTodoExpanded(previous) { return !previous; });
|
|
392
|
+
}, { isActive: isRawModeSupported && !pickers.isPickerActive && !snapshot.pendingApproval });
|
|
393
|
+
const isLiveTurnTerminal = Boolean(snapshot.liveTurnId && isTerminalStatus(snapshot.status));
|
|
394
|
+
const inlineHistoryItems = isViewportMode ? [] : [
|
|
395
|
+
TUI_BRAND_HISTORY_ITEM,
|
|
396
|
+
...snapshot.transcript.filter(function selectInlineHistoryItem(item) {
|
|
397
|
+
if (item.kind === "notice")
|
|
398
|
+
return false;
|
|
399
|
+
if (item.kind === "turn")
|
|
400
|
+
return item.turnId !== snapshot.liveTurnId || isLiveTurnTerminal;
|
|
401
|
+
return item.key !== snapshot.activePrompt?.key || isLiveTurnTerminal;
|
|
402
|
+
}),
|
|
403
|
+
];
|
|
404
|
+
const activePrompt = snapshot.activePrompt && !isLiveTurnTerminal ? snapshot.activePrompt : undefined;
|
|
405
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, "aria-role": "list", children: [jobView ? _jsx(TuiJobs, { view: jobView, selectedIndex: jobSelectedIndex, useColor: props.useColor, screenReader: props.screenReader }) : contextView ? _jsx(TuiContextReport, { usage: contextView.usage, expanded: contextView.expanded, columns: columns, useColor: props.useColor, screenReader: props.screenReader }) : isViewportMode ? _jsxs(_Fragment, { children: [_jsx(TuiViewportTimeline, { lines: viewportLines, state: viewportState, columns: columns, useColor: props.useColor, selection: selection }), snapshot.pendingApproval ? _jsx(TuiApprovalPrompt, { pendingApproval: snapshot.pendingApproval, selectedIndex: pickers.approvalPickerIndex, useColor: props.useColor, screenReader: props.screenReader }) : null] }) : _jsxs(_Fragment, { children: [inlineHistoryItems.map(function renderInlineHistoryItem(item) {
|
|
406
|
+
if (item.kind !== "brand") {
|
|
407
|
+
return _jsx(MemoizedTuiTranscriptItem, { item: item, columns: columns, useColor: props.useColor, screenReader: props.screenReader }, item.key);
|
|
408
|
+
}
|
|
409
|
+
return _jsx(TuiBrandHeader, { statusView: statusView, columns: columns, useColor: props.useColor, screenReader: props.screenReader, reducedMotion: props.reducedMotion ?? false, animationActive: false }, item.key);
|
|
410
|
+
}), activePrompt ? _jsx(TuiPromptLine, { content: activePrompt.content, columns: columns, useColor: props.useColor, keyValue: activePrompt.key }) : null, _jsx(TuiLiveTurn, { turnId: isLiveTurnTerminal ? undefined : snapshot.liveTurnId, events: snapshot.liveEvents, presentation: snapshot.livePresentation, pendingApproval: snapshot.pendingApproval, approvalSelectedIndex: pickers.approvalPickerIndex, status: snapshot.status, metrics: snapshot.liveMetrics, useColor: props.useColor, screenReader: props.screenReader, todoExpanded: todoExpanded, todoRowLimit: todoRowLimit, maximumWidth: Math.max(1, columns - 2) }), snapshot.pendingInputs.map(function renderPendingInput(item) {
|
|
411
|
+
return item.delivery === "queue" ? _jsx(TuiPendingInputItem, { item: item, useColor: props.useColor }, item.key) : null;
|
|
412
|
+
})] }), _jsx(TuiActivityLine, { activity: snapshot.activity, status: snapshot.status, waitingForApproval: snapshot.pendingApproval !== undefined, useColor: props.useColor, screenReader: props.screenReader, reducedMotion: props.reducedMotion ?? false, animationActive: animationActive, clock: animationClock, startedAt: snapshot.liveMetrics?.startedAt, activeForm: activeTodoForm }), _jsx(TuiInfoLines, { messages: infoMessages, useColor: props.useColor }), _jsx(TuiInputArea, { store: props.store, columns: columns, busy: busy, keepOpenAfterCancel: keepOpenAfterCancel, lastSubmittedPromptRef: lastSubmittedPromptRef, rawInputSequenceRef: rawInputSequenceRef, dispatchAction: dispatchAction, onEof: props.onEof, onRequestInterrupt: props.onRequestInterrupt, onRequestCancel: props.onRequestCancel, isRawModeSupported: isRawModeSupported, isInputActive: jobView === undefined && contextView === undefined, showInput: jobView === undefined && contextView === undefined, useColor: props.useColor, screenReader: props.screenReader, pickers: pickers, derivedRef: derivedRef, fileMentionIndex: fileMentionIndex, onLayoutChange: handleInputLayoutChange }), _jsx(TuiStatusLine, { snapshot: snapshot, statusView: statusView, useColor: props.useColor, screenReader: props.screenReader, selectionCharacters: selectedText.length })] }));
|
|
413
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 审批菜单:根据待审批工具收窄选项,并以 CC 风格上下选择、回车确认。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliAction, CliProjection } from "../types.js";
|
|
4
|
+
/** TTY 审批菜单只保存公开决定与可见标签,不携带冻结工具输入。 */
|
|
5
|
+
export interface CliTuiApprovalOption {
|
|
6
|
+
decision: Extract<CliAction, {
|
|
7
|
+
type: "approve";
|
|
8
|
+
}>["decision"];
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
/** 根据待审批工具收窄菜单;旧记录缺少冻结范围时保持最保守的单次/拒绝。 */
|
|
12
|
+
export declare function createTuiApprovalOptions(pendingApproval: NonNullable<CliProjection["pendingApproval"]>): CliTuiApprovalOption[];
|
|
13
|
+
/** 以 CC 风格上下选择、回车确认渲染当前审批菜单。 */
|
|
14
|
+
export declare function TuiApprovalPrompt(props: {
|
|
15
|
+
pendingApproval: NonNullable<CliProjection["pendingApproval"]>;
|
|
16
|
+
selectedIndex: number;
|
|
17
|
+
useColor: boolean;
|
|
18
|
+
screenReader: boolean;
|
|
19
|
+
}): ReactNode;
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
import { getTuiToolLabel } from "./format.js";
|
|
5
|
+
/** 根据待审批工具收窄菜单;旧记录缺少冻结范围时保持最保守的单次/拒绝。 */
|
|
6
|
+
export function createTuiApprovalOptions(pendingApproval) {
|
|
7
|
+
if (pendingApproval.allowedDecisions) {
|
|
8
|
+
// run_command 的工作区级授权展示为"记住此命令"(精确命令约束,与 Web 审批卡同一文案);
|
|
9
|
+
// todo_write 的单次批准即任务文件集合批准,与 Web 审批卡同一文案。
|
|
10
|
+
const labels = {
|
|
11
|
+
allow_once: pendingApproval.toolName === "todo_write"
|
|
12
|
+
? pendingApproval.constraint === "task-ext-files:v1" ? "批准本任务外部文件修改" : "批准本任务文件修改"
|
|
13
|
+
: "允许本次",
|
|
14
|
+
allow_session: "本次会话允许",
|
|
15
|
+
allow_workspace: pendingApproval.toolName === "run_command" ? "记住此命令" : "此工作区允许",
|
|
16
|
+
deny: "拒绝",
|
|
17
|
+
};
|
|
18
|
+
return pendingApproval.allowedDecisions.map(function createFrozenOption(decision) { return { decision, label: labels[decision] }; });
|
|
19
|
+
}
|
|
20
|
+
if (pendingApproval.toolName === "run_command") {
|
|
21
|
+
return [
|
|
22
|
+
{ decision: "allow_once", label: "允许本次" },
|
|
23
|
+
{ decision: "deny", label: "拒绝" },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
return [
|
|
27
|
+
{ decision: "allow_once", label: "允许本次" },
|
|
28
|
+
{ decision: "allow_session", label: "本次会话允许" },
|
|
29
|
+
{ decision: "allow_workspace", label: "此工作区允许" },
|
|
30
|
+
{ decision: "deny", label: "拒绝" },
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
/** 以 CC 风格上下选择、回车确认渲染当前审批菜单。 */
|
|
34
|
+
export function TuiApprovalPrompt(props) {
|
|
35
|
+
const options = createTuiApprovalOptions(props.pendingApproval);
|
|
36
|
+
const selectedIndex = Math.max(0, Math.min(props.selectedIndex, options.length - 1));
|
|
37
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: props.useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u9700\u8981\u4F60\u7684\u786E\u8BA4" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", props.pendingApproval.display?.operation ?? getTuiToolLabel(props.pendingApproval.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", props.pendingApproval.reason] }), props.pendingApproval.display?.resource ? _jsxs(Text, { children: ["\u5F71\u54CD\uFF1A", props.pendingApproval.display.resource] }) : null, props.pendingApproval.display?.cwd ? _jsxs(Text, { children: ["\u5DE5\u4F5C\u76EE\u5F55\uFF1A", props.pendingApproval.display.cwd] }) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution === true ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u547D\u4EE4\u58F0\u660E\u4E86\u5DE5\u4F5C\u533A\u5916\u76EE\u6807\uFF0C\u4F46\u5BBF\u4E3B\u8FDB\u7A0B\u4E0D\u53D7\u8BE5\u6587\u4EF6\u8303\u56F4\u7EA6\u675F\uFF0C\u53EF\u80FD\u540C\u65F6\u5F71\u54CD\u5176\u4ED6\u6587\u4EF6\uFF1B\u6279\u51C6\u4E0D\u6784\u6210\u7CBE\u786E\u8303\u56F4\u4FDD\u8BC1" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution !== true && props.pendingApproval.display?.unknownScope ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u4FEE\u6539\u8303\u56F4\u65E0\u6CD5\u7CBE\u786E\u9650\u5B9A\uFF1A\u8BE5\u547D\u4EE4\u672A\u58F0\u660E\u7CBE\u786E\u5916\u90E8\u76EE\u6807\uFF0C\u8BF7\u5BA1\u67E5\u5B8C\u6574\u547D\u4EE4\u540E\u518D\u51B3\u5B9A" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.display?.hostExecution !== true && !props.pendingApproval.display?.unknownScope ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u76EE\u6807\u4F4D\u4E8E\u5DE5\u4F5C\u533A\u4E4B\u5916\uFF0C\u6388\u6743\u4EC5\u8986\u76D6\u4E0A\u8FF0\u7CBE\u786E\u76EE\u6807" })) : null, props.pendingApproval.display?.hostExecution === true ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "Windows \u5BBF\u4E3B\u6267\u884C\uFF1A\u672C\u6B21\u547D\u4EE4\u5C06\u5728\u5F53\u524D\u7528\u6237\u6743\u9650\u4E0B\u76F4\u63A5\u8FD0\u884C\uFF0C\u672A\u542F\u7528\u6C99\u7BB1\u9694\u79BB\uFF1B\u6279\u51C6\u4EC5\u5BF9\u672C\u6B21\u8C03\u7528\u6709\u6548\uFF0C\u62D2\u7EDD\u5219\u4E0D\u6267\u884C" })) : null, props.pendingApproval.toolName === "run_command" && options.some(function hasWorkspaceOption(option) { return option.decision === "allow_workspace"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u9009\u62E9\u201C\u8BB0\u4F4F\u6B64\u547D\u4EE4\u201D\u4F1A\u8BB0\u4F4F\u672C\u6761\u547D\u4EE4\uFF08\u7A0B\u5E8F\u3001\u53C2\u6570\u4E0E\u5DE5\u4F5C\u76EE\u5F55\u5B8C\u5168\u4E00\u81F4\uFF09\uFF0C\u6B64\u540E\u5728\u672C\u5DE5\u4F5C\u533A\u81EA\u52A8\u653E\u884C\uFF1B\u53C2\u6570\u6216\u76EE\u5F55\u4E0D\u540C\u7684\u547D\u4EE4\u4ECD\u4F1A\u91CD\u65B0\u8BE2\u95EE" })) : null, props.pendingApproval.constraint === "task-ext-files:v1" ? (_jsx(Text, { color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: "\u26A0 \u5916\u90E8\u8DEF\u5F84\uFF1A\u76EE\u6807\u4F4D\u4E8E\u5DE5\u4F5C\u533A\u4E4B\u5916\uFF0C\u4E0E\u5DE5\u4F5C\u533A\u6587\u4EF6\u96C6\u5408\u5206\u5F00\u5355\u72EC\u6279\u51C6\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u6E05\u5355\u5185\u7CBE\u786E\u6587\u4EF6\uFF0C\u4E0D\u542B\u7236\u76EE\u5F55\u6216\u9012\u5F52\u6743\u9650" })) : null, props.pendingApproval.toolName === "todo_write" && props.pendingApproval.constraint !== "task-ext-files:v1" && options.some(function hasOnceOption(option) { return option.decision === "allow_once"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u6279\u51C6\u540E\uFF0C\u672C\u4EFB\u52A1\u5185\u5BF9\u6E05\u5355\u5185\u6587\u4EF6\u7684\u5BF9\u5E94\u4FEE\u6539\u5C06\u81EA\u52A8\u653E\u884C\uFF1B\u6E05\u5355\u5916\u7684\u65B0\u6587\u4EF6\u4ECD\u4F1A\u9010\u6B21\u8BE2\u95EE\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u5F53\u524D\u4EFB\u52A1" })) : null, props.pendingApproval.constraint === "task-ext-files:v1" && options.some(function hasOnceOption(option) { return option.decision === "allow_once"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u6279\u51C6\u540E\uFF0C\u672C\u4EFB\u52A1\u5185\u5BF9\u6E05\u5355\u5185\u7CBE\u786E\u6587\u4EF6\u7684\u5BF9\u5E94\u4FEE\u6539\u5C06\u81EA\u52A8\u653E\u884C\uFF1B\u6E05\u5355\u5916\u76EE\u6807\u4ECD\u9010\u6B21\u8BE2\u95EE\uFF1B\u6388\u6743\u4EC5\u8986\u76D6\u5F53\u524D\u4EFB\u52A1\uFF0C\u65B0\u76EE\u5F55\u6216\u65B0\u6587\u4EF6\u4E0D\u4F1A\u56E0\u6B64\u83B7\u5F97\u6388\u6743" })) : null, props.pendingApproval.constraint === "no-sandbox-exec:v1" ? (_jsx(Text, { color: props.useColor ? "yellow" : undefined, children: "\u6C99\u7BB1\u542F\u52A8\u5931\u8D25\uFF1A\u672C\u6B21\u5C06\u65E0\u6C99\u7BB1\u9694\u79BB\u76F4\u63A5\u6267\u884C\u4E0A\u8FF0\u547D\u4EE4\uFF1B\u6279\u51C6\u4EC5\u5BF9\u672C\u6B21\u8C03\u7528\u6709\u6548\uFF0C\u62D2\u7EDD\u5219\u4EFB\u52A1\u505C\u6B62" })) : null, props.pendingApproval.display?.effect === "external" && props.pendingApproval.toolName !== "run_command" && options.some(function hasWorkspaceOption(option) { return option.decision === "allow_workspace"; }) ? (_jsx(Text, { dimColor: props.useColor, children: "\u9009\u62E9\u201C\u6B64\u5DE5\u4F5C\u533A\u5141\u8BB8\u201D\u5C06\u628A\u8BE5\u89C4\u5219\u5199\u5165\u5DE5\u4F5C\u533A .autopilot/settings.local.json" })) : null, options.map(function renderApprovalOption(option, index) {
|
|
38
|
+
const selected = index === selectedIndex;
|
|
39
|
+
return _jsxs(Text, { inverse: selected && props.useColor, children: [selected ? "›" : " ", " ", option.label, selected && props.screenReader ? "(已选择)" : ""] }, option.decision);
|
|
40
|
+
}), _jsx(Text, { dimColor: props.useColor, children: "\u2191/\u2193 \u9009\u62E9 \u00B7 Enter \u786E\u8BA4" })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 顶部品牌头:静态像素字标 + Autopilot 版本/模型/推理强度信息。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliStatusView } from "../../types.js";
|
|
4
|
+
import type { CliTuiTranscriptItem } from "../types.js";
|
|
5
|
+
/** 内联历史项标记:用于把品牌头和 transcript 放入同一棵可重排的渲染树。 */
|
|
6
|
+
export declare const TUI_BRAND_HISTORY_ITEM: {
|
|
7
|
+
kind: "brand";
|
|
8
|
+
key: string;
|
|
9
|
+
};
|
|
10
|
+
/** 内联历史项:品牌头或 transcript 节点。 */
|
|
11
|
+
export type TuiInlineHistoryItem = typeof TUI_BRAND_HISTORY_ITEM | CliTuiTranscriptItem;
|
|
12
|
+
/** 渲染顶部品牌头。 */
|
|
13
|
+
export declare function TuiBrandHeader(props: {
|
|
14
|
+
statusView?: CliStatusView;
|
|
15
|
+
columns: number;
|
|
16
|
+
useColor: boolean;
|
|
17
|
+
screenReader: boolean;
|
|
18
|
+
reducedMotion: boolean;
|
|
19
|
+
animationActive: boolean;
|
|
20
|
+
}): ReactNode;
|