@vantran-se/openchamber-web 1.22.1 → 1.23.0

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 (238) hide show
  1. package/README.md +2 -3
  2. package/bin/cli.js +11 -1
  3. package/bin/cli.test.js +92 -0
  4. package/bin/lib/cli-startup.js +49 -0
  5. package/bin/lib/commands-startup.js +41 -6
  6. package/dist/assets/{CodeMirrorCommentWidgets-C53Q7pbX.js → CodeMirrorCommentWidgets-DFg2wnpo.js} +1 -1
  7. package/dist/assets/DiffView-3Dn4QHbu.js +4 -0
  8. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +1 -0
  9. package/dist/assets/FilesView-BdTOpew9.js +17 -0
  10. package/dist/assets/GitView-Dqf9vHJh.js +6 -0
  11. package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-BKdsjg5B.js} +1 -1
  12. package/dist/assets/LinearIssuesView-CmbeL7I6.js +1 -0
  13. package/dist/assets/MarkdownImageGallery-_5gEVr0X.js +1 -0
  14. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +12 -0
  15. package/dist/assets/OnboardingScreen-DdP4_nuc.js +1 -0
  16. package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-am3SAss2.js} +1 -1
  17. package/dist/assets/PierreDiffViewer-DCsydlKs.js +161 -0
  18. package/dist/assets/PlanView-BoJxdRZe.js +5 -0
  19. package/dist/assets/SettingsView-CcM8pF5O.js +28 -0
  20. package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DVTdDSol.js} +1 -1
  21. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  22. package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-BgfTbDZ4.js} +6 -6
  23. package/dist/assets/ToolPartDiffPreview-C7WLk5i9.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-DcdgkH-x.js} +1 -1
  25. package/dist/assets/WalkthroughView-BEVUDxuw.js +4 -0
  26. package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-C8byMnRh.js} +2 -2
  27. package/dist/assets/contextPanelEmbeddedChat-CirXEPJq.js +1 -0
  28. package/dist/assets/de-C8jV7J9y.js +10 -0
  29. package/dist/assets/es-BI6J1M5R.js +16 -0
  30. package/dist/assets/fr-D6oDFt7E.js +14 -0
  31. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  32. package/dist/assets/{index-DmhujXbT.js → index-BB7ryz8y.js} +1 -1
  33. package/dist/assets/index-BqhiCmYS.js +39 -0
  34. package/dist/assets/index-FqSOBz0J.css +1 -0
  35. package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-45ujMPGb.js} +1 -1
  36. package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-Cc4KFSYi.js} +1 -1
  37. package/dist/assets/ja-C_4i5EQu.js +16 -0
  38. package/dist/assets/ko-BFbOTpbq.js +16 -0
  39. package/dist/assets/{main-B2a_JfKZ.js → main-C9ETLKFc.js} +2 -2
  40. package/dist/assets/main-CJmb2dF8.js +930 -0
  41. package/dist/assets/markdownCore-DVuZpnli.js +5 -0
  42. package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-B4Ytoq2n.js} +2 -2
  43. package/dist/assets/{mobile-QNcFIVCW.js → mobile-BUT6-lo1.js} +2 -2
  44. package/dist/assets/patchFileDiff-8j1apICU.js +13 -0
  45. package/dist/assets/pl-COFvT56l.js +16 -0
  46. package/dist/assets/pt-BR-DtVOO2Lj.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +2 -0
  48. package/dist/assets/renderMobileApp-BNgHraq6.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +17 -0
  50. package/dist/assets/scheduledTasksApi-GC4epIYA.js +1 -0
  51. package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-BJISeZsQ.js} +1 -1
  52. package/dist/assets/surface-BvRzS58U.js +1 -0
  53. package/dist/assets/tr-B8Q0Go2l.js +7 -0
  54. package/dist/assets/uk-Dk4a-Fn-.js +16 -0
  55. package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-DpFNefUA.js} +153 -179
  56. package/dist/assets/useGitComparison-Dfxaqd0s.js +1 -0
  57. package/dist/assets/useInlineCommentController-CscsE4t-.js +2 -0
  58. package/dist/assets/useRootScrollLock-BaCvuRlT.js +1 -0
  59. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
  60. package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-DzlSIu_W.js} +2 -2
  61. package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
  62. package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
  63. package/dist/assets/{web-components-BNMwa0qm.js → web-components-CdGfR2XJ.js} +1 -1
  64. package/dist/assets/zh-CN-B_BXxG1Q.js +16 -0
  65. package/dist/assets/zh-TW-DLrl1SPZ.js +16 -0
  66. package/dist/index.html +5 -43
  67. package/dist/mini-chat.html +4 -4
  68. package/dist/mobile.html +4 -4
  69. package/package.json +8 -5
  70. package/server/index.d.ts +15 -0
  71. package/server/index.js +82 -13
  72. package/server/lib/agent-tool/runtime.js +2 -19
  73. package/server/lib/changelog/update-notes.js +82 -0
  74. package/server/lib/changelog/update-notes.test.js +59 -0
  75. package/server/lib/data-dir-migration.js +40 -0
  76. package/server/lib/data-dir-migration.test.js +59 -0
  77. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  78. package/server/lib/dev-tunnel/runtime.js +4 -4
  79. package/server/lib/dev-tunnel/tunnel.test.js +20 -0
  80. package/server/lib/event-stream/runtime.test.js +12 -12
  81. package/server/lib/fs/DOCUMENTATION.md +5 -0
  82. package/server/lib/fs/routes.js +63 -21
  83. package/server/lib/fs/routes.test.js +187 -1
  84. package/server/lib/git/DOCUMENTATION.md +32 -6
  85. package/server/lib/git/routes.js +40 -4
  86. package/server/lib/git/service.js +291 -215
  87. package/server/lib/git/service.test.js +593 -25
  88. package/server/lib/inherited-env.js +16 -9
  89. package/server/lib/inherited-env.test.js +8 -9
  90. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  91. package/server/lib/mcp-reconnect/runtime.js +122 -0
  92. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  93. package/server/lib/message-queue/DOCUMENTATION.md +161 -0
  94. package/server/lib/message-queue/runtime.js +870 -0
  95. package/server/lib/message-queue/runtime.test.js +482 -0
  96. package/server/lib/network-defaults.js +23 -0
  97. package/server/lib/network-defaults.test.js +52 -0
  98. package/server/lib/openchamber-sessions/routes.js +102 -0
  99. package/server/lib/openchamber-sessions/routes.test.js +88 -0
  100. package/server/lib/opencode/DOCUMENTATION.md +30 -6
  101. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  102. package/server/lib/opencode/config-paths.test.js +116 -0
  103. package/server/lib/opencode/core-routes.js +1 -0
  104. package/server/lib/opencode/feature-routes-runtime.js +7 -0
  105. package/server/lib/opencode/input-history-scope.js +14 -0
  106. package/server/lib/opencode/managed-plugin-config.js +35 -0
  107. package/server/lib/opencode/openchamber-routes.js +64 -4
  108. package/server/lib/opencode/openchamber-routes.test.js +130 -3
  109. package/server/lib/opencode/plugins.js +2 -2
  110. package/server/lib/opencode/providers.js +44 -2
  111. package/server/lib/opencode/providers.test.js +131 -4
  112. package/server/lib/opencode/routes.js +9 -7
  113. package/server/lib/opencode/server-startup-runtime.js +1 -1
  114. package/server/lib/opencode/settings-files.js +214 -0
  115. package/server/lib/opencode/settings-helpers.js +113 -41
  116. package/server/lib/opencode/settings-helpers.test.js +342 -0
  117. package/server/lib/opencode/settings-registry.json +747 -0
  118. package/server/lib/opencode/settings-runtime.js +141 -20
  119. package/server/lib/opencode/settings-runtime.test.js +191 -4
  120. package/server/lib/opencode/shared.js +4 -1
  121. package/server/lib/opencode/shutdown-runtime.js +2 -0
  122. package/server/lib/opencode/skill-routes.js +2 -1
  123. package/server/lib/opencode/snippets.js +1 -2
  124. package/server/lib/package-manager.js +4 -29
  125. package/server/lib/package-manager.test.js +2 -2
  126. package/server/lib/project-context/DOCUMENTATION.md +25 -1
  127. package/server/lib/project-context/routes.http.test.js +18 -0
  128. package/server/lib/project-context/routes.js +15 -0
  129. package/server/lib/project-context/runtime.js +248 -23
  130. package/server/lib/project-context/runtime.test.js +106 -3
  131. package/server/lib/projects/DOCUMENTATION.md +76 -0
  132. package/server/lib/projects/project-config.js +142 -9
  133. package/server/lib/projects/project-id.js +17 -0
  134. package/server/lib/projects/project-setup.js +409 -0
  135. package/server/lib/projects/project-setup.test.js +557 -0
  136. package/server/lib/projects/routes.js +59 -0
  137. package/server/lib/quota/DOCUMENTATION.md +40 -2
  138. package/server/lib/quota/credentials/providers.js +4 -0
  139. package/server/lib/quota/credentials/store.js +1 -1
  140. package/server/lib/quota/credentials/store.test.js +4 -4
  141. package/server/lib/quota/index.js +1 -0
  142. package/server/lib/quota/providers/cline-pass.js +125 -0
  143. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  144. package/server/lib/quota/providers/exe-dev.js +75 -0
  145. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  146. package/server/lib/quota/providers/hyper.js +118 -0
  147. package/server/lib/quota/providers/hyper.test.js +126 -0
  148. package/server/lib/quota/providers/index.js +22 -0
  149. package/server/lib/quota/providers/neuralwatt.js +3 -6
  150. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  151. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  152. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  153. package/server/lib/quota/providers/opencode-go.js +2 -1
  154. package/server/lib/quota/providers/opencode-go.test.js +31 -2
  155. package/server/lib/quota/providers/openrouter.js +119 -24
  156. package/server/lib/quota/providers/openrouter.test.js +330 -0
  157. package/server/lib/quota/routes.js +2 -0
  158. package/server/lib/quota/utils/auth.js +1 -1
  159. package/server/lib/relay/DOCUMENTATION.md +56 -1
  160. package/server/lib/relay/cross-compat.test.js +26 -0
  161. package/server/lib/relay/downstream-scheduler.js +154 -0
  162. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  163. package/server/lib/relay/e2ee.js +10 -8
  164. package/server/lib/relay/flow-control.test.js +177 -0
  165. package/server/lib/relay/host-client.js +37 -8
  166. package/server/lib/relay/host-client.test.js +9 -1
  167. package/server/lib/relay/tunnel-codec.js +9 -0
  168. package/server/lib/relay/tunnel-host.js +69 -18
  169. package/server/lib/relay/tunnel-host.test.js +9 -1
  170. package/server/lib/security/request-security.js +21 -6
  171. package/server/lib/security/request-security.test.js +42 -8
  172. package/server/lib/session-assist/runtime.js +7 -10
  173. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  174. package/server/lib/session-goal/create.js +3 -1
  175. package/server/lib/session-goal/runtime.js +81 -20
  176. package/server/lib/session-goal/runtime.test.js +426 -0
  177. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  178. package/server/lib/skills-catalog/install.js +3 -2
  179. package/server/lib/small-model/DOCUMENTATION.md +7 -2
  180. package/server/lib/small-model/call.js +17 -6
  181. package/server/lib/small-model/call.test.js +52 -0
  182. package/server/lib/small-model/index.js +7 -11
  183. package/server/lib/small-model/routes.js +2 -1
  184. package/server/lib/small-model/runtime-providers.js +15 -7
  185. package/server/lib/small-model/runtime-providers.test.js +7 -1
  186. package/server/lib/system-prompt/runtime.js +2 -21
  187. package/server/lib/terminal/DOCUMENTATION.md +14 -10
  188. package/server/lib/terminal/runtime.js +190 -31
  189. package/server/lib/terminal/runtime.test.js +571 -37
  190. package/server/lib/terminal/shells.js +17 -0
  191. package/server/lib/terminal/shells.test.js +32 -1
  192. package/server/lib/ui-auth/ui-auth.js +4 -1
  193. package/server/lib/ui-auth/ui-auth.test.js +16 -0
  194. package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
  195. package/server/lib/walkthrough/index.js +1 -0
  196. package/server/lib/walkthrough/model-settings.js +8 -12
  197. package/server/lib/walkthrough/prompt.js +3 -1
  198. package/server/lib/walkthrough/routes.test.js +26 -5
  199. package/server/lib/walkthrough/sources.js +23 -2
  200. package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
  201. package/dist/assets/DiffView-Dh1ey2x9.js +0 -4
  202. package/dist/assets/FilesView-3ZYP72ki.js +0 -17
  203. package/dist/assets/GitView-CMuN7z2Z.js +0 -6
  204. package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
  205. package/dist/assets/MarkdownImageGallery-bA0PS-tn.js +0 -1
  206. package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
  207. package/dist/assets/OnboardingScreen-D3kYJIb6.js +0 -1
  208. package/dist/assets/PierreDiffViewer-Ct2A13NO.js +0 -157
  209. package/dist/assets/PlanView-6AUjzNQn.js +0 -5
  210. package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
  211. package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
  212. package/dist/assets/WalkthroughView-AX-u2pFg.js +0 -4
  213. package/dist/assets/contextPanelEmbeddedChat-C9EcRSDF.js +0 -1
  214. package/dist/assets/de-DqVn5vme.js +0 -9
  215. package/dist/assets/es-hj43Z53i.js +0 -15
  216. package/dist/assets/fr-BmkZt2mF.js +0 -13
  217. package/dist/assets/index-L_ncV-6H.css +0 -1
  218. package/dist/assets/index-qES19dlJ.js +0 -7
  219. package/dist/assets/ja-DioMIEGF.js +0 -15
  220. package/dist/assets/ko-BqPgaQJb.js +0 -15
  221. package/dist/assets/main-CYgSu6dx.js +0 -928
  222. package/dist/assets/markdownCore-CWRCqSH2.js +0 -3
  223. package/dist/assets/patchFileDiff-BpMQZnYu.js +0 -10
  224. package/dist/assets/pl-slULBF4R.js +0 -15
  225. package/dist/assets/pt-BR-BqH8fuCH.js +0 -15
  226. package/dist/assets/renderElectronMiniChatApp-D77bmLqo.js +0 -2
  227. package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
  228. package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
  229. package/dist/assets/scheduledTasksApi-BSRCtP6T.js +0 -1
  230. package/dist/assets/tr-CjEbw1Aw.js +0 -6
  231. package/dist/assets/uk-BkkfxbOs.js +0 -15
  232. package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
  233. package/dist/assets/useRootScrollLock-C0N9mCwz.js +0 -1
  234. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  235. package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
  236. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
  237. package/dist/assets/zh-CN-C6FMdVgv.js +0 -15
  238. package/dist/assets/zh-TW-CdcMjDxv.js +0 -15
