@vantran-se/openchamber-web 1.22.1 → 1.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +2 -3
  2. package/bin/cli.js +4 -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-CV1HNV5l.js} +1 -1
  7. package/dist/assets/{DiffView-Dh1ey2x9.js → DiffView-DkkSa30n.js} +3 -3
  8. package/dist/assets/DirtyBranchSwitchDialog-DW4WaXPc.js +1 -0
  9. package/dist/assets/FilesView-DDu8zDy5.js +17 -0
  10. package/dist/assets/GitView-1Ixo8z-M.js +6 -0
  11. package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-DvCLL7dT.js} +1 -1
  12. package/dist/assets/LinearIssuesView-Bs1rZrF2.js +1 -0
  13. package/dist/assets/{MarkdownImageGallery-bA0PS-tn.js → MarkdownImageGallery-DyJUc6h9.js} +1 -1
  14. package/dist/assets/MarkdownRendererImpl-Bdm8vMkS.js +12 -0
  15. package/dist/assets/{OnboardingScreen-D3kYJIb6.js → OnboardingScreen-Dg66B6oB.js} +1 -1
  16. package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-CAduNjfZ.js} +1 -1
  17. package/dist/assets/{PierreDiffViewer-Ct2A13NO.js → PierreDiffViewer-CTIb1GYD.js} +2 -2
  18. package/dist/assets/PlanView-trDaszfq.js +5 -0
  19. package/dist/assets/SettingsView-DStMfBmy.js +28 -0
  20. package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DQQFnrSZ.js} +1 -1
  21. package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-SjY2XZlA.js} +6 -6
  22. package/dist/assets/ToolPartDiffPreview-DJdzUQPE.js +8 -0
  23. package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-Iehm4iMH.js} +1 -1
  24. package/dist/assets/{WalkthroughView-AX-u2pFg.js → WalkthroughView-BALp2d9O.js} +2 -2
  25. package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-BKZA43vN.js} +2 -2
  26. package/dist/assets/{contextPanelEmbeddedChat-C9EcRSDF.js → contextPanelEmbeddedChat-D2PM_pEv.js} +1 -1
  27. package/dist/assets/{de-DqVn5vme.js → de-DlSNLAcy.js} +6 -5
  28. package/dist/assets/{es-hj43Z53i.js → es-D1GbowZ3.js} +6 -5
  29. package/dist/assets/{fr-BmkZt2mF.js → fr-BPOLXIAY.js} +6 -5
  30. package/dist/assets/{index-DmhujXbT.js → index-Ce0LR32V.js} +1 -1
  31. package/dist/assets/index-D42fPvt4.css +1 -0
  32. package/dist/assets/index-DlFbCkEy.js +39 -0
  33. package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-DaB2u_Kv.js} +1 -1
  34. package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-yJZa2IAC.js} +1 -1
  35. package/dist/assets/{ja-DioMIEGF.js → ja-Dm2-m9io.js} +7 -6
  36. package/dist/assets/{ko-BqPgaQJb.js → ko-CIXlzQ5-.js} +7 -6
  37. package/dist/assets/main-B7qTsd_w.js +930 -0
  38. package/dist/assets/{main-B2a_JfKZ.js → main-CyQrD9dG.js} +2 -2
  39. package/dist/assets/{markdownCore-CWRCqSH2.js → markdownCore-CeEwGgEK.js} +2 -2
  40. package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-7cPEx4sV.js} +2 -2
  41. package/dist/assets/{mobile-QNcFIVCW.js → mobile-QtzkPTM8.js} +2 -2
  42. package/dist/assets/{patchFileDiff-BpMQZnYu.js → patchFileDiff-No55fDiD.js} +1 -1
  43. package/dist/assets/pl-B2zmkOpw.js +16 -0
  44. package/dist/assets/{pt-BR-BqH8fuCH.js → pt-BR-C-JE5H6y.js} +6 -5
  45. package/dist/assets/{renderElectronMiniChatApp-D77bmLqo.js → renderElectronMiniChatApp-ePTkBJNS.js} +2 -2
  46. package/dist/assets/renderMobileApp-D9RjB9DX.js +25 -0
  47. package/dist/assets/runtimeEndpointReset-DfJIWwwo.js +17 -0
  48. package/dist/assets/{scheduledTasksApi-BSRCtP6T.js → scheduledTasksApi-DuhdyYEa.js} +1 -1
  49. package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-EHEg58-n.js} +1 -1
  50. package/dist/assets/tr-MJmRIzEY.js +7 -0
  51. package/dist/assets/{uk-BkkfxbOs.js → uk-BmD7eM8k.js} +6 -5
  52. package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-CuJPKtI1.js} +151 -180
  53. package/dist/assets/useInlineCommentController-DD7_7OP_.js +2 -0
  54. package/dist/assets/{useRootScrollLock-C0N9mCwz.js → useRootScrollLock-DRGAe8PN.js} +1 -1
  55. package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-9ZzoH0o0.js} +2 -2
  56. package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
  57. package/dist/assets/{web-components-BNMwa0qm.js → web-components-PttKBrhi.js} +1 -1
  58. package/dist/assets/{zh-CN-C6FMdVgv.js → zh-CN-JJHl4ehT.js} +6 -5
  59. package/dist/assets/{zh-TW-CdcMjDxv.js → zh-TW-DPc4DEEW.js} +6 -5
  60. package/dist/index.html +5 -5
  61. package/dist/mini-chat.html +4 -4
  62. package/dist/mobile.html +4 -4
  63. package/package.json +8 -4
  64. package/server/index.js +38 -5
  65. package/server/lib/agent-tool/runtime.js +2 -19
  66. package/server/lib/changelog/update-notes.js +82 -0
  67. package/server/lib/changelog/update-notes.test.js +59 -0
  68. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  69. package/server/lib/dev-tunnel/runtime.js +4 -4
  70. package/server/lib/dev-tunnel/tunnel.test.js +20 -0
  71. package/server/lib/fs/DOCUMENTATION.md +2 -0
  72. package/server/lib/fs/routes.js +32 -21
  73. package/server/lib/fs/routes.test.js +77 -0
  74. package/server/lib/git/DOCUMENTATION.md +3 -1
  75. package/server/lib/git/routes.js +16 -0
  76. package/server/lib/git/service.js +107 -17
  77. package/server/lib/git/service.test.js +171 -0
  78. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  79. package/server/lib/mcp-reconnect/runtime.js +122 -0
  80. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  81. package/server/lib/message-queue/DOCUMENTATION.md +158 -0
  82. package/server/lib/message-queue/runtime.js +870 -0
  83. package/server/lib/message-queue/runtime.test.js +482 -0
  84. package/server/lib/openchamber-sessions/routes.js +102 -0
  85. package/server/lib/openchamber-sessions/routes.test.js +88 -0
  86. package/server/lib/opencode/DOCUMENTATION.md +13 -5
  87. package/server/lib/opencode/config-paths.test.js +116 -0
  88. package/server/lib/opencode/core-routes.js +1 -0
  89. package/server/lib/opencode/feature-routes-runtime.js +5 -0
  90. package/server/lib/opencode/input-history-scope.js +14 -0
  91. package/server/lib/opencode/managed-plugin-config.js +35 -0
  92. package/server/lib/opencode/openchamber-routes.test.js +2 -2
  93. package/server/lib/opencode/plugins.js +2 -2
  94. package/server/lib/opencode/providers.js +44 -2
  95. package/server/lib/opencode/providers.test.js +131 -4
  96. package/server/lib/opencode/routes.js +4 -4
  97. package/server/lib/opencode/server-startup-runtime.js +1 -1
  98. package/server/lib/opencode/settings-helpers.js +28 -0
  99. package/server/lib/opencode/settings-helpers.test.js +177 -0
  100. package/server/lib/opencode/settings-runtime.js +2 -2
  101. package/server/lib/opencode/settings-runtime.test.js +29 -0
  102. package/server/lib/opencode/shared.js +4 -1
  103. package/server/lib/opencode/shutdown-runtime.js +2 -0
  104. package/server/lib/opencode/skill-routes.js +2 -1
  105. package/server/lib/opencode/snippets.js +1 -2
  106. package/server/lib/package-manager.js +4 -29
  107. package/server/lib/package-manager.test.js +2 -2
  108. package/server/lib/quota/DOCUMENTATION.md +2 -1
  109. package/server/lib/quota/credentials/providers.js +4 -0
  110. package/server/lib/quota/credentials/store.js +1 -1
  111. package/server/lib/quota/credentials/store.test.js +4 -4
  112. package/server/lib/quota/providers/exe-dev.js +75 -0
  113. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  114. package/server/lib/quota/providers/index.js +7 -0
  115. package/server/lib/quota/providers/opencode-go.js +1 -0
  116. package/server/lib/quota/providers/opencode-go.test.js +5 -1
  117. package/server/lib/quota/routes.js +2 -0
  118. package/server/lib/quota/utils/auth.js +1 -1
  119. package/server/lib/relay/DOCUMENTATION.md +1 -1
  120. package/server/lib/relay/host-client.test.js +9 -1
  121. package/server/lib/relay/tunnel-host.js +3 -1
  122. package/server/lib/relay/tunnel-host.test.js +9 -1
  123. package/server/lib/security/request-security.js +21 -6
  124. package/server/lib/security/request-security.test.js +42 -8
  125. package/server/lib/session-assist/runtime.js +1 -0
  126. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  127. package/server/lib/session-goal/create.js +3 -1
  128. package/server/lib/session-goal/runtime.js +77 -11
  129. package/server/lib/session-goal/runtime.test.js +426 -0
  130. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  131. package/server/lib/skills-catalog/install.js +3 -2
  132. package/server/lib/small-model/DOCUMENTATION.md +4 -0
  133. package/server/lib/small-model/call.js +8 -2
  134. package/server/lib/small-model/call.test.js +23 -0
  135. package/server/lib/small-model/index.js +2 -1
  136. package/server/lib/small-model/routes.js +2 -1
  137. package/server/lib/system-prompt/runtime.js +2 -21
  138. package/server/lib/terminal/DOCUMENTATION.md +11 -7
  139. package/server/lib/terminal/runtime.js +180 -26
  140. package/server/lib/terminal/runtime.test.js +553 -26
  141. package/server/lib/terminal/shells.js +17 -0
  142. package/server/lib/terminal/shells.test.js +32 -1
  143. package/server/lib/ui-auth/ui-auth.js +4 -1
  144. package/server/lib/ui-auth/ui-auth.test.js +16 -0
  145. package/server/lib/walkthrough/index.js +1 -0
  146. package/server/lib/walkthrough/routes.test.js +26 -5
  147. package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
  148. package/dist/assets/FilesView-3ZYP72ki.js +0 -17
  149. package/dist/assets/GitView-CMuN7z2Z.js +0 -6
  150. package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
  151. package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
  152. package/dist/assets/PlanView-6AUjzNQn.js +0 -5
  153. package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
  154. package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
  155. package/dist/assets/index-L_ncV-6H.css +0 -1
  156. package/dist/assets/index-qES19dlJ.js +0 -7
  157. package/dist/assets/main-CYgSu6dx.js +0 -928
  158. package/dist/assets/pl-slULBF4R.js +0 -15
  159. package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
  160. package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
  161. package/dist/assets/tr-CjEbw1Aw.js +0 -6
  162. package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
  163. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
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-CyQrD9dG.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-CK21ihK-.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-DlFbCkEy.js">
520
+ <link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-D2PM_pEv.js">
521
+ <link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.css">
522
522
  </head>
