min-agent 0.4.1 → 0.5.1

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 (69) hide show
  1. package/README.md +46 -2
  2. package/dist/agent.js +89 -29
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/ctx.js +7 -0
  5. package/dist/cli/commands/exec.js +3 -0
  6. package/dist/cli/commands/index.js +32 -7
  7. package/dist/cli/commands/memory.js +33 -15
  8. package/dist/cli/commands/setup.js +55 -3
  9. package/dist/cli/commands/shared.js +10 -1
  10. package/dist/cli/commands/think.js +12 -0
  11. package/dist/cli/commands/write-config.js +22 -0
  12. package/dist/cli/option-helpers.js +13 -1
  13. package/dist/cli/program.js +57 -14
  14. package/dist/cli/setup/detect.js +17 -0
  15. package/dist/cli/setup/flags.js +12 -0
  16. package/dist/cli/setup/flow.js +108 -0
  17. package/dist/cli/setup/provider-form.js +102 -0
  18. package/dist/cli/setup/ui.js +534 -0
  19. package/dist/code-mode.js +1 -1
  20. package/dist/config.js +93 -159
  21. package/dist/context-window.js +39 -49
  22. package/dist/ctx-cli.js +30 -0
  23. package/dist/ctx.js +80 -0
  24. package/dist/memory-cli.js +33 -0
  25. package/dist/memory.js +127 -46
  26. package/dist/model-catalog.js +285 -0
  27. package/dist/ollama-model.js +234 -0
  28. package/dist/ollama-openai-bridge.js +383 -0
  29. package/dist/permission-cli.js +1 -4
  30. package/dist/provider.js +4 -1
  31. package/dist/reasoning-stream.js +158 -0
  32. package/dist/sandbox-cli.js +1 -4
  33. package/dist/scope.js +23 -0
  34. package/dist/serve/common.js +22 -1
  35. package/dist/serve/routes-chat.js +21 -1
  36. package/dist/serve/routes-memory.js +31 -2
  37. package/dist/serve/routes-meta.js +69 -6
  38. package/dist/think-cli.js +36 -0
  39. package/dist/thinking-wire.js +239 -0
  40. package/dist/thinking.js +166 -0
  41. package/dist/token-display.js +10 -7
  42. package/dist/tools/todo.js +22 -8
  43. package/dist/tui/App.js +48 -8
  44. package/dist/tui/CtxPicker.js +68 -0
  45. package/dist/tui/InputBar.js +112 -37
  46. package/dist/tui/MessageList.js +53 -22
  47. package/dist/tui/StatusBar.js +7 -3
  48. package/dist/tui/ThinkPicker.js +75 -0
  49. package/dist/tui/bracketed-paste.js +37 -0
  50. package/dist/tui/caret-pos.js +10 -8
  51. package/dist/tui/index.js +13 -1
  52. package/dist/tui/layout.js +17 -0
  53. package/dist/tui/overlay-input.js +12 -0
  54. package/dist/tui/paste-draft.js +173 -0
  55. package/dist/tui/selection.js +8 -2
  56. package/dist/tui/slash-commands.js +24 -1
  57. package/dist/tui/slash-handler.js +88 -18
  58. package/dist/tui/text-width.js +6 -6
  59. package/dist/tui-chat.js +85 -7
  60. package/docs/API.md +69 -6
  61. package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
  62. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  63. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  64. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
  65. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  66. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  67. package/package.json +1 -1
  68. package/skills/self-config/SKILL.md +7 -4
  69. package/skills/self-config/reference.md +12 -6
@@ -5,11 +5,15 @@ import { setCaretPosition } from "./caret.js";
5
5
  import { displayWidth } from "./text-width.js";
6
6
  import { visualRows, moveCaretHorizontal, moveCaretVertical, insertAt, backspaceAt, deleteAt, caretIndexFromClick, scanDeleteKeys, scanHomeEndKeys, moveToLineStart, moveToLineEnd, deleteToLineStart, deleteToLineEnd, deleteWordBefore, } from "./caret-pos.js";
7
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";
8
+ import { filterSlashCommands, sessionSlashCommands, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, slashMenuWindow, } from "./slash-commands.js";
9
+ import { getActiveProvider, getEffectiveConfig } from "../config.js";
9
10
  import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
10
11
  import { scanSgrMouse } from "./mouse.js";
11
- import { TEXT_START_COLUMN, inputTextWidth } from "./layout.js";
12
+ import { TEXT_START_COLUMN, inputTextWidth, inputBarPaintRows, slashMenuMaxVisible, SLASH_COMMAND_MENU_CHROME, SLASH_ARG_MENU_CHROME, INPUT_BAR_ROWS, } from "./layout.js";
12
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";
13
17
  const GUTTER = "❯ ";