package/dist/index.html CHANGED
@@ -509,16 +509,16 @@
509
509
  pointer-events: none;
510
510
  }
511
511
  </style>
512
- <script type="module" crossorigin src="/assets/main-B2a_JfKZ.js"></script>
512
+ <script type="module" crossorigin src="/assets/main-C9ETLKFc.js"></script>
513
513
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
514
514
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
515
515
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
516
516
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
517
- <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BLfBSgBh.js">
517
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
518
518
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
519
- <link rel="modulepreload" crossorigin href="/assets/index-qES19dlJ.js">
520
- <link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-C9EcRSDF.js">
521
- <link rel="stylesheet" crossorigin href="/assets/index-L_ncV-6H.css">
519
+ <link rel="modulepreload" crossorigin href="/assets/index-BqhiCmYS.js">
520
+ <link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-CirXEPJq.js">
521
+ <link rel="stylesheet" crossorigin href="/assets/index-FqSOBz0J.css">
522
522
  </head>
523
523
  <body class="h-full bg-background text-foreground">
524
524
  <div id="root" class="h-full">
@@ -603,44 +603,6 @@
603
603
  }, 10000);
604
604
  </script>
605
605
 
606
- <!-- CSS Font Loading API for reliable Nerd Font loading. The ~2 MB of
607
- Nerd Fonts are only used by the terminal viewport, so they are no
608
- longer downloaded on every cold start: the terminal requests them on
609
- first mount through this idempotent hook. -->
610
- <script>
611
- (function() {
612
- let nerdFontsPromise = null;
613
- window.__openchamberEnsureNerdFonts = function() {
614
- if (nerdFontsPromise) {
615
- return nerdFontsPromise;
616
- }
617
- const fonts = [
618
- {
619
- name: 'JetBrainsMono Nerd Font',
620
- url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2'
621
- },
622
- {
623
- name: 'FiraCode Nerd Font',
624
- url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2'
625
- }
626
- ];
627
-
628
- const fontPromises = fonts.map(font => {
629
- const fontFace = new FontFace(font.name, `url(${font.url}) format('woff2')`);
630
- document.fonts.add(fontFace);
631
- return fontFace.load().catch(err => {
632
- console.warn(`Failed to load font: ${font.name}`, err);
633
- });
634
- });
635
-
636
- nerdFontsPromise = Promise.allSettled(fontPromises).then(() => {
637
- document.documentElement.classList.add('fonts-loaded');
638
- });
639
- return nerdFontsPromise;
640
- };
641
- })();
642
- </script>
643
-
644
606
  <!-- Polyfill for process before loading React -->