523
523
  <body class="h-full bg-background text-foreground">
524
524
  <div id="root" class="h-full">
@@ -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-7cPEx4sV.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-CK21ihK-.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-DlFbCkEy.js">
85
+ <link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.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-QtzkPTM8.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-CK21ihK-.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-DlFbCkEy.js">
74
+ <link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.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.22.2",
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.29",
29
29
  "@simplewebauthn/server": "13.3.1",
30
30
  "bun-pty": "^0.4.5",
31
31
  "compression": "^1.8.1",
@@ -107,5 +107,9 @@
107
107
  "public",
108
108
  "package.json",
109
109
  "README.md"
110
- ]
110
+ ],
111
+ "repository": {
112
+ "type": "git",
113
+ "url": "git+https://github.com/vantran-se/openchamber.git"
114
+ }
111
115
  }
package/server/index.js CHANGED
@@ -91,6 +91,7 @@ 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';
96
97
  import { createProjectContextRuntime } from './lib/project-context/runtime.js';
@@ -110,6 +111,7 @@ import { createDevServerScanner } from './lib/dev-servers/routes.js';
110
111
  import { createDevTunnelRuntime } from './lib/dev-tunnel/runtime.js';
111
112
  import { registerBrowserControlRoutes } from './lib/browser-control/routes.js';
