min-agent 0.4.1 → 0.5.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 (54) hide show
  1. package/README.md +32 -2
  2. package/dist/agent.js +36 -22
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/exec.js +3 -0
  5. package/dist/cli/commands/index.js +22 -5
  6. package/dist/cli/commands/memory.js +33 -15
  7. package/dist/cli/commands/think.js +12 -0
  8. package/dist/cli/commands/write-config.js +22 -0
  9. package/dist/cli/option-helpers.js +13 -1
  10. package/dist/cli/program.js +50 -13
  11. package/dist/code-mode.js +1 -1
  12. package/dist/config.js +41 -0
  13. package/dist/context-window.js +8 -28
  14. package/dist/memory-cli.js +33 -0
  15. package/dist/memory.js +127 -46
  16. package/dist/model-catalog.js +285 -0
  17. package/dist/permission-cli.js +1 -4
  18. package/dist/provider.js +4 -1
  19. package/dist/reasoning-stream.js +158 -0
  20. package/dist/sandbox-cli.js +1 -4
  21. package/dist/scope.js +23 -0
  22. package/dist/serve/common.js +22 -1
  23. package/dist/serve/routes-chat.js +21 -1
  24. package/dist/serve/routes-memory.js +31 -2
  25. package/dist/serve/routes-meta.js +34 -6
  26. package/dist/think-cli.js +36 -0
  27. package/dist/thinking-wire.js +228 -0
  28. package/dist/thinking.js +142 -0
  29. package/dist/token-display.js +10 -7
  30. package/dist/tools/todo.js +22 -8
  31. package/dist/tui/App.js +36 -8
  32. package/dist/tui/InputBar.js +109 -36
  33. package/dist/tui/MessageList.js +53 -22
  34. package/dist/tui/StatusBar.js +7 -3
  35. package/dist/tui/ThinkPicker.js +77 -0
  36. package/dist/tui/bracketed-paste.js +37 -0
  37. package/dist/tui/caret-pos.js +10 -8
  38. package/dist/tui/index.js +7 -1
  39. package/dist/tui/layout.js +17 -0
  40. package/dist/tui/overlay-input.js +12 -0
  41. package/dist/tui/paste-draft.js +173 -0
  42. package/dist/tui/selection.js +8 -2
  43. package/dist/tui/slash-commands.js +18 -1
  44. package/dist/tui/slash-handler.js +61 -17
  45. package/dist/tui/text-width.js +6 -6
  46. package/dist/tui-chat.js +63 -7
  47. package/docs/API.md +50 -4
  48. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  49. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  50. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  51. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  52. package/package.json +1 -1
  53. package/skills/self-config/SKILL.md +5 -4
  54. package/skills/self-config/reference.md +10 -5
@@ -5,11 +5,14 @@ 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, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, slashMenuWindow, } from "./slash-commands.js";
9
9
  import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
10
10
  import { scanSgrMouse } from "./mouse.js";
11
- import { TEXT_START_COLUMN, inputTextWidth } from "./layout.js";
11
+ import { TEXT_START_COLUMN, inputTextWidth, inputBarPaintRows, slashMenuMaxVisible, SLASH_COMMAND_MENU_CHROME, SLASH_ARG_MENU_CHROME, INPUT_BAR_ROWS, } from "./layout.js";
12
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";
13
16
  const GUTTER = "❯ ";
14
17
  const SHIFT_ENTER_SEQ = "\x1b[13;2u";
15
18
  /**
@@ -50,7 +53,7 @@ export function accumulateKittyInput(buffer, chunk) {
50
53
  * exactly; the hardware cursor is parked on that cell so IME composition
51
54
  * (Chinese, Japanese, Korean) shows up in the right place.
52
55
  */
53
- export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, }) {
56
+ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRows, onSelectionMode, onExit, onRowsChange, onNotice, }) {
54
57
  const [editing, setEditing] = useState({ value: "", caretIndex: 0 });
55
58
  const { value, caretIndex } = editing;
56
59
  const [menuIndex, setMenuIndex] = useState(0);
@@ -61,6 +64,11 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
61
64
  const mouseBuffer = useRef("");
62
65
  const deleteBuffer = useRef("");
63
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;
64
72
  const width = columns || 80;
65
73
  const [historyState, setHistoryState] = useState(() => createHistory(loadInputHistory()));
66
74
  const historyRef = useRef(historyState);
@@ -99,18 +107,33 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
99
107
  const after = edit(editingRef.current.value, editingRef.current.caretIndex);
100
108
  editingRef.current = after;
101
109
  setEditing(after);
110
+ draftRef.current = pruneAttachments(draftRef.current, after.value);
102
111
  const nextHistory = resetHistory(historyRef.current, after.value);
103
112
  if (nextHistory !== historyRef.current) {
104
113
  historyRef.current = nextHistory;
105
114
  setHistoryState(nextHistory);
106
115
  }
107
116
  }, [clearExitHint]);
