@vantran-se/openchamber-web 2.0.3 → 2.0.4

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 (93) hide show
  1. package/README.md +20 -28
  2. package/bin/cli.js +3 -1
  3. package/bin/cli.test.js +13 -4
  4. package/bin/lib/DOCUMENTATION.md +4 -1
  5. package/bin/lib/commands-serve.js +9 -0
  6. package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DZF8ntaq.js} +1 -1
  7. package/dist/assets/{DiffView-CX3wAX2S.js → DiffView-D2p4Ar0H.js} +1 -1
  8. package/dist/assets/{FilesView-OL3CON3r.js → FilesView-BeTtbsip.js} +1 -1
  9. package/dist/assets/{GitView-DKIjpnu4.js → GitView-DwQwqDvu.js} +1 -1
  10. package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-CoXM0gh_.js} +1 -1
  11. package/dist/assets/{GuestSurfacePane-B_cqX5I5.js → GuestSurfacePane-DIJvkkM-.js} +1 -1
  12. package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-Cq_27v0y.js} +1 -1
  13. package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-CsSSRMhn.js} +1 -1
  14. package/dist/assets/{MarkdownImageGallery-DFiTFZgj.js → MarkdownImageGallery-CY0dWTaE.js} +1 -1
  15. package/dist/assets/{MarkdownRendererImpl-DZm5sf1W.js → MarkdownRendererImpl-ILdXzLrt.js} +1 -1
  16. package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-ClatMQAX.js} +1 -1
  17. package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-DOwlrIK9.js} +1 -1
  18. package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-CrcGqqD_.js} +1 -1
  19. package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-BGLsMgQk.js} +1 -1
  20. package/dist/assets/{PluginPane-BibJit86.js → PluginPane-B39JEUL8.js} +1 -1
  21. package/dist/assets/{SettingsView-h-VpVO47.js → SettingsView-13B2_G-A.js} +1 -1
  22. package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-B0BAy-f9.js} +1 -1
  23. package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-fcdDzCSW.js} +1 -1
  24. package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-B7xaUz2H.js} +1 -1
  25. package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-CkWCaUmZ.js} +1 -1
  26. package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-DONC4eI2.js} +1 -1
  27. package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-CXDImOXW.js} +1 -1
  28. package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-Br2TLSlb.js} +1 -1
  29. package/dist/assets/{commitAndPush-BIPfOMXY.js → commitAndPush-CcI4ol7c.js} +1 -1
  30. package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-_-20umCs.js} +1 -1
  31. package/dist/assets/{de-BqCNGROu.js → de-BtYnUgII.js} +1 -1
  32. package/dist/assets/{es-BTPpTRgN.js → es-crP5qPye.js} +1 -1
  33. package/dist/assets/{fr-hxbzMdAg.js → fr-CLSRuALk.js} +1 -1
  34. package/dist/assets/{index-CL7ZQ94-.js → index-OKDYjcMF.js} +1 -1
  35. package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-D77M6q1f.js} +1 -1
  36. package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-C54B4Mns.js} +1 -1
  37. package/dist/assets/{ja-_zLwwYiy.js → ja-SZ1-ucLV.js} +1 -1
  38. package/dist/assets/{ko-CFn3Dzhc.js → ko-HthbT_Ve.js} +1 -1
  39. package/dist/assets/{main-BLJaug9I.js → main-BJklC7Cl.js} +4 -4
  40. package/dist/assets/{main-quLSA7jc.js → main-CGEvLMd2.js} +2 -2
  41. package/dist/assets/{markdownCore-DS9FdwrI.js → markdownCore-BzQuETm0.js} +1 -1
  42. package/dist/assets/{miniChat-D-lJJibr.js → miniChat-BSxkRQLj.js} +2 -2
  43. package/dist/assets/{mobile-VTtphbW6.js → mobile-C5KRTNOB.js} +2 -2
  44. package/dist/assets/{patchFileDiff-D9iBcTEi.js → patchFileDiff-DGw2KryM.js} +1 -1
  45. package/dist/assets/{pl-DFHE7xac.js → pl-C8eYfjL1.js} +1 -1
  46. package/dist/assets/{pt-BR-CUjZF179.js → pt-BR-cYOrZdxS.js} +1 -1
  47. package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-CnkAQV6o.js} +2 -2
  48. package/dist/assets/{renderMobileApp-CNQhdXsi.js → renderMobileApp-CMxjwisj.js} +3 -3
  49. package/dist/assets/{runtimeEndpointReset-DeaERc0M.js → runtimeEndpointReset-C45jrFq4.js} +3 -3
  50. package/dist/assets/{scheduledTasksApi-BD6Vx9pu.js → scheduledTasksApi-CkK1F9s2.js} +1 -1
  51. package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-4O0ZM14D.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-yNq7b0DZ.js} +1 -1
  53. package/dist/assets/{tr-BB6sA_zH.js → tr-B4IQ5t-T.js} +1 -1
  54. package/dist/assets/{uk-CB4o8g-o.js → uk-DCoXG6ve.js} +1 -1
  55. package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-Bzyszv2N.js} +9 -9
  56. package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-zOf6ORAq.js} +1 -1
  57. package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-DCaZ4BWi.js} +2 -2
  58. package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-6cEFPNUR.js} +2 -2
  59. package/dist/assets/{web-components-C583Pmfm.js → web-components-BEo4-ISu.js} +1 -1
  60. package/dist/assets/{zh-CN-BRyDuWhc.js → zh-CN-BXBoxsFk.js} +1 -1
  61. package/dist/assets/{zh-TW-BDGbWA1Q.js → zh-TW-C6oelpnL.js} +1 -1
  62. package/dist/index.html +1 -1
  63. package/dist/mini-chat.html +1 -1
  64. package/dist/mobile.html +1 -1
  65. package/package.json +1 -1
  66. package/server/index.d.ts +2 -0
  67. package/server/index.js +114 -52
  68. package/server/lib/agent-tool/DOCUMENTATION.md +47 -127
  69. package/server/lib/agent-tool/runtime.js +66 -47
  70. package/server/lib/agent-tool/runtime.test.js +2 -0
  71. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  72. package/server/lib/event-stream/global-hub.js +22 -6
  73. package/server/lib/event-stream/global-hub.test.js +93 -2
  74. package/server/lib/event-stream/rebind.test.js +1 -0
  75. package/server/lib/event-stream/runtime.js +1 -2
  76. package/server/lib/opencode/DOCUMENTATION.md +23 -23
  77. package/server/lib/opencode/lifecycle.js +126 -0
  78. package/server/lib/opencode/lifecycle.test.js +228 -1
  79. package/server/lib/opencode/network-runtime.js +16 -7
  80. package/server/lib/opencode/network-runtime.test.js +29 -1
  81. package/server/lib/opencode/proxy.js +15 -14
  82. package/server/lib/opencode/server-startup-runtime.js +25 -0
  83. package/server/lib/opencode/server-startup-runtime.test.js +67 -1
  84. package/server/lib/opencode/shared-service-runtime.js +155 -0
  85. package/server/lib/opencode/shared-service-runtime.test.js +180 -0
  86. package/server/lib/opencode/shutdown-runtime.js +11 -5
  87. package/server/lib/opencode/shutdown-runtime.test.js +77 -2
  88. package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
  89. package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
  90. package/server/lib/opencode/watcher.test.js +54 -0
  91. package/server/opencode-proxy.test.js +158 -0
  92. package/server/proxy-headers.js +8 -2
  93. package/server/proxy-headers.test.js +20 -0