112
113
  import { createSystemPromptRuntime } from './lib/system-prompt/runtime.js';
114
+ import { createMcpReconnectRuntime } from './lib/mcp-reconnect/runtime.js';
113
115
  import { createOpenChamberSessionService } from './lib/openchamber-sessions/routes.js';
114
116
  import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
115
117
  import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
@@ -267,6 +269,11 @@ const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProje
267
269
  const OPENCHAMBER_USER_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
268
270
  const OPENCHAMBER_USER_THEMES_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'themes');
269
271
  const OPENCHAMBER_PROJECTS_CONFIG_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'projects');
272
+ // OPENCHAMBER_CHATS_DIR relocates managed chat worktrees — needed when the
273
+ // OpenCode server runs as a separate user that cannot traverse $HOME.
274
+ const OPENCHAMBER_CHATS_DIR = process.env.OPENCHAMBER_CHATS_DIR && process.env.OPENCHAMBER_CHATS_DIR.trim()
275
+ ? path.resolve(process.env.OPENCHAMBER_CHATS_DIR.trim())
276
+ : path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats');
270
277
 
271
278
  const MAX_THEME_JSON_BYTES = 512 * 1024;
272
279
 
@@ -284,6 +291,7 @@ const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromD
284
291
  let notificationTemplateRuntime = null;