108
- const submit = (text) => {
109
- historyRef.current = pushInput(historyRef.current, text);
117
+ const submit = (prompt) => {
118
+ historyRef.current = pushInput(historyRef.current, prompt.display);
110
119
  setHistoryState(historyRef.current);
111
120
  saveInputHistory(historyRef.current.entries);
112
- onSubmit(text);
121
+ onSubmit(prompt);
113
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]);
114
137
  const browseTo = (direction) => {
115
138
  const { text, state } = browseHistory(historyRef.current, direction, isKnownSlashCommandInput);
116
139
  historyRef.current = state;
@@ -144,7 +167,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
144
167
  applyEdit(() => ({ value: `/${cmd.name} `, caretIndex: cmd.name.length + 2 }));
145
168
  }
146
169
  else {
147
- submit(`/${cmd.name}`);
170
+ submit({ display: `/${cmd.name}`, content: `/${cmd.name}` });
171
+ draftRef.current = resetPasteDraft();
148
172
  applyEdit(() => ({ value: "", caretIndex: 0 }));
149
173
  }
150
174
  };
@@ -158,14 +182,12 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
158
182
  acceptCommandRef.current = acceptCommand;
159
183
  const acceptArgRef = useRef(acceptArg);
160
184
  acceptArgRef.current = acceptArg;
161
- const matchesRef = useRef(matches);
162
- matchesRef.current = matches;
163
- const argOptionsRef = useRef(argOptions);
164
- argOptionsRef.current = argOptions;
165
185
  const menuOpenRef = useRef(menuOpen);
166
186
  menuOpenRef.current = menuOpen;
167
187
  const argMenuOpenRef = useRef(argMenuOpen);
168
188
  argMenuOpenRef.current = argMenuOpen;
189
+ const applyIncomingPasteRef = useRef(applyIncomingPaste);
190
+ applyIncomingPasteRef.current = applyIncomingPaste;
169
191
  useInput((input, key) => {
170
192
  if (disabled)
171
193
  return;
@@ -187,6 +209,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
187
209
  return;
188
210
  }
189
211
  clearExitHint();
212
+ draftRef.current = resetPasteDraft();
190
213
  applyEdit(() => ({ value: "", caretIndex: 0 }));
191
214
  return;
192
215
  }
@@ -195,6 +218,19 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
195
218
  onSelectionMode?.();
196
219
  return;
197
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;
198
234
  // Fast typing or paste can deliver "text\r" as a single chunk — treat a
199
235
  // trailing Enter as Enter and strip it from the value.
200
236
  const cleanInput = input.replace(/[\r\n]+$/, "");
@@ -256,7 +292,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
256
292
  // Move one visual row first; only browse history when already at the edge.
