@vantran-se/openchamber-web 1.24.4 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-B82Oye3V.js} +1 -1
  5. package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
  6. package/dist/assets/FilesView-DjksmZRo.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
  11. package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
  14. package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
  17. package/dist/assets/PlanView-_eZvb-G5.js +5 -0
  18. package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
  19. package/dist/assets/SettingsView-BRfr46hb.js +29 -0
  20. package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
  22. package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
  26. package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
  29. package/dist/assets/de-CUO1-KqF.js +10 -0
  30. package/dist/assets/es-BlbC6sGJ.js +16 -0
  31. package/dist/assets/fr-D2nwe_0W.js +14 -0
  32. package/dist/assets/index-BQOoYIOu.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-D0obJ2p8.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
  37. package/dist/assets/ja-CA7vnDsZ.js +16 -0
  38. package/dist/assets/ko-Chy6a9l-.js +16 -0
  39. package/dist/assets/main-B9JFK3vF.js +896 -0
  40. package/dist/assets/main-Bhiqnrhd.js +2 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
  42. package/dist/assets/miniChat-B0UOL5zA.js +2 -0
  43. package/dist/assets/mobile-BJppFIEo.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
  45. package/dist/assets/pl-CTR5A_1P.js +16 -0
  46. package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
  48. package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
  50. package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
  53. package/dist/assets/tr-Bh5vK6cW.js +7 -0
  54. package/dist/assets/uk-CLDkZFy_.js +16 -0
  55. package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
  57. package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.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-63H1h21j.js} +2 -2
  64. package/dist/assets/zh-CN-DODgcmYj.js +16 -0
  65. package/dist/assets/zh-TW-ByNfIwLA.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,313 @@
