@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  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 +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -1,105 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "bun:test";
2
- import type { SSHConnectionTarget } from "../connection-manager";
3
- import * as connectionManager from "../connection-manager";
4
- import { listRemoteDir, readRemoteFile, statRemotePath, writeRemoteFile } from "../file-transfer";
5
-
6
- describe("ssh file-transfer POSIX guard", () => {
7
- afterEach(() => {
8
- vi.restoreAllMocks();
9
- });
10
-
11
- it("rejects a confirmed Windows remote before running any POSIX command", async () => {
12
- // Stub BOTH the connection and the host-info probe so the guard is reached
13
- // without opening a real SSH connection and before any command is spawned.
14
- const ensureConnectionSpy = vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
15
- const ensureHostInfoSpy = vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
16
- version: 4,
17
- os: "windows",
18
- shell: "powershell",
19
- compatEnabled: false,
20
- });
21
- const target: SSHConnectionTarget = { name: "winbox", host: "winbox" };
22
- await expect(readRemoteFile(target, "C:/x.txt", { maxBytes: 1024 })).rejects.toThrow(/Windows host/);
23
- await expect(writeRemoteFile(target, "C:/x.txt", new Uint8Array([1]), {})).rejects.toThrow(/Windows host/);
24
- // Prove the guard ran through the stubbed transport rather than failing early
25
- // for an unrelated reason (e.g. a future import refactor bypassing the mocks).
26
- expect(ensureConnectionSpy).toHaveBeenCalled();
27
- expect(ensureHostInfoSpy).toHaveBeenCalled();
28
- });
29
-
30
- it("rejects a non-Windows remote with no verified transferShell", async () => {
31
- // No transferShell means the capability probe never confirmed any of
32
- // sh/bash/zsh works. The guard refuses regardless of `shell` because the
33
- // real ssh:// contract is "did we verify a POSIX shell works", not
34
- // "what name did the login shell self-report" (#3719).
35
- vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
36
- vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
37
- version: 4,
38
- os: "linux",
39
- shell: "unknown",
40
- compatEnabled: false,
41
- });
42
- const target: SSHConnectionTarget = { name: "noshell", host: "noshell" };
43
- await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/no verified POSIX shell/);
44
- await expect(writeRemoteFile(target, "/tmp/x", new Uint8Array([1]), {})).rejects.toThrow(
45
- /no verified POSIX shell/,
46
- );
47
- });
48
-
49
- it("dispatches transfer commands through the verified transferShell, not the login shell", async () => {
50
- // The bug fix: if the login shell is fish/csh/tcsh, the legacy guard
51
- // would refuse the host — but allowing it isn't enough on its own.
52
- // OpenSSH still hands our snippets to `$SHELL -c`, so a fish login
53
- // shell would choke on `if [ … ]; then …`. Every transfer command
54
- // must be wrapped in `<transferShell> -c '…'` to force parsing
55
- // under the shell we verified can run it (#3719).
56
- vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
57
- vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
58
- version: 4,
59
- os: "linux",
60
- // Login shell is fish; only `transferShell` indicates a working POSIX shell.
61
- shell: "unknown",
62
- transferShell: "bash",
63
- compatEnabled: false,
64
- });
65
- const buildSpy = vi
66
- .spyOn(connectionManager, "buildRemoteCommand")
67
- .mockRejectedValue(new Error("stop-before-spawn"));
68
- const target: SSHConnectionTarget = { name: "fishbox", host: "fishbox" };
69
-
70
- await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/stop-before-spawn/);
71
- await expect(writeRemoteFile(target, "/tmp/x", new Uint8Array([1]), {})).rejects.toThrow(/stop-before-spawn/);
72
- await expect(statRemotePath(target, "/etc/hosts")).rejects.toThrow(/stop-before-spawn/);
73
- await expect(listRemoteDir(target, "/etc")).rejects.toThrow(/stop-before-spawn/);
74
-
75
- // Each dispatch must start with `bash -c '…'` and embed the original
76
- // POSIX snippet inside the quoted command. Read also drops `-n`
77
- // (allowStdin: true) because cat-staging needs stdin streaming.
78
- const dispatches = buildSpy.mock.calls.map(call => call[1] as string);
79
- expect(dispatches[0]).toMatch(/^bash -c '.*head -c 1025/);
80
- expect(dispatches[1]).toMatch(/^bash -c '.*cat > /);
81
- expect(buildSpy.mock.calls[1]?.[2]).toMatchObject({ allowStdin: true });
82
- expect(dispatches[2]).toMatch(/^bash -c '.*if \[ -d /);
83
- expect(dispatches[3]).toMatch(/^bash -c '.*LC_ALL=C ls -1Ap /);
84
- });
85
-
86
- it("uses sh -c when transferShell is sh (the most universal POSIX fallback)", async () => {
87
- // Belt-and-suspenders: the common happy path with a sh-family login
88
- // shell still routes through `sh -c` to keep one dispatch shape.
89
- vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
90
- vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
91
- version: 4,
92
- os: "linux",
93
- shell: "sh",
94
- transferShell: "sh",
95
- compatEnabled: false,
96
- });
97
- const buildSpy = vi
98
- .spyOn(connectionManager, "buildRemoteCommand")
99
- .mockRejectedValue(new Error("stop-before-spawn"));
100
- const target: SSHConnectionTarget = { name: "shbox", host: "shbox" };
101
-
102
- await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/stop-before-spawn/);
103
- expect(buildSpy.mock.calls[0]?.[1]).toMatch(/^sh -c '.*head -c 1025/);
104
- });
105
- });
@@ -1,13 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import * as path from "node:path";
3
- import { isMounted } from "../sshfs-mount";
4
-
5
- describe("isMounted", () => {
6
- it("detects a macOS mount point when mountpoint is unavailable", async () => {
7
- const parentPath = import.meta.dir;
8
- const mountPath = path.join(parentPath, "mounted");
9
- const stat = async (filePath: string) => ({ dev: filePath === mountPath ? 2 : 1 });
10
-
11
- await expect(isMounted(mountPath, { platform: "darwin", stat, which: () => null })).resolves.toBe(true);
12
- });
13
- });
@@ -1,236 +0,0 @@
1
- import { describe, expect, it, spyOn } from "bun:test";
2
- import * as fs from "node:fs/promises";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { buildSystemPrompt } from "./system-prompt";
6
-
7
- interface ProbeRunResult {
8
- elapsedMs: number;
9
- childElapsedMs: number;
10
- cached: unknown;
11
- count: number;
12
- }
13
-
14
- async function runProbeScenario(options: {
15
- runs: number;
16
- sleepSeconds?: number;
17
- holdStdoutOpen?: boolean;
18
- descendantHoldsStdout?: boolean;
19
- validOutput?: string;
20
- }): Promise<ProbeRunResult> {
21
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gpu-probe-"));
22
- try {
23
- const binDir = path.join(tempRoot, "bin");
24
- const cacheRoot = path.join(tempRoot, "cache");
25
- const probeCountPath = path.join(tempRoot, "probe-count");
26
- await fs.mkdir(binDir, { recursive: true });
27
- await fs.mkdir(path.join(cacheRoot, "omp"), { recursive: true });
28
- const lspciPath = path.join(binDir, "lspci");
29
- await Bun.write(
30
- lspciPath,
31
- '#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ -n "$OMP_GPU_PROBE_VALID_OUTPUT" ]; then printf "%s\\n" "$OMP_GPU_PROBE_VALID_OUTPUT"; fi\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n',
32
- );
33
- await fs.chmod(lspciPath, 0o755);
34
-
35
- const scenarioPath = path.join(tempRoot, "scenario.ts");
36
- await Bun.write(
37
- scenarioPath,
38
- `import { getGpuCachePath, refreshDirsFromEnv } from ${JSON.stringify(path.resolve(import.meta.dir, "../../utils/src/index.ts"))};
39
- import { buildSystemPrompt } from ${JSON.stringify(path.join(import.meta.dir, "system-prompt.ts"))};
40
-
41
- refreshDirsFromEnv();
42
- const buildOptions = {
43
- contextFiles: [],
44
- skills: [],
45
- toolNames: [],
46
- workspaceTree: {
47
- rootPath: process.cwd(),
48
- rendered: "",
49
- truncated: false,
50
- totalLines: 0,
51
- agentsMdFiles: [],
52
- },
53
- activeRepoContext: null,
54
- };
55
- const startedAt = performance.now();
56
- for (let index = 0; index < Number(process.env.OMP_GPU_PROBE_RUNS ?? "1"); index += 1) {
57
- await buildSystemPrompt(buildOptions);
58
- }
59
- const cacheFile = Bun.file(getGpuCachePath());
60
- const cached = await cacheFile.exists() ? await cacheFile.json() : null;
61
- const countFile = Bun.file(process.env.OMP_GPU_PROBE_COUNT ?? "");
62
- const count = await countFile.exists() ? (await countFile.text()).length : 0;
63
- console.log(JSON.stringify({ elapsedMs: Math.round(performance.now() - startedAt), cached, count }));
64
- `,
65
- );
66
-
67
- const env: Record<string, string | undefined> = {
68
- ...process.env,
69
- PATH: `${binDir}:${process.env.PATH ?? ""}`,
70
- XDG_CACHE_HOME: cacheRoot,
71
- OMP_GPU_PROBE_COUNT: probeCountPath,
72
- OMP_GPU_PROBE_RUNS: String(options.runs),
73
- };
74
- // Strip inherited dirs-resolver overrides so XDG_CACHE_HOME above wins and
75
- // the test cannot touch the developer/CI profile's real gpu_cache.json.
76
- for (const key of ["PI_CODING_AGENT_DIR", "OMP_PROFILE", "PI_PROFILE", "PI_CONFIG_DIR"]) {
77
- delete env[key];
78
- }
79
- if (options.sleepSeconds === undefined) {
80
- delete env.OMP_GPU_PROBE_SLEEP;
81
- } else {
82
- env.OMP_GPU_PROBE_SLEEP = String(options.sleepSeconds);
83
- }
84
- if (options.holdStdoutOpen) {
85
- env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN = "true";
86
- } else {
87
- delete env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN;
88
- }
89
- if (options.descendantHoldsStdout) {
90
- env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT = "true";
91
- } else {
92
- delete env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT;
93
- }
94
- if (options.validOutput !== undefined) {
95
- env.OMP_GPU_PROBE_VALID_OUTPUT = options.validOutput;
96
- } else {
97
- delete env.OMP_GPU_PROBE_VALID_OUTPUT;
98
- }
99
-
100
- const childStartedAt = performance.now();
101
- const child = Bun.spawn([process.execPath, scenarioPath], { stdout: "pipe", stderr: "pipe", env });
102
- const [stdout, stderr, exitCode] = await Promise.all([
103
- new Response(child.stdout).text(),
104
- new Response(child.stderr).text(),
105
- child.exited,
106
- ]);
107
- const childElapsedMs = Math.round(performance.now() - childStartedAt);
108
- if (exitCode !== 0) {
109
- throw new Error(`GPU probe scenario failed with exit ${exitCode}: ${stderr}`);
110
- }
111
- return { ...JSON.parse(stdout.trim()), childElapsedMs };
112
- } finally {
113
- await fs.rm(tempRoot, { recursive: true, force: true });
114
- }
115
- }
116
-
117
- describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => {
118
- it("caches empty GPU probe results", async () => {
119
- const result = await runProbeScenario({ runs: 2 });
120
-
121
- expect(result.cached).toEqual({ gpu: null });
122
- expect(result.count).toBe(1);
123
- }, 15_000);
124
-
125
- it("kills the GPU probe at the prep deadline", async () => {
126
- const result = await runProbeScenario({ runs: 1, sleepSeconds: 12, holdStdoutOpen: true });
127
-
128
- expect(result.cached).toEqual({ gpu: null });
129
- // Probe is SIGKILLed at ~4.5s and the drain wait is bounded, so in-child
130
- // time sits near the deadline; waiting on the descendant would push it
131
- // past the 12s sleep.
132
- expect(result.elapsedMs).toBeLessThan(6500);
133
- // Codex#3838: the child process MUST exit shortly after the deadline, not
134
- // linger until a descendant holding stdout (sleep 12) exits on its own.
135
- // The bound over in-child time budgets bun spawn/startup on loaded runners
136
- // while staying far below the descendant's 12s exit.
137
- expect(result.childElapsedMs).toBeLessThan(9000);
138
- }, 20_000);
139
-
140
- it("does not wait on stdout held by a descendant after a successful probe", async () => {
141
- const result = await runProbeScenario({ runs: 1, sleepSeconds: 8, descendantHoldsStdout: true });
142
-
143
- expect(result.cached).toEqual({ gpu: null });
144
- // Probe exits 0 immediately but leaves a backgrounded sleep holding the stdout
145
- // pipe. The success path MUST bound the drain wait, not block until sleep exits.
146
- expect(result.elapsedMs).toBeLessThan(2000);
147
- // Budgets bun spawn/startup overhead; blocking on the descendant would
148
- // take at least the 8s sleep.
149
- expect(result.childElapsedMs).toBeLessThan(5000);
150
- }, 20_000);
151
-
152
- it("keeps probe output captured before a descendant delays EOF", async () => {
153
- const result = await runProbeScenario({
154
- runs: 1,
155
- sleepSeconds: 8,
156
- descendantHoldsStdout: true,
157
- validOutput: "00:02.0 VGA compatible controller: NVIDIA TestGPU",
158
- });
159
-
160
- // Probe exited 0 with valid output before bg sleep held stdout open.
161
- // Captured stdout MUST be cached, not discarded as if the probe failed.
162
- expect(result.cached).toEqual({ gpu: "02.0 VGA compatible controller: NVIDIA TestGPU" });
163
- expect(result.elapsedMs).toBeLessThan(2000);
164
- // Budgets bun spawn/startup overhead; blocking on the descendant would
165
- // take at least the 8s sleep.
166
- expect(result.childElapsedMs).toBeLessThan(5000);
167
- }, 20_000);
168
- });
169
-
170
- describe.skipIf(process.platform !== "linux")("system prompt CPU model", () => {
171
- it("does not call os.cpus while building the workstation block", async () => {
172
- const cpus = spyOn(os, "cpus").mockImplementation(() => [
173
- {
174
- model: "Synthetic Slow CPU",
175
- speed: 0,
176
- times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
177
- },
178
- ]);
179
- try {
180
- await buildSystemPrompt({
181
- resolvedCustomPrompt: "Base prompt",
182
- contextFiles: [],
183
- skills: [],
184
- rules: [],
185
- workspaceTree: {
186
- rootPath: import.meta.dir,
187
- rendered: "",
188
- truncated: false,
189
- totalLines: 0,
190
- agentsMdFiles: [],
191
- },
192
- activeRepoContext: null,
193
- });
194
-
195
- expect(cpus).not.toHaveBeenCalled();
196
- } finally {
197
- cpus.mockRestore();
198
- }
199
- });
200
- });
201
-
202
- describe("non-Linux system prompt CPU model", () => {
203
- it("includes the model returned by os.cpus", async () => {
204
- const originalPlatform = process.platform;
205
- Object.defineProperty(process, "platform", { value: "darwin" });
206
- const cpus = spyOn(os, "cpus").mockImplementation(() => [
207
- {
208
- model: "Synthetic Non-Linux CPU",
209
- speed: 0,
210
- times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
211
- },
212
- ]);
213
- try {
214
- const systemPrompt = await buildSystemPrompt({
215
- resolvedCustomPrompt: "Base prompt",
216
- contextFiles: [],
217
- skills: [],
218
- rules: [],
219
- workspaceTree: {
220
- rootPath: import.meta.dir,
221
- rendered: "",
222
- truncated: false,
223
- totalLines: 0,
224
- agentsMdFiles: [],
225
- },
226
- activeRepoContext: null,
227
- });
228
-
229
- expect(cpus).toHaveBeenCalledTimes(1);
230
- expect(systemPrompt.systemPrompt.join("\n")).toContain("- CPU: Synthetic Non-Linux CPU");
231
- } finally {
232
- cpus.mockRestore();
233
- Object.defineProperty(process, "platform", { value: originalPlatform });
234
- }
235
- });
236
- });
@@ -1,290 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it } from "bun:test";
2
- import { Settings } from "../config/settings";
3
- import { getThemeByName, setThemeInstance, type Theme } from "../modes/theme/theme";
4
- import { renderResult } from "./render";
5
- import { taskToolRenderer } from "./renderer";
6
- import type { AgentProgress, SingleResult, TaskToolDetails } from "./types";
7
-
8
- const strip = (lines: readonly string[]): string =>
9
- lines
10
- .join("\n")
11
- .replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "")
12
- .replace(/\x1b\[[0-9;]*m/g, "");
13
-
14
- const originalRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "rows");
15
-
16
- function setViewportRows(rows: number): void {
17
- Object.defineProperty(process.stdout, "rows", { configurable: true, value: rows });
18
- }
19
-
20
- function restoreViewportRows(): void {
21
- if (originalRowsDescriptor) {
22
- Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
23
- return;
24
- }
25
- Reflect.deleteProperty(process.stdout, "rows");
26
- }
27
-
28
- function makeProgress(recentOutput: string[]): AgentProgress {
29
- return {
30
- index: 0,
31
- id: "NoisySubagent",
32
- agent: "task",
33
- agentSource: "bundled",
34
- status: "running",
35
- task: "produce noisy output",
36
- recentTools: [],
37
- recentOutput,
38
- toolCount: 1,
39
- requests: 1,
40
- tokens: 0,
41
- cost: 0,
42
- durationMs: 0,
43
- };
44
- }
45
-
46
- function makeParentWithNestedProgress(childCount: number): AgentProgress {
47
- const children = Array.from(
48
- { length: childCount },
49
- (_, index): AgentProgress => ({
50
- ...makeProgress([]),
51
- index,
52
- id: `Nested${index + 1}`,
53
- task: `nested child ${index + 1}`,
54
- }),
55
- );
56
- return {
57
- ...makeProgress([]),
58
- id: "Parent",
59
- task: "parent task",
60
- inflightTaskDetails: {
61
- projectAgentsDir: null,
62
- results: [],
63
- totalDurationMs: 1,
64
- progress: children,
65
- },
66
- };
67
- }
68
-
69
- function makeSingleResult(index: number, overrides?: Partial<SingleResult>): SingleResult {
70
- return {
71
- index,
72
- id: `Done${index + 1}`,
73
- agent: "task",
74
- agentSource: "bundled",
75
- task: `finished child ${index + 1}`,
76
- exitCode: 0,
77
- output: "ok",
78
- stderr: "",
79
- truncated: false,
80
- durationMs: 5,
81
- tokens: 0,
82
- requests: 1,
83
- ...overrides,
84
- };
85
- }
86
-
87
- function makeParentWithNestedResults(childCount: number): TaskToolDetails {
88
- const nested: TaskToolDetails = {
89
- projectAgentsDir: null,
90
- // Failure on the LAST child in display order (equal durations, index tiebreak):
91
- // a plain head-of-list pick would elide it, so its visibility proves the
92
- // failure-first selection of `selectCollapsedResults`.
93
- results: Array.from({ length: childCount }, (_, index) =>
94
- makeSingleResult(index, index === childCount - 1 ? { exitCode: 1, error: "boom" } : undefined),
95
- ),
96
- totalDurationMs: 1,
97
- };
98
- const parent = makeSingleResult(0, { id: "Parent", extractedToolData: { task: [nested] } });
99
- return { projectAgentsDir: null, results: [parent], totalDurationMs: 1 };
100
- }
101
-
102
- function renderResultText(details: TaskToolDetails, expanded: boolean, uiTheme: Theme): string {
103
- const component = renderResult(
104
- { content: [{ type: "text", text: "Ran 1 agent" }], details },
105
- { expanded, isPartial: false },
106
- uiTheme,
107
- );
108
- return strip(component.render(120));
109
- }
110
-
111
- function renderProgressText(progress: AgentProgress, expanded: boolean, uiTheme: Theme): string {
112
- const details: TaskToolDetails = {
113
- projectAgentsDir: null,
114
- results: [],
115
- totalDurationMs: 1,
116
- progress: [progress],
117
- };
118
- const component = renderResult(
119
- { content: [{ type: "text", text: "Running 1 agent..." }], details },
120
- { expanded, isPartial: true },
121
- uiTheme,
122
- );
123
- return strip(component.render(120));
124
- }
125
-
126
- describe("task live progress rendering", () => {
127
- let uiTheme: Theme;
128
-
129
- beforeAll(async () => {
130
- await Settings.init({ inMemory: true });
131
- const loaded = await getThemeByName("dark");
132
- if (!loaded) throw new Error("theme unavailable");
133
- uiTheme = loaded;
134
- setThemeInstance(uiTheme);
135
- });
136
-
137
- afterEach(() => {
138
- restoreViewportRows();
139
- });
140
-
141
- it("caps subagent recent output with the viewport budget instead of a fixed six lines", () => {
142
- setViewportRows(40);
143
- const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
144
- const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
145
-
146
- expect(text).toContain("line 1");
147
- expect(text).toContain("line 8");
148
- expect(text).not.toContain("more lines");
149
- });
150
-
151
- it("keeps the newest subagent output when the viewport cap truncates", () => {
152
- setViewportRows(24);
153
- const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
154
- const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
155
-
156
- expect(text).toContain("… 3 earlier lines");
157
- expect(text).not.toContain("line 1");
158
- expect(text).not.toContain("line 3");
159
- expect(text).toContain("line 4");
160
- expect(text).toContain("line 8");
161
- });
162
-
163
- it("strips bash footer notices from expanded subagent recent output", () => {
164
- setViewportRows(40);
165
- const chronological = [
166
- "line 1",
167
- "line 2",
168
- "line 3",
169
- "line 4",
170
- "line 5",
171
- "line 6",
172
- "Wall time: 0.03 seconds",
173
- "[raw output: artifact://123]",
174
- ];
175
- const progress = makeProgress([...chronological].reverse());
176
-
177
- const expandedText = renderProgressText(progress, true, uiTheme);
178
- expect(expandedText).toContain("line 1");
179
- expect(expandedText).toContain("line 6");
180
- expect(expandedText).not.toContain("Wall time:");
181
- expect(expandedText).not.toContain("raw output:");
182
-
183
- const collapsedText = renderProgressText(progress, false, uiTheme);
184
- expect(collapsedText).not.toContain("line 1");
185
- expect(collapsedText).not.toContain("raw output:");
186
- });
187
- it("sanitizes control sequences from expanded subagent recent output", () => {
188
- setViewportRows(40);
189
- const progress = makeProgress(["safe after \x1b[2Kclear", "raw\rprompt"]);
190
-
191
- const text = renderProgressText(progress, true, uiTheme);
192
-
193
- expect(text).toContain("safe after clear");
194
- expect(text).toContain("rawprompt");
195
- expect(text).not.toContain("\x1b[2K");
196
- expect(text).not.toContain("\r");
197
- });
198
- it("sanitizes control sequences from finalized subagent results", () => {
199
- const output = JSON.stringify({ "\x1b[2Kkey": "safe value" });
200
- const details: TaskToolDetails = {
201
- projectAgentsDir: null,
202
- results: [
203
- makeSingleResult(0, {
204
- id: "Final\x1b[2KAgent",
205
- description: "description\rtext",
206
- aborted: true,
207
- abortReason: "aborted \x1b[2Kreason",
208
- output,
209
- }),
210
- ],
211
- totalDurationMs: 1,
212
- };
213
-
214
- const text = renderResultText(details, true, uiTheme);
215
-
216
- expect(text).toContain("FinalAgent");
217
- expect(text).toContain("descriptiontext");
218
- expect(text).toContain("aborted reason");
219
- expect(text).toContain("key");
220
- expect(text).not.toContain("\x1b[2K");
221
- expect(text).not.toContain("\r");
222
- });
223
-
224
- it("caps collapsed nested task progress at four rows plus an elision line", () => {
225
- setViewportRows(40);
226
- const text = renderProgressText(makeParentWithNestedProgress(6), false, uiTheme);
227
-
228
- expect(text).not.toContain("Nested1");
229
- expect(text).not.toContain("Nested2");
230
- expect(text).toContain("Nested3");
231
- expect(text).toContain("Nested4");
232
- expect(text).toContain("Nested5");
233
- expect(text).toContain("Nested6");
234
- expect(text).toContain("2 more agents");
235
- });
236
-
237
- it("shows every nested task progress row when expanded", () => {
238
- setViewportRows(40);
239
- const text = renderProgressText(makeParentWithNestedProgress(6), true, uiTheme);
240
-
241
- for (let index = 1; index <= 6; index++) {
242
- expect(text).toContain(`Nested${index}`);
243
- }
244
- expect(text).not.toContain("more agents");
245
- });
246
-
247
- it("caps collapsed finalized nested task results and keeps the failed child visible", () => {
248
- setViewportRows(40);
249
- const text = renderResultText(makeParentWithNestedResults(6), false, uiTheme);
250
-
251
- expect(text).toContain("Done6"); // failed child wins a slot despite sorting last
252
- expect(text).toContain("2 more agents");
253
- const visibleChildren = [1, 2, 3, 4, 5, 6].filter(index => text.includes(`Done${index}`));
254
- expect(visibleChildren).toHaveLength(4);
255
- });
256
-
257
- it("shows every finalized nested task result when expanded", () => {
258
- setViewportRows(40);
259
- const text = renderResultText(makeParentWithNestedResults(6), true, uiTheme);
260
-
261
- for (let index = 1; index <= 6; index++) {
262
- expect(text).toContain(`Done${index}`);
263
- }
264
- expect(text).not.toContain("more agents");
265
- });
266
-
267
- it("does not request spinner ticks for static partial progress", () => {
268
- expect("animatedPartialResult" in taskToolRenderer).toBe(false);
269
- });
270
-
271
- it("renders running progress identically across spinner frames", () => {
272
- const progress = makeProgress([]);
273
- const details: TaskToolDetails = {
274
- projectAgentsDir: null,
275
- results: [],
276
- totalDurationMs: 1,
277
- progress: [progress],
278
- };
279
- const render = (spinnerFrame: number) =>
280
- strip(
281
- renderResult(
282
- { content: [{ type: "text", text: "Running 1 agent..." }], details },
283
- { expanded: false, isPartial: true, spinnerFrame },
284
- uiTheme,
285
- ).render(120),
286
- );
287
-
288
- expect(render(0)).toBe(render(1));
289
- });
290
- });