@mirasim/computer-use 0.0.0-stage → 0.1.0-rc.3

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 (341) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +154 -2
  4. package/THIRD_PARTY_NOTICES +488 -0
  5. package/bin/mirasim-cu.mjs +30 -0
  6. package/dist/entries/cli.d.ts +2 -0
  7. package/dist/entries/cli.js +32 -0
  8. package/dist/entries/commands/approvals.d.ts +1 -0
  9. package/dist/entries/commands/approvals.js +17 -0
  10. package/dist/entries/commands/bridge-host.d.ts +1 -0
  11. package/dist/entries/commands/bridge-host.js +7 -0
  12. package/dist/entries/commands/bridge.d.ts +1 -0
  13. package/dist/entries/commands/bridge.js +16 -0
  14. package/dist/entries/commands/browser-state.d.ts +1 -0
  15. package/dist/entries/commands/browser-state.js +8 -0
  16. package/dist/entries/commands/capture-server.d.ts +1 -0
  17. package/dist/entries/commands/capture-server.js +5 -0
  18. package/dist/entries/commands/help.d.ts +2 -0
  19. package/dist/entries/commands/help.js +1 -0
  20. package/dist/entries/commands/native-host-options.d.ts +32 -0
  21. package/dist/entries/commands/native-host-options.js +7 -0
  22. package/dist/entries/commands/setup-macos.d.ts +1 -0
  23. package/dist/entries/commands/setup-macos.js +4 -0
  24. package/dist/entries/commands/setup.d.ts +1 -0
  25. package/dist/entries/commands/setup.js +15 -0
  26. package/dist/entries/mcp.d.ts +1 -0
  27. package/dist/entries/mcp.js +6 -0
  28. package/dist/entries/version.d.ts +1 -0
  29. package/dist/entries/version.js +1 -0
  30. package/dist/index.d.ts +11 -0
  31. package/dist/index.js +1 -0
  32. package/dist/platform/browser/acquire.d.ts +15 -0
  33. package/dist/platform/browser/acquire.js +1 -0
  34. package/dist/platform/browser/bridge/acquire-bridge.d.ts +7 -0
  35. package/dist/platform/browser/bridge/acquire-bridge.js +3 -0
  36. package/dist/platform/browser/bridge/client.d.ts +69 -0
  37. package/dist/platform/browser/bridge/client.js +1 -0
  38. package/dist/platform/browser/bridge/extension-channel.d.ts +12 -0
  39. package/dist/platform/browser/bridge/extension-channel.js +1 -0
  40. package/dist/platform/browser/bridge/extension-id.d.ts +8 -0
  41. package/dist/platform/browser/bridge/extension-id.js +1 -0
  42. package/dist/platform/browser/bridge/extension-updates.d.ts +37 -0
  43. package/dist/platform/browser/bridge/extension-updates.js +1 -0
  44. package/dist/platform/browser/bridge/model-commands.d.ts +20 -0
  45. package/dist/platform/browser/bridge/model-commands.js +1 -0
  46. package/dist/platform/browser/bridge/model-core.d.ts +59 -0
  47. package/dist/platform/browser/bridge/model-core.js +1 -0
  48. package/dist/platform/browser/bridge/model.d.ts +4 -0
  49. package/dist/platform/browser/bridge/model.js +1 -0
  50. package/dist/platform/browser/bridge/native-frames.d.ts +9 -0
  51. package/dist/platform/browser/bridge/native-frames.js +1 -0
  52. package/dist/platform/browser/bridge/native-host.d.ts +24 -0
  53. package/dist/platform/browser/bridge/native-host.js +5 -0
  54. package/dist/platform/browser/bridge/runtime-contexts.d.ts +12 -0
  55. package/dist/platform/browser/bridge/runtime-contexts.js +1 -0
  56. package/dist/platform/browser/bridge/server-core.d.ts +66 -0
  57. package/dist/platform/browser/bridge/server-core.js +1 -0
  58. package/dist/platform/browser/bridge/server.d.ts +13 -0
  59. package/dist/platform/browser/bridge/server.js +1 -0
  60. package/dist/platform/browser/bridge-tabs.d.ts +22 -0
  61. package/dist/platform/browser/bridge-tabs.js +1 -0
  62. package/dist/platform/browser/cdp-connection.d.ts +3 -0
  63. package/dist/platform/browser/cdp-connection.js +1 -0
  64. package/dist/platform/browser/chrome-binary.d.ts +6 -0
  65. package/dist/platform/browser/chrome-binary.js +1 -0
  66. package/dist/platform/browser/config.d.ts +29 -0
  67. package/dist/platform/browser/config.js +1 -0
  68. package/dist/platform/browser/devtools-port.d.ts +3 -0
  69. package/dist/platform/browser/devtools-port.js +2 -0
  70. package/dist/platform/browser/downloads.d.ts +22 -0
  71. package/dist/platform/browser/downloads.js +1 -0
  72. package/dist/platform/browser/driver-connect.d.ts +13 -0
  73. package/dist/platform/browser/driver-connect.js +1 -0
  74. package/dist/platform/browser/driver-core.d.ts +50 -0
  75. package/dist/platform/browser/driver-core.js +1 -0
  76. package/dist/platform/browser/driver-tabs.d.ts +16 -0
  77. package/dist/platform/browser/driver-tabs.js +1 -0
  78. package/dist/platform/browser/driver-verbs.d.ts +13 -0
  79. package/dist/platform/browser/driver-verbs.js +2 -0
  80. package/dist/platform/browser/driver.d.ts +38 -0
  81. package/dist/platform/browser/driver.js +3 -0
  82. package/dist/platform/browser/focus-guard.d.ts +2 -0
  83. package/dist/platform/browser/focus-guard.js +1 -0
  84. package/dist/platform/browser/journal.d.ts +21 -0
  85. package/dist/platform/browser/journal.js +2 -0
  86. package/dist/platform/browser/keep-in-tab.d.ts +1 -0
  87. package/dist/platform/browser/keep-in-tab.js +1 -0
  88. package/dist/platform/browser/messages.d.ts +25 -0
  89. package/dist/platform/browser/messages.js +8 -0
  90. package/dist/platform/browser/navigate.d.ts +4 -0
  91. package/dist/platform/browser/navigate.js +2 -0
  92. package/dist/platform/browser/occupancy.d.ts +2 -0
  93. package/dist/platform/browser/occupancy.js +2 -0
  94. package/dist/platform/browser/overlay-guard.d.ts +3 -0
  95. package/dist/platform/browser/overlay-guard.js +1 -0
  96. package/dist/platform/browser/save-picker.d.ts +2 -0
  97. package/dist/platform/browser/save-picker.js +1 -0
  98. package/dist/platform/browser/session-files.d.ts +11 -0
  99. package/dist/platform/browser/session-files.js +1 -0
  100. package/dist/platform/browser/state.d.ts +25 -0
  101. package/dist/platform/browser/state.js +2 -0
  102. package/dist/platform/browser/tabs.d.ts +15 -0
  103. package/dist/platform/browser/tabs.js +2 -0
  104. package/dist/platform/browser/window-placement.d.ts +8 -0
  105. package/dist/platform/browser/window-placement.js +1 -0
  106. package/dist/platform/chromium/actions-value.d.ts +11 -0
  107. package/dist/platform/chromium/actions-value.js +5 -0
  108. package/dist/platform/chromium/actions.d.ts +20 -0
  109. package/dist/platform/chromium/actions.js +2 -0
  110. package/dist/platform/chromium/checkable-target.d.ts +1 -0
  111. package/dist/platform/chromium/checkable-target.js +1 -0
  112. package/dist/platform/chromium/collect-viewport.d.ts +1 -0
  113. package/dist/platform/chromium/collect-viewport.js +1 -0
  114. package/dist/platform/chromium/collect.d.ts +11 -0
  115. package/dist/platform/chromium/collect.js +1 -0
  116. package/dist/platform/chromium/coverer-label.d.ts +8 -0
  117. package/dist/platform/chromium/coverer-label.js +1 -0
  118. package/dist/platform/chromium/frames.d.ts +2 -0
  119. package/dist/platform/chromium/frames.js +1 -0
  120. package/dist/platform/chromium/headings.d.ts +1 -0
  121. package/dist/platform/chromium/headings.js +1 -0
  122. package/dist/platform/chromium/locate.d.ts +2 -0
  123. package/dist/platform/chromium/locate.js +1 -0
  124. package/dist/platform/chromium/page-acts.d.ts +9 -0
  125. package/dist/platform/chromium/page-acts.js +1 -0
  126. package/dist/platform/chromium/page-base.d.ts +33 -0
  127. package/dist/platform/chromium/page-base.js +1 -0
  128. package/dist/platform/chromium/page-driver.d.ts +18 -0
  129. package/dist/platform/chromium/page-driver.js +2 -0
  130. package/dist/platform/chromium/page-refs.d.ts +4 -0
  131. package/dist/platform/chromium/page-refs.js +1 -0
  132. package/dist/platform/chromium/page-text.d.ts +15 -0
  133. package/dist/platform/chromium/page-text.js +1 -0
  134. package/dist/platform/chromium/page-waits.d.ts +7 -0
  135. package/dist/platform/chromium/page-waits.js +1 -0
  136. package/dist/platform/chromium/preview-stream.d.ts +37 -0
  137. package/dist/platform/chromium/preview-stream.js +1 -0
  138. package/dist/platform/chromium/preview.d.ts +3 -0
  139. package/dist/platform/chromium/preview.js +1 -0
  140. package/dist/platform/chromium/read-page-script.d.ts +1 -0
  141. package/dist/platform/chromium/read-page-script.js +1 -0
  142. package/dist/platform/chromium/read-page.d.ts +11 -0
  143. package/dist/platform/chromium/read-page.js +6 -0
  144. package/dist/platform/chromium/run-js.d.ts +4 -0
  145. package/dist/platform/chromium/run-js.js +2 -0
  146. package/dist/platform/chromium/scroll-regions.d.ts +1 -0
  147. package/dist/platform/chromium/scroll-regions.js +1 -0
  148. package/dist/platform/chromium/scroll-scripts.d.ts +3 -0
  149. package/dist/platform/chromium/scroll-scripts.js +1 -0
  150. package/dist/platform/chromium/scroll.d.ts +18 -0
  151. package/dist/platform/chromium/scroll.js +5 -0
  152. package/dist/platform/chromium/select-text.d.ts +4 -0
  153. package/dist/platform/chromium/select-text.js +2 -0
  154. package/dist/platform/chromium/shot.d.ts +10 -0
  155. package/dist/platform/chromium/shot.js +2 -0
  156. package/dist/platform/chromium/unreached.d.ts +8 -0
  157. package/dist/platform/chromium/unreached.js +1 -0
  158. package/dist/platform/cli-command.d.ts +1 -0
  159. package/dist/platform/cli-command.js +1 -0
  160. package/dist/platform/driver-base.d.ts +83 -0
  161. package/dist/platform/driver-base.js +1 -0
  162. package/dist/platform/driver-diagnostics.d.ts +30 -0
  163. package/dist/platform/driver-diagnostics.js +4 -0
  164. package/dist/platform/driver-lines.d.ts +15 -0
  165. package/dist/platform/driver-lines.js +2 -0
  166. package/dist/platform/driver-narrate.d.ts +26 -0
  167. package/dist/platform/driver-narrate.js +3 -0
  168. package/dist/platform/driver-refs.d.ts +16 -0
  169. package/dist/platform/driver-refs.js +1 -0
  170. package/dist/platform/driver-render.d.ts +26 -0
  171. package/dist/platform/driver-render.js +4 -0
  172. package/dist/platform/driver-select.d.ts +31 -0
  173. package/dist/platform/driver-select.js +2 -0
  174. package/dist/platform/driver-settle.d.ts +22 -0
  175. package/dist/platform/driver-settle.js +4 -0
  176. package/dist/platform/driver-verdict.d.ts +11 -0
  177. package/dist/platform/driver-verdict.js +5 -0
  178. package/dist/platform/driver-wait.d.ts +9 -0
  179. package/dist/platform/driver-wait.js +1 -0
  180. package/dist/platform/driver-waits.d.ts +10 -0
  181. package/dist/platform/driver-waits.js +1 -0
  182. package/dist/platform/driver.d.ts +39 -0
  183. package/dist/platform/driver.js +15 -0
  184. package/dist/platform/electron/driver.d.ts +30 -0
  185. package/dist/platform/electron/driver.js +7 -0
  186. package/dist/platform/electron/preflight.d.ts +9 -0
  187. package/dist/platform/electron/preflight.js +1 -0
  188. package/dist/platform/electron/verbs.d.ts +3 -0
  189. package/dist/platform/electron/verbs.js +1 -0
  190. package/dist/platform/home.d.ts +2 -0
  191. package/dist/platform/home.js +1 -0
  192. package/dist/platform/macos/act-element.d.ts +27 -0
  193. package/dist/platform/macos/act-element.js +1 -0
  194. package/dist/platform/macos/app-names.d.ts +2 -0
  195. package/dist/platform/macos/app-names.js +2 -0
  196. package/dist/platform/macos/app-notes.d.ts +2 -0
  197. package/dist/platform/macos/app-notes.js +86 -0
  198. package/dist/platform/macos/approvals.d.ts +10 -0
  199. package/dist/platform/macos/approvals.js +2 -0
  200. package/dist/platform/macos/ax-client.d.ts +53 -0
  201. package/dist/platform/macos/ax-client.js +3 -0
  202. package/dist/platform/macos/ax-errors.d.ts +16 -0
  203. package/dist/platform/macos/ax-errors.js +1 -0
  204. package/dist/platform/macos/ax-shapes.d.ts +53 -0
  205. package/dist/platform/macos/ax-shapes.js +1 -0
  206. package/dist/platform/macos/capture-client.d.ts +11 -0
  207. package/dist/platform/macos/capture-client.js +2 -0
  208. package/dist/platform/macos/click-verb.d.ts +4 -0
  209. package/dist/platform/macos/click-verb.js +1 -0
  210. package/dist/platform/macos/driver-apps.d.ts +59 -0
  211. package/dist/platform/macos/driver-apps.js +2 -0
  212. package/dist/platform/macos/driver-read.d.ts +47 -0
  213. package/dist/platform/macos/driver-read.js +1 -0
  214. package/dist/platform/macos/driver-targets.d.ts +9 -0
  215. package/dist/platform/macos/driver-targets.js +1 -0
  216. package/dist/platform/macos/driver.d.ts +65 -0
  217. package/dist/platform/macos/driver.js +2 -0
  218. package/dist/platform/macos/element-lines.d.ts +12 -0
  219. package/dist/platform/macos/element-lines.js +1 -0
  220. package/dist/platform/macos/element-shapes.d.ts +54 -0
  221. package/dist/platform/macos/element-shapes.js +0 -0
  222. package/dist/platform/macos/element-text.d.ts +3 -0
  223. package/dist/platform/macos/element-text.js +2 -0
  224. package/dist/platform/macos/elements.d.ts +26 -0
  225. package/dist/platform/macos/elements.js +1 -0
  226. package/dist/platform/macos/extra-verbs.d.ts +2 -0
  227. package/dist/platform/macos/extra-verbs.js +1 -0
  228. package/dist/platform/macos/interaction-notes.d.ts +2 -0
  229. package/dist/platform/macos/interaction-notes.js +1 -0
  230. package/dist/platform/macos/interaction-policy.d.ts +5 -0
  231. package/dist/platform/macos/interaction-policy.js +1 -0
  232. package/dist/platform/macos/menu-commands.d.ts +22 -0
  233. package/dist/platform/macos/menu-commands.js +5 -0
  234. package/dist/platform/macos/mouse.d.ts +23 -0
  235. package/dist/platform/macos/mouse.js +1 -0
  236. package/dist/platform/macos/narrate.d.ts +20 -0
  237. package/dist/platform/macos/narrate.js +1 -0
  238. package/dist/platform/macos/permissions.d.ts +5 -0
  239. package/dist/platform/macos/permissions.js +1 -0
  240. package/dist/platform/macos/press-rungs.d.ts +49 -0
  241. package/dist/platform/macos/press-rungs.js +5 -0
  242. package/dist/platform/macos/preview-stream.d.ts +15 -0
  243. package/dist/platform/macos/preview-stream.js +1 -0
  244. package/dist/platform/macos/prompt-fragment.d.ts +1 -0
  245. package/dist/platform/macos/prompt-fragment.js +1 -0
  246. package/dist/platform/macos/scroll-pages.d.ts +1 -0
  247. package/dist/platform/macos/scroll-pages.js +1 -0
  248. package/dist/platform/macos/scroll-verb.d.ts +5 -0
  249. package/dist/platform/macos/scroll-verb.js +1 -0
  250. package/dist/platform/macos/select-text-verb.d.ts +8 -0
  251. package/dist/platform/macos/select-text-verb.js +1 -0
  252. package/dist/platform/macos/type-verb.d.ts +5 -0
  253. package/dist/platform/macos/type-verb.js +1 -0
  254. package/dist/platform/ownership.d.ts +20 -0
  255. package/dist/platform/ownership.js +1 -0
  256. package/dist/platform/platform-package.d.ts +1 -0
  257. package/dist/platform/platform-package.js +1 -0
  258. package/dist/platform/preview.d.ts +39 -0
  259. package/dist/platform/preview.js +1 -0
  260. package/dist/platform/profile.d.ts +20 -0
  261. package/dist/platform/profile.js +1 -0
  262. package/dist/platform/registry.d.ts +12 -0
  263. package/dist/platform/registry.js +1 -0
  264. package/dist/platform/tools/api-contract.d.ts +33 -0
  265. package/dist/platform/tools/api-contract.js +3 -0
  266. package/dist/platform/tools/api-expect.d.ts +3 -0
  267. package/dist/platform/tools/api-expect.js +1 -0
  268. package/dist/platform/tools/api-run.d.ts +6 -0
  269. package/dist/platform/tools/api-run.js +1 -0
  270. package/dist/platform/tools/api-target.d.ts +22 -0
  271. package/dist/platform/tools/api-target.js +1 -0
  272. package/dist/platform/tools/api-wait.d.ts +3 -0
  273. package/dist/platform/tools/api-wait.js +1 -0
  274. package/dist/platform/tools/api.d.ts +7 -0
  275. package/dist/platform/tools/api.js +1 -0
  276. package/dist/platform/tools/find-target.d.ts +54 -0
  277. package/dist/platform/tools/find-target.js +1 -0
  278. package/dist/platform/tools/index.d.ts +8 -0
  279. package/dist/platform/tools/index.js +1 -0
  280. package/dist/platform/tools/normalize.d.ts +2 -0
  281. package/dist/platform/tools/normalize.js +1 -0
  282. package/dist/platform/tools/repl-budget.d.ts +8 -0
  283. package/dist/platform/tools/repl-budget.js +4 -0
  284. package/dist/platform/tools/repl-code.d.ts +1 -0
  285. package/dist/platform/tools/repl-code.js +4 -0
  286. package/dist/platform/tools/repl-docs.d.ts +5 -0
  287. package/dist/platform/tools/repl-docs.js +4 -0
  288. package/dist/platform/tools/repl-state.d.ts +21 -0
  289. package/dist/platform/tools/repl-state.js +0 -0
  290. package/dist/platform/tools/repl.d.ts +8 -0
  291. package/dist/platform/tools/repl.js +19 -0
  292. package/dist/platform/tools/shot-file.d.ts +7 -0
  293. package/dist/platform/tools/shot-file.js +1 -0
  294. package/dist/platform/tools/validate.d.ts +2 -0
  295. package/dist/platform/tools/validate.js +1 -0
  296. package/dist/platform/types.d.ts +220 -0
  297. package/dist/platform/types.js +1 -0
  298. package/dist/platform/windows/app-gate.d.ts +24 -0
  299. package/dist/platform/windows/app-gate.js +2 -0
  300. package/dist/platform/windows/background-verb.d.ts +13 -0
  301. package/dist/platform/windows/background-verb.js +1 -0
  302. package/dist/platform/windows/dates.d.ts +2 -0
  303. package/dist/platform/windows/dates.js +1 -0
  304. package/dist/platform/windows/driver-apps.d.ts +9 -0
  305. package/dist/platform/windows/driver-apps.js +1 -0
  306. package/dist/platform/windows/driver-read.d.ts +12 -0
  307. package/dist/platform/windows/driver-read.js +3 -0
  308. package/dist/platform/windows/driver-targets.d.ts +58 -0
  309. package/dist/platform/windows/driver-targets.js +1 -0
  310. package/dist/platform/windows/driver-yield.d.ts +10 -0
  311. package/dist/platform/windows/driver-yield.js +1 -0
  312. package/dist/platform/windows/driver.d.ts +39 -0
  313. package/dist/platform/windows/driver.js +2 -0
  314. package/dist/platform/windows/elements.d.ts +66 -0
  315. package/dist/platform/windows/elements.js +2 -0
  316. package/dist/platform/windows/launch.d.ts +23 -0
  317. package/dist/platform/windows/launch.js +1 -0
  318. package/dist/platform/windows/menu-verb.d.ts +7 -0
  319. package/dist/platform/windows/menu-verb.js +6 -0
  320. package/dist/platform/windows/prompt-fragment.d.ts +2 -0
  321. package/dist/platform/windows/prompt-fragment.js +1 -0
  322. package/dist/platform/windows/text-verbs.d.ts +10 -0
  323. package/dist/platform/windows/text-verbs.js +5 -0
  324. package/dist/platform/windows/uia-client.d.ts +32 -0
  325. package/dist/platform/windows/uia-client.js +3 -0
  326. package/dist/platform/windows/verbs.d.ts +2 -0
  327. package/dist/platform/windows/verbs.js +1 -0
  328. package/extension/background.js +280 -0
  329. package/extension/connect.html +27 -0
  330. package/extension/connect.js +28 -0
  331. package/extension/group.js +283 -0
  332. package/extension/icons/icon-128.png +0 -0
  333. package/extension/icons/icon-16.png +0 -0
  334. package/extension/icons/icon-32.png +0 -0
  335. package/extension/icons/icon-48.png +0 -0
  336. package/extension/manifest.json +36 -0
  337. package/extension/relay.js +361 -0
  338. package/extension/runtime-info.js +9 -0
  339. package/extension/transport.js +81 -0
  340. package/package.json +76 -4
  341. package/scripts/extension.mjs +111 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Portions Copyright (c) Microsoft Corporation (Playwright), licensed under the Apache License,