285
292
  let agentToolRuntime = null;
286
293
  let systemPromptRuntime = null;
294
+ let mcpReconnectRuntime = null;
287
295
 
288
296
  const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
289
297
  const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
@@ -878,6 +886,19 @@ notificationTriggerRuntime.setGetIsSessionAutoAccepting(
878
886
  (sessionId, directory) => permissionAutoAcceptRuntime.isSessionAutoAccepting(sessionId, directory),
879
887
  );
880
888
 
889
+ // Queued follow-up messages are delivered by the server so a closed tab or a
890
+ // dropped connection no longer strands them (VS Code keeps its UI-side queue).
891
+ const messageQueueRuntime = createMessageQueueRuntime({
892
+ globalEventHub: globalMessageStreamHub,
893
+ buildOpenCodeUrl,
894
+ getOpenCodeAuthHeaders,
895
+ sessionKnowledgeRuntime,
896
+ broadcastGlobalUiEvent,
897
+ onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
898
+ dataDir: OPENCHAMBER_DATA_DIR,
899
+ });
900
+ messageQueueRuntime.start();
901
+
881
902
  const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
882
903
  waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
883
904
  buildOpenCodeUrl,
@@ -1208,11 +1229,15 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
1208
1229
  const managedEnv = includeControl || includeWeb || includeMemory
1209
1230
  ? await (agentToolRuntime?.prepareManagedOpenCodeEnv({ includeControl, includeWeb, includeMemory }) || {})
1210
1231
  : {};
1211
- if (settings?.optimizeSystemPrompt !== true) return managedEnv;
1212
1232
 
1213
- const configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
1214
- const systemPromptEnv = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent);
1215
- return { ...managedEnv, ...systemPromptEnv };
1233
+ // Each managed plugin appends itself to the config the previous one produced.
1234
+ let configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
1235
+ if (settings?.optimizeSystemPrompt === true) {
1236
+ ({ OPENCODE_CONFIG_CONTENT: configContent } = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent));
1237
+ }
1238
+ // Always on for managed OpenCode: it only retries servers OpenCode gave up on.
1239
+ const mcpReconnectEnv = await mcpReconnectRuntime.prepareManagedOpenCodeEnv(configContent);
1240
+ return { ...managedEnv, ...mcpReconnectEnv };
1216
1241
  },
1217
1242
  });
1218
1243
 