package/server/index.js CHANGED
@@ -51,7 +51,13 @@ import {
51
51
  UPSTREAM_STALL_TIMEOUT_CONCURRENT_MS,
52
52
  } from './lib/event-stream/index.js';
53
53
  import { createFsSearchRuntime as createFsSearchRuntimeFactory } from './lib/fs/search.js';
54
- import { createOpenCodeLifecycleRuntime } from './lib/opencode/lifecycle.js';
54
+ import {
55
+ createOpenCodeLifecycleControls,
56
+ createOpenCodeLifecycleRuntime,
57
+ createOpenCodeRecoveryCallbacks,
58
+ createOpenCodeServerComposition,
59
+ } from './lib/opencode/lifecycle.js';
60
+ import { createSharedOpenCodeServiceRuntime } from './lib/opencode/shared-service-runtime.js';
55
61
  import { createOpenCodeEnvRuntime } from './lib/opencode/env-runtime.js';
56
62
  import { providedLoginShellEnvSnapshot } from './lib/opencode/login-shell-env.js';
57
63
  import { resolveOpenCodeEnvConfig } from './lib/opencode/env-config.js';
@@ -655,6 +661,8 @@ const initialOpenCodeAuthState = hmrStateRuntime.resolveOpenCodeAuthFromState({
655
661
  });
656
662
  let openCodeAuthPassword = initialOpenCodeAuthState.openCodeAuthPassword;
657
663
  let openCodeAuthSource = initialOpenCodeAuthState.openCodeAuthSource;
664
+ let openCodeConnectionAdapter = null;
665
+ let sharedOpenCodeServiceRuntime = null;
658
666
 
659
667
  // Sync helper - call after modifying any HMR state variable
