min-agent 0.3.0 → 0.4.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 +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tools/write.js
CHANGED
|
@@ -2,7 +2,9 @@ import { tool, jsonSchema } from "ai";
|
|
|
2
2
|
import { mkdir, writeFile } from "fs/promises";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { confirm,
|
|
5
|
+
import { confirm, isEditAutoApprove } from "../confirm.js";
|
|
6
|
+
import { pathAccessError } from "../sandbox.js";
|
|
7
|
+
import { atomicWriteFile } from "./atomic-file.js";
|
|
6
8
|
const MAX_WRITE_BYTES = 10 * 1024 * 1024;
|
|
7
9
|
export const writeTool = tool({
|
|
8
10
|
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does (unless `append` is true). Creates parent directories as needed.",
|
|
@@ -19,20 +21,35 @@ export const writeTool = tool({
|
|
|
19
21
|
if (Buffer.byteLength(content, "utf-8") > MAX_WRITE_BYTES) {
|
|
20
22
|
return `Error: content exceeds ${MAX_WRITE_BYTES} bytes. Write in smaller chunks or check the content size.`;
|
|
21
23
|
}
|
|
24
|
+
const denied = pathAccessError(filePath, "write");
|
|
25
|
+
if (denied)
|
|
26
|
+
return `Error: ${denied}`;
|
|
22
27
|
const resolved = path.resolve(process.cwd(), filePath);
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
// Same confirmation semantics as edit: in ask mode every mutation —
|
|
29
|
+
// create, overwrite or append — is confirmed; accept-edits skips them all.
|
|
30
|
+
if (!isEditAutoApprove()) {
|
|
31
|
+
const exists = existsSync(resolved);
|
|
32
|
+
const action = append
|
|
33
|
+
? `Append to file: ${filePath}`
|
|
34
|
+
: exists
|
|
35
|
+
? `Overwrite existing file: ${filePath}`
|
|
36
|
+
: `Create new file: ${filePath}`;
|
|
37
|
+
const approved = await confirm(action);
|
|
26
38
|
if (!approved)
|
|
27
39
|
return "Write rejected by user.";
|
|
28
40
|
}
|
|
29
41
|
try {
|
|
30
42
|
await mkdir(path.dirname(resolved), { recursive: true });
|
|
31
|
-
|
|
43
|
+
if (append) {
|
|
44
|
+
await writeFile(resolved, content, { flag: "a", encoding: "utf-8" });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await atomicWriteFile(resolved, content);
|
|
48
|
+
}
|
|
32
49
|
return `Written ${Buffer.byteLength(content, "utf-8")} bytes to ${filePath}${append ? " (appended)" : ""}`;
|
|
33
50
|
}
|
|
34
51
|
catch (err) {
|
|
35
|
-
return `Error writing file: ${err.message}`;
|
|
52
|
+
return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
|
|
36
53
|
}
|
|
37
54
|
},
|
|
38
55
|
});
|
package/dist/tui/App.js
CHANGED
|
@@ -8,9 +8,11 @@ import { ConfirmBar, confirmBarRows } from "./ConfirmBar.js";
|
|
|
8
8
|
import { QuestionBar, questionBarRows } from "./QuestionBar.js";
|
|
9
9
|
import { StatusBar } from "./StatusBar.js";
|
|
10
10
|
import { ModelPicker, modelPickerRows } from "./ModelPicker.js";
|
|
11
|
+
import { SessionPicker, sessionPickerRows } from "./SessionPicker.js";
|
|
11
12
|
import { setCaretPosition } from "./caret.js";
|
|
12
|
-
import { computeMessageMaxHeight } from "./layout.js";
|
|
13
|
-
|
|
13
|
+
import { computeMessageMaxHeight, frameRows, INPUT_BAR_ROWS } from "./layout.js";
|
|
14
|
+
import { isCtrlC } from "./input-history.js";
|
|
15
|
+
export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, }) {
|
|
14
16
|
// Ink recalculates its own layout on resize without re-rendering React, so
|
|
15
17
|
// track terminal size ourselves to keep heights/widths in sync.
|
|
16
18
|
const { stdout } = useStdout();
|
|
@@ -18,32 +20,79 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
18
20
|
useEffect(() => {
|
|
19
21
|
const onResize = () => setTerminal({ columns: stdout.columns || 80, rows: stdout.rows || 0 });
|
|
20
22
|
stdout.on("resize", onResize);
|
|
21
|
-
return () => {
|
|
23
|
+
return () => {
|
|
24
|
+
stdout.off("resize", onResize);
|
|
25
|
+
};
|
|
22
26
|
}, [stdout]);
|
|
23
27
|
useInput((input, key) => {
|
|
24
|
-
if (
|
|
25
|
-
|
|
28
|
+
if (isCtrlC(input, key)) {
|
|
29
|
+
if (initialState.confirmMessage) {
|
|
30
|
+
onConfirm(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (initialState.question) {
|
|
34
|
+
onQuestionAnswer(null);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (initialState.modelPicker) {
|
|
38
|
+
onModelCancel();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (initialState.sessionPicker) {
|
|
42
|
+
onSessionCancel();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (initialState.selectionMode) {
|
|
46
|
+
onExitSelectionMode();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (initialState.isRunning) {
|
|
50
|
+
onExit();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.ctrl && input === "b") {
|
|
55
|
+
if (initialState.confirmMessage ||
|
|
56
|
+
initialState.question ||
|
|
57
|
+
initialState.modelPicker ||
|
|
58
|
+
initialState.sessionPicker)
|
|
59
|
+
return;
|
|
60
|
+
if (initialState.isRunning)
|
|
61
|
+
return;
|
|
62
|
+
if (!initialState.selectionMode)
|
|
63
|
+
return;
|
|
64
|
+
onToggleSelectionMode();
|
|
26
65
|
return;
|
|
27
66
|
}
|
|
28
|
-
if (key.escape &&
|
|
67
|
+
if (key.escape &&
|
|
68
|
+
initialState.isRunning &&
|
|
69
|
+
!initialState.confirmMessage &&
|
|
70
|
+
!initialState.question &&
|
|
71
|
+
!initialState.modelPicker &&
|
|
72
|
+
!initialState.sessionPicker) {
|
|
29
73
|
onExit();
|
|
30
74
|
}
|
|
31
75
|
});
|
|
32
|
-
if (initialState.confirmMessage || initialState.question || initialState.modelPicker)
|
|
76
|
+
if (initialState.confirmMessage || initialState.question || initialState.modelPicker || initialState.sessionPicker)
|
|
33
77
|
setCaretPosition(null);
|
|
34
78
|
const columns = terminal.columns;
|
|
35
79
|
const terminalRows = terminal.rows;
|
|
80
|
+
const layoutRows = frameRows(terminalRows);
|
|
36
81
|
const confirmActive = Boolean(initialState.confirmMessage);
|
|
37
82
|
const questionActive = Boolean(initialState.question);
|
|
38
83
|
const modelPickerActive = Boolean(initialState.modelPicker);
|
|
84
|
+
const sessionPickerActive = Boolean(initialState.sessionPicker);
|
|
39
85
|
const footerRows = confirmActive
|
|
40
|
-
? confirmBarRows(initialState.confirmMessage ?? "", columns)
|
|
86
|
+
? confirmBarRows(initialState.confirmMessage ?? "", columns, layoutRows)
|
|
41
87
|
: questionActive
|
|
42
|
-
? questionBarRows(initialState.question.prompt, initialState.question.options, columns)
|
|
88
|
+
? questionBarRows(initialState.question.prompt, initialState.question.options, columns, layoutRows)
|
|
43
89
|
: modelPickerActive
|
|
44
|
-
? modelPickerRows(
|
|
45
|
-
:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
90
|
+
? modelPickerRows(layoutRows)
|
|
91
|
+
: sessionPickerActive
|
|
92
|
+
? sessionPickerRows(layoutRows)
|
|
93
|
+
: INPUT_BAR_ROWS;
|
|
94
|
+
const spinnerVisible = initialState.isRunning && !confirmActive && !questionActive && !modelPickerActive && !sessionPickerActive;
|
|
95
|
+
const spinnerRows = spinnerVisible ? 1 : 0;
|
|
96
|
+
const messageMaxHeight = computeMessageMaxHeight(layoutRows, footerRows, spinnerRows);
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: layoutRows || undefined, overflow: "hidden", children: [!modelPickerActive && (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", width: "100%", children: _jsx(MessageList, { messages: initialState.messages, maxHeight: messageMaxHeight, columns: columns, onCopyNotice: onCopyNotice, selectionMode: Boolean(initialState.selectionMode), onExitSelectionMode: onExitSelectionMode, interactive: !confirmActive && !questionActive }) })), spinnerVisible && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, width: "100%", children: _jsx(Spinner, { label: initialState.spinnerText || "思考中..." }) })), _jsx(StatusBar, { state: initialState, columns: columns }), initialState.confirmMessage ? (_jsx(ConfirmBar, { message: initialState.confirmMessage, onConfirm: onConfirm, columns: columns, maxRows: footerRows })) : initialState.question ? (_jsx(QuestionBar, { prompt: initialState.question.prompt, options: initialState.question.options, onAnswer: onQuestionAnswer, columns: columns, maxRows: footerRows })) : initialState.modelPicker ? (_jsx(ModelPicker, { currentModel: initialState.model, onSelect: onModelPick, onCancel: onModelCancel })) : initialState.sessionPicker ? (_jsx(SessionPicker, { currentId: initialState.sessionId, onSelect: onSessionPick, onCancel: onSessionCancel })) : (_jsx(InputBar, { onSubmit: onSubmit, disabled: initialState.isRunning || Boolean(initialState.selectionMode), placeholder: initialState.isRunning ? "按 Esc 取消运行..." : "输入消息... (Ctrl+J 换行)", columns: columns, terminalRows: layoutRows, onSelectionMode: onToggleSelectionMode, onExit: onExit }))] }));
|
|
49
98
|
}
|
package/dist/tui/ConfirmBar.js
CHANGED
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
2
3
|
import { Box, Text, useInput } from "ink";
|
|
3
4
|
import { wrapByWidth } from "./text-width.js";
|
|
4
|
-
import { innerTextWidth } from "./layout.js";
|
|
5
|
+
import { CONFIRM_CHROME_ROWS, footerMaxRows, innerTextWidth, sliceLineWindow } from "./layout.js";
|
|
5
6
|
import { theme } from "./theme.js";
|
|
7
|
+
export function confirmMessageLines(message, columns) {
|
|
8
|
+
const textWidth = innerTextWidth(columns);
|
|
9
|
+
return String(message ?? "")
|
|
10
|
+
.split("\n")
|
|
11
|
+
.flatMap((line) => wrapByWidth(line, textWidth));
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
14
|
* Terminal rows the confirm bar occupies, mirroring the JSX layout below.
|
|
8
|
-
*
|
|
9
|
-
* terminal height while the prompt is up.
|
|
15
|
+
* Capped so a huge permission payload cannot push [y]/[N] off the screen.
|
|
10
16
|
*/
|
|
11
|
-
export function confirmBarRows(message, columns) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
for (const line of message.split("\n"))
|
|
15
|
-
rows += wrapByWidth(line, textWidth).length;
|
|
16
|
-
return rows + 5; /* borders2 + title1 + marginTop1 + prompt1 */
|
|
17
|
+
export function confirmBarRows(message, columns, frameRowCount = 0) {
|
|
18
|
+
const natural = confirmMessageLines(message, columns).length + CONFIRM_CHROME_ROWS;
|
|
19
|
+
return Math.min(natural, footerMaxRows(frameRowCount, CONFIRM_CHROME_ROWS));
|
|
17
20
|
}
|
|
18
|
-
export function ConfirmBar({ message, onConfirm }) {
|
|
21
|
+
export function ConfirmBar({ message, onConfirm, columns, maxRows }) {
|
|
22
|
+
const lines = useMemo(() => confirmMessageLines(message, columns), [message, columns]);
|
|
23
|
+
const bodyMax = Math.max(0, maxRows - CONFIRM_CHROME_ROWS);
|
|
24
|
+
const [top, setTop] = useState(0);
|
|
25
|
+
const window = useMemo(() => sliceLineWindow(lines, bodyMax, top), [lines, bodyMax, top]);
|
|
26
|
+
const scrollable = window.above > 0 || window.below > 0;
|
|
19
27
|
useInput((input, key) => {
|
|
20
28
|
if (key.escape) {
|
|
21
29
|
onConfirm(false);
|
|
22
30
|
return;
|
|
23
31
|
}
|
|
24
32
|
const hasEnter = key.return || /[\r\n]/.test(input);
|
|
25
|
-
const stripped = input
|
|
33
|
+
const stripped = input
|
|
34
|
+
.replace(/[\r\n]+/g, "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
26
37
|
if (stripped === "y") {
|
|
27
38
|
onConfirm(true);
|
|
39
|
+
return;
|
|
28
40
|
}
|
|
29
|
-
|
|
41
|
+
if (stripped === "n" || (stripped === "" && hasEnter)) {
|
|
30
42
|
onConfirm(false);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!scrollable)
|
|
46
|
+
return;
|
|
47
|
+
const view = Math.max(1, window.lines.length);
|
|
48
|
+
if (key.upArrow) {
|
|
49
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t - 1).start);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.downArrow) {
|
|
53
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t + 1).start);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.pageUp) {
|
|
57
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t - view).start);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.pageDown) {
|
|
61
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t + view).start);
|
|
62
|
+
return;
|
|
31
63
|
}
|
|
32
64
|
});
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.confirmBorder, paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: theme.confirmBorder, children: "\u26A0 \u8BF7\u6C42\u6267\u884C\u6743\u9650" }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { children:
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.confirmBorder, paddingX: 1, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { bold: true, color: theme.confirmBorder, wrap: "truncate-end", children: "\u26A0 \u8BF7\u6C42\u6267\u884C\u6743\u9650" }) }), bodyMax > 0 && (_jsxs(Box, { flexDirection: "column", height: bodyMax, overflow: "hidden", flexShrink: 0, width: "100%", children: [window.above > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4EE5\u4E0A ", window.above, " \u884C"] }) })), window.lines.map((line, i) => (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsx(Text, { wrap: "truncate-end", children: line }) }, i))), window.below > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2193 \u8FD8\u6709 ", window.below, " \u884C"] }) }))] })), _jsxs(Box, { marginTop: 1, flexShrink: 0, height: 1, overflow: "hidden", width: "100%", children: [_jsxs(Text, { bold: true, color: theme.confirmBorder, children: ["? \u662F\u5426\u5141\u8BB8\u6267\u884C\uFF1F", " "] }), _jsx(Text, { color: theme.success, children: "[y]" }), _jsx(Text, { color: theme.muted, children: " / " }), _jsx(Text, { color: theme.error, children: "[N]" })] })] }));
|
|
34
66
|
}
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -3,10 +3,9 @@ import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
|
3
3
|
import { Box, Text, useInput, useStdin, useStdout } from "ink";
|
|
4
4
|
import { setCaretPosition } from "./caret.js";
|
|
5
5
|
import { displayWidth } from "./text-width.js";
|
|
6
|
-
import { visualRows, moveCaretHorizontal, insertAt, backspaceAt, deleteAt, caretIndexFromClick, scanDeleteKeys, moveToLineStart, moveToLineEnd, deleteToLineStart, deleteToLineEnd, deleteWordBefore } from "./caret-pos.js";
|
|
7
|
-
import { searchHistory } from "./input-history.js";
|
|
8
|
-
import {
|
|
9
|
-
import { filterSlashCommands, getSlashArgOptions, applySlashArgCompletion } from "./slash-commands.js";
|
|
6
|
+
import { visualRows, moveCaretHorizontal, moveCaretVertical, insertAt, backspaceAt, deleteAt, caretIndexFromClick, scanDeleteKeys, scanHomeEndKeys, moveToLineStart, moveToLineEnd, deleteToLineStart, deleteToLineEnd, deleteWordBefore, } from "./caret-pos.js";
|
|
7
|
+
import { searchHistory, loadInputHistory, saveInputHistory, createHistory, pushInput, browseHistory, resetHistory, isCtrlC, clearInputOnCtrlC, EXIT_CTRL_C_WINDOW_MS, } from "./input-history.js";
|
|
8
|
+
import { filterSlashCommands, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, } from "./slash-commands.js";
|
|
10
9
|
import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
|
|
11
10
|
import { scanSgrMouse } from "./mouse.js";
|
|
12
11
|
import { TEXT_START_COLUMN, inputTextWidth } from "./layout.js";
|
|
@@ -51,7 +50,7 @@ export function accumulateKittyInput(buffer, chunk) {
|
|
|
51
50
|
* exactly; the hardware cursor is parked on that cell so IME composition
|
|
52
51
|
* (Chinese, Japanese, Korean) shows up in the right place.
|
|
53
52
|
*/
|
|
54
|
-
export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows }) {
|
|
53
|
+
export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, }) {
|
|
55
54
|
const [editing, setEditing] = useState({ value: "", caretIndex: 0 });
|
|
56
55
|
const { value, caretIndex } = editing;
|
|
57
56
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
@@ -61,15 +60,42 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
61
60
|
const kittyBuffer = useRef("");
|
|
62
61
|
const mouseBuffer = useRef("");
|
|
63
62
|
const deleteBuffer = useRef("");
|
|
63
|
+
const homeEndBuffer = useRef("");
|
|
64
64
|
const width = columns || 80;
|
|
65
|
-
const [historyState, setHistoryState] = useState(() => createHistory());
|
|
65
|
+
const [historyState, setHistoryState] = useState(() => createHistory(loadInputHistory()));
|
|
66
66
|
const historyRef = useRef(historyState);
|
|
67
|
+
const [exitHint, setExitHint] = useState(false);
|
|
68
|
+
const exitArmedAtRef = useRef(null);
|
|
69
|
+
const exitHintTimer = useRef(null);
|
|
70
|
+
const clearExitHint = useCallback(() => {
|
|
71
|
+
if (exitArmedAtRef.current === null && exitHintTimer.current === null)
|
|
72
|
+
return;
|
|
73
|
+
if (exitHintTimer.current !== null) {
|
|
74
|
+
clearTimeout(exitHintTimer.current);
|
|
75
|
+
exitHintTimer.current = null;
|
|
76
|
+
}
|
|
77
|
+
exitArmedAtRef.current = null;
|
|
78
|
+
setExitHint(false);
|
|
79
|
+
}, []);
|
|
80
|
+
const showExitHint = useCallback((armedAt) => {
|
|
81
|
+
exitArmedAtRef.current = armedAt;
|
|
82
|
+
setExitHint(true);
|
|
83
|
+
if (exitHintTimer.current !== null)
|
|
84
|
+
clearTimeout(exitHintTimer.current);
|
|
85
|
+
exitHintTimer.current = setTimeout(() => {
|
|
86
|
+
exitHintTimer.current = null;
|
|
87
|
+
exitArmedAtRef.current = null;
|
|
88
|
+
setExitHint(false);
|
|
89
|
+
}, EXIT_CTRL_C_WINDOW_MS);
|
|
90
|
+
}, []);
|
|
91
|
+
useEffect(() => () => clearExitHint(), [clearExitHint]);
|
|
67
92
|
// The ref mirrors `editing` synchronously, so edits from separate stdin
|
|
68
93
|
// listeners (Ink's useInput for text, the raw listener for delete keys)
|
|
69
94
|
// always apply to the newest state, in arrival order, even when React has
|
|
70
95
|
// not re-rendered yet (fast key repeats, text + delete in one chunk).
|
|
71
96
|
const editingRef = useRef(editing);
|
|
72
97
|
const applyEdit = useCallback((edit) => {
|
|
98
|
+
clearExitHint();
|
|
73
99
|
const after = edit(editingRef.current.value, editingRef.current.caretIndex);
|
|
74
100
|
editingRef.current = after;
|
|
75
101
|
setEditing(after);
|
|
@@ -78,14 +104,15 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
78
104
|
historyRef.current = nextHistory;
|
|
79
105
|
setHistoryState(nextHistory);
|
|
80
106
|
}
|
|
81
|
-
}, []);
|
|
107
|
+
}, [clearExitHint]);
|
|
82
108
|
const submit = (text) => {
|
|
83
109
|
historyRef.current = pushInput(historyRef.current, text);
|
|
84
110
|
setHistoryState(historyRef.current);
|
|
111
|
+
saveInputHistory(historyRef.current.entries);
|
|
85
112
|
onSubmit(text);
|
|
86
113
|
};
|
|
87
114
|
const browseTo = (direction) => {
|
|
88
|
-
const { text, state } = browseHistory(historyRef.current, direction);
|
|
115
|
+
const { text, state } = browseHistory(historyRef.current, direction, isKnownSlashCommandInput);
|
|
89
116
|
historyRef.current = state;
|
|
90
117
|
setHistoryState(state);
|
|
91
118
|
if (text !== editingRef.current.value) {
|
|
@@ -95,7 +122,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
95
122
|
}
|
|
96
123
|
};
|
|
97
124
|
// Release the caret when the input bar goes away (e.g. confirm prompt).
|
|
98
|
-
useEffect(() => () => {
|
|
125
|
+
useEffect(() => () => {
|
|
126
|
+
setCaretPosition(null);
|
|
127
|
+
}, []);
|
|
99
128
|
// Slash command menu — closes once a space follows the command name so the
|
|
100
129
|
// argument menu can take over immediately.
|
|
101
130
|
const matches = useMemo(() => filterSlashCommands(value), [value]);
|
|
@@ -107,7 +136,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
107
136
|
const argMenuOpen = !menuOpen && !menuDismissed && argOptions.length > 0;
|
|
108
137
|
const argIndex = argMenuOpen ? Math.min(menuIndex, argOptions.length - 1) : 0;
|
|
109
138
|
const argSelected = argMenuOpen ? argOptions[argIndex] : undefined;
|
|
110
|
-
useEffect(() => {
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setMenuIndex(0);
|
|
141
|
+
}, []);
|
|
111
142
|
const acceptCommand = (cmd) => {
|
|
112
143
|
if (cmd.argHint) {
|
|
113
144
|
applyEdit(() => ({ value: `/${cmd.name} `, caretIndex: cmd.name.length + 2 }));
|
|
@@ -123,9 +154,47 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
123
154
|
return { value: next, caretIndex: Array.from(next).length };
|
|
124
155
|
});
|
|
125
156
|
};
|
|
157
|
+
const acceptCommandRef = useRef(acceptCommand);
|
|
158
|
+
acceptCommandRef.current = acceptCommand;
|
|
159
|
+
const acceptArgRef = useRef(acceptArg);
|
|
160
|
+
acceptArgRef.current = acceptArg;
|
|
161
|
+
const matchesRef = useRef(matches);
|
|
162
|
+
matchesRef.current = matches;
|
|
163
|
+
const argOptionsRef = useRef(argOptions);
|
|
164
|
+
argOptionsRef.current = argOptions;
|
|
165
|
+
const menuOpenRef = useRef(menuOpen);
|
|
166
|
+
menuOpenRef.current = menuOpen;
|
|
167
|
+
const argMenuOpenRef = useRef(argMenuOpen);
|
|
168
|
+
argMenuOpenRef.current = argMenuOpen;
|
|
126
169
|
useInput((input, key) => {
|
|
127
170
|
if (disabled)
|
|
128
171
|
return;
|
|
172
|
+
if (isCtrlC(input, key)) {
|
|
173
|
+
const current = editingRef.current.value;
|
|
174
|
+
const result = clearInputOnCtrlC(historyRef.current, current, exitArmedAtRef.current, Date.now());
|
|
175
|
+
if (result.exit) {
|
|
176
|
+
clearExitHint();
|
|
177
|
+
onExit();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (result.state.entries !== historyRef.current.entries) {
|
|
181
|
+
saveInputHistory(result.state.entries);
|
|
182
|
+
}
|
|
183
|
+
historyRef.current = result.state;
|
|
184
|
+
setHistoryState(result.state);
|
|
185
|
+
if (result.armedAt !== null) {
|
|
186
|
+
showExitHint(result.armedAt);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
clearExitHint();
|
|
190
|
+
applyEdit(() => ({ value: "", caretIndex: 0 }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
clearExitHint();
|
|
194
|
+
if (key.ctrl && input === "b") {
|
|
195
|
+
onSelectionMode?.();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
129
198
|
// Fast typing or paste can deliver "text\r" as a single chunk — treat a
|
|
130
199
|
// trailing Enter as Enter and strip it from the value.
|
|
131
200
|
const cleanInput = input.replace(/[\r\n]+$/, "");
|
|
@@ -148,48 +217,60 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
148
217
|
}
|
|
149
218
|
// Any other key re-opens the menu
|
|
150
219
|
setMenuDismissed(false);
|
|
151
|
-
// Slash menu
|
|
220
|
+
// Slash menu: Tab/Shift+Tab pick, Enter runs. ↑↓ always recall sent input.
|
|
152
221
|
if (menuOpen) {
|
|
153
|
-
if (key.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
setMenuIndex((i) => (i - 1 + matches.length) % matches.length);
|
|
222
|
+
if (key.tab) {
|
|
223
|
+
if (matches.length === 1 && selected) {
|
|
224
|
+
acceptCommand(selected);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
setMenuIndex((i) => (key.shift ? (i - 1 + matches.length) % matches.length : (i + 1) % matches.length));
|
|
159
228
|
return;
|
|
160
229
|
}
|
|
161
|
-
if (pressedEnter
|
|
230
|
+
if (pressedEnter) {
|
|
162
231
|
if (selected)
|
|
163
232
|
acceptCommand(selected);
|
|
164
233
|
return;
|
|
165
234
|
}
|
|
166
|
-
// Left/right are ignored while the menu is open
|
|
167
235
|
if (key.leftArrow || key.rightArrow)
|
|
168
236
|
return;
|
|
169
237
|
}
|
|
170
|
-
// Argument menu:
|
|
171
|
-
// fully typed command is never swallowed by the suggestion list.
|
|
238
|
+
// Argument menu: Tab completes the highlight. Enter still submits the line
|
|
239
|
+
// so a fully typed command is never swallowed by the suggestion list.
|
|
172
240
|
if (argMenuOpen) {
|
|
173
|
-
if (key.downArrow) {
|
|
174
|
-
setMenuIndex((i) => (i + 1) % argOptions.length);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (key.upArrow) {
|
|
178
|
-
setMenuIndex((i) => (i - 1 + argOptions.length) % argOptions.length);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
241
|
if (key.tab) {
|
|
182
242
|
if (argSelected)
|
|
183
243
|
acceptArg(argSelected);
|
|
184
244
|
return;
|
|
185
245
|
}
|
|
186
246
|
}
|
|
187
|
-
|
|
247
|
+
const inkKey = key;
|
|
248
|
+
if (inkKey.home) {
|
|
249
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveToLineStart(v, c) }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (inkKey.end) {
|
|
253
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveToLineEnd(v, c) }));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Move one visual row first; only browse history when already at the edge.
|
|
188
257
|
if (key.upArrow) {
|
|
258
|
+
const { value: v, caretIndex: c } = editingRef.current;
|
|
259
|
+
const next = moveCaretVertical(v, c, -1, inputTextWidth(columns));
|
|
260
|
+
if (next !== c) {
|
|
261
|
+
applyEdit(() => ({ value: v, caretIndex: next }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
189
264
|
browseTo(1);
|
|
190
265
|
return;
|
|
191
266
|
}
|
|
192
267
|
if (key.downArrow) {
|
|
268
|
+
const { value: v, caretIndex: c } = editingRef.current;
|
|
269
|
+
const next = moveCaretVertical(v, c, 1, inputTextWidth(columns));
|
|
270
|
+
if (next !== c) {
|
|
271
|
+
applyEdit(() => ({ value: v, caretIndex: next }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
193
274
|
browseTo(-1);
|
|
194
275
|
return;
|
|
195
276
|
}
|
|
@@ -298,6 +379,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
298
379
|
}
|
|
299
380
|
}, { isActive: !disabled });
|
|
300
381
|
const isEmpty = value === "";
|
|
382
|
+
const shownPlaceholder = exitHint ? "再按一次 Ctrl+C 退出" : placeholder;
|
|
301
383
|
const logicalLines = value.split("\n");
|
|
302
384
|
const isMultiline = logicalLines.length > 1;
|
|
303
385
|
const borderColor = disabled ? theme.inputBorderDisabled : theme.inputBorder;
|
|
@@ -335,7 +417,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
335
417
|
// (single-row) multi-line hint — plus the slash menu when it floats below
|
|
336
418
|
// the input bar.
|
|
337
419
|
const hintRows = isMultiline ? 1 : 0;
|
|
338
|
-
const
|
|
420
|
+
const argShown = argMenuOpen ? Math.min(8, argOptions.length) : 0;
|
|
421
|
+
const menuRows = menuOpen ? matches.length + 5 : argMenuOpen ? argShown + 4 : 0;
|
|
339
422
|
// Input bar sits at the bottom of App's footer: its content area starts
|
|
340
423
|
// `terminalRows` rows up minus the bar's own height (menu + bottom border +
|
|
341
424
|
// hint above it in screen order, from the bottom of the terminal up).
|
|
@@ -346,6 +429,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
346
429
|
rowsRef.current = rows;
|
|
347
430
|
const inputTopRowRef = useRef(inputTopRow);
|
|
348
431
|
inputTopRowRef.current = inputTopRow;
|
|
432
|
+
const hintRowsRef = useRef(hintRows);
|
|
433
|
+
hintRowsRef.current = hintRows;
|
|
349
434
|
// Listen for Kitty protocol Shift+Enter: ESC[13;2u, which may arrive split
|
|
350
435
|
// across stdin chunks — accumulate and reassemble here. Ink's useInput gets
|
|
351
436
|
// each raw chunk and would append fragments to the value, so isKittySequenceFragment
|
|
@@ -375,6 +460,16 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
375
460
|
applyEdit((v, c) => (action.kind === "backspace" ? backspaceAt(v, c) : deleteAt(v, c)));
|
|
376
461
|
}
|
|
377
462
|
}
|
|
463
|
+
const { buffer: homeBuf, actions: homeActions } = scanHomeEndKeys(homeEndBuffer.current, chunk);
|
|
464
|
+
homeEndBuffer.current = homeBuf;
|
|
465
|
+
if (homeActions.length > 0) {
|
|
466
|
+
for (const action of homeActions) {
|
|
467
|
+
applyEdit((v, c) => ({
|
|
468
|
+
value: v,
|
|
469
|
+
caretIndex: action.kind === "home" ? moveToLineStart(v, c) : moveToLineEnd(v, c),
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
378
473
|
mouseBuffer.current += chunk;
|
|
379
474
|
let sgr;
|
|
380
475
|
while ((sgr = scanSgrMouse(mouseBuffer.current))) {
|
|
@@ -388,6 +483,22 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
388
483
|
caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN),
|
|
389
484
|
}));
|
|
390
485
|
}
|
|
486
|
+
else {
|
|
487
|
+
const chrome = 3;
|
|
488
|
+
const header = menuOpenRef.current ? 1 : 0;
|
|
489
|
+
const optionIndex = sgr.event.row - (inputTopRowRef.current + rowsRef.current.length + hintRowsRef.current + chrome + header);
|
|
490
|
+
if (menuOpenRef.current) {
|
|
491
|
+
const cmd = matchesRef.current[optionIndex];
|
|
492
|
+
if (cmd)
|
|
493
|
+
acceptCommandRef.current(cmd);
|
|
494
|
+
}
|
|
495
|
+
else if (argMenuOpenRef.current) {
|
|
496
|
+
const shown = argOptionsRef.current.slice(0, 8);
|
|
497
|
+
const opt = shown[optionIndex];
|
|
498
|
+
if (opt)
|
|
499
|
+
acceptArgRef.current(opt);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
391
502
|
}
|
|
392
503
|
}
|
|
393
504
|
if (mouseBuffer.current.length > 64) {
|
|
@@ -396,7 +507,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
396
507
|
}
|
|
397
508
|
};
|
|
398
509
|
stdin.on("data", handleData);
|
|
399
|
-
return () => {
|
|
510
|
+
return () => {
|
|
511
|
+
stdin.off("data", handleData);
|
|
512
|
+
};
|
|
400
513
|
}, [stdin, disabled, applyEdit]);
|
|
401
514
|
// Enable the SGR mouse protocol so clicks on the input bar can position the
|
|
402
515
|
// caret. Events arrive on raw stdin; parseSgrMouse picks them out of the
|
|
@@ -404,7 +517,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
404
517
|
// simply never send events — keyboard navigation still works.
|
|
405
518
|
useEffect(() => {
|
|
406
519
|
stdout.write(MOUSE_ENABLE);
|
|
407
|
-
return () => {
|
|
520
|
+
return () => {
|
|
521
|
+
stdout.write(MOUSE_DISABLE);
|
|
522
|
+
};
|
|
408
523
|
}, [stdout]);
|
|
409
524
|
// Must publish during render (not in effect) — Ink writes the frame in
|
|
410
525
|
// resetAfterCommit before effects run. If delayed to useEffect, the
|
|
@@ -417,12 +532,12 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
417
532
|
column: TEXT_START_COLUMN + caretColumnOffset,
|
|
418
533
|
});
|
|
419
534
|
}
|
|
420
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: borderColor, paddingX: 1, children: [isEmpty &&
|
|
535
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: borderColor, paddingX: 1, width: "100%", children: [isEmpty && shownPlaceholder ? (_jsxs(Box, { children: [_jsx(Text, { color: disabled ? theme.inputBorderDisabled : theme.inputBorder, children: GUTTER }), _jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: shownPlaceholder })] })) : (rows.map((row, i) => {
|
|
421
536
|
const chars = Array.from(row.text);
|
|
422
537
|
const caretHere = i === caretRowIndex;
|
|
423
538
|
const offset = caretHere ? caretCharOffset : chars.length;
|
|
424
539
|
const before = chars.slice(0, offset).join("");
|
|
425
540
|
const after = chars.slice(offset).join("");
|
|
426
541
|
return (_jsxs(Box, { children: [_jsx(Text, { color: disabled ? theme.inputBorderDisabled : theme.inputBorder, children: i === 0 ? GUTTER : " " }), _jsxs(Text, { wrap: "truncate-end", children: [before, caretHere && _jsx(Text, { inverse: true, children: " " }), after] })] }, i));
|
|
427
|
-
})), isMultiline && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [logicalLines.length, " \u884C | Shift+Enter/Ctrl+J \u6362\u884C | Enter \u53D1\u9001"] }) }))] }), menuOpen && selected && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.inputBorder, paddingX: 1, marginTop: 1, children: [_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: value.trim() === "/" ? "命令" : `/${value.slice(1)} 命令` }) }), matches.map((c, i) => (_jsxs(Box, { children: [_jsx(Text, { color: i === shownIndex ? theme.accent : theme.muted, children: i === shownIndex ? "❯ " : " " }), _jsxs(Text, { bold: i === shownIndex, color: i === shownIndex ? theme.accent : undefined, children: ["/", c.name, c.argHint ? ` ${c.argHint}` : ""] }), _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [" ", c.description] })] }, c.name))), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "
|
|
542
|
+
})), isMultiline && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [logicalLines.length, " \u884C | Shift+Enter/Ctrl+J \u6362\u884C | Enter \u53D1\u9001"] }) }))] }), menuOpen && selected && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.inputBorder, paddingX: 1, marginTop: 1, width: "100%", children: [_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: value.trim() === "/" ? "命令" : `/${value.slice(1)} 命令` }) }), matches.map((c, i) => (_jsxs(Box, { children: [_jsx(Text, { color: i === shownIndex ? theme.accent : theme.muted, children: i === shownIndex ? "❯ " : " " }), _jsxs(Text, { bold: i === shownIndex, color: i === shownIndex ? theme.accent : undefined, children: ["/", c.name, c.argHint ? ` ${c.argHint}` : ""] }), _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [" ", c.description] })] }, c.name))), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "Tab \u9009\u62E9 \u00B7 Enter \u6267\u884C \u00B7 Esc \u5173\u95ED" }) })] })), argMenuOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.inputBorder, paddingX: 1, marginTop: 1, width: "100%", children: [argOptions.slice(0, 8).map((o, i) => (_jsxs(Box, { children: [_jsx(Text, { color: i === argIndex ? theme.accent : theme.muted, children: i === argIndex ? "❯ " : " " }), _jsx(Text, { bold: i === argIndex, color: i === argIndex ? theme.accent : undefined, children: o.value }), o.description && (_jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [" ", o.description] }))] }, o.value))), _jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [argOptions.length > 8 ? `还有 ${argOptions.length - 8} 项 · ` : "", "Tab \u8865\u5168 \u00B7 Enter \u6267\u884C \u00B7 Esc \u5173\u95ED"] }) })] }))] }));
|
|
428
543
|
}
|