@vantran-se/openchamber-web 1.24.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-COVQhWGd.js +0 -4
  316. package/dist/assets/FilesView-B1V5JxFX.js +0 -13
  317. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
  320. package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
  321. package/dist/assets/PlanView-LW2z_EAP.js +0 -5
  322. package/dist/assets/PluginPane-B4tKno2h.js +0 -3
  323. package/dist/assets/SettingsView-DEaMulN9.js +0 -29
  324. package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
  326. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
  328. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
  329. package/dist/assets/de-DgxgFyGG.js +0 -10
  330. package/dist/assets/es-BFsZj5F_.js +0 -16
  331. package/dist/assets/fr-jdZ5KKQi.js +0 -14
  332. package/dist/assets/index-CKHjPKbq.js +0 -1
  333. package/dist/assets/index-Cbci4i1W.js +0 -40
  334. package/dist/assets/index-DqaMWNrC.css +0 -1
  335. package/dist/assets/ja-C5kKrhO4.js +0 -16
  336. package/dist/assets/ko-DXDTT_0O.js +0 -16
  337. package/dist/assets/main-CZALtVek.js +0 -896
  338. package/dist/assets/main-CgXMBNvG.js +0 -2
  339. package/dist/assets/miniChat-CwTmDixi.js +0 -2
  340. package/dist/assets/mobile-CLf2vCtO.js +0 -2
  341. package/dist/assets/pl-D-7x4Y8Q.js +0 -16
  342. package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
  344. package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
  346. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
  347. package/dist/assets/tr-Dr-oJ22L.js +0 -7
  348. package/dist/assets/uk-9Nx83UcI.js +0 -16
  349. package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
  350. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-Bxy_Hj3r.js +0 -16
  354. package/dist/assets/zh-TW-0BB4gpHM.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -3,17 +3,21 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import yaml from 'yaml';
5
5
  import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
6
+ import { readSectionEntry, readMcpEntry } from './config-v2.js';
6
7
 
7
8
  // ============== PATH CONSTANTS ==============
8
9
 
9
- const OPENCODE_CONFIG_DIR = path.join(
10
- process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'),
11
- 'opencode',
12
- );
10
+ // OpenCode 2 resolves its global config directory as `OPENCODE_CONFIG_DIR`
11
+ // when set, else `$XDG_CONFIG_HOME/opencode`, else `~/.config/opencode`.
12
+ const OPENCODE_CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR?.trim()
13
+ ? path.resolve(process.env.OPENCODE_CONFIG_DIR.trim())
14
+ : path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'), 'opencode');
13
15
  const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
14
16
  const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
15
17
  const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
16
- const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'config.json');
18
+ // OpenCode 2 reads only `opencode.json(c)`; the v1-era `config.json` is not
19
+ // discovered any more, so it is neither read nor written here.
20
+ const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
17
21
  const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
18
22
 
19
23
  // ============== SCOPE TYPE CONSTANTS ==============
@@ -122,13 +126,21 @@ function writeMdFile(filePath, frontmatter, body) {
122
126
 
123
127
  // ============== CONFIG FILE OPERATIONS ==============
124
128
 
129
+ /**
130
+ * Project config files in the order OpenCode 2 lets them win: a file under
131
+ * `.opencode/` overrides the one beside it at the project root, and
132
+ * `opencode.json` overrides `opencode.jsonc`. When several exist, the
133
+ * highest-priority one is the file OpenChamber reads and writes for the
134
+ * project scope (OpenCode merges them all; an entry that lives only in a
135
+ * lower file is visible through the resolved catalog but not editable here).
136
+ */
125
137
  function getProjectConfigCandidates(workingDirectory) {
126
138
  if (!workingDirectory) return [];
127
139
  return [
128
- path.join(workingDirectory, 'opencode.json'),
129
- path.join(workingDirectory, 'opencode.jsonc'),
130
140
  path.join(workingDirectory, '.opencode', 'opencode.json'),
131
141
  path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
142
+ path.join(workingDirectory, 'opencode.json'),
143
+ path.join(workingDirectory, 'opencode.jsonc'),
132
144
  ];
133
145
  }
134
146
 
@@ -143,13 +155,13 @@ function getProjectConfigPath(workingDirectory) {
143
155
  }
144
156
  }
