@vantran-se/openchamber-web 1.22.1 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/bin/cli.js +11 -1
- package/bin/cli.test.js +92 -0
- package/bin/lib/cli-startup.js +49 -0
- package/bin/lib/commands-startup.js +41 -6
- package/dist/assets/{CodeMirrorCommentWidgets-C53Q7pbX.js → CodeMirrorCommentWidgets-DFg2wnpo.js} +1 -1
- package/dist/assets/DiffView-3Dn4QHbu.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +1 -0
- package/dist/assets/FilesView-BdTOpew9.js +17 -0
- package/dist/assets/GitView-Dqf9vHJh.js +6 -0
- package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-BKdsjg5B.js} +1 -1
- package/dist/assets/LinearIssuesView-CmbeL7I6.js +1 -0
- package/dist/assets/MarkdownImageGallery-_5gEVr0X.js +1 -0
- package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +12 -0
- package/dist/assets/OnboardingScreen-DdP4_nuc.js +1 -0
- package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-am3SAss2.js} +1 -1
- package/dist/assets/PierreDiffViewer-DCsydlKs.js +161 -0
- package/dist/assets/PlanView-BoJxdRZe.js +5 -0
- package/dist/assets/SettingsView-CcM8pF5O.js +28 -0
- package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DVTdDSol.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-BgfTbDZ4.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-C7WLk5i9.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-DcdgkH-x.js} +1 -1
- package/dist/assets/WalkthroughView-BEVUDxuw.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-C8byMnRh.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-CirXEPJq.js +1 -0
- package/dist/assets/de-C8jV7J9y.js +10 -0
- package/dist/assets/es-BI6J1M5R.js +16 -0
- package/dist/assets/fr-D6oDFt7E.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/{index-DmhujXbT.js → index-BB7ryz8y.js} +1 -1
- package/dist/assets/index-BqhiCmYS.js +39 -0
- package/dist/assets/index-FqSOBz0J.css +1 -0
- package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-45ujMPGb.js} +1 -1
- package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-Cc4KFSYi.js} +1 -1
- package/dist/assets/ja-C_4i5EQu.js +16 -0
- package/dist/assets/ko-BFbOTpbq.js +16 -0
- package/dist/assets/{main-B2a_JfKZ.js → main-C9ETLKFc.js} +2 -2
- package/dist/assets/main-CJmb2dF8.js +930 -0
- package/dist/assets/markdownCore-DVuZpnli.js +5 -0
- package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-B4Ytoq2n.js} +2 -2
- package/dist/assets/{mobile-QNcFIVCW.js → mobile-BUT6-lo1.js} +2 -2
- package/dist/assets/patchFileDiff-8j1apICU.js +13 -0
- package/dist/assets/pl-COFvT56l.js +16 -0
- package/dist/assets/pt-BR-DtVOO2Lj.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +2 -0
- package/dist/assets/renderMobileApp-BNgHraq6.js +25 -0
- package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +17 -0
- package/dist/assets/scheduledTasksApi-GC4epIYA.js +1 -0
- package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-BJISeZsQ.js} +1 -1
- package/dist/assets/surface-BvRzS58U.js +1 -0
- package/dist/assets/tr-B8Q0Go2l.js +7 -0
- package/dist/assets/uk-Dk4a-Fn-.js +16 -0
- package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-DpFNefUA.js} +153 -179
- package/dist/assets/useGitComparison-Dfxaqd0s.js +1 -0
- package/dist/assets/useInlineCommentController-CscsE4t-.js +2 -0
- package/dist/assets/useRootScrollLock-BaCvuRlT.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-DzlSIu_W.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-BNMwa0qm.js → web-components-CdGfR2XJ.js} +1 -1
- package/dist/assets/zh-CN-B_BXxG1Q.js +16 -0
- package/dist/assets/zh-TW-DLrl1SPZ.js +16 -0
- package/dist/index.html +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +8 -5
- package/server/index.d.ts +15 -0
- package/server/index.js +82 -13
- package/server/lib/agent-tool/runtime.js +2 -19
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/runtime.js +4 -4
- package/server/lib/dev-tunnel/tunnel.test.js +20 -0
- package/server/lib/event-stream/runtime.test.js +12 -12
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +63 -21
- package/server/lib/fs/routes.test.js +187 -1
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/routes.js +40 -4
- package/server/lib/git/service.js +291 -215
- package/server/lib/git/service.test.js +593 -25
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/openchamber-sessions/routes.js +102 -0
- package/server/lib/openchamber-sessions/routes.test.js +88 -0
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.js +7 -0
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +130 -3
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/routes.js +9 -7
- package/server/lib/opencode/server-startup-runtime.js +1 -1
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +113 -41
- package/server/lib/opencode/settings-helpers.test.js +342 -0
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +4 -1
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/package-manager.js +4 -29
- package/server/lib/package-manager.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +15 -0
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +142 -9
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +1 -1
- package/server/lib/quota/credentials/store.test.js +4 -4
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +2 -1
- package/server/lib/quota/providers/opencode-go.test.js +31 -2
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +37 -8
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/security/request-security.js +21 -6
- package/server/lib/security/request-security.test.js +42 -8
- package/server/lib/session-assist/runtime.js +7 -10
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +3 -1
- package/server/lib/session-goal/runtime.js +81 -20
- package/server/lib/session-goal/runtime.test.js +426 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +17 -6
- package/server/lib/small-model/call.test.js +52 -0
- package/server/lib/small-model/index.js +7 -11
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +2 -21
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +571 -37
- package/server/lib/terminal/shells.js +17 -0
- package/server/lib/terminal/shells.test.js +32 -1
- package/server/lib/ui-auth/ui-auth.js +4 -1
- package/server/lib/ui-auth/ui-auth.test.js +16 -0
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
- package/dist/assets/DiffView-Dh1ey2x9.js +0 -4
- package/dist/assets/FilesView-3ZYP72ki.js +0 -17
- package/dist/assets/GitView-CMuN7z2Z.js +0 -6
- package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
- package/dist/assets/MarkdownImageGallery-bA0PS-tn.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
- package/dist/assets/OnboardingScreen-D3kYJIb6.js +0 -1
- package/dist/assets/PierreDiffViewer-Ct2A13NO.js +0 -157
- package/dist/assets/PlanView-6AUjzNQn.js +0 -5
- package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
- package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
- package/dist/assets/WalkthroughView-AX-u2pFg.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C9EcRSDF.js +0 -1
- package/dist/assets/de-DqVn5vme.js +0 -9
- package/dist/assets/es-hj43Z53i.js +0 -15
- package/dist/assets/fr-BmkZt2mF.js +0 -13
- package/dist/assets/index-L_ncV-6H.css +0 -1
- package/dist/assets/index-qES19dlJ.js +0 -7
- package/dist/assets/ja-DioMIEGF.js +0 -15
- package/dist/assets/ko-BqPgaQJb.js +0 -15
- package/dist/assets/main-CYgSu6dx.js +0 -928
- package/dist/assets/markdownCore-CWRCqSH2.js +0 -3
- package/dist/assets/patchFileDiff-BpMQZnYu.js +0 -10
- package/dist/assets/pl-slULBF4R.js +0 -15
- package/dist/assets/pt-BR-BqH8fuCH.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-D77bmLqo.js +0 -2
- package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
- package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
- package/dist/assets/scheduledTasksApi-BSRCtP6T.js +0 -1
- package/dist/assets/tr-CjEbw1Aw.js +0 -6
- package/dist/assets/uk-BkkfxbOs.js +0 -15
- package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
- package/dist/assets/useRootScrollLock-C0N9mCwz.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-C6FMdVgv.js +0 -15
- package/dist/assets/zh-TW-CdcMjDxv.js +0 -15
|
@@ -29,11 +29,13 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
29
29
|
readSettingsFromDiskMigrated,
|
|
30
30
|
fetchFreeZenModels,
|
|
31
31
|
getCachedZenModels,
|
|
32
|
+
desktopUpdater,
|
|
32
33
|
} = dependencies;
|
|
33
34
|
|
|
35
|
+
let desktopRestartError = null;
|
|
36
|
+
|
|
34
37
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
35
38
|
try {
|
|
36
|
-
const { checkForUpdates } = await import('../package-manager.js');
|
|
37
39
|
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
38
40
|
const parseReportUsage = (value) => {
|
|
39
41
|
if (typeof value !== 'string') return true;
|
|
@@ -49,8 +51,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
49
51
|
return 'desktop';
|
|
50
52
|
};
|
|
51
53
|
const userAgent = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : '';
|
|
52
|
-
|
|
53
|
-
const updateInfo = await checkForUpdates({
|
|
54
|
+
const updateRequest = {
|
|
54
55
|
appType: parseString(req.query.appType),
|
|
55
56
|
deviceClass: parseString(req.query.deviceClass) || inferDeviceClass(userAgent),
|
|
56
57
|
platform: parseString(req.query.platform),
|
|
@@ -59,7 +60,31 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
59
60
|
currentVersion: parseString(req.query.currentVersion),
|
|
60
61
|
installId: parseString(req.query.installId),
|
|
61
62
|
reportUsage: parseReportUsage(parseString(req.query.reportUsage)),
|
|
62
|
-
}
|
|
63
|
+
};
|
|
64
|
+
let updateInfo;
|
|
65
|
+
if (process.env.OPENCHAMBER_RUNTIME === 'desktop' && updateRequest.appType === 'web') {
|
|
66
|
+
if (desktopRestartError && req.query.updateStatus === 'true') {
|
|
67
|
+
return res.status(503).json({
|
|
68
|
+
code: 'DESKTOP_UPDATE_RESTART_FAILED',
|
|
69
|
+
error: desktopRestartError,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (typeof desktopUpdater?.check !== 'function') {
|
|
73
|
+
return res.status(503).json({
|
|
74
|
+
available: false,
|
|
75
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
76
|
+
error: 'The desktop updater is not available.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
updateInfo = {
|
|
80
|
+
...await desktopUpdater.check(),
|
|
81
|
+
packageManager: 'electron',
|
|
82
|
+
updateOwner: 'electron-updater',
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
const { checkForUpdates } = await import('../package-manager.js');
|
|
86
|
+
updateInfo = await checkForUpdates(updateRequest);
|
|
87
|
+
}
|
|
63
88
|
res.json(updateInfo);
|
|
64
89
|
} catch (error) {
|
|
65
90
|
console.error('Failed to check for updates:', error);
|
|
@@ -72,6 +97,41 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
72
97
|
|
|
73
98
|
app.post('/api/openchamber/update-install', async (_req, res) => {
|
|
74
99
|
try {
|
|
100
|
+
if (process.env.OPENCHAMBER_RUNTIME === 'desktop') {
|
|
101
|
+
if (typeof desktopUpdater?.install !== 'function' || typeof desktopUpdater?.restart !== 'function') {
|
|
102
|
+
return res.status(503).json({
|
|
103
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
104
|
+
error: 'The desktop updater is not available.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
desktopRestartError = null;
|
|
109
|
+
const updateInfo = await desktopUpdater.install();
|
|
110
|
+
if (!updateInfo?.available) {
|
|
111
|
+
return res.status(400).json({ error: 'No update available' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.json({
|
|
115
|
+
success: true,
|
|
116
|
+
message: 'Desktop update downloaded, host will restart shortly',
|
|
117
|
+
version: updateInfo.version,
|
|
118
|
+
packageManager: 'electron',
|
|
119
|
+
updateOwner: 'electron-updater',
|
|
120
|
+
autoRestart: true,
|
|
121
|
+
restartManager: 'electron-updater',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
setImmediate(() => {
|
|
125
|
+
Promise.resolve()
|
|
126
|
+
.then(() => desktopUpdater.restart())
|
|
127
|
+
.catch((error) => {
|
|
128
|
+
desktopRestartError = error instanceof Error ? error.message : 'Failed to restart after desktop update';
|
|
129
|
+
console.error('Failed to restart after desktop update:', error);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
75
135
|
const { spawn: spawnChild, spawnSync } = await import('child_process');
|
|
76
136
|
const {
|
|
77
137
|
checkForUpdates,
|
|
@@ -18,7 +18,7 @@ const childProcess = await import('child_process');
|
|
|
18
18
|
const packageManager = await import('../package-manager.js');
|
|
19
19
|
const { registerOpenChamberRoutes } = await import('./openchamber-routes.js');
|
|
20
20
|
|
|
21
|
-
const createApp = ({ environment = {}, storedOptions = {} } = {}) => {
|
|
21
|
+
const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}) => {
|
|
22
22
|
const app = express();
|
|
23
23
|
const dependencies = {
|
|
24
24
|
fs: {
|
|
@@ -47,6 +47,7 @@ const createApp = ({ environment = {}, storedOptions = {} } = {}) => {
|
|
|
47
47
|
readSettingsFromDiskMigrated: vi.fn(),
|
|
48
48
|
fetchFreeZenModels: vi.fn(),
|
|
49
49
|
getCachedZenModels: vi.fn(),
|
|
50
|
+
desktopUpdater,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
registerOpenChamberRoutes(app, dependencies);
|
|
@@ -62,7 +63,7 @@ beforeEach(() => {
|
|
|
62
63
|
packageManager: 'npm',
|
|
63
64
|
});
|
|
64
65
|
packageManager.getUpdateCommand.mockReturnValue(
|
|
65
|
-
'npm install -g @vantran-se/openchamber-web@latest --registry=https://
|
|
66
|
+
'npm install -g @vantran-se/openchamber-web@latest --registry=https://registry.npmjs.org',
|
|
66
67
|
);
|
|
67
68
|
});
|
|
68
69
|
|
|
@@ -71,6 +72,132 @@ afterEach(() => {
|
|
|
71
72
|
vi.clearAllMocks();
|
|
72
73
|
});
|
|
73
74
|
|
|
75
|
+
describe('OpenChamber desktop host update route', () => {
|
|
76
|
+
it('reports a restart rejection until the user retries installation', async () => {
|
|
77
|
+
const desktopUpdater = {
|
|
78
|
+
check: vi.fn(async () => ({ available: true, currentVersion: '1.17.0', version: '1.17.1' })),
|
|
79
|
+
install: vi.fn(async () => ({ available: true, version: '1.17.1' })),
|
|
80
|
+
restart: vi.fn().mockRejectedValueOnce(new Error('Signature rejected')).mockResolvedValue(undefined),
|
|
81
|
+
};
|
|
82
|
+
const { app } = createApp({ environment: { OPENCHAMBER_RUNTIME: 'desktop' }, desktopUpdater });
|
|
83
|
+
const logError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
84
|
+
|
|
85
|
+
await request(app).post('/api/openchamber/update-install').expect(200);
|
|
86
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
87
|
+
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false&updateStatus=true').expect(503, {
|
|
88
|
+
code: 'DESKTOP_UPDATE_RESTART_FAILED', error: 'Signature rejected',
|
|
89
|
+
});
|
|
90
|
+
expect(desktopUpdater.check).not.toHaveBeenCalled();
|
|
91
|
+
expect(logError).toHaveBeenCalledOnce();
|
|
92
|
+
// Availability remains reachable after a browser reload, so users can retry.
|
|
93
|
+
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
|
94
|
+
|
|
95
|
+
await request(app).post('/api/openchamber/update-install').expect(200);
|
|
96
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
97
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
|
98
|
+
expect(response.body.currentVersion).toBe('1.17.0');
|
|
99
|
+
expect(response.body.updateOwner).toBe('electron-updater');
|
|
100
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects native checks without a bridge and preserves explicit non-web checks', async () => {
|
|
104
|
+
const { app } = createApp({ environment: { OPENCHAMBER_RUNTIME: 'desktop' } });
|
|
105
|
+
await request(app).get('/api/openchamber/update-check?appType=web').expect(503, {
|
|
106
|
+
available: false, code: 'DESKTOP_UPDATER_UNAVAILABLE', error: 'The desktop updater is not available.',
|
|
107
|
+
});
|
|
108
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
109
|
+
await request(app).get('/api/openchamber/update-check?appType=desktop-electron').expect(200);
|
|
110
|
+
expect(packageManager.checkForUpdates).toHaveBeenCalledOnce();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('uses electron-updater to check for Web client updates', async () => {
|
|
114
|
+
const desktopUpdater = {
|
|
115
|
+
check: vi.fn(async () => ({
|
|
116
|
+
available: true,
|
|
117
|
+
currentVersion: '1.17.0',
|
|
118
|
+
version: '1.17.1',
|
|
119
|
+
})),
|
|
120
|
+
install: vi.fn(),
|
|
121
|
+
restart: vi.fn(),
|
|
122
|
+
};
|
|
123
|
+
const { app } = createApp({
|
|
124
|
+
environment: {
|
|
125
|
+
OPENCHAMBER_RUNTIME: 'desktop',
|
|
126
|
+
},
|
|
127
|
+
desktopUpdater,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await request(app)
|
|
131
|
+
.get('/api/openchamber/update-check?appType=web&reportUsage=false')
|
|
132
|
+
.expect(200, {
|
|
133
|
+
available: true,
|
|
134
|
+
currentVersion: '1.17.0',
|
|
135
|
+
version: '1.17.1',
|
|
136
|
+
packageManager: 'electron',
|
|
137
|
+
updateOwner: 'electron-updater',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(desktopUpdater.check).toHaveBeenCalledOnce();
|
|
141
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('installs through electron-updater and restarts after responding', async () => {
|
|
145
|
+
const desktopUpdater = {
|
|
146
|
+
check: vi.fn(),
|
|
147
|
+
install: vi.fn(async () => ({
|
|
148
|
+
available: true,
|
|
149
|
+
version: '1.17.1',
|
|
150
|
+
})),
|
|
151
|
+
restart: vi.fn(),
|
|
152
|
+
};
|
|
153
|
+
const { app } = createApp({
|
|
154
|
+
environment: {
|
|
155
|
+
OPENCHAMBER_RUNTIME: 'desktop',
|
|
156
|
+
},
|
|
157
|
+
desktopUpdater,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await request(app)
|
|
161
|
+
.post('/api/openchamber/update-install')
|
|
162
|
+
.expect(200, {
|
|
163
|
+
success: true,
|
|
164
|
+
message: 'Desktop update downloaded, host will restart shortly',
|
|
165
|
+
version: '1.17.1',
|
|
166
|
+
packageManager: 'electron',
|
|
167
|
+
updateOwner: 'electron-updater',
|
|
168
|
+
autoRestart: true,
|
|
169
|
+
restartManager: 'electron-updater',
|
|
170
|
+
});
|
|
171
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
172
|
+
|
|
173
|
+
expect(desktopUpdater.install).toHaveBeenCalledOnce();
|
|
174
|
+
expect(desktopUpdater.restart).toHaveBeenCalledOnce();
|
|
175
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
176
|
+
expect(packageManager.detectPackageManagerDetails).not.toHaveBeenCalled();
|
|
177
|
+
expect(packageManager.getUpdateCommand).not.toHaveBeenCalled();
|
|
178
|
+
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
179
|
+
expect(childProcess.spawnSync).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('fails safely when the Electron updater bridge is unavailable', async () => {
|
|
183
|
+
const { app } = createApp({
|
|
184
|
+
environment: {
|
|
185
|
+
OPENCHAMBER_RUNTIME: 'desktop',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await request(app)
|
|
190
|
+
.post('/api/openchamber/update-install')
|
|
191
|
+
.expect(503, {
|
|
192
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
193
|
+
error: 'The desktop updater is not available.',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
197
|
+
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
74
201
|
describe('OpenChamber foreground update route', () => {
|
|
75
202
|
it('rejects a foreground update when the server is not owned by systemd', async () => {
|
|
76
203
|
const { app } = createApp();
|
|
@@ -133,7 +260,7 @@ describe('OpenChamber foreground update route', () => {
|
|
|
133
260
|
'--setenv=PATH=/home/syu/.npm-global/bin:/usr/bin:/bin',
|
|
134
261
|
'/bin/sh',
|
|
135
262
|
'-c',
|
|
136
|
-
"set -eu\nnpm install -g @vantran-se/openchamber-web@latest --registry=https://
|
|
263
|
+
"set -eu\nnpm install -g @vantran-se/openchamber-web@latest --registry=https://registry.npmjs.org\nsystemctl --user restart 'openchamber@wsl.service'",
|
|
137
264
|
], {
|
|
138
265
|
encoding: 'utf8',
|
|
139
266
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import {
|
|
5
4
|
AGENT_SCOPE,
|
|
5
|
+
OPENCODE_CONFIG_DIR,
|
|
6
6
|
readConfigFile,
|
|
7
7
|
readConfigLayer,
|
|
8
8
|
writeConfig,
|
|
@@ -71,7 +71,7 @@ function getActiveOpencodeConfigDir() {
|
|
|
71
71
|
if (customConfigPath) {
|
|
72
72
|
return path.dirname(path.resolve(customConfigPath));
|
|
73
73
|
}
|
|
74
|
-
return
|
|
74
|
+
return OPENCODE_CONFIG_DIR;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function getActiveUserConfigPaths() {
|
|
@@ -152,6 +152,37 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
152
152
|
return { ok: true, value: { providerId, config: normalized } };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function mergeCustomProviderConfig(existingValue, normalizedConfig) {
|
|
156
|
+
const existing = isPlainObject(existingValue) ? existingValue : {};
|
|
157
|
+
const existingOptions = isPlainObject(existing.options) ? existing.options : {};
|
|
158
|
+
const normalizedOptions = isPlainObject(normalizedConfig.options) ? normalizedConfig.options : {};
|
|
159
|
+
const mergedOptions = { ...existingOptions, ...normalizedOptions };
|
|
160
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'headers')) {
|
|
161
|
+
delete mergedOptions.headers;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const existingModels = isPlainObject(existing.models) ? existing.models : {};
|
|
165
|
+
const normalizedModels = isPlainObject(normalizedConfig.models) ? normalizedConfig.models : {};
|
|
166
|
+
const mergedModels = Object.fromEntries(
|
|
167
|
+
Object.entries(normalizedModels).map(([modelId, normalizedModel]) => {
|
|
168
|
+
const existingModel = isPlainObject(existingModels[modelId]) ? existingModels[modelId] : {};
|
|
169
|
+
const nextModel = isPlainObject(normalizedModel) ? normalizedModel : {};
|
|
170
|
+
return [modelId, { ...existingModel, ...nextModel }];
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const merged = {
|
|
175
|
+
...existing,
|
|
176
|
+
...normalizedConfig,
|
|
177
|
+
options: mergedOptions,
|
|
178
|
+
models: mergedModels,
|
|
179
|
+
};
|
|
180
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedConfig, 'env')) {
|
|
181
|
+
delete merged.env;
|
|
182
|
+
}
|
|
183
|
+
return merged;
|
|
184
|
+
}
|
|
185
|
+
|
|
155
186
|
/**
|
|
156
187
|
* Persist (create or update) a custom provider block in OpenCode user/project/custom config.
|
|
157
188
|
* Does not write secrets — API keys remain in auth.json via the OpenCode auth API.
|
|
@@ -183,8 +214,19 @@ function upsertProviderConfig(providerId, config, workingDirectory, scope = 'use
|
|
|
183
214
|
|
|
184
215
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
185
216
|
const providerConfig = isPlainObject(targetConfig.provider) ? { ...targetConfig.provider } : {};
|
|
186
|
-
|
|
217
|
+
const providersAlias = isPlainObject(targetConfig.providers) ? { ...targetConfig.providers } : {};
|
|
218
|
+
const existingProvider = providerConfig[validated.value.providerId] ?? providersAlias[validated.value.providerId];
|
|
219
|
+
const mergedConfig = mergeCustomProviderConfig(existingProvider, validated.value.config);
|
|
220
|
+
providerConfig[validated.value.providerId] = mergedConfig;
|
|
187
221
|
targetConfig.provider = providerConfig;
|
|
222
|
+
if (Object.prototype.hasOwnProperty.call(providersAlias, validated.value.providerId)) {
|
|
223
|
+
delete providersAlias[validated.value.providerId];
|
|
224
|
+
if (Object.keys(providersAlias).length === 0) {
|
|
225
|
+
delete targetConfig.providers;
|
|
226
|
+
} else {
|
|
227
|
+
targetConfig.providers = providersAlias;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
188
230
|
|
|
189
231
|
if (Array.isArray(targetConfig.disabled_providers)) {
|
|
190
232
|
targetConfig.disabled_providers = targetConfig.disabled_providers.filter(
|
|
@@ -198,7 +240,7 @@ function upsertProviderConfig(providerId, config, workingDirectory, scope = 'use
|
|
|
198
240
|
return {
|
|
199
241
|
providerId: validated.value.providerId,
|
|
200
242
|
path: writePath,
|
|
201
|
-
config:
|
|
243
|
+
config: mergedConfig,
|
|
202
244
|
};
|
|
203
245
|
}
|
|
204
246
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getProviderSources,
|
|
10
10
|
removeProviderConfig,
|
|
11
11
|
} from './providers.js';
|
|
12
|
+
import { OPENCODE_CONFIG_DIR } from './shared.js';
|
|
12
13
|
|
|
13
14
|
let projectDir;
|
|
14
15
|
|
|
@@ -162,6 +163,132 @@ describe('custom provider config persistence', () => {
|
|
|
162
163
|
expect(written.disabled_providers).toEqual(['other']);
|
|
163
164
|
});
|
|
164
165
|
|
|
166
|
+
test('upsertProviderConfig preserves unmanaged provider and model metadata', () => {
|
|
167
|
+
const configPath = path.join(projectDir, 'opencode.json');
|
|
168
|
+
writeJson(configPath, {
|
|
169
|
+
provider: {
|
|
170
|
+
'campus-llm': {
|
|
171
|
+
npm: '@ai-sdk/openai-compatible',
|
|
172
|
+
name: 'Old',
|
|
173
|
+
customProviderField: { owner: 'user' },
|
|
174
|
+
env: ['OLD_KEY'],
|
|
175
|
+
options: {
|
|
176
|
+
baseURL: 'https://old.example.edu/v1',
|
|
177
|
+
headers: { 'X-Old': '1' },
|
|
178
|
+
timeout: 45_000,
|
|
179
|
+
},
|
|
180
|
+
models: {
|
|
181
|
+
retained: {
|
|
182
|
+
name: 'Old retained name',
|
|
183
|
+
reasoning: true,
|
|
184
|
+
attachment: true,
|
|
185
|
+
tool_call: true,
|
|
186
|
+
modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
|
|
187
|
+
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
|
188
|
+
options: { instructions: 'Keep this instruction' },
|
|
189
|
+
variants: {
|
|
190
|
+
low: { reasoningEffort: 'low' },
|
|
191
|
+
high: { reasoningEffort: 'high' },
|
|
192
|
+
},
|
|
193
|
+
customModelField: { source: 'manual' },
|
|
194
|
+
},
|
|
195
|
+
removed: {
|
|
196
|
+
name: 'Remove me',
|
|
197
|
+
reasoning: true,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
upsertProviderConfig('campus-llm', {
|
|
205
|
+
name: 'Campus LLM',
|
|
206
|
+
options: { baseURL: 'https://new.example.edu/v1' },
|
|
207
|
+
models: {
|
|
208
|
+
retained: { name: 'Retained model' },
|
|
209
|
+
added: { name: 'Added model' },
|
|
210
|
+
},
|
|
211
|
+
}, projectDir, 'project', { hasStoredAuth: true });
|
|
212
|
+
|
|
213
|
+
const written = readJson(configPath).provider['campus-llm'];
|
|
214
|
+
expect(written).toEqual({
|
|
215
|
+
npm: '@ai-sdk/openai-compatible',
|
|
216
|
+
name: 'Campus LLM',
|
|
217
|
+
customProviderField: { owner: 'user' },
|
|
218
|
+
options: {
|
|
219
|
+
baseURL: 'https://new.example.edu/v1',
|
|
220
|
+
timeout: 45_000,
|
|
221
|
+
},
|
|
222
|
+
models: {
|
|
223
|
+
retained: {
|
|
224
|
+
name: 'Retained model',
|
|
225
|
+
reasoning: true,
|
|
226
|
+
attachment: true,
|
|
227
|
+
tool_call: true,
|
|
228
|
+
modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
|
|
229
|
+
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
|
230
|
+
options: { instructions: 'Keep this instruction' },
|
|
231
|
+
variants: {
|
|
232
|
+
low: { reasoningEffort: 'low' },
|
|
233
|
+
high: { reasoningEffort: 'high' },
|
|
234
|
+
},
|
|
235
|
+
customModelField: { source: 'manual' },
|
|
236
|
+
},
|
|
237
|
+
added: { name: 'Added model' },
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('upsertProviderConfig preserves metadata while migrating the legacy providers alias', () => {
|
|
243
|
+
const configPath = path.join(projectDir, 'opencode.json');
|
|
244
|
+
writeJson(configPath, {
|
|
245
|
+
providers: {
|
|
246
|
+
legacy: {
|
|
247
|
+
name: 'Legacy provider',
|
|
248
|
+
options: { baseURL: 'https://old.example.com/v1', timeout: 30_000 },
|
|
249
|
+
models: { model: { name: 'Old model', reasoning: true } },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
upsertProviderConfig('legacy', {
|
|
255
|
+
name: 'Updated provider',
|
|
256
|
+
options: { baseURL: 'https://new.example.com/v1' },
|
|
257
|
+
models: { model: { name: 'Updated model' } },
|
|
258
|
+
}, projectDir, 'project', { hasStoredAuth: true });
|
|
259
|
+
|
|
260
|
+
const written = readJson(configPath);
|
|
261
|
+
expect(written.providers).toBeUndefined();
|
|
262
|
+
expect(written.provider.legacy).toEqual({
|
|
263
|
+
npm: '@ai-sdk/openai-compatible',
|
|
264
|
+
name: 'Updated provider',
|
|
265
|
+
options: { baseURL: 'https://new.example.com/v1', timeout: 30_000 },
|
|
266
|
+
models: { model: { name: 'Updated model', reasoning: true } },
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('migrating one legacy providers entry keeps the other legacy entries', () => {
|
|
271
|
+
const configPath = path.join(projectDir, 'opencode.json');
|
|
272
|
+
writeJson(configPath, {
|
|
273
|
+
providers: {
|
|
274
|
+
legacy: { name: 'Legacy provider', options: { baseURL: 'https://old.example.com/v1' }, models: { model: { name: 'Old model' } } },
|
|
275
|
+
untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
upsertProviderConfig('legacy', {
|
|
280
|
+
name: 'Updated provider',
|
|
281
|
+
options: { baseURL: 'https://new.example.com/v1' },
|
|
282
|
+
models: { model: { name: 'Updated model' } },
|
|
283
|
+
}, projectDir, 'project', { hasStoredAuth: true });
|
|
284
|
+
|
|
285
|
+
const written = readJson(configPath);
|
|
286
|
+
expect(written.providers).toEqual({
|
|
287
|
+
untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
288
|
+
});
|
|
289
|
+
expect(written.provider.legacy.name).toBe('Updated provider');
|
|
290
|
+
});
|
|
291
|
+
|
|
165
292
|
test('upsert then remove restores absence', () => {
|
|
166
293
|
upsertProviderConfig('temp-provider', {
|
|
167
294
|
name: 'Temp',
|
|
@@ -230,8 +357,8 @@ describe('custom provider config persistence', () => {
|
|
|
230
357
|
expect(sources.sources.custom.exists).toBe(false);
|
|
231
358
|
|
|
232
359
|
for (const userPath of [
|
|
233
|
-
path.join(
|
|
234
|
-
path.join(
|
|
360
|
+
path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
361
|
+
path.join(OPENCODE_CONFIG_DIR, 'config.json'),
|
|
235
362
|
]) {
|
|
236
363
|
if (!fs.existsSync(userPath)) continue;
|
|
237
364
|
const userConfig = readJson(userPath);
|
|
@@ -269,8 +396,8 @@ describe('custom provider config persistence', () => {
|
|
|
269
396
|
expect(sources.sources.project.exists).toBe(false);
|
|
270
397
|
|
|
271
398
|
for (const userPath of [
|
|
272
|
-
path.join(
|
|
273
|
-
path.join(
|
|
399
|
+
path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
400
|
+
path.join(OPENCODE_CONFIG_DIR, 'config.json'),
|
|
274
401
|
]) {
|
|
275
402
|
if (!fs.existsSync(userPath)) continue;
|
|
276
403
|
const userConfig = readJson(userPath);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import {
|
|
7
6
|
buildDeferredRestartResponse,
|
|
8
7
|
} from './config-mutation-response.js';
|
|
9
8
|
import { getClaudeCliAuthStatus } from './claude-cli-auth.js';
|
|
9
|
+
import { OPENCODE_CONFIG_DIR } from './shared.js';
|
|
10
|
+
import { settingsSurfaceOf } from './settings-files.js';
|
|
10
11
|
|
|
11
12
|
export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
12
13
|
const {
|
|
@@ -208,9 +209,10 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
|
|
|
208
209
|
}
|
|
209
210
|
};
|
|
210
211
|
|
|
211
|
-
app.get('/api/config/settings', async (
|
|
212
|
+
app.get('/api/config/settings', async (req, res) => {
|
|
212
213
|
try {
|
|
213
|
-
|
|
214
|
+
// The surface kind resolves the per-surface profile keys; absent means base.
|
|
215
|
+
const settings = await readSettingsFromDiskMigrated({ surface: settingsSurfaceOf(req) });
|
|
214
216
|
res.json(formatSettingsResponse(settings));
|
|
215
217
|
} catch (error) {
|
|
216
218
|
console.error('Failed to read settings:', error);
|
|
@@ -422,7 +424,7 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
|
|
|
422
424
|
|
|
423
425
|
app.put('/api/config/settings', async (req, res) => {
|
|
424
426
|
try {
|
|
425
|
-
const updated = await persistSettings(req.body ?? {});
|
|
427
|
+
const updated = await persistSettings(req.body ?? {}, { surface: settingsSurfaceOf(req) });
|
|
426
428
|
res.json(updated);
|
|
427
429
|
} catch (error) {
|
|
428
430
|
console.error('[API:PUT /api/config/settings] Failed to save settings:', error);
|
|
@@ -807,7 +809,7 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
|
|
|
807
809
|
});
|
|
808
810
|
|
|
809
811
|
// Behavior / Global AGENTS.md endpoints
|
|
810
|
-
const AGENTS_MD_PATH = path.join(
|
|
812
|
+
const AGENTS_MD_PATH = path.join(OPENCODE_CONFIG_DIR, 'AGENTS.md');
|
|
811
813
|
const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024; // 1 MB
|
|
812
814
|
|
|
813
815
|
app.get('/api/behavior/agents-md', async (_req, res) => {
|
|
@@ -815,10 +817,10 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
|
|
|
815
817
|
try {
|
|
816
818
|
await fs.promises.access(AGENTS_MD_PATH);
|
|
817
819
|
} catch {
|
|
818
|
-
return res.json({ content: '', exists: false });
|
|
820
|
+
return res.json({ content: '', exists: false, path: AGENTS_MD_PATH });
|
|
819
821
|
}
|
|
820
822
|
const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
|
|
821
|
-
return res.json({ content, exists: true });
|
|
823
|
+
return res.json({ content, exists: true, path: AGENTS_MD_PATH });
|
|
822
824
|
} catch (error) {
|
|
823
825
|
console.error('Failed to read AGENTS.md:', error);
|
|
824
826
|
return res.status(500).json({ error: 'Failed to read AGENTS.md' });
|
|
@@ -138,7 +138,7 @@ export const createServerStartupRuntime = (dependencies) => {
|
|
|
138
138
|
// Cover every signal a shell or dev harness may use to stop/restart us, so
|
|
139
139
|
// the managed OpenCode child is always torn down gracefully instead of
|
|
140
140
|
// orphaned: SIGINT/SIGQUIT (Ctrl+C/Ctrl+\), SIGTERM (kill/default), SIGHUP
|
|
141
|
-
// (terminal close), SIGUSR2 (nodemon restart
|
|
141
|
+
// (terminal close), SIGUSR2 (nodemon restart outside Windows).
|
|
142
142
|
process.on('SIGTERM', handleSignal);
|
|
143
143
|
process.on('SIGINT', handleSignal);
|
|
144
144
|
process.on('SIGQUIT', handleSignal);
|