645
607
  <script>
646
608
  if (typeof process === 'undefined') {
@@ -74,15 +74,15 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-B3CSU_Ps.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-B4Ytoq2n.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
82
- <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BLfBSgBh.js">
82
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
84
- <link rel="modulepreload" crossorigin href="/assets/index-qES19dlJ.js">
85
- <link rel="stylesheet" crossorigin href="/assets/index-L_ncV-6H.css">
84
+ <link rel="modulepreload" crossorigin href="/assets/index-BqhiCmYS.js">
85
+ <link rel="stylesheet" crossorigin href="/assets/index-FqSOBz0J.css">
86
86
  </head>
87
87
  <body class="h-full bg-background text-foreground">
88
88
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,15 +63,15 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-QNcFIVCW.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-BUT6-lo1.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
70
70
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
71
- <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BLfBSgBh.js">
71
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
72
72
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
73
- <link rel="modulepreload" crossorigin href="/assets/index-qES19dlJ.js">
74
- <link rel="stylesheet" crossorigin href="/assets/index-L_ncV-6H.css">
73
+ <link rel="modulepreload" crossorigin href="/assets/index-BqhiCmYS.js">
74
+ <link rel="stylesheet" crossorigin href="/assets/index-FqSOBz0J.css">
75
75
  </head>
76
76
  <body class="h-full bg-background text-foreground">
77
77
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantran-se/openchamber-web",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -14,7 +14,7 @@
14
14
  "scripts": {
15
15
  "dev": "bun run build:watch",
16
16
  "dev:server": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} bun server/index.js --port ${OPENCHAMBER_PORT:-3001}",
17
- "dev:server:watch": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} nodemon --watch server --ext js --exec \"bun server/index.js --port ${OPENCHAMBER_PORT:-3001}\"",
17
+ "dev:server:watch": "node ./scripts/dev-server-watch.mjs",
18
18
  "build": "vite build",
