@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.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 (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
  3. package/dist/cli.js +4438 -3962
  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/gc-cli.d.ts +1 -0
  9. package/dist/types/cli/help-extra.d.ts +2 -0
  10. package/dist/types/cli/thinking-levels.d.ts +5 -0
  11. package/dist/types/cli/update-cli.d.ts +13 -0
  12. package/dist/types/commands/browser-relay.d.ts +38 -0
  13. package/dist/types/commands/commit.d.ts +3 -0
  14. package/dist/types/commands/complete.d.ts +1 -1
  15. package/dist/types/commands/completions.d.ts +6 -0
  16. package/dist/types/commands/grep.d.ts +3 -0
  17. package/dist/types/commands/join.d.ts +4 -0
  18. package/dist/types/commands/launch-help.d.ts +171 -0
  19. package/dist/types/commands/launch.d.ts +5 -1
  20. package/dist/types/commands/models.d.ts +3 -0
  21. package/dist/types/commands/say.d.ts +10 -0
  22. package/dist/types/commands/worktree.d.ts +3 -0
  23. package/dist/types/config/keybindings.d.ts +5 -0
  24. package/dist/types/config/model-discovery.d.ts +2 -2
  25. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  26. package/dist/types/config/models-config-schema.d.ts +2 -0
  27. package/dist/types/config/models-config.d.ts +1 -0
  28. package/dist/types/config/service-tier.d.ts +2 -0
  29. package/dist/types/config/settings-schema.d.ts +69 -21
  30. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  31. package/dist/types/cursor.d.ts +7 -4
  32. package/dist/types/edit/index.d.ts +3 -3
  33. package/dist/types/edit/modes/replace.d.ts +22 -15
  34. package/dist/types/edit/renderer.d.ts +1 -1
  35. package/dist/types/eval/py/kernel.d.ts +3 -1
  36. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  37. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  38. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  40. package/dist/types/launch/broker.d.ts +1 -1
  41. package/dist/types/launch/client.d.ts +15 -4
  42. package/dist/types/launch/ensure.d.ts +8 -0
  43. package/dist/types/launch/paths.d.ts +1 -1
  44. package/dist/types/launch/protocol.d.ts +19 -2
  45. package/dist/types/lsp/client.d.ts +2 -0
  46. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  47. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  48. package/dist/types/lsp/mux/server.d.ts +18 -0
  49. package/dist/types/lsp/types.d.ts +22 -2
  50. package/dist/types/main.d.ts +2 -0
  51. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  52. package/dist/types/mnemopi/state.d.ts +0 -23
  53. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  54. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  56. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  57. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  58. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  59. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  60. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  62. package/dist/types/modes/interactive-mode.d.ts +5 -1
  63. package/dist/types/modes/types.d.ts +4 -1
  64. package/dist/types/modes/workflow.d.ts +2 -1
  65. package/dist/types/sdk.d.ts +3 -1
  66. package/dist/types/secrets/index.d.ts +1 -1
  67. package/dist/types/security/contracts/schemas.d.ts +405 -403
  68. package/dist/types/session/agent-session-types.d.ts +8 -1
  69. package/dist/types/session/agent-session.d.ts +26 -2
  70. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  71. package/dist/types/session/launch-completion.d.ts +10 -0
  72. package/dist/types/session/session-advisors.d.ts +1 -1
  73. package/dist/types/session/session-context.d.ts +2 -1
  74. package/dist/types/session/session-entries.d.ts +15 -1
  75. package/dist/types/session/session-maintenance.d.ts +1 -0
  76. package/dist/types/session/session-manager.d.ts +23 -11
  77. package/dist/types/session/session-stats.d.ts +10 -0
  78. package/dist/types/session/session-storage.d.ts +15 -3
  79. package/dist/types/session/yield-queue.d.ts +6 -1
  80. package/dist/types/system-prompt.d.ts +4 -1
  81. package/dist/types/task/index.d.ts +2 -1
  82. package/dist/types/task/spawn-policy.d.ts +5 -0
  83. package/dist/types/thinking.d.ts +1 -7
  84. package/dist/types/tools/ast-grep.d.ts +1 -1
  85. package/dist/types/tools/browser/registry.d.ts +2 -1
  86. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  87. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  88. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  89. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  90. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  91. package/dist/types/tools/browser/render.d.ts +1 -0
  92. package/dist/types/tools/browser.d.ts +3 -0
  93. package/dist/types/tools/computer/exposure.d.ts +9 -7
  94. package/dist/types/tools/computer/protocol.d.ts +70 -17
  95. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  96. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  97. package/dist/types/tools/computer/worker.d.ts +42 -7
  98. package/dist/types/tools/computer-renderer.d.ts +7 -5
  99. package/dist/types/tools/computer.d.ts +25 -44
  100. package/dist/types/tools/context.d.ts +3 -0
  101. package/dist/types/tools/glob.d.ts +1 -1
  102. package/dist/types/tools/grep.d.ts +1 -1
  103. package/dist/types/tools/index.d.ts +9 -0
  104. package/dist/types/tools/render-utils.d.ts +2 -0
  105. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  106. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  107. package/dist/types/tools/xdev.d.ts +8 -0
  108. package/dist/types/utils/clipboard.d.ts +2 -2
  109. package/dist/types/web/kagi.d.ts +1 -0
  110. package/dist/types/web/search/index.d.ts +1 -1
  111. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  112. package/dist/types/web/search/providers/base.d.ts +2 -0
  113. package/dist/types/web/search/providers/brave.d.ts +1 -0
  114. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  115. package/dist/types/web/search/providers/codex.d.ts +1 -0
  116. package/dist/types/web/search/providers/exa.d.ts +1 -0
  117. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  118. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  119. package/dist/types/web/search/providers/jina.d.ts +1 -0
  120. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  121. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  122. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  123. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  124. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  125. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  126. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  127. package/dist/types/web/search/providers/utils.d.ts +6 -9
  128. package/dist/types/web/search/providers/zai.d.ts +1 -0
  129. package/dist/types/web/search/types.d.ts +4 -0
  130. package/package.json +12 -12
  131. package/src/cli/args.ts +7 -87
  132. package/src/cli/browser-relay-cli.ts +119 -0
  133. package/src/cli/command-help.ts +103 -0
  134. package/src/cli/completion-gen.ts +5 -5
  135. package/src/cli/flag-tables.ts +9 -0
  136. package/src/cli/gc-cli.ts +641 -21
  137. package/src/cli/help-extra.ts +89 -0
  138. package/src/cli/setup-cli.ts +13 -21
  139. package/src/cli/thinking-levels.ts +7 -0
  140. package/src/cli/update-cli.ts +26 -7
  141. package/src/cli-commands.ts +170 -33
  142. package/src/cli.ts +17 -5
  143. package/src/commands/acp.ts +3 -1
  144. package/src/commands/agents.ts +3 -2
  145. package/src/commands/auth-broker.ts +3 -2
  146. package/src/commands/auth-gateway.ts +3 -2
  147. package/src/commands/bench.ts +2 -3
  148. package/src/commands/browser-relay.ts +53 -0
  149. package/src/commands/cleanse.ts +2 -2
  150. package/src/commands/commit.ts +3 -2
  151. package/src/commands/complete.ts +2 -1
  152. package/src/commands/completions.ts +3 -2
  153. package/src/commands/config.ts +3 -2
  154. package/src/commands/dry-balance.ts +2 -2
  155. package/src/commands/gallery.ts +3 -2
  156. package/src/commands/gc.ts +3 -2
  157. package/src/commands/grep.ts +3 -2
  158. package/src/commands/grievances.ts +3 -2
  159. package/src/commands/install.ts +2 -2
  160. package/src/commands/join.ts +3 -2
  161. package/src/commands/launch-help.ts +116 -0
  162. package/src/commands/launch.ts +7 -191
  163. package/src/commands/models.ts +3 -2
  164. package/src/commands/plugin.ts +3 -2
  165. package/src/commands/read.ts +3 -2
  166. package/src/commands/say.ts +3 -2
  167. package/src/commands/setup.ts +3 -2
  168. package/src/commands/shell.ts +3 -2
  169. package/src/commands/ssh.ts +3 -2
  170. package/src/commands/stats.ts +3 -2
  171. package/src/commands/tiny-models.ts +2 -2
  172. package/src/commands/token.ts +2 -2
  173. package/src/commands/ttsr.ts +2 -2
  174. package/src/commands/update.ts +3 -2
  175. package/src/commands/usage.ts +3 -2
  176. package/src/commands/web-search.ts +3 -2
  177. package/src/commands/worktree.ts +3 -2
  178. package/src/config/keybindings.ts +5 -0
  179. package/src/config/model-discovery.ts +44 -27
  180. package/src/config/model-registry.ts +19 -13
  181. package/src/config/models-config-schema-bundle.ts +9 -0
  182. package/src/config/resolve-config-value.ts +2 -1
  183. package/src/config/service-tier.ts +5 -0
  184. package/src/config/settings-schema.ts +72 -20
  185. package/src/config/settings.ts +1 -1
  186. package/src/cursor-bridge-tools.ts +1 -1
  187. package/src/cursor.ts +10 -14
  188. package/src/edit/diff.ts +17 -8
  189. package/src/edit/index.ts +20 -7
  190. package/src/edit/modes/replace.ts +93 -40
  191. package/src/edit/renderer.ts +1 -1
  192. package/src/edit/streaming.ts +14 -17
  193. package/src/eval/py/kernel.ts +2 -1
  194. package/src/eval/py/spawn-options.ts +24 -29
  195. package/src/export/html/index.ts +10 -3
  196. package/src/export/html/template.js +8 -26
  197. package/src/export/share.ts +4 -0
  198. package/src/extensibility/extensions/wrapper.ts +1 -0
  199. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  200. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  201. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  202. package/src/extensibility/skills.ts +11 -0
  203. package/src/extensibility/utils.ts +10 -2
  204. package/src/launch/broker.ts +223 -7
  205. package/src/launch/client.ts +192 -21
  206. package/src/launch/ensure.ts +73 -0
  207. package/src/launch/paths.ts +1 -1
  208. package/src/launch/protocol.ts +54 -2
  209. package/src/lsp/client.ts +23 -2
  210. package/src/lsp/index.ts +8 -0
  211. package/src/lsp/mux/daemon.ts +348 -0
  212. package/src/lsp/mux/protocol.ts +96 -0
  213. package/src/lsp/mux/server.ts +797 -0
  214. package/src/lsp/types.ts +28 -2
  215. package/src/main.ts +20 -8
  216. package/src/mcp/config-writer.ts +1 -1
  217. package/src/mcp/oauth-flow.ts +11 -1
  218. package/src/mnemopi/embed-client.ts +49 -2
  219. package/src/mnemopi/state.ts +57 -23
  220. package/src/modes/components/assistant-message.ts +11 -0
  221. package/src/modes/components/btw-panel.ts +41 -4
  222. package/src/modes/components/chat-transcript-builder.ts +4 -0
  223. package/src/modes/components/custom-editor.ts +9 -0
  224. package/src/modes/components/read-tool-group.ts +10 -0
  225. package/src/modes/components/status-line/component.ts +1 -1
  226. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  227. package/src/modes/components/tool-execution.ts +8 -0
  228. package/src/modes/components/tree-selector.ts +46 -48
  229. package/src/modes/controllers/btw-controller.ts +55 -7
  230. package/src/modes/controllers/command-controller.ts +31 -0
  231. package/src/modes/controllers/event-controller.ts +17 -0
  232. package/src/modes/controllers/input-controller.ts +63 -8
  233. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  234. package/src/modes/controllers/selector-controller.ts +20 -0
  235. package/src/modes/interactive-mode.ts +18 -2
  236. package/src/modes/types.ts +9 -1
  237. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  238. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  239. package/src/modes/utils/ui-helpers.ts +6 -10
  240. package/src/modes/workflow.ts +8 -2
  241. package/src/prompts/agents/init.md +1 -1
  242. package/src/prompts/session/launch-completion.md +1 -0
  243. package/src/prompts/system/plan-mode-active.md +2 -2
  244. package/src/prompts/system/system-prompt.md +26 -52
  245. package/src/prompts/system/workflow-notice.md +3 -3
  246. package/src/prompts/tools/ast-grep.md +1 -1
  247. package/src/prompts/tools/bash.md +1 -1
  248. package/src/prompts/tools/browser.md +1 -0
  249. package/src/prompts/tools/computer.md +21 -21
  250. package/src/prompts/tools/glob.md +1 -1
  251. package/src/prompts/tools/grep.md +1 -1
  252. package/src/prompts/tools/replace.md +5 -4
  253. package/src/prompts/tools/task.md +4 -4
  254. package/src/sdk.ts +49 -6
  255. package/src/secrets/index.ts +1 -1
  256. package/src/security/contracts/schemas.ts +205 -183
  257. package/src/security/contracts/validation.ts +5 -1
  258. package/src/security/store.ts +2 -2
  259. package/src/session/agent-session-types.ts +9 -1
  260. package/src/session/agent-session.ts +204 -15
  261. package/src/session/indexed-session-storage.ts +9 -0
  262. package/src/session/launch-completion.ts +37 -0
  263. package/src/session/prewalk.ts +24 -2
  264. package/src/session/session-advisors.ts +2 -2
  265. package/src/session/session-context.ts +42 -12
  266. package/src/session/session-dump-format.ts +1 -1
  267. package/src/session/session-entries.ts +17 -1
  268. package/src/session/session-maintenance.ts +38 -4
  269. package/src/session/session-manager.ts +123 -67
  270. package/src/session/session-paths.ts +118 -55
  271. package/src/session/session-stats.ts +48 -3
  272. package/src/session/session-storage.ts +30 -22
  273. package/src/session/stream-guards.ts +1 -1
  274. package/src/session/yield-queue.ts +121 -16
  275. package/src/slash-commands/builtin-registry.ts +37 -24
  276. package/src/system-prompt.ts +8 -3
  277. package/src/task/index.ts +23 -8
  278. package/src/task/spawn-policy.ts +14 -0
  279. package/src/thinking.ts +2 -8
  280. package/src/tools/ast-grep.ts +10 -4
  281. package/src/tools/browser/cmux/cmux-tab.ts +97 -14
  282. package/src/tools/browser/cmux/rpc.ts +2 -8
  283. package/src/tools/browser/registry.ts +53 -2
  284. package/src/tools/browser/relay/bridge.ts +945 -0
  285. package/src/tools/browser/relay/daemon.ts +117 -0
  286. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  287. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  288. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  289. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  290. package/src/tools/browser/relay/kind.ts +41 -0
  291. package/src/tools/browser/relay/protocol.ts +54 -0
  292. package/src/tools/browser/relay/server.ts +141 -0
  293. package/src/tools/browser/render.ts +4 -1
  294. package/src/tools/browser/shared-daemon.ts +7 -58
  295. package/src/tools/browser/tab-supervisor.ts +5 -4
  296. package/src/tools/browser/tab-worker.ts +33 -12
  297. package/src/tools/browser.ts +26 -1
  298. package/src/tools/computer/exposure.ts +11 -35
  299. package/src/tools/computer/protocol.ts +51 -10
  300. package/src/tools/computer/supervisor.ts +227 -124
  301. package/src/tools/computer/worker-entry.ts +11 -6
  302. package/src/tools/computer/worker.ts +695 -81
  303. package/src/tools/computer-renderer.ts +104 -65
  304. package/src/tools/computer.ts +134 -428
  305. package/src/tools/context.ts +3 -0
  306. package/src/tools/glob.ts +9 -2
  307. package/src/tools/grep.ts +12 -6
  308. package/src/tools/hub/launch.ts +122 -6
  309. package/src/tools/index.ts +9 -0
  310. package/src/tools/inspect-image.ts +32 -6
  311. package/src/tools/render-utils.ts +2 -0
  312. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  313. package/src/tools/tool-timeouts.ts +1 -0
  314. package/src/tools/xdev.ts +29 -2
  315. package/src/utils/clipboard.ts +40 -14
  316. package/src/utils/command-args.ts +26 -28
  317. package/src/utils/tool-choice.ts +0 -13
  318. package/src/web/kagi.ts +2 -1
  319. package/src/web/search/index.ts +18 -2
  320. package/src/web/search/providers/anthropic.ts +4 -1
  321. package/src/web/search/providers/base.ts +2 -0
  322. package/src/web/search/providers/brave.ts +3 -1
  323. package/src/web/search/providers/browser-page.ts +6 -4
  324. package/src/web/search/providers/codex.ts +4 -1
  325. package/src/web/search/providers/duckduckgo.ts +2 -1
  326. package/src/web/search/providers/ecosia.ts +2 -1
  327. package/src/web/search/providers/exa.ts +4 -2
  328. package/src/web/search/providers/firecrawl.ts +3 -1
  329. package/src/web/search/providers/gemini.ts +8 -2
  330. package/src/web/search/providers/google.ts +2 -1
  331. package/src/web/search/providers/jina.ts +12 -2
  332. package/src/web/search/providers/kagi.ts +3 -0
  333. package/src/web/search/providers/kimi.ts +5 -1
  334. package/src/web/search/providers/mojeek.ts +2 -1
  335. package/src/web/search/providers/parallel.ts +5 -1
  336. package/src/web/search/providers/perplexity.ts +6 -3
  337. package/src/web/search/providers/public.ts +1 -1
  338. package/src/web/search/providers/searxng.ts +11 -5
  339. package/src/web/search/providers/startpage.ts +10 -4
  340. package/src/web/search/providers/synthetic.ts +11 -5
  341. package/src/web/search/providers/tavily.ts +3 -1
  342. package/src/web/search/providers/tinyfish.ts +3 -1
  343. package/src/web/search/providers/utils.ts +12 -10
  344. package/src/web/search/providers/xai.ts +2 -2
  345. package/src/web/search/providers/zai.ts +18 -3
  346. package/src/web/search/types.ts +6 -0
  347. package/dist/types/config/file-lock.d.ts +0 -29
  348. package/src/config/file-lock.ts +0 -164
@@ -102,39 +102,42 @@ function formatFastModeStatus(session: AgentSession): string {
102
102
  }
103
103
 
104
104
  /** Detailed, session-effective `/computer status` diagnostics. */
105
- function formatComputerUseStatus(session: AgentSession): string {
105
+ async function formatComputerUseStatus(session: AgentSession): Promise<string> {
106
106
  const enabled = session.settings.get("computer.enabled");
107
107
  const active = session.getEnabledToolNames().includes("computer");
108
108
  const model = session.model;
109
109
  const modelName = model ? formatModelString(model) : "none";
110
- const exposure = !enabled || !active ? "not exposed" : computerExposureMode(model);
111
- const toolState = active ? "active" : enabled ? "unavailable" : "inactive";
110
+ const exposure = !enabled
111
+ ? "not exposed (disabled)"
112
+ : !active
113
+ ? "not exposed (tool inactive)"
114
+ : computerExposureMode(model);
112
115
  const configured = {
113
- backend: session.settings.get("computer.backend"),
114
116
  display: session.settings.get("computer.display"),
115
117
  maxWidth: session.settings.get("computer.maxWidth"),
116
118
  maxHeight: session.settings.get("computer.maxHeight"),
117
119
  };
118
- const computerTool = session.getToolByName("computer") as Pick<ComputerTool, "effectiveConfiguration"> | undefined;
119
- const effective = computerTool?.effectiveConfiguration ?? configured;
120
- const configurationChanged =
121
- effective.backend !== configured.backend ||
122
- effective.display !== configured.display ||
123
- effective.maxWidth !== configured.maxWidth ||
124
- effective.maxHeight !== configured.maxHeight;
120
+ const computerTool = active
121
+ ? (session.getToolByName("computer") as Pick<ComputerTool, "capabilities"> | undefined)
122
+ : undefined;
123
+ const capabilities = await computerTool?.capabilities();
124
+ const capabilityStatus = capabilities
125
+ ? [
126
+ `backend=${capabilities.backend}${capabilities.displayServer ? `/${capabilities.displayServer}` : ""}`,
127
+ `capture=${capabilities.capture} (${capabilities.capturePermission})`,
128
+ `input=${capabilities.input} (${capabilities.inputPermission})`,
129
+ `ax=${capabilities.ax} (${capabilities.axPermission})`,
130
+ `backgroundWindowInput=${capabilities.backgroundWindowInput}`,
131
+ `deliveryModes=${capabilities.deliveryModes.join(",") || "none"}`,
132
+ ].join(", ")
133
+ : "session not started";
125
134
  return [
126
135
  `Computer use: ${enabled ? "enabled" : "disabled"}`,
127
- `tool: ${toolState}`,
128
- `backend: ${effective.backend}`,
129
- `display: ${effective.display}`,
130
- `capture: ${effective.maxWidth}×${effective.maxHeight}`,
131
- ...(configurationChanged
132
- ? [
133
- `next-session settings: backend=${configured.backend}, display=${configured.display}, capture=${configured.maxWidth}×${configured.maxHeight}`,
134
- ]
135
- : []),
136
- `model: ${modelName}`,
136
+ `tool: ${active ? "active" : "inactive"}`,
137
137
  `exposure: ${exposure}`,
138
+ `model: ${modelName}`,
139
+ `configured: display=${configured.display}, maxWidth=${configured.maxWidth}, maxHeight=${configured.maxHeight}`,
140
+ `capabilities: ${capabilityStatus}`,
138
141
  ].join(" · ");
139
142
  }
140
143
 
@@ -151,7 +154,7 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
151
154
  }
152
155
  session.settings.override("computer.enabled", enable);
153
156
  return enable
154
- ? `Computer use enabled for this session. ${formatComputerUseStatus(session)}`
157
+ ? `Computer use enabled for this session. ${await formatComputerUseStatus(session)}`
155
158
  : "Computer use disabled for this session.";
156
159
  }
