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/bun.lock +39 -40
- package/package.json +10 -11
- package/src/agent.ts +38 -15
- package/src/errors.ts +15 -0
- package/src/git.ts +95 -19
- package/src/index.ts +125 -6
- package/src/prompt.ts +7 -4
- package/src/settings.ts +66 -4
- package/src/submit.ts +4 -6
- package/src/tools.ts +671 -6
- package/src/ui/agent.ts +1 -4
- package/src/ui/commands.test.ts +3 -0
- package/src/ui/commands.ts +1 -4
- package/src/ui/conversation.test.ts +214 -15
- package/src/ui/conversation.ts +269 -37
- package/src/ui/input.test.ts +5 -1
- package/src/ui/input.ts +7 -1
- package/src/ui/status.test.ts +3 -0
- package/src/ui.ts +6 -0
- package/src/version.ts +48 -0
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
|
*
|
package/src/ui/commands.test.ts
CHANGED
package/src/ui/commands.ts
CHANGED
|
@@ -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 === "
|
|
253
|
+
expect(text.filter((line) => line === "shell ->")).toHaveLength(1);
|
|
248
254
|
expect(text).toContain("echo hi");
|
|
249
|
-
expect(text).toContain("
|
|
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("
|
|
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
|
|
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
|
|
525
|
+
const node = renderAssistantMessage(assistant, RENDER_OPTS);
|
|
526
|
+
const text = collectText(node);
|
|
520
527
|
|
|
521
528
|
// Assert
|
|
522
|
-
expect(text).toContain("
|
|
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).
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
});
|