@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.5

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 (319) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-q2w43aw3.md} +51 -0
  3. package/dist/cli.js +4148 -3717
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/help-extra.d.ts +2 -0
  9. package/dist/types/cli/thinking-levels.d.ts +5 -0
  10. package/dist/types/cli/update-cli.d.ts +13 -0
  11. package/dist/types/commands/browser-relay.d.ts +38 -0
  12. package/dist/types/commands/commit.d.ts +3 -0
  13. package/dist/types/commands/complete.d.ts +1 -1
  14. package/dist/types/commands/completions.d.ts +6 -0
  15. package/dist/types/commands/grep.d.ts +3 -0
  16. package/dist/types/commands/join.d.ts +4 -0
  17. package/dist/types/commands/launch-help.d.ts +171 -0
  18. package/dist/types/commands/launch.d.ts +5 -1
  19. package/dist/types/commands/models.d.ts +3 -0
  20. package/dist/types/commands/say.d.ts +10 -0
  21. package/dist/types/commands/worktree.d.ts +3 -0
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/model-discovery.d.ts +2 -2
  24. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  25. package/dist/types/config/models-config-schema.d.ts +2 -0
  26. package/dist/types/config/models-config.d.ts +1 -0
  27. package/dist/types/config/service-tier.d.ts +2 -0
  28. package/dist/types/config/settings-schema.d.ts +69 -21
  29. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  30. package/dist/types/cursor.d.ts +7 -4
  31. package/dist/types/edit/index.d.ts +3 -3
  32. package/dist/types/edit/modes/replace.d.ts +22 -15
  33. package/dist/types/edit/renderer.d.ts +1 -1
  34. package/dist/types/eval/py/kernel.d.ts +3 -1
  35. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  39. package/dist/types/launch/broker.d.ts +1 -1
  40. package/dist/types/launch/client.d.ts +6 -3
  41. package/dist/types/launch/ensure.d.ts +8 -0
  42. package/dist/types/launch/paths.d.ts +1 -1
  43. package/dist/types/launch/protocol.d.ts +2 -2
  44. package/dist/types/lsp/client.d.ts +2 -0
  45. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  46. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  47. package/dist/types/lsp/mux/server.d.ts +18 -0
  48. package/dist/types/lsp/types.d.ts +22 -2
  49. package/dist/types/main.d.ts +2 -0
  50. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  51. package/dist/types/mnemopi/state.d.ts +0 -23
  52. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  53. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  54. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  55. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  56. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  57. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  58. package/dist/types/modes/interactive-mode.d.ts +1 -0
  59. package/dist/types/modes/types.d.ts +1 -0
  60. package/dist/types/modes/workflow.d.ts +2 -1
  61. package/dist/types/sdk.d.ts +3 -1
  62. package/dist/types/secrets/index.d.ts +1 -1
  63. package/dist/types/session/agent-session-types.d.ts +3 -1
  64. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  65. package/dist/types/session/session-advisors.d.ts +1 -1
  66. package/dist/types/session/session-context.d.ts +2 -1
  67. package/dist/types/session/session-maintenance.d.ts +1 -0
  68. package/dist/types/session/session-manager.d.ts +16 -11
  69. package/dist/types/session/session-stats.d.ts +10 -0
  70. package/dist/types/session/session-storage.d.ts +15 -3
  71. package/dist/types/system-prompt.d.ts +4 -1
  72. package/dist/types/task/index.d.ts +2 -1
  73. package/dist/types/task/spawn-policy.d.ts +5 -0
  74. package/dist/types/thinking.d.ts +1 -7
  75. package/dist/types/tools/ast-grep.d.ts +1 -1
  76. package/dist/types/tools/browser/registry.d.ts +2 -1
  77. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  78. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  79. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  80. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  81. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  82. package/dist/types/tools/browser/render.d.ts +1 -0
  83. package/dist/types/tools/browser.d.ts +3 -0
  84. package/dist/types/tools/computer/exposure.d.ts +9 -7
  85. package/dist/types/tools/computer/protocol.d.ts +70 -17
  86. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  87. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  88. package/dist/types/tools/computer/worker.d.ts +42 -7
  89. package/dist/types/tools/computer-renderer.d.ts +7 -5
  90. package/dist/types/tools/computer.d.ts +25 -44
  91. package/dist/types/tools/context.d.ts +3 -0
  92. package/dist/types/tools/glob.d.ts +1 -1
  93. package/dist/types/tools/grep.d.ts +1 -1
  94. package/dist/types/tools/render-utils.d.ts +2 -0
  95. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  96. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  97. package/dist/types/tools/xdev.d.ts +8 -0
  98. package/dist/types/utils/clipboard.d.ts +2 -2
  99. package/dist/types/web/kagi.d.ts +1 -0
  100. package/dist/types/web/search/index.d.ts +1 -1
  101. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  102. package/dist/types/web/search/providers/base.d.ts +2 -0
  103. package/dist/types/web/search/providers/brave.d.ts +1 -0
  104. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  105. package/dist/types/web/search/providers/codex.d.ts +1 -0
  106. package/dist/types/web/search/providers/exa.d.ts +1 -0
  107. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  108. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  109. package/dist/types/web/search/providers/jina.d.ts +1 -0
  110. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  111. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  112. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  113. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  114. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  115. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  116. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  117. package/dist/types/web/search/providers/utils.d.ts +6 -9
  118. package/dist/types/web/search/providers/zai.d.ts +1 -0
  119. package/dist/types/web/search/types.d.ts +4 -0
  120. package/package.json +12 -12
  121. package/src/cli/args.ts +7 -87
  122. package/src/cli/browser-relay-cli.ts +119 -0
  123. package/src/cli/command-help.ts +103 -0
  124. package/src/cli/completion-gen.ts +5 -5
  125. package/src/cli/flag-tables.ts +9 -0
  126. package/src/cli/help-extra.ts +89 -0
  127. package/src/cli/setup-cli.ts +13 -21
  128. package/src/cli/thinking-levels.ts +7 -0
  129. package/src/cli/update-cli.ts +26 -7
  130. package/src/cli-commands.ts +170 -33
  131. package/src/cli.ts +17 -5
  132. package/src/commands/acp.ts +3 -1
  133. package/src/commands/agents.ts +3 -2
  134. package/src/commands/auth-broker.ts +3 -2
  135. package/src/commands/auth-gateway.ts +3 -2
  136. package/src/commands/bench.ts +2 -3
  137. package/src/commands/browser-relay.ts +53 -0
  138. package/src/commands/cleanse.ts +2 -2
  139. package/src/commands/commit.ts +3 -2
  140. package/src/commands/complete.ts +2 -1
  141. package/src/commands/completions.ts +3 -2
  142. package/src/commands/config.ts +3 -2
  143. package/src/commands/dry-balance.ts +2 -2
  144. package/src/commands/gallery.ts +3 -2
  145. package/src/commands/gc.ts +3 -2
  146. package/src/commands/grep.ts +3 -2
  147. package/src/commands/grievances.ts +3 -2
  148. package/src/commands/install.ts +2 -2
  149. package/src/commands/join.ts +3 -2
  150. package/src/commands/launch-help.ts +116 -0
  151. package/src/commands/launch.ts +7 -191
  152. package/src/commands/models.ts +3 -2
  153. package/src/commands/plugin.ts +3 -2
  154. package/src/commands/read.ts +3 -2
  155. package/src/commands/say.ts +3 -2
  156. package/src/commands/setup.ts +3 -2
  157. package/src/commands/shell.ts +3 -2
  158. package/src/commands/ssh.ts +3 -2
  159. package/src/commands/stats.ts +3 -2
  160. package/src/commands/tiny-models.ts +2 -2
  161. package/src/commands/token.ts +2 -2
  162. package/src/commands/ttsr.ts +2 -2
  163. package/src/commands/update.ts +3 -2
  164. package/src/commands/usage.ts +3 -2
  165. package/src/commands/web-search.ts +3 -2
  166. package/src/commands/worktree.ts +3 -2
  167. package/src/config/keybindings.ts +5 -0
  168. package/src/config/model-discovery.ts +30 -13
  169. package/src/config/model-registry.ts +9 -5
  170. package/src/config/models-config-schema-bundle.ts +9 -0
  171. package/src/config/resolve-config-value.ts +2 -1
  172. package/src/config/service-tier.ts +5 -0
  173. package/src/config/settings-schema.ts +72 -20
  174. package/src/cursor-bridge-tools.ts +1 -1
  175. package/src/cursor.ts +10 -14
  176. package/src/edit/diff.ts +17 -8
  177. package/src/edit/index.ts +20 -7
  178. package/src/edit/modes/replace.ts +93 -40
  179. package/src/edit/renderer.ts +1 -1
  180. package/src/edit/streaming.ts +14 -17
  181. package/src/eval/py/kernel.ts +2 -1
  182. package/src/eval/py/spawn-options.ts +24 -29
  183. package/src/export/html/template.js +8 -26
  184. package/src/extensibility/extensions/wrapper.ts +1 -0
  185. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  186. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  187. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  188. package/src/extensibility/skills.ts +11 -0
  189. package/src/extensibility/utils.ts +10 -2
  190. package/src/launch/broker.ts +1 -1
  191. package/src/launch/client.ts +32 -13
  192. package/src/launch/ensure.ts +73 -0
  193. package/src/launch/paths.ts +1 -1
  194. package/src/launch/protocol.ts +2 -2
  195. package/src/lsp/client.ts +23 -2
  196. package/src/lsp/index.ts +8 -0
  197. package/src/lsp/mux/daemon.ts +348 -0
  198. package/src/lsp/mux/protocol.ts +96 -0
  199. package/src/lsp/mux/server.ts +797 -0
  200. package/src/lsp/types.ts +28 -2
  201. package/src/main.ts +7 -2
  202. package/src/mcp/oauth-flow.ts +11 -1
  203. package/src/mnemopi/embed-client.ts +49 -2
  204. package/src/mnemopi/state.ts +57 -23
  205. package/src/modes/components/assistant-message.ts +11 -0
  206. package/src/modes/components/chat-transcript-builder.ts +4 -0
  207. package/src/modes/components/custom-editor.ts +9 -0
  208. package/src/modes/components/read-tool-group.ts +10 -0
  209. package/src/modes/components/status-line/component.ts +1 -1
  210. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  211. package/src/modes/components/tool-execution.ts +8 -0
  212. package/src/modes/components/tree-selector.ts +46 -48
  213. package/src/modes/controllers/event-controller.ts +17 -0
  214. package/src/modes/controllers/input-controller.ts +39 -1
  215. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  216. package/src/modes/controllers/selector-controller.ts +20 -0
  217. package/src/modes/interactive-mode.ts +2 -0
  218. package/src/modes/types.ts +1 -0
  219. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  220. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  221. package/src/modes/utils/ui-helpers.ts +6 -10
  222. package/src/modes/workflow.ts +8 -2
  223. package/src/prompts/agents/init.md +1 -1
  224. package/src/prompts/system/plan-mode-active.md +2 -2
  225. package/src/prompts/system/system-prompt.md +26 -52
  226. package/src/prompts/system/workflow-notice.md +3 -3
  227. package/src/prompts/tools/ast-grep.md +1 -1
  228. package/src/prompts/tools/bash.md +1 -1
  229. package/src/prompts/tools/browser.md +1 -0
  230. package/src/prompts/tools/computer.md +21 -21
  231. package/src/prompts/tools/glob.md +1 -1
  232. package/src/prompts/tools/grep.md +1 -1
  233. package/src/prompts/tools/replace.md +5 -4
  234. package/src/prompts/tools/task.md +4 -4
  235. package/src/sdk.ts +34 -6
  236. package/src/secrets/index.ts +1 -1
  237. package/src/session/agent-session-types.ts +3 -1
  238. package/src/session/agent-session.ts +13 -1
  239. package/src/session/indexed-session-storage.ts +9 -0
  240. package/src/session/prewalk.ts +24 -2
  241. package/src/session/session-advisors.ts +2 -2
  242. package/src/session/session-context.ts +16 -12
  243. package/src/session/session-dump-format.ts +1 -1
  244. package/src/session/session-maintenance.ts +38 -4
  245. package/src/session/session-manager.ts +102 -67
  246. package/src/session/session-paths.ts +118 -55
  247. package/src/session/session-stats.ts +48 -3
  248. package/src/session/session-storage.ts +30 -22
  249. package/src/session/stream-guards.ts +1 -1
  250. package/src/slash-commands/builtin-registry.ts +27 -24
  251. package/src/system-prompt.ts +8 -3
  252. package/src/task/index.ts +23 -8
  253. package/src/task/spawn-policy.ts +14 -0
  254. package/src/thinking.ts +2 -8
  255. package/src/tools/ast-grep.ts +10 -4
  256. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  257. package/src/tools/browser/cmux/rpc.ts +2 -8
  258. package/src/tools/browser/registry.ts +53 -2
  259. package/src/tools/browser/relay/bridge.ts +945 -0
  260. package/src/tools/browser/relay/daemon.ts +117 -0
  261. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  262. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  263. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  264. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  265. package/src/tools/browser/relay/kind.ts +41 -0
  266. package/src/tools/browser/relay/protocol.ts +54 -0
  267. package/src/tools/browser/relay/server.ts +141 -0
  268. package/src/tools/browser/render.ts +4 -1
  269. package/src/tools/browser/shared-daemon.ts +7 -58
  270. package/src/tools/browser/tab-supervisor.ts +5 -4
  271. package/src/tools/browser/tab-worker.ts +33 -12
  272. package/src/tools/browser.ts +26 -1
  273. package/src/tools/computer/exposure.ts +11 -35
  274. package/src/tools/computer/protocol.ts +51 -10
  275. package/src/tools/computer/supervisor.ts +227 -124
  276. package/src/tools/computer/worker-entry.ts +11 -6
  277. package/src/tools/computer/worker.ts +695 -81
  278. package/src/tools/computer-renderer.ts +104 -65
  279. package/src/tools/computer.ts +134 -428
  280. package/src/tools/context.ts +3 -0
  281. package/src/tools/glob.ts +9 -2
  282. package/src/tools/grep.ts +12 -6
  283. package/src/tools/inspect-image.ts +32 -6
  284. package/src/tools/render-utils.ts +2 -0
  285. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  286. package/src/tools/tool-timeouts.ts +1 -0
  287. package/src/tools/xdev.ts +29 -2
  288. package/src/utils/clipboard.ts +40 -14
  289. package/src/utils/command-args.ts +26 -28
  290. package/src/utils/tool-choice.ts +0 -13
  291. package/src/web/kagi.ts +2 -1
  292. package/src/web/search/index.ts +18 -2
  293. package/src/web/search/providers/anthropic.ts +4 -1
  294. package/src/web/search/providers/base.ts +2 -0
  295. package/src/web/search/providers/brave.ts +3 -1
  296. package/src/web/search/providers/browser-page.ts +6 -4
  297. package/src/web/search/providers/codex.ts +4 -1
  298. package/src/web/search/providers/duckduckgo.ts +2 -1
  299. package/src/web/search/providers/ecosia.ts +2 -1
  300. package/src/web/search/providers/exa.ts +4 -2
  301. package/src/web/search/providers/firecrawl.ts +3 -1
  302. package/src/web/search/providers/gemini.ts +8 -2
  303. package/src/web/search/providers/google.ts +2 -1
  304. package/src/web/search/providers/jina.ts +12 -2
  305. package/src/web/search/providers/kagi.ts +3 -0
  306. package/src/web/search/providers/kimi.ts +5 -1
  307. package/src/web/search/providers/mojeek.ts +2 -1
  308. package/src/web/search/providers/parallel.ts +5 -1
  309. package/src/web/search/providers/perplexity.ts +6 -3
  310. package/src/web/search/providers/public.ts +1 -1
  311. package/src/web/search/providers/searxng.ts +11 -5
  312. package/src/web/search/providers/startpage.ts +10 -4
  313. package/src/web/search/providers/synthetic.ts +11 -5
  314. package/src/web/search/providers/tavily.ts +3 -1
  315. package/src/web/search/providers/tinyfish.ts +3 -1
  316. package/src/web/search/providers/utils.ts +12 -10
  317. package/src/web/search/providers/xai.ts +2 -2
  318. package/src/web/search/providers/zai.ts +18 -3
  319. package/src/web/search/types.ts +6 -0
