@vantran-se/openchamber-web 1.24.4 → 2.0.1

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -0,0 +1,35 @@
1
+ /**
2
+ * One HTTP `Range` header, reduced to the single byte span this server serves.
3
+ *
4
+ * Media elements ask for `bytes=0-` first and then for `bytes=<offset>-` on
5
+ * every seek; without a 206 answer Chromium and WebKit refuse to seek at all,
6
+ * and Safari refuses to play. Multi-range requests are answered with the whole
7
+ * file: no media consumer sends them, and a multipart body is not worth
8
+ * carrying for a viewer.
9
+ */
10
+
11
+ /**
12
+ * @param {unknown} header
13
+ * @param {number} size
14
+ * @returns {{ kind: 'range', start: number, end: number } | { kind: 'whole' } | { kind: 'unsatisfiable' }}
15
+ */
16
+ export const resolveByteRange = (header, size) => {
17
+ if (!header || size <= 0) return { kind: 'whole' };
18
+ const match = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim());
19
+ if (!match) return { kind: 'whole' };
20
+ const [, rawStart, rawEnd] = match;
21
+ if (rawStart === '' && rawEnd === '') return { kind: 'whole' };
22
+
23
+ // `bytes=-500` means the final 500 bytes.
24
+ if (rawStart === '') {
25
+ const suffix = Number(rawEnd);
26
+ if (suffix === 0) return { kind: 'unsatisfiable' };
27
+ return { kind: 'range', start: Math.max(0, size - suffix), end: size - 1 };
28
+ }
29
+
30
+ const start = Number(rawStart);
31
+ if (start >= size) return { kind: 'unsatisfiable' };
32
+ const end = rawEnd === '' ? size - 1 : Math.min(Number(rawEnd), size - 1);
33
+ if (end < start) return { kind: 'unsatisfiable' };
34
+ return { kind: 'range', start, end };
35
+ };
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveByteRange } from './byte-range.js';
4
+
5
+ describe('resolveByteRange', () => {
6
+ it('serves the whole file without a header, for an empty file, and for anything but one bytes span', () => {
7
+ expect(resolveByteRange(undefined, 100)).toEqual({ kind: 'whole' });
8
+ expect(resolveByteRange('bytes=0-', 0)).toEqual({ kind: 'whole' });
9
+ expect(resolveByteRange('items=0-1', 100)).toEqual({ kind: 'whole' });
10
+ expect(resolveByteRange('bytes=0-1,5-9', 100)).toEqual({ kind: 'whole' });
11
+ expect(resolveByteRange('bytes=-', 100)).toEqual({ kind: 'whole' });
12
+ });
13
+
14
+ it('answers the open-ended span a media element sends on seek', () => {
15
+ expect(resolveByteRange('bytes=0-', 100)).toEqual({ kind: 'range', start: 0, end: 99 });
16
+ expect(resolveByteRange('bytes=40-', 100)).toEqual({ kind: 'range', start: 40, end: 99 });
17
+ });
18
+
19
+ it('clamps a closed span to the file and honours a suffix span', () => {
20
+ expect(resolveByteRange('bytes=10-19', 100)).toEqual({ kind: 'range', start: 10, end: 19 });
21
+ expect(resolveByteRange('bytes=90-500', 100)).toEqual({ kind: 'range', start: 90, end: 99 });
22
+ expect(resolveByteRange('bytes=-30', 100)).toEqual({ kind: 'range', start: 70, end: 99 });
23
+ expect(resolveByteRange('bytes=-300', 100)).toEqual({ kind: 'range', start: 0, end: 99 });
24
+ });
25
+
26
+ it('rejects a span that starts past the end or runs backwards', () => {
27
+ expect(resolveByteRange('bytes=100-', 100)).toEqual({ kind: 'unsatisfiable' });
28
+ expect(resolveByteRange('bytes=50-40', 100)).toEqual({ kind: 'unsatisfiable' });
29
+ expect(resolveByteRange('bytes=-0', 100)).toEqual({ kind: 'unsatisfiable' });
30
+ });
31
+ });
@@ -1,4 +1,5 @@
1
1
  import { createRealpathCache } from '../path-realpath-cache.js';
2
+ import { resolveByteRange } from './byte-range.js';
2
3
  import nodeFsPromises from 'node:fs/promises';
3
4
  import nodePath from 'node:path';
4
5
 
@@ -67,25 +68,6 @@ export const mintOutsideFileGrant = async (targetPath, {
67
68
  };
68
69
  };
69
70
 
