min-agent 0.5.1 → 0.6.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.
Files changed (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,618 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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, sessionSlashCommands, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, slashMenuWindow, } from "./slash-commands.js";
9
- import { getActiveProvider, getEffectiveConfig } from "../config.js";
10
- import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
11
- import { scanSgrMouse } from "./mouse.js";
12
- import { TEXT_START_COLUMN, inputTextWidth, inputBarPaintRows, slashMenuMaxVisible, SLASH_COMMAND_MENU_CHROME, SLASH_ARG_MENU_CHROME, INPUT_BAR_ROWS, } from "./layout.js";
13
- import { theme } from "./theme.js";
14
- import { getClipboardImage } from "../clipboard.js";
15
- import { BRACKETED_PASTE_DISABLE, BRACKETED_PASTE_ENABLE, scanBracketedPaste, shouldIgnorePasteInput, } from "./bracketed-paste.js";
16
- import { applyClipboardImage, applyPastedText, buildSubmittedPrompt, chipClusterWidth, createPasteDraft, paintChar, pruneAttachments, resetPasteDraft, } from "./paste-draft.js";
17
- const GUTTER = "❯ ";
18
- const SHIFT_ENTER_SEQ = "\x1b[13;2u";
19
- /**
20
- * True when `input` looks like the Kitty Shift+Enter sequence
21
- * ("[13;2u" — the leading ESC is stripped by Ink) or a fragment of it.
22
- * Fragments may arrive split across stdin chunks; they must never be
23
- * appended to the input value — the raw stdin listener reassembles them.
24
- */
25
- export function isKittySequenceFragment(input) {
26
- return (/^\[\d{1,2}(;\d{0,2})?u?$/.test(input) || // prefix or complete "[13;2u"
27
- /^\d{1,2};\d{1,2}u$/.test(input) // suffix after a split ("3;2u")
28
- );
29
- }
30
- /**
31
- * Accumulate raw stdin bytes and detect the complete Kitty Shift+Enter
32
- * sequence, which may span multiple chunks. Non-sequence bytes are dropped;
33
- * the trailing partial prefix (from the last ESC) is kept for the next chunk.
34
- */
35
- export function accumulateKittyInput(buffer, chunk) {
36
- let buf = buffer + chunk;
37
- let shiftEnter = false;
38
- let idx;
39
- while ((idx = buf.indexOf(SHIFT_ENTER_SEQ)) !== -1) {
40
- buf = buf.slice(idx + SHIFT_ENTER_SEQ.length);
41
- shiftEnter = true;
42
- }
43
- const escIdx = buf.lastIndexOf("\x1b");
44
- return { buffer: escIdx === -1 ? "" : buf.slice(escIdx), shiftEnter };
45
- }
46
- /**
47
- * Multi-line input bar.
48
- * - Enter: submit
49
- * - Shift+Enter (Kitty protocol terminals): new line
50
- * - Ctrl+J: new line (universal fallback)
51
- * - Backslash at end + Enter: continue on next line
52
- *
53
- * Wrapping is done here rather than by Ink so the caret's row/column is known
54
- * exactly; the hardware cursor is parked on that cell so IME composition
55
- * (Chinese, Japanese, Korean) shows up in the right place.
56
- */
57
- export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, onRowsChange, onNotice, }) {
58
- const [editing, setEditing] = useState({ value: "", caretIndex: 0 });
59
- const { value, caretIndex } = editing;
60
- const [menuIndex, setMenuIndex] = useState(0);
61
- const [menuDismissed, setMenuDismissed] = useState(false);
62
- const { stdin } = useStdin();
63
- const { stdout } = useStdout();
64
- const kittyBuffer = useRef("");
65
- const mouseBuffer = useRef("");
66
- const deleteBuffer = useRef("");
67
- const homeEndBuffer = useRef("");
68
- const pasteBuffer = useRef("");
69
- const pasteActiveRef = useRef(false);
70
- const draftRef = useRef(createPasteDraft());
71
- const onNoticeRef = useRef(onNotice);
72
- onNoticeRef.current = onNotice;
73
- const width = columns || 80;
74
- const [historyState, setHistoryState] = useState(() => createHistory(loadInputHistory()));
75
- const historyRef = useRef(historyState);
76
- const [exitHint, setExitHint] = useState(false);
77
- const exitArmedAtRef = useRef(null);
78
- const exitHintTimer = useRef(null);
79
- const clearExitHint = useCallback(() => {
80
- if (exitArmedAtRef.current === null && exitHintTimer.current === null)
81
- return;
82
- if (exitHintTimer.current !== null) {
83
- clearTimeout(exitHintTimer.current);
84
- exitHintTimer.current = null;
85
- }
86
- exitArmedAtRef.current = null;
87
- setExitHint(false);
88
- }, []);
89
- const showExitHint = useCallback((armedAt) => {
90
- exitArmedAtRef.current = armedAt;
91
- setExitHint(true);
92
- if (exitHintTimer.current !== null)
93
- clearTimeout(exitHintTimer.current);
94
- exitHintTimer.current = setTimeout(() => {
95
- exitHintTimer.current = null;
96
- exitArmedAtRef.current = null;
97
- setExitHint(false);
98
- }, EXIT_CTRL_C_WINDOW_MS);
99
- }, []);
100
- useEffect(() => () => clearExitHint(), [clearExitHint]);
101
- // The ref mirrors `editing` synchronously, so edits from separate stdin
102
- // listeners (Ink's useInput for text, the raw listener for delete keys)
103
- // always apply to the newest state, in arrival order, even when React has
104
- // not re-rendered yet (fast key repeats, text + delete in one chunk).
105
- const editingRef = useRef(editing);
106
- const applyEdit = useCallback((edit) => {
107
- clearExitHint();
108
- const after = edit(editingRef.current.value, editingRef.current.caretIndex);
109
- editingRef.current = after;
110
- setEditing(after);
111
- draftRef.current = pruneAttachments(draftRef.current, after.value);
112
- const nextHistory = resetHistory(historyRef.current, after.value);
113
- if (nextHistory !== historyRef.current) {
114
- historyRef.current = nextHistory;
115
- setHistoryState(nextHistory);
116
- }
117
- }, [clearExitHint]);
118
- const submit = (prompt) => {
119
- historyRef.current = pushInput(historyRef.current, prompt.display);
120
- setHistoryState(historyRef.current);
121
- saveInputHistory(historyRef.current.entries);
122
- onSubmit(prompt);
123
- };
124
- const applyIncomingPaste = useCallback((raw) => {
125
- const result = applyPastedText(draftRef.current, raw);
126
- if (result.kind === "text") {
127
- if (result.text)
128
- applyEdit((v, c) => insertAt(v, c, result.text));
129
- return;
130
- }
131
- if (result.kind === "snippet") {
132
- draftRef.current = result.draft;
133
- applyEdit((v, c) => insertAt(v, c, result.char));
134
- return;
135
- }
136
- onNoticeRef.current?.(result.notice);
137
- }, [applyEdit]);
138
- const browseTo = (direction) => {
139
- const { text, state } = browseHistory(historyRef.current, direction, isKnownSlashCommandInput);
140
- historyRef.current = state;
141
- setHistoryState(state);
142
- if (text !== editingRef.current.value) {
143
- const after = { value: text, caretIndex: Array.from(text).length };
144
- editingRef.current = after;
145
- setEditing(after);
146
- }
147
- };
148
- // Release the caret when the input bar goes away (e.g. confirm prompt).
149
- useEffect(() => () => {
150
- setCaretPosition(null);
151
- }, []);
152
- // Slash command menu — closes once a space follows the command name so the
153
- // argument menu can take over immediately.
154
- const providerType = getActiveProvider(getEffectiveConfig())?.type;
155
- const matches = useMemo(() => filterSlashCommands(value, sessionSlashCommands(providerType)), [value, providerType]);
156
- const menuOpen = value.startsWith("/") && !/\s/.test(value.slice(1)) && !menuDismissed && matches.length > 0;
157
- const shownIndex = menuOpen ? Math.min(menuIndex, matches.length - 1) : 0;
158
- const selected = menuOpen ? matches[shownIndex] : undefined;
159
- // Argument completion (e.g. `/skills disable <name>`), fed by app-registered providers
160
- const argOptions = useMemo(() => (menuOpen ? [] : getSlashArgOptions(value)), [value, menuOpen]);
161
- const argMenuOpen = !menuOpen && !menuDismissed && argOptions.length > 0;
162
- const argIndex = argMenuOpen ? Math.min(menuIndex, argOptions.length - 1) : 0;
163
- const argSelected = argMenuOpen ? argOptions[argIndex] : undefined;
164
- useEffect(() => {
165
- setMenuIndex(0);
166
- }, []);
167
- const acceptCommand = (cmd) => {
168
- if (cmd.argHint) {
169
- applyEdit(() => ({ value: `/${cmd.name} `, caretIndex: cmd.name.length + 2 }));
170
- }
171
- else {
172
- submit({ display: `/${cmd.name}`, content: `/${cmd.name}` });
173
- draftRef.current = resetPasteDraft();
174
- applyEdit(() => ({ value: "", caretIndex: 0 }));
175
- }
176
- };
177
- const acceptArg = (option) => {
178
- applyEdit((v) => {
179
- const next = applySlashArgCompletion(v, option.value);
180
- return { value: next, caretIndex: Array.from(next).length };
181
- });
182
- };
183
- const acceptCommandRef = useRef(acceptCommand);
184
- acceptCommandRef.current = acceptCommand;
185
- const acceptArgRef = useRef(acceptArg);
186
- acceptArgRef.current = acceptArg;
187
- const menuOpenRef = useRef(menuOpen);
188
- menuOpenRef.current = menuOpen;
189
- const argMenuOpenRef = useRef(argMenuOpen);
190
- argMenuOpenRef.current = argMenuOpen;
191
- const applyIncomingPasteRef = useRef(applyIncomingPaste);
192
- applyIncomingPasteRef.current = applyIncomingPaste;
193
- useInput((input, key) => {
194
- if (disabled)
195
- return;
196
- if (isCtrlC(input, key)) {
197
- const current = editingRef.current.value;
198
- const result = clearInputOnCtrlC(historyRef.current, current, exitArmedAtRef.current, Date.now());
199
- if (result.exit) {
200
- clearExitHint();
201
- onExit();
202
- return;
203
- }
204
- if (result.state.entries !== historyRef.current.entries) {
205
- saveInputHistory(result.state.entries);
206
- }
207
- historyRef.current = result.state;
208
- setHistoryState(result.state);
209
- if (result.armedAt !== null) {
210
- showExitHint(result.armedAt);
211
- return;
212
- }
213
- clearExitHint();
214
- draftRef.current = resetPasteDraft();
215
- applyEdit(() => ({ value: "", caretIndex: 0 }));
216
- return;
217
- }
218
- clearExitHint();
219
- if (key.ctrl && input === "b") {
220
- onSelectionMode?.();
221
- return;
222
- }
223
- if (key.ctrl && input === "v") {
224
- const result = applyClipboardImage(draftRef.current, getClipboardImage());
225
- if (result.kind === "image") {
226
- draftRef.current = result.draft;
227
- applyEdit((v, c) => insertAt(v, c, result.char));
228
- }
229
- else if (result.kind === "reject") {
230
- onNoticeRef.current?.(result.notice);
231
- }
232
- return;
233
- }
234
- if (shouldIgnorePasteInput(input, pasteActiveRef.current))
235
- return;
236
- // Fast typing or paste can deliver "text\r" as a single chunk — treat a
237
- // trailing Enter as Enter and strip it from the value.
238
- const cleanInput = input.replace(/[\r\n]+$/, "");
239
- const pressedEnter = key.return || /[\r\n]+$/.test(input);
240
- // Esc closes the slash menu; while browsing history it returns to the draft
241
- if (key.escape) {
242
- if (menuOpen || argMenuOpen)
243
- setMenuDismissed(true);
244
- else if (historyRef.current.index !== -1) {
245
- const next = resetHistory(historyRef.current, historyRef.current.draft);
246
- historyRef.current = next;
247
- setHistoryState(next);
248
- if (next.draft !== editingRef.current.value) {
249
- const after = { value: next.draft, caretIndex: Array.from(next.draft).length };
250
- editingRef.current = after;
251
- setEditing(after);
252
- }
253
- }
254
- return;
255
- }
256
- // Any other key re-opens the menu
257
- setMenuDismissed(false);
258
- // Slash menu: Tab/Shift+Tab pick, Enter runs. ↑↓ always recall sent input.
259
- if (menuOpen) {
260
- if (key.tab) {
261
- if (matches.length === 1 && selected) {
262
- acceptCommand(selected);
263
- return;
264
- }
265
- setMenuIndex((i) => (key.shift ? (i - 1 + matches.length) % matches.length : (i + 1) % matches.length));
266
- return;
267
- }
268
- if (pressedEnter) {
269
- if (selected)
270
- acceptCommand(selected);
271
- return;
272
- }
273
- if (key.leftArrow || key.rightArrow)
274
- return;
275
- }
276
- // Argument menu: Tab completes the highlight. Enter still submits the line
277
- // so a fully typed command is never swallowed by the suggestion list.
278
- if (argMenuOpen) {
279
- if (key.tab) {
280
- if (argSelected)
281
- acceptArg(argSelected);
282
- return;
283
- }
284
- }
285
- const inkKey = key;
286
- if (inkKey.home) {
287
- applyEdit((v, c) => ({ value: v, caretIndex: moveToLineStart(v, c) }));
288
- return;
289
- }
290
- if (inkKey.end) {
291
- applyEdit((v, c) => ({ value: v, caretIndex: moveToLineEnd(v, c) }));
292
- return;
293
- }
294
- // Move one visual row first; only browse history when already at the edge.
295
- if (key.upArrow) {
296
- const { value: v, caretIndex: c } = editingRef.current;
297
- const maxW = inputTextWidth(columns);
298
- const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
299
- const next = moveCaretVertical(v, c, -1, maxW, lookup);
300
- if (next !== c) {
301
- applyEdit(() => ({ value: v, caretIndex: next }));
302
- return;
303
- }
304
- browseTo(1);
305
- return;
306
- }
307
- if (key.downArrow) {
308
- const { value: v, caretIndex: c } = editingRef.current;
309
- const maxW = inputTextWidth(columns);
310
- const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
311
- const next = moveCaretVertical(v, c, 1, maxW, lookup);
312
- if (next !== c) {
313
- applyEdit(() => ({ value: v, caretIndex: next }));
314
- return;
315
- }
316
- browseTo(-1);
317
- return;
318
- }
319
- // Ctrl+J: insert newline. The terminal sends LF (0x0A) for Ctrl+J and CR
320
- // (0x0D) for Enter; Ink parses both as "enter", but key.return is only
321
- // true for the CR case, so a lone "\n" without key.return is Ctrl+J.
322
- if (input === "\n" && !key.return) {
323
- applyEdit((v, c) => insertAt(v, c, "\n"));
324
- return;
325
- }
326
- // Enter: submit or continue if ends with backslash
327
- if (pressedEnter) {
328
- const v = editingRef.current.value;
329
- if (v.endsWith("\\")) {
330
- applyEdit((cv, cc) => insertAt(cv.slice(0, -1), cc, "\n"));
331
- return;
332
- }
333
- const prompt = buildSubmittedPrompt(v, draftRef.current);
334
- if (prompt.display.trim() || draftRef.current.attachments.size > 0)
335
- submit(prompt);
336
- draftRef.current = resetPasteDraft();
337
- applyEdit(() => ({ value: "", caretIndex: 0 }));
338
- return;
339
- }
340
- // Left / right move the caret through the text
341
- if (key.leftArrow) {
342
- applyEdit((v, c) => ({ value: v, caretIndex: moveCaretHorizontal(v, c, -1) }));
343
- return;
344
- }
345
- if (key.rightArrow) {
346
- applyEdit((v, c) => ({ value: v, caretIndex: moveCaretHorizontal(v, c, 1) }));
347
- return;
348
- }
349
- // Backspace (\b, Ctrl+H) — remove the code point before the caret. Most
350
- // terminals send DEL (0x7F) for the Backspace key and CSI 3 ~ for Delete;
351
- // Ink reports both of those as key.delete (not key.backspace), so they
352
- // never reach this branch and are handled by the raw stdin listener,
353
- // which can tell them apart.
354
- if (key.backspace) {
355
- applyEdit((v, c) => backspaceAt(v, c));
356
- return;
357
- }
358
- if (key.escape || key.pageUp || key.pageDown)
359
- return;
360
- // Ctrl+R: history search by current input
361
- if (key.ctrl && input === "r") {
362
- const hit = searchHistory(historyRef.current, editingRef.current.value);
363
- if (hit) {
364
- const after = { value: hit, caretIndex: Array.from(hit).length };
365
- editingRef.current = after;
366
- setEditing(after);
367
- }
368
- return;
369
- }
370
- // Readline keys
371
- if (key.ctrl && input === "a") {
372
- applyEdit((v, c) => ({ value: v, caretIndex: moveToLineStart(v, c) }));
373
- return;
374
- }
375
- if (key.ctrl && input === "e") {
376
- applyEdit((v, c) => ({ value: v, caretIndex: moveToLineEnd(v, c) }));
377
- return;
378
- }
379
- if (key.ctrl && input === "k") {
380
- applyEdit((v, c) => deleteToLineEnd(v, c));
381
- return;
382
- }
383
- if (key.ctrl && input === "u") {
384
- applyEdit((v, c) => deleteToLineStart(v, c));
385
- return;
386
- }
387
- if (key.ctrl && input === "w") {
388
- applyEdit((v, c) => deleteWordBefore(v, c));
389
- return;
390
- }
391
- if (key.meta && input === "b") {
392
- applyEdit((v, c) => ({ value: v, caretIndex: Math.max(0, c - 1) }));
393
- return;
394
- }
395
- if (key.meta && input === "f") {
396
- applyEdit((v, c) => ({ value: v, caretIndex: Math.min(Array.from(v).length, c + 1) }));
397
- return;
398
- }
399
- if (key.ctrl || key.meta)
400
- return;
401
- // Kitty Shift+Enter sequence (or a fragment of it) — handled by the raw
402
- // stdin listener, never as text
403
- if (isKittySequenceFragment(input))
404
- return;
405
- if (/^\[<.*[Mm]$/.test(input) || input.startsWith("[<") || /^[\d;,]+[Mm]$/.test(input))
406
- return;
407
- // Tab → 2 spaces
408
- if (key.tab) {
409
- applyEdit((v, c) => insertAt(v, c, " "));
410
- return;
411
- }
412
- // Normal character input (including CJK)
413
- if (input) {
414
- applyEdit((v, c) => insertAt(v, c, cleanInput));
415
- }
416
- }, { isActive: !disabled });
417
- const isEmpty = value === "";
418
- const shownPlaceholder = exitHint ? "再按一次 Ctrl+C 退出" : placeholder;
419
- const logicalLines = value.split("\n");
420
- const isMultiline = logicalLines.length > 1;
421
- const borderColor = disabled ? theme.inputBorderDisabled : theme.inputBorder;
422
- const textWidth = inputTextWidth(width);
423
- const chipLookup = (ch) => chipClusterWidth(ch, draftRef.current, textWidth);
424
- // Visual rows of the value, plus the caret position inside them.
425
- const rows = visualRows(value, textWidth, chipLookup);
426
- const clampedCaret = Math.min(caretIndex, Array.from(value).length);
427
- // Locate the caret's visual row and its character offset inside that row.
428
- let caretRowIndex = rows.length - 1;
429
- let caretCharOffset = Array.from(rows[caretRowIndex].text).length;
430
- for (let i = 0; i < rows.length; i++) {
431
- const row = rows[i];
432
- const end = row.startIndex + Array.from(row.text).length;
433
- if (clampedCaret >= row.startIndex && clampedCaret <= end) {
434
- caretRowIndex = i;
435
- caretCharOffset = clampedCaret - row.startIndex;
436
- break;
437
- }
438
- }
439
- // A caret at the end of a full row has no cell left — move it to the start
440
- // of the next visual row (pushing an empty one when it is the last row).
441
- let caretColumnOffset = displayWidth(Array.from(rows[caretRowIndex].text).slice(0, caretCharOffset).join(""), chipLookup);
442
- if (caretColumnOffset >= textWidth) {
443
- if (caretRowIndex === rows.length - 1) {
444
- rows.push({
445
- text: "",
446
- startIndex: rows[caretRowIndex].startIndex + Array.from(rows[caretRowIndex].text).length,
447
- });
448
- }
449
- caretRowIndex++;
450
- caretCharOffset = 0;
451
- caretColumnOffset = 0;
452
- }
453
- // The caret may sit on any content row; the only thing below it is the
454
- // (single-row) multi-line hint — plus the slash menu when it floats below
455
- // the input bar.
456
- const hintRows = isMultiline ? 1 : 0;
457
- const commandMax = slashMenuMaxVisible(terminalRows, rows.length, hintRows, SLASH_COMMAND_MENU_CHROME);
458
- const argMax = slashMenuMaxVisible(terminalRows, rows.length, hintRows, SLASH_ARG_MENU_CHROME);
459
- const commandWindow = menuOpen ? slashMenuWindow(matches, shownIndex, commandMax) : null;
460
- const argWindow = argMenuOpen ? slashMenuWindow(argOptions, argIndex, argMax) : null;
461
- const menuRows = commandWindow
462
- ? commandWindow.shown.length + SLASH_COMMAND_MENU_CHROME
463
- : argWindow
464
- ? argWindow.shown.length + SLASH_ARG_MENU_CHROME
465
- : 0;
466
- const paintedRows = inputBarPaintRows(rows.length, hintRows, commandWindow?.shown.length ?? 0, argWindow?.shown.length ?? 0);
467
- // Input bar sits at the bottom of App's footer: its content area starts
468
- // `terminalRows` rows up minus the bar's own height (menu + bottom border +
469
- // hint above it in screen order, from the bottom of the terminal up).
470
- const inputTopRow = terminalRows - rows.length - hintRows - menuRows;
471
- // Latest geometry for the raw stdin listener, which subscribes only once
472
- // (independent of re-renders) and needs these when a click arrives.
473
- const rowsRef = useRef(rows);
474
- rowsRef.current = rows;
475
- const inputTopRowRef = useRef(inputTopRow);
476
- inputTopRowRef.current = inputTopRow;
477
- const hintRowsRef = useRef(hintRows);
478
- hintRowsRef.current = hintRows;
479
- const commandWindowRef = useRef(commandWindow);
480
- commandWindowRef.current = commandWindow;
481
- const argWindowRef = useRef(argWindow);
482
- argWindowRef.current = argWindow;
483
- const chipLookupRef = useRef(chipLookup);
484
- chipLookupRef.current = chipLookup;
485
- useEffect(() => {
486
- onRowsChange?.(paintedRows);
487
- }, [paintedRows, onRowsChange]);
488
- useEffect(() => () => onRowsChange?.(INPUT_BAR_ROWS), [onRowsChange]);
489
- // Listen for Kitty protocol Shift+Enter: ESC[13;2u, which may arrive split
490
- // across stdin chunks — accumulate and reassemble here. Ink's useInput gets
491
- // each raw chunk and would append fragments to the value, so isKittySequenceFragment
492
- // below filters them out. Backspace (DEL 0x7F) and Delete (CSI 3 ~ and
493
- // variants) are also picked out of the same raw stream, since Ink collapses
494
- // them into one key.delete. SGR mouse click events arrive on the same raw
495
- // stream; parse them and move the caret to the clicked cell. The listener
496
- // subscribes once; every edit goes through applyEdit, which reads the
497
- // synchronous editingRef, so fast key repeats never operate on stale text.
498
- useEffect(() => {
499
- if (!stdin || disabled)
500
- return;
501
- const handleData = (data) => {
502
- const chunk = data.toString("utf-8");
503
- const pasteScan = scanBracketedPaste(pasteBuffer.current, chunk);
504
- pasteBuffer.current = pasteScan.buffer;
505
- pasteActiveRef.current = pasteScan.active;
506
- for (const raw of pasteScan.pastes)
507
- applyIncomingPasteRef.current(raw);
508
- // Kitty Shift+Enter accumulation
509
- const { buffer: kittyBuf, shiftEnter } = accumulateKittyInput(kittyBuffer.current, chunk);
510
- kittyBuffer.current = kittyBuf;
511
- if (shiftEnter)
512
- applyEdit((v, c) => insertAt(v, c, "\n"));
513
- // Backspace (DEL 0x7F) and Delete (CSI 3 ~ / variants): Ink reports
514
- // them all as key.delete, so they are distinguished and applied here.
515
- // The buffer handles a sequence split across chunks.
516
- const { buffer: delBuf, actions } = scanDeleteKeys(deleteBuffer.current, chunk);
517
- deleteBuffer.current = delBuf;
518
- if (actions.length > 0) {
519
- for (const action of actions) {
520
- applyEdit((v, c) => (action.kind === "backspace" ? backspaceAt(v, c) : deleteAt(v, c)));
521
- }
522
- }
523
- const { buffer: homeBuf, actions: homeActions } = scanHomeEndKeys(homeEndBuffer.current, chunk);
524
- homeEndBuffer.current = homeBuf;
525
- if (homeActions.length > 0) {
526
- for (const action of homeActions) {
527
- applyEdit((v, c) => ({
528
- value: v,
529
- caretIndex: action.kind === "home" ? moveToLineStart(v, c) : moveToLineEnd(v, c),
530
- }));
531
- }
532
- }
533
- mouseBuffer.current += chunk;
534
- let sgr;
535
- while ((sgr = scanSgrMouse(mouseBuffer.current))) {
536
- mouseBuffer.current = sgr.rest;
537
- if (sgr.event.button === 0 && sgr.event.press) {
538
- const contentRow = sgr.event.row - inputTopRowRef.current;
539
- if (contentRow >= 0 && contentRow < rowsRef.current.length) {
540
- const evCol = sgr.event.col;
541
- applyEdit((v) => ({
542
- value: v,
543
- caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN, chipLookupRef.current),
544
- }));
545
- }
546
- else {
547
- const chrome = 3;
548
- const header = menuOpenRef.current ? 1 : 0;
549
- const optionIndex = sgr.event.row - (inputTopRowRef.current + rowsRef.current.length + hintRowsRef.current + chrome + header);
550
- if (menuOpenRef.current) {
551
- const cmd = commandWindowRef.current?.shown[optionIndex];
552
- if (cmd)
553
- acceptCommandRef.current(cmd);
554
- }
555
- else if (argMenuOpenRef.current) {
556
- const opt = argWindowRef.current?.shown[optionIndex];
557
- if (opt)
558
- acceptArgRef.current(opt);
559
- }
560
- }
561
- }
562
- }
563
- if (mouseBuffer.current.length > 64) {
564
- const esc = mouseBuffer.current.lastIndexOf("\x1b");
565
- mouseBuffer.current = esc === -1 ? "" : mouseBuffer.current.slice(esc);
566
- }
567
- };
568
- stdin.on("data", handleData);
569
- return () => {
570
- stdin.off("data", handleData);
571
- };
572
- }, [stdin, disabled, applyEdit]);
573
- // Enable the SGR mouse protocol so clicks on the input bar can position the
574
- // caret. Events arrive on raw stdin; parseSgrMouse picks them out of the
575
- // same chunk stream as the Kitty sequence. Terminals without mouse support
576
- // simply never send events — keyboard navigation still works.
577
- useEffect(() => {
578
- stdout.write(`${BRACKETED_PASTE_ENABLE}${MOUSE_ENABLE}`);
579
- return () => {
580
- stdout.write(`${MOUSE_DISABLE}${BRACKETED_PASTE_DISABLE}`);
581
- };
582
- }, [stdout]);
583
- // Must publish during render (not in effect) — Ink writes the frame in
584
- // resetAfterCommit before effects run. If delayed to useEffect, the
585
- // hardware cursor (IME preedit anchor) lags by one frame and appears
586
- // displaced while composing CJK.
587
- {
588
- const rowsBelowCaret = rows.length - 1 - caretRowIndex;
589
- setCaretPosition({
590
- rowsAbove: rowsBelowCaret + 1 /* bottom border */ + hintRows + menuRows,
591
- column: TEXT_START_COLUMN + caretColumnOffset,
592
- });
593
- }
594
- 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) => {
595
- const chars = Array.from(row.text);
596
- const caretHere = i === caretRowIndex;
597
- const offset = caretHere ? caretCharOffset : chars.length;
598
- const before = chars
599
- .slice(0, offset)
600
- .map((c) => paintChar(c, draftRef.current, textWidth))
601
- .join("");
602
- const after = chars
603
- .slice(offset)
604
- .map((c) => paintChar(c, draftRef.current, textWidth))
605
- .join("");
606
- 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));
607
- })), 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)} 命令` }) }), commandWindow?.shown.map((c, i) => {
608
- const active = commandWindow.start + i === shownIndex;
609
- return (_jsxs(Box, { children: [_jsx(Text, { color: active ? theme.accent : theme.muted, children: active ? "❯ " : " " }), _jsxs(Text, { bold: active, color: active ? 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));
610
- }), _jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [commandWindow && commandWindow.extraAbove + commandWindow.extraBelow > 0
611
- ? `还有 ${commandWindow.extraAbove + commandWindow.extraBelow} 项 · `
612
- : "", "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: [argWindow?.shown.map((o, i) => {
613
- const active = argWindow.start + i === argIndex;
614
- return (_jsxs(Box, { children: [_jsx(Text, { color: active ? theme.accent : theme.muted, children: active ? "❯ " : " " }), _jsx(Text, { bold: active, color: active ? theme.accent : undefined, children: o.value }), o.description && (_jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [" ", o.description] }))] }, o.value));
615
- }), _jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: [argWindow && argWindow.extraAbove + argWindow.extraBelow > 0
616
- ? `还有 ${argWindow.extraAbove + argWindow.extraBelow} 项 · `
617
- : "", "Tab \u8865\u5168 \u00B7 Enter \u6267\u884C \u00B7 Esc \u5173\u95ED"] }) })] }))] }));
618
- }