1
+ /**
2
+ * OpenCode v2 wire events -> the vocabulary the server's own consumers speak.
3
+ *
4
+ * The browser gets raw wire payloads (the UI translates them itself in
5
+ * `packages/ui/src/lib/opencode/events.ts`). Everything that reacts on the
6
+ * server — notifications, the message queue, session activity, goal mode,
7
+ * permission auto-accept, Linear status — was written against OpenCode v1
8
+ * event names, so this module is the single place that maps v2 onto them.
9
+ * Keeping one translator means a consumer never has to know which OpenCode
10
+ * shape produced its event.
11
+ *
12
+ * Two differences from v1 are load-bearing and cannot be papered over:
13
+ *
14
+ * - v2 emits no `session.status` and no `session.idle` of its own. Live status
15
+ * comes from `session.execution.started|succeeded|interrupted|failed`, so
16
+ * those are what synthesize the status vocabulary here.
17
+ * - a v2 turn is a sequence of steps. Each `session.step.ended` becomes an
18
+ * assistant `message.updated`; only the last one carries `finish: "stop"`,
19
+ * which is the same signal v1 gave once per turn.
20
+ */
21
+
22
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
23
+
24
+ const trimmed = (value) => (typeof value === 'string' ? value.trim() : '');
25
+
26
+ const compact = (value) => {
27
+ const result = {};
28
+ for (const [key, entry] of Object.entries(value)) {
29
+ if (entry !== undefined) result[key] = entry;
30
+ }
31
+ return result;
32
+ };
33
+
34
+ /** Directory an event belongs to. Execution events carry no location. */
35
+ export const wireEventDirectory = (payload) => {
36
+ if (!isRecord(payload)) return '';
37
+ return trimmed(payload.location?.directory);
38
+ };
39
+
40
+ /** Structured errors travel as `{ type, message }`; consumers still read `name`. */
41
+ const toServerError = (error) => {
42
+ if (!isRecord(error)) return { name: 'Error', type: 'Error', message: typeof error === 'string' ? error : '' };
43
+ const type = trimmed(error.type) || 'Error';
44
+ return compact({ name: type, type, message: trimmed(error.message), status: error.status });
45
+ };
46
+
47
+ const ABORTED_ERROR = {
48
+ name: 'MessageAbortedError',
49
+ type: 'MessageAbortedError',
50
+ message: 'The running turn was interrupted.',
51
+ };
52
+
53
+ /**
54
+ * Translates one v2 wire payload into zero or more server-vocabulary events.
55
+ *
56
+ * Every returned event carries the source event id and the directory, because
57
+ * consumers deduplicate on the id and route their own OpenCode calls by
58
+ * directory.
59
+ */
60
+ export function translateWireEvent(payload) {
61
+ if (!isRecord(payload)) return [];
62
+ const type = trimmed(payload.type);
63
+ if (!type) return [];
64
+ const data = isRecord(payload.data) ? payload.data : {};
65
+ const directory = wireEventDirectory(payload);
66
+ const created = Number.isFinite(payload.created) ? payload.created : Date.now();
67
+
68
+ const event = (outType, properties) => ({
69
+ type: outType,
70
+ id: typeof payload.id === 'string' ? payload.id : '',
71
+ created,
72
+ properties: compact({ ...properties, directory: directory || undefined }),
73
+ });
74
+
75
+ const sessionID = trimmed(data.sessionID);
76
+
77
+ const status = (statusValue) => event('session.status', { sessionID, status: statusValue });
78
+
79
+ switch (type) {
80
+ case 'server.connected':
81
+ return [event('server.connected', {})];
82
+
83
+ case 'installation.update-available':
84
+ return [event('installation.update-available', { version: trimmed(data.version) })];
85
+
86
+ // --- session lifecycle --------------------------------------------------
87
+
88
+ case 'session.created': {
89
+ if (!sessionID) return [];
90
+ const location = isRecord(data.location) ? data.location : {};
91
+ return [event('session.created', {
92
+ sessionID,
93
+ info: compact({
94
+ id: sessionID,
95
+ parentID: trimmed(data.parentID) || undefined,
96
+ projectID: trimmed(data.projectID) || undefined,
97
+ directory: trimmed(location.directory) || directory || undefined,
98
+ title: typeof data.title === 'string' ? data.title : undefined,
99
+ agent: trimmed(data.agent) || undefined,
100
+ model: data.model,
101
+ metadata: data.metadata,
102
+ permissions: data.permissions,
103
+ time: { created, updated: created },
104
+ }),
105
+ })];
106
+ }
107
+
108
+ case 'session.renamed':
109
+ if (!sessionID) return [];
110
+ return [event('session.updated', {
111
+ sessionID,
112
+ info: compact({ id: sessionID, title: typeof data.title === 'string' ? data.title : undefined, directory: directory || undefined, time: { updated: created } }),
113
+ })];
114
+
115
+ case 'session.moved': {
116
+ if (!sessionID) return [];
117
+ const location = isRecord(data.location) ? data.location : {};
118
+ return [event('session.updated', {
119
+ sessionID,
120
+ info: compact({ id: sessionID, directory: trimmed(location.directory) || undefined, projectID: trimmed(data.projectID) || undefined, time: { updated: created } }),
121
+ })];
122
+ }
123
+
124
+ case 'session.usage.updated':
125
+ if (!sessionID) return [];
126
+ return [event('session.updated', {
127
+ sessionID,
128
+ info: compact({ id: sessionID, directory: directory || undefined, cost: data.cost, tokens: data.tokens, time: { updated: created } }),
129
+ })];
130
+
131
+ case 'session.deleted':
132
+ if (!sessionID) return [];
133
+ return [event('session.deleted', { sessionID, info: { id: sessionID } })];
134
+
135
+ // --- live status --------------------------------------------------------
136
+
137
+ // v2 still declares `session.status` and `session.idle`; a 2.0.2 server was
138
+ // observed never to emit them, so the execution events below are what
139
+ // actually drive status. Both paths are translated so a server that does
140
+ // emit them stays correct.
141
+ case 'session.status':
142
+ if (!sessionID || !isRecord(data.status)) return [];
143
+ return [status(data.status)];
144
+
145
+ case 'session.idle':
146
+ if (!sessionID) return [];
147
+ return [status({ type: 'idle' }), event('session.idle', { sessionID })];
148
+
149
+ case 'session.execution.started':
150
+ if (!sessionID) return [];
151
+ return [status({ type: 'busy' })];
152
+
153
+ case 'session.execution.succeeded':
154
+ if (!sessionID) return [];
155
+ return [status({ type: 'idle' }), event('session.idle', { sessionID })];
156
+
157
+ case 'session.execution.interrupted': {
158
+ if (!sessionID) return [];
159
+ const reason = trimmed(data.reason) || 'user';
160
+ // A shutdown is not the end of the turn: OpenCode keeps the execution
161
+ // claim and the resumed drain continues the same turn after restart
162
+ // (v2.0.14 `session/execution.ts` settled(), `projector.ts` projectIdle,
163
+ // `message-updater.ts`), and none of its own projections mark the
164
+ // session idle. Reporting it as an abort would make the goal runtime
165
+ // pause the goal as a user stop and the queue hold the next prompt, so
166
+ // it produces nothing: the session stays busy until OpenCode resumes it
167
+ // and reports the real terminal outcome.
168
+ if (reason === 'shutdown') return [];
169
+ // Every other interruption ends the turn without failing it: consumers
170
+ // that treat `session.error` as a failure must not fire, but the queue
171
+ // and goal runtimes still have to know the turn was aborted.
172
+ return [
173
+ status({ type: 'idle' }),
174
+ event('session.idle', { sessionID, aborted: true, reason, error: ABORTED_ERROR }),
175
+ ];
176
+ }
177
+
178
+ case 'session.execution.failed':
179
+ if (!sessionID) return [];
180
+ return [status({ type: 'idle' }), event('session.error', { sessionID, error: toServerError(data.error) })];
181
+
182
+ // --- messages -----------------------------------------------------------
183
+
184
+ case 'session.inbox.enqueued': {
185
+ const item = isRecord(data.item) ? data.item : {};
186
+ const messageID = trimmed(data.inboxID);
187
+ if (!sessionID || !messageID) return [];
188
+ const itemPayload = isRecord(item.payload) ? item.payload : {};
189
+ if (item.type === 'user') {
190
+ const text = typeof itemPayload.text === 'string' ? itemPayload.text : '';
191
+ return [event('message.updated', {
192
+ sessionID,
193
+ info: compact({
194
+ id: messageID,
195
+ sessionID,
196
+ role: 'user',
197
+ text,
198
+ parts: text ? [{ type: 'text', text }] : [],
199
+ metadata: itemPayload.metadata,
200
+ time: { created },
201
+ }),
202
+ parts: text ? [{ type: 'text', text }] : [],
203
+ })];
204
+ }
205
+ return [];
206
+ }
207
+
208
+ case 'session.step.started': {
209
+ const messageID = trimmed(data.assistantMessageID);
210
+ if (!sessionID || !messageID) return [];
211
+ const model = isRecord(data.model) ? data.model : {};
212
+ return [event('message.updated', {
213
+ sessionID,
214
+ info: compact({
215
+ id: messageID,
216
+ sessionID,
217
+ role: 'assistant',
218
+ agent: trimmed(data.agent) || undefined,
219
+ providerID: trimmed(model.providerID) || undefined,
220
+ modelID: trimmed(model.id) || undefined,
221
+ time: { created },
222
+ }),
223
+ })];
224
+ }
225
+
226
+ case 'session.step.ended': {
227
+ const messageID = trimmed(data.assistantMessageID);
228
+ if (!sessionID || !messageID) return [];
229
+ return [event('message.updated', {
230
+ sessionID,
231
+ info: compact({
232
+ id: messageID,
233
+ sessionID,
234
+ role: 'assistant',
235
+ finish: trimmed(data.finish) || undefined,
236
+ cost: data.cost,
237
+ tokens: data.tokens,
238
+ time: { completed: created },
239
+ }),
240
+ })];
241
+ }
242
+
243
+ case 'session.step.failed': {
244
+ const messageID = trimmed(data.assistantMessageID);
245
+ if (!sessionID || !messageID) return [];
246
+ return [event('message.updated', {
247
+ sessionID,
248
+ info: compact({
249
+ id: messageID,
250
+ sessionID,
251
+ role: 'assistant',
252
+ finish: trimmed(data.finish) || 'error',
253
+ error: toServerError(data.error),
254
+ cost: data.cost,
255
+ tokens: data.tokens,
256
+ time: { completed: created },
257
+ }),
258
+ })];
259
+ }
260
+
261
+ // --- requests to the user -------------------------------------------------
262
+
263
+ case 'permission.asked': {
264
+ const requestID = trimmed(data.id);
265
+ if (!requestID || !sessionID) return [];
266
+ return [event('permission.asked', compact({
267
+ id: requestID,
268
+ sessionID,
269
+ action: trimmed(data.action),
270
+ resources: Array.isArray(data.resources) ? data.resources : [],
271
+ save: Array.isArray(data.save) ? data.save : undefined,
272
+ metadata: data.metadata,
273
+ source: data.source,
274
+ message: typeof data.message === 'string' ? data.message : undefined,
275
+ }))];
276
+ }
277
+
278
+ case 'permission.replied':
279
+ if (!sessionID) return [];
280
+ return [event('permission.replied', { sessionID, requestID: trimmed(data.requestID) })];
281
+
282
+ case 'form.created': {
283
+ const form = isRecord(data.form) ? data.form : null;
284
+ if (!form || !trimmed(form.id)) return [];
285
+ return [event('form.created', { sessionID: trimmed(form.sessionID), form })];
286
+ }
287
+
288
+ case 'form.replied':
289
+ case 'form.cancelled':
290
+ if (!sessionID) return [];
291
+ return [event('form.settled', { sessionID, formID: trimmed(data.id) })];
292
+
293
+ // --- location-level notices ------------------------------------------------
294
+
295
+ case 'vcs.branch.updated':
296
+ return [event('vcs.branch.updated', { branch: typeof data.branch === 'string' ? data.branch : undefined })];
297
+
298
+ case 'mcp.status.changed':
299
+ return [event('mcp.status.changed', { server: trimmed(data.server) })];
300
+
301
+ default:
302
+ return [];
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Convenience for consumers that hand one payload to one handler: translates
308
+ * and forwards every resulting event in order.
309
+ */
310
+ export function forwardTranslatedWireEvent(payload, handle) {
311
+ if (typeof handle !== 'function') return;
312
+ for (const translated of translateWireEvent(payload)) handle(translated);
313
+ }
@@ -0,0 +1,209 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { forwardTranslatedWireEvent, translateWireEvent, wireEventDirectory } from './translate-v2.js';
3
+
4
+ const wire = (type, data, extra = {}) => ({
5
+ id: 'evt_1',
6
+ created: 1000,
7
+ type,
8
+ data,
9
+ location: { directory: '/repo' },
10
+ ...extra,
11
+ });
12
+
13
+ describe('translateWireEvent', () => {
14
+ test('ignores non-objects and unknown types', () => {
15
+ expect(translateWireEvent(null)).toEqual([]);
16
+ expect(translateWireEvent('nope')).toEqual([]);
17
+ expect(translateWireEvent(wire('session.tool.progress', { sessionID: 's1' }))).toEqual([]);
18
+ });
19
+
20
+ test('tags every event with its source id and directory', () => {
21
+ const [event] = translateWireEvent(wire('session.idle', { sessionID: 's1' }));
22
+ expect(event.id).toBe('evt_1');
23
+ expect(event.created).toBe(1000);
24
+ expect(event.properties.directory).toBe('/repo');
25
+ });
26
+
27
+ test('server.connected and update availability pass through', () => {
28
+ expect(translateWireEvent(wire('server.connected', {}))[0].type).toBe('server.connected');
29
+ const [update] = translateWireEvent(wire('installation.update-available', { version: '2.1.0' }));
30
+ expect(update).toMatchObject({ type: 'installation.update-available', properties: { version: '2.1.0' } });
31
+ });
32
+
33
+ test('session.created carries the directory from its location', () => {
34
+ const [event] = translateWireEvent(wire('session.created', {
35
+ sessionID: 's1',
36
+ projectID: 'p1',
37
+ parentID: 's0',
38
+ title: 'Fix login',
39
+ agent: 'build',
40
+ location: { directory: '/other' },
41
+ }));
42
+ expect(event.type).toBe('session.created');
43
+ expect(event.properties.info).toMatchObject({
44
+ id: 's1',
45
+ parentID: 's0',
46
+ projectID: 'p1',
47
+ directory: '/other',
48
+ title: 'Fix login',
49
+ agent: 'build',
50
+ });
51
+ });
52
+
53
+ test('rename, move and usage all read as session.updated', () => {
54
+ expect(translateWireEvent(wire('session.renamed', { sessionID: 's1', title: 'New' }))[0])
55
+ .toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', title: 'New' } } });
56
+ expect(translateWireEvent(wire('session.moved', { sessionID: 's1', projectID: 'p2', location: { directory: '/moved' } }))[0])
57
+ .toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', directory: '/moved', projectID: 'p2' } } });
58
+ expect(translateWireEvent(wire('session.usage.updated', { sessionID: 's1', cost: 0.5, tokens: { input: 10 } }))[0])
59
+ .toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', cost: 0.5 } } });
60
+ });
61
+
62
+ test('session.deleted keeps both the id and the legacy info shape', () => {
63
+ const [event] = translateWireEvent(wire('session.deleted', { sessionID: 's1' }));
64
+ expect(event).toMatchObject({ type: 'session.deleted', properties: { sessionID: 's1', info: { id: 's1' } } });
65
+ });
66
+
67
+ test('execution events synthesize the status vocabulary v2 no longer emits', () => {
68
+ expect(translateWireEvent(wire('session.execution.started', { sessionID: 's1' })))
69
+ .toMatchObject([{ type: 'session.status', properties: { sessionID: 's1', status: { type: 'busy' } } }]);
70
+
71
+ const succeeded = translateWireEvent(wire('session.execution.succeeded', { sessionID: 's1' }));
72
+ expect(succeeded.map((entry) => entry.type)).toEqual(['session.status', 'session.idle']);
73
+ expect(succeeded[0].properties.status).toEqual({ type: 'idle' });
74
+ });
75
+
76
+ test('an interruption ends the turn without reporting a failure', () => {
77
+ const events = translateWireEvent(wire('session.execution.interrupted', { sessionID: 's1', reason: 'user' }));
78
+ expect(events.map((entry) => entry.type)).toEqual(['session.status', 'session.idle']);
79
+ expect(events[1].properties).toMatchObject({ aborted: true, reason: 'user' });
80
+ expect(events[1].properties.error.name).toBe('MessageAbortedError');
81
+ expect(events.some((entry) => entry.type === 'session.error')).toBe(false);
82
+ });
83
+
84
+ test('a shutdown interruption is not an abort: the turn resumes after restart', () => {
85
+ // OpenCode keeps the execution claim across a shutdown and continues the
86
+ // turn on restart, so nothing here may read as the user pressing Stop.
87
+ expect(translateWireEvent(wire('session.execution.interrupted', { sessionID: 's1', reason: 'shutdown' }))).toEqual([]);
88
+ });
89
+
90
+ test('a failed execution reports a structured error', () => {
91
+ const events = translateWireEvent(wire('session.execution.failed', {
92
+ sessionID: 's1',
93
+ error: { type: 'ProviderAuthError', message: 'bad key', status: 401 },
94
+ }));
95
+ expect(events.map((entry) => entry.type)).toEqual(['session.status', 'session.error']);
96
+ expect(events[1].properties.error).toEqual({
97
+ name: 'ProviderAuthError',
98
+ type: 'ProviderAuthError',
99
+ message: 'bad key',
100
+ status: 401,
101
+ });
102
+ });
103
+
104
+ test('a passed-through session.status is kept as is', () => {
105
+ const [event] = translateWireEvent(wire('session.status', { sessionID: 's1', status: { type: 'retry', attempt: 2 } }));
106
+ expect(event.properties.status).toEqual({ type: 'retry', attempt: 2 });
107
+ });
108
+
109
+ test('an enqueued user prompt becomes a user message with text parts', () => {
110
+ const [event] = translateWireEvent(wire('session.inbox.enqueued', {
111
+ sessionID: 's1',
112
+ inboxID: 'msg_1',
113
+ item: { type: 'user', payload: { text: 'hello' } },
114
+ }));
115
+ expect(event.type).toBe('message.updated');
116
+ expect(event.properties.info).toMatchObject({ id: 'msg_1', sessionID: 's1', role: 'user', text: 'hello' });
117
+ expect(event.properties.info.parts).toEqual([{ type: 'text', text: 'hello' }]);
118
+ });
119
+
120
+ test('non-user inbox items are not messages', () => {
121
+ expect(translateWireEvent(wire('session.inbox.enqueued', {
122
+ sessionID: 's1',
123
+ inboxID: 'msg_1',
124
+ item: { type: 'compaction', payload: {} },
125
+ }))).toEqual([]);
126
+ });
127
+
128
+ test('steps become assistant message updates', () => {
129
+ const [started] = translateWireEvent(wire('session.step.started', {
130
+ sessionID: 's1',
131
+ assistantMessageID: 'msg_2',
132
+ agent: 'build',
133
+ model: { id: 'gpt-5.6-luna', providerID: 'openai' },
134
+ }));
135
+ expect(started.properties.info).toMatchObject({
136
+ id: 'msg_2', role: 'assistant', agent: 'build', providerID: 'openai', modelID: 'gpt-5.6-luna',
137
+ });
138
+
139
+ const [ended] = translateWireEvent(wire('session.step.ended', {
140
+ sessionID: 's1', assistantMessageID: 'msg_2', finish: 'stop', cost: 1, tokens: { input: 2 },
141
+ }));
142
+ expect(ended.properties.info).toMatchObject({ id: 'msg_2', role: 'assistant', finish: 'stop' });
143
+ expect(ended.properties.info.time.completed).toBe(1000);
144
+
145
+ const [failed] = translateWireEvent(wire('session.step.failed', {
146
+ sessionID: 's1', assistantMessageID: 'msg_2', error: { type: 'Overloaded', message: 'busy' },
147
+ }));
148
+ expect(failed.properties.info).toMatchObject({ finish: 'error' });
149
+ expect(failed.properties.info.error.name).toBe('Overloaded');
150
+ });
151
+
152
+ test('permission requests keep the v2 action and resources', () => {
153
+ const [event] = translateWireEvent(wire('permission.asked', {
154
+ id: 'per_1',
155
+ sessionID: 's1',
156
+ action: 'bash',
157
+ resources: ['rm -rf /'],
158
+ source: { type: 'tool', messageID: 'msg_1', id: 'call_1' },
159
+ }));
160
+ expect(event.properties).toMatchObject({
161
+ id: 'per_1', sessionID: 's1', action: 'bash', resources: ['rm -rf /'],
162
+ });
163
+
164
+ const [replied] = translateWireEvent(wire('permission.replied', { sessionID: 's1', requestID: 'per_1', reply: 'once' }));
165
+ expect(replied.properties).toMatchObject({ sessionID: 's1', requestID: 'per_1' });
166
+ });
167
+
168
+ test('forms replace questions', () => {
169
+ const form = { id: 'frm_1', sessionID: 's1', title: 'Switch to plan mode?', fields: [] };
170
+ const [created] = translateWireEvent(wire('form.created', { form }));
171
+ expect(created).toMatchObject({ type: 'form.created', properties: { sessionID: 's1', form } });
172
+
173
+ expect(translateWireEvent(wire('form.replied', { id: 'frm_1', sessionID: 's1' }))[0])
174
+ .toMatchObject({ type: 'form.settled', properties: { sessionID: 's1', formID: 'frm_1' } });
175
+ expect(translateWireEvent(wire('form.cancelled', { id: 'frm_1', sessionID: 's1' }))[0])
176
+ .toMatchObject({ type: 'form.settled', properties: { sessionID: 's1', formID: 'frm_1' } });
177
+ });
178
+
179
+ test('branch and mcp notices pass through', () => {
180
+ expect(translateWireEvent(wire('vcs.branch.updated', { branch: 'main' }))[0].properties.branch).toBe('main');
181
+ expect(translateWireEvent(wire('mcp.status.changed', { server: 'linear' }))[0].properties.server).toBe('linear');
182
+ });
183
+
184
+ test('events without a session id are dropped rather than half-translated', () => {
185
+ expect(translateWireEvent(wire('session.execution.started', {}))).toEqual([]);
186
+ expect(translateWireEvent(wire('session.step.ended', { sessionID: 's1' }))).toEqual([]);
187
+ expect(translateWireEvent(wire('permission.asked', { sessionID: 's1' }))).toEqual([]);
188
+ });
189
+ });
190
+
191
+ describe('wireEventDirectory', () => {
192
+ test('reads location.directory and tolerates its absence', () => {
193
+ expect(wireEventDirectory(wire('session.idle', { sessionID: 's1' }))).toBe('/repo');
194
+ expect(wireEventDirectory({ type: 'session.execution.started', data: {} })).toBe('');
195
+ expect(wireEventDirectory(null)).toBe('');
196
+ });
197
+ });
198
+
199
+ describe('forwardTranslatedWireEvent', () => {
200
+ test('forwards every translated event in order', () => {
201
+ const seen = [];
202
+ forwardTranslatedWireEvent(wire('session.execution.succeeded', { sessionID: 's1' }), (event) => seen.push(event.type));
203
+ expect(seen).toEqual(['session.status', 'session.idle']);
204
+ });
205
+
206
+ test('does nothing without a handler', () => {
207
+ expect(() => forwardTranslatedWireEvent(wire('session.idle', { sessionID: 's1' }), null)).not.toThrow();
208
+ });
209
+ });
@@ -65,12 +65,12 @@ describe('createUpstreamSseReader', () => {
65
65
  let reader;
66
66
 
67
67
  reader = createUpstreamSseReader({
68
- buildUrl: () => 'http://127.0.0.1:4096/global/event',
68
+ buildUrl: () => 'http://127.0.0.1:4096/api/event',
69
69
  reconnectDelayMs: 0,
70
70
  fetchImpl: async (_url, options) => createSseResponse({
71
71
  signal: options.signal,
72
72
  blocks: [
73
- 'id: evt-1\r\ndata: {"type":"server.connected","properties":{"directory":"/tmp/project"}}\r\n\r\n',
73
+ 'data: {"id":"evt-1","type":"session.renamed","location":{"directory":"/tmp/project"},"data":{"sessionID":"s1","title":"t"}}\r\n\r\n',
74
74
  ],
75
75
  }),
76
76
  onEvent(event) {
@@ -85,10 +85,10 @@ describe('createUpstreamSseReader', () => {
85
85
  expect(events[0].eventId).toBe('evt-1');
86
86
  expect(events[0].directory).toBe('/tmp/project');
87
87
  expect(events[0].payload).toEqual({
88
- type: 'server.connected',
89
- properties: {
90
- directory: '/tmp/project',
91
- },
88
+ id: 'evt-1',
89
+ type: 'session.renamed',
90
+ location: { directory: '/tmp/project' },
91
+ data: { sessionID: 's1', title: 't' },
92
92
  });
93
93
  expect(reader.getLastEventId()).toBe('evt-1');
94
94
  });
@@ -100,7 +100,7 @@ describe('createUpstreamSseReader', () => {
100
100
  let reader;
101
101
 
102
102
  reader = createUpstreamSseReader({
103
- buildUrl: () => 'http://127.0.0.1:4096/global/event',
103
+ buildUrl: () => 'http://127.0.0.1:4096/api/event',
104
104
  stallTimeoutMs: 10,
105
105
  reconnectDelayMs: 0,
106
106
  fetchImpl: async (_url, options) => {
@@ -112,7 +112,7 @@ describe('createUpstreamSseReader', () => {
112
112
  signal: options.signal,
113
113
  holdOpen: true,
114
114
  blocks: [
115
- 'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
115
+ 'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
116
116
  ],
117
117
  });
118
118
  }
@@ -120,7 +120,7 @@ describe('createUpstreamSseReader', () => {
120
120
  return createSseResponse({
121
121
  signal: options.signal,
122
122
  blocks: [
123
- 'id: evt-2\ndata: {"type":"session.updated","properties":{}}\n\n',
123
+ 'data: {"id":"evt-2","type":"session.updated","properties":{}}\n\n',
124
124
  ],
125
125
  });
126
126
  },
@@ -146,7 +146,7 @@ describe('createUpstreamSseReader', () => {
146
146
  let reader;
147
147
 
148
148
  reader = createUpstreamSseReader({
149
- buildUrl: () => 'http://127.0.0.1:4096/global/event',
149
+ buildUrl: () => 'http://127.0.0.1:4096/api/event',
150
150
  stallTimeoutMs: () => currentTimeout,
151
151
  reconnectDelayMs: 0,
152
152
  fetchImpl: async (_url, options) => {
@@ -158,7 +158,7 @@ describe('createUpstreamSseReader', () => {
158
158
  signal: options.signal,
159
159
  holdOpen: true,
160
160
  blocks: [
161
- 'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
161
+ 'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
162
162
  ],
163
163
  });
164
164
  }
@@ -166,7 +166,7 @@ describe('createUpstreamSseReader', () => {
166
166
  return createSseResponse({
167
167
  signal: options.signal,
168
168
  blocks: [
169
- 'id: evt-2\ndata: {"type":"session.updated","properties":{}}\n\n',
169
+ 'data: {"id":"evt-2","type":"session.updated","properties":{}}\n\n',
170
170
  ],
171
171
  });
172
172
  },
@@ -191,7 +191,7 @@ describe('createUpstreamSseReader', () => {
191
191
  let reader;
192
192
 
193
193
  reader = createUpstreamSseReader({
194
- buildUrl: () => 'http://127.0.0.1:4096/global/event',
194
+ buildUrl: () => 'http://127.0.0.1:4096/api/event',
195
195
  reconnectDelayMs: 0,
196
196
  fetchImpl: async (_url, options) => {
197
197
  attempt += 1;
@@ -210,7 +210,7 @@ describe('createUpstreamSseReader', () => {
210
210
  return createSseResponse({
211
211
  signal: options.signal,
212
212
  blocks: [
213
- 'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
213
+ 'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
214
214
  ],
215
215
  });
216
216
  },
@@ -240,7 +240,7 @@ describe('createUpstreamSseReader', () => {
240
240
  let reader;
241
241
 
242
242
  reader = createUpstreamSseReader({
243
- buildUrl: () => 'http://127.0.0.1:4096/global/event',
243
+ buildUrl: () => 'http://127.0.0.1:4096/api/event',
244
244
  reconnectDelayMs: 1,
245
245
  signal: tracked.signal,
246
246
  fetchImpl: async (_url, options) => {
@@ -258,7 +258,7 @@ describe('createUpstreamSseReader', () => {
258
258
  return createSseResponse({
259
259
  signal: options.signal,
260
260
  blocks: [
261
- 'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
261
+ 'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
262
262
  ],
263
263
  });
264
264
  },
@@ -53,9 +53,11 @@ twelve children.
53
53
  - Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
54
54
  - `GET /api/fs/home` preserves `{ home, chatsRoot }` and adds `canonicalChatsRoot` and `canonicalLegacyChatsRoot`, resolved by the server's filesystem. If a root does not exist yet, it resolves the nearest existing ancestor and appends the missing segments, without creating directories. Errors resolving the configured root fail the request. A failed legacy lookup warns and omits only that optional alias, retaining exact legacy matching without blocking an accessible relocated root. Clients use confirmed aliases for exact membership while keeping the original roots as folder/scope identities. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
55
55
  - Workspace authorization accepts both the configured managed paths and their filesystem-confirmed canonical roots. Canonical lookup runs only when lexical workspace/managed-root checks fail. One failed managed-root lookup cannot reject an independently authorized root or worktree. Path comparisons remain case-sensitive on POSIX: distinct Linux directories named `chats` and `Chats` never become aliases through string folding. Shared UI keeps exact root membership for classification and directory cleanup.
56
- - Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
56
+ - Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Workspace-boundary policy denials must not use that marker because a native folder picker cannot remediate them.
57
57
  - `GET /api/fs/directory-stat?path=...` uses one `stat` without listing contents or resolving project topology. It follows the same authenticated directory-discovery path policy as `/api/fs/list`, including targets outside the active workspace. A directory returns `{ isDirectory: true }`; `ENOENT` returns `not-found`, and a file or `ENOTDIR` returns `not-directory`. Permission and other failures remain distinct from a missing path. VS Code explicitly returns 501, so the shared client treats its probe as unknown.
58
- - Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
58
+ - A page shown through `GET /api/fs/serve/…` loads its relative images, styles and scripts without a token; `ui-auth.js` accepts the page's own `oc_url_token` from the `Referer` for a subresource, but only when both URLs sit under `/api/fs/serve/`. Without this, HTML previews on desktop (no session cookie on the loopback API origin) show broken images.
59
+ - `GET /api/fs/raw` answers one `Range: bytes=…` span with `206`, `Content-Range` and a stream from disk (`byte-range.js` reduces the header to a span). Media elements send a span on every seek, and Chromium and WebKit refuse to seek without a `206`, so the viewer's audio and video players depend on this; a whole-file request still reads and sends the buffer. The MIME table (`FILE_MIME_MAP`) is shared with `/api/fs/serve` and covers images, PDF, audio, video, fonts, CSV/TSV and Mermaid.
60
+ - File read, raw and stat routes accept outside paths with `allowOutsideWorkspace=true` under the normal server authentication and OS permissions. They do not require a separate file grant. Legacy grant parameters are ignored. Workspace-scoped reads resolve symlinks after checking the requested path. Write routes keep canonical-target boundary checks.
59
61
  - If adding new `/api/fs/*` endpoints, add them in `routes.js` and extend this document.
60
62
  - `GET /api/fs/list` may resolve symlinks with `realpath` to read directory contents, but the response `path` and each entry `path` must stay in the caller's requested path space (`path.join(requestedPath, name)`). Returning real paths breaks file-tree expansion for directories reached through workspace symlinks.
61
63
  - `POST /api/fs/upload` accepts one `application/octet-stream` body with `path` and optional `overwrite=true` query parameters. The body streams into a same-directory temp file with a 100 MiB default cap configurable through `OPENCHAMBER_FS_UPLOAD_MAX_BYTES`; failed and oversized uploads clean up that temp file. New files commit through an atomic no-replace link, existing files return `409` unless overwrite is explicit, directory targets are rejected, and the destination parent resolves before writing so uploads cannot escape through workspace symlinks.