@vantran-se/openchamber-web 2.0.4 → 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/dist/assets/{CodeMirrorCommentWidgets-DZF8ntaq.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-DwQwqDvu.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-CoXM0gh_.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-Cq_27v0y.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-CsSSRMhn.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-ClatMQAX.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff-DOwlrIK9.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CrcGqqD_.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-BGLsMgQk.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-B0BAy-f9.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-fcdDzCSW.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-B7xaUz2H.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-CkWCaUmZ.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-DONC4eI2.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-CXDImOXW.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-Br2TLSlb.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-_-20umCs.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-OKDYjcMF.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D77M6q1f.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-C54B4Mns.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-CGEvLMd2.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-BSxkRQLj.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-C5KRTNOB.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-CnkAQV6o.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-4O0ZM14D.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-yNq7b0DZ.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-Bzyszv2N.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-zOf6ORAq.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-DCaZ4BWi.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-6cEFPNUR.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-BEo4-ISu.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.js +82 -0
- package/server/lib/agent-tool/DOCUMENTATION.md +132 -47
- package/server/lib/agent-tool/runtime.js +34 -15
- package/server/lib/agent-tool/runtime.test.js +15 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/global-hub.js +23 -1
- package/server/lib/event-stream/global-hub.test.js +35 -0
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- 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/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 +30 -2
- package/server/lib/opencode/lifecycle.test.js +27 -0
- 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/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 +38 -2
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -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.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- 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/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 +113 -0
- package/dist/assets/DiffView-D2p4Ar0H.js +0 -4
- package/dist/assets/FilesView-BeTtbsip.js +0 -17
- package/dist/assets/GuestSurfacePane-DIJvkkM-.js +0 -1
- package/dist/assets/MarkdownImageGallery-CY0dWTaE.js +0 -1
- package/dist/assets/MarkdownRendererImpl-ILdXzLrt.js +0 -12
- package/dist/assets/PluginPane-B39JEUL8.js +0 -3
- package/dist/assets/SettingsView-13B2_G-A.js +0 -29
- package/dist/assets/commitAndPush-CcI4ol7c.js +0 -1
- package/dist/assets/de-BtYnUgII.js +0 -10
- package/dist/assets/es-crP5qPye.js +0 -16
- package/dist/assets/fr-CLSRuALk.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-SZ1-ucLV.js +0 -16
- package/dist/assets/ko-HthbT_Ve.js +0 -16
- package/dist/assets/main-BJklC7Cl.js +0 -896
- package/dist/assets/markdownCore-BzQuETm0.js +0 -5
- package/dist/assets/patchFileDiff-DGw2KryM.js +0 -13
- package/dist/assets/pl-C8eYfjL1.js +0 -16
- package/dist/assets/pt-BR-cYOrZdxS.js +0 -16
- package/dist/assets/renderMobileApp-CMxjwisj.js +0 -25
- package/dist/assets/runtimeEndpointReset-C45jrFq4.js +0 -27
- package/dist/assets/scheduledTasksApi-CkK1F9s2.js +0 -1
- package/dist/assets/tr-B4IQ5t-T.js +0 -7
- package/dist/assets/uk-DCoXG6ve.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-BXBoxsFk.js +0 -16
- package/dist/assets/zh-TW-C6oelpnL.js +0 -16
|
@@ -125,9 +125,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
125
125
|
return { scope: SKILL_SCOPE.USER, source };
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
// Returns null when OpenCode's list could not be read (not running, error,
|
|
129
|
+
// timeout, malformed payload) so callers can tell a failed fetch from a
|
|
130
|
+
// genuinely empty list.
|
|
128
131
|
const fetchOpenCodeDiscoveredSkills = async (workingDirectory) => {
|
|
129
132
|
if (!getOpenCodePort()) {
|
|
130
|
-
return
|
|
133
|
+
return null;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
try {
|
|
@@ -145,13 +148,19 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
145
148
|
const response = await client.skill.list();
|
|
146
149
|
const payload = response?.data;
|
|
147
150
|
if (!Array.isArray(payload)) {
|
|
148
|
-
return
|
|
151
|
+
return null;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
return payload
|
|
152
155
|
.map((item) => {
|
|
153
156
|
const name = typeof item?.name === 'string' ? item.name.trim() : '';
|
|
154
|
-
|
|
157
|
+
// OpenCode v1's skill payload used `location`; v2 renamed the field
|
|
158
|
+
// to `path`. Accept both, or the whole authoritative list is dropped
|
|
159
|
+
// and the panel falls back to the (smaller) local disk scan.
|
|
160
|
+
const rawLocation = typeof item?.path === 'string' ? item.path : (typeof item?.location === 'string' ? item.location : '');
|
|
161
|
+
// v1 marked built-in skills with `<built-in>`; v2 gives them a synthetic
|
|
162
|
+
// `/builtin/<id>.md` path. Normalize so they stay read-only in the panel.
|
|
163
|
+
const location = rawLocation.startsWith('/builtin/') ? '<built-in>' : rawLocation;
|
|
155
164
|
const description = typeof item?.description === 'string' ? item.description : '';
|
|
156
165
|
const content = typeof item?.content === 'string' ? item.content : '';
|
|
157
166
|
if (!name || !location) {
|
|
@@ -183,7 +192,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
183
192
|
.filter(Boolean);
|
|
184
193
|
} catch (error) {
|
|
185
194
|
console.error('Failed to list OpenCode skills:', error);
|
|
186
|
-
return
|
|
195
|
+
return null;
|
|
187
196
|
}
|
|
188
197
|
};
|
|
189
198
|
|
|
@@ -244,7 +253,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
244
253
|
}
|
|
245
254
|
const openCodeSkills = await fetchOpenCodeDiscoveredSkills(directory);
|
|
246
255
|
const localSkills = discoverSkills(directory);
|
|
247
|
-
const skills = mergeDiscoveredSkills(openCodeSkills, localSkills);
|
|
256
|
+
const skills = mergeDiscoveredSkills(openCodeSkills ?? [], localSkills);
|
|
248
257
|
|
|
249
258
|
const enrichedSkills = skills.map((skill) => {
|
|
250
259
|
const sources = getSkillSources(skill.name, directory, skill);
|
|
@@ -268,7 +277,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
268
277
|
// OpenCode's own skill-list endpoint is not usable for this: on 1.18.14
|
|
269
278
|
// it returns only global and builtin skills, omitting the project
|
|
270
279
|
// `.agents`/`.claude` skills the agent demonstrably has.
|
|
271
|
-
|
|
280
|
+
const body = {
|
|
272
281
|
skills: enrichedSkills,
|
|
273
282
|
externalSkills: {
|
|
274
283
|
// `OPENCODE_DISABLE_CLAUDE_CODE` is the broad switch; the specific
|
|
@@ -277,7 +286,14 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
277
286
|
|| isEnvFlagEnabled(process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS),
|
|
278
287
|
allDisabled: isEnvFlagEnabled(process.env.OPENCODE_DISABLE_EXTERNAL_SKILLS),
|
|
279
288
|
},
|
|
280
|
-
}
|
|
289
|
+
};
|
|
290
|
+
// The list is only the disk scan when OpenCode's own list failed:
|
|
291
|
+
// built-in skills and anything OpenCode finds only through its config
|
|
292
|
+
// are missing, so the client must not treat this as complete.
|
|
293
|
+
if (openCodeSkills === null) {
|
|
294
|
+
body.openCodeSkillsUnavailable = true;
|
|
295
|
+
}
|
|
296
|
+
res.json(body);
|
|
281
297
|
} catch (error) {
|
|
282
298
|
console.error('Failed to list skills:', error);
|
|
283
299
|
res.status(500).json({ error: 'Failed to list skills' });
|
|
@@ -366,7 +382,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
const resolvedDiscovered = mergeDiscoveredSkills(
|
|
369
|
-
await fetchOpenCodeDiscoveredSkills(directory),
|
|
385
|
+
(await fetchOpenCodeDiscoveredSkills(directory)) ?? [],
|
|
370
386
|
discoverSkills(directory),
|
|
371
387
|
);
|
|
372
388
|
const installedByName = new Map(resolvedDiscovered.map((s) => [s.name, s]));
|
|
@@ -538,7 +554,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
538
554
|
if (error) {
|
|
539
555
|
return res.status(400).json({ error });
|
|
540
556
|
}
|
|
541
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
557
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
542
558
|
.find((skill) => skill.name === skillName) || null;
|
|
543
559
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
544
560
|
|
|
@@ -567,7 +583,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
567
583
|
return res.status(400).json({ error });
|
|
568
584
|
}
|
|
569
585
|
|
|
570
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
586
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
571
587
|
.find((skill) => skill.name === skillName) || null;
|
|
572
588
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
573
589
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -661,7 +677,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
661
677
|
return res.status(400).json({ error });
|
|
662
678
|
}
|
|
663
679
|
|
|
664
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
680
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
665
681
|
.find((skill) => skill.name === skillName) || null;
|
|
666
682
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
667
683
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -695,7 +711,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
695
711
|
return res.status(400).json({ error });
|
|
696
712
|
}
|
|
697
713
|
|
|
698
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
714
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
699
715
|
.find((skill) => skill.name === skillName) || null;
|
|
700
716
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
701
717
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -28,7 +28,7 @@ const createTempProject = () => {
|
|
|
28
28
|
return projectRoot;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const startSkillsApp = ({ projectRoot }) => {
|
|
31
|
+
const startSkillsApp = ({ projectRoot, overrides = {} }) => {
|
|
32
32
|
const app = express();
|
|
33
33
|
app.use(express.json());
|
|
34
34
|
|
|
@@ -76,6 +76,7 @@ const startSkillsApp = ({ projectRoot }) => {
|
|
|
76
76
|
fetchGitHubRepoMetas: async () => ({}),
|
|
77
77
|
getProfiles: () => [],
|
|
78
78
|
getProfile: () => null,
|
|
79
|
+
...overrides,
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
const server = app.listen(0);
|
|
@@ -213,4 +214,123 @@ describe('skill-routes directory soft fallback', () => {
|
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
});
|
|
217
|
+
|
|
218
|
+
it('lists OpenCode skills reported with the v2 `path` field and keeps the v1 `location` fallback', async () => {
|
|
219
|
+
projectRoot = createTempProject();
|
|
220
|
+
|
|
221
|
+
// OpenCode v2's skill payload renamed `location` to `path`. The route maps
|
|
222
|
+
// the fetch result to { name, path, ... }; with the old field only, the
|
|
223
|
+
// whole authoritative list was dropped and the panel fell back to the
|
|
224
|
+
// smaller local disk scan. Serve a v2-style payload from a stub.
|
|
225
|
+
const stub = express();
|
|
226
|
+
stub.get('/api/skill', (_req, res) => {
|
|
227
|
+
res.json({
|
|
228
|
+
location: { directory: projectRoot },
|
|
229
|
+
data: [
|
|
230
|
+
{
|
|
231
|
+
id: 'v2-path-skill',
|
|
232
|
+
name: 'v2-path-skill',
|
|
233
|
+
path: path.join(projectRoot, '.agents', 'skills', 'v2-path-skill'),
|
|
234
|
+
description: 'Delivered with the v2 path field',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'v1-location-skill',
|
|
238
|
+
name: 'v1-location-skill',
|
|
239
|
+
location: path.join(projectRoot, '.agents', 'skills', 'v1-location-skill'),
|
|
240
|
+
description: 'Delivered with the legacy location field',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'opencode',
|
|
244
|
+
name: 'OpenCode',
|
|
245
|
+
path: '/builtin/opencode.md',
|
|
246
|
+
description: 'v2 built-in skill with a synthetic path',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'unlocated-skill',
|
|
250
|
+
name: 'unlocated-skill',
|
|
251
|
+
description: 'Has neither field; must be dropped',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
const stubServer = await new Promise((resolve) => {
|
|
257
|
+
const server = stub.listen(0, () => resolve(server));
|
|
258
|
+
});
|
|
259
|
+
const stubPort = stubServer.address().port;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
appHandle = startSkillsApp({
|
|
263
|
+
projectRoot,
|
|
264
|
+
overrides: {
|
|
265
|
+
buildOpenCodeUrl: () => `http://127.0.0.1:${stubPort}/`,
|
|
266
|
+
getOpenCodePort: () => stubPort,
|
|
267
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const listResponse = await fetch(
|
|
272
|
+
`${appHandle.baseUrl}/api/config/skills?directory=${encodeURIComponent(projectRoot)}`,
|
|
273
|
+
);
|
|
274
|
+
expect(listResponse.status).toBe(200);
|
|
275
|
+
const payload = await listResponse.json();
|
|
276
|
+
const byName = new Map(payload.skills.map((skill) => [skill.name, skill]));
|
|
277
|
+
|
|
278
|
+
expect(byName.has('v2-path-skill')).toBe(true);
|
|
279
|
+
expect(byName.get('v2-path-skill').path).toContain('v2-path-skill');
|
|
280
|
+
|
|
281
|
+
expect(byName.has('v1-location-skill')).toBe(true);
|
|
282
|
+
expect(byName.get('v1-location-skill').path).toContain('v1-location-skill');
|
|
283
|
+
|
|
284
|
+
expect(byName.get('OpenCode')?.path).toBe('<built-in>');
|
|
285
|
+
expect(byName.get('OpenCode')?.renamable).toBe(false);
|
|
286
|
+
|
|
287
|
+
expect(byName.has('unlocated-skill')).toBe(false);
|
|
288
|
+
} finally {
|
|
289
|
+
stubServer.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
it('flags the list as partial when OpenCode skill list fails, and not when it succeeds', async () => {
|
|
293
|
+
projectRoot = createTempProject();
|
|
294
|
+
fs.mkdirSync(path.join(projectRoot, '.agents', 'skills', 'disk-skill'), { recursive: true });
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
path.join(projectRoot, '.agents', 'skills', 'disk-skill', 'SKILL.md'),
|
|
297
|
+
'---\nname: disk-skill\ndescription: On disk\n---\nBody\n',
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
let failing = true;
|
|
301
|
+
const stub = express();
|
|
302
|
+
stub.get('/api/skill', (_req, res) => {
|
|
303
|
+
if (failing) {
|
|
304
|
+
res.status(500).json({ error: 'boom' });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
res.json({ data: [] });
|
|
308
|
+
});
|
|
309
|
+
const stubServer = await new Promise((resolve) => {
|
|
310
|
+
const server = stub.listen(0, () => resolve(server));
|
|
311
|
+
});
|
|
312
|
+
const stubPort = stubServer.address().port;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
appHandle = startSkillsApp({
|
|
316
|
+
projectRoot,
|
|
317
|
+
overrides: {
|
|
318
|
+
buildOpenCodeUrl: () => `http://127.0.0.1:${stubPort}/`,
|
|
319
|
+
getOpenCodePort: () => stubPort,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const url = `${appHandle.baseUrl}/api/config/skills?directory=${encodeURIComponent(projectRoot)}`;
|
|
323
|
+
|
|
324
|
+
const failed = await (await fetch(url)).json();
|
|
325
|
+
expect(failed.openCodeSkillsUnavailable).toBe(true);
|
|
326
|
+
expect(failed.skills.map((skill) => skill.name)).toContain('disk-skill');
|
|
327
|
+
|
|
328
|
+
failing = false;
|
|
329
|
+
const complete = await (await fetch(url)).json();
|
|
330
|
+
expect(complete.openCodeSkillsUnavailable).toBeUndefined();
|
|
331
|
+
expect(complete.skills.map((skill) => skill.name)).toContain('disk-skill');
|
|
332
|
+
} finally {
|
|
333
|
+
stubServer.close();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
216
336
|
});
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
// TEMPORARY WORKAROUND — Windows ARM64: native opencode.exe fails with a Bun
|
|
2
|
-
// FFI/TinyCC dlopen error (https://github.com/anomalyco/opencode/issues/19130).
|
|
3
|
-
// Disable OpenCode self-upgrade on ARM64 so it can't overwrite the working x64
|
|
4
|
-
// binary with the broken ARM64 build. Remove when the upstream issue is resolved.
|
|
5
|
-
const isWindowsArm64 = () => process.platform === 'win32' && process.arch === 'arm64';
|
|
6
|
-
|
|
7
1
|
export const resolveOpenCodeUpgradeCapability = ({
|
|
8
2
|
isExternal,
|
|
9
3
|
hasManagedProcess,
|
|
10
4
|
activeBinary,
|
|
11
5
|
isBundledBinary,
|
|
12
6
|
}) => {
|
|
13
|
-
if (isWindowsArm64()) {
|
|
14
|
-
return {
|
|
15
|
-
supported: false,
|
|
16
|
-
manager: 'openchamber',
|
|
17
|
-
reason: 'windows-arm64-workaround',
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
7
|
if (isExternal) {
|
|
22
8
|
return {
|
|
23
9
|
supported: false,
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
* seen v2 activity. Every visited session gets its `session_message` rows
|
|
22
22
|
* deleted and replaced by the V1 transform, so a migrated session that was
|
|
23
23
|
* continued in v2 would lose that conversation.
|
|
24
|
-
* 3.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
24
|
+
* 3. Only 1.x activity since the last completed import triggers a new one.
|
|
25
|
+
* A v2 delete removes the `session_v2` row but leaves the legacy `session`
|
|
26
|
+
* row behind, so "missing from v2" alone would bring deleted sessions back
|
|
27
|
+
* on every start. A legacy session counts as missing only when its
|
|
28
|
+
* `time_updated` is newer than the completion stamp OpenCode writes on
|
|
29
|
+
* `migration.v1-v2`: only OpenCode 1.x writes that table, so an unchanged
|
|
30
|
+
* table means nobody ran 1.x since and the top-up skips. Decided by the
|
|
31
|
+
* maintainer (2026-09-24). When 1.x was used again, OpenCode still walks
|
|
32
|
+
* every id below the cursor, so a deleted session sorting below a fresh one
|
|
33
|
+
* comes back with it; that needs an import route taking an explicit list.
|
|
32
34
|
*
|
|
33
35
|
* The loop OpenCode runs is `SELECT id FROM session WHERE id < cursor ORDER BY
|
|
34
36
|
* id DESC LIMIT 1`, one session at a time, so the cursor has to be strictly
|
|
@@ -196,11 +198,15 @@ const findRevisitedSessionsWithV2Activity = (db, cursor, completedAt) =>
|
|
|
196
198
|
return parsed.success ? [parsed.data.id] : [];
|
|
197
199
|
});
|
|
198
200
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
+
/**
|
|
202
|
+
* A legacy session with no `session_v2` twin that 1.x touched after the last
|
|
203
|
+
* completed import (see rule 3). Bound parameter: the completion timestamp.
|
|
204
|
+
*/
|
|
205
|
+
const MISSING_CONDITION = 'id NOT IN (SELECT id FROM session_v2) AND time_updated > ?';
|
|
201
206
|
|
|
202
|
-
const countMissingSessions = (db) =>
|
|
203
|
-
firstRow(countRowSchema, db.all(`SELECT COUNT(*) AS value FROM session WHERE ${MISSING_CONDITION}
|
|
207
|
+
const countMissingSessions = (db, completedAt) =>
|
|
208
|
+
firstRow(countRowSchema, db.all(`SELECT COUNT(*) AS value FROM session WHERE ${MISSING_CONDITION}`, [completedAt]))
|
|
209
|
+
?.value ?? 0;
|
|
204
210
|
|
|
205
211
|
const countRevisitedSessions = (db, cursor) =>
|
|
206
212
|
firstRow(
|
|
@@ -244,12 +250,12 @@ export const topUpV1Migration = (options = {}) => {
|
|
|
244
250
|
const migration = readCompletedMigration(db);
|
|
245
251
|
if (!migration) return outcome('skipped', 'migration-not-completed');
|
|
246
252
|
|
|
247
|
-
const missingCount = countMissingSessions(db);
|
|
253
|
+
const missingCount = countMissingSessions(db, migration.completedAt);
|
|
248
254
|
if (missingCount === 0) return outcome('skipped', 'nothing-missing');
|
|
249
255
|
|
|
250
256
|
const maxMissing = firstRow(
|
|
251
257
|
nullableIdRowSchema,
|
|
252
|
-
db.all(`SELECT MAX(id) AS id FROM session WHERE ${MISSING_CONDITION}
|
|
258
|
+
db.all(`SELECT MAX(id) AS id FROM session WHERE ${MISSING_CONDITION}`, [migration.completedAt]),
|
|
253
259
|
)?.id;
|
|
254
260
|
if (!maxMissing) return outcome('skipped', 'nothing-missing');
|
|
255
261
|
|
|
@@ -271,7 +277,7 @@ export const topUpV1Migration = (options = {}) => {
|
|
|
271
277
|
MIGRATION_STATE_KEY,
|
|
272
278
|
]);
|
|
273
279
|
logger.log(
|
|
274
|
-
`[OpenCode] Scheduled the import of ${missingCount} OpenCode 1.x session(s)
|
|
280
|
+
`[OpenCode] Scheduled the import of ${missingCount} OpenCode 1.x session(s) changed since the last v2 import; ` +
|
|
275
281
|
`OpenCode will re-import ${revisitedCount} untouched session(s) on the way.`,
|
|
276
282
|
);
|
|
277
283
|
return outcome('scheduled', undefined, missingCount, revisitedCount);
|
|
@@ -226,13 +226,21 @@ describe.skipIf(!sqlite)('topUpV1Migration', () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
it('
|
|
230
|
-
// ses_a was
|
|
231
|
-
//
|
|
232
|
-
// back over losing ses_d: deleting again is cheap, a lost session is not.
|
|
229
|
+
it('does not bring back a session deleted in v2 when 1.x was not used since', () => {
|
|
230
|
+
// ses_a was imported and then deleted in v2: only its legacy row is left,
|
|
231
|
+
// untouched since the last completed import.
|
|
233
232
|
createDatabase();
|
|
234
|
-
seed({ v1: [{ id: 'ses_a', timeCreated: COMPLETED_AT - 1 }
|
|
235
|
-
expect(run()).toEqual({ status: '
|
|
233
|
+
seed({ v1: [{ id: 'ses_a', timeCreated: COMPLETED_AT - 1 }] });
|
|
234
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'nothing-missing' });
|
|
235
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('imports only sessions 1.x changed since the last import, cursor included', () => {
|
|
239
|
+
// ses_z (deleted in v2, old) sorts above ses_d (fresh from 1.x): the
|
|
240
|
+
// cursor must stop at ses_d so ses_z stays gone.
|
|
241
|
+
createDatabase();
|
|
242
|
+
seed({ v1: [{ id: 'ses_z', timeCreated: COMPLETED_AT - 1 }, 'ses_d'] });
|
|
243
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 1, revisited: 0 });
|
|
236
244
|
expect(readMigrationRow().state).toEqual({ phase: 'sessions', cursor: computeResumeCursor('ses_d') });
|
|
237
245
|
});
|
|
238
246
|
|
|
@@ -18,10 +18,10 @@ const packageDistSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// OpenCode's installer is a bash script, so Windows installs the npm platform
|
|
21
|
-
// package that script downloads
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
21
|
+
// package that script downloads, the same one the desktop bundle picks in
|
|
22
|
+
// packages/electron/scripts/prepare-opencode-cli.mjs. x64 takes the baseline
|
|
23
|
+
// build so hosts without AVX2 still run it.
|
|
24
|
+
const windowsPackage = (arch) => (arch === 'arm64' ? '@opencode/cli-windows-arm64' : '@opencode/cli-windows-x64-baseline');
|
|
25
25
|
|
|
26
26
|
export const supportsOpenCodeV2Install = (platform = process.platform) =>
|
|
27
27
|
(platform === 'darwin' || platform === 'linux' || platform === 'win32') && (process.arch === 'x64' || process.arch === 'arm64');
|
|
@@ -72,8 +72,8 @@ const prepareInstallerScript = async ({ version, workDirectory, env, fetchImpl }
|
|
|
72
72
|
* Downloads the Windows platform package, checks it against the integrity npm
|
|
73
73
|
* publishes for it, and unpacks it. Returns the step that replaces the binary.
|
|
74
74
|
*/
|
|
75
|
-
const prepareWindowsBinary = async ({ version, directory, workDirectory, fetchImpl, tarCommand }) => {
|
|
76
|
-
const metadataResponse = await fetchImpl(`${NPM_REGISTRY}/${
|
|
75
|
+
const prepareWindowsBinary = async ({ version, arch, directory, workDirectory, fetchImpl, tarCommand }) => {
|
|
76
|
+
const metadataResponse = await fetchImpl(`${NPM_REGISTRY}/${windowsPackage(arch).replace('/', '%2F')}/${version}`, { signal: AbortSignal.timeout(15_000) });
|
|
77
77
|
if (!metadataResponse.ok) throw new Error('Could not resolve the OpenCode v2 package.');
|
|
78
78
|
const { dist } = packageDistSchema.parse(await metadataResponse.json());
|
|
79
79
|
if (new URL(dist.tarball).origin !== NPM_REGISTRY) throw new Error('The OpenCode v2 package is not served by the npm registry.');
|
|
@@ -100,6 +100,7 @@ export const installOpenCodeV2 = async ({
|
|
|
100
100
|
env = process.env,
|
|
101
101
|
fetchImpl = fetch,
|
|
102
102
|
platform = process.platform,
|
|
103
|
+
arch = process.arch,
|
|
103
104
|
// Windows' own bsdtar. Git Bash's GNU tar on PATH reads `C:\...` as a remote host.
|
|
104
105
|
tarCommand = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe'),
|
|
105
106
|
} = {}) => {
|
|
@@ -119,7 +120,7 @@ export const installOpenCodeV2 = async ({
|
|
|
119
120
|
if (!releaseResponse.ok) throw new Error('Could not resolve the OpenCode v2 release.');
|
|
120
121
|
const { version } = releaseSchema.parse(await releaseResponse.json());
|
|
121
122
|
const replaceBinary = windows
|
|
122
|
-
? await prepareWindowsBinary({ version, directory, workDirectory: lock, fetchImpl, tarCommand })
|
|
123
|
+
? await prepareWindowsBinary({ version, arch, directory, workDirectory: lock, fetchImpl, tarCommand })
|
|
123
124
|
: await prepareInstallerScript({ version, workDirectory: lock, env, fetchImpl });
|
|
124
125
|
for (const name of binaryNames) {
|
|
125
126
|
const target = path.join(directory, name);
|
|
@@ -63,7 +63,7 @@ chmod 755 ${quote(binary)}
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
describe('on Windows', () => {
|
|
66
|
-
const
|
|
66
|
+
const tarballOf = (name) => `https://registry.npmjs.org/@opencode/${name}/-/${name}-2.0.15.tgz`;
|
|
67
67
|
let exe;
|
|
68
68
|
const integrityOf = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
|
|
69
69
|
// The platform package keeps its binary at package/bin/opencode.exe. Here it is a
|
|
@@ -77,14 +77,17 @@ chmod 755 ${quote(binary)}
|
|
|
77
77
|
await fs.rm(root, { recursive: true, force: true });
|
|
78
78
|
return bytes;
|
|
79
79
|
};
|
|
80
|
-
const
|
|
80
|
+
const PACKAGE_BY_ARCH = { x64: 'cli-windows-x64-baseline', arm64: 'cli-windows-arm64' };
|
|
81
|
+
const runWindows = (archive, integrity = integrityOf(archive), arch = 'x64') => installOpenCodeV2({
|
|
81
82
|
homeDirectory,
|
|
82
83
|
platform: 'win32',
|
|
84
|
+
arch,
|
|
83
85
|
tarCommand: 'tar',
|
|
84
86
|
fetchImpl: async (url) => {
|
|
87
|
+
const name = PACKAGE_BY_ARCH[arch];
|
|
85
88
|
if (url.endsWith('/@opencode%2Fcli/latest')) return Response.json({ version: '2.0.15' });
|
|
86
|
-
if (url.endsWith(
|
|
87
|
-
if (url ===
|
|
89
|
+
if (url.endsWith(`/@opencode%2F${name}/2.0.15`)) return Response.json({ dist: { tarball: tarballOf(name), integrity } });
|
|
90
|
+
if (url === tarballOf(name)) return new Response(archive);
|
|
88
91
|
return new Response(null, { status: 404 });
|
|
89
92
|
},
|
|
90
93
|
});
|
|
@@ -101,6 +104,12 @@ chmod 755 ${quote(binary)}
|
|
|
101
104
|
await expect(fs.stat(path.join(path.dirname(exe), '.openchamber-install'))).rejects.toMatchObject({ code: 'ENOENT' });
|
|
102
105
|
});
|
|
103
106
|
|
|
107
|
+
it('installs the native arm64 package on Windows ARM64', async () => {
|
|
108
|
+
const archive = await pack('2.0.15');
|
|
109
|
+
expect(await runWindows(archive, integrityOf(archive), 'arm64')).toBe(exe);
|
|
110
|
+
expect(await readOpenCodeCliVersion({ binary: exe, args: [] })).toBe('2.0.15');
|
|
111
|
+
});
|
|
112
|
+
|
|
104
113
|
it('rejects a package that does not match its published integrity before touching the binary', async () => {
|
|
105
114
|
const archive = await pack('2.0.15');
|
|
106
115
|
await expect(runWindows(archive, integrityOf(Buffer.from('other')))).rejects.toThrow('integrity');
|
|
@@ -47,13 +47,15 @@ export const createOpenCodeWatcherRuntime = (deps) => {
|
|
|
47
47
|
const signal = abortController.signal;
|
|
48
48
|
|
|
49
49
|
if (globalEventHub) {
|
|
50
|
+
// The events of isolated spaces feed this watcher too, so live status, unread marks and
|
|
51
|
+
// notifications work for a space's sessions as for the host's.
|
|
50
52
|
unsubscribeEvent = globalEventHub.subscribeEvent((event) => {
|
|
51
53
|
const payload = unwrapGlobalEventPayload(event.payload);
|
|
52
54
|
if (!payload || typeof payload !== 'object') {
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
emitTranslated(payload);
|
|
56
|
-
});
|
|
58
|
+
}, { spaces: true });
|
|
57
59
|
unsubscribeStatus = globalEventHub.subscribeStatus((status) => {
|
|
58
60
|
if (signal.aborted) {
|
|
59
61
|
return;
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
readConfigLayers,
|
|
10
10
|
writeConfig,
|
|
11
11
|
} from './shared.js';
|
|
12
|
-
import { findWebSearchProjectOverride, writeWebSearchSelection } from './config-v2.js';
|
|
12
|
+
import { findWebSearchProjectOverride, writeWarmingEnabled, writeWebSearchSelection } from './config-v2.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Writes the `websearch` choice into the config OpenCode reads last among the
|
|
@@ -26,6 +26,18 @@ export function setWebSearchSelection(selection) {
|
|
|
26
26
|
return { path: target.path, changed };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Turns session warming on or off in the same file the web search choice goes
|
|
31
|
+
* to; OpenCode watches it, so the change applies without a restart.
|
|
32
|
+
*/
|
|
33
|
+
export function setWarmingEnabled(enabled) {
|
|
34
|
+
const layers = readConfigLayers(null);
|
|
35
|
+
const target = getJsonWriteTarget(layers, AGENT_SCOPE.USER);
|
|
36
|
+
const changed = writeWarmingEnabled(target.config, enabled);
|
|
37
|
+
if (changed) writeConfig(target.config, target.path);
|
|
38
|
+
return { path: target.path, changed };
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* Where the effective `websearch` value for `directory` comes from, as far as
|
|
31
43
|
* a Settings write is concerned: `projectPath` is the project config that
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export const providerId = 'kimi-for-coding';
|
|
14
14
|
export const providerName = 'Kimi for Coding';
|
|
15
|
-
const aliases = ['kimi-for-coding', 'kimi'];
|
|
15
|
+
const aliases = ['kimi-for-coding', 'kimi', 'kimi-code-plan-global'];
|
|
16
16
|
|
|
17
17
|
// Kimi's weekly `usage` block reports `used`; its rate-limit `limits[].detail`
|
|
18
18
|
// blocks report `remaining` instead. Neither field is guaranteed present, so
|
|
@@ -10,10 +10,14 @@ const isAllowedSsePath = (pathname) => {
|
|
|
10
10
|
|| pathname === '/api/notifications/stream';
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// The same three sockets of an isolated space, under `/api/spaces/<id>/`, matched by shape.
|
|
14
|
+
const SPACE_WS_PATH = /^\/api\/spaces\/[0-9a-f]{12}\/(?:terminal\/ws|event\/ws|global\/event\/ws)$/;
|
|
15
|
+
|
|
13
16
|
const isAllowedWebSocketPath = (pathname) => {
|
|
14
17
|
return pathname === '/api/event/ws'
|
|
15
18
|
|| pathname === '/api/global/event/ws'
|
|
16
|
-
|| pathname === '/api/terminal/ws'
|
|
19
|
+
|| pathname === '/api/terminal/ws'
|
|
20
|
+
|| SPACE_WS_PATH.test(pathname);
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
const normalizeBaseUrl = (value) => {
|
|
@@ -228,6 +228,35 @@ describe('realtime proxy', () => {
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
it('proxies the sockets of an isolated space by path shape, and refuses another path under the prefix', async () => {
|
|
232
|
+
const seen = [];
|
|
233
|
+
const upstreamServer = http.createServer();
|
|
234
|
+
const upstreamWs = new WebSocketServer({ server: upstreamServer });
|
|
235
|
+
upstreamWs.on('connection', (socket, request) => {
|
|
236
|
+
seen.push(request.url);
|
|
237
|
+
socket.send('ready');
|
|
238
|
+
});
|
|
239
|
+
const upstreamOrigin = await listen(upstreamServer);
|
|
240
|
+
const { origin, runtime } = await startProxyServer({ apiBaseUrl: upstreamOrigin });
|
|
241
|
+
const attempt = (path) => new Promise((resolve, reject) => {
|
|
242
|
+
const client = new WebSocket(buildRealtimeProxyWsUrl(origin, `${upstreamOrigin.replace(/^http:/, 'ws:')}${path}`), { headers: { Origin: 'openchamber-ui://app' } });
|
|
243
|
+
client.once('message', (data) => { client.close(); resolve({ message: data.toString() }); });
|
|
244
|
+
client.once('close', (code) => resolve({ code }));
|
|
245
|
+
client.once('error', reject);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
expect(await attempt('/api/spaces/a1b2c3d4e5f6/terminal/ws')).toEqual({ message: 'ready' });
|
|
250
|
+
expect(await attempt('/api/spaces/a1b2c3d4e5f6/event/ws?directory=%2Fspaces%2Fa1b2c3d4e5f6%2Frepo')).toEqual({ message: 'ready' });
|
|
251
|
+
expect(await attempt('/api/spaces/a1b2c3d4e5f6/dictation/ws')).toEqual({ code: 1008 });
|
|
252
|
+
expect(await attempt('/api/spaces/A1B2C3D4E5F6/terminal/ws')).toEqual({ code: 1008 });
|
|
253
|
+
expect(seen).toEqual(['/api/spaces/a1b2c3d4e5f6/terminal/ws', '/api/spaces/a1b2c3d4e5f6/event/ws?directory=%2Fspaces%2Fa1b2c3d4e5f6%2Frepo']);
|
|
254
|
+
upstreamWs.close();
|
|
255
|
+
} finally {
|
|
256
|
+
runtime.stop();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
231
260
|
it('allows first passwordless WebSocket proxy upgrade without an existing cookie', async () => {
|
|
232
261
|
const upstreamServer = http.createServer();
|
|
233
262
|
const upstreamWs = new WebSocketServer({ server: upstreamServer });
|
|
@@ -37,7 +37,10 @@ const ALLOWED_WS_PATHS = new Set([
|
|
|
37
37
|
// Extension surfaces carry the extension id in the path, so they are matched
|
|
38
38
|
// by shape rather than listed.
|
|
39
39
|
const GUEST_SURFACE_WS_PATH = /^\/api\/guests\/[a-z][a-z0-9-]*\/surface\/ws$/;
|
|
40
|
-
|
|
40
|
+
// The sockets of an isolated space carry the space id in the path: the same four sockets,
|
|
41
|
+
// under `/api/spaces/<id>/`, matched by shape.
|
|
42
|
+
const SPACE_WS_PATH = /^\/api\/spaces\/[0-9a-f]{12}\/(?:terminal\/ws|dev-tunnel|event\/ws|global\/event\/ws)$/;
|
|
43
|
+
export const isAllowedRelayWebSocketPath = (pathname) => ALLOWED_WS_PATHS.has(pathname) || GUEST_SURFACE_WS_PATH.test(pathname) || SPACE_WS_PATH.test(pathname);
|
|
41
44
|
|
|
42
45
|
// Hop-by-hop headers stripped from tunneled requests; `host` is set by fetch
|
|
43
46
|
// to the loopback origin. content-length is dropped too because the body is
|
|
@@ -153,6 +153,17 @@ describe('relay host WebSocket allowlist', () => {
|
|
|
153
153
|
expect(isAllowedRelayWebSocketPath('/api/database/ws')).toBe(false);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
test('allows the sockets of an isolated space by path shape only', () => {
|
|
157
|
+
for (const socket of ['terminal/ws', 'dev-tunnel', 'event/ws', 'global/event/ws']) {
|
|
158
|
+
expect(isAllowedRelayWebSocketPath(`/api/spaces/a1b2c3d4e5f6/${socket}`)).toBe(true);
|
|
159
|
+
}
|
|
160
|
+
expect(isAllowedRelayWebSocketPath('/api/spaces/a1b2c3d4e5f6/dictation/ws')).toBe(false);
|
|
161
|
+
expect(isAllowedRelayWebSocketPath('/api/spaces/A1B2C3D4E5F6/terminal/ws')).toBe(false);
|
|
162
|
+
expect(isAllowedRelayWebSocketPath('/api/spaces/a1b2c3d4e5f/terminal/ws')).toBe(false);
|
|
163
|
+
expect(isAllowedRelayWebSocketPath('/api/spaces/a1b2c3d4e5f6/terminal/ws/x')).toBe(false);
|
|
164
|
+
expect(isAllowedRelayWebSocketPath('/api/spaces//terminal/ws')).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
156
167
|
test('allows an extension surface socket by path shape only', () => {
|
|
157
168
|
expect(isAllowedRelayWebSocketPath('/api/guests/server-chrome/surface/ws')).toBe(true);
|
|
158
169
|
expect(isAllowedRelayWebSocketPath('/api/guests/Server/surface/ws')).toBe(false);
|