@vantran-se/openchamber-web 1.24.3 → 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-BjJHrzmU.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-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-D0C9nU7K.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-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-DyxZVVVR.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-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.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-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-CL2vsrIZ.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-Dh7WEsdM.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-Bsba8KFp.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-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.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-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.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-B8IcVQAA.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-COVQhWGd.js +0 -4
  316. package/dist/assets/FilesView-B1V5JxFX.js +0 -13
  317. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
  320. package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
  321. package/dist/assets/PlanView-LW2z_EAP.js +0 -5
  322. package/dist/assets/PluginPane-B4tKno2h.js +0 -3
  323. package/dist/assets/SettingsView-DEaMulN9.js +0 -29
  324. package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
  326. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
  328. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
  329. package/dist/assets/de-DgxgFyGG.js +0 -10
  330. package/dist/assets/es-BFsZj5F_.js +0 -16
  331. package/dist/assets/fr-jdZ5KKQi.js +0 -14
  332. package/dist/assets/index-CKHjPKbq.js +0 -1
  333. package/dist/assets/index-Cbci4i1W.js +0 -40
  334. package/dist/assets/index-DqaMWNrC.css +0 -1
  335. package/dist/assets/ja-C5kKrhO4.js +0 -16
  336. package/dist/assets/ko-DXDTT_0O.js +0 -16
  337. package/dist/assets/main-CZALtVek.js +0 -896
  338. package/dist/assets/main-CgXMBNvG.js +0 -2
  339. package/dist/assets/miniChat-CwTmDixi.js +0 -2
  340. package/dist/assets/mobile-CLf2vCtO.js +0 -2
  341. package/dist/assets/pl-D-7x4Y8Q.js +0 -16
  342. package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
  344. package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
  346. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
  347. package/dist/assets/tr-Dr-oJ22L.js +0 -7
  348. package/dist/assets/uk-9Nx83UcI.js +0 -16
  349. package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
  350. package/dist/assets/useRootScrollLock-DdvHf9Pt.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-Bxy_Hj3r.js +0 -16
  354. package/dist/assets/zh-TW-0BB4gpHM.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
