@vantran-se/openchamber-web 1.24.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The agent asking the user to look at a file.
3
+ *
4
+ * The viewer lives in a renderer, so the server cannot open anything itself;
5
+ * it checks that the file is really there and tells every connected client to
6
+ * show it. No answer comes back: opening a tab is not something that fails
7
+ * quietly on the client, and the agent only needs to know whether anyone was
8
+ * there to see it.
9
+ */
10
+ import path from 'node:path';
11
+ import fsPromises from 'node:fs/promises';
12
+
13
+ import { OpenChamberControlError } from './error.js';
14
+
15
+ /**
16
+ * @param {{
17
+ * emit: (request: { path: string, directory: string | null, sessionId: string | null }) => number,
18
+ * stat?: (target: string) => Promise<{ isFile(): boolean, size: number }>,
19
+ * }} dependencies
20
+ */
21
+ export const createFileOpenRequester = ({ emit, stat = (target) => fsPromises.stat(target) }) => {
22
+ /** `path` arrives already reduced to a trimmed string or null by the service. */
23
+ const request = async ({ path: raw, directory, sessionId }) => {
24
+ if (!raw) throw new OpenChamberControlError('path is required for file.open', 400);
25
+
26
+ // A relative path means "in the project I am working in"; without a
27
+ // project there is nothing to resolve it against.
28
+ let absolute;
29
+ if (path.isAbsolute(raw)) {
30
+ absolute = path.normalize(raw);
31
+ } else if (directory) {
32
+ absolute = path.resolve(directory, raw);
33
+ } else {
34
+ throw new OpenChamberControlError('path must be absolute when no session directory is known', 400);
35
+ }
36
+
37
+ let stats;
38
+ try {
39
+ stats = await stat(absolute);
40
+ } catch (error) {
41
+ if (error?.code === 'ENOENT') {
42
+ throw new OpenChamberControlError(`File not found: ${absolute}`, 404);
43
+ }
44
+ throw new OpenChamberControlError(`Cannot read ${absolute}: ${error instanceof Error ? error.message : String(error)}`, 500);
45
+ }
46
+ if (!stats.isFile()) {
47
+ throw new OpenChamberControlError(`Not a file: ${absolute}`, 400);
48
+ }
49
+
50
+ const delivered = emit({ path: absolute, directory: directory ?? null, sessionId: sessionId ?? null });
51
+ if (delivered === 0) {
52
+ throw new OpenChamberControlError('No OpenChamber window is connected to show the file', 503);
53
+ }
54
+
55
+ return { path: absolute, size: stats.size, opened: true };
56
+ };
57
+
58
+ return { request };
59
+ };
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createFileOpenRequester } from './file-open.js';
4
+
5
+ const file = { isFile: () => true, size: 42 };
6
+
7
+ describe('createFileOpenRequester', () => {
8
+ it('resolves a relative path against the session directory and tells the clients', async () => {
9
+ const emit = vi.fn(() => 2);
10
+ const stat = vi.fn(async () => file);
11
+ const requester = createFileOpenRequester({ emit, stat });
12
+
13
+ const result = await requester.request({ path: 'out/report.csv', directory: '/repo', sessionId: 'ses_1' });
14
+
15
+ expect(stat).toHaveBeenCalledWith('/repo/out/report.csv');
16
+ expect(emit).toHaveBeenCalledWith({ path: '/repo/out/report.csv', directory: '/repo', sessionId: 'ses_1' });
17
+ expect(result).toEqual({ path: '/repo/out/report.csv', size: 42, opened: true });
18
+ });
19
+
20
+ it('keeps an absolute path outside the project, which is where screenshots and recordings often land', async () => {
21
+ const emit = vi.fn(() => 1);
22
+ const requester = createFileOpenRequester({ emit, stat: async () => file });
23
+
24
+ const result = await requester.request({ path: '/tmp/shot.png', directory: '/repo', sessionId: null });
25
+
26
+ expect(result.path).toBe('/tmp/shot.png');
27
+ expect(emit).toHaveBeenCalledWith({ path: '/tmp/shot.png', directory: '/repo', sessionId: null });
28
+ });
29
+
30
+ it('refuses a relative path when no directory is known, instead of guessing one', async () => {
31
+ const requester = createFileOpenRequester({ emit: () => 1, stat: async () => file });
32
+ await expect(requester.request({ path: 'report.csv', directory: null, sessionId: null }))
33
+ .rejects.toMatchObject({ statusCode: 400 });
34
+ });
35
+
36
+ it('reports a missing file and a directory before asking anyone to open them', async () => {
37
+ const emit = vi.fn(() => 1);
38
+ const missing = createFileOpenRequester({
39
+ emit,
40
+ stat: async () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); },
41
+ });
42
+ await expect(missing.request({ path: '/repo/gone.png', directory: '/repo' }))
43
+ .rejects.toMatchObject({ statusCode: 404 });
44
+
45
+ const folder = createFileOpenRequester({ emit, stat: async () => ({ isFile: () => false, size: 0 }) });
46
+ await expect(folder.request({ path: '/repo/src', directory: '/repo' }))
47
+ .rejects.toMatchObject({ statusCode: 400 });
48
+
49
+ expect(emit).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it('fails with 503 when no client is connected rather than claiming the file was shown', async () => {
53
+ const requester = createFileOpenRequester({ emit: () => 0, stat: async () => file });
54
+ await expect(requester.request({ path: '/repo/a.png', directory: '/repo' }))
55
+ .rejects.toMatchObject({ statusCode: 503 });
56
+ });
57
+ });
@@ -2,6 +2,7 @@ import express from 'express';
2
2
  import request from 'supertest';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
 
