mini-coder 0.5.1 → 0.5.3

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/src/ui/agent.ts CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { AssistantMessage } from "@mariozechner/pi-ai";
13
13
  import type { AgentEvent } from "../agent.ts";
14
+ import { getErrorMessage } from "../errors.ts";
14
15
  import type { AppState } from "../index.ts";
15
16
  import { resolveRawInput, submitResolvedInput } from "../submit.ts";
16
17
  import type {
@@ -75,10 +76,6 @@ export function getStreamingConversationState(): StreamingConversationState {
75
76
  };
76
77
  }
77
78
 
78
- function getErrorMessage(error: unknown): string {
79
- return error instanceof Error ? error.message : String(error);
80
- }
81
-
82
79
  /**
83
80
  * Create the UI agent controller bound to runtime hooks supplied by `ui.ts`.
84
81
  *
@@ -91,6 +91,9 @@ function createTestState(): AppState {
91
91
  activeTurnPromise: null,
92
92
  showReasoning: true,
93
93
  verbose: false,
94
+ versionLabel: "dev",
95
+ customModels: [],
96
+ startupWarnings: [],
94
97
  };
95
98
  }
96
99
 
@@ -14,6 +14,7 @@ import { Select } from "@cel-tui/components";
14
14
  import type { Model, ThinkingLevel } from "@mariozechner/pi-ai";
15
15
  import type { OAuthProviderInterface } from "@mariozechner/pi-ai/oauth";
16
16
  import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
17
+ import { getErrorMessage } from "../errors.ts";
17
18
  import type { AppState } from "../index.ts";
18
19
  import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
19
20
  import { COMMANDS } from "../input.ts";
@@ -39,10 +40,6 @@ const EFFORT_LEVELS: { label: string; value: ThinkingLevel }[] = [
39
40
  { label: "xhigh", value: "xhigh" },
40
41
  ];
41
42
 
42
- function getErrorMessage(error: unknown): string {
43
- return error instanceof Error ? error.message : String(error);
44
- }
45
-
46
43
  /** Runtime hooks injected from the stateful UI module. */