157
160
 
@@ -673,7 +676,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
673
676
  handle: async (command, runtime) => {
674
677
  const arg = command.args.trim().toLowerCase();
675
678
  if (arg === "status") {
676
- await runtime.output(formatComputerUseStatus(runtime.session));
679
+ await runtime.output(await formatComputerUseStatus(runtime.session));
677
680
  return commandConsumed();
678
681
  }
679
682
  if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
@@ -686,7 +689,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
686
689
  handleTui: async (command, runtime) => {
687
690
  const arg = command.args.trim().toLowerCase();
688
691
  if (arg === "status") {
689
- runtime.ctx.showStatus(formatComputerUseStatus(runtime.ctx.session));
692
+ runtime.ctx.showStatus(await formatComputerUseStatus(runtime.ctx.session));
690
693
  runtime.ctx.editor.setText("");
691
694
  return;
692
695
  }
@@ -1716,6 +1719,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1716
1719
  await runtime.ctx.handleFreshCommand();
1717
1720
  },
1718
1721
  },
1722
+ {
1723
+ name: "reset",
1724
+ description: "Reset the conversation context in place, keeping the session",
1725
+ getTuiAutocompleteDescription: runtime =>
1726
+ runtime.ctx.session.isStreaming ? "Reset: unavailable while streaming" : "Reset: drop context, keep session",
1727
+ handleTui: async (_command, runtime) => {
1728
+ runtime.ctx.editor.setText("");
1729
+ await runtime.ctx.handleResetContextCommand();
1730
+ },
1731
+ },
1719
1732
  {
1720
1733
  name: "drop",
1721
1734
  description: "Delete the current session and start a new one",
@@ -498,7 +498,8 @@ export interface BuildSystemPromptOptions {
498
498
  /**
499
499
  * Whether provider-native tool calling is active (no owned/in-band syntax).
500
500
  * When true and `inlineToolDescriptors` is false, the inventory renders as a
501
- * compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
501
+ * compact tool-name list; otherwise it renders the full Harmony-style
502
+ * `namespace functions { … }` catalog. Default: true
502
503
  */
503
504
  nativeTools?: boolean;
504
505
  /** Skills settings for discovery. */
@@ -525,6 +526,9 @@ export interface BuildSystemPromptOptions {
525
526
  taskMaxConcurrency?: number;
526
527
  /** Whether IRC-backed parallel coordination can be included in delegation policy. */
527
528
  taskIrcEnabled?: boolean;
529
+ /** Whether the read-only `scout` subagent is spawnable (not disabled, allowed by spawn policy). Defaults to true. */
530
+ scoutAvailable?: boolean;
531
+
528
532
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
529
533
  alwaysApplyRules?: AlwaysApplyRule[];
530
534
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
@@ -599,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
599
603
  taskIrcEnabled = false,
600
604
  secretsEnabled = false,
601
605
  workspaceTree: providedWorkspaceTree,
606
+ scoutAvailable = true,
602
607
  memoryRootEnabled = false,
603
608
  securityEnabled = false,
604
609
  model,
@@ -793,7 +798,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
793
798
 
794
799
  // List mode shows a compact tool-name list; it only applies when descriptors
795
800
  // stay in provider-native tool schemas AND native tool calling is active.
796
- // Otherwise render full `# Tool:` sections inline in the system prompt.
801
+ // Otherwise render the full functions-namespace catalog in the system prompt.
797
802
  const toolListMode = !inlineToolDescriptors && nativeTools;
798
803
  // Build tool descriptions for system prompt rendering.
799
804
  const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
@@ -826,7 +831,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
826
831
  examples: meta?.examples,
827
832
  };
828
833
  }),