70
- const resolveOutsideFileGrant = async ({ token, targetPath, scope, fsPromises }) => {
71
- pruneOutsideFileGrants();
72
- if (typeof token !== 'string' || !token.trim()) {
73
- return { ok: false, error: 'Outside workspace file access requires a grant' };
74
- }
75
- const grant = outsideFileGrants.get(token.trim());
76
- if (!grant) {
77
- return { ok: false, error: 'Outside workspace file grant is invalid or expired' };
78
- }
79
- if (!grant.scopes.has(scope)) {
80
- return { ok: false, error: 'Outside workspace file grant does not allow this operation' };
81
- }
82
- const canonicalPath = await fsPromises.realpath(targetPath);
83
- if (canonicalPath !== grant.canonicalPath) {
84
- return { ok: false, error: 'Outside workspace file grant does not match requested path' };
85
- }
86
- return { ok: true, base: grant.base, resolved: canonicalPath, granted: true };
87
- };
88
-
89
71
  const createCommandTimeoutMs = () => {
90
72
  const raw = Number(process.env.OPENCHAMBER_FS_EXEC_TIMEOUT_MS);
91
73
  if (Number.isFinite(raw) && raw > 0) return raw;
@@ -125,14 +107,30 @@ const FILE_MIME_MAP = Object.freeze({
125
107
  '.xml': 'application/xml',
126
108
  '.txt': 'text/plain',
127
109
  '.md': 'text/markdown',
110
+ '.mmd': 'text/plain',
128
111
  '.pdf': 'application/pdf',
129
112
  '.csv': 'text/csv',
113
+ '.tsv': 'text/tab-separated-values',
130
114
  '.woff2': 'font/woff2',
131
115
  '.woff': 'font/woff',
132
116
  '.ttf': 'font/ttf',
117
+ '.otf': 'font/otf',
133
118
  '.eot': 'application/vnd.ms-fontobject',
134
119
  '.mp3': 'audio/mpeg',
120
+ '.m4a': 'audio/mp4',
121
+ '.aac': 'audio/aac',
122
+ '.wav': 'audio/wav',
123
+ '.flac': 'audio/flac',
124
+ '.ogg': 'audio/ogg',
125
+ '.oga': 'audio/ogg',
126
+ '.opus': 'audio/ogg',
127
+ '.weba': 'audio/webm',
135
128
  '.mp4': 'video/mp4',
129
+ '.m4v': 'video/mp4',
130
+ '.webm': 'video/webm',
131
+ '.mov': 'video/quicktime',
132
+ '.ogv': 'video/ogg',
133
+ '.mkv': 'video/x-matroska',
136
134
  '.png': 'image/png',
137
135
  '.jpg': 'image/jpeg',
138
136
  '.jpeg': 'image/jpeg',
@@ -450,19 +448,14 @@ const escapeCloneSshKeyPath = (sshKeyPath) => {
450
448
  return `'${normalized.replace(/'/g, "'\\''")}'`;
451
449
  };
452
450
 
453
- const resolveReadPathFromContext = async ({ req, targetPath, scope, resolveProjectDirectory, path, os, fsPromises, normalizeDirectoryPath, managedRoots }) => {
451
+ const resolveReadPathFromContext = async ({ req, targetPath, resolveProjectDirectory, path, os, fsPromises, normalizeDirectoryPath, managedRoots }) => {
454
452
  if (req.query?.allowOutsideWorkspace === 'true') {
455
453
  const normalized = normalizeDirectoryPath(targetPath);
456
454
  if (!normalized || typeof normalized !== 'string') {
457
455
  return { ok: false, error: 'Path is required' };
458
456
  }
459
457
  const resolved = path.resolve(normalized);
460
- return resolveOutsideFileGrant({
461
- token: req.query?.outsideFileGrant,
462
- targetPath: resolved,
463
- scope,
464
- fsPromises,
465
- });
458
+ return { ok: true, base: path.dirname(resolved), resolved };
466
459
  }
467
460
 
468
461
  return resolveWorkspacePathFromContext({
@@ -1088,19 +1081,7 @@ export const registerFsRoutes = (app, dependencies) => {
1088
1081
  }
1089
1082
 
1090
1083
  const ext = path.extname(canonicalPath).toLowerCase();
1091
- const mimeMap = {
1092
- '.png': 'image/png',
1093
- '.jpg': 'image/jpeg',
1094
- '.jpeg': 'image/jpeg',
1095
- '.gif': 'image/gif',
1096
- '.svg': 'image/svg+xml',
1097
- '.webp': 'image/webp',
1098
- '.ico': 'image/x-icon',
1099
- '.bmp': 'image/bmp',
1100
- '.avif': 'image/avif',
1101
- '.pdf': 'application/pdf',
1102
- };
1103
- const mimeType = mimeMap[ext] || 'application/octet-stream';
1084
+ const mimeType = FILE_MIME_MAP[ext] || 'application/octet-stream';
1104
1085
 
1105
1086
  const download = req.query.download === 'true';
1106
1087
  if (download) {
@@ -1114,11 +1095,35 @@ export const registerFsRoutes = (app, dependencies) => {
1114
1095
  res.setHeader('Content-Disposition', `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`);
1115
1096
  }
1116
1097
 
1117
- const content = await fsPromises.readFile(canonicalPath);
1118
1098
  res.setHeader('Cache-Control', 'no-store');
1119
- if (resolved.granted) {
1120
- res.setHeader('Referrer-Policy', 'no-referrer');
1099
+ res.setHeader('Referrer-Policy', 'no-referrer');
1100
+ res.setHeader('Accept-Ranges', 'bytes');
1101
+
1102
+ // A byte span is streamed from disk rather than read whole: the audio
1103
+ // and video players ask for one on every seek, and a recording can be
1104
+ // hundreds of megabytes.
1105
+ const range = resolveByteRange(req.headers?.range, stats.size);
1106
+ if (range.kind === 'unsatisfiable') {
1107
+ res.setHeader('Content-Range', `bytes */${stats.size}`);
1108
+ return res.status(416).end();
1109
+ }
1110
+ if (range.kind === 'range') {
1111
+ const handle = await fsPromises.open(canonicalPath, 'r');
1112
+ res.status(206);
1113
+ res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`);
1114
+ res.setHeader('Content-Length', String(range.end - range.start + 1));
1115
+ res.type(mimeType);
1116
+ // The handle closes with the stream, on success and on failure alike.
1117
+ const stream = handle.createReadStream({ start: range.start, end: range.end });
1118
+ stream.on('error', (error) => {
1119
+ console.error('Failed to stream raw file range:', error);
1120
+ res.destroy(error);
1121
+ });
1122
+ stream.pipe(res);
1123
+ return undefined;
1121
1124
  }
1125
+
1126
+ const content = await fsPromises.readFile(canonicalPath);
1122
1127
  return res.type(mimeType).send(content);
1123
1128
  } catch (error) {
1124
1129
  const err = error;
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { Readable, Writable } from 'node:stream';
2
3
  import path from 'path';
3
4
  import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
4
5
  import * as nativeFs from 'node:fs/promises';
@@ -572,8 +573,7 @@ describe('fs read', () => {
572
573
  expect(fsPromises.readFile).toHaveBeenCalledWith('/shared/target.txt', 'utf8');
573
574
  });
574
575
 
575
- it('rejects outside workspace reads without a grant', async () => {
576
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
576
+ it('reads outside workspace files without a grant', async () => {
577
577
  const fsPromises = {
578
578
  stat: vi.fn(async () => ({ isFile: () => true, size: 3 })),
579
579
  readFile: vi.fn(async () => 'secret'),
@@ -582,10 +582,9 @@ describe('fs read', () => {
582
582
 
583
583
  const res = await callRead(handler, { path: '/etc/passwd', allowOutsideWorkspace: 'true' });
584
584
 
585
- expect(res.statusCode).toBe(400);
586
- expect(res.body).toEqual({ error: 'Outside workspace file access requires a grant' });
587
- expect(fsPromises.readFile).not.toHaveBeenCalled();
588
- warn.mockRestore();
585
+ expect(res.statusCode).toBe(200);
586
+ expect(res.body).toBe('secret');
587
+ expect(fsPromises.readFile).toHaveBeenCalledWith('/etc/passwd', 'utf8');
589
588
  });
590
589
 
591
590
  it('allows outside workspace reads with an exact-path grant', async () => {
@@ -611,7 +610,7 @@ describe('fs read', () => {
611
610
  expect(res.body).toBe('secret');
612
611
  });
613
612
 
614
- it('rejects outside workspace grants for a different canonical path', async () => {
613
+ it('ignores legacy grants when reading another outside file', async () => {
615
614
  const fsPromises = {
616
615
  realpath: vi.fn(async (targetPath) => targetPath),
617
616
  stat: vi.fn(async () => ({ isFile: () => true, size: 6 })),
@@ -630,35 +629,58 @@ describe('fs read', () => {
630
629
  outsideFileGrant: grant.outsideFileGrant,
631
630
  });
632
631
 
633
- expect(res.statusCode).toBe(400);
634
- expect(res.body).toEqual({ error: 'Outside workspace file grant does not match requested path' });
635
- expect(fsPromises.readFile).not.toHaveBeenCalled();
632
+ expect(res.statusCode).toBe(200);
633
+ expect(res.body).toBe('secret');
634
+ expect(fsPromises.readFile).toHaveBeenCalledWith('/outside/b.txt', 'utf8');
636
635
  });
637
636
 
638
- it('sets no-referrer on raw responses served through outside file grants', async () => {
637
+ it('reads outside raw files without a grant and sets no-referrer', async () => {
639
638
  const fsPromises = {
640
639
  realpath: vi.fn(async (targetPath) => targetPath),
641
640
  stat: vi.fn(async () => ({ isFile: () => true, size: 6 })),
642
641
  readFile: vi.fn(async () => Buffer.from('secret')),
643
642
  };
644
- const grant = await mintOutsideFileGrant('/outside/image.png', {
645
- scopes: ['raw'],
646
- fsPromises,
647
- path: path.posix,
648
- crypto: { randomUUID: () => 'grant-raw' },
649
- });
650
643
  const handler = registerRaw(fsPromises);
651
644
 
652
645
  const res = await callRaw(handler, {
653
646
  path: '/outside/image.png',
654
647
  allowOutsideWorkspace: 'true',
655
- outsideFileGrant: grant.outsideFileGrant,
656
648
  });
657
649
 
658
650
  expect(res.statusCode).toBe(200);
659
651
  expect(res.getHeader('referrer-policy')).toBe('no-referrer');
660
652
  });
661
653
 
654
+ it('stats outside files without a grant', async () => {
655
+ const { app, getRoute } = createRouteRegistry();
656
+ registerFsRoutes(app, {
657
+ path: path.posix,
658
+ os: { homedir: () => '/home/user' },
659
+ fsPromises: {
660
+ realpath: async (targetPath) => targetPath,
661
+ stat: async () => ({ isFile: () => true, size: 100, mtimeMs: 123 }),
662
+ },
663
+ normalizeDirectoryPath: (p) => p,
664
+ resolveProjectDirectory: async () => ({ directory: '/repo' }),
665
+ openchamberUserConfigRoot: '/home/user/.config',
666
+ });
667
+ const res = await callRead(getRoute('GET', '/api/fs/stat'), { path: '/tmp/plan.txt', allowOutsideWorkspace: 'true' });
668
+ expect(res.statusCode).toBe(200);
669
+ expect(res.body).toEqual({ path: '/tmp/plan.txt', isFile: true, size: 100, mtimeMs: 123 });
670
+ });
671
+
672
+ it.each([
673
+ ['ENOENT', 404, { error: 'File not found' }],
674
+ ['EACCES', 403, { error: 'Access to file denied', reason: 'os-permission' }],
675
+ ])('reports %s for outside files instead of requiring a grant', async (code, status, body) => {
676
+ const handler = registerRead({
677
+ realpath: async () => { throw Object.assign(new Error(code), { code }); },
678
+ });
679
+ const res = await callRead(handler, { path: '/tmp/plan.txt', allowOutsideWorkspace: 'true' });
680
+ expect(res.statusCode).toBe(status);
681
+ expect(res.body).toEqual(body);
682
+ });
683
+
662
684
  it('rejects outside workspace mkdir without a trusted directory grant', async () => {
663
685
  const fsPromises = {
664
686
  mkdir: vi.fn(async () => undefined),
@@ -1012,6 +1034,88 @@ describe('fs exec git-read cache', () => {
1012
1034
  });
1013
1035
  });
1014
1036
 
1037
+ describe('fs raw byte ranges', () => {
1038
+ const createStreamingResponse = () => {
1039
+ const chunks = [];
1040
+ const headers = new Map();
1041
+ let statusCode = 200;
1042
+ const res = new Writable({
1043
+ write(chunk, _encoding, callback) {
1044
+ chunks.push(Buffer.from(chunk));
1045
+ callback();
1046
+ },
1047
+ });
1048
+ Object.assign(res, {
1049
+ status(code) { statusCode = code; return res; },
1050
+ json(payload) { chunks.push(Buffer.from(JSON.stringify(payload))); return res; },
1051
+ type() { return res; },
1052
+ send(payload) { chunks.push(Buffer.from(payload)); return res; },
1053
+ setHeader(name, value) { headers.set(name.toLowerCase(), value); return res; },
1054
+ getHeader(name) { return headers.get(name.toLowerCase()); },
1055
+ });
1056
+ return {
1057
+ res,
1058
+ finished: new Promise((resolve) => res.on('finish', resolve)),
1059
+ get statusCode() { return statusCode; },
1060
+ get body() { return Buffer.concat(chunks).toString('utf8'); },
1061
+ };
1062
+ };
1063
+
1064
+ const registerRawWithFile = (bytes) => {
1065
+ const open = vi.fn(async () => ({
1066
+ createReadStream: ({ start, end }) => Readable.from([bytes.subarray(start, end + 1)]),
1067
+ }));
1068
+ const readFile = vi.fn(async () => bytes);
1069
+ const handler = registerRaw({
1070
+ stat: async () => ({ isFile: () => true, size: bytes.length }),
1071
+ open,
1072
+ readFile,
1073
+ });
1074
+ return { handler, open, readFile };
1075
+ };
1076
+
1077
+ it('answers a bytes span with 206, the span headers, and only those bytes', async () => {
1078
+ const { handler, open, readFile } = registerRawWithFile(Buffer.from('0123456789'));
1079
+ const response = createStreamingResponse();
1080
+
1081
+ await handler({ query: { path: '/repo/clip.mp4' }, headers: { range: 'bytes=3-' } }, response.res);
1082
+ await response.finished;
1083
+
1084
+ expect(response.statusCode).toBe(206);
1085
+ expect(response.getHeader?.('content-range') ?? response.res.getHeader('content-range')).toBe('bytes 3-9/10');
1086
+ expect(response.res.getHeader('content-length')).toBe('7');
1087
+ expect(response.res.getHeader('accept-ranges')).toBe('bytes');
1088
+ expect(response.body).toBe('3456789');
1089
+ expect(open).toHaveBeenCalledWith('/repo/clip.mp4', 'r');
1090
+ expect(readFile).not.toHaveBeenCalled();
1091
+ });
1092
+
1093
+ it('serves the whole file, advertising ranges, when no span is asked for', async () => {
1094
+ const { handler, open } = registerRawWithFile(Buffer.from('0123456789'));
1095
+ const res = createMockResponse();
1096
+
1097
+ await handler({ query: { path: '/repo/clip.mp4' }, headers: {} }, res);
1098
+
1099
+ expect(res.statusCode).toBe(200);
1100
+ expect(res.getHeader('accept-ranges')).toBe('bytes');
1101
+ expect(res.body.toString('utf8')).toBe('0123456789');
1102
+ expect(open).not.toHaveBeenCalled();
1103
+ });
1104
+
1105
+ it('rejects a span past the end with 416 and the file size', async () => {
1106
+ const { handler, open } = registerRawWithFile(Buffer.from('0123456789'));
1107
+ const res = createMockResponse();
1108
+ res.end = vi.fn(() => res);
1109
+
1110
+ await handler({ query: { path: '/repo/clip.mp4' }, headers: { range: 'bytes=10-' } }, res);
1111
+
1112
+ expect(res.statusCode).toBe(416);
1113
+ expect(res.getHeader('content-range')).toBe('bytes */10');
1114
+ expect(res.end).toHaveBeenCalled();
1115
+ expect(open).not.toHaveBeenCalled();
1116
+ });
1117
+ });
1118
+
1015
1119
  describe('fs raw download Content-Disposition', () => {
1016
1120
  it('uses RFC 5987 filename*= encoding for non-ASCII filenames on download', async () => {
1017
1121
  const fsPromises = {
@@ -1446,6 +1550,7 @@ describe('fs stat directory error handling', () => {
1446
1550
  try {
1447
1551
  await mkdir(path.join(directory, 'fs'));
1448
1552
  await copyFile(new URL('./routes.js', import.meta.url), path.join(directory, 'fs/routes.mjs'));
1553
+ await copyFile(new URL('./byte-range.js', import.meta.url), path.join(directory, 'fs/byte-range.js'));
1449
1554
  await copyFile(new URL('../path-realpath-cache.js', import.meta.url), path.join(directory, 'path-realpath-cache.js'));
1450
1555
  expect(() => execFileSync('node', [
1451
1556
  '--input-type=module',
@@ -2501,6 +2501,18 @@ export async function getTrackingBranch(directory) {
2501
2501
  return tracking || null;
2502
2502
  }
2503
2503
 
2504
+ // Whether `sha` is reachable from the checked-out HEAD. An object git has never
2505
+ // fetched fails the same way an unrelated commit does: not an ancestor.
2506
+ export async function isAncestorOfHead(directory, sha) {
2507
+ const normalizedDirectory = normalizeDirectoryPath(directory);
2508
+ const normalizedSha = typeof sha === 'string' ? sha.trim() : '';
2509
+ if (!normalizedDirectory || !/^[0-9a-f]{7,64}$/i.test(normalizedSha)) {
2510
+ return false;
2511
+ }
2512
+ const result = await runGitCommand(normalizedDirectory, ['merge-base', '--is-ancestor', normalizedSha, 'HEAD']);
2513
+ return result.success;
2514
+ }
2515
+
2504
2516
  async function readStatus(normalizedDirectory, lightMode) {
2505
2517
  try {
2506
2518
  // Prefer an explicit non-repo check before simple-git status so a missing
@@ -83,7 +83,7 @@ that page, so callers cannot mistake a partial page for a complete one.
83
83
  - It first searches for **open** PRs by likely source owner plus exact head branch.
84
84
  - If that fails, it falls back to broader GitHub search for open PRs on the branch name.
85
85
  - An **open PR from any candidate repo always wins** over a closed/merged one, so a merged fork PR can never hide an open upstream PR for the same head.
86
- - Only when no target has an open PR does it return the branch's newest closed/merged PR, as history.
86
+ - Only when no target has an open PR does it return the branch's newest closed/merged PR, as history — and only when that PR's head commit is an ancestor of the checkout's `HEAD`. History is matched by branch name, and names get reused: a fresh worktree cut from the default branch under a name that was merged before must not inherit the old PR.
87
87
  - History is looked up **only for the ranked-first remote and the branch's own name** — the repo it actually pushes to. Live status is worth searching the whole fork network for; history is not, and asking every target for it multiplies serial GitHub calls until the route hits its `12s` resolve timeout and returns no status at all.
88
88
  - The history answer is remembered per repo+branch so discovery polls do not re-query it: a found closed/merged record for `6h`, and "no history yet" for `10m`. A found record only changes if a second PR appears on the same head, and while that one is open the open-PR path wins without ever reading this cache.
89
89
  - Creating, merging, or closing a PR invalidates both the shared repo pull list and that remembered history.
@@ -1,5 +1,5 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { getRemotes, getTrackingBranch } from '../git/index.js';
2
+ import { getRemotes, getTrackingBranch, isAncestorOfHead } from '../git/index.js';
3
3
  import { resolveGitHubRepoFromDirectory } from './repo/index.js';
4
4
  import { noteIfGitHubRateLimit } from './rate-limit.js';
5
5
 
@@ -543,6 +543,26 @@ const searchFallbackPr = async ({ octokit, branch, repoNames }) => {
543
543
 
544
544
  const isTerminalPr = (pr) => Boolean(pr) && (pr.state === 'closed' || Boolean(pr.merged_at));
545
545
 
546
+ // A closed/merged PR is matched by head branch NAME, and names get reused: a
547
+ // fresh worktree called `feature` cut from the default branch would inherit
548
+ // the merged PR of last month's `feature`. The PR only belongs to this checkout
549
+ // when the commit it was merged or closed at is part of the checkout's history.
550
+ // `isAncestor` is the git check, replaceable so the tests need no git repository.
551
+ const isHistoricalPrOfCheckout = async (directory, pr, { isAncestor = isAncestorOfHead } = {}) => {
552
+ const headSha = normalizeText(pr?.head?.sha);
553
+ if (!headSha) {
554
+ return false;
555
+ }
556
+ try {
557
+ return await isAncestor(directory, headSha);
558
+ } catch {
559
+ return false;
560
+ }
561
+ };
562
+
563
+ // Exported for focused unit tests.
564
+ export { isHistoricalPrOfCheckout };
565
+
546
566
  /**
547
567
  * Resolve the PRs a branch is associated with in one repo target.
548
568
  *
@@ -764,7 +784,7 @@ export async function resolveGitHubPrStatus({ octokit, directory, branch, remote
764
784
  }
765
785
  }
766
786
 
767
- if (historicalMatch) {
787
+ if (historicalMatch && await isHistoricalPrOfCheckout(directory, historicalMatch.pr)) {
768
788
  return historicalMatch;
769
789
  }
770
790
 
@@ -1,21 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test, vi } from 'bun:test';
2
2
 
3
- const listMock = mock(async () => ({ data: [] }));
4
-
5
- mock.module('../git/index.js', () => ({
6
- getRemotes: async () => [],
7
- getTrackingBranch: async () => null,
8
- }));
3
+ import { findBranchPrCandidates, invalidateRepoPullsCache, isHistoricalPrOfCheckout } from './pr-status.js';
9
4
 
10
- mock.module('./repo/index.js', () => ({
11
- resolveGitHubRepoFromDirectory: async () => null,
12
- }));
13
-
14
- mock.module('./rate-limit.js', () => ({
15
- noteIfGitHubRateLimit: () => {},
16
- }));
5
+ const listMock = mock(async () => ({ data: [] }));
17
6
 
18
- const { findBranchPrCandidates, invalidateRepoPullsCache } = await import('./pr-status.js');
7
+ const isAncestorMock = mock(async () => false);
19
8
 
20
9
  const openPr = {
21
10
  number: 15,
@@ -180,3 +169,27 @@ describe('findBranchPrCandidates', () => {
180
169
  expect(listMock.mock.calls.length).toBeGreaterThan(callsAfterFirst + 1);
181
170
  });
182
171
  });
172
+
173
+ describe('isHistoricalPrOfCheckout', () => {
174
+ beforeEach(() => {
175
+ isAncestorMock.mockReset();
176
+ });
177
+
178
+ test('a merged PR whose head commit is in the checkout history belongs to it', async () => {
179
+ isAncestorMock.mockImplementation(async () => true);
180
+ const pr = { ...mergedPr, head: { ...mergedPr.head, sha: 'abc1234' } };
181
+ expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock })).toBe(true);
182
+ expect(isAncestorMock).toHaveBeenCalledWith('/repo', 'abc1234');
183
+ });
184
+
185
+ test('a reused branch name without the merged commits does not inherit the PR', async () => {
186
+ isAncestorMock.mockImplementation(async () => false);
187
+ const pr = { ...mergedPr, head: { ...mergedPr.head, sha: 'abc1234' } };
188
+ expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock })).toBe(false);
189
+ });
190
+
191
+ test('a PR without a head sha is never attributed', async () => {
192
+ expect(await isHistoricalPrOfCheckout('/repo', mergedPr, { isAncestor: isAncestorMock })).toBe(false);
193
+ expect(isAncestorMock).not.toHaveBeenCalled();
194
+ });
195
+ });
@@ -62,7 +62,20 @@ A missing folder is omitted from the list. Other guests stay. A corrupt `extensi
62
62
  - `contributes.actions`, `contributes.commands`, and `contributes.tools` are validated by the SDK parse at install (`invalid-actions` / `invalid-commands` / `invalid-tools` surface as `invalid-manifest`) and copied to the catalog row unchanged. Tool presentations are applied in the UI only (`packages/ui/src/lib/guests/tool-presentation.ts`); no server route reads them. No server route runs an action or a command: the message or session item is built in the UI from the same records Export as Markdown uses, and a slash command is answered by the guest frame over `postMessage`. The rail badge is UI memory only.
63
63
  - `contributes.attach` is copied as `"panel"` or `"dialog"`. The composer + menu and New Worktree read that field. Omitted guests stay off those menus. The object form's `entry` (dialog only) is checked at install like `panel.entry`: missing HTML is `invalid-manifest`, an unbuilt script it loads is `missing-build`; the catalog exposes it as `attachEntry` and the attach window loads that page instead of `panel.entry`. The optional attach `data` payload lives only in the UI's chip, queued context part, and session metadata snapshot; no server route parses it.
64
64
  - `request` never leaves the declared `apiOrigin`. A 401 refreshes once. If that fails, tokens for that guest are dropped. Host Linear does not drop Linear auth on a failed GraphQL query. A paused guest never proxies `request` or `serviceRequest`.
65
+ - A service with `provides: ["browser"]` is also driven by the host (`../browser-control/provider.js`), not only by a panel: `proxyGuestServiceRequest` takes `timeoutMs`, `responseMax`, `idleStopMs`, and `signal` for that. A service that never became ready is `SERVICE_FAILED` (nothing was sent); a request that was sent and got no usable answer is `REQUEST_FAILED` (the service may have acted on it), and callers word their errors accordingly. `idleStopMs` arms a per-guest timer re-armed at the end of every request; when it fires with no request in flight the process is discarded (not a Pause: no epoch bump) and the next request respawns it. A user-facing stop clears the timer. Panel requests pass none of these and keep the SDK limits. The guest routes call `onGuestDeactivated({ guestId, guestName })` after a pause, a removal, or a withdrawn approval so a host role the extension held is handed back and the user told.
66
+ - `surface.js` hosts shared surfaces (`contributes.service.surface`). `/api/guests/:id/surface/ws` (URL-token WebSocket, allowlisted in `ui-auth.js` and `relay/tunnel-host.js`; upgrade refused with 404 unless `isSurfaceGuest`: enabled, fully approved, declares the surface) attaches a viewer to the extension's one session. The session holds the service (`holdGuestService`) and runs one pump: `GET /surface/frame?after=<seq>&wait=25000` through `openGuestServiceRequest`, newest frame kept, delivered per viewer only after that viewer acknowledged the previous one (`frame` text message + one binary message). Control is the session's: `none`, `agent` (set by `noteAgentActivity`, expires after `SURFACE_AGENT_HOLD_MS`), or a viewer id; the first deliberate input (anything but a button-less pointer move) from a viewer while nobody or the agent holds it takes it, `release` or the holder's disconnect gives it back, a second viewer's input is refused with a `control` message. Every change is broadcast (`mine` per connection) and posted to `POST /surface/control`. Input, control notices, resizes, and clipboard reads run through one promise chain per session, in socket order; work queued for an ended session is dropped, and an ended session's close handlers never call the service, so closing a panel after a pause, removal, or withdrawn approval cannot restart the process under the session's old authorization. When the last viewer leaves normally the session stays registered until its queue (including the final `none`) has drained, so a deactivation in that window still finds and cancels it; a viewer that reattaches meanwhile restarts the pump. Every hand-off (release, holder disconnect) bumps `session.handoffs`; an input batch remembers the count it arrived under and, if it no longer holds control, may take control only when nothing was handed off since, so a click queued behind a slow request cannot re-take control after the user let go, and a viewer that left (`gone`) never becomes the controller. `userControls` gates the browser provider; `endForGuest` (pause, removal, withdrawn approval) and a failed frame request end the session with an `ended` reason, and `stop()` closes every viewer on shutdown.
65
67
  - `serviceRequest` never leaves that guest's loopback port. Auth to the service is `Authorization: Bearer` with a host-minted token. The service env is an allowlist (PATH, HOME, temp, locale, Windows system variables) plus `OPENCHAMBER_SERVICE_*`; host secrets are not inherited. There is no sandbox: an allowed service has the user's full access, and the grant card says so. Ready is `GET /health` → 200. Parallel first requests share one spawn; they must not kill each other mid-start. Host quit stops every service. Disabling a guest stops its service without clearing OAuth or grants.
66
68
  - File access is decided in `files.js` on canonical paths, never on the string the guest sent. `matchesFilesystemPattern`, `expandHome`, and `globToRegExp` are pure; `resolveGuestFilePath` and `runGuestFileOperation` take `fsPromises` / `nodePath` so tests can stub the OS. The project directory is whatever `resolveOptionalProjectDirectory` validated for this request, so a guest cannot name a project the UI did not open.
67
69
  - Update swap (`updates.js` `updateGuest`): the new clone lands in `{dataDir}/extensions/.tmp-<id>-<random>` and is inspected exactly like an install before anything else moves. Only then: stop the guest's service, rename current → `.old-<id>`, rename temp → final, delete `.old-<id>`. Any failure before the swap deletes the temp dir and leaves the install untouched; a failed second rename moves `.old-<id>` back. The store row is not rewritten (same path, same id), the catalog cache and that guest's update cache entry are dropped.
68
70
  - The OAuth callback skips UI auth. Everything else under `/api/guests` stays behind the usual session.
71
+
72
+ ## Host shutdown
73
+
74
+ `beginGuestServiceShutdown` closes service admission synchronously. Requests retain
75
+ their host lifecycle across store reads and startup awaits, so a late completion
76
+ cannot spawn a process during shutdown or enter a later host lifecycle.
77
+ `stopAllGuestServices` cancels and drains pending starts as well as running and
78
+ already-stopping children. Per-guest Pause and ordinary cleanup do not close host
79
+ admission. `beginGuestServiceHost` opens a new lifecycle only after that drain.
80
+ The common graceful-shutdown runtime stops surface viewers before the services.
81
+ Crash/SIGKILL recovery remains outside this in-memory registry.
@@ -197,6 +197,12 @@ export const inspectGuestPackage = async (packageRoot, { openchamberVersion, ski
197
197
  if (panel.entry) {
198
198
  guest.entry = panel.entry;
199
199
  }
200
+ if (panel.dock !== undefined) {
201
+ guest.entryDock = panel.dock;
202
+ }
203
+ if (panel.size !== undefined) {
204
+ guest.entrySize = panel.size;
205
+ }
200
206
  const backgroundEntry = parsed.manifest.contributes.background?.entry;
201
207
  if (backgroundEntry) {
202
208
  if (!await resolveGuestAssetPath(packageRoot, backgroundEntry)) {
@@ -293,6 +299,12 @@ export const toPublicGuest = (guest) => {
293
299
  if (guest.entry) {
294
300
  row.entry = guest.entry;
295
301
  }
302
+ if (guest.entryDock) {
303
+ row.entryDock = guest.entryDock;
304
+ }
305
+ if (typeof guest.entrySize === 'number') {
306
+ row.entrySize = guest.entrySize;
307
+ }
296
308
  if (guest.backgroundEntry) {
297
309
  row.backgroundEntry = guest.backgroundEntry;
298
310
  }
@@ -204,6 +204,39 @@ describe('listInstalledGuests', () => {
204
204
 
205
205
  await fs.rm(dir, { recursive: true, force: true });
206
206
  });
207
+
208
+ test('a page docked beside a shared surface reaches the public row with its edge and size', async () => {
209
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-'));
210
+ const guestRoot = path.join(dir, 'sim');
211
+ await fs.mkdir(path.join(guestRoot, 'panel'), { recursive: true });
212
+ await fs.writeFile(path.join(guestRoot, 'panel', 'index.html'), '<script src="./main.js"></script>');
213
+ await fs.writeFile(path.join(guestRoot, 'panel', 'main.js'), 'console.log("strip")');
214
+ await fs.writeFile(path.join(guestRoot, 'service.js'), 'console.log("service")');
215
+ await fs.writeFile(path.join(guestRoot, 'package.json'), JSON.stringify({
216
+ name: '@openchamber/sim',
217
+ version: '1.0.0',
218
+ openchamber: {
219
+ apiVersion: 1,
220
+ contributes: {
221
+ panel: { id: 'sim', name: 'Sim', icon: 'window', entry: 'panel/index.html', dock: 'right', size: 240 },
222
+ service: { entry: 'service.js', runtime: 'host', surface: true },
223
+ },
224
+ },
225
+ }));
226
+
227
+ const inspected = await inspectGuestPackage(guestRoot, { openchamberVersion: '1.0.0' });
228
+ expect(inspected.ok).toBe(true);
229
+ if (inspected.ok) {
230
+ expect(toPublicGuest({ ...inspected.guest, source: 'path', path: guestRoot })).toMatchObject({
231
+ entry: 'panel/index.html',
232
+ entryDock: 'right',
233
+ entrySize: 240,
234
+ service: expect.objectContaining({ surface: true }),
235
+ });
236
+ }
237
+
238
+ await fs.rm(dir, { recursive: true, force: true });
239
+ });
207
240
  });
208
241
 
209
242
  describe('page-less packages', () => {