@vantran-se/openchamber-web 2.0.3 → 2.0.5
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 +20 -28
- package/bin/cli.js +3 -1
- package/bin/cli.test.js +13 -4
- package/bin/lib/DOCUMENTATION.md +4 -1
- package/bin/lib/commands-serve.js +9 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DCAQTLMM.js} +1 -1
- package/dist/assets/DiffView-uzNpHBlV.js +4 -0
- package/dist/assets/FilesView-Bkq0sjNx.js +17 -0
- package/dist/assets/{GitView-DKIjpnu4.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-D3Azksju.js} +1 -1
- package/dist/assets/MarkdownImageGallery-BGY6tuiK.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DJmhQ37f.js +12 -0
- package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-Bqc-xuDe.js} +4 -4
- package/dist/assets/PluginPane-DQ0k41Q-.js +3 -0
- package/dist/assets/SettingsView-BAiJU5hJ.js +30 -0
- package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-CiuyzfSW.js} +1 -1
- package/dist/assets/changesTree-CePHbFcj.js +1 -0
- package/dist/assets/commitAndPush-DTKb6IwT.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-kM_zJNMt.js} +2 -2
- package/dist/assets/de-BMnfXm-G.js +10 -0
- package/dist/assets/es-DKEvCCL6.js +16 -0
- package/dist/assets/fr-D9fgE2xE.js +14 -0
- package/dist/assets/index-DUke1nme.css +1 -0
- package/dist/assets/{index-CL7ZQ94-.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-bt5VHvcd.js} +1 -1
- package/dist/assets/ja-BEXad-KW.js +16 -0
- package/dist/assets/ko-DGrQ0lEe.js +16 -0
- package/dist/assets/{main-quLSA7jc.js → main-BpLQbZC2.js} +2 -2
- package/dist/assets/main-CI9DtHJv.js +896 -0
- package/dist/assets/{markdown-shiki.worker-CahLvHIq.js → markdown-shiki.worker-Bg8SoAJJ.js} +8 -5
- package/dist/assets/markdownCore-DK2KK5RW.js +7 -0
- package/dist/assets/{miniChat-D-lJJibr.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-VTtphbW6.js → mobile-CnWKEYIP.js} +2 -2
- package/dist/assets/patchFileDiff-Cqn0C0az.js +13 -0
- package/dist/assets/pl-BlhkmR5b.js +16 -0
- package/dist/assets/pt-BR-Bs96Ew9I.js +16 -0
- package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-D9t0-9nV.js} +2 -2
- package/dist/assets/renderMobileApp-CW7fVs_s.js +25 -0
- package/dist/assets/runtimeEndpointReset-D0PcCb3K.js +27 -0
- package/dist/assets/scheduledTasksApi-Dahv3ws0.js +1 -0
- package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-C-kGcOpD.js} +1 -1
- package/dist/assets/tr-CehUK1M9.js +7 -0
- package/dist/assets/uk-Bno5WGNz.js +16 -0
- package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-oKF0MAwd.js} +2 -2
- package/dist/assets/vendor-opencode-client-DSZl7jSi.js +12 -0
- package/dist/assets/vendor-zod-BSoePkpP.js +39 -0
- package/dist/assets/{web-components-C583Pmfm.js → web-components-Bc1WPd4i.js} +1 -1
- package/dist/assets/zh-CN-DnAweIWo.js +16 -0
- package/dist/assets/zh-TW-BbBBK25p.js +16 -0
- package/dist/index.html +5 -5
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.d.ts +2 -0
- package/server/index.js +196 -52
- package/server/lib/agent-tool/DOCUMENTATION.md +5 -0
- package/server/lib/agent-tool/runtime.js +89 -51
- package/server/lib/agent-tool/runtime.test.js +17 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/global-hub.js +45 -7
- package/server/lib/event-stream/global-hub.test.js +128 -2
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- package/server/lib/event-stream/rebind.test.js +1 -0
- package/server/lib/event-stream/runtime.js +1 -2
- package/server/lib/event-stream/upstream-reader.js +3 -2
- package/server/lib/git/DOCUMENTATION.md +1 -1
- package/server/lib/git/service.js +22 -0
- package/server/lib/git/service.test.js +8 -0
- package/server/lib/guests/DOCUMENTATION.md +4 -3
- package/server/lib/guests/catalog.js +19 -1
- package/server/lib/guests/catalog.test.js +22 -0
- package/server/lib/guests/html-styles.js +5 -3
- package/server/lib/guests/html-styles.test.js +3 -1
- package/server/lib/guests/routes.js +11 -3
- package/server/lib/guests/service.js +5 -0
- package/server/lib/guests/surface.js +55 -9
- package/server/lib/guests/surface.test.js +56 -6
- package/server/lib/notifications/DOCUMENTATION.md +10 -0
- package/server/lib/notifications/emit-route.js +118 -0
- package/server/lib/notifications/emit-route.test.js +112 -0
- package/server/lib/openchamber-control/actions.js +16 -2
- package/server/lib/openchamber-control/service.js +23 -0
- package/server/lib/openchamber-control/service.test.js +31 -0
- package/server/lib/openchamber-sessions/fork-inheritance.js +103 -0
- package/server/lib/openchamber-sessions/fork-inheritance.test.js +69 -0
- package/server/lib/openchamber-sessions/routes.js +11 -0
- package/server/lib/openchamber-sessions/routes.test.js +18 -0
- package/server/lib/opencode/DOCUMENTATION.md +23 -23
- package/server/lib/opencode/bootstrap-runtime.js +14 -1
- package/server/lib/opencode/compatibility.js +6 -3
- package/server/lib/opencode/compatibility.test.js +8 -0
- package/server/lib/opencode/config-entity-v2.test.js +27 -0
- package/server/lib/opencode/config-paths.test.js +12 -0
- package/server/lib/opencode/config-v2.d.ts +2 -0
- package/server/lib/opencode/config-v2.js +42 -0
- package/server/lib/opencode/config-v2.test.js +17 -0
- package/server/lib/opencode/core-routes.js +20 -4
- package/server/lib/opencode/core-routes.test.js +41 -0
- package/server/lib/opencode/env-runtime.js +1 -8
- package/server/lib/opencode/feature-routes-runtime.js +2 -1
- package/server/lib/opencode/lifecycle.js +156 -2
- package/server/lib/opencode/lifecycle.test.js +255 -1
- package/server/lib/opencode/managed-config-file.js +18 -6
- package/server/lib/opencode/managed-config-file.test.js +23 -7
- package/server/lib/opencode/mcp.js +7 -2
- package/server/lib/opencode/network-runtime.js +16 -7
- package/server/lib/opencode/network-runtime.test.js +29 -1
- package/server/lib/opencode/npm-registry-config.d.ts +1 -0
- package/server/lib/opencode/npm-registry-config.js +123 -0
- package/server/lib/opencode/npm-registry-config.test.js +167 -0
- package/server/lib/opencode/npm-registry.js +5 -6
- package/server/lib/opencode/npm-registry.test.js +30 -0
- package/server/lib/opencode/openchamber-routes.js +29 -11
- package/server/lib/opencode/openchamber-routes.test.js +18 -0
- package/server/lib/opencode/project-directory-runtime.js +8 -0
- package/server/lib/opencode/project-directory-runtime.test.js +13 -0
- package/server/lib/opencode/provider-source-route.test.js +59 -0
- package/server/lib/opencode/providers.js +3 -0
- package/server/lib/opencode/providers.test.js +62 -0
- package/server/lib/opencode/proxy.js +53 -16
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -1
- package/server/lib/opencode/server-startup-runtime.js +25 -0
- package/server/lib/opencode/server-startup-runtime.test.js +67 -1
- package/server/lib/opencode/server-utils-runtime.js +5 -0
- package/server/lib/opencode/settings-helpers.js +10 -0
- package/server/lib/opencode/settings-helpers.test.js +2 -2
- package/server/lib/opencode/settings-registry.json +17 -0
- package/server/lib/opencode/settings-runtime.js +1 -0
- package/server/lib/opencode/shared-service-runtime.js +155 -0
- package/server/lib/opencode/shared-service-runtime.test.js +180 -0
- package/server/lib/opencode/shared.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +14 -5
- package/server/lib/opencode/shutdown-runtime.test.js +77 -2
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
- package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
- package/server/lib/opencode/upgrade-capability.js +0 -14
- package/server/lib/opencode/v1-migration-topup.js +21 -15
- package/server/lib/opencode/v1-migration-topup.test.js +14 -6
- package/server/lib/opencode/v2-install.d.ts +1 -0
- package/server/lib/opencode/v2-install.js +8 -7
- package/server/lib/opencode/v2-install.test.js +13 -4
- package/server/lib/opencode/watcher.js +3 -1
- package/server/lib/opencode/watcher.test.js +54 -0
- package/server/lib/opencode/websearch-config.js +13 -1
- package/server/lib/quota/providers/kimi.js +1 -1
- package/server/lib/realtime-proxy.js +5 -1
- package/server/lib/realtime-proxy.test.js +29 -0
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +11 -0
- package/server/lib/session-assist/DOCUMENTATION.md +17 -5
- package/server/lib/session-assist/runtime.js +19 -2
- package/server/lib/session-assist/runtime.test.js +8 -0
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/small-model/DOCUMENTATION.md +11 -10
- package/server/lib/small-model/index.js +27 -32
- package/server/lib/small-model/index.test.js +28 -16
- package/server/lib/spaces/DOCUMENTATION.md +404 -32
- package/server/lib/spaces/code-in-bait.js +145 -6
- package/server/lib/spaces/code-in.js +10 -150
- package/server/lib/spaces/code-in.test.js +7 -83
- package/server/lib/spaces/code-out.js +1819 -0
- package/server/lib/spaces/code-out.test.js +3945 -0
- package/server/lib/spaces/code-transfer.js +173 -0
- package/server/lib/spaces/command-stream.test.js +181 -0
- package/server/lib/spaces/dispatcher.js +670 -0
- package/server/lib/spaces/dispatcher.test.js +582 -0
- package/server/lib/spaces/gatekeeper-channel.js +61 -7
- package/server/lib/spaces/gatekeeper-channel.test.js +43 -7
- package/server/lib/spaces/gatekeeper-program.cjs +68 -14
- package/server/lib/spaces/gatekeeper-program.test.js +118 -3
- package/server/lib/spaces/hardening.js +20 -4
- package/server/lib/spaces/hardening.test.js +18 -3
- package/server/lib/spaces/host-git.js +7 -4
- package/server/lib/spaces/host-git.test.js +6 -2
- package/server/lib/spaces/host.js +310 -0
- package/server/lib/spaces/host.test.js +273 -0
- package/server/lib/spaces/journey.js +552 -0
- package/server/lib/spaces/journey.test.js +589 -0
- package/server/lib/spaces/layout.js +42 -8
- package/server/lib/spaces/manager.js +4 -3
- package/server/lib/spaces/manager.test.js +6 -0
- package/server/lib/spaces/places/code-out.docker.live.test.js +412 -0
- package/server/lib/spaces/places/contract-suite.js +36 -0
- package/server/lib/spaces/places/dispatcher.docker.live.test.js +296 -0
- package/server/lib/spaces/places/docker-live-support.js +27 -4
- package/server/lib/spaces/places/docker.js +33 -5
- package/server/lib/spaces/places/docker.test.js +42 -3
- package/server/lib/spaces/places/escape-suite.js +118 -14
- package/server/lib/spaces/places/fake-docker.js +3 -1
- package/server/lib/spaces/places/grants.docker.live.test.js +197 -0
- package/server/lib/spaces/places/memory-place.js +24 -1
- package/server/lib/spaces/places/registry.js +1 -1
- package/server/lib/spaces/places/server.docker.live.test.js +69 -33
- package/server/lib/spaces/places/sessions.docker.live.test.js +222 -0
- package/server/lib/spaces/routes.js +216 -0
- package/server/lib/spaces/routes.test.js +213 -0
- package/server/lib/spaces/run-command.js +261 -8
- package/server/lib/spaces/run-command.test.js +193 -3
- package/server/lib/spaces/space-events.js +153 -0
- package/server/lib/spaces/space-events.test.js +160 -0
- package/server/lib/spaces/space-opencode.js +52 -0
- package/server/lib/spaces/space-opencode.test.js +41 -0
- package/server/lib/spaces/space-records.js +150 -0
- package/server/lib/spaces/space-records.test.js +113 -0
- package/server/lib/spaces/space-sessions.js +210 -0
- package/server/lib/spaces/space-sessions.test.js +161 -0
- package/server/lib/spaces/websocket.js +329 -0
- package/server/lib/spaces/websocket.test.js +345 -0
- package/server/lib/ui-auth/ui-auth.js +8 -2
- package/server/lib/ui-auth/ui-auth.test.js +29 -0
- package/server/opencode-proxy.test.js +271 -0
- package/server/proxy-headers.js +8 -2
- package/server/proxy-headers.test.js +20 -0
- package/dist/assets/DiffView-CX3wAX2S.js +0 -4
- package/dist/assets/FilesView-OL3CON3r.js +0 -17
- package/dist/assets/GuestSurfacePane-B_cqX5I5.js +0 -1
- package/dist/assets/MarkdownImageGallery-DFiTFZgj.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DZm5sf1W.js +0 -12
- package/dist/assets/PluginPane-BibJit86.js +0 -3
- package/dist/assets/SettingsView-h-VpVO47.js +0 -29
- package/dist/assets/commitAndPush-BIPfOMXY.js +0 -1
- package/dist/assets/de-BqCNGROu.js +0 -10
- package/dist/assets/es-BTPpTRgN.js +0 -16
- package/dist/assets/fr-hxbzMdAg.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-_zLwwYiy.js +0 -16
- package/dist/assets/ko-CFn3Dzhc.js +0 -16
- package/dist/assets/main-BLJaug9I.js +0 -896
- package/dist/assets/markdownCore-DS9FdwrI.js +0 -5
- package/dist/assets/patchFileDiff-D9iBcTEi.js +0 -13
- package/dist/assets/pl-DFHE7xac.js +0 -16
- package/dist/assets/pt-BR-CUjZF179.js +0 -16
- package/dist/assets/renderMobileApp-CNQhdXsi.js +0 -25
- package/dist/assets/runtimeEndpointReset-DeaERc0M.js +0 -27
- package/dist/assets/scheduledTasksApi-BD6Vx9pu.js +0 -1
- package/dist/assets/tr-BB6sA_zH.js +0 -7
- package/dist/assets/uk-CB4o8g-o.js +0 -16
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +0 -12
- package/dist/assets/vendor-zod-Gea9uBlY.js +0 -39
- package/dist/assets/zh-CN-BRyDuWhc.js +0 -16
- package/dist/assets/zh-TW-BDGbWA1Q.js +0 -16
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { applyForkInheritance, sourceOwnedLinksPatch } from './fork-inheritance.js';
|
|
3
|
+
|
|
4
|
+
const goal = (objectiveFile) => ({ id: 'goal_1', objective: objectiveFile ? '' : 'Ship it', objectiveFile, status: 'paused' });
|
|
5
|
+
|
|
6
|
+
const harness = (metadata, overrides = {}) => {
|
|
7
|
+
const deps = {
|
|
8
|
+
readObjective: vi.fn(async () => 'Objective from file'),
|
|
9
|
+
writeObjective: vi.fn(async () => undefined),
|
|
10
|
+
writeMetadata: vi.fn(async () => undefined),
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
const run = () => applyForkInheritance({ sourceSessionID: 'ses_source', fork: { id: 'ses_fork', metadata }, ...deps });
|
|
14
|
+
return { deps, run };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('sourceOwnedLinksPatch', () => {
|
|
18
|
+
it('pauses an active goal so the fork does not pursue it in parallel', () => {
|
|
19
|
+
expect(sourceOwnedLinksPatch({ openchamber: { goal: { id: 'g', status: 'active' } } }))
|
|
20
|
+
.toEqual({ goal: { status: 'paused', statusReason: 'paused in fork' } });
|
|
21
|
+
expect(sourceOwnedLinksPatch({ openchamber: { goal: { id: 'g', status: 'complete' } } })).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('removes the btw and review links but keeps the goal', () => {
|
|
25
|
+
expect(sourceOwnedLinksPatch({ openchamber: { goal: goal(false), btwSessionID: 'b', reviewSessionID: 'r' } }))
|
|
26
|
+
.toEqual({ btwSessionID: null, reviewSessionID: null });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('removes the btw marker of a btw source, keeps a review marker', () => {
|
|
30
|
+
expect(sourceOwnedLinksPatch({ openchamber: { kind: 'btw', originalSessionID: 'p', btwBoundaryMessageID: 'm' } }))
|
|
31
|
+
.toEqual({ kind: null, originalSessionID: null, btwBoundaryMessageID: null });
|
|
32
|
+
expect(sourceOwnedLinksPatch({ openchamber: { kind: 'review', originalSessionID: 'p' } })).toBeNull();
|
|
33
|
+
expect(sourceOwnedLinksPatch(undefined)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('applyForkInheritance', () => {
|
|
38
|
+
it('does nothing for an inline goal without source links', async () => {
|
|
39
|
+
const h = harness({ openchamber: { goal: goal(false) } });
|
|
40
|
+
await h.run();
|
|
41
|
+
expect(h.deps.readObjective).not.toHaveBeenCalled();
|
|
42
|
+
expect(h.deps.writeMetadata).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('copies a file-backed objective and strips source links', async () => {
|
|
46
|
+
const h = harness({ openchamber: { goal: goal(true), btwSessionID: 'b' } });
|
|
47
|
+
await h.run();
|
|
48
|
+
expect(h.deps.readObjective).toHaveBeenCalledWith('ses_source');
|
|
49
|
+
expect(h.deps.writeObjective).toHaveBeenCalledWith('ses_fork', 'Objective from file');
|
|
50
|
+
expect(h.deps.writeMetadata).toHaveBeenCalledWith('ses_fork', { openchamber: { btwSessionID: null } });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('inlines the objective when the file copy fails', async () => {
|
|
54
|
+
const h = harness({ openchamber: { goal: goal(true) } }, {
|
|
55
|
+
writeObjective: vi.fn(async () => { throw new Error('disk full'); }),
|
|
56
|
+
});
|
|
57
|
+
await h.run();
|
|
58
|
+
expect(h.deps.writeMetadata).toHaveBeenCalledWith('ses_fork', {
|
|
59
|
+
openchamber: { goal: { objective: 'Objective from file', objectiveFile: false } },
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('swallows a metadata write failure', async () => {
|
|
64
|
+
const h = harness({ openchamber: { reviewSessionID: 'r' } }, {
|
|
65
|
+
writeMetadata: vi.fn(async () => { throw new Error('offline'); }),
|
|
66
|
+
});
|
|
67
|
+
await expect(h.run()).resolves.toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -9,7 +9,9 @@ import { AUTO_MODEL_REF, isAutoModel } from '../routing/defaults.js';
|
|
|
9
9
|
import { parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
|
|
10
10
|
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
|
|
11
11
|
import { OpenChamberControlError, asControlError } from '../openchamber-control/error.js';
|
|
12
|
+
import { readObjective, writeObjective } from '../session-goal/objectives.js';
|
|
12
13
|
import { createArchiveStore } from './archive-store.js';
|
|
14
|
+
import { applyForkInheritance } from './fork-inheritance.js';
|
|
13
15
|
import { createOpenCodeClient as defaultCreateOpenCodeClient } from './opencode-client.js';
|
|
14
16
|
import { createSessionMetadataStore, createOpenCodeSessionMetadata } from './session-metadata-store.js';
|
|
15
17
|
|
|
@@ -889,6 +891,15 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
889
891
|
messageID: asNonEmptyString(payload.messageId) || undefined,
|
|
890
892
|
});
|
|
891
893
|
targetSessionID = targetSession.id;
|
|
894
|
+
// Before the prompt goes out, so a goal armed by this dispatch writes
|
|
895
|
+
// over the copied objective rather than the other way round.
|
|
896
|
+
await applyForkInheritance({
|
|
897
|
+
sourceSessionID,
|
|
898
|
+
fork: targetSession,
|
|
899
|
+
readObjective,
|
|
900
|
+
writeObjective,
|
|
901
|
+
writeMetadata: (sessionID, patch) => writeMetadata(sessionID, patch, directory),
|
|
902
|
+
});
|
|
892
903
|
}
|
|
893
904
|
|
|
894
905
|
const baselineAssistantMessageId = await latestCompletedAssistantMessageID({
|
|
@@ -920,6 +920,24 @@ describe('openchamber session routes', () => {
|
|
|
920
920
|
}));
|
|
921
921
|
});
|
|
922
922
|
|
|
923
|
+
it('strips source-owned links from a fork before dispatching', async () => {
|
|
924
|
+
sessionForkMock.mockImplementationOnce(async () => ({
|
|
925
|
+
id: 'ses_fork',
|
|
926
|
+
title: 'Forked session',
|
|
927
|
+
metadata: { openchamber: { btwSessionID: 'ses_btw', reviewSessionID: 'ses_review', assist: { recap: 'kept' } } },
|
|
928
|
+
}));
|
|
929
|
+
const { app, sessionMetadataStore } = createApp();
|
|
930
|
+
sessionMetadataStore.entries.set('ses_fork', {
|
|
931
|
+
openchamber: { btwSessionID: 'ses_btw', reviewSessionID: 'ses_review', assist: { recap: 'kept' } },
|
|
932
|
+
});
|
|
933
|
+
await request(app)
|
|
934
|
+
.post('/api/openchamber/sessions/ses_source/fork')
|
|
935
|
+
.send({ directory: '/repo/app', prompt: 'Carry on', model: 'openai/gpt-5.5', agent: 'build' })
|
|
936
|
+
.expect(200);
|
|
937
|
+
|
|
938
|
+
expect(sessionMetadataStore.entries.get('ses_fork')).toEqual({ openchamber: { assist: { recap: 'kept' } } });
|
|
939
|
+
});
|
|
940
|
+
|
|
923
941
|
it('rejects send and fork requests without a prompt before calling OpenCode', async () => {
|
|
924
942
|
const { app } = createApp();
|
|
925
943
|
await request(app)
|
|
@@ -11,13 +11,14 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
11
11
|
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
|
|
12
12
|
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
|
|
13
13
|
- `packages/web/server/lib/opencode/v1-migration-topup.js`: re-arms OpenCode's own V1 -> V2 session import for V1 sessions created after that migration already completed; runs only before a managed spawn. See "v1-migration-topup.js" below.
|
|
14
|
-
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime
|
|
14
|
+
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode connection policy and the managed process lifecycle runtime. The composition helper selects `explicit-external` when `OPENCODE_HOST` is configured, `managed-owned` for Desktop, and `shared-local` for ordinary Web. The managed path still owns startup, restart, readiness, health monitoring, orphan reaping, credentials, and V1 migration top-up. After readiness it warms the most recently used directories (`getWarmupDirectories` dep, sequential and best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open.
|
|
15
15
|
- `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names. Canonical implementation shared by web lifecycle and the VS Code managed spawn path (`packages/vscode/src/provider-env-aliases.ts` re-exports this module).
|
|
16
16
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
17
17
|
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
|
|
18
18
|
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
|
|
19
19
|
- `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
|
|
20
|
-
- `packages/web/server/lib/opencode/network-runtime.js`: OpenCode URL construction, health-probe readiness checks, and API prefix runtime.
|
|
20
|
+
- `packages/web/server/lib/opencode/network-runtime.js`: OpenCode URL construction, health-probe readiness checks, and API prefix runtime. URL and service headers are resolved for every request so shared-service endpoint or credential rotation is immediate; lowercase service `authorization` is normalized to `Authorization` while other service headers are preserved.
|
|
21
|
+
- `packages/web/server/lib/opencode/shared-service-runtime.js`: discovery-first connection used by ordinary Web when no explicit `OPENCODE_HOST` is configured. It probes `/api/info` before accepting an endpoint, refuses to replace an incompatible discovered service, and never owns or stops the shared process. Because the pinned client's discovery hides incompatible and unhealthy registrations, Web checks that the registration file is genuinely absent before calling its destructive `ensure`; an existing hidden registration fails startup untouched. Shared restart and health controls recover the connection, then rebind live event readers so their established reconnect edge repairs authoritative client state without synthetic interruption. Desktop does not instantiate this runtime.
|
|
21
22
|
- `packages/web/server/lib/opencode/project-directory-runtime.js`: request-scoped and settings-backed project directory resolution/validation runtime.
|
|
22
23
|
- `packages/web/server/lib/opencode/config-entity-routes.js`: route registration for agent/command/MCP config orchestration. OpenCode 2 watches these files, so a write is live as soon as it lands and the route answers plain success.
|
|
23
24
|
- `packages/web/server/lib/opencode/websearch-config.js`: writes OpenCode's `websearch` choice (`PUT /api/config/websearch` in `routes.js`; body `{ selection: false | null | "random" | "<provider id>" }`, `null` removes the key, so OpenCode falls back to the answer given in its chat consent form, which it keeps in its own store) to `OPENCODE_CONFIG` when set, else the user config. `GET /api/config/websearch` returns `{ projectPath }`: the project config whose `websearch` overrides that write (OpenCode merges user < project < `OPENCODE_CONFIG`), so Settings disables the choice and names the file instead of letting it snap back; `findWebSearchProjectOverride` in `config-v2.js` holds the rule. The pure transform is `writeWebSearchSelection` in `config-v2.js`, shared with the VS Code bridge (`api:config/websearch`).
|
|
@@ -25,18 +26,18 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
25
26
|
- `packages/web/server/lib/opencode/snippets.js`: opencode-snippets-compatible snippet file CRUD, discovery, and hashtag expansion.
|
|
26
27
|
- `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
|
|
27
28
|
- `packages/web/server/lib/opencode/core-routes.js`: server status/system routes, auth/access guard routes, and settings utility route registration.
|
|
28
|
-
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration
|
|
29
|
-
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen
|
|
30
|
-
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset
|
|
31
|
-
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition
|
|
32
|
-
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot
|
|
33
|
-
- `packages/web/server/lib/opencode/upgrade-capability.js`:
|
|
34
|
-
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service
|
|
35
|
-
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`:
|
|
29
|
+
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration. Only `managed-owned` may close an OpenCode child, kill by port, or wait for port release. Shared and external connections dispose local state only.
|
|
30
|
+
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen, startup tunnel flow, and process or signal handler orchestration.
|
|
31
|
+
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset and SPA fallback route registration.
|
|
32
|
+
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition for dynamic route modules.
|
|
33
|
+
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot for settings routes and diagnostics.
|
|
34
|
+
- `packages/web/server/lib/opencode/upgrade-capability.js`: upgrade ownership policy. Bundled, external, and unresolved runtimes fail closed; only managed non-bundled runtimes delegate upgrades to OpenCode.
|
|
35
|
+
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service and active-port wiring.
|
|
36
|
+
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: terminal, proxy, static route, and listen orchestration. Shared Web starts no managed-tool installation or registration step.
|
|
36
37
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
37
|
-
- `packages/web/server/lib/agent-tool/runtime.js`:
|
|
38
|
-
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the `OPENCODE_CONFIG_CONTENT` merge used only
|
|
39
|
-
- `packages/web/server/lib/opencode/managed-config-file.js`:
|
|
38
|
+
- `packages/web/server/lib/agent-tool/runtime.js`: Desktop's custom-tool schema and source generation, managed plugin materialization, environment injection, same-machine authentication, and fixed action dispatch.
|
|
39
|
+
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the `OPENCODE_CONFIG_CONTENT` merge used only when the user's environment owns `OPENCODE_CONFIG`.
|
|
40
|
+
- `packages/web/server/lib/opencode/managed-config-file.js`: Desktop's managed OpenCode config layer for environment-backed OpenChamber tools and the system prompt optimizer.
|
|
40
41
|
|
|
41
42
|
### Managed plugins on OpenCode 2.x
|
|
42
43
|
A configured plugin must be a DIRECTORY holding a `package.json` that resolves an
|
|
@@ -296,7 +297,8 @@ Installer output is discarded, not forwarded to clients or logs.
|
|
|
296
297
|
The runtime maintains active-session count incrementally from idempotent activity phase transitions. Upstream stall-timeout and lifecycle health checks read it in O(1); the hourly cleanup removes activity phases older than 24 hours without broadcasting synthetic state transitions. Snapshot generation remains reserved for the session-activity API.
|
|
297
298
|
|
|
298
299
|
## Public exports (lifecycle.js)
|
|
299
|
-
- `
|
|
300
|
+
- `createOpenCodeRecoveryCallbacks(dependencies)`: composes the production recovery callbacks used by `index.js`. Managed restart resets providers, rebinds event readers, calls `interruptBusySessionsAfterRestart()`, and broadcasts the existing interruption notification. Shared recovery resets providers and rebinds readers only, leaving authoritative browser reconnect repair to the existing second `ready` edge.
|
|
301
|
+
- `createOpenCodeLifecycleRuntime(dependencies)`: creates lifecycle runtime for managed/external OpenCode process orchestration. The optional `onOpenCodeRestarted` dependency (default `null`) is fired after a successful managed restart. `index.js` supplies the managed callback above.
|
|
300
302
|
- Returned API:
|
|
301
303
|
- `startOpenCode()`
|
|
302
304
|
- `restartOpenCode()`
|
|
@@ -396,7 +398,7 @@ ConPTY or Console Window Host behavior.
|
|
|
396
398
|
- `setupBaseRoutes(app, options)`
|
|
397
399
|
|
|
398
400
|
## Public exports (network-runtime.js)
|
|
399
|
-
- `createOpenCodeNetworkRuntime(dependencies)`: creates runtime for OpenCode network and URL concerns.
|
|
401
|
+
- `createOpenCodeNetworkRuntime(dependencies)`: creates runtime for OpenCode network and URL concerns. `getOpenCodeBaseUrl` and `getOpenCodeAuthHeaders` are read at call time; credentials remain request headers and never enter generated URLs.
|
|
400
402
|
- Returned API:
|
|
401
403
|
- `waitForReady(url, timeoutMs?)`
|
|
402
404
|
- `normalizeApiPrefix(prefix)`
|
|
@@ -751,15 +753,15 @@ headers }` or v1 `{ npm, options }`. The stored entry is always a
|
|
|
751
753
|
- `setupProxy(app)`
|
|
752
754
|
|
|
753
755
|
## Public exports (shutdown-runtime.js)
|
|
754
|
-
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown
|
|
755
|
-
- Daemon signals, `POST /api/system/shutdown`, and embedded `stop()` share one shutdown promise. Guest admission closes
|
|
756
|
-
-
|
|
756
|
+
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown for Web-owned resources and explicit OpenCode process ownership.
|
|
757
|
+
- Daemon signals, `POST /api/system/shutdown`, and embedded `stop()` share one shutdown promise. Guest admission closes before asynchronous cleanup. Each cleanup is best-effort and runs once, including after partial startup. The application-owned global event hub is stopped here after its watcher, so embedded shutdown aborts the upstream event fetch and reconnect loop; watcher and WebSocket bridges do not stop injected hubs.
|
|
758
|
+
- Only `managed-owned` may close the OpenCode child, kill by port, or wait for port release. `shared-local` disposes only local connection state. `explicit-external` performs no OpenCode process teardown. TCP connection tracking closes the Web listener and remaining sockets after owned runtimes stop, while shared or external OpenCode and TUI work continue.
|
|
757
759
|
- Returned API:
|
|
758
760
|
- `gracefulShutdown(options?)`
|
|
759
761
|
- `trackServerConnections(server)`: call once before listening; closed sockets leave the tracking set, and the server close event removes the connection listener.
|
|
760
762
|
|
|
761
763
|
## Public exports (server-startup-runtime.js)
|
|
762
|
-
- `createServerStartupRuntime(dependencies)`: creates runtime for server bind/startup tunnel and process handler wiring.
|
|
764
|
+
- `createServerStartupRuntime(dependencies)`: creates runtime for server bind/startup tunnel and process handler wiring. Daemon readiness is emitted only after listener-dependent startup completes. A startup failure sends `openchamber:error`, closes the listener and accepted connections, and rejects startup without a ready event.
|
|
763
765
|
- Returned API:
|
|
764
766
|
- `resolveBindHost(host)`
|
|
765
767
|
- `startListeningAndMaybeTunnel(options)`
|
|
@@ -786,13 +788,11 @@ headers }` or v1 `{ npm, options }`. The stored entry is always a
|
|
|
786
788
|
- `initialize(app, initialPort, hasUiPassword)`
|
|
787
789
|
|
|
788
790
|
## Public exports (startup-pipeline-runtime.js)
|
|
789
|
-
- `createStartupPipelineRuntime(dependencies)`: creates
|
|
791
|
+
- `createStartupPipelineRuntime(dependencies)`: creates terminal, proxy, static route, and server listen orchestration.
|
|
790
792
|
- Returned API:
|
|
791
793
|
- `run(options)`
|
|
792
794
|
|
|
793
|
-
The pipeline
|
|
794
|
-
before starting managed OpenCode. The managed custom tool therefore receives
|
|
795
|
-
an authoritative loopback callback URL even when OpenChamber binds port `0`.
|
|
795
|
+
The pipeline publishes the listener port before bootstrapping OpenCode, then waits for connection authentication and version validation before daemon readiness. Desktop's generated custom tool therefore receives the correct loopback callback URL even when OpenChamber binds port `0`, while ordinary Web cannot advertise readiness with an unusable shared service. Shared Web does not install a plugin or publish callback registration.
|
|
796
796
|
|
|
797
797
|
## Public exports (openchamber-routes.js)
|
|
798
798
|
Browser completion checks use `appType=web&updateStatus=true` to stay on the
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerNotificationEmitRoutes } from '../notifications/emit-route.js';
|
|
2
|
+
|
|
1
3
|
export const createBootstrapRuntime = (dependencies) => {
|
|
2
4
|
const {
|
|
3
5
|
createUiAuth,
|
|
@@ -63,7 +65,9 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
63
65
|
getCachedZenModels,
|
|
64
66
|
setAutoAcceptSession,
|
|
65
67
|
agentToolRuntime,
|
|
68
|
+
pluginNotificationEmitter,
|
|
66
69
|
desktopUpdater,
|
|
70
|
+
skipBodyParsing,
|
|
67
71
|
} = options;
|
|
68
72
|
|
|
69
73
|
const uiAuthController = createUiAuth({
|
|
@@ -90,10 +94,17 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
90
94
|
uiAuthController,
|
|
91
95
|
});
|
|
92
96
|
|
|
93
|
-
registerCommonRequestMiddleware(app, { express, verboseRequestLogs });
|
|
97
|
+
registerCommonRequestMiddleware(app, { express, verboseRequestLogs, skipBodyParsing });
|
|
94
98
|
|
|
95
99
|
registerAgentToolRoutes(app, { express, agentToolRuntime });
|
|
96
100
|
|
|
101
|
+
const notificationEmitRoutes = registerNotificationEmitRoutes(app, {
|
|
102
|
+
express,
|
|
103
|
+
isAgentToolRequestAuthorized: (req) => agentToolRuntime?.authorizeRequest?.(req) === true,
|
|
104
|
+
emitter: pluginNotificationEmitter,
|
|
105
|
+
});
|
|
106
|
+
notificationEmitRoutes.registerPluginRoute();
|
|
107
|
+
|
|
97
108
|
registerAuthAndAccessRoutes(app, {
|
|
98
109
|
express,
|
|
99
110
|
tunnelAuthController,
|
|
@@ -110,6 +121,8 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
110
121
|
normalizeTunnelSessionTtlMs,
|
|
111
122
|
});
|
|
112
123
|
|
|
124
|
+
notificationEmitRoutes.registerApiRoute();
|
|
125
|
+
|
|
113
126
|
registerTtsRoutes(app, { sayTTSCapability });
|
|
114
127
|
|
|
115
128
|
registerNotificationRoutes(app, {
|
|
@@ -63,13 +63,16 @@ export const requireOpenCodeV2 = async (launch, options) => {
|
|
|
63
63
|
|
|
64
64
|
// External URLs have no local executable. A legacy probe identifies v1 only
|
|
65
65
|
// from its JSON contract; neither HTML fallbacks nor auth failures imply v1.
|
|
66
|
+
// v1 has no `/api/info`: its catch-all serves the web UI, and a v1 build
|
|
67
|
+
// without the embedded UI proxies that path to app.opencode.ai, which can hang
|
|
68
|
+
// or fail. A failed v2 probe therefore still falls through to the v1 probe.
|
|
66
69
|
export const readExternalOpenCodeVersion = async (baseUrl, headers, fetchImpl = fetch) => {
|
|
67
70
|
const request = (pathname) => fetchImpl(new URL(pathname, baseUrl), {
|
|
68
71
|
headers: { ...headers, Accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(5000),
|
|
69
72
|
});
|
|
70
|
-
const response = await request('/api/info');
|
|
71
|
-
if (response.status === 401 || response.status === 403) return null;
|
|
72
|
-
const info = await readOpenCodeInfo(response);
|
|
73
|
+
const response = await request('/api/info').catch(() => null);
|
|
74
|
+
if (response && (response.status === 401 || response.status === 403)) return null;
|
|
75
|
+
const info = response ? await readOpenCodeInfo(response) : null;
|
|
73
76
|
if (info) return info.version;
|
|
74
77
|
const legacy = await request('/global/health');
|
|
75
78
|
if (!legacy.ok) return null;
|
|
@@ -30,6 +30,14 @@ describe('OpenCode compatibility', () => {
|
|
|
30
30
|
expect(requests).toEqual(['/api/info', '/global/health']);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it('still identifies v1 when its /api/info fallback hangs or fails', async () => {
|
|
34
|
+
const fetchImpl = async (url) => {
|
|
35
|
+
if (url.pathname === '/api/info') throw new DOMException('The operation timed out.', 'TimeoutError');
|
|
36
|
+
return Response.json({ healthy: true, version: '1.18.32' });
|
|
37
|
+
};
|
|
38
|
+
expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, fetchImpl)).toBe('1.18.32');
|
|
39
|
+
});
|
|
40
|
+
|
|
33
41
|
it.each([401, 403])('does not call an auth failure v1 (%s)', async (status) => {
|
|
34
42
|
let requests = 0;
|
|
35
43
|
expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, async () => {
|
|
@@ -261,6 +261,33 @@ describe('entity modules speak OpenCode 2 shapes', () => {
|
|
|
261
261
|
}
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
it('lists servers from a global opencode.jsonc next to opencode.json and edits them in place', () => {
|
|
265
|
+
const configDir = path.join(process.env.XDG_CONFIG_HOME, 'opencode');
|
|
266
|
+
const jsonPath = path.join(configDir, 'opencode.json');
|
|
267
|
+
const jsoncPath = path.join(configDir, 'opencode.jsonc');
|
|
268
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
269
|
+
fs.writeFileSync(jsonPath, JSON.stringify({ autoupdate: true }, null, 2), 'utf8');
|
|
270
|
+
fs.writeFileSync(jsoncPath, [
|
|
271
|
+
'{',
|
|
272
|
+
' // servers live only in the jsonc file',
|
|
273
|
+
' "mcp": { "servers": { "docs": { "type": "remote", "url": "https://docs.example.com/mcp" } } }',
|
|
274
|
+
'}',
|
|
275
|
+
].join('\n'), 'utf8');
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
expect(listMcpConfigs(projectDir).map((entry) => entry.name)).toContain('docs');
|
|
279
|
+
expect(getMcpConfig('docs', projectDir)).toEqual(expect.objectContaining({ scope: 'user' }));
|
|
280
|
+
|
|
281
|
+
updateMcpConfig('docs', { disabled: true }, projectDir);
|
|
282
|
+
|
|
283
|
+
expect(getMcpConfig('docs', projectDir)).toEqual(expect.objectContaining({ disabled: true }));
|
|
284
|
+
expect(readJson(jsonPath)).toEqual({ autoupdate: true });
|
|
285
|
+
} finally {
|
|
286
|
+
fs.rmSync(jsonPath, { force: true });
|
|
287
|
+
fs.rmSync(jsoncPath, { force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
264
291
|
it('reads a v1 mcp entry and rewrites it under mcp.servers in the same file', () => {
|
|
265
292
|
const configPath = write('opencode.json', JSON.stringify({
|
|
266
293
|
mcp: {
|
|
@@ -111,6 +111,18 @@ describe('OpenCode global config paths', () => {
|
|
|
111
111
|
expect(response.json).toHaveBeenLastCalledWith({
|
|
112
112
|
content: 'Global behavior', exists: true, path: path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'),
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
// A write based on an outdated copy is refused and leaves the file alone.
|
|
116
|
+
await handlers.get('PUT /api/behavior/agents-md')({
|
|
117
|
+
body: { content: 'Stale draft', expectedContent: 'Older behavior' },
|
|
118
|
+
}, response);
|
|
119
|
+
expect(response.status).toHaveBeenLastCalledWith(409);
|
|
120
|
+
expect(fs.readFileSync(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'), 'utf8')).toBe('Global behavior');
|
|
121
|
+
|
|
122
|
+
await handlers.get('PUT /api/behavior/agents-md')({
|
|
123
|
+
body: { content: 'Current draft', expectedContent: 'Global behavior' },
|
|
124
|
+
}, response);
|
|
125
|
+
expect(fs.readFileSync(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'), 'utf8')).toBe('Current draft');
|
|
114
126
|
fs.rmSync(root, { recursive: true, force: true });
|
|
115
127
|
});
|
|
116
128
|
});
|
|
@@ -162,6 +162,7 @@ export function deleteMcpEntry(config: Record<string, unknown>, name: string): b
|
|
|
162
162
|
export function toProviderPackage(value: unknown): string | undefined;
|
|
163
163
|
export function toNpmPackage(value: unknown): string | undefined;
|
|
164
164
|
export function toProviderEntity(raw: unknown): ProviderEntity;
|
|
165
|
+
export function readStoredProviderEntry(configs: unknown[], providerId: string): ProviderEntity | null;
|
|
165
166
|
|
|
166
167
|
export function toPluginEntity(raw: unknown): PluginEntity | null;
|
|
167
168
|
export function fromPluginEntity(entity: unknown): string | PluginEntity | null;
|
|
@@ -171,6 +172,7 @@ export function readPluginList(config: unknown): Array<{ entry: PluginEntity; ke
|
|
|
171
172
|
export type WebSearchSelection = false | string | null;
|
|
172
173
|
export function parseWebSearchSelection(value: unknown): WebSearchSelection | undefined;
|
|
173
174
|
export function writeWebSearchSelection(config: Record<string, unknown>, selection: WebSearchSelection): boolean;
|
|
175
|
+
export function writeWarmingEnabled(config: Record<string, unknown>, enabled: boolean): boolean;
|
|
174
176
|
export interface WebSearchConfigLayers {
|
|
175
177
|
userConfig: object | null;
|
|
176
178
|
projectConfig: object | null;
|
|
@@ -668,6 +668,28 @@ function toProviderEntity(raw) {
|
|
|
668
668
|
]);
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
/**
|
|
672
|
+
* The provider entry an editor should start from: the winning config layer's
|
|
673
|
+
* stored entry (layers passed highest precedence first) in v2 shape, without
|
|
674
|
+
* the literal `settings.apiKey` secret. Returns null when no layer defines it.
|
|
675
|
+
* Unlike the live provider OpenCode serves, this carries `env` and only the
|
|
676
|
+
* reasoning levels the user wrote.
|
|
677
|
+
*/
|
|
678
|
+
function readStoredProviderEntry(configs, providerId) {
|
|
679
|
+
for (const config of configs) {
|
|
680
|
+
const { value } = readSectionEntry(config, 'providers', providerId);
|
|
681
|
+
if (value === undefined) continue;
|
|
682
|
+
const entity = toProviderEntity(value);
|
|
683
|
+
if (entity.settings && 'apiKey' in entity.settings) {
|
|
684
|
+
const { apiKey: _secret, ...settings } = entity.settings;
|
|
685
|
+
if (Object.keys(settings).length) entity.settings = settings;
|
|
686
|
+
else delete entity.settings;
|
|
687
|
+
}
|
|
688
|
+
return entity;
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
671
693
|
// ============== PLUGINS ==============
|
|
672
694
|
|
|
673
695
|
const PLUGIN_SECTION = { v2: 'plugins', v1: 'plugin' };
|
|
@@ -742,6 +764,24 @@ function writeWebSearchSelection(config, selection) {
|
|
|
742
764
|
return JSON.stringify(config.websearch) !== before;
|
|
743
765
|
}
|
|
744
766
|
|
|
767
|
+
// ============== SESSION WARMING ==============
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* The `warming` key: `true` keeps idle sessions' prompt cache warm with
|
|
771
|
+
* OpenCode's defaults, an object tunes prompt/interval/duration. Turning it on
|
|
772
|
+
* keeps a hand-tuned object as is; turning it off removes the key (OpenCode's
|
|
773
|
+
* default is off). Returns whether the config changed.
|
|
774
|
+
*/
|
|
775
|
+
function writeWarmingEnabled(config, enabled) {
|
|
776
|
+
const before = JSON.stringify(config.warming);
|
|
777
|
+
if (!enabled) {
|
|
778
|
+
delete config.warming;
|
|
779
|
+
} else if (typeof config.warming !== 'object' || config.warming === null) {
|
|
780
|
+
config.warming = true;
|
|
781
|
+
}
|
|
782
|
+
return JSON.stringify(config.warming) !== before;
|
|
783
|
+
}
|
|
784
|
+
|
|
745
785
|
const hasWebSearchKey = (config) => config != null && Object.hasOwn(config, 'websearch');
|
|
746
786
|
|
|
747
787
|
/**
|
|
@@ -792,10 +832,12 @@ export {
|
|
|
792
832
|
toProviderPackage,
|
|
793
833
|
toNpmPackage,
|
|
794
834
|
toProviderEntity,
|
|
835
|
+
readStoredProviderEntry,
|
|
795
836
|
toPluginEntity,
|
|
796
837
|
fromPluginEntity,
|
|
797
838
|
readPluginList,
|
|
798
839
|
parseWebSearchSelection,
|
|
799
840
|
writeWebSearchSelection,
|
|
800
841
|
findWebSearchProjectOverride,
|
|
842
|
+
writeWarmingEnabled,
|
|
801
843
|
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
parseModelSelection,
|
|
10
10
|
formatModelSelection,
|
|
11
11
|
toAgentEntity,
|
|
12
|
+
writeWarmingEnabled,
|
|
12
13
|
fromAgentEntity,
|
|
13
14
|
isLegacyAgentFrontmatter,
|
|
14
15
|
toCommandEntity,
|
|
@@ -464,3 +465,19 @@ describe('findWebSearchProjectOverride', () => {
|
|
|
464
465
|
expect(findWebSearchProjectOverride(layers(), [{ path: userPath, config: { websearch: false } }])).toBeNull();
|
|
465
466
|
});
|
|
466
467
|
});
|
|
468
|
+
|
|
469
|
+
describe('session warming', () => {
|
|
470
|
+
it('turns warming on and off, keeping a hand-tuned object', () => {
|
|
471
|
+
const config = { model: 'openai/gpt-5' };
|
|
472
|
+
expect(writeWarmingEnabled(config, true)).toBe(true);
|
|
473
|
+
expect(config.warming).toBe(true);
|
|
474
|
+
expect(writeWarmingEnabled(config, true)).toBe(false);
|
|
475
|
+
expect(writeWarmingEnabled(config, false)).toBe(true);
|
|
476
|
+
expect(config).toEqual({ model: 'openai/gpt-5' });
|
|
477
|
+
expect(writeWarmingEnabled(config, false)).toBe(false);
|
|
478
|
+
|
|
479
|
+
const tuned = { warming: { interval: '3 minutes' } };
|
|
480
|
+
expect(writeWarmingEnabled(tuned, true)).toBe(false);
|
|
481
|
+
expect(tuned.warming).toEqual({ interval: '3 minutes' });
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -94,6 +94,7 @@ export const registerServerStatusRoutes = (app, dependencies) => {
|
|
|
94
94
|
'api.health.v1',
|
|
95
95
|
'api.runtime-url.v1',
|
|
96
96
|
'api.raw-file.v1',
|
|
97
|
+
'api.notifications.emit.v1',
|
|
97
98
|
'realtime.sse.v1',
|
|
98
99
|
'realtime.websocket.global-events.v1',
|
|
99
100
|
'terminal.websocket.v1',
|
|
@@ -1074,10 +1075,15 @@ export const registerSettingsUtilityRoutes = (app, dependencies) => {
|
|
|
1074
1075
|
};
|
|
1075
1076
|
|
|
1076
1077
|
export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
1077
|
-
|
|
1078
|
+
// `skipBodyParsing(req)` names a request whose body must reach its route untouched: a
|
|
1079
|
+
// request the isolated-spaces dispatcher streams into a space, where a parsed body would
|
|
1080
|
+
// otherwise be consumed here and lost.
|
|
1081
|
+
const { express, verboseRequestLogs = false, skipBodyParsing = () => false } = dependencies;
|
|
1078
1082
|
|
|
1079
1083
|
app.use((req, res, next) => {
|
|
1080
|
-
if (req
|
|
1084
|
+
if (skipBodyParsing(req)) {
|
|
1085
|
+
next();
|
|
1086
|
+
} else if (req.path === '/api/config/themes' || req.path.startsWith('/api/config/themes/')) {
|
|
1081
1087
|
express.json({ limit: '1mb' })(req, res, next);
|
|
1082
1088
|
} else if (req.path.startsWith('/api/behavior')) {
|
|
1083
1089
|
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
@@ -1093,6 +1099,8 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1093
1099
|
req.path.startsWith('/api/config/settings') ||
|
|
1094
1100
|
req.path.startsWith('/api/config/skills') ||
|
|
1095
1101
|
req.path.startsWith('/api/config/plugins') ||
|
|
1102
|
+
req.path.startsWith('/api/config/websearch') ||
|
|
1103
|
+
req.path.startsWith('/api/config/warming') ||
|
|
1096
1104
|
req.path.startsWith('/api/projects') ||
|
|
1097
1105
|
req.path.startsWith('/api/fs') ||
|
|
1098
1106
|
req.path.startsWith('/api/git') ||
|
|
@@ -1112,7 +1120,8 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1112
1120
|
req.path.startsWith('/api/text') ||
|
|
1113
1121
|
req.path.startsWith('/api/voice') ||
|
|
1114
1122
|
req.path.startsWith('/api/tts') ||
|
|
1115
|
-
req.path.startsWith('/api/openchamber/tunnel')
|
|
1123
|
+
req.path.startsWith('/api/openchamber/tunnel') ||
|
|
1124
|
+
req.path.startsWith('/api/openchamber/spaces')
|
|
1116
1125
|
) {
|
|
1117
1126
|
express.json({ limit: '50mb' })(req, res, next);
|
|
1118
1127
|
} else if (req.path.startsWith('/api')) {
|
|
@@ -1122,7 +1131,14 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1122
1131
|
}
|
|
1123
1132
|
});
|
|
1124
1133
|
|
|
1125
|
-
|
|
1134
|
+
const urlencoded = express.urlencoded({ extended: true, limit: '50mb' });
|
|
1135
|
+
app.use((req, res, next) => {
|
|
1136
|
+
if (skipBodyParsing(req)) {
|
|
1137
|
+
next();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
urlencoded(req, res, next);
|
|
1141
|
+
});
|
|
1126
1142
|
|
|
1127
1143
|
app.use((req, _res, next) => {
|
|
1128
1144
|
if (verboseRequestLogs) {
|
|
@@ -127,6 +127,47 @@ describe('core-routes', () => {
|
|
|
127
127
|
expect(response.body).toEqual({ body: { content: 'Snippet body' } });
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
it('leaves the body of a request alone when told to, for both parsers', async () => {
|
|
131
|
+
const app = express();
|
|
132
|
+
registerCommonRequestMiddleware(app, { express, skipBodyParsing: (req) => req.path.startsWith('/api/spaces/') });
|
|
133
|
+
const raw = (req, res) => {
|
|
134
|
+
// A body a parser read is complete already, and its stream ends no second time.
|
|
135
|
+
if (req.complete) {
|
|
136
|
+
res.json({ body: req.body ?? null, raw: '' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const chunks = [];
|
|
140
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
141
|
+
req.on('end', () => res.json({ body: req.body ?? null, raw: Buffer.concat(chunks).toString('utf8') }));
|
|
142
|
+
};
|
|
143
|
+
app.post('/api/spaces/abc/form', raw);
|
|
144
|
+
app.post('/api/spaces/abc/config/snippets/x', raw);
|
|
145
|
+
app.post('/api/other/form', raw);
|
|
146
|
+
|
|
147
|
+
const kept = await request(app).post('/api/spaces/abc/form').type('form').send('a=1&b=2').expect(200);
|
|
148
|
+
expect(kept.body).toEqual({ body: null, raw: 'a=1&b=2' });
|
|
149
|
+
const keptJson = await request(app).post('/api/spaces/abc/config/snippets/x').send({ content: 'x' }).expect(200);
|
|
150
|
+
expect(keptJson.body).toEqual({ body: null, raw: '{"content":"x"}' });
|
|
151
|
+
// Elsewhere the parsers still run, and a parsed body leaves nothing to read.
|
|
152
|
+
const parsed = await request(app).post('/api/other/form').type('form').send('a=1&b=2').expect(200);
|
|
153
|
+
expect(parsed.body).toEqual({ body: { a: '1', b: '2' }, raw: '' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should parse JSON bodies for the web search config route', async () => {
|
|
157
|
+
const app = express();
|
|
158
|
+
registerCommonRequestMiddleware(app, { express });
|
|
159
|
+
app.put('/api/config/websearch', (req, res) => {
|
|
160
|
+
res.json({ body: req.body });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const response = await request(app)
|
|
164
|
+
.put('/api/config/websearch')
|
|
165
|
+
.send({ selection: 'random' })
|
|
166
|
+
.expect(200);
|
|
167
|
+
|
|
168
|
+
expect(response.body).toEqual({ body: { selection: 'random' } });
|
|
169
|
+
});
|
|
170
|
+
|
|
130
171
|
it('should parse JSON bodies for custom provider upsert routes', async () => {
|
|
131
172
|
const app = express();
|
|
132
173
|
registerCommonRequestMiddleware(app, { express });
|
|
@@ -698,15 +698,8 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
698
698
|
];
|
|
699
699
|
|
|
700
700
|
const getWindowsNativeOpencodePackageNames = () => {
|
|
701
|
-
// TEMPORARY WORKAROUND — Windows ARM64: native opencode.exe fails with a Bun
|
|
702
|
-
// FFI/TinyCC dlopen error (https://github.com/anomalyco/opencode/issues/19130).
|
|
703
|
-
// prepare-opencode-cli.mjs bundles x64-baseline instead; match that here so
|
|
704
|
-
// the runtime resolver looks for the same x64-baseline package. Restore the
|
|
705
|
-
// arm64 branch below when the upstream issue is resolved.
|
|
706
701
|
if (process.arch === 'arm64') {
|
|
707
|
-
|
|
708
|
-
// return ['@opencode/cli-windows-arm64', 'opencode-windows-arm64'];
|
|
709
|
-
return WINDOWS_X64_NATIVE_PACKAGES;
|
|
702
|
+
return [path.join('@opencode', 'cli-windows-arm64'), 'opencode-windows-arm64'];
|
|
710
703
|
}
|
|
711
704
|
if (process.arch === 'x64') {
|
|
712
705
|
// Prefer the baseline build when bypassing package-manager wrappers so the
|
|
@@ -103,6 +103,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
103
103
|
createFsSearchRuntime,
|
|
104
104
|
openchamberDataDir,
|
|
105
105
|
onGuestDeactivated,
|
|
106
|
+
surfaceViewerHeaders,
|
|
106
107
|
openchamberUserConfigRoot,
|
|
107
108
|
managedChatsRoot,
|
|
108
109
|
normalizeDirectoryPath,
|
|
@@ -331,7 +332,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
331
332
|
registerGitHubRoutes(app);
|
|
332
333
|
registerLinearRoutes(app);
|
|
333
334
|
await registerBuiltInGuests({ persistPath: extensionsPersistPath(openchamberDataDir), root: routeDependencies.builtInExtensionsDir });
|
|
334
|
-
registerGuestRoutes(app, { openchamberDataDir, openchamberVersion, resolveGitBinaryForSpawn, resolveOptionalProjectDirectory, getSmallModelService, onGuestDeactivated });
|
|
335
|
+
registerGuestRoutes(app, { openchamberDataDir, openchamberVersion, resolveGitBinaryForSpawn, resolveOptionalProjectDirectory, getSmallModelService, onGuestDeactivated, surfaceViewerHeaders });
|
|
335
336
|
registerGitRoutes(app, {
|
|
336
337
|
emitWorktreeChanged: ({ directories, at }) => {
|
|
337
338
|
const clients = getOpenChamberEventClients();
|