mini-coder 0.5.11 → 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.
@@ -0,0 +1,360 @@
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";
4
+ import { DEFAULT_THEME } from "../theme.ts";
5
+ import {
6
+ buildConversationLogNodes,
7
+ renderAssistantMessage,
8
+ renderToolResult,
9
+ } from "./conversation.ts";
10
+
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
+ }
20
+
21
+ function collectRenderedLines(node: Node | null): string[] {
22
+ if (!node) {
23
+ return [];
24
+ }
25
+ if (node.type === "text") {
26
+ return [node.content];
27
+ }
28
+ if (node.type === "textinput") {
29
+ return [];
30
+ }
31
+ if (
32
+ node.type === "hstack" &&
33
+ node.children.length === 2 &&
34
+ node.children[0]?.type === "text" &&
35
+ node.children[1]?.type === "vstack"
36
+ ) {
37
+ const prefix = node.children[0].content;
38
+ return collectRenderedLines(node.children[1]).map(
39
+ (line) => `${prefix}${line}`,
40
+ );
41
+ }
42
+ if (node.type === "hstack") {
43
+ return [collectInlineText(node)];
44
+ }
45
+ return node.children.flatMap((child) => collectRenderedLines(child));
46
+ }
47
+
48
+ function collectTextNodes(node: Node | null): TextNode[] {
49
+ if (!node || node.type === "textinput") {
50
+ return [];
51
+ }
52
+ if (node.type === "text") {
53
+ return [node];
54
+ }
55
+ return node.children.flatMap((child) => collectTextNodes(child));
56
+ }
57
+
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}`);
65
+ }
66
+ return textNode;
67
+ }
68
+
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
+ };
100
+ }
101
+
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
+ }
114
+
115
+ describe("ui/conversation", () => {
116
+ test("read tool-call previews render structured arguments instead of raw JSON", () => {
117
+ const node = renderAssistantMessage(
118
+ {
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
+ ],
131
+ },
132
+ {
133
+ showReasoning: true,
134
+ verbose: false,
135
+ theme: DEFAULT_THEME,
136
+ cwd: "/tmp/project",
137
+ previewWidth: 80,
138
+ },
139
+ );
140
+
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"');
147
+ });
148
+
149
+ test("shell tool-call previews use cel-tui syntax scopes for custom colors", () => {
150
+ const node = renderAssistantMessage(
151
+ {
152
+ content: [
153
+ {
154
+ type: "toolCall",
155
+ id: "call-shell",
156
+ name: "shell",
157
+ arguments: {
158
+ command: 'if true; then echo "$HOME"; fi',
159
+ },
160
+ },
161
+ ],
162
+ },
163
+ {
164
+ showReasoning: true,
165
+ verbose: true,
166
+ theme: DEFAULT_THEME,
167
+ cwd: "/tmp/project",
168
+ previewWidth: 80,
169
+ },
170
+ );
171
+
172
+ expect(findTextNode(node, "true").props.fgColor).toBe(
173
+ DEFAULT_THEME.secondaryAccentText,
174
+ );
175
+ });
176
+
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]";
181
+
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
+ ),
196
+ );
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
+ ),
211
+ );
212
+
213
+ expect(compactLines[0]).toBe(
214
+ "│ read <- /tmp/project/src/ui/conversation.ts",
215
+ );
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
+ });
221
+
222
+ test("read tool results preserve literal continuation-looking lines from the file body without showing the model paging hint", () => {
223
+ const nodes = buildConversationLogNodes(
224
+ {
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",
243
+ },
244
+ {
245
+ isStreaming: false,
246
+ content: [],
247
+ pendingToolResults: [],
248
+ },
249
+ 0,
250
+ 80,
251
+ );
252
+
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.");
256
+ });
257
+
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
+ ),
273
+ );
274
+
275
+ expect(lines).toContain("│ alpha");
276
+ expect(lines).toContain("│ beta");
277
+ expect(lines.some((line) => line.includes("\r"))).toBe(false);
278
+ });
279
+
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",
285
+ false,
286
+ {
287
+ showReasoning: true,
288
+ verbose: true,
289
+ theme: DEFAULT_THEME,
290
+ cwd: "/tmp/project",
291
+ previewWidth: 20,
292
+ },
293
+ );
294
+
295
+ expect(findTextNode(node, "42").props.fgColor).toBe(
296
+ DEFAULT_THEME.secondaryAccentText,
297
+ );
298
+ expect(
299
+ findTextNode(node, "supercalifragilisticexpialidociousIdentifier")
300
+ .content,
301
+ ).toBe("supercalifragilisticexpialidociousIdentifier");
302
+ expect(
303
+ findTextNode(node, "supercalifragilisticexpialidociousIdentifier").props
304
+ .fgColor,
305
+ ).toBeUndefined();
306
+ });
307
+
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: [
314
+ {
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
+ },
327
+ ],
328
+ },
329
+ ],
330
+ },
331
+ null,
332
+ 2,
333
+ );
334
+
335
+ const node = renderToolResult(
336
+ "grep",
337
+ { pattern: "renderToolBlock" },
338
+ resultText,
339
+ false,
340
+ {
341
+ showReasoning: true,
342
+ verbose: true,
343
+ theme: DEFAULT_THEME,
344
+ cwd: "/tmp/project",
345
+ previewWidth: 80,
346
+ },
347
+ );
348
+
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);
359
+ });
360
+ });