14
18
  const SHIFT_ENTER_SEQ = "\x1b[13;2u";
15
19
  /**
@@ -50,7 +54,7 @@ export function accumulateKittyInput(buffer, chunk) {
50
54
  * exactly; the hardware cursor is parked on that cell so IME composition
51
55
  * (Chinese, Japanese, Korean) shows up in the right place.
52
56
  */
53
- export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, }) {
57
+ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, onRowsChange, onNotice, }) {
54
58
  const [editing, setEditing] = useState({ value: "", caretIndex: 0 });
55
59
  const { value, caretIndex } = editing;
56
60
  const [menuIndex, setMenuIndex] = useState(0);
@@ -61,6 +65,11 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
61
65
  const mouseBuffer = useRef("");
62
66
  const deleteBuffer = useRef("");
63
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;
64
73
  const width = columns || 80;
65
74
  const [historyState, setHistoryState] = useState(() => createHistory(loadInputHistory()));
66
75
  const historyRef = useRef(historyState);
@@ -99,18 +108,33 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
99
108
  const after = edit(editingRef.current.value, editingRef.current.caretIndex);
100
109
  editingRef.current = after;
101
110
  setEditing(after);
111
+ draftRef.current = pruneAttachments(draftRef.current, after.value);
102
112
  const nextHistory = resetHistory(historyRef.current, after.value);
103
113
  if (nextHistory !== historyRef.current) {
104
114
  historyRef.current = nextHistory;
105
115
  setHistoryState(nextHistory);
106
116
  }
107
117
  }, [clearExitHint]);
108
- const submit = (text) => {
109
- historyRef.current = pushInput(historyRef.current, text);
118
+ const submit = (prompt) => {
119
+ historyRef.current = pushInput(historyRef.current, prompt.display);
110
120
  setHistoryState(historyRef.current);
111
121
  saveInputHistory(historyRef.current.entries);
112
- onSubmit(text);
122
+ onSubmit(prompt);
113
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]);
114
138
  const browseTo = (direction) => {
115
139
  const { text, state } = browseHistory(historyRef.current, direction, isKnownSlashCommandInput);
116
140
  historyRef.current = state;
@@ -127,7 +151,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
127
151
  }, []);
128
152
  // Slash command menu — closes once a space follows the command name so the
129
153
  // argument menu can take over immediately.
130
- const matches = useMemo(() => filterSlashCommands(value), [value]);
154
+ const providerType = getActiveProvider(getEffectiveConfig())?.type;
155
+ const matches = useMemo(() => filterSlashCommands(value, sessionSlashCommands(providerType)), [value, providerType]);
131
156
  const menuOpen = value.startsWith("/") && !/\s/.test(value.slice(1)) && !menuDismissed && matches.length > 0;
132
157
  const shownIndex = menuOpen ? Math.min(menuIndex, matches.length - 1) : 0;
133
158
  const selected = menuOpen ? matches[shownIndex] : undefined;
@@ -144,7 +169,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
144
169
  applyEdit(() => ({ value: `/${cmd.name} `, caretIndex: cmd.name.length + 2 }));
145
170
  }
146
171
  else {
147
- submit(`/${cmd.name}`);
172
+ submit({ display: `/${cmd.name}`, content: `/${cmd.name}` });
173
+ draftRef.current = resetPasteDraft();
148
174
  applyEdit(() => ({ value: "", caretIndex: 0 }));
149
175
  }
150
176
  };
@@ -158,14 +184,12 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
158
184
  acceptCommandRef.current = acceptCommand;
159
185
  const acceptArgRef = useRef(acceptArg);
160
186
  acceptArgRef.current = acceptArg;
161
- const matchesRef = useRef(matches);
162
- matchesRef.current = matches;
163
- const argOptionsRef = useRef(argOptions);
164
- argOptionsRef.current = argOptions;
165
187
  const menuOpenRef = useRef(menuOpen);
166
188
  menuOpenRef.current = menuOpen;
167
189
  const argMenuOpenRef = useRef(argMenuOpen);
168
190
  argMenuOpenRef.current = argMenuOpen;
191
+ const applyIncomingPasteRef = useRef(applyIncomingPaste);
192
+ applyIncomingPasteRef.current = applyIncomingPaste;
169
193
  useInput((input, key) => {
170
194
  if (disabled)
171
195
  return;
@@ -187,6 +211,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
187
211
  return;
188
212
  }
189
213
  clearExitHint();
214
+ draftRef.current = resetPasteDraft();
190
215
  applyEdit(() => ({ value: "", caretIndex: 0 }));
191
216
  return;
192
217
  }
@@ -195,6 +220,19 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
195
220
  onSelectionMode?.();
196
221
  return;
