@openagt/dashboard 0.0.0-stage → 0.9.0

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 (610) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -2
  3. package/dist/agent-id.d.ts +13 -0
  4. package/dist/agent-id.d.ts.map +1 -0
  5. package/dist/agent-id.js +20 -0
  6. package/dist/agent-id.js.map +1 -0
  7. package/dist/agent-locks.d.ts +3 -0
  8. package/dist/agent-locks.d.ts.map +1 -0
  9. package/dist/agent-locks.js +40 -0
  10. package/dist/agent-locks.js.map +1 -0
  11. package/dist/agent-view.d.ts +39 -0
  12. package/dist/agent-view.d.ts.map +1 -0
  13. package/dist/agent-view.js +45 -0
  14. package/dist/agent-view.js.map +1 -0
  15. package/dist/bin.d.ts +3 -0
  16. package/dist/bin.d.ts.map +1 -0
  17. package/dist/bin.js +11 -0
  18. package/dist/bin.js.map +1 -0
  19. package/dist/branch-name.d.ts +8 -0
  20. package/dist/branch-name.d.ts.map +1 -0
  21. package/dist/branch-name.js +20 -0
  22. package/dist/branch-name.js.map +1 -0
  23. package/dist/bridge-browser.d.ts +217 -0
  24. package/dist/bridge-browser.d.ts.map +1 -0
  25. package/dist/bridge-browser.js +520 -0
  26. package/dist/bridge-browser.js.map +1 -0
  27. package/dist/browser-stream.d.ts +20 -0
  28. package/dist/browser-stream.d.ts.map +1 -0
  29. package/dist/browser-stream.js +51 -0
  30. package/dist/browser-stream.js.map +1 -0
  31. package/dist/browser.d.ts +16 -0
  32. package/dist/browser.d.ts.map +1 -0
  33. package/dist/browser.js +38 -0
  34. package/dist/browser.js.map +1 -0
  35. package/dist/built-in.d.ts +38 -0
  36. package/dist/built-in.d.ts.map +1 -0
  37. package/dist/built-in.js +119 -0
  38. package/dist/built-in.js.map +1 -0
  39. package/dist/chrome-extension/background.js +532 -0
  40. package/dist/chrome-extension/content.js +1475 -0
  41. package/dist/chrome-extension/driver-plan.js +41 -0
  42. package/dist/chrome-extension/fingerprint.js +43 -0
  43. package/dist/chrome-extension/manifest.json +31 -0
  44. package/dist/chrome-extension/options.html +48 -0
  45. package/dist/chrome-extension/options.js +122 -0
  46. package/dist/cli.d.ts +43 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +186 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/client.d.ts +16 -0
  51. package/dist/client.d.ts.map +1 -0
  52. package/dist/client.js +30 -0
  53. package/dist/client.js.map +1 -0
  54. package/dist/cloud-run-state.d.ts +27 -0
  55. package/dist/cloud-run-state.d.ts.map +1 -0
  56. package/dist/cloud-run-state.js +36 -0
  57. package/dist/cloud-run-state.js.map +1 -0
  58. package/dist/cloud-scratch-refs.d.ts +85 -0
  59. package/dist/cloud-scratch-refs.d.ts.map +1 -0
  60. package/dist/cloud-scratch-refs.js +275 -0
  61. package/dist/cloud-scratch-refs.js.map +1 -0
  62. package/dist/cloud-work.d.ts +66 -0
  63. package/dist/cloud-work.d.ts.map +1 -0
  64. package/dist/cloud-work.js +204 -0
  65. package/dist/cloud-work.js.map +1 -0
  66. package/dist/daemon-runtime.d.ts +42 -0
  67. package/dist/daemon-runtime.d.ts.map +1 -0
  68. package/dist/daemon-runtime.js +183 -0
  69. package/dist/daemon-runtime.js.map +1 -0
  70. package/dist/daemon-services.d.ts +37 -0
  71. package/dist/daemon-services.d.ts.map +1 -0
  72. package/dist/daemon-services.js +126 -0
  73. package/dist/daemon-services.js.map +1 -0
  74. package/dist/daemon-tick.d.ts +68 -0
  75. package/dist/daemon-tick.d.ts.map +1 -0
  76. package/dist/daemon-tick.js +97 -0
  77. package/dist/daemon-tick.js.map +1 -0
  78. package/dist/daemon.d.ts +65 -0
  79. package/dist/daemon.d.ts.map +1 -0
  80. package/dist/daemon.js +202 -0
  81. package/dist/daemon.js.map +1 -0
  82. package/dist/dashboard/activity.d.ts +41 -0
  83. package/dist/dashboard/activity.d.ts.map +1 -0
  84. package/dist/dashboard/activity.js +56 -0
  85. package/dist/dashboard/activity.js.map +1 -0
  86. package/dist/dashboard/agent-handoff.d.ts +244 -0
  87. package/dist/dashboard/agent-handoff.d.ts.map +1 -0
  88. package/dist/dashboard/agent-handoff.js +310 -0
  89. package/dist/dashboard/agent-handoff.js.map +1 -0
  90. package/dist/dashboard/bridge-endpoints.d.ts +158 -0
  91. package/dist/dashboard/bridge-endpoints.d.ts.map +1 -0
  92. package/dist/dashboard/bridge-endpoints.js +382 -0
  93. package/dist/dashboard/bridge-endpoints.js.map +1 -0
  94. package/dist/dashboard/bridge-question.d.ts +33 -0
  95. package/dist/dashboard/bridge-question.d.ts.map +1 -0
  96. package/dist/dashboard/bridge-question.js +20 -0
  97. package/dist/dashboard/bridge-question.js.map +1 -0
  98. package/dist/dashboard/bridge-sessions.d.ts +25 -0
  99. package/dist/dashboard/bridge-sessions.d.ts.map +1 -0
  100. package/dist/dashboard/bridge-sessions.js +46 -0
  101. package/dist/dashboard/bridge-sessions.js.map +1 -0
  102. package/dist/dashboard/bridge-starts.d.ts +61 -0
  103. package/dist/dashboard/bridge-starts.d.ts.map +1 -0
  104. package/dist/dashboard/bridge-starts.js +94 -0
  105. package/dist/dashboard/bridge-starts.js.map +1 -0
  106. package/dist/dashboard/bridge-store.d.ts +179 -0
  107. package/dist/dashboard/bridge-store.d.ts.map +1 -0
  108. package/dist/dashboard/bridge-store.js +262 -0
  109. package/dist/dashboard/bridge-store.js.map +1 -0
  110. package/dist/dashboard/bundle.d.ts +12 -0
  111. package/dist/dashboard/bundle.d.ts.map +1 -0
  112. package/dist/dashboard/bundle.js +22 -0
  113. package/dist/dashboard/bundle.js.map +1 -0
  114. package/dist/dashboard/cache.d.ts +47 -0
  115. package/dist/dashboard/cache.d.ts.map +1 -0
  116. package/dist/dashboard/cache.js +87 -0
  117. package/dist/dashboard/cache.js.map +1 -0
  118. package/dist/dashboard/content-type.d.ts +3 -0
  119. package/dist/dashboard/content-type.d.ts.map +1 -0
  120. package/dist/dashboard/content-type.js +31 -0
  121. package/dist/dashboard/content-type.js.map +1 -0
  122. package/dist/dashboard/dashboard.d.ts +38 -0
  123. package/dist/dashboard/dashboard.d.ts.map +1 -0
  124. package/dist/dashboard/dashboard.js +42 -0
  125. package/dist/dashboard/dashboard.js.map +1 -0
  126. package/dist/dashboard/docs.d.ts +39 -0
  127. package/dist/dashboard/docs.d.ts.map +1 -0
  128. package/dist/dashboard/docs.js +59 -0
  129. package/dist/dashboard/docs.js.map +1 -0
  130. package/dist/dashboard/git-status.d.ts +44 -0
  131. package/dist/dashboard/git-status.d.ts.map +1 -0
  132. package/dist/dashboard/git-status.js +37 -0
  133. package/dist/dashboard/git-status.js.map +1 -0
  134. package/dist/dashboard/http.d.ts +29 -0
  135. package/dist/dashboard/http.d.ts.map +1 -0
  136. package/dist/dashboard/http.js +74 -0
  137. package/dist/dashboard/http.js.map +1 -0
  138. package/dist/dashboard/index.d.ts +24 -0
  139. package/dist/dashboard/index.d.ts.map +1 -0
  140. package/dist/dashboard/index.js +23 -0
  141. package/dist/dashboard/index.js.map +1 -0
  142. package/dist/dashboard/interventions.d.ts +67 -0
  143. package/dist/dashboard/interventions.d.ts.map +1 -0
  144. package/dist/dashboard/interventions.js +169 -0
  145. package/dist/dashboard/interventions.js.map +1 -0
  146. package/dist/dashboard/keyed-watcher.d.ts +29 -0
  147. package/dist/dashboard/keyed-watcher.d.ts.map +1 -0
  148. package/dist/dashboard/keyed-watcher.js +42 -0
  149. package/dist/dashboard/keyed-watcher.js.map +1 -0
  150. package/dist/dashboard/keys.d.ts +31 -0
  151. package/dist/dashboard/keys.d.ts.map +1 -0
  152. package/dist/dashboard/keys.js +37 -0
  153. package/dist/dashboard/keys.js.map +1 -0
  154. package/dist/dashboard/models.d.ts +25 -0
  155. package/dist/dashboard/models.d.ts.map +1 -0
  156. package/dist/dashboard/models.js +30 -0
  157. package/dist/dashboard/models.js.map +1 -0
  158. package/dist/dashboard/module-host.d.ts +44 -0
  159. package/dist/dashboard/module-host.d.ts.map +1 -0
  160. package/dist/dashboard/module-host.js +91 -0
  161. package/dist/dashboard/module-host.js.map +1 -0
  162. package/dist/dashboard/module-serve.d.ts +15 -0
  163. package/dist/dashboard/module-serve.d.ts.map +1 -0
  164. package/dist/dashboard/module-serve.js +45 -0
  165. package/dist/dashboard/module-serve.js.map +1 -0
  166. package/dist/dashboard/open-in-app.d.ts +43 -0
  167. package/dist/dashboard/open-in-app.d.ts.map +1 -0
  168. package/dist/dashboard/open-in-app.js +98 -0
  169. package/dist/dashboard/open-in-app.js.map +1 -0
  170. package/dist/dashboard/open-questions.d.ts +45 -0
  171. package/dist/dashboard/open-questions.d.ts.map +1 -0
  172. package/dist/dashboard/open-questions.js +62 -0
  173. package/dist/dashboard/open-questions.js.map +1 -0
  174. package/dist/dashboard/overview.d.ts +78 -0
  175. package/dist/dashboard/overview.d.ts.map +1 -0
  176. package/dist/dashboard/overview.js +87 -0
  177. package/dist/dashboard/overview.js.map +1 -0
  178. package/dist/dashboard/projects.d.ts +101 -0
  179. package/dist/dashboard/projects.d.ts.map +1 -0
  180. package/dist/dashboard/projects.js +63 -0
  181. package/dist/dashboard/projects.js.map +1 -0
  182. package/dist/dashboard/pull-requests.d.ts +127 -0
  183. package/dist/dashboard/pull-requests.d.ts.map +1 -0
  184. package/dist/dashboard/pull-requests.js +137 -0
  185. package/dist/dashboard/pull-requests.js.map +1 -0
  186. package/dist/dashboard/queue.d.ts +17 -0
  187. package/dist/dashboard/queue.d.ts.map +1 -0
  188. package/dist/dashboard/queue.js +19 -0
  189. package/dist/dashboard/queue.js.map +1 -0
  190. package/dist/dashboard/quota.d.ts +53 -0
  191. package/dist/dashboard/quota.d.ts.map +1 -0
  192. package/dist/dashboard/quota.js +42 -0
  193. package/dist/dashboard/quota.js.map +1 -0
  194. package/dist/dashboard/relay-endpoints.d.ts +32 -0
  195. package/dist/dashboard/relay-endpoints.d.ts.map +1 -0
  196. package/dist/dashboard/relay-endpoints.js +115 -0
  197. package/dist/dashboard/relay-endpoints.js.map +1 -0
  198. package/dist/dashboard/remote-run.d.ts +95 -0
  199. package/dist/dashboard/remote-run.d.ts.map +1 -0
  200. package/dist/dashboard/remote-run.js +235 -0
  201. package/dist/dashboard/remote-run.js.map +1 -0
  202. package/dist/dashboard/rpc-serve.d.ts +106 -0
  203. package/dist/dashboard/rpc-serve.d.ts.map +1 -0
  204. package/dist/dashboard/rpc-serve.js +180 -0
  205. package/dist/dashboard/rpc-serve.js.map +1 -0
  206. package/dist/dashboard/run-inbox.d.ts +31 -0
  207. package/dist/dashboard/run-inbox.d.ts.map +1 -0
  208. package/dist/dashboard/run-inbox.js +51 -0
  209. package/dist/dashboard/run-inbox.js.map +1 -0
  210. package/dist/dashboard/server.d.ts +105 -0
  211. package/dist/dashboard/server.d.ts.map +1 -0
  212. package/dist/dashboard/server.js +231 -0
  213. package/dist/dashboard/server.js.map +1 -0
  214. package/dist/dashboard/static.d.ts +9 -0
  215. package/dist/dashboard/static.d.ts.map +1 -0
  216. package/dist/dashboard/static.js +54 -0
  217. package/dist/dashboard/static.js.map +1 -0
  218. package/dist/dashboard/types.d.ts +135 -0
  219. package/dist/dashboard/types.d.ts.map +1 -0
  220. package/dist/dashboard/types.js +2 -0
  221. package/dist/dashboard/types.js.map +1 -0
  222. package/dist/dashboard/web-start-endpoints.d.ts +32 -0
  223. package/dist/dashboard/web-start-endpoints.d.ts.map +1 -0
  224. package/dist/dashboard/web-start-endpoints.js +75 -0
  225. package/dist/dashboard/web-start-endpoints.js.map +1 -0
  226. package/dist/dashboard-bundle/assets/client-CbBRB9m4.js +9 -0
  227. package/dist/dashboard-bundle/assets/jsx-runtime-By8HlURe.js +1 -0
  228. package/dist/dashboard-bundle/assets/main-B98UOA3_.js +140 -0
  229. package/dist/dashboard-bundle/assets/main-ByRjbCiA.css +2 -0
  230. package/dist/dashboard-bundle/assets/module-v213KWfH.js +21 -0
  231. package/dist/dashboard-bundle/assets/react-CZI7_Jkm.js +1 -0
  232. package/dist/dashboard-bundle/assets/react-dom-cJyBB_A4.js +1 -0
  233. package/dist/dashboard-bundle/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  234. package/dist/dashboard-bundle/host/module.js +1 -0
  235. package/dist/dashboard-bundle/host/react-dom-client.js +1 -0
  236. package/dist/dashboard-bundle/host/react-dom.js +1 -0
  237. package/dist/dashboard-bundle/host/react-jsx-runtime.js +1 -0
  238. package/dist/dashboard-bundle/host/react.js +1 -0
  239. package/dist/dashboard-bundle/index.html +42 -0
  240. package/dist/dashboard-bundle/logo-animated.svg +34 -0
  241. package/dist/dashboard-bundle/logo.svg +16 -0
  242. package/dist/dashboard-rpc/context.d.ts +56 -0
  243. package/dist/dashboard-rpc/context.d.ts.map +1 -0
  244. package/dist/dashboard-rpc/context.js +106 -0
  245. package/dist/dashboard-rpc/context.js.map +1 -0
  246. package/dist/dashboard-rpc/control.d.ts +124 -0
  247. package/dist/dashboard-rpc/control.d.ts.map +1 -0
  248. package/dist/dashboard-rpc/control.js +284 -0
  249. package/dist/dashboard-rpc/control.js.map +1 -0
  250. package/dist/dashboard-rpc/devices.d.ts +9 -0
  251. package/dist/dashboard-rpc/devices.d.ts.map +1 -0
  252. package/dist/dashboard-rpc/devices.js +12 -0
  253. package/dist/dashboard-rpc/devices.js.map +1 -0
  254. package/dist/dashboard-rpc/events-tail.d.ts +68 -0
  255. package/dist/dashboard-rpc/events-tail.d.ts.map +1 -0
  256. package/dist/dashboard-rpc/events-tail.js +257 -0
  257. package/dist/dashboard-rpc/events-tail.js.map +1 -0
  258. package/dist/dashboard-rpc/events.d.ts +39 -0
  259. package/dist/dashboard-rpc/events.d.ts.map +1 -0
  260. package/dist/dashboard-rpc/events.js +47 -0
  261. package/dist/dashboard-rpc/events.js.map +1 -0
  262. package/dist/dashboard-rpc/index.d.ts +29 -0
  263. package/dist/dashboard-rpc/index.d.ts.map +1 -0
  264. package/dist/dashboard-rpc/index.js +40 -0
  265. package/dist/dashboard-rpc/index.js.map +1 -0
  266. package/dist/dashboard-rpc/models.d.ts +4 -0
  267. package/dist/dashboard-rpc/models.d.ts.map +1 -0
  268. package/dist/dashboard-rpc/models.js +8 -0
  269. package/dist/dashboard-rpc/models.js.map +1 -0
  270. package/dist/dashboard-rpc/modules.d.ts +45 -0
  271. package/dist/dashboard-rpc/modules.d.ts.map +1 -0
  272. package/dist/dashboard-rpc/modules.js +79 -0
  273. package/dist/dashboard-rpc/modules.js.map +1 -0
  274. package/dist/dashboard-rpc/preferences.d.ts +41 -0
  275. package/dist/dashboard-rpc/preferences.d.ts.map +1 -0
  276. package/dist/dashboard-rpc/preferences.js +65 -0
  277. package/dist/dashboard-rpc/preferences.js.map +1 -0
  278. package/dist/dashboard-rpc/projects.d.ts +85 -0
  279. package/dist/dashboard-rpc/projects.d.ts.map +1 -0
  280. package/dist/dashboard-rpc/projects.js +129 -0
  281. package/dist/dashboard-rpc/projects.js.map +1 -0
  282. package/dist/dashboard-rpc/quota.d.ts +4 -0
  283. package/dist/dashboard-rpc/quota.d.ts.map +1 -0
  284. package/dist/dashboard-rpc/quota.js +15 -0
  285. package/dist/dashboard-rpc/quota.js.map +1 -0
  286. package/dist/dashboard-rpc/reads.d.ts +185 -0
  287. package/dist/dashboard-rpc/reads.d.ts.map +1 -0
  288. package/dist/dashboard-rpc/reads.js +370 -0
  289. package/dist/dashboard-rpc/reads.js.map +1 -0
  290. package/dist/dashboard-rpc/relay-agent.d.ts +9 -0
  291. package/dist/dashboard-rpc/relay-agent.d.ts.map +1 -0
  292. package/dist/dashboard-rpc/relay-agent.js +21 -0
  293. package/dist/dashboard-rpc/relay-agent.js.map +1 -0
  294. package/dist/dashboard-rpc/relay-dispatch.d.ts +9 -0
  295. package/dist/dashboard-rpc/relay-dispatch.d.ts.map +1 -0
  296. package/dist/dashboard-rpc/relay-dispatch.js +24 -0
  297. package/dist/dashboard-rpc/relay-dispatch.js.map +1 -0
  298. package/dist/dashboard-rpc/stream-forward.d.ts +17 -0
  299. package/dist/dashboard-rpc/stream-forward.d.ts.map +1 -0
  300. package/dist/dashboard-rpc/stream-forward.js +38 -0
  301. package/dist/dashboard-rpc/stream-forward.js.map +1 -0
  302. package/dist/dashboard-rpc/test-context.d.ts +23 -0
  303. package/dist/dashboard-rpc/test-context.d.ts.map +1 -0
  304. package/dist/dashboard-rpc/test-context.js +43 -0
  305. package/dist/dashboard-rpc/test-context.js.map +1 -0
  306. package/dist/driver-names.d.ts +22 -0
  307. package/dist/driver-names.d.ts.map +1 -0
  308. package/dist/driver-names.js +29 -0
  309. package/dist/driver-names.js.map +1 -0
  310. package/dist/error-message.d.ts +8 -0
  311. package/dist/error-message.d.ts.map +1 -0
  312. package/dist/error-message.js +10 -0
  313. package/dist/error-message.js.map +1 -0
  314. package/dist/event-stream.d.ts +33 -0
  315. package/dist/event-stream.d.ts.map +1 -0
  316. package/dist/event-stream.js +104 -0
  317. package/dist/event-stream.js.map +1 -0
  318. package/dist/events.d.ts +236 -0
  319. package/dist/events.d.ts.map +1 -0
  320. package/dist/events.js +5 -0
  321. package/dist/events.js.map +1 -0
  322. package/dist/format-bytes.d.ts +3 -0
  323. package/dist/format-bytes.d.ts.map +1 -0
  324. package/dist/format-bytes.js +18 -0
  325. package/dist/format-bytes.js.map +1 -0
  326. package/dist/has-remote.d.ts +8 -0
  327. package/dist/has-remote.d.ts.map +1 -0
  328. package/dist/has-remote.js +10 -0
  329. package/dist/has-remote.js.map +1 -0
  330. package/dist/index.d.ts +42 -0
  331. package/dist/index.d.ts.map +1 -0
  332. package/dist/index.js +16 -0
  333. package/dist/index.js.map +1 -0
  334. package/dist/install.d.ts +31 -0
  335. package/dist/install.d.ts.map +1 -0
  336. package/dist/install.js +54 -0
  337. package/dist/install.js.map +1 -0
  338. package/dist/jsonl-tail.d.ts +59 -0
  339. package/dist/jsonl-tail.d.ts.map +1 -0
  340. package/dist/jsonl-tail.js +152 -0
  341. package/dist/jsonl-tail.js.map +1 -0
  342. package/dist/loopback-host.d.ts +20 -0
  343. package/dist/loopback-host.d.ts.map +1 -0
  344. package/dist/loopback-host.js +31 -0
  345. package/dist/loopback-host.js.map +1 -0
  346. package/dist/module-server.d.ts +56 -0
  347. package/dist/module-types/dashboard/components/DiffStat.d.ts +6 -0
  348. package/dist/module-types/dashboard/components/DriverModelMenu.d.ts +23 -0
  349. package/dist/module-types/dashboard/components/LinkActions.d.ts +26 -0
  350. package/dist/module-types/dashboard/components/Markdown.d.ts +4 -0
  351. package/dist/module-types/dashboard/components/SettingsRows.d.ts +37 -0
  352. package/dist/module-types/dashboard/components/StartAgentButton.d.ts +39 -0
  353. package/dist/module-types/dashboard/components/ui/badge.d.ts +2 -0
  354. package/dist/module-types/dashboard/components/ui/button.d.ts +10 -0
  355. package/dist/module-types/dashboard/components/ui/card.d.ts +5 -0
  356. package/dist/module-types/dashboard/components/ui/checkbox.d.ts +7 -0
  357. package/dist/module-types/dashboard/components/ui/dropdown-menu.d.ts +16 -0
  358. package/dist/module-types/dashboard/components/ui/input.d.ts +2 -0
  359. package/dist/module-types/dashboard/components/ui/option-label.d.ts +4 -0
  360. package/dist/module-types/dashboard/components/ui/popover.d.ts +8 -0
  361. package/dist/module-types/dashboard/components/ui/scroll-area.d.ts +10 -0
  362. package/dist/module-types/dashboard/components/ui/separator.d.ts +3 -0
  363. package/dist/module-types/dashboard/components/ui/skeleton.d.ts +2 -0
  364. package/dist/module-types/dashboard/components/ui/slider.d.ts +9 -0
  365. package/dist/module-types/dashboard/components/ui/tooltip.d.ts +11 -0
  366. package/dist/module-types/dashboard/lib/agent-settings.d.ts +2 -0
  367. package/dist/module-types/dashboard/lib/draft-handoff.d.ts +15 -0
  368. package/dist/module-types/dashboard/lib/format-date.d.ts +51 -0
  369. package/dist/module-types/dashboard/lib/host-services.d.ts +12 -0
  370. package/dist/module-types/dashboard/lib/models.d.ts +24 -0
  371. package/dist/module-types/dashboard/lib/route.d.ts +38 -0
  372. package/dist/module-types/dashboard/lib/rpc.d.ts +31 -0
  373. package/dist/module-types/dashboard/lib/use-action.d.ts +25 -0
  374. package/dist/module-types/dashboard/lib/use-async.d.ts +45 -0
  375. package/dist/module-types/dashboard/lib/use-modules.d.ts +79 -0
  376. package/dist/module-types/dashboard/lib/utils.d.ts +3 -0
  377. package/dist/module-types/dashboard/module/index.d.ts +335 -0
  378. package/dist/module-types/dashboard/rpc/models.d.ts +2 -0
  379. package/dist/module-types/dashboard/rpc/modules.d.ts +5 -0
  380. package/dist/module-types/src/agent-id.d.ts +12 -0
  381. package/dist/module-types/src/agent-locks.d.ts +2 -0
  382. package/dist/module-types/src/agent-view.d.ts +38 -0
  383. package/dist/module-types/src/bridge-browser.d.ts +216 -0
  384. package/dist/module-types/src/browser-stream.d.ts +19 -0
  385. package/dist/module-types/src/browser.d.ts +15 -0
  386. package/dist/module-types/src/built-in.d.ts +37 -0
  387. package/dist/module-types/src/client.d.ts +15 -0
  388. package/dist/module-types/src/cloud-run-state.d.ts +26 -0
  389. package/dist/module-types/src/dashboard/activity.d.ts +40 -0
  390. package/dist/module-types/src/dashboard/agent-handoff.d.ts +243 -0
  391. package/dist/module-types/src/dashboard/bridge-endpoints.d.ts +157 -0
  392. package/dist/module-types/src/dashboard/bridge-question.d.ts +32 -0
  393. package/dist/module-types/src/dashboard/bridge-store.d.ts +178 -0
  394. package/dist/module-types/src/dashboard/cache.d.ts +46 -0
  395. package/dist/module-types/src/dashboard/content-type.d.ts +2 -0
  396. package/dist/module-types/src/dashboard/dashboard.d.ts +37 -0
  397. package/dist/module-types/src/dashboard/docs.d.ts +38 -0
  398. package/dist/module-types/src/dashboard/git-status.d.ts +43 -0
  399. package/dist/module-types/src/dashboard/http.d.ts +28 -0
  400. package/dist/module-types/src/dashboard/interventions.d.ts +66 -0
  401. package/dist/module-types/src/dashboard/keyed-watcher.d.ts +28 -0
  402. package/dist/module-types/src/dashboard/keys.d.ts +30 -0
  403. package/dist/module-types/src/dashboard/models.d.ts +24 -0
  404. package/dist/module-types/src/dashboard/module-host.d.ts +43 -0
  405. package/dist/module-types/src/dashboard/module-serve.d.ts +14 -0
  406. package/dist/module-types/src/dashboard/open-in-app.d.ts +42 -0
  407. package/dist/module-types/src/dashboard/open-questions.d.ts +44 -0
  408. package/dist/module-types/src/dashboard/overview.d.ts +77 -0
  409. package/dist/module-types/src/dashboard/projects.d.ts +100 -0
  410. package/dist/module-types/src/dashboard/pull-requests.d.ts +126 -0
  411. package/dist/module-types/src/dashboard/queue.d.ts +16 -0
  412. package/dist/module-types/src/dashboard/quota.d.ts +52 -0
  413. package/dist/module-types/src/dashboard/remote-run.d.ts +94 -0
  414. package/dist/module-types/src/dashboard/rpc-serve.d.ts +105 -0
  415. package/dist/module-types/src/dashboard/run-inbox.d.ts +30 -0
  416. package/dist/module-types/src/dashboard/types.d.ts +134 -0
  417. package/dist/module-types/src/dashboard-rpc/context.d.ts +55 -0
  418. package/dist/module-types/src/dashboard-rpc/control.d.ts +123 -0
  419. package/dist/module-types/src/dashboard-rpc/devices.d.ts +8 -0
  420. package/dist/module-types/src/dashboard-rpc/events-tail.d.ts +67 -0
  421. package/dist/module-types/src/dashboard-rpc/events.d.ts +38 -0
  422. package/dist/module-types/src/dashboard-rpc/index.d.ts +28 -0
  423. package/dist/module-types/src/dashboard-rpc/models.d.ts +3 -0
  424. package/dist/module-types/src/dashboard-rpc/modules.d.ts +44 -0
  425. package/dist/module-types/src/dashboard-rpc/preferences.d.ts +40 -0
  426. package/dist/module-types/src/dashboard-rpc/projects.d.ts +84 -0
  427. package/dist/module-types/src/dashboard-rpc/quota.d.ts +3 -0
  428. package/dist/module-types/src/dashboard-rpc/reads.d.ts +184 -0
  429. package/dist/module-types/src/dashboard-rpc/relay-agent.d.ts +8 -0
  430. package/dist/module-types/src/dashboard-rpc/stream-forward.d.ts +16 -0
  431. package/dist/module-types/src/driver-names.d.ts +21 -0
  432. package/dist/module-types/src/error-message.d.ts +7 -0
  433. package/dist/module-types/src/event-stream.d.ts +32 -0
  434. package/dist/module-types/src/events.d.ts +235 -0
  435. package/dist/module-types/src/format-bytes.d.ts +2 -0
  436. package/dist/module-types/src/has-remote.d.ts +7 -0
  437. package/dist/module-types/src/index.d.ts +41 -0
  438. package/dist/module-types/src/jsonl-tail.d.ts +58 -0
  439. package/dist/module-types/src/loopback-host.d.ts +19 -0
  440. package/dist/module-types/src/node-fs.d.ts +32 -0
  441. package/dist/module-types/src/open-choices.d.ts +14 -0
  442. package/dist/module-types/src/openagent-dir.d.ts +10 -0
  443. package/dist/module-types/src/openagent-gitignore.d.ts +11 -0
  444. package/dist/module-types/src/pick-directory.d.ts +29 -0
  445. package/dist/module-types/src/preference-defaults.d.ts +42 -0
  446. package/dist/module-types/src/project-commands.d.ts +23 -0
  447. package/dist/module-types/src/project-errors.d.ts +59 -0
  448. package/dist/module-types/src/project-hooks.d.ts +129 -0
  449. package/dist/module-types/src/project-modules.d.ts +49 -0
  450. package/dist/module-types/src/project-presets.d.ts +29 -0
  451. package/dist/module-types/src/project.d.ts +28 -0
  452. package/dist/module-types/src/publish-levels.d.ts +32 -0
  453. package/dist/module-types/src/quota-boundary.d.ts +64 -0
  454. package/dist/module-types/src/quota-poller.d.ts +82 -0
  455. package/dist/module-types/src/registry.d.ts +226 -0
  456. package/dist/module-types/src/store/agent-checkout.d.ts +53 -0
  457. package/dist/module-types/src/store/agent-store.d.ts +266 -0
  458. package/dist/module-types/src/store/branches.d.ts +163 -0
  459. package/dist/module-types/src/store/git-host.d.ts +113 -0
  460. package/dist/module-types/src/store/index.d.ts +9 -0
  461. package/dist/module-types/src/store/provided.d.ts +16 -0
  462. package/dist/module-types/src/store/queue.d.ts +45 -0
  463. package/dist/module-types/src/store/repository.d.ts +32 -0
  464. package/dist/module-types/src/store/run-record.d.ts +21 -0
  465. package/dist/module-types/src/store/runs.d.ts +107 -0
  466. package/dist/module-types/src/store/tickets.d.ts +72 -0
  467. package/dist/module-types/src/terminal.d.ts +3 -0
  468. package/dist/module-types/src/worktrees.d.ts +89 -0
  469. package/dist/module.css +50 -0
  470. package/dist/node-fs.d.ts +33 -0
  471. package/dist/node-fs.d.ts.map +1 -0
  472. package/dist/node-fs.js +57 -0
  473. package/dist/node-fs.js.map +1 -0
  474. package/dist/open-choices.d.ts +15 -0
  475. package/dist/open-choices.d.ts.map +1 -0
  476. package/dist/open-choices.js +31 -0
  477. package/dist/open-choices.js.map +1 -0
  478. package/dist/openagent-dir.d.ts +11 -0
  479. package/dist/openagent-dir.d.ts.map +1 -0
  480. package/dist/openagent-dir.js +11 -0
  481. package/dist/openagent-dir.js.map +1 -0
  482. package/dist/openagent-gitignore.d.ts +12 -0
  483. package/dist/openagent-gitignore.d.ts.map +1 -0
  484. package/dist/openagent-gitignore.js +19 -0
  485. package/dist/openagent-gitignore.js.map +1 -0
  486. package/dist/pick-directory.d.ts +30 -0
  487. package/dist/pick-directory.d.ts.map +1 -0
  488. package/dist/pick-directory.js +102 -0
  489. package/dist/pick-directory.js.map +1 -0
  490. package/dist/preference-defaults.d.ts +43 -0
  491. package/dist/preference-defaults.d.ts.map +1 -0
  492. package/dist/preference-defaults.js +34 -0
  493. package/dist/preference-defaults.js.map +1 -0
  494. package/dist/project-commands.d.ts +24 -0
  495. package/dist/project-commands.d.ts.map +1 -0
  496. package/dist/project-commands.js +57 -0
  497. package/dist/project-commands.js.map +1 -0
  498. package/dist/project-errors.d.ts +60 -0
  499. package/dist/project-errors.d.ts.map +1 -0
  500. package/dist/project-errors.js +51 -0
  501. package/dist/project-errors.js.map +1 -0
  502. package/dist/project-hooks.d.ts +130 -0
  503. package/dist/project-hooks.d.ts.map +1 -0
  504. package/dist/project-hooks.js +232 -0
  505. package/dist/project-hooks.js.map +1 -0
  506. package/dist/project-modules.d.ts +50 -0
  507. package/dist/project-modules.d.ts.map +1 -0
  508. package/dist/project-modules.js +110 -0
  509. package/dist/project-modules.js.map +1 -0
  510. package/dist/project-pass.d.ts +32 -0
  511. package/dist/project-pass.d.ts.map +1 -0
  512. package/dist/project-pass.js +45 -0
  513. package/dist/project-pass.js.map +1 -0
  514. package/dist/project-presets.d.ts +30 -0
  515. package/dist/project-presets.d.ts.map +1 -0
  516. package/dist/project-presets.js +74 -0
  517. package/dist/project-presets.js.map +1 -0
  518. package/dist/project.d.ts +29 -0
  519. package/dist/project.d.ts.map +1 -0
  520. package/dist/project.js +41 -0
  521. package/dist/project.js.map +1 -0
  522. package/dist/publish-levels.d.ts +33 -0
  523. package/dist/publish-levels.d.ts.map +1 -0
  524. package/dist/publish-levels.js +53 -0
  525. package/dist/publish-levels.js.map +1 -0
  526. package/dist/quota-boundary.d.ts +65 -0
  527. package/dist/quota-boundary.d.ts.map +1 -0
  528. package/dist/quota-boundary.js +135 -0
  529. package/dist/quota-boundary.js.map +1 -0
  530. package/dist/quota-poller.d.ts +83 -0
  531. package/dist/quota-poller.d.ts.map +1 -0
  532. package/dist/quota-poller.js +118 -0
  533. package/dist/quota-poller.js.map +1 -0
  534. package/dist/registry.d.ts +227 -0
  535. package/dist/registry.d.ts.map +1 -0
  536. package/dist/registry.js +364 -0
  537. package/dist/registry.js.map +1 -0
  538. package/dist/remove-files.d.ts +24 -0
  539. package/dist/remove-files.d.ts.map +1 -0
  540. package/dist/remove-files.js +98 -0
  541. package/dist/remove-files.js.map +1 -0
  542. package/dist/request-path.d.ts +10 -0
  543. package/dist/request-path.d.ts.map +1 -0
  544. package/dist/request-path.js +16 -0
  545. package/dist/request-path.js.map +1 -0
  546. package/dist/store/agent-checkout.d.ts +54 -0
  547. package/dist/store/agent-checkout.d.ts.map +1 -0
  548. package/dist/store/agent-checkout.js +61 -0
  549. package/dist/store/agent-checkout.js.map +1 -0
  550. package/dist/store/agent-store.d.ts +267 -0
  551. package/dist/store/agent-store.d.ts.map +1 -0
  552. package/dist/store/agent-store.js +190 -0
  553. package/dist/store/agent-store.js.map +1 -0
  554. package/dist/store/branches.d.ts +164 -0
  555. package/dist/store/branches.d.ts.map +1 -0
  556. package/dist/store/branches.js +187 -0
  557. package/dist/store/branches.js.map +1 -0
  558. package/dist/store/git-host.d.ts +114 -0
  559. package/dist/store/git-host.d.ts.map +1 -0
  560. package/dist/store/git-host.js +103 -0
  561. package/dist/store/git-host.js.map +1 -0
  562. package/dist/store/index.d.ts +10 -0
  563. package/dist/store/index.d.ts.map +1 -0
  564. package/dist/store/index.js +10 -0
  565. package/dist/store/index.js.map +1 -0
  566. package/dist/store/provided.d.ts +17 -0
  567. package/dist/store/provided.d.ts.map +1 -0
  568. package/dist/store/provided.js +37 -0
  569. package/dist/store/provided.js.map +1 -0
  570. package/dist/store/queue.d.ts +46 -0
  571. package/dist/store/queue.d.ts.map +1 -0
  572. package/dist/store/queue.js +61 -0
  573. package/dist/store/queue.js.map +1 -0
  574. package/dist/store/repository.d.ts +33 -0
  575. package/dist/store/repository.d.ts.map +1 -0
  576. package/dist/store/repository.js +25 -0
  577. package/dist/store/repository.js.map +1 -0
  578. package/dist/store/run-record.d.ts +22 -0
  579. package/dist/store/run-record.d.ts.map +1 -0
  580. package/dist/store/run-record.js +108 -0
  581. package/dist/store/run-record.js.map +1 -0
  582. package/dist/store/runs.d.ts +108 -0
  583. package/dist/store/runs.d.ts.map +1 -0
  584. package/dist/store/runs.js +146 -0
  585. package/dist/store/runs.js.map +1 -0
  586. package/dist/store/test-branches.d.ts +18 -0
  587. package/dist/store/test-branches.d.ts.map +1 -0
  588. package/dist/store/test-branches.js +59 -0
  589. package/dist/store/test-branches.js.map +1 -0
  590. package/dist/store/test-runs.d.ts +15 -0
  591. package/dist/store/test-runs.d.ts.map +1 -0
  592. package/dist/store/test-runs.js +37 -0
  593. package/dist/store/test-runs.js.map +1 -0
  594. package/dist/store/tickets.d.ts +73 -0
  595. package/dist/store/tickets.d.ts.map +1 -0
  596. package/dist/store/tickets.js +67 -0
  597. package/dist/store/tickets.js.map +1 -0
  598. package/dist/terminal.d.ts +4 -0
  599. package/dist/terminal.d.ts.map +1 -0
  600. package/dist/terminal.js +97 -0
  601. package/dist/terminal.js.map +1 -0
  602. package/dist/update-check.d.ts +44 -0
  603. package/dist/update-check.d.ts.map +1 -0
  604. package/dist/update-check.js +71 -0
  605. package/dist/update-check.js.map +1 -0
  606. package/dist/worktrees.d.ts +90 -0
  607. package/dist/worktrees.d.ts.map +1 -0
  608. package/dist/worktrees.js +123 -0
  609. package/dist/worktrees.js.map +1 -0
  610. package/package.json +103 -3