829
- model ?? "",
830
834
  );
831
835
 
832
836
  // Filter skills for the rendered system prompt:
@@ -879,6 +883,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
879
883
  eagerTasksAlways,
880
884
  taskBatch,
881
885
  MAX_CONCURRENCY: normalizeConcurrencyLimit(taskMaxConcurrency),
886
+ scoutAvailable,
882
887
  taskIrcEnabled,
883
888
  secretsEnabled,
884
889
  hasMemoryRoot: memoryRootEnabled,
package/src/task/index.ts CHANGED
@@ -27,7 +27,7 @@ import { TASK_EFFORTS, type TaskEffort } from "../thinking";
27
27
  import { truncateForPrompt } from "../tools/approval";
28
28
  import { isIrcEnabled } from "../tools/hub";
29
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
30
- import { resolveSpawnPolicy } from "./spawn-policy";
30
+ import { isScoutSpawnable, resolveSpawnPolicy } from "./spawn-policy";
31
31
  import {
32
32
  type AgentDefinition,
33
33
  type AgentProgress,
@@ -188,8 +188,10 @@ function renderDescription(options: TaskDescriptionOptions): string {
188
188
  readOnly: isReadOnlyAgent(agent),
189
189
  blocking: agent.blocking === true,
190
190
  }));