660
668
  const syncToHmrState = () => {
@@ -735,7 +743,23 @@ const openCodeAuthStateRuntime = createOpenCodeAuthStateRuntime({
735
743
  syncToHmrState,
736
744
  });
737
745
 
738
- const getOpenCodeAuthHeaders = (...args) => openCodeAuthStateRuntime.getOpenCodeAuthHeaders(...args);
746
+ const getManagedOpenCodeAuthHeaders = (...args) => openCodeAuthStateRuntime.getOpenCodeAuthHeaders(...args);
747
+ const getConnectionOpenCodeAuthHeaders = (...args) => (
748
+ openCodeConnectionAdapter?.getOpenCodeAuthHeaders(...args) ?? getManagedOpenCodeAuthHeaders(...args)
749
+ );
750
+ const getOpenCodeBaseUrl = () => openCodeConnectionAdapter?.getOpenCodeBaseUrl() ?? openCodeBaseUrl;
751
+ const getOpenCodeConnectionKind = () => (
752
+ openCodeConnectionAdapter?.getOpenCodeConnectionKind() ?? 'managed-owned'
753
+ );
754
+ const ownsOpenCodeProcess = () => openCodeConnectionAdapter?.ownsOpenCodeProcess() ?? true;
755
+ const setSharedOpenCodeConnection = (connection) => {
756
+ openCodeBaseUrl = connection.endpoint.url;
757
+ const parsedUrl = new URL(connection.endpoint.url);
758
+ const parsedPort = parsedUrl.port
759
+ ? Number.parseInt(parsedUrl.port, 10)
760
+ : (parsedUrl.protocol === 'https:' ? 443 : 80);
761
+ openCodePort = Number.isFinite(parsedPort) ? parsedPort : null;
762
+ };
739
763
  const isOpenCodeConnectionSecure = (...args) => openCodeAuthStateRuntime.isOpenCodeConnectionSecure(...args);
740
764
  const ensureLocalOpenCodeServerPassword = (...args) => openCodeAuthStateRuntime.ensureLocalOpenCodeServerPassword(...args);
741
765
 
@@ -750,10 +774,12 @@ Object.defineProperties(openCodeNetworkState, {
750
774
 
751
775
  const openCodeNetworkRuntime = createOpenCodeNetworkRuntime({
752
776
  state: openCodeNetworkState,
753
- getOpenCodeAuthHeaders,
777
+ getOpenCodeBaseUrl,
778
+ getOpenCodeAuthHeaders: getConnectionOpenCodeAuthHeaders,
754
779
  configuredOpenCodeHostname: ENV_CONFIGURED_OPENCODE_HOSTNAME,
755
780
  });
756
781
 
782
+ const getOpenCodeAuthHeaders = (...args) => openCodeNetworkRuntime.getOpenCodeAuthHeaders(...args);
757
783
  const waitForReady = (...args) => openCodeNetworkRuntime.waitForReady(...args);
758
784
  const normalizeApiPrefix = (...args) => openCodeNetworkRuntime.normalizeApiPrefix(...args);
759
785
  const setDetectedOpenCodeApiPrefix = (...args) => openCodeNetworkRuntime.setDetectedOpenCodeApiPrefix(...args);
@@ -1113,7 +1139,7 @@ const serverUtilsRuntime = createServerUtilsRuntime({
1113
1139
  longRequestTimeoutMs: LONG_REQUEST_TIMEOUT_MS,
1114
1140
  getRuntime: () => ({
1115
1141
  openCodePort,
1116
- openCodeBaseUrl,
1142
+ openCodeBaseUrl: getOpenCodeBaseUrl(),
1117
1143
  openCodeNotReadySince,
1118
1144
  isOpenCodeReady,
1119
1145
  isRestartingOpenCode,
@@ -1260,6 +1286,19 @@ Object.defineProperties(openCodeLifecycleState, {
1260
1286
  resolvedWslDistro: { get: () => resolvedWslDistro, set: (value) => { resolvedWslDistro = value; } },
1261
1287
  });
1262
1288
 
1289
+ const openCodeRecoveryCallbacks = createOpenCodeRecoveryCallbacks({
1290
+ resetOpenCodeRuntimeProviders,
1291
+ rebindUpstream: () => {
1292
+ if (messageStreamRuntime) {
1293
+ messageStreamRuntime.rebindUpstream();
1294
+ } else {
1295
+ globalMessageStreamHub.rebind();
1296
+ }
1297
+ },
1298
+ interruptBusySessionsAfterRestart: () => sessionRuntime.interruptBusySessionsAfterRestart(),
1299
+ broadcastUiNotification,
1300
+ });
1301
+
1263
1302
  const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
1264
1303
  state: openCodeLifecycleState,
1265
1304
  env: {
@@ -1314,41 +1353,16 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
1314
1353
  // so the UI keeps receiving events instead of staying pinned to the old
1315
1354
  // process (#2638). The runtime is created later by the startup pipeline;
1316
1355
  // by the time any restart runs, it is assigned.
1317
- onOpenCodeRestarted: () => {
1318
- // A restart reloads plugins: provider ports, credentials and the provider
1319
- // list itself can all differ from what was cached.
1320
- resetOpenCodeRuntimeProviders();
1321
- try {
1322
- messageStreamRuntime?.rebindUpstream();
1323
- } catch (error) {
1324
- console.warn('Failed to rebind message stream after OpenCode restart:', error?.message ?? error);
1325
- }
1326
- try {
1327
- const { sessionIds } = sessionRuntime.interruptBusySessionsAfterRestart();
1328
- if (sessionIds.length > 0) {
1329
- const multiple = sessionIds.length > 1;
1330
- broadcastUiNotification({
1331
- title: multiple ? 'Chats interrupted' : 'Chat interrupted',
1332
- body: multiple
1333
- ? 'OpenCode restarted during running responses. Send a message in each chat to continue.'
1334
- : 'OpenCode restarted during a running response. Send a message to continue.',
1335
- tag: 'opencode-restart-interrupted',
1336
- kind: 'opencode-restart-interrupted',
1337
- sessionId: sessionIds[0],
1338
- });
1339
- }
1340
- } catch (error) {
1341
- console.warn('Failed to reconcile sessions after OpenCode restart:', error?.message ?? error);
1342
- }
1343
- },
1356
+ onOpenCodeRestarted: () => openCodeRecoveryCallbacks.onManagedRestarted(),
1344
1357
  getManagedOpenCodeEnv: async () => (managedConfigRuntime ? managedConfigRuntime.buildManagedChildEnv() : {}),
1345
1358
  });
1346
1359
 
1347
1360
  const getOpenCodeCompatibility = async () => {
1348
- if (isExternalOpenCode || ENV_SKIP_OPENCODE_START) {
1349
- const base = ENV_CONFIGURED_OPENCODE_HOST?.origin || openCodeBaseUrl || `http://127.0.0.1:${openCodePort || ENV_EFFECTIVE_PORT}`;
1361
+ if (getOpenCodeConnectionKind() !== 'managed-owned' || ENV_SKIP_OPENCODE_START) {
1362
+ const base = ENV_CONFIGURED_OPENCODE_HOST?.origin || getOpenCodeBaseUrl() || `http://127.0.0.1:${openCodePort || ENV_EFFECTIVE_PORT}`;
1350
1363
  const version = await readExternalOpenCodeVersion(base, getOpenCodeAuthHeaders()).catch(() => null);
1351
- return describeOpenCodeCompatibility(version, 'external', false);
1364
+ const installation = getOpenCodeConnectionKind() === 'shared-local' ? 'shared' : 'external';
1365
+ return describeOpenCodeCompatibility(version, installation, false);
1352
1366
  }
1353
1367
  const binary = ensureOpencodeCliEnv();
1354
1368
  const installation = isBundledOpenCodeCliPath(binary) ? 'bundled' : 'managed';
@@ -1357,6 +1371,14 @@ const getOpenCodeCompatibility = async () => {
1357
1371
  };
1358
1372
 
1359
1373
  const getOpenCodeUpgradeCapability = () => {
1374
+ if (getOpenCodeConnectionKind() === 'shared-local') {
1375
+ return resolveOpenCodeUpgradeCapability({
1376
+ isExternal: true,
1377
+ hasManagedProcess: false,
1378
+ activeBinary: null,
1379
+ isBundledBinary: isBundledOpenCodeCliPath,
1380
+ });
1381
+ }
1360
1382
  const activeBinary = lastOpenCodeLaunchDiagnostics?.sourceBinary
1361
1383
  || lastOpenCodeLaunchDiagnostics?.binary
1362
1384
  || resolvedOpencodeBinary;
@@ -1368,12 +1390,19 @@ const getOpenCodeUpgradeCapability = () => {
1368
1390
  });
1369
1391
  };
1370
1392
 
1371
- const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args);
1393
+ const openCodeLifecycleControls = createOpenCodeLifecycleControls({
1394
+ getKind: getOpenCodeConnectionKind,
1395
+ restartLifecycle: (...args) => openCodeLifecycleRuntime.restartOpenCode(...args),
1396
+ triggerLifecycleHealthCheck: (...args) => openCodeLifecycleRuntime.triggerHealthCheck(...args),
1397
+ recoverShared: () => sharedOpenCodeServiceRuntime.recover(),
1398
+ onSharedRecovered: openCodeRecoveryCallbacks.onSharedRecovered,
1399
+ });
1400
+ const restartOpenCode = (...args) => openCodeLifecycleControls.restart(...args);
1372
1401
  const waitForOpenCodeReady = (...args) => openCodeLifecycleRuntime.waitForOpenCodeReady(...args);
1373
1402
  const waitForAgentPresence = (...args) => openCodeLifecycleRuntime.waitForAgentPresence(...args);
1374
1403
  const refreshOpenCodeAfterConfigChange = (...args) => openCodeLifecycleRuntime.refreshOpenCodeAfterConfigChange(...args);
1375
1404
  const startHealthMonitoring = () => openCodeLifecycleRuntime.startHealthMonitoring(HEALTH_CHECK_INTERVAL);
1376
- const triggerHealthCheck = () => openCodeLifecycleRuntime.triggerHealthCheck();
1405
+ const triggerHealthCheck = (...args) => openCodeLifecycleControls.triggerHealthCheck(...args);
1377
1406
  const scheduledTasksRuntime = createScheduledTasksRuntime({
1378
1407
  projectConfigRuntime,
1379
1408
  listProjects: async () => {
@@ -1595,9 +1624,19 @@ const ensureGlobalWatcherStarted = async () => {
1595
1624
  return globalWatcherStartPromise;
1596
1625
  };
1597
1626
  const bootstrapOpenCodeAtStartup = async (...args) => {
1598
- await openCodeLifecycleRuntime.bootstrapOpenCodeAtStartup(...args);
1627
+ if (getOpenCodeConnectionKind() === 'shared-local') {
1628
+ const connection = await sharedOpenCodeServiceRuntime.connect();
1629
+ setSharedOpenCodeConnection(connection);
1630
+ isOpenCodeReady = true;
1631
+ isExternalOpenCode = true;
1632
+ lastOpenCodeError = null;
1633
+ openCodeNotReadySince = 0;
1634
+ syncToHmrState();
1635
+ } else {
1636
+ await openCodeLifecycleRuntime.bootstrapOpenCodeAtStartup(...args);
1637
+ }
1599
1638
  scheduleOpenCodeApiDetection();
1600
- if (openCodeLifecycleState.openCodeProcess && !openCodeLifecycleState.isExternalOpenCode) {
1639
+ if (ownsOpenCodeProcess() && openCodeLifecycleState.openCodeProcess && !openCodeLifecycleState.isExternalOpenCode) {
1601
1640
  startHealthMonitoring();
1602
1641
  }
1603
1642
  // The global watcher used to start only for desktop notifications; the
@@ -1634,6 +1673,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1634
1673
  contextObligatoryRuntime,
1635
1674
  messageQueueRuntime,
1636
1675
  sessionRuntime,
1676
+ globalEventHub: globalMessageStreamHub,
1637
1677
  getHealthCheckInterval: () => healthCheckInterval,
1638
1678
  clearHealthCheckInterval: (value) => clearInterval(value),
1639
1679
  getTerminalRuntime: () => terminalRuntime,
@@ -1644,7 +1684,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1644
1684
  setMessageStreamRuntime: (value) => {
1645
1685
  messageStreamRuntime = value;
1646
1686
  },
1647
- shouldSkipOpenCodeStop: () => ENV_SKIP_OPENCODE_START || isExternalOpenCode,
1687
+ getOpenCodeConnectionKind,
1648
1688
  getOpenCodePort: () => openCodePort,
1649
1689
  getOpenCodeProcess: () => openCodeProcess,
1650
1690
  setOpenCodeProcess: (value) => {
@@ -1652,6 +1692,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1652
1692
  },
1653
1693
  killProcessOnPort,
1654
1694
  waitForPortRelease,
1695
+ disposeSharedOpenCodeService: () => sharedOpenCodeServiceRuntime?.dispose(),
1655
1696
  getServer: () => server,
1656
1697
  getUiAuthController: () => uiAuthController,
1657
1698
  setUiAuthController: (value) => {
@@ -1676,6 +1717,11 @@ const gracefulShutdown = (...args) => gracefulShutdownRuntime.gracefulShutdown(.
1676
1717
 
1677
1718
  async function main(options = {}) {
1678
1719
  beginGuestServiceHost();
1720
+ const runtimeName = process.env.OPENCHAMBER_RUNTIME || 'web';
1721
+ const lifecycleMode = options.openCodeLifecycleMode === 'managed'
1722
+ || options.openCodeLifecycleMode === 'shared'
1723
+ ? options.openCodeLifecycleMode
1724
+ : undefined;
1679
1725
  const port = Number.isFinite(options.port) && options.port >= 0 ? Math.trunc(options.port) : DEFAULT_PORT;
1680
1726
  const host = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined;
1681
1727
  const effectiveBindHost = host
@@ -1687,7 +1733,6 @@ async function main(options = {}) {
1687
1733
  fsPromises,
1688
1734
  path,
1689
1735
  dataDir: OPENCHAMBER_DATA_DIR,
1690
- env: process.env,
1691
1736
  executeAction: (...args) => openChamberControlService.execute(...args),
1692
1737
  // A v2 tool call carries no directory, only the session it runs in.
1693
1738
  resolveSessionDirectory: (sessionID) => openChamberControlService.resolveSessionDirectory(sessionID),
@@ -1698,15 +1743,31 @@ async function main(options = {}) {
1698
1743
  // A pipe listener reports a string here, which has no address to bind back to.
1699
1744
  getActiveHost: () => server?.address?.()?.address ?? null,
1700
1745
  });
1701
- managedConfigRuntime = createManagedConfigRuntime({
1702
- fsPromises,
1703
- path,
1704
- dataDir: OPENCHAMBER_DATA_DIR,
1705
- env: process.env,
1706
- agentToolRuntime,
1707
- readSettings: () => readSettingsFromDiskMigrated(),
1708
- isAgentMemoryAvailable: isAgentMemoryFeatureAvailable,
1746
+ const openCodeComposition = createOpenCodeServerComposition({
1747
+ runtime: runtimeName,
1748
+ configuredHost: ENV_CONFIGURED_OPENCODE_HOST,
1749
+ lifecycleMode,
1750
+ createSharedRuntime: () => {
1751
+ const binary = ensureOpencodeCliEnv();
1752
+ return createSharedOpenCodeServiceRuntime({
1753
+ ensureOptions: binary ? { command: [binary, 'serve', '--service'] } : undefined,
1754
+ });
1755
+ },
1756
+ createManagedPluginRuntime: () => createManagedConfigRuntime({
1757
+ fsPromises,
1758
+ path,
1759
+ dataDir: OPENCHAMBER_DATA_DIR,
1760
+ env: process.env,
1761
+ agentToolRuntime,
1762
+ readSettings: () => readSettingsFromDiskMigrated(),
1763
+ isAgentMemoryAvailable: isAgentMemoryFeatureAvailable,
1764
+ }),
1765
+ getManagedBaseUrl: () => openCodeBaseUrl,
1766
+ getManagedAuthHeaders: getManagedOpenCodeAuthHeaders,
1709
1767
  });
1768
+ sharedOpenCodeServiceRuntime = openCodeComposition.sharedRuntime;
1769
+ managedConfigRuntime = openCodeComposition.managedPluginRuntime;
1770
+ openCodeConnectionAdapter = openCodeComposition.connection;
1710
1771
 
1711
1772
  // Pairing transports advertised to the create-device dialog. LAN reachability is
1712
1773
  // derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
@@ -1903,7 +1964,7 @@ async function main(options = {}) {
1903
1964
  const bootstrapResult = bootstrapRuntime.setupBaseRoutes(app, {
1904
1965
  process,
1905
1966
  openchamberVersion: OPENCHAMBER_VERSION,
1906
- runtimeName: process.env.OPENCHAMBER_RUNTIME || 'web',
1967
+ runtimeName,
1907
1968
  serverStartedAt,
1908
1969
  gracefulShutdown,
1909
1970
  getHealthSnapshot: () => {
@@ -2199,7 +2260,6 @@ async function main(options = {}) {
2199
2260
  terminalRebindWindowMs: TERMINAL_INPUT_WS_REBIND_WINDOW_MS,
2200
2261
  terminalMaxRebindsPerWindow: TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW,
2201
2262
  setupProxy,
2202
- scheduleOpenCodeApiDetection,
2203
2263
  bootstrapOpenCodeAtStartup,
2204
2264
  triggerHealthCheck,
2205
2265
  staticRoutesRuntime,
@@ -2264,10 +2324,12 @@ async function main(options = {}) {
2264
2324
  scheduledTasks: scheduledTasksRuntime.getStatus(),
2265
2325
  }),
2266
2326
  isReady: () => isOpenCodeReady,
2267
- getManagedOpenCodePreflight: () => openCodeLifecycleRuntime.getManagedOpenCodePreflight(),
2327
+ getManagedOpenCodePreflight: () => ownsOpenCodeProcess()
2328
+ ? openCodeLifecycleRuntime.getManagedOpenCodePreflight()
2329
+ : Promise.resolve(false),
2268
2330
  restartOpenCode: () => restartOpenCode(),
2269
2331
  getOpenCodeProcessInfo: () => {
2270
- const managed = Boolean((openCodeProcess || openCodePort) && !ENV_SKIP_OPENCODE_START && !isExternalOpenCode);
2332
+ const managed = Boolean(ownsOpenCodeProcess() && (openCodeProcess || openCodePort) && !ENV_SKIP_OPENCODE_START && !isExternalOpenCode);
2271
2333
  // Only ever expose pid/port for a server WE manage. The Electron-side
2272
2334
  // killer kills by port (lsof + kill -KILL), so returning a port we don't
2273
2335
  // own — e.g. an external/desktop OpenCode on 4096 we attached to — would
@@ -1,116 +1,55 @@
1
- # Managed OpenChamber Agent Tool
1
+ # Managed OpenChamber agent tools
2
2
 
3
3
  ## Purpose
4
4
 
5
- This module exposes OpenChamber to agents as typed OpenCode custom tools. There
6
- are two, because controlling sessions and driving a page are separate intents
7
- the user can want independently:
8
-
9
- - `openchamber` — projects, sessions, worktrees, scheduled tasks, and showing
10
- a file to the user (`file.open`). Enabled while the persisted
11
- `agentControlToolEnabled` setting is not `false`.
12
- - `openchamber_web` — looking at and interacting with the page in OpenChamber's
13
- browser panel. Enabled while `agentWebToolEnabled` is not `false`.
14
-
15
- Both default to on, are toggled in Settings → General → OpenCode CLI, and take
16
- effect in the running OpenCode within a couple of seconds — OpenChamber rewrites
17
- the managed config file OpenCode watches (see
18
- `lib/opencode/managed-config-file.js`). Installs where the user's own
19
- environment sets `OPENCODE_CONFIG` fall back to `OPENCODE_CONFIG_CONTENT` and
20
- still need a restart for a toggle. Each tool carries only its own actions and
21
- only the parameters those actions use, so turning one off removes its inputs
22
- from the schema rather than leaving them visible. The plugin is injected only
23
- when OpenChamber launches and owns the OpenCode process, and not at all when
24
- both settings are `false`.
25
-
26
- - The plugin accepts the action's inputs either inside `parameters` or beside
27
- `action`, because models produce both shapes; an explicit `parameters` object
28
- wins on a conflict. Rejecting the flattened shape turned a call that plainly
29
- carried a `url` into "url is required", which reads as a broken tool rather
30
- than a malformed call.
31
-
32
- ## Runtime flow
33
-
34
- 1. The OpenChamber HTTP listener binds and publishes its authoritative port.
35
- 2. `materializePlugin()` writes the plugin under
36
- `<openchamber-data-dir>/agent-tool/` and returns its directory; the managed
37
- config layer lists that directory in `<data-dir>/opencode.managed.json` and
38
- rewrites the file whenever a tool setting changes.
39
- 3. `createChildEnv()` adds a random per-child token and the callback URL to the
40
- managed OpenCode child environment. They are present even while every tool
41
- is off, so a tool switched on later can call back without a restart. The URL
42
- points at loopback, except when the listener is bound to one concrete
43
- address (`--host <ip>`): that socket does not answer on loopback, so the URL
44
- uses the bound address instead.
45
- 4. The plugin calls `POST /api/openchamber/agent-tool` with its typed input and
46
- the session id OpenCode gives the tool; OpenChamber resolves the session's
47
- directory on its own side.
48
- 5. The route delegates the fixed action allowlist directly to the shared
49
- OpenChamber control service. The CLI uses the same service through its
50
- authenticated HTTP adapter, so Goal Mode ordering, wait behavior,
51
- partial-failure reporting, and scheduled-task contracts have one owner.
52
- 6. Each action definition owns a short presentation title and a separate
53
- agent-facing description. The generated schema uses the description to state
54
- required inputs or one non-obvious behavior, while completed calls use the
55
- short title in native tool metadata.
56
-
57
- ## Agent context budget
58
-
59
- - The tool exposes one shared parameter object rather than repeating parameters
60
- in a large per-action union. Action descriptions carry only required inputs,
61
- defaults, or one non-obvious semantic detail.
62
- - The action schema carries `oneOf` and no `enum`. A node combining `enum` and
63
- `oneOf` is valid JSON Schema, but some OpenAI-compatible gateways reject it
64
- and answer with an empty completion instead of an error.
65
- - Obvious fields rely on their names and JSON types. Parameter descriptions are
66
- reserved for formats, dependencies, scope, and behavior that cannot be safely
67
- inferred from the field name.
68
- - Session dispatches do not wait by default. Agents are told to set `wait` only
69
- when the user asks or the next step requires the completed result.
70
- - The tool exposes only agent-relevant actions
71
- (`OPENCHAMBER_AGENT_TOOL_ACTIONS`): `schedule.status` stays CLI-only because
72
- `schedule.list` already returns scheduler status, and enable/disable are one
73
- `schedule.toggle` action driven by the `disabled` boolean.
74
- - The tool description frames intent: created sessions and scheduled tasks are
75
- user-facing work the user follows up with. The agent must not decide on its
76
- own to delegate parts of its current task, but an explicit user request to
77
- create, send, or schedule always wins, even when it relates to the current
78
- task (strict models otherwise read the old unconditional "never delegate" as
79
- a hard ban and refused user-requested sends).
80
- - Optional behavior switches (`worktree`, `goal`, `agent`, `variant`, `wait`)
81
- state their default and an explicit "only when the user asks" rule so agents
82
- do not invent worktrees, goal mode, or waits the user never requested.
83
- - Detailed combination rules are enforced by the shared control service and
84
- returned as actionable usage errors only after an invalid call. Per-action
85
- examples and a repeated per-action parameter schema are intentionally omitted.
5
+ This module exposes OpenChamber actions as typed OpenCode custom tools for Electron Desktop's managed OpenCode child.
6
+
7
+ The main tools are:
8
+
9
+ - `openchamber` for projects, sessions, worktrees, scheduled tasks, and `file.open`
10
+ - `openchamber_web` for inspecting and interacting with the page in OpenChamber's browser panel
11
+
12
+ Their persisted settings control whether each tool is present. Tool definitions include only the actions and inputs enabled for that tool.
13
+
14
+ ## Runtime boundary
15
+
16
+ Electron Desktop owns the managed integration:
17
+
18
+ 1. `materializePlugin()` writes the generated plugin under `<openchamber-data-dir>/agent-tool/`.
19
+ 2. The managed OpenCode config lists that plugin directory.
20
+ 3. `createChildEnv()` gives the owned OpenCode child a random callback token and loopback callback URL.
21
+ 4. The plugin calls `POST /api/openchamber/agent-tool` with its typed input and OpenCode session ID.
22
+ 5. OpenChamber resolves the session directory and delegates the fixed action allowlist to the shared control service.
23
+
24
+ The callback token stays in the managed child environment. It is never persisted, logged, returned to the UI, or written into the generated plugin.
25
+
26
+ Ordinary Web does not install a plugin, publish callback registration, or advertise managed OpenChamber tools to the shared local OpenCode service. This keeps shared Web limited to sessions and live events. A valid `OPENCODE_HOST` is also outside managed-tool ownership, so OpenChamber does not guarantee tools on that endpoint.
27
+
28
+ VS Code keeps its separate lifecycle. Hosted and Capacitor clients do not run tools themselves. They may connect to a Desktop-owned backend that has managed tools, but an ordinary Web backend does not add them.
29
+
30
+ ## Input contract
31
+
32
+ The plugin accepts action inputs inside `parameters` or beside `action`. An explicit `parameters` object wins when both forms provide the same field.
33
+
34
+ Each action definition has a short presentation title and a separate agent-facing description. The generated schema uses descriptions only for required inputs, defaults, or behavior that a field name cannot explain.
35
+
36
+ The schema uses one shared parameter object with `oneOf` and no `enum`. Some OpenAI-compatible gateways reject a node that combines both and return an empty completion.
37
+
38
+ Session dispatch does not wait by default. Optional switches such as `worktree`, `goal`, `agent`, `variant`, and `wait` state their defaults and tell the agent not to invent them.
86
39
 
87
40
  ## Security invariants
88
41
 
89
- - The callback accepts same-machine requests only and requires the current
90
- per-child bearer token using a timing-safe comparison. Same-machine means a
91
- loopback source, or, for a listener bound to one concrete address, a source
92
- equal to that address: the OS sources a local connection to `<ip>` from
93
- `<ip>`. A wildcard bind keeps the loopback-only rule, and another machine on
94
- the network always arrives with its own address.
95
- - The token is never persisted, logged, returned to the UI, or written into
96
- the materialized plugin.
97
- - The plugin adds the callback host to `NO_PROXY`/`no_proxy` inside the managed
98
- child when it loads. Without that, an `HTTP_PROXY` in the child's environment
99
- would receive a non-loopback callback, token included, because `fetch` has no
100
- per-request way to skip the environment proxy.
101
- - Inputs map to a fixed action and parameter allowlist. There is no arbitrary
102
- CLI, shell, route, or URL forwarding.
103
- - Session/worktree deletion and project-path registration are not exposed.
104
- - A cancelled turn aborts the session's in-flight actions. OpenCode 2 gives a
105
- plugin tool no abort signal and the plugin's request stays open, so the
106
- server tracks in-flight actions per session and `abortSession(sessionID)` is
107
- called from the event dispatch in `server/index.js` when the stream reports
108
- `session.idle` with `aborted: true`; the abort signal then reaches the
109
- shared service as before. A dropped HTTP request still aborts as well.
42
+ - The callback accepts same-machine requests only and requires the current managed-child bearer token with a timing-safe comparison.
43
+ - A concrete listener address may accept a source equal to that address. A wildcard listener remains loopback-only.
44
+ - The plugin adds its callback host to `NO_PROXY` and `no_proxy` in the managed child so an environment proxy cannot receive the callback token.
45
+ - Inputs map to fixed tool, action, and parameter allowlists. There is no arbitrary CLI, shell, route, or URL forwarding.
46
+ - Project-path registration and session or worktree deletion are not exposed.
47
+ - A dropped callback request aborts its action.
48
+ - OpenCode 2 does not give plugin tools an abort signal. The server therefore tracks actions by session and aborts them when the event stream reports an aborted `session.idle` transition.
110
49
 
111
50
  ## Result contract
112
51
 
113
- Every completed call returns JSON:
52
+ Completed calls return JSON:
114
53
 
115
54
  ```json
116
55
  {
@@ -121,29 +60,10 @@ Every completed call returns JSON:
121
60
  }
122
61
  ```
123
62
 
124
- Command and operational failures use the same envelope with `ok: false` and
125
- an `error` object. OpenCode-level cancellation can still produce a native tool
126
- error state.
127
-
128
- ## Runtime parity
129
-
130
- - Web and Desktop managed OpenCode: injected automatically.
131
- - External OpenCode selected with `OPENCODE_HOST` or skip-start: not injected,
132
- because OpenChamber does not control that process environment.
133
- - VS Code: not injected; the extension owns a separate OpenCode lifecycle.
134
- - Hosted and Capacitor mobile clients use the server's managed OpenCode tool
135
- when connected to such a server; no tool runs in the client runtime.
63
+ Command and operational failures use the same envelope with `ok: false` and an `error` object. OpenCode-level cancellation can still produce a native tool error.
136
64
 
137
- ## The calling tool is part of the request
65
+ ## Action resolution
138
66
 
139
- Each generated tool sends its own name with every callback. Models routinely
140
- drop the namespace their tool's name appears to supply — `openchamber_memory`
141
- asked for `memory.read` gets called as `read` — and resolving the bare name
142
- inside the calling tool's action set makes that unambiguous even where it is not
143
- globally (`delete` belongs to both schedule and memory).
67
+ Each generated tool sends its own name with every callback. Action resolution stays within that tool's allowlist. For example, a bare `open` requested by a memory tool cannot drive the browser.
144
68
 
145
- Resolution never reaches outside the tool that asked: `open` from the memory
146
- tool fails rather than driving the browser. An unresolvable action answers with
147
- the actions that tool actually has, because an error that only says
148
- "unsupported" leaves the model to guess a second wrong name — which is exactly
149
- what happened before this existed.
69
+ When an action name is invalid, the response lists the actions available to the calling tool. This lets the model correct the call without guessing across tool namespaces.