mini-coder 0.5.10 → 0.5.12
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/BENCHMARK.md +408 -0
- package/PROGRESS.md +5 -0
- package/README.md +14 -1
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-loop.sh +19 -0
- package/package.json +1 -1
- package/src/agent.ts +5 -1
- package/src/headless.ts +62 -21
- package/src/index.ts +35 -56
- package/src/prompt.ts +8 -0
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +19 -15
- package/src/shared.ts +39 -0
- package/src/submit.ts +3 -25
- package/src/text.ts +71 -0
- package/src/tool-common.ts +91 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +869 -0
- package/src/tools.ts +186 -995
- package/src/ui/agent.ts +199 -110
- package/src/ui/commands.test.ts +16 -302
- package/src/ui/commands.ts +21 -47
- package/src/ui/conversation.test.ts +263 -1389
- package/src/ui/conversation.ts +496 -151
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +196 -114
- package/src/ui/agent.test.ts +0 -49
- package/src/ui/help.test.ts +0 -65
- package/src/ui/overlay.test.ts +0 -42
- package/src/ui/render-performance.test.ts +0 -444
- package/src/ui/status.test.ts +0 -489
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
MockTerminal,
|
|
5
|
-
measureContentHeight,
|
|
6
|
-
Text,
|
|
7
|
-
VStack,
|
|
8
|
-
} from "@cel-tui/core";
|
|
9
|
-
import type { Node } from "@cel-tui/types";
|
|
10
|
-
import {
|
|
11
|
-
fauxAssistantMessage,
|
|
12
|
-
fauxText,
|
|
13
|
-
fauxThinking,
|
|
14
|
-
fauxToolCall,
|
|
15
|
-
} from "@mariozechner/pi-ai";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Node, TextNode } from "@cel-tui/types";
|
|
3
|
+
import type { AssistantMessage, ToolResultMessage } from "@mariozechner/pi-ai";
|
|
16
4
|
import { DEFAULT_THEME } from "../theme.ts";
|
|
17
5
|
import {
|
|
18
6
|
buildConversationLogNodes,
|
|
19
|
-
type PendingToolResult,
|
|
20
7
|
renderAssistantMessage,
|
|
21
8
|
renderToolResult,
|
|
22
|
-
resetConversationRenderCache,
|
|
23
9
|
} from "./conversation.ts";
|
|
24
10
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
11
|
+
function collectInlineText(node: Node): string {
|
|
12
|
+
if (node.type === "text") {
|
|
13
|
+
return node.content;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === "textinput") {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
return node.children.map((child) => collectInlineText(child)).join("");
|
|
19
|
+
}
|
|
32
20
|
|
|
33
|
-
function
|
|
21
|
+
function collectRenderedLines(node: Node | null): string[] {
|
|
34
22
|
if (!node) {
|
|
35
23
|
return [];
|
|
36
24
|
}
|
|
@@ -42,1445 +30,331 @@ function collectText(node: Node | null): string[] {
|
|
|
42
30
|
}
|
|
43
31
|
if (
|
|
44
32
|
node.type === "hstack" &&
|
|
45
|
-
node.children.
|
|
33
|
+
node.children.length === 2 &&
|
|
34
|
+
node.children[0]?.type === "text" &&
|
|
35
|
+
node.children[1]?.type === "vstack"
|
|
46
36
|
) {
|
|
47
|
-
|
|
37
|
+
const prefix = node.children[0].content;
|
|
38
|
+
return collectRenderedLines(node.children[1]).map(
|
|
39
|
+
(line) => `${prefix}${line}`,
|
|
40
|
+
);
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function measureRenderedHeight(node: Node | null, width: number): number {
|
|
53
|
-
if (!node) {
|
|
54
|
-
return 0;
|
|
42
|
+
if (node.type === "hstack") {
|
|
43
|
+
return [collectInlineText(node)];
|
|
55
44
|
}
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function waitForCelRender(): Promise<void> {
|
|
60
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
45
|
+
return node.children.flatMap((child) => collectRenderedLines(child));
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
|
|
64
|
-
node
|
|
65
|
-
cols = PREVIEW_WIDTH,
|
|
66
|
-
rows = 24,
|
|
67
|
-
): Promise<
|
|
68
|
-
Array<{
|
|
69
|
-
text: string;
|
|
70
|
-
fgColors: Array<string | null>;
|
|
71
|
-
bold: boolean[];
|
|
72
|
-
italic: boolean[];
|
|
73
|
-
underline: boolean[];
|
|
74
|
-
}>
|
|
75
|
-
> {
|
|
76
|
-
if (!node) {
|
|
48
|
+
function collectTextNodes(node: Node | null): TextNode[] {
|
|
49
|
+
if (!node || node.type === "textinput") {
|
|
77
50
|
return [];
|
|
78
51
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
cel.init(terminal);
|
|
82
|
-
cel.viewport(() => VStack({ width: cols, height: rows }, [node]));
|
|
83
|
-
await waitForCelRender();
|
|
84
|
-
|
|
85
|
-
const buffer = cel._getBuffer();
|
|
86
|
-
if (!buffer) {
|
|
87
|
-
throw new Error("Expected cel-tui to produce a render buffer");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const snapshot: Array<{
|
|
91
|
-
text: string;
|
|
92
|
-
fgColors: Array<string | null>;
|
|
93
|
-
bold: boolean[];
|
|
94
|
-
italic: boolean[];
|
|
95
|
-
underline: boolean[];
|
|
96
|
-
}> = [];
|
|
97
|
-
for (let y = 0; y < rows; y++) {
|
|
98
|
-
let text = "";
|
|
99
|
-
const fgColors: Array<string | null> = [];
|
|
100
|
-
const bold: boolean[] = [];
|
|
101
|
-
const italic: boolean[] = [];
|
|
102
|
-
const underline: boolean[] = [];
|
|
103
|
-
for (let x = 0; x < cols; x++) {
|
|
104
|
-
const cell = buffer.get(x, y);
|
|
105
|
-
text += cell.char;
|
|
106
|
-
fgColors.push(cell.fgColor);
|
|
107
|
-
bold.push(cell.bold);
|
|
108
|
-
italic.push(cell.italic);
|
|
109
|
-
underline.push(cell.underline);
|
|
110
|
-
}
|
|
111
|
-
snapshot.push({ text, fgColors, bold, italic, underline });
|
|
52
|
+
if (node.type === "text") {
|
|
53
|
+
return [node];
|
|
112
54
|
}
|
|
113
|
-
|
|
114
|
-
cel.stop();
|
|
115
|
-
return snapshot;
|
|
55
|
+
return node.children.flatMap((child) => collectTextNodes(child));
|
|
116
56
|
}
|
|
117
57
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (const row of snapshot) {
|
|
127
|
-
const normalized = row.text.trim().replace(/^│\s*/, "");
|
|
128
|
-
if (normalized !== "") {
|
|
129
|
-
lines.push(normalized);
|
|
130
|
-
}
|
|
58
|
+
function findTextNode(node: Node | null, content: string): TextNode {
|
|
59
|
+
const textNode = collectTextNodes(node).find(
|
|
60
|
+
(text) => text.content === content,
|
|
61
|
+
);
|
|
62
|
+
expect(textNode).toBeDefined();
|
|
63
|
+
if (!textNode) {
|
|
64
|
+
throw new Error(`Missing text node: ${content}`);
|
|
131
65
|
}
|
|
66
|
+
return textNode;
|
|
67
|
+
}
|
|
132
68
|
|
|
133
|
-
|
|
69
|
+
function makeAssistantToolCallMessage(): AssistantMessage {
|
|
70
|
+
return {
|
|
71
|
+
role: "assistant",
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "toolCall",
|
|
75
|
+
id: "call-read",
|
|
76
|
+
name: "read",
|
|
77
|
+
arguments: { path: "hint.txt", limit: 3 },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
api: "anthropic-messages",
|
|
81
|
+
provider: "anthropic",
|
|
82
|
+
model: "claude-sonnet-4-20250514",
|
|
83
|
+
usage: {
|
|
84
|
+
input: 0,
|
|
85
|
+
output: 0,
|
|
86
|
+
cacheRead: 0,
|
|
87
|
+
cacheWrite: 0,
|
|
88
|
+
totalTokens: 0,
|
|
89
|
+
cost: {
|
|
90
|
+
input: 0,
|
|
91
|
+
output: 0,
|
|
92
|
+
cacheRead: 0,
|
|
93
|
+
cacheWrite: 0,
|
|
94
|
+
total: 0,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
stopReason: "toolUse",
|
|
98
|
+
timestamp: 1,
|
|
99
|
+
};
|
|
134
100
|
}
|
|
135
101
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
102
|
+
function makeReadToolResultMessage(
|
|
103
|
+
content: ToolResultMessage["content"],
|
|
104
|
+
): ToolResultMessage {
|
|
105
|
+
return {
|
|
106
|
+
role: "toolResult",
|
|
107
|
+
toolCallId: "call-read",
|
|
108
|
+
toolName: "read",
|
|
109
|
+
content,
|
|
110
|
+
isError: false,
|
|
111
|
+
timestamp: 2,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
140
114
|
|
|
141
115
|
describe("ui/conversation", () => {
|
|
142
|
-
test("
|
|
143
|
-
|
|
144
|
-
const message = fauxAssistantMessage(
|
|
145
|
-
"# Heading\n\nUse **bold** and `code`.\n- item",
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Act
|
|
149
|
-
const text = collectText(renderAssistantMessage(message, RENDER_OPTS));
|
|
150
|
-
|
|
151
|
-
// Assert
|
|
152
|
-
expect(text).toContain("# Heading");
|
|
153
|
-
expect(text).toContain("Use **bold** and `code`.");
|
|
154
|
-
expect(text).toContain("- item");
|
|
155
|
-
expect(text).not.toContain("Use bold and code.");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("renderAssistantMessage with reasoning enabled shows thinking blocks", () => {
|
|
159
|
-
// Arrange
|
|
160
|
-
const message = fauxAssistantMessage([
|
|
161
|
-
fauxThinking("I should inspect the tests first."),
|
|
162
|
-
fauxText("Done."),
|
|
163
|
-
]);
|
|
164
|
-
|
|
165
|
-
// Act
|
|
166
|
-
const text = collectText(
|
|
167
|
-
renderAssistantMessage(message, {
|
|
168
|
-
...RENDER_OPTS,
|
|
169
|
-
showReasoning: true,
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// Assert
|
|
174
|
-
expect(text).toContain("I should inspect the tests first.");
|
|
175
|
-
expect(text).toContain("Done.");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("renderAssistantMessage with reasoning hidden shows a thinking line-count placeholder", () => {
|
|
179
|
-
// Arrange
|
|
180
|
-
const message = fauxAssistantMessage([
|
|
181
|
-
fauxThinking("line one\nline two\nline three"),
|
|
182
|
-
fauxText("Done."),
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
// Act
|
|
186
|
-
const text = collectText(
|
|
187
|
-
renderAssistantMessage(message, {
|
|
188
|
-
...RENDER_OPTS,
|
|
189
|
-
showReasoning: false,
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
// Assert
|
|
194
|
-
expect(text).toContain("Thinking... 3 lines.");
|
|
195
|
-
expect(text).toContain("Done.");
|
|
196
|
-
expect(text).not.toContain("line one");
|
|
197
|
-
expect(text).not.toContain("line two");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("renderAssistantMessage with mixed top-level blocks keeps a single blank line between sections", () => {
|
|
201
|
-
// Arrange
|
|
202
|
-
const message = fauxAssistantMessage([
|
|
203
|
-
fauxThinking("Plan first."),
|
|
204
|
-
fauxText("Done."),
|
|
205
|
-
fauxToolCall("shell", { command: "echo hi" }, { id: "tool-1" }),
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
// Act
|
|
209
|
-
const height = measureRenderedHeight(
|
|
210
|
-
renderAssistantMessage(message, {
|
|
211
|
-
...RENDER_OPTS,
|
|
212
|
-
showReasoning: true,
|
|
213
|
-
}),
|
|
214
|
-
PREVIEW_WIDTH,
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// Assert
|
|
218
|
-
expect(height).toBe(6);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("renderAssistantMessage syntax-highlights markdown tokens with theme-derived colors", async () => {
|
|
222
|
-
// Arrange
|
|
223
|
-
const theme = {
|
|
224
|
-
...DEFAULT_THEME,
|
|
225
|
-
accentText: "color14",
|
|
226
|
-
secondaryAccentText: "color09",
|
|
227
|
-
diffAdded: "color10",
|
|
228
|
-
mutedText: "color13",
|
|
229
|
-
} satisfies typeof DEFAULT_THEME;
|
|
230
|
-
const message = fauxAssistantMessage("# Heading\n- item\n> quote\n`code`");
|
|
231
|
-
|
|
232
|
-
// Act
|
|
233
|
-
const rows = await renderBufferRows(
|
|
234
|
-
renderAssistantMessage(message, {
|
|
235
|
-
...RENDER_OPTS,
|
|
236
|
-
theme,
|
|
237
|
-
}),
|
|
238
|
-
32,
|
|
239
|
-
12,
|
|
240
|
-
);
|
|
241
|
-
const headingRow = rows.find((row) => row.text.includes("# Heading"));
|
|
242
|
-
const bulletRow = rows.find((row) => row.text.includes("- item"));
|
|
243
|
-
const quoteRow = rows.find((row) => row.text.includes("> quote"));
|
|
244
|
-
const codeRow = rows.find((row) => row.text.includes("`code`"));
|
|
245
|
-
|
|
246
|
-
// Assert
|
|
247
|
-
expect(headingRow).toBeDefined();
|
|
248
|
-
expect(bulletRow).toBeDefined();
|
|
249
|
-
expect(quoteRow).toBeDefined();
|
|
250
|
-
expect(codeRow).toBeDefined();
|
|
251
|
-
expect(headingRow?.fgColors[headingRow.text.indexOf("#")]).toBe(
|
|
252
|
-
theme.accentText ?? null,
|
|
253
|
-
);
|
|
254
|
-
expect(bulletRow?.fgColors[bulletRow.text.indexOf("-")]).toBe(
|
|
255
|
-
theme.secondaryAccentText ?? null,
|
|
256
|
-
);
|
|
257
|
-
expect(quoteRow?.fgColors[quoteRow.text.indexOf(">")]).toBe(
|
|
258
|
-
theme.mutedText ?? null,
|
|
259
|
-
);
|
|
260
|
-
expect(quoteRow?.italic[quoteRow.text.indexOf(">")]).toBe(true);
|
|
261
|
-
expect(codeRow?.fgColors[codeRow.text.indexOf("`")]).toBe(
|
|
262
|
-
theme.diffAdded ?? null,
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("renderAssistantMessage syntax-highlights markdown emphasis and links with style cues", async () => {
|
|
267
|
-
// Arrange
|
|
268
|
-
const theme = {
|
|
269
|
-
...DEFAULT_THEME,
|
|
270
|
-
accentText: "color14",
|
|
271
|
-
diffAdded: "color10",
|
|
272
|
-
} satisfies typeof DEFAULT_THEME;
|
|
273
|
-
const message = fauxAssistantMessage(
|
|
274
|
-
"*italic* **bold** [label](https://example.com)",
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
// Act
|
|
278
|
-
const rows = await renderBufferRows(
|
|
279
|
-
renderAssistantMessage(message, {
|
|
280
|
-
...RENDER_OPTS,
|
|
281
|
-
theme,
|
|
282
|
-
}),
|
|
283
|
-
64,
|
|
284
|
-
12,
|
|
285
|
-
);
|
|
286
|
-
const contentRow = rows.find((row) =>
|
|
287
|
-
row.text.includes("*italic* **bold** [label](https://example.com)"),
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
// Assert
|
|
291
|
-
expect(contentRow).toBeDefined();
|
|
292
|
-
expect(contentRow?.italic[contentRow.text.indexOf("*italic*")]).toBe(true);
|
|
293
|
-
expect(contentRow?.bold[contentRow.text.indexOf("**bold**")]).toBe(true);
|
|
294
|
-
expect(contentRow?.fgColors[contentRow.text.indexOf("label")]).toBe(
|
|
295
|
-
theme.diffAdded ?? null,
|
|
296
|
-
);
|
|
297
|
-
expect(
|
|
298
|
-
contentRow?.fgColors[contentRow.text.indexOf("https://example.com")],
|
|
299
|
-
).toBe(theme.accentText ?? null);
|
|
300
|
-
expect(
|
|
301
|
-
contentRow?.underline[contentRow.text.indexOf("https://example.com")],
|
|
302
|
-
).toBe(true);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("renderAssistantMessage with adjacent text blocks preserves markdown structure across the block boundary", async () => {
|
|
306
|
-
// Arrange
|
|
307
|
-
const singleBlock = {
|
|
308
|
-
content: [fauxText("```js\nconst x = 1;\n```")],
|
|
309
|
-
};
|
|
310
|
-
const splitBlocks = {
|
|
311
|
-
content: [fauxText("```js\n"), fauxText("const x = 1;\n```")],
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
// Act
|
|
315
|
-
const singleRows = await renderBufferRows(
|
|
316
|
-
renderAssistantMessage(singleBlock, RENDER_OPTS),
|
|
317
|
-
40,
|
|
318
|
-
12,
|
|
319
|
-
);
|
|
320
|
-
const splitRows = await renderBufferRows(
|
|
321
|
-
renderAssistantMessage(splitBlocks, RENDER_OPTS),
|
|
322
|
-
40,
|
|
323
|
-
12,
|
|
324
|
-
);
|
|
325
|
-
const singleHeight = measureRenderedHeight(
|
|
326
|
-
renderAssistantMessage(singleBlock, RENDER_OPTS),
|
|
327
|
-
40,
|
|
328
|
-
);
|
|
329
|
-
const splitHeight = measureRenderedHeight(
|
|
330
|
-
renderAssistantMessage(splitBlocks, RENDER_OPTS),
|
|
331
|
-
40,
|
|
332
|
-
);
|
|
333
|
-
const singleRow = singleRows.find((row) =>
|
|
334
|
-
row.text.includes("const x = 1;"),
|
|
335
|
-
);
|
|
336
|
-
const splitRow = splitRows.find((row) => row.text.includes("const x = 1;"));
|
|
337
|
-
|
|
338
|
-
// Assert
|
|
339
|
-
expect(singleRow).toBeDefined();
|
|
340
|
-
expect(splitRow).toBeDefined();
|
|
341
|
-
expect(singleHeight).toBe(splitHeight);
|
|
342
|
-
expect(singleRow?.fgColors[singleRow.text.indexOf("const")]).not.toBe(null);
|
|
343
|
-
expect(splitRow?.fgColors[splitRow.text.indexOf("const")]).toBe(
|
|
344
|
-
singleRow?.fgColors[singleRow.text.indexOf("const")],
|
|
345
|
-
);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test("buildConversationLogNodes with a pending shell result keeps the streamed call and result append-only", () => {
|
|
349
|
-
// Arrange
|
|
350
|
-
const pendingToolResults: PendingToolResult[] = [
|
|
116
|
+
test("read tool-call previews render structured arguments instead of raw JSON", () => {
|
|
117
|
+
const node = renderAssistantMessage(
|
|
351
118
|
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "toolCall",
|
|
122
|
+
id: "call-read",
|
|
123
|
+
name: "read",
|
|
124
|
+
arguments: {
|
|
125
|
+
path: "src/ui/conversation.ts",
|
|
126
|
+
offset: 820,
|
|
127
|
+
limit: 80,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
356
131
|
},
|
|
357
|
-
];
|
|
358
|
-
|
|
359
|
-
// Act
|
|
360
|
-
const nodes = buildConversationLogNodes(
|
|
361
132
|
{
|
|
362
|
-
|
|
363
|
-
fauxAssistantMessage([
|
|
364
|
-
fauxText("Working..."),
|
|
365
|
-
fauxToolCall("shell", { command: "echo hi" }, { id: "tool-1" }),
|
|
366
|
-
]),
|
|
367
|
-
],
|
|
368
|
-
showReasoning: false,
|
|
133
|
+
showReasoning: true,
|
|
369
134
|
verbose: false,
|
|
370
135
|
theme: DEFAULT_THEME,
|
|
136
|
+
cwd: "/tmp/project",
|
|
137
|
+
previewWidth: 80,
|
|
371
138
|
},
|
|
372
|
-
{
|
|
373
|
-
isStreaming: true,
|
|
374
|
-
content: [],
|
|
375
|
-
pendingToolResults,
|
|
376
|
-
},
|
|
377
|
-
0,
|
|
378
|
-
PREVIEW_WIDTH,
|
|
379
139
|
);
|
|
380
|
-
const text = collectText({
|
|
381
|
-
type: "vstack",
|
|
382
|
-
props: {},
|
|
383
|
-
children: nodes,
|
|
384
|
-
});
|
|
385
140
|
|
|
386
|
-
|
|
387
|
-
expect(
|
|
388
|
-
expect(
|
|
389
|
-
expect(
|
|
390
|
-
expect(
|
|
391
|
-
expect(
|
|
392
|
-
expect(text).not.toContain("Exit code: 0");
|
|
141
|
+
const lines = collectRenderedLines(node);
|
|
142
|
+
expect(lines).toContain("│ read ->");
|
|
143
|
+
expect(lines).toContain("│ src/ui/conversation.ts");
|
|
144
|
+
expect(lines).toContain("│ offset: 820");
|
|
145
|
+
expect(lines).toContain("│ limit: 80");
|
|
146
|
+
expect(lines.join("\n")).not.toContain('"path"');
|
|
393
147
|
});
|
|
394
148
|
|
|
395
|
-
test("
|
|
396
|
-
|
|
397
|
-
const nodes = buildConversationLogNodes(
|
|
149
|
+
test("shell tool-call previews use cel-tui syntax scopes for custom colors", () => {
|
|
150
|
+
const node = renderAssistantMessage(
|
|
398
151
|
{
|
|
399
|
-
|
|
400
|
-
fauxAssistantMessage([
|
|
401
|
-
fauxToolCall(
|
|
402
|
-
"edit",
|
|
403
|
-
{
|
|
404
|
-
path: "src/app.ts",
|
|
405
|
-
oldText: "before",
|
|
406
|
-
newText: "after",
|
|
407
|
-
},
|
|
408
|
-
{ id: "tool-1" },
|
|
409
|
-
),
|
|
410
|
-
]),
|
|
152
|
+
content: [
|
|
411
153
|
{
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
154
|
+
type: "toolCall",
|
|
155
|
+
id: "call-shell",
|
|
156
|
+
name: "shell",
|
|
157
|
+
arguments: {
|
|
158
|
+
command: 'if true; then echo "$HOME"; fi',
|
|
159
|
+
},
|
|
418
160
|
},
|
|
419
161
|
],
|
|
420
|
-
showReasoning: false,
|
|
421
|
-
verbose: false,
|
|
422
|
-
theme: DEFAULT_THEME,
|
|
423
162
|
},
|
|
424
163
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
164
|
+
showReasoning: true,
|
|
165
|
+
verbose: true,
|
|
166
|
+
theme: DEFAULT_THEME,
|
|
167
|
+
cwd: "/tmp/project",
|
|
168
|
+
previewWidth: 80,
|
|
428
169
|
},
|
|
429
|
-
1,
|
|
430
|
-
PREVIEW_WIDTH,
|
|
431
170
|
);
|
|
432
|
-
const text = collectText({
|
|
433
|
-
type: "vstack",
|
|
434
|
-
props: {},
|
|
435
|
-
children: nodes,
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Assert
|
|
439
|
-
expect(text).toContain("edit <-");
|
|
440
|
-
expect(text).toContain("~ src/app.ts");
|
|
441
|
-
expect(text).not.toContain("before");
|
|
442
|
-
expect(text).not.toContain("after");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("buildConversationLogNodes with unchanged state reuses cached committed nodes", () => {
|
|
446
|
-
// Arrange
|
|
447
|
-
const state = {
|
|
448
|
-
messages: [fauxAssistantMessage("Committed response")],
|
|
449
|
-
showReasoning: false,
|
|
450
|
-
verbose: false,
|
|
451
|
-
theme: DEFAULT_THEME,
|
|
452
|
-
};
|
|
453
|
-
const streaming = {
|
|
454
|
-
isStreaming: false,
|
|
455
|
-
content: [],
|
|
456
|
-
pendingToolResults: [],
|
|
457
|
-
};
|
|
458
171
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const second = buildConversationLogNodes(
|
|
462
|
-
state,
|
|
463
|
-
streaming,
|
|
464
|
-
0,
|
|
465
|
-
PREVIEW_WIDTH,
|
|
172
|
+
expect(findTextNode(node, "true").props.fgColor).toBe(
|
|
173
|
+
DEFAULT_THEME.secondaryAccentText,
|
|
466
174
|
);
|
|
467
|
-
|
|
468
|
-
// Assert
|
|
469
|
-
expect(second).toBe(first);
|
|
470
175
|
});
|
|
471
176
|
|
|
472
|
-
test("
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
showReasoning: false,
|
|
477
|
-
verbose: false,
|
|
478
|
-
theme: DEFAULT_THEME,
|
|
479
|
-
};
|
|
177
|
+
test("read tool results include the resolved path, hide model paging hints, and render fewer body lines when verbose is off", () => {
|
|
178
|
+
const fileBody =
|
|
179
|
+
Array.from({ length: 14 }, (_, index) => `line ${index + 1}`).join("\n") +
|
|
180
|
+
"\n\n[use offset=14 limit=14 to continue]";
|
|
480
181
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
182
|
+
const compactLines = collectRenderedLines(
|
|
183
|
+
renderToolResult(
|
|
184
|
+
"read",
|
|
185
|
+
{ path: "src/ui/conversation.ts" },
|
|
186
|
+
fileBody,
|
|
187
|
+
false,
|
|
188
|
+
{
|
|
189
|
+
showReasoning: true,
|
|
190
|
+
verbose: false,
|
|
191
|
+
theme: DEFAULT_THEME,
|
|
192
|
+
cwd: "/tmp/project",
|
|
193
|
+
previewWidth: 48,
|
|
194
|
+
},
|
|
195
|
+
),
|
|
491
196
|
);
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
197
|
+
const verboseLines = collectRenderedLines(
|
|
198
|
+
renderToolResult(
|
|
199
|
+
"read",
|
|
200
|
+
{ path: "src/ui/conversation.ts" },
|
|
201
|
+
fileBody,
|
|
202
|
+
false,
|
|
203
|
+
{
|
|
204
|
+
showReasoning: true,
|
|
205
|
+
verbose: true,
|
|
206
|
+
theme: DEFAULT_THEME,
|
|
207
|
+
cwd: "/tmp/project",
|
|
208
|
+
previewWidth: 48,
|
|
209
|
+
},
|
|
210
|
+
),
|
|
501
211
|
);
|
|
502
|
-
const text = collectText({
|
|
503
|
-
type: "vstack",
|
|
504
|
-
props: {},
|
|
505
|
-
children: withStreamingTail,
|
|
506
|
-
});
|
|
507
212
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
expect(text).toContain("Committed response");
|
|
511
|
-
expect(text).toContain("Streaming tail");
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("buildConversationLogNodes when verbose mode changes rebuilds cached tool nodes", async () => {
|
|
515
|
-
// Arrange
|
|
516
|
-
const output = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join(
|
|
517
|
-
"\n",
|
|
213
|
+
expect(compactLines[0]).toBe(
|
|
214
|
+
"│ read <- /tmp/project/src/ui/conversation.ts",
|
|
518
215
|
);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
{
|
|
525
|
-
role: "toolResult" as const,
|
|
526
|
-
toolCallId: "tool-1",
|
|
527
|
-
toolName: "shell",
|
|
528
|
-
content: [{ type: "text" as const, text: output }],
|
|
529
|
-
isError: false,
|
|
530
|
-
timestamp: Date.now(),
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
showReasoning: false,
|
|
534
|
-
verbose: false,
|
|
535
|
-
theme: DEFAULT_THEME,
|
|
536
|
-
};
|
|
216
|
+
expect(compactLines.join("\n")).not.toContain("Use offset=14 limit=14");
|
|
217
|
+
expect(verboseLines.join("\n")).not.toContain("Use offset=14 limit=14");
|
|
218
|
+
expect(verboseLines).toContain("│ line 14");
|
|
219
|
+
expect(compactLines.length).toBeLessThan(verboseLines.length);
|
|
220
|
+
});
|
|
537
221
|
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
state,
|
|
222
|
+
test("read tool results preserve literal continuation-looking lines from the file body without showing the model paging hint", () => {
|
|
223
|
+
const nodes = buildConversationLogNodes(
|
|
541
224
|
{
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
225
|
+
messages: [
|
|
226
|
+
makeAssistantToolCallMessage(),
|
|
227
|
+
makeReadToolResultMessage([
|
|
228
|
+
{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: "alpha\n\n[use offset=99 limit=10 to continue]\n",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: "[use offset=3 limit=3 to continue]",
|
|
235
|
+
},
|
|
236
|
+
]),
|
|
237
|
+
],
|
|
238
|
+
showReasoning: true,
|
|
239
|
+
verbose: true,
|
|
240
|
+
theme: DEFAULT_THEME,
|
|
241
|
+
cwd: "/tmp/project",
|
|
242
|
+
versionLabel: "test",
|
|
545
243
|
},
|
|
546
|
-
0,
|
|
547
|
-
PREVIEW_WIDTH,
|
|
548
|
-
);
|
|
549
|
-
const previewText = await renderVisibleText(
|
|
550
|
-
VStack({}, previewNodes),
|
|
551
|
-
PREVIEW_WIDTH,
|
|
552
|
-
24,
|
|
553
|
-
);
|
|
554
|
-
|
|
555
|
-
const verboseNodes = buildConversationLogNodes(
|
|
556
|
-
{ ...state, verbose: true },
|
|
557
244
|
{
|
|
558
245
|
isStreaming: false,
|
|
559
246
|
content: [],
|
|
560
247
|
pendingToolResults: [],
|
|
561
248
|
},
|
|
562
249
|
0,
|
|
563
|
-
|
|
564
|
-
);
|
|
565
|
-
const verboseText = await renderVisibleText(
|
|
566
|
-
VStack({}, verboseNodes),
|
|
567
|
-
PREVIEW_WIDTH,
|
|
568
|
-
40,
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
// Assert
|
|
572
|
-
expect(previewNodes).not.toBe(verboseNodes);
|
|
573
|
-
expect(previewText).toContain("line 18");
|
|
574
|
-
expect(previewText).toContain("line 25");
|
|
575
|
-
expect(previewText).toContain("And 17 lines more");
|
|
576
|
-
expect(previewText).not.toContain("line 17");
|
|
577
|
-
expect(verboseText).toContain("line 17");
|
|
578
|
-
expect(verboseText).toContain("line 25");
|
|
579
|
-
expect(verboseText).not.toContain("And 17 lines more");
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test("buildConversationLogNodes when preview width changes rebuilds cached tool nodes", () => {
|
|
583
|
-
// Arrange
|
|
584
|
-
const state = {
|
|
585
|
-
messages: [
|
|
586
|
-
fauxAssistantMessage([
|
|
587
|
-
fauxToolCall(
|
|
588
|
-
"shell",
|
|
589
|
-
{
|
|
590
|
-
command:
|
|
591
|
-
"printf 'this is a very long wrapped line that depends on width'",
|
|
592
|
-
},
|
|
593
|
-
{ id: "tool-1" },
|
|
594
|
-
),
|
|
595
|
-
]),
|
|
596
|
-
],
|
|
597
|
-
showReasoning: false,
|
|
598
|
-
verbose: false,
|
|
599
|
-
theme: DEFAULT_THEME,
|
|
600
|
-
};
|
|
601
|
-
const streaming = {
|
|
602
|
-
isStreaming: false,
|
|
603
|
-
content: [],
|
|
604
|
-
pendingToolResults: [],
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
// Act
|
|
608
|
-
const wide = buildConversationLogNodes(state, streaming, 0, 40);
|
|
609
|
-
const narrow = buildConversationLogNodes(state, streaming, 0, 20);
|
|
610
|
-
|
|
611
|
-
// Assert
|
|
612
|
-
expect(narrow).not.toBe(wide);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
test("renderAssistantMessage with in-progress reasoning visible shows thinking text", () => {
|
|
616
|
-
// Arrange
|
|
617
|
-
const assistant = {
|
|
618
|
-
content: [fauxThinking("Reasoning in progress")],
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// Act
|
|
622
|
-
const text = collectText(
|
|
623
|
-
renderAssistantMessage(assistant, {
|
|
624
|
-
...RENDER_OPTS,
|
|
625
|
-
showReasoning: true,
|
|
626
|
-
}),
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
// Assert
|
|
630
|
-
expect(text).toContain("Reasoning in progress");
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
test("renderAssistantMessage with in-progress reasoning hidden shows a one-line placeholder", () => {
|
|
634
|
-
// Arrange
|
|
635
|
-
const assistant = {
|
|
636
|
-
content: [fauxThinking("some thinking")],
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
// Act
|
|
640
|
-
const text = collectText(
|
|
641
|
-
renderAssistantMessage(assistant, {
|
|
642
|
-
...RENDER_OPTS,
|
|
643
|
-
showReasoning: false,
|
|
644
|
-
}),
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
// Assert
|
|
648
|
-
expect(text).toContain("Thinking... 1 line.");
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
test("renderAssistantMessage for a shell tool call renders an unbracketed header inside the pill", () => {
|
|
652
|
-
// Arrange
|
|
653
|
-
const assistant = {
|
|
654
|
-
content: [
|
|
655
|
-
fauxToolCall("shell", { command: "echo hi" }, { id: "tool-1" }),
|
|
656
|
-
],
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
// Act
|
|
660
|
-
const node = renderAssistantMessage(assistant, RENDER_OPTS);
|
|
661
|
-
const text = collectText(node);
|
|
662
|
-
|
|
663
|
-
// Assert
|
|
664
|
-
expect(text).toContain("shell ->");
|
|
665
|
-
expect(text).not.toContain("[shell ->]");
|
|
666
|
-
expect(text).toContain("echo hi");
|
|
667
|
-
expect(text).not.toContain('"command": "echo hi"');
|
|
668
|
-
|
|
669
|
-
expect(node?.type).toBe("vstack");
|
|
670
|
-
if (!node || node.type !== "vstack") {
|
|
671
|
-
throw new Error("Expected the assistant node to be a vstack");
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const toolBlock = node.children[0];
|
|
675
|
-
expect(toolBlock?.type).toBe("hstack");
|
|
676
|
-
if (!toolBlock || toolBlock.type !== "hstack") {
|
|
677
|
-
throw new Error("Expected the tool block to be an hstack");
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const contentColumn = toolBlock.children[1];
|
|
681
|
-
expect(contentColumn?.type).toBe("vstack");
|
|
682
|
-
if (!contentColumn || contentColumn.type !== "vstack") {
|
|
683
|
-
throw new Error("Expected the tool content column to be a vstack");
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const headerRow = contentColumn.children[0];
|
|
687
|
-
expect(headerRow?.type).toBe("hstack");
|
|
688
|
-
if (!headerRow || headerRow.type !== "hstack") {
|
|
689
|
-
throw new Error("Expected the tool header row to be an hstack");
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const headerPill = headerRow.children[0];
|
|
693
|
-
expect(headerPill?.type).toBe("hstack");
|
|
694
|
-
if (!headerPill || headerPill.type !== "hstack") {
|
|
695
|
-
throw new Error("Expected the tool header pill to be an hstack");
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
expect(headerPill.props.bgColor).toBe(DEFAULT_THEME.toolBorder);
|
|
699
|
-
expect(headerPill.props.padding).toEqual({ x: 1 });
|
|
700
|
-
expect(collectText(headerPill)).toEqual(["shell ->"]);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
test("renderAssistantMessage for a shell tool call syntax-highlights bash tokens", async () => {
|
|
704
|
-
// Arrange
|
|
705
|
-
const assistant = {
|
|
706
|
-
content: [
|
|
707
|
-
fauxToolCall(
|
|
708
|
-
"shell",
|
|
709
|
-
{ command: 'if true; then echo "$HOME"; fi' },
|
|
710
|
-
{ id: "tool-1" },
|
|
711
|
-
),
|
|
712
|
-
],
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
// Act
|
|
716
|
-
const rows = await renderBufferRows(
|
|
717
|
-
renderAssistantMessage(assistant, RENDER_OPTS),
|
|
718
|
-
48,
|
|
719
|
-
12,
|
|
720
|
-
);
|
|
721
|
-
const commandRow = rows.find((row) =>
|
|
722
|
-
row.text.includes('if true; then echo "$HOME"; fi'),
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
// Assert
|
|
726
|
-
expect(commandRow).toBeDefined();
|
|
727
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf("if")]).toBe(
|
|
728
|
-
DEFAULT_THEME.secondaryAccentText ?? null,
|
|
729
|
-
);
|
|
730
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf("echo")]).toBe(
|
|
731
|
-
DEFAULT_THEME.accentText ?? null,
|
|
732
|
-
);
|
|
733
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf('"$HOME"')]).toBe(
|
|
734
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
735
|
-
);
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
test("renderAssistantMessage for a multiline shell tool call preserves syntax state across lines", async () => {
|
|
739
|
-
// Arrange
|
|
740
|
-
const assistant = {
|
|
741
|
-
content: [
|
|
742
|
-
fauxToolCall(
|
|
743
|
-
"shell",
|
|
744
|
-
{ command: "printf 'foo\nbar'" },
|
|
745
|
-
{ id: "tool-1" },
|
|
746
|
-
),
|
|
747
|
-
],
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
// Act
|
|
751
|
-
const rows = await renderBufferRows(
|
|
752
|
-
renderAssistantMessage(assistant, {
|
|
753
|
-
...RENDER_OPTS,
|
|
754
|
-
verbose: true,
|
|
755
|
-
}),
|
|
756
|
-
32,
|
|
757
|
-
12,
|
|
758
|
-
);
|
|
759
|
-
const firstRow = rows.find((row) => row.text.includes("printf 'foo"));
|
|
760
|
-
const secondRow = rows.find((row) => row.text.includes("bar'"));
|
|
761
|
-
|
|
762
|
-
// Assert
|
|
763
|
-
expect(firstRow).toBeDefined();
|
|
764
|
-
expect(secondRow).toBeDefined();
|
|
765
|
-
expect(firstRow?.fgColors[firstRow.text.indexOf("foo")]).toBe(
|
|
766
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
767
|
-
);
|
|
768
|
-
expect(secondRow?.fgColors[secondRow.text.indexOf("bar")]).toBe(
|
|
769
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
770
|
-
);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
test("renderAssistantMessage for a shell tool call uses theme-derived syntax colors", async () => {
|
|
774
|
-
// Arrange
|
|
775
|
-
const theme = {
|
|
776
|
-
...DEFAULT_THEME,
|
|
777
|
-
accentText: "color14",
|
|
778
|
-
secondaryAccentText: "color09",
|
|
779
|
-
diffAdded: "color10",
|
|
780
|
-
mutedText: "color13",
|
|
781
|
-
toolText: "color15",
|
|
782
|
-
} satisfies typeof DEFAULT_THEME;
|
|
783
|
-
const assistant = {
|
|
784
|
-
content: [
|
|
785
|
-
fauxToolCall(
|
|
786
|
-
"shell",
|
|
787
|
-
{ command: 'if true; then echo "$HOME"; fi' },
|
|
788
|
-
{ id: "tool-1" },
|
|
789
|
-
),
|
|
790
|
-
],
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
// Act
|
|
794
|
-
const rows = await renderBufferRows(
|
|
795
|
-
renderAssistantMessage(assistant, {
|
|
796
|
-
...RENDER_OPTS,
|
|
797
|
-
theme,
|
|
798
|
-
}),
|
|
799
|
-
48,
|
|
800
|
-
12,
|
|
801
|
-
);
|
|
802
|
-
const commandRow = rows.find((row) =>
|
|
803
|
-
row.text.includes('if true; then echo "$HOME"; fi'),
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
// Assert
|
|
807
|
-
expect(commandRow).toBeDefined();
|
|
808
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf("if")]).toBe(
|
|
809
|
-
theme.secondaryAccentText ?? null,
|
|
810
|
-
);
|
|
811
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf("echo")]).toBe(
|
|
812
|
-
theme.accentText ?? null,
|
|
813
|
-
);
|
|
814
|
-
expect(commandRow?.fgColors[commandRow.text.indexOf('"$HOME"')]).toBe(
|
|
815
|
-
theme.diffAdded ?? null,
|
|
816
|
-
);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test("renderAssistantMessage for a long single-token shell argument wraps through the tail in verbose mode", async () => {
|
|
820
|
-
// Arrange
|
|
821
|
-
const command = `printf ${"x".repeat(60)}TAIL`;
|
|
822
|
-
const assistant = {
|
|
823
|
-
content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
// Act
|
|
827
|
-
const text = await renderVisibleText(
|
|
828
|
-
renderAssistantMessage(assistant, {
|
|
829
|
-
...RENDER_OPTS,
|
|
830
|
-
verbose: true,
|
|
831
|
-
previewWidth: 24,
|
|
832
|
-
}),
|
|
833
|
-
24,
|
|
834
|
-
20,
|
|
250
|
+
80,
|
|
835
251
|
);
|
|
836
252
|
|
|
837
|
-
|
|
838
|
-
expect(
|
|
253
|
+
const lines = nodes.flatMap((node) => collectRenderedLines(node));
|
|
254
|
+
expect(lines).toContain("│ [use offset=99 limit=10 to continue]");
|
|
255
|
+
expect(lines.join("\n")).not.toContain("Use offset=3 limit=3 to continue.");
|
|
839
256
|
});
|
|
840
257
|
|
|
841
|
-
test("
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
20,
|
|
857
|
-
);
|
|
858
|
-
|
|
859
|
-
// Assert
|
|
860
|
-
expect(text.some((line) => line.includes("TAIL"))).toBe(true);
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
test("renderAssistantMessage for a long quoted shell string preserves string color across wrapped rows", async () => {
|
|
864
|
-
// Arrange
|
|
865
|
-
const command = `printf "${"x".repeat(80)}TAIL"`;
|
|
866
|
-
const assistant = {
|
|
867
|
-
content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
// Act
|
|
871
|
-
const rows = await renderBufferRows(
|
|
872
|
-
renderAssistantMessage(assistant, {
|
|
873
|
-
...RENDER_OPTS,
|
|
874
|
-
verbose: true,
|
|
875
|
-
previewWidth: 24,
|
|
876
|
-
}),
|
|
877
|
-
24,
|
|
878
|
-
20,
|
|
879
|
-
);
|
|
880
|
-
const headRowIndex = rows.findIndex((row) => row.text.includes('"x'));
|
|
881
|
-
const headRow = headRowIndex >= 0 ? rows[headRowIndex] : undefined;
|
|
882
|
-
const wrappedRow =
|
|
883
|
-
headRowIndex >= 0
|
|
884
|
-
? rows
|
|
885
|
-
.slice(headRowIndex + 1)
|
|
886
|
-
.find((row) => row.text.includes("xxxxxxxx"))
|
|
887
|
-
: undefined;
|
|
888
|
-
|
|
889
|
-
// Assert
|
|
890
|
-
expect(headRow).toBeDefined();
|
|
891
|
-
expect(wrappedRow).toBeDefined();
|
|
892
|
-
expect(headRow?.fgColors[headRow.text.indexOf('"')]).toBe(
|
|
893
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
894
|
-
);
|
|
895
|
-
expect(wrappedRow?.fgColors[wrappedRow.text.indexOf("x")]).toBe(
|
|
896
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
897
|
-
);
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
test("renderAssistantMessage for long inline markdown code keeps the tail visible", async () => {
|
|
901
|
-
// Arrange
|
|
902
|
-
const message = fauxAssistantMessage(`Use \`${"x".repeat(80)}TAIL\``);
|
|
903
|
-
|
|
904
|
-
// Act
|
|
905
|
-
const rows = await renderBufferRows(
|
|
906
|
-
renderAssistantMessage(message, {
|
|
907
|
-
...RENDER_OPTS,
|
|
908
|
-
previewWidth: 24,
|
|
909
|
-
}),
|
|
910
|
-
24,
|
|
911
|
-
20,
|
|
912
|
-
);
|
|
913
|
-
const codeRows = rows.filter(
|
|
914
|
-
(row) => row.text.includes("x") || row.text.includes("TAIL`"),
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
// Assert
|
|
918
|
-
expect(codeRows.length).toBeGreaterThan(1);
|
|
919
|
-
expect(rows.some((row) => row.text.includes("TAIL`"))).toBe(true);
|
|
920
|
-
expect(codeRows[0]?.fgColors[codeRows[0].text.indexOf("x")]).toBe(
|
|
921
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
922
|
-
);
|
|
923
|
-
expect(codeRows.at(-1)?.fgColors[codeRows.at(-1)!.text.indexOf("T")]).toBe(
|
|
924
|
-
DEFAULT_THEME.diffAdded ?? null,
|
|
925
|
-
);
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
test("renderAssistantMessage for a long single-token shell command uses wrapped preview height when verbose is off", () => {
|
|
929
|
-
// Arrange
|
|
930
|
-
const command = `printf ${"x".repeat(220)}TAIL`;
|
|
931
|
-
const assistant = {
|
|
932
|
-
content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
// Act
|
|
936
|
-
const height = measureRenderedHeight(
|
|
937
|
-
renderAssistantMessage(assistant, {
|
|
938
|
-
...RENDER_OPTS,
|
|
939
|
-
previewWidth: 24,
|
|
940
|
-
}),
|
|
941
|
-
24,
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
// Assert
|
|
945
|
-
expect(height).toBe(9);
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
test("renderAssistantMessage for a wrapped shell command keeps a fixed preview height when verbose is off", () => {
|
|
949
|
-
// Arrange
|
|
950
|
-
const command = Array.from(
|
|
951
|
-
{ length: 4 },
|
|
952
|
-
() =>
|
|
953
|
-
"printf 'this wrapped command line is intentionally long for the preview'",
|
|
954
|
-
).join("\n");
|
|
955
|
-
const assistant = {
|
|
956
|
-
content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
|
|
957
|
-
};
|
|
958
|
-
|
|
959
|
-
// Act
|
|
960
|
-
const node = renderAssistantMessage(assistant, {
|
|
961
|
-
...RENDER_OPTS,
|
|
962
|
-
previewWidth: 24,
|
|
963
|
-
});
|
|
964
|
-
const height = measureRenderedHeight(node, 24);
|
|
965
|
-
|
|
966
|
-
// Assert
|
|
967
|
-
expect(height).toBe(10);
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
test("renderAssistantMessage for a long single-line shell command keeps the command start visible in non-verbose mode", async () => {
|
|
971
|
-
// Arrange
|
|
972
|
-
const command = `IMPORTANT_PREFIX ${"x".repeat(400)}`;
|
|
973
|
-
const assistant = {
|
|
974
|
-
content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
// Act
|
|
978
|
-
const text = await renderVisibleText(
|
|
979
|
-
renderAssistantMessage(assistant, {
|
|
980
|
-
...RENDER_OPTS,
|
|
981
|
-
previewWidth: 24,
|
|
982
|
-
}),
|
|
983
|
-
24,
|
|
984
|
-
20,
|
|
258
|
+
test("read tool fallback rendering normalizes CRLF line endings", () => {
|
|
259
|
+
const lines = collectRenderedLines(
|
|
260
|
+
renderToolResult(
|
|
261
|
+
"read",
|
|
262
|
+
{ path: "notes.txt" },
|
|
263
|
+
"alpha\r\nbeta\r\n",
|
|
264
|
+
false,
|
|
265
|
+
{
|
|
266
|
+
showReasoning: true,
|
|
267
|
+
verbose: true,
|
|
268
|
+
theme: DEFAULT_THEME,
|
|
269
|
+
cwd: "/tmp/project",
|
|
270
|
+
previewWidth: 80,
|
|
271
|
+
},
|
|
272
|
+
),
|
|
985
273
|
);
|
|
986
274
|
|
|
987
|
-
|
|
988
|
-
expect(
|
|
275
|
+
expect(lines).toContain("│ alpha");
|
|
276
|
+
expect(lines).toContain("│ beta");
|
|
277
|
+
expect(lines.some((line) => line.includes("\r"))).toBe(false);
|
|
989
278
|
});
|
|
990
279
|
|
|
991
|
-
test("
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
"shell",
|
|
997
|
-
{ command: "seq 1 25" },
|
|
998
|
-
Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join("\n"),
|
|
280
|
+
test("read tool results use direct syntax-highlight rendering without chunking long tokens", () => {
|
|
281
|
+
const node = renderToolResult(
|
|
282
|
+
"read",
|
|
283
|
+
{ path: "src/example.ts" },
|
|
284
|
+
"const supercalifragilisticexpialidociousIdentifier = 42",
|
|
999
285
|
false,
|
|
1000
286
|
{
|
|
1001
|
-
|
|
1002
|
-
|
|
287
|
+
showReasoning: true,
|
|
288
|
+
verbose: true,
|
|
289
|
+
theme: DEFAULT_THEME,
|
|
290
|
+
cwd: "/tmp/project",
|
|
291
|
+
previewWidth: 20,
|
|
1003
292
|
},
|
|
1004
293
|
);
|
|
1005
294
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
VStack(
|
|
1009
|
-
{
|
|
1010
|
-
width: 24,
|
|
1011
|
-
height: 10,
|
|
1012
|
-
overflow: "scroll",
|
|
1013
|
-
scrollOffset: outerScrollOffset,
|
|
1014
|
-
onScroll: (offset) => {
|
|
1015
|
-
outerScrollOffset = offset;
|
|
1016
|
-
},
|
|
1017
|
-
},
|
|
1018
|
-
[
|
|
1019
|
-
Text("before 1"),
|
|
1020
|
-
toolNode,
|
|
1021
|
-
Text("after 1"),
|
|
1022
|
-
Text("after 2"),
|
|
1023
|
-
Text("after 3"),
|
|
1024
|
-
Text("after 4"),
|
|
1025
|
-
Text("after 5"),
|
|
1026
|
-
],
|
|
1027
|
-
),
|
|
295
|
+
expect(findTextNode(node, "42").props.fgColor).toBe(
|
|
296
|
+
DEFAULT_THEME.secondaryAccentText,
|
|
1028
297
|
);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
test("renderAssistantMessage for a readImage tool call shows the path rather than JSON", () => {
|
|
1040
|
-
// Arrange
|
|
1041
|
-
const assistant = {
|
|
1042
|
-
content: [
|
|
1043
|
-
fauxToolCall(
|
|
1044
|
-
"readImage",
|
|
1045
|
-
{ path: "assets/preview.png" },
|
|
1046
|
-
{ id: "tool-1" },
|
|
1047
|
-
),
|
|
1048
|
-
],
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
// Act
|
|
1052
|
-
const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
|
|
1053
|
-
|
|
1054
|
-
// Assert
|
|
1055
|
-
expect(text).toContain("read image ->");
|
|
1056
|
-
expect(text).toContain("assets/preview.png");
|
|
1057
|
-
expect(text).not.toContain("{");
|
|
298
|
+
expect(
|
|
299
|
+
findTextNode(node, "supercalifragilisticexpialidociousIdentifier")
|
|
300
|
+
.content,
|
|
301
|
+
).toBe("supercalifragilisticexpialidociousIdentifier");
|
|
302
|
+
expect(
|
|
303
|
+
findTextNode(node, "supercalifragilisticexpialidociousIdentifier").props
|
|
304
|
+
.fgColor,
|
|
305
|
+
).toBeUndefined();
|
|
1058
306
|
});
|
|
1059
307
|
|
|
1060
|
-
test("
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
308
|
+
test("grep tool results render grouped files and lines instead of raw JSON", () => {
|
|
309
|
+
const resultText = JSON.stringify(
|
|
310
|
+
{
|
|
311
|
+
limit: 10,
|
|
312
|
+
truncated: false,
|
|
313
|
+
files: [
|
|
1066
314
|
{
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
{
|
|
315
|
+
path: "src/ui/conversation.ts",
|
|
316
|
+
lines: [
|
|
317
|
+
{
|
|
318
|
+
kind: "match",
|
|
319
|
+
lineNumber: 857,
|
|
320
|
+
text: "function renderToolBlock(\n",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
kind: "context",
|
|
324
|
+
lineNumber: 858,
|
|
325
|
+
text: " spec: ToolBlockSpec,\n",
|
|
326
|
+
},
|
|
1070
327
|
],
|
|
1071
328
|
},
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
// Act
|
|
1078
|
-
const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
|
|
1079
|
-
|
|
1080
|
-
// Assert
|
|
1081
|
-
expect(text).toContain("todo write ->");
|
|
1082
|
-
expect(text).toContain("Updating todos... 2 todos updated");
|
|
1083
|
-
expect(text).not.toContain("{");
|
|
1084
|
-
expect(text).not.toContain('"todos"');
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
test("renderAssistantMessage for an edit tool call shows both old and new content without diff prefixes", () => {
|
|
1088
|
-
// Arrange
|
|
1089
|
-
const assistant = {
|
|
1090
|
-
content: [
|
|
1091
|
-
fauxToolCall(
|
|
1092
|
-
"edit",
|
|
1093
|
-
{
|
|
1094
|
-
path: "src/file.ts",
|
|
1095
|
-
oldText: "old line",
|
|
1096
|
-
newText: "new line",
|
|
1097
|
-
},
|
|
1098
|
-
{ id: "tool-1" },
|
|
1099
|
-
),
|
|
1100
|
-
],
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
// Act
|
|
1104
|
-
const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
|
|
1105
|
-
|
|
1106
|
-
// Assert
|
|
1107
|
-
expect(text).toContain("edit ->");
|
|
1108
|
-
expect(text).toContain("src/file.ts");
|
|
1109
|
-
expect(text).toContain("old line");
|
|
1110
|
-
expect(text).toContain("new line");
|
|
1111
|
-
expect(text).not.toContain("+new line");
|
|
1112
|
-
expect(text).not.toContain("-old line");
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
test("renderAssistantMessage for an edit tool call colors old text red and new text green", async () => {
|
|
1116
|
-
// Arrange
|
|
1117
|
-
const assistant = {
|
|
1118
|
-
content: [
|
|
1119
|
-
fauxToolCall(
|
|
1120
|
-
"edit",
|
|
1121
|
-
{
|
|
1122
|
-
path: "src/file.ts",
|
|
1123
|
-
oldText: "old line",
|
|
1124
|
-
newText: "new line",
|
|
1125
|
-
},
|
|
1126
|
-
{ id: "tool-1" },
|
|
1127
|
-
),
|
|
1128
|
-
],
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
// Act
|
|
1132
|
-
const rows = await renderBufferRows(
|
|
1133
|
-
renderAssistantMessage(assistant, RENDER_OPTS),
|
|
1134
|
-
PREVIEW_WIDTH,
|
|
1135
|
-
12,
|
|
1136
|
-
);
|
|
1137
|
-
const oldRow = rows.find((row) => row.text.includes("old line"));
|
|
1138
|
-
const newRow = rows.find((row) => row.text.includes("new line"));
|
|
1139
|
-
|
|
1140
|
-
// Assert
|
|
1141
|
-
expect(oldRow).toBeDefined();
|
|
1142
|
-
expect(newRow).toBeDefined();
|
|
1143
|
-
|
|
1144
|
-
const oldColor = oldRow?.fgColors[oldRow.text.indexOf("o")];
|
|
1145
|
-
const newColor = newRow?.fgColors[newRow.text.indexOf("n")];
|
|
1146
|
-
expect(oldColor).toBe(DEFAULT_THEME.diffRemoved ?? null);
|
|
1147
|
-
expect(newColor).toBe(DEFAULT_THEME.diffAdded ?? null);
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
test("renderToolResult for shell output in non-verbose mode shows the visible tail under a result header", async () => {
|
|
1151
|
-
// Arrange
|
|
1152
|
-
const output = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join(
|
|
1153
|
-
"\n",
|
|
1154
|
-
);
|
|
1155
|
-
|
|
1156
|
-
// Act
|
|
1157
|
-
const text = await renderVisibleText(
|
|
1158
|
-
renderToolResult(
|
|
1159
|
-
"shell",
|
|
1160
|
-
{ command: "seq 1 25" },
|
|
1161
|
-
output,
|
|
1162
|
-
false,
|
|
1163
|
-
RENDER_OPTS,
|
|
1164
|
-
),
|
|
1165
|
-
PREVIEW_WIDTH,
|
|
1166
|
-
24,
|
|
1167
|
-
);
|
|
1168
|
-
|
|
1169
|
-
// Assert
|
|
1170
|
-
expect(text).toContain("shell <-");
|
|
1171
|
-
expect(text).toContain("line 18");
|
|
1172
|
-
expect(text).toContain("line 25");
|
|
1173
|
-
expect(text).toContain("And 17 lines more");
|
|
1174
|
-
expect(text).not.toContain("line 17");
|
|
1175
|
-
expect(text).not.toContain("seq 1 25");
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
test("renderToolResult for a wrapped shell preview keeps the summary directly below the visible tail", async () => {
|
|
1179
|
-
// Arrange
|
|
1180
|
-
const output = [
|
|
1181
|
-
"line 1",
|
|
1182
|
-
"line 2",
|
|
1183
|
-
"line 3",
|
|
1184
|
-
"line 4",
|
|
1185
|
-
"this is a very long wrapped line that will take more than six rendered rows in the preview width so it gets dropped entirely",
|
|
1186
|
-
"tail A",
|
|
1187
|
-
"tail B",
|
|
1188
|
-
].join("\n");
|
|
1189
|
-
|
|
1190
|
-
// Act
|
|
1191
|
-
const rows = await renderBufferRows(
|
|
1192
|
-
renderToolResult("shell", { command: "demo" }, output, false, {
|
|
1193
|
-
...RENDER_OPTS,
|
|
1194
|
-
previewWidth: 24,
|
|
1195
|
-
}),
|
|
1196
|
-
24,
|
|
1197
|
-
20,
|
|
1198
|
-
);
|
|
1199
|
-
const tailRowIndex = rows.findIndex((row) => row.text.includes("tail B"));
|
|
1200
|
-
const summaryRowIndex = rows.findIndex(
|
|
1201
|
-
(row) => row.text.includes("And ") && row.text.includes(" lines more"),
|
|
1202
|
-
);
|
|
1203
|
-
|
|
1204
|
-
// Assert
|
|
1205
|
-
expect(tailRowIndex).toBeGreaterThan(-1);
|
|
1206
|
-
expect(summaryRowIndex).toBe(tailRowIndex + 1);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
test("renderToolResult for shell output in verbose mode shows the full stored output", () => {
|
|
1210
|
-
// Arrange
|
|
1211
|
-
const output = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join(
|
|
1212
|
-
"\n",
|
|
1213
|
-
);
|
|
1214
|
-
|
|
1215
|
-
// Act
|
|
1216
|
-
const text = collectText(
|
|
1217
|
-
renderToolResult("shell", { command: "seq 1 25" }, output, false, {
|
|
1218
|
-
...RENDER_OPTS,
|
|
1219
|
-
verbose: true,
|
|
1220
|
-
}),
|
|
1221
|
-
);
|
|
1222
|
-
|
|
1223
|
-
// Assert
|
|
1224
|
-
expect(text).toContain("line 17");
|
|
1225
|
-
expect(text).toContain("line 25");
|
|
1226
|
-
expect(text).not.toContain("And 17 lines more");
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
test("renderToolResult for shell errors normalizes exit-code and stderr labels", () => {
|
|
1230
|
-
// Arrange
|
|
1231
|
-
const resultText = "Exit code: 42\n[stderr]\nboom";
|
|
1232
|
-
|
|
1233
|
-
// Act
|
|
1234
|
-
const text = collectText(
|
|
1235
|
-
renderToolResult(
|
|
1236
|
-
"shell",
|
|
1237
|
-
{ command: "exit 42" },
|
|
1238
|
-
resultText,
|
|
1239
|
-
true,
|
|
1240
|
-
RENDER_OPTS,
|
|
1241
|
-
),
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
// Assert
|
|
1245
|
-
expect(text).toContain("shell <-");
|
|
1246
|
-
expect(text).toContain("exit 42");
|
|
1247
|
-
expect(text).toContain("boom");
|
|
1248
|
-
expect(text).not.toContain("Exit code: 42");
|
|
1249
|
-
expect(text).not.toContain("[stderr]");
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
test("renderToolResult for a readImage success shows a compact path result", () => {
|
|
1253
|
-
// Arrange
|
|
1254
|
-
const args = { path: "diagram.png" };
|
|
1255
|
-
|
|
1256
|
-
// Act
|
|
1257
|
-
const text = collectText(
|
|
1258
|
-
renderToolResult("readImage", args, "", false, RENDER_OPTS),
|
|
1259
|
-
);
|
|
1260
|
-
|
|
1261
|
-
// Assert
|
|
1262
|
-
expect(text).toContain("read image <-");
|
|
1263
|
-
expect(text).toContain("diagram.png");
|
|
1264
|
-
expect(text).not.toContain("Read image.");
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
test("renderToolResult for a readImage error shows the full error even when verbose is off", () => {
|
|
1268
|
-
// Arrange
|
|
1269
|
-
const errorText = Array.from(
|
|
1270
|
-
{ length: 25 },
|
|
1271
|
-
(_, i) => `error ${i + 1}`,
|
|
1272
|
-
).join("\n");
|
|
1273
|
-
|
|
1274
|
-
// Act
|
|
1275
|
-
const text = collectText(
|
|
1276
|
-
renderToolResult(
|
|
1277
|
-
"readImage",
|
|
1278
|
-
{ path: "diagram.png" },
|
|
1279
|
-
errorText,
|
|
1280
|
-
true,
|
|
1281
|
-
RENDER_OPTS,
|
|
1282
|
-
),
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
null,
|
|
332
|
+
2,
|
|
1283
333
|
);
|
|
1284
334
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
// Arrange
|
|
1293
|
-
const args = {
|
|
1294
|
-
path: "src/file.ts",
|
|
1295
|
-
oldText: "before",
|
|
1296
|
-
newText: "after",
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
// Act
|
|
1300
|
-
const previewText = collectText(
|
|
1301
|
-
renderToolResult("edit", args, "Edited src/file.ts", false, RENDER_OPTS),
|
|
1302
|
-
);
|
|
1303
|
-
const verboseText = collectText(
|
|
1304
|
-
renderToolResult("edit", args, "Edited src/file.ts", false, {
|
|
1305
|
-
...RENDER_OPTS,
|
|
335
|
+
const node = renderToolResult(
|
|
336
|
+
"grep",
|
|
337
|
+
{ pattern: "renderToolBlock" },
|
|
338
|
+
resultText,
|
|
339
|
+
false,
|
|
340
|
+
{
|
|
341
|
+
showReasoning: true,
|
|
1306
342
|
verbose: true,
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
expect(previewText).toContain("edit <-");
|
|
1312
|
-
expect(previewText).toContain("~ src/file.ts");
|
|
1313
|
-
expect(previewText).not.toContain("before");
|
|
1314
|
-
expect(previewText).not.toContain("after");
|
|
1315
|
-
expect(previewText).not.toContain("And 1 lines more");
|
|
1316
|
-
expect(verboseText).toEqual(previewText);
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
test("renderToolResult for an edit error uses the preview policy in non-verbose mode", async () => {
|
|
1320
|
-
// Arrange
|
|
1321
|
-
const errorText = Array.from(
|
|
1322
|
-
{ length: 25 },
|
|
1323
|
-
(_, i) => `error ${i + 1}`,
|
|
1324
|
-
).join("\n");
|
|
1325
|
-
|
|
1326
|
-
// Act
|
|
1327
|
-
const text = await renderVisibleText(
|
|
1328
|
-
renderToolResult(
|
|
1329
|
-
"edit",
|
|
1330
|
-
{
|
|
1331
|
-
path: "src/file.ts",
|
|
1332
|
-
oldText: "before",
|
|
1333
|
-
newText: "after",
|
|
1334
|
-
},
|
|
1335
|
-
errorText,
|
|
1336
|
-
true,
|
|
1337
|
-
RENDER_OPTS,
|
|
1338
|
-
),
|
|
1339
|
-
PREVIEW_WIDTH,
|
|
1340
|
-
24,
|
|
1341
|
-
);
|
|
1342
|
-
|
|
1343
|
-
// Assert
|
|
1344
|
-
expect(text).toContain("edit <-");
|
|
1345
|
-
expect(text).toContain("error 18");
|
|
1346
|
-
expect(text).toContain("error 25");
|
|
1347
|
-
expect(text).toContain("And 17 lines more");
|
|
1348
|
-
expect(text).not.toContain("error 17");
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
test("renderToolResult for todoWrite shows the full checklist even when verbose is off", async () => {
|
|
1352
|
-
const snapshot = JSON.stringify({
|
|
1353
|
-
todos: [
|
|
1354
|
-
{ content: "Review prompt wording", status: "completed" },
|
|
1355
|
-
{ content: "Implement todo tools", status: "in_progress" },
|
|
1356
|
-
{ content: "Add /todo command", status: "pending" },
|
|
1357
|
-
{ content: "Run the full verification suite", status: "pending" },
|
|
1358
|
-
],
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
const text = await renderVisibleText(
|
|
1362
|
-
renderToolResult("todoWrite", {}, snapshot, false, RENDER_OPTS),
|
|
1363
|
-
PREVIEW_WIDTH,
|
|
1364
|
-
24,
|
|
1365
|
-
);
|
|
1366
|
-
|
|
1367
|
-
expect(text).toContain("todo write <-");
|
|
1368
|
-
expect(text).toContain("[x] Review prompt wording");
|
|
1369
|
-
expect(text).toContain("[~] Implement todo tools");
|
|
1370
|
-
expect(text).toContain("[ ] Add /todo command");
|
|
1371
|
-
expect(text.join(" ")).toContain("[ ] Run the full verification suite");
|
|
1372
|
-
expect(text.some((line) => line.startsWith("And "))).toBe(false);
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
test("buildConversationLogNodes syntax-highlights markdown-formatted UI info messages", async () => {
|
|
1376
|
-
const theme = {
|
|
1377
|
-
...DEFAULT_THEME,
|
|
1378
|
-
accentText: "color14",
|
|
1379
|
-
secondaryAccentText: "color09",
|
|
1380
|
-
diffAdded: "color10",
|
|
1381
|
-
} satisfies typeof DEFAULT_THEME;
|
|
1382
|
-
const state = {
|
|
1383
|
-
messages: [
|
|
1384
|
-
{
|
|
1385
|
-
role: "ui" as const,
|
|
1386
|
-
kind: "info" as const,
|
|
1387
|
-
format: "markdown" as const,
|
|
1388
|
-
content: "# Help\n\n## Commands\n\n- `/model` — Select a model",
|
|
1389
|
-
timestamp: 1,
|
|
1390
|
-
},
|
|
1391
|
-
],
|
|
1392
|
-
showReasoning: false,
|
|
1393
|
-
verbose: false,
|
|
1394
|
-
theme,
|
|
1395
|
-
};
|
|
1396
|
-
|
|
1397
|
-
const rows = await renderBufferRows(
|
|
1398
|
-
VStack(
|
|
1399
|
-
{},
|
|
1400
|
-
buildConversationLogNodes(
|
|
1401
|
-
state,
|
|
1402
|
-
{ isStreaming: false, content: [], pendingToolResults: [] },
|
|
1403
|
-
0,
|
|
1404
|
-
PREVIEW_WIDTH,
|
|
1405
|
-
),
|
|
1406
|
-
),
|
|
1407
|
-
PREVIEW_WIDTH,
|
|
1408
|
-
24,
|
|
1409
|
-
);
|
|
1410
|
-
const headingRow = rows.find((row) => row.text.includes("# Help"));
|
|
1411
|
-
const bulletRow = rows.find((row) => row.text.includes("- `/model`"));
|
|
1412
|
-
|
|
1413
|
-
expect(headingRow).toBeDefined();
|
|
1414
|
-
expect(bulletRow).toBeDefined();
|
|
1415
|
-
expect(headingRow?.fgColors[headingRow.text.indexOf("#")]).toBe(
|
|
1416
|
-
theme.accentText ?? null,
|
|
1417
|
-
);
|
|
1418
|
-
expect(bulletRow?.fgColors[bulletRow.text.indexOf("-")]).toBe(
|
|
1419
|
-
theme.secondaryAccentText ?? null,
|
|
1420
|
-
);
|
|
1421
|
-
expect(bulletRow?.fgColors[bulletRow.text.indexOf("`")]).toBe(
|
|
1422
|
-
theme.diffAdded ?? null,
|
|
1423
|
-
);
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
test("buildConversationLogNodes renders UI todo messages with the shared checklist block", async () => {
|
|
1427
|
-
const state = {
|
|
1428
|
-
messages: [
|
|
1429
|
-
{
|
|
1430
|
-
role: "ui" as const,
|
|
1431
|
-
kind: "todo" as const,
|
|
1432
|
-
todos: [
|
|
1433
|
-
{ content: "Review prompt wording", status: "completed" as const },
|
|
1434
|
-
{ content: "Implement todo tools", status: "in_progress" as const },
|
|
1435
|
-
{ content: "Add /todo command", status: "pending" as const },
|
|
1436
|
-
],
|
|
1437
|
-
timestamp: 1,
|
|
1438
|
-
},
|
|
1439
|
-
],
|
|
1440
|
-
showReasoning: false,
|
|
1441
|
-
verbose: false,
|
|
1442
|
-
theme: DEFAULT_THEME,
|
|
1443
|
-
};
|
|
1444
|
-
|
|
1445
|
-
const text = await renderVisibleText(
|
|
1446
|
-
VStack(
|
|
1447
|
-
{},
|
|
1448
|
-
buildConversationLogNodes(
|
|
1449
|
-
state,
|
|
1450
|
-
{ isStreaming: false, content: [], pendingToolResults: [] },
|
|
1451
|
-
0,
|
|
1452
|
-
PREVIEW_WIDTH,
|
|
1453
|
-
),
|
|
1454
|
-
),
|
|
1455
|
-
PREVIEW_WIDTH,
|
|
1456
|
-
24,
|
|
1457
|
-
);
|
|
1458
|
-
|
|
1459
|
-
expect(text).toContain("todo");
|
|
1460
|
-
expect(text).toContain("[x] Review prompt wording");
|
|
1461
|
-
expect(text).toContain("[~] Implement todo tools");
|
|
1462
|
-
expect(text).toContain("[ ] Add /todo command");
|
|
1463
|
-
expect(text.some((line) => line.includes('"todos"'))).toBe(false);
|
|
1464
|
-
});
|
|
1465
|
-
|
|
1466
|
-
test("renderToolResult for a generic plugin tool uses the shared result header", () => {
|
|
1467
|
-
// Arrange
|
|
1468
|
-
const args = { query: "session persistence sqlite turn numbering" };
|
|
1469
|
-
|
|
1470
|
-
// Act
|
|
1471
|
-
const text = collectText(
|
|
1472
|
-
renderToolResult(
|
|
1473
|
-
"mcp/search",
|
|
1474
|
-
args,
|
|
1475
|
-
"session persistence sqlite turn numbering",
|
|
1476
|
-
false,
|
|
1477
|
-
RENDER_OPTS,
|
|
1478
|
-
),
|
|
343
|
+
theme: DEFAULT_THEME,
|
|
344
|
+
cwd: "/tmp/project",
|
|
345
|
+
previewWidth: 80,
|
|
346
|
+
},
|
|
1479
347
|
);
|
|
1480
348
|
|
|
1481
|
-
|
|
1482
|
-
expect(
|
|
1483
|
-
expect(
|
|
1484
|
-
expect(
|
|
349
|
+
const lines = collectRenderedLines(node);
|
|
350
|
+
expect(lines).toContain("│ grep <-");
|
|
351
|
+
expect(lines).toContain("│ src/ui/conversation.ts");
|
|
352
|
+
expect(lines).toContain("│ 857: function renderToolBlock(");
|
|
353
|
+
expect(lines).toContain("│ 858: spec: ToolBlockSpec,");
|
|
354
|
+
expect(lines.join("\n")).not.toContain('"files"');
|
|
355
|
+
expect(lines.join("\n")).not.toContain('"kind"');
|
|
356
|
+
expect(
|
|
357
|
+
findTextNode(node, " 857: function renderToolBlock(").props.fgColor,
|
|
358
|
+
).toBe(DEFAULT_THEME.toolText);
|
|
1485
359
|
});
|
|
1486
360
|
});
|