min-agent 0.3.0 → 0.4.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 +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tui/MessageList.js
CHANGED
|
@@ -2,10 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useMemo, useEffect, useRef, useState, useCallback } from "react";
|
|
3
3
|
import { Box, Text, useInput, useStdin } from "ink";
|
|
4
4
|
import { MarkdownRenderer } from "../markdown.js";
|
|
5
|
-
import { wrapByWidth,
|
|
5
|
+
import { wrapByWidth, clustersOf, displayWidth } from "./text-width.js";
|
|
6
|
+
import { messageTextWidth } from "./layout.js";
|
|
6
7
|
import { scanSgrMouse, WHEEL_UP, WHEEL_DOWN, MOTION } from "./mouse.js";
|
|
7
|
-
import { applySelection,
|
|
8
|
+
import { applySelection, selectionRanges, stripAnsi, textFromSelection, wordRangeAt, mouseToCell, splitRangeAt, alignPartSelections, visualPrefix, } from "./selection.js";
|
|
8
9
|
import { createDragMachine, clearDrag, stepDrag } from "./drag-state.js";
|
|
10
|
+
import { createClickCounter, noteClick } from "./click-count.js";
|
|
9
11
|
import { writeClipboard } from "../clipboard.js";
|
|
10
12
|
const RESET = "\x1b[0m";
|
|
11
13
|
/** Split a single ANSI-formatted line into physical rows of at most `maxWidth`
|
|
@@ -25,15 +27,14 @@ function splitAnsiRows(line, maxWidth) {
|
|
|
25
27
|
current += token;
|
|
26
28
|
continue;
|
|
27
29
|
}
|
|
28
|
-
for (const
|
|
29
|
-
const w = charWidth(ch);
|
|
30
|
+
for (const { text: g, width: w } of clustersOf(token)) {
|
|
30
31
|
if (width > 0 && width + w > maxWidth) {
|
|
31
32
|
rows.push(current + RESET);
|
|
32
|
-
current = prefix +
|
|
33
|
+
current = prefix + g;
|
|
33
34
|
width = w;
|
|
34
35
|
}
|
|
35
36
|
else {
|
|
36
|
-
current +=
|
|
37
|
+
current += g;
|
|
37
38
|
width += w;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -45,11 +46,10 @@ function splitAnsiRows(line, maxWidth) {
|
|
|
45
46
|
function clipToWidth(text, maxWidth) {
|
|
46
47
|
let shown = "";
|
|
47
48
|
let width = 0;
|
|
48
|
-
for (const
|
|
49
|
-
const w = charWidth(ch);
|
|
49
|
+
for (const { text: g, width: w } of clustersOf(text)) {
|
|
50
50
|
if (width + w > maxWidth)
|
|
51
51
|
break;
|
|
52
|
-
shown +=
|
|
52
|
+
shown += g;
|
|
53
53
|
width += w;
|
|
54
54
|
}
|
|
55
55
|
return shown;
|
|
@@ -74,34 +74,37 @@ function resultPreviewLines(result) {
|
|
|
74
74
|
* default; clicking the row (see MessageList) toggles `expanded` to reveal the
|
|
75
75
|
* full tool result. */
|
|
76
76
|
export function messageLines(msg, columns, expanded) {
|
|
77
|
-
const textWidth =
|
|
77
|
+
const textWidth = messageTextWidth(columns);
|
|
78
78
|
switch (msg.role) {
|
|
79
79
|
case "user":
|
|
80
80
|
return wrapByWidth(`> ${msg.content}`, textWidth);
|
|
81
81
|
case "assistant": {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
start
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
rows
|
|
101
|
-
|
|
82
|
+
try {
|
|
83
|
+
const md = new MarkdownRenderer();
|
|
84
|
+
const rendered = md.write(typeof msg.content === "string" ? msg.content : String(msg.content ?? "")) + md.flush();
|
|
85
|
+
const raw = rendered.replace(/\n$/, "");
|
|
86
|
+
if (raw === "")
|
|
87
|
+
return [];
|
|
88
|
+
const lines = raw.split("\n");
|
|
89
|
+
let start = 0;
|
|
90
|
+
let end = lines.length;
|
|
91
|
+
while (start < end && lines[start].trim() === "")
|
|
92
|
+
start++;
|
|
93
|
+
while (end > start && lines[end - 1].trim() === "")
|
|
94
|
+
end--;
|
|
95
|
+
if (start === end)
|
|
96
|
+
return [];
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (let i = start; i < end; i++)
|
|
99
|
+
rows.push(...splitAnsiRows(lines[i], textWidth));
|
|
100
|
+
return rows;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return wrapByWidth(typeof msg.content === "string" ? msg.content : String(msg.content ?? ""), textWidth);
|
|
104
|
+
}
|
|
102
105
|
}
|
|
103
106
|
case "thinking":
|
|
104
|
-
return msg.content
|
|
107
|
+
return msg.content?.trim() ? wrapByWidth(msg.content, textWidth) : [];
|
|
105
108
|
case "tool": {
|
|
106
109
|
const name = msg.toolName ?? "";
|
|
107
110
|
// The call summary is a single logical line; collapse any stray newlines
|
|
@@ -111,14 +114,18 @@ export function messageLines(msg, columns, expanded) {
|
|
|
111
114
|
const contentWidth = Math.max(1, textWidth - displayWidth(label));
|
|
112
115
|
if (!expanded) {
|
|
113
116
|
const tail = msg.toolResultSummary ? ` → ${msg.toolResultSummary}` : "";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const room = Math.max(1, contentWidth - displayWidth(tail) - 1);
|
|
119
|
-
return [`${label}${clipToWidth(call, room)}…${tail}`];
|
|
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)];
|
|
120
121
|
}
|
|
121
122
|
const rows = wrapByWidth(`${label}${call}`, textWidth);
|
|
123
|
+
if (msg.toolChildren && msg.toolChildren.length > 0) {
|
|
124
|
+
for (const child of msg.toolChildren) {
|
|
125
|
+
const summary = child.summary ? ` ${child.summary}` : "";
|
|
126
|
+
rows.push(...wrapByWidth(` ↳ ${child.name}${summary}`, textWidth));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
122
129
|
const result = msg.toolResult;
|
|
123
130
|
if (result && result.trim() !== "") {
|
|
124
131
|
for (const line of resultPreviewLines(result)) {
|
|
@@ -158,6 +165,7 @@ function cacheKey(msg, columns, expanded) {
|
|
|
158
165
|
expanded ? "1" : "0",
|
|
159
166
|
msg.toolName ?? "",
|
|
160
167
|
msg.toolResultSummary ?? "",
|
|
168
|
+
(msg.toolChildren ?? []).map((c) => `${c.name}:${c.summary}`).join("|"),
|
|
161
169
|
].join("\u0000");
|
|
162
170
|
}
|
|
163
171
|
function cachedMessageLines(msg, columns, expanded) {
|
|
@@ -179,11 +187,16 @@ const MARGIN = 1;
|
|
|
179
187
|
function messageRowCount(msg, columns, expanded) {
|
|
180
188
|
return cachedMessageLines(msg, columns, expanded).length + (msg.role === "user" ? MARGIN : 0);
|
|
181
189
|
}
|
|
190
|
+
/** Rows a window actually paints: content plus one indicator per hidden side. */
|
|
191
|
+
export function slicePaintRows(slice) {
|
|
192
|
+
return slice.winBottom - slice.winTop + (slice.above > 0 ? 1 : 0) + (slice.below > 0 ? 1 : 0);
|
|
193
|
+
}
|
|
182
194
|
/**
|
|
183
195
|
* Compute the visible window of a message list for `maxHeight` rows, given
|
|
184
196
|
* the physical row count of each message and an optional scroll offset
|
|
185
|
-
* (`null` = stick to the bottom).
|
|
186
|
-
* indicator
|
|
197
|
+
* (`null` = stick to the bottom). Each hidden side (above / below) takes its
|
|
198
|
+
* own indicator row, and those rows are subtracted from the content budget
|
|
199
|
+
* so the painted window never exceeds `maxHeight`.
|
|
187
200
|
*/
|
|
188
201
|
export function sliceWindow(messages, rowCounts, maxHeight, top) {
|
|
189
202
|
const total = rowCounts.reduce((a, b) => a + b, 0);
|
|
@@ -196,13 +209,16 @@ export function sliceWindow(messages, rowCounts, maxHeight, top) {
|
|
|
196
209
|
parts: messages.map((msg, i) => ({ msg, start: 0, count: rowCounts[i] ?? 0 })),
|
|
197
210
|
};
|
|
198
211
|
}
|
|
199
|
-
//
|
|
212
|
+
// Indicator rows depend on whether the window has hidden content on each
|
|
213
|
+
// side, which depends on view height — iterate until the chrome settles
|
|
214
|
+
// (0 → 1 → 2 indicators).
|
|
200
215
|
let above = 0;
|
|
201
216
|
let below = 0;
|
|
202
217
|
let winTop = 0;
|
|
203
218
|
let viewH = maxHeight;
|
|
204
|
-
for (let pass = 0; pass <
|
|
205
|
-
|
|
219
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
220
|
+
const chrome = (above > 0 ? 1 : 0) + (below > 0 ? 1 : 0);
|
|
221
|
+
viewH = Math.max(1, maxHeight - chrome);
|
|
206
222
|
const maxTop = Math.max(0, total - viewH);
|
|
207
223
|
winTop = top === null ? maxTop : Math.min(top, maxTop);
|
|
208
224
|
above = winTop;
|
|
@@ -228,7 +244,7 @@ export function sliceWindow(messages, rowCounts, maxHeight, top) {
|
|
|
228
244
|
* lost. Without `maxHeight` (terminal height unknown) messages flow
|
|
229
245
|
* naturally.
|
|
230
246
|
*/
|
|
231
|
-
export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice }) {
|
|
247
|
+
export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, selectionMode, onExitSelectionMode, interactive = true, }) {
|
|
232
248
|
const [windowTop, setWindowTop] = useState(null);
|
|
233
249
|
const [expanded, setExpanded] = useState(new Set());
|
|
234
250
|
const [selection, setSelection] = useState(null);
|
|
@@ -244,47 +260,29 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
244
260
|
const area = useMemo(() => buildArea(slice, columns, expanded), [slice, columns, expanded]);
|
|
245
261
|
const areaRef = useRef(area.rows);
|
|
246
262
|
areaRef.current = area.rows;
|
|
263
|
+
const kindsRef = useRef(area.kinds);
|
|
264
|
+
kindsRef.current = area.kinds;
|
|
265
|
+
const partMetaRef = useRef(area.partMeta);
|
|
266
|
+
partMetaRef.current = area.partMeta;
|
|
247
267
|
const onCopyRef = useRef(onCopyNotice);
|
|
248
268
|
onCopyRef.current = onCopyNotice;
|
|
269
|
+
const selectionModeRef = useRef(Boolean(selectionMode));
|
|
270
|
+
selectionModeRef.current = Boolean(selectionMode);
|
|
271
|
+
const lastCellRef = useRef(null);
|
|
272
|
+
const clicks = useRef(createClickCounter());
|
|
249
273
|
// Terminal coordinates (1-based) → selection module coordinates: rows index
|
|
250
274
|
// into `area.rows`, columns start at terminal col 2 (paddingLeft=1).
|
|
251
|
-
const toCell = (p) => (
|
|
275
|
+
const toCell = (p) => mouseToCell(area.rows, p.row, p.col);
|
|
252
276
|
// Per-part highlight ranges, aligned with each part's shown lines. Null
|
|
253
277
|
// while no selection is active so MessageRow keeps its memo hit.
|
|
254
278
|
const ranges = selection ? selectionRanges(area.rows, toCell(selection.anchor), toCell(selection.cur)) : null;
|
|
255
|
-
const partSelections = (
|
|
256
|
-
if (!ranges)
|
|
257
|
-
return null;
|
|
258
|
-
const map = new Map();
|
|
259
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
260
|
-
const meta = area.partMeta[i];
|
|
261
|
-
const range = ranges[i];
|
|
262
|
-
if (!meta || !range)
|
|
263
|
-
continue;
|
|
264
|
-
let arr = map.get(meta.part);
|
|
265
|
-
if (!arr) {
|
|
266
|
-
arr = [];
|
|
267
|
-
map.set(meta.part, arr);
|
|
268
|
-
}
|
|
269
|
-
arr[meta.shown] = range;
|
|
270
|
-
}
|
|
271
|
-
return map;
|
|
272
|
-
})();
|
|
279
|
+
const partSelections = ranges ? alignPartSelections(ranges, area.partMeta) : null;
|
|
273
280
|
// Mouse release after a drag: turn the terminal-coordinate selection into
|
|
274
281
|
// the selected text and push it to the clipboard. The highlight stays until
|
|
275
282
|
// the next click/key/wheel.
|
|
276
283
|
const finalizeCopy = useCallback((sel) => {
|
|
277
284
|
const rows = areaRef.current;
|
|
278
|
-
const
|
|
279
|
-
const parts = [];
|
|
280
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
281
|
-
const range = ranges[i];
|
|
282
|
-
if (range && rows[i])
|
|
283
|
-
parts.push(extractText(rows[i], range.start, range.end));
|
|
284
|
-
}
|
|
285
|
-
while (parts.length > 0 && parts[parts.length - 1] === "")
|
|
286
|
-
parts.pop();
|
|
287
|
-
const text = parts.join("\n");
|
|
285
|
+
const text = textFromSelection(rows, kindsRef.current, selectionRanges(rows, mouseToCell(rows, sel.anchor.row, sel.anchor.col), mouseToCell(rows, sel.cur.row, sel.cur.col)));
|
|
288
286
|
if (text === "")
|
|
289
287
|
onCopyRef.current?.("未选中文本");
|
|
290
288
|
else if (writeClipboard(text))
|
|
@@ -294,6 +292,50 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
294
292
|
}, []);
|
|
295
293
|
const finalizeRef = useRef(finalizeCopy);
|
|
296
294
|
finalizeRef.current = finalizeCopy;
|
|
295
|
+
const selectionRef = useRef(selection);
|
|
296
|
+
selectionRef.current = selection;
|
|
297
|
+
const onExitRef = useRef(onExitSelectionMode);
|
|
298
|
+
onExitRef.current = onExitSelectionMode;
|
|
299
|
+
const seedSelection = () => {
|
|
300
|
+
const current = selectionRef.current;
|
|
301
|
+
if (current)
|
|
302
|
+
return current;
|
|
303
|
+
if (lastCellRef.current)
|
|
304
|
+
return { anchor: lastCellRef.current, cur: lastCellRef.current };
|
|
305
|
+
const rows = areaRef.current;
|
|
306
|
+
const i = rows.findIndex((r) => r != null);
|
|
307
|
+
const cell = i >= 0 ? { row: i + 1, col: 2 } : { row: 1, col: 2 };
|
|
308
|
+
return { anchor: cell, cur: cell };
|
|
309
|
+
};
|
|
310
|
+
const clampSelection = (sel) => {
|
|
311
|
+
const n = Math.max(1, areaRef.current.length);
|
|
312
|
+
return {
|
|
313
|
+
anchor: sel.anchor,
|
|
314
|
+
cur: { row: Math.max(1, Math.min(n, sel.cur.row)), col: Math.max(1, sel.cur.col) },
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
const nudgeSelection = (drow, dcol) => {
|
|
318
|
+
const s = seedSelection();
|
|
319
|
+
const n = Math.max(1, areaRef.current.length);
|
|
320
|
+
let row = Math.max(1, Math.min(n, s.cur.row + drow));
|
|
321
|
+
if (drow !== 0) {
|
|
322
|
+
while (row >= 1 && row <= n && areaRef.current[row - 1] == null) {
|
|
323
|
+
const next = row + Math.sign(drow);
|
|
324
|
+
if (next < 1 || next > n)
|
|
325
|
+
break;
|
|
326
|
+
row = next;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const line = areaRef.current[row - 1];
|
|
330
|
+
const width = line ? displayWidth(stripAnsi(line)) : 0;
|
|
331
|
+
const col = Math.max(1, Math.min(2 + width, s.cur.col + dcol));
|
|
332
|
+
setSelection({ anchor: s.anchor, cur: { row, col } });
|
|
333
|
+
};
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (!selectionMode)
|
|
336
|
+
return;
|
|
337
|
+
setSelection((prev) => prev ?? seedSelection());
|
|
338
|
+
}, [selectionMode, seedSelection]);
|
|
297
339
|
const toggleExpanded = (id) => {
|
|
298
340
|
setExpanded((prev) => {
|
|
299
341
|
const next = new Set(prev);
|
|
@@ -322,14 +364,61 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
322
364
|
};
|
|
323
365
|
const drag = useRef(createDragMachine());
|
|
324
366
|
useInput((_input, key) => {
|
|
367
|
+
if (selectionModeRef.current) {
|
|
368
|
+
const inkKey = key;
|
|
369
|
+
if (key.escape) {
|
|
370
|
+
setSelection(null);
|
|
371
|
+
onExitRef.current?.();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (key.return) {
|
|
375
|
+
const sel = selectionRef.current ?? seedSelection();
|
|
376
|
+
finalizeRef.current(sel);
|
|
377
|
+
onExitRef.current?.();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (key.leftArrow) {
|
|
381
|
+
nudgeSelection(0, -1);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (key.rightArrow) {
|
|
385
|
+
nudgeSelection(0, 1);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (key.upArrow) {
|
|
389
|
+
nudgeSelection(-1, 0);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (key.downArrow) {
|
|
393
|
+
nudgeSelection(1, 0);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (inkKey.home) {
|
|
397
|
+
const s = seedSelection();
|
|
398
|
+
setSelection({ anchor: s.anchor, cur: { row: s.cur.row, col: 1 } });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (inkKey.end) {
|
|
402
|
+
const s = seedSelection();
|
|
403
|
+
const line = areaRef.current[s.cur.row - 1];
|
|
404
|
+
const width = line ? displayWidth(stripAnsi(line)) : 0;
|
|
405
|
+
setSelection({ anchor: s.anchor, cur: { row: s.cur.row, col: 2 + width } });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (key.pageUp) {
|
|
409
|
+
scrollUp(slice.winBottom - slice.winTop);
|
|
410
|
+
setSelection((prev) => (prev ? clampSelection(prev) : prev));
|
|
411
|
+
}
|
|
412
|
+
else if (key.pageDown) {
|
|
413
|
+
scrollDown(slice.winBottom - slice.winTop);
|
|
414
|
+
setSelection((prev) => (prev ? clampSelection(prev) : prev));
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
325
418
|
if (!_input.startsWith("[<") && !/^[\d;,]+[Mm]$/.test(_input)) {
|
|
326
419
|
clearDrag(drag.current);
|
|
327
420
|
setSelection(null);
|
|
328
421
|
}
|
|
329
|
-
// Enter on a tool row toggles expansion (keyboard alternative to click)
|
|
330
|
-
if (key.return && !_input) {
|
|
331
|
-
// No-op: tool toggle is click-only; keep Enter for app-level shortcuts
|
|
332
|
-
}
|
|
333
422
|
if (key.pageUp) {
|
|
334
423
|
const step = slice.winBottom - slice.winTop;
|
|
335
424
|
scrollUp(step);
|
|
@@ -338,7 +427,6 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
338
427
|
const step = slice.winBottom - slice.winTop;
|
|
339
428
|
scrollDown(step);
|
|
340
429
|
}
|
|
341
|
-
// Keyboard expand/collapse: when a tool message is at window center, toggle it
|
|
342
430
|
if ((_input === " " || _input === "o") && !key.ctrl && !key.meta) {
|
|
343
431
|
const center = Math.floor((slice.winTop + slice.winBottom) / 2);
|
|
344
432
|
let cursor = 0;
|
|
@@ -351,8 +439,8 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
351
439
|
cursor += len;
|
|
352
440
|
}
|
|
353
441
|
}
|
|
354
|
-
}, { isActive: Boolean(maxHeight && maxHeight > 0) });
|
|
355
|
-
// Scrolling back to the bottom when content shrinks (e.g. /
|
|
442
|
+
}, { isActive: Boolean(interactive && maxHeight && maxHeight > 0) });
|
|
443
|
+
// Scrolling back to the bottom when content shrinks (e.g. /new, /undo)
|
|
356
444
|
// so the view never hangs at a stale offset.
|
|
357
445
|
const prevMessageCount = useRef(messages.length);
|
|
358
446
|
useEffect(() => {
|
|
@@ -382,6 +470,42 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
382
470
|
offset -= part.count;
|
|
383
471
|
}
|
|
384
472
|
};
|
|
473
|
+
const selectWord = (row, col) => {
|
|
474
|
+
const rows = areaRef.current;
|
|
475
|
+
const cell = mouseToCell(rows, row, col);
|
|
476
|
+
const line = rows[cell.row];
|
|
477
|
+
if (!line)
|
|
478
|
+
return;
|
|
479
|
+
const range = wordRangeAt(stripAnsi(line), cell.col);
|
|
480
|
+
if (range.end <= range.start)
|
|
481
|
+
return;
|
|
482
|
+
const sel = {
|
|
483
|
+
anchor: { row: cell.row + 1, col: range.start + 2 },
|
|
484
|
+
cur: { row: cell.row + 1, col: range.end + 1 },
|
|
485
|
+
};
|
|
486
|
+
setSelection(sel);
|
|
487
|
+
finalizeRef.current(sel);
|
|
488
|
+
};
|
|
489
|
+
const selectMessage = (row) => {
|
|
490
|
+
const rowIndex = row - 1;
|
|
491
|
+
const meta = partMetaRef.current[rowIndex];
|
|
492
|
+
if (!meta)
|
|
493
|
+
return;
|
|
494
|
+
let start = rowIndex;
|
|
495
|
+
let end = rowIndex;
|
|
496
|
+
while (start > 0 && partMetaRef.current[start - 1]?.part === meta.part)
|
|
497
|
+
start--;
|
|
498
|
+
while (end < partMetaRef.current.length - 1 && partMetaRef.current[end + 1]?.part === meta.part)
|
|
499
|
+
end++;
|
|
500
|
+
const lastLine = areaRef.current[end];
|
|
501
|
+
const width = lastLine ? displayWidth(stripAnsi(lastLine)) : 0;
|
|
502
|
+
const sel = {
|
|
503
|
+
anchor: { row: start + 1, col: 2 },
|
|
504
|
+
cur: { row: end + 1, col: Math.max(2, 1 + width) },
|
|
505
|
+
};
|
|
506
|
+
setSelection(sel);
|
|
507
|
+
finalizeRef.current(sel);
|
|
508
|
+
};
|
|
385
509
|
// Mouse handling in the message area: drag with the left button selects text
|
|
386
510
|
// and auto-copies on release; a click (press+release without motion) still
|
|
387
511
|
// toggles tool rows; the wheel scrolls. The drag state machine (drag-state)
|
|
@@ -400,17 +524,23 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
400
524
|
let scanned;
|
|
401
525
|
while ((scanned = scanSgrMouse(wheelBuffer.current))) {
|
|
402
526
|
wheelBuffer.current = scanned.rest;
|
|
403
|
-
const { button, row
|
|
527
|
+
const { button, row } = scanned.event;
|
|
404
528
|
const areaHeight = areaRef.current.length;
|
|
405
529
|
// Wheel (base 64/65, or with the motion flag while a button is held).
|
|
406
|
-
if (button === WHEEL_UP ||
|
|
530
|
+
if (button === WHEEL_UP ||
|
|
531
|
+
button === WHEEL_DOWN ||
|
|
532
|
+
button === WHEEL_UP + MOTION ||
|
|
533
|
+
button === WHEEL_DOWN + MOTION) {
|
|
407
534
|
if (row >= 1 && row <= areaHeight + 2) {
|
|
408
535
|
clearDrag(drag.current);
|
|
409
|
-
|
|
536
|
+
if (!selectionModeRef.current)
|
|
537
|
+
setSelection(null);
|
|
410
538
|
if (button === WHEEL_UP || button === WHEEL_UP + MOTION)
|
|
411
539
|
scrollUp(3);
|
|
412
540
|
else
|
|
413
541
|
scrollDown(3);
|
|
542
|
+
if (selectionModeRef.current)
|
|
543
|
+
setSelection((prev) => (prev ? clampSelection(prev) : prev));
|
|
414
544
|
}
|
|
415
545
|
continue;
|
|
416
546
|
}
|
|
@@ -419,8 +549,16 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
419
549
|
setSelection(outcome.selection);
|
|
420
550
|
else if (outcome?.type === "copy")
|
|
421
551
|
finalizeRef.current(outcome.selection);
|
|
422
|
-
else if (outcome?.type === "click")
|
|
423
|
-
|
|
552
|
+
else if (outcome?.type === "click") {
|
|
553
|
+
lastCellRef.current = { row: outcome.row, col: outcome.col };
|
|
554
|
+
const count = noteClick(clicks.current, { row: outcome.row, col: outcome.col, at: Date.now() });
|
|
555
|
+
if (count >= 3)
|
|
556
|
+
selectMessage(outcome.row);
|
|
557
|
+
else if (count === 2)
|
|
558
|
+
selectWord(outcome.row, outcome.col);
|
|
559
|
+
else
|
|
560
|
+
handleClick(outcome.row);
|
|
561
|
+
}
|
|
424
562
|
}
|
|
425
563
|
if (wheelBuffer.current.length > 64) {
|
|
426
564
|
const esc = wheelBuffer.current.lastIndexOf("\x1b");
|
|
@@ -428,24 +566,32 @@ export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice })
|
|
|
428
566
|
}
|
|
429
567
|
};
|
|
430
568
|
stdin.on("data", handleData);
|
|
431
|
-
return () => {
|
|
432
|
-
|
|
569
|
+
return () => {
|
|
570
|
+
stdin.off("data", handleData);
|
|
571
|
+
};
|
|
572
|
+
}, [stdin, maxHeight, clampSelection, handleClick, scrollDown, scrollUp, selectMessage, selectWord]);
|
|
433
573
|
if (!maxHeight || maxHeight <= 0) {
|
|
434
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)))] }));
|
|
435
575
|
}
|
|
436
|
-
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" }) })), slice.above > 0 && (_jsx(Box, { paddingLeft: 1, 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, children: _jsx(Text, { color: "gray", dimColor: true, children: "\u2193 \u6309 PageDown \u56DE\u5230\u5E95\u90E8" }) }))] }));
|
|
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" }) }))] }));
|
|
437
577
|
}
|
|
438
578
|
/** Map a window slice (rows incl. margin) to renderable lines. */
|
|
439
579
|
function sliceLines(msg, lines, start, count = lines.length) {
|
|
440
580
|
if (msg.role !== "user")
|
|
441
|
-
return { lines: lines.slice(start, start + count), marginTop: false };
|
|
581
|
+
return { lines: lines.slice(start, start + count), marginTop: false, lineOffset: start };
|
|
442
582
|
const margin = MARGIN;
|
|
443
583
|
if (start === 0) {
|
|
444
|
-
|
|
445
|
-
return { lines: lines.slice(0, Math.max(0, count - margin)), marginTop: true };
|
|
584
|
+
return { lines: lines.slice(0, Math.max(0, count - margin)), marginTop: true, lineOffset: 0 };
|
|
446
585
|
}
|
|
447
586
|
const contentStart = start - margin;
|
|
448
|
-
return { lines: lines.slice(contentStart, contentStart + count), marginTop: false };
|
|
587
|
+
return { lines: lines.slice(contentStart, contentStart + count), marginTop: false, lineOffset: contentStart };
|
|
588
|
+
}
|
|
589
|
+
function copyKindFor(msg, lineIndex) {
|
|
590
|
+
if (msg.role === "user")
|
|
591
|
+
return lineIndex === 0 ? "user-head" : "plain";
|
|
592
|
+
if (msg.role === "tool")
|
|
593
|
+
return lineIndex === 0 ? "tool-head" : "tool-body";
|
|
594
|
+
return "plain";
|
|
449
595
|
}
|
|
450
596
|
/**
|
|
451
597
|
* Rebuild the exact rows the message list renders, one entry per terminal row:
|
|
@@ -455,29 +601,34 @@ function sliceLines(msg, lines, start, count = lines.length) {
|
|
|
455
601
|
*/
|
|
456
602
|
function buildArea(slice, columns, expanded) {
|
|
457
603
|
const rows = [];
|
|
604
|
+
const kinds = [];
|
|
458
605
|
const partMeta = [];
|
|
459
606
|
if (slice.above > 0) {
|
|
460
607
|
rows.push(null);
|
|
608
|
+
kinds.push("plain");
|
|
461
609
|
partMeta.push(null);
|
|
462
610
|
}
|
|
463
611
|
for (let p = 0; p < slice.parts.length; p++) {
|
|
464
612
|
const part = slice.parts[p];
|
|
465
613
|
const lines = cachedMessageLines(part.msg, columns, expanded.has(part.msg.id));
|
|
466
|
-
const { lines: shown, marginTop } = sliceLines(part.msg, lines, part.start, part.count);
|
|
614
|
+
const { lines: shown, marginTop, lineOffset } = sliceLines(part.msg, lines, part.start, part.count);
|
|
467
615
|
if (marginTop) {
|
|
468
616
|
rows.push(null);
|
|
617
|
+
kinds.push("plain");
|
|
469
618
|
partMeta.push(null);
|
|
470
619
|
}
|
|
471
620
|
for (let i = 0; i < shown.length; i++) {
|
|
472
621
|
rows.push(shown[i]);
|
|
622
|
+
kinds.push(copyKindFor(part.msg, lineOffset + i));
|
|
473
623
|
partMeta.push({ part: p, shown: i });
|
|
474
624
|
}
|
|
475
625
|
}
|
|
476
626
|
if (slice.below > 0) {
|
|
477
627
|
rows.push(null);
|
|
628
|
+
kinds.push("plain");
|
|
478
629
|
partMeta.push(null);
|
|
479
630
|
}
|
|
480
|
-
return { rows, partMeta };
|
|
631
|
+
return { rows, kinds, partMeta };
|
|
481
632
|
}
|
|
482
633
|
/** Message row — memoized so streaming updates to the last assistant message
|
|
483
634
|
* don't re-render every row. `selection` (aligned with the shown lines) adds
|
|
@@ -485,57 +636,47 @@ function buildArea(slice, columns, expanded) {
|
|
|
485
636
|
* the memo hit during streaming. */
|
|
486
637
|
const MessageRow = React.memo(function MessageRow({ message, lines, start = 0, count, selection, }) {
|
|
487
638
|
const { lines: shown, marginTop } = sliceLines(message, lines, start, count);
|
|
488
|
-
if (shown.length === 0
|
|
489
|
-
|
|
490
|
-
|
|
639
|
+
if (shown.length === 0) {
|
|
640
|
+
if (!marginTop)
|
|
641
|
+
return null;
|
|
642
|
+
return _jsx(Box, { height: 1, flexShrink: 0 });
|
|
643
|
+
}
|
|
644
|
+
const sel = selection;
|
|
491
645
|
const apply = (i, line) => {
|
|
492
646
|
const range = sel?.[i];
|
|
493
647
|
return range && range.end > range.start ? applySelection(line, range.start, range.end) : line;
|
|
494
648
|
};
|
|
495
|
-
|
|
496
|
-
// "⚡ name ▸ ") is rendered as its own Text before the content slice.
|
|
497
|
-
const splitFirst = (prefixLen) => {
|
|
649
|
+
const splitFirst = (prefix) => {
|
|
498
650
|
const range = sel?.[0];
|
|
499
651
|
if (!range)
|
|
500
|
-
return {
|
|
501
|
-
|
|
502
|
-
? { start: range.start, end: Math.min(prefixLen, range.end) }
|
|
503
|
-
: null;
|
|
504
|
-
const contentRange = range.end > prefixLen
|
|
505
|
-
? { start: Math.max(0, range.start - prefixLen), end: range.end - prefixLen }
|
|
506
|
-
: null;
|
|
507
|
-
return { prefix: headRange, content: contentRange };
|
|
652
|
+
return { head: null, tail: null };
|
|
653
|
+
return splitRangeAt(range, displayWidth(prefix));
|
|
508
654
|
};
|
|
655
|
+
const paintPrefix = (prefix, head) => head ? applySelection(prefix, head.start, head.end) : prefix;
|
|
656
|
+
const paintBody = (body, tail) => tail ? applySelection(body, tail.start, tail.end) : body;
|
|
509
657
|
switch (message.role) {
|
|
510
658
|
case "user": {
|
|
511
659
|
const first = shown[0] ?? "";
|
|
512
660
|
const rest = shown.slice(1);
|
|
513
|
-
const
|
|
514
|
-
const split =
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
? applySelection(first.slice(2), split.content.start, split.content.end)
|
|
518
|
-
: first.slice(2)
|
|
519
|
-
: apply(0, first), rest.length > 0 ? `\n${rest.map((l, k) => apply(k + 1, l)).join("\n")}` : ""] })] }));
|
|
661
|
+
const prefix = visualPrefix(first, "user");
|
|
662
|
+
const split = prefix ? splitFirst(prefix) : null;
|
|
663
|
+
const headBody = prefix ? paintBody(first.slice(prefix.length), split.tail) : apply(0, first);
|
|
664
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: marginTop ? 1 : 0, flexShrink: 0, children: [_jsxs(Box, { flexWrap: "nowrap", children: [prefix !== "" && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "cyan", bold: true, children: paintPrefix(prefix, split.head) }) })), _jsx(Text, { wrap: "truncate-end", children: headBody })] }), rest.map((l, k) => (_jsx(Text, { wrap: "truncate-end", children: apply(k + 1, l) }, k)))] }));
|
|
520
665
|
}
|
|
521
666
|
case "assistant":
|
|
522
|
-
return (_jsx(Box, { paddingLeft: 1,
|
|
667
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: shown.map((l, i) => (_jsx(Text, { wrap: "truncate-end", children: apply(i, l) }, i))) }));
|
|
523
668
|
case "thinking":
|
|
524
|
-
return (_jsx(Box, {
|
|
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))) }));
|
|
525
670
|
case "tool": {
|
|
526
671
|
const first = shown[0] ?? "";
|
|
527
672
|
const rest = shown.slice(1);
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
return (_jsxs(Box, { paddingLeft: 1, children: [
|
|
532
|
-
? split.content
|
|
533
|
-
? applySelection(first.slice(prefixLen), split.content.start, split.content.end)
|
|
534
|
-
: first.slice(prefixLen)
|
|
535
|
-
: apply(0, first), rest.length > 0 ? `\n${rest.map((l, k) => apply(k + 1, l)).join("\n")}` : ""] })] }));
|
|
673
|
+
const prefix = visualPrefix(first, "tool");
|
|
674
|
+
const split = prefix ? splitFirst(prefix) : null;
|
|
675
|
+
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)))] }));
|
|
536
677
|
}
|
|
537
678
|
case "system":
|
|
538
|
-
return (_jsx(Box, { paddingLeft: 1,
|
|
679
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, flexShrink: 0, children: shown.map((l, i) => (_jsx(Text, { wrap: "truncate-end", children: apply(i, l) }, i))) }));
|
|
539
680
|
default:
|
|
540
681
|
return null;
|
|
541
682
|
}
|