191
+ const scoutAvailable = isScoutSpawnable(options.disabledAgents, options.parentSpawns);
191
192
  return prompt.render(taskDescriptionTemplate, {
192
193
  agents: renderedAgents,
194
+ scoutAvailable,
193
195
  spawningDisabled,
194
196
  defaultAgent: spawnPolicy.defaultAgent,
195
197
  isolationEnabled: options.isolationEnabled,
@@ -398,15 +400,19 @@ const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "sonic"]);
398
400
  * (DepthCapacity: it currently has the `task` tool). `agentNames` are the
399
401
  * per-item resolved agent types. Returns undefined when no nudge applies.
400
402
  */
401
- export function buildSpecializationAdvisory(agentNames: string[], depthCapacity: boolean): string | undefined {
403
+ export function buildSpecializationAdvisory(
404
+ agentNames: string[],
405
+ depthCapacity: boolean,
406
+ scoutAvailable = true,
407
+ ): string | undefined {
402
408
  if (!depthCapacity) return undefined;
403
409
  const generics = agentNames.filter(name => GENERIC_SPAWN_AGENTS.has(name));
404
410
  if (generics.length < 2) return undefined;
405
- return (
406
- `Tip: this call spawned ${generics.length} generic \`${generics[0]}\` workers. ` +
407
- `Check the agent list for a closer specialist type — e.g. read-only research belongs on ` +
408
- `\`agent: "scout"\`, which runs on a faster model.`
409
- );
411
+ const specialist = scoutAvailable
412
+ ? `Check the agent list for a closer specialist type — e.g. read-only research belongs on ` +
413
+ `\`agent: "scout"\`, which runs on a faster model.`
414
+ : `Check the agent list for a closer specialist type.`;
415
+ return `Tip: this call spawned ${generics.length} generic \`${generics[0]}\` workers. ${specialist}`;
410
416
  }
411
417
 
412
418
  /**
@@ -443,10 +449,11 @@ export function composeSpawnAdvisory(args: {
443
449
  depthCapacity: boolean;
444
450
  ircEnabled: boolean;
445
451
  willRunAsync: boolean;
452
+ scoutAvailable?: boolean;
446
453
  }): string | undefined {
447
454
  return (
448
455
  [
449
- buildSpecializationAdvisory(args.agents, args.depthCapacity),
456
+ buildSpecializationAdvisory(args.agents, args.depthCapacity, args.scoutAvailable),
450
457
  args.willRunAsync ? buildCoordinationAdvisory(args.items, args.depthCapacity, args.ircEnabled) : undefined,
451
458
  ]
452
459
  .filter(Boolean)
@@ -751,6 +758,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
751
758
  depthCapacity,
752
759
  ircEnabled,
753
760
  willRunAsync: false,
761
+ scoutAvailable: isScoutSpawnable(
762
+ this.session.settings.get("task.disabledAgents") as string[] | undefined,
763
+ this.session.getSessionSpawns?.() ?? "*",
764
+ ),
754
765
  });
755
766
  const result = await this.#executeSyncFanout(
756
767
  toolCallId,
@@ -784,6 +795,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
784
795
  depthCapacity,
785
796
  ircEnabled,
786
797
  willRunAsync: asyncItems.length > 0,
798
+ scoutAvailable: isScoutSpawnable(
799
+ this.session.settings.get("task.disabledAgents") as string[] | undefined,
800
+ this.session.getSessionSpawns?.() ?? "*",
801
+ ),
787
802
  });
788
803
  // Returns a fresh result (copied content array, copied text part) rather
789
804
  // than mutating the caller's — task results are short-lived here, but an
@@ -56,3 +56,17 @@ export function resolveSpawnPolicy(parentSpawns: string | boolean | null | undef
56
56
  allowedPromptText: allowedAgents.map(agent => `\`${agent}\``).join(", "),
57
57
  };
58
58
  }
59
+
60
+ /**
61
+ * Whether the `scout` agent is spawnable in a session: not disabled via
62
+ * `task.disabledAgents`, and permitted by the session spawn policy.
63
+ */
64
+ export function isScoutSpawnable(
65
+ disabledAgents: readonly string[] | undefined,
66
+ spawns: string | boolean | null | undefined,
67
+ ): boolean {
68
+ if (disabledAgents?.includes("scout")) return false;
69
+ const policy = resolveSpawnPolicy(spawns);
70
+ if (!policy.enabled) return false;
71
+ return policy.allowedAgents === null || policy.allowedAgents.includes("scout");
72
+ }
package/src/thinking.ts CHANGED
@@ -3,6 +3,8 @@ import { Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
3
3
  import { clampThinkingLevelForModel, getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
4
4
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
5
5
 
6
+ export { CLI_THINKING_LEVELS } from "./cli/thinking-levels";
7
+
6
8
  /**
7
9
  * Metadata used to render thinking selector values in the coding-agent UI.
8
10
  */
@@ -209,14 +211,6 @@ export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLeve
209
211
  return level === AUTO_THINKING ? AUTO_THINKING_METADATA : getThinkingLevelMetadata(level);
210
212
  }
211
213
 
212
- /**
213
- * Thinking selectors accepted by the `--thinking` CLI flag, in display order:
214
- * `off`, every concrete effort (`minimal`..`max`), then `auto`. Single source
215
- * for the flag's `options` list, shell completions, and the "invalid level"
216
- * warning so all three stay in sync.
217
- */
218
- export const CLI_THINKING_LEVELS: readonly string[] = [ThinkingLevel.Off, ...THINKING_EFFORTS, AUTO_THINKING];
219
-
220
214
  /**
221
215
  * Parses a `--thinking` CLI value. Accepts every {@link parseConfiguredThinkingLevel}
222
216
  * selector (`off`, `auto`, `minimal`..`max`) but rejects
@@ -11,6 +11,7 @@ import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapsh
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import type { Theme } from "../modes/theme/theme";
13
13
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
14
+ import { isScoutSpawnable } from "../task/spawn-policy";
14
15
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
15
16
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
16
17
  import type { ToolSession } from ".";
@@ -150,7 +151,14 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
150
151
  readonly approval = "read" as const;
151
152
  readonly label = "AST Grep";
152
153
  readonly summary = "Search code with AST patterns (structural grep)";
153
- readonly description: string;
154
+ get description(): string {
155
+ return prompt.render(astGrepDescription, {
156
+ scoutAvailable: isScoutSpawnable(
157
+ this.session.settings.get("task.disabledAgents") as string[] | undefined,
158
+ this.session.getSessionSpawns?.() ?? "*",
159
+ ),
160
+ });
161
+ }
154
162
  readonly parameters = astGrepSchema;
155
163
  readonly strict = true;
156
164
 
@@ -178,9 +186,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
178
186
  ];
179
187
  readonly loadMode = "discoverable";
180
188
 
181
- constructor(private readonly session: ToolSession) {
182
- this.description = prompt.render(astGrepDescription);
183
- }
189
+ constructor(private readonly session: ToolSession) {}
184
190
 
185
191
  async execute(
186
192
  _toolCallId: string,
@@ -8,16 +8,11 @@ import { resizeImage } from "../../../utils/image-resize";
8
8
  import type { ToolSession } from "../../index";
9
9
  import { resolveToCwd } from "../../path-utils";
10
10
  import { formatScreenshot } from "../../render-utils";
11
+ import { bindRunFacade, resolvePredicateTimeout, type WaitPredicateOptions, waitForRun } from "../../run-scope";
11
12
  import { ToolAbortError, ToolError, throwIfAborted } from "../../tool-errors";
12
13
  import { type AriaSnapshotOptions, assertSelectorString, buildAriaSnapshotScript } from "../aria/aria-snapshot";
13
14
  import { DEFAULT_VIEWPORT } from "../launch";
14
15
  import { extractReadableFromHtml, type ReadableFormat } from "../readable";
15
- import {
16
- bindBrowserRunFacade,
17
- resolvePredicateTimeout,
18
- type WaitPredicateOptions,
19
- waitForBrowserRun,
20
- } from "../run-cancellation";
21
16
  import { cloneSafe, RunOutput } from "../run-output";
22
17
  import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
23
18
  import {
@@ -259,6 +254,65 @@ export interface RunCmuxCodeOptions {
259
254
  snapshot: SessionSnapshot;
260
255
  }
261
256
 
257
+ interface ActiveCmuxRun {
258
+ filename: string;
259
+ floatingRejections: unknown[];
260
+ }
261
+
262
+ const RECENT_CMUX_RUN_FILES_MAX = 256;
263
+ const activeCmuxRuns = new Map<string, ActiveCmuxRun>();
264
+ const recentCmuxRunFiles = new Set<string>();
265
+
266
+ function consumeCmuxRunRejection(reason: unknown): boolean {
267
+ // cmux runs guest JS in the shared main-process realm (TTS/STT/MCP and other
268
+ // subsystems live here too), so — like the eval inline fallback — only a
269
+ // guest-file stack frame can safely attribute a rejection. A stackless or
270
+ // non-run-stack reason is indistinguishable from a subsystem failure and
271
+ // keeps the default fatal path; worker isolation is the long-term fix.
272
+ const stack = reason instanceof Error && typeof reason.stack === "string" ? reason.stack : undefined;
273
+ if (!stack) return false;
274
+
275
+ let owner: ActiveCmuxRun | undefined;
276
+ let ownerIndex = -1;
277
+ for (const run of activeCmuxRuns.values()) {
278
+ const index = stack.lastIndexOf(run.filename);
279
+ if (index > ownerIndex) {
280
+ ownerIndex = index;
281
+ owner = run;
282
+ }
283
+ }
284
+ if (owner) {
285
+ owner.floatingRejections.push(reason);
286
+ return true;
287
+ }
288
+
289
+ let recent: string | undefined;
290
+ let recentIndex = -1;
291
+ for (const filename of recentCmuxRunFiles) {
292
+ const index = stack.lastIndexOf(filename);
293
+ if (index > recentIndex) {
294
+ recentIndex = index;
295
+ recent = filename;
296
+ }
297
+ }
298
+ if (!recent) return false;
299
+ logger.warn("Unhandled rejection from a finished cmux browser run (missing await?)", {
300
+ filename: recent,
301
+ error: reason,
302
+ });
303
+ return true;
304
+ }
305
+
306
+ function rememberCmuxRunFile(filename: string): void {
307
+ recentCmuxRunFiles.delete(filename);
308
+ recentCmuxRunFiles.add(filename);
309
+ if (recentCmuxRunFiles.size <= RECENT_CMUX_RUN_FILES_MAX) return;
310
+ const oldest = recentCmuxRunFiles.values().next().value;
311
+ if (oldest !== undefined) recentCmuxRunFiles.delete(oldest);
312
+ }
313
+
314
+ postmortem.interceptUnhandledRejections(consumeCmuxRunRejection);
315
+
262
316
  export class CmuxTab {
263
317
  readonly #client: CmuxSocketClient;
264
318
  readonly #surfaceId: string;
@@ -1317,6 +1371,9 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1317
1371
  const output = new RunOutput();
1318
1372
  const screenshots: ScreenshotResult[] = [];
1319
1373
  const runId = crypto.randomUUID();
1374
+ const filename = `cmux-run-${runId}.js`;
1375
+ const activeRun: ActiveCmuxRun = { filename, floatingRejections: [] };
1376
+ activeCmuxRuns.set(filename, activeRun);
1320
1377
  tab.setRunContext({ session: opts.snapshot, output, screenshots, signal, timeoutMs: opts.timeoutMs });
1321
1378
 
1322
1379
  const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
@@ -1345,16 +1402,16 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1345
1402
  // Keep both inside try so a concurrent in-process eval/browser run surfaces as
1346
1403
  // a rejected promise the supervisor can report, never an unhandled rejection.
1347
1404
  runtime.setCwd(opts.snapshot.cwd);
1348
- const runTab = bindBrowserRunFacade(tab, signal);
1405
+ const runTab = bindRunFacade(tab, signal);
1349
1406
  runtime.setRunScope({
1350
- page: bindBrowserRunFacade(tab.page, signal),
1351
- browser: bindBrowserRunFacade(tab.browser, signal),
1407
+ page: bindRunFacade(tab.page, signal),
1408
+ browser: bindRunFacade(tab.browser, signal),
1352
1409
  tab: runTab,
1353
1410
  assert: (cond: unknown, text?: string): void => {
1354
1411
  if (!cond) throw new ToolError(text ?? "Assertion failed");
1355
1412
  },
1356
1413
  wait: (msOrPredicate: number | (() => unknown), waitOpts?: WaitPredicateOptions): Promise<unknown> =>
1357
- waitForBrowserRun(
1414
+ waitForRun(
1358
1415
  msOrPredicate,
1359
1416
  signal,
1360
1417
  typeof msOrPredicate === "number"
@@ -1383,14 +1440,40 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1383
1440
  };
1384
1441
  // Like the inline worker fallback, cmux runs user JS in-process: awaited cmux/tool calls
1385
1442
  // observe this abort signal, but a synchronous infinite loop cannot be interrupted here.
1386
- const returnValue = await Promise.race([
1387
- runtime.run(opts.code, `cmux-run-${runId}.js`, hooks, { runId, cwd: opts.snapshot.cwd }),
1388
- cancelRejection,
1389
- ]);
1443
+ let returnValue: unknown;
1444
+ let runError: unknown;
1445
+ let runFailed = false;
1446
+ try {
1447
+ returnValue = await Promise.race([
1448
+ runtime.run(opts.code, filename, hooks, { runId, cwd: opts.snapshot.cwd }),
1449
+ cancelRejection,
1450
+ ]);
1451
+ } catch (error) {
1452
+ runFailed = true;
1453
+ runError = error;
1454
+ }
1455
+ // Let rejection callbacks run while this run can still own guest-created promises.
1456
+ await Bun.sleep(0);
1457
+ if (runFailed) {
1458
+ for (const reason of activeRun.floatingRejections) {
1459
+ logger.warn("Unhandled rejection accompanied a failed cmux browser run", { filename, error: reason });
1460
+ }
1461
+ throw runError;
1462
+ }
1463
+ if (activeRun.floatingRejections.length > 0) {
1464
+ const messages = activeRun.floatingRejections.map(reason =>
1465
+ reason instanceof Error ? reason.message : String(reason),
1466
+ );
1467
+ throw new ToolError(`Unhandled rejection (missing await?): ${messages.join("\n[unhandled rejection] ")}`, {
1468
+ rejections: activeRun.floatingRejections,
1469
+ });
1470
+ }
1390
1471
  return { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots };
1391
1472
  } finally {
1392
1473
  signal.removeEventListener("abort", onAbort);
1393
1474
  runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
1475
+ activeCmuxRuns.delete(filename);
1476
+ rememberCmuxRunFile(filename);
1394
1477
  tab.clearRunContext();
1395
1478
  }
1396
1479
  }
@@ -1,3 +1,4 @@
1
+ import { parseFlag } from "@oh-my-pi/pi-utils";
1
2
  import { ToolError } from "../../tool-errors";
2
3
  import type { Observation, ObservationEntry } from "../tab-protocol";
3
4
 
@@ -174,13 +175,6 @@ export function mapWaitUntil(waitUntil: string | undefined): "interactive" | "co
174
175
  return waitUntil === "domcontentloaded" ? "interactive" : "complete";
175
176
  }
176
177
 
177
- const TRUTHY_ENV_VALUES = new Set(["1", "Y", "y", "TRUE", "true", "YES", "yes", "ON", "on"]);
178
-
179
- function resolveCmuxEnabled(envValue: string | undefined, settingEnabled: boolean): boolean {
180
- if (!envValue) return settingEnabled;
181
- return TRUTHY_ENV_VALUES.has(envValue);
182
- }
183
-
184
178
  export interface ResolveCmuxKindOptions {
185
179
  surface?: string;
186
180
  settingEnabled?: boolean;
@@ -190,7 +184,7 @@ export function resolveCmuxKind(
190
184
  options?: ResolveCmuxKindOptions | null,
191
185
  env: Record<string, string | undefined> = process.env,
192
186
  ): CmuxKind | null {
193
- if (!resolveCmuxEnabled(env.PI_BROWSER_CMUX, options?.settingEnabled ?? true)) {
187
+ if (!parseFlag(env.PI_BROWSER_CMUX, options?.settingEnabled ?? true)) {
194
188
  return null;
195
189
  }
196
190
  const socketPath = env.CMUX_SOCKET_PATH;
@@ -14,12 +14,15 @@ import {
14
14
  removeUserDataDir,
15
15
  type UserAgentOverride,
16
16
  } from "./launch";
17
+ import { ensureRelayDaemon, isLoopbackRelayUrl } from "./relay/daemon";
18
+ import type { RelayKind } from "./relay/kind";
17
19
  import { ensureSharedBrowser } from "./shared-daemon";
18
20
 
19
21
  export type PuppeteerBrowserKind =
20
22
  | { kind: "headless"; headless: boolean }
21
23
  | { kind: "spawned"; path: string }
22
- | { kind: "connected"; cdpUrl: string };
24
+ | { kind: "connected"; cdpUrl: string }
25
+ | RelayKind;
23
26
 
24
27
  export type BrowserKind = PuppeteerBrowserKind | CmuxKind;
25
28
 
@@ -31,6 +34,12 @@ export type BrowserKindTag = BrowserKind["kind"];
31
34
  * forever (issue #5260), so we cap the wait and force-kill on timeout.
32
35
  */
33
36
  const HEADLESS_CLOSE_TIMEOUT_MS = 5_000;
37
+ /**
38
+ * How long a relay open waits for the extension handshake (503 → 200). A
39
+ * reaped extension service worker is revived by its 30s keepalive alarm, so
40
+ * the wait must cover one full alarm period plus the dial.
41
+ */
42
+ const RELAY_EXTENSION_WAIT_MS = 35_000;
34
43
 
35
44
  interface BrowserHandleCommon {
36
45
  key: string;
@@ -78,6 +87,8 @@ function browserKey(kind: BrowserKind): string {
78
87
  return `spawned:${kind.path}`;
79
88
  case "connected":
80
89
  return `connected:${kind.cdpUrl}`;
90
+ case "relay":
91
+ return `relay:${kind.cdpUrl}`;
81
92
  case "cmux":
82
93
  return `cmux:${kind.socketPath}`;
83
94
  }
@@ -201,6 +212,45 @@ async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions)
201
212
  stealth: { browserSession: null, override: null },
202
213
  };
203
214
  }
