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.
- package/README.md +46 -2
- package/dist/agent.js +89 -29
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +32 -7
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- 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 +57 -14
- package/dist/cli/setup/detect.js +17 -0
- package/dist/cli/setup/flags.js +12 -0
- package/dist/cli/setup/flow.js +108 -0
- package/dist/cli/setup/provider-form.js +102 -0
- package/dist/cli/setup/ui.js +534 -0
- package/dist/code-mode.js +1 -1
- package/dist/config.js +93 -159
- package/dist/context-window.js +39 -49
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -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 +69 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +239 -0
- package/dist/thinking.js +166 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +48 -8
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +112 -37
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +75 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +13 -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 +24 -1
- package/dist/tui/slash-handler.js +88 -18
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +85 -7
- package/docs/API.md +69 -6
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- 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-cli-setup-design.md +282 -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 +7 -4
- package/skills/self-config/reference.md +12 -6
package/dist/tui/InputBar.js
CHANGED
|
@@ -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 = (
|
|
109
|
-
historyRef.current = pushInput(historyRef.current,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
292
|
-
if (
|
|
293
|
-
submit(
|
|
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
|
|
421
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
540
|
-
|
|
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)} 命令` }) }),
|
|
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
|
}
|
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,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
|
+
}
|