19
19
  "build:watch": "vite build --watch",
20
20
  "type-check": "tsc --noEmit",
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^1.1.0",
27
27
  "@octokit/rest": "^22.0.1",
28
- "@opencode-ai/sdk": "1.18.25",
28
+ "@opencode-ai/sdk": "1.18.30",
29
29
  "@simplewebauthn/server": "13.3.1",
30
30
  "bun-pty": "^0.4.5",
31
31
  "compression": "^1.8.1",
@@ -77,7 +77,6 @@
77
77
  "eslint": "^9.33.0",
78
78
  "eslint-plugin-react-hooks": "^5.2.0",
79
79
  "eslint-plugin-react-refresh": "^0.5.0",
80
- "ghostty-web": "0.4.0",
81
80
  "globals": "^16.3.0",
82
81
  "next-themes": "^0.4.6",
83
82
  "nodemon": "^3.1.7",
@@ -107,5 +106,9 @@
107
106
  "public",
108
107
  "package.json",
109
108
  "README.md"
110
- ]
109
+ ],
110
+ "repository": {
111
+ "type": "git",
112
+ "url": "git+https://github.com/vantran-se/openchamber.git"
113
+ }
111
114
  }
package/server/index.d.ts CHANGED
@@ -11,12 +11,27 @@ export interface WebUiServerController {
11
11
  stop: (options?: { exitProcess?: boolean }) => Promise<void>;
12
12
  }
13
13
 
14
+ export interface DesktopUpdateInfo {
15
+ available: boolean;
16
+ currentVersion?: string;
17
+ version?: string | null;
18
+ body?: string | null;
19
+ date?: string | null;
20
+ }
21
+
22
+ export interface DesktopUpdater {
23
+ check: () => Promise<DesktopUpdateInfo>;
24
+ install: () => Promise<DesktopUpdateInfo>;
25
+ restart: () => Promise<void> | void;
26
+ }
27
+
14
28
  export interface StartWebUiServerOptions {
15
29
  port?: number;
16
30
  host?: string;
17
31
  attachSignals?: boolean;
18
32
  exitOnShutdown?: boolean;
19
33
  uiPassword?: string | null;
34
+ desktopUpdater?: DesktopUpdater;
20
35
  }
21
36
 
22
37
  export declare function startWebUiServer(
package/server/index.js CHANGED
@@ -91,8 +91,10 @@ import { createPushRuntime } from './lib/notifications/push-runtime.js';
91
91
  import { createApnsRuntime } from './lib/notifications/apns-runtime.js';
92
92
  import { createNotificationTemplateRuntime } from './lib/notifications/template-runtime.js';
93
93
  import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/runtime.js';
94
+ import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
94
95
  import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
95
96
  import { createProjectConfigRuntime } from './lib/projects/project-config.js';
97
+ import { migrateLegacyUserDirs } from './lib/data-dir-migration.js';
96
98
  import { createProjectContextRuntime } from './lib/project-context/runtime.js';
97
99
  import { createAgentMemoryRuntime } from './lib/agent-memory/runtime.js';
98
100
  import { createAgentMemoryActions } from './lib/agent-memory/actions.js';
@@ -110,11 +112,16 @@ import { createDevServerScanner } from './lib/dev-servers/routes.js';
110
112
  import { createDevTunnelRuntime } from './lib/dev-tunnel/runtime.js';
111
113
  import { registerBrowserControlRoutes } from './lib/browser-control/routes.js';
112
114
  import { createSystemPromptRuntime } from './lib/system-prompt/runtime.js';
115
+ import { createMcpReconnectRuntime } from './lib/mcp-reconnect/runtime.js';
113
116
  import { createOpenChamberSessionService } from './lib/openchamber-sessions/routes.js';
114
117
  import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
115
118
  import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
116
119
  import { OpenChamberControlError } from './lib/openchamber-control/error.js';
117
120
  import webPush from 'web-push';
121
+ import { applyConnectAttemptTimeout } from './lib/network-defaults.js';
122
+
123
+ // Background CLI launches enter here in a fresh process, without CLI defaults.
124
+ applyConnectAttemptTimeout();
118
125
 
119
126
  const __filename = fileURLToPath(import.meta.url);
120
127
  const __dirname = path.dirname(__filename);
@@ -257,16 +264,27 @@ const normalizeManagedRemoteTunnelPresets = (...args) =>
257
264
  const normalizeManagedRemoteTunnelPresetTokens = (...args) =>
258
265
  settingsNormalizationRuntime.normalizeManagedRemoteTunnelPresetTokens(...args);
259
266
  const isUnsafeSkillRelativePath = (...args) => settingsNormalizationRuntime.isUnsafeSkillRelativePath(...args);
260
- const sanitizeTypographySizesPartial = (...args) =>
261
- settingsNormalizationRuntime.sanitizeTypographySizesPartial(...args);
262
267
  const normalizeStringArray = (...args) => settingsNormalizationRuntime.normalizeStringArray(...args);
263
268
  const sanitizeModelRefs = (...args) => settingsNormalizationRuntime.sanitizeModelRefs(...args);
264
269
  const sanitizeSkillCatalogs = (...args) => settingsNormalizationRuntime.sanitizeSkillCatalogs(...args);
265
270
  const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProjects(...args);
266
271
 
267
- const OPENCHAMBER_USER_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
272
+ // Every OpenChamber-owned file and folder hangs off one root: the default
273
+ // `~/.config/openchamber`, or `OPENCHAMBER_DATA_DIR` when set. The user
274
+ // folders (`projects/`, `themes/`, `speech-models/`) are copied into a custom
275
+ // root once at startup (`migrateLegacyUserDirs`), because they used to ignore
276
+ // the variable.
277
+ const OPENCHAMBER_DEFAULT_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
278
+ const OPENCHAMBER_USER_CONFIG_ROOT = process.env.OPENCHAMBER_DATA_DIR
279
+ ? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
280
+ : OPENCHAMBER_DEFAULT_CONFIG_ROOT;
268
281
  const OPENCHAMBER_USER_THEMES_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'themes');