@@ -1,131 +1,745 @@
1
- import type { DesktopAction, DesktopCapture, DesktopSession, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ import type {
6
+ AxNode,
7
+ AxQuery,
8
+ AxSnapshotOptions,
9
+ DesktopCapabilities,
10
+ DesktopDisplay,
11
+ DesktopPoint,
12
+ DesktopSessionOptions,
13
+ DesktopWindow,
14
+ PointerOptions,
15
+ } from "@oh-my-pi/pi-natives";
2
16
  import { createDesktopSession } from "@oh-my-pi/pi-natives/desktop";
3
- import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
17
+ import * as postmortem from "@oh-my-pi/pi-utils/postmortem";
18
+ import { Snowflake } from "@oh-my-pi/pi-utils/snowflake";
19
+ import { JsRuntime, type RuntimeHooks } from "../../eval/js/shared/runtime";
20
+ import { copyToClipboard, readTextFromClipboard } from "../../utils/clipboard";
21
+ import { cloneSafe, RunOutput } from "../browser/run-output";
22
+ import {
23
+ bindRunFacade,
24
+ markHandled,
25
+ resolvePredicateTimeout,
26
+ type WaitPredicateOptions,
27
+ waitForRun,
28
+ } from "../run-scope";
29
+ import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
30
+ import type {
31
+ ComputerScreenshot,
32
+ ComputerSessionSnapshot,
33
+ ComputerWorkerInbound,
34
+ ComputerWorkerTransport,
35
+ RunErrorPayload,
36
+ ToolReply,
37
+ } from "./protocol";
4
38
 
39
+ /** Native desktop operations consumed by the script runtime. */
5
40
  export interface NativeDesktopSession {
6
- readonly capabilities: DesktopSession["capabilities"];
7
- capture(): Promise<DesktopCapture>;
8
- execute(actions: DesktopAction[]): Promise<DesktopCapture>;
41
+ readonly capabilities: DesktopCapabilities;
42
+ listDisplays(): Promise<DesktopDisplay[]>;
43
+ listWindows(): Promise<DesktopWindow[]>;
44
+ capture(
45
+ target: string,
46
+ caps?: { maxWidth?: number; maxHeight?: number } | null,
47
+ ): Promise<{
48
+ data: Uint8Array;
49
+ width: number;
50
+ height: number;
51
+ sourceWidth: number;
52
+ sourceHeight: number;
53
+ target: string;
54
+ }>;
55
+ click(target: string, x: number, y: number, opts?: PointerOptions | null): Promise<void>;
56
+ moveMouse(target: string, x: number, y: number, opts?: PointerOptions | null): Promise<void>;
57
+ drag(target: string, points: DesktopPoint[], opts?: PointerOptions | null): Promise<void>;
58
+ scroll(target: string, x: number, y: number, dx: number, dy: number, opts?: PointerOptions | null): Promise<void>;
59
+ typeText(target: string, text: string, opts?: PointerOptions | null): Promise<void>;
60
+ keyChord(target: string, keys: string[], opts?: PointerOptions | null): Promise<void>;
61
+ raiseWindow(windowId: string): Promise<void>;
62
+ axSnapshot(target: string, opts?: AxSnapshotOptions | null): Promise<{ text: string }>;
63
+ axQuery(target: string, query: AxQuery): Promise<AxNode[]>;
64
+ axElementAt(target: string, x: number, y: number): Promise<AxNode | null | undefined>;
65
+ axFocused(): Promise<AxNode | null | undefined>;
66
+ axNode(ref: string): Promise<AxNode>;
67
+ axAttributes(ref: string): Promise<Array<[string, string]>>;
68
+ axChildren(ref: string): Promise<AxNode[]>;
69
+ axParent(ref: string): Promise<AxNode | null | undefined>;
70
+ axPerform(ref: string, action: string): Promise<void>;
71
+ axSetValue(ref: string, value: string): Promise<void>;
72
+ axFocus(ref: string): Promise<void>;
73
+ axClick(ref: string, opts?: PointerOptions | null): Promise<void>;
9
74
  close(): Promise<void>;
10
75
  }
11
76
 
77
+ /** Creates the native session co-located with the computer worker runtime. */
12
78
  export type NativeDesktopSessionFactory = (options: DesktopSessionOptions) => NativeDesktopSession;
13
79
 
14
- const COORDINATE_ACTIONS: ReadonlySet<DesktopAction["type"]> = new Set([
15
- "click",
16
- "double_click",
17
- "drag",
18
- "move",
19
- "scroll",
20
- ]);
80
+ type WindowFilter = { app?: string; title?: string };
81
+ type DeliveryOptions = { delivery?: string };
82
+ type ScreenshotOptions = { silent?: boolean };
83
+ type ClickOptions = DeliveryOptions & { button?: string; count?: number; modifiers?: string[] };
84
+ type DragOptions = DeliveryOptions & { modifiers?: string[] };
85
+ type ScrollOptions = DeliveryOptions & { dx?: number; dy?: number };
86
+ type AxOptions = Pick<AxSnapshotOptions, "all" | "maxDepth">;
87
+
88
+ type PendingTool = { resolve(value: unknown): void; reject(reason?: unknown): void };
89
+ interface ActiveRun {
90
+ id: string;
91
+ ac: AbortController;
92
+ signal: AbortSignal;
93
+ pendingTools: Map<string, PendingTool>;
94
+ }
95
+
96
+ interface ComputerRunContext {
97
+ signal: AbortSignal;
98
+ readOnly: boolean;
99
+ snapshot: ComputerSessionSnapshot;
100
+ output: RunOutput;
101
+ screenshots: ComputerScreenshot[];
102
+ }
103
+
104
+ type RunContextAccessor = () => ComputerRunContext;
21
105
 
22
- function serializeError(error: unknown): ComputerWorkerError {
106
+ function errorPayload(error: unknown): RunErrorPayload {
107
+ if (error instanceof ToolAbortError) {
108
+ return { name: error.name, message: error.message, stack: error.stack, isToolError: false, isAbort: true };
109
+ }
110
+ if (error instanceof ToolError) {
111
+ return { name: error.name, message: error.message, stack: error.stack, isToolError: true, isAbort: false };
112
+ }
23
113
  if (error instanceof Error) {
24
- return { name: error.name, message: error.message, ...(error.stack ? { stack: error.stack } : {}) };
114
+ return { name: error.name, message: error.message, stack: error.stack, isToolError: false, isAbort: false };
115
+ }
116
+ return { name: "Error", message: String(error), isToolError: false, isAbort: false };
117
+ }
118
+
119
+ function replyError(payload: RunErrorPayload): Error {
120
+ if (payload.isAbort) {
121
+ const error = new ToolAbortError(payload.message || "Tool call aborted");
122
+ if (payload.stack) error.stack = payload.stack;
123
+ return error;
124
+ }
125
+ const ErrorType = payload.isToolError ? ToolError : Error;
126
+ const error = new ErrorType(payload.message);
127
+ if (payload.name) error.name = payload.name;
128
+ if (payload.stack) error.stack = payload.stack;
129
+ return error;
130
+ }
131
+
132
+ function nativeError(error: unknown): ToolError {
133
+ return new ToolError(error instanceof Error ? error.message : String(error));
134
+ }
135
+
136
+ async function nativeCall<T>(signal: AbortSignal, call: () => Promise<T>): Promise<T> {
137
+ throwIfAborted(signal);
138
+ try {
139
+ const value = await call();
140
+ throwIfAborted(signal);
141
+ return value;
142
+ } catch (error) {
143
+ if (error instanceof ToolAbortError) throw error;
144
+ throw nativeError(error);
25
145
  }
26
- return { name: "Error", message: String(error) };
27
146
  }
28
147
 
29
- function captureTransfer(capture: DesktopCapture): Bun.Transferable[] {
30
- const buffer = capture.data.buffer;
31
- return buffer instanceof ArrayBuffer ? [buffer] : [];
148
+ function pointerOptions(options?: ClickOptions | DragOptions | DeliveryOptions): PointerOptions | undefined {
149
+ if (!options) return undefined;
150
+ const mapped: PointerOptions = {};
151
+ if ("button" in options && options.button !== undefined) mapped.button = options.button;
152
+ if ("count" in options && options.count !== undefined) mapped.count = options.count;
153
+ if ("modifiers" in options && options.modifiers !== undefined) mapped.modifiers = options.modifiers;
154
+ if (options.delivery !== undefined) mapped.deliveryMode = options.delivery;
155
+ return mapped;
156
+ }
157
+
158
+ function chordKeys(chord: string | string[]): string[] {
159
+ return typeof chord === "string"
160
+ ? chord
161
+ .split("+")
162
+ .map(key => key.trim())
163
+ .filter(Boolean)
164
+ : chord;
165
+ }
166
+
167
+ function matchesFilter(window: DesktopWindow, filter?: WindowFilter): boolean {
168
+ if (!filter) return true;
169
+ const app = filter.app?.toLocaleLowerCase();
170
+ const title = filter.title?.toLocaleLowerCase();
171
+ return (
172
+ (!app || window.app.toLocaleLowerCase().includes(app)) &&
173
+ (!title || window.title.toLocaleLowerCase().includes(title))
174
+ );
175
+ }
176
+
177
+ function guardRun(context: ComputerRunContext, method: string): void {
178
+ if (context.readOnly) throw new ToolError(`read-only run: '${method}' requires read_only: false`);
179
+ throwIfAborted(context.signal);
180
+ }
181
+
182
+ async function captureScreenshot(
183
+ session: NativeDesktopSession,
184
+ getContext: RunContextAccessor,
185
+ target: string,
186
+ options?: ScreenshotOptions,
187
+ ): Promise<{ path: string; width: number; height: number }> {
188
+ const context = getContext();
189
+ const frame = await nativeCall(context.signal, () =>
190
+ session.capture(target, {
191
+ maxWidth: context.snapshot.captureMaxWidth,
192
+ maxHeight: context.snapshot.captureMaxHeight,
193
+ }),
194
+ );
195
+ const destination = path.join(os.tmpdir(), `omp-computer-${Snowflake.next()}.png`);
196
+ await Bun.write(destination, frame.data);
197
+ const scaled = frame.width !== frame.sourceWidth || frame.height !== frame.sourceHeight;
198
+ context.screenshots.push({
199
+ path: destination,
200
+ width: frame.width,
201
+ height: frame.height,
202
+ sourceWidth: frame.sourceWidth,
203
+ sourceHeight: frame.sourceHeight,
204
+ target: frame.target,
205
+ });
206
+ if (!options?.silent) {
207
+ context.output.push({
208
+ type: "text",
209
+ text: scaled
210
+ ? `screenshot ${frame.target} ${frame.width}×${frame.height} (scaled from ${frame.sourceWidth}×${frame.sourceHeight}) → ${destination}`
211
+ : `screenshot ${frame.target} ${frame.width}×${frame.height} → ${destination}`,
212
+ });
213
+ context.output.push({
214
+ type: "image",
215
+ data: Buffer.from(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength).toString("base64"),
216
+ mimeType: "image/png",
217
+ });
218
+ }
219
+ return { path: destination, width: frame.width, height: frame.height };
220
+ }
221
+
222
+ class El {
223
+ readonly ref: string;
224
+ readonly role: string;
225
+ readonly nativeRole: string;
226
+ readonly title?: string;
227
+ readonly description?: string;
228
+ readonly enabled: boolean;
229
+ readonly focused: boolean;
230
+ readonly childCount: number;
231
+ readonly #session: NativeDesktopSession;
232
+ readonly #getContext: RunContextAccessor;
233
+
234
+ constructor(session: NativeDesktopSession, getContext: RunContextAccessor, node: AxNode) {
235
+ this.#session = session;
236
+ this.#getContext = getContext;
237
+ this.ref = node.ref;
238
+ this.role = node.role;
239
+ this.nativeRole = node.nativeRole;
240
+ this.title = node.title;
241
+ this.description = node.description;
242
+ this.enabled = node.enabled;
243
+ this.focused = node.focused;
244
+ this.childCount = node.childCount;
245
+ }
246
+
247
+ async value(): Promise<string | undefined> {
248
+ const { signal } = this.#getContext();
249
+ return (await nativeCall(signal, () => this.#session.axNode(this.ref))).value;
250
+ }
251
+
252
+ async setValue(value: string): Promise<void> {
253
+ const context = this.#getContext();
254
+ guardRun(context, "setValue");
255
+ await nativeCall(context.signal, () => this.#session.axSetValue(this.ref, value));
256
+ }
257
+
258
+ async bounds(): Promise<{ x: number; y: number; width: number; height: number } | null> {
259
+ const { signal } = this.#getContext();
260
+ const node = await nativeCall(signal, () => this.#session.axNode(this.ref));
261
+ if (node.x === undefined || node.y === undefined || node.width === undefined || node.height === undefined)
262
+ return null;
263
+ return { x: node.x, y: node.y, width: node.width, height: node.height };
264
+ }
265
+
266
+ async attributes(): Promise<Record<string, string>> {
267
+ const { signal } = this.#getContext();
268
+ return Object.fromEntries(await nativeCall(signal, () => this.#session.axAttributes(this.ref)));
269
+ }
270
+
271
+ async actions(): Promise<string[]> {
272
+ const { signal } = this.#getContext();
273
+ return (await nativeCall(signal, () => this.#session.axNode(this.ref))).actions ?? [];
274
+ }
275
+
276
+ async perform(action: string): Promise<void> {
277
+ const context = this.#getContext();
278
+ guardRun(context, "perform");
279
+ await nativeCall(context.signal, () => this.#session.axPerform(this.ref, action));
280
+ }
281
+
282
+ async press(): Promise<void> {
283
+ const context = this.#getContext();
284
+ guardRun(context, "press");
285
+ await nativeCall(context.signal, () => this.#session.axPerform(this.ref, "press"));
286
+ }
287
+
288
+ async click(options?: DeliveryOptions): Promise<void> {
289
+ const context = this.#getContext();
290
+ guardRun(context, "click");
291
+ await nativeCall(context.signal, () => this.#session.axClick(this.ref, pointerOptions(options)));
292
+ }
293
+
294
+ async focus(): Promise<void> {
295
+ const context = this.#getContext();
296
+ guardRun(context, "focus");
297
+ await nativeCall(context.signal, () => this.#session.axFocus(this.ref));
298
+ }
299
+
300
+ async parent(): Promise<El | null> {
301
+ const { signal } = this.#getContext();
302
+ const node = await nativeCall(signal, () => this.#session.axParent(this.ref));
303
+ return node ? new El(this.#session, this.#getContext, node) : null;
304
+ }
305
+
306
+ async children(): Promise<El[]> {
307
+ const { signal } = this.#getContext();
308
+ return (await nativeCall(signal, () => this.#session.axChildren(this.ref))).map(
309
+ node => new El(this.#session, this.#getContext, node),
310
+ );
311
+ }
312
+ }
313
+
314
+ class Win {
315
+ readonly id: string;
316
+ readonly app: string;
317
+ readonly title: string;
318
+ readonly pid?: number;
319
+ readonly bounds: { x: number; y: number; width: number; height: number };
320
+ readonly focused: boolean;
321
+ readonly #session: NativeDesktopSession;
322
+ readonly #getContext: RunContextAccessor;
323
+
324
+ constructor(session: NativeDesktopSession, getContext: RunContextAccessor, window: DesktopWindow) {
325
+ this.#session = session;
326
+ this.#getContext = getContext;
327
+ this.id = window.id;
328
+ this.app = window.app;
329
+ this.title = window.title;
330
+ this.pid = window.pid;
331
+ this.bounds = { x: window.x, y: window.y, width: window.width, height: window.height };
332
+ this.focused = window.focused;
333
+ }
334
+
335
+ screenshot(options?: ScreenshotOptions): Promise<{ path: string; width: number; height: number }> {
336
+ return captureScreenshot(this.#session, this.#getContext, this.id, options);
337
+ }
338
+
339
+ async click(x: number, y: number, options?: ClickOptions): Promise<void> {
340
+ const context = this.#getContext();
341
+ guardRun(context, "click");
342
+ await nativeCall(context.signal, () => this.#session.click(this.id, x, y, pointerOptions(options)));
343
+ }
344
+
345
+ async doubleClick(x: number, y: number, options?: Omit<ClickOptions, "count">): Promise<void> {
346
+ const context = this.#getContext();
347
+ guardRun(context, "doubleClick");
348
+ await nativeCall(context.signal, () =>
349
+ this.#session.click(this.id, x, y, pointerOptions({ ...options, count: 2 })),
350
+ );
351
+ }
352
+
353
+ async move(x: number, y: number): Promise<void> {
354
+ const context = this.#getContext();
355
+ guardRun(context, "move");
356
+ await nativeCall(context.signal, () => this.#session.moveMouse(this.id, x, y));
357
+ }
358
+
359
+ async drag(points: Array<[number, number]>, options?: DragOptions): Promise<void> {
360
+ const context = this.#getContext();
361
+ guardRun(context, "drag");
362
+ await nativeCall(context.signal, () =>
363
+ this.#session.drag(
364
+ this.id,
365
+ points.map(([x, y]) => ({ x, y })),
366
+ pointerOptions(options),
367
+ ),
368
+ );
369
+ }
370
+
371
+ async scroll(x: number, y: number, options: ScrollOptions = {}): Promise<void> {
372
+ const context = this.#getContext();
373
+ guardRun(context, "scroll");
374
+ await nativeCall(context.signal, () =>
375
+ this.#session.scroll(this.id, x, y, options.dx ?? 0, options.dy ?? 0, pointerOptions(options)),
376
+ );
377
+ }
378
+
379
+ async type(text: string, options?: DeliveryOptions): Promise<void> {
380
+ const context = this.#getContext();
381
+ guardRun(context, "type");
382
+ await nativeCall(context.signal, () => this.#session.typeText(this.id, text, pointerOptions(options)));
383
+ }
384
+
385
+ async press(chord: string | string[], options?: DeliveryOptions): Promise<void> {
386
+ const context = this.#getContext();
387
+ guardRun(context, "press");
388
+ await nativeCall(context.signal, () =>
389
+ this.#session.keyChord(this.id, chordKeys(chord), pointerOptions(options)),
390
+ );
391
+ }
392
+
393
+ async raise(): Promise<void> {
394
+ const context = this.#getContext();
395
+ guardRun(context, "raise");
396
+ await nativeCall(context.signal, () => this.#session.raiseWindow(this.id));
397
+ }
398
+
399
+ async ax(options?: AxOptions): Promise<string> {
400
+ const { signal } = this.#getContext();
401
+ return (await nativeCall(signal, () => this.#session.axSnapshot(this.id, options))).text;
402
+ }
403
+
404
+ async find(query: AxQuery): Promise<El[]> {
405
+ const { signal } = this.#getContext();
406
+ return (await nativeCall(signal, () => this.#session.axQuery(this.id, query))).map(
407
+ node => new El(this.#session, this.#getContext, node),
408
+ );
409
+ }
410
+
411
+ async ref(ref: string): Promise<El> {
412
+ const { signal } = this.#getContext();
413
+ return new El(this.#session, this.#getContext, await nativeCall(signal, () => this.#session.axNode(ref)));
414
+ }
32
415
  }
33
416
 
417
+ /** Hosts the persistent JavaScript runtime and native desktop session. */
34
418
  export class ComputerWorkerCore {
419
+ readonly #transport: ComputerWorkerTransport;
420
+ readonly #createSession: NativeDesktopSessionFactory;
421
+ readonly #unsubscribe: () => void;
35
422
  #session?: NativeDesktopSession;
36
- #hasReturnedFrame = false;
423
+ #runtime?: JsRuntime;
424
+ #active: ActiveRun | null = null;
425
+ /**
426
+ * Per-run context, carried through AsyncLocalStorage so async work leaked
427
+ * from an ended run (timers, dangling promises) keeps that run's aborted
428
+ * context instead of borrowing the next run's signal and read-only policy.
429
+ */
430
+ readonly #runContexts = new AsyncLocalStorage<ComputerRunContext>();
37
431
  #closed = false;
38
- #tail: Promise<void> = Promise.resolve();
39
- readonly #unsubscribe: () => void;
40
432
 
41
- constructor(
42
- private readonly transport: ComputerWorkerTransport,
43
- private readonly createSession: NativeDesktopSessionFactory = createDesktopSession,
44
- ) {
45
- this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
433
+ constructor(transport: ComputerWorkerTransport, createSession: NativeDesktopSessionFactory = createDesktopSession) {
434
+ this.#transport = transport;
435
+ this.#createSession = createSession;
436
+ this.#unsubscribe = transport.onMessage(message => this.handle(message));
437
+ this.#transport.send({ type: "ready" });
46
438
  }
47
439
 
48
- #onMessage(message: ComputerWorkerInbound): void {
49
- if (message.type === "ping") {
50
- this.transport.send({ type: "pong", id: message.id });
51
- return;
52
- }
53
- if (message.type === "close") {
54
- this.#tail = this.#tail.then(() => this.#close());
55
- return;
56
- }
57
- if (message.type === "init") {
58
- this.#tail = this.#tail.then(() => this.#init(message.options));
59
- return;
440
+ /** Routes one supervisor command into the persistent worker state. */
441
+ handle(message: ComputerWorkerInbound): void {
442
+ switch (message.type) {
443
+ case "ping":
444
+ this.#transport.send({ type: "pong", id: message.id });
445
+ return;
446
+ case "run":
447
+ void this.#run(message);
448
+ return;
449
+ case "abort":
450
+ if (this.#active?.id === message.id) this.#active.ac.abort(new ToolAbortError());
451
+ return;
452
+ case "tool-reply":
453
+ this.#deliverToolReply(message.id, message.reply);
454
+ return;
455
+ case "close":
456
+ void this.#close();
60
457
  }
61
- this.#tail = this.#tail.then(() => this.#execute(message.id, message.actions));
62
458
  }
63
459
 
64
- async #init(options: DesktopSessionOptions): Promise<void> {
65
- if (this.#closed) return;
66
- if (this.#session) {
67
- this.transport.send({
68
- type: "error",
69
- error: { name: "Error", message: "Computer worker already initialized" },
70
- });
71
- return;
72
- }
460
+ #ensureSession(snapshot: ComputerSessionSnapshot): NativeDesktopSession {
461
+ if (this.#session) return this.#session;
73
462
  try {
74
- this.#session = this.createSession(options);
75
- this.#hasReturnedFrame = false;
76
- this.transport.send({ type: "ready", capabilities: this.#session.capabilities });
463
+ this.#session = this.#createSession({ display: snapshot.display });
464
+ return this.#session;
77
465
  } catch (error) {
78
- this.transport.send({ type: "error", error: serializeError(error) });
466
+ throw nativeError(error);
79
467
  }
80
468
  }
81
469
 
82
- async #execute(id: string, actions: DesktopAction[]): Promise<void> {
83
- const session = this.#session;
84
- if (!session) {
85
- this.transport.send({
86
- type: "error",
87
- id,
88
- error: { name: "Error", message: "Computer worker is not initialized" },
470
+ #ensureRuntime(snapshot: ComputerSessionSnapshot): JsRuntime {
471
+ if (this.#runtime) return this.#runtime;
472
+ this.#runtime = new JsRuntime({ initialCwd: snapshot.cwd, sessionId: snapshot.sessionId });
473
+ return this.#runtime;
474
+ }
475
+
476
+ async #run(message: Extract<ComputerWorkerInbound, { type: "run" }>): Promise<void> {
477
+ if (this.#closed) {
478
+ this.#transport.send({
479
+ type: "result",
480
+ id: message.id,
481
+ ok: false,
482
+ error: errorPayload(new ToolError("Computer worker is closed")),
89
483
  });
90
484
  return;
91
485
  }
486
+ if (this.#active) {
487
+ this.#transport.send({
488
+ type: "result",
489
+ id: message.id,
490
+ ok: false,
491
+ error: errorPayload(new ToolError("Computer worker is busy")),
492
+ });
493
+ return;
494
+ }
495
+ const timeoutSignal = AbortSignal.timeout(message.timeoutMs);
496
+ const ac = new AbortController();
497
+ const runAc = new AbortController();
498
+ const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
499
+ const active: ActiveRun = { id: message.id, ac, signal, pendingTools: new Map() };
500
+ this.#active = active;
501
+ const output = new RunOutput();
502
+ const screenshots: ComputerScreenshot[] = [];
503
+ const runContext: ComputerRunContext = {
504
+ signal,
505
+ readOnly: message.session.readOnly,
506
+ snapshot: message.session,
507
+ output,
508
+ screenshots,
509
+ };
510
+ let returnValue: unknown;
511
+ let failure: { error: unknown } | undefined;
512
+ let completed = false;
92
513
  try {
93
- if (!this.#hasReturnedFrame && actions.some(action => COORDINATE_ACTIONS.has(action.type))) {
94
- this.transport.send({
95
- type: "error",
96
- id,
97
- error: {
98
- name: "Error",
99
- message:
100
- "Coordinate computer actions require a screenshot returned to the provider; request a screenshot first",
101
- },
514
+ throwIfAborted(signal);
515
+ const session = this.#ensureSession(message.session);
516
+ const runtime = this.#ensureRuntime(message.session);
517
+ runtime.setCwd(message.session.cwd);
518
+ const desktop = this.#createDesktopScope(session);
519
+ runtime.setRunScope({
520
+ desktop: bindRunFacade(desktop, signal),
521
+ assert: (condition: unknown, text?: string): void => {
522
+ if (!condition) throw new ToolError(text ?? "Assertion failed");
523
+ },
524
+ wait: (msOrPredicate: number | (() => unknown), options?: WaitPredicateOptions): Promise<unknown> => {
525
+ const resolved =
526
+ typeof msOrPredicate === "number"
527
+ ? undefined
528
+ : {
529
+ timeout: resolvePredicateTimeout(message.timeoutMs, options?.timeout),
530
+ interval: options?.interval,
531
+ };
532
+ return markHandled(waitForRun(msOrPredicate, signal, resolved));
533
+ },
534
+ });
535
+ const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
536
+ const onCancel = (): void => {
537
+ const abortError =
538
+ signal.reason instanceof ToolAbortError
539
+ ? signal.reason
540
+ : new ToolAbortError(undefined, { cause: signal.reason });
541
+ rejectCancel(
542
+ timeoutSignal.aborted
543
+ ? new ToolError(`Computer code execution timed out after ${message.timeoutMs}ms`)
544
+ : abortError,
545
+ );
546
+ const toolAbort = timeoutSignal.aborted
547
+ ? postmortem.markExpectedCleanupError(new ToolAbortError(undefined, { cause: timeoutSignal.reason }))
548
+ : abortError;
549
+ for (const pending of active.pendingTools.values()) pending.reject(toolAbort);
550
+ active.pendingTools.clear();
551
+ };
552
+ if (signal.aborted) onCancel();
553
+ else signal.addEventListener("abort", onCancel, { once: true });
554
+ try {
555
+ returnValue = await Promise.race([
556
+ this.#runContexts.run(runContext, () =>
557
+ runtime.run(message.code, `computer-run-${message.id}.js`, this.#runtimeHooks(active, output), {
558
+ runId: message.id,
559
+ cwd: message.session.cwd,
560
+ }),
561
+ ),
562
+ cancelRejection,
563
+ ]);
564
+ completed = true;
565
+ } finally {
566
+ signal.removeEventListener("abort", onCancel);
567
+ }
568
+ } catch (error) {
569
+ failure = { error };
570
+ } finally {
571
+ runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Computer run ended")));
572
+ if (this.#active?.id === message.id) this.#active = null;
573
+ }
574
+ if (failure !== undefined) {
575
+ this.#transport.send({ type: "result", id: message.id, ok: false, error: errorPayload(failure.error) });
576
+ return;
577
+ }
578
+ if (completed) {
579
+ let capabilities: DesktopCapabilities;
580
+ try {
581
+ capabilities = this.#ensureSession(message.session).capabilities;
582
+ } catch (error) {
583
+ this.#transport.send({
584
+ type: "result",
585
+ id: message.id,
586
+ ok: false,
587
+ error: errorPayload(nativeError(error)),
102
588
  });
103
589
  return;
104
590
  }
105
- const capture = await session.execute(actions);
106
- this.transport.send(
107
- { type: "result", id, capture, capabilities: session.capabilities },
108
- captureTransfer(capture),
109
- );
110
- this.#hasReturnedFrame = true;
111
- } catch (error) {
112
- this.transport.send({ type: "error", id, error: serializeError(error) });
591
+ this.#transport.send({
592
+ type: "result",
593
+ id: message.id,
594
+ ok: true,
595
+ payload: { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots, capabilities },
596
+ });
113
597
  }
114
598
  }
115
599
 
600
+ #runtimeHooks(active: ActiveRun, output: RunOutput): RuntimeHooks {
601
+ return {
602
+ onText: chunk => {
603
+ throwIfAborted(active.signal);
604
+ output.pushText(chunk);
605
+ },
606
+ onDisplay: display => {
607
+ throwIfAborted(active.signal);
608
+ output.pushDisplay(display);
609
+ },
610
+ callTool: (name, args) => {
611
+ throwIfAborted(active.signal);
612
+ return this.#callTool(active, name, args);
613
+ },
614
+ };
615
+ }
616
+
617
+ async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
618
+ const id = `computer-tc-${active.id}-${crypto.randomUUID()}`;
619
+ const { promise, resolve, reject } = Promise.withResolvers<unknown>();
620
+ active.pendingTools.set(id, { resolve, reject });
621
+ this.#transport.send({ type: "tool-call", id, runId: active.id, name, args });
622
+ return await promise;
623
+ }
624
+
625
+ #deliverToolReply(id: string, reply: ToolReply): void {
626
+ const pending = this.#active?.pendingTools.get(id);
627
+ if (!pending) return;
628
+ this.#active?.pendingTools.delete(id);
629
+ if (reply.ok) pending.resolve(reply.value);
630
+ else pending.reject(replyError(reply.error));
631
+ }
632
+
633
+ #currentRunContext = (): ComputerRunContext => {
634
+ const context = this.#runContexts.getStore();
635
+ if (!context) throw new ToolError("no active computer run");
636
+ return context;
637
+ };
638
+
639
+ #createDesktopScope(session: NativeDesktopSession): object {
640
+ const getContext = this.#currentRunContext;
641
+ const makeWin = (window: DesktopWindow): Win => new Win(session, getContext, window);
642
+ const el = (node: AxNode): El => new El(session, getContext, node);
643
+ const desktopTarget = new Win(session, getContext, {
644
+ id: "desktop",
645
+ app: "desktop",
646
+ title: "desktop",
647
+ x: 0,
648
+ y: 0,
649
+ width: 0,
650
+ height: 0,
651
+ focused: false,
652
+ });
653
+ return {
654
+ capabilities: (): DesktopCapabilities => {
655
+ const { signal } = getContext();
656
+ throwIfAborted(signal);
657
+ try {
658
+ return session.capabilities;
659
+ } catch (error) {
660
+ throw nativeError(error);
661
+ }
662
+ },
663
+ displays: async (): Promise<DesktopDisplay[]> => {
664
+ const { signal } = getContext();
665
+ return await nativeCall(signal, () => session.listDisplays());
666
+ },
667
+ windows: async (filter?: WindowFilter): Promise<DesktopWindow[]> => {
668
+ const { signal } = getContext();
669
+ return (await nativeCall(signal, () => session.listWindows())).filter(window =>
670
+ matchesFilter(window, filter),
671
+ );
672
+ },
673
+ window: async (selector: string | WindowFilter): Promise<Win> => {
674
+ const { signal } = getContext();
675
+ const windows = await nativeCall(signal, () => session.listWindows());
676
+ const matches =
677
+ typeof selector === "string"
678
+ ? windows.filter(window => window.id === selector)
679
+ : windows.filter(window => matchesFilter(window, selector));
680
+ if (matches.length === 0) throw new ToolError(`no window matches ${JSON.stringify(selector)}`);
681
+ if (matches.length > 1) {
682
+ const candidates = matches
683
+ .map(window => `${window.id} ${window.app} ${JSON.stringify(window.title)}`)
684
+ .join("\n");
685
+ throw new ToolError(`multiple windows match ${JSON.stringify(selector)}:\n${candidates}`);
686
+ }
687
+ return makeWin(matches[0]!);
688
+ },
689
+ focusedWindow: async (): Promise<Win | null> => {
690
+ const { signal } = getContext();
691
+ const window = (await nativeCall(signal, () => session.listWindows())).find(candidate => candidate.focused);
692
+ return window ? makeWin(window) : null;
693
+ },
694
+ screenshot: (options?: ScreenshotOptions) => captureScreenshot(session, getContext, "desktop", options),
695
+ click: desktopTarget.click.bind(desktopTarget),
696
+ doubleClick: desktopTarget.doubleClick.bind(desktopTarget),
697
+ move: desktopTarget.move.bind(desktopTarget),
698
+ drag: desktopTarget.drag.bind(desktopTarget),
699
+ scroll: desktopTarget.scroll.bind(desktopTarget),
700
+ type: desktopTarget.type.bind(desktopTarget),
701
+ press: desktopTarget.press.bind(desktopTarget),
702
+ elementAt: async (x: number, y: number): Promise<El | null> => {
703
+ const { signal } = getContext();
704
+ const node = await nativeCall(signal, () => session.axElementAt("desktop", x, y));
705
+ return node ? el(node) : null;
706
+ },
707
+ focusedElement: async (): Promise<El | null> => {
708
+ const { signal } = getContext();
709
+ const node = await nativeCall(signal, () => session.axFocused());
710
+ return node ? el(node) : null;
711
+ },
712
+ clipboard: {
713
+ read: async (): Promise<string> => {
714
+ const { signal } = getContext();
715
+ throwIfAborted(signal);
716
+ const text = await readTextFromClipboard();
717
+ throwIfAborted(signal);
718
+ return text;
719
+ },
720
+ write: async (text: string): Promise<void> => {
721
+ const context = getContext();
722
+ guardRun(context, "clipboard.write");
723
+ await copyToClipboard(text);
724
+ throwIfAborted(context.signal);
725
+ },
726
+ },
727
+ };
728
+ }
729
+
116
730
  async #close(): Promise<void> {
117
731
  if (this.#closed) return;
118
732
  this.#closed = true;
733
+ this.#active?.ac.abort(new ToolAbortError());
119
734
  try {
120
735
  await this.#session?.close();
121
- } catch (error) {
122
- this.transport.send({ type: "error", error: serializeError(error) });
736
+ } catch {
737
+ // Closing is best-effort; the worker is exiting and has no request to report this against.
123
738
  } finally {
124
739
  this.#session = undefined;
125
- this.#hasReturnedFrame = false;
126
740
  this.#unsubscribe();
127
- this.transport.send({ type: "closed" });
128
- this.transport.close();
741
+ this.#transport.send({ type: "closed" });
742
+ this.#transport.close();
129
743
  }
130
744
  }
131
745
  }