specrails-desktop 2.36.0 → 2.42.2

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 (510) hide show
  1. package/README.md +94 -247
  2. package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
  3. package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
  7. package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
  9. package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
  11. package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
  12. package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
  13. package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
  16. package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
  17. package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
  20. package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
  22. package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
  24. package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
  25. package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
  26. package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
  29. package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
  30. package/client/dist/assets/agent-BdT0SB1D.js +1 -0
  31. package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
  32. package/client/dist/assets/agent-C4ibdFk9.js +1 -0
  33. package/client/dist/assets/agent-CSFY22Zo.js +1 -0
  34. package/client/dist/assets/agent-DAnqxUn8.js +1 -0
  35. package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
  36. package/client/dist/assets/agent-T54WuR_Y.js +1 -0
  37. package/client/dist/assets/agent-i75WDZOA.js +1 -0
  38. package/client/dist/assets/browser-05vRDg_y.js +1 -0
  39. package/client/dist/assets/browser-Bulb-cB1.js +1 -0
  40. package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
  41. package/client/dist/assets/browser-CG0PE1j_.js +1 -0
  42. package/client/dist/assets/browser-CMzaqAjo.js +1 -0
  43. package/client/dist/assets/browser-DMajYdNX.js +1 -0
  44. package/client/dist/assets/browser-DxcOHrdP.js +1 -0
  45. package/client/dist/assets/browser-ObE7xVo3.js +1 -0
  46. package/client/dist/assets/builder-765NeYTp.js +1 -0
  47. package/client/dist/assets/builder-8MG3V6ea.js +1 -0
  48. package/client/dist/assets/builder-B-1UBs5K.js +1 -0
  49. package/client/dist/assets/builder-BTiGnPT0.js +1 -0
  50. package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
  51. package/client/dist/assets/builder-DKFMELBl.js +1 -0
  52. package/client/dist/assets/builder-OcmKrC0h.js +1 -0
  53. package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
  54. package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
  55. package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
  56. package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
  57. package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
  58. package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
  59. package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
  60. package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
  61. package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
  62. package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
  63. package/client/dist/assets/commands-0XjAftla.js +1 -0
  64. package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
  65. package/client/dist/assets/commands-BBwowgiE.js +1 -0
  66. package/client/dist/assets/commands-BacjIZLN.js +1 -0
  67. package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
  68. package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
  69. package/client/dist/assets/commands-DlbFzgrp.js +1 -0
  70. package/client/dist/assets/commands-LjPSsi5F.js +1 -0
  71. package/client/dist/assets/common-B5AZNYMH.js +1 -0
  72. package/client/dist/assets/common-BFZ0c_vN.js +1 -0
  73. package/client/dist/assets/common-BrlwEnL7.js +1 -0
  74. package/client/dist/assets/common-BttQx3z4.js +1 -0
  75. package/client/dist/assets/common-CM65k_-e.js +1 -0
  76. package/client/dist/assets/common-D4mvFFlu.js +1 -0
  77. package/client/dist/assets/common-TZ9Ny96S.js +1 -0
  78. package/client/dist/assets/common-nFpSF4GC.js +1 -0
  79. package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
  80. package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
  81. package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
  82. package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
  83. package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
  84. package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
  85. package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
  86. package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
  87. package/client/dist/assets/dialog-SRw01_QU.js +45 -0
  88. package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
  89. package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
  91. package/client/dist/assets/index-DwAmL7m5.css +2 -0
  92. package/client/dist/assets/index-EzrkUb8B.js +76 -0
  93. package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
  94. package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
  95. package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
  96. package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
  97. package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
  98. package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
  99. package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
  100. package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
  101. package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
  102. package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
  103. package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
  104. package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
  105. package/client/dist/assets/mcp-CCpSelOh.js +1 -0
  106. package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
  107. package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
  108. package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
  109. package/client/dist/assets/mcp-DUVQamDz.js +1 -0
  110. package/client/dist/assets/narration-B-ynWTpn.js +1 -0
  111. package/client/dist/assets/narration-Bj_laZwg.js +1 -0
  112. package/client/dist/assets/narration-BxHpmMv_.js +1 -0
  113. package/client/dist/assets/narration-C1Px0-0j.js +1 -0
  114. package/client/dist/assets/narration-CAXhGbzM.js +1 -0
  115. package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
  116. package/client/dist/assets/narration-XISXwqZd.js +1 -0
  117. package/client/dist/assets/narration-kwzfglQo.js +1 -0
  118. package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
  119. package/client/dist/assets/nav-B34QkESm.js +1 -0
  120. package/client/dist/assets/nav-Bap_UXAq.js +1 -0
  121. package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
  122. package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
  123. package/client/dist/assets/nav-CygC_WN-.js +1 -0
  124. package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
  125. package/client/dist/assets/nav-m00uxYUG.js +1 -0
  126. package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
  127. package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
  128. package/client/dist/assets/packet-C3Ievb9R.js +1 -0
  129. package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
  130. package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
  131. package/client/dist/assets/packet-DUTGX52M.js +1 -0
  132. package/client/dist/assets/packet-DhsKtUPB.js +1 -0
  133. package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
  134. package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
  135. package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
  136. package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
  137. package/client/dist/assets/settings-BJB9RKJj.js +1 -0
  138. package/client/dist/assets/settings-CHwQR2rB.js +1 -0
  139. package/client/dist/assets/settings-CrMi3kSi.js +1 -0
  140. package/client/dist/assets/settings-DKyhDGD1.js +1 -0
  141. package/client/dist/assets/settings-Deupji9c.js +1 -0
  142. package/client/dist/assets/settings-DqRMmwAM.js +1 -0
  143. package/client/dist/assets/settings-J4rj6vo-.js +1 -0
  144. package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
  145. package/client/dist/assets/setup-Ba11l2JA.js +1 -0
  146. package/client/dist/assets/setup-BzW4kMok.js +1 -0
  147. package/client/dist/assets/setup-COzwYhkJ.js +1 -0
  148. package/client/dist/assets/setup-CRVULhtF.js +1 -0
  149. package/client/dist/assets/setup-DgvlxwpE.js +1 -0
  150. package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
  151. package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
  152. package/client/dist/assets/spending-BqbdPm_4.js +1 -0
  153. package/client/dist/assets/square-Ci-PFcFe.js +1 -0
  154. package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
  155. package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
  156. package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
  157. package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
  158. package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
  159. package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
  160. package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
  161. package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
  162. package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
  163. package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
  164. package/client/dist/index.html +34 -35
  165. package/docs/README.md +3 -0
  166. package/docs/agent-live-steering.md +57 -0
  167. package/docs/ci-cd.md +57 -0
  168. package/docs/code-explorer.md +48 -0
  169. package/docs/codex.md +12 -1
  170. package/docs/features/detachable-mission-windows.md +59 -0
  171. package/docs/guide/de/integrations/5-mcp-server.md +13 -9
  172. package/docs/guide/de/integrations/6-agent-chat.md +1 -0
  173. package/docs/guide/en/integrations/5-mcp-server.md +13 -9
  174. package/docs/guide/en/integrations/6-agent-chat.md +1 -0
  175. package/docs/guide/es/integrations/5-mcp-server.md +13 -9
  176. package/docs/guide/es/integrations/6-agent-chat.md +1 -0
  177. package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
  178. package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
  179. package/docs/guide/it/integrations/5-mcp-server.md +13 -9
  180. package/docs/guide/it/integrations/6-agent-chat.md +1 -0
  181. package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
  182. package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
  183. package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
  184. package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
  185. package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
  186. package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
  187. package/docs/internals/README.md +1 -0
  188. package/docs/internals/browser-capture-performance.md +55 -3
  189. package/docs/internals/browser-login-popups.md +45 -0
  190. package/docs/internals/browser-native-retina-audit.md +39 -0
  191. package/docs/internals/ci-cd-audit.md +39 -0
  192. package/docs/internals/core-runtime-updates.md +68 -0
  193. package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
  194. package/docs/internals/implementation-reliability-audit.md +93 -0
  195. package/docs/internals/interactive-jobs.md +110 -6
  196. package/docs/internals/mcp-mission-audit.md +76 -0
  197. package/docs/internals/project-builder.md +404 -42
  198. package/docs/internals/safe-pr-review-flow.md +52 -10
  199. package/docs/internals/startup-recovery-audit.md +58 -0
  200. package/docs/mcp.md +74 -41
  201. package/docs/mission-processes.md +59 -0
  202. package/docs/multi-repo-projects.md +51 -0
  203. package/docs/platforms/windows-parity.md +70 -0
  204. package/docs/platforms/windows.md +13 -11
  205. package/docs/running-pipelines.md +14 -0
  206. package/mcp-bridge/dist/specrails-mcp.js +7186 -0
  207. package/package.json +9 -4
  208. package/server/dist/agent-chat-manager.js +537 -68
  209. package/server/dist/agent-chat-router.js +87 -6
  210. package/server/dist/agent-context-resolver.js +129 -25
  211. package/server/dist/agent-defaults.js +19 -11
  212. package/server/dist/agent-input-store.js +206 -0
  213. package/server/dist/agent-mcp-config.js +54 -15
  214. package/server/dist/agent-operator-prompt.js +255 -65
  215. package/server/dist/agent-spec-framing.js +202 -0
  216. package/server/dist/agent-steering.js +218 -0
  217. package/server/dist/agent-store.js +153 -0
  218. package/server/dist/api-not-found.js +8 -0
  219. package/server/dist/background-process-control.js +164 -0
  220. package/server/dist/background-process-service.js +61 -0
  221. package/server/dist/background-process-store.js +242 -0
  222. package/server/dist/background-windows-bootstrap.js +238 -0
  223. package/server/dist/blueprint-chat-manager.js +476 -62
  224. package/server/dist/blueprint-commit.js +21 -1
  225. package/server/dist/blueprint-draft-parser.js +122 -17
  226. package/server/dist/blueprint-generation.js +238 -0
  227. package/server/dist/blueprint-operator-prompt.js +91 -63
  228. package/server/dist/blueprint-router.js +55 -3
  229. package/server/dist/blueprint-spec-fixtures.js +85 -0
  230. package/server/dist/blueprint-spec-quality.js +57 -16
  231. package/server/dist/blueprint-store.js +92 -1
  232. package/server/dist/browser-capture-manager.js +86 -30
  233. package/server/dist/browser-playwright.js +168 -45
  234. package/server/dist/browser-viewport.js +26 -0
  235. package/server/dist/chat-manager.js +23 -15
  236. package/server/dist/chromium-archive.cjs +83 -0
  237. package/server/dist/chromium-resolver.js +72 -64
  238. package/server/dist/code-activity.js +145 -0
  239. package/server/dist/code-explorer-router.js +714 -114
  240. package/server/dist/core-compat.js +28 -1
  241. package/server/dist/core-execution.js +111 -0
  242. package/server/dist/core-package.js +4 -16
  243. package/server/dist/core-runtime.js +202 -0
  244. package/server/dist/core-update-manager.js +161 -60
  245. package/server/dist/core-update-state.js +29 -0
  246. package/server/dist/db.js +66 -35
  247. package/server/dist/delivery-evidence.js +142 -39
  248. package/server/dist/desktop-db.js +239 -43
  249. package/server/dist/desktop-router.js +109 -9
  250. package/server/dist/dev-ports.js +8 -3
  251. package/server/dist/file-provenance.js +34 -22
  252. package/server/dist/file-story-manager.js +62 -64
  253. package/server/dist/file-story.js +118 -35
  254. package/server/dist/file-summary-generator.js +46 -5
  255. package/server/dist/file-summary-manager.js +355 -106
  256. package/server/dist/framework-manager.js +98 -110
  257. package/server/dist/framework-reseed.js +76 -10
  258. package/server/dist/git-diagnostics.js +2 -2
  259. package/server/dist/headroom-manager.js +38 -3
  260. package/server/dist/host-control.js +52 -0
  261. package/server/dist/index.js +43 -2
  262. package/server/dist/interactive-job-session.js +149 -16
  263. package/server/dist/internal-api.js +48 -0
  264. package/server/dist/jira/jira-materializer.js +1 -0
  265. package/server/dist/json-tolerant.js +174 -0
  266. package/server/dist/legacy-migration.js +4 -2
  267. package/server/dist/loop-command-catalog.js +83 -21
  268. package/server/dist/loop-constants.js +6 -1
  269. package/server/dist/loop-decider.js +11 -1
  270. package/server/dist/loop-executors.js +194 -45
  271. package/server/dist/loop-factory.js +11 -4
  272. package/server/dist/loop-graph.js +90 -4
  273. package/server/dist/loop-run-manager.js +301 -69
  274. package/server/dist/loop-shell-invocation.js +21 -0
  275. package/server/dist/loop-step-idle.js +55 -0
  276. package/server/dist/loop-templates.js +31 -7
  277. package/server/dist/mcp/agent-capability.js +37 -4
  278. package/server/dist/mcp/guide.js +149 -36
  279. package/server/dist/mcp/mcp-admin-router.js +20 -4
  280. package/server/dist/mcp/mcp-server.js +89 -43
  281. package/server/dist/mcp/mcp-tiers.js +6 -5
  282. package/server/dist/mcp/mcp-token.js +3 -1
  283. package/server/dist/mcp/resources.js +5 -4
  284. package/server/dist/mcp/tools/catalog.js +4 -0
  285. package/server/dist/mcp/tools/code.js +125 -14
  286. package/server/dist/mcp/tools/context.js +153 -0
  287. package/server/dist/mcp/tools/git.js +15 -5
  288. package/server/dist/mcp/tools/jobs.js +84 -71
  289. package/server/dist/mcp/tools/loops.js +17 -6
  290. package/server/dist/mcp/tools/meta.js +74 -27
  291. package/server/dist/mcp/tools/mission.js +29 -0
  292. package/server/dist/mcp/tools/projects.js +22 -6
  293. package/server/dist/mcp/tools/rails.js +48 -11
  294. package/server/dist/mcp/tools/specs.js +86 -14
  295. package/server/dist/mcp/tools/support.js +1 -1
  296. package/server/dist/mcp/tools/types.js +117 -43
  297. package/server/dist/mcp/tools/watch.js +144 -51
  298. package/server/dist/milestone-chain-store.js +164 -0
  299. package/server/dist/milestone-chain.js +487 -0
  300. package/server/dist/milestone-progress.js +348 -0
  301. package/server/dist/mobile/mobile-devices.js +10 -7
  302. package/server/dist/mobile/mobile-gateway.js +2 -0
  303. package/server/dist/mobile/mobile-missions.js +214 -0
  304. package/server/dist/mobile/mobile-redact.js +17 -3
  305. package/server/dist/mobile/mobile-router.js +46 -4
  306. package/server/dist/mobile/mobile-ws.js +49 -4
  307. package/server/dist/multi-repo-bases.js +56 -0
  308. package/server/dist/multi-repo-checkout.js +101 -0
  309. package/server/dist/multi-repo-delivery.js +100 -0
  310. package/server/dist/multi-repo-execution-store.js +65 -0
  311. package/server/dist/multi-repo-execution.js +453 -0
  312. package/server/dist/offline-assemble.js +10 -8
  313. package/server/dist/openspec-runtime-plugin-commands.json +18 -0
  314. package/server/dist/openspec-runtime-plugin.js +120 -0
  315. package/server/dist/plugins/codex-spawn.js +49 -0
  316. package/server/dist/plugins/serena/manifest.js +2 -2
  317. package/server/dist/plugins/serena/templates/instructions.md +16 -0
  318. package/server/dist/pr-publisher.js +1 -1
  319. package/server/dist/profiles-router.js +3 -18
  320. package/server/dist/project-code-discovery.js +142 -0
  321. package/server/dist/project-git.js +19 -21
  322. package/server/dist/project-registry.js +272 -20
  323. package/server/dist/project-repositories.js +157 -0
  324. package/server/dist/project-repository-provenance.js +31 -0
  325. package/server/dist/project-router-background-processes.js +105 -0
  326. package/server/dist/project-router-git.js +75 -16
  327. package/server/dist/project-router-jobs.js +2 -126
  328. package/server/dist/project-router-loop-runs.js +72 -2
  329. package/server/dist/project-router-tickets.js +53 -8
  330. package/server/dist/project-router.js +239 -38
  331. package/server/dist/provider-detection.js +18 -6
  332. package/server/dist/provider-limit.js +64 -0
  333. package/server/dist/providers/claude-adapter.js +26 -1
  334. package/server/dist/providers/claude-background-tasks.js +18 -0
  335. package/server/dist/providers/claude-live-session.js +383 -0
  336. package/server/dist/providers/codex-adapter.js +19 -0
  337. package/server/dist/providers/codex-live-session.js +545 -0
  338. package/server/dist/providers/gemini-adapter.js +1 -0
  339. package/server/dist/providers/live-session-types.js +12 -0
  340. package/server/dist/providers/live-session.js +13 -0
  341. package/server/dist/providers/terminal-result.js +24 -0
  342. package/server/dist/queue-manager.js +2 -0
  343. package/server/dist/rail-isolated-launch.js +141 -16
  344. package/server/dist/rail-pr-decision.js +364 -99
  345. package/server/dist/rail-pr-store.js +24 -10
  346. package/server/dist/rail-worktree-release.js +34 -2
  347. package/server/dist/rail-worktrees-store.js +4 -4
  348. package/server/dist/rails-router.js +249 -55
  349. package/server/dist/repo-lock.js +92 -17
  350. package/server/dist/review-packet.js +23 -13
  351. package/server/dist/schemas/file-summary.v1.json +59 -59
  352. package/server/dist/schemas/profile.v1.json +137 -137
  353. package/server/dist/setup-manager.js +52 -8
  354. package/server/dist/shell-integration/bash-shim.bash +45 -0
  355. package/server/dist/shell-integration/fish-shim.fish +36 -0
  356. package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
  357. package/server/dist/shell-integration/zsh-shim.zsh +47 -0
  358. package/server/dist/smash-runner.js +1 -0
  359. package/server/dist/spawn-lifecycle.js +44 -27
  360. package/server/dist/spec-contract-prompt.js +143 -0
  361. package/server/dist/spec-draft-parser.js +6 -1
  362. package/server/dist/spec-models.js +1 -0
  363. package/server/dist/stuck-run-detector.js +1 -0
  364. package/server/dist/terminal-manager.js +21 -1
  365. package/server/dist/terminal-shell-integration.js +4 -1
  366. package/server/dist/transient-children.js +619 -230
  367. package/server/dist/util/cli-prompt.js +78 -5
  368. package/server/dist/util/sqlite-migrations.js +20 -0
  369. package/server/dist/util/win-spawn.js +18 -3
  370. package/server/dist/util/windows-provider-shell.js +29 -0
  371. package/server/dist/verification-sentinel.js +14 -0
  372. package/server/dist/vitest-setup.js +2 -0
  373. package/server/dist/windows-job-supervisor.js +133 -0
  374. package/server/dist/workspace-manager.js +9 -30
  375. package/server/dist/worktree-manager.js +17 -2
  376. package/server/dist/worktree-overlay.js +3 -1
  377. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
  378. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
  379. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
  380. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
  381. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
  382. package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
  383. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
  384. package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
  385. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
  386. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
  387. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
  388. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
  389. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
  390. package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
  391. package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
  392. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
  393. package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
  394. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
  395. package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
  396. package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
  397. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
  398. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
  399. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
  400. package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
  401. package/client/dist/assets/agent-BQgKv_2F.js +0 -1
  402. package/client/dist/assets/agent-C1NRDJqL.js +0 -1
  403. package/client/dist/assets/agent-CLNleiy6.js +0 -1
  404. package/client/dist/assets/agent-CmceArkQ.js +0 -1
  405. package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
  406. package/client/dist/assets/agent-DB1JeE64.js +0 -1
  407. package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
  408. package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
  409. package/client/dist/assets/auth-D_5bQv5W.js +0 -1
  410. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  411. package/client/dist/assets/bot-DzuuuC5R.js +0 -1
  412. package/client/dist/assets/browser-BgqHDMXP.js +0 -1
  413. package/client/dist/assets/browser-CsKJtrpv.js +0 -1
  414. package/client/dist/assets/browser-Cu58EpZU.js +0 -1
  415. package/client/dist/assets/browser-CvXOJm-j.js +0 -1
  416. package/client/dist/assets/browser-DOrFh1c7.js +0 -1
  417. package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
  418. package/client/dist/assets/browser-LPgxvJu2.js +0 -1
  419. package/client/dist/assets/browser-eL-3MIEI.js +0 -1
  420. package/client/dist/assets/builder-0IWtosUm.js +0 -1
  421. package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
  422. package/client/dist/assets/builder-BnqaaBkT.js +0 -1
  423. package/client/dist/assets/builder-BxxfGgAO.js +0 -1
  424. package/client/dist/assets/builder-C5IIgWsq.js +0 -1
  425. package/client/dist/assets/builder-CHuahItu.js +0 -1
  426. package/client/dist/assets/builder-CjWosXL_.js +0 -1
  427. package/client/dist/assets/builder-CpCIOoKj.js +0 -1
  428. package/client/dist/assets/code-CHPkOkDS.js +0 -1
  429. package/client/dist/assets/commands-6tYQih5m.js +0 -1
  430. package/client/dist/assets/commands-BK2QinXN.js +0 -1
  431. package/client/dist/assets/commands-C2Nca76h.js +0 -1
  432. package/client/dist/assets/commands-CKdLihNX.js +0 -1
  433. package/client/dist/assets/commands-CYIBTAlc.js +0 -1
  434. package/client/dist/assets/commands-CjUtYJIM.js +0 -1
  435. package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
  436. package/client/dist/assets/commands-D_ZHYQug.js +0 -1
  437. package/client/dist/assets/common-6MLbD-jM.js +0 -1
  438. package/client/dist/assets/common-BKGEu4rm.js +0 -1
  439. package/client/dist/assets/common-BNbnv8cE.js +0 -1
  440. package/client/dist/assets/common-BqGWtDbt.js +0 -1
  441. package/client/dist/assets/common-DBtTzeYm.js +0 -1
  442. package/client/dist/assets/common-DEW3Nr8n.js +0 -1
  443. package/client/dist/assets/common-DHsv9N5d.js +0 -1
  444. package/client/dist/assets/common-mkrEcW-Y.js +0 -1
  445. package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
  446. package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
  447. package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
  448. package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
  449. package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
  450. package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
  451. package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
  452. package/client/dist/assets/dialog-CdWCb02j.js +0 -45
  453. package/client/dist/assets/dist-bVqfYmby.js +0 -1
  454. package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
  455. package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
  456. package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
  457. package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
  458. package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
  459. package/client/dist/assets/index-DIhMEAVw.js +0 -66
  460. package/client/dist/assets/index-DwygcU7s.css +0 -2
  461. package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
  462. package/client/dist/assets/mcp-C2AojZZN.js +0 -1
  463. package/client/dist/assets/mcp-D8BT78_L.js +0 -1
  464. package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
  465. package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
  466. package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
  467. package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
  468. package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
  469. package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
  470. package/client/dist/assets/narration-BGuiEDj3.js +0 -1
  471. package/client/dist/assets/narration-BuSoqRmv.js +0 -1
  472. package/client/dist/assets/narration-CB9t29fy.js +0 -1
  473. package/client/dist/assets/narration-CU3x8FHD.js +0 -1
  474. package/client/dist/assets/narration-CWeXhfP5.js +0 -1
  475. package/client/dist/assets/narration-DRaDnhT5.js +0 -1
  476. package/client/dist/assets/narration-DtEbQgCw.js +0 -1
  477. package/client/dist/assets/nav-ApJVg3xu.js +0 -1
  478. package/client/dist/assets/nav-BKNky1Bg.js +0 -1
  479. package/client/dist/assets/nav-C6vWWKYx.js +0 -1
  480. package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
  481. package/client/dist/assets/nav-CsmFBIDf.js +0 -1
  482. package/client/dist/assets/nav-DjkmQJjd.js +0 -1
  483. package/client/dist/assets/packet-B1IxaL5f.js +0 -1
  484. package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
  485. package/client/dist/assets/packet-By8fqYxW.js +0 -1
  486. package/client/dist/assets/packet-CZdvA44E.js +0 -1
  487. package/client/dist/assets/packet-CrjHRQCP.js +0 -1
  488. package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
  489. package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
  490. package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
  491. package/client/dist/assets/play-DNlrawbS.js +0 -1
  492. package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
  493. package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
  494. package/client/dist/assets/settings-C1-7cXzC.js +0 -1
  495. package/client/dist/assets/settings-CAFaT38b.js +0 -1
  496. package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
  497. package/client/dist/assets/settings-DHh7_IF5.js +0 -1
  498. package/client/dist/assets/settings-DQXAmdcv.js +0 -1
  499. package/client/dist/assets/settings-guzlhpup.js +0 -1
  500. package/client/dist/assets/settings-sZfTPMEH.js +0 -1
  501. package/client/dist/assets/setup-3jWaoVUO.js +0 -1
  502. package/client/dist/assets/setup-BjgRneb2.js +0 -1
  503. package/client/dist/assets/setup-BzFIjv-f.js +0 -1
  504. package/client/dist/assets/setup-C4VfQsxE.js +0 -1
  505. package/client/dist/assets/setup-DfUkpDbN.js +0 -1
  506. package/client/dist/assets/setup-ECe90Pmu.js +0 -1
  507. package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
  508. package/client/dist/assets/setup-jwfLMiM2.js +0 -1
  509. package/client/dist/assets/spending-EC9xqio-.js +0 -1
  510. package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
