mini-coder 0.5.14 → 0.6.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.
Files changed (70) hide show
  1. package/README.md +25 -109
  2. package/bin/mc.ts +8 -11
  3. package/bun.lock +79 -269
  4. package/package.json +17 -22
  5. package/src/agent.ts +237 -1403
  6. package/src/args.ts +289 -0
  7. package/src/headless.ts +43 -358
  8. package/src/index.ts +29 -1016
  9. package/src/oauth.ts +117 -0
  10. package/src/prompt.ts +227 -284
  11. package/src/session.ts +55 -1306
  12. package/src/shared.ts +117 -38
  13. package/src/tool-bash.ts +110 -0
  14. package/src/tool-edit.ts +133 -0
  15. package/src/tool-task.ts +114 -0
  16. package/src/tui-components.ts +150 -0
  17. package/src/tui-conversation.ts +262 -0
  18. package/src/tui-editor.ts +29 -0
  19. package/src/tui-overlay.ts +403 -0
  20. package/src/tui.ts +236 -0
  21. package/src/types.ts +160 -0
  22. package/tsconfig.json +17 -0
  23. package/BENCHMARK.md +0 -107
  24. package/LICENSE +0 -9
  25. package/PROGRESS.md +0 -5
  26. package/assets/icon-1-minimal.svg +0 -31
  27. package/assets/icon-2-dark-terminal.svg +0 -48
  28. package/assets/icon-3-gradient-modern.svg +0 -45
  29. package/assets/icon-4-filled-bold.svg +0 -54
  30. package/assets/icon-5-community-badge.svg +0 -63
  31. package/assets/mc-claude-smart.png +0 -0
  32. package/assets/mc-gpt-smart.png +0 -0
  33. package/assets/preview-0-5-0.png +0 -0
  34. package/assets/preview.gif +0 -0
  35. package/benchmark-baseline.sh +0 -15
  36. package/benchmark-loop.sh +0 -19
  37. package/skills-lock.json +0 -15
  38. package/src/assistant-output.ts +0 -73
  39. package/src/cli.ts +0 -134
  40. package/src/delegation.ts +0 -238
  41. package/src/errors.ts +0 -15
  42. package/src/git.ts +0 -247
  43. package/src/input.ts +0 -168
  44. package/src/mcp.ts +0 -609
  45. package/src/paths.ts +0 -37
  46. package/src/session-message.ts +0 -385
  47. package/src/settings.ts +0 -449
  48. package/src/skills.ts +0 -271
  49. package/src/submit.ts +0 -376
  50. package/src/text.ts +0 -71
  51. package/src/theme.ts +0 -330
  52. package/src/tool-common.ts +0 -93
  53. package/src/tool-delegate.ts +0 -125
  54. package/src/tool-grep.ts +0 -606
  55. package/src/tool-read.ts +0 -313
  56. package/src/tool-shell.ts +0 -1051
  57. package/src/tools.ts +0 -1179
  58. package/src/ui/agent.ts +0 -320
  59. package/src/ui/commands.test.ts +0 -957
  60. package/src/ui/commands.ts +0 -848
  61. package/src/ui/conversation.test.ts +0 -585
  62. package/src/ui/conversation.ts +0 -1836
  63. package/src/ui/help.ts +0 -158
  64. package/src/ui/input.test.ts +0 -64
  65. package/src/ui/input.ts +0 -138
  66. package/src/ui/overlay.ts +0 -59
  67. package/src/ui/runtime.ts +0 -69
  68. package/src/ui/status.ts +0 -220
  69. package/src/ui.ts +0 -1190
  70. package/src/version.ts +0 -48
