@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

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 (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +374 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -7,9 +7,24 @@ import type { ToolSession } from "../sdk";
7
7
  import { enforceInlineByteCap } from "../session/streaming-output";
8
8
  import { truncateForPrompt } from "./approval";
9
9
  import { resolveCmuxKind } from "./browser/cmux/rpc";
10
- import { acquireBrowser, type BrowserHandle, type BrowserKind, type BrowserKindTag } from "./browser/registry";
10
+ import {
11
+ acquireBrowser,
12
+ type BrowserHandle,
13
+ type BrowserKind,
14
+ type BrowserKindTag,
15
+ holdBrowser,
16
+ releaseBrowser,
17
+ } from "./browser/registry";
11
18
  import type { Observation, ScreenshotResult } from "./browser/tab-protocol";
12
- import { acquireTab, dropHeadlessTabs, getTab, releaseAllTabs, releaseTab, runInTab } from "./browser/tab-supervisor";
19
+ import {
20
+ type AcquireTabResult,
21
+ acquireTab,
22
+ dropHeadlessTabs,
23
+ getTab,
24
+ releaseAllTabs,
25
+ releaseTab,
26
+ runInTab,
27
+ } from "./browser/tab-supervisor";
13
28
  import type { OutputMeta } from "./output-meta";
14
29
  import { resolveToCwd } from "./path-utils";
15
30
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
@@ -232,52 +247,84 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
232
247
  );
233
248
  }
234
249
 
235
- const browser = await untilAborted(signal, () =>
236
- acquireBrowser(kind, {
237
- cwd: this.session.cwd,
238
- viewport: params.viewport
239
- ? {
240
- width: params.viewport.width,
241
- height: params.viewport.height,
242
- deviceScaleFactor: params.viewport.scale,
243
- }
244
- : undefined,
245
- appArgs: params.app?.args,
246
- signal,
247
- }),
248
- );
250
+ // The requested timeout must cover the *entire* open — browser
251
+ // acquisition (CDP discovery/connect), queued tab acquisition, worker
252
+ // creation, and navigation — not only `acquireTab`. Compose one deadline
253
+ // from the caller signal and `params.timeout` and thread it through both
254
+ // stages so a stalled acquisition rejects at the requested boundary.
255
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
256
+ const openSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
257
+ try {
258
+ const browser = await untilAborted(openSignal, () =>
259
+ acquireBrowser(kind, {
260
+ cwd: this.session.cwd,
261
+ viewport: params.viewport
262
+ ? {
263
+ width: params.viewport.width,
264
+ height: params.viewport.height,
265
+ deviceScaleFactor: params.viewport.scale,
266
+ }
267
+ : undefined,
268
+ appArgs: params.app?.args,
269
+ signal: openSignal,
270
+ }),
271
+ );
272
+
273
+ // Hold one open-acquisition lease across the whole tab acquisition.
274
+ // A freshly-created browser sits in the registry at refCount 0 until a
275
+ // tab takes a hold; without this lease an abort/timeout mid-acquisition
276
+ // (or a sibling open of a different tab name on the same browser that
277
+ // fails) could dispose it out from under this operation. The lease is
278
+ // released exactly once — the success and failure paths are mutually
279
+ // exclusive — transferring ownership to the published tab on success or
280
+ // rolling the fresh browser back on failure.
281
+ holdBrowser(browser);
282
+ let result: AcquireTabResult;
283
+ try {
284
+ result = await untilAborted(openSignal, () =>
285
+ acquireTab(name, browser, {
286
+ url: params.url,
287
+ waitUntil: params.wait_until,
288
+ viewport: params.viewport
289
+ ? {
290
+ width: params.viewport.width,
291
+ height: params.viewport.height,
292
+ deviceScaleFactor: params.viewport.scale,
293
+ }
294
+ : undefined,
295
+ target: params.app?.target,
296
+ timeoutMs,
297
+ dialogs: params.dialogs,
298
+ signal: openSignal,
299
+ ownerSessionId: this.session.getSessionId?.() ?? undefined,
300
+ }),
301
+ );
302
+ } catch (error) {
303
+ await releaseBrowser(browser, { kill: false });
304
+ throw error;
305
+ }
306
+ await releaseBrowser(browser, { kill: false });
249
307
 
250
- const result = await untilAborted(signal, () =>
251
- acquireTab(name, browser, {
252
- url: params.url,
253
- waitUntil: params.wait_until,
254
- viewport: params.viewport
255
- ? {
256
- width: params.viewport.width,
257
- height: params.viewport.height,
258
- deviceScaleFactor: params.viewport.scale,
259
- }
260
- : undefined,
261
- target: params.app?.target,
262
- timeoutMs,
263
- dialogs: params.dialogs,
264
- signal,
265
- ownerSessionId: this.session.getSessionId?.() ?? undefined,
266
- }),
267
- );
268
- const tab = result.tab;
269
- const url = tab.info.url;
270
- const title = tab.info.title ?? "";
271
- details.url = url;
272
- details.viewport = tab.info.viewport;
273
- const verb = result.created ? "Opened" : "Reused";
274
- const lines = [
275
- `${verb} tab ${JSON.stringify(name)} on ${describeBrowser(browser)}`,
276
- `URL: ${url}`,
277
- title ? `Title: ${title}` : null,
278
- ].filter((l): l is string => typeof l === "string");
279
- details.result = lines.join("\n");
280
- return toolResult(details).text(lines.join("\n")).done();
308
+ const tab = result.tab;
309
+ const url = tab.info.url;
310
+ const title = tab.info.title ?? "";
311
+ details.url = url;
312
+ details.viewport = tab.info.viewport;
313
+ const verb = result.created ? "Opened" : "Reused";
314
+ const lines = [
315
+ `${verb} tab ${JSON.stringify(name)} on ${describeBrowser(browser)}`,
316
+ `URL: ${url}`,
317
+ title ? `Title: ${title}` : null,
318
+ ].filter((l): l is string => typeof l === "string");
319
+ details.result = lines.join("\n");
320
+ return toolResult(details).text(lines.join("\n")).done();
321
+ } catch (error) {
322
+ // Caller cancellation stays a ToolAbortError; the requested timeout
323
+ // becomes a timeout ToolError; anything else passes through unchanged.
324
+ if (signal?.aborted) throw error instanceof ToolAbortError ? error : new ToolAbortError();
325
+ if (timeoutSignal.aborted) throw new ToolError(`Browser open timed out after ${timeoutMs}ms`);
326
+ throw error;
327
+ }
281
328
  }