269
282
  const OPENCHAMBER_PROJECTS_CONFIG_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'projects');
283
+ // OPENCHAMBER_CHATS_DIR relocates managed chat worktrees — needed when the
284
+ // OpenCode server runs as a separate user that cannot traverse $HOME.
285
+ const OPENCHAMBER_CHATS_DIR = process.env.OPENCHAMBER_CHATS_DIR && process.env.OPENCHAMBER_CHATS_DIR.trim()
286
+ ? path.resolve(process.env.OPENCHAMBER_CHATS_DIR.trim())
287
+ : path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats');
270
288
 
271
289
  const MAX_THEME_JSON_BYTES = 512 * 1024;
272
290
 
@@ -284,6 +302,7 @@ const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromD
284
302
  let notificationTemplateRuntime = null;
285
303
  let agentToolRuntime = null;
286
304
  let systemPromptRuntime = null;
305
+ let mcpReconnectRuntime = null;
287
306
 
288
307
  const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
289
308
  const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
@@ -297,9 +316,7 @@ const maybeCacheSessionInfoFromEvent = (...args) => notificationTemplateRuntime.
297
316
  const buildTemplateVariables = (...args) => notificationTemplateRuntime.buildTemplateVariables(...args);
298
317
  const getCachedZenModels = (...args) => notificationTemplateRuntime.getCachedZenModels(...args);
299
318
 
300
- const OPENCHAMBER_DATA_DIR = process.env.OPENCHAMBER_DATA_DIR
301
- ? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
302
- : path.join(os.homedir(), '.config', 'openchamber');
319
+ const OPENCHAMBER_DATA_DIR = OPENCHAMBER_USER_CONFIG_ROOT;
303
320
  const SETTINGS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'settings.json');
304
321
  const PUSH_SUBSCRIPTIONS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'push-subscriptions.json');
305
322
  const APNS_TOKENS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'apns-tokens.json');