@@ -1,585 +0,0 @@
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 makeAssistantToolCallMessage(): AssistantMessage {
59
- return {
60
- role: "assistant",
61
- content: [
62
- {
63
- type: "toolCall",
64
- id: "call-read",
65
- name: "read",
66
- arguments: { path: "hint.txt", limit: 3 },
67
- },
68
- ],
69
- api: "anthropic-messages",
70
- provider: "anthropic",
71
- model: "claude-sonnet-4-20250514",
72
- usage: {
73
- input: 0,
74
- output: 0,
75
- cacheRead: 0,
76
- cacheWrite: 0,
77
- totalTokens: 0,
78
- cost: {
79
- input: 0,
80
- output: 0,
81
- cacheRead: 0,
82
- cacheWrite: 0,
83
- total: 0,
84
- },
85
- },
86
- stopReason: "toolUse",
87
- timestamp: 1,
88
- };
89
- }
90
-
91
- function makeReadToolResultMessage(
92
- content: ToolResultMessage["content"],
93
- ): ToolResultMessage {
94
- return {
95
- role: "toolResult",
96
- toolCallId: "call-read",
97
- toolName: "read",
98
- content,
99
- isError: false,
100
- timestamp: 2,
101
- };
102
- }
103
-
104
- describe("ui/conversation", () => {
105
- test("read tool-call previews render structured arguments instead of raw JSON", () => {
106
- const node = renderAssistantMessage(
107
- {
108
- content: [
109
- {
110
- type: "toolCall",
111
- id: "call-read",
112
- name: "read",
113
- arguments: {
114
- path: "src/ui/conversation.ts",
115
- offset: 820,
116
- limit: 80,
117
- },
118
- },
119
- ],
120
- },
121
- {
122
- showReasoning: true,
123
- verbose: false,
124
- theme: DEFAULT_THEME,
125
- cwd: "/tmp/project",
126
- previewWidth: 80,
127
- },
128
- );
129
-
130
- const lines = collectRenderedLines(node);
131
- expect(lines).toContain("│ read ->");
132
- expect(lines).toContain("│ src/ui/conversation.ts");
133
- expect(lines).toContain("│ offset: 820");
134
- expect(lines).toContain("│ limit: 80");
135
- expect(lines.join("\n")).not.toContain('"path"');
136
- });
137
-
138
- test("shell tool-call previews render the command instead of raw JSON", () => {
139
- const node = renderAssistantMessage(
140
- {
141
- content: [
142
- {
143
- type: "toolCall",
144
- id: "call-shell",
145
- name: "shell",
146
- arguments: {
147
- command: 'if true; then echo "$HOME"; fi',
148
- },
149
- },
150
- ],
151
- },
152
- {
153
- showReasoning: true,
154
- verbose: true,
155
- theme: DEFAULT_THEME,
156
- cwd: "/tmp/project",
157
- previewWidth: 80,
158
- },
159
- );
160
-
161
- const lines = collectRenderedLines(node);
162
- expect(lines).toContain("│ shell ->");
163
- expect(lines.join("\n")).toContain('if true; then echo "$HOME"; fi');
164
- expect(lines.join("\n")).not.toContain('"command"');
165
- });
166
-
167
- test("compact shell tool-call previews only render the visible tail slice", () => {
168
- const command = Array.from(
169
- { length: 14 },
170
- (_, index) => `echo line ${index + 1}`,
171
- ).join("\n");
172
-
173
- const compactLines = collectRenderedLines(
174
- renderAssistantMessage(
175
- {
176
- content: [
177
- {
178
- type: "toolCall",
179
- id: "call-shell",
180
- name: "shell",
181
- arguments: { command },
182
- },
183
- ],
184
- },
185
- {
186
- showReasoning: true,
187
- verbose: false,
188
- theme: DEFAULT_THEME,
189
- cwd: "/tmp/project",
190
- previewWidth: 80,
191
- },
192
- ),
193
- );
194
- const verboseLines = collectRenderedLines(
195
- renderAssistantMessage(
196
- {
197
- content: [
198
- {
199
- type: "toolCall",
200
- id: "call-shell",
201
- name: "shell",
202
- arguments: { command },
203
- },
204
- ],
205
- },
206
- {
207
- showReasoning: true,
208
- verbose: true,
209
- theme: DEFAULT_THEME,
210
- cwd: "/tmp/project",
211
- previewWidth: 80,
212
- },
213
- ),
214
- );
215
-
216
- expect(compactLines).toContain("│ shell ->");
217
- expect(compactLines).toContain("│ echo line 14");
218
- expect(compactLines).toContain("│ And 6 lines more");
219
- expect(compactLines).not.toContain("│ echo line 1");
220
- expect(verboseLines).toContain("│ echo line 1");
221
- expect(compactLines.length).toBeLessThan(verboseLines.length);
222
- });
223
-
224
- test("MCP tool-call previews honor verbose mode", () => {
225
- const args = {
226
- query: "routing",
227
- section: "guides",
228
- limit: 10,
229
- offset: 0,
230
- filter1: "alpha",
231
- filter2: "beta",
232
- filter3: "gamma",
233
- filter4: "delta",
234
- filter5: "epsilon",
235
- filter6: "zeta",
236
- filter7: "eta",
237
- filter8: "theta",
238
- };
239
-
240
- const compactLines = collectRenderedLines(
241
- renderAssistantMessage(
242
- {
243
- content: [
244
- {
245
- type: "toolCall",
246
- id: "call-mcp",
247
- name: "docs__search",
248
- arguments: args,
249
- },
250
- ],
251
- },
252
- {
253
- showReasoning: true,
254
- verbose: false,
255
- theme: DEFAULT_THEME,
256
- cwd: "/tmp/project",
257
- previewWidth: 80,
258
- },
259
- ),
260
- );
261
- const verboseLines = collectRenderedLines(
262
- renderAssistantMessage(
263
- {
264
- content: [
265
- {
266
- type: "toolCall",
267
- id: "call-mcp",
268
- name: "docs__search",
269
- arguments: args,
270
- },
271
- ],
272
- },
273
- {
274
- showReasoning: true,
275
- verbose: true,
276
- theme: DEFAULT_THEME,
277
- cwd: "/tmp/project",
278
- previewWidth: 80,
279
- },
280
- ),
281
- );
282
-
283
- expect(compactLines).toContain("│ docs__search ->");
284
- expect(compactLines.some((line) => line.includes("And "))).toBe(true);
285
- expect(compactLines.join("\n")).not.toContain('"query": "routing"');
286
- expect(verboseLines.join("\n")).toContain('"query": "routing"');
287
- expect(compactLines.length).toBeLessThan(verboseLines.length);
288
- });
289
-
290
- test("shell tool results render structured stdout/stderr details without stderr labels or error styling", () => {
291
- const stdout = Array.from(
292
- { length: 12 },
293
- (_, index) => `line ${index + 1}`,
294
- ).join("\n");
295
- const node = renderToolResult(
296
- "shell",
297
- { command: "run-tests" },
298
- "Exit code: 1\nlegacy text should be ignored when details exist",
299
- true,
300
- {
301
- showReasoning: true,
302
- verbose: false,
303
- theme: DEFAULT_THEME,
304
- cwd: "/tmp/project",
305
- previewWidth: 48,
306
- },
307
- {
308
- stdout,
309
- stderr: "boom",
310
- exitCode: 1,
311
- },
312
- );
313
- const lines = collectRenderedLines(node);
314
- const textNodes = collectTextNodes(node);
315
-
316
- expect(lines).toContain("│ shell <-");
317
- expect(lines).toContain("│ boom");
318
- expect(lines).toContain("│ exit 1");
319
- expect(lines).toContain("│ And 6 lines more");
320
- expect(lines.join("\n")).not.toContain("stderr:");
321
- expect(lines.join("\n")).not.toContain("legacy text should be ignored");
322
- expect(
323
- textNodes.some(
324
- (textNode) =>
325
- textNode.content === "boom" &&
326
- textNode.props.fgColor === DEFAULT_THEME.toolText,
327
- ),
328
- ).toBe(true);
329
- expect(
330
- textNodes.some(
331
- (textNode) =>
332
- textNode.content === "exit 1" &&
333
- textNode.props.fgColor === DEFAULT_THEME.toolText,
334
- ),
335
- ).toBe(true);
336
- });
337
-
338
- test("shell tool results still render legacy flattened results from persisted history without stderr labels", () => {
339
- const node = renderToolResult(
340
- "shell",
341
- { command: "run-tests" },
342
- "Exit code: 1\nout\n\n[stderr]\nerr",
343
- true,
344
- {
345
- showReasoning: true,
346
- verbose: true,
347
- theme: DEFAULT_THEME,
348
- cwd: "/tmp/project",
349
- previewWidth: 80,
350
- },
351
- );
352
- const lines = collectRenderedLines(node);
353
- const textNodes = collectTextNodes(node);
354
-
355
- expect(lines).toContain("│ out");
356
- expect(lines).toContain("│ err");
357
- expect(lines).toContain("│ exit 1");
358
- expect(lines.join("\n")).not.toContain("stderr:");
359
- expect(lines.join("\n")).not.toContain("[stderr]");
360
- expect(lines.join("\n")).not.toContain("Exit code:");
361
- expect(
362
- textNodes.some(
363
- (textNode) =>
364
- textNode.content === "err" &&
365
- textNode.props.fgColor === DEFAULT_THEME.toolText,
366
- ),
367
- ).toBe(true);
368
- expect(
369
- textNodes.some(
370
- (textNode) =>
371
- textNode.content === "exit 1" &&
372
- textNode.props.fgColor === DEFAULT_THEME.toolText,
373
- ),
374
- ).toBe(true);
375
- });
376
-
377
- test("read tool results include the resolved path, hide model paging hints, and render fewer body lines when verbose is off", () => {
378
- const fileBody =
379
- Array.from({ length: 14 }, (_, index) => `line ${index + 1}`).join("\n") +
380
- "\n\n[use offset=14 limit=14 to continue]";
381
-
382
- const compactLines = collectRenderedLines(
383
- renderToolResult(
384
- "read",
385
- { path: "src/ui/conversation.ts" },
386
- fileBody,
387
- false,
388
- {
389
- showReasoning: true,
390
- verbose: false,
391
- theme: DEFAULT_THEME,
392
- cwd: "/tmp/project",
393
- previewWidth: 48,
394
- },
395
- ),
396
- );
397
- const verboseLines = collectRenderedLines(
398
- renderToolResult(
399
- "read",
400
- { path: "src/ui/conversation.ts" },
401
- fileBody,
402
- false,
403
- {
404
- showReasoning: true,
405
- verbose: true,
406
- theme: DEFAULT_THEME,
407
- cwd: "/tmp/project",
408
- previewWidth: 48,
409
- },
410
- ),
411
- );
412
-
413
- expect(compactLines[0]).toBe(
414
- "│ read <- /tmp/project/src/ui/conversation.ts",
415
- );
416
- expect(compactLines.join("\n")).not.toContain("Use offset=14 limit=14");
417
- expect(verboseLines.join("\n")).not.toContain("Use offset=14 limit=14");
418
- expect(verboseLines).toContain("│ line 14");
419
- expect(compactLines.length).toBeLessThan(verboseLines.length);
420
- });
421
-
422
- test("read tool results preserve literal continuation-looking lines from the file body without showing the model paging hint", () => {
423
- const nodes = buildConversationLogNodes(
424
- {
425
- messages: [
426
- makeAssistantToolCallMessage(),
427
- makeReadToolResultMessage([
428
- {
429
- type: "text",
430
- text: "alpha\n\n[use offset=99 limit=10 to continue]\n",
431
- },
432
- {
433
- type: "text",
434
- text: "[use offset=3 limit=3 to continue]",
435
- },
436
- ]),
437
- ],
438
- showReasoning: true,
439
- verbose: true,
440
- theme: DEFAULT_THEME,
441
- cwd: "/tmp/project",
442
- versionLabel: "test",
443
- },
444
- {
445
- isStreaming: false,
446
- content: [],
447
- pendingToolResults: [],
448
- },
449
- 0,
450
- 80,
451
- );
452
-
453
- const lines = nodes.flatMap((node) => collectRenderedLines(node));
454
- expect(lines).toContain("│ [use offset=99 limit=10 to continue]");
455
- expect(lines.join("\n")).not.toContain("Use offset=3 limit=3 to continue.");
456
- });
457
-
458
- test("read tool fallback rendering normalizes CRLF line endings", () => {
459
- const lines = collectRenderedLines(
460
- renderToolResult(
461
- "read",
462
- { path: "notes.txt" },
463
- "alpha\r\nbeta\r\n",
464
- false,
465
- {
466
- showReasoning: true,
467
- verbose: true,
468
- theme: DEFAULT_THEME,
469
- cwd: "/tmp/project",
470
- previewWidth: 80,
471
- },
472
- ),
473
- );
474
-
475
- expect(lines).toContain("│ alpha");
476
- expect(lines).toContain("│ beta");
477
- expect(lines.some((line) => line.includes("\r"))).toBe(false);
478
- });
479
-
480
- test("read tool results use direct syntax-highlight rendering without chunking long tokens", () => {
481
- const node = renderToolResult(
482
- "read",
483
- { path: "src/example.ts" },
484
- "const supercalifragilisticexpialidociousIdentifier = 42",
485
- false,
486
- {
487
- showReasoning: true,
488
- verbose: true,
489
- theme: DEFAULT_THEME,
490
- cwd: "/tmp/project",
491
- previewWidth: 20,
492
- },
493
- );
494
-
495
- expect(
496
- collectTextNodes(node).some(
497
- (textNode) =>
498
- textNode.content === "supercalifragilisticexpialidociousIdentifier",
499
- ),
500
- ).toBe(true);
501
- });
502
-
503
- test("grep tool results render grouped files and lines instead of raw JSON", () => {
504
- const resultText = JSON.stringify(
505
- {
506
- limit: 10,
507
- truncated: false,
508
- files: [
509
- {
510
- path: "src/ui/conversation.ts",
511
- lines: [
512
- {
513
- kind: "match",
514
- lineNumber: 857,
515
- text: "function renderToolBlock(\n",
516
- },
517
- {
518
- kind: "context",
519
- lineNumber: 858,
520
- text: " spec: ToolBlockSpec,\n",
521
- },
522
- ],
523
- },
524
- ],
525
- },
526
- null,
527
- 2,
528
- );
529
-
530
- const node = renderToolResult(
531
- "grep",
532
- { pattern: "renderToolBlock" },
533
- resultText,
534
- false,
535
- {
536
- showReasoning: true,
537
- verbose: true,
538
- theme: DEFAULT_THEME,
539
- cwd: "/tmp/project",
540
- previewWidth: 80,
541
- },
542
- );
543
-
544
- const lines = collectRenderedLines(node);
545
- expect(lines).toContain("│ grep <-");
546
- expect(lines).toContain("│ src/ui/conversation.ts");
547
- expect(lines).toContain("│ 857: function renderToolBlock(");
548
- expect(lines).toContain("│ 858: spec: ToolBlockSpec,");
549
- expect(lines.join("\n")).not.toContain('"files"');
550
- expect(lines.join("\n")).not.toContain('"kind"');
551
- });
552
-
553
- test("MCP tool results honor verbose mode", () => {
554
- const output = Array.from(
555
- { length: 14 },
556
- (_, index) => `result ${index + 1}`,
557
- ).join("\n");
558
-
559
- const compactLines = collectRenderedLines(
560
- renderToolResult("docs__search", { query: "routing" }, output, false, {
561
- showReasoning: true,
562
- verbose: false,
563
- theme: DEFAULT_THEME,
564
- cwd: "/tmp/project",
565
- previewWidth: 80,
566
- }),
567
- );
568
- const verboseLines = collectRenderedLines(
569
- renderToolResult("docs__search", { query: "routing" }, output, false, {
570
- showReasoning: true,
571
- verbose: true,
572
- theme: DEFAULT_THEME,
573
- cwd: "/tmp/project",
574
- previewWidth: 80,
575
- }),
576
- );
577
-
578
- expect(compactLines).toContain("│ docs__search <-");
579
- expect(compactLines).toContain("│ result 14");
580
- expect(compactLines).toContain("│ And 6 lines more");
581
- expect(compactLines).not.toContain("│ result 1");
582
- expect(verboseLines).toContain("│ result 1");
583
- expect(compactLines.length).toBeLessThan(verboseLines.length);
584
- });
585
- });