@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.
Files changed (234) hide show
  1. package/CHANGELOG.md +10 -19
  2. package/README.md +5 -5
  3. package/dist/agent-history-list.d.ts +19 -0
  4. package/dist/agent-history-list.d.ts.map +1 -0
  5. package/dist/agent-history-list.js +69 -0
  6. package/dist/agent-history-list.js.map +1 -0
  7. package/dist/agent-history.d.ts +2 -1
  8. package/dist/agent-history.d.ts.map +1 -0
  9. package/dist/agent-history.js +32 -34
  10. package/dist/agent-history.js.map +1 -0
  11. package/dist/agent-manager.d.ts +24 -8
  12. package/dist/agent-manager.d.ts.map +1 -0
  13. package/dist/agent-manager.js +171 -93
  14. package/dist/agent-manager.js.map +1 -0
  15. package/dist/agent-runner.d.ts +1 -0
  16. package/dist/agent-runner.d.ts.map +1 -0
  17. package/dist/agent-runner.js +75 -86
  18. package/dist/agent-runner.js.map +1 -0
  19. package/dist/agent-types.d.ts +1 -0
  20. package/dist/agent-types.d.ts.map +1 -0
  21. package/dist/agent-types.js +28 -45
  22. package/dist/agent-types.js.map +1 -0
  23. package/dist/context.d.ts +1 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/context.js +3 -6
  26. package/dist/context.js.map +1 -0
  27. package/dist/cross-extension-rpc.d.ts +1 -0
  28. package/dist/cross-extension-rpc.d.ts.map +1 -0
  29. package/dist/cross-extension-rpc.js +6 -9
  30. package/dist/cross-extension-rpc.js.map +1 -0
  31. package/dist/custom-agents.d.ts +1 -0
  32. package/dist/custom-agents.d.ts.map +1 -0
  33. package/dist/custom-agents.js +16 -18
  34. package/dist/custom-agents.js.map +1 -0
  35. package/dist/default-agents.d.ts +1 -0
  36. package/dist/default-agents.d.ts.map +1 -0
  37. package/dist/default-agents.js +2 -4
  38. package/dist/default-agents.js.map +1 -0
  39. package/dist/enabled-models.d.ts +1 -0
  40. package/dist/enabled-models.d.ts.map +1 -0
  41. package/dist/enabled-models.js +12 -16
  42. package/dist/enabled-models.js.map +1 -0
  43. package/dist/env.d.ts +1 -0
  44. package/dist/env.d.ts.map +1 -0
  45. package/dist/env.js +2 -4
  46. package/dist/env.js.map +1 -0
  47. package/dist/group-join.d.ts +1 -0
  48. package/dist/group-join.d.ts.map +1 -0
  49. package/dist/group-join.js +2 -5
  50. package/dist/group-join.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +306 -292
  54. package/dist/index.js.map +1 -0
  55. package/dist/invocation-config.d.ts +1 -0
  56. package/dist/invocation-config.d.ts.map +1 -0
  57. package/dist/invocation-config.js +3 -6
  58. package/dist/invocation-config.js.map +1 -0
  59. package/dist/memory.d.ts +1 -0
  60. package/dist/memory.d.ts.map +1 -0
  61. package/dist/memory.js +25 -34
  62. package/dist/memory.js.map +1 -0
  63. package/dist/model-resolver.d.ts +1 -0
  64. package/dist/model-resolver.d.ts.map +1 -0
  65. package/dist/model-resolver.js +2 -4
  66. package/dist/model-resolver.js.map +1 -0
  67. package/dist/nico-overrides.d.ts +1 -0
  68. package/dist/nico-overrides.d.ts.map +1 -0
  69. package/dist/nico-overrides.js +15 -20
  70. package/dist/nico-overrides.js.map +1 -0
  71. package/dist/output-file.d.ts +1 -0
  72. package/dist/output-file.d.ts.map +1 -0
  73. package/dist/output-file.js +17 -22
  74. package/dist/output-file.js.map +1 -0
  75. package/dist/prompts.d.ts +1 -0
  76. package/dist/prompts.d.ts.map +1 -0
  77. package/dist/prompts.js +2 -4
  78. package/dist/prompts.js.map +1 -0
  79. package/dist/schedule-store.d.ts +1 -0
  80. package/dist/schedule-store.d.ts.map +1 -0
  81. package/dist/schedule-store.js +17 -21
  82. package/dist/schedule-store.js.map +1 -0
  83. package/dist/schedule.d.ts +1 -0
  84. package/dist/schedule.d.ts.map +1 -0
  85. package/dist/schedule.js +9 -12
  86. package/dist/schedule.js.map +1 -0
  87. package/dist/settings.d.ts +1 -0
  88. package/dist/settings.d.ts.map +1 -0
  89. package/dist/settings.js +16 -23
  90. package/dist/settings.js.map +1 -0
  91. package/dist/skill-loader.d.ts +1 -0
  92. package/dist/skill-loader.d.ts.map +1 -0
  93. package/dist/skill-loader.js +21 -23
  94. package/dist/skill-loader.js.map +1 -0
  95. package/dist/status-note.d.ts +1 -0
  96. package/dist/status-note.d.ts.map +1 -0
  97. package/dist/status-note.js +2 -4
  98. package/dist/status-note.js.map +1 -0
  99. package/dist/types.d.ts +5 -1
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -4
  102. package/dist/types.js.map +1 -0
  103. package/dist/ui/agent-widget.d.ts +2 -5
  104. package/dist/ui/agent-widget.d.ts.map +1 -0
  105. package/dist/ui/agent-widget.js +29 -51
  106. package/dist/ui/agent-widget.js.map +1 -0
  107. package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
  108. package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
  109. package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
  110. package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
  111. package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
  112. package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
  113. package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
  114. package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
  115. package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
  116. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
  117. package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
  118. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
  119. package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
  120. package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
  121. package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
  122. package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
  123. package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
  124. package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
  125. package/dist/ui/ccstyle/diff/render-utils.js +25 -0
  126. package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
  127. package/dist/ui/ccstyle/diff/types.d.ts +22 -0
  128. package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
  129. package/dist/ui/ccstyle/diff/types.js +10 -0
  130. package/dist/ui/ccstyle/diff/types.js.map +1 -0
  131. package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
  132. package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
  133. package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
  134. package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
  135. package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
  136. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
  137. package/dist/ui/ccstyle/tool-renderer.js +58 -0
  138. package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
  139. package/dist/ui/ccstyle/tool-result.d.ts +24 -0
  140. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
  141. package/dist/ui/ccstyle/tool-result.js +193 -0
  142. package/dist/ui/ccstyle/tool-result.js.map +1 -0
  143. package/dist/ui/conversation-blocks.d.ts +46 -0
  144. package/dist/ui/conversation-blocks.d.ts.map +1 -0
  145. package/dist/ui/conversation-blocks.js +306 -0
  146. package/dist/ui/conversation-blocks.js.map +1 -0
  147. package/dist/ui/conversation-nvim.d.ts +8 -0
  148. package/dist/ui/conversation-nvim.d.ts.map +1 -0
  149. package/dist/ui/conversation-nvim.js +117 -0
  150. package/dist/ui/conversation-nvim.js.map +1 -0
  151. package/dist/ui/conversation-role.d.ts +13 -0
  152. package/dist/ui/conversation-role.d.ts.map +1 -0
  153. package/dist/ui/conversation-role.js +39 -0
  154. package/dist/ui/conversation-role.js.map +1 -0
  155. package/dist/ui/conversation-search.d.ts +39 -0
  156. package/dist/ui/conversation-search.d.ts.map +1 -0
  157. package/dist/ui/conversation-search.js +124 -0
  158. package/dist/ui/conversation-search.js.map +1 -0
  159. package/dist/ui/conversation-timeline.d.ts +62 -0
  160. package/dist/ui/conversation-timeline.d.ts.map +1 -0
  161. package/dist/ui/conversation-timeline.js +263 -0
  162. package/dist/ui/conversation-timeline.js.map +1 -0
  163. package/dist/ui/conversation-viewer.d.ts +62 -20
  164. package/dist/ui/conversation-viewer.d.ts.map +1 -0
  165. package/dist/ui/conversation-viewer.js +449 -224
  166. package/dist/ui/conversation-viewer.js.map +1 -0
  167. package/dist/ui/fleet-list.d.ts +24 -11
  168. package/dist/ui/fleet-list.d.ts.map +1 -0
  169. package/dist/ui/fleet-list.js +86 -78
  170. package/dist/ui/fleet-list.js.map +1 -0
  171. package/dist/ui/schedule-menu.d.ts +1 -0
  172. package/dist/ui/schedule-menu.d.ts.map +1 -0
  173. package/dist/ui/schedule-menu.js +2 -4
  174. package/dist/ui/schedule-menu.js.map +1 -0
  175. package/dist/ui/viewer-keys.d.ts +1 -0
  176. package/dist/ui/viewer-keys.d.ts.map +1 -0
  177. package/dist/ui/viewer-keys.js +8 -10
  178. package/dist/ui/viewer-keys.js.map +1 -0
  179. package/dist/usage.d.ts +1 -0
  180. package/dist/usage.d.ts.map +1 -0
  181. package/dist/usage.js +5 -10
  182. package/dist/usage.js.map +1 -0
  183. package/dist/worktree.d.ts +1 -0
  184. package/dist/worktree.d.ts.map +1 -0
  185. package/dist/worktree.js +27 -31
  186. package/dist/worktree.js.map +1 -0
  187. package/dist/write-execution.d.ts +42 -0
  188. package/dist/write-execution.d.ts.map +1 -0
  189. package/dist/write-execution.js +138 -0
  190. package/dist/write-execution.js.map +1 -0
  191. package/package.json +1 -1
  192. package/src/agent-history-list.ts +91 -0
  193. package/src/agent-history.ts +12 -17
  194. package/src/agent-manager.ts +169 -81
  195. package/src/agent-runner.ts +12 -13
  196. package/src/index.ts +117 -98
  197. package/src/output-file.ts +1 -1
  198. package/src/types.ts +4 -1
  199. package/src/ui/agent-widget.ts +3 -9
  200. package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
  201. package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
  202. package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
  203. package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
  204. package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
  205. package/src/ui/ccstyle/diff/render-utils.ts +32 -0
  206. package/src/ui/ccstyle/diff/types.ts +33 -0
  207. package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
  208. package/src/ui/ccstyle/tool-renderer.ts +71 -0
  209. package/src/ui/ccstyle/tool-result.ts +238 -0
  210. package/src/ui/conversation-blocks.ts +404 -0
  211. package/src/ui/conversation-nvim.ts +127 -0
  212. package/src/ui/conversation-role.ts +45 -0
  213. package/src/ui/conversation-search.ts +161 -0
  214. package/src/ui/conversation-timeline.ts +315 -0
  215. package/src/ui/conversation-viewer.ts +418 -210
  216. package/src/ui/fleet-list.ts +73 -52
  217. package/src/write-execution.ts +180 -0
  218. package/test/agent-history-list.test.ts +121 -0
  219. package/test/agent-manager-history.test.ts +59 -58
  220. package/test/agent-menu-history-navigation.test.ts +86 -0
  221. package/test/output-file-history.test.ts +50 -0
  222. package/test/ui/conversation-blocks.test.ts +103 -0
  223. package/test/ui/conversation-diff.test.ts +91 -0
  224. package/test/ui/conversation-search.test.ts +55 -0
  225. package/test/ui/conversation-viewer.test.ts +182 -0
  226. package/test/ui/fleet-list.test.ts +131 -0
  227. package/test/write-execution.test.ts +83 -0
  228. package/tsconfig.json +16 -0
  229. package/vitest.temp.config.mts +6 -0
  230. package/dist/ui/markdown-result.d.ts +0 -3
  231. package/dist/ui/markdown-result.js +0 -53
  232. package/src/ui/markdown-result.ts +0 -56
  233. package/test/agent-history.test.ts +0 -63
  234. 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
