@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.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 (195) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/multirun-BvSG94mW.js +4 -0
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/projectMeta-DOe49dM_.js +3 -0
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
  52. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
  53. package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/src-BzX5UI-c.css +2 -0
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/textarea-3XR8sUfy.js +1 -0
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
  63. package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
  168. package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
  170. package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
  171. package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CcIuekQ3.js +0 -7
  174. package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-CFLmpNC5.js +0 -169
  178. package/dist/assets/main-Ckj_IpUy.js +0 -2
  179. package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
  180. package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
  181. package/dist/assets/multirun-BGUKIJys.js +0 -4
  182. package/dist/assets/pl-DW2XJGC4.js +0 -15
  183. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  184. package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
  185. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  186. package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
  187. package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
  188. package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
  189. package/dist/assets/src-rS3zCcsC.css +0 -2
  190. package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
  191. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  192. package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
  193. package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
  194. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  195. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -10,6 +10,7 @@ const originalPath = process.env.PATH;
10
10
  const originalLocalAppData = process.env.LOCALAPPDATA;
11
11
  const originalSystemRoot = process.env.SystemRoot;
12
12
  const originalBundledOpencodeCliDir = process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
13
+ const originalOpenChamberDataDir = process.env.OPENCHAMBER_DATA_DIR;
13
14
  const originalResourcesPath = process.resourcesPath;
14
15
  const originalWslBinary = process.env.WSL_BINARY;
15
16
  const originalOpenChamberWslBinary = process.env.OPENCHAMBER_WSL_BINARY;
@@ -23,6 +24,16 @@ const createTempDir = (prefix) => {
23
24
  return dir;
24
25
  };
25
26
 