197
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;
198
236
  // Fast typing or paste can deliver "text\r" as a single chunk — treat a
199
237
  // trailing Enter as Enter and strip it from the value.
200
238
  const cleanInput = input.replace(/[\r\n]+$/, "");
@@ -256,7 +294,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
256
294
  // Move one visual row first; only browse history when already at the edge.
257
295
  if (key.upArrow) {
258
296
  const { value: v, caretIndex: c } = editingRef.current;
259
- const next = moveCaretVertical(v, c, -1, inputTextWidth(columns));
297
+ const maxW = inputTextWidth(columns);
298
+ const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
299
+ const next = moveCaretVertical(v, c, -1, maxW, lookup);
260
300
  if (next !== c) {
261
301
  applyEdit(() => ({ value: v, caretIndex: next }));
262
302
  return;
@@ -266,7 +306,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
266
306
  }
267
307
  if (key.downArrow) {
268
308
  const { value: v, caretIndex: c } = editingRef.current;
269
- const next = moveCaretVertical(v, c, 1, inputTextWidth(columns));
309
+ const maxW = inputTextWidth(columns);
310
+ const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
311
+ const next = moveCaretVertical(v, c, 1, maxW, lookup);
270
312
  if (next !== c) {
271
313
  applyEdit(() => ({ value: v, caretIndex: next }));
272
314
  return;
@@ -288,9 +330,10 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
288
330
  applyEdit((cv, cc) => insertAt(cv.slice(0, -1), cc, "\n"));
289
331
  return;
290
332
  }
291
- const text = v.trim();
292
- if (text)
293
- submit(text);
333
+ const prompt = buildSubmittedPrompt(v, draftRef.current);
334
+ if (prompt.display.trim() || draftRef.current.attachments.size > 0)
335
+ submit(prompt);
336
+ draftRef.current = resetPasteDraft();
294
337
  applyEdit(() => ({ value: "", caretIndex: 0 }));
295
338
  return;
296
339
  }
@@ -361,13 +404,6 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
361
404
  return;
362
405
  if (/^\[<.*[Mm]$/.test(input) || input.startsWith("[<") || /^[\d;,]+[Mm]$/.test(input))
363
406
  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
- }
371
407
  // Tab → 2 spaces
372
408
  if (key.tab) {
373
409
  applyEdit((v, c) => insertAt(v, c, " "));
@@ -384,8 +420,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
384
420
  const isMultiline = logicalLines.length > 1;
385
421
  const borderColor = disabled ? theme.inputBorderDisabled : theme.inputBorder;
386
422
  const textWidth = inputTextWidth(width);
423
+ const chipLookup = (ch) => chipClusterWidth(ch, draftRef.current, textWidth);
387
424
  // Visual rows of the value, plus the caret position inside them.
388
- const rows = visualRows(value, textWidth);
425
+ const rows = visualRows(value, textWidth, chipLookup);
389
426
  const clampedCaret = Math.min(caretIndex, Array.from(value).length);
390
427
  // Locate the caret's visual row and its character offset inside that row.
391
428
  let caretRowIndex = rows.length - 1;
@@ -401,7 +438,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
401
438
  }
402
439
  // A caret at the end of a full row has no cell left — move it to the start
403
440
  // 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(""));
441
+ let caretColumnOffset = displayWidth(Array.from(rows[caretRowIndex].text).slice(0, caretCharOffset).join(""), chipLookup);
405
442
  if (caretColumnOffset >= textWidth) {
406
443
  if (caretRowIndex === rows.length - 1) {
407
444
  rows.push({
@@ -417,8 +454,16 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
417
454
  // (single-row) multi-line hint — plus the slash menu when it floats below
418
455
  // the input bar.
419
456
  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;
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);
422
467
  // Input bar sits at the bottom of App's footer: its content area starts
423
468
  // `terminalRows` rows up minus the bar's own height (menu + bottom border +
424
469
  // hint above it in screen order, from the bottom of the terminal up).
@@ -431,6 +476,16 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
431
476
  inputTopRowRef.current = inputTopRow;
432
477
  const hintRowsRef = useRef(hintRows);
433
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]);
434
489
  // Listen for Kitty protocol Shift+Enter: ESC[13;2u, which may arrive split
435
490
  // across stdin chunks — accumulate and reassemble here. Ink's useInput gets
436
491
  // each raw chunk and would append fragments to the value, so isKittySequenceFragment
@@ -445,6 +500,11 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
445
500
  return;
446
501
  const handleData = (data) => {
447
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);
448
508
  // Kitty Shift+Enter accumulation
449
509
  const { buffer: kittyBuf, shiftEnter } = accumulateKittyInput(kittyBuffer.current, chunk);
450
510
  kittyBuffer.current = kittyBuf;
@@ -480,7 +540,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
480
540
  const evCol = sgr.event.col;
481
541
  applyEdit((v) => ({
482
542
  value: v,
483
- caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN),
543
+ caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN, chipLookupRef.current),
484
544
  }));