3
+ * Version 2.0; modified by MIRASIM, INC. See LICENSE and NOTICE.
4
+ *
5
+ * The mirasim tab group — which tabs are the AGENT'S OWN, inside the user's browser.
6
+ *
7
+ * The agent sees every tab; the Chrome tab group titled "mirasim" says whose each tab is:
8
+ * - tabs the bridge creates (popups, Target.createTarget) are pulled into the group;
9
+ * - a tab outside the group is the user's: attached and actable, but the driver will not close
10
+ * or recycle it, and its targets line says `(user's)` (platform/browser/driver.ts
11
+ * `listTargets`, types.ts `TargetInfo.own`).
12
+ *
13
+ * The group is deliberately NOT a visibility boundary (a task about a page the user already has
14
+ * open would need it handed over first): the agent can read anything and close only its own.
15
+ *
16
+ * Unlike Playwright's extension (which ungroups on disconnect), the group is DURABLE: tabs and
17
+ * group stay on disconnect, and the next connection adopts whatever is still in it.
18
+ *
19
+ * Mechanism adapted from Microsoft Playwright's browser extension (Apache-2.0).
20
+ */
21
+
22
+ import { debugLog } from "./relay.js";
23
+
24
+ export const GROUP_TITLE = "mirasim";
25
+ const GROUP_COLOR = "cyan";
26
+ /** Where the browser-session nonce lives — see {@link browserSessionId}. */
27
+ const SESSION_KEY = "mirasim-browser-session";
28
+ const NON_DEBUGGABLE_SCHEMES = ["chrome:", "edge:", "devtools:", "chrome-extension:"];
29
+ const CONNECTED_BADGE = { text: "✓", color: "#0e7490", title: "Mirasim agent is connected to this tab" };
30
+
31
+ export function isNonDebuggableUrl(url) {
32
+ return !!url && NON_DEBUGGABLE_SCHEMES.some((s) => url.startsWith(s));
33
+ }
34
+
35
+ /**
36
+ * A nonce identifying THIS browser session, handed to the bridge on every handshake so it can
37
+ * tell one reconnection from another (model.ts `onExtensionConnected`). Data, not a decision.
38
+ *
39
+ * Chrome tab ids are never reused WITHIN a browser session, so across a worker eviction or
40
+ * reconnect the bridge may keep what it knew per tab id. After a browser RESTART the ids start
41
+ * over and the same number can name the user's own page, which `/recycle` must not close.
42
+ * `chrome.storage.session` has exactly that lifetime (survives eviction, cleared on restart): a
43
+ * stored nonce says "same browser session", a missing one says "assume nothing".
44
+ */
45
+ export async function browserSessionId() {
46
+ const stored = await chrome.storage.session.get(SESSION_KEY).catch(() => ({}));
47
+ const existing = stored?.[SESSION_KEY];
48
+ if (typeof existing === "string" && existing) return existing;
49
+ const minted = crypto.randomUUID();
50
+ await chrome.storage.session.set({ [SESSION_KEY]: minted }).catch((e) => debugLog("session nonce not stored:", e));
51
+ return minted;
52
+ }
53
+
54
+ export class MirasimTabGroup {
55
+ updateAvailable(version) {
56
+ this._connection.updateAvailable(version);
57
+ }
58
+
59
+ /**
60
+ * @param {import("./relay.js").RelayConnection} connection
61
+ */
62
+ constructor(connection) {
63
+ this._connection = connection;
64
+ this._groupId = null;
65
+ /** Cached group membership so hot-path checks stay synchronous. */
66
+ this._groupTabIds = new Set();
67
+ /**
68
+ * Tabs attached because they were simply OPEN, not because the agent opened them.
69
+ * `_onTabAttached` skips these; otherwise the sweep would pull every user tab into the group
70
+ * (the only record of ownership), and with no current window throw "No current window".
71
+ */
72
+ this._userTabIds = new Set();
73
+ this._onTabCreatedListener = this._onTabCreated.bind(this);
74
+ this._onTabUpdatedListener = this._onTabUpdated.bind(this);
75
+ this._onTabRemovedListener = this._onTabRemoved.bind(this);
76
+ this._onTabReplacedListener = this._onTabReplaced.bind(this);
77
+
78
+ /** @type {(() => void) | undefined} */
79
+ this.onclose = undefined;
80
+
81
+ this._connection.onclose = () => this._onConnectionClose();
82
+ this._connection.ontabattached = (tabId) => this._onTabAttached(tabId);
83
+ this._connection.ontabdetached = (tabId) => this._onTabDetached(tabId);
84
+ chrome.tabs.onCreated.addListener(this._onTabCreatedListener);
85
+ chrome.tabs.onUpdated.addListener(this._onTabUpdatedListener);
86
+ chrome.tabs.onRemoved.addListener(this._onTabRemovedListener);
87
+ chrome.tabs.onReplaced.addListener(this._onTabReplacedListener);
88
+
89
+ /** Resolves once the initial handshake (adopt + initialized) is done. */
90
+ this.ready = this._initialize();
91
+ }
92
+
93
+ connectedTabIds() {
94
+ return [...this._groupTabIds];
95
+ }
96
+
97
+ close(reason) {
98
+ this._connection.close(reason);
99
+ }
100
+
101
+ /**
102
+ * Adopt the existing mirasim group's tabs, and open NOTHING: a connect is not a request for a
103
+ * page (the driver binds a listed tab or opens its own, platform/browser/tabs.ts
104
+ * `claimSessionTab`), and a seeded blank tab would later count as the user's, unclosable. The
105
+ * group comes back with the agent's first tab (`_addTabToGroup`). Ends with `didInitialize`,
106
+ * which unblocks the bridge's CDP side against a populated tab model.
107
+ */
108
+ async _initialize() {
109
+ // Identity BEFORE any tab: the bridge keys what it remembers on the browser session, and a
110
+ // tab announced ahead of the nonce is a tab the bridge forgets (relay.js `didConnect`).
111
+ this._connection.didConnect(await browserSessionId());
112
+ const existing = (await chrome.tabGroups.query({ title: GROUP_TITLE }))[0];
113
+ if (existing) {
114
+ this._groupId = existing.id;
115
+ const tabs = await chrome.tabs.query({ groupId: existing.id });
116
+ for (const tab of tabs) {
117
+ if (tab.id === undefined) continue;
118
+ this._groupTabIds.add(tab.id);
119
+ if (!isNonDebuggableUrl(tab.url)) this._connection.attachTab(tab);
120
+ }
121
+ }
122
+ // Everything else the browser has open: attached, never grouped. Failures are per tab and
123
+ // silent by design — an undebuggable page (chrome://, the Web Store) is simply not a surface.
124
+ await this._attachEverythingElse();
125
+ this._connection.didInitialize();
126
+ }
127
+
128
+ /**
129
+ * A tab opened FROM one of the user's tabs (a link, `window.open`) is the user's. Mark it here,
130
+ * at creation: the bridge attaches it at once as a popup (relay.js), before `_onTabUpdated`
131
+ * would see it, and `_onTabAttached` would then group it. Bridge-created tabs have no opener;
132
+ * one opened from a group tab is the agent's, and Chrome groups it itself.
133
+ */
134
+ _onTabCreated(tab) {
135
+ if (tab.id === undefined || tab.openerTabId === undefined) return;
136
+ if (!this._groupTabIds.has(tab.openerTabId)) this._userTabIds.add(tab.id);
137
+ }
138
+
139
+ _onTabUpdated(tabId, changeInfo, tab) {
140
+ if (changeInfo.groupId !== undefined) this._onTabGroupChanged(tabId, tab);
141
+ if (changeInfo.url === undefined && changeInfo.discarded === undefined && changeInfo.status === undefined) return;
142
+ if (tab.discarded || tab.status === "unloaded") {
143
+ if (!isNonDebuggableUrl(tab.url)) this._connection.attachTab(tab);
144
+ return;
145
+ }
146
+ // Chrome resets per-tab badge state on navigation; re-apply it.
147
+ if (this._connection.attachedTabs.has(tabId)) void this._updateBadge(tabId, CONNECTED_BADGE);
148
+ // Any tab, not just the group's: a tab opened after connect becomes a surface on its first
149
+ // navigation. Group membership decides OWNERSHIP, not reachability.
150
+ else if (!isNonDebuggableUrl(tab.url)) {
151
+ if (!this._groupTabIds.has(tabId)) this._userTabIds.add(tabId);
152
+ this._connection.attachTab(tab);
153
+ }
154
+ }
155
+
156
+ /** Single entry point for membership changes, user-dragged or self-inflicted. */
157
+ _onTabGroupChanged(tabId, tab) {
158
+ const inOurGroup = this._groupId !== null && tab.groupId === this._groupId;
159
+ const wasInGroup = this._groupTabIds.has(tabId);
160
+ if (inOurGroup === wasInGroup) return;
161
+ if (inOurGroup) {
162
+ // Dragging a tab in grants OWNERSHIP, not access — every tab is reachable already.
163
+ this._groupTabIds.add(tabId);
164
+ this._userTabIds.delete(tabId);
165
+ if (!isNonDebuggableUrl(tab.url)) this._connection.attachTab(tab);
166
+ } else {
167
+ // Dragged out: theirs again. NOT detached — only the agent's claim is taken back.
168
+ this._groupTabIds.delete(tabId);
169
+ this._userTabIds.add(tabId);
170
+ }
171
+ }
172
+
173
+ _onTabRemoved(tabId) {
174
+ this._groupTabIds.delete(tabId);
175
+ this._userTabIds.delete(tabId);
176
+ }
177
+
178
+ /** Newer Chrome versions replace the tab id when discarding its renderer. */
179
+ async _onTabReplaced(addedTabId, removedTabId) {
180
+ const grouped = this._groupTabIds.has(removedTabId);
181
+ this._onTabRemoved(removedTabId);
182
+ let tab;
183
+ try { tab = await chrome.tabs.get(addedTabId); } catch { return; }
184
+ this._userTabIds.delete(addedTabId);
185
+ if (grouped) this._groupTabIds.add(addedTabId);
186
+ else this._userTabIds.add(addedTabId);
187
+ if (!isNonDebuggableUrl(tab.url)) this._connection.attachTab(tab);
188
+ }
189
+
190
+ /**
191
+ * Attach every open tab not already attached — the connect-time sweep. Tabs opened LATER
192
+ * arrive through `_onTabUpdated`.
193
+ */
194
+ async _attachEverythingElse() {
195
+ let tabs = [];
196
+ try { tabs = await chrome.tabs.query({}); } catch (e) { debugLog("tab query failed:", e); return; }
197
+ for (const tab of tabs) {
198
+ if (tab.id === undefined || this._connection.attachedTabs.has(tab.id)) continue;
199
+ if (isNonDebuggableUrl(tab.url)) continue;
200
+ if (!this._groupTabIds.has(tab.id)) this._userTabIds.add(tab.id);
201
+ this._connection.attachTab(tab);
202
+ }
203
+ }
204
+
205
+ _onTabAttached(tabId) {
206
+ void this._updateBadge(tabId, CONNECTED_BADGE);
207
+ // The user's own tab is attached but NOT grouped: that would relabel it and physically move
208
+ // it in their window.
209
+ if (this._userTabIds.has(tabId)) return;
210
+ void this._addTabToGroup(tabId);
211
+ }
212
+
213
+ /** Detached (drag-out, close, external). Clear the badge; the tab keeps its group
214
+ * membership — the user's intent is still there and a navigation re-attaches. */
215
+ _onTabDetached(tabId) {
216
+ void this._updateBadge(tabId, { text: "" });
217
+ }
218
+
219
+ _onConnectionClose() {
220
+ chrome.tabs.onCreated.removeListener(this._onTabCreatedListener);
221
+ chrome.tabs.onUpdated.removeListener(this._onTabUpdatedListener);
222
+ chrome.tabs.onRemoved.removeListener(this._onTabRemovedListener);
223
+ chrome.tabs.onReplaced.removeListener(this._onTabReplacedListener);
224
+ // Keep the group and its tabs (the next connection adopts them); only clear the badges.
225
+ for (const tabId of this._groupTabIds) void this._updateBadge(tabId, { text: "" });
226
+ this._groupTabIds.clear();
227
+ this.onclose?.();
228
+ }
229
+
230
+ async _updateBadge(tabId, { text, color, title }) {
231
+ try {
232
+ await Promise.all([
233
+ chrome.action.setBadgeText({ tabId, text }),
234
+ chrome.action.setTitle({ tabId, title: title || "" }),
235
+ color ? chrome.action.setBadgeBackgroundColor({ tabId, color }) : Promise.resolve(),
236
+ ]);
237
+ } catch {
238
+ /* tab already closed */
239
+ }
240
+ }
241
+
242
+ /** Move an attached tab into the group, creating the group on first use. */
243
+ async _addTabToGroup(tabId) {
244
+ if (this._groupTabIds.has(tabId)) return;
245
+ // The group can vanish under us: Chrome deletes one whose last tab closed (what `/recycle`
246
+ // does). A stale `_groupId` would make every join throw and leave the agent's tabs outside
247
+ // the group, so a failed join drops the id and retries once via the create branch.
248
+ for (const attempt of [0, 1]) {
249
+ try {
250
+ await this._retryOnDrag(async () => {
251
+ if (this._groupId === null) {
252
+ this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
253
+ await chrome.tabGroups.update(this._groupId, { color: GROUP_COLOR, title: GROUP_TITLE });
254
+ } else {
255
+ await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
256
+ }
257
+ });
258
+ this._groupTabIds.add(tabId);
259
+ return;
260
+ } catch (e) {
261
+ if (attempt === 0 && this._groupId !== null) { this._groupId = null; continue; }
262
+ debugLog("error adding tab to group:", e);
263
+ }
264
+ }
265
+ }
266
+
267
+ /** Chrome throws "user may be dragging a tab" mid-drag; retry until it clears. */
268
+ async _retryOnDrag(fn) {
269
+ const delays = [0, 100, 200, 400, 800];
270
+ let lastError;
271
+ for (const delay of delays) {
272
+ if (delay) await new Promise((r) => setTimeout(r, delay));
273
+ try {
274
+ await fn();
275
+ return;
276
+ } catch (e) {
277
+ if (!String(e?.message).includes("user may be dragging a tab")) throw e;
278
+ lastError = e;
279
+ }
280
+ }
281
+ throw lastError;
282
+ }
283
+ }
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,36 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Mirasim",
4
+ "version": "0.1.0",
5
+ "description": "Connect your browser to Mirasim or the open-source Mirasim Computer Use so your agent can complete web tasks you authorize.",
6
+ "permissions": [
7
+ "debugger",
8
+ "tabs",
9
+ "downloads",
10
+ "tabGroups",
11
+ "storage",
12
+ "nativeMessaging",
13
+ "alarms"
14
+ ],
15
+ "background": {
16
+ "service_worker": "background.js",
17
+ "type": "module"
18
+ },
19
+ "icons": {
20
+ "16": "icons/icon-16.png",
21
+ "32": "icons/icon-32.png",
22
+ "48": "icons/icon-48.png",
23
+ "128": "icons/icon-128.png"
24
+ },
25
+ "action": {
26
+ "default_title": "Mirasim",
27
+ "default_icon": {
28
+ "16": "icons/icon-16.png",
29
+ "32": "icons/icon-32.png"
30
+ }
31
+ },
32
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmKZp4T1ESV0xpq+w0n4wH6tCSpSdXGwUyQCM3vmmsxyX8zn1ITThwNgAzu2AK7J7HO9jhqfjJ5brCoKUui/DDOuPPcGLUWM9zcPNGvsqpCpOONd47MAkQL9Tv2hSotcZv27q9M33I8pUOn5S++g38BQXMZbM1f4ZC/dUAmqIQa4uFc0N7VKbCLt7/CxGl7+d0jrvLc/6cFiWIxDX1d73UoZiTbnUiC879wauYcSBSi21MTtiZyooSh5+6hFykbStxTUV1Pa52KD5VEYDMcQmsJHCXOZxk62Ox6wJ2rtdqkZNs6TpwGK5cfpX8irFoWYTNX0Nfvo8kNTxPZh3fZX4cwIDAQAB",
33
+ "content_security_policy": {
34
+ "extension_pages": "script-src 'self'; object-src 'self'; connect-src ws://127.0.0.1:* ws://localhost:* http://127.0.0.1:* http://localhost:*"
35
+ }
36
+ }
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Portions Copyright (c) Microsoft Corporation (Playwright), licensed under the Apache License,
3
+ * Version 2.0; modified by MIRASIM, INC. See LICENSE and NOTICE.
4
+ *
5
+ * WebSocket connection to the local mirasim bridge.
6
+ *
7
+ * Deliberately dumb: it executes an allow-list of chrome.* commands sent by the
8
+ * bridge (positional params spread into the call) and forwards the matching
9
+ * chrome.* events back. All CDP/tab-model logic lives in the bridge process,
10
+ * where it is testable; the extension is a hand, not a brain.
11
+ *
12
+ * Mechanism adapted from Microsoft Playwright's browser extension
13
+ * (packages/extension, Apache-2.0) — same wire shape, so the failure modes
14
+ * that implementation already paid for (navigation-detach reattach, drag
15
+ * bookkeeping, service-worker keepalive) stay solved.
16
+ */
17
+
18
+ import { RUNTIME_INFO } from "./runtime-info.js";
19
+
20
+ export function debugLog(...args) {
21
+ // eslint-disable-next-line no-console
22
+ console.log("[mirasim-bridge]", ...args);
23
+ }
24
+
25
+ /** chrome.* calls the bridge may invoke — anything else is rejected. */
26
+ const ALLOWED_COMMANDS = new Set([
27
+ "chrome.debugger.attach",
28
+ "chrome.debugger.detach",
29
+ "chrome.debugger.sendCommand",
30
+ "chrome.debugger.getTargets",
31
+ "chrome.tabs.create",
32
+ "chrome.tabs.remove",
33
+ "chrome.tabs.query",
34
+ "chrome.tabs.reload",
35
+ "chrome.downloads.search",
36
+ ]);
37
+
38
+ /** chrome.* events forwarded to the bridge (positional args, as the listener got them). */
39
+ const FORWARDED_EVENTS = [
40
+ "chrome.debugger.onEvent",
41
+ "chrome.debugger.onDetach",
42
+ "chrome.tabs.onCreated",
43
+ "chrome.tabs.onRemoved",
44
+ "chrome.tabs.onReplaced",
45
+ ];
46
+
47
+ /** How soon after a navigation-caused detach we try to re-attach. */
48
+ const REATTACH_DELAY_MS = 150;
49
+ /** How long we wait for the re-attach to stick before giving up on the tab. */
50
+ const REATTACH_VERIFY_MS = 2500;
51
+ /** A tab that re-detaches within this window is not fought over again. */
52
+ const REATTACH_COOLDOWN_MS = 3000;
53
+
54
+ /**
55
+ * How long the bridge may say NOTHING before this connection is presumed dead: three missed
56
+ * pings, since the bridge pings every 20s even when idle (`server.ts` KEEPALIVE_INTERVAL_MS).
57
+ *
58
+ * The only way to notice a peer that is still there but not moving (a host stopped by the OS, a
59
+ * machine resumed from sleep, a wedged event loop): the port stays open, every flag reads
60
+ * "connected", and each command dies on its own 30s timeout.
61
+ */
62
+ const SILENCE_TIMEOUT_MS = 60_000;
63
+ /** Cheap enough to run on a live connection; fine-grained enough that the verdict lands
64
+ * within a ping of the deadline. */
65
+ const SILENCE_CHECK_MS = 15_000;
66
+
67
+ export class RelayConnection {
68
+ /** @param {import("./transport.js").Transport} transport — native port or ws, same messages */
69
+ constructor(transport) {
70
+ this._transport = transport;
71
+ /** Tabs whose debugger this connection holds. */
72
+ this._attachedTabs = new Set();
73
+ this._eventListeners = [];
74
+ this._closed = false;
75
+ this._pendingReattach = new Set();
76
+ this._recentReattach = new Set();
77
+
78
+ /** @type {(() => void) | undefined} */
79
+ this.onclose = undefined;
80
+ /** @type {((tabId: number) => void) | undefined} */
81
+ this.ontabattached = undefined;
82
+ /** @type {((tabId: number) => void) | undefined} */
83
+ this.ontabdetached = undefined;
84
+
85
+ this._lastInbound = Date.now();
86
+ this._inflightCommands = 0;
87
+ this._reloading = false;
88
+ this._silenceTimer = setInterval(() => this._checkSilence(), SILENCE_CHECK_MS);
89
+
90
+ this._installEventForwarders();
91
+ this._transport.onmessage = (message) => void this._onMessage(message);
92
+ this._transport.onclose = () => this._onClose();
93
+ }
94
+
95
+ /** Nothing from the bridge for {@link SILENCE_TIMEOUT_MS}: drop the line so the worker's
96
+ * reconnect loop can build a new one. Closing is safe — a bridge that IS alive answers the
97
+ * next connection, and the tab group survives a reconnect. */
98
+ _checkSilence() {
99
+ if (this._closed) return;
100
+ const quiet = Date.now() - this._lastInbound;
101
+ if (quiet < SILENCE_TIMEOUT_MS) return;
102
+ debugLog(`bridge silent for ${Math.round(quiet / 1000)}s — treating the connection as dead`);
103
+ this.close("Bridge stopped answering");
104
+ }
105
+
106
+ /** Which pipe this connection runs on — 'native' or 'ws'. */
107
+ get transportKind() {
108
+ return this._transport.kind;
109
+ }
110
+
111
+ get attachedTabs() {
112
+ return this._attachedTabs;
113
+ }
114
+
115
+ /** Opens the handshake by saying WHICH browser session the tab ids that follow belong to
116
+ * (the nonce from group.js `browserSessionId`); the bridge decides what to do with it.
117
+ *
118
+ * It must come BEFORE the first `attachTab`: the bridge drops what it remembers about a
119
+ * session it cannot recognise, so a tab announced ahead of the nonce would be forgotten. A
120
+ * bridge that predates this logs it as an unknown event and carries on. */
121
+ didConnect(browserSession) {
122
+ this._send({ method: "extension.connected", params: [browserSession] });
123
+ }
124
+
125
+ /** Ends the initial handshake: the bridge holds client CDP traffic until this arrives,
126
+ * so `Target.setAutoAttach` is always answered from a populated tab model. */
127
+ didInitialize() {
128
+ this._send({ method: "extension.initialized", params: [RUNTIME_INFO] });
129
+ }
130
+
131
+ updateAvailable(version) {
132
+ this._send({ method: "extension.updateAvailable", params: [version] });
133
+ }
134
+
135
+ close(reason) {
136
+ this._transport.close(reason);
137
+ // ws.onclose fires asynchronously; run the bookkeeping now so no CDP event
138
+ // is forwarded to a connection we already decided is gone.
139
+ this._onClose();
140
+ }
141
+
142
+ /** Announce a tab to the bridge as if it had just been created; the bridge responds by
143
+ * attaching the debugger, which flows back through the command channel.
144
+ *
145
+ * Always `{ agent: false }`: tabs the agent opens are created by the bridge, which marks
146
+ * them itself (`createTarget`). */
147
+ attachTab(tab) {
148
+ if (this._closed || (this._attachedTabs.has(tab.id) && !tab.discarded && tab.status !== "unloaded")) return;
149
+ this._send({ method: "chrome.tabs.onCreated", params: [tab, { agent: false }] });
150
+ }
151
+
152
+ /** Detach a tab the user dragged out of the group. chrome.debugger.detach does not fire
153
+ * onDetach for the caller, so the event the bridge needs is synthesized. */
154
+ detachTab(tabId) {
155
+ if (this._closed || !this._attachedTabs.has(tabId)) return;
156
+ chrome.debugger.detach({ tabId }).catch((e) => debugLog("detach failed:", e));
157
+ this._notifyDetached(tabId);
158
+ this._send({ method: "chrome.debugger.onDetach", params: [{ tabId }, "target_closed"] });
159
+ }
160
+
161
+ /**
162
+ * Release a tab the bridge is done with (its last CDP client went away), WITHOUT
163
+ * telling the bridge — it already knows; it asked.
164
+ *
165
+ * The detach is the point: one debugger session per tab is shared by every client, so a later
166
+ * client's `Runtime.enable` lands on an already-enabled domain, Chrome replays no execution
167
+ * contexts, and its `evaluate` hangs forever. A fresh attach brings the replay back. The tab
168
+ * keeps its group membership.
169
+ */
170
+ releaseTab(tabId) {
171
+ if (this._closed) return;
172
+ chrome.debugger.detach({ tabId }).catch(() => {}); // already gone is the good case
173
+ if (this._attachedTabs.has(tabId)) this._notifyDetached(tabId);
174
+ this._pendingReattach.delete(tabId);
175
+ }
176
+
177
+ _notifyAttached(tabId) {
178
+ this._attachedTabs.add(tabId);
179
+ this._pendingReattach.delete(tabId);
180
+ this.ontabattached?.(tabId);
181
+ }
182
+
183
+ _notifyDetached(tabId) {
184
+ this._attachedTabs.delete(tabId);
185
+ this.ontabdetached?.(tabId);
186
+ }
187
+
188
+ _installEventForwarders() {
189
+ for (const fullMethod of FORWARDED_EVENTS) {
190
+ const target = resolveChromeMember(fullMethod);
191
+ const listener = (...args) => this._onChromeEvent(fullMethod, args);
192
+ target.obj[target.name].addListener(listener);
193
+ this._eventListeners.push(() => target.obj[target.name].removeListener(listener));
194
+ }
195
+ }
196
+
197
+ _onClose() {
198
+ if (this._closed) return;
199
+ this._closed = true;
200
+ clearInterval(this._silenceTimer);
201
+ this._pendingReattach.clear();
202
+ this._recentReattach.clear();
203
+ for (const remove of this._eventListeners) remove();
204
+ this._eventListeners = [];
205
+ for (const tabId of [...this._attachedTabs]) {
206
+ chrome.debugger.detach({ tabId }).catch(() => {});
207
+ this._notifyDetached(tabId);
208
+ }
209
+ this.onclose?.();
210
+ }
211
+
212
+
213
+ /** Forward chrome.* events that concern attached tabs; run detach bookkeeping. */
214
+ _onChromeEvent(fullMethod, args) {
215
+ const tabId = this._tabIdForEvent(fullMethod, args);
216
+ if (tabId === undefined) return;
217
+ if (!this._attachedTabs.has(tabId) && !this._forwardsWhenDetached(fullMethod)) return;
218
+ this._send({ method: fullMethod, params: args });
219
+ if (fullMethod === "chrome.debugger.onDetach") {
220
+ const reason = args[1];
221
+ this._notifyDetached(tabId);
222
+ // A cross-process navigation detaches the debugger with `target_closed` even though
223
+ // the tab lives on. Re-attach after a beat; a tab that detaches again immediately is
224
+ // left alone (something else owns it).
225
+ if (reason === "target_closed" && this._maybeScheduleReattach(tabId)) return;
226
+ } else if (fullMethod === "chrome.tabs.onReplaced") {
227
+ this._notifyDetached(tabId);
228
+ }
229
+ }
230
+
231
+ _maybeScheduleReattach(tabId) {
232
+ if (this._closed) return false;
233
+ if (this._recentReattach.has(tabId)) return false;
234
+ this._recentReattach.add(tabId);
235
+ setTimeout(() => this._recentReattach.delete(tabId), REATTACH_COOLDOWN_MS);
236
+ this._pendingReattach.add(tabId);
237
+ setTimeout(() => void this._tryReattach(tabId), REATTACH_DELAY_MS);
238
+ return true;
239
+ }
240
+
241
+ async _tryReattach(tabId) {
242
+ if (this._closed || !this._pendingReattach.has(tabId)) return;
243
+ let tab;
244
+ try {
245
+ tab = await chrome.tabs.get(tabId);
246
+ } catch {
247
+ this._pendingReattach.delete(tabId);
248
+ return;
249
+ }
250
+ if (this._closed || !this._pendingReattach.has(tabId)) return;
251
+ if (this._attachedTabs.has(tabId)) {
252
+ this._pendingReattach.delete(tabId);
253
+ return;
254
+ }
255
+ this.attachTab(tab);
256
+ setTimeout(() => {
257
+ if (this._closed || !this._pendingReattach.has(tabId)) return;
258
+ this._pendingReattach.delete(tabId);
259
+ if (!this._attachedTabs.has(tabId)) debugLog("gave up re-attaching tab", tabId);
260
+ }, REATTACH_VERIFY_MS);
261
+ }
262
+
263
+ _tabIdForEvent(fullMethod, args) {
264
+ switch (fullMethod) {
265
+ case "chrome.debugger.onEvent":
266
+ case "chrome.debugger.onDetach":
267
+ return args[0]?.tabId;
268
+ case "chrome.tabs.onCreated":
269
+ // Only popups opened by an attached tab are the bridge's business.
270
+ return args[0]?.openerTabId;
271
+ case "chrome.tabs.onRemoved":
272
+ return args[0];
273
+ case "chrome.tabs.onReplaced":
274
+ return args[1]; // removed id; Chrome assigns a new id when discarding a tab
275
+ default:
276
+ return undefined;
277
+ }
278
+ }
279
+
280
+ /** Does this event still concern the bridge even though we hold no debugger for the tab?
281
+ * Closures do: a tab released between tasks is still one the bridge lists and may hand to
282
+ * the next client, and a client handed a closed tab waits on a renderer that is gone. */
283
+ _forwardsWhenDetached(fullMethod) {
284
+ return fullMethod === "chrome.tabs.onRemoved" || fullMethod === "chrome.tabs.onReplaced";
285
+ }
286
+
287
+ async _onMessage(message) {
288
+ this._lastInbound = Date.now();
289
+ this._inflightCommands += 1;
290
+ const response = { id: message.id };
291
+ try {
292
+ response.result = await this._handleCommand(message);
293
+ } catch (e) {
294
+ response.error = e?.message ?? String(e);
295
+ } finally {
296
+ this._inflightCommands -= 1;
297
+ }
298
+ this._send(response);
299
+ }
300
+
301
+ async _handleCommand(message) {
302
+ if (this._reloading) throw new Error("The Mirasim extension is reloading; reconnect before continuing.");
303
+ if (message.method === "bridge.reload") {
304
+ // The bridge also waits for all CDP clients, including clients between tool calls.
305
+ // This last guard protects against commands or attachments that have not drained yet.
306
+ if (this._inflightCommands !== 1 || this._attachedTabs.size) throw new Error("The Mirasim extension is busy; finish the browser task before reloading.");
307
+ this._reloading = true;
308
+ setTimeout(() => chrome.runtime.reload(), 100); // send the receipt before closing the port
309
+ return { scheduled: true };
310
+ }
311
+ // Application-level keepalive: receiving and answering it resets the MV3
312
+ // service-worker idle timer in both directions.
313
+ if (message.method === "bridge.ping") return {};
314
+ if (message.method === "bridge.releaseTab") {
315
+ this.releaseTab((message.params ?? [])[0]);
316
+ return {};
317
+ }
318
+ if (!ALLOWED_COMMANDS.has(message.method)) throw new Error(`Unknown method: ${message.method}`);
319
+ const args = message.params ?? [];
320
+ if (message.method === "chrome.debugger.attach") {
321
+ const target = args[0];
322
+ // A zombie attach from a previous service-worker life throws "Another debugger is
323
+ // already attached" against ourselves. Clearing our own leftover is safe; a REAL
324
+ // other debugger (devtools) re-throws on the second try and that error is honest.
325
+ try {
326
+ await invokeChromeMethod(message.method, args);
327
+ } catch (e) {
328
+ if (!String(e?.message).includes("already attached")) throw e;
329
+ await chrome.debugger.detach({ tabId: target?.tabId }).catch(() => {});
330
+ await invokeChromeMethod(message.method, args);
331
+ }
332
+ if (target?.tabId !== undefined) this._notifyAttached(target.tabId);
333
+ return {};
334
+ }
335
+ const result = await invokeChromeMethod(message.method, args);
336
+ return result ?? {};
337
+ }
338
+
339
+ _send(message) {
340
+ this._transport.send(message);
341
+ }
342
+ }
343
+
344
+ /** Resolve chrome.<api>.<member> for reflective invocation and listener setup. */
345
+ function resolveChromeMember(fullMethod) {
346
+ const parts = fullMethod.split(".");
347
+ if (parts[0] !== "chrome" || parts.length < 3) throw new Error(`Invalid chrome method: ${fullMethod}`);
348
+ let obj = chrome;
349
+ for (let i = 1; i < parts.length - 1; i++) {
350
+ obj = obj?.[parts[i]];
351
+ if (obj === undefined) throw new Error(`Unknown chrome path in ${fullMethod}`);
352
+ }
353
+ return { obj, name: parts[parts.length - 1] };
354
+ }
355
+
356
+ async function invokeChromeMethod(fullMethod, args) {
357
+ const { obj, name } = resolveChromeMember(fullMethod);
358
+ const fn = obj[name];
359
+ if (typeof fn !== "function") throw new Error(`Not a function: ${fullMethod}`);
360
+ return await fn.apply(obj, args);
361
+ }