145
157
 
158
+ // A new project config goes beside the project's other `.opencode/` files.
146
159
  return candidates[0];
147
160
  }
148
161
 
149
162
  function getConfigPaths(workingDirectory) {
150
163
  return {
151
164
  userPaths: [
152
- path.join(OPENCODE_CONFIG_DIR, 'config.json'),
153
165
  path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
154
166
  path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
155
167
  ],
@@ -357,30 +369,49 @@ function throwIfLayerError(layers, filePath) {
357
369
  throw error;
358
370
  }
359
371
 
360
- function getJsonEntrySource(layers, sectionKey, entryName) {
372
+ /**
373
+ * Look one entry up in a config layer, accepting both OpenCode 2 section keys
374
+ * and the v1 keys v2 still decodes. `sectionKind` is `agents`, `commands`,
375
+ * `providers`, or `mcp`.
376
+ */
377
+ function lookupSectionEntry(config, sectionKind, entryName) {
378
+ if (sectionKind === 'mcp') {
379
+ return readMcpEntry(config, entryName);
380
+ }
381
+ return readSectionEntry(config, sectionKind, entryName);
382
+ }
383
+
384
+ function getJsonEntrySource(layers, sectionKind, entryName) {
361
385
  const { userConfig, projectConfig, customConfig, paths } = layers;
386
+ const found = (config, filePath) => {
387
+ const entry = lookupSectionEntry(config, sectionKind, entryName);
388
+ if (entry.value === undefined) return null;
389
+ return {
390
+ section: entry.value,
391
+ config,
392
+ path: filePath,
393
+ exists: true,
394
+ sectionKey: entry.key,
395
+ legacy: entry.legacy,
396
+ };
397
+ };
398
+
362
399
  if (paths.customPath) {
363
400
  throwIfLayerError(layers, paths.customPath);
364
- const customSection = customConfig?.[sectionKey]?.[entryName];
365
- if (customSection !== undefined) {
366
- return { section: customSection, config: customConfig, path: paths.customPath, exists: true };
367
- }
401
+ const custom = found(customConfig, paths.customPath);
402
+ if (custom) return custom;
368
403
  }
369
404
 
370
405
  if (paths.projectPath && !getLayerError(layers, paths.projectPath)) {
371
- const projectSection = projectConfig?.[sectionKey]?.[entryName];
372
- if (projectSection !== undefined) {
373
- return { section: projectSection, config: projectConfig, path: paths.projectPath, exists: true };
374
- }
406
+ const project = found(projectConfig, paths.projectPath);
407
+ if (project) return project;
375
408
  }
376
409
 
377
410
  throwIfLayerError(layers, paths.userPath);
378
- const userSection = userConfig?.[sectionKey]?.[entryName];
379
- if (userSection !== undefined) {
380
- return { section: userSection, config: userConfig, path: paths.userPath, exists: true };
381
- }
411
+ const user = found(userConfig, paths.userPath);
412
+ if (user) return user;
382
413
 
383
- return { section: null, config: null, path: null, exists: false };
414
+ return { section: null, config: null, path: null, exists: false, sectionKey: null, legacy: false };
384
415
  }
385
416
 
386
417
  function getJsonWriteTarget(layers, preferredScope) {
@@ -659,6 +690,7 @@ export {
659
690
  readConfig,
660
691
  getConfigForPath,
661
692
  writeConfig,
693
+ lookupSectionEntry,
662
694
  getJsonEntrySource,
663
695
  getJsonWriteTarget,
664
696
  getAncestors,
@@ -165,10 +165,12 @@ describe('updateAgent frontmatter preservation', () => {
165
165
  expect(content.match(/^---\r?\n/g)).toHaveLength(1);
166
166
 
167
167
  const parsed = parseMdFile(agentPath);
168
+ // `temperature` is a v1 field: reading accepts it, writing moves it under
169
+ // the v2 `request.body` overlay.
168
170
  expect(parsed.frontmatter).toEqual({
169
171
  description: 'Strategy agent',
170
172
  model: 'openai/gpt-5',
171
- temperature: 0.7,
173
+ request: { body: { temperature: 0.7 } },
172
174
  });
173
175
  expect(parsed.body).toBe('');
174
176
  });
@@ -196,7 +198,7 @@ describe('updateAgent frontmatter preservation', () => {
196
198
  expect(parsed.frontmatter).toEqual({
197
199
  description: 'Updated strategy agent',
198
200
  mode: 'primary',
199
- temperature: 0.7,
201
+ request: { body: { temperature: 0.7 } },
200
202
  });
201
203
  expect(parsed.body).toBe('Body of strateg.');
202
204
  });
@@ -372,10 +374,12 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
372
374
  }),
373
375
  ]);
374
376
 
375
- updateMcpConfig('openproject', { enabled: false }, projectDir);
377
+ updateMcpConfig('openproject', { disabled: true }, projectDir);
376
378
  const rewritten = JSON.parse(fs.readFileSync(custom, 'utf8'));
377
379
  expect(rewritten.plugin).toEqual(['opencode-see-image']);
378
- expect(rewritten.mcp.openproject.enabled).toBe(false);
380
+ // The v1 `mcp.<name>` entry is rewritten in place into `mcp.servers`.
381
+ expect(rewritten.mcp.openproject).toBeUndefined();
382
+ expect(rewritten.mcp.servers.openproject.disabled).toBe(true);
379
383
  expect(fs.readFileSync(projectFile, 'utf8')).toBe(PARTIAL_PARSE_CONFIG);
380
384
  expect(fs.existsSync(`${projectFile}.openchamber.backup`)).toBe(false);
381
385
  } finally {
@@ -31,25 +31,67 @@ export const createGracefulShutdownRuntime = (dependencies) => {
31
31
  getActiveTunnelController,
32
32
  setActiveTunnelController,
33
33
  tunnelAuthController,
34
+ beginGuestServiceShutdown,
35
+ stopAllGuestServices,
36
+ getGuestSurfaceRuntime,
37
+ getRealtimeProxyRuntime,
38
+ getDictationRuntime,
39
+ getRelayService,
40
+ getRelayReconcileTimer,
34
41
  } = dependencies;
35
42
 
36
43
  let shutdownPromise = null;
44
+ const serverConnections = new Set();
45
+ let closingHttpServer = false;
46
+
47
+ // Track TCP sockets before listen(): HTTP stops tracking them on upgrade,
48
+ // even when no WebSocket handler completes the handshake.
49
+ const trackServerConnections = (server) => {
50
+ const onConnection = (socket) => {
51
+ if (closingHttpServer) {
52
+ socket.destroy();
53
+ return;
54
+ }
55
+ serverConnections.add(socket);
56
+ socket.once('close', () => serverConnections.delete(socket));
57
+ };
58
+ server.on('connection', onConnection);
59
+ server.once('close', () => server.off('connection', onConnection));
60
+ };
37
61
 
38
62
  const runShutdown = async (options = {}) => {
39
63
  if (getIsShuttingDown()) return;
40
64
 
41
65
  setIsShuttingDown(true);
66
+ beginGuestServiceShutdown();
42
67
  syncToHmrState();
43
68
  console.log('Starting graceful shutdown...');
44
69
  const exitProcess = typeof options.exitProcess === 'boolean' ? options.exitProcess : getExitOnShutdown();
45
70
 
46
- openCodeWatcherRuntime.stop();
47
- sessionRuntime.dispose();
48
- sessionAssistRuntime?.stop?.();
49
- sessionGoalRuntime?.stop?.();
50
- contextObligatoryRuntime?.stop?.();
51
- messageQueueRuntime?.stop?.();
52
- scheduledTasksRuntime?.stop?.();
71
+ // Both embedded stop() and daemon exits use this sequence. Close admission
72
+ // synchronously above, then stop viewers before draining their services.
73
+ const cleanupOperations = [
74
+ () => clearInterval(getRelayReconcileTimer()),
75
+ () => getGuestSurfaceRuntime()?.stop(),
76
+ () => getRealtimeProxyRuntime()?.stop(),
77
+ () => getRelayService()?.stop(),
78
+ () => getDictationRuntime()?.stop(),
79
+ () => openCodeWatcherRuntime.stop(),
80
+ () => sessionRuntime.dispose(),
81
+ () => sessionAssistRuntime?.stop?.(),
82
+ () => sessionGoalRuntime?.stop?.(),
83
+ () => contextObligatoryRuntime?.stop?.(),
84
+ () => messageQueueRuntime?.stop?.(),
85
+ () => scheduledTasksRuntime?.stop?.(),
86
+ stopAllGuestServices,
87
+ ];
88
+ for (const cleanup of cleanupOperations) {
89
+ try {
90
+ await cleanup();
91
+ } catch {
92
+ // One failed runtime must not skip the rest of host teardown.
93
+ }
94
+ }
53
95
 
54
96
  const healthCheckInterval = getHealthCheckInterval();
55
97
  if (healthCheckInterval) {
@@ -100,6 +142,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
100
142
 
101
143
  const server = getServer();
102
144
  if (server) {
145
+ closingHttpServer = true;
103
146
  let closeTimeout = null;
104
147
  try {
105
148
  await Promise.race([
@@ -111,6 +154,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
111
154
  // The backend has stopped. Active SSE/HTTP clients must not keep
112
155
  // Desktop waiting for the outer shutdown deadline.
113
156
  server.closeAllConnections?.();
157
+ // Includes upgraded sockets and reconnects accepted while the
158
+ // services above were draining. No child-process grace is cut short.
159
+ for (const socket of serverConnections) socket.destroy();
114
160
  }),
115
161
  new Promise((resolve) => {
116
162
  closeTimeout = setTimeout(() => {
@@ -154,5 +200,6 @@ export const createGracefulShutdownRuntime = (dependencies) => {
154
200
 
155
201
  return {
156
202
  gracefulShutdown,
203
+ trackServerConnections,
157
204
  };
158
205
  };
@@ -1,37 +1,51 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import http from 'node:http';
3
+ import net from 'node:net';
4
+ import { once } from 'node:events';
3
5
 
4
6
  import { createGracefulShutdownRuntime } from './shutdown-runtime.js';
5
7
 
6
- const createRuntime = (server) => createGracefulShutdownRuntime({
7
- process: { exit: vi.fn() },
8
- shutdownTimeoutMs: 1000,
9
- getExitOnShutdown: () => false,
10
- getIsShuttingDown: () => false,
11
- setIsShuttingDown: vi.fn(),
12
- syncToHmrState: vi.fn(),
13
- openCodeWatcherRuntime: { stop: vi.fn() },
14
- sessionRuntime: { dispose: vi.fn() },
15
- scheduledTasksRuntime: { stop: vi.fn() },
16
- getHealthCheckInterval: () => null,
17
- clearHealthCheckInterval: vi.fn(),
18
- getTerminalRuntime: () => null,
19
- setTerminalRuntime: vi.fn(),
20
- getMessageStreamRuntime: () => null,
21
- setMessageStreamRuntime: vi.fn(),
22
- shouldSkipOpenCodeStop: () => true,
23
- getOpenCodePort: () => null,
24
- getOpenCodeProcess: () => null,
25
- setOpenCodeProcess: vi.fn(),
26
- killProcessOnPort: vi.fn(),
27
- waitForPortRelease: vi.fn(async () => true),
28
- getServer: () => server,
29
- getUiAuthController: () => null,
30
- setUiAuthController: vi.fn(),
31
- getActiveTunnelController: () => null,
32
- setActiveTunnelController: vi.fn(),
33
- tunnelAuthController: { clearActiveTunnel: vi.fn() },
34
- });
8
+ const createRuntime = (server, overrides = {}) => {
9
+ const runtime = createGracefulShutdownRuntime({
10
+ process: { exit: vi.fn() },
11
+ shutdownTimeoutMs: 1000,
12
+ getExitOnShutdown: () => false,
13
+ getIsShuttingDown: () => false,
14
+ setIsShuttingDown: vi.fn(),
15
+ syncToHmrState: vi.fn(),
16
+ openCodeWatcherRuntime: { stop: vi.fn() },
17
+ sessionRuntime: { dispose: vi.fn() },
18
+ scheduledTasksRuntime: { stop: vi.fn() },
19
+ getHealthCheckInterval: () => null,
20
+ clearHealthCheckInterval: vi.fn(),
21
+ getTerminalRuntime: () => null,
22
+ setTerminalRuntime: vi.fn(),
23
+ getMessageStreamRuntime: () => null,
24
+ setMessageStreamRuntime: vi.fn(),
25
+ shouldSkipOpenCodeStop: () => true,
26
+ getOpenCodePort: () => null,
27
+ getOpenCodeProcess: () => null,
28
+ setOpenCodeProcess: vi.fn(),
29
+ killProcessOnPort: vi.fn(),
30
+ waitForPortRelease: vi.fn(async () => true),
31
+ getServer: () => server,
32
+ getUiAuthController: () => null,
33
+ setUiAuthController: vi.fn(),
34
+ getActiveTunnelController: () => null,
35
+ setActiveTunnelController: vi.fn(),
36
+ tunnelAuthController: { clearActiveTunnel: vi.fn() },
37
+ beginGuestServiceShutdown: vi.fn(),
38
+ stopAllGuestServices: vi.fn(),
39
+ getGuestSurfaceRuntime: () => null,
40
+ getRealtimeProxyRuntime: () => null,
41
+ getDictationRuntime: () => null,
42
+ getRelayService: () => null,
43
+ getRelayReconcileTimer: () => null,
44
+ ...overrides,
45
+ });
46
+ if (server instanceof http.Server) runtime.trackServerConnections(server);
47
+ return runtime;
48
+ };
35
49
 
36
50
  describe('graceful shutdown runtime', () => {
37
51
  afterEach(() => {
@@ -62,6 +76,7 @@ describe('graceful shutdown runtime', () => {
62
76
  response.writeHead(200, { 'content-type': 'text/event-stream' });
63
77
  response.write('data: fixture\n\n');
64
78
  });
79
+ const runtime = createRuntime(server);
65
80
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
66
81
  const request = http.get(`http://127.0.0.1:${server.address().port}`);
67
82
  request.on('error', () => {});
@@ -70,7 +85,7 @@ describe('graceful shutdown runtime', () => {
70
85
  const closed = new Promise((resolve) => response.once('close', resolve));
71
86
  const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
72
87
  try {
73
- await createRuntime(server).gracefulShutdown({ exitProcess: false });
88
+ await runtime.gracefulShutdown({ exitProcess: false });
74
89
  expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
75
90
  await closed;
76
91
  } finally {
@@ -79,4 +94,183 @@ describe('graceful shutdown runtime', () => {
79
94
  await new Promise((resolve) => server.close(resolve));
80
95
  }
81
96
  });
97
+
98
+ it.each([false, true])('closes upgraded sockets without the HTTP timeout (handshake completed: %s)', async (completeHandshake) => {
99
+ const server = http.createServer();
100
+ const runtime = createRuntime(server);
101
+ const sockets = new Set();
102
+ server.on('upgrade', (_request, socket) => {
103
+ sockets.add(socket);
104
+ if (completeHandshake) {
105
+ socket.write('HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n');
106
+ }
107
+ // A listener for another WS route can leave the upgrade unanswered.
108
+ });
109
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
110
+ const client = net.connect(server.address().port, '127.0.0.1');
111
+ client.on('error', () => {});
112
+ client.resume();
113
+ const clientClosed = once(client, 'close');
114
+ const upgraded = once(server, 'upgrade');
115
+ client.write('GET /api/global/event/ws HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n');
116
+ await upgraded;
117
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
118
+ try {
119
+ await runtime.gracefulShutdown({ exitProcess: false });
120
+ expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
121
+ await clientClosed;
122
+ const connections = await new Promise((resolve, reject) => {
123
+ server.getConnections((error, count) => error ? reject(error) : resolve(count));
124
+ });
125
+ expect(connections).toBe(0);
126
+ } finally {
127
+ for (const socket of sockets) socket.destroy();
128
+ client.destroy();
129
+ await new Promise((resolve) => server.close(resolve));
130
+ }
131
+ });
132
+
133
+ it('drains processes before closing sockets accepted during cleanup, including after a cleanup failure', async () => {
134
+ const server = http.createServer();
135
+ const cleanupStarted = Promise.withResolvers();
136
+ const finishCleanup = Promise.withResolvers();
137
+ const sockets = new Set();
138
+ const clients = [];
139
+ server.on('upgrade', (_request, socket) => sockets.add(socket));
140
+ const terminalShutdown = vi.fn(async () => {
141
+ expect([...sockets].every((socket) => !socket.destroyed)).toBe(true);
142
+ });
143
+ const processClose = vi.fn(async () => {
144
+ expect(terminalShutdown).toHaveBeenCalledOnce();
145
+ expect([...sockets].every((socket) => !socket.destroyed)).toBe(true);
146
+ });
147
+ const runtime = createRuntime(server, {
148
+ stopAllGuestServices: async () => {
149
+ cleanupStarted.resolve();
150
+ await finishCleanup.promise;
151
+ throw new Error('fixture cleanup failure');
152
+ },
153
+ getTerminalRuntime: () => ({ shutdown: terminalShutdown }),
154
+ shouldSkipOpenCodeStop: () => false,
155
+ getOpenCodeProcess: () => ({ close: processClose }),
156
+ });
157
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
158
+ const shutdown = runtime.gracefulShutdown({ exitProcess: false });
159
+ expect(runtime.gracefulShutdown({ exitProcess: false })).toBe(shutdown);
160
+ await cleanupStarted.promise;
161
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
162
+ try {
163
+ const closed = [];
164
+ for (let index = 0; index < 8; index += 1) {
165
+ const client = net.connect(server.address().port, '127.0.0.1');
166
+ clients.push(client);
167
+ client.resume();
168
+ closed.push(once(client, 'close'));
169
+ const upgraded = once(server, 'upgrade');
170
+ client.write('GET /api/global/event/ws HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n');
171
+ await upgraded;
172
+ }
173
+ expect(sockets.size).toBe(8);
174
+ expect(processClose).not.toHaveBeenCalled();
175
+ finishCleanup.resolve();
176
+ await shutdown;
177
+ expect(processClose).toHaveBeenCalledOnce();
178
+ expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
179
+ await Promise.all(closed);
180
+ expect(server.listening).toBe(false);
181
+ } finally {
182
+ finishCleanup.resolve();
183
+ for (const socket of sockets) socket.destroy();
184
+ for (const client of clients) client.destroy();
185
+ await shutdown;
186
+ await new Promise((resolve) => server.close(resolve));
187
+ }
188
+ });
189
+
190
+ it('stops guest services during shutdown', async () => {
191
+ const server = {
192
+ close: vi.fn((callback) => {
193
+ callback();
194
+ }),
195
+ };
196
+ const stopAllGuestServices = vi.fn(async () => {});
197
+
198
+ const runtime = createRuntime(server, { stopAllGuestServices });
199
+ await runtime.gracefulShutdown({ exitProcess: false });
200
+
201
+ expect(stopAllGuestServices).toHaveBeenCalledTimes(1);
202
+ });
203
+
204
+ it('continues shutdown when stopping guest services fails', async () => {
205
+ const server = {
206
+ close: vi.fn((callback) => {
207
+ callback();
208
+ }),
209
+ };
210
+ const stopAllGuestServices = vi.fn(async () => {
211
+ throw new Error('guest teardown failed');
212
+ });
213
+ const terminalRuntime = { shutdown: vi.fn(async () => {}) };
214
+
215
+ const runtime = createRuntime(server, {
216
+ stopAllGuestServices,
217
+ getTerminalRuntime: () => terminalRuntime,
218
+ });
219
+ await runtime.gracefulShutdown({ exitProcess: false });
220
+
221
+ expect(stopAllGuestServices).toHaveBeenCalledTimes(1);
222
+ expect(terminalRuntime.shutdown).toHaveBeenCalledTimes(1);
223
+ expect(server.close).toHaveBeenCalled();
224
+ });
225
+
226
+ it('closes guest admission synchronously and cleans every runtime once across repeated shutdown calls', async () => {
227
+ vi.useFakeTimers();
228
+ const order = [];
229
+ const cleanup = (name) => vi.fn(() => { order.push(name); });
230
+ const viewers = { stop: cleanup('viewers') };
231
+ const proxy = { stop: cleanup('proxy') };
232
+ const dictation = { stop: cleanup('dictation') };
233
+ const relay = { stop: cleanup('relay') };
234
+ const gate = cleanup('gate');
235
+ const guests = cleanup('guests');
236
+ const reconcile = vi.fn();
237
+ const timer = setInterval(reconcile, 100);
238
+ const runtime = createRuntime(null, {
239
+ beginGuestServiceShutdown: gate,
240
+ stopAllGuestServices: guests,
241
+ getGuestSurfaceRuntime: () => viewers,
242
+ getRealtimeProxyRuntime: () => proxy,
243
+ getDictationRuntime: () => dictation,
244
+ getRelayService: () => relay,
245
+ getRelayReconcileTimer: () => timer,
246
+ });
247
+ const first = runtime.gracefulShutdown();
248
+ expect(gate).toHaveBeenCalledTimes(1);
249
+ expect(runtime.gracefulShutdown()).toBe(first);
250
+ await first;
251
+ await runtime.gracefulShutdown();
252
+ expect(order).toEqual(['gate', 'viewers', 'proxy', 'relay', 'dictation', 'guests']);
253
+ await vi.advanceTimersByTimeAsync(200);
254
+ expect(reconcile).not.toHaveBeenCalled();
255
+ expect(vi.getTimerCount()).toBe(0);
256
+ });
257
+
258
+ it('isolates failed viewer and relay cleanup and still drains guests and exits', async () => {
259
+ const stop = vi.fn(() => { throw new Error('stop failed'); });
260
+ const stopAllGuestServices = vi.fn(async () => {});
261
+ const dictation = { stop: vi.fn() };
262
+ const process = { exit: vi.fn() };
263
+ const runtime = createRuntime(null, {
264
+ process,
265
+ getGuestSurfaceRuntime: () => ({ stop }),
266
+ getRelayService: () => ({ stop }),
267
+ getDictationRuntime: () => dictation,
268
+ stopAllGuestServices,
269
+ });
270
+ await runtime.gracefulShutdown({ exitProcess: true });
271
+ expect(stop).toHaveBeenCalledTimes(2);
272
+ expect(dictation.stop).toHaveBeenCalledTimes(1);
273
+ expect(stopAllGuestServices).toHaveBeenCalledTimes(1);
274
+ expect(process.exit).toHaveBeenCalledWith(0);
275
+ });
82
276
  });
@@ -1,5 +1,5 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
- import { buildDeferredRestartResponse } from './config-mutation-response.js';
1
+ import { OpenCode } from '@opencode/client';
2
+ import { buildAppliedResponse } from './config-mutation-response.js';
3
3
  import { OPENCODE_CONFIG_DIR } from './shared.js';
4
4
 
5
5
  /**
@@ -131,16 +131,18 @@ export const registerSkillRoutes = (app, dependencies) => {
131
131
  }
132
132
 
133
133
  try {
134
- const client = createOpencodeClient({
134
+ const client = OpenCode.make({
135
135
  baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
136
- directory: workingDirectory || undefined,
137
- headers: getOpenCodeAuthHeaders(),
138
- fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
136
+ headers: {
137
+ ...getOpenCodeAuthHeaders(),
138
+ // v2 scopes a request with a header, not a `directory` option, and
139
+ // rejects non-ASCII header values, so the path is percent-encoded.
140
+ ...(workingDirectory ? { 'x-opencode-directory': encodeURIComponent(workingDirectory) } : {}),
141
+ },
142
+ fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(8_000) }),
139
143
  });
140
144
 
141
- const response = await client.app.skills(
142
- workingDirectory ? { directory: workingDirectory } : undefined,
143
- );
145
+ const response = await client.skill.list();
144
146
  const payload = response?.data;
145
147
  if (!Array.isArray(payload)) {
146
148
  return [];
@@ -510,14 +512,14 @@ export const registerSkillRoutes = (app, dependencies) => {
510
512
 
511
513
  const installed = result.installed || [];
512
514
  const skipped = result.skipped || [];
513
- const requiresRestart = installed.length > 0;
515
+ const installedAny = installed.length > 0;
514
516
 
515
517
  res.json({
516
518
  ok: true,
517
519
  installed,
518
520
  skipped,
519
- ...(requiresRestart
520
- ? buildDeferredRestartResponse('Skills installed successfully. Restart OpenCode to apply.')
521
+ ...(installedAny
522
+ ? buildAppliedResponse('Skills installed successfully.')
521
523
  : {
522
524
  requiresReload: false,
523
525
  message: 'No skills were installed',
@@ -602,8 +604,8 @@ export const registerSkillRoutes = (app, dependencies) => {
602
604
  console.log('[Server] Scope:', scope, 'Working directory:', directory);
603
605
 
604
606
  createSkill(skillName, { ...config, source: skillSource }, directory, scope);
605
- res.json(buildDeferredRestartResponse(
606
- `Skill ${skillName} created successfully. Restart OpenCode to apply.`,
607
+ res.json(buildAppliedResponse(
608
+ `Skill ${skillName} created successfully.`,
607
609
  ));
608
610
  } catch (error) {
609
611
  console.error('Failed to create skill:', error);
@@ -625,14 +627,11 @@ export const registerSkillRoutes = (app, dependencies) => {
625
627
  console.log(`[Server] Renaming skill: ${skillName} -> ${newName}`);
626
628
  console.log('[Server] Working directory:', directory);
627
629
  renameSkill(skillName, newName, directory);
628
- await refreshOpenCodeAfterConfigChange('skill rename');
629
-
630
+ // OpenCode 2 watches the skills directories: the renamed folder is
631
+ // picked up like any other write, no restart and no client reload.
630
632
  return res.json({
631
- success: true,
633
+ ...buildAppliedResponse(`Skill renamed to ${newName} successfully.`),
632
634
  name: newName,
633
- requiresReload: true,
634
- message: `Skill renamed to ${newName} successfully. Reloading interface…`,
635
- reloadDelayMs: clientReloadDelayMs,
636
635
  });
637
636
  }
638
637
 
@@ -640,8 +639,8 @@ export const registerSkillRoutes = (app, dependencies) => {
640
639
  console.log('[Server] Working directory:', directory);
641
640
 
642
641
  updateSkill(skillName, updates, directory, updates?.targetPath);
643
- res.json(buildDeferredRestartResponse(
644
- `Skill ${skillName} updated successfully. Restart OpenCode to apply.`,
642
+ res.json(buildAppliedResponse(
643
+ `Skill ${skillName} updated successfully.`,
645
644
  ));
646
645
  } catch (error) {
647
646
  console.error('[Server] Failed to update skill:', error);
@@ -727,8 +726,8 @@ export const registerSkillRoutes = (app, dependencies) => {
727
726
  }
728
727
 
729
728
  deleteSkill(skillName, directory);
730
- res.json(buildDeferredRestartResponse(
731
- `Skill ${skillName} deleted successfully. Restart OpenCode to apply.`,
729
+ res.json(buildAppliedResponse(
730
+ `Skill ${skillName} deleted successfully.`,
732
731
  ));
733
732
  } catch (error) {
734
733
  console.error('Failed to delete skill:', error);