485
545
  }
486
546
  else {
@@ -488,13 +548,12 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
488
548
  const header = menuOpenRef.current ? 1 : 0;
489
549
  const optionIndex = sgr.event.row - (inputTopRowRef.current + rowsRef.current.length + hintRowsRef.current + chrome + header);
490
550
  if (menuOpenRef.current) {
491
- const cmd = matchesRef.current[optionIndex];
551
+ const cmd = commandWindowRef.current?.shown[optionIndex];
492
552
  if (cmd)
493
553
  acceptCommandRef.current(cmd);
494
554
  }
495
555
  else if (argMenuOpenRef.current) {
496
- const shown = argOptionsRef.current.slice(0, 8);
497
- const opt = shown[optionIndex];
556
+ const opt = argWindowRef.current?.shown[optionIndex];
498
557
  if (opt)
499
558
  acceptArgRef.current(opt);
500
559
  }
@@ -516,9 +575,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
516
575
  // same chunk stream as the Kitty sequence. Terminals without mouse support
517
576
  // simply never send events — keyboard navigation still works.
518
577
  useEffect(() => {
519
- stdout.write(MOUSE_ENABLE);
578
+ stdout.write(`${BRACKETED_PASTE_ENABLE}${MOUSE_ENABLE}`);
520
579
  return () => {
521
- stdout.write(MOUSE_DISABLE);
580
+ stdout.write(`${MOUSE_DISABLE}${BRACKETED_PASTE_DISABLE}`);
522
581
  };
523
582
  }, [stdout]);
524
583
  // Must publish during render (not in effect) — Ink writes the frame in
@@ -536,8 +595,24 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
536
595
  const chars = Array.from(row.text);
537
596
  const caretHere = i === caretRowIndex;
538
597
  const offset = caretHere ? caretCharOffset : chars.length;
539
- const before = chars.slice(0, offset).join("");
540
- const after = chars.slice(offset).join("");
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("");
541
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));
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"] }) })] }))] }));
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"] }) })] }))] }));
543
618
  }
@@ -67,12 +67,27 @@ function resultPreviewLines(result) {
67
67
  const hidden = lines.length - TOOL_RESULT_MAX_LINES;
68
68
  return [...lines.slice(0, TOOL_RESULT_MAX_LINES), `… 结果还有 ${hidden} 行未显示`];
69
69
  }
