@esso0428/pi-subagents 0.15.5 → 0.15.7
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/CHANGELOG.md +10 -19
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +19 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +69 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +2 -1
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +32 -34
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +24 -8
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +171 -93
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +1 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +75 -86
- package/dist/agent-runner.js.map +1 -0
- package/dist/agent-types.d.ts +1 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +28 -45
- package/dist/agent-types.js.map +1 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -6
- package/dist/context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +1 -0
- package/dist/cross-extension-rpc.d.ts.map +1 -0
- package/dist/cross-extension-rpc.js +6 -9
- package/dist/cross-extension-rpc.js.map +1 -0
- package/dist/custom-agents.d.ts +1 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +16 -18
- package/dist/custom-agents.js.map +1 -0
- package/dist/default-agents.d.ts +1 -0
- package/dist/default-agents.d.ts.map +1 -0
- package/dist/default-agents.js +2 -4
- package/dist/default-agents.js.map +1 -0
- package/dist/enabled-models.d.ts +1 -0
- package/dist/enabled-models.d.ts.map +1 -0
- package/dist/enabled-models.js +12 -16
- package/dist/enabled-models.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -4
- package/dist/env.js.map +1 -0
- package/dist/group-join.d.ts +1 -0
- package/dist/group-join.d.ts.map +1 -0
- package/dist/group-join.js +2 -5
- package/dist/group-join.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -292
- package/dist/index.js.map +1 -0
- package/dist/invocation-config.d.ts +1 -0
- package/dist/invocation-config.d.ts.map +1 -0
- package/dist/invocation-config.js +3 -6
- package/dist/invocation-config.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -34
- package/dist/memory.js.map +1 -0
- package/dist/model-resolver.d.ts +1 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +2 -4
- package/dist/model-resolver.js.map +1 -0
- package/dist/nico-overrides.d.ts +1 -0
- package/dist/nico-overrides.d.ts.map +1 -0
- package/dist/nico-overrides.js +15 -20
- package/dist/nico-overrides.js.map +1 -0
- package/dist/output-file.d.ts +1 -0
- package/dist/output-file.d.ts.map +1 -0
- package/dist/output-file.js +17 -22
- package/dist/output-file.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +2 -4
- package/dist/prompts.js.map +1 -0
- package/dist/schedule-store.d.ts +1 -0
- package/dist/schedule-store.d.ts.map +1 -0
- package/dist/schedule-store.js +17 -21
- package/dist/schedule-store.js.map +1 -0
- package/dist/schedule.d.ts +1 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +9 -12
- package/dist/schedule.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +16 -23
- package/dist/settings.js.map +1 -0
- package/dist/skill-loader.d.ts +1 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +21 -23
- package/dist/skill-loader.js.map +1 -0
- package/dist/status-note.d.ts +1 -0
- package/dist/status-note.d.ts.map +1 -0
- package/dist/status-note.js +2 -4
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +2 -5
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +29 -51
- package/dist/ui/agent-widget.js.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.js +25 -0
- package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/types.d.ts +22 -0
- package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/types.js +10 -0
- package/dist/ui/ccstyle/diff/types.js.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.js +58 -0
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
- package/dist/ui/ccstyle/tool-result.d.ts +24 -0
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-result.js +193 -0
- package/dist/ui/ccstyle/tool-result.js.map +1 -0
- package/dist/ui/conversation-blocks.d.ts +46 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +306 -0
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +8 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +117 -0
- package/dist/ui/conversation-nvim.js.map +1 -0
- package/dist/ui/conversation-role.d.ts +13 -0
- package/dist/ui/conversation-role.d.ts.map +1 -0
- package/dist/ui/conversation-role.js +39 -0
- package/dist/ui/conversation-role.js.map +1 -0
- package/dist/ui/conversation-search.d.ts +39 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +124 -0
- package/dist/ui/conversation-search.js.map +1 -0
- package/dist/ui/conversation-timeline.d.ts +62 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -0
- package/dist/ui/conversation-timeline.js +263 -0
- package/dist/ui/conversation-timeline.js.map +1 -0
- package/dist/ui/conversation-viewer.d.ts +62 -20
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +449 -224
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +24 -11
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +86 -78
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -0
- package/dist/ui/schedule-menu.js +2 -4
- package/dist/ui/schedule-menu.js.map +1 -0
- package/dist/ui/viewer-keys.d.ts +1 -0
- package/dist/ui/viewer-keys.d.ts.map +1 -0
- package/dist/ui/viewer-keys.js +8 -10
- package/dist/ui/viewer-keys.js.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +5 -10
- package/dist/usage.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +27 -31
- package/dist/worktree.js.map +1 -0
- package/dist/write-execution.d.ts +42 -0
- package/dist/write-execution.d.ts.map +1 -0
- package/dist/write-execution.js +138 -0
- package/dist/write-execution.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +169 -81
- package/src/agent-runner.ts +12 -13
- package/src/index.ts +117 -98
- package/src/output-file.ts +1 -1
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +3 -9
- package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
- package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
- package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
- package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
- package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
- package/src/ui/ccstyle/diff/render-utils.ts +32 -0
- package/src/ui/ccstyle/diff/types.ts +33 -0
- package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
- package/src/ui/ccstyle/tool-renderer.ts +71 -0
- package/src/ui/ccstyle/tool-result.ts +238 -0
- package/src/ui/conversation-blocks.ts +404 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +418 -210
- package/src/ui/fleet-list.ts +73 -52
- package/src/write-execution.ts +180 -0
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +103 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +182 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
|
@@ -1,87 +1,88 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { AgentManager } from "../src/agent-manager.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
id: string;
|
|
6
|
-
type: string;
|
|
7
|
-
description: string;
|
|
8
|
-
status: string;
|
|
9
|
-
result?: unknown;
|
|
10
|
-
error?: unknown;
|
|
11
|
-
startedAt: number;
|
|
12
|
-
completedAt: number;
|
|
13
|
-
transcriptPath?: string;
|
|
14
|
-
};
|
|
4
|
+
const managers: AgentManager[] = [];
|
|
15
5
|
|
|
16
|
-
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
for (const manager of managers.splice(0)) manager.dispose();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function makeManager(): AgentManager {
|
|
11
|
+
const manager = new AgentManager();
|
|
12
|
+
managers.push(manager);
|
|
13
|
+
return manager;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function record(overrides: Record<string, unknown> = {}) {
|
|
17
17
|
return {
|
|
18
18
|
id: "agent-1",
|
|
19
|
-
type: "
|
|
20
|
-
description: "
|
|
19
|
+
type: "Explore",
|
|
20
|
+
description: "Inspect the repository",
|
|
21
21
|
status: "completed",
|
|
22
|
-
result: "
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
result: "done",
|
|
23
|
+
toolUses: 2,
|
|
24
|
+
startedAt: 1_000,
|
|
25
|
+
completedAt: 2_000,
|
|
25
26
|
...overrides,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
describe("AgentManager.restoreCompleted", () => {
|
|
30
|
-
|
|
31
|
+
it("restores all five terminal statuses without sessions", () => {
|
|
32
|
+
const manager = makeManager();
|
|
33
|
+
manager.restoreCompleted(["completed", "steered", "stopped", "aborted", "error"].map((status, i) =>
|
|
34
|
+
record({ id: `agent-${i}`, status }),
|
|
35
|
+
));
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
manager
|
|
34
|
-
|
|
37
|
+
expect(manager.listAgents()).toHaveLength(5);
|
|
38
|
+
for (const restored of manager.listAgents()) {
|
|
39
|
+
expect(["completed", "steered", "stopped", "aborted", "error"]).toContain(restored.status);
|
|
40
|
+
expect(restored.session).toBeUndefined();
|
|
41
|
+
expect(restored.abortController).toBeUndefined();
|
|
42
|
+
expect(restored.promise).toBeUndefined();
|
|
43
|
+
expect(restored.lifetimeUsage).toEqual({ input: 0, output: 0, cacheWrite: 0 });
|
|
44
|
+
expect(restored.compactionCount).toBe(0);
|
|
45
|
+
}
|
|
35
46
|
});
|
|
36
47
|
|
|
37
|
-
it("
|
|
38
|
-
manager =
|
|
39
|
-
|
|
48
|
+
it("ignores running, queued, and invalid records", () => {
|
|
49
|
+
const manager = makeManager();
|
|
40
50
|
manager.restoreCompleted([
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
persisted({ id: "agent-queued", status: "queued" }),
|
|
45
|
-
persisted({ id: "agent-invalid", completedAt: Number.NaN }),
|
|
46
|
-
persisted({ id: "agent-unsafe", transcriptPath: ".pi-subagents/../private.jsonl" }),
|
|
51
|
+
record({ id: "running", status: "running" }),
|
|
52
|
+
record({ id: "queued", status: "queued" }),
|
|
53
|
+
{ id: "invalid" },
|
|
47
54
|
]);
|
|
48
55
|
|
|
49
|
-
expect(manager.listAgents()
|
|
50
|
-
"agent-1",
|
|
51
|
-
"agent-error",
|
|
52
|
-
]);
|
|
53
|
-
const restored = manager.getRecord("agent-1");
|
|
54
|
-
expect(restored).toMatchObject({
|
|
55
|
-
status: "completed",
|
|
56
|
-
result: "restored result",
|
|
57
|
-
toolUses: 0,
|
|
58
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
59
|
-
compactionCount: 0,
|
|
60
|
-
isBackground: true,
|
|
61
|
-
transcriptPath: undefined,
|
|
62
|
-
});
|
|
63
|
-
expect(restored?.session).toBeUndefined();
|
|
64
|
-
expect(restored?.promise).toBeUndefined();
|
|
65
|
-
expect(restored?.abortController).toBeUndefined();
|
|
56
|
+
expect(manager.listAgents()).toEqual([]);
|
|
66
57
|
});
|
|
67
58
|
|
|
68
|
-
it("
|
|
69
|
-
manager =
|
|
70
|
-
|
|
59
|
+
it("keeps the last valid duplicate input", () => {
|
|
60
|
+
const manager = makeManager();
|
|
71
61
|
manager.restoreCompleted([
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
record({ result: "first", startedAt: 1_000, completedAt: 1_100 }),
|
|
63
|
+
record({ result: "last", startedAt: 2_000, completedAt: 2_100 }),
|
|
74
64
|
]);
|
|
75
65
|
|
|
76
|
-
expect(manager.getRecord("agent-1")?.result).toBe("
|
|
66
|
+
expect(manager.getRecord("agent-1")?.result).toBe("last");
|
|
67
|
+
expect(manager.getRecord("agent-1")?.startedAt).toBe(2_000);
|
|
77
68
|
});
|
|
78
69
|
|
|
79
|
-
it("does not
|
|
80
|
-
manager =
|
|
70
|
+
it("does not replace a live record and rejects unsafe persisted data", () => {
|
|
71
|
+
const manager = makeManager();
|
|
72
|
+
// Exercise the public map through a restored terminal, then make it live
|
|
73
|
+
// without starting an LLM session.
|
|
74
|
+
manager.restoreCompleted([record({ id: "live", result: "original" })]);
|
|
75
|
+
const liveRecord = manager.getRecord("live")!;
|
|
76
|
+
liveRecord.status = "running";
|
|
81
77
|
|
|
82
|
-
manager.restoreCompleted([
|
|
83
|
-
|
|
78
|
+
manager.restoreCompleted([
|
|
79
|
+
record({ id: "live", result: "replacement" }),
|
|
80
|
+
record({ id: "bad-time", startedAt: Number.NaN }),
|
|
81
|
+
record({ id: "bad-path", transcriptPath: "../../outside.jsonl" }),
|
|
82
|
+
]);
|
|
84
83
|
|
|
85
|
-
expect(manager.getRecord("
|
|
84
|
+
expect(manager.getRecord("live")?.result).toBe("original");
|
|
85
|
+
expect(manager.getRecord("bad-time")).toBeUndefined();
|
|
86
|
+
expect(manager.getRecord("bad-path")).toBeUndefined();
|
|
86
87
|
});
|
|
87
88
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import extension from "../src/index.js";
|
|
6
|
+
import { agentHistoryLocator } from "../src/agent-history.js";
|
|
7
|
+
|
|
8
|
+
const tempDirectories: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const directory of tempDirectories.splice(0)) rmSync(directory, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("/agents history navigation", () => {
|
|
15
|
+
it("returns to the Agents menu after closing a history viewer", async () => {
|
|
16
|
+
const cwd = mkdtempSync(join(tmpdir(), "pi-subagents-menu-test-"));
|
|
17
|
+
tempDirectories.push(cwd);
|
|
18
|
+
const transcript = ".pi-subagents/agent-transcripts/agent-1.jsonl";
|
|
19
|
+
mkdirSync(join(cwd, ".pi-subagents/agent-transcripts"), { recursive: true });
|
|
20
|
+
writeFileSync(join(cwd, transcript), JSON.stringify({ message: { role: "user", content: "hello" } }) + "\n");
|
|
21
|
+
|
|
22
|
+
const eventHandlers = new Map<string, Set<(...args: any[]) => any>>();
|
|
23
|
+
const events = {
|
|
24
|
+
on: (name: string, handler: (...args: any[]) => any) => {
|
|
25
|
+
const handlersForEvent = eventHandlers.get(name) ?? new Set<(...args: any[]) => any>();
|
|
26
|
+
handlersForEvent.add(handler);
|
|
27
|
+
eventHandlers.set(name, handlersForEvent);
|
|
28
|
+
return () => handlersForEvent.delete(handler);
|
|
29
|
+
},
|
|
30
|
+
emit: (name: string, ...args: any[]) => {
|
|
31
|
+
for (const handler of eventHandlers.get(name) ?? []) handler(...args);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const handlers = new Map<string, (...args: any[]) => any>();
|
|
35
|
+
let command: { handler: (_args: unknown, ctx: any) => Promise<void> } | undefined;
|
|
36
|
+
const pi: any = {
|
|
37
|
+
events,
|
|
38
|
+
registerMessageRenderer: () => {},
|
|
39
|
+
registerTool: () => {},
|
|
40
|
+
registerCommand: (_name: string, definition: typeof command) => { command = definition; },
|
|
41
|
+
on: (name: string, handler: (...args: any[]) => any) => { handlers.set(name, handler); },
|
|
42
|
+
appendEntry: () => {},
|
|
43
|
+
sendMessage: () => {},
|
|
44
|
+
};
|
|
45
|
+
extension(pi);
|
|
46
|
+
|
|
47
|
+
const titles: string[] = [];
|
|
48
|
+
let historySelections = 0;
|
|
49
|
+
const ui = {
|
|
50
|
+
notify: () => {},
|
|
51
|
+
setWidget: () => {},
|
|
52
|
+
setStatus: () => {},
|
|
53
|
+
onTerminalInput: () => () => {},
|
|
54
|
+
select: async (title: string, options: string[]) => {
|
|
55
|
+
titles.push(title);
|
|
56
|
+
if (title === "Agents" && titles.filter(item => item === "Agents").length === 1) {
|
|
57
|
+
return options.find(option => option.startsWith("Agent history ("));
|
|
58
|
+
}
|
|
59
|
+
if (title === "Agent history" && historySelections++ === 0) return options[0];
|
|
60
|
+
return undefined;
|
|
61
|
+
},
|
|
62
|
+
custom: async () => undefined,
|
|
63
|
+
};
|
|
64
|
+
const ctx = {
|
|
65
|
+
cwd,
|
|
66
|
+
ui,
|
|
67
|
+
sessionManager: {
|
|
68
|
+
getSessionId: () => undefined,
|
|
69
|
+
getBranch: () => [{
|
|
70
|
+
type: "custom",
|
|
71
|
+
customType: "subagents:record",
|
|
72
|
+
data: {
|
|
73
|
+
id: "agent-1", type: "Explore", description: "Inspect", status: "completed",
|
|
74
|
+
startedAt: 1, completedAt: 2, transcriptPath: agentHistoryLocator(cwd, join(cwd, transcript)),
|
|
75
|
+
},
|
|
76
|
+
}],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
81
|
+
await command?.handler({}, ctx);
|
|
82
|
+
await handlers.get("session_shutdown")?.();
|
|
83
|
+
|
|
84
|
+
expect(titles).toEqual(["Agents", "Agent history", "Agents"]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { streamToOutputFile, writeInitialEntry } from "../src/output-file.js";
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("streamToOutputFile durable message snapshots", () => {
|
|
14
|
+
it("writes tool result details to both output and project history files", async () => {
|
|
15
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-subagents-output-test-"));
|
|
16
|
+
tempDirs.push(dir);
|
|
17
|
+
const outputPath = join(dir, "agent.output");
|
|
18
|
+
const historyPath = join(dir, "history.jsonl");
|
|
19
|
+
writeInitialEntry(outputPath, "agent-1", "run", dir);
|
|
20
|
+
writeInitialEntry(historyPath, "agent-1", "run", dir);
|
|
21
|
+
|
|
22
|
+
let listener: ((event: any) => void) | undefined;
|
|
23
|
+
const session = {
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: "user", content: "run" },
|
|
26
|
+
{
|
|
27
|
+
role: "toolResult",
|
|
28
|
+
toolCallId: "call-1",
|
|
29
|
+
toolName: "write",
|
|
30
|
+
content: [{ type: "text", text: "Successfully wrote to file.txt" }],
|
|
31
|
+
details: { diff: "+1 new", patch: "+new" },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
subscribe(next: (event: any) => void) {
|
|
35
|
+
listener = next;
|
|
36
|
+
return () => { listener = undefined; };
|
|
37
|
+
},
|
|
38
|
+
} as any;
|
|
39
|
+
|
|
40
|
+
const cleanup = streamToOutputFile(session, outputPath, "agent-1", dir, historyPath);
|
|
41
|
+
listener?.({ type: "turn_end" });
|
|
42
|
+
cleanup();
|
|
43
|
+
|
|
44
|
+
for (const path of [outputPath, historyPath]) {
|
|
45
|
+
const entries = (await readFile(path, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
|
46
|
+
expect(entries).toHaveLength(2);
|
|
47
|
+
expect(entries[1].message.details).toEqual({ diff: "+1 new", patch: "+new" });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatConversationMessages } from "../../src/ui/conversation-blocks.js";
|
|
3
|
+
|
|
4
|
+
describe("formatConversationMessages", () => {
|
|
5
|
+
it("keeps markdown source for user and assistant blocks", () => {
|
|
6
|
+
const blocks = formatConversationMessages([
|
|
7
|
+
{ role: "user", content: "# Ask\n\nUse `x`." },
|
|
8
|
+
{ role: "assistant", content: [
|
|
9
|
+
{ type: "text", text: "## Answer\n\n```ts\nconst x: number = 1;\n```" },
|
|
10
|
+
] },
|
|
11
|
+
] as any);
|
|
12
|
+
|
|
13
|
+
expect(blocks.filter((b) => b.kind === "text").map((b) => b.markdown)).toEqual([
|
|
14
|
+
"# Ask\n\nUse `x`.",
|
|
15
|
+
"## Answer\n\n```ts\nconst x: number = 1;\n```",
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("separates assistant tool calls and bounds tool results", () => {
|
|
20
|
+
const blocks = formatConversationMessages([
|
|
21
|
+
{ role: "assistant", content: [
|
|
22
|
+
{ type: "toolCall", name: "read", arguments: { path: "src/index.ts" } },
|
|
23
|
+
] },
|
|
24
|
+
{ role: "toolResult", toolName: "read", isError: false, content: [
|
|
25
|
+
{ type: "text", text: "a".repeat(100) },
|
|
26
|
+
] },
|
|
27
|
+
] as any, { maxToolResultChars: 40 });
|
|
28
|
+
|
|
29
|
+
expect(blocks.some((b) => b.toolLine?.includes("read"))).toBe(true);
|
|
30
|
+
const result = blocks.find((b) => b.role === "tool" && b.fullText.includes("read"));
|
|
31
|
+
expect(result?.fullText.length).toBeLessThanOrEqual(40);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("pairs tool results by id, then same-name source order, and preserves orphans", () => {
|
|
35
|
+
const blocks = formatConversationMessages([
|
|
36
|
+
{ role: "assistant", content: [
|
|
37
|
+
{ type: "toolCall", id: "call-read", name: "read", arguments: { path: "a.ts" } },
|
|
38
|
+
{ type: "toolCall", id: "call-write", name: "write", arguments: { path: "b.ts", content: "new" } },
|
|
39
|
+
] },
|
|
40
|
+
{ role: "toolResult", toolCallId: "call-read", toolName: "read", isError: false, details: { source: "id" }, content: [{ type: "text", text: "read output" }] },
|
|
41
|
+
{ role: "toolResult", toolName: "write", isError: true, details: { source: "name" }, content: [{ type: "text", text: "write failed" }] },
|
|
42
|
+
{ role: "toolResult", toolCallId: "missing", toolName: "bash", isError: true, content: [{ type: "text", text: "orphan" }] },
|
|
43
|
+
] as any);
|
|
44
|
+
|
|
45
|
+
const tools = blocks.filter((block) => block.kind === "tool");
|
|
46
|
+
expect(tools).toHaveLength(3);
|
|
47
|
+
expect(tools[0]?.toolCallId).toBe("call-read");
|
|
48
|
+
expect(tools[0]?.toolResult?.details).toEqual({ source: "id" });
|
|
49
|
+
expect(tools[1]?.toolCallId).toBe("call-write");
|
|
50
|
+
expect(tools[1]?.toolResult?.isError).toBe(true);
|
|
51
|
+
expect(tools[2]?.toolResult?.content).toEqual([{ type: "text", text: "orphan" }]);
|
|
52
|
+
expect(tools[2]?.fullText).toContain("orphan");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("uses a stable synthetic id when a tool call omits its id", () => {
|
|
56
|
+
const blocks = formatConversationMessages([
|
|
57
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "a.ts" } }] },
|
|
58
|
+
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "ok" }] },
|
|
59
|
+
] as any);
|
|
60
|
+
|
|
61
|
+
expect(blocks[0]?.toolCallId).toBe("message-0-tool-0");
|
|
62
|
+
expect(blocks[0]?.toolResult?.content).toEqual([{ type: "text", text: "ok" }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("normalizes CRLF and bounds text with a deterministic truncation marker", () => {
|
|
66
|
+
const blocks = formatConversationMessages([
|
|
67
|
+
{ role: "user", content: "one\r\ntwo\rthree\nfour" },
|
|
68
|
+
] as any, { maxEntryChars: 12 });
|
|
69
|
+
|
|
70
|
+
expect(blocks[0]?.markdown).toBe("\n… [truncate");
|
|
71
|
+
expect(blocks[0]?.id).toBe("message-0");
|
|
72
|
+
expect(blocks[0]?.fullText.length).toBeLessThanOrEqual(12);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("formats bash, images, summaries, and deterministic tool ids", () => {
|
|
76
|
+
const blocks = formatConversationMessages([
|
|
77
|
+
{ role: "assistant", content: [
|
|
78
|
+
{ type: "text", text: "answer" },
|
|
79
|
+
{ type: "toolCall", name: "read", arguments: { path: "a.ts" } },
|
|
80
|
+
{ type: "toolCall", name: "write", arguments: { path: "b.ts" } },
|
|
81
|
+
] },
|
|
82
|
+
{ role: "bashExecution", command: "echo hi", output: "hi" },
|
|
83
|
+
{ role: "custom", customType: "note", content: [{ type: "image" }] },
|
|
84
|
+
{ role: "compactionSummary", summary: "Earlier context" },
|
|
85
|
+
] as any);
|
|
86
|
+
|
|
87
|
+
expect(blocks.map((block) => block.id)).toEqual([
|
|
88
|
+
"message-0", "message-0-tool-0", "message-0-tool-1", "message-1", "message-2", "message-3",
|
|
89
|
+
]);
|
|
90
|
+
expect(blocks[1]?.toolLine).toContain("· read → a.ts");
|
|
91
|
+
expect(blocks[3]?.fullText).toContain("$ echo hi\nhi");
|
|
92
|
+
expect(blocks[4]?.markdown).toBe("[image]");
|
|
93
|
+
expect(blocks[5]?.role).toBe("meta");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("skips unsupported or empty messages without throwing", () => {
|
|
97
|
+
expect(formatConversationMessages([
|
|
98
|
+
{ role: "unknown", content: { not: "text" } },
|
|
99
|
+
{ role: "user", content: [{ type: "thinking", thinking: "hidden" }] },
|
|
100
|
+
{ role: "custom", display: false, content: "hidden" },
|
|
101
|
+
] as any)).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
renderEditDiffResult,
|
|
5
|
+
renderWriteDiffResult,
|
|
6
|
+
} from "../../src/ui/ccstyle/diff/diff-renderer.js";
|
|
7
|
+
import { renderRichToolResult } from "../../src/ui/ccstyle/tool-result.js";
|
|
8
|
+
import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../../src/ui/ccstyle/diff/types.js";
|
|
9
|
+
|
|
10
|
+
const theme = {
|
|
11
|
+
fg(color: string, text: string) {
|
|
12
|
+
return `<${color}>${text}</${color}>`;
|
|
13
|
+
},
|
|
14
|
+
bg(color: string, text: string) {
|
|
15
|
+
return `<bg:${color}>${text}</bg:${color}>`;
|
|
16
|
+
},
|
|
17
|
+
bold(text: string) {
|
|
18
|
+
return `<bold>${text}</bold>`;
|
|
19
|
+
},
|
|
20
|
+
getBgAnsi() {
|
|
21
|
+
return "";
|
|
22
|
+
},
|
|
23
|
+
getFgAnsi() {
|
|
24
|
+
return "";
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const config = {
|
|
29
|
+
...DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
30
|
+
diffViewMode: "unified" as const,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("viewer-local ccstyle diff renderer", () => {
|
|
34
|
+
it("renders persisted write diff after the underlying file changes", () => {
|
|
35
|
+
const persisted = {
|
|
36
|
+
diff: "-1 old\n+1 new",
|
|
37
|
+
patch: "--- file.txt\n+++ file.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const first = renderWriteDiffResult(undefined, {
|
|
41
|
+
expanded: true,
|
|
42
|
+
filePath: "file.txt",
|
|
43
|
+
persistedDiff: persisted,
|
|
44
|
+
}, config, theme, "");
|
|
45
|
+
const second = renderWriteDiffResult(undefined, {
|
|
46
|
+
expanded: true,
|
|
47
|
+
filePath: "file.txt",
|
|
48
|
+
persistedDiff: persisted,
|
|
49
|
+
}, config, theme, "");
|
|
50
|
+
|
|
51
|
+
expect(second.render(100).join("\n")).toContain("new");
|
|
52
|
+
expect(second.render(100).join("\n")).toBe(first.render(100).join("\n"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("prefers persisted write diff over the current content fallback", () => {
|
|
56
|
+
const component = renderWriteDiffResult("later\n", {
|
|
57
|
+
expanded: true,
|
|
58
|
+
filePath: "file.txt",
|
|
59
|
+
previousContent: "old\n",
|
|
60
|
+
fileExistedBeforeWrite: true,
|
|
61
|
+
persistedDiff: { diff: "-1 old\n+1 new", patch: "--- file.txt\n+++ file.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n" },
|
|
62
|
+
}, config, theme, "");
|
|
63
|
+
|
|
64
|
+
const rendered = component.render(100).join("\n");
|
|
65
|
+
expect(rendered).toContain("new");
|
|
66
|
+
expect(rendered).not.toContain("later");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns an explicit unavailable row instead of an empty success card", () => {
|
|
70
|
+
const component = renderRichToolResult("write", {
|
|
71
|
+
content: [{ type: "text", text: "Successfully wrote" }],
|
|
72
|
+
details: { diffUnavailableReason: "durable diff exceeds limit" },
|
|
73
|
+
}, { expanded: true }, theme, { args: { path: "large.txt" } });
|
|
74
|
+
|
|
75
|
+
expect(component?.render(100).join("\n")).toContain("diff unavailable");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps edit diff and all output rows within the requested width", () => {
|
|
79
|
+
const component = renderEditDiffResult(
|
|
80
|
+
{ diff: "-1 a very long removed line\n+1 a very long added line" },
|
|
81
|
+
{ expanded: true, filePath: "file.ts" },
|
|
82
|
+
config,
|
|
83
|
+
theme,
|
|
84
|
+
"",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
for (const width of [20, 40, 100]) {
|
|
88
|
+
expect(component.render(width).every((line) => visibleWidth(line) <= width)).toBe(true);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
findConversationMatches,
|
|
4
|
+
highlightConversationLine,
|
|
5
|
+
nearestConversationMatchAfter,
|
|
6
|
+
nearestConversationMatchBefore,
|
|
7
|
+
stripAnsi,
|
|
8
|
+
} from "../../src/ui/conversation-search.js";
|
|
9
|
+
|
|
10
|
+
describe("conversation search helpers", () => {
|
|
11
|
+
it("uses smartcase substring matching", () => {
|
|
12
|
+
const lines = [{ plain: "const Answer = 42;" }, { plain: "answer again" }];
|
|
13
|
+
expect(findConversationMatches(lines, "answer")).toEqual([
|
|
14
|
+
{ lineIdx: 0, col: 6, len: 6 },
|
|
15
|
+
{ lineIdx: 1, col: 0, len: 6 },
|
|
16
|
+
]);
|
|
17
|
+
expect(findConversationMatches(lines, "Answer")).toEqual([
|
|
18
|
+
{ lineIdx: 0, col: 6, len: 6 },
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("highlights matches without consuming ANSI escape sequences", () => {
|
|
23
|
+
const styled = "\x1b[31mAnswer\x1b[39m";
|
|
24
|
+
const result = highlightConversationLine(
|
|
25
|
+
styled,
|
|
26
|
+
[{ lineIdx: 0, col: 0, len: 6, current: true }],
|
|
27
|
+
"<m>",
|
|
28
|
+
"<c>",
|
|
29
|
+
"</>",
|
|
30
|
+
);
|
|
31
|
+
expect(result).toContain("<c>");
|
|
32
|
+
expect(result).toContain("Answer");
|
|
33
|
+
expect(result).toContain("\x1b[31m");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("strips CSI and OSC ANSI sequences", () => {
|
|
37
|
+
expect(stripAnsi("\x1b[31mred\x1b[39m")).toBe("red");
|
|
38
|
+
expect(stripAnsi("before\x1b]0;title\x07after")).toBe("beforeafter");
|
|
39
|
+
expect(stripAnsi("before\x1b]0;title\x1b\\after")).toBe("beforeafter");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("wraps nearest-match navigation", () => {
|
|
43
|
+
const matches = [
|
|
44
|
+
{ lineIdx: 2, col: 0, len: 3 },
|
|
45
|
+
{ lineIdx: 5, col: 1, len: 3 },
|
|
46
|
+
{ lineIdx: 9, col: 2, len: 3 },
|
|
47
|
+
];
|
|
48
|
+
expect(nearestConversationMatchAfter(matches, 5)).toBe(1);
|
|
49
|
+
expect(nearestConversationMatchAfter(matches, 10)).toBe(0);
|
|
50
|
+
expect(nearestConversationMatchBefore(matches, 5)).toBe(1);
|
|
51
|
+
expect(nearestConversationMatchBefore(matches, 1)).toBe(2);
|
|
52
|
+
expect(nearestConversationMatchAfter([], 0)).toBe(-1);
|
|
53
|
+
expect(nearestConversationMatchBefore([], 0)).toBe(-1);
|
|
54
|
+
});
|
|
55
|
+
});
|