@@ -7,7 +7,13 @@ exports.updateBlueprintConversation = updateBlueprintConversation;
7
7
  exports.deleteBlueprintConversation = deleteBlueprintConversation;
8
8
  exports.addBlueprintMessage = addBlueprintMessage;
9
9
  exports.listBlueprintMessages = listBlueprintMessages;
10
+ exports.saveBlueprintSnapshot = saveBlueprintSnapshot;
11
+ exports.saveBlueprintSnapshotIssue = saveBlueprintSnapshotIssue;
12
+ exports.getBlueprintSnapshot = getBlueprintSnapshot;
13
+ exports.markBlueprintCommitted = markBlueprintCommitted;
14
+ exports.listResumableBlueprintConversations = listResumableBlueprintConversations;
10
15
  const crypto_1 = require("crypto");
16
+ const blueprint_draft_parser_1 = require("./blueprint-draft-parser");
11
17
  function createBlueprintConversation(db, input = {}) {
12
18
  const id = (0, crypto_1.randomUUID)();
13
19
  db.prepare('INSERT INTO blueprint_conversations (id, provider, model) VALUES (?, ?, ?)').run(id, input.provider ?? 'claude', input.model ?? null);
@@ -38,7 +44,7 @@ function deleteBlueprintConversation(db, id) {
38
44
  }
39
45
  function addBlueprintMessage(db, input) {
40
46
  const id = (0, crypto_1.randomUUID)();
41
- db.prepare('INSERT INTO blueprint_messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content);
47
+ db.prepare('INSERT INTO blueprint_messages (id, conversation_id, role, content, raw_content, intent) VALUES (?, ?, ?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content, input.rawContent ?? null, input.intent ?? null);
42
48
  db.prepare("UPDATE blueprint_conversations SET updated_at = datetime('now') WHERE id = ?").run(input.conversationId);
43
49
  return db.prepare('SELECT * FROM blueprint_messages WHERE id = ?').get(id);
44
50
  }
@@ -47,3 +53,88 @@ function listBlueprintMessages(db, conversationId) {
47
53
  .prepare('SELECT * FROM blueprint_messages WHERE conversation_id = ? ORDER BY created_at, rowid')
48
54
  .all(conversationId);
49
55
  }
56
+ function parseJsonColumn(value) {
57
+ if (!value)
58
+ return null;
59
+ try {
60
+ return JSON.parse(value);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** Persist the ACCEPTED snapshot pair and clear any pending rejection. */
67
+ function saveBlueprintSnapshot(db, id, snapshot) {
68
+ db.prepare(`UPDATE blueprint_conversations
69
+ SET blueprint_json = ?, raw_blueprint_json = ?, snapshot_updated_at = datetime('now'),
70
+ snapshot_issue_json = NULL, updated_at = datetime('now')
71
+ WHERE id = ?`).run(JSON.stringify(snapshot.blueprint), JSON.stringify(snapshot.rawBlueprint ?? snapshot.blueprint), id);
72
+ }
73
+ /** Record why the last block was rejected (kept until a snapshot is accepted). */
74
+ function saveBlueprintSnapshotIssue(db, id, issue) {
75
+ db.prepare("UPDATE blueprint_conversations SET snapshot_issue_json = ?, updated_at = datetime('now') WHERE id = ?").run(issue ? JSON.stringify(issue) : null, id);
76
+ }
77
+ /** Read the persisted snapshot (normalized through `coerceBlueprint` so a
78
+ * row written by an older build stays readable; corrupt JSON reads as null). */
79
+ function getBlueprintSnapshot(db, id) {
80
+ const row = getBlueprintConversation(db, id);
81
+ if (!row)
82
+ return { blueprint: null, rawBlueprint: null, snapshotUpdatedAt: null, issue: null };
83
+ const rawBlueprint = parseJsonColumn(row.raw_blueprint_json);
84
+ const blueprint = (0, blueprint_draft_parser_1.coerceBlueprint)(parseJsonColumn(row.blueprint_json) ?? rawBlueprint);
85
+ const issueValue = parseJsonColumn(row.snapshot_issue_json);
86
+ const issue = issueValue && typeof issueValue.reason === 'string' && typeof issueValue.detail === 'string'
87
+ ? { reason: issueValue.reason, detail: issueValue.detail, at: typeof issueValue.at === 'string' ? issueValue.at : '' }
88
+ : null;
89
+ return { blueprint, rawBlueprint: blueprint ? rawBlueprint ?? blueprint : null, snapshotUpdatedAt: row.snapshot_updated_at, issue };
90
+ }
91
+ function markBlueprintCommitted(db, id, projectId) {
92
+ db.prepare("UPDATE blueprint_conversations SET committed_project_id = ?, updated_at = datetime('now') WHERE id = ?").run(projectId, id);
93
+ }
94
+ function summarizeBlueprint(bp) {
95
+ if (!bp)
96
+ return { productName: null, platform: null, specCount: 0, specsComplete: false, dimensionsFilled: 0 };
97
+ const dims = [
98
+ bp.product.name !== '' && bp.product.pitch !== '',
99
+ bp.coreFlow !== '',
100
+ bp.platform !== '',
101
+ bp.stack.language !== '' || bp.stack.framework !== '',
102
+ bp.milestones.length > 0,
103
+ ];
104
+ return {
105
+ productName: bp.product.name || null,
106
+ platform: bp.platform || null,
107
+ specCount: bp.m1Specs.length,
108
+ specsComplete: bp.specsComplete,
109
+ dimensionsFilled: dims.filter(Boolean).length,
110
+ };
111
+ }
112
+ /**
113
+ * Unfinished Builder conversations worth resuming: not committed and with at
114
+ * least one assistant reply (an empty bootstrap row is noise). Newest first.
115
+ */
116
+ function listResumableBlueprintConversations(db, limit = 8) {
117
+ const rows = db
118
+ .prepare(`SELECT c.*, (SELECT COUNT(*) FROM blueprint_messages m WHERE m.conversation_id = c.id) AS message_count
119
+ FROM blueprint_conversations c
120
+ WHERE c.committed_project_id IS NULL
121
+ AND EXISTS (SELECT 1 FROM blueprint_messages m WHERE m.conversation_id = c.id AND m.role = 'assistant')
122
+ ORDER BY c.updated_at DESC, c.rowid DESC
123
+ LIMIT ?`)
124
+ .all(limit);
125
+ return rows.map((row) => {
126
+ const snapshot = getBlueprintSnapshot(db, row.id);
127
+ return {
128
+ id: row.id,
129
+ title: row.title,
130
+ provider: row.provider,
131
+ model: row.model,
132
+ created_at: row.created_at,
133
+ updated_at: row.updated_at,
134
+ messageCount: row.message_count,
135
+ ...summarizeBlueprint(snapshot.blueprint),
136
+ hasSnapshot: snapshot.blueprint !== null,
137
+ pendingIssue: snapshot.issue?.reason ?? null,
138
+ };
139
+ });
140
+ }
@@ -10,6 +10,7 @@ const ids_1 = require("./ids");
10
10
  const attachment_manager_1 = require("./attachment-manager");
11
11
  const browser_capture_types_1 = require("./browser-capture-types");
12
12
  const browser_playwright_1 = require("./browser-playwright");
13
+ const browser_viewport_1 = require("./browser-viewport");
13
14
  const WS_OPEN = 1;
14
15
  const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
15
16
  /** Playwright throws this family when the page/context/browser died underneath an
@@ -187,6 +188,14 @@ class BrowserCaptureManager {
187
188
  this._reserved--;
188
189
  throw err;
189
190
  }
191
+ if (this.disposed) {
192
+ this._reserved--;
193
+ try {
194
+ await page.close();
195
+ }
196
+ catch { /* ignore */ }
197
+ throw new browser_capture_types_1.BrowserLaunchError('manager disposed');
198
+ }
190
199
  // Re-check AFTER the awaits: another create that started before us may have
191
200
  // committed its session in the meantime. If the cap is now exceeded, tear the
192
201
  // extra page down and reject rather than silently exceeding the limit.
@@ -296,7 +305,7 @@ class BrowserCaptureManager {
296
305
  s.popupView = true;
297
306
  // Popups inherit the session viewport so coordinate mapping + screencast
298
307
  // dimensions stay identical to the root page.
299
- void popup.setViewport(s.viewport.width, s.viewport.height).catch(() => { });
308
+ void popup.setViewport(s.viewport.width, s.viewport.height, s.viewport.deviceScaleFactor).catch(() => { });
300
309
  popup.onClose?.(() => this.dropPopup(s, popup));
301
310
  popup.onNavigated?.(() => {
302
311
  // Keep the "Login window — <origin>" label fresh while the top popup
@@ -398,27 +407,55 @@ class BrowserCaptureManager {
398
407
  }
399
408
  catch { /* ignore */ }
400
409
  }
410
+ s.lastFrame = null; // never replay a previous popup/root frame under new chrome
401
411
  if (desired) {
402
- s.screencastPage = desired;
403
- await desired.startScreencast((frame) => {
404
- if (s.closed)
412
+ for (let attempt = 0; attempt < 2; attempt++) {
413
+ if (s.closed || !s.screencastDesired || this.activePage(s) !== desired)
414
+ return;
415
+ try {
416
+ await desired.startScreencast((frame) => {
417
+ if (s.closed || !s.screencastDesired || this.activePage(s) !== desired)
418
+ return;
419
+ s.lastFrame = frame.data;
420
+ for (const client of s.clients) {
421
+ if (client.readyState !== WS_OPEN)
422
+ continue;
423
+ // Keep slow clients current instead of queueing stale JPEGs.
424
+ if ((client.bufferedAmount ?? 0) > MAX_CLIENT_BUFFERED_BYTES)
425
+ continue;
426
+ try {
427
+ client.send(frame.data);
428
+ }
429
+ catch { /* drop */ }
430
+ }
431
+ });
432
+ s.screencastPage = desired;
405
433
  return;
406
- s.lastFrame = frame.data;
407
- for (const client of s.clients) {
408
- if (client.readyState !== WS_OPEN)
409
- continue;
410
- // Conflate for slow consumers: when the socket's send-buffer is
411
- // already backed up, skip this frame for that client instead of
412
- // queueing ever-staler frames behind the backlog (latency, not
413
- // fps, is what makes a screencast feel sluggish).
414
- if ((client.bufferedAmount ?? 0) > MAX_CLIENT_BUFFERED_BYTES)
415
- continue;
434
+ }
435
+ catch {
436
+ // A rejected CDP start previously left this session permanently
437
+ // marked as casting. Release partial resources before one retry.
416
438
  try {
417
- client.send(frame.data);
439
+ await desired.stopScreencast();
418
440
  }
419
- catch { /* drop */ }
441
+ catch { /* ignore */ }
420
442
  }
421
- });
443
+ }
444
+ // An OAuth window can self-close during the final failed CDP attach or
445
+ // its cleanup. The queued transition will resume its opener; a failure
446
+ // belonging to the old popup must not disconnect the whole session.
447
+ if (s.closed || !s.screencastDesired || this.activePage(s) !== desired)
448
+ return;
449
+ s.screencastDesired = false;
450
+ // Persistent failure must surface as a disconnected stream, not a ready
451
+ // socket that displays a blank canvas forever. A later attach can retry.
452
+ for (const client of s.clients) {
453
+ try {
454
+ client.close(1011, 'browser_stream_failed');
455
+ }
456
+ catch { /* ignore */ }
457
+ }
458
+ s.clients.clear();
422
459
  }
423
460
  }).catch(() => { });
424
461
  return s.screencastOp;
@@ -461,16 +498,21 @@ class BrowserCaptureManager {
461
498
  if (!s)
462
499
  return;
463
500
  if (event.type === 'resize') {
501
+ const normalized = (0, browser_viewport_1.normalizeBrowserViewport)(event.width, event.height, event.deviceScaleFactor ?? s.viewport.deviceScaleFactor);
464
502
  s.viewport = {
465
- width: Math.max(1, Math.round(event.width)),
466
- height: Math.max(1, Math.round(event.height)),
503
+ width: normalized.width,
504
+ height: normalized.height,
505
+ ...(event.deviceScaleFactor !== undefined || s.viewport.deviceScaleFactor !== undefined
506
+ ? { deviceScaleFactor: (0, browser_viewport_1.normalizeBrowserDeviceScaleFactor)(event.deviceScaleFactor ?? s.viewport.deviceScaleFactor ?? 1) }
507
+ : {}),
467
508
  };
468
509
  // Keep every page of the session (root + popups) at the same viewport so
469
510
  // switching between them never re-maps coordinates or resizes the canvas.
470
- await s.page.dispatchInput(event);
511
+ const resize = { type: 'resize', ...s.viewport };
512
+ await s.page.dispatchInput(resize);
471
513
  for (const popup of s.popups) {
472
514
  try {
473
- await popup.dispatchInput(event);
515
+ await popup.dispatchInput(resize);
474
516
  }
475
517
  catch { /* ignore */ }
476
518
  }
@@ -486,22 +528,36 @@ class BrowserCaptureManager {
486
528
  const s = this.getSessionForActivity(sessionId);
487
529
  if (!s)
488
530
  return null;
531
+ // Snapshot before awaiting: an OAuth window can close, or the user can
532
+ // switch views, while a navigation is in flight. Its result still belongs
533
+ // to the page the user acted on, never to the newly visible opener.
534
+ const page = this.activePage(s);
535
+ const isPopup = page !== s.page;
489
536
  this.beginOperation(s);
490
537
  try {
491
538
  let result;
492
539
  if (action === 'goto')
493
- result = await s.page.goto(url ?? 'about:blank');
540
+ result = await page.goto(url ?? 'about:blank');
494
541
  else if (action === 'back')
495
- result = await s.page.goBack();
542
+ result = await page.goBack();
496
543
  else if (action === 'forward')
497
- result = await s.page.goForward();
544
+ result = await page.goForward();
498
545
  else
499
- result = await s.page.reload();
500
- s.url = result.url;
501
- s.title = result.title;
502
- if (result.url && result.url !== 'about:blank')
503
- this.setLastUrl(result.url);
504
- this.broadcastControl(s, { type: 'nav', url: result.url, title: result.title });
546
+ result = await page.reload();
547
+ if (isPopup) {
548
+ if (!s.closed && !this.disposed && this.topPopup(s) === page)
549
+ this.broadcastPopupState(s);
550
+ // Popup redirect/callback URLs must never replace the project page's
551
+ // remembered URL, title or nav events (nor the client's root state).
552
+ return { ...result, target: 'popup' };
553
+ }
554
+ if (!s.closed && !this.disposed) {
555
+ s.url = result.url;
556
+ s.title = result.title;
557
+ if (result.url && result.url !== 'about:blank')
558
+ this.setLastUrl(result.url);
559
+ this.broadcastControl(s, { type: 'nav', url: result.url, title: result.title });
560
+ }
505
561
  return result;
506
562
  }
507
563
  finally {
@@ -8,6 +8,7 @@
8
8
  // Screencast uses CDP (`Page.startScreencast`) since Playwright exposes no
9
9
  // screencast API; input + screenshot + DOM use Playwright's high-level page API.
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.PlaywrightPageHandle = void 0;
11
12
  exports.isNavigableUrl = isNavigableUrl;
12
13
  exports.screencastParams = screencastParams;
13
14
  exports.normalizeUrl = normalizeUrl;
@@ -15,6 +16,7 @@ exports.chromiumLaunchArgs = chromiumLaunchArgs;
15
16
  exports.createPlaywrightLauncher = createPlaywrightLauncher;
16
17
  const chromium_resolver_1 = require("./chromium-resolver");
17
18
  const browser_network_1 = require("./browser-network");
19
+ const browser_viewport_1 = require("./browser-viewport");
18
20
  /**
19
21
  * Centralized SSRF / scheme guard for EVERY navigation path (WS `navigate`, REST
20
22
  * `navigate`, REST create `initialUrl`). Returns true only for an `http`/`https`
@@ -117,7 +119,7 @@ function screencastParams(env = process.env) {
117
119
  if (Number.isFinite(n))
118
120
  quality = Math.min(100, Math.max(1, Math.round(n)));
119
121
  }
120
- return { format: 'jpeg', quality, everyNthFrame: 1, maxWidth: 3840, maxHeight: 2400 };
122
+ return { format: 'jpeg', quality, everyNthFrame: 1, maxWidth: browser_viewport_1.MAX_BROWSER_RASTER.width, maxHeight: browser_viewport_1.MAX_BROWSER_RASTER.height };
121
123
  }
122
124
  /**
123
125
  * Normalize a user-supplied address into a safe, navigable URL. A bare host
@@ -144,6 +146,14 @@ class PlaywrightPageHandle {
144
146
  // dependency on playwright's exported types leaking through the abstraction.
145
147
  cdp = null;
146
148
  screencastHandler = null;
149
+ screencastStart = null;
150
+ // Captures can render at display density independently of the interactive
151
+ // viewport. CDP emulation scale/DPR changes break screencast or wheel semantics.
152
+ captureCdp = null;
153
+ viewportOp = null;
154
+ pendingViewport = null;
155
+ requestedDeviceScaleFactor = 1;
156
+ captureDeviceScaleFactor = 1;
147
157
  // A dedicated, long-lived CDP session for the Network domain — kept SEPARATE
148
158
  // from `cdp` (which is attached/detached around screencast) so request capture
149
159
  // survives screencast toggles.
@@ -207,11 +217,33 @@ class PlaywrightPageHandle {
207
217
  return '';
208
218
  }
209
219
  }
210
- async setViewport(width, height) {
220
+ async setViewport(width, height, deviceScaleFactor) {
221
+ if (deviceScaleFactor !== undefined)
222
+ this.requestedDeviceScaleFactor = deviceScaleFactor;
223
+ this.pendingViewport = (0, browser_viewport_1.normalizeBrowserViewport)(width, height, this.requestedDeviceScaleFactor);
224
+ if (this.viewportOp)
225
+ return this.viewportOp;
226
+ const operation = (async () => {
227
+ while (this.pendingViewport) {
228
+ const next = this.pendingViewport;
229
+ this.pendingViewport = null;
230
+ try {
231
+ await this.page.setViewportSize({ width: next.width, height: next.height });
232
+ this.captureDeviceScaleFactor = next.deviceScaleFactor;
233
+ }
234
+ catch {
235
+ this.captureDeviceScaleFactor = 1; // page closed/navigating; input stays best-effort
236
+ }
237
+ }
238
+ })();
239
+ this.viewportOp = operation;
211
240
  try {
212
- await this.page.setViewportSize({ width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) });
241
+ await operation;
242
+ }
243
+ finally {
244
+ if (this.viewportOp === operation)
245
+ this.viewportOp = null;
213
246
  }
214
- catch { /* ignore */ }
215
247
  }
216
248
  async dispatchInput(event) {
217
249
  try {
@@ -257,7 +289,7 @@ class PlaywrightPageHandle {
257
289
  }
258
290
  }
259
291
  else if (event.type === 'resize') {
260
- await this.setViewport(event.width, event.height);
292
+ await this.setViewport(event.width, event.height, event.deviceScaleFactor);
261
293
  }
262
294
  }
263
295
  catch {
@@ -265,44 +297,81 @@ class PlaywrightPageHandle {
265
297
  }
266
298
  }
267
299
  async startScreencast(onFrame) {
300
+ if (this.screencastStart)
301
+ return this.screencastStart;
268
302
  if (this.screencastHandler)
269
303
  return;
270
- this.screencastHandler = onFrame;
271
- const ctx = this.page.context();
272
- this.cdp = await ctx.newCDPSession(this.page);
273
- this.cdp.on('Page.screencastFrame', (evt) => {
274
- // Ack FIRST, fire-and-forget: Chromium throttles the screencast on unacked
275
- // frames, so acking before the fan-out lets it capture+compress the next
276
- // frame while this one is being delivered (pipeline overlap → higher fps,
277
- // lower frame age). The frames are independent JPEGs, so there is no
278
- // ordering hazard in acking early.
304
+ const startup = (async () => {
305
+ let cdp = null;
279
306
  try {
280
- this.cdp?.send('Page.screencastFrameAck', { sessionId: evt.sessionId })?.catch?.(() => { });
281
- }
282
- catch { /* ignore */ }
283
- try {
284
- this.screencastHandler?.({
285
- data: Buffer.from(evt.data, 'base64'),
286
- width: evt.metadata?.deviceWidth ?? 0,
287
- height: evt.metadata?.deviceHeight ?? 0,
307
+ cdp = await this.page.context().newCDPSession(this.page);
308
+ this.cdp = cdp;
309
+ this.screencastHandler = onFrame;
310
+ cdp.on('Page.screencastFrame', (evt) => {
311
+ // Ack on the session that emitted the frame. A late callback from an
312
+ // old cast must never ack or render into its replacement.
313
+ try {
314
+ cdp.send('Page.screencastFrameAck', { sessionId: evt.sessionId })?.catch?.(() => { });
315
+ }
316
+ catch { /* ignore */ }
317
+ if (this.cdp !== cdp)
318
+ return;
319
+ try {
320
+ this.screencastHandler?.({
321
+ data: Buffer.from(evt.data, 'base64'),
322
+ width: evt.metadata?.deviceWidth ?? 0,
323
+ height: evt.metadata?.deviceHeight ?? 0,
324
+ });
325
+ }
326
+ catch { /* never let a handler error kill the CDP session */ }
288
327
  });
328
+ await cdp.send('Page.startScreencast', screencastParams());
289
329
  }
290
- catch { /* never let a handler error kill the CDP session */ }
291
- });
292
- await this.cdp.send('Page.startScreencast', screencastParams());
330
+ catch (err) {
331
+ this.screencastHandler = null;
332
+ if (this.cdp === cdp)
333
+ this.cdp = null;
334
+ if (cdp) {
335
+ try {
336
+ await cdp.send('Page.stopScreencast');
337
+ }
338
+ catch { /* ignore */ }
339
+ try {
340
+ await cdp.detach();
341
+ }
342
+ catch { /* ignore */ }
343
+ }
344
+ throw err;
345
+ }
346
+ })();
347
+ this.screencastStart = startup;
348
+ try {
349
+ await startup;
350
+ }
351
+ finally {
352
+ if (this.screencastStart === startup)
353
+ this.screencastStart = null;
354
+ }
293
355
  }
294
356
  async stopScreencast() {
357
+ // Closing while newCDPSession/start is awaiting must also close the eventual
358
+ // CDP handle. Otherwise unmount can leave an invisible capture running.
359
+ try {
360
+ await this.screencastStart;
361
+ }
362
+ catch { /* startup already cleaned up */ }
295
363
  this.screencastHandler = null;
296
- if (this.cdp) {
364
+ const cdp = this.cdp;
365
+ this.cdp = null;
366
+ if (cdp) {
297
367
  try {
298
- await this.cdp.send('Page.stopScreencast');
368
+ await cdp.send('Page.stopScreencast');
299
369
  }
300
370
  catch { /* ignore */ }
301
371
  try {
302
- await this.cdp.detach();
372
+ await cdp.detach();
303
373
  }
304
374
  catch { /* ignore */ }
305
- this.cdp = null;
306
375
  }
307
376
  }
308
377
  async screenshotClip(rect) {
@@ -312,6 +381,29 @@ class PlaywrightPageHandle {
312
381
  width: Math.max(1, Math.round(rect.width)),
313
382
  height: Math.max(1, Math.round(rect.height)),
314
383
  };
384
+ await this.viewportOp;
385
+ if (this.captureDeviceScaleFactor > 1) {
386
+ // Screenshot clip.scale re-rasterizes vector/text detail at display
387
+ // density without changing page DPR, input units, scroll, or live JPEGs.
388
+ // CSS layout metrics supply the document offset of the viewport crop.
389
+ this.captureCdp ??= await this.page.context().newCDPSession(this.page);
390
+ try {
391
+ const { cssVisualViewport } = await this.captureCdp.send('Page.getLayoutMetrics');
392
+ const result = await this.captureCdp.send('Page.captureScreenshot', {
393
+ format: 'png', captureBeyondViewport: false,
394
+ clip: { ...clip, x: clip.x + (cssVisualViewport?.pageX ?? 0), y: clip.y + (cssVisualViewport?.pageY ?? 0), scale: this.captureDeviceScaleFactor },
395
+ });
396
+ return Buffer.from(result.data, 'base64');
397
+ }
398
+ catch (error) {
399
+ try {
400
+ await this.captureCdp.detach();
401
+ }
402
+ catch { /* ignore */ }
403
+ this.captureCdp = null;
404
+ throw error;
405
+ }
406
+ }
315
407
  return this.page.screenshot({ clip, type: 'png' });
316
408
  }
317
409
  async extractDom(rect, htmlByteCap) {
@@ -496,17 +588,15 @@ class PlaywrightPageHandle {
496
588
  // context — cookies and the window.opener/postMessage relationship are
497
589
  // intact, which is exactly what an OAuth redirect/popup flow needs.
498
590
  //
499
- // HARDENING (Okta-class IdPs): a popup opened from a cross-origin IFRAME
500
- // (out-of-process sign-in widgets) is attributed to the iframe's target,
501
- // and the page-level 'popup' event can miss it — the login window then
502
- // "never appears". So we ALSO watch the shared context for new pages whose
503
- // opener chain resolves to this page and adopt those. `seen` dedupes the
504
- // two doors; pages from other sessions have a different opener and are
505
- // ignored.
591
+ // Also watch the context for pages whose direct opener is this page. A
592
+ // chained login can open its next window before its own handle is adopted,
593
+ // so replay existing pages after subscribing. Deduplication covers both
594
+ // events and the replay; another session's pages never share this opener.
506
595
  try {
507
596
  const seen = new WeakSet();
597
+ let active = !this.page.isClosed?.();
508
598
  const adopt = (p) => {
509
- if (seen.has(p))
599
+ if (!active || p === this.page || p.isClosed?.() || seen.has(p))
510
600
  return;
511
601
  seen.add(p);
512
602
  try {
@@ -520,9 +610,11 @@ class PlaywrightPageHandle {
520
610
  const onContextPage = (p) => {
521
611
  void (async () => {
522
612
  try {
523
- if (seen.has(p) || p === this.page)
613
+ if (!active || seen.has(p) || p === this.page || p.isClosed?.())
524
614
  return;
525
615
  const opener = await p.opener?.();
616
+ // `opener()` is async; the opener or popup may have closed while
617
+ // it was pending. adopt checks that lifetime again.
526
618
  if (opener === this.page)
527
619
  adopt(p);
528
620
  }
@@ -530,13 +622,20 @@ class PlaywrightPageHandle {
530
622
  })();
531
623
  };
532
624
  ctx.on('page', onContextPage);
533
- // The shared context outlives this page — detach on page close so a
534
- // long-lived app never accumulates dead listeners.
535
- this.page.on('close', () => {
625
+ for (const p of ctx.pages?.() ?? [])
626
+ onContextPage(p);
627
+ // The shared context outlives this page. Invalidate pending lookups as
628
+ // well as detaching listeners, or a late result can revive a dead tab.
629
+ this.onClose(() => {
630
+ active = false;
536
631
  try {
537
632
  ctx.off?.('page', onContextPage);
538
633
  }
539
634
  catch { /* ignore */ }
635
+ try {
636
+ this.page.off?.('popup', adopt);
637
+ }
638
+ catch { /* ignore */ }
540
639
  });
541
640
  }
542
641
  }
@@ -544,12 +643,21 @@ class PlaywrightPageHandle {
544
643
  }
545
644
  onClose(cb) {
546
645
  try {
547
- this.page.on('close', () => {
646
+ let delivered = false;
647
+ const notify = () => {
648
+ if (delivered)
649
+ return;
650
+ delivered = true;
548
651
  try {
549
652
  cb();
550
653
  }
551
654
  catch { /* ignore */ }
552
- });
655
+ };
656
+ this.page.on('close', notify);
657
+ // An immediate OAuth callback can self-close before adoption finishes.
658
+ // Deliver that state too; waiting only for a future event strands it.
659
+ if (this.page.isClosed?.())
660
+ queueMicrotask(notify);
553
661
  }
554
662
  catch { /* ignore */ }
555
663
  }
@@ -587,6 +695,14 @@ class PlaywrightPageHandle {
587
695
  }
588
696
  async close() {
589
697
  await this.stopScreencast();
698
+ await this.viewportOp;
699
+ if (this.captureCdp) {
700
+ try {
701
+ await this.captureCdp.detach();
702
+ }
703
+ catch { /* ignore */ }
704
+ this.captureCdp = null;
705
+ }
590
706
  if (this.netCdp) {
591
707
  try {
592
708
  await this.netCdp.send('Network.disable');
@@ -604,6 +720,7 @@ class PlaywrightPageHandle {
604
720
  catch { /* ignore */ }
605
721
  }
606
722
  }
723
+ exports.PlaywrightPageHandle = PlaywrightPageHandle;
607
724
  // Serialised into the page via page.evaluate. Self-contained: no closures over
608
725
  // Node scope. Returns the html + nodes; the Node side stamps url/title/time.
609
726
  function domExtractScript(arg) {
@@ -1100,8 +1217,12 @@ class PlaywrightContextHandle {
1100
1217
  * sandbox frequently fails to initialise inside a packaged/headless app and would
1101
1218
  * otherwise refuse to launch. We never disable the sandbox where it functions.
1102
1219
  */
1103
- function chromiumLaunchArgs() {
1104
- const args = ['--disable-dev-shm-usage', '--disable-gpu'];
1220
+ function chromiumLaunchArgs(env = process.env) {
1221
+ const args = ['--disable-dev-shm-usage'];
1222
+ // Native acceleration is the browser default. Retain an explicit escape hatch
1223
+ // for machines/drivers that need software rendering, without forcing it on Macs.
1224
+ if (env.SPECRAILS_BROWSER_DISABLE_GPU === 'true')
1225
+ args.push('--disable-gpu');
1105
1226
  if (process.platform === 'linux')
1106
1227
  args.unshift('--no-sandbox');
1107
1228
  return args;
@@ -1123,6 +1244,8 @@ function createPlaywrightLauncher() {
1123
1244
  const context = await chromium.launchPersistentContext(opts.userDataDir, {
1124
1245
  headless: true,
1125
1246
  executablePath,
1247
+ // Playwright defaults this option to false even without --no-sandbox.
1248
+ chromiumSandbox: process.platform !== 'linux',
1126
1249
  viewport: opts.viewport,
1127
1250
  args: chromiumLaunchArgs(),
1128
1251
  });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_BROWSER_RASTER = void 0;
4
+ exports.normalizeBrowserDeviceScaleFactor = normalizeBrowserDeviceScaleFactor;
5
+ exports.normalizeBrowserViewport = normalizeBrowserViewport;
6
+ /** CSS layout stays independent from the bounded physical capture raster. */
7
+ exports.MAX_BROWSER_RASTER = { width: 3840, height: 2400 };
8
+ function normalizeBrowserDeviceScaleFactor(value) {
9
+ return Number.isFinite(value) ? Math.min(2, Math.max(1, value)) : 1;
10
+ }
11
+ function normalizeBrowserViewport(width, height, deviceScaleFactor = 1) {
12
+ const dimension = (value, fallback, max) => Number.isFinite(value)
13
+ ? Math.min(max, Math.max(1, Math.round(value)))
14
+ : fallback;
15
+ const cssWidth = dimension(width, 1280, exports.MAX_BROWSER_RASTER.width);
16
+ const cssHeight = dimension(height, 800, exports.MAX_BROWSER_RASTER.height);
17
+ const requestedScale = normalizeBrowserDeviceScaleFactor(deviceScaleFactor);
18
+ const scale = Math.min(requestedScale, exports.MAX_BROWSER_RASTER.width / cssWidth, exports.MAX_BROWSER_RASTER.height / cssHeight);
19
+ return {
20
+ width: cssWidth,
21
+ height: cssHeight,
22
+ deviceScaleFactor: scale,
23
+ rasterWidth: Math.round(cssWidth * scale),
24
+ rasterHeight: Math.round(cssHeight * scale),
25
+ };
26
+ }