@@ -15,7 +15,8 @@ export const createPushRuntime = (deps) => {
15
15
  const {
16
16
  fsPromises,
17
17
  path,
18
- webPush,
18
+ // Resolves the web-push module; it is loaded on first use, not with the server.
19
+ loadWebPush,
19
20
  PUSH_SUBSCRIPTIONS_FILE_PATH,
20
21
  readSettingsFromDiskMigrated,
21
22
  writeSettingsToDisk,
@@ -86,6 +87,7 @@ export const createPushRuntime = (deps) => {
86
87
  return { publicKey: existing.publicKey, privateKey: existing.privateKey };
87
88
  }
88
89
 
90
+ const webPush = await loadWebPush();
89
91
  const generated = webPush.generateVAPIDKeys();
90
92
  const next = {
91
93
  ...settings,
@@ -210,6 +212,7 @@ export const createPushRuntime = (deps) => {
210
212
  };
211
213
 
212
214
  try {
215
+ const webPush = await loadWebPush();
213
216
  await webPush.sendNotification(pushSubscription, body);
214
217
  } catch (error) {
215
218
  const statusCode = typeof error?.statusCode === 'number' ? error.statusCode : null;
@@ -344,6 +347,7 @@ export const createPushRuntime = (deps) => {
344
347
  console.warn('[Push] No public origin configured for VAPID; set OPENCHAMBER_VAPID_SUBJECT or enable push once from a real origin.');
345
348
  }
346
349
 
350
+ const webPush = await loadWebPush();
347
351
  webPush.setVapidDetails(subject, keys.publicKey, keys.privateKey);
348
352
  pushInitialized = true;
349
353
  };
@@ -9,11 +9,11 @@ const createRuntime = () => createPushRuntime({
9
9
  writeFile: vi.fn(async () => {}),
10
10
  },
11
11
  path: { dirname: () => '/tmp' },
12
- webPush: {
12
+ loadWebPush: async () => ({
13
13
  generateVAPIDKeys: vi.fn(() => ({ publicKey: 'public', privateKey: 'private' })),
14
14
  sendNotification: vi.fn(async () => {}),
15
15
  setVapidDetails: vi.fn(),
16
- },
16
+ }),
17
17
  PUSH_SUBSCRIPTIONS_FILE_PATH: '/tmp/push-subscriptions.json',
18
18
  readSettingsFromDiskMigrated: vi.fn(async () => ({})),
19
19
  writeSettingsToDisk: vi.fn(async () => {}),
@@ -297,7 +297,7 @@ export const registerNotificationRoutes = (app, dependencies) => {
297
297
  });
298
298
 
299
299
  // Cross-project seed for clients that do not initialize every directory:
300
- // live status per session plus the permission and question requests still
300
+ // live status per session plus the permission requests and forms still
301
301
  // waiting for an answer. Both come from the server's single upstream stream.
302
302
  app.get('/api/sessions/status', async (_req, res) => {
303
303
  await ensureSessionWatcher();
@@ -1,3 +1,7 @@
1
+ import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
2
+ import { createSessionActivityProbe } from '../opencode/session-activity.js';
3
+
4
+
1
5
  export const createNotificationTriggerRuntime = (deps) => {
2
6
  const {
3
7
  readSettingsFromDisk,
@@ -14,8 +18,22 @@ export const createNotificationTriggerRuntime = (deps) => {
14
18
  isAnyInteractiveClientVisible,
15
19
  buildOpenCodeUrl,
16
20
  getOpenCodeAuthHeaders,
21
+ // OpenChamber's own session metadata (the goal lives there, not on the
22
+ // OpenCode record). Optional: without it no goal can suppress a push.
23
+ readSessionMetadata = null,
17
24
  } = deps;
18
25
  let getIsSessionAutoAccepting = deps.getIsSessionAutoAccepting;
26
+ const activityProbe = createSessionActivityProbe({ buildOpenCodeUrl, getOpenCodeAuthHeaders, timeoutMs: 2000 });
27
+
28
+ // A parent goes idle while a background subagent still works; OpenCode then
29
+ // hands the result back and the parent runs again. That first idle is a
30
+ // pause, so it announces nothing. When the check cannot be made, the idle is
31
+ // announced as before: a missed "ready" is worse than an early one.
32
+ const isPausedForSubagents = async (sessionId) => {
33
+ const statuses = await activityProbe.fetchActiveSessionStatuses();
34
+ if (!statuses) return false;
35
+ return (await activityProbe.hasWorkingChildren(sessionId, statuses)) === true;
36
+ };
19
37
  const setGetIsSessionAutoAccepting = (resolver) => {
20
38
  getIsSessionAutoAccepting = typeof resolver === 'function' ? resolver : undefined;
21
39
  };
@@ -157,8 +175,57 @@ export const createNotificationTriggerRuntime = (deps) => {
157
175
  const getParentIdFromPayload = (payload) => {
158
176
  if (!payload || typeof payload !== 'object') return undefined;
159
177
  if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
160
- const parentID = payload.properties?.info?.parentID ?? null;
161
- return typeof parentID === 'string' && parentID.length > 0 ? parentID : null;
178
+ const parentID = payload.properties?.info?.parentID;
179
+ if (typeof parentID === 'string' && parentID.length > 0) return parentID;
180
+ // Only the full record from `session.created` proves a session has no
181
+ // parent. `session.updated` also carries partial records (usage, title)
182
+ // without `parentID`, and reading those as "no parent" turned a subagent
183
+ // into a main session, so its finish announced "ready" with subagent
184
+ // notifications turned off.
185
+ return payload.type === 'session.created' ? null : undefined;
186
+ };
187
+
188
+ // v2 splits one assistant turn over two events: `session.step.started`
189
+ // names the agent and model, `session.step.ended` carries the finish. The
190
+ // translator hands both over as `message.updated` on the same message id,
191
+ // so the start is remembered here and folded into the finish, which is the
192
+ // event the "ready" push is built from. Bounded: a session emits a handful
193
+ // of steps and the map is per process.
194
+ const ASSISTANT_STEP_CACHE_LIMIT = 500;
195
+ const assistantStepMeta = new Map();
196
+ // The reply a turn ended on: the idle event names only the session, so the
197
+ // ready notification borrows this step's id for its agent, model and text.
198
+ const lastAssistantStepBySession = new Map();
199
+ const rememberAssistantStep = (info) => {
200
+ if (!info || info.role !== 'assistant') return;
201
+ const id = String(info.id ?? '');
202
+ const agent = String(info.agent ?? info.mode ?? '');
203
+ const modelID = String(info.modelID ?? '');
204
+ if (!id || (!agent && !modelID)) return;
205
+ assistantStepMeta.delete(id);
206
+ assistantStepMeta.set(id, { agent, modelID });
207
+ const sessionId = String(info.sessionID ?? '');
208
+ if (sessionId) {
209
+ lastAssistantStepBySession.delete(sessionId);
210
+ lastAssistantStepBySession.set(sessionId, id);
211
+ while (lastAssistantStepBySession.size > ASSISTANT_STEP_CACHE_LIMIT) {
212
+ lastAssistantStepBySession.delete(lastAssistantStepBySession.keys().next().value);
213
+ }
214
+ }
215
+ while (assistantStepMeta.size > ASSISTANT_STEP_CACHE_LIMIT) {
216
+ assistantStepMeta.delete(assistantStepMeta.keys().next().value);
217
+ }
218
+ };
219
+ const withRememberedStep = (info) => {
220
+ const remembered = info ? assistantStepMeta.get(String(info.id ?? '')) : undefined;
221
+ if (!remembered) return info;
222
+ const merged = { ...info };
223
+ if (!merged.agent && !merged.mode) {
224
+ merged.agent = remembered.agent;
225
+ merged.mode = remembered.agent;
226
+ }
227
+ if (!merged.modelID) merged.modelID = remembered.modelID;
228
+ return merged;
162
229
  };
163
230
 
164
231
  const maybeCacheSessionParentFromPayload = (payload) => {
@@ -177,7 +244,7 @@ export const createNotificationTriggerRuntime = (deps) => {
177
244
  if (cached !== undefined) return cached;
178
245
 
179
246
  try {
180
- const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
247
+ const base = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
181
248
  const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
182
249
  const response = await fetch(url, {
183
250
  method: 'GET',
@@ -190,7 +257,7 @@ export const createNotificationTriggerRuntime = (deps) => {
190
257
  if (!response.ok) {
191
258
  return undefined;
192
259
  }
193
- const session = await response.json().catch(() => null);
260
+ const session = unwrapOpenCodeResponse(await response.json().catch(() => null));
194
261
  if (!session || typeof session !== 'object') {
195
262
  return undefined;
196
263
  }
@@ -281,19 +348,11 @@ export const createNotificationTriggerRuntime = (deps) => {
281
348
  // A session with an ACTIVE goal suppresses per-turn ready notifications;
282
349
  // the session-goal runtime sends its own notification when the goal
283
350
  // settles. Fetch failures fall through to normal notification behavior.
284
- const hasActiveSessionGoal = async (sessionId, directory) => {
285
- if (!sessionId) return false;
351
+ const hasActiveSessionGoal = async (sessionId) => {
352
+ if (!sessionId || typeof readSessionMetadata !== 'function') return false;
286
353
  try {
287
- const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
288
- const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
289
- const response = await fetch(url, {
290
- method: 'GET',
291
- headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
292
- signal: AbortSignal.timeout(2000),
293
- });
294
- if (!response.ok) return false;
295
- const session = await response.json().catch(() => null);
296
- const goal = session?.metadata?.openchamber?.goal;
354
+ const metadata = await readSessionMetadata(sessionId);
355
+ const goal = metadata?.openchamber?.goal;
297
356
  return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
298
357
  } catch {
299
358
  return false;
@@ -309,7 +368,16 @@ export const createNotificationTriggerRuntime = (deps) => {
309
368
 
310
369
  const sessionId = extractSessionIdFromPayload(payload);
311
370
  const notificationDirectory = extractDirectoryFromPayload(payload);
371
+ // A user abort arrives as `session.idle` with `aborted: true`: the turn
372
+ // did not finish, so there is nothing to announce as ready.
373
+ if (payload.type === 'session.idle' && payload.properties?.aborted === true) {
374
+ return;
375
+ }
376
+
312
377
  if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
378
+ if (payload.type === 'session.idle' && await isPausedForSubagents(sessionId)) {
379
+ return;
380
+ }
313
381
  const error = payload.properties?.error;
314
382
  const errorText = typeof error?.message === 'string'
315
383
  ? error.message
@@ -319,7 +387,10 @@ export const createNotificationTriggerRuntime = (deps) => {
319
387
  type: 'message.updated',
320
388
  properties: {
321
389
  ...payload.properties,
390
+ // Only a turn end announces readiness; a step's `stop` does not.
391
+ turnEnded: true,
322
392
  info: {
393
+ id: lastAssistantStepBySession.get(sessionId),
323
394
  sessionID: sessionId,
324
395
  role: 'assistant',
325
396
  finish: payload.type === 'session.error' ? 'error' : 'stop',
@@ -331,8 +402,13 @@ export const createNotificationTriggerRuntime = (deps) => {
331
402
  }
332
403
 
333
404
  if (payload.type === 'message.updated') {
334
- const info = payload.properties?.info;
335
- if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId) {
405
+ rememberAssistantStep(payload.properties?.info);
406
+ const info = withRememberedStep(payload.properties?.info);
407
+ // v2 ends every step with a `message.updated`; the last one says `stop`,
408
+ // but the execution can still drain steering input after it. Readiness
409
+ // is announced from the execution's terminal `session.idle` instead.
410
+ if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId && payload.properties?.turnEnded === true) {
411
+ payload = { ...payload, properties: { ...payload.properties, info } };
336
412
  const settings = await readSettingsFromDisk();
337
413
 
338
414
  if (settings.notifyOnSubtasks === false) {
@@ -353,7 +429,7 @@ export const createNotificationTriggerRuntime = (deps) => {
353
429
  // While a goal drives the session, per-turn "ready" notifications are
354
430
  // noise produced by the goal loop itself — the goal's own settle
355
431
  // notification (complete/blocked/budget) is the final word instead.
356
- if (await hasActiveSessionGoal(sessionId, notificationDirectory)) {
432
+ if (await hasActiveSessionGoal(sessionId)) {
357
433
  return;
358
434
  }
359
435
 
@@ -504,7 +580,7 @@ export const createNotificationTriggerRuntime = (deps) => {
504
580
  return;
505
581
  }
506
582
 
507
- if (payload.type === 'question.asked' && sessionId) {
583
+ if (payload.type === 'form.created' && sessionId) {
508
584
  const existingTimer = pushQuestionDebounceTimers.get(sessionId);
509
585
  if (existingTimer) {
510
586
  clearTimeout(existingTimer);
@@ -522,9 +598,14 @@ export const createNotificationTriggerRuntime = (deps) => {
522
598
  return;
523
599
  }
524
600
 
525
- const firstQuestion = payload.properties?.questions?.[0];
526
- const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
527
- const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
601
+ // v2 replaced questions with forms: the form's title is the ask, and
602
+ // the first field carries the detail a question used to put in its body.
603
+ const form = payload.properties?.form;
604
+ const header = typeof form?.title === 'string' ? form.title.trim() : '';
605
+ const firstField = Array.isArray(form?.fields) ? form.fields[0] : null;
606
+ const fieldTitle = typeof firstField?.title === 'string' ? firstField.title.trim() : '';
607
+ const fieldDescription = typeof firstField?.description === 'string' ? firstField.description.trim() : '';
608
+ const questionText = fieldDescription || fieldTitle;
528
609
 
529
610
  let title = /plan\s*mode/i.test(header)
530
611
  ? 'Switch to plan mode'
@@ -604,7 +685,15 @@ export const createNotificationTriggerRuntime = (deps) => {
604
685
 
605
686
  if (payload.type === 'permission.asked' && sessionId) {
606
687
  const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
607
- const permission = payload.properties?.permission;
688
+ // v2 describes a permission request as an action plus the resources it
689
+ // touches; the v1 single `permission` string is gone.
690
+ const permissionAction = typeof payload.properties?.action === 'string' ? payload.properties.action.trim() : '';
691
+ const permissionResource = Array.isArray(payload.properties?.resources)
692
+ ? String(payload.properties.resources[0] ?? '').trim()
693
+ : '';
694
+ const permissionMessage = typeof payload.properties?.message === 'string' ? payload.properties.message.trim() : '';
695
+ const permission = permissionMessage
696
+ || (permissionAction && permissionResource ? `${permissionAction}: ${permissionResource}` : permissionAction);
608
697
  const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
609
698
  if (requestKey && notifiedPermissionRequests.has(requestKey)) {
610
699
  return;
@@ -713,7 +802,7 @@ export const createNotificationTriggerRuntime = (deps) => {
713
802
  const sendGoalSettlePush = async ({ sessionId, directory, status, title, body }) => {
714
803
  let sessionName = '';
715
804
  try {
716
- const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
805
+ const base = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
717
806
  const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
718
807
  const response = await fetch(url, {
719
808
  method: 'GET',
@@ -721,7 +810,7 @@ export const createNotificationTriggerRuntime = (deps) => {
721
810
  signal: AbortSignal.timeout(2000),
722
811
  });
723
812
  if (response.ok) {
724
- const session = await response.json().catch(() => null);
813
+ const session = unwrapOpenCodeResponse(await response.json().catch(() => null));
725
814
  if (typeof session?.title === 'string') sessionName = session.title.trim();
726
815
  }
727
816
  } catch {
@@ -1,4 +1,5 @@
1
1
  import { summarizeText as summarizeSharedText } from '../text/summarization.js';
2
+ import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
2
3
 
3
4
  export const createNotificationTemplateRuntime = (deps) => {
4
5
  const {
@@ -136,7 +137,7 @@ export const createNotificationTemplateRuntime = (deps) => {
136
137
  if (!sessionId) return '';
137
138
 
138
139
  try {
139
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
140
+ const url = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}/message`, '');
140
141
  const response = await fetch(`${url}?limit=5`, {
141
142
  method: 'GET',
142
143
  headers: {
@@ -148,26 +149,23 @@ export const createNotificationTemplateRuntime = (deps) => {
148
149
 
149
150
  if (!response.ok) return '';
150
151
 
151
- const messages = await response.json().catch(() => null);
152
- if (!Array.isArray(messages)) return '';
152
+ // v2 pages messages as `{ data, cursor }`, newest first, and a message is
153
+ // a flat record: an assistant one carries `content[]`, not `parts`.
154
+ const page = await response.json().catch(() => null);
155
+ const messages = Array.isArray(page?.data) ? page.data : null;
156
+ if (!messages) return '';
153
157
 
154
158
  let target = null;
155
159
  if (messageId) {
156
- target = messages.find((message) => message?.info?.id === messageId && message?.info?.role === 'assistant');
160
+ target = messages.find((message) => message?.id === messageId && message?.type === 'assistant');
157
161
  }
158
162
  if (!target) {
159
- for (let i = messages.length - 1; i >= 0; i -= 1) {
160
- const message = messages[i];
161
- if (message?.info?.role === 'assistant' && message?.info?.finish === 'stop') {
162
- target = message;
163
- break;
164
- }
165
- }
163
+ target = messages.find((message) => message?.type === 'assistant' && message?.finish === 'stop') ?? null;
166
164
  }
167
165
 
168
- if (!target || !Array.isArray(target.parts)) return '';
166
+ if (!target || !Array.isArray(target.content)) return '';
169
167
 
170
- return extractTextFromParts(target.parts, maxLength);
168
+ return extractTextFromParts(target.content, maxLength);
171
169
  } catch {
172
170
  return '';
173
171
  }
@@ -201,7 +199,7 @@ export const createNotificationTemplateRuntime = (deps) => {
201
199
  }
202
200
 
203
201
  try {
204
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
202
+ const url = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
205
203
  const response = await fetch(url, {
206
204
  method: 'GET',
207
205
  headers: { Accept: 'application/json' },
@@ -211,7 +209,7 @@ export const createNotificationTemplateRuntime = (deps) => {
211
209
  console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`);
212
210
  return null;
213
211
  }
214
- const data = await response.json().catch(() => null);
212
+ const data = unwrapOpenCodeResponse(await response.json().catch(() => null));
215
213
  if (data && typeof data === 'object') {
216
214
  sessionInfoCache.set(sessionId, { data, at: Date.now() });
217
215
  return data;
@@ -69,15 +69,22 @@ describe('notification template message extraction', () => {
69
69
 
70
70
  it('excludes reasoning parts when fetching assistant messages', async () => {
71
71
  const runtime = createRuntime();
72
- globalThis.fetch = vi.fn(async () => new Response(JSON.stringify([
73
- {
74
- info: { id: 'msg-1', role: 'assistant', finish: 'stop' },
75
- parts: [
76
- { type: 'reasoning', text: 'private chain of thought' },
77
- { type: 'text', text: 'final answer' },
78
- ],
79
- },
80
- ])));
72
+ // v2 pages messages as `{ data, cursor }` and an assistant message is a
73
+ // flat record carrying `content[]`.
74
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
75
+ data: [
76
+ {
77
+ id: 'msg-1',
78
+ type: 'assistant',
79
+ finish: 'stop',
80
+ content: [
81
+ { type: 'reasoning', text: 'private chain of thought' },
82
+ { type: 'text', text: 'final answer' },
83
+ ],
84
+ },
85
+ ],
86
+ cursor: {},
87
+ })));
81
88
 
82
89
  await expect(runtime.fetchLastAssistantMessageText('session-1', 'msg-1')).resolves.toBe('final answer');
83
90
  });
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createNotificationTriggerRuntime } from './runtime.js';
4
+
5
+ /**
6
+ * The "ready" push is built from the translated `message.updated` events.
7
+ * v2 splits a turn: the step start names agent and model, the step end
8
+ * carries the finish. What is pinned here: the finish is announced with the
9
+ * agent and model from the start, a user abort announces nothing, and an
10
+ * active goal (which lives in OpenChamber's own metadata) silences the push.
11
+ */
12
+ const makeRuntime = ({ metadata = {} } = {}) => {
13
+ const emitDesktopNotification = vi.fn(() => true);
14
+ const runtime = createNotificationTriggerRuntime({
15
+ readSettingsFromDisk: async () => ({ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnCompletion: true }),
16
+ prepareNotificationLastMessage: async ({ message }) => message,
17
+ buildTemplateVariables: async () => ({}),
18
+ extractLastMessageText: () => 'done',
19
+ fetchLastAssistantMessageText: async () => 'done',
20
+ resolveNotificationTemplate: () => '',
21
+ shouldApplyResolvedTemplateMessage: () => false,
22
+ emitDesktopNotification,
23
+ broadcastUiNotification: vi.fn(),
24
+ sendPushToAllUiSessions: vi.fn(async () => undefined),
25
+ sendApnsToAllUiSessions: vi.fn(async () => undefined),
26
+ isAnyInteractiveClientVisible: () => true,
27
+ buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
28
+ getOpenCodeAuthHeaders: () => ({}),
29
+ readSessionMetadata: async () => metadata,
30
+ });
31
+ return { runtime, emitDesktopNotification };
32
+ };
33
+
34
+ const stepStarted = (sessionID, id) => ({
35
+ type: 'message.updated',
36
+ properties: { sessionID, info: { id, sessionID, role: 'assistant', agent: 'build', providerID: 'anthropic', modelID: 'claude-sonnet-5', time: { created: 1 } } },
37
+ });
38
+ const stepEnded = (sessionID, id) => ({
39
+ type: 'message.updated',
40
+ properties: { sessionID, info: { id, sessionID, role: 'assistant', finish: 'stop', time: { completed: 2 } } },
41
+ });
42
+
43
+ const turnEnded = (sessionID) => ({ type: 'session.idle', properties: { sessionID } });
44
+
45
+ describe('ready notification on v2 step events', () => {
46
+ it('announces the turn end with the agent and model of its last step', async () => {
47
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
48
+ const { runtime, emitDesktopNotification } = makeRuntime();
49
+
50
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
51
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
52
+
53
+ // A step's `stop` is not the end of the turn: the execution may still
54
+ // drain steering input, so only its idle event announces readiness.
55
+ await runtime.maybeSendPushForTrigger(stepEnded('ses_1', 'msg_1'));
56
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
57
+
58
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
59
+ expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
60
+ expect(emitDesktopNotification.mock.calls[0][0]).toMatchObject({
61
+ kind: 'ready',
62
+ title: 'Build agent is ready',
63
+ body: 'Claude Sonnet 5 completed the task',
64
+ });
65
+ });
66
+
67
+ it('announces nothing for a user abort', async () => {
68
+ const { runtime, emitDesktopNotification } = makeRuntime();
69
+
70
+ await runtime.maybeSendPushForTrigger({
71
+ type: 'session.idle',
72
+ properties: { sessionID: 'ses_2', aborted: true, reason: 'user', error: { name: 'MessageAbortedError', message: 'aborted' } },
73
+ });
74
+
75
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it('stays quiet while a goal from OpenChamber\'s own metadata is active', async () => {
79
+ const { runtime, emitDesktopNotification } = makeRuntime({
80
+ metadata: { openchamber: { goal: { id: 'g', status: 'active', objective: 'x' } } },
81
+ });
82
+
83
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_3', 'msg_3'));
84
+ await runtime.maybeSendPushForTrigger(stepEnded('ses_3', 'msg_3'));
85
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_3'));
86
+
87
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
88
+ });
89
+ });
90
+
91
+ describe('ready notification while background subagents run', () => {
92
+ const stubOpenCode = ({ active, children }) => {
93
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
94
+ const url = new URL(String(input));
95
+ if (url.pathname === '/api/session/active') return Response.json({ data: active() });
96
+ if (url.pathname === '/api/session') return Response.json({ data: children, cursor: {} });
97
+ return Response.json({});
98
+ }));
99
+ };
100
+
101
+ it('stays silent on the pause and announces the real turn end', async () => {
102
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
103
+ let active = { ses_child: { type: 'running' } };
104
+ stubOpenCode({ active: () => active, children: [{ id: 'ses_child', parentID: 'ses_1' }] });
105
+ const { runtime, emitDesktopNotification } = makeRuntime();
106
+
107
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
108
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
109
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
110
+
111
+ // The subagent finished and OpenCode ran the parent again with its result.
112
+ active = {};
113
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
114
+ expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
115
+ vi.unstubAllGlobals();
116
+ });
117
+
118
+ it('announces the idle when the subagent check cannot be made', async () => {
119
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
120
+ vi.stubGlobal('fetch', vi.fn(async () => new Response('down', { status: 503 })));
121
+ const { runtime, emitDesktopNotification } = makeRuntime();
122
+
123
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
124
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
125
+ expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
126
+ vi.unstubAllGlobals();
127
+ });
128
+ });
129
+
130
+ describe('subagent finish with subagent notifications off', () => {
131
+ const makeSubtaskRuntime = () => {
132
+ const emitDesktopNotification = vi.fn(() => true);
133
+ const runtime = createNotificationTriggerRuntime({
134
+ readSettingsFromDisk: async () => ({ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnCompletion: true, notifyOnSubtasks: false }),
135
+ prepareNotificationLastMessage: async ({ message }) => message,
136
+ buildTemplateVariables: async () => ({}),
137
+ extractLastMessageText: () => 'done',
138
+ fetchLastAssistantMessageText: async () => 'done',
139
+ resolveNotificationTemplate: () => '',
140
+ shouldApplyResolvedTemplateMessage: () => false,
141
+ emitDesktopNotification,
142
+ broadcastUiNotification: vi.fn(),
143
+ sendPushToAllUiSessions: vi.fn(async () => undefined),
144
+ sendApnsToAllUiSessions: vi.fn(async () => undefined),
145
+ isAnyInteractiveClientVisible: () => true,
146
+ buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
147
+ getOpenCodeAuthHeaders: () => ({}),
148
+ readSessionMetadata: async () => ({}),
149
+ });
150
+ return { runtime, emitDesktopNotification };
151
+ };
152
+
153
+ // OpenCode answers `GET /api/session/:id` as `{ data }` with no `location`.
154
+ const stubSessionRecord = () => {
155
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
156
+ const url = new URL(String(input));
157
+ if (url.pathname === '/api/session/active') return Response.json({ data: {} });
158
+ if (url.pathname === '/api/session/ses_child') return Response.json({ data: { id: 'ses_child', parentID: 'ses_parent' } });
159
+ return Response.json({ data: [], cursor: {} });
160
+ }));
161
+ };
162
+
163
+ it('reads the parent from the session record envelope', async () => {
164
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
165
+ stubSessionRecord();
166
+ const { runtime, emitDesktopNotification } = makeSubtaskRuntime();
167
+
168
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_child', 'msg_c'));
169
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_child'));
170
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
171
+ vi.unstubAllGlobals();
172
+ });
173
+
174
+ it('a partial session update does not erase a known parent', async () => {
175
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
176
+ vi.stubGlobal('fetch', vi.fn(async () => new Response('down', { status: 503 })));
177
+ const { runtime, emitDesktopNotification } = makeSubtaskRuntime();
178
+
179
+ await runtime.maybeSendPushForTrigger({ type: 'session.created', properties: { sessionID: 'ses_child', info: { id: 'ses_child', parentID: 'ses_parent' } } });
180
+ // Usage updates arrive as `session.updated` with a partial record.
181
+ await runtime.maybeSendPushForTrigger({ type: 'session.updated', properties: { sessionID: 'ses_child', info: { id: 'ses_child', cost: 1 } } });
182
+ await runtime.maybeSendPushForTrigger(stepStarted('ses_child', 'msg_c'));
183
+ await runtime.maybeSendPushForTrigger(turnEnded('ses_child'));
184
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
185
+ vi.unstubAllGlobals();
186
+ });
187
+ });
@@ -59,6 +59,16 @@ other.
59
59
  directory and does not erase other session results.
60
60
  - Destructive session/worktree deletion and project-path registration are not
61
61
  part of the action contract.
62
+ - `file.open` shows a file in the user's viewer. `file-open.js` resolves a
63
+ relative path against the session directory (an explicit `directory` wins),
64
+ refuses a relative path with no directory at all, checks the target is an
65
+ existing file, then hands `{ path, directory, sessionId }` to the injected
66
+ `emit`, which `index.js` writes to every UI control stream as
67
+ `openchamber:file-open-request`. Nothing comes back: opening a tab does not
68
+ fail quietly on a client, so the count of clients reached is the signal, and
69
+ zero is a 503, never a claimed success. Paths outside the workspace are
70
+ allowed on purpose: screenshots and recordings often land in a temp
71
+ directory, and the viewer already reads such files.
62
72
  - `browser.capture` writes its image on the server, into
63
73
  `.openchamber/screenshots/` under the scoped project directory, and returns
64
74
  the project-relative path rather than the image bytes. The client that took
@@ -22,6 +22,7 @@ export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
22
22
  { action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
23
23
  { action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
24
24
  { action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
25
+ { action: 'file.open', title: 'Show a file to the user', description: 'Open path in the user\'s file panel, in front of whatever they had open, so they can look at a result you produced: a screenshot, report, CSV, recording, or generated page. Relative to the session directory. Use it only when seeing the file moves the work forward, not for every file you touch' },
25
26
  ]);
26
27
 
27
28
  const OPENCHAMBER_CONTROL_ACTIONS = Object.freeze(
@@ -65,9 +66,9 @@ export const OPENCHAMBER_WEB_ACTIONS = Object.freeze(
65
66
  * reads as a complete fact is exactly the one whose conditions get lost.
66
67
  */
67
68
  export const OPENCHAMBER_MEMORY_ACTION_DEFINITIONS = Object.freeze([
68
- { action: 'memory.read', title: 'Read a stored memory', description: 'Read the full text of one memory listed in the session index. The index shows titles only, and a title omits the conditions that decide how the memory applies, so read before acting rather than working from the title. Requires title (as the index spells it) or memoryId; scope is optional and both stores are searched without it' },
69
+ { action: 'memory.read', title: 'Read a stored memory', description: 'Read the full text of one memory listed in the session index. The index shows titles only, and a title omits the conditions that decide how the memory applies, so read before acting rather than working from the title. Once read, an entry stays in your context; do not read it again in the same conversation. Requires title (as the index spells it) or memoryId; scope is optional and both stores are searched without it' },
69
70
  { action: 'memory.list', title: 'List stored memories', description: 'List stored memory titles when the session index is missing or stale; scope is global, project, or both (default)' },
70
- { action: 'memory.save', title: 'Remember something', description: 'Store a durable fact, preference, or reference; requires title and body, plus scope global (about the user) or project (about this codebase). Restating something already stored updates it. Do not store secrets, one-off task state, or anything the user asked you not to keep' },
71
+ { action: 'memory.save', title: 'Remember something', description: 'Store a durable fact, preference, or reference; requires title and body, plus scope global (about the user) or project (about this codebase). Restating something already stored updates it. Do not store secrets, one-off task state, or anything the user asked you not to keep; when the user explicitly asks you to remember something, store it unless it is a secret' },
71
72
  { action: 'memory.delete', title: 'Forget a memory', description: 'Delete a memory that turned out to be wrong or obsolete; requires memoryId and scope' },
72
73
  ]);
73
74
 
@@ -10,7 +10,7 @@ export class OpenChamberControlError extends Error {
10
10
  export const asControlError = (error, fallbackMessage, fallbackStatus = 500) => {
11
11
  if (error instanceof OpenChamberControlError) return error;
12
12
  const message = error instanceof Error ? error.message : fallbackMessage;
13
- return new OpenChamberControlError(message || fallbackMessage, Number(error?.statusCode) || fallbackStatus, {
13
+ return new OpenChamberControlError(message || fallbackMessage, Number(error?.statusCode) || Number(error?.status) || fallbackStatus, {
14
14
  ...(error?.goalConfigured === true ? { goalConfigured: true } : {}),
15
15
  });
16
16
  };