5
+ import { BrowserControlError } from '../browser-control/broker.js';
5
6
  import { OpenChamberControlError } from './error.js';
6
7
  import { registerOpenChamberControlRoutes } from './routes.js';
7
8
 
@@ -43,4 +44,15 @@ describe('OpenChamber control route', () => {
43
44
  directory: '/repo',
44
45
  });
45
46
  });
47
+
48
+ it('keeps the status of a browser refusal instead of reporting 500', async () => {
49
+ const execute = vi.fn(async () => {
50
+ throw new BrowserControlError('The user is interacting with this page in the panel right now.', 409);
51
+ });
52
+ const response = await request(createApp(execute))
53
+ .post('/api/openchamber/control')
54
+ .send({ action: 'browser.click', input: {} })
55
+ .expect(409);
56
+ expect(response.body).toEqual({ error: 'The user is interacting with this page in the panel right now.' });
57
+ });
46
58
  });
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
+ import { OpenCode } from '@opencode/client';
3
3
  import { OpenChamberControlError, asControlError } from './error.js';
4
4
  import { OPENCHAMBER_ALL_ACTIONS } from './actions.js';
5
5
  import { writeScreenshot } from './screenshots.js';
@@ -38,23 +38,32 @@ const normalizeWaitTimeoutMs = (value) => {
38
38
  return seconds * 1000;
39
39
  };
40
40
 
41
+ // v2 messages are not { info, parts }: the record itself is the message, a
42
+ // user message carries `text`, and an assistant message carries `content[]`
43
+ // whose text items are the visible answer.
44
+ const messageText = (record) => {
45
+ if (record.type === 'user') return typeof record.text === 'string' ? record.text.trim() : '';
46
+ return (Array.isArray(record.content) ? record.content : [])
47
+ .filter((item) => item?.type === 'text' && typeof item.text === 'string')
48
+ .map((item) => item.text)
49
+ .join('')
50
+ .trim();
51
+ };
52
+
41
53
  const extractTextMessages = (messages, role = 'all') => {
42
54
  const result = [];
43
55
  for (const record of Array.isArray(messages) ? messages : []) {
44
- const info = record?.info;
45
- const messageRole = info?.role;
56
+ const messageRole = record?.type;
46
57
  if ((messageRole !== 'user' && messageRole !== 'assistant') || (role !== 'all' && role !== messageRole)) continue;
47
- const text = Array.isArray(record?.parts)
48
- ? record.parts.filter((part) => part?.type === 'text' && typeof part.text === 'string').map((part) => part.text).join('').trim()
49
- : '';
58
+ const text = messageText(record);
50
59
  if (!text) continue;
51
- const providerID = asNonEmptyString(info.providerID);
52
- const modelID = asNonEmptyString(info.modelID);
60
+ const providerID = asNonEmptyString(record.model?.providerID);
61
+ const modelID = asNonEmptyString(record.model?.id);
53
62
  result.push({
54
- id: asNonEmptyString(info.id) || '',
63
+ id: asNonEmptyString(record.id) || '',
55
64
  role: messageRole,
56
- createdAt: Number.isFinite(info?.time?.created) ? info.time.created : null,
57
- completedAt: Number.isFinite(info?.time?.completed) ? info.time.completed : null,
65
+ createdAt: Number.isFinite(record?.time?.created) ? record.time.created : null,
66
+ completedAt: Number.isFinite(record?.time?.completed) ? record.time.completed : null,
58
67
  model: providerID && modelID ? `${providerID}/${modelID}` : null,
59
68
  text,
60
69
  });
@@ -144,8 +153,12 @@ export const createOpenChamberControlService = (dependencies) => {
144
153
  sessionService,
145
154
  scheduledTaskService,
146
155
  browserControl = null,
156
+ fileOpen = null,
147
157
  agentMemoryActions = null,
148
- createClient = createOpencodeClient,
158
+ // Archive lives in OpenChamber's own store now — v2 has no route that sets
159
+ // Session.time.archived — so an unwired store simply means nothing is archived.
160
+ archiveStore = null,
161
+ createClient = OpenCode.make,
149
162
  sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration)),
150
163
  now = Date.now,
151
164
  } = dependencies;
