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.
- package/README.md +32 -2
- package/dist/agent.js +36 -22
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +22 -5
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/think.js +12 -0
- package/dist/cli/commands/write-config.js +22 -0
- package/dist/cli/option-helpers.js +13 -1
- package/dist/cli/program.js +50 -13
- package/dist/code-mode.js +1 -1
- package/dist/config.js +41 -0
- package/dist/context-window.js +8 -28
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/permission-cli.js +1 -4
- package/dist/provider.js +4 -1
- package/dist/reasoning-stream.js +158 -0
- package/dist/sandbox-cli.js +1 -4
- package/dist/scope.js +23 -0
- package/dist/serve/common.js +22 -1
- package/dist/serve/routes-chat.js +21 -1
- package/dist/serve/routes-memory.js +31 -2
- package/dist/serve/routes-meta.js +34 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +228 -0
- package/dist/thinking.js +142 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +36 -8
- package/dist/tui/InputBar.js +109 -36
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +77 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +7 -1
- package/dist/tui/layout.js +17 -0
- package/dist/tui/overlay-input.js +12 -0
- package/dist/tui/paste-draft.js +173 -0
- package/dist/tui/selection.js +8 -2
- package/dist/tui/slash-commands.js +18 -1
- package/dist/tui/slash-handler.js +61 -17
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +63 -7
- package/docs/API.md +50 -4
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +5 -4
- package/skills/self-config/reference.md +10 -5
package/dist/tui/InputBar.js
CHANGED
|
@@ -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 = (
|
|
109
|
-
historyRef.current = pushInput(historyRef.current,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
292
|
-
if (
|
|
293
|
-
submit(
|
|
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
|
|
421
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
540
|
-
|
|
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)} 命令` }) }),
|
|
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
|
}
|
package/dist/tui/MessageList.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
75
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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]
|
|
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.
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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))) }));
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
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, "
|
|
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
|
+
}
|