282
329
 
283
330
  async #close(
@@ -13,6 +13,7 @@ export const BUILTIN_TOOL_NAMES = [
13
13
  "lsp",
14
14
  "inspect_image",
15
15
  "browser",
16
+ "computer",
16
17
  "checkpoint",
17
18
  "rewind",
18
19
  "task",
@@ -0,0 +1,28 @@
1
+ import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
2
+
3
+ export const COMPUTER_WORKER_ARG = "__omp_worker_computer";
4
+
5
+ export type ComputerWorkerInbound =
6
+ | { type: "ping"; id: string }
7
+ | { type: "init"; options: DesktopSessionOptions }
8
+ | { type: "execute"; id: string; actions: DesktopAction[] }
9
+ | { type: "close" };
10
+
11
+ export type ComputerWorkerOutbound =
12
+ | { type: "pong"; id: string }
13
+ | { type: "ready"; capabilities: DesktopCapabilities }
14
+ | { type: "result"; id: string; capture: DesktopCapture; capabilities: DesktopCapabilities }
15
+ | { type: "error"; id?: string; error: ComputerWorkerError }
16
+ | { type: "closed" };
17
+
18
+ export interface ComputerWorkerError {
19
+ name: string;
20
+ message: string;
21
+ stack?: string;
22
+ }
23
+
24
+ export interface ComputerWorkerTransport {
25
+ send(message: ComputerWorkerOutbound, transfer?: Bun.Transferable[]): void;
26
+ onMessage(handler: (message: ComputerWorkerInbound) => void): () => void;
27
+ close(): void;
28
+ }
@@ -0,0 +1,258 @@
1
+ import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
2
+ import { withTimeout, workerHostEntry } from "@oh-my-pi/pi-utils";
3
+ import { ToolAbortError, ToolError } from "../tool-errors";
4
+ import {
5
+ COMPUTER_WORKER_ARG,
6
+ type ComputerWorkerError,
7
+ type ComputerWorkerInbound,
8
+ type ComputerWorkerOutbound,
9
+ } from "./protocol";
10
+
11
+ const START_TIMEOUT_MS = 10_000;
12
+ const CLOSE_TIMEOUT_MS = 1_500;
13
+ const SMOKE_TIMEOUT_MS = 5_000;
14
+
15
+ export interface ComputerController {
16
+ readonly capabilities: DesktopCapabilities | undefined;
17
+ execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture>;
18
+ close(): Promise<void>;
19
+ }
20
+
21
+ export interface ComputerWorkerHandle {
22
+ send(message: ComputerWorkerInbound): void;
23
+ onMessage(handler: (message: ComputerWorkerOutbound) => void): () => void;
24
+ onError(handler: (error: Error) => void): () => void;
25
+ terminate(): Promise<void>;
26
+ }
27
+
28
+ export interface ComputerSupervisorTimeouts {
29
+ startMs: number;
30
+ closeMs: number;
31
+ }
32
+
33
+ const DEFAULT_TIMEOUTS: ComputerSupervisorTimeouts = {
34
+ startMs: START_TIMEOUT_MS,
35
+ closeMs: CLOSE_TIMEOUT_MS,
36
+ };
37
+ export type ComputerWorkerFactory = () => ComputerWorkerHandle;
38
+
39
+ function workerError(error: ComputerWorkerError): Error {
40
+ const result = new ToolError(error.message);
41
+ result.name = error.name;
42
+ if (error.stack) result.stack = error.stack;
43
+ return result;
44
+ }
45
+
46
+ function wrapWorker(worker: Worker): ComputerWorkerHandle {
47
+ return {
48
+ send(message) {
49
+ worker.postMessage(message);
50
+ },
51
+ onMessage(handler) {
52
+ const listener = (event: MessageEvent): void => handler(event.data as ComputerWorkerOutbound);
53
+ worker.addEventListener("message", listener);
54
+ return () => worker.removeEventListener("message", listener);
55
+ },
56
+ onError(handler) {
57
+ const listener = (event: ErrorEvent): void =>
58
+ handler(event.error instanceof Error ? event.error : new Error(event.message));
59
+ worker.addEventListener("error", listener);
60
+ return () => worker.removeEventListener("error", listener);
61
+ },
62
+ async terminate() {
63
+ worker.terminate();
64
+ },
65
+ };
66
+ }
67
+
68
+ export function spawnComputerWorker(): ComputerWorkerHandle {
69
+ const hostEntry = workerHostEntry();
70
+ const worker = hostEntry
71
+ ? new Worker(hostEntry, { type: "module", argv: [COMPUTER_WORKER_ARG] })
72
+ : new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
73
+ return wrapWorker(worker);
74
+ }
75
+
76
+ interface PendingRequest {
77
+ resolve(capture: DesktopCapture): void;
78
+ reject(error: unknown): void;
79
+ }
80
+
81
+ export class ComputerSupervisor implements ComputerController {
82
+ #worker?: ComputerWorkerHandle;
83
+ #startPromise?: Promise<void>;
84
+ #capabilities?: DesktopCapabilities;
85
+ #pending = new Map<string, PendingRequest>();
86
+ #nextId = 0;
87
+ #serial: Promise<void> = Promise.resolve();
88
+ #closed = false;
89
+ #unsubscribeMessage?: () => void;
90
+ #unsubscribeError?: () => void;
91
+
92
+ constructor(
93
+ private readonly options: DesktopSessionOptions,
94
+ private readonly createWorker: ComputerWorkerFactory = spawnComputerWorker,
95
+ private readonly timeouts: ComputerSupervisorTimeouts = DEFAULT_TIMEOUTS,
96
+ ) {}
97
+
98
+ get capabilities(): DesktopCapabilities | undefined {
99
+ return this.#capabilities;
100
+ }
101
+
102
+ execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
103
+ if (this.#closed) return Promise.reject(new ToolError("Computer session is closed"));
104
+ const run = (): Promise<DesktopCapture> => this.#execute(actions, signal);
105
+ const result = this.#serial.then(run, run);
106
+ this.#serial = result.then(
107
+ () => undefined,
108
+ () => undefined,
109
+ );
110
+ return result;
111
+ }
112
+
113
+ async #execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
114
+ if (this.#closed) throw new ToolError("Computer session is closed");
115
+ if (signal?.aborted) throw new ToolAbortError();
116
+ await this.#start();
117
+ if (signal?.aborted) throw new ToolAbortError();
118
+ const id = `computer-${++this.#nextId}`;
119
+ const request = Promise.withResolvers<DesktopCapture>();
120
+ this.#pending.set(id, request);
121
+ this.#worker!.send({ type: "execute", id, actions });
122
+ if (!signal) return request.promise;
123
+
124
+ const aborted = Promise.withResolvers<never>();
125
+ const onAbort = (): void => aborted.reject(new ToolAbortError());
126
+ signal.addEventListener("abort", onAbort, { once: true });
127
+ try {
128
+ return await Promise.race([request.promise, aborted.promise]);
129
+ } catch (error) {
130
+ if (error instanceof ToolAbortError) await this.#terminate(error);
131
+ throw error;
132
+ } finally {
133
+ signal.removeEventListener("abort", onAbort);
134
+ }
135
+ }
136
+
137
+ #start(): Promise<void> {
138
+ if (this.#startPromise) return this.#startPromise;
139
+ const ready = Promise.withResolvers<void>();
140
+ try {
141
+ const worker = this.createWorker();
142
+ this.#worker = worker;
143
+ this.#unsubscribeMessage = worker.onMessage(message => {
144
+ if (message.type === "ready") {
145
+ this.#capabilities = message.capabilities;
146
+ ready.resolve();
147
+ return;
148
+ }
149
+ if (message.type === "result") {
150
+ this.#capabilities = message.capabilities;
151
+ const pending = this.#pending.get(message.id);
152
+ this.#pending.delete(message.id);
153
+ pending?.resolve(message.capture);
154
+ return;
155
+ }
156
+ if (message.type === "error") {
157
+ const error = workerError(message.error);
158
+ if (message.id) {
159
+ const pending = this.#pending.get(message.id);
160
+ this.#pending.delete(message.id);
161
+ pending?.reject(error);
162
+ } else {
163
+ ready.reject(error);
164
+ }
165
+ }
166
+ });
167
+ this.#unsubscribeError = worker.onError(error => {
168
+ ready.reject(error);
169
+ void this.#terminate(error);
170
+ });
171
+ worker.send({ type: "init", options: this.options });
172
+ } catch (error) {
173
+ ready.reject(error);
174
+ }
175
+ this.#startPromise = withTimeout(
176
+ ready.promise,
177
+ this.timeouts.startMs,
178
+ "Timed out starting native computer worker",
179
+ ).catch(async error => {
180
+ await this.#terminate(error);
181
+ throw error;
182
+ });
183
+ return this.#startPromise;
184
+ }
185
+
186
+ async #terminate(reason: unknown): Promise<void> {
187
+ const worker = this.#worker;
188
+ this.#worker = undefined;
189
+ this.#startPromise = undefined;
190
+ this.#capabilities = undefined;
191
+ this.#unsubscribeMessage?.();
192
+ this.#unsubscribeMessage = undefined;
193
+ this.#unsubscribeError?.();
194
+ this.#unsubscribeError = undefined;
195
+ for (const pending of this.#pending.values()) pending.reject(reason);
196
+ this.#pending.clear();
197
+ await worker?.terminate();
198
+ }
199
+
200
+ async close(): Promise<void> {
201
+ if (this.#closed) return;
202
+ this.#closed = true;
203
+ const worker = this.#worker;
204
+ if (!worker) return;
205
+ const closed = Promise.withResolvers<void>();
206
+ const unsubscribe = worker.onMessage(message => {
207
+ if (message.type === "closed") closed.resolve();
208
+ });
209
+ try {
210
+ worker.send({ type: "close" });
211
+ await withTimeout(closed.promise, this.timeouts.closeMs, "Timed out closing native computer worker");
212
+ } catch {
213
+ // Forced termination below is the bounded close fallback.
214
+ } finally {
215
+ unsubscribe();
216
+ await this.#terminate(new ToolError("Computer session closed"));
217
+ }
218
+ }
219
+ }
220
+
221
+ const ownedSupervisors = new Map<string, Set<ComputerController>>();
222
+
223
+ export function registerComputerController(ownerId: string | undefined, controller: ComputerController): () => void {
224
+ if (!ownerId) return () => {};
225
+ const controllers = ownedSupervisors.get(ownerId) ?? new Set<ComputerController>();
226
+ controllers.add(controller);
227
+ ownedSupervisors.set(ownerId, controllers);
228
+ return () => {
229
+ controllers.delete(controller);
230
+ if (controllers.size === 0) ownedSupervisors.delete(ownerId);
231
+ };
232
+ }
233
+
234
+ export async function releaseComputerSessionsForOwner(ownerId: string | undefined): Promise<void> {
235
+ if (!ownerId) return;
236
+ const controllers = ownedSupervisors.get(ownerId);
237
+ if (!controllers) return;
238
+ ownedSupervisors.delete(ownerId);
239
+ await Promise.allSettled(Array.from(controllers, controller => controller.close()));
240
+ }
241
+
242
+ export async function smokeTestComputerWorker(timeoutMs = SMOKE_TIMEOUT_MS): Promise<void> {
243
+ const worker = spawnComputerWorker();
244
+ const id = "computer-smoke";
245
+ const pong = Promise.withResolvers<void>();
246
+ const unsubscribeMessage = worker.onMessage(message => {
247
+ if (message.type === "pong" && message.id === id) pong.resolve();
248
+ });
249
+ const unsubscribeError = worker.onError(error => pong.reject(error));
250
+ try {
251
+ worker.send({ type: "ping", id });
252
+ await withTimeout(pong.promise, timeoutMs, "Computer worker smoke ping timed out");
253
+ } finally {
254
+ unsubscribeMessage();
255
+ unsubscribeError();
256
+ await worker.terminate();
257
+ }
258
+ }
@@ -0,0 +1,25 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
3
+ import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
4
+ import { ComputerWorkerCore } from "./worker";
5
+
6
+ if (!parentPort) throw new Error("computer-worker-entry: missing parentPort");
7
+
8
+ const port = parentPort;
9
+ const inbox = consumeWorkerInbox();
10
+ const transport: ComputerWorkerTransport = {
11
+ send(message, transfer) {
12
+ port.postMessage(message, transfer ?? []);
13
+ },
14
+ onMessage(handler) {
15
+ if (inbox) return inbox.bind(message => handler(message as ComputerWorkerInbound));
16
+ const listener = (message: unknown): void => handler(message as ComputerWorkerInbound);
17
+ port.on("message", listener);
18
+ return () => port.off("message", listener);
19
+ },
20
+ close() {
21
+ port.close();
22
+ },
23
+ };
24
+
25
+ new ComputerWorkerCore(transport);
@@ -0,0 +1,135 @@
1
+ import {
2
+ type DesktopAction,
3
+ type DesktopCapture,
4
+ DesktopSession,
5
+ type DesktopSessionOptions,
6
+ } from "@oh-my-pi/pi-natives";
7
+ import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
8
+
9
+ export interface NativeDesktopSession {
10
+ readonly capabilities: DesktopSession["capabilities"];
11
+ capture(): Promise<DesktopCapture>;
12
+ execute(actions: DesktopAction[]): Promise<DesktopCapture>;
13
+ close(): Promise<void>;
14
+ }
15
+
16
+ export type NativeDesktopSessionFactory = (options: DesktopSessionOptions) => NativeDesktopSession;
17
+
18
+ const COORDINATE_ACTIONS: ReadonlySet<DesktopAction["type"]> = new Set([
19
+ "click",
20
+ "double_click",
21
+ "drag",
22
+ "move",
23
+ "scroll",
24
+ ]);
25
+
26
+ function serializeError(error: unknown): ComputerWorkerError {
27
+ if (error instanceof Error) {
28
+ return { name: error.name, message: error.message, ...(error.stack ? { stack: error.stack } : {}) };
29
+ }
30
+ return { name: "Error", message: String(error) };
31
+ }
32
+
33
+ function captureTransfer(capture: DesktopCapture): Bun.Transferable[] {
34
+ const buffer = capture.data.buffer;
35
+ return buffer instanceof ArrayBuffer ? [buffer] : [];
36
+ }
37
+
38
+ export class ComputerWorkerCore {
39
+ #session?: NativeDesktopSession;
40
+ #hasReturnedFrame = false;
41
+ #closed = false;
42
+ #tail: Promise<void> = Promise.resolve();
43
+ readonly #unsubscribe: () => void;
44
+
45
+ constructor(
46
+ private readonly transport: ComputerWorkerTransport,
47
+ private readonly createSession: NativeDesktopSessionFactory = options => new DesktopSession(options),
48
+ ) {
49
+ this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
50
+ }
51
+
52
+ #onMessage(message: ComputerWorkerInbound): void {
53
+ if (message.type === "ping") {
54
+ this.transport.send({ type: "pong", id: message.id });
55
+ return;
56
+ }
57
+ if (message.type === "close") {
58
+ this.#tail = this.#tail.then(() => this.#close());
59
+ return;
60
+ }
61
+ if (message.type === "init") {
62
+ this.#tail = this.#tail.then(() => this.#init(message.options));
63
+ return;
64
+ }
65
+ this.#tail = this.#tail.then(() => this.#execute(message.id, message.actions));
66
+ }
67
+
68
+ async #init(options: DesktopSessionOptions): Promise<void> {
69
+ if (this.#closed) return;
70
+ if (this.#session) {
71
+ this.transport.send({
72
+ type: "error",
73
+ error: { name: "Error", message: "Computer worker already initialized" },
74
+ });
75
+ return;
76
+ }
77
+ try {
78
+ this.#session = this.createSession(options);
79
+ this.#hasReturnedFrame = false;
80
+ this.transport.send({ type: "ready", capabilities: this.#session.capabilities });
81
+ } catch (error) {
82
+ this.transport.send({ type: "error", error: serializeError(error) });
83
+ }
84
+ }
85
+
86
+ async #execute(id: string, actions: DesktopAction[]): Promise<void> {
87
+ const session = this.#session;
88
+ if (!session) {
89
+ this.transport.send({
90
+ type: "error",
91
+ id,
92
+ error: { name: "Error", message: "Computer worker is not initialized" },
93
+ });
94
+ return;
95
+ }
96
+ try {
97
+ if (!this.#hasReturnedFrame && actions.some(action => COORDINATE_ACTIONS.has(action.type))) {
98
+ this.transport.send({
99
+ type: "error",
100
+ id,
101
+ error: {
102
+ name: "Error",
103
+ message:
104
+ "Coordinate computer actions require a screenshot returned to the provider; request a screenshot first",
105
+ },
106
+ });
107
+ return;
108
+ }
109
+ const capture = await session.execute(actions);
110
+ this.transport.send(
111
+ { type: "result", id, capture, capabilities: session.capabilities },
112
+ captureTransfer(capture),
113
+ );
114
+ this.#hasReturnedFrame = true;
115
+ } catch (error) {
116
+ this.transport.send({ type: "error", id, error: serializeError(error) });
117
+ }
118
+ }
119
+
120
+ async #close(): Promise<void> {
121
+ if (this.#closed) return;
122
+ this.#closed = true;
123
+ try {
124
+ await this.#session?.close();
125
+ } catch (error) {
126
+ this.transport.send({ type: "error", error: serializeError(error) });
127
+ } finally {
128
+ this.#session = undefined;
129
+ this.#hasReturnedFrame = false;
130
+ this.#unsubscribe();
131
+ this.transport.send({ type: "closed" });
132
+ this.transport.close();
133
+ }
134
+ }
135
+ }