@@ -169,14 +182,25 @@ export const createOpenChamberControlService = (dependencies) => {
169
182
  });
170
183
  };
171
184
 
172
- const getClient = async () => {
185
+ // Every v2 route lives under /api and the client appends it, so it only
186
+ // wants the origin. Per-directory scoping is a request header.
187
+ const getClient = async (directory = '') => {
173
188
  if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
174
189
  return createClient({
175
- baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
176
- headers: getOpenCodeAuthHeaders(),
190
+ baseUrl: new URL(buildOpenCodeUrl('/api/info', '')).origin,
191
+ headers: {
192
+ ...getOpenCodeAuthHeaders(),
193
+ ...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
194
+ },
195
+ fetch,
177
196
  });
178
197
  };
179
198
 
199
+ const archivedAt = (sessionID) => {
200
+ if (!archiveStore || typeof archiveStore.isArchived !== 'function') return null;
201
+ return archiveStore.isArchived(sessionID) || null;
202
+ };
203
+
180
204
  const projects = async () => {
181
205
  const settings = await readSettingsFromDiskMigrated();
182
206
  return sanitizeProjects(settings?.projects || []).map((project) => ({
@@ -197,22 +221,32 @@ export const createOpenChamberControlService = (dependencies) => {
197
221
  };
198
222
  };
199
223
 
200
- const sessionStatus = async (client, sessionID, directory) => {
201
- const response = await client.session.status({ directory });
202
- const statuses = response?.data;
224
+ // /api/session/active is global in v2 and reports only `{ type: 'running' }`.
225
+ // The action contract stays busy/idle, so running is reported as busy.
226
+ const activeSessions = async (client) => {
227
+ const statuses = await client.session.active();
203
228
  if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
204
229
  throw new OpenChamberControlError('Invalid session status response', 500);
205
230
  }
206
- return statuses[sessionID] || { type: 'idle' };
231
+ return statuses;
207
232
  };
208
233
 
209
- const sessionMessages = async (client, sessionID, directory, role, limit) => {
234
+ const asPublicStatus = (active) => (active ? { type: 'busy' } : { type: 'idle' });
235
+
236
+ const sessionStatus = async (client, sessionID) => {
237
+ const statuses = await activeSessions(client);
238
+ return asPublicStatus(statuses[sessionID]);
239
+ };
240
+
241
+ // `order: 'desc'` puts the newest messages in the limited page;
242
+ // extractTextMessages re-sorts them oldest-first for the caller.
243
+ const sessionMessages = async (client, sessionID, role, limit) => {
210
244
  const fetchLimit = limit === undefined ? undefined : Math.max(100, limit * 4);
211
- let response = await client.session.messages({ sessionID, directory, ...(fetchLimit ? { limit: fetchLimit } : {}) });
245
+ let response = await client.message.list({ sessionID, ...(fetchLimit ? { limit: fetchLimit, order: 'desc' } : {}) });
212
246
  let raw = Array.isArray(response?.data) ? response.data : [];
213
247
  let messages = extractTextMessages(raw, role);
214
248
  if (limit !== undefined && messages.length < limit && raw.length >= fetchLimit) {
215
- response = await client.session.messages({ sessionID, directory });
249
+ response = await client.message.list({ sessionID });
216
250
  raw = Array.isArray(response?.data) ? response.data : [];
217
251
  messages = extractTextMessages(raw, role);
218
252
  }
@@ -224,13 +258,13 @@ export const createOpenChamberControlService = (dependencies) => {
224
258
  let observedActivity = false;
225
259
  while (true) {
226
260
  if (signal?.aborted) throw new OpenChamberControlError('OpenChamber action was cancelled', 499);
227
- const status = await sessionStatus(client, sessionID, directory);
228
- if (status.type === 'busy' || status.type === 'retry') {
261
+ const status = await sessionStatus(client, sessionID);
262
+ if (status.type === 'busy') {
229
263
  observedActivity = true;
230
264
  } else if (!requireActivity || observedActivity) {
231
265
  return status;
232
266
  } else {
233
- const messages = await sessionMessages(client, sessionID, directory, 'assistant', 1);
267
+ const messages = await sessionMessages(client, sessionID, 'assistant', 1);
234
268
  const message = messages[0];
235
269
  if (message?.completedAt && (baselineMessageID ? message.id !== baselineMessageID : message.completedAt >= startedAt)) {
236
270
  return status;
@@ -245,17 +279,17 @@ export const createOpenChamberControlService = (dependencies) => {
245
279
  };
246
280
 
247
281
  // session.send/fork default the directory to the caller's context directory,
248
- // which is wrong for sessions living in other worktrees: prompt_async then
282
+ // which is wrong for sessions living in other worktrees: the prompt then
249
283
  // targets an instance that does not hold the session and the run dies with
250
284
  // UnknownError. Resolve the target session's directory from the global
251
285
  // session list when the caller did not scope explicitly.
252
286
  const resolveSessionDirectory = async (sessionID) => {
253
287
  try {
254
288
  const client = await getClient();
255
- const response = await client.experimental?.session?.list?.({});
289
+ const response = await client.session.list({});
256
290
  const sessions = Array.isArray(response?.data) ? response.data : [];
257
291
  const session = sessions.find((item) => item?.id === sessionID);
258
- return asNonEmptyString(session?.directory) || null;
292
+ return asNonEmptyString(session?.location?.directory) || null;
259
293
  } catch {
260
294
  return null;
261
295
  }
@@ -305,7 +339,7 @@ export const createOpenChamberControlService = (dependencies) => {
305
339
  delete publicResult.baselineAssistantMessageId;
306
340
  return publicResult;
307
341
  }
308
- const client = await getClient();
342
+ const client = await getClient(result.directory);
309
343
  const status = await waitForIdle({
310
344
  client,
311
345
  sessionID: result.sessionId,
@@ -319,7 +353,7 @@ export const createOpenChamberControlService = (dependencies) => {
319
353
  const publicResult = { ...result, sessionStatus: status };
320
354
  delete publicResult.baselineAssistantMessageId;
321
355
  if (input.lastAssistant === true) {
322
- publicResult.lastAssistantMessage = (await sessionMessages(client, result.sessionId, result.directory, 'assistant', 1))[0] || null;
356
+ publicResult.lastAssistantMessage = (await sessionMessages(client, result.sessionId, 'assistant', 1))[0] || null;
323
357
  }
324
358
  return publicResult;
325
359
  };
@@ -329,7 +363,7 @@ export const createOpenChamberControlService = (dependencies) => {
329
363
  * should come back as a usage error the agent can correct, without waking a
330
364
  * client or waiting for a round trip.
331
365
  */
332
- const browserAction = async (action, input, signal, contextDirectory) => {
366
+ const browserAction = async (action, input, signal, contextDirectory, contextSessionId) => {
333
367
  const parameters = {};
334
368
 
335
369
  const readViewport = (required) => {
@@ -417,7 +451,13 @@ export const createOpenChamberControlService = (dependencies) => {
417
451
  // exceed the client's own wait; sharing one timeout with the quick actions
418
452
  // made a slow page indistinguishable from an unreachable browser.
419
453
  const timeoutMs = action === 'browser.open' ? 45_000 : 20_000;
420
- const result = await browserControl.request(action, parameters, { signal, timeoutMs });
454
+ // Where the call came from, for a provider that keeps one browser per
455
+ // project or chat. Filled by the tool plugin, never by the model.
456
+ const context = {
457
+ directory: asNonEmptyString(contextDirectory),
458
+ sessionId: asNonEmptyString(contextSessionId),
459
+ };
460
+ const result = await browserControl.request(action, parameters, { signal, timeoutMs, context });
421
461
 
422
462
  // The image is written here rather than in the renderer: the file belongs
423
463
  // beside the code it documents, and the client that took it may be on a
@@ -469,7 +509,17 @@ export const createOpenChamberControlService = (dependencies) => {
469
509
  if (!browserControl) {
470
510
  throw new OpenChamberControlError('The in-app browser is not available on this server', 503);
471
511
  }
472
- return browserAction(action, input, options.signal, contextDirectory);
512
+ return browserAction(action, input, options.signal, contextDirectory, options.contextSessionId);
513
+ }
514
+ if (action === 'file.open') {
515
+ if (!fileOpen) {
516
+ throw new OpenChamberControlError('The file viewer is not available on this server', 503);
517
+ }
518
+ return fileOpen.request({
519
+ path: asNonEmptyString(input.path),
520
+ directory: asNonEmptyString(input.directory) || asNonEmptyString(contextDirectory),
521
+ sessionId: asNonEmptyString(options.contextSessionId),
522
+ });
473
523
  }
474
524
  if (action === 'projects.list') return { projects: await projects() };
475
525
  if (action === 'models.list') return models();
@@ -514,24 +564,24 @@ export const createOpenChamberControlService = (dependencies) => {
514
564
  if (action.startsWith('session.')) {
515
565
  const directory = asNonEmptyString(input.directory) || asNonEmptyString(contextDirectory);
516
566
  const sessionID = asNonEmptyString(input.sessionId);
517
- const client = await getClient();
567
+ const client = await getClient(directory);
518
568
  if (action === 'session.list') {
519
569
  const limit = positiveInteger(input.limit, 10, 'limit');
520
570
  const response = await client.session.list(directory ? { directory } : {});
521
- let sessions = Array.isArray(response?.data) ? response.data : [];
571
+ // Archive is OpenChamber state; overlay it so callers keep reading
572
+ // it off the session the way OpenCode used to report it.
573
+ let sessions = (Array.isArray(response?.data) ? response.data : []).map((session) => {
574
+ const archived = archivedAt(session?.id);
575
+ return archived ? { ...session, time: { ...session.time, archived } } : session;
576
+ });
522
577
  if (input.all !== true) sessions = sessions.filter((session) => !session?.time?.archived);
523
578
  sessions = sessions.slice(0, limit);
524
579
  if (input.withStatus === true) {
525
- const cache = new Map();
526
- sessions = await Promise.all(sessions.map(async (session) => {
527
- const sessionDirectory = asNonEmptyString(session?.directory);
528
- if (!sessionDirectory) return { ...session, status: { type: 'unknown' } };
529
- if (!cache.has(sessionDirectory)) {
530
- const statusRequest = client.session.status({ directory: sessionDirectory }).catch(() => null);
531
- cache.set(sessionDirectory, statusRequest);
532
- }
533
- const statusResponse = await cache.get(sessionDirectory);
534
- return { ...session, status: statusResponse?.data?.[session.id] || (statusResponse ? { type: 'idle' } : { type: 'unknown' }) };
580
+ // One global call now: v2 reports active sessions across directories.
581
+ const statuses = await activeSessions(client).catch(() => null);
582
+ sessions = sessions.map((session) => ({
583
+ ...session,
584
+ status: statuses ? asPublicStatus(statuses[session.id]) : { type: 'unknown' },
535
585
  }));
536
586
  }
537
587
  return { sessions, limit, directory, archived: input.all === true ? 'included' : 'excluded' };
@@ -539,7 +589,7 @@ export const createOpenChamberControlService = (dependencies) => {
539
589
  if (!sessionID) throw new OpenChamberControlError('sessionId is required', 400);
540
590
  if (!directory) throw new OpenChamberControlError('directory is required', 400);
541
591
  if (action === 'session.status') {
542
- return { sessionId: sessionID, directory, sessionStatus: await sessionStatus(client, sessionID, directory) };
592
+ return { sessionId: sessionID, directory, sessionStatus: await sessionStatus(client, sessionID) };
543
593
  }
544
594
  if (action === 'session.messages') {
545
595
  if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
@@ -550,9 +600,9 @@ export const createOpenChamberControlService = (dependencies) => {
550
600
  if (last && input.limit !== undefined) throw new OpenChamberControlError('last cannot be combined with limit', 400);
551
601
  const currentStatus = input.wait === true
552
602
  ? await waitForIdle({ client, sessionID, directory, timeoutMs: normalizeWaitTimeoutMs(input.timeout), requireActivity: false, startedAt: now(), signal: options.signal })
553
- : await sessionStatus(client, sessionID, directory);
603
+ : await sessionStatus(client, sessionID);
554
604
  const limit = input.all === true ? undefined : (last ? 1 : positiveInteger(input.limit, 10, 'limit'));
555
- return { sessionId: sessionID, directory, role, sessionStatus: currentStatus, messages: await sessionMessages(client, sessionID, directory, role, limit) };
605
+ return { sessionId: sessionID, directory, role, sessionStatus: currentStatus, messages: await sessionMessages(client, sessionID, role, limit) };
556
606
  }
557
607
  }
558
608
  throw new OpenChamberControlError(`Unsupported OpenChamber action: ${action || 'missing'}`, 400);
@@ -561,5 +611,8 @@ export const createOpenChamberControlService = (dependencies) => {
561
611
  }
562
612
  };
563
613
 
564
- return { execute };
614
+ // The managed agent-tool plugin can no longer report the session's directory
615
+ // (v2 dropped `context.directory` from a tool call), so it sends the session
616
+ // id and the directory is resolved here, where it is authoritative.
617
+ return { execute, resolveSessionDirectory };
565
618
  };
@@ -10,8 +10,10 @@ const createService = (overrides = {}) => {
10
10
  const client = {
11
11
  session: {
12
12
  list: vi.fn(async () => ({ data: [] })),
13
- status: vi.fn(async () => ({ data: {} })),
14
- messages: vi.fn(async () => ({ data: [] })),
13
+ active: vi.fn(async () => ({})),
14
+ },
15
+ message: {
16
+ list: vi.fn(async () => ({ data: [] })),
15
17
  },
16
18
  };
17
19
  const sessionService = {
@@ -137,20 +139,12 @@ describe('OpenChamber control service', () => {
137
139
  });
138
140
 
139
141
  it('resolves the target session directory from the global session list when send omits it', async () => {
140
- const { service, sessionService, client } = createService({
141
- createClient: () => ({
142
- ...client,
143
- experimental: {
144
- session: {
145
- list: vi.fn(async () => ({
146
- data: [
147
- { id: 'ses_other', directory: '/repo/worktrees/other' },
148
- { id: 'ses_target', directory: '/repo/worktrees/target' },
149
- ],
150
- })),
151
- },
152
- },
153
- }),
142
+ const { service, sessionService, client } = createService();
143
+ client.session.list.mockResolvedValue({
144
+ data: [
145
+ { id: 'ses_other', location: { directory: '/repo/worktrees/other' } },
146
+ { id: 'ses_target', location: { directory: '/repo/worktrees/target' } },
147
+ ],
154
148
  });
155
149
  sessionService.send.mockResolvedValue({ sessionId: 'ses_target', directory: '/repo/worktrees/target', promptDispatched: true });
156
150
 
@@ -160,12 +154,7 @@ describe('OpenChamber control service', () => {
160
154
  });
161
155
 
162
156
  it('falls back to the context directory when the session is not in the global list', async () => {
163
- const { service, sessionService, client } = createService({
164
- createClient: () => ({
165
- ...client,
166
- experimental: { session: { list: vi.fn(async () => ({ data: [] })) } },
167
- }),
168
- });
157
+ const { service, sessionService } = createService();
169
158
  sessionService.send.mockResolvedValue({ sessionId: 'ses_unknown', directory: '/repo', promptDispatched: true });
170
159
 
171
160
  await service.execute('session.send', { sessionId: 'ses_unknown', prompt: 'Continue' }, '/repo');
@@ -185,11 +174,11 @@ describe('OpenChamber control service', () => {
185
174
  promptDispatched: true,
186
175
  baselineAssistantMessageId: 'msg_old',
187
176
  });
188
- client.session.status.mockResolvedValue({ data: { ses_1: { type: 'idle' } } });
189
- client.session.messages
190
- .mockResolvedValueOnce({ data: [{ info: { id: 'msg_old', role: 'assistant', time: { completed: 900 } }, parts: [{ type: 'text', text: 'old' }] }] })
191
- .mockResolvedValueOnce({ data: [{ info: { id: 'msg_new', role: 'assistant', time: { completed: 1500 } }, parts: [{ type: 'text', text: 'done' }] }] })
192
- .mockResolvedValueOnce({ data: [{ info: { id: 'msg_new', role: 'assistant', time: { completed: 1500 } }, parts: [{ type: 'text', text: 'done' }] }] });
177
+ client.session.active.mockResolvedValue({});
178
+ client.message.list
179
+ .mockResolvedValueOnce({ data: [{ id: 'msg_old', type: 'assistant', time: { completed: 900 }, content: [{ type: 'text', text: 'old' }] }] })
180
+ .mockResolvedValueOnce({ data: [{ id: 'msg_new', type: 'assistant', time: { completed: 1500 }, content: [{ type: 'text', text: 'done' }] }] })
181
+ .mockResolvedValueOnce({ data: [{ id: 'msg_new', type: 'assistant', time: { completed: 1500 }, content: [{ type: 'text', text: 'done' }] }] });
193
182
 
194
183
  await expect(service.execute('session.create', {
195
184
  directory: '/repo',
@@ -201,24 +190,24 @@ describe('OpenChamber control service', () => {
201
190
  sessionStatus: { type: 'idle' },
202
191
  lastAssistantMessage: expect.objectContaining({ id: 'msg_new', text: 'done' }),
203
192
  }));
204
- expect(client.session.status).toHaveBeenCalledTimes(2);
193
+ expect(client.session.active).toHaveBeenCalledTimes(2);
205
194
  });
206
195
 
207
- it('filters archived sessions and adds directory-scoped statuses', async () => {
208
- const { service, client } = createService();
196
+ it('filters sessions archived in OpenChamber state and adds global statuses', async () => {
197
+ const { service, client } = createService({
198
+ archiveStore: { isArchived: (id) => (id === 'ses_archived' ? 100 : null) },
199
+ });
209
200
  client.session.list.mockResolvedValue({ data: [
210
- { id: 'ses_active', directory: '/repo', time: {} },
211
- { id: 'ses_archived', directory: '/repo', time: { archived: 100 } },
212
- { id: 'ses_other', directory: '/other', time: {} },
201
+ { id: 'ses_active', location: { directory: '/repo' }, time: {} },
202
+ { id: 'ses_archived', location: { directory: '/repo' }, time: {} },
203
+ { id: 'ses_other', location: { directory: '/other' }, time: {} },
213
204
  ] });
214
- client.session.status
215
- .mockResolvedValueOnce({ data: { ses_active: { type: 'busy' } } })
216
- .mockRejectedValueOnce(new Error('unavailable'));
205
+ client.session.active.mockResolvedValue({ ses_active: { type: 'running' } });
217
206
 
218
207
  await expect(service.execute('session.list', { limit: 10, withStatus: true })).resolves.toEqual({
219
208
  sessions: [
220
- { id: 'ses_active', directory: '/repo', time: {}, status: { type: 'busy' } },
221
- { id: 'ses_other', directory: '/other', time: {}, status: { type: 'unknown' } },
209
+ { id: 'ses_active', location: { directory: '/repo' }, time: {}, status: { type: 'busy' } },
210
+ { id: 'ses_other', location: { directory: '/other' }, time: {}, status: { type: 'idle' } },
222
211
  ],
223
212
  limit: 10,
224
213
  directory: null,
@@ -226,21 +215,34 @@ describe('OpenChamber control service', () => {
226
215
  });
227
216
  });
228
217
 
218
+ it('reports unknown status when the active-session read fails', async () => {
219
+ const { service, client } = createService();
220
+ client.session.list.mockResolvedValue({ data: [{ id: 'ses_active', location: { directory: '/repo' }, time: {} }] });
221
+ client.session.active.mockRejectedValue(new Error('unavailable'));
222
+
223
+ await expect(service.execute('session.list', { limit: 10, withStatus: true })).resolves.toEqual({
224
+ sessions: [{ id: 'ses_active', location: { directory: '/repo' }, time: {}, status: { type: 'unknown' } }],
225
+ limit: 10,
226
+ directory: null,
227
+ archived: 'excluded',
228
+ });
229
+ });
230
+
229
231
  it('names limit in positive-integer validation errors', async () => {
230
232
  const { service, client } = createService();
231
233
  await expect(service.execute('session.list', { limit: 0 })).rejects.toThrow('limit must be a positive integer');
232
234
  expect(client.session.list).not.toHaveBeenCalled();
233
235
  });
234
236
 
235
- it('projects only ordered text parts from session messages', async () => {
237
+ it('projects only ordered text content from session messages', async () => {
236
238
  const { service, client } = createService();
237
- client.session.messages.mockResolvedValue({ data: [
239
+ client.message.list.mockResolvedValue({ data: [
238
240
  {
239
- info: { id: 'msg_assistant', role: 'assistant', providerID: 'openai', modelID: 'gpt-5.4-mini', time: { created: 20, completed: 30 } },
240
- parts: [{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'First ' }, { type: 'tool' }, { type: 'text', text: 'answer' }],
241
+ id: 'msg_assistant', type: 'assistant', model: { providerID: 'openai', id: 'gpt-5.4-mini' }, time: { created: 20, completed: 30 },
242
+ content: [{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'First ' }, { type: 'tool' }, { type: 'text', text: 'answer' }],
241
243
  },
242
- { info: { id: 'msg_user', role: 'user', time: { created: 10 } }, parts: [{ type: 'text', text: 'Question' }] },
243
- { info: { id: 'msg_tool', role: 'assistant', time: { created: 15 } }, parts: [{ type: 'tool' }] },
244
+ { id: 'msg_user', type: 'user', time: { created: 10 }, text: 'Question' },
245
+ { id: 'msg_tool', type: 'assistant', time: { created: 15 }, content: [{ type: 'tool' }] },
244
246
  ] });
245
247
 
246
248
  await expect(service.execute('session.messages', {
@@ -266,6 +268,32 @@ describe('OpenChamber control service', () => {
266
268
  });
267
269
  });
268
270
 
271
+ describe('file.open', () => {
272
+ it('hands the path, the session directory and the session to the file viewer', async () => {
273
+ const request = vi.fn(async () => ({ path: '/repo/out.csv', size: 3, opened: true }));
274
+ const { service } = createService({ fileOpen: { request } });
275
+
276
+ const result = await service.execute('file.open', { path: 'out.csv' }, '/repo', { contextSessionId: 'ses_1' });
277
+
278
+ expect(request).toHaveBeenCalledWith({ path: 'out.csv', directory: '/repo', sessionId: 'ses_1' });
279
+ expect(result).toEqual({ path: '/repo/out.csv', size: 3, opened: true });
280
+ });
281
+
282
+ it('lets an explicit directory win over the session directory', async () => {
283
+ const request = vi.fn(async () => ({ path: '/other/out.csv', size: 3, opened: true }));
284
+ const { service } = createService({ fileOpen: { request } });
285
+
286
+ await service.execute('file.open', { path: 'out.csv', directory: '/other' }, '/repo');
287
+
288
+ expect(request).toHaveBeenCalledWith({ path: 'out.csv', directory: '/other', sessionId: null });
289
+ });
290
+
291
+ it('answers 503 when this server has no file viewer wired', async () => {
292
+ const { service } = createService({});
293
+ await expect(service.execute('file.open', { path: 'out.csv' }, '/repo')).rejects.toMatchObject({ statusCode: 503 });
294
+ });
295
+ });
296
+
269
297
  describe('browser capture', () => {
270
298
  const pixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
271
299
 
@@ -315,4 +343,16 @@ describe('browser capture', () => {
315
343
  await service.execute('browser.capture', { label: 'before' }, directory);
316
344
  expect(request).toHaveBeenCalledWith('browser.capture', { label: 'before' }, expect.anything());
317
345
  });
346
+
347
+ it('tells the browser which project and chat the action came from', async () => {
348
+ const { service, directory, request } = await createBrowserService({ base64: pixel, mime: 'image/png' });
349
+ await service.execute('browser.capture', {}, directory, { contextSessionId: 'ses_1' });
350
+ expect(request).toHaveBeenCalledWith('browser.capture', {}, expect.objectContaining({
351
+ context: { directory, sessionId: 'ses_1' },
352
+ }));
353
+ await service.execute('browser.capture', {}, directory);
354
+ expect(request).toHaveBeenLastCalledWith('browser.capture', {}, expect.objectContaining({
355
+ context: { directory, sessionId: null },
356
+ }));
357
+ });
318
358
  });