@@ -1295,7 +1320,7 @@ const resolveMemoryProjectId = createMemoryProjectResolver({
1295
1320
  return sanitizeProjects(settings?.projects || []).map((project) => project.path);
1296
1321
  },
1297
1322
  resolvePrimaryWorktreeRoot,
1298
- managedProjectRoots: [path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats')],
1323
+ managedProjectRoots: [...new Set([path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats'), OPENCHAMBER_CHATS_DIR])],
1299
1324
  });
1300
1325
 
1301
1326
  /**
@@ -1427,6 +1452,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1427
1452
  sessionAssistRuntime,
1428
1453
  sessionGoalRuntime,
1429
1454
  contextObligatoryRuntime,
1455
+ messageQueueRuntime,
1430
1456
  sessionRuntime,
1431
1457
  getHealthCheckInterval: () => healthCheckInterval,
1432
1458
  clearHealthCheckInterval: (value) => clearInterval(value),
@@ -1485,6 +1511,11 @@ async function main(options = {}) {
1485
1511
  path,
1486
1512
  dataDir: OPENCHAMBER_DATA_DIR,
1487
1513
  });
1514
+ mcpReconnectRuntime = createMcpReconnectRuntime({
1515
+ fsPromises,
1516
+ path,
1517
+ dataDir: OPENCHAMBER_DATA_DIR,
1518
+ });
1488
1519
 
1489
1520
  // Pairing transports advertised to the create-device dialog. LAN reachability is
1490
1521
  // derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
@@ -1877,6 +1908,7 @@ async function main(options = {}) {
1877
1908
  createFsSearchRuntime: createFsSearchRuntimeFactory,
1878
1909
  openchamberDataDir: OPENCHAMBER_DATA_DIR,
1879
1910
  openchamberUserConfigRoot: OPENCHAMBER_USER_CONFIG_ROOT,
1911
+ managedChatsRoot: OPENCHAMBER_CHATS_DIR,
1880
1912
  normalizeDirectoryPath,
1881
1913
  resolveProjectDirectory,
1882
1914
  resolveOptionalProjectDirectory,
@@ -1914,6 +1946,7 @@ async function main(options = {}) {
1914
1946
  getOpenChamberEventClients: () => uiOpenChamberEventClients,
1915
1947
  writeSseEvent,
1916
1948
  permissionAutoAcceptRuntime,
1949
+ messageQueueRuntime,
1917
1950
  });
1918
1951
 
1919
1952
  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
+ });
@@ -20,7 +20,13 @@ and what made it fragile per framework.
20
20
  - `client.js` is the local end: it binds a loopback listener on the user's
21
21
  machine and pipes each accepted connection through one WebSocket. It lives in
22
22
  this package because it needs a WebSocket client the package already depends
23
- on; the desktop shell drives it over IPC.
23
+ on; the desktop shell drives it over IPC for directly reachable HTTP(S)
24
+ runtimes.
25
+ - Relay-only runtimes use `packages/electron/relay-dev-tunnel.mjs` for the local
26
+ listener. Each accepted connection gets an Electron `MessagePort`; the trusted
27
+ renderer carries its bytes through the active E2EE relay. This keeps relay
28
+ credentials and encryption in their existing renderer owner instead of
29
+ duplicating them in Electron main.
24
30
  - Port discovery is not owned here. `runtime.js` is given the reachable set by
25
31
  the same dev-server discovery the user's own list is built from.
26
32
  - The browser panel decides when to tunnel; this module never chooses a target.
@@ -40,9 +46,12 @@ and what made it fragile per framework.
40
46
  usual origin allowlist applies unchanged. That check is a CSRF defence: a
41
47
  hostile page can make a browser open a WebSocket carrying ambient cookies,
42
48
  and the origin is what exposes it.
43
- - With no `Origin` the request must carry client-token auth. A browser cannot
44
- reach this path the WebSocket API always sends an origin and never lets a
45
- page set an `Authorization` header so this case is the desktop shell.
49
+ - With no `Origin` the request must carry client-token auth or a short-lived
50
+ URL token. The bearer case is the desktop main process. The URL-token case
51
+ is the trusted renderer carrying the socket through the E2EE relay.
52
+ - Through the E2EE relay, the trusted renderer mints a short-lived URL token
53
+ and includes it in the virtual WebSocket URL. The relay host and URL-token
54
+ allowlists accept exactly `/api/dev-tunnel`, not subpaths.
46
55
  - Concurrency is capped per host, not per page, because one page load opens
47
56
  many sockets.
48
57
  - A connection that cannot be established fails the socket rather than holding
@@ -20,9 +20,9 @@
20
20
  *
21
21
  * - With an `Origin` header, the request came from a browser context and the
22
22
  * usual origin check applies unchanged.
23
- * - With no `Origin`, the request must carry client-token auth. A browser
24
- * cannot reach this path: the WebSocket API always sends an origin and never
25
- * lets a page set an `Authorization` header.
23
+ * - With no `Origin`, the request must carry client-token auth or a short-lived
24
+ * URL token. The URL-token case is used only by the trusted renderer through
25
+ * the E2EE relay; the UI-auth allowlist limits it to this exact path.
26
26
  */
27
27
  import net from 'node:net';
28
28
  import { WebSocketServer } from 'ws';
@@ -133,7 +133,7 @@ export function createDevTunnelRuntime({
133
133
  void (async () => {
134
134
  try {
135
135
  if (uiAuthController?.enabled) {
136
- const auth = await uiAuthController.resolveAuthContext(req, null, { allowUrlToken: false });
136
+ const auth = await uiAuthController.resolveAuthContext(req, null, { allowUrlToken: true });
137
137
  if (!auth) {
138
138
  rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
139
139
  return;
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test';
2
2
  import http from 'node:http';
3
3
  import net from 'node:net';
4
+ import WebSocket from 'ws';
4
5
 
5
6
  import { createDevTunnelClient } from './client.js';
6
7
  import { createDevTunnelRuntime, isDevTunnelPath } from './runtime.js';
@@ -228,6 +229,12 @@ describe('dev tunnel authentication', () => {
228
229
  enabled: true,
229
230
  resolveAuthContext: async () => ({ type: 'session' }),
230
231
  };
232
+ const urlTokenAuth = {
233
+ enabled: true,
234
+ resolveAuthContext: async (req, _res, options) => (
235
+ options?.allowUrlToken === true && req.url.includes('oc_url_token=good') ? { type: 'client', token: 'url:authenticated' } : null
236
+ ),
237
+ };
231
238
 
232
239
  test('accepts a bearer-authenticated client that sends no origin', async () => {
233
240
  const devPort = await startDevServer((_req, res) => res.end('ok'));
@@ -243,6 +250,19 @@ describe('dev tunnel authentication', () => {
243
250
  expect((await httpGet(localPort, '/')).body).toBe('ok');
244
251
  });
245
252
 
253
+ test('accepts a URL-token client carried by the E2EE relay', async () => {
254
+ const devPort = await startDevServer((_req, res) => res.end('relay-ok'));
255
+ const host = await startHost({ allowedPorts: [devPort], auth: urlTokenAuth });
256
+
257
+ const body = await new Promise((resolve, reject) => {
258
+ const socket = new WebSocket(`ws://127.0.0.1:${host.port}/api/dev-tunnel?port=${devPort}&oc_url_token=good`);
259
+ socket.on('open', () => socket.send('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'));
260
+ socket.on('message', (data) => resolve(Buffer.from(data).toString()));
261
+ socket.on('error', reject);
262
+ });
263
+ expect(body).toContain('relay-ok');
264
+ });
265
+
246
266
  test('rejects a client with no credentials', async () => {
247
267
  const devPort = await startDevServer((_req, res) => res.end('ok'));
248
268
  const host = await startHost({ allowedPorts: [devPort], auth: clientAuth });
@@ -40,6 +40,8 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
40
40
 
41
41
  ## Notes for contributors
42
42
  - Keep filesystem policy (workspace root checks, error mapping, exec timeout behavior) inside this module, not in the composition root.
43
+ - Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
44
+ - `GET /api/fs/home` answers `{ home, chatsRoot }`. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
43
45
  - Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
44
46
  - Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
45
47
  - If adding new `/api/fs/*` endpoints, add them in `routes.js` and extend this document.