@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
@@ -3,6 +3,8 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { mergePathValues } from './path-utils.js';
6
+ import { PINNED_OPENCODE2_VERSION, isAcceptableOpenCode2HealthVersion, isOpenCode1xVersion } from './opencode2-pin.js';
7
+ import { ensurePinnedOpenCode2Cli, installedOpenCode2BinaryPath, readOpenCode2BinaryVersion } from './ensure-cli.js';
6
8
 
7
9
  export const createOpenCodeEnvRuntime = (deps) => {
8
10
  const {
@@ -12,6 +14,9 @@ export const createOpenCodeEnvRuntime = (deps) => {
12
14
  } = deps;
13
15
  const runSpawnSync = typeof deps.spawnSync === 'function' ? deps.spawnSync : spawnSync;
14
16
  const resolveHomeDir = typeof deps.homedir === 'function' ? deps.homedir : () => os.homedir();
17
+ const ensurePinnedCli = typeof deps.ensurePinnedOpenCode2Cli === 'function'
18
+ ? deps.ensurePinnedOpenCode2Cli
19
+ : ensurePinnedOpenCode2Cli;
15
20
 
16
21
  const parseNullSeparatedEnvSnapshot = (raw) => {
17
22
  if (typeof raw !== 'string' || raw.length === 0) {
@@ -275,41 +280,20 @@ export const createOpenCodeEnvRuntime = (deps) => {
275
280
  return normalized.endsWith(`${path.sep}programs${path.sep}opencode${path.sep}opencode.exe`);
276
281
  };
277
282
 
278
- const bundledOpenCodeCliCandidates = () => {
279
- const names = process.platform === 'win32' ? ['opencode.exe'] : ['opencode'];
280
- const roots = [
281
- process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR,
282
- typeof process.resourcesPath === 'string' ? path.join(process.resourcesPath, 'opencode-cli') : null,
283
- ]
284
- .map((value) => (typeof value === 'string' ? value.trim() : ''))
285
- .filter(Boolean);
286
-
287
- const candidates = [];
288
- for (const root of roots) {
289
- for (const name of names) {
290
- candidates.push(path.join(root, name));
291
- }
292
- }
293
- return candidates;
294
- };
295
-
296
- const resolveBundledOpenCodeCliPath = () => {
297
- for (const candidate of bundledOpenCodeCliCandidates()) {
298
- if (isExecutable(candidate) && !isWindowsOpenCodeDesktopAppPath(candidate)) {
299
- return candidate;
300
- }
283
+ const resolveInstalledOpenCodeCliPath = () => {
284
+ const installed = installedOpenCode2BinaryPath(PINNED_OPENCODE2_VERSION, {
285
+ env: process.env,
286
+ homedir: resolveHomeDir,
287
+ platform: process.platform,
288
+ });
289
+ if (isExecutable(installed) && !isWindowsOpenCodeDesktopAppPath(installed)) {
290
+ clearWslOpencodeResolution();
291
+ state.resolvedOpencodeBinarySource = 'installed';
292
+ return installed;
301
293
  }
302
294
  return null;
303
295
  };
304
296
 
305
- const bundledOpenCodeCliFallback = () => {
306
- const bundled = resolveBundledOpenCodeCliPath();
307
- if (!bundled) return null;
308
- clearWslOpencodeResolution();
309
- state.resolvedOpencodeBinarySource = 'bundled';
310
- return bundled;
311
- };
312
-
313
297
  const clearWslOpencodeResolution = () => {
314
298
  state.useWslForOpencode = false;
315
299
  state.resolvedWslBinary = null;
@@ -330,6 +314,28 @@ export const createOpenCodeEnvRuntime = (deps) => {
330
314
  return trimmed;
331
315
  };
332
316
 
317
+ const createOpenCode1xVersionError = (candidate, version) => {
318
+ const error = new Error(
319
+ `OpenCode 1.x is not supported (${candidate} reports ${version}). Install OpenCode v2 (opencode).`
320
+ );
321
+ error.code = 'OPENCODE_BINARY_INVALID';
322
+ return error;
323
+ };
324
+
325
+ const acceptOpenCodeV2Candidate = (candidate, source) => {
326
+ if (typeof candidate !== 'string' || !candidate.trim()) return null;
327
+ if (!isExecutable(candidate) || isWindowsOpenCodeDesktopAppPath(candidate) || isMacOpenCodeAppBundlePath(candidate)) {
328
+ return null;
329
+ }
330
+ const version = readOpenCode2BinaryVersion(candidate);
331
+ if (!isAcceptableOpenCode2HealthVersion(version)) {
332
+ return null;
333
+ }
334
+ clearWslOpencodeResolution();
335
+ state.resolvedOpencodeBinarySource = source;
336
+ return candidate;
337
+ };
338
+
333
339
  const resolveOpencodeCliPath = () => {
334
340
  const explicit = [
335
341
  process.env.OPENCODE_BINARY,
@@ -341,118 +347,111 @@ export const createOpenCodeEnvRuntime = (deps) => {
341
347
  .filter(Boolean);
342
348
 
343
349
  for (const candidate of explicit) {
344
- if (isExecutable(candidate) && !isWindowsOpenCodeDesktopAppPath(candidate)) {
345
- clearWslOpencodeResolution();
346
- state.resolvedOpencodeBinarySource = 'env';
347
- return candidate;
350
+ if (isWindowsOpenCodeDesktopAppPath(candidate) || isMacOpenCodeAppBundlePath(candidate)) {
351
+ continue;
348
352
  }
353
+ if (!isExecutable(candidate)) continue;
354
+ const version = readOpenCode2BinaryVersion(candidate);
355
+ if (isOpenCode1xVersion(version)) {
356
+ throw createOpenCode1xVersionError(candidate, version);
357
+ }
358
+ const accepted = acceptOpenCodeV2Candidate(candidate, 'env');
359
+ if (accepted) return accepted;
349
360
  }
350
361
 
351
- // The bundled CLI is the LAST resort (see bundledOpenCodeCliFallback at the
352
- // exit points below): a user's own OpenCode install — PATH, known install
353
- // locations, or shell-resolved — must win over the pinned bundled copy.
354
- const resolvedFromPath = searchPathFor('opencode');
355
- if (resolvedFromPath) {
356
- clearWslOpencodeResolution();
357
- state.resolvedOpencodeBinarySource = 'path';
358
- return resolvedFromPath;
362
+ // Match master: reuse a globally installed CLI. Official v2 is `opencode`;
363
+ // `opencode2` remains a compatibility alias. Skip 1.x binaries by version.
364
+ for (const name of process.platform === 'win32' ? ['opencode', 'opencode2'] : ['opencode', 'opencode2']) {
365
+ const resolvedFromPath = searchPathFor(name);
366
+ const accepted = acceptOpenCodeV2Candidate(resolvedFromPath, 'path');
367
+ if (accepted) return accepted;
359
368
  }
360
369
 
361
370
  const home = resolveHomeDir();
362
- const unixFallbacks = [
363
- path.join(home, '.opencode', 'bin', 'opencode'),
364
- path.join(home, '.bun', 'bin', 'opencode'),
365
- path.join(home, '.local', 'bin', 'opencode'),
366
- path.join(home, 'bin', 'opencode'),
367
- '/opt/homebrew/bin/opencode',
368
- '/usr/local/bin/opencode',
369
- '/home/linuxbrew/.linuxbrew/bin/opencode',
370
- '/usr/bin/opencode',
371
- '/bin/opencode',
372
- ];
371
+ const unixFallbacks = ['opencode', 'opencode2'].flatMap((name) => [
372
+ path.join(home, '.opencode', 'bin', name),
373
+ path.join(home, '.bun', 'bin', name),
374
+ path.join(home, '.local', 'bin', name),
375
+ path.join(home, 'bin', name),
376
+ `/opt/homebrew/bin/${name}`,
377
+ `/usr/local/bin/${name}`,
378
+ `/home/linuxbrew/.linuxbrew/bin/${name}`,
379
+ `/usr/bin/${name}`,
380
+ `/bin/${name}`,
381
+ ]);
373
382
 
374
383
  const winFallbacks = (() => {
375
384
  const userProfile = process.env.USERPROFILE || home;
376
385
  const appData = process.env.APPDATA || '';
377
386
  const localAppData = process.env.LOCALAPPDATA || '';
378
387
  const programData = process.env.ProgramData || 'C:\\ProgramData';
379
-
380
388
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
381
-
382
- return [
383
- path.join(userProfile, '.opencode', 'bin', 'opencode.exe'),
384
- path.join(userProfile, '.opencode', 'bin', 'opencode.cmd'),
385
- path.join(appData, 'npm', 'opencode.cmd'),
386
- // System-wide Node installer keeps the global npm prefix here
387
- // (npm i -g opencode-ai → opencode.cmd shim).
388
- path.join(programFiles, 'nodejs', 'opencode.cmd'),
389
- path.join(userProfile, 'scoop', 'shims', 'opencode.exe'),
390
- path.join(userProfile, 'scoop', 'shims', 'opencode.cmd'),
391
- path.join(programData, 'chocolatey', 'bin', 'opencode.exe'),
392
- path.join(programData, 'chocolatey', 'bin', 'opencode.cmd'),
393
- path.join(userProfile, '.bun', 'bin', 'opencode.exe'),
394
- path.join(userProfile, '.bun', 'bin', 'opencode.cmd'),
395
- ].filter(Boolean);
389
+ const names = ['opencode.exe', 'opencode.cmd', 'opencode2.exe', 'opencode2.cmd'];
390
+ const dirs = [
391
+ path.join(userProfile, '.opencode', 'bin'),
392
+ path.join(appData, 'npm'),
393
+ path.join(programFiles, 'nodejs'),
394
+ path.join(userProfile, 'scoop', 'shims'),
395
+ path.join(programData, 'chocolatey', 'bin'),
396
+ path.join(userProfile, '.bun', 'bin'),
397
+ ];
398
+ return dirs.flatMap((dir) => names.map((name) => path.join(dir, name))).filter(Boolean);
396
399
  })();
397
400
 
398
401
  const fallbacks = process.platform === 'win32' ? winFallbacks : unixFallbacks;
399
402
  for (const candidate of fallbacks) {
400
- if (isExecutable(candidate)) {
401
- clearWslOpencodeResolution();
402
- state.resolvedOpencodeBinarySource = 'fallback';
403
- return candidate;
404
- }
403
+ const accepted = acceptOpenCodeV2Candidate(candidate, 'fallback');
404
+ if (accepted) return accepted;
405
405
  }
406
406
 
407
407
  if (process.platform === 'win32') {
408
- try {
409
- const result = runSpawnSync('where', ['opencode'], {
410
- encoding: 'utf8',
411
- stdio: ['ignore', 'pipe', 'pipe'],
412
- windowsHide: true,
413
- });
414
- if (result.status === 0) {
415
- const lines = (result.stdout || '')
416
- .split(/\r?\n/)
417
- .map((line) => line.trim())
418
- .filter(Boolean);
419
- const found = lines.find((line) => isExecutable(line) && !isWindowsOpenCodeDesktopAppPath(line));
420
- if (found) {
421
- clearWslOpencodeResolution();
422
- state.resolvedOpencodeBinarySource = 'where';
423
- return found;
408
+ for (const name of ['opencode', 'opencode2']) {
409
+ try {
410
+ const result = runSpawnSync('where', [name], {
411
+ encoding: 'utf8',
412
+ stdio: ['ignore', 'pipe', 'pipe'],
413
+ windowsHide: true,
414
+ });
415
+ if (result.status === 0) {
416
+ const lines = (result.stdout || '')
417
+ .split(/\r?\n/)
418
+ .map((line) => line.trim())
419
+ .filter(Boolean);
420
+ for (const line of lines) {
421
+ const accepted = acceptOpenCodeV2Candidate(line, 'where');
422
+ if (accepted) return accepted;
423
+ }
424
424
  }
425
+ } catch {
425
426
  }
426
- } catch {
427
427
  }
428
428
  // Do not auto-detect OpenCode from WSL. OpenCode sessions are keyed by
429
429
  // server-visible directories, and mixing Windows paths with WSL paths
430
430
  // creates duplicate/missing project state in the desktop app.
431
- return bundledOpenCodeCliFallback();
431
+ return resolveInstalledOpenCodeCliPath();
432
432
  }
433
433
 
434
434
  const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
435
- for (const shell of shells) {
436
- if (!isExecutable(shell)) continue;
437
- try {
438
- const result = runSpawnSync(shell, ['-lic', 'command -v opencode'], {
439
- encoding: 'utf8',
440
- stdio: ['ignore', 'pipe', 'pipe'],
441
- windowsHide: true,
442
- });
443
- if (result.status === 0) {
444
- const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
445
- if (found && isExecutable(found)) {
446
- clearWslOpencodeResolution();
447
- state.resolvedOpencodeBinarySource = 'shell';
448
- return found;
435
+ for (const name of ['opencode', 'opencode2']) {
436
+ for (const shell of shells) {
437
+ if (!isExecutable(shell)) continue;
438
+ try {
439
+ const result = runSpawnSync(shell, ['-lic', `command -v ${name}`], {
440
+ encoding: 'utf8',
441
+ stdio: ['ignore', 'pipe', 'pipe'],
442
+ windowsHide: true,
443
+ });
444
+ if (result.status === 0) {
445
+ const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
446
+ const accepted = acceptOpenCodeV2Candidate(found, 'shell');
447
+ if (accepted) return accepted;
449
448
  }
449
+ } catch {
450
450
  }
451
- } catch {
452
451
  }
453
452
  }
454
453
 
455
- return bundledOpenCodeCliFallback();
454
+ return resolveInstalledOpenCodeCliPath();
456
455
  };
457
456
 
458
457
  const resolveNodeCliPath = () => {
@@ -912,7 +911,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
912
911
  const createConfiguredOpencodeBinaryError = (raw, normalized) => {
913
912
  const configured = typeof raw === 'string' ? raw.trim() : '';
914
913
  const candidate = typeof normalized === 'string' && normalized.trim().length > 0 ? normalized.trim() : configured;
915
- const messageSuffix = 'OpenChamber needs the standalone opencode CLI. Install it and set settings.opencodeBinary to the CLI path, for example ~/.opencode/bin/opencode, or leave the setting empty to use PATH lookup.';
914
+ const messageSuffix = 'OpenChamber needs the standalone OpenCode v2 CLI (`opencode`). Install it and set settings.opencodeBinary to the CLI path, for example ~/.opencode/bin/opencode, or leave the setting empty to use PATH lookup.';
916
915
  const error = (() => {
917
916
  if (isKnownOpenCodeDesktopAppPath(candidate) || isKnownOpenCodeDesktopAppPath(configured)) {
918
917
  const platformName = process.platform === 'win32' ? 'Windows desktop app install' : 'macOS desktop app bundle';
@@ -1013,6 +1012,25 @@ export const createOpenCodeEnvRuntime = (deps) => {
1013
1012
  return null;
1014
1013
  }
1015
1014
 
1015
+ if (normalized && isKnownOpenCodeDesktopAppPath(normalized)) {
1016
+ if (strict) {
1017
+ throw createConfiguredOpencodeBinaryError(raw, normalized);
1018
+ }
1019
+ console.warn(`Configured settings.opencodeBinary points at a desktop app, not the CLI: ${normalized}`);
1020
+ return null;
1021
+ }
1022
+
1023
+ if (normalized && isExecutable(normalized)) {
1024
+ const version = readOpenCode2BinaryVersion(normalized);
1025
+ if (isOpenCode1xVersion(version)) {
1026
+ if (strict) {
1027
+ throw createOpenCode1xVersionError(normalized, version);
1028
+ }
1029
+ console.warn(`Configured settings.opencodeBinary points at OpenCode 1.x, which OpenChamber refuses: ${normalized}`);
1030
+ return null;
1031
+ }
1032
+ }
1033
+
1016
1034
  if (normalized && isExecutable(normalized) && !isKnownOpenCodeDesktopAppPath(normalized)) {
1017
1035
  clearWslOpencodeResolution();
1018
1036
  process.env.OPENCODE_BINARY = normalized;
@@ -1048,6 +1066,9 @@ export const createOpenCodeEnvRuntime = (deps) => {
1048
1066
  }
1049
1067
 
1050
1068
  const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
1069
+ if (existing && isExecutable(existing) && isOpenCode1xVersion(readOpenCode2BinaryVersion(existing))) {
1070
+ throw createOpenCode1xVersionError(existing, readOpenCode2BinaryVersion(existing));
1071
+ }
1051
1072
  if (existing && isExecutable(existing)) {
1052
1073
  clearWslOpencodeResolution();
1053
1074
  state.resolvedOpencodeBinary = existing;
@@ -1079,6 +1100,67 @@ export const createOpenCodeEnvRuntime = (deps) => {
1079
1100
  return null;
1080
1101
  };
1081
1102
 
1103
+ // Last-resort only: used after pin install fails. Never consulted by resolveOpencodeCliPath.
1104
+ const resolveBundledOpenCodeCliPath = () => {
1105
+ const dirs = [];
1106
+ const envDir = typeof process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR === 'string'
1107
+ ? process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR.trim()
1108
+ : '';
1109
+ if (envDir) {
1110
+ dirs.push(envDir);
1111
+ }
1112
+ if (typeof process.resourcesPath === 'string' && process.resourcesPath.trim()) {
1113
+ dirs.push(path.join(process.resourcesPath, 'opencode-cli'));
1114
+ }
1115
+
1116
+ // Official v2 name + packaging contract name (see electron opencode2-bundle-contract).
1117
+ const names = ['opencode', 'opencode.exe', 'opencode2', 'opencode2.exe'];
1118
+ for (const dir of dirs) {
1119
+ for (const name of names) {
1120
+ const accepted = acceptOpenCodeV2Candidate(path.join(dir, name), 'bundled');
1121
+ if (accepted) {
1122
+ return accepted;
1123
+ }
1124
+ }
1125
+ }
1126
+ return null;
1127
+ };
1128
+
1129
+ const ensurePinnedOpenCode2CliEnv = async (options = {}) => {
1130
+ const discovered = state.resolvedOpencodeBinary || resolveOpencodeCliPath();
1131
+ const source = state.resolvedOpencodeBinarySource;
1132
+ const explicit = source === 'env' || source === 'settings';
1133
+ try {
1134
+ const result = await ensurePinnedCli({
1135
+ discoveredPath: discovered,
1136
+ preferDiscovered: explicit,
1137
+ env: process.env,
1138
+ homedir: resolveHomeDir,
1139
+ ...options,
1140
+ });
1141
+ if (!result?.path) {
1142
+ return ensureOpencodeCliEnv();
1143
+ }
1144
+ if (result.source === 'installed') {
1145
+ clearWslOpencodeResolution();
1146
+ process.env.OPENCODE_BINARY = result.path;
1147
+ state.resolvedOpencodeBinary = result.path;
1148
+ state.resolvedOpencodeBinarySource = 'installed';
1149
+ }
1150
+ return ensureOpencodeCliEnv();
1151
+ } catch (error) {
1152
+ const bundled = resolveBundledOpenCodeCliPath();
1153
+ if (!bundled) {
1154
+ throw error;
1155
+ }
1156
+ clearWslOpencodeResolution();
1157
+ process.env.OPENCODE_BINARY = bundled;
1158
+ state.resolvedOpencodeBinary = bundled;
1159
+ state.resolvedOpencodeBinarySource = 'bundled';
1160
+ return ensureOpencodeCliEnv();
1161
+ }
1162
+ };
1163
+
1082
1164
  const resolveGitBinaryForSpawn = () => {
1083
1165
  if (process.platform !== 'win32') {
1084
1166
  return 'git';
@@ -1162,6 +1244,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
1162
1244
  return {
1163
1245
  applyLoginShellEnvSnapshot,
1164
1246
  ensureOpencodeCliEnv,
1247
+ ensurePinnedOpenCode2CliEnv,
1165
1248
  applyOpencodeBinaryFromSettings,
1166
1249
  getLoginShellEnvSnapshot,
1167
1250
  resolveOpencodeCliPath,