215
+ if (kind.kind === "relay") {
216
+ const cdpUrl = normalizeConnectedCdpUrl(kind.cdpUrl);
217
+ // Loopback relays are owned by a machine-global broker and auto-started
218
+ // on demand (the extension dials in on its own). Hosts without a CLI
219
+ // worker entry (bun test, SDK embedding) never spawn brokers. Remote
220
+ // relay URLs must already be serving.
221
+ let autoStarted = false;
222
+ if (isLoopbackRelayUrl(cdpUrl) && (isCompiledBinary() || workerHostEntry() !== null)) {
223
+ autoStarted = await ensureRelayDaemon({ cdpUrl, signal: opts.signal });
224
+ }
225
+ // The relay answers /json/version with 503 until its extension dials in.
226
+ // A freshly revived extension service worker can take up to ~30s (its
227
+ // keepalive alarm) to reconnect, so give the handshake that long.
228
+ try {
229
+ await waitForCdp(cdpUrl, RELAY_EXTENSION_WAIT_MS, opts.signal);
230
+ } catch (err) {
231
+ if (err instanceof ToolAbortError) throw err;
232
+ if (err instanceof Error && err.name === "AbortError") throw err;
233
+ throw new ToolError(
234
+ autoStarted
235
+ ? `omp browser relay is serving at ${cdpUrl} but its extension never connected. Install it with \`omp browser-relay install\` and check the toolbar badge shows "on".`
236
+ : `omp browser relay is not reachable at ${cdpUrl}. Start it with \`omp browser-relay\` (or check the endpoint), and make sure the OMP Browser Relay extension is loaded in Chrome.`,
237
+ );
238
+ }
239
+ const puppeteer = await loadPuppeteer();
240
+ const browser = await puppeteer.connect({
241
+ browserURL: cdpUrl,
242
+ defaultViewport: null,
243
+ protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
244
+ });
245
+ return {
246
+ key: browserKey(kind),
247
+ kind,
248
+ browser,
249
+ cdpUrl,
250
+ refCount: 0,
251
+ stealth: { browserSession: null, override: null },
252
+ };
253
+ }
204
254
 
205
255
  const exe = kind.path;
206
256
  if (!path.isAbsolute(exe)) {
@@ -320,7 +370,8 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: ReleaseBrowserO
320
370
  if (handle.userDataDir) await removeUserDataDir(handle.userDataDir);
321
371
  return;
322
372
  }
323
- if (handle.kind.kind === "connected") {
373
+ // Connected and relay browsers belong to the user: drop our CDP link, never kill.
374
+ if (handle.kind.kind === "connected" || handle.kind.kind === "relay") {
324
375
  if (handle.browser.connected) {
325
376
  try {
326
377
  handle.browser.disconnect();