min-agent 0.2.1 → 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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- 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 +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- 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 +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- 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-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -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 +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tui/InputBar.js
CHANGED
|
@@ -1,83 +1,543 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
3
|
-
import { Box, Text, useInput, useStdin } from "ink";
|
|
2
|
+
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdin, useStdout } from "ink";
|
|
4
|
+
import { setCaretPosition } from "./caret.js";
|
|
5
|
+
import { displayWidth } from "./text-width.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";
|
|
9
|
+
import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
|
|
10
|
+
import { scanSgrMouse } from "./mouse.js";
|
|
11
|
+
import { TEXT_START_COLUMN, inputTextWidth } from "./layout.js";
|
|
12
|
+
import { theme } from "./theme.js";
|
|
13
|
+
const GUTTER = "❯ ";
|
|
14
|
+
const SHIFT_ENTER_SEQ = "\x1b[13;2u";
|
|
15
|
+
/**
|
|
16
|
+
* True when `input` looks like the Kitty Shift+Enter sequence
|
|
17
|
+
* ("[13;2u" — the leading ESC is stripped by Ink) or a fragment of it.
|
|
18
|
+
* Fragments may arrive split across stdin chunks; they must never be
|
|
19
|
+
* appended to the input value — the raw stdin listener reassembles them.
|
|
20
|
+
*/
|
|
21
|
+
export function isKittySequenceFragment(input) {
|
|
22
|
+
return (/^\[\d{1,2}(;\d{0,2})?u?$/.test(input) || // prefix or complete "[13;2u"
|
|
23
|
+
/^\d{1,2};\d{1,2}u$/.test(input) // suffix after a split ("3;2u")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Accumulate raw stdin bytes and detect the complete Kitty Shift+Enter
|
|
28
|
+
* sequence, which may span multiple chunks. Non-sequence bytes are dropped;
|
|
29
|
+
* the trailing partial prefix (from the last ESC) is kept for the next chunk.
|
|
30
|
+
*/
|
|
31
|
+
export function accumulateKittyInput(buffer, chunk) {
|
|
32
|
+
let buf = buffer + chunk;
|
|
33
|
+
let shiftEnter = false;
|
|
34
|
+
let idx;
|
|
35
|
+
while ((idx = buf.indexOf(SHIFT_ENTER_SEQ)) !== -1) {
|
|
36
|
+
buf = buf.slice(idx + SHIFT_ENTER_SEQ.length);
|
|
37
|
+
shiftEnter = true;
|
|
38
|
+
}
|
|
39
|
+
const escIdx = buf.lastIndexOf("\x1b");
|
|
40
|
+
return { buffer: escIdx === -1 ? "" : buf.slice(escIdx), shiftEnter };
|
|
41
|
+
}
|
|
4
42
|
/**
|
|
5
43
|
* Multi-line input bar.
|
|
6
44
|
* - Enter: submit
|
|
7
45
|
* - Shift+Enter (Kitty protocol terminals): new line
|
|
8
46
|
* - Ctrl+J: new line (universal fallback)
|
|
9
47
|
* - Backslash at end + Enter: continue on next line
|
|
48
|
+
*
|
|
49
|
+
* Wrapping is done here rather than by Ink so the caret's row/column is known
|
|
50
|
+
* exactly; the hardware cursor is parked on that cell so IME composition
|
|
51
|
+
* (Chinese, Japanese, Korean) shows up in the right place.
|
|
10
52
|
*/
|
|
11
|
-
export function InputBar({ onSubmit, disabled, placeholder }) {
|
|
12
|
-
const [
|
|
13
|
-
const
|
|
53
|
+
export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, }) {
|
|
54
|
+
const [editing, setEditing] = useState({ value: "", caretIndex: 0 });
|
|
55
|
+
const { value, caretIndex } = editing;
|
|
56
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
57
|
+
const [menuDismissed, setMenuDismissed] = useState(false);
|
|
14
58
|
const { stdin } = useStdin();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
59
|
+
const { stdout } = useStdout();
|
|
60
|
+
const kittyBuffer = useRef("");
|
|
61
|
+
const mouseBuffer = useRef("");
|
|
62
|
+
const deleteBuffer = useRef("");
|
|
63
|
+
const homeEndBuffer = useRef("");
|
|
64
|
+
const width = columns || 80;
|
|
65
|
+
const [historyState, setHistoryState] = useState(() => createHistory(loadInputHistory()));
|
|
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)
|
|
18
72
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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]);
|
|
92
|
+
// The ref mirrors `editing` synchronously, so edits from separate stdin
|
|
93
|
+
// listeners (Ink's useInput for text, the raw listener for delete keys)
|
|
94
|
+
// always apply to the newest state, in arrival order, even when React has
|
|
95
|
+
// not re-rendered yet (fast key repeats, text + delete in one chunk).
|
|
96
|
+
const editingRef = useRef(editing);
|
|
97
|
+
const applyEdit = useCallback((edit) => {
|
|
98
|
+
clearExitHint();
|
|
99
|
+
const after = edit(editingRef.current.value, editingRef.current.caretIndex);
|
|
100
|
+
editingRef.current = after;
|
|
101
|
+
setEditing(after);
|
|
102
|
+
const nextHistory = resetHistory(historyRef.current, after.value);
|
|
103
|
+
if (nextHistory !== historyRef.current) {
|
|
104
|
+
historyRef.current = nextHistory;
|
|
105
|
+
setHistoryState(nextHistory);
|
|
106
|
+
}
|
|
107
|
+
}, [clearExitHint]);
|
|
108
|
+
const submit = (text) => {
|
|
109
|
+
historyRef.current = pushInput(historyRef.current, text);
|
|
110
|
+
setHistoryState(historyRef.current);
|
|
111
|
+
saveInputHistory(historyRef.current.entries);
|
|
112
|
+
onSubmit(text);
|
|
113
|
+
};
|
|
114
|
+
const browseTo = (direction) => {
|
|
115
|
+
const { text, state } = browseHistory(historyRef.current, direction, isKnownSlashCommandInput);
|
|
116
|
+
historyRef.current = state;
|
|
117
|
+
setHistoryState(state);
|
|
118
|
+
if (text !== editingRef.current.value) {
|
|
119
|
+
const after = { value: text, caretIndex: Array.from(text).length };
|
|
120
|
+
editingRef.current = after;
|
|
121
|
+
setEditing(after);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// Release the caret when the input bar goes away (e.g. confirm prompt).
|
|
125
|
+
useEffect(() => () => {
|
|
126
|
+
setCaretPosition(null);
|
|
127
|
+
}, []);
|
|
128
|
+
// Slash command menu — closes once a space follows the command name so the
|
|
129
|
+
// argument menu can take over immediately.
|
|
130
|
+
const matches = useMemo(() => filterSlashCommands(value), [value]);
|
|
131
|
+
const menuOpen = value.startsWith("/") && !/\s/.test(value.slice(1)) && !menuDismissed && matches.length > 0;
|
|
132
|
+
const shownIndex = menuOpen ? Math.min(menuIndex, matches.length - 1) : 0;
|
|
133
|
+
const selected = menuOpen ? matches[shownIndex] : undefined;
|
|
134
|
+
// Argument completion (e.g. `/skills disable <name>`), fed by app-registered providers
|
|
135
|
+
const argOptions = useMemo(() => (menuOpen ? [] : getSlashArgOptions(value)), [value, menuOpen]);
|
|
136
|
+
const argMenuOpen = !menuOpen && !menuDismissed && argOptions.length > 0;
|
|
137
|
+
const argIndex = argMenuOpen ? Math.min(menuIndex, argOptions.length - 1) : 0;
|
|
138
|
+
const argSelected = argMenuOpen ? argOptions[argIndex] : undefined;
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setMenuIndex(0);
|
|
141
|
+
}, []);
|
|
142
|
+
const acceptCommand = (cmd) => {
|
|
143
|
+
if (cmd.argHint) {
|
|
144
|
+
applyEdit(() => ({ value: `/${cmd.name} `, caretIndex: cmd.name.length + 2 }));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
submit(`/${cmd.name}`);
|
|
148
|
+
applyEdit(() => ({ value: "", caretIndex: 0 }));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const acceptArg = (option) => {
|
|
152
|
+
applyEdit((v) => {
|
|
153
|
+
const next = applySlashArgCompletion(v, option.value);
|
|
154
|
+
return { value: next, caretIndex: Array.from(next).length };
|
|
155
|
+
});
|
|
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;
|
|
30
169
|
useInput((input, key) => {
|
|
31
170
|
if (disabled)
|
|
32
171
|
return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
198
|
+
// Fast typing or paste can deliver "text\r" as a single chunk — treat a
|
|
199
|
+
// trailing Enter as Enter and strip it from the value.
|
|
200
|
+
const cleanInput = input.replace(/[\r\n]+$/, "");
|
|
201
|
+
const pressedEnter = key.return || /[\r\n]+$/.test(input);
|
|
202
|
+
// Esc closes the slash menu; while browsing history it returns to the draft
|
|
203
|
+
if (key.escape) {
|
|
204
|
+
if (menuOpen || argMenuOpen)
|
|
205
|
+
setMenuDismissed(true);
|
|
206
|
+
else if (historyRef.current.index !== -1) {
|
|
207
|
+
const next = resetHistory(historyRef.current, historyRef.current.draft);
|
|
208
|
+
historyRef.current = next;
|
|
209
|
+
setHistoryState(next);
|
|
210
|
+
if (next.draft !== editingRef.current.value) {
|
|
211
|
+
const after = { value: next.draft, caretIndex: Array.from(next.draft).length };
|
|
212
|
+
editingRef.current = after;
|
|
213
|
+
setEditing(after);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Any other key re-opens the menu
|
|
219
|
+
setMenuDismissed(false);
|
|
220
|
+
// Slash menu: Tab/Shift+Tab pick, Enter runs. ↑↓ always recall sent input.
|
|
221
|
+
if (menuOpen) {
|
|
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));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (pressedEnter) {
|
|
231
|
+
if (selected)
|
|
232
|
+
acceptCommand(selected);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (key.leftArrow || key.rightArrow)
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
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.
|
|
240
|
+
if (argMenuOpen) {
|
|
241
|
+
if (key.tab) {
|
|
242
|
+
if (argSelected)
|
|
243
|
+
acceptArg(argSelected);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
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) }));
|
|
36
254
|
return;
|
|
37
255
|
}
|
|
38
|
-
//
|
|
39
|
-
if (key.
|
|
40
|
-
|
|
256
|
+
// Move one visual row first; only browse history when already at the edge.
|
|
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
|
+
}
|
|
264
|
+
browseTo(1);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
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
|
+
}
|
|
274
|
+
browseTo(-1);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Ctrl+J: insert newline. The terminal sends LF (0x0A) for Ctrl+J and CR
|
|
278
|
+
// (0x0D) for Enter; Ink parses both as "enter", but key.return is only
|
|
279
|
+
// true for the CR case, so a lone "\n" without key.return is Ctrl+J.
|
|
280
|
+
if (input === "\n" && !key.return) {
|
|
281
|
+
applyEdit((v, c) => insertAt(v, c, "\n"));
|
|
41
282
|
return;
|
|
42
283
|
}
|
|
43
284
|
// Enter: submit or continue if ends with backslash
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
285
|
+
if (pressedEnter) {
|
|
286
|
+
const v = editingRef.current.value;
|
|
287
|
+
if (v.endsWith("\\")) {
|
|
288
|
+
applyEdit((cv, cc) => insertAt(cv.slice(0, -1), cc, "\n"));
|
|
47
289
|
return;
|
|
48
290
|
}
|
|
49
|
-
const text =
|
|
50
|
-
if (text)
|
|
51
|
-
|
|
52
|
-
|
|
291
|
+
const text = v.trim();
|
|
292
|
+
if (text)
|
|
293
|
+
submit(text);
|
|
294
|
+
applyEdit(() => ({ value: "", caretIndex: 0 }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Left / right move the caret through the text
|
|
298
|
+
if (key.leftArrow) {
|
|
299
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveCaretHorizontal(v, c, -1) }));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (key.rightArrow) {
|
|
303
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveCaretHorizontal(v, c, 1) }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Backspace (\b, Ctrl+H) — remove the code point before the caret. Most
|
|
307
|
+
// terminals send DEL (0x7F) for the Backspace key and CSI 3 ~ for Delete;
|
|
308
|
+
// Ink reports both of those as key.delete (not key.backspace), so they
|
|
309
|
+
// never reach this branch and are handled by the raw stdin listener,
|
|
310
|
+
// which can tell them apart.
|
|
311
|
+
if (key.backspace) {
|
|
312
|
+
applyEdit((v, c) => backspaceAt(v, c));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (key.escape || key.pageUp || key.pageDown)
|
|
316
|
+
return;
|
|
317
|
+
// Ctrl+R: history search by current input
|
|
318
|
+
if (key.ctrl && input === "r") {
|
|
319
|
+
const hit = searchHistory(historyRef.current, editingRef.current.value);
|
|
320
|
+
if (hit) {
|
|
321
|
+
const after = { value: hit, caretIndex: Array.from(hit).length };
|
|
322
|
+
editingRef.current = after;
|
|
323
|
+
setEditing(after);
|
|
53
324
|
}
|
|
54
325
|
return;
|
|
55
326
|
}
|
|
56
|
-
//
|
|
57
|
-
if (key.
|
|
58
|
-
|
|
327
|
+
// Readline keys
|
|
328
|
+
if (key.ctrl && input === "a") {
|
|
329
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveToLineStart(v, c) }));
|
|
59
330
|
return;
|
|
60
331
|
}
|
|
61
|
-
|
|
62
|
-
|
|
332
|
+
if (key.ctrl && input === "e") {
|
|
333
|
+
applyEdit((v, c) => ({ value: v, caretIndex: moveToLineEnd(v, c) }));
|
|
63
334
|
return;
|
|
64
|
-
|
|
335
|
+
}
|
|
336
|
+
if (key.ctrl && input === "k") {
|
|
337
|
+
applyEdit((v, c) => deleteToLineEnd(v, c));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (key.ctrl && input === "u") {
|
|
341
|
+
applyEdit((v, c) => deleteToLineStart(v, c));
|
|
65
342
|
return;
|
|
343
|
+
}
|
|
344
|
+
if (key.ctrl && input === "w") {
|
|
345
|
+
applyEdit((v, c) => deleteWordBefore(v, c));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (key.meta && input === "b") {
|
|
349
|
+
applyEdit((v, c) => ({ value: v, caretIndex: Math.max(0, c - 1) }));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (key.meta && input === "f") {
|
|
353
|
+
applyEdit((v, c) => ({ value: v, caretIndex: Math.min(Array.from(v).length, c + 1) }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
66
356
|
if (key.ctrl || key.meta)
|
|
67
357
|
return;
|
|
358
|
+
// Kitty Shift+Enter sequence (or a fragment of it) — handled by the raw
|
|
359
|
+
// stdin listener, never as text
|
|
360
|
+
if (isKittySequenceFragment(input))
|
|
361
|
+
return;
|
|
362
|
+
if (/^\[<.*[Mm]$/.test(input) || input.startsWith("[<") || /^[\d;,]+[Mm]$/.test(input))
|
|
363
|
+
return;
|
|
364
|
+
// Bracketed paste: ESC[200~ ... ESC[200~ — Ink may deliver without ESC, treat as bulk insert
|
|
365
|
+
if (input.includes("\x1b[200~") || input.includes("[200~")) {
|
|
366
|
+
const cleaned = input.replace(/\x1b?\[200~|\x1b?\[201~/g, "");
|
|
367
|
+
if (cleaned)
|
|
368
|
+
applyEdit((v, c) => insertAt(v, c, cleaned));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
68
371
|
// Tab → 2 spaces
|
|
69
372
|
if (key.tab) {
|
|
70
|
-
|
|
373
|
+
applyEdit((v, c) => insertAt(v, c, " "));
|
|
71
374
|
return;
|
|
72
375
|
}
|
|
73
376
|
// Normal character input (including CJK)
|
|
74
377
|
if (input) {
|
|
75
|
-
|
|
378
|
+
applyEdit((v, c) => insertAt(v, c, cleanInput));
|
|
76
379
|
}
|
|
77
380
|
}, { isActive: !disabled });
|
|
78
381
|
const isEmpty = value === "";
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
382
|
+
const shownPlaceholder = exitHint ? "再按一次 Ctrl+C 退出" : placeholder;
|
|
383
|
+
const logicalLines = value.split("\n");
|
|
384
|
+
const isMultiline = logicalLines.length > 1;
|
|
385
|
+
const borderColor = disabled ? theme.inputBorderDisabled : theme.inputBorder;
|
|
386
|
+
const textWidth = inputTextWidth(width);
|
|
387
|
+
// Visual rows of the value, plus the caret position inside them.
|
|
388
|
+
const rows = visualRows(value, textWidth);
|
|
389
|
+
const clampedCaret = Math.min(caretIndex, Array.from(value).length);
|
|
390
|
+
// Locate the caret's visual row and its character offset inside that row.
|
|
391
|
+
let caretRowIndex = rows.length - 1;
|
|
392
|
+
let caretCharOffset = Array.from(rows[caretRowIndex].text).length;
|
|
393
|
+
for (let i = 0; i < rows.length; i++) {
|
|
394
|
+
const row = rows[i];
|
|
395
|
+
const end = row.startIndex + Array.from(row.text).length;
|
|
396
|
+
if (clampedCaret >= row.startIndex && clampedCaret <= end) {
|
|
397
|
+
caretRowIndex = i;
|
|
398
|
+
caretCharOffset = clampedCaret - row.startIndex;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// A caret at the end of a full row has no cell left — move it to the start
|
|
403
|
+
// of the next visual row (pushing an empty one when it is the last row).
|
|
404
|
+
let caretColumnOffset = displayWidth(Array.from(rows[caretRowIndex].text).slice(0, caretCharOffset).join(""));
|
|
405
|
+
if (caretColumnOffset >= textWidth) {
|
|
406
|
+
if (caretRowIndex === rows.length - 1) {
|
|
407
|
+
rows.push({
|
|
408
|
+
text: "",
|
|
409
|
+
startIndex: rows[caretRowIndex].startIndex + Array.from(rows[caretRowIndex].text).length,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
caretRowIndex++;
|
|
413
|
+
caretCharOffset = 0;
|
|
414
|
+
caretColumnOffset = 0;
|
|
415
|
+
}
|
|
416
|
+
// The caret may sit on any content row; the only thing below it is the
|
|
417
|
+
// (single-row) multi-line hint — plus the slash menu when it floats below
|
|
418
|
+
// the input bar.
|
|
419
|
+
const hintRows = isMultiline ? 1 : 0;
|
|
420
|
+
const argShown = argMenuOpen ? Math.min(8, argOptions.length) : 0;
|
|
421
|
+
const menuRows = menuOpen ? matches.length + 5 : argMenuOpen ? argShown + 4 : 0;
|
|
422
|
+
// Input bar sits at the bottom of App's footer: its content area starts
|
|
423
|
+
// `terminalRows` rows up minus the bar's own height (menu + bottom border +
|
|
424
|
+
// hint above it in screen order, from the bottom of the terminal up).
|
|
425
|
+
const inputTopRow = terminalRows - rows.length - hintRows - menuRows;
|
|
426
|
+
// Latest geometry for the raw stdin listener, which subscribes only once
|
|
427
|
+
// (independent of re-renders) and needs these when a click arrives.
|
|
428
|
+
const rowsRef = useRef(rows);
|
|
429
|
+
rowsRef.current = rows;
|
|
430
|
+
const inputTopRowRef = useRef(inputTopRow);
|
|
431
|
+
inputTopRowRef.current = inputTopRow;
|
|
432
|
+
const hintRowsRef = useRef(hintRows);
|
|
433
|
+
hintRowsRef.current = hintRows;
|
|
434
|
+
// Listen for Kitty protocol Shift+Enter: ESC[13;2u, which may arrive split
|
|
435
|
+
// across stdin chunks — accumulate and reassemble here. Ink's useInput gets
|
|
436
|
+
// each raw chunk and would append fragments to the value, so isKittySequenceFragment
|
|
437
|
+
// below filters them out. Backspace (DEL 0x7F) and Delete (CSI 3 ~ and
|
|
438
|
+
// variants) are also picked out of the same raw stream, since Ink collapses
|
|
439
|
+
// them into one key.delete. SGR mouse click events arrive on the same raw
|
|
440
|
+
// stream; parse them and move the caret to the clicked cell. The listener
|
|
441
|
+
// subscribes once; every edit goes through applyEdit, which reads the
|
|
442
|
+
// synchronous editingRef, so fast key repeats never operate on stale text.
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
if (!stdin || disabled)
|
|
445
|
+
return;
|
|
446
|
+
const handleData = (data) => {
|
|
447
|
+
const chunk = data.toString("utf-8");
|
|
448
|
+
// Kitty Shift+Enter accumulation
|
|
449
|
+
const { buffer: kittyBuf, shiftEnter } = accumulateKittyInput(kittyBuffer.current, chunk);
|
|
450
|
+
kittyBuffer.current = kittyBuf;
|
|
451
|
+
if (shiftEnter)
|
|
452
|
+
applyEdit((v, c) => insertAt(v, c, "\n"));
|
|
453
|
+
// Backspace (DEL 0x7F) and Delete (CSI 3 ~ / variants): Ink reports
|
|
454
|
+
// them all as key.delete, so they are distinguished and applied here.
|
|
455
|
+
// The buffer handles a sequence split across chunks.
|
|
456
|
+
const { buffer: delBuf, actions } = scanDeleteKeys(deleteBuffer.current, chunk);
|
|
457
|
+
deleteBuffer.current = delBuf;
|
|
458
|
+
if (actions.length > 0) {
|
|
459
|
+
for (const action of actions) {
|
|
460
|
+
applyEdit((v, c) => (action.kind === "backspace" ? backspaceAt(v, c) : deleteAt(v, c)));
|
|
461
|
+
}
|
|
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
|
+
}
|
|
473
|
+
mouseBuffer.current += chunk;
|
|
474
|
+
let sgr;
|
|
475
|
+
while ((sgr = scanSgrMouse(mouseBuffer.current))) {
|
|
476
|
+
mouseBuffer.current = sgr.rest;
|
|
477
|
+
if (sgr.event.button === 0 && sgr.event.press) {
|
|
478
|
+
const contentRow = sgr.event.row - inputTopRowRef.current;
|
|
479
|
+
if (contentRow >= 0 && contentRow < rowsRef.current.length) {
|
|
480
|
+
const evCol = sgr.event.col;
|
|
481
|
+
applyEdit((v) => ({
|
|
482
|
+
value: v,
|
|
483
|
+
caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN),
|
|
484
|
+
}));
|
|
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
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (mouseBuffer.current.length > 64) {
|
|
505
|
+
const esc = mouseBuffer.current.lastIndexOf("\x1b");
|
|
506
|
+
mouseBuffer.current = esc === -1 ? "" : mouseBuffer.current.slice(esc);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
stdin.on("data", handleData);
|
|
510
|
+
return () => {
|
|
511
|
+
stdin.off("data", handleData);
|
|
512
|
+
};
|
|
513
|
+
}, [stdin, disabled, applyEdit]);
|
|
514
|
+
// Enable the SGR mouse protocol so clicks on the input bar can position the
|
|
515
|
+
// caret. Events arrive on raw stdin; parseSgrMouse picks them out of the
|
|
516
|
+
// same chunk stream as the Kitty sequence. Terminals without mouse support
|
|
517
|
+
// simply never send events — keyboard navigation still works.
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
stdout.write(MOUSE_ENABLE);
|
|
520
|
+
return () => {
|
|
521
|
+
stdout.write(MOUSE_DISABLE);
|
|
522
|
+
};
|
|
523
|
+
}, [stdout]);
|
|
524
|
+
// Must publish during render (not in effect) — Ink writes the frame in
|
|
525
|
+
// resetAfterCommit before effects run. If delayed to useEffect, the
|
|
526
|
+
// hardware cursor (IME preedit anchor) lags by one frame and appears
|
|
527
|
+
// displaced while composing CJK.
|
|
528
|
+
{
|
|
529
|
+
const rowsBelowCaret = rows.length - 1 - caretRowIndex;
|
|
530
|
+
setCaretPosition({
|
|
531
|
+
rowsAbove: rowsBelowCaret + 1 /* bottom border */ + hintRows + menuRows,
|
|
532
|
+
column: TEXT_START_COLUMN + caretColumnOffset,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
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) => {
|
|
536
|
+
const chars = Array.from(row.text);
|
|
537
|
+
const caretHere = i === caretRowIndex;
|
|
538
|
+
const offset = caretHere ? caretCharOffset : chars.length;
|
|
539
|
+
const before = chars.slice(0, offset).join("");
|
|
540
|
+
const after = chars.slice(offset).join("");
|
|
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));
|
|
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"] }) })] }))] }));
|
|
83
543
|
}
|