- type PersistedRecord = {
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
- function persisted(overrides: Partial<PersistedRecord> = {}): PersistedRecord {
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: "general-purpose",
20
- description: "history test",
19
+ type: "Explore",
20
+ description: "Inspect the repository",
21
21
  status: "completed",
22
- result: "restored result",
23
- startedAt: 100,
24
- completedAt: 200,
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
- let manager: AgentManager | undefined;
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
- afterEach(() => {
33
- manager?.dispose();
34
- manager = undefined;
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("restores terminal records without creating live runtime state", () => {
38
- manager = new AgentManager();
39
-
48
+ it("ignores running, queued, and invalid records", () => {
49
+ const manager = makeManager();
40
50
  manager.restoreCompleted([
41
- persisted(),
42
- persisted({ id: "agent-error", status: "error", error: "failed", transcriptPath: ".pi-subagents/agent-transcripts/agent-error.jsonl" }),
43
- persisted({ id: "agent-running", status: "running" }),
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().map((record) => record.id).sort()).toEqual([
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("uses the newest branch entry when an id appears more than once", () => {
69
- manager = new AgentManager();
70
-
59
+ it("keeps the last valid duplicate input", () => {
60
+ const manager = makeManager();
71
61
  manager.restoreCompleted([
72
- persisted({ result: "old", completedAt: 200 }),
73
- persisted({ result: "new", completedAt: 300 }),
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("new");
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 overwrite a record already held by the manager", () => {
80
- manager = new AgentManager();
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([persisted({ result: "first" })]);
83
- manager.restoreCompleted([persisted({ result: "second" })]);
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("agent-1")?.result).toBe("first");
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
+ });