257
293
  if (key.upArrow) {
258
294
  const { value: v, caretIndex: c } = editingRef.current;
259
- const next = moveCaretVertical(v, c, -1, inputTextWidth(columns));
295
+ const maxW = inputTextWidth(columns);
296
+ const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
297
+ const next = moveCaretVertical(v, c, -1, maxW, lookup);
260
298
  if (next !== c) {
261
299
  applyEdit(() => ({ value: v, caretIndex: next }));
262
300
  return;
@@ -266,7 +304,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
266
304
  }
267
305
  if (key.downArrow) {
268
306
  const { value: v, caretIndex: c } = editingRef.current;
269
- const next = moveCaretVertical(v, c, 1, inputTextWidth(columns));
307
+ const maxW = inputTextWidth(columns);
308
+ const lookup = (ch) => chipClusterWidth(ch, draftRef.current, maxW);
309
+ const next = moveCaretVertical(v, c, 1, maxW, lookup);
270
310
  if (next !== c) {
271
311
  applyEdit(() => ({ value: v, caretIndex: next }));
272
312
  return;
@@ -288,9 +328,10 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
288
328
  applyEdit((cv, cc) => insertAt(cv.slice(0, -1), cc, "\n"));
289
329
  return;
290
330
  }
291
- const text = v.trim();
292
- if (text)
293
- submit(text);
331
+ const prompt = buildSubmittedPrompt(v, draftRef.current);
332
+ if (prompt.display.trim() || draftRef.current.attachments.size > 0)
333
+ submit(prompt);
334
+ draftRef.current = resetPasteDraft();
294
335
  applyEdit(() => ({ value: "", caretIndex: 0 }));
295
336
  return;
296
337
  }
@@ -361,13 +402,6 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
361
402
  return;
362
403
  if (/^\[<.*[Mm]$/.test(input) || input.startsWith("[<") || /^[\d;,]+[Mm]$/.test(input))
363
404
  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
405
  // Tab → 2 spaces
372
406
  if (key.tab) {
373
407
  applyEdit((v, c) => insertAt(v, c, " "));
@@ -384,8 +418,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
384
418
  const isMultiline = logicalLines.length > 1;
385
419
  const borderColor = disabled ? theme.inputBorderDisabled : theme.inputBorder;
386
420
  const textWidth = inputTextWidth(width);
421
+ const chipLookup = (ch) => chipClusterWidth(ch, draftRef.current, textWidth);
387
422
  // Visual rows of the value, plus the caret position inside them.
388
- const rows = visualRows(value, textWidth);
423
+ const rows = visualRows(value, textWidth, chipLookup);
389
424
  const clampedCaret = Math.min(caretIndex, Array.from(value).length);
390
425
  // Locate the caret's visual row and its character offset inside that row.
391
426
  let caretRowIndex = rows.length - 1;
@@ -401,7 +436,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
401
436
  }
402
437
  // A caret at the end of a full row has no cell left — move it to the start
403
438
  // 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(""));
439
+ let caretColumnOffset = displayWidth(Array.from(rows[caretRowIndex].text).slice(0, caretCharOffset).join(""), chipLookup);
405
440
  if (caretColumnOffset >= textWidth) {
406
441
  if (caretRowIndex === rows.length - 1) {
407
442
  rows.push({
@@ -417,8 +452,16 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
417
452
  // (single-row) multi-line hint — plus the slash menu when it floats below
418
453
  // the input bar.
419
454
  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;
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);
422
465
  // Input bar sits at the bottom of App's footer: its content area starts
423
466
  // `terminalRows` rows up minus the bar's own height (menu + bottom border +
424
467
  // hint above it in screen order, from the bottom of the terminal up).
@@ -431,6 +474,16 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
431
474
  inputTopRowRef.current = inputTopRow;
432
475
  const hintRowsRef = useRef(hintRows);
433
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]);
434
487
  // Listen for Kitty protocol Shift+Enter: ESC[13;2u, which may arrive split
435
488
  // across stdin chunks — accumulate and reassemble here. Ink's useInput gets
436
489
  // each raw chunk and would append fragments to the value, so isKittySequenceFragment
@@ -445,6 +498,11 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
445
498
  return;
446
499
  const handleData = (data) => {
447
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);
448
506
  // Kitty Shift+Enter accumulation
449
507
  const { buffer: kittyBuf, shiftEnter } = accumulateKittyInput(kittyBuffer.current, chunk);
450
508
  kittyBuffer.current = kittyBuf;
@@ -480,7 +538,7 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
480
538
  const evCol = sgr.event.col;
481
539
  applyEdit((v) => ({
482
540
  value: v,
483
- caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN),
541
+ caretIndex: caretIndexFromClick(rowsRef.current, contentRow, evCol, TEXT_START_COLUMN, chipLookupRef.current),
484
542
  }));
485
543
  }
486
544
  else {
@@ -488,13 +546,12 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
488
546
  const header = menuOpenRef.current ? 1 : 0;
489
547
  const optionIndex = sgr.event.row - (inputTopRowRef.current + rowsRef.current.length + hintRowsRef.current + chrome + header);
490
548
  if (menuOpenRef.current) {
491
- const cmd = matchesRef.current[optionIndex];
549
+ const cmd = commandWindowRef.current?.shown[optionIndex];
492
550
  if (cmd)
493
551
  acceptCommandRef.current(cmd);
494
552
  }
495
553
  else if (argMenuOpenRef.current) {
496
- const shown = argOptionsRef.current.slice(0, 8);
497
- const opt = shown[optionIndex];
554
+ const opt = argWindowRef.current?.shown[optionIndex];
498
555
  if (opt)
499
556
  acceptArgRef.current(opt);
500
557
  }
@@ -516,9 +573,9 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
516
573
  // same chunk stream as the Kitty sequence. Terminals without mouse support
517
574
  // simply never send events — keyboard navigation still works.
518
575
  useEffect(() => {
519
- stdout.write(MOUSE_ENABLE);
576
+ stdout.write(`${BRACKETED_PASTE_ENABLE}${MOUSE_ENABLE}`);
520
577
  return () => {
521
- stdout.write(MOUSE_DISABLE);
578
+ stdout.write(`${MOUSE_DISABLE}${BRACKETED_PASTE_DISABLE}`);
522
579
  };
523
580
  }, [stdout]);
524
581
  // Must publish during render (not in effect) — Ink writes the frame in
@@ -536,8 +593,24 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
536
593
  const chars = Array.from(row.text);
537
594
  const caretHere = i === caretRowIndex;
538
595
  const offset = caretHere ? caretCharOffset : chars.length;
539
- const before = chars.slice(0, offset).join("");
540
- const after = chars.slice(offset).join("");
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("");
541
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));
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"] }) })] }))] }));
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"] }) })] }))] }));
543
616
  }