70
- /** Physical rows for one message. The leading marker ("> " / "⚡ name ") lives
70
+ function isExpandable(msg) {
71
+ return msg.role === "tool" || msg.role === "thinking";
72
+ }
73
+ /** Thinking rows start expanded so the chain is visible; tools start collapsed.
74
+ * `toggled` records the ids the user has clicked, which inverts that default. */
75
+ function rowExpanded(msg, toggled) {
76
+ return msg.role === "thinking" ? !toggled.has(msg.id) : toggled.has(msg.id);
77
+ }
78
+ function packCollapsedRow(label, preview, tail, textWidth) {
79
+ const contentWidth = Math.max(1, textWidth - displayWidth(label));
80
+ const packed = displayWidth(preview + tail) <= contentWidth
81
+ ? `${label}${preview}${tail}`
82
+ : `${label}${clipToWidth(preview, Math.max(1, contentWidth - displayWidth(tail) - 1))}…${tail}`;
83
+ return [clipToWidth(packed, textWidth)];
84
+ }
85
+ /** Physical rows for one message. The leading marker ("> " / "⚡ name " / "💭 思考 ") lives
71
86
  * in the first row only, mirroring the JSX layout below.
72
87
  *
73
88
  * Tool calls render as a single row (arguments plus a result summary) by
74
- * default; clicking the row (see MessageList) toggles `expanded` to reveal the
75
- * full tool result. */
89
+ * default; thinking rows start expanded so the full chain is visible. Clicking
90
+ * either row (see MessageList) toggles `expanded`. */
76
91
  export function messageLines(msg, columns, expanded) {
77
92
  const textWidth = messageTextWidth(columns);
78
93
  switch (msg.role) {
@@ -103,21 +118,32 @@ export function messageLines(msg, columns, expanded) {
103
118
  return wrapByWidth(typeof msg.content === "string" ? msg.content : String(msg.content ?? ""), textWidth);
104
119
  }
105
120
  }
106
- case "thinking":
107
- return msg.content?.trim() ? wrapByWidth(msg.content, textWidth) : [];
121
+ case "thinking": {
122
+ const body = msg.content.trim();
123
+ if (!body)
124
+ return [];
125
+ const preview = body.replace(/\s+/g, " ");
126
+ const label = `💭 思考 ${expanded ? "▾" : "▸"} `;
127
+ if (!expanded) {
128
+ const lineCount = body.split("\n").length;
129
+ const tail = lineCount > 1 ? ` → ${lineCount} 行` : "";
130
+ return packCollapsedRow(label, preview, tail, textWidth);
131
+ }
132
+ const rows = wrapByWidth(label, textWidth);
133
+ for (const line of body.replace(/\s+$/, "").split("\n")) {
134
+ rows.push(...wrapByWidth(` ${line}`, textWidth));
135
+ }
136
+ return rows;
137
+ }
108
138
  case "tool": {
109
139
  const name = msg.toolName ?? "";
110
140
  // The call summary is a single logical line; collapse any stray newlines
111
141
  // so it cannot push the row layout around.
112
142
  const call = msg.content.replace(/\s+/g, " ").trim();
113
143
  const label = `⚡ ${name} ${expanded ? "▾" : "▸"} `;
114
- const contentWidth = Math.max(1, textWidth - displayWidth(label));
115
144
  if (!expanded) {
116
145
  const tail = msg.toolResultSummary ? ` → ${msg.toolResultSummary}` : "";
117
- const packed = displayWidth(call + tail) <= contentWidth
118
- ? `${label}${call}${tail}`
119
- : `${label}${clipToWidth(call, Math.max(1, contentWidth - displayWidth(tail) - 1))}…${tail}`;
120
- return [clipToWidth(packed, textWidth)];
146
+ return packCollapsedRow(label, call, tail, textWidth);
121
147
  }
122
148
  const rows = wrapByWidth(`${label}${call}`, textWidth);
123
149
  if (msg.toolChildren && msg.toolChildren.length > 0) {
@@ -246,9 +272,11 @@ export function sliceWindow(messages, rowCounts, maxHeight, top) {
246
272
  */
247
273
  export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, selectionMode, onExitSelectionMode, interactive = true, }) {
248
274
  const [windowTop, setWindowTop] = useState(null);
275
+ // Ids the user has clicked. Tools start collapsed; thinking starts expanded,
276
+ // so membership here inverts that default (see rowExpanded).
249
277
  const [expanded, setExpanded] = useState(new Set());
250
278
  const [selection, setSelection] = useState(null);
251
- const rowCounts = useMemo(() => messages.map((m) => messageRowCount(m, columns, expanded.has(m.id))), [messages, columns, expanded]);
279
+ const rowCounts = useMemo(() => messages.map((m) => messageRowCount(m, columns, rowExpanded(m, expanded))), [messages, columns, expanded]);
252
280
  const slice = useMemo(() => sliceWindow(messages, rowCounts, maxHeight ?? 0, windowTop), [messages, rowCounts, maxHeight, windowTop]);
253
281
  // Mouse wheel needs the current window geometry in the raw stdin listener,
254
282
  // which subscribes once per maxHeight change.
@@ -432,7 +460,7 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, s
432
460
  let cursor = 0;
433
461
  for (let i = 0; i < messages.length; i++) {
434
462
  const len = rowCounts[i] ?? 0;
435
- if (center >= cursor && center < cursor + len && messages[i].role === "tool") {
463
+ if (center >= cursor && center < cursor + len && isExpandable(messages[i])) {
436
464
  toggleRef.current(messages[i].id);
437
465
  break;
438
466
  }
@@ -449,8 +477,8 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, s
449
477
  prevMessageCount.current = messages.length;
450
478
  }, [messages]);
451
479
  // Map a terminal row (1-based) to the visible message row it lands on and
452
- // toggle its expansion. Only tool rows are clickable; the top indicator row
453
- // and anything below the window are ignored.
480
+ // toggle its expansion. Tool and thinking rows are clickable; the top
481
+ // indicator row and anything below the window are ignored.
454
482
  const handleClick = (row) => {
455
483
  const { above, parts } = sliceRef.current;
456
484
  let offset = row - 1;
@@ -463,7 +491,7 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, s
463
491
  }
464
492
  for (const part of parts) {
465
493
  if (offset < part.count) {
466
- if (part.msg.role === "tool")
494
+ if (isExpandable(part.msg))
467
495
  toggleRef.current(part.msg.id);
468
496
  return;
469
497
  }
@@ -508,7 +536,7 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, s
508
536
  };
509
537
  // Mouse handling in the message area: drag with the left button selects text
510
538
  // and auto-copies on release; a click (press+release without motion) still
511
- // toggles tool rows; the wheel scrolls. The drag state machine (drag-state)
539
+ // toggles tool and thinking rows; the wheel scrolls. The drag state machine (drag-state)
512
540
  // is fed every non-wheel SGR event; presses are accepted anywhere inside the
513
541
  // allocated message region (maxHeight), so a drag that starts on the blank
514
542
  // gap below a short conversation still works. A drag/release may land
@@ -571,9 +599,9 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, s
571
599
  };
572
600
  }, [stdin, maxHeight, clampSelection, handleClick, scrollDown, scrollUp, selectMessage, selectWord]);
