min-agent 0.2.1 → 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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- 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 +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- 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 +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- 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-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -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 +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tui/MessageList.js
CHANGED
|
@@ -1,33 +1,687 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import React, { useMemo, useEffect, useRef, useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdin } from "ink";
|
|
3
4
|
import { MarkdownRenderer } from "../markdown.js";
|
|
5
|
+
import { wrapByWidth, clustersOf, displayWidth } from "./text-width.js";
|
|
6
|
+
import { messageTextWidth } from "./layout.js";
|
|
7
|
+
import { scanSgrMouse, WHEEL_UP, WHEEL_DOWN, MOTION } from "./mouse.js";
|
|
8
|
+
import { applySelection, selectionRanges, stripAnsi, textFromSelection, wordRangeAt, mouseToCell, splitRangeAt, alignPartSelections, visualPrefix, } from "./selection.js";
|
|
9
|
+
import { createDragMachine, clearDrag, stepDrag } from "./drag-state.js";
|
|
10
|
+
import { createClickCounter, noteClick } from "./click-count.js";
|
|
11
|
+
import { writeClipboard } from "../clipboard.js";
|
|
12
|
+
const RESET = "\x1b[0m";
|
|
13
|
+
/** Split a single ANSI-formatted line into physical rows of at most `maxWidth`
|
|
14
|
+
* cells. Each row re-applies the styles active at its start and ends with a
|
|
15
|
+
* reset, so a window sliced in the middle of a message keeps correct colors. */
|
|
16
|
+
function splitAnsiRows(line, maxWidth) {
|
|
17
|
+
const rows = [];
|
|
18
|
+
let current = "";
|
|
19
|
+
let width = 0;
|
|
20
|
+
let prefix = "";
|
|
21
|
+
for (const token of line.split(/(\x1b\[[0-9;]*m)/g)) {
|
|
22
|
+
if (token.startsWith("\x1b[")) {
|
|
23
|
+
if (token === RESET)
|
|
24
|
+
prefix = "";
|
|
25
|
+
else
|
|
26
|
+
prefix += token;
|
|
27
|
+
current += token;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const { text: g, width: w } of clustersOf(token)) {
|
|
31
|
+
if (width > 0 && width + w > maxWidth) {
|
|
32
|
+
rows.push(current + RESET);
|
|
33
|
+
current = prefix + g;
|
|
34
|
+
width = w;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
current += g;
|
|
38
|
+
width += w;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
rows.push(current + RESET);
|
|
43
|
+
return rows;
|
|
44
|
+
}
|
|
45
|
+
/** Longest prefix of `text` that fits in `maxWidth` cells. */
|
|
46
|
+
function clipToWidth(text, maxWidth) {
|
|
47
|
+
let shown = "";
|
|
48
|
+
let width = 0;
|
|
49
|
+
for (const { text: g, width: w } of clustersOf(text)) {
|
|
50
|
+
if (width + w > maxWidth)
|
|
51
|
+
break;
|
|
52
|
+
shown += g;
|
|
53
|
+
width += w;
|
|
54
|
+
}
|
|
55
|
+
return shown;
|
|
56
|
+
}
|
|
57
|
+
/** Rows shown for an expanded tool result — enough to read a result list. */
|
|
58
|
+
const TOOL_RESULT_MAX_LINES = 120;
|
|
59
|
+
/**
|
|
60
|
+
* Cap an expanded tool result so a 50KB output cannot take over the viewport.
|
|
61
|
+
* Tools already persist their full output to disk when they truncate.
|
|
62
|
+
*/
|
|
63
|
+
function resultPreviewLines(result) {
|
|
64
|
+
const lines = result.replace(/\s+$/, "").split("\n");
|
|
65
|
+
if (lines.length <= TOOL_RESULT_MAX_LINES)
|
|
66
|
+
return lines;
|
|
67
|
+
const hidden = lines.length - TOOL_RESULT_MAX_LINES;
|
|
68
|
+
return [...lines.slice(0, TOOL_RESULT_MAX_LINES), `… 结果还有 ${hidden} 行未显示`];
|
|
69
|
+
}
|
|
70
|
+
/** Physical rows for one message. The leading marker ("> " / "⚡ name ") lives
|
|
71
|
+
* in the first row only, mirroring the JSX layout below.
|
|
72
|
+
*
|
|
73
|
+
* Tool calls render as a single row (arguments plus a result summary) by
|
|
74
|
+
* default; clicking the row (see MessageList) toggles `expanded` to reveal the
|
|
75
|
+
* full tool result. */
|
|
76
|
+
export function messageLines(msg, columns, expanded) {
|
|
77
|
+
const textWidth = messageTextWidth(columns);
|
|
78
|
+
switch (msg.role) {
|
|
79
|
+
case "user":
|
|
80
|
+
return wrapByWidth(`> ${msg.content}`, textWidth);
|
|
81
|
+
case "assistant": {
|
|
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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
case "thinking":
|
|
107
|
+
return msg.content?.trim() ? wrapByWidth(msg.content, textWidth) : [];
|
|
108
|
+
case "tool": {
|
|
109
|
+
const name = msg.toolName ?? "";
|
|
110
|
+
// The call summary is a single logical line; collapse any stray newlines
|
|
111
|
+
// so it cannot push the row layout around.
|
|
112
|
+
const call = msg.content.replace(/\s+/g, " ").trim();
|
|
113
|
+
const label = `⚡ ${name} ${expanded ? "▾" : "▸"} `;
|
|
114
|
+
const contentWidth = Math.max(1, textWidth - displayWidth(label));
|
|
115
|
+
if (!expanded) {
|
|
116
|
+
const tail = msg.toolResultSummary ? ` → ${msg.toolResultSummary}` : "";
|
|
117
|
+
const packed = displayWidth(call + tail) <= contentWidth
|
|
118
|
+
? `${label}${call}${tail}`
|
|
119
|
+
: `${label}${clipToWidth(call, Math.max(1, contentWidth - displayWidth(tail) - 1))}…${tail}`;
|
|
120
|
+
return [clipToWidth(packed, textWidth)];
|
|
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
|
+
}
|
|
129
|
+
const result = msg.toolResult;
|
|
130
|
+
if (result && result.trim() !== "") {
|
|
131
|
+
for (const line of resultPreviewLines(result)) {
|
|
132
|
+
rows.push(...wrapByWidth(` ${line}`, textWidth));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return rows;
|
|
136
|
+
}
|
|
137
|
+
default: {
|
|
138
|
+
if (!msg.content)
|
|
139
|
+
return [];
|
|
140
|
+
const rows = [];
|
|
141
|
+
for (const line of msg.content.split("\n"))
|
|
142
|
+
rows.push(...wrapByWidth(line, textWidth));
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Streaming appends mutate the last assistant message every frame; keying on
|
|
148
|
+
// id + content means the cache stays valid for the exact content rendered.
|
|
149
|
+
const LINE_CACHE_LIMIT = 256;
|
|
150
|
+
const lineCache = new Map();
|
|
151
|
+
function hashContent(s) {
|
|
152
|
+
let h = 0;
|
|
153
|
+
for (let i = 0; i < s.length; i++)
|
|
154
|
+
h = (h * 31 + s.charCodeAt(i)) | 0;
|
|
155
|
+
return String(h);
|
|
156
|
+
}
|
|
157
|
+
function cacheKey(msg, columns, expanded) {
|
|
158
|
+
return [
|
|
159
|
+
msg.id,
|
|
160
|
+
String(msg.content.length),
|
|
161
|
+
hashContent(msg.content.slice(0, 64) + msg.content.slice(-32)),
|
|
162
|
+
String(msg.toolResult?.length ?? 0),
|
|
163
|
+
msg.toolResult ? hashContent(msg.toolResult.slice(0, 64)) : "",
|
|
164
|
+
String(columns),
|
|
165
|
+
expanded ? "1" : "0",
|
|
166
|
+
msg.toolName ?? "",
|
|
167
|
+
msg.toolResultSummary ?? "",
|
|
168
|
+
(msg.toolChildren ?? []).map((c) => `${c.name}:${c.summary}`).join("|"),
|
|
169
|
+
].join("\u0000");
|
|
170
|
+
}
|
|
171
|
+
function cachedMessageLines(msg, columns, expanded) {
|
|
172
|
+
const key = cacheKey(msg, columns, expanded);
|
|
173
|
+
const hit = lineCache.get(key);
|
|
174
|
+
if (hit !== undefined)
|
|
175
|
+
return hit;
|
|
176
|
+
const lines = messageLines(msg, columns, expanded);
|
|
177
|
+
if (lineCache.size >= LINE_CACHE_LIMIT) {
|
|
178
|
+
const first = lineCache.keys().next().value;
|
|
179
|
+
if (first !== undefined)
|
|
180
|
+
lineCache.delete(first);
|
|
181
|
+
}
|
|
182
|
+
lineCache.set(key, lines);
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
/** User messages carry a 1-row margin above them. */
|
|
186
|
+
const MARGIN = 1;
|
|
187
|
+
function messageRowCount(msg, columns, expanded) {
|
|
188
|
+
return cachedMessageLines(msg, columns, expanded).length + (msg.role === "user" ? MARGIN : 0);
|
|
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
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compute the visible window of a message list for `maxHeight` rows, given
|
|
196
|
+
* the physical row count of each message and an optional scroll offset
|
|
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`.
|
|
200
|
+
*/
|
|
201
|
+
export function sliceWindow(messages, rowCounts, maxHeight, top) {
|
|
202
|
+
const total = rowCounts.reduce((a, b) => a + b, 0);
|
|
203
|
+
if (maxHeight <= 0) {
|
|
204
|
+
return {
|
|
205
|
+
winTop: 0,
|
|
206
|
+
winBottom: total,
|
|
207
|
+
above: 0,
|
|
208
|
+
below: 0,
|
|
209
|
+
parts: messages.map((msg, i) => ({ msg, start: 0, count: rowCounts[i] ?? 0 })),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
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).
|
|
215
|
+
let above = 0;
|
|
216
|
+
let below = 0;
|
|
217
|
+
let winTop = 0;
|
|
218
|
+
let viewH = maxHeight;
|
|
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);
|
|
222
|
+
const maxTop = Math.max(0, total - viewH);
|
|
223
|
+
winTop = top === null ? maxTop : Math.min(top, maxTop);
|
|
224
|
+
above = winTop;
|
|
225
|
+
below = Math.max(0, total - (winTop + viewH));
|
|
226
|
+
}
|
|
227
|
+
const winBottom = Math.min(total, winTop + viewH);
|
|
228
|
+
const parts = [];
|
|
229
|
+
let cursor = 0;
|
|
230
|
+
for (let i = 0; i < messages.length; i++) {
|
|
231
|
+
const len = rowCounts[i] ?? 0;
|
|
232
|
+
if (cursor < winBottom && cursor + len > winTop) {
|
|
233
|
+
const start = Math.max(0, winTop - cursor);
|
|
234
|
+
const count = Math.min(len - start, winBottom - cursor - start);
|
|
235
|
+
parts.push({ msg: messages[i], start, count });
|
|
236
|
+
}
|
|
237
|
+
cursor += len;
|
|
238
|
+
}
|
|
239
|
+
return { winTop, winBottom, above, below, parts };
|
|
240
|
+
}
|
|
4
241
|
/**
|
|
5
|
-
* Message list — renders
|
|
6
|
-
*
|
|
242
|
+
* Message list — renders the tail of the conversation with row-level
|
|
243
|
+
* scrolling (PageUp/PageDown and mouse wheel) so trimmed content is never
|
|
244
|
+
* lost. Without `maxHeight` (terminal height unknown) messages flow
|
|
245
|
+
* naturally.
|
|
7
246
|
*/
|
|
8
|
-
export function MessageList({ messages }) {
|
|
9
|
-
|
|
247
|
+
export function MessageList({ messages, maxHeight, columns = 80, onCopyNotice, selectionMode, onExitSelectionMode, interactive = true, }) {
|
|
248
|
+
const [windowTop, setWindowTop] = useState(null);
|
|
249
|
+
const [expanded, setExpanded] = useState(new Set());
|
|
250
|
+
const [selection, setSelection] = useState(null);
|
|
251
|
+
const rowCounts = useMemo(() => messages.map((m) => messageRowCount(m, columns, expanded.has(m.id))), [messages, columns, expanded]);
|
|
252
|
+
const slice = useMemo(() => sliceWindow(messages, rowCounts, maxHeight ?? 0, windowTop), [messages, rowCounts, maxHeight, windowTop]);
|
|
253
|
+
// Mouse wheel needs the current window geometry in the raw stdin listener,
|
|
254
|
+
// which subscribes once per maxHeight change.
|
|
255
|
+
const sliceRef = useRef(slice);
|
|
256
|
+
sliceRef.current = slice;
|
|
257
|
+
// The area mirrors the rendered rows; the same listener also maps drag
|
|
258
|
+
// coordinates to text. Rebuilt every render so streaming/scroll never
|
|
259
|
+
// desync the highlight from what is on screen.
|
|
260
|
+
const area = useMemo(() => buildArea(slice, columns, expanded), [slice, columns, expanded]);
|
|
261
|
+
const areaRef = useRef(area.rows);
|
|
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;
|
|
267
|
+
const onCopyRef = useRef(onCopyNotice);
|
|
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());
|
|
273
|
+
// Terminal coordinates (1-based) → selection module coordinates: rows index
|
|
274
|
+
// into `area.rows`, columns start at terminal col 2 (paddingLeft=1).
|
|
275
|
+
const toCell = (p) => mouseToCell(area.rows, p.row, p.col);
|
|
276
|
+
// Per-part highlight ranges, aligned with each part's shown lines. Null
|
|
277
|
+
// while no selection is active so MessageRow keeps its memo hit.
|
|
278
|
+
const ranges = selection ? selectionRanges(area.rows, toCell(selection.anchor), toCell(selection.cur)) : null;
|
|
279
|
+
const partSelections = ranges ? alignPartSelections(ranges, area.partMeta) : null;
|
|
280
|
+
// Mouse release after a drag: turn the terminal-coordinate selection into
|
|
281
|
+
// the selected text and push it to the clipboard. The highlight stays until
|
|
282
|
+
// the next click/key/wheel.
|
|
283
|
+
const finalizeCopy = useCallback((sel) => {
|
|
284
|
+
const rows = areaRef.current;
|
|
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)));
|
|
286
|
+
if (text === "")
|
|
287
|
+
onCopyRef.current?.("未选中文本");
|
|
288
|
+
else if (writeClipboard(text))
|
|
289
|
+
onCopyRef.current?.(`已复制 ${Array.from(text).length} 字`);
|
|
290
|
+
else
|
|
291
|
+
onCopyRef.current?.("复制失败");
|
|
292
|
+
}, []);
|
|
293
|
+
const finalizeRef = useRef(finalizeCopy);
|
|
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]);
|
|
339
|
+
const toggleExpanded = (id) => {
|
|
340
|
+
setExpanded((prev) => {
|
|
341
|
+
const next = new Set(prev);
|
|
342
|
+
if (next.has(id))
|
|
343
|
+
next.delete(id);
|
|
344
|
+
else
|
|
345
|
+
next.add(id);
|
|
346
|
+
return next;
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
const toggleRef = useRef(toggleExpanded);
|
|
350
|
+
toggleRef.current = toggleExpanded;
|
|
351
|
+
const scrollUp = (step) => {
|
|
352
|
+
setWindowTop((t) => {
|
|
353
|
+
const cur = t ?? sliceRef.current.winTop;
|
|
354
|
+
return Math.max(0, cur - step);
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
const scrollDown = (step) => {
|
|
358
|
+
setWindowTop((t) => {
|
|
359
|
+
if (t === null)
|
|
360
|
+
return t;
|
|
361
|
+
const maxTop = sliceRef.current.winTop + sliceRef.current.below;
|
|
362
|
+
return t + step >= maxTop ? null : t + step;
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
const drag = useRef(createDragMachine());
|
|
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
|
+
}
|
|
418
|
+
if (!_input.startsWith("[<") && !/^[\d;,]+[Mm]$/.test(_input)) {
|
|
419
|
+
clearDrag(drag.current);
|
|
420
|
+
setSelection(null);
|
|
421
|
+
}
|
|
422
|
+
if (key.pageUp) {
|
|
423
|
+
const step = slice.winBottom - slice.winTop;
|
|
424
|
+
scrollUp(step);
|
|
425
|
+
}
|
|
426
|
+
else if (key.pageDown) {
|
|
427
|
+
const step = slice.winBottom - slice.winTop;
|
|
428
|
+
scrollDown(step);
|
|
429
|
+
}
|
|
430
|
+
if ((_input === " " || _input === "o") && !key.ctrl && !key.meta) {
|
|
431
|
+
const center = Math.floor((slice.winTop + slice.winBottom) / 2);
|
|
432
|
+
let cursor = 0;
|
|
433
|
+
for (let i = 0; i < messages.length; i++) {
|
|
434
|
+
const len = rowCounts[i] ?? 0;
|
|
435
|
+
if (center >= cursor && center < cursor + len && messages[i].role === "tool") {
|
|
436
|
+
toggleRef.current(messages[i].id);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
cursor += len;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}, { isActive: Boolean(interactive && maxHeight && maxHeight > 0) });
|
|
443
|
+
// Scrolling back to the bottom when content shrinks (e.g. /new, /undo)
|
|
444
|
+
// so the view never hangs at a stale offset.
|
|
445
|
+
const prevMessageCount = useRef(messages.length);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (messages.length < prevMessageCount.current)
|
|
448
|
+
setWindowTop(null);
|
|
449
|
+
prevMessageCount.current = messages.length;
|
|
450
|
+
}, [messages]);
|
|
451
|
+
// Map a terminal row (1-based) to the visible message row it lands on and
|
|
452
|
+
// toggle its expansion. Only tool rows are clickable; the top indicator row
|
|
453
|
+
// and anything below the window are ignored.
|
|
454
|
+
const handleClick = (row) => {
|
|
455
|
+
const { above, parts } = sliceRef.current;
|
|
456
|
+
let offset = row - 1;
|
|
457
|
+
if (offset < 0)
|
|
458
|
+
return;
|
|
459
|
+
if (above > 0) {
|
|
460
|
+
offset -= 1;
|
|
461
|
+
if (offset < 0)
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const part of parts) {
|
|
465
|
+
if (offset < part.count) {
|
|
466
|
+
if (part.msg.role === "tool")
|
|
467
|
+
toggleRef.current(part.msg.id);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
offset -= part.count;
|
|
471
|
+
}
|
|
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
|
+
};
|
|
509
|
+
// Mouse handling in the message area: drag with the left button selects text
|
|
510
|
+
// 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)
|
|
512
|
+
// is fed every non-wheel SGR event; presses are accepted anywhere inside the
|
|
513
|
+
// allocated message region (maxHeight), so a drag that starts on the blank
|
|
514
|
+
// gap below a short conversation still works. A drag/release may land
|
|
515
|
+
// outside the region, in which case the coordinates are clamped by the
|
|
516
|
+
// selection math.
|
|
517
|
+
const { stdin } = useStdin();
|
|
518
|
+
const wheelBuffer = useRef("");
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (!stdin || !maxHeight || maxHeight <= 0)
|
|
521
|
+
return;
|
|
522
|
+
const handleData = (data) => {
|
|
523
|
+
wheelBuffer.current += data.toString("utf-8");
|
|
524
|
+
let scanned;
|
|
525
|
+
while ((scanned = scanSgrMouse(wheelBuffer.current))) {
|
|
526
|
+
wheelBuffer.current = scanned.rest;
|
|
527
|
+
const { button, row } = scanned.event;
|
|
528
|
+
const areaHeight = areaRef.current.length;
|
|
529
|
+
// Wheel (base 64/65, or with the motion flag while a button is held).
|
|
530
|
+
if (button === WHEEL_UP ||
|
|
531
|
+
button === WHEEL_DOWN ||
|
|
532
|
+
button === WHEEL_UP + MOTION ||
|
|
533
|
+
button === WHEEL_DOWN + MOTION) {
|
|
534
|
+
if (row >= 1 && row <= areaHeight + 2) {
|
|
535
|
+
clearDrag(drag.current);
|
|
536
|
+
if (!selectionModeRef.current)
|
|
537
|
+
setSelection(null);
|
|
538
|
+
if (button === WHEEL_UP || button === WHEEL_UP + MOTION)
|
|
539
|
+
scrollUp(3);
|
|
540
|
+
else
|
|
541
|
+
scrollDown(3);
|
|
542
|
+
if (selectionModeRef.current)
|
|
543
|
+
setSelection((prev) => (prev ? clampSelection(prev) : prev));
|
|
544
|
+
}
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const outcome = stepDrag(drag.current, scanned.event, maxHeight);
|
|
548
|
+
if (outcome?.type === "selection")
|
|
549
|
+
setSelection(outcome.selection);
|
|
550
|
+
else if (outcome?.type === "copy")
|
|
551
|
+
finalizeRef.current(outcome.selection);
|
|
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
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (wheelBuffer.current.length > 64) {
|
|
564
|
+
const esc = wheelBuffer.current.lastIndexOf("\x1b");
|
|
565
|
+
wheelBuffer.current = esc === -1 ? "" : wheelBuffer.current.slice(esc);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
stdin.on("data", handleData);
|
|
569
|
+
return () => {
|
|
570
|
+
stdin.off("data", handleData);
|
|
571
|
+
};
|
|
572
|
+
}, [stdin, maxHeight, clampSelection, handleClick, scrollDown, scrollUp, selectMessage, selectWord]);
|
|
573
|
+
if (!maxHeight || maxHeight <= 0) {
|
|
574
|
+
return (_jsxs(Box, { flexDirection: "column", children: [messages.length === 0 && (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Text, { color: "gray", children: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\uFF0C\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CEsc \u53D6\u6D88\u8FD0\u884C" }) })), messages.map((msg) => (_jsx(MessageRow, { message: msg, lines: cachedMessageLines(msg, columns, expanded.has(msg.id)) }, msg.id)))] }));
|
|
575
|
+
}
|
|
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" }) }))] }));
|
|
577
|
+
}
|
|
578
|
+
/** Map a window slice (rows incl. margin) to renderable lines. */
|
|
579
|
+
function sliceLines(msg, lines, start, count = lines.length) {
|
|
580
|
+
if (msg.role !== "user")
|
|
581
|
+
return { lines: lines.slice(start, start + count), marginTop: false, lineOffset: start };
|
|
582
|
+
const margin = MARGIN;
|
|
583
|
+
if (start === 0) {
|
|
584
|
+
return { lines: lines.slice(0, Math.max(0, count - margin)), marginTop: true, lineOffset: 0 };
|
|
585
|
+
}
|
|
586
|
+
const contentStart = start - margin;
|
|
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";
|
|
10
595
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
596
|
+
/**
|
|
597
|
+
* Rebuild the exact rows the message list renders, one entry per terminal row:
|
|
598
|
+
* chrome/blank rows are null, message rows carry their ANSI line. `partMeta`
|
|
599
|
+
* maps each row back to (part index, shown-line index) so selection ranges can
|
|
600
|
+
* be handed to MessageRow aligned with its own `lines` slicing.
|
|
601
|
+
*/
|
|
602
|
+
function buildArea(slice, columns, expanded) {
|
|
603
|
+
const rows = [];
|
|
604
|
+
const kinds = [];
|
|
605
|
+
const partMeta = [];
|
|
606
|
+
if (slice.above > 0) {
|
|
607
|
+
rows.push(null);
|
|
608
|
+
kinds.push("plain");
|
|
609
|
+
partMeta.push(null);
|
|
610
|
+
}
|
|
611
|
+
for (let p = 0; p < slice.parts.length; p++) {
|
|
612
|
+
const part = slice.parts[p];
|
|
613
|
+
const lines = cachedMessageLines(part.msg, columns, expanded.has(part.msg.id));
|
|
614
|
+
const { lines: shown, marginTop, lineOffset } = sliceLines(part.msg, lines, part.start, part.count);
|
|
615
|
+
if (marginTop) {
|
|
616
|
+
rows.push(null);
|
|
617
|
+
kinds.push("plain");
|
|
618
|
+
partMeta.push(null);
|
|
619
|
+
}
|
|
620
|
+
for (let i = 0; i < shown.length; i++) {
|
|
621
|
+
rows.push(shown[i]);
|
|
622
|
+
kinds.push(copyKindFor(part.msg, lineOffset + i));
|
|
623
|
+
partMeta.push({ part: p, shown: i });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (slice.below > 0) {
|
|
627
|
+
rows.push(null);
|
|
628
|
+
kinds.push("plain");
|
|
629
|
+
partMeta.push(null);
|
|
630
|
+
}
|
|
631
|
+
return { rows, kinds, partMeta };
|
|
17
632
|
}
|
|
18
|
-
|
|
633
|
+
/** Message row — memoized so streaming updates to the last assistant message
|
|
634
|
+
* don't re-render every row. `selection` (aligned with the shown lines) adds
|
|
635
|
+
* inverse-video highlights; it is undefined when no drag is active, keeping
|
|
636
|
+
* the memo hit during streaming. */
|
|
637
|
+
const MessageRow = React.memo(function MessageRow({ message, lines, start = 0, count, selection, }) {
|
|
638
|
+
const { lines: shown, marginTop } = sliceLines(message, lines, start, count);
|
|
639
|
+
if (shown.length === 0) {
|
|
640
|
+
if (!marginTop)
|
|
641
|
+
return null;
|
|
642
|
+
return _jsx(Box, { height: 1, flexShrink: 0 });
|
|
643
|
+
}
|
|
644
|
+
const sel = selection;
|
|
645
|
+
const apply = (i, line) => {
|
|
646
|
+
const range = sel?.[i];
|
|
647
|
+
return range && range.end > range.start ? applySelection(line, range.start, range.end) : line;
|
|
648
|
+
};
|
|
649
|
+
const splitFirst = (prefix) => {
|
|
650
|
+
const range = sel?.[0];
|
|
651
|
+
if (!range)
|
|
652
|
+
return { head: null, tail: null };
|
|
653
|
+
return splitRangeAt(range, displayWidth(prefix));
|
|
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;
|
|
19
657
|
switch (message.role) {
|
|
20
|
-
case "user":
|
|
21
|
-
|
|
658
|
+
case "user": {
|
|
659
|
+
const first = shown[0] ?? "";
|
|
660
|
+
const rest = shown.slice(1);
|
|
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)))] }));
|
|
665
|
+
}
|
|
22
666
|
case "assistant":
|
|
23
|
-
return (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { children:
|
|
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))) }));
|
|
24
668
|
case "thinking":
|
|
25
|
-
return (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { dimColor: true, children:
|
|
26
|
-
case "tool":
|
|
27
|
-
|
|
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
|
+
case "tool": {
|
|
671
|
+
const first = shown[0] ?? "";
|
|
672
|
+
const rest = shown.slice(1);
|
|
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)))] }));
|
|
677
|
+
}
|
|
28
678
|
case "system":
|
|
29
|
-
return (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { children:
|
|
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))) }));
|
|
30
680
|
default:
|
|
31
681
|
return null;
|
|
32
682
|
}
|
|
33
|
-
}
|
|
683
|
+
}, (prev, next) => prev.message.id === next.message.id &&
|
|
684
|
+
prev.start === next.start &&
|
|
685
|
+
prev.count === next.count &&
|
|
686
|
+
prev.lines === next.lines &&
|
|
687
|
+
prev.selection === next.selection);
|