@@ -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,77 @@
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 { getModelCatalog } from "../model-catalog.js";
6
+ import { resolveThinkingEffort, thinkingChoiceIndex, thinkingChoicesForModel, thinkingChoicesFromReasoning, thinkingEffortLabel, thinkingSourceLabel, } from "../thinking.js";
7
+ import { shouldAcceptOverlayConfirm } from "./overlay-input.js";
8
+ import { theme } from "./theme.js";
9
+ export function thinkPickerRows() {
10
+ return 2 + 1 + 5 + 1 + 2;
11
+ }
12
+ export function ThinkPicker({ scope, modelId, onSelect, onCancel }) {
13
+ const provider = getActiveProvider(getEffectiveConfig());
14
+ const id = modelId ?? provider?.defaultModel;
15
+ const current = resolveThinkingEffort(undefined, id);
16
+ const [choices, setChoices] = useState(() => thinkingChoicesForModel(id));
17
+ const [index, setIndex] = useState(() => thinkingChoiceIndex(current.thinking, thinkingChoicesForModel(id)));
18
+ const armed = useRef(false);
19
+ const moved = useRef(false);
20
+ const indexRef = useRef(index);
21
+ const choicesRef = useRef(choices);
22
+ indexRef.current = index;
23
+ choicesRef.current = choices;
24
+ useEffect(() => {
25
+ const timer = setTimeout(() => {
26
+ armed.current = true;
27
+ }, 0);
28
+ return () => clearTimeout(timer);
29
+ }, []);
30
+ useEffect(() => {
31
+ if (!id)
32
+ return;
33
+ let cancelled = false;
34
+ void getModelCatalog(id, provider?.baseURL).then((entry) => {
35
+ if (cancelled || !entry)
36
+ return;
37
+ const next = thinkingChoicesFromReasoning(entry.reasoning);
38
+ setChoices(next);
39
+ if (moved.current) {
40
+ setIndex((i) => Math.min(i, Math.max(0, next.length - 1)));
41
+ return;
42
+ }
43
+ setIndex(thinkingChoiceIndex(resolveThinkingEffort(undefined, id).thinking, next));
44
+ });
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [id, provider?.baseURL]);
49
+ useInput((input, key) => {
50
+ if (key.escape) {
51
+ onCancel();
52
+ return;
53
+ }
54
+ const list = choicesRef.current;
55
+ if (list.length === 0)
56
+ return;
57
+ if (key.downArrow) {
58
+ moved.current = true;
59
+ setIndex((i) => (i + 1) % list.length);
60
+ return;
61
+ }
62
+ if (key.upArrow) {
63
+ moved.current = true;
64
+ setIndex((i) => (i - 1 + list.length) % list.length);
65
+ return;
66
+ }
67
+ if (shouldAcceptOverlayConfirm(armed.current, key, input)) {
68
+ const picked = list[indexRef.current];
69
+ if (picked)
70
+ onSelect(picked);
71
+ }
72
+ });
73
+ 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) => {
74
+ const active = i === index;
75
+ 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));
76
+ }), _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" }) })] }));
77
+ }
@@ -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
+ }