@@ -337,7 +354,6 @@ const settingsHelpers = createSettingsHelpers({
337
354
  normalizeManagedRemoteTunnelHostname,
338
355
  normalizeManagedRemoteTunnelPresets,
339
356
  normalizeManagedRemoteTunnelPresetTokens,
340
- sanitizeTypographySizesPartial,
341
357
  normalizeStringArray,
342
358
  sanitizeModelRefs,
343
359
  sanitizeSkillCatalogs,
@@ -476,6 +492,17 @@ const getUpstreamStallTimeoutMs = () => (
476
492
  : DEFAULT_UPSTREAM_STALL_TIMEOUT_MS
477
493
  );
478
494
 
495
+ const movedUserDirs = await migrateLegacyUserDirs({
496
+ fsPromises,
497
+ path,
498
+ dataDir: OPENCHAMBER_USER_CONFIG_ROOT,
499
+ legacyRoot: OPENCHAMBER_DEFAULT_CONFIG_ROOT,
500
+ warn: (message) => console.warn(`[data-dir] ${message}`),
501
+ });
502
+ if (movedUserDirs.length > 0) {
503
+ console.log(`[data-dir] Copied ${movedUserDirs.join(', ')} into ${OPENCHAMBER_USER_CONFIG_ROOT}`);
504
+ }
505
+
479
506
  const projectConfigRuntime = createProjectConfigRuntime({
480
507
  fsPromises,
481
508
  path,
@@ -486,6 +513,7 @@ const projectContextRuntime = createProjectContextRuntime({
486
513
  fsPromises,
487
514
  path,
488
515
  projectsDirPath: OPENCHAMBER_PROJECTS_CONFIG_DIR,
516
+ resolveSharedPlansDir: (projectId) => projectConfigRuntime.resolveSharedPlansDir(projectId),
489
517
  });
490
518
 
491
519
  const agentMemoryRuntime = createAgentMemoryRuntime({
@@ -878,6 +906,25 @@ notificationTriggerRuntime.setGetIsSessionAutoAccepting(
878
906
  (sessionId, directory) => permissionAutoAcceptRuntime.isSessionAutoAccepting(sessionId, directory),
879
907
  );
880
908
 
909
+ // Queued follow-up messages are delivered by the server so a closed tab or a
910
+ // dropped connection no longer strands them (VS Code keeps its UI-side queue).
911
+ const messageQueueRuntime = createMessageQueueRuntime({
912
+ globalEventHub: globalMessageStreamHub,
913
+ buildOpenCodeUrl,
914
+ getOpenCodeAuthHeaders,
915
+ sessionKnowledgeRuntime,
916
+ // OpenCode's /global/event SSE proxy cannot carry OpenChamber-owned events.
917
+ // Use the shared control stream for SSE clients and the existing WS fan-out.
918
+ broadcastGlobalUiEvent: createGlobalUiEventBroadcaster({
919
+ sseClients: uiOpenChamberEventClients,
920
+ wsClients: uiNotificationWsClients,
921
+ writeSseEvent,
922
+ }),
923
+ onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
924
+ dataDir: OPENCHAMBER_DATA_DIR,
925
+ });
926
+ messageQueueRuntime.start();
927
+
881
928
  const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
882
929
  waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
883
930
  buildOpenCodeUrl,
@@ -1208,11 +1255,15 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
1208
1255
  const managedEnv = includeControl || includeWeb || includeMemory
1209
1256
  ? await (agentToolRuntime?.prepareManagedOpenCodeEnv({ includeControl, includeWeb, includeMemory }) || {})
1210
1257
  : {};
1211
- if (settings?.optimizeSystemPrompt !== true) return managedEnv;
1212
1258
 
1213
- const configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
1214
- const systemPromptEnv = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent);
1215
- return { ...managedEnv, ...systemPromptEnv };
1259
+ // Each managed plugin appends itself to the config the previous one produced.
1260
+ let configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
1261
+ if (settings?.optimizeSystemPrompt === true) {
1262
+ ({ OPENCODE_CONFIG_CONTENT: configContent } = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent));
1263
+ }
1264
+ // Always on for managed OpenCode: it only retries servers OpenCode gave up on.
1265
+ const mcpReconnectEnv = await mcpReconnectRuntime.prepareManagedOpenCodeEnv(configContent);
1266
+ return { ...managedEnv, ...mcpReconnectEnv };
1216
1267
  },
1217
1268
  });
1218
1269
 
@@ -1295,7 +1346,7 @@ const resolveMemoryProjectId = createMemoryProjectResolver({
1295
1346
  return sanitizeProjects(settings?.projects || []).map((project) => project.path);
1296
1347
  },
1297
1348
  resolvePrimaryWorktreeRoot,
1298
- managedProjectRoots: [path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats')],
1349
+ managedProjectRoots: [...new Set([path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats'), OPENCHAMBER_CHATS_DIR])],
1299
1350
  });
1300
1351
 
1301
1352
  /**
@@ -1427,6 +1478,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1427
1478
  sessionAssistRuntime,
1428
1479
  sessionGoalRuntime,
1429
1480
  contextObligatoryRuntime,
1481
+ messageQueueRuntime,
1430
1482
  sessionRuntime,
1431
1483
  getHealthCheckInterval: () => healthCheckInterval,
1432
1484
  clearHealthCheckInterval: (value) => clearInterval(value),
@@ -1485,6 +1537,11 @@ async function main(options = {}) {
1485
1537
  path,
1486
1538
  dataDir: OPENCHAMBER_DATA_DIR,
1487
1539
  });
1540
+ mcpReconnectRuntime = createMcpReconnectRuntime({
1541
+ fsPromises,
1542
+ path,
1543
+ dataDir: OPENCHAMBER_DATA_DIR,
1544
+ });
1488
1545
 
1489
1546
  // Pairing transports advertised to the create-device dialog. LAN reachability is
1490
1547
  // derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
@@ -1611,6 +1668,12 @@ async function main(options = {}) {
1611
1668
  const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
1612
1669
  ? options.getDesktopRuntimeConfig
1613
1670
  : null;
1671
+ const desktopUpdater = options.desktopUpdater
1672
+ && typeof options.desktopUpdater.check === 'function'
1673
+ && typeof options.desktopUpdater.install === 'function'
1674
+ && typeof options.desktopUpdater.restart === 'function'
1675
+ ? options.desktopUpdater
1676
+ : null;
1614
1677
 
1615
1678
  console.log(`Starting OpenChamber on port ${port === 0 ? 'auto' : port}`);
1616
1679
 
@@ -1645,7 +1708,10 @@ async function main(options = {}) {
1645
1708
  res.setHeader('Access-Control-Allow-Origin', origin);
1646
1709
  res.setHeader('Access-Control-Allow-Credentials', 'true');
1647
1710
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
1648
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Requested-With,Cache-Control,X-OpenCode-Directory,X-OpenCode-Directory-Encoding,Ngrok-Skip-Browser-Warning');
1711
+ // The packaged desktop UI (openchamber-ui://) and the dev UI sit on a
1712
+ // different origin, so every custom request header must be listed here or
1713
+ // the browser refuses the request at preflight, before it reaches a route.
1714
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Requested-With,Cache-Control,X-OpenCode-Directory,X-OpenCode-Directory-Encoding,Ngrok-Skip-Browser-Warning,X-OpenChamber-Surface');
1649
1715
  res.setHeader('Access-Control-Expose-Headers', 'x-next-cursor');
1650
1716
  res.setHeader('Vary', 'Origin');
1651
1717
  if (req.method === 'OPTIONS') {
@@ -1786,6 +1852,7 @@ async function main(options = {}) {
1786
1852
  getCachedZenModels,
1787
1853
  setAutoAcceptSession,
1788
1854
  agentToolRuntime,
1855
+ desktopUpdater,
1789
1856
  });
1790
1857
  uiAuthController = bootstrapResult.uiAuthController;
1791
1858
  realtimeProxyRuntime = attachRealtimeProxy({
@@ -1877,6 +1944,7 @@ async function main(options = {}) {
1877
1944
  createFsSearchRuntime: createFsSearchRuntimeFactory,
1878
1945
  openchamberDataDir: OPENCHAMBER_DATA_DIR,
1879
1946
  openchamberUserConfigRoot: OPENCHAMBER_USER_CONFIG_ROOT,
1947
+ managedChatsRoot: OPENCHAMBER_CHATS_DIR,
1880
1948
  normalizeDirectoryPath,
1881
1949
  resolveProjectDirectory,
1882
1950
  resolveOptionalProjectDirectory,
@@ -1914,6 +1982,7 @@ async function main(options = {}) {
1914
1982
  getOpenChamberEventClients: () => uiOpenChamberEventClients,
1915
1983
  writeSseEvent,
1916
1984
  permissionAutoAcceptRuntime,
1985
+ messageQueueRuntime,
1917
1986
  });
1918
1987
 
1919
1988
  const startupPipelineResult = await startupPipelineRuntime.run({
@@ -1,5 +1,5 @@
1
- import { parse as parseJsonc } from 'jsonc-parser';
2
1
  import { pathToFileURL } from 'node:url';
2
+ import { appendManagedPlugin } from '../opencode/managed-plugin-config.js';
3
3
  import {
4
4
  OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS,
5
5
  OPENCHAMBER_AGENT_TOOL_ACTIONS,
@@ -252,23 +252,6 @@ ${entries.join('')} },
252
252
  `;
253
253
  };
254
254
 
255
- const mergePluginConfig = (rawConfig, pluginUrl) => {
256
- const errors = [];
257
- const parsed = asNonEmptyString(rawConfig) ? parseJsonc(rawConfig, errors, { allowTrailingComma: true }) : {};
258
- if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
259
- throw new Error('OPENCODE_CONFIG_CONTENT must contain a valid JSON object before OpenChamber can inject its managed tool');
260
- }
261
- if (parsed.plugin !== undefined && !Array.isArray(parsed.plugin)) {
262
- throw new Error('OPENCODE_CONFIG_CONTENT plugin must be an array before OpenChamber can inject its managed tool');
263
- }
264
- const configured = Array.isArray(parsed.plugin) ? parsed.plugin : [];
265
- parsed.plugin = [
266
- ...configured.filter((value) => value !== pluginUrl && (!Array.isArray(value) || value[0] !== pluginUrl)),
267
- pluginUrl,
268
- ];
269
- return JSON.stringify(parsed);
270
- };
271
-
272
255
  export const createAgentToolRuntime = (dependencies) => {
273
256
  const {
274
257
  crypto,
@@ -296,7 +279,7 @@ export const createAgentToolRuntime = (dependencies) => {
296
279
  activeToken = crypto.randomBytes(32).toString('base64url');
297
280
  const pluginUrl = pathToFileURL(pluginPath).href;
298
281
  return {
299
- OPENCODE_CONFIG_CONTENT: mergePluginConfig(env.OPENCODE_CONFIG_CONTENT, pluginUrl),
282
+ OPENCODE_CONFIG_CONTENT: appendManagedPlugin(env.OPENCODE_CONFIG_CONTENT, pluginUrl, 'managed tool'),
300
283
  OPENCHAMBER_AGENT_TOOL_URL: `http://127.0.0.1:${port}/api/openchamber/agent-tool`,
301
284
  OPENCHAMBER_AGENT_TOOL_TOKEN: activeToken,
302
285
  };
@@ -0,0 +1,82 @@
1
+ // Release notes for the update dialog.
2
+ //
3
+ // The repo publishes `changelog/index.json` on `main`: one object per release,
4
+ // newest first, with the app notes grouped as New / Improvements / Fixes /
5
+ // Misc. This module fetches it and renders the releases between the installed
6
+ // version (exclusive) and the offered one (inclusive) as the Markdown the
7
+ // dialog already understands: a `## [x.y.z] - YYYY-MM-DD` header per release,
8
+ // the release title in bold, the intro, then the groups.
9
+ //
10
+ // Any failure (network, 404, unexpected shape) yields null: the update is
11
+ // still offered, only without notes.
12
+
13
+ export const CHANGELOG_INDEX_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/changelog/index.json';
14
+
15
+ const GROUPS = [
16
+ ['new', 'New'],
17
+ ['improvements', 'Improvements'],
18
+ ['fixes', 'Fixes'],
19
+ ['misc', 'Misc'],
20
+ ];
21
+
22
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
23
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
24
+
25
+ // The file crosses a network boundary with no schema library on this side, so
26
+ // each field is coerced into its domain shape here; a release without a valid
27
+ // version and date is dropped.
28
+ const text = (value) => (value === null || value === undefined ? null : String(value).trim() || null);
29
+ const bulletList = (value) => (Array.isArray(value) ? value.map((item) => String(item)) : []);
30
+
31
+ const parseRelease = (entry) => {
32
+ if (entry === null || entry === undefined) return null;
33
+ const version = text(entry.version);
34
+ const date = text(entry.date);
35
+ if (!version || !VERSION_PATTERN.test(version) || !date || !DATE_PATTERN.test(date)) return null;
36
+ const app = entry.app ?? {};
37
+ return {
38
+ version,
39
+ date,
40
+ title: text(entry.title),
41
+ intro: text(entry.intro),
42
+ groups: GROUPS.map(([key, heading]) => ({ heading, bullets: bulletList(app[key]) })),
43
+ };
44
+ };
45
+
46
+ const renderRelease = (release) => {
47
+ const lines = [`## [${release.version}] - ${release.date}`, ''];
48
+ if (release.title) lines.push(`**${release.title}**`, '');
49
+ if (release.intro) lines.push(release.intro, '');
50
+ for (const { heading, bullets } of release.groups) {
51
+ if (bullets.length === 0) continue;
52
+ lines.push(`### ${heading}`, '', ...bullets.map((bullet) => `- ${bullet}`), '');
53
+ }
54
+ return lines.join('\n').trim();
55
+ };
56
+
57
+ /**
58
+ * Markdown for the releases in (fromVersion, toVersion], newest first, or
59
+ * null when the index holds none of them.
60
+ */
61
+ export function renderUpdateNotes(index, fromVersion, toVersion, compareVersions) {
62
+ if (!Array.isArray(index)) return null;
63
+ const relevant = index
64
+ .map(parseRelease)
65
+ .filter((release) => release !== null)
66
+ .filter((release) => compareVersions(release.version, fromVersion) > 0 && compareVersions(release.version, toVersion) <= 0)
67
+ .sort((a, b) => compareVersions(b.version, a.version));
68
+ if (relevant.length === 0) return null;
69
+ return relevant.map(renderRelease).join('\n\n');
70
+ }
71
+
72
+ /** Fetch the index and render the notes for one update; null on any failure. */
73
+ export async function fetchUpdateNotes(fromVersion, toVersion, compareVersions, options = {}) {
74
+ const fetchImpl = options.fetch ?? fetch;
75
+ try {
76
+ const response = await fetchImpl(CHANGELOG_INDEX_URL, { signal: AbortSignal.timeout(options.timeoutMs ?? 10_000) });
77
+ if (!response.ok) return null;
78
+ return renderUpdateNotes(await response.json(), fromVersion, toVersion, compareVersions);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { fetchUpdateNotes, renderUpdateNotes } from './update-notes.js';
3
+
4
+ const compare = (a, b) => {
5
+ const pa = a.split('.').map(Number);
6
+ const pb = b.split('.').map(Number);
7
+ for (let i = 0; i < 3; i += 1) if (pa[i] !== pb[i]) return pa[i] - pb[i];
8
+ return 0;
9
+ };
10
+
11
+ const index = [
12
+ { version: '1.2.3', date: '2026-03-03', title: 'Comments everywhere', intro: 'A short intro.', app: { new: ['**Comments:** on code.'], improvements: [], fixes: ['Chat: no freeze.'], misc: [] }, vscode: null },
13
+ { version: '1.2.2', date: '2026-03-02', title: null, intro: null, app: { new: [], improvements: ['Faster.'], fixes: [], misc: [] }, vscode: null },
14
+ { version: '1.2.1', date: '2026-03-01', title: 'Old', intro: null, app: { new: ['Older.'], improvements: [], fixes: [], misc: [] }, vscode: null },
15
+ ];
16
+
17
+ describe('renderUpdateNotes', () => {
18
+ it('renders the releases after the installed version up to the offered one, newest first', () => {
19
+ expect(renderUpdateNotes(index, '1.2.1', '1.2.3', compare)).toBe(`## [1.2.3] - 2026-03-03
20
+
21
+ **Comments everywhere**
22
+
23
+ A short intro.
24
+
25
+ ### New
26
+
27
+ - **Comments:** on code.
28
+
29
+ ### Fixes
30
+
31
+ - Chat: no freeze.
32
+
33
+ ## [1.2.2] - 2026-03-02
34
+
35
+ ### Improvements
36
+
37
+ - Faster.`);
38
+ });
39
+
40
+ it('returns null when nothing lies in the range or the payload is not an index', () => {
41
+ expect(renderUpdateNotes(index, '1.2.3', '1.2.3', compare)).toBe(null);
42
+ expect(renderUpdateNotes({ entries: [] }, '1.0.0', '9.9.9', compare)).toBe(null);
43
+ expect(renderUpdateNotes([{ version: 'nope' }, { version: '1.2.2', date: '2026' }], '1.0.0', '9.9.9', compare)).toBe(null);
44
+ });
45
+ });
46
+
47
+ describe('fetchUpdateNotes', () => {
48
+ it('turns a failed request or a thrown fetch into null', async () => {
49
+ const notFound = async () => ({ ok: false });
50
+ expect(await fetchUpdateNotes('1.0.0', '2.0.0', compare, { fetch: notFound })).toBe(null);
51
+ const throwing = async () => { throw new Error('offline'); };
52
+ expect(await fetchUpdateNotes('1.0.0', '2.0.0', compare, { fetch: throwing })).toBe(null);
53
+ });
54
+
55
+ it('renders a successful response', async () => {
56
+ const ok = async () => ({ ok: true, json: async () => index });
57
+ expect(await fetchUpdateNotes('1.2.2', '1.2.3', compare, { fetch: ok })).toMatch(/^## \[1\.2\.3\] - 2026-03-03\n\n\*\*Comments everywhere\*\*/);
58
+ });
59
+ });
@@ -0,0 +1,40 @@
1
+ // One-time copy of the user-authored folders into a custom data directory.
2
+ //
3
+ // `OPENCHAMBER_DATA_DIR` is documented as "the OpenChamber data directory",
4
+ // but for a long time only the flat files (settings, auth, push tokens)
5
+ // followed it while `projects/`, `themes/`, and `speech-models/` stayed under
6
+ // `~/.config/openchamber`. Now every folder hangs off the data directory, so an
7
+ // instance that already ran with a custom directory finds those folders in
8
+ // the old place. This copies each one over once: only when the new location
9
+ // does not exist yet and the old one does. It copies rather than moves
10
+ // because a second instance on the same machine (a custom directory next to
11
+ // the default one) must not strip the default instance of its projects.
12
+
13
+ const USER_DIR_ENTRIES = Object.freeze(['projects', 'themes', 'speech-models']);
14
+
15
+ const exists = async (fsPromises, target) => fsPromises.access(target).then(() => true, () => false);
16
+
17
+ /**
18
+ * Copy the user folders from `legacyRoot` into `dataDir`. Returns the entries
19
+ * copied. A no-op when the two roots are the same directory. A folder whose
20
+ * copy fails is reported through `warn` and its partial copy removed; the
21
+ * server keeps starting, reading the (empty) new location.
22
+ */
23
+ export const migrateLegacyUserDirs = async ({ fsPromises, path, dataDir, legacyRoot, warn = () => {}, entries = USER_DIR_ENTRIES }) => {
24
+ if (path.resolve(dataDir) === path.resolve(legacyRoot)) return [];
25
+ const moved = [];
26
+ for (const entry of entries) {
27
+ const from = path.join(legacyRoot, entry);
28
+ const to = path.join(dataDir, entry);
29
+ if (await exists(fsPromises, to) || !(await exists(fsPromises, from))) continue;
30
+ try {
31
+ await fsPromises.mkdir(dataDir, { recursive: true });
32
+ await fsPromises.cp(from, to, { recursive: true, errorOnExist: true, force: false });
33
+ moved.push(entry);
34
+ } catch (error) {
35
+ await fsPromises.rm(to, { recursive: true, force: true }).catch(() => {});
36
+ warn(`Failed to copy ${from} to ${to}: ${error instanceof Error ? error.message : String(error)}`);
37
+ }
38
+ }
39
+ return moved;
40
+ };