573
601
  if (!maxHeight || maxHeight <= 0) {
574
- return (_jsxs(Box, { flexDirection: "column", children: [messages.length === 0 && (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Text, { color: "gray", children: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\uFF0C\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CEsc \u53D6\u6D88\u8FD0\u884C" }) })), messages.map((msg) => (_jsx(MessageRow, { message: msg, lines: cachedMessageLines(msg, columns, expanded.has(msg.id)) }, msg.id)))] }));
602
+ return (_jsxs(Box, { flexDirection: "column", children: [messages.length === 0 && (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Text, { color: "gray", children: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\uFF0C\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CEsc \u53D6\u6D88\u8FD0\u884C" }) })), messages.map((msg) => (_jsx(MessageRow, { message: msg, lines: cachedMessageLines(msg, columns, rowExpanded(msg, expanded)) }, msg.id)))] }));
575
603
  }
576
- return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", width: "100%", children: [messages.length === 0 && (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Text, { color: "gray", children: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\uFF0C\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CEsc \u53D6\u6D88\u8FD0\u884C" }) })), slice.above > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2026 \u4EE5\u4E0A ", slice.above, " \u884C"] }) })), slice.parts.map(({ msg, start, count }, p) => (_jsx(MessageRow, { message: msg, lines: cachedMessageLines(msg, columns, expanded.has(msg.id)), start: start, count: count, selection: partSelections?.get(p) }, msg.id))), slice.below > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsx(Text, { color: "gray", dimColor: true, children: "\u2193 \u6309 PageDown \u56DE\u5230\u5E95\u90E8" }) }))] }));
604
+ return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", width: "100%", children: [messages.length === 0 && (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Text, { color: "gray", children: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\uFF0C\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CEsc \u53D6\u6D88\u8FD0\u884C" }) })), slice.above > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2026 \u4EE5\u4E0A ", slice.above, " \u884C"] }) })), slice.parts.map(({ msg, start, count }, p) => (_jsx(MessageRow, { message: msg, lines: cachedMessageLines(msg, columns, rowExpanded(msg, expanded)), start: start, count: count, selection: partSelections?.get(p) }, msg.id))), slice.below > 0 && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, children: _jsx(Text, { color: "gray", dimColor: true, children: "\u2193 \u6309 PageDown \u56DE\u5230\u5E95\u90E8" }) }))] }));
577
605
  }
578
606
  /** Map a window slice (rows incl. margin) to renderable lines. */
579
607
  function sliceLines(msg, lines, start, count = lines.length) {
@@ -591,6 +619,8 @@ function copyKindFor(msg, lineIndex) {
591
619
  return lineIndex === 0 ? "user-head" : "plain";
592
620
  if (msg.role === "tool")
593
621
  return lineIndex === 0 ? "tool-head" : "tool-body";
622
+ if (msg.role === "thinking")
623
+ return lineIndex === 0 ? "thinking-head" : "thinking-body";
594
624
  return "plain";
595
625
  }
596
626
  /**
@@ -610,7 +640,7 @@ function buildArea(slice, columns, expanded) {
610
640
  }
611
641
  for (let p = 0; p < slice.parts.length; p++) {
612
642
  const part = slice.parts[p];
613
- const lines = cachedMessageLines(part.msg, columns, expanded.has(part.msg.id));
643
+ const lines = cachedMessageLines(part.msg, columns, rowExpanded(part.msg, expanded));
614
644
  const { lines: shown, marginTop, lineOffset } = sliceLines(part.msg, lines, part.start, part.count);
615
645
  if (marginTop) {
616
646
  rows.push(null);
@@ -666,14 +696,15 @@ const MessageRow = React.memo(function MessageRow({ message, lines, start = 0, c
666
696
  case "assistant":
667
697
  return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: shown.map((l, i) => (_jsx(Text, { wrap: "truncate-end", children: apply(i, l) }, i))) }));
668
698
  case "thinking":
669
- return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: shown.map((l, i) => (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: apply(i, l) }, i))) }));
670
699
  case "tool": {
671
700
  const first = shown[0] ?? "";
672
701
  const rest = shown.slice(1);
673
- const prefix = visualPrefix(first, "tool");
702
+ const thinking = message.role === "thinking";
703
+ const prefix = visualPrefix(first, thinking ? "thinking" : "tool");
674
704
  const split = prefix ? splitFirst(prefix) : null;
675
705
  const headBody = prefix ? paintBody(first.slice(prefix.length), split.tail) : apply(0, first);
676
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: [_jsxs(Box, { flexWrap: "nowrap", children: [prefix !== "" && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "yellow", children: paintPrefix(prefix, split.head) }) })), _jsx(Text, { color: prefix ? (message.toolIsError ? "red" : "gray") : undefined, wrap: "truncate-end", children: headBody })] }), rest.map((l, k) => (_jsx(Text, { color: message.toolIsError ? "red" : undefined, wrap: "truncate-end", children: apply(k + 1, l) }, k)))] }));
706
+ const bodyColor = thinking ? undefined : message.toolIsError ? "red" : prefix ? "gray" : undefined;
707
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: [_jsxs(Box, { flexWrap: "nowrap", children: [prefix !== "" && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: thinking ? "gray" : "yellow", dimColor: thinking, children: paintPrefix(prefix, split.head) }) })), _jsx(Text, { color: bodyColor, dimColor: thinking, wrap: "truncate-end", children: headBody })] }), rest.map((l, k) => (_jsx(Text, { color: bodyColor, dimColor: thinking, wrap: "truncate-end", children: apply(k + 1, l) }, k)))] }));
677
708
  }
678
709
  case "system":
679
710
  return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: shown.map((l, i) => (_jsx(Text, { wrap: "truncate-end", children: apply(i, l) }, i))) }));
@@ -1,9 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { formatTokenStatusLabel } from "../token-display.js";
3
+ import { formatModelStatusMeta, formatTokenStatusLabel } from "../token-display.js";
4
+ import { resolveThinkingEffort, thinkingEffortLabel } from "../thinking.js";
5
+ import { isMemoryEnabled } from "../memory.js";
4
6
  import { displayWidth } from "./text-width.js";
5
7
  export function StatusBar({ state, columns = 80 }) {
6
8
  const model = state.model || "unknown";
9
+ const thinking = thinkingEffortLabel(resolveThinkingEffort(undefined, state.model).thinking);
10
+ const modelMeta = formatModelStatusMeta(thinking, state.tokenInfo?.contextWindow, isMemoryEnabled());
7
11
  const tokensLabel = state.tokenInfo ? formatTokenStatusLabel(state.tokenInfo) : "";
8
12
  // Transient copy feedback rides on the separator row itself (right end) so
9
13
  // it never adds a row and the layout does not jump while it is visible.
@@ -14,5 +18,5 @@ export function StatusBar({ state, columns = 80 }) {
14
18
  ? "选择模式 ↑↓←→扩选 Enter复制 Esc退出"
15
19
  : null;
16
20
  const noticeWidth = notice ? displayWidth(notice) : 0;
17
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: notice ? `${"─".repeat(Math.max(0, sepWidth - noticeWidth - 2))} ${notice}` : "─".repeat(sepWidth) }), _jsxs(Box, { children: [_jsx(Text, { children: " \uD83E\uDD16 min-agent" }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", "(", model, ")"] }), tokensLabel && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: tokensLabel })] }))] })] }));
21
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: notice ? `${"─".repeat(Math.max(0, sepWidth - noticeWidth - 2))} ${notice}` : "─".repeat(sepWidth) }), _jsxs(Box, { children: [_jsx(Text, { children: " \uD83E\uDD16 min-agent" }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", "(", model, " \u00B7 ", modelMeta, ")", tokensLabel ? ` ${tokensLabel}` : ""] })] })] }));
18
22
  }
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { getActiveProvider, getEffectiveConfig } from "../config.js";
5
+ import { resolveThinkingEffort, thinkingChoiceIndex, thinkingChoicesForModel, thinkingEffortLabel, thinkingSourceLabel, refreshThinkingChoices, } from "../thinking.js";
6
+ import { shouldAcceptOverlayConfirm } from "./overlay-input.js";
7
+ import { theme } from "./theme.js";
8
+ export function thinkPickerRows() {
9
+ return 2 + 1 + 5 + 1 + 2;
10
+ }
11
+ export function ThinkPicker({ scope, modelId, onSelect, onCancel }) {
12
+ const provider = getActiveProvider(getEffectiveConfig());
13
+ const id = modelId ?? provider?.defaultModel;
14
+ const current = resolveThinkingEffort(undefined, id);
15
+ const [choices, setChoices] = useState(() => thinkingChoicesForModel(id));
16
+ const [index, setIndex] = useState(() => thinkingChoiceIndex(current.thinking, thinkingChoicesForModel(id)));
17
+ const armed = useRef(false);
18
+ const moved = useRef(false);
19
+ const indexRef = useRef(index);
20
+ const choicesRef = useRef(choices);
21
+ indexRef.current = index;
22
+ choicesRef.current = choices;
23
+ useEffect(() => {
24
+ const timer = setTimeout(() => {
25
+ armed.current = true;
26
+ }, 0);
27
+ return () => clearTimeout(timer);
28
+ }, []);
29
+ useEffect(() => {
30
+ if (!id)
31
+ return;
32
+ let cancelled = false;
33
+ void refreshThinkingChoices(id, { type: provider?.type, baseURL: provider?.baseURL }).then((next) => {
34
+ if (cancelled || next.length === 0)
35
+ return;
36
+ setChoices(next);
37
+ if (moved.current) {
38
+ setIndex((i) => Math.min(i, Math.max(0, next.length - 1)));
39
+ return;
40
+ }
41
+ setIndex(thinkingChoiceIndex(resolveThinkingEffort(undefined, id).thinking, next));
42
+ });
43
+ return () => {
44
+ cancelled = true;
45
+ };
46
+ }, [id, provider?.baseURL, provider?.type]);
47
+ useInput((input, key) => {
48
+ if (key.escape) {
49
+ onCancel();
50
+ return;
51
+ }
52
+ const list = choicesRef.current;
53
+ if (list.length === 0)
54
+ return;
55
+ if (key.downArrow) {
56
+ moved.current = true;
57
+ setIndex((i) => (i + 1) % list.length);
58
+ return;
59
+ }
60
+ if (key.upArrow) {
61
+ moved.current = true;
62
+ setIndex((i) => (i - 1 + list.length) % list.length);
63
+ return;
64
+ }
65
+ if (shouldAcceptOverlayConfirm(armed.current, key, input)) {
66
+ const picked = list[indexRef.current];
67
+ if (picked)
68
+ onSelect(picked);
69
+ }
70
+ });
71
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.pickerBorder, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.pickerBorder, children: "\u9009\u62E9\u601D\u8003\u5F3A\u5EA6" }), scope === "project" && _jsx(Text, { color: theme.muted, children: "\uFF08\u5199\u5165\u5F53\u524D\u9879\u76EE\uFF09" })] }), _jsx(Box, { children: _jsxs(Text, { color: theme.muted, children: ["\u5F53\u524D: ", thinkingEffortLabel(current.thinking), "\uFF08", thinkingSourceLabel(current.source), "\uFF09"] }) }), choices.map((effort, i) => {
72
+ const active = i === index;
73
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? theme.accent : theme.muted, children: active ? "❯ " : " " }), _jsx(Text, { bold: active, color: active ? theme.accent : undefined, children: thinkingEffortLabel(effort) }), effort === current.thinking && _jsx(Text, { color: theme.success, children: " \u2190 \u5F53\u524D" })] }, effort));
74
+ }), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \u00B7 Enter \u786E\u8BA4 \u00B7 Esc \u53D6\u6D88" }) })] }));
75
+ }
@@ -0,0 +1,37 @@
1
+ export const BRACKETED_PASTE_ENABLE = "\x1b[?2004h";
2
+ export const BRACKETED_PASTE_DISABLE = "\x1b[?2004l";
3
+ const PASTE_START = "\x1b[200~";
4
+ const PASTE_END = "\x1b[201~";
5
+ export function scanBracketedPaste(buffer, chunk) {
6
+ const pastes = [];
7
+ let buf = buffer + chunk;
8
+ while (true) {
9
+ const start = buf.indexOf(PASTE_START);
10
+ if (start === -1) {
11
+ return {
12
+ buffer: pastes.length > 0 ? buf : incompleteStartPrefix(buf),
13
+ pastes,
14
+ active: false,
15
+ };
16
+ }
17
+ const body = buf.slice(start + PASTE_START.length);
18
+ const end = body.indexOf(PASTE_END);
19
+ if (end === -1) {
20
+ return { buffer: PASTE_START + body, pastes, active: true };
21
+ }
22
+ pastes.push(body.slice(0, end));
23
+ buf = body.slice(end + PASTE_END.length);
24
+ }
25
+ }
26
+ export function shouldIgnorePasteInput(input, active) {
27
+ if (active)
28
+ return true;
29
+ return input.includes(PASTE_START) || input.includes(PASTE_END) || input.includes("[200~") || input.includes("[201~");
30
+ }
31
+ function incompleteStartPrefix(buf) {
32
+ const esc = buf.lastIndexOf("\x1b");
33
+ if (esc === -1)
34
+ return "";
35
+ const prefix = buf.slice(esc);
36
+ return PASTE_START.startsWith(prefix) ? prefix : "";
37
+ }