47
44
  interface UiCommandRuntime {
48
45
  /** Open an overlay and trigger a re-render. */
@@ -40,6 +40,12 @@ function collectText(node: Node | null): string[] {
40
40
  if (node.type === "textinput") {
41
41
  return [];
42
42
  }
43
+ if (
44
+ node.type === "hstack" &&
45
+ node.children.every((child) => child.type === "text")
46
+ ) {
47
+ return [node.children.map((child) => child.content).join("")];
48
+ }
43
49
  return node.children.flatMap((child) => collectText(child));
44
50
  }
45
51
 
@@ -244,9 +250,9 @@ describe("ui/conversation", () => {
244
250
 
245
251
  // Assert
246
252
  expect(text).toContain("Working...");
247
- expect(text.filter((line) => line === "[shell ->]")).toHaveLength(1);
253
+ expect(text.filter((line) => line === "shell ->")).toHaveLength(1);
248
254
  expect(text).toContain("echo hi");
249
- expect(text).toContain("[shell <-]");
255
+ expect(text).toContain("shell <-");
250
256
  expect(text).toContain("partial output");
251
257
  expect(text).not.toContain("Exit code: 0");
252
258
  });
@@ -295,7 +301,7 @@ describe("ui/conversation", () => {
295
301
  });
296
302
 
297
303
  // Assert
298
- expect(text).toContain("[edit <-]");
304
+ expect(text).toContain("edit <-");
299
305
  expect(text).toContain("~ src/app.ts");
300
306
  expect(text).not.toContain("before");
301
307
  expect(text).not.toContain("after");
@@ -507,7 +513,7 @@ describe("ui/conversation", () => {
507
513
  expect(text).toContain("Thinking... 1 line.");
508
514
  });
509
515
 
510
- test("renderAssistantMessage for a shell tool call shows the command under the new header pill", () => {
516
+ test("renderAssistantMessage for a shell tool call renders an unbracketed header inside the pill", () => {
511
517
  // Arrange
512
518
  const assistant = {
513
519
  content: [
@@ -516,12 +522,205 @@ describe("ui/conversation", () => {
516
522
  };
517
523
 
518
524
  // Act
519
- const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
525
+ const node = renderAssistantMessage(assistant, RENDER_OPTS);
526
+ const text = collectText(node);
520
527
 
521
528
  // Assert
522
- expect(text).toContain("[shell ->]");
529
+ expect(text).toContain("shell ->");
530
+ expect(text).not.toContain("[shell ->]");
523
531
  expect(text).toContain("echo hi");
524
532
  expect(text).not.toContain('"command": "echo hi"');
533
+
534
+ expect(node?.type).toBe("vstack");
535
+ if (!node || node.type !== "vstack") {
536
+ throw new Error("Expected the assistant node to be a vstack");
537
+ }
538
+
539
+ const toolBlock = node.children[0];
540
+ expect(toolBlock?.type).toBe("hstack");
541
+ if (!toolBlock || toolBlock.type !== "hstack") {
542
+ throw new Error("Expected the tool block to be an hstack");
543
+ }
544
+
545
+ const contentColumn = toolBlock.children[1];
546
+ expect(contentColumn?.type).toBe("vstack");
547
+ if (!contentColumn || contentColumn.type !== "vstack") {
548
+ throw new Error("Expected the tool content column to be a vstack");
549
+ }
550
+
551
+ const headerRow = contentColumn.children[0];
552
+ expect(headerRow?.type).toBe("hstack");
553
+ if (!headerRow || headerRow.type !== "hstack") {
554
+ throw new Error("Expected the tool header row to be an hstack");
555
+ }
556
+
557
+ const headerPill = headerRow.children[0];
558
+ expect(headerPill?.type).toBe("hstack");
559
+ if (!headerPill || headerPill.type !== "hstack") {
560
+ throw new Error("Expected the tool header pill to be an hstack");
561
+ }
562
+
563
+ expect(headerPill.props.bgColor).toBe(DEFAULT_THEME.toolBorder);
564
+ expect(headerPill.props.padding).toEqual({ x: 1 });
565
+ expect(collectText(headerPill)).toEqual(["shell ->"]);
566
+ });
567
+
568
+ test("renderAssistantMessage for a shell tool call syntax-highlights bash tokens", async () => {
569
+ // Arrange
570
+ const assistant = {
571
+ content: [
572
+ fauxToolCall(
573
+ "shell",
574
+ { command: 'if true; then echo "$HOME"; fi' },
575
+ { id: "tool-1" },
576
+ ),
577
+ ],
578
+ };
579
+
580
+ // Act
581
+ const rows = await renderBufferRows(
582
+ renderAssistantMessage(assistant, RENDER_OPTS),
583
+ 48,
584
+ 12,
585
+ );
586
+ const commandRow = rows.find((row) =>
587
+ row.text.includes('if true; then echo "$HOME"; fi'),
588
+ );
589
+
590
+ // Assert
591
+ expect(commandRow).toBeDefined();
592
+ expect(commandRow?.fgColors[commandRow.text.indexOf("if")]).toBe(
593
+ DEFAULT_THEME.secondaryAccentText ?? null,
594
+ );
595
+ expect(commandRow?.fgColors[commandRow.text.indexOf("echo")]).toBe(
596
+ DEFAULT_THEME.accentText ?? null,
597
+ );
598
+ expect(commandRow?.fgColors[commandRow.text.indexOf('"$HOME"')]).toBe(
599
+ DEFAULT_THEME.diffAdded ?? null,
600
+ );
601
+ });
602
+
603
+ test("renderAssistantMessage for a multiline shell tool call preserves syntax state across lines", async () => {
604
+ // Arrange
605
+ const assistant = {
606
+ content: [
607
+ fauxToolCall(
608
+ "shell",
609
+ { command: "printf 'foo\nbar'" },
610
+ { id: "tool-1" },
611
+ ),
612
+ ],
613
+ };
614
+
615
+ // Act
616
+ const rows = await renderBufferRows(
617
+ renderAssistantMessage(assistant, {
618
+ ...RENDER_OPTS,
619
+ verbose: true,
620
+ }),
621
+ 32,
622
+ 12,
623
+ );
624
+ const firstRow = rows.find((row) => row.text.includes("printf 'foo"));
625
+ const secondRow = rows.find((row) => row.text.includes("bar'"));
626
+
627
+ // Assert
628
+ expect(firstRow).toBeDefined();
629
+ expect(secondRow).toBeDefined();
630
+ expect(firstRow?.fgColors[firstRow.text.indexOf("foo")]).toBe(
631
+ DEFAULT_THEME.diffAdded ?? null,
632
+ );
633
+ expect(secondRow?.fgColors[secondRow.text.indexOf("bar")]).toBe(
634
+ DEFAULT_THEME.diffAdded ?? null,
635
+ );
636
+ });
637
+
638
+ test("renderAssistantMessage for a shell tool call uses theme-derived syntax colors", async () => {
639
+ // Arrange
640
+ const theme = {
641
+ ...DEFAULT_THEME,
642
+ accentText: "color14",
643
+ secondaryAccentText: "color09",
644
+ diffAdded: "color10",
645
+ mutedText: "color13",
646
+ toolText: "color15",
647
+ } satisfies typeof DEFAULT_THEME;
648
+ const assistant = {
649
+ content: [
650
+ fauxToolCall(
651
+ "shell",
652
+ { command: 'if true; then echo "$HOME"; fi' },
653
+ { id: "tool-1" },
654
+ ),
655
+ ],
656
+ };
657
+
658
+ // Act
659
+ const rows = await renderBufferRows(
660
+ renderAssistantMessage(assistant, {
661
+ ...RENDER_OPTS,
662
+ theme,
663
+ }),
664
+ 48,
665
+ 12,
666
+ );
667
+ const commandRow = rows.find((row) =>
668
+ row.text.includes('if true; then echo "$HOME"; fi'),
669
+ );
670
+
671
+ // Assert
672
+ expect(commandRow).toBeDefined();
673
+ expect(commandRow?.fgColors[commandRow.text.indexOf("if")]).toBe(
674
+ theme.secondaryAccentText ?? null,
675
+ );
676
+ expect(commandRow?.fgColors[commandRow.text.indexOf("echo")]).toBe(
677
+ theme.accentText ?? null,
678
+ );
679
+ expect(commandRow?.fgColors[commandRow.text.indexOf('"$HOME"')]).toBe(
680
+ theme.diffAdded ?? null,
681
+ );
682
+ });
683
+
684
+ test("renderAssistantMessage for a long single-token shell argument wraps through the tail in verbose mode", async () => {
685
+ // Arrange
686
+ const command = `printf ${"x".repeat(60)}TAIL`;
687
+ const assistant = {
688
+ content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
689
+ };
690
+
691
+ // Act
692
+ const text = await renderVisibleText(
693
+ renderAssistantMessage(assistant, {
694
+ ...RENDER_OPTS,
695
+ verbose: true,
696
+ previewWidth: 24,
697
+ }),
698
+ 24,
699
+ 20,
700
+ );
701
+
702
+ // Assert
703
+ expect(text.some((line) => line.includes("TAIL"))).toBe(true);
704
+ });
705
+
706
+ test("renderAssistantMessage for a long single-token shell command uses wrapped preview height when verbose is off", () => {
707
+ // Arrange
708
+ const command = `printf ${"x".repeat(220)}TAIL`;
709
+ const assistant = {
710
+ content: [fauxToolCall("shell", { command }, { id: "tool-1" })],
711
+ };
712
+
713
+ // Act
714
+ const height = measureRenderedHeight(
715
+ renderAssistantMessage(assistant, {
716
+ ...RENDER_OPTS,
717
+ previewWidth: 24,
718
+ }),
719
+ 24,
720
+ );
721
+
722
+ // Assert
723
+ expect(height).toBe(9);
525
724
  });
526
725
 
527
726
  test("renderAssistantMessage for a wrapped shell command keeps a fixed preview height when verbose is off", () => {
@@ -564,7 +763,7 @@ describe("ui/conversation", () => {
564
763
  );
565
764
 
566
765
  // Assert
567
- expect(text).toContain("IMPORTANT_PREFIX");
766
+ expect(text.some((line) => line.includes("IMPORTANT_PREFIX"))).toBe(true);
568
767
  });
569
768
 
570
769
  test("renderToolResult for a shell preview allows the outer conversation scroll to handle mouse wheel events", async () => {
@@ -631,7 +830,7 @@ describe("ui/conversation", () => {
631
830
  const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
632
831
 
633
832
  // Assert
634
- expect(text).toContain("[read image ->]");
833
+ expect(text).toContain("read image ->");
635
834
  expect(text).toContain("assets/preview.png");
636
835
  expect(text).not.toContain("{");
637
836
  });
@@ -656,7 +855,7 @@ describe("ui/conversation", () => {
656
855
  const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
657
856
 
658
857
  // Assert
659
- expect(text).toContain("[edit ->]");
858
+ expect(text).toContain("edit ->");
660
859
  expect(text).toContain("src/file.ts");
661
860
  expect(text).toContain("old line");
662
861
  expect(text).toContain("new line");
@@ -719,7 +918,7 @@ describe("ui/conversation", () => {
719
918
  );
720
919
 
721
920
  // Assert
722
- expect(text).toContain("[shell <-]");
921
+ expect(text).toContain("shell <-");
723
922
  expect(text).toContain("line 18");
724
923
  expect(text).toContain("line 25");
725
924
  expect(text).toContain("And 17 lines more");
@@ -763,7 +962,7 @@ describe("ui/conversation", () => {
763
962
  );
764
963
 
765
964
  // Assert
766
- expect(text).toContain("[shell <-]");
965
+ expect(text).toContain("shell <-");
767
966
  expect(text).toContain("exit 42");
768
967
  expect(text).toContain("boom");
769
968
  expect(text).not.toContain("Exit code: 42");
@@ -780,7 +979,7 @@ describe("ui/conversation", () => {
780
979
  );
781
980
 
782
981
  // Assert
783
- expect(text).toContain("[read image <-]");
982
+ expect(text).toContain("read image <-");
784
983
  expect(text).toContain("diagram.png");
785
984
  expect(text).not.toContain("Read image.");
786
985
  });
@@ -829,7 +1028,7 @@ describe("ui/conversation", () => {
829
1028
  );
830
1029
 
831
1030
  // Assert
832
- expect(previewText).toContain("[edit <-]");
1031
+ expect(previewText).toContain("edit <-");
833
1032
  expect(previewText).toContain("~ src/file.ts");
834
1033
  expect(previewText).not.toContain("before");
835
1034
  expect(previewText).not.toContain("after");
@@ -862,7 +1061,7 @@ describe("ui/conversation", () => {
862
1061
  );
863
1062
 
864
1063
  // Assert
865
- expect(text).toContain("[edit <-]");
1064
+ expect(text).toContain("edit <-");
866
1065
  expect(text).toContain("error 18");
867
1066
  expect(text).toContain("error 25");
868
1067
  expect(text).toContain("And 17 lines more");
@@ -885,7 +1084,7 @@ describe("ui/conversation", () => {
885
1084
  );
886
1085
 
887
1086
  // Assert
888
- expect(text).toContain("[mcp/search <-]");
1087
+ expect(text).toContain("mcp/search <-");
889
1088
  expect(text).toContain("session persistence sqlite turn numbering");
890
1089
  expect(text).not.toContain('"query"');
891
1090
  });