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