27
+ const writeVersionBinary = (filePath, version) => {
28
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
+ if (process.platform === 'win32') {
30
+ fs.writeFileSync(filePath, `@echo off\r\necho ${version}\r\n`);
31
+ return;
32
+ }
33
+ fs.writeFileSync(filePath, `#!${process.execPath}\nconsole.log(${JSON.stringify(version)});\n`);
34
+ fs.chmodSync(filePath, 0o755);
35
+ };
36
+
26
37
  const setPlatform = (platform) => {
27
38
  Object.defineProperty(process, 'platform', {
28
39
  value: platform,
@@ -74,6 +85,12 @@ afterEach(() => {
74
85
  delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
75
86
  }
76
87
 
88
+ if (typeof originalOpenChamberDataDir === 'string') {
89
+ process.env.OPENCHAMBER_DATA_DIR = originalOpenChamberDataDir;
90
+ } else {
91
+ delete process.env.OPENCHAMBER_DATA_DIR;
92
+ }
93
+
77
94
  Object.defineProperty(process, 'resourcesPath', {
78
95
  configurable: true,
79
96
  value: originalResourcesPath,
@@ -112,6 +129,7 @@ const createRuntime = (settings, options = {}) => {
112
129
  readSettingsFromDiskMigrated: async () => settings,
113
130
  spawnSync: options.spawnSync,
114
131
  homedir: options.homedir,
132
+ ensurePinnedOpenCode2Cli: options.ensurePinnedOpenCode2Cli,
115
133
  });
116
134
 
117
135
  return { runtime, state };
@@ -119,11 +137,11 @@ const createRuntime = (settings, options = {}) => {
119
137
 
120
138
  describe('OpenCode env runtime', () => {
121
139
  it('throws a specific error for a missing configured OpenCode binary in strict mode', async () => {
122
- const { runtime } = createRuntime({ opencodeBinary: '/missing/opencode' });
140
+ const { runtime } = createRuntime({ opencodeBinary: '/missing/opencode2' });
123
141
 
124
142
  await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).rejects.toMatchObject({
125
143
  code: 'OPENCODE_BINARY_INVALID',
126
- message: expect.stringContaining('Configured OpenCode binary not found: /missing/opencode'),
144
+ message: expect.stringContaining('Configured OpenCode binary not found: /missing/opencode2'),
127
145
  });
128
146
  });
129
147
 
@@ -139,7 +157,7 @@ describe('OpenCode env runtime', () => {
139
157
 
140
158
  it('applies a valid configured executable OpenCode binary', async () => {
141
159
  const dir = createTempDir('openchamber-opencode-bin-');
142
- const binary = path.join(dir, 'opencode');
160
+ const binary = path.join(dir, 'opencode2');
143
161
  fs.writeFileSync(binary, '#!/bin/sh\nexit 0\n');
144
162
  fs.chmodSync(binary, 0o755);
145
163
  const { runtime, state } = createRuntime({ opencodeBinary: binary });
@@ -150,16 +168,78 @@ describe('OpenCode env runtime', () => {
150
168
  expect(state.resolvedOpencodeBinarySource).toBe('settings');
151
169
  });
152
170
 
171
+ it('rejects a configured 1.x opencode version in strict mode', async () => {
172
+ const dir = createTempDir('openchamber-opencode-1x-');
173
+ const binary = path.join(dir, 'opencode');
174
+ writeVersionBinary(binary, '1.18.4');
175
+ const { runtime } = createRuntime({ opencodeBinary: binary });
176
+
177
+ await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).rejects.toMatchObject({
178
+ code: 'OPENCODE_BINARY_INVALID',
179
+ message: expect.stringMatching(/1\.x.*OpenCode v2/s),
180
+ });
181
+ });
182
+
183
+ it('rejects an explicit OPENCODE_BINARY that reports a 1.x version', () => {
184
+ const dir = createTempDir('openchamber-env-opencode-1x-');
185
+ const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
186
+ writeVersionBinary(binary, '1.18.4');
187
+ process.env.OPENCODE_BINARY = binary;
188
+ const { runtime } = createRuntime({});
189
+
190
+ expect(() => runtime.resolveOpencodeCliPath()).toThrow(
191
+ expect.objectContaining({
192
+ code: 'OPENCODE_BINARY_INVALID',
193
+ message: expect.stringMatching(/1\.x.*OpenCode v2/s),
194
+ })
195
+ );
196
+ });
197
+
198
+ it('discovers opencode from a home-directory install location', () => {
199
+ const home = createTempDir('openchamber-home-opencode-');
200
+ const binary = path.join(home, '.bun', 'bin', 'opencode');
201
+ writeVersionBinary(binary, '2.0.12');
202
+ process.env.PATH = createTempDir('openchamber-empty-path-home-');
203
+ delete process.env.OPENCODE_BINARY;
204
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
205
+ const { runtime, state } = createRuntime({}, {
206
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
207
+ homedir: () => home,
208
+ });
209
+
210
+ expect(runtime.resolveOpencodeCliPath()).toBe(binary);
211
+ expect(state.resolvedOpencodeBinarySource).toBe('fallback');
212
+ });
213
+
214
+ it('does not treat a PATH 1.x opencode binary as a resolved CLI', () => {
215
+ const pathDir = createTempDir('openchamber-path-opencode-1x-');
216
+ const pathBinary = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
217
+ fs.writeFileSync(pathBinary, '#!/bin/sh\nexit 0\n');
218
+ if (process.platform !== 'win32') {
219
+ fs.chmodSync(pathBinary, 0o755);
220
+ }
221
+ process.env.PATH = pathDir;
222
+ delete process.env.OPENCODE_BINARY;
223
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
224
+ const emptyHome = createTempDir('openchamber-empty-home-1x-');
225
+ const { runtime, state } = createRuntime({}, {
226
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
227
+ homedir: () => emptyHome,
228
+ });
229
+
230
+ expect(runtime.resolveOpencodeCliPath()).toBeNull();
231
+ expect(state.resolvedOpencodeBinarySource).toBeNull();
232
+ });
233
+
153
234
  it('prefers a user-installed OpenCode from PATH over the bundled CLI', () => {
154
235
  const bundledDir = createTempDir('openchamber-bundled-opencode-');
155
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
236
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
156
237
  const pathDir = createTempDir('openchamber-path-opencode-');
157
238
  const pathBinary = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
158
239
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
159
- fs.writeFileSync(pathBinary, '#!/bin/sh\nexit 0\n');
240
+ writeVersionBinary(pathBinary, '2.0.12');
160
241
  if (process.platform !== 'win32') {
161
242
  fs.chmodSync(bundledBinary, 0o755);
162
- fs.chmodSync(pathBinary, 0o755);
163
243
  }
164
244
  process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
165
245
  process.env.PATH = pathDir;
@@ -172,14 +252,13 @@ describe('OpenCode env runtime', () => {
172
252
 
173
253
  it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
174
254
  const bundledDir = createTempDir('openchamber-bundled-opencode-');
175
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
255
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
176
256
  const explicitDir = createTempDir('openchamber-explicit-opencode-');
177
- const explicitBinary = path.join(explicitDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
257
+ const explicitBinary = path.join(explicitDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
178
258
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
179
- fs.writeFileSync(explicitBinary, '#!/bin/sh\nexit 0\n');
259
+ writeVersionBinary(explicitBinary, '2.0.12');
180
260
  if (process.platform !== 'win32') {
181
261
  fs.chmodSync(bundledBinary, 0o755);
182
- fs.chmodSync(explicitBinary, 0o755);
183
262
  }
184
263
  process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
185
264
  process.env.OPENCODE_BINARY = explicitBinary;
@@ -189,10 +268,10 @@ describe('OpenCode env runtime', () => {
189
268
  expect(state.resolvedOpencodeBinarySource).toBe('env');
190
269
  });
191
270
 
192
- it('falls back to the bundled OpenCode CLI from Electron resourcesPath when nothing else is installed', () => {
271
+ it('does not use a bundled OpenCode CLI from Electron resourcesPath', () => {
193
272
  const resourcesPath = createTempDir('openchamber-resources-');
194
273
  const bundledDir = path.join(resourcesPath, 'opencode-cli');
195
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
274
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
196
275
  fs.mkdirSync(bundledDir, { recursive: true });
197
276
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
198
277
  if (process.platform !== 'win32') {
@@ -203,18 +282,142 @@ describe('OpenCode env runtime', () => {
203
282
  value: resourcesPath,
204
283
  });
205
284
  process.env.PATH = createTempDir('openchamber-empty-path-');
206
- delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
285
+ process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
207
286
  delete process.env.OPENCODE_BINARY;
208
- // The bundled CLI is the LAST resort now — hide the machine's own installs
209
- // from the home-directory fallbacks and shell discovery.
210
287
  const emptyHome = createTempDir('openchamber-empty-home-');
211
288
  const { runtime, state } = createRuntime({}, {
212
289
  spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
213
290
  homedir: () => emptyHome,
214
291
  });
215
292
 
216
- expect(runtime.resolveOpencodeCliPath()).toBe(bundledBinary);
293
+ expect(runtime.resolveOpencodeCliPath()).toBeNull();
294
+ expect(state.resolvedOpencodeBinarySource).toBeNull();
295
+ });
296
+
297
+ it('discovers a previously installed pin from the OpenChamber data dir', () => {
298
+ const dataDir = createTempDir('openchamber-installed-cli-');
299
+ const installedDir = path.join(dataDir, 'opencode-cli', '2.0.12');
300
+ const installedBinary = path.join(installedDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
301
+ fs.mkdirSync(installedDir, { recursive: true });
302
+ fs.writeFileSync(installedBinary, '#!/bin/sh\nexit 0\n');
303
+ if (process.platform !== 'win32') {
304
+ fs.chmodSync(installedBinary, 0o755);
305
+ }
306
+ process.env.OPENCHAMBER_DATA_DIR = dataDir;
307
+ process.env.PATH = createTempDir('openchamber-empty-path-installed-');
308
+ delete process.env.OPENCODE_BINARY;
309
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
310
+ const emptyHome = createTempDir('openchamber-empty-home-installed-');
311
+ const { runtime, state } = createRuntime({}, {
312
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
313
+ homedir: () => emptyHome,
314
+ });
315
+
316
+ expect(runtime.resolveOpencodeCliPath()).toBe(installedBinary);
317
+ expect(state.resolvedOpencodeBinarySource).toBe('installed');
318
+ });
319
+
320
+ it('installs the pinned CLI when discovery finds nothing', async () => {
321
+ const installed = path.join(createTempDir('openchamber-ensure-install-'), process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
322
+ fs.writeFileSync(installed, '#!/bin/sh\nexit 0\n');
323
+ if (process.platform !== 'win32') {
324
+ fs.chmodSync(installed, 0o755);
325
+ }
326
+ process.env.PATH = createTempDir('openchamber-empty-path-ensure-');
327
+ delete process.env.OPENCODE_BINARY;
328
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
329
+ const emptyHome = createTempDir('openchamber-empty-home-ensure-');
330
+ const { runtime, state } = createRuntime({}, {
331
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
332
+ homedir: () => emptyHome,
333
+ ensurePinnedOpenCode2Cli: async () => ({
334
+ path: installed,
335
+ version: '2.0.12',
336
+ source: 'installed',
337
+ installed: true,
338
+ }),
339
+ });
340
+
341
+ await expect(runtime.ensurePinnedOpenCode2CliEnv()).resolves.toBe(installed);
342
+ expect(state.resolvedOpencodeBinary).toBe(installed);
343
+ expect(state.resolvedOpencodeBinarySource).toBe('installed');
344
+ expect(process.env.OPENCODE_BINARY).toBe(installed);
345
+ });
346
+
347
+ it('falls back to a bundled 2.x CLI only after pin install fails', async () => {
348
+ const bundledDir = createTempDir('openchamber-bundled-fallback-');
349
+ const bundledName = process.platform === 'win32' ? 'opencode2.exe' : 'opencode2';
350
+ const bundledBinary = path.join(bundledDir, bundledName);
351
+ writeVersionBinary(bundledBinary, '2.0.12');
352
+ process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
353
+ process.env.PATH = createTempDir('openchamber-empty-path-bundled-fallback-');
354
+ process.env.OPENCHAMBER_DATA_DIR = createTempDir('openchamber-empty-data-bundled-fallback-');
355
+ delete process.env.OPENCODE_BINARY;
356
+ const emptyHome = createTempDir('openchamber-empty-home-bundled-fallback-');
357
+ const missingError = Object.assign(new Error('OpenCode CLI missing'), { code: 'OPENCODE_CLI_MISSING' });
358
+ const { runtime, state } = createRuntime({}, {
359
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
360
+ homedir: () => emptyHome,
361
+ ensurePinnedOpenCode2Cli: async () => {
362
+ throw missingError;
363
+ },
364
+ });
365
+
366
+ await expect(runtime.ensurePinnedOpenCode2CliEnv()).resolves.toBe(bundledBinary);
367
+ expect(state.resolvedOpencodeBinary).toBe(bundledBinary);
217
368
  expect(state.resolvedOpencodeBinarySource).toBe('bundled');
369
+ expect(process.env.OPENCODE_BINARY).toBe(bundledBinary);
370
+ });
371
+
372
+ it('falls back to bundled opencode name when pin install fails', async () => {
373
+ const bundledDir = createTempDir('openchamber-bundled-opencode-name-');
374
+ const bundledName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
375
+ const bundledBinary = path.join(bundledDir, bundledName);
376
+ writeVersionBinary(bundledBinary, '2.0.12');
377
+ process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
378
+ process.env.PATH = createTempDir('openchamber-empty-path-bundled-opencode-');
379
+ process.env.OPENCHAMBER_DATA_DIR = createTempDir('openchamber-empty-data-bundled-opencode-');
380
+ delete process.env.OPENCODE_BINARY;
381
+ const emptyHome = createTempDir('openchamber-empty-home-bundled-opencode-');
382
+ const missingError = Object.assign(new Error('OpenCode CLI missing'), { code: 'OPENCODE_CLI_MISSING' });
383
+ const { runtime, state } = createRuntime({}, {
384
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
385
+ homedir: () => emptyHome,
386
+ ensurePinnedOpenCode2Cli: async () => {
387
+ throw missingError;
388
+ },
389
+ });
390
+
391
+ await expect(runtime.ensurePinnedOpenCode2CliEnv()).resolves.toBe(bundledBinary);
392
+ expect(state.resolvedOpencodeBinarySource).toBe('bundled');
393
+ expect(process.env.OPENCODE_BINARY).toBe(bundledBinary);
394
+ });
395
+
396
+ it('does not adopt a bundled 1.x CLI after pin install fails', async () => {
397
+ const bundledDir = createTempDir('openchamber-bundled-1x-');
398
+ const bundledName = process.platform === 'win32' ? 'opencode2.exe' : 'opencode2';
399
+ const bundledBinary = path.join(bundledDir, bundledName);
400
+ writeVersionBinary(bundledBinary, '1.18.4');
401
+ process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
402
+ process.env.PATH = createTempDir('openchamber-empty-path-bundled-1x-');
403
+ process.env.OPENCHAMBER_DATA_DIR = createTempDir('openchamber-empty-data-bundled-1x-');
404
+ delete process.env.OPENCODE_BINARY;
405
+ const emptyHome = createTempDir('openchamber-empty-home-bundled-1x-');
406
+ const missingError = Object.assign(new Error('OpenCode CLI missing'), { code: 'OPENCODE_CLI_MISSING' });
407
+ const { runtime, state } = createRuntime({}, {
408
+ spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
409
+ homedir: () => emptyHome,
410
+ ensurePinnedOpenCode2Cli: async () => {
411
+ throw missingError;
412
+ },
413
+ });
414
+
415
+ await expect(runtime.ensurePinnedOpenCode2CliEnv()).rejects.toMatchObject({
416
+ code: 'OPENCODE_CLI_MISSING',
417
+ });
418
+ expect(state.resolvedOpencodeBinary).toBeNull();
419
+ expect(state.resolvedOpencodeBinarySource).toBeNull();
420
+ expect(process.env.OPENCODE_BINARY).toBeUndefined();
218
421
  });
219
422
 
220
423
  itIf(process.platform === 'darwin')('rejects known macOS OpenCode app bundle executable paths', async () => {
@@ -265,7 +468,8 @@ describe('OpenCode env runtime', () => {
265
468
  const cliBinary = path.join(createTempDir('openchamber-cli-'), 'opencode.exe');
266
469
  fs.mkdirSync(path.dirname(desktopBinary), { recursive: true });
267
470
  fs.writeFileSync(desktopBinary, '');
268
- fs.writeFileSync(cliBinary, '');
471
+ fs.writeFileSync(cliBinary, `#!${process.execPath}\nconsole.log('2.0.12');\n`);
472
+ fs.chmodSync(cliBinary, 0o755);
269
473
  process.env.LOCALAPPDATA = localAppData;
270
474
  process.env.PATH = createTempDir('openchamber-empty-path-');
271
475
  process.env.SystemRoot = createTempDir('openchamber-empty-systemroot-');
@@ -2,6 +2,7 @@ import { registerFsRoutes } from '../fs/routes.js';
2
2
  import { registerQuotaRoutes } from '../quota/routes.js';
3
3
  import { registerSmallModelRoutes } from '../small-model/routes.js';
4
4
  import { registerSessionGoalRoutes } from '../session-goal/routes.js';
5
+ import { registerSessionMetadataRoutes } from '../session-metadata/routes.js';
5
6
  import { registerGitHubRoutes } from '../github/routes.js';
6
7
  import { registerGitRoutes } from '../git/routes.js';
7
8
  import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
@@ -113,6 +114,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
113
114
  isUnsafeSkillRelativePath,
114
115
  buildOpenCodeUrl,
115
116
  getOpenCodeAuthHeaders,
117
+ getIsExternalOpenCode = () => false,
116
118
  getSmallModelService: routeGetSmallModelService,
117
119
  getOpenCodePort,
118
120
  buildAugmentedPath,
@@ -132,6 +134,10 @@ export const createFeatureRoutesRuntime = (dependencies) => {
132
134
  getServerId,
133
135
  sessionIndexService,
134
136
  notifyContactTurnComplete,
137
+ sessionMetadataStore = null,
138
+ onSessionMetadataWritten = null,
139
+ persistSessionGoal = null,
140
+ readSessionMetadata = null,
135
141
  } = routeDependencies;
136
142
 
137
143
  registerSettingsUtilityRoutes(app, {
@@ -164,6 +170,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
164
170
  refreshOpenCodeAfterConfigChange,
165
171
  buildOpenCodeUrl,
166
172
  getOpenCodeAuthHeaders,
173
+ getIsExternalOpenCode,
167
174
  onSettingsPersisted: (updated, changes) => {
168
175
  if (!questionAutoDelegateRuntime) return;
169
176
  if (!Object.prototype.hasOwnProperty.call(changes ?? {}, 'questionAutoDelegateEnabled')) return;
@@ -293,6 +300,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
293
300
  resolveOptionalProjectDirectory,
294
301
  refreshOpenCodeAfterConfigChange,
295
302
  clientReloadDelayMs,
303
+ waitForOpenCodeReady,
296
304
  getAgentSources,
297
305
  getAgentConfig,
298
306
  createAgent,
@@ -387,7 +395,35 @@ export const createFeatureRoutesRuntime = (dependencies) => {
387
395
  throw new Error('Small model service is not configured');
388
396
  });
389
397
  registerSmallModelRoutes(app, { getSmallModelService });
390
- registerSessionGoalRoutes(app);
398
+ registerSessionGoalRoutes(app, {
399
+ // Reuse the index-provided store seam — do not invent a second store.
400
+ persistSessionGoal: typeof persistSessionGoal === 'function'
401
+ ? persistSessionGoal
402
+ : (sessionMetadataStore
403
+ ? async (sessionId, _directory, goal) => {
404
+ const metadata = await sessionMetadataStore.setSessionMetadata(sessionId, {
405
+ openchamber: { goal },
406
+ });
407
+ return metadata;
408
+ }
409
+ : null),
410
+ readSessionMetadata: typeof readSessionMetadata === 'function'
411
+ ? readSessionMetadata
412
+ : (sessionMetadataStore
413
+ ? (sessionId) => sessionMetadataStore.get(sessionId)
414
+ : null),
415
+ // Same arm path as PUT /api/openchamber/sessions/:id/metadata.
416
+ onGoalPersisted: typeof onSessionMetadataWritten === 'function'
417
+ ? onSessionMetadataWritten
418
+ : null,
419
+ });
420
+ if (sessionMetadataStore) {
421
+ registerSessionMetadataRoutes(app, {
422
+ sessionMetadataStore,
423
+ broadcastGlobalUiEvent,
424
+ onMetadataWritten: onSessionMetadataWritten,
425
+ });
426
+ }
391
427
  registerGitHubRoutes(app);
392
428
  const broadcastOpenChamberEvent = createOpenChamberEventBroadcaster({
393
429
  getOpenChamberEventClients,