@@ -0,0 +1,1475 @@
1
+ // The page half of the Claude web bridge (#1237): find the question a cloud session is parked
2
+ // on, and hand it to the service worker, which is the only part that talks to the daemon.
3
+ //
4
+ // The token and the fetch deliberately live in the worker, not here. A content script shares a
5
+ // tab with claude.ai, so nothing in this file should ever hold the secret that talks to a
6
+ // daemon, and a fetch from here would carry the page's origin into a CORS check the daemon
7
+ // refuses on purpose.
8
+ //
9
+ // Four rounds of a read-only spike shaped the extraction below, and each failure is worth
10
+ // keeping because each was a different obstacle:
11
+ // 1. the page has <code> blocks with no <pre>, so a `pre code` selector examined nothing
12
+ // 2. the message body sits behind shadow roots, so document.body.innerText never saw it
13
+ // 3. fixed prefixes guessed an indentation nobody promised, so brace matching replaced them
14
+ // 4. our own system prompt renders on the page, so its await-choices spec is a decoy
15
+ //
16
+ // `check.mjs` covers all four offline, in jsdom, with no browser and no live session.
17
+
18
+ const POLL_MS = 2000
19
+ // How many surveys this page has run; read by the offline harness (check.mjs) to prove one page
20
+ // change costs one survey (#1707).
21
+ let surveys = 0
22
+
23
+ // Every observer `watch` runs, so the script's own drawing can pause them (#1707): the corner
24
+ // panel and the Driver overlay live under the observed root, and without the pause each survey's
25
+ // redraw was the next survey's trigger — a full survey about four times a second, forever, on a
26
+ // page where nothing had changed.
27
+ const watchers = new Set()
28
+ const OBSERVED = { childList: true, subtree: true, characterData: true }
29
+ let drawing = 0
30
+
31
+ /** Run `draw`, which writes the script's own elements, without the observers taking it for a page change. */
32
+ function ownWrites(draw) {
33
+ if (drawing++ === 0) for (const observer of watchers) observer.disconnect()
34
+ try {
35
+ draw()
36
+ } finally {
37
+ if (--drawing === 0) for (const observer of watchers) observer.observe(document.documentElement, OBSERVED)
38
+ }
39
+ }
40
+ /** The on-page panel's element id. */
41
+ const PANEL_ID = 'oa-bridge-panel'
42
+ const IS_TOP = window.top === window
43
+
44
+ /** The cloud session this page is showing, which is the key the daemon joins runs on. */
45
+ function sessionIdFromUrl() {
46
+ const match = /\/code\/(session_[A-Za-z0-9]+)/.exec(location.href)
47
+ return match ? match[1] : undefined
48
+ }
49
+
50
+ /**
51
+ * Hand a found question to the service worker, which owns the token and the fetch.
52
+ *
53
+ * Guarded on `chrome` existing so the offline harness (check.mjs) can run this file in jsdom,
54
+ * where there is no extension runtime. Failures are swallowed: a page that cannot reach the
55
+ * daemon must still show its panel rather than throw inside a DOM observer.
56
+ */
57
+ /** What the daemon last said about our report, so the panel can say why nothing arrived. */
58
+ let bridgeStatus = 'not sent yet'
59
+
60
+ function reportToDaemon(parsed) {
61
+ const sessionId = sessionIdFromUrl()
62
+ if (!sessionId) return
63
+ // The block's own shape (#1554): `multi`, and per option `default` and `stop`, travel with
64
+ // the labels so the dashboard renders the gate as a local one and a stopping pick is typed
65
+ // back as a hand-over rather than a continuation.
66
+ const question = {
67
+ sessionId,
68
+ title: String(parsed.title ?? '').slice(0, 500),
69
+ options: (parsed.options ?? [])
70
+ .map(o =>
71
+ typeof o === 'string'
72
+ ? { label: o }
73
+ : {
74
+ label: String(o?.label ?? ''),
75
+ ...(o?.detail ? { detail: String(o.detail).slice(0, 500) } : {}),
76
+ ...(o?.default === true ? { default: true } : {}),
77
+ ...(o?.stop === true ? { stop: true } : {}),
78
+ },
79
+ )
80
+ .filter(o => o.label)
81
+ .slice(0, 20),
82
+ ...(parsed.recommended ? { recommended: String(parsed.recommended) } : {}),
83
+ ...(parsed.multi === true ? { multi: true } : {}),
84
+ }
85
+ if (!question.title || !question.options.length) return
86
+ // Offline (check.mjs, no extension runtime): expose what would have been posted, so the
87
+ // harness can pin the shape the daemon receives, not just the panel's summary of it.
88
+ if (typeof chrome === 'undefined') {
89
+ window.__oaBridgeQuestion = question
90
+ return
91
+ }
92
+ if (!chrome.runtime?.sendMessage) return
93
+ try {
94
+ chrome.runtime.sendMessage({ type: 'oa-question', question }, reply => {
95
+ // Silence here was the first thing to go wrong live: the worker answered "no token set"
96
+ // and nobody ever saw it, so a configured-looking extension simply did nothing.
97
+ if (chrome.runtime.lastError) bridgeStatus = chrome.runtime.lastError.message ?? 'worker unreachable'
98
+ else if (!reply) bridgeStatus = 'no reply from the worker'
99
+ else if (reply.ok) bridgeStatus = reply.skipped ? `sent (${reply.skipped})` : 'sent'
100
+ else bridgeStatus = reply.error ?? 'failed'
101
+ })
102
+ } catch (err) {
103
+ bridgeStatus = String(err?.message ?? err)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * What we last sent per session and position, so an unchanged message is not re-posted on every
109
+ * mutation. Keyed by session too, because the Driver tab moves between sessions inside one page
110
+ * (#1332) and a position alone would collide across them.
111
+ */
112
+ const sentEvents = new Map()
113
+
114
+ /** What happened to the last transcript report, for the panel. */
115
+ let transcriptStatus = 'not sent yet'
116
+
117
+ /**
118
+ * Where claude.ai marks the conversation's turns (#1225).
119
+ *
120
+ * The page renders no `<article>` elements — the session's transcript is a `role="feed"` of
121
+ * `transcript-row` elements, one per turn, each naming its position (`data-index`) and its kind
122
+ * (`data-perf-row`: `human`, `assistant`, or a `marker` such as "Resumed session"). The list is
123
+ * virtual, so a long session only keeps its tail in the DOM; the position is what lets the daemon
124
+ * keep one copy per turn regardless of which turns happen to be rendered.
125
+ */
126
+ const TURN_ROW = '[data-testid="transcript-row"]'
127
+ /** Which row kinds are conversation; every other kind (markers) is left out of the mirror. */
128
+ const TURN_ROLES = { human: 'user', assistant: 'agent' }
129
+
130
+ function turnRows() {
131
+ return deepQueryAll(TURN_ROW)
132
+ }
133
+
134
+ /** The opening message: the run's own prompt, which quotes our whole protocol (#1568). */
135
+ function openingMessage() {
136
+ return turnRows().find(row => row.getAttribute('data-index') === '0')
137
+ }
138
+
139
+ /**
140
+ * The transcript, one entry per conversation turn (#1225).
141
+ *
142
+ * Each turn carries up to 8000 characters from its start. Only the opening turn — the run's
143
+ * prompt — is ever that long; a reply that is still streaming is sent as it grows and replaced
144
+ * in place, since the daemon keys entries by position.
145
+ */
146
+ function transcript() {
147
+ return turnRows().flatMap(row => {
148
+ const role = TURN_ROLES[row.getAttribute('data-perf-row')]
149
+ const seq = Number(row.getAttribute('data-index'))
150
+ if (!role || !Number.isInteger(seq) || seq < 0) return []
151
+ const text = cleanText(row.innerText ?? row.textContent ?? '')
152
+ return text ? [{ seq, role, text: text.slice(0, 8000) }] : []
153
+ })
154
+ }
155
+
156
+ /** Private-use codepoints are icon-font glyphs, which carry no meaning outside their font. */
157
+ const ICON_GLYPHS = /[\uE000-\uF8FF]/g
158
+
159
+ /** A turn's text as prose: no icon glyphs, no blank lines, no leading or trailing whitespace. */
160
+ function cleanText(text) {
161
+ return text
162
+ .replace(ICON_GLYPHS, '')
163
+ .split('\n')
164
+ .map(line => line.trim())
165
+ .filter(Boolean)
166
+ .join('\n')
167
+ .trim()
168
+ }
169
+
170
+ /**
171
+ * Tell the daemon what this injected script is and what it just saw.
172
+ *
173
+ * Diagnosis kept requiring a screenshot of the panel, so every wrong guess cost a round trip
174
+ * through a person. This says it directly: which version is running in the page, and what the
175
+ * last scrape found.
176
+ */
177
+ function sayHello(sessionId, note) {
178
+ if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) return
179
+ const version = chrome.runtime.getManifest?.().version ?? '?'
180
+ try {
181
+ chrome.runtime.sendMessage({ type: 'oa-hello', sessionId, version, note }, reply => {
182
+ // The worker's reply says whether this tab is the Driver (#1332): the overlay goes up
183
+ // the moment the page loads, not when the first cycle reaches it.
184
+ if (!chrome.runtime.lastError && reply?.driver) setDriver(true)
185
+ })
186
+ } catch {
187
+ // Nothing to do; the next pass tries again.
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Post whatever changed since the last look. Only what changed: the observer fires on every DOM
193
+ * mutation and a session's transcript is mostly stable, so sending it all each time would be a
194
+ * few hundred kilobytes a second for no new information.
195
+ */
196
+ function reportTranscript() {
197
+ const sessionId = sessionIdFromUrl()
198
+ if (!sessionId) return
199
+ const rows = turnRows()
200
+ const blocks = transcript()
201
+ // Offline (check.mjs, no extension runtime): expose what would have been posted.
202
+ if (typeof chrome === 'undefined') window.__oaBridgeTranscript = blocks
203
+ if (!rows.length) {
204
+ // Named, not guessed at: a page with no turn rows is a layout the mirror does not know, and
205
+ // saying so beats mirroring whatever text happens to be on screen.
206
+ transcriptStatus = 'no transcript rows found'
207
+ sayHello(sessionId, `rows 0, ${transcriptStatus}`)
208
+ return
209
+ }
210
+ const kinds = [...new Set(rows.map(row => row.getAttribute('data-perf-row') ?? '?'))].join(',')
211
+ sayHello(sessionId, `rows ${rows.length} (${kinds}), turns ${blocks.length}, ${transcriptStatus}`)
212
+ if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) return
213
+ const events = blocks.filter(event => sentEvents.get(`${sessionId}#${event.seq}`) !== event.text)
214
+ if (!events.length) {
215
+ transcriptStatus = `${blocks.length} turn(s), unchanged`
216
+ return
217
+ }
218
+ try {
219
+ chrome.runtime.sendMessage({ type: 'oa-events', sessionId, events }, reply => {
220
+ if (chrome.runtime.lastError) {
221
+ transcriptStatus = chrome.runtime.lastError.message ?? 'worker unreachable'
222
+ return
223
+ }
224
+ // Only remember what the daemon actually took, so a rejected batch is retried.
225
+ if (reply?.ok) {
226
+ for (const event of events) sentEvents.set(`${sessionId}#${event.seq}`, event.text)
227
+ transcriptStatus = `sent ${events.length} of ${blocks.length} turn(s)`
228
+ } else {
229
+ transcriptStatus = reply?.error ?? 'failed'
230
+ }
231
+ })
232
+ } catch {
233
+ // Worker asleep or context invalidated; the next mutation retries.
234
+ }
235
+ }
236
+
237
+ /** Every element matching a selector, including inside open shadow roots. */
238
+ function deepQueryAll(selector, root = document, seen = new Set()) {
239
+ const out = [...root.querySelectorAll(selector)]
240
+ for (const el of root.querySelectorAll('*')) {
241
+ if (!el.shadowRoot || seen.has(el)) continue
242
+ seen.add(el)
243
+ out.push(...deepQueryAll(selector, el.shadowRoot, seen))
244
+ }
245
+ return out
246
+ }
247
+
248
+ /** How many open shadow roots exist, which tells us whether that is where content hides. */
249
+ function countShadowRoots(root = document, seen = new Set()) {
250
+ let n = 0
251
+ for (const el of root.querySelectorAll('*')) {
252
+ if (!el.shadowRoot || seen.has(el)) continue
253
+ seen.add(el)
254
+ n += 1 + countShadowRoots(el.shadowRoot, seen)
255
+ }
256
+ return n
257
+ }
258
+
259
+ /** Page text including shadow content, which plain body.innerText misses. */
260
+ function deepText() {
261
+ let text = document.body?.innerText ?? ''
262
+ for (const host of deepQueryAll('*')) {
263
+ if (host.shadowRoot) text += `\n${host.shadowRoot.textContent ?? ''}`
264
+ }
265
+ return text
266
+ }
267
+
268
+ /**
269
+ * Our own await-protocol spec is part of the system prompt, and the page renders the prompt,
270
+ * so a session shows a JSON block with `options` before the agent has asked anything. Its
271
+ * values are the spec's placeholders. Round 3 extracted exactly that and reported
272
+ * `<the question>` as the question, which is the decoy working as designed.
273
+ *
274
+ * Two more decoys slipped that net on a live session (#1568). The browser-handoff example's
275
+ * title is placeholders *plus punctuation* — `<what…> (<the page…>)` — so the whole-string
276
+ * placeholder test missed it, and its labels are literal. And the approval example is literal
277
+ * end to end (`Ship this?`, Approve/Decline), which no placeholder heuristic can catch — it is
278
+ * matched verbatim instead, accepting that an agent asking the doc's exact sample question
279
+ * word-for-word loses (it was told to put a real title there).
280
+ */
281
+ function isTemplate(parsed) {
282
+ const placeholder = v => typeof v === 'string' && /^<.*>$/.test(v.trim())
283
+ const title = typeof parsed.title === 'string' ? parsed.title : ''
284
+ if (placeholder(title)) return true
285
+ // Placeholders joined by punctuation only: strip every <…> group; a title with no letters
286
+ // left is the spec talking, while a real title mentioning `<SESSION_NAME>` keeps its words.
287
+ if (/<[^<>]+>/.test(title) && !/\p{L}/u.test(title.replace(/<[^<>]*>/g, ''))) return true
288
+ const labels = parsed.options.map(o => (typeof o === 'string' ? o : o?.label))
289
+ if (labels.length > 0 && labels.every(placeholder)) return true
290
+ const set = labels.filter(l => typeof l === 'string').join('|')
291
+ // The two literal examples the protocol ships, verbatim.
292
+ if (set === 'Handled it|Could not handle it') return true
293
+ if (title === 'Ship this?' && set === 'Approve|Decline') return true
294
+ return false
295
+ }
296
+
297
+ /**
298
+ * Whether an element renders inside the transcript's opening message, crossing shadow
299
+ * boundaries on the way up. The page opens with the run's prompt — which quotes our whole
300
+ * protocol, examples included — so nothing inside that first message is ever a question the
301
+ * session asked (#1568). Roots are walked via their host so a block inside a shadowed
302
+ * highlighter still resolves to the turn row that carries it.
303
+ */
304
+ function inFirstMessage(el, first) {
305
+ if (!first) return false
306
+ // A shadow tree's top parent is its ShadowRoot, whose `host` re-enters the outer tree.
307
+ for (let node = el; node; node = node.parentNode ?? node.host ?? null) {
308
+ if (node === first) return true
309
+ }
310
+ return false
311
+ }
312
+
313
+ /** Every JSON object carrying `options` in the text, in the order they appear. */
314
+ function extractChoices(text, stats) {
315
+ const found = []
316
+ collectChoices(text, stats, found)
317
+ return found
318
+ }
319
+
320
+ /**
321
+ * Brace-match every `options` object out of arbitrary surrounding text.
322
+ *
323
+ * Round 2 reached the text but could not read it, because matching on the fixed prefixes
324
+ * `{"title"` and `{\n "title"` guesses at an indentation nobody promised, and the block
325
+ * arrives with prose around it. Strings and escapes are tracked so a brace inside a label
326
+ * cannot close the object early.
327
+ */
328
+ function collectChoices(text, stats, found) {
329
+ if (!text.includes('"options"')) return found
330
+ for (let i = 0; i < text.length; i++) {
331
+ if (text[i] !== '{') continue
332
+ let depth = 0
333
+ let inString = false
334
+ let escaped = false
335
+ for (let j = i; j < text.length; j++) {
336
+ const ch = text[j]
337
+ if (escaped) {
338
+ escaped = false
339
+ continue
340
+ }
341
+ if (ch === '\\') {
342
+ escaped = true
343
+ continue
344
+ }
345
+ if (ch === '"') {
346
+ inString = !inString
347
+ continue
348
+ }
349
+ if (inString) continue
350
+ if (ch === '{') depth++
351
+ else if (ch === '}') {
352
+ depth--
353
+ if (depth > 0) continue
354
+ const slice = text.slice(i, j + 1)
355
+ if (slice.includes('"options"')) {
356
+ if (stats) stats.candidates++
357
+ try {
358
+ const parsed = JSON.parse(slice)
359
+ if (Array.isArray(parsed.options)) {
360
+ found.push(parsed)
361
+ // Keep scanning past this object: the spec comes first and the real question
362
+ // after it, so the last one is the one that matters.
363
+ i = j
364
+ } else if (stats) stats.parsedWithoutOptions++
365
+ } catch (err) {
366
+ // Not this one, or still streaming. Record why, so a failing report says whether
367
+ // the block was never found or found and unparseable.
368
+ if (stats) {
369
+ stats.parseErrors++
370
+ stats.lastError = String(err.message ?? err).slice(0, 80)
371
+ }
372
+ }
373
+ }
374
+ break
375
+ }
376
+ }
377
+ }
378
+ return found
379
+ }
380
+
381
+ /**
382
+ * The transcript row an element renders in, looking out through any shadow roots it sits behind:
383
+ * the rows are in the light DOM while a message's body is not.
384
+ */
385
+ function rowOf(el) {
386
+ for (let node = el; node; node = node.getRootNode?.()?.host) {
387
+ const row = node.closest?.(TURN_ROW)
388
+ if (row) return row
389
+ }
390
+ return undefined
391
+ }
392
+
393
+ /**
394
+ * Whether the user has already answered past that row: a human turn at a later position means
395
+ * the question was taken and the session moved on, however long the answered block stays on the
396
+ * page (#1703). A block not placed in any row is measured from the session's latest turn instead.
397
+ * With no positioned turns at all there is nothing to measure against, and the block stands.
398
+ */
399
+ function answeredAfter(row) {
400
+ const rows = turnRows()
401
+ const position = r => Number(r.getAttribute('data-index'))
402
+ const kind = r => r.getAttribute('data-perf-row')
403
+ const at = row ? position(row) : Math.max(-1, ...rows.filter(r => kind(r) === 'assistant').map(position))
404
+ if (!Number.isInteger(at) || at < 0) return false
405
+ return rows.some(r => kind(r) === 'human' && position(r) > at)
406
+ }
407
+
408
+ /** Why the last survey reported no question when a block was on the page, for the panel. */
409
+ let choiceNote = ''
410
+
411
+ /**
412
+ * The question a parked run is waiting on. Our agents emit it as a fenced JSON block carrying
413
+ * `options`. Element-scoped first so the winner names where it was found, then the whole page
414
+ * as a fallback for a block split across elements by a syntax highlighter. A block the user has
415
+ * already answered — a human turn follows it — is not pending, whatever any store remembers: the
416
+ * daemon forgets what was answered when it restarts, and re-asking would type a second answer
417
+ * into a session that moved on (#1703).
418
+ */
419
+ function findPendingChoice() {
420
+ choiceNote = ''
421
+ // `code` on its own: the page has <code> blocks with no <pre> wrapper (round 1), and they
422
+ // sit inside shadow roots (round 2). DOM order tracks transcript order, so the last real
423
+ // question wins over both the spec and any earlier answered one.
424
+ // The opening message is the run's own prompt, which quotes the whole protocol — examples
425
+ // included — so blocks inside it are documentation, never the session asking (#1568). A
426
+ // virtual list may have scrolled that row out of the DOM, in which case its decoys are gone
427
+ // with it and isTemplate alone stands.
428
+ const firstMessage = openingMessage()
429
+ const fromElements = []
430
+ for (const el of deepQueryAll('pre code, pre, code')) {
431
+ if (inFirstMessage(el, firstMessage)) continue
432
+ for (const parsed of extractChoices(el.textContent ?? '')) {
433
+ fromElements.push({ via: el.tagName.toLowerCase(), parsed, row: rowOf(el) })
434
+ }
435
+ }
436
+ // The page-text fallback reads everything, opening message included, so any block that also
437
+ // renders inside that first message is subtracted from it by value.
438
+ const inPrompt = new Set(extractChoices(firstMessage?.textContent ?? '').map(p => JSON.stringify(p)))
439
+ const candidates = fromElements.length
440
+ ? fromElements
441
+ : extractChoices(deepText())
442
+ .filter(parsed => !inPrompt.has(JSON.stringify(parsed)))
443
+ .map(parsed => ({ via: 'page-text', parsed }))
444
+ const real = candidates.filter(c => !isTemplate(c.parsed))
445
+ const last = real.at(-1)
446
+ const answered = Boolean(last) && answeredAfter(last.row)
447
+ if (answered) choiceNote = 'already answered'
448
+ const found = answered ? undefined : last
449
+ if (found) reportToDaemon(found.parsed)
450
+ reportTranscript()
451
+ return found ?? undefined
452
+ }
453
+
454
+ /** The box an answer would be typed into, if this ever becomes two-way. */
455
+ function findComposer() {
456
+ const editable = deepQueryAll('div[contenteditable="true"]')[0]
457
+ if (editable) return { via: 'contenteditable', el: editable }
458
+ const textarea = deepQueryAll('textarea')[0]
459
+ if (textarea) return { via: 'textarea', el: textarea }
460
+ return undefined
461
+ }
462
+
463
+ /** What the last delivery attempt did, for the panel. */
464
+ let answerStatus = 'none delivered'
465
+
466
+ /** Put text in the composer. execCommand is what the live page accepts; jsdom has no such thing. */
467
+ function fillComposer(composer, text) {
468
+ if (composer.via === 'textarea') {
469
+ composer.el.value = text
470
+ composer.el.dispatchEvent(new Event('input', { bubbles: true }))
471
+ return true
472
+ }
473
+ composer.el.focus()
474
+ let inserted = false
475
+ try {
476
+ inserted = document.execCommand('insertText', false, text)
477
+ } catch {
478
+ inserted = false
479
+ }
480
+ if (!inserted) {
481
+ // The offline harness lands here; a real ProseMirror/Lexical editor may ignore it, which the
482
+ // submit check below would surface as an empty send.
483
+ composer.el.textContent = text
484
+ composer.el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }))
485
+ }
486
+ return true
487
+ }
488
+
489
+ /** The button that sends the composer, wherever the page keeps it this week. */
490
+ function findSendButton() {
491
+ const labelled = deepQueryAll('button[aria-label]').filter(
492
+ b => /send/i.test(b.getAttribute('aria-label') ?? '') && !b.disabled,
493
+ )
494
+ // The last one: pages render hidden or historical buttons above the live composer's.
495
+ return labelled.at(-1) ?? deepQueryAll('button[type="submit"]').at(-1)
496
+ }
497
+
498
+ /**
499
+ * Type the dashboard's pick into the composer and submit it (#1237).
500
+ *
501
+ * This is the one place the extension acts rather than observes, so it reports exactly what it
502
+ * did: which fill path, and whether the send was a button click or an Enter fallback. The text
503
+ * only ever comes from the daemon, which only accepts a label of the parked question, so what
504
+ * can be typed here is bounded by what the session itself offered.
505
+ */
506
+ async function deliverAnswer(text) {
507
+ // Wait for the composer rather than failing on its absence: the first live delivery landed
508
+ // right after a tab revive, and claude.ai takes well over the naive 5 seconds to render, so
509
+ // "no composer on the page" mostly means "not yet". The harness shortens the wait.
510
+ const deadline = Date.now() + (window.__oaComposerWaitMs ?? 20000)
511
+ let composer = findComposer()
512
+ while (!composer && Date.now() < deadline) {
513
+ await new Promise(resolve => setTimeout(resolve, 500))
514
+ composer = findComposer()
515
+ }
516
+ if (!composer) {
517
+ answerStatus = 'failed: no composer on the page'
518
+ return { ok: false, note: 'no composer on the page' }
519
+ }
520
+ fillComposer(composer, text)
521
+ // Let the editor's own event handling settle before submitting, or the click sends nothing.
522
+ await new Promise(resolve => setTimeout(resolve, 400))
523
+ const button = findSendButton()
524
+ if (button) {
525
+ button.click()
526
+ answerStatus = `sent via ${composer.via} + button`
527
+ return { ok: true, note: `filled ${composer.via}, clicked send button` }
528
+ }
529
+ for (const type of ['keydown', 'keyup']) {
530
+ composer.el.dispatchEvent(
531
+ new KeyboardEvent(type, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }),
532
+ )
533
+ }
534
+ answerStatus = `sent via ${composer.via} + enter`
535
+ return { ok: true, note: `filled ${composer.via}, no send button, sent Enter` }
536
+ }
537
+
538
+ // ---------------------------------------------------------------------------
539
+ // Creating a session (#1328). A session created through this page's repo picker is repo-bound,
540
+ // and those are the ones that can push and open a pull request (#1320). So the daemon queues a repo, a branch and a prompt,
541
+ // and this drives the same controls a person would on the new-session page: the repo chip and
542
+ // its searchable list, the branch chip that appears beside it, the composer, send.
543
+ //
544
+ // Every selector here is a guess about someone else's UI, so this half reports rather than
545
+ // insists: `probeNewSession` describes what the page offers without touching it, and every
546
+ // failure below names the control it could not find.
547
+
548
+ /**
549
+ * Visible text of a control, for matching chips and entries: the icon-font glyphs the page puts
550
+ * beside a label (a repo icon, a branch icon, the check on the chosen entry) are dropped, since
551
+ * "openagt/openagent" followed by a check glyph is the entry, not another entry.
552
+ */
553
+ function controlText(el) {
554
+ return (el.textContent ?? '').replace(ICON_GLYPHS, '').replace(/\s+/g, ' ').trim()
555
+ }
556
+
557
+ /**
558
+ * Whether a control is actually usable: rendered, enabled, not aria-hidden. The page keeps a
559
+ * closed picker's entries in the DOM, so an entry that is not visible is not on offer — a browser
560
+ * answers that exactly; the offline harness has no layout and answers nothing, which counts as yes.
561
+ */
562
+ function usable(el) {
563
+ if (!el || el.disabled) return false
564
+ if (el.getAttribute?.('aria-hidden') === 'true') return false
565
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false
566
+ return true
567
+ }
568
+
569
+ /** Every button-like control on the page, most explicit picker shapes first. */
570
+ function menuTriggers() {
571
+ const out = []
572
+ const add = (el, via) => {
573
+ if (usable(el) && !out.some(entry => entry.el === el)) out.push({ el, via, text: controlText(el) })
574
+ }
575
+ for (const el of deepQueryAll('button[role="combobox"]')) add(el, 'combobox')
576
+ for (const el of deepQueryAll('button[aria-haspopup="listbox"], button[aria-haspopup="menu"], button[aria-haspopup="true"]')) add(el, 'haspopup')
577
+ for (const el of deepQueryAll('button[aria-expanded]')) add(el, 'expandable')
578
+ for (const el of deepQueryAll('button')) add(el, 'button')
579
+ return out
580
+ }
581
+
582
+ /**
583
+ * The composer's chips, in the order the page lays them out: the repository, then the branch,
584
+ * then "add another repository". Each is a combobox button; the branch chip exists only once a
585
+ * repository is chosen.
586
+ */
587
+ function chips() {
588
+ return deepQueryAll('button[role="combobox"]').filter(usable).map(el => ({ el, via: 'combobox', text: controlText(el) }))
589
+ }
590
+
591
+ /** The entries an open picker currently offers — visible options only, the closed ones stay in the DOM. */
592
+ function menuEntries() {
593
+ return deepQueryAll('[role="option"]')
594
+ .filter(usable)
595
+ .map(el => ({ el, text: controlText(el) }))
596
+ }
597
+
598
+ /** The open picker's search box, if it has one. */
599
+ function pickerSearch() {
600
+ return deepQueryAll('input[role="combobox"], input[type="search"], input[placeholder*="search" i]').filter(usable).at(-1)
601
+ }
602
+
603
+ /** Type into a React-controlled input so the page sees the change: the native setter, then an input event. */
604
+ function typeInto(input, text) {
605
+ input.focus()
606
+ const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), 'value')?.set ?? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
607
+ if (setter) setter.call(input, text)
608
+ else input.value = text
609
+ input.dispatchEvent(new Event('input', { bubbles: true }))
610
+ }
611
+
612
+ /** Wait for `read` to return something truthy, or give up. */
613
+ async function waitFor(read, timeoutMs, stepMs = 150) {
614
+ const deadline = Date.now() + timeoutMs
615
+ for (;;) {
616
+ const value = read()
617
+ if (value) return value
618
+ if (Date.now() >= deadline) return undefined
619
+ await new Promise(resolve => setTimeout(resolve, stepMs))
620
+ }
621
+ }
622
+
623
+ /** How long to wait for a picker to render its entries, and for the chips to settle. */
624
+ const MENU_WAIT_MS = 6000
625
+
626
+ /**
627
+ * Open `trigger` and choose the entry whose text is one of `wanted` (lower-cased; the first is
628
+ * the full name, later ones aliases). Types the full name into the picker's search box when it
629
+ * has one, since a long list is filtered rather than scrolled. Never clicks the trigger itself
630
+ * or anything inside it as an entry. Returns a note either way.
631
+ */
632
+ async function chooseFrom(trigger, wanted, hint) {
633
+ trigger.el.click()
634
+ const outside = e => e.el !== trigger.el && !trigger.el.contains(e.el)
635
+ const entries = () => menuEntries().filter(outside)
636
+ const find = () => {
637
+ const now = entries()
638
+ for (const w of wanted) {
639
+ const exact = now.find(e => e.text.toLowerCase() === w)
640
+ if (exact) return exact
641
+ }
642
+ return undefined
643
+ }
644
+ // The list is fetched after the picker opens: wait for it to hold something before filtering
645
+ // it, and filter only when it has a search box. A filter that finds nothing is cleared and the
646
+ // whole list scanned, in case the page's search matches differently than expected.
647
+ const search = await waitFor(pickerSearch, 1500, 100)
648
+ const loaded = await waitFor(() => entries().length > 0, MENU_WAIT_MS)
649
+ let hit = loaded ? find() : undefined
650
+ if (!hit && search) {
651
+ typeInto(search, wanted[0])
652
+ hit = await waitFor(find, 3000)
653
+ if (!hit) {
654
+ typeInto(search, '')
655
+ hit = await waitFor(find, 3000)
656
+ }
657
+ }
658
+ if (!hit) {
659
+ const seen = entries()
660
+ const diag = `search ${search ? `"${search.placeholder}"` : 'none'}, ${seen.length} visible of ${deepQueryAll('[role="option"]').length} options${seen.length ? `: ${seen.slice(0, 5).map(e => JSON.stringify(e.text.slice(0, 40))).join(', ')}` : ''}; chips ${JSON.stringify(chips().map(c => c.text))}`
661
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
662
+ return { ok: false, note: `${hint}: the list offered no "${wanted[0]}" (${diag})` }
663
+ }
664
+ hit.el.click()
665
+ return { ok: true, note: `${hint}: clicked "${hit.text}" via ${trigger.via}` }
666
+ }
667
+
668
+ // The model (#1697). The page's model picker is a menu button beside the composer reading the
669
+ // current model — "Opus 5" — and its menu lists the models as radio items, each label beside a
670
+ // hidden keyboard-shortcut digit and, on the current one, a hidden check glyph; a "More models"
671
+ // item opens a submenu of older versions. A run started with a model must run on it, so when the
672
+ // request names one it is picked before anything is sent, and a menu that does not offer it, or
673
+ // a page with no picker, fails the creation rather than sending on whatever the page defaults to.
674
+
675
+ /** A model name as the dashboard sends it: `opus`, or a full label such as `Opus 4.8`. */
676
+ const MODEL_WORDS = /\b(fable|opus|sonnet|haiku)\b/i
677
+
678
+ /** The model picker's trigger: the menu button reading a model name. Not a chip, so the chips' order is untouched. */
679
+ function modelTrigger() {
680
+ return menuTriggers().find(t => t.el.getAttribute('aria-haspopup') === 'menu' && MODEL_WORDS.test(t.text))
681
+ }
682
+
683
+ /**
684
+ * A control's label without its decorations: the hidden shortcut digit and check glyph the page
685
+ * puts beside a model's name are not part of it, so "Sonnet 5" is read, not "Sonnet 53".
686
+ */
687
+ function labelText(el) {
688
+ const parts = []
689
+ const walk = node => {
690
+ if (node.nodeType === Node.TEXT_NODE) parts.push(node.textContent)
691
+ else if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute('aria-hidden') !== 'true' && node.tagName !== 'KBD') node.childNodes.forEach(walk)
692
+ }
693
+ walk(el)
694
+ return parts.join('').replace(ICON_GLYPHS, '').replace(/\s+/g, ' ').trim()
695
+ }
696
+
697
+ /** The models the open menu offers, by label. */
698
+ function modelEntries() {
699
+ return deepQueryAll('[role="menuitemradio"]')
700
+ .filter(usable)
701
+ .map(el => ({ el, text: labelText(el) }))
702
+ }
703
+
704
+ /** Whether a label names the wanted model: the whole label, or the model's word in it — "opus" reads "Opus 5". */
705
+ function readsModel(text, wanted) {
706
+ const label = text.toLowerCase()
707
+ return label === wanted || new RegExp(`(^|[^a-z0-9])${wanted.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^a-z0-9])`).test(label)
708
+ }
709
+
710
+ /**
711
+ * Open the model menu and click the entry for `wanted` (lower-cased): the exact label first, else
712
+ * the first entry carrying its word, which is the newest version, since the menu lists the current
713
+ * versions and keeps older ones behind "More models" — searched too when the menu itself offers
714
+ * nothing. Returns a note either way; nothing is clicked as an entry that is not one.
715
+ */
716
+ async function chooseModel(trigger, wanted) {
717
+ trigger.el.click()
718
+ const entries = () => modelEntries().filter(e => e.el !== trigger.el && !trigger.el.contains(e.el))
719
+ const find = () => {
720
+ const now = entries()
721
+ return now.find(e => e.text.toLowerCase() === wanted) ?? now.find(e => readsModel(e.text, wanted))
722
+ }
723
+ const loaded = await waitFor(() => entries().length > 0, MENU_WAIT_MS)
724
+ let hit = loaded ? find() : undefined
725
+ let via = 'menu'
726
+ if (!hit) {
727
+ const more = deepQueryAll('[role="menuitem"][aria-haspopup="menu"]').filter(usable).find(el => /more models/i.test(labelText(el)))
728
+ if (more) {
729
+ const before = entries().length
730
+ more.click()
731
+ if (await waitFor(() => entries().length > before, MENU_WAIT_MS)) {
732
+ hit = find()
733
+ via = 'More models'
734
+ }
735
+ }
736
+ }
737
+ if (!hit) {
738
+ const seen = entries()
739
+ const diag = `${seen.length} entries${seen.length ? `: ${seen.slice(0, 12).map(e => JSON.stringify(e.text.slice(0, 40))).join(', ')}` : ''}`
740
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
741
+ return { ok: false, note: `model: the menu offered no "${wanted}" (${diag})` }
742
+ }
743
+ hit.el.click()
744
+ return { ok: true, note: `model: clicked "${hit.text}" via ${via}` }
745
+ }
746
+
747
+ /**
748
+ * Describe the controls on this page without touching any of them. Text only, capped, so the
749
+ * report is safe to paste into an issue — the point is that a failed first run says what the
750
+ * page offers, so the selectors above can be aimed at the real markup.
751
+ */
752
+ function probeNewSession() {
753
+ return {
754
+ url: location.href,
755
+ sessionId: sessionIdFromUrl() ?? null,
756
+ composer: findComposer()?.via ?? null,
757
+ sendButton: Boolean(findSendButton()),
758
+ model: modelTrigger()?.text ?? null,
759
+ chips: chips().map(c => c.text.slice(0, 80)),
760
+ triggers: menuTriggers()
761
+ .slice(0, 40)
762
+ .map(trigger => ({ via: trigger.via, text: trigger.text.slice(0, 80) }))
763
+ .filter(trigger => trigger.text),
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Drive the new-session flow: repo, branch, model, prompt, send, and report the session it became.
769
+ *
770
+ * The page remembers the last repository picked, so it may open already showing ours, another
771
+ * one, or none — the chips are waited for and read rather than assumed. The branch is checked,
772
+ * not assumed: the branch chip must read the requested ref before anything is sent, because a
773
+ * session opened on the wrong branch would push its work somewhere the run never looks. The
774
+ * model, when the request names one, is checked the same way (#1697): the picker must read it
775
+ * before anything is sent, since a session on the page's default model is the choice silently
776
+ * dropped. The session id is read from the URL, the one thing the page is guaranteed to tell us
777
+ * and exactly what the daemon joins runs on; a send that never becomes a session URL is a
778
+ * failure with a reason, never a silent success.
779
+ */
780
+ async function createSession({ repo, branch, prompt, model }) {
781
+ const composer = await waitFor(findComposer, window.__oaComposerWaitMs ?? 20000)
782
+ if (!composer) return { ok: false, note: 'no composer on the new-session page' }
783
+
784
+ const bare = String(repo).split('/').pop() ?? repo
785
+ const repoWanted = [String(repo).toLowerCase(), bare.toLowerCase()]
786
+ const selectRepo = () => menuTriggers().find(t => /select repo|add repo|choose repo/i.test(t.text) || /select repo|choose repo/i.test(t.el.getAttribute('aria-label') ?? ''))
787
+ // The chips render a beat after the composer: wait for either a chip or the bare picker.
788
+ await waitFor(() => chips().some(c => c.text) || selectRepo(), MENU_WAIT_MS)
789
+ await new Promise(resolve => setTimeout(resolve, window.__oaMenuSettleMs ?? 800))
790
+
791
+ let repoNote
792
+ if (repoWanted.includes(chips()[0]?.text.toLowerCase())) {
793
+ repoNote = `repo already ${chips()[0].text}`
794
+ } else {
795
+ // A remembered other repository's chip is the picker; with nothing remembered, the page offers one.
796
+ const trigger = chips()[0]?.text ? chips()[0] : selectRepo()
797
+ if (!trigger) return { ok: false, note: `no repo picker on the page (${JSON.stringify(probeNewSession())})` }
798
+ let pick = await chooseFrom(trigger, repoWanted, 'repo')
799
+ if (!pick.ok) {
800
+ // The page may have finished loading its remembered repository under us: read again once.
801
+ await new Promise(resolve => setTimeout(resolve, 1500))
802
+ if (repoWanted.includes(chips()[0]?.text.toLowerCase())) pick = { ok: true, note: `repo already ${chips()[0].text} (after a late render)` }
803
+ else return pick
804
+ }
805
+ repoNote = pick.note
806
+ const chip = await waitFor(() => (repoWanted.includes(chips()[0]?.text.toLowerCase()) ? chips()[0] : undefined), MENU_WAIT_MS)
807
+ if (!chip) return { ok: false, note: `${repoNote}, but the repo chip does not read "${repo}" afterwards (chips: ${JSON.stringify(chips().map(c => c.text))})` }
808
+ }
809
+
810
+ // The branch chip appears beside the repo chip once a repository is chosen, reading the default branch.
811
+ const branchWanted = [String(branch).toLowerCase()]
812
+ const readsBranch = () => (chips()[1]?.text.toLowerCase() === branchWanted[0] ? chips()[1] : undefined)
813
+ let branchChip = await waitFor(() => (chips()[1]?.text ? chips()[1] : undefined), MENU_WAIT_MS)
814
+ if (!branchChip) return { ok: false, note: `${repoNote}; no branch chip appeared beside the repo (chips: ${JSON.stringify(chips().map(c => c.text))})` }
815
+ let branchNote
816
+ if (readsBranch()) {
817
+ branchNote = `branch already ${branchChip.text}`
818
+ } else {
819
+ const pick = await chooseFrom(branchChip, branchWanted, 'branch')
820
+ if (!pick.ok) return { ok: false, note: `${repoNote}; ${pick.note}` }
821
+ branchNote = pick.note
822
+ branchChip = await waitFor(readsBranch, MENU_WAIT_MS)
823
+ if (!branchChip) return { ok: false, note: `${repoNote}; ${branchNote}, but the branch chip does not read "${branch}" — not sending (chips: ${JSON.stringify(chips().map(c => c.text))})` }
824
+ }
825
+
826
+ // The model picker remembers the last pick too, so it is read before it is opened.
827
+ let modelNote
828
+ if (model) {
829
+ const wanted = String(model).trim().toLowerCase()
830
+ const readsWanted = () => {
831
+ const trigger = modelTrigger()
832
+ return trigger && readsModel(trigger.text, wanted) ? trigger : undefined
833
+ }
834
+ const trigger = await waitFor(modelTrigger, MENU_WAIT_MS)
835
+ if (!trigger) return { ok: false, note: `${repoNote}; ${branchNote}; no model picker on the page — not sending (${JSON.stringify(probeNewSession())})` }
836
+ if (readsWanted()) {
837
+ modelNote = `model already ${trigger.text}`
838
+ } else {
839
+ const pick = await chooseModel(trigger, wanted)
840
+ if (!pick.ok) return { ok: false, note: `${repoNote}; ${branchNote}; ${pick.note}` }
841
+ modelNote = pick.note
842
+ if (!(await waitFor(readsWanted, MENU_WAIT_MS))) {
843
+ return { ok: false, note: `${repoNote}; ${branchNote}; ${modelNote}, but the model picker does not read "${model}" afterwards — not sending (reads ${JSON.stringify(modelTrigger()?.text ?? null)})` }
844
+ }
845
+ }
846
+ }
847
+
848
+ fillComposer(composer, prompt)
849
+ await new Promise(resolve => setTimeout(resolve, 400))
850
+ const button = findSendButton()
851
+ if (button) button.click()
852
+ else {
853
+ for (const type of ['keydown', 'keyup']) {
854
+ composer.el.dispatchEvent(new KeyboardEvent(type, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }))
855
+ }
856
+ }
857
+
858
+ const sessionId = await waitFor(sessionIdFromUrl, window.__oaSessionWaitMs ?? 60000, 500)
859
+ const notes = [repoNote, branchNote, ...(modelNote ? [modelNote] : [])].join('; ')
860
+ if (!sessionId) return { ok: false, note: `sent, but the page never became a session URL (${notes})` }
861
+ return { ok: true, sessionId, note: `${notes}; sent via ${button ? 'button' : 'enter'}` }
862
+ }
863
+
864
+
865
+ // ---------------------------------------------------------------------------
866
+ // The Driver tab (#1332): one tab serving every watched session. The worker asks this page to
867
+ // read claude.ai's own session list — the status icon beside each session carries a text label —
868
+ // and then to visit the sessions it picked, navigating inside the app: clicking a session's row
869
+ // and the "New" link back, never a page load, so fifty sessions cost fifty clicks. A visited
870
+ // session goes through the same survey and mirror as any other page.
871
+
872
+ /** The list's labels, and the word each becomes on the bridge. Anything else is reported as unknown, with its label. */
873
+ const LIST_LABELS = new Map([
874
+ ['Awaiting input', 'awaiting'],
875
+ ['Unread response', 'unread'],
876
+ ['Idle', 'idle'],
877
+ ['Running', 'running'],
878
+ ])
879
+ /**
880
+ * A pull request's label stands in for the status only with no status word on the row: merged or
881
+ * closed, the session's work landed; in any other state — "Open", "Draft" — the session went
882
+ * quiet and is idle (#1707). Only landed is spelled out: a state the bridge has not seen is
883
+ * still a pull request that has not landed.
884
+ */
885
+ const PR_LABEL = /^#\d+ · /
886
+ const LANDED_LABEL = /^#\d+ · (Merged|Closed)\b/
887
+ const NAV_WAIT_MS = 15000
888
+ const ROWS_WAIT_MS = 20000
889
+ const SEND_WAIT_MS = 15000
890
+ const LIST_PAGE_WAIT_MS = 5000
891
+ const MAX_LIST_PAGES = 10
892
+ const LOG_LINES = 300
893
+ const OVERLAY_ID = 'oa-driver-overlay'
894
+
895
+ /** claude.ai's sign-in page, and the sign-out step that redirects to it: where a person must act. */
896
+ const signInPage = () => /^\/(login|logout)(\/|$)/.test(location.pathname)
897
+
898
+ /** Take the overlay down, if it is up. */
899
+ function removeOverlay() {
900
+ const overlay = document.getElementById(OVERLAY_ID)
901
+ if (overlay) ownWrites(() => overlay.remove())
902
+ }
903
+
904
+ let isDriver = false
905
+ const driverLines = []
906
+
907
+ /**
908
+ * Whether a list read or a drive is running in this page. A second instruction meanwhile is
909
+ * refused as busy rather than run alongside it: two drives would navigate over each other. It
910
+ * happens when the worker that sent the first one ended mid-cycle — the page drives on regardless.
911
+ */
912
+ let driving = false
913
+
914
+ async function whileFree(work) {
915
+ if (driving) return { ok: false, busy: true, note: 'the Driver is still busy with an earlier cycle in this page' }
916
+ driving = true
917
+ try {
918
+ return await work()
919
+ } finally {
920
+ driving = false
921
+ }
922
+ }
923
+
924
+ function setDriver(on) {
925
+ if (!on || isDriver) return
926
+ isDriver = true
927
+ ensureOverlay()
928
+ driverLog(`driver armed on ${location.pathname}`)
929
+ }
930
+
931
+ /**
932
+ * One line of the cycle log: shown behind the overlay's "Show debug logs", and sent to the
933
+ * worker, which stays awake for as long as lines keep coming.
934
+ */
935
+ function driverLog(line) {
936
+ const stamped = `${new Date().toISOString().slice(11, 19)} ${line}`
937
+ driverLines.push(stamped)
938
+ if (driverLines.length > LOG_LINES) driverLines.shift()
939
+ renderOverlay()
940
+ if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) return
941
+ try {
942
+ chrome.runtime.sendMessage({ type: 'oa-driver-log', line: stamped }, () => void chrome.runtime.lastError)
943
+ } catch {
944
+ // Dead context; the log still renders.
945
+ }
946
+ }
947
+
948
+ /** What the list's status icon says about a session row. */
949
+ function statusOf(anchor) {
950
+ const labels = [...anchor.querySelectorAll('[aria-label]')]
951
+ .map(el => el.getAttribute('aria-label') ?? '')
952
+ .filter(label => label && !/^more options/i.test(label))
953
+ for (const label of labels) {
954
+ const known = LIST_LABELS.get(label)
955
+ if (known) return { status: known }
956
+ }
957
+ // Only with no status word on the row does its pull request stand in for one: a row can carry
958
+ // both, and "Awaiting input" beside a pull request is still a session stopped for its user.
959
+ if (labels.some(label => LANDED_LABEL.test(label))) return { status: 'landed' }
960
+ if (labels.some(label => PR_LABEL.test(label))) return { status: 'idle' }
961
+ return { status: 'unknown', label: (labels[0] ?? '').slice(0, 80) }
962
+ }
963
+
964
+ /** The session rows the list shows, by session id: each row links to its session, so the id is in its address. */
965
+ function sessionListRows() {
966
+ const rows = new Map()
967
+ for (const anchor of deepQueryAll('a[href*="/code/session_"]')) {
968
+ const id = /\/code\/(session_[A-Za-z0-9]+)/.exec(anchor.getAttribute('href') ?? '')?.[1]
969
+ if (!id) continue
970
+ const row = { anchor, ...statusOf(anchor) }
971
+ // The first link wins unless a later one carries a status and it did not: a link to the
972
+ // session elsewhere on the page is not its list row.
973
+ if (!rows.has(id) || (rows.get(id).status === 'unknown' && row.status !== 'unknown')) rows.set(id, row)
974
+ }
975
+ return rows
976
+ }
977
+
978
+ /**
979
+ * How far up from a "Show N more" button the container holding session rows may be for the
980
+ * button to count as the list's own. The list's button sits one level from its rows; the
981
+ * middle panel's sits twenty-eight from them, at the app's root (measured 2026-08-25).
982
+ */
983
+ const LIST_BUTTON_REACH = 3
984
+
985
+ /** The list's "Show N more" button, if the list has more to show. The middle panel's is not it. */
986
+ function showMoreButton() {
987
+ const candidates = deepQueryAll('button').filter(b => /^show \d+ more$/i.test((b.textContent ?? '').trim()) && usable(b))
988
+ for (const button of candidates) {
989
+ let el = button.parentElement
990
+ for (let distance = 0; el && distance <= LIST_BUTTON_REACH; el = el.parentElement, distance++) {
991
+ if (el.querySelector('a[href*="/code/session_"]')) return button
992
+ }
993
+ }
994
+ return undefined
995
+ }
996
+
997
+ /**
998
+ * Read the list for these sessions, paging it until each is found or the list ends. The list is
999
+ * fetched after the page reports loaded, so it is waited for before it is read; a page with no
1000
+ * session rows at all is not a list — signed out, or not on the sessions page — and is named
1001
+ * rather than read as every session missing.
1002
+ */
1003
+ async function readSessionList(ids) {
1004
+ if (!(await waitFor(() => sessionListRows().size > 0, ROWS_WAIT_MS))) {
1005
+ driverLog(`list: no session rows on ${location.pathname}`)
1006
+ return { ok: false, note: `no session rows on ${location.pathname}` }
1007
+ }
1008
+ let rows = sessionListRows()
1009
+ let pages = 0
1010
+ while (ids.some(id => !rows.has(id)) && pages < MAX_LIST_PAGES) {
1011
+ const more = showMoreButton()
1012
+ if (!more) break
1013
+ const before = rows.size
1014
+ more.click()
1015
+ pages++
1016
+ const grew = await waitFor(() => sessionListRows().size > before, LIST_PAGE_WAIT_MS)
1017
+ rows = sessionListRows()
1018
+ if (!grew) break
1019
+ }
1020
+ const statuses = ids.map(id => {
1021
+ const row = rows.get(id)
1022
+ return row ? { sessionId: id, status: row.status, ...(row.label ? { label: row.label } : {}) } : { sessionId: id, status: 'missing' }
1023
+ })
1024
+ driverLog(
1025
+ `list: ${rows.size} rows${pages ? ` after ${pages} more page(s)` : ''}; ${statuses.map(s => `${s.sessionId.slice(8, 14)}=${s.status}${s.label ? `(${s.label})` : ''}`).join(' ')}`,
1026
+ )
1027
+ return { ok: true, statuses, listed: rows.size, pages }
1028
+ }
1029
+
1030
+ const atHome = () => location.pathname.replace(/\/+$/, '') === '/code'
1031
+
1032
+ /** Back to the session list through the app's own "New" link: no page load. */
1033
+ async function goHome() {
1034
+ if (atHome()) return true
1035
+ const link = deepQueryAll('a[href="/code"]').find(usable)
1036
+ if (!link) return false
1037
+ link.click()
1038
+ return Boolean(await waitFor(atHome, NAV_WAIT_MS))
1039
+ }
1040
+
1041
+ function composerText() {
1042
+ const composer = findComposer()
1043
+ if (!composer) return ''
1044
+ return (composer.via === 'textarea' ? composer.el.value : composer.el.textContent) ?? ''
1045
+ }
1046
+
1047
+ /**
1048
+ * Visit one session: click its row, wait for its transcript, run the survey (question and mirror
1049
+ * reach the daemon as from any page), and type the queued answer if there is one — counted as
1050
+ * sent only once the composer is empty again and the transcript gained the turn.
1051
+ */
1052
+ async function visitSession(visit) {
1053
+ const row = sessionListRows().get(visit.id)
1054
+ if (!row) return { id: visit.id, ok: false, note: 'not on the list' }
1055
+ driverLog(`visit ${visit.id} (${visit.status})`)
1056
+ const stale = new Set(turnRows())
1057
+ row.anchor.click()
1058
+ if (!(await waitFor(() => sessionIdFromUrl() === visit.id, NAV_WAIT_MS))) return { id: visit.id, ok: false, note: `the page did not become ${visit.id}` }
1059
+ // The address changes before the page does: wait for turn rows that are not the previous
1060
+ // session's, so what is surveyed and typed into is this session's page.
1061
+ await waitFor(() => turnRows().some(r => !stale.has(r)), ROWS_WAIT_MS)
1062
+ await new Promise(resolve => setTimeout(resolve, window.__oaSettleMs ?? 1500))
1063
+ const found = findPendingChoice()
1064
+ const result = { id: visit.id, ok: true, rows: turnRows().length, ...(found ? { question: String(found.parsed.title ?? '').slice(0, 100) } : {}) }
1065
+ if (visit.answer) {
1066
+ const before = new Set(turnRows())
1067
+ let outcome = await deliverAnswer(visit.answer.text)
1068
+ if (outcome.ok) {
1069
+ // Taken once the composer is empty again and a turn row exists that did not before — a
1070
+ // new row, not a higher count, since a long transcript keeps only its tail rendered.
1071
+ const taken = await waitFor(() => !composerText().trim() && turnRows().some(r => !before.has(r)), window.__oaSendWaitMs ?? SEND_WAIT_MS)
1072
+ if (!taken) {
1073
+ outcome = {
1074
+ ok: false,
1075
+ note: `${outcome.note}, but the page did not take the send: composer ${composerText().trim() ? 'still holds text' : 'empty'}, rows ${before.size} -> ${turnRows().length}`,
1076
+ }
1077
+ }
1078
+ }
1079
+ reportTranscript()
1080
+ result.delivered = { sessionId: visit.id, id: visit.answer.id, ok: outcome.ok, ...(outcome.note ? { note: outcome.note } : {}) }
1081
+ }
1082
+ return result
1083
+ }
1084
+
1085
+ /**
1086
+ * One cycle: the session the daemon asked for first, if any — a run is waiting on it, and the
1087
+ * new-session page is where a cycle starts — then the visits, then home.
1088
+ */
1089
+ async function drive({ visits = [], start }) {
1090
+ setDriver(true)
1091
+ driverLog(`cycle: ${start ? 'one session to create, ' : ''}${visits.length} visit(s)`)
1092
+ let started
1093
+ if (start) {
1094
+ const wasHome = atHome()
1095
+ const there = wasHome || (await goHome())
1096
+ // A navigation home swaps the page under us a beat after the address changes.
1097
+ if (there && !wasHome) await new Promise(resolve => setTimeout(resolve, window.__oaSettleMs ?? 1500))
1098
+ started = there ? await createSession(start) : { ok: false, note: 'could not reach the new-session page' }
1099
+ driverLog(`create: ${started.ok ? started.sessionId : started.note}`)
1100
+ }
1101
+ const visited = []
1102
+ const delivered = []
1103
+ for (const visit of visits) {
1104
+ const result = await visitSession(visit)
1105
+ visited.push(result)
1106
+ if (result.delivered) delivered.push(result.delivered)
1107
+ driverLog(
1108
+ ` ${visit.id}: ${
1109
+ result.ok
1110
+ ? `${result.rows} rows${result.question ? `, question "${result.question}"` : ''}${result.delivered ? `, answer ${result.delivered.ok ? 'sent' : `failed: ${result.delivered.note}`}` : ''}`
1111
+ : result.note
1112
+ }`,
1113
+ )
1114
+ }
1115
+ const home = await goHome()
1116
+ if (!home) driverLog('could not get back to the session list')
1117
+ return { ok: true, visited, delivered, ...(started ? { started } : {}), ...(home ? {} : { note: 'could not get back to the session list' }) }
1118
+ }
1119
+
1120
+ /**
1121
+ * The full-page overlay: this tab is not for people. Appended beside the app's root rather than
1122
+ * inside it, so an in-app navigation leaves it alone; the watcher re-appends it if anything does
1123
+ * remove it. Typing goes through the composer's own value and the send button's click, not
1124
+ * through the pointer, so covering the composer costs nothing.
1125
+ */
1126
+ function ensureOverlay() {
1127
+ if (!isDriver) return
1128
+ // The sign-in pages are the one place a person has to act in this tab (the daemon's bridge
1129
+ // browser is signed in exactly there, once), so the overlay stands aside on them and comes back
1130
+ // with the next page.
1131
+ if (signInPage()) return removeOverlay()
1132
+ if (document.getElementById(OVERLAY_ID)) return
1133
+ const overlay = document.createElement('div')
1134
+ overlay.id = OVERLAY_ID
1135
+ overlay.style.cssText = [
1136
+ 'position:fixed', 'inset:0', 'z-index:2147483647', 'background:#0f1218', 'color:#dbdbdb',
1137
+ 'font:15px/1.5 ui-sans-serif,system-ui,sans-serif', 'display:flex', 'flex-direction:column',
1138
+ 'align-items:center', 'justify-content:center', 'padding:32px', 'box-sizing:border-box', 'overflow:auto',
1139
+ ].join(';')
1140
+ const heading = document.createElement('h1')
1141
+ heading.textContent = 'OpenAgent Driver'
1142
+ heading.style.cssText = 'font-size:32px;margin:0 0 12px;color:#a7c080;font-weight:600'
1143
+ const phrase = document.createElement('p')
1144
+ phrase.textContent =
1145
+ 'OpenAgent is using this tab to watch your Claude Code sessions and to type your answers into them. Use another tab for claude.ai. Closing this tab pauses the bridge; the extension’s options page reopens it.'
1146
+ phrase.style.cssText = 'max-width:560px;text-align:center;margin:0 0 16px;color:#aab2c0'
1147
+ const status = document.createElement('div')
1148
+ status.className = 'oa-driver-status'
1149
+ status.style.cssText = 'font:12px/1.45 ui-monospace,monospace;color:#7f8797;text-align:center;max-width:800px'
1150
+ const details = document.createElement('details')
1151
+ details.style.cssText = 'margin-top:20px;width:min(900px,100%)'
1152
+ const summary = document.createElement('summary')
1153
+ summary.textContent = 'Show debug logs'
1154
+ summary.style.cssText = 'cursor:pointer;color:#7f8797;text-align:center'
1155
+ const log = document.createElement('pre')
1156
+ log.className = 'oa-driver-log'
1157
+ log.style.cssText = 'max-height:50vh;overflow:auto;background:#1f2430;padding:12px;border-radius:8px;font:12px/1.45 ui-monospace,monospace;white-space:pre-wrap;margin:8px 0 0'
1158
+ details.append(summary, log)
1159
+ overlay.append(heading, phrase, status, details)
1160
+ ownWrites(() => document.documentElement.appendChild(overlay))
1161
+ renderOverlay()
1162
+ }
1163
+
1164
+ function renderOverlay() {
1165
+ if (signInPage()) return removeOverlay()
1166
+ const overlay = document.getElementById(OVERLAY_ID)
1167
+ if (!overlay) return
1168
+ const version = typeof chrome !== 'undefined' && chrome.runtime?.getManifest ? chrome.runtime.getManifest().version : '?'
1169
+ ownWrites(() => {
1170
+ overlay.querySelector('.oa-driver-status').textContent = `bridge v${version} · ${location.pathname} · ${turnRows().length} turn rows · question ${bridgeStatus} · transcript ${transcriptStatus}`
1171
+ overlay.querySelector('.oa-driver-log').textContent = driverLines.join('\n')
1172
+ })
1173
+ }
1174
+
1175
+ // The worker drives the top frame only: the list and the composer live there, and a child frame
1176
+ // acting too would click and submit twice.
1177
+ if (IS_TOP && typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
1178
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1179
+ if (message?.type === 'oa-read-list' && Array.isArray(message.ids)) {
1180
+ setDriver(true)
1181
+ void whileFree(() => readSessionList(message.ids.filter(id => typeof id === 'string')))
1182
+ .then(sendResponse)
1183
+ .catch(err => sendResponse({ ok: false, note: String(err?.message ?? err) }))
1184
+ return true
1185
+ }
1186
+ if (message?.type !== 'oa-drive') return false
1187
+ void whileFree(() => drive({ visits: Array.isArray(message.visits) ? message.visits : [], start: message.start }))
1188
+ .then(sendResponse)
1189
+ .catch(err => sendResponse({ ok: false, note: String(err?.message ?? err) }))
1190
+ return true
1191
+ })
1192
+ }
1193
+
1194
+ // The offline harness (check.mjs) runs this file in jsdom, where there is no extension runtime
1195
+ // to hand answers through. Exposed only there: on a real page `chrome` exists and nothing is
1196
+ // added to the window claude.ai can see.
1197
+ if (typeof chrome === 'undefined') {
1198
+ window.__oaBridgeDeliverAnswer = deliverAnswer
1199
+ window.__oaBridgeCreateSession = createSession
1200
+ window.__oaBridgeProbeNewSession = probeNewSession
1201
+ window.__oaBridgeReadSessionList = ids => whileFree(() => readSessionList(ids))
1202
+ window.__oaBridgeDrive = args => whileFree(() => drive(args))
1203
+ window.__oaBridgeSurveys = () => surveys
1204
+ }
1205
+
1206
+ /**
1207
+ * What the page looks like when extraction fails. Structure and lengths only, never message
1208
+ * text, so the report stays safe to paste into a public issue.
1209
+ */
1210
+ function diagnostics() {
1211
+ const codes = deepQueryAll('pre, code')
1212
+ .map(el => (el.textContent ?? '').trim())
1213
+ .filter(t => t.includes('"options"') || t.startsWith('{'))
1214
+ const frames = [...document.querySelectorAll('iframe')]
1215
+ let reachableFrames = 0
1216
+ for (const f of frames) {
1217
+ try {
1218
+ if (f.contentDocument?.body) reachableFrames++
1219
+ } catch {
1220
+ // Cross origin, which is itself the answer.
1221
+ }
1222
+ }
1223
+ const deep = deepText()
1224
+ // Did the brace matcher find object-shaped candidates, and did any fail to parse? This is
1225
+ // what separates "never found it" from "found it and could not read it".
1226
+ const stats = { candidates: 0, parseErrors: 0, parsedWithoutOptions: 0, lastError: '' }
1227
+ const all = extractChoices(deep, stats)
1228
+ return {
1229
+ frame: IS_TOP ? 'top' : 'child',
1230
+ jsonCandidates: stats.candidates,
1231
+ jsonObjects: all.length,
1232
+ jsonTemplates: all.filter(isTemplate).length,
1233
+ jsonParseErrors: stats.parseErrors,
1234
+ jsonLastError: stats.lastError,
1235
+ pre: deepQueryAll('pre').length,
1236
+ code: deepQueryAll('code').length,
1237
+ jsonishBlocks: codes.length,
1238
+ shadowRoots: countShadowRoots(),
1239
+ iframes: frames.length,
1240
+ reachableFrames,
1241
+ contentEditables: deepQueryAll('div[contenteditable="true"]').length,
1242
+ textareas: deepQueryAll('textarea').length,
1243
+ // The decisive pair: does the block exist in the top body, and does it exist once
1244
+ // shadow content is included?
1245
+ optionsInBodyText: (document.body?.innerText ?? '').includes('"options"'),
1246
+ optionsInDeepText: deep.includes('"options"'),
1247
+ deepTextLength: deep.length,
1248
+ }
1249
+ }
1250
+
1251
+ function survey() {
1252
+ surveys++
1253
+ const choice = findPendingChoice()
1254
+ const composer = findComposer()
1255
+ return {
1256
+ url: location.href,
1257
+ choiceFound: Boolean(choice),
1258
+ choiceVia: choice?.via,
1259
+ choiceTitle: choice?.parsed?.title,
1260
+ choiceOptions: (choice?.parsed?.options ?? []).map(o => o.label ?? o),
1261
+ composerFound: Boolean(composer),
1262
+ composerVia: composer?.via,
1263
+ diagnostics: diagnostics(),
1264
+ }
1265
+ }
1266
+
1267
+ // A child frame has no panel of its own: it reports upward, so the top frame's report says
1268
+ // whether the content was in a frame and which one. This is what settles the iframe question.
1269
+ if (!IS_TOP) {
1270
+ const send = () => {
1271
+ const s = survey()
1272
+ if (s.choiceFound || s.diagnostics.jsonishBlocks || s.diagnostics.optionsInDeepText) {
1273
+ parent.postMessage({ __oaBridge: s }, '*')
1274
+ }
1275
+ }
1276
+ send()
1277
+ watch(send)
1278
+ } else {
1279
+ let fromFrame
1280
+ window.addEventListener('message', e => {
1281
+ if (e.data && typeof e.data === 'object' && e.data.__oaBridge) fromFrame = e.data.__oaBridge
1282
+ })
1283
+
1284
+ const panel = document.createElement('div')
1285
+ panel.id = PANEL_ID
1286
+ panel.style.cssText = [
1287
+ 'position:fixed', 'bottom:16px', 'right:16px', 'z-index:2147483647',
1288
+ 'width:360px', 'max-height:70vh', 'overflow:auto',
1289
+ 'background:#1f2430', 'color:#dbdbdb', 'font:12px/1.45 ui-monospace,monospace',
1290
+ 'border:1px solid #3a4150', 'border-radius:8px', 'padding:10px 12px',
1291
+ 'box-shadow:0 6px 24px rgba(0,0,0,.35)',
1292
+ ].join(';')
1293
+ document.documentElement.appendChild(panel)
1294
+
1295
+ let latest = survey()
1296
+ // Announce the page once whatever it is: on the session list there is no session to report,
1297
+ // but the worker's reply is what tells the Driver tab it is the Driver (#1332).
1298
+ sayHello(sessionIdFromUrl(), `page loaded on ${location.pathname}`)
1299
+
1300
+ // Whether the panel is folded down to a compact OA tab. Remembered in chrome.storage rather than
1301
+ // the page's localStorage: the preference should survive a reload, and nothing the extension
1302
+ // keeps should be readable by the page it watches. Restored asynchronously, so the panel can
1303
+ // open expanded for a beat before the remembered fold lands.
1304
+ let collapsed = false
1305
+ if (typeof chrome !== 'undefined' && chrome.storage?.local) {
1306
+ try {
1307
+ chrome.storage.local.get('panelCollapsed', stored => {
1308
+ if (!chrome.runtime.lastError && stored?.panelCollapsed) {
1309
+ collapsed = true
1310
+ render()
1311
+ }
1312
+ })
1313
+ } catch {
1314
+ // Dead context; the panel starts expanded, and reloading the tab is the fix anyway.
1315
+ }
1316
+ }
1317
+
1318
+ const button = (label, onClick) => {
1319
+ const b = document.createElement('button')
1320
+ b.textContent = label
1321
+ b.style.cssText = 'margin-top:8px;width:100%;padding:5px;cursor:pointer;background:#2d3441;color:#dbdbdb;border:1px solid #3a4150;border-radius:5px;font:inherit'
1322
+ b.addEventListener('click', onClick)
1323
+ return b
1324
+ }
1325
+
1326
+ // Drawn under `ownWrites`: the redraw must not read as a page change (#1707).
1327
+ const render = () => ownWrites(() => {
1328
+ const top = survey()
1329
+ // A child frame's find wins: it means the content lives there, which is the finding.
1330
+ latest = { ...top, fromFrame: fromFrame ?? null }
1331
+ // The Driver tab (#1332) shows its overlay instead of the corner panel; the survey above
1332
+ // still ran, so its sessions are read and mirrored like any other page's.
1333
+ if (isDriver) {
1334
+ panel.style.display = 'none'
1335
+ ensureOverlay()
1336
+ renderOverlay()
1337
+ return
1338
+ }
1339
+ panel.style.display = ''
1340
+ const winner = top.choiceFound ? top : fromFrame?.choiceFound ? fromFrame : top
1341
+ const d = top.diagnostics
1342
+ panel.innerHTML = ''
1343
+ panel.style.width = collapsed ? 'auto' : '360px'
1344
+ panel.style.padding = collapsed ? '6px 8px' : '10px 12px'
1345
+ const head = document.createElement('div')
1346
+ head.style.cssText = `display:flex;align-items:center;gap:8px${collapsed ? '' : ';margin-bottom:6px'}`
1347
+ const title = document.createElement('span')
1348
+ const version = typeof chrome !== 'undefined' && chrome.runtime?.getManifest ? chrome.runtime.getManifest().version : '?'
1349
+ const fullTitle = `OpenAgent bridge v${version}`
1350
+ // Folded, the label shrinks to "OA": the fold exists to give the corner back, so the full
1351
+ // name and version retreat to the tooltip.
1352
+ title.textContent = collapsed ? 'OA' : fullTitle
1353
+ if (collapsed) title.title = fullTitle
1354
+ title.style.cssText = 'font-weight:600;color:#a7c080;flex:1'
1355
+ const toggle = document.createElement('button')
1356
+ toggle.textContent = collapsed ? '+' : '−'
1357
+ toggle.title = collapsed ? 'Expand' : 'Collapse'
1358
+ toggle.setAttribute('aria-label', toggle.title)
1359
+ toggle.setAttribute('aria-expanded', String(!collapsed))
1360
+ toggle.style.cssText =
1361
+ 'flex:none;width:20px;height:20px;padding:0;cursor:pointer;background:#2d3441;color:#dbdbdb;border:1px solid #3a4150;border-radius:5px;font:inherit;line-height:1'
1362
+ toggle.addEventListener('click', () => {
1363
+ collapsed = !collapsed
1364
+ if (typeof chrome !== 'undefined' && chrome.storage?.local) {
1365
+ try {
1366
+ chrome.storage.local.set({ panelCollapsed: collapsed }, () => void chrome.runtime.lastError)
1367
+ } catch {
1368
+ // Dead context; the choice still holds until the tab reloads.
1369
+ }
1370
+ }
1371
+ render()
1372
+ })
1373
+ head.append(title, toggle)
1374
+ panel.appendChild(head)
1375
+ // Folded, the OA tab is all there is to draw. The survey above still ran: collapsing
1376
+ // hides the detail, not the bridge, so the daemon keeps hearing from this page.
1377
+ if (collapsed) return
1378
+ const rows = [
1379
+ ['question found', winner.choiceFound ? `yes (${winner.choiceVia}${winner === fromFrame ? ', in iframe' : ''})` : `no${choiceNote ? ` (${choiceNote})` : ''}`],
1380
+ ['title', winner.choiceTitle ?? '-'],
1381
+ ['options', winner.choiceOptions?.length ? winner.choiceOptions.join(' | ') : '-'],
1382
+ ['composer', top.composerFound ? top.composerVia : 'not found'],
1383
+ ['bridge', bridgeStatus],
1384
+ ['answer', answerStatus],
1385
+ // Always shown: the transcript is the half that runs even when no question was asked, so
1386
+ // it needs its own line rather than sharing the question's.
1387
+ ['transcript', transcriptStatus],
1388
+ ['turn rows', turnRows().length],
1389
+ ]
1390
+ if (!winner.choiceFound) {
1391
+ rows.push(
1392
+ ['code / pre', `${d.code} / ${d.pre}`],
1393
+ ['shadow roots', d.shadowRoots],
1394
+ ['iframes', `${d.iframes} (reachable ${d.reachableFrames})`],
1395
+ ['"options" body', String(d.optionsInBodyText)],
1396
+ ['"options" deep', String(d.optionsInDeepText)],
1397
+ ['json candidates', `${d.jsonCandidates} (parse errors ${d.jsonParseErrors})`],
1398
+ ...(d.jsonLastError ? [['last parse error', d.jsonLastError]] : []),
1399
+ ['frame reported', fromFrame ? 'yes' : 'no'],
1400
+ )
1401
+ }
1402
+ for (const [k, v] of rows) {
1403
+ const line = document.createElement('div')
1404
+ line.style.cssText = 'display:flex;gap:8px;margin:2px 0'
1405
+ const key = document.createElement('span')
1406
+ key.style.cssText = 'color:#7f8797;min-width:104px;flex:none'
1407
+ key.textContent = k
1408
+ const val = document.createElement('span')
1409
+ val.style.cssText = 'word-break:break-word'
1410
+ val.textContent = String(v)
1411
+ line.append(key, val)
1412
+ panel.appendChild(line)
1413
+ }
1414
+ panel.appendChild(button('Copy report', () => {
1415
+ void navigator.clipboard.writeText(JSON.stringify(latest, null, 2))
1416
+ }))
1417
+ // Fills without submitting, on purpose: it proves the write path exists without the
1418
+ // extension ever speaking for the user.
1419
+ panel.appendChild(button('Fill composer (does not send)', () => {
1420
+ const composer = findComposer()
1421
+ if (!composer) return
1422
+ const text = winner.choiceOptions?.[0] ?? 'test'
1423
+ if (composer.via === 'textarea') {
1424
+ composer.el.value = text
1425
+ composer.el.dispatchEvent(new Event('input', { bubbles: true }))
1426
+ } else {
1427
+ composer.el.focus()
1428
+ document.execCommand('insertText', false, text)
1429
+ }
1430
+ }))
1431
+ })
1432
+
1433
+ render()
1434
+ watch(render)
1435
+ }
1436
+
1437
+ /**
1438
+ * Re-survey when the page actually changes, plus a slow heartbeat.
1439
+ *
1440
+ * Not a fast `setInterval`: this is designed to run in a background tab, and Chrome clamps timers
1441
+ * in a tab hidden for more than about five minutes to roughly once a minute. The session's own
1442
+ * stream mutates the DOM whenever anything happens, so an observer catches it immediately and the
1443
+ * interval is only a backstop for a mutation we failed to see.
1444
+ */
1445
+ function watch(run) {
1446
+ let queued = false
1447
+ let interval
1448
+ // Reloading the extension invalidates this script's runtime but leaves it running in the tab,
1449
+ // where every chrome.* call now throws. Those throws land on the extension's error page and
1450
+ // read as bugs, so a dead context stops watching instead of erroring once a minute forever.
1451
+ const alive = () => {
1452
+ try {
1453
+ if (typeof chrome === 'undefined' || chrome.runtime?.id) return true
1454
+ } catch {
1455
+ // Reading chrome.runtime itself can throw once the context is gone.
1456
+ }
1457
+ observer.disconnect()
1458
+ watchers.delete(observer)
1459
+ clearInterval(interval)
1460
+ return false
1461
+ }
1462
+ const observer = new MutationObserver(() => {
1463
+ if (queued || !alive()) return
1464
+ queued = true
1465
+ setTimeout(() => {
1466
+ queued = false
1467
+ run()
1468
+ }, 250)
1469
+ })
1470
+ observer.observe(document.documentElement, OBSERVED)
1471
+ watchers.add(observer)
1472
+ interval = setInterval(() => {
1473
+ if (alive()) run()
1474
+ }, 30 * POLL_MS)
1475
+ }