@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -1,229 +0,0 @@
1
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils";
6
- import { Settings, settings } from "../../../config/settings";
7
- import type { AgentSession } from "../../../session/agent-session";
8
- import * as git from "../../../utils/git";
9
- import * as jj from "../../../utils/jj";
10
- import { getThemeByName, setThemeInstance } from "../../theme/theme";
11
- import { StatusLineComponent } from "./component";
12
-
13
- // Minimal session the git-only status line render path touches: state.messages
14
- // (token-rate scan), model window, streaming flag, and the async-job snapshot.
15
- // The `git` segment reads none of these — they only keep #buildStatusLine from
16
- // throwing while it renders the single segment under test.
17
- function makeSession(): AgentSession {
18
- const model = { contextWindow: 128000 } as const;
19
- return {
20
- state: { messages: [], model },
21
- messages: [],
22
- model,
23
- isStreaming: false,
24
- sessionFile: undefined,
25
- getAsyncJobSnapshot: () => null,
26
- getContextUsage: () => ({ tokens: 0, contextWindow: 128000 }),
27
- } as unknown as AgentSession;
28
- }
29
-
30
- // Drain the microtask queue so the fire-and-forget async jj lookups started by a
31
- // render() have applied their result to the cache. No real timers: only
32
- // already-resolved promises are awaited, so this stays deterministic.
33
- async function flushMicrotasks(): Promise<void> {
34
- for (let i = 0; i < 8; i++) await Promise.resolve();
35
- }
36
-
37
- // Strip SGR/OSC escapes so assertions match on the visible branch label text.
38
- function visible(s: string): string {
39
- return s.replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b\[[0-9;]*m/g, "");
40
- }
41
-
42
- const WIDTH = 200;
43
-
44
- let spies: Array<{ mockRestore(): void }> = [];
45
- let tmpA: string;
46
- let tmpB: string;
47
- let originalProjectDir: string;
48
- const ROOT_A = "/virtual/jj-root-a";
49
- const ROOT_B = "/virtual/jj-root-b";
50
-
51
- beforeAll(async () => {
52
- await Settings.init({ inMemory: true });
53
- const loaded = await getThemeByName("dark");
54
- if (!loaded) throw new Error("theme unavailable");
55
- setThemeInstance(loaded);
56
- // Render only the git segment: shrinks the render surface to the code under
57
- // test and keeps the fake session tiny. Constructor snapshots these, so they
58
- // must be set before any `new StatusLineComponent`.
59
- settings.override("statusLine.preset", "custom");
60
- settings.override("statusLine.leftSegments", ["git"]);
61
- settings.override("statusLine.rightSegments", []);
62
- originalProjectDir = getProjectDir();
63
- tmpA = fs.mkdtempSync(path.join(os.tmpdir(), "jjcache-a-"));
64
- tmpB = fs.mkdtempSync(path.join(os.tmpdir(), "jjcache-b-"));
65
- });
66
-
67
- afterAll(() => {
68
- settings.clearOverride("statusLine.preset");
69
- settings.clearOverride("statusLine.leftSegments");
70
- settings.clearOverride("statusLine.rightSegments");
71
- setProjectDir(originalProjectDir);
72
- fs.rmSync(tmpA, { recursive: true, force: true });
73
- fs.rmSync(tmpB, { recursive: true, force: true });
74
- });
75
-
76
- beforeEach(() => {
77
- // Force the jj fallback branch in #buildSegmentContext: git HEAD resolves to
78
- // null (so gitBranch === null triggers #getJjBranch), and no repo/worktree is
79
- // detected (so effectiveGitCwd stays the raw project dir we control).
80
- spies = [
81
- spyOn(git.head, "resolveSync").mockReturnValue(null),
82
- spyOn(git.repo, "resolveSync").mockReturnValue(null),
83
- spyOn(git.repo, "linkedWorktreeSync").mockReturnValue(null),
84
- // jj status: return a clean summary so #getJjStatus never falls through to
85
- // the real `git status` subprocess. Keeps the git segment's status empty so
86
- // the visible content is exactly the branch label.
87
- spyOn(jj.status, "summary").mockResolvedValue({ staged: 0, unstaged: 0, untracked: 0 }),
88
- // Map each controlled project dir to a stable virtual jj root.
89
- spyOn(jj.repo, "rootSync").mockImplementation(cwd => {
90
- if (cwd === tmpA) return ROOT_A;
91
- if (cwd === tmpB) return ROOT_B;
92
- return null;
93
- }),
94
- ];
95
- setProjectDir(tmpA);
96
- });
97
-
98
- afterEach(() => {
99
- for (const s of spies) s.mockRestore();
100
- spies = [];
101
- });
102
-
103
- describe("StatusLineComponent jj cache coherence", () => {
104
- it("invalidate() drops the throttled jj branch cache within its TTL and refetches", async () => {
105
- // A live jj bookmark label; a second query for the SAME root returns a new
106
- // label, simulating a colocated bookmark/HEAD move mid-TTL.
107
- const branchSpy = spyOn(jj.workingCopy, "label").mockResolvedValue("bookmark-v1");
108
- spies.push(branchSpy);
109
-
110
- const statusLine = new StatusLineComponent(makeSession());
111
-
112
- // First render kicks off the async lookup (returns the empty cache); after
113
- // it resolves, a second render paints the fetched label.
114
- statusLine.getTopBorder(WIDTH);
115
- await flushMicrotasks();
116
- expect(visible(statusLine.getTopBorder(WIDTH).content)).toContain("bookmark-v1");
117
- expect(branchSpy).toHaveBeenCalledTimes(1);
118
-
119
- // Move the bookmark: a plain render within the 5s TTL must keep serving the
120
- // cached label without a refetch (guards that the TTL is real, so the next
121
- // assertion proves invalidate() — not TTL expiry — forces the refresh).
122
- branchSpy.mockResolvedValue("bookmark-v2");
123
- const throttled = visible(statusLine.getTopBorder(WIDTH).content);
124
- await flushMicrotasks();
125
- expect(throttled).toContain("bookmark-v1");
126
- expect(branchSpy).toHaveBeenCalledTimes(1);
127
-
128
- // invalidate() (public watcher trigger) must reset the jj caches so the
129
- // next render refetches despite being inside the TTL, and paint the new
130
- // label — the finding-1 contract.
131
- statusLine.invalidate();
132
- statusLine.getTopBorder(WIDTH);
133
- await flushMicrotasks();
134
- expect(branchSpy).toHaveBeenCalledTimes(2);
135
- expect(visible(statusLine.getTopBorder(WIDTH).content)).toContain("bookmark-v2");
136
- expect(visible(statusLine.getTopBorder(WIDTH).content)).not.toContain("bookmark-v1");
137
- });
138
-
139
- it("a jj lookup that resolves after the root changed never lands in the new root's cache", async () => {
140
- // Root A's query hangs on a deferred so it is still in flight when we
141
- // switch repos; root B resolves with its own label.
142
- const deferredA = Promise.withResolvers<string | null>();
143
- const branchSpy = spyOn(jj.workingCopy, "label").mockImplementation(async root => {
144
- if (root === ROOT_A) return deferredA.promise;
145
- if (root === ROOT_B) return "branch-B";
146
- return null;
147
- });
148
- spies.push(branchSpy);
149
-
150
- const statusLine = new StatusLineComponent(makeSession());
151
-
152
- // Render in repo A: starts the (hanging) lookup for ROOT_A. Nothing painted
153
- // yet — the cache is empty and the query is unresolved.
154
- expect(visible(statusLine.getTopBorder(WIDTH).content)).not.toContain("branch-A-STALE");
155
- expect(branchSpy).toHaveBeenCalledTimes(1);
156
-
157
- // Switch to repo B mid-flight. #jjRootFor(tmpB) re-points #jjRoot to ROOT_B
158
- // and resets the jj caches; ROOT_A's lookup is now stale. The render can't
159
- // start B's lookup yet — the single in-flight flag is still held by A.
160
- setProjectDir(tmpB);
161
- statusLine.getTopBorder(WIDTH);
162
- await flushMicrotasks();
163
- expect(branchSpy).toHaveBeenCalledTimes(1);
164
-
165
- // Let ROOT_A's slow query finish, then drain its continuation. The
166
- // root-keyed guard must DROP it: #jjRoot is ROOT_B, so A's label must never
167
- // become B's cached branch, and A's completion must not advance B's
168
- // throttle (leaving B free to refetch) — the finding-2/4 contract.
169
- deferredA.resolve("branch-A-STALE");
170
- await flushMicrotasks();
171
-
172
- // The very next render paints synchronously from the current cache BEFORE
173
- // any new fetch resolves. This is the only window the transient stale is
174
- // observable: with the fix the cache is empty here (A's result was
175
- // dropped), so this render both refuses to paint A's label AND is free to
176
- // launch B's refetch. Without the fix, A's stale label is already sitting
177
- // in B's cache and paints here.
178
- const paintedImmediate = visible(statusLine.getTopBorder(WIDTH).content);
179
- expect(paintedImmediate).not.toContain("branch-A-STALE");
180
-
181
- // Steady state: B's (now unthrottled) refetch resolves and paints B's real
182
- // label — confirming the switch left B's cache coherent, not poisoned.
183
- await flushMicrotasks();
184
- expect(visible(statusLine.getTopBorder(WIDTH).content)).toContain("branch-B");
185
- });
186
-
187
- it("a jj lookup that resolves after a same-root invalidation never lands as stale", async () => {
188
- // The finding-6 race: a HEAD/bookmark move invalidates the caches while a
189
- // branch query for the SAME root is still in flight. #invalidateGitCaches
190
- // resets #jjRoot, but the next render re-resolves it to the identical root
191
- // string — so a guard keyed only on root equality would accept the
192
- // pre-invalidation result. The generation token must reject it.
193
- const deferred = Promise.withResolvers<string | null>();
194
- let call = 0;
195
- const branchSpy = spyOn(jj.workingCopy, "label").mockImplementation(async () => {
196
- call++;
197
- // First query (pre-invalidation) hangs; later queries return the fresh label.
198
- return call === 1 ? deferred.promise : "bookmark-fresh";
199
- });
200
- spies.push(branchSpy);
201
-
202
- const statusLine = new StatusLineComponent(makeSession());
203
-
204
- // Render in ROOT_A: starts the (hanging) first lookup. Nothing cached yet.
205
- statusLine.getTopBorder(WIDTH);
206
- await flushMicrotasks();
207
- expect(branchSpy).toHaveBeenCalledTimes(1);
208
-
209
- // A HEAD/bookmark move fires the watcher → invalidate(). The cwd is
210
- // unchanged, so the next #jjRootFor re-resolves #jjRoot to the SAME ROOT_A.
211
- statusLine.invalidate();
212
- statusLine.getTopBorder(WIDTH);
213
- await flushMicrotasks();
214
-
215
- // The stale first query now resolves. The generation captured at its launch
216
- // no longer matches (invalidate bumped it), so its label must be dropped —
217
- // never cached, and the throttle must NOT be advanced on it.
218
- deferred.resolve("bookmark-stale");
219
- await flushMicrotasks();
220
- const paintedImmediate = visible(statusLine.getTopBorder(WIDTH).content);
221
- expect(paintedImmediate).not.toContain("bookmark-stale");
222
-
223
- // Because the stale result did not advance the throttle, the post-invalidate
224
- // cache is free to refetch and paint the fresh label.
225
- await flushMicrotasks();
226
- expect(visible(statusLine.getTopBorder(WIDTH).content)).toContain("bookmark-fresh");
227
- expect(branchSpy).toHaveBeenCalledTimes(2);
228
- });
229
- });
@@ -1,84 +0,0 @@
1
- import { beforeAll, describe, expect, it } from "bun:test";
2
- import { Settings } from "../../../config/settings";
3
- import type { AgentSession } from "../../../session/agent-session";
4
- import { getThemeByName, setThemeInstance } from "../../theme/theme";
5
- import { StatusLineComponent } from "./component";
6
-
7
- function makeSessionWithLastMessage(lastMessage: unknown, prewalkArmed: boolean = false) {
8
- return {
9
- messages: lastMessage ? [lastMessage] : [],
10
- model: { contextWindow: 128000 },
11
- contextUsageRevision: 0,
12
- systemPrompt: [],
13
- agent: { state: { tools: [] } },
14
- skills: [],
15
- getContextUsage: () => ({ tokens: 42, contextWindow: 128000 }),
16
- state: {
17
- messages: lastMessage ? [lastMessage] : [],
18
- model: { contextWindow: 128000 },
19
- },
20
- sessionManager: {
21
- getUsageStatistics: () => ({
22
- input: 0,
23
- output: 0,
24
- cacheRead: 0,
25
- cacheWrite: 0,
26
- totalTokens: 0,
27
- orchestrationInput: 0,
28
- orchestrationOutput: 0,
29
- orchestrationCacheRead: 0,
30
- premiumRequests: 0,
31
- cost: 0,
32
- tokensPerSecond: null,
33
- }),
34
- getSessionName: () => "test-session",
35
- },
36
- getPrewalkState: () => (prewalkArmed ? { target: { id: "cheap-model", provider: "openai" } } : undefined),
37
- getAsyncJobSnapshot: () => undefined,
38
- isAdvisorActive: () => false,
39
- getAdvisorStatusOverview: () => ({ configured: false, advisors: [] }),
40
- isFastModeActive: () => false,
41
- configuredThinkingLevel: () => undefined,
42
- modelRegistry: {
43
- isUsingOAuth: () => false,
44
- },
45
- };
46
- }
47
-
48
- beforeAll(async () => {
49
- await Settings.init({ inMemory: true });
50
- const loaded = await getThemeByName("dark");
51
- if (!loaded) throw new Error("theme unavailable");
52
- setThemeInstance(loaded);
53
- });
54
-
55
- describe("StatusLineComponent", () => {
56
- it("fingerprints tool-call arguments containing bigint values", () => {
57
- const statusLine = new StatusLineComponent(
58
- makeSessionWithLastMessage({
59
- role: "assistant",
60
- timestamp: 1,
61
- content: [
62
- {
63
- type: "toolCall",
64
- name: "read",
65
- arguments: { offset: 1n, nested: { limit: 2n } },
66
- },
67
- ],
68
- }) as unknown as AgentSession,
69
- );
70
-
71
- expect(statusLine.getCachedContextBreakdown()).toEqual({ usedTokens: 42, contextWindow: 128000 });
72
- });
73
-
74
- it("renders Prewalk annotation when prewalk is armed", () => {
75
- const statusLine = new StatusLineComponent(makeSessionWithLastMessage(null, true) as unknown as AgentSession);
76
-
77
- // By default preset, 'mode' segment is included in left/right segments.
78
- // Let's get the border and see if Prewalk is rendered.
79
- const border = statusLine.getTopBorder(100);
80
- // SGR codes might be included, so we check if the stripped content contains "Prewalk"
81
- const stripped = border.content.replace(/\x1b\[[0-9;]*m/g, "");
82
- expect(stripped).toContain("Prewalk");
83
- });
84
- });
@@ -1,162 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it } from "bun:test";
2
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
- import { type Component, Text } from "@oh-my-pi/pi-tui";
4
- import { Settings, settings } from "../../config/settings";
5
- import { renderMCPResult } from "../../mcp/render";
6
- import type { MCPToolDetails } from "../../mcp/tool-bridge";
7
- import { getThemeByName, setThemeInstance, theme } from "../theme/theme";
8
- import { ToolExecutionComponent, type ToolExecutionUi } from "./tool-execution";
9
-
10
- class BoldTypeErrorComponent implements Component {
11
- render(_width: number): readonly string[] {
12
- throw new TypeError("th.bold is not a function");
13
- }
14
- }
15
-
16
- function visibleText(lines: readonly string[]): string {
17
- let text = lines.join("\n");
18
- text = text.replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "");
19
- text = text.replace(/\x1b\[[0-9;]*m/g, "");
20
- return text;
21
- }
22
-
23
- describe("ToolExecutionComponent custom renderer failures", () => {
24
- beforeAll(async () => {
25
- await Settings.init({ inMemory: true });
26
- const loaded = await getThemeByName("dark");
27
- if (!loaded) throw new Error("theme unavailable");
28
- setThemeInstance(loaded);
29
- });
30
-
31
- afterEach(() => {
32
- settings.set("mcp.renderMarkdownResults", true);
33
- });
34
-
35
- it("falls back to the custom tool label when a renderCall child component throws during render", () => {
36
- const tool: AgentTool = {
37
- name: "graphify_graph",
38
- label: "Graphify Graph",
39
- description: "renders a graph",
40
- parameters: { type: "object", additionalProperties: true },
41
- renderCall() {
42
- return new BoldTypeErrorComponent();
43
- },
44
- async execute() {
45
- return { content: [{ type: "text", text: "ok" }] };
46
- },
47
- };
48
- const ui: ToolExecutionUi = {
49
- requestRender() {},
50
- requestComponentRender(_component: Component) {},
51
- resetDisplay() {},
52
- };
53
- const component = new ToolExecutionComponent(
54
- "graphify_graph",
55
- {},
56
- { showImages: false },
57
- tool,
58
- ui,
59
- process.cwd(),
60
- );
61
- let text = "";
62
-
63
- expect(() => {
64
- text = visibleText(component.render(80));
65
- }).not.toThrow();
66
- expect(text).toContain("Graphify Graph");
67
- });
68
-
69
- it("preserves raw result text when a renderResult child component throws during render", () => {
70
- const rawResultText = "raw result survives child renderer failure";
71
- const tool: AgentTool = {
72
- name: "crashy_result_renderer",
73
- label: "Crashy Result Renderer",
74
- description: "renders result output",
75
- parameters: { type: "object", additionalProperties: true },
76
- renderCall() {
77
- return new Text(theme.fg("toolTitle", theme.bold("Crashy Result Renderer")), 0, 0);
78
- },
79
- renderResult() {
80
- return new BoldTypeErrorComponent();
81
- },
82
- async execute() {
83
- return { content: [{ type: "text", text: rawResultText }] };
84
- },
85
- };
86
- const ui: ToolExecutionUi = {
87
- requestRender() {},
88
- requestComponentRender(_component: Component) {},
89
- resetDisplay() {},
90
- };
91
- const component = new ToolExecutionComponent(
92
- "crashy_result_renderer",
93
- {},
94
- { showImages: false },
95
- tool,
96
- ui,
97
- process.cwd(),
98
- );
99
- component.updateResult({ content: [{ type: "text", text: rawResultText }] }, false);
100
- let text = "";
101
-
102
- expect(() => {
103
- text = visibleText(component.render(80));
104
- }).not.toThrow();
105
- expect(text).toContain(rawResultText);
106
- });
107
- });
108
-
109
- describe("MCP result Markdown rendering", () => {
110
- const details: MCPToolDetails = {
111
- serverName: "context-mode",
112
- mcpToolName: "ctx_search",
113
- };
114
-
115
- beforeAll(async () => {
116
- await Settings.init({ inMemory: true });
117
- const loaded = await getThemeByName("dark");
118
- if (!loaded) throw new Error("theme unavailable");
119
- setThemeInstance(loaded);
120
- });
121
-
122
- afterEach(() => {
123
- settings.set("mcp.renderMarkdownResults", true);
124
- });
125
-
126
- it("renders inline Markdown by default", () => {
127
- const component = renderMCPResult(
128
- { content: [{ type: "text", text: "**bold result** and `code`" }], details },
129
- { expanded: true, isPartial: false },
130
- theme,
131
- );
132
- const rendered = visibleText(component.render(80));
133
-
134
- expect(rendered).toContain("bold result and code");
135
- expect(rendered).not.toContain("**bold result**");
136
- expect(rendered).not.toContain("`code`");
137
- });
138
-
139
- it("keeps Markdown syntax literal when the setting is disabled", () => {
140
- settings.set("mcp.renderMarkdownResults", false);
141
- const component = renderMCPResult(
142
- { content: [{ type: "text", text: "**bold result**" }], details },
143
- { expanded: true, isPartial: false },
144
- theme,
145
- );
146
-
147
- expect(visibleText(component.render(80))).toContain("**bold result**");
148
- });
149
-
150
- it("preserves structured JSON rendering when Markdown is enabled", () => {
151
- settings.set("mcp.renderMarkdownResults", true);
152
- const component = renderMCPResult(
153
- { content: [{ type: "text", text: '{"status":"**ok**"}' }], details },
154
- { expanded: true, isPartial: false },
155
- theme,
156
- );
157
- const rendered = visibleText(component.render(80));
158
-
159
- expect(rendered).toContain("status");
160
- expect(rendered).toContain("**ok**");
161
- });
162
- });