@aiworker/web 1.22.2 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
- package/dist/assets/DiffView-DC9RX3MF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
- package/dist/assets/FilesView-BFZkuu4O.js +13 -0
- package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
- package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
- package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
- package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
- package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
- package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
- package/dist/assets/PlanView-BmQUYT_z.js +5 -0
- package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
- package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
- package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DJsQr-p7.js +10 -0
- package/dist/assets/es-BuYMlA9g.js +16 -0
- package/dist/assets/fr-ePDay33C.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-BD8ILbxF.js +40 -0
- package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
- package/dist/assets/index-zhgg-SDz.css +1 -0
- package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
- package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
- package/dist/assets/ja-BzR9xOY_.js +16 -0
- package/dist/assets/ko-BEjHAm14.js +16 -0
- package/dist/assets/main-BydQ1tr_.js +930 -0
- package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
- package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
- package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
- package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
- package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
- package/dist/assets/pl-rQmpxNIF.js +16 -0
- package/dist/assets/pt-BR-CorbsSW-.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
- package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
- package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
- package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
- package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
- package/dist/assets/surface-COKb77Ua.js +1 -0
- package/dist/assets/tr-CoAMXPtI.js +7 -0
- package/dist/assets/uk-YlAX0rRU.js +16 -0
- package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
- package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
- package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
- package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-B6551dwG.js +39 -0
- package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
- package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
- package/dist/assets/zh-TW-CQKaCayr.js +16 -0
- package/dist/index.html +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +2 -3
- package/server/index.d.ts +15 -0
- package/server/index.js +48 -22
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +72 -12
- package/server/lib/fs/DOCUMENTATION.md +3 -0
- package/server/lib/fs/routes.js +31 -0
- package/server/lib/fs/routes.test.js +110 -1
- package/server/lib/git/DOCUMENTATION.md +39 -6
- package/server/lib/git/routes.js +45 -12
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +343 -202
- package/server/lib/git/service.test.js +496 -25
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +11 -1
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +35 -5
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +17 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +88 -41
- package/server/lib/opencode/settings-helpers.test.js +176 -1
- package/server/lib/opencode/settings-registry.json +750 -0
- package/server/lib/opencode/settings-runtime.js +175 -27
- package/server/lib/opencode/settings-runtime.test.js +321 -7
- package/server/lib/project-context/DOCUMENTATION.md +38 -4
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +15 -0
- package/server/lib/project-context/runtime.js +256 -25
- package/server/lib/project-context/runtime.test.js +160 -3
- package/server/lib/projects/DOCUMENTATION.md +77 -0
- package/server/lib/projects/project-config.js +179 -17
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +43 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +31 -1
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/index.js +7 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +1 -1
- package/server/lib/quota/providers/opencode-go.test.js +26 -1
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/relay/DOCUMENTATION.md +75 -0
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +44 -8
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +117 -267
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/session-goal/runtime.js +4 -9
- package/server/lib/small-model/DOCUMENTATION.md +3 -2
- package/server/lib/small-model/call.js +9 -4
- package/server/lib/small-model/call.test.js +29 -0
- package/server/lib/small-model/index.js +5 -10
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/terminal/DOCUMENTATION.md +4 -4
- package/server/lib/terminal/runtime.js +11 -6
- package/server/lib/terminal/runtime.test.js +20 -13
- package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +36 -4
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-hDBKesA_.js +0 -4
- package/dist/assets/FilesView-CdWzeOp9.js +0 -17
- package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
- package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
- package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
- package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
- package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
- package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
- package/dist/assets/de-B5JaA3SA.js +0 -10
- package/dist/assets/es-EPIH88ii.js +0 -16
- package/dist/assets/fr-CAMfxUdZ.js +0 -14
- package/dist/assets/index-DF5kMP-i.js +0 -39
- package/dist/assets/index-Dl3xbAB_.css +0 -1
- package/dist/assets/ja-CifOE1VN.js +0 -16
- package/dist/assets/ko-BBDyAyYQ.js +0 -16
- package/dist/assets/main-MhSaOZoB.js +0 -930
- package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
- package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
- package/dist/assets/pl-DgEHYfFM.js +0 -16
- package/dist/assets/pt-BR-DuWHojUw.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
- package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
- package/dist/assets/tr-D5m2yIWL.js +0 -7
- package/dist/assets/uk-D_q60oog.js +0 -16
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
- package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
- package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
- package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const POSIX_ENV_BINARIES = ['/usr/bin/env', '/bin/env'];
|
|
16
|
+
/** Variables a PTY shell must never inherit from the AiWorker host process. */
|
|
17
|
+
const PTY_HOST_PRIVATE_VARIABLES = Object.freeze(['ARGV0', 'NODE_CHANNEL_FD']);
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Remove AppImage `ARGV0` from a mutable env object (or `process.env`).
|
|
@@ -49,26 +51,31 @@ export function clearAppImageArgv0FromProcessEnv() {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
* Resolve a
|
|
54
|
+
* Resolve a POSIX PTY launch that unsets host-private variables before the
|
|
55
|
+
* shell starts.
|
|
53
56
|
*
|
|
54
|
-
* `bun-pty` merges the OS environ into the child, so deleting
|
|
55
|
-
* JS env object alone is not enough
|
|
56
|
-
*
|
|
57
|
+
* `bun-pty` merges the OS environ into the child, so deleting a variable from
|
|
58
|
+
* the JS env object alone is not enough: AppImage `ARGV0` came back on Linux,
|
|
59
|
+
* and the daemon's `NODE_CHANNEL_FD` came back everywhere (Node then warns
|
|
60
|
+
* "Failed to parse IPC channel number" when a CLI such as opencode exits).
|
|
61
|
+
* Wrapping with `env -u NAME ...` unsets them before execing the real shell.
|
|
62
|
+
* No-op on Windows and when no `env` binary is found.
|
|
57
63
|
*
|
|
58
64
|
* @param {string} executable
|
|
59
65
|
* @param {string[]} args
|
|
66
|
+
* @param {readonly string[]} variables
|
|
60
67
|
* @returns {{ executable: string, args: string[] }}
|
|
61
68
|
*/
|
|
62
|
-
export function
|
|
63
|
-
if (process.platform
|
|
69
|
+
export function resolvePosixPtyLaunch(executable, args = [], variables = PTY_HOST_PRIVATE_VARIABLES) {
|
|
70
|
+
if (process.platform === 'win32' || variables.length === 0) {
|
|
64
71
|
return { executable, args };
|
|
65
72
|
}
|
|
66
|
-
const envBinary =
|
|
73
|
+
const envBinary = POSIX_ENV_BINARIES.find((candidate) => existsSync(candidate));
|
|
67
74
|
if (!envBinary) {
|
|
68
75
|
return { executable, args };
|
|
69
76
|
}
|
|
70
77
|
return {
|
|
71
78
|
executable: envBinary,
|
|
72
|
-
args: ['-u',
|
|
79
|
+
args: [...variables.flatMap((name) => ['-u', name]), executable, ...args],
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
clearAppImageArgv0FromProcessEnv,
|
|
4
|
-
|
|
4
|
+
resolvePosixPtyLaunch,
|
|
5
5
|
stripAppImageArgv0Leak,
|
|
6
6
|
} from './inherited-env.js';
|
|
7
7
|
|
|
@@ -46,18 +46,17 @@ describe('clearAppImageArgv0FromProcessEnv', () => {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
describe('
|
|
50
|
-
it('wraps the shell with env -u
|
|
51
|
-
if (process.platform
|
|
52
|
-
expect(
|
|
49
|
+
describe('resolvePosixPtyLaunch', () => {
|
|
50
|
+
it('wraps the shell with env -u for every host-private variable on POSIX', () => {
|
|
51
|
+
if (process.platform === 'win32') return;
|
|
52
|
+
expect(resolvePosixPtyLaunch('/bin/zsh', ['-l'])).toEqual({
|
|
53
53
|
executable: expect.stringMatching(/\/env$/),
|
|
54
|
-
args: ['-u', 'ARGV0', '/bin/zsh', '-l'],
|
|
54
|
+
args: ['-u', 'ARGV0', '-u', 'NODE_CHANNEL_FD', '/bin/zsh', '-l'],
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it('leaves
|
|
59
|
-
|
|
60
|
-
expect(resolveLinuxPtyLaunch('/bin/zsh', ['-l'])).toEqual({
|
|
58
|
+
it('leaves the launch unchanged with nothing to unset', () => {
|
|
59
|
+
expect(resolvePosixPtyLaunch('/bin/zsh', ['-l'], [])).toEqual({
|
|
61
60
|
executable: '/bin/zsh',
|
|
62
61
|
args: ['-l'],
|
|
63
62
|
});
|
|
@@ -31,6 +31,7 @@ send never re-resolves mutable UI state:
|
|
|
31
31
|
{
|
|
32
32
|
id, createdAt,
|
|
33
33
|
content, // raw text for display and editing
|
|
34
|
+
contextPreview?, // bounded display-only summary captured by the UI
|
|
34
35
|
text, // text to deliver (agent mention stripped, file mentions resolved); defaults to content
|
|
35
36
|
agentMention?, // delivered as an `agent` part
|
|
36
37
|
attachments: [{ id, filename, mimeType, size, source, serverPath?, dataUrl }],
|
|
@@ -56,6 +57,12 @@ context entry). Public snapshots and broadcasts strip the payloads —
|
|
|
56
57
|
attachment `dataUrl` (megabytes of base64) and `context` (a PR diff, say) —
|
|
57
58
|
so they do not ride every update; the only way to get them back is a `take`.
|
|
58
59
|
|
|
60
|
+
Snapshots retain `contextPreview`, capped at 100 characters plus an ellipsis.
|
|
61
|
+
It carries the attached comment or context label when `content` is empty and
|
|
62
|
+
never replaces editable text or delivered parts. Older items without a summary
|
|
63
|
+
derive one from the attached comment metadata or the first non-instruction
|
|
64
|
+
context text. This optional field needs no queue-file migration.
|
|
65
|
+
|
|
59
66
|
## Persistence
|
|
60
67
|
|
|
61
68
|
`<data-dir>/message-queue.json` (`AIWORKER_DATA_DIR` or
|
|
@@ -140,7 +147,10 @@ allowlists.
|
|
|
140
147
|
|
|
141
148
|
Every mutation broadcasts `aiworker:message-queue.updated` with
|
|
142
149
|
`{ revision, session }` to all connected clients (SSE and WS), so several
|
|
143
|
-
devices on one server see one queue.
|
|
150
|
+
devices on one server see one queue. SSE uses the shared control stream at
|
|
151
|
+
`/api/openchamber/events`; `/api/global/event` carries no AiWorker events.
|
|
152
|
+
The UI subscribes independently of its OpenCode transport and re-reads the
|
|
153
|
+
snapshot whenever either stream reconnects. The session in that payload always names
|
|
144
154
|
its `directory`, including the broadcast that removes the last item: the UI
|
|
145
155
|
keys its projection by directory, and a broadcast without one left the
|
|
146
156
|
delivered message on screen (a session's directory is remembered until the
|
|
@@ -133,6 +133,8 @@ export const parseQueuedItemInput = (value) => {
|
|
|
133
133
|
if (agentMention) item.agentMention = agentMention;
|
|
134
134
|
item.attachments = attachments;
|
|
135
135
|
item.context = context;
|
|
136
|
+
const contextPreview = asNonEmptyString(raw.contextPreview).slice(0, 103);
|
|
137
|
+
if (contextPreview) item.contextPreview = contextPreview;
|
|
136
138
|
item.sendConfig = sendConfig;
|
|
137
139
|
return item;
|
|
138
140
|
};
|
|
@@ -157,6 +159,17 @@ const toPublicItem = (item) => {
|
|
|
157
159
|
const publicItem = { id: item.id, createdAt: item.createdAt, content: item.content, text: item.text };
|
|
158
160
|
if (item.agentMention) publicItem.agentMention = item.agentMention;
|
|
159
161
|
publicItem.attachments = item.attachments.map(toPublicAttachment);
|
|
162
|
+
// Older persisted items have no UI summary. Prefer their attached comment
|
|
163
|
+
// before falling back to the model-facing context text.
|
|
164
|
+
const contextPreview = item.contextPreview || item.context
|
|
165
|
+
.filter((part) => part.kind !== 'instruction')
|
|
166
|
+
.map((part) => asNonEmptyString(asRecord(part.metadata?.aiworkerContext)?.text) || part.text.trim())
|
|
167
|
+
.find(Boolean);
|
|
168
|
+
if (contextPreview) {
|
|
169
|
+
const firstLine = contextPreview.split('\n', 1)[0];
|
|
170
|
+
publicItem.contextPreview = firstLine.slice(0, 100)
|
|
171
|
+
+ (contextPreview.length > firstLine.length || firstLine.length > 100 ? '...' : '');
|
|
172
|
+
}
|
|
160
173
|
publicItem.sendConfig = { ...item.sendConfig };
|
|
161
174
|
return publicItem;
|
|
162
175
|
};
|
|
@@ -251,7 +251,7 @@ describe('message queue runtime', () => {
|
|
|
251
251
|
const first = createRuntime({ dataDir });
|
|
252
252
|
first.runtime.start();
|
|
253
253
|
first.openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
254
|
-
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted' }));
|
|
254
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted', contextPreview: 'Saved context preview' }));
|
|
255
255
|
await first.runtime.flush();
|
|
256
256
|
first.runtime.stop();
|
|
257
257
|
|
|
@@ -259,6 +259,7 @@ describe('message queue runtime', () => {
|
|
|
259
259
|
second.runtime.start();
|
|
260
260
|
await second.runtime.load();
|
|
261
261
|
expect(second.runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['persisted']);
|
|
262
|
+
expect(second.runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('Saved context preview');
|
|
262
263
|
second.connect();
|
|
263
264
|
await settle();
|
|
264
265
|
expect(second.openCode.state.sent).toHaveLength(1);
|
|
@@ -315,6 +316,35 @@ describe('message queue runtime', () => {
|
|
|
315
316
|
expect(runtime.snapshot().sessions).toEqual([]);
|
|
316
317
|
});
|
|
317
318
|
|
|
319
|
+
it('retains a bounded context preview in snapshots and broadcasts without exposing the full payload', async () => {
|
|
320
|
+
const { runtime, broadcasts } = createRuntime();
|
|
321
|
+
runtime.start();
|
|
322
|
+
const context = [{ kind: 'context', text: 'Full quoted content', metadata: { aiworkerContext: { kind: 'chat-quote', quote: 'Original answer', text: 'Explain this' } } }];
|
|
323
|
+
const { itemId } = await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context, contextPreview: 'Explain this' }));
|
|
324
|
+
const projected = runtime.sessionSnapshot(SESSION).items[0];
|
|
325
|
+
expect(projected.contextPreview).toBe('Explain this');
|
|
326
|
+
expect(projected.content).toBe('');
|
|
327
|
+
expect(projected.text).toBe('');
|
|
328
|
+
expect(projected).not.toHaveProperty('context');
|
|
329
|
+
expect(broadcasts.at(-1).properties.session.items[0].contextPreview).toBe('Explain this');
|
|
330
|
+
const taken = await runtime.take(SESSION, itemId);
|
|
331
|
+
expect(taken.item.context).toEqual(context);
|
|
332
|
+
expect(taken.item.content).toBe('');
|
|
333
|
+
|
|
334
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context, contextPreview: 'a'.repeat(5000) }));
|
|
335
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('a'.repeat(100) + '...');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('derives a preview for older queued annotations without a saved summary', async () => {
|
|
339
|
+
const { runtime } = createRuntime();
|
|
340
|
+
runtime.start();
|
|
341
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context: [
|
|
342
|
+
{ kind: 'instruction', text: 'Use the skill' },
|
|
343
|
+
{ kind: 'context', text: 'Model-facing wrapper', metadata: { aiworkerContext: { kind: 'browser-annotation', text: 'Fix the button\nMore detail' } } },
|
|
344
|
+
] }));
|
|
345
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('Fix the button...');
|
|
346
|
+
});
|
|
347
|
+
|
|
318
348
|
it('names the directory in the broadcast that empties a queue', async () => {
|
|
319
349
|
// The UI keys its projection by directory; without it the client cannot
|
|
320
350
|
// tell which queue just delivered its last message and keeps showing it.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
// Node caps each happy-eyeballs connect attempt at 250ms by default
|
|
4
|
+
// (autoSelectFamilyAttemptTimeout). TCP handshakes to provider endpoints that are
|
|
5
|
+
// geographically distant routinely take 300-1500ms, so fetch() from a Node process
|
|
6
|
+
// aborts every attempt (ETIMEDOUT) and surfaces "fetch failed" even though the host
|
|
7
|
+
// is reachable — e.g. the z.ai quota endpoint from an IPv4-only egress (#3399).
|
|
8
|
+
//
|
|
9
|
+
// Every Node process entrypoint that performs provider fetches raises the
|
|
10
|
+
// per-attempt cap. Family autoselection itself stays enabled, so the IPv6→IPv4
|
|
11
|
+
// fallback and ::1/localhost servers keep working; disabling it instead breaks
|
|
12
|
+
// local MCP servers. Runtimes without the setter (Bun's fetch path, older Node)
|
|
13
|
+
// are a no-op.
|
|
14
|
+
export const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
export const applyConnectAttemptTimeout = (netModule = net) => {
|
|
17
|
+
try {
|
|
18
|
+
netModule.setDefaultAutoSelectFamilyAttemptTimeout(CONNECT_ATTEMPT_TIMEOUT_MS);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
import { applyConnectAttemptTimeout, CONNECT_ATTEMPT_TIMEOUT_MS } from './network-defaults.js';
|
|
6
|
+
|
|
7
|
+
describe('applyConnectAttemptTimeout', () => {
|
|
8
|
+
it('initializes the server entrypoint in a fresh Node process', () => {
|
|
9
|
+
const serverUrl = new URL('../index.js', import.meta.url).href;
|
|
10
|
+
const result = spawnSync('node', ['--input-type=module', '--eval', `
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
const family = net.getDefaultAutoSelectFamily();
|
|
14
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(250);
|
|
15
|
+
await import(${JSON.stringify(serverUrl)});
|
|
16
|
+
assert.equal(net.getDefaultAutoSelectFamilyAttemptTimeout(), 5000);
|
|
17
|
+
assert.equal(net.getDefaultAutoSelectFamily(), family);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
`], { encoding: 'utf8', timeout: 15_000, windowsHide: true });
|
|
20
|
+
expect(result.status, result.stderr).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('raises the per-attempt connect timeout on runtimes that expose the setter', () => {
|
|
24
|
+
const previous = net.getDefaultAutoSelectFamilyAttemptTimeout();
|
|
25
|
+
try {
|
|
26
|
+
expect(applyConnectAttemptTimeout()).toBe(true);
|
|
27
|
+
expect(net.getDefaultAutoSelectFamilyAttemptTimeout()).toBe(CONNECT_ATTEMPT_TIMEOUT_MS);
|
|
28
|
+
} finally {
|
|
29
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(previous);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('is a no-op on runtimes without the setter', () => {
|
|
34
|
+
expect(applyConnectAttemptTimeout({})).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('survives a throwing setter', () => {
|
|
38
|
+
const setDefaultAutoSelectFamilyAttemptTimeout = vi.fn(() => {
|
|
39
|
+
throw new Error('not supported');
|
|
40
|
+
});
|
|
41
|
+
expect(applyConnectAttemptTimeout({ setDefaultAutoSelectFamilyAttemptTimeout })).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('leaves family autoselection itself untouched', () => {
|
|
45
|
+
const setDefaultAutoSelectFamily = vi.fn();
|
|
46
|
+
const setDefaultAutoSelectFamilyAttemptTimeout = vi.fn();
|
|
47
|
+
applyConnectAttemptTimeout({ setDefaultAutoSelectFamily, setDefaultAutoSelectFamilyAttemptTimeout });
|
|
48
|
+
expect(setDefaultAutoSelectFamilyAttemptTimeout).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(setDefaultAutoSelectFamilyAttemptTimeout).toHaveBeenCalledWith(CONNECT_ATTEMPT_TIMEOUT_MS);
|
|
50
|
+
expect(setDefaultAutoSelectFamily).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -77,7 +77,7 @@ This module provides notification message preparation utilities for the web serv
|
|
|
77
77
|
### Emitter runtime API (emitter-runtime.js)
|
|
78
78
|
- `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.
|
|
79
79
|
- Returned API:
|
|
80
|
-
|
|
80
|
+
- `writeSseEvent(res, payload, serializedPayload?)`, where broadcast callers may share one JSON encoding across recipients
|
|
81
81
|
- `emitDesktopNotification(payload)`
|
|
82
82
|
- `broadcastUiNotification(payload)`
|
|
83
83
|
|
|
@@ -22,8 +22,8 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
22
22
|
onDesktopNotification = typeof cb === 'function' ? cb : null;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const writeSseEvent = (res, payload) => {
|
|
26
|
-
res.write(`data: ${
|
|
25
|
+
const writeSseEvent = (res, payload, serializedPayload = JSON.stringify(payload)) => {
|
|
26
|
+
res.write(`data: ${serializedPayload}\n\n`);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const emitDesktopNotification = (payload) => {
|
|
@@ -91,9 +91,10 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const serializedPayload = JSON.stringify(syntheticPayload);
|
|
94
95
|
for (const res of clients) {
|
|
95
96
|
try {
|
|
96
|
-
writeSseEvent(res, syntheticPayload);
|
|
97
|
+
writeSseEvent(res, syntheticPayload, serializedPayload);
|
|
97
98
|
} catch {
|
|
98
99
|
// ignore
|
|
99
100
|
}
|
|
@@ -34,8 +34,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
34
34
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
35
35
|
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, loopback authentication, and fixed CLI action dispatch.
|
|
36
36
|
- `packages/web/server/lib/system-prompt/runtime.js`: opt-in managed OpenCode system-prompt optimizer materialization and plugin injection.
|
|
37
|
-
- `packages/web/server/lib/
|
|
38
|
-
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer, MCP reconnect) appends itself through.
|
|
37
|
+
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer) appends itself through.
|
|
39
38
|
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
|
|
40
39
|
- `packages/web/server/lib/opencode/openchamber-routes.js`: AiWorker update and models metadata route registration.
|
|
41
40
|
- `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
|
|
@@ -132,8 +131,12 @@ The runtime maintains active-session count incrementally from idempotent activit
|
|
|
132
131
|
- `killProcessOnPort(port)`
|
|
133
132
|
|
|
134
133
|
Managed OpenCode launch also merges the environment returned by the agent-tool
|
|
135
|
-
runtime
|
|
136
|
-
|
|
134
|
+
runtime and the opt-in system prompt optimizer, each appending its `file://`
|
|
135
|
+
entry to the previous one's config. AiWorker adds no automatic MCP reconnect
|
|
136
|
+
loop; recovery after a failed connection is manual for both local and remote
|
|
137
|
+
servers. Previously generated reconnect plugin files are inert because managed
|
|
138
|
+
launch no longer registers them. User-configured plugins remain user-owned.
|
|
139
|
+
PATH and `OPENCODE_SERVER_PASSWORD` remain lifecycle-owned and cannot
|
|
137
140
|
be replaced by injected values. External OpenCode processes receive no
|
|
138
141
|
AiWorker tool injection. Managed launch env strips AppImage `ARGV0` before
|
|
139
142
|
spawn so zsh-backed OpenCode tools do not rewrite child argv[0] to the AppImage
|
|
@@ -156,6 +159,7 @@ Managed health failures are classified as `timeout`, `connection_refused`, `conn
|
|
|
156
159
|
## Public exports (env-runtime.js)
|
|
157
160
|
- `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state.
|
|
158
161
|
- OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery.
|
|
162
|
+
- Automatic bundled resolution under `AIWORKER_RUNTIME=desktop` stays in runtime state and is returned to the managed launch function, including on OpenCode restart. It does not populate `process.env.OPENCODE_BINARY`: AppImage updater relaunch inherits that environment and would mistake the previous bundle path for an explicit override. Explicit settings/env selections and non-desktop or non-bundled resolution retain their existing environment behavior. This prevents future inheritance; it does not reinterpret overrides already inherited from older releases.
|
|
159
163
|
- Returned API:
|
|
160
164
|
- `applyLoginShellEnvSnapshot()`
|
|
161
165
|
- `getLoginShellEnvSnapshot()`
|
|
@@ -212,7 +216,12 @@ Managed health failures are classified as `timeout`, `connection_refused`, `conn
|
|
|
212
216
|
- `persistSettings(changes)`
|
|
213
217
|
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
|
214
218
|
- Queued follow-up messages live in `<data-dir>/message-queue.json`, not in settings; execution ownership lives in `lib/message-queue/`.
|
|
215
|
-
- Shared sidebar preferences are stored as validated top-level fields: `sidebarProjectDisplayMode`, `sidebarSessionGroupingMode`, `sidebarProjectSortOrder`, and `sidebarShowRecentSection`. Device-local picker selection and sticky-header state do not enter
|
|
219
|
+
- Shared sidebar preferences are stored as validated top-level fields: `sidebarProjectDisplayMode`, `sidebarSessionGroupingMode`, `sidebarProjectSortOrder`, and `sidebarShowRecentSection`. Device-local picker selection and sticky-header state do not enter either settings file.
|
|
220
|
+
- Two files (`settings-files.js`): `settings.json` holds instance facts and any legacy or unknown keys; `preferences.json` beside it holds every key the generated registry snapshot (`settings-registry.json`) marks `profile`, as `{ version: 1, fields: { key: { value, updatedAt, surfaces? } } }`. Keys the snapshot marks `perSurface` are stored per surface kind: `GET`/`PUT /api/config/settings` read the client's kind from the `surface` query parameter (`settingsSurfaceOf`; the legacy `x-openchamber-surface` header is still honoured, but a header forces a CORS preflight that cross-origin shells and older instances refuse, so clients must not send one) (`web`, `desktop`, `vscode`, `mobile`; anything else means base), `persistSettings(changes, { surface })` writes a changed per-surface key under `surfaces[surface]` and never touches its base, and `readSettingsFromDisk({ surface })` resolves that kind's value first, the base otherwise. Callers without a surface (migrations, the seed, server-side feature writers) read and write the base. `readSettingsFromDisk()` returns the merged document and seeds `preferences.json` once from an existing `settings.json` (which it leaves intact). An existing `preferences.json` that fails to parse is a failure, not an empty profile: it is never seeded or overwritten, the merged read serves the instance part, and `persistSettings` drops profile keys with a warning until the file is fixed or removed. `writeSettingsToDisk(document)` splits by scope and writes `settings.json` as the instance part plus a copy of the profile's base values (`legacySettingsDocumentOf`): a build from before the split reads only that file, so a rollback keeps the user's preferences, while current builds ignore the copy because `preferences.json` wins in the merge; device keys are dropped from writes. Modules that read one profile key off the disk on a hot path use `readMergedSettingsSync`.
|
|
221
|
+
|
|
222
|
+
## Public exports (settings-files.js)
|
|
223
|
+
- `parsePreferencesDocument(raw)`, `serializePreferencesDocument(fields)`, `flattenPreferences(fields)`, `buildPreferencesFields(previousFields, document, now)`, `instancePartOf(document)`, `seedPreferencesFrom(document, now)`, `readMergedSettingsSync({ fs, path, settingsFilePath })`, `getSettingsScope(key)`, `isProfileSettingsKey(key)`, `isDeviceSettingsKey(key)`, `preferencesFilePathFor(settingsFilePath, path)`.
|
|
224
|
+
- The VS Code extension host writes the same two files with the same shape (`packages/vscode/src/settings-files.ts`); format changes go to both.
|
|
216
225
|
|
|
217
226
|
## Public exports (settings-helpers.js)
|
|
218
227
|
- `createSettingsHelpers(dependencies)`: creates settings helper runtime for settings request/response shaping.
|
|
@@ -363,9 +372,20 @@ before starting managed OpenCode. The managed custom tool therefore receives
|
|
|
363
372
|
an authoritative loopback callback URL even when AiWorker binds port `0`.
|
|
364
373
|
|
|
365
374
|
## Public exports (openchamber-routes.js)
|
|
375
|
+
Browser completion checks use `appType=web&updateStatus=true` to stay on the
|
|
376
|
+
Desktop Host's native updater. A rejected native restart is retained in the
|
|
377
|
+
server process and returned to these polls as `DESKTOP_UPDATE_RESTART_FAILED`;
|
|
378
|
+
ordinary availability checks remain usable so a browser reload can offer a
|
|
379
|
+
retry. Starting another installation clears the previous restart error.
|
|
380
|
+
The shared UI's `lib/web-update.ts` parses install/check responses and waits
|
|
381
|
+
for the installed native target version, rather than treating absence of a
|
|
382
|
+
newer release as installation success. Poll requests have individual deadlines
|
|
383
|
+
within a ten-minute overall deadline.
|
|
384
|
+
|
|
366
385
|
- `registerAiWorkerRoutes(app, dependencies)`: registers AiWorker endpoints:
|
|
367
386
|
- `GET /api/openchamber/update-check`
|
|
368
387
|
- `POST /api/openchamber/update-install`
|
|
388
|
+
- Desktop-managed hosts delegate authenticated Web update requests to the Electron main process, which checks, downloads, and applies the update through `electron-updater` before restarting the host.
|
|
369
389
|
- Foreground servers running under a systemd user unit queue installation in
|
|
370
390
|
a separate transient unit and restart the configured service afterwards.
|
|
371
391
|
`AIWORKER_SYSTEMD_UNIT` overrides the default `aiworker.service`.
|
|
@@ -406,6 +426,16 @@ an authoritative loopback callback URL even when AiWorker binds port `0`.
|
|
|
406
426
|
- Generic `/api/*` forwarding with hop-by-hop header filtering
|
|
407
427
|
- Windows `/session` merge fallback path behavior
|
|
408
428
|
- OpenCode readiness gate for proxied `/api` requests
|
|
429
|
+
- Worktree checkout gate before directory-scoped upstream reads and writes
|
|
430
|
+
|
|
431
|
+
Git bootstrap must reach `git-ready` before OpenCode can cache a new worktree's
|
|
432
|
+
project identity or config. Setup scripts may still be running; the optional UI
|
|
433
|
+
setup wait remains separate. Failed or timed-out checkout returns 503 without
|
|
434
|
+
forwarding. The shared draft creator keeps the project directory selected until
|
|
435
|
+
creation returns, because preview paths have no bootstrap state.
|
|
436
|
+
|
|
437
|
+
This server gate covers web, Electron, hosted mobile, and Capacitor connections.
|
|
438
|
+
The VS Code extension owns its separate Git and proxy implementation.
|
|
409
439
|
|
|
410
440
|
## Public exports (watcher.js)
|
|
411
441
|
- `createOpenCodeWatcherRuntime(dependencies)`: creates global event watcher runtime backed by the shared upstream SSE reader.
|
|
@@ -63,6 +63,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
63
63
|
getCachedZenModels,
|
|
64
64
|
setAutoAcceptSession,
|
|
65
65
|
agentToolRuntime,
|
|
66
|
+
desktopUpdater,
|
|
66
67
|
} = options;
|
|
67
68
|
|
|
68
69
|
const uiAuthController = createUiAuth({
|
|
@@ -153,6 +154,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
153
154
|
readSettingsFromDiskMigrated,
|
|
154
155
|
fetchFreeZenModels,
|
|
155
156
|
getCachedZenModels,
|
|
157
|
+
desktopUpdater,
|
|
156
158
|
});
|
|
157
159
|
|
|
158
160
|
return {
|
|
@@ -1110,7 +1110,11 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1110
1110
|
return resolved;
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
process.env.
|
|
1113
|
+
// AppImage updates inherit process.env. Keep automatic desktop bundle
|
|
1114
|
+
// selection local so the next app does not treat the old mount as an override.
|
|
1115
|
+
if (process.env.AIWORKER_RUNTIME !== 'desktop' || state.resolvedOpencodeBinarySource !== 'bundled') {
|
|
1116
|
+
process.env.OPENCODE_BINARY = resolved;
|
|
1117
|
+
}
|
|
1114
1118
|
prependToPath(path.dirname(resolved));
|
|
1115
1119
|
ensureOpencodeShimRuntime(resolved);
|
|
1116
1120
|
state.resolvedOpencodeBinary = resolved;
|
|
@@ -14,6 +14,7 @@ const originalResourcesPath = process.resourcesPath;
|
|
|
14
14
|
const originalWslBinary = process.env.WSL_BINARY;
|
|
15
15
|
const originalAiWorkerWslBinary = process.env.AIWORKER_WSL_BINARY;
|
|
16
16
|
const originalPlatform = process.platform;
|
|
17
|
+
const originalRuntime = process.env.AIWORKER_RUNTIME;
|
|
17
18
|
const tempDirs = [];
|
|
18
19
|
const itIf = (condition) => condition ? it : it.skip;
|
|
19
20
|
|
|
@@ -30,6 +31,8 @@ const setPlatform = (platform) => {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
afterEach(() => {
|
|
34
|
+
if (originalRuntime === undefined) delete process.env.AIWORKER_RUNTIME;
|
|
35
|
+
else process.env.AIWORKER_RUNTIME = originalRuntime;
|
|
33
36
|
Object.defineProperty(process, 'platform', {
|
|
34
37
|
value: originalPlatform,
|
|
35
38
|
});
|
|
@@ -117,7 +120,76 @@ const createRuntime = (settings, options = {}) => {
|
|
|
117
120
|
return { runtime, state };
|
|
118
121
|
};
|
|
119
122
|
|
|
123
|
+
const createBundledCli = () => {
|
|
124
|
+
const resourcesPath = createTempDir('openchamber-resources-');
|
|
125
|
+
const directory = path.join(resourcesPath, 'opencode-cli');
|
|
126
|
+
fs.mkdirSync(directory);
|
|
127
|
+
const binary = path.join(directory, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
128
|
+
fs.writeFileSync(binary, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
|
129
|
+
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: resourcesPath });
|
|
130
|
+
return binary;
|
|
131
|
+
};
|
|
132
|
+
|
|
120
133
|
describe('OpenCode env runtime', () => {
|
|
134
|
+
it.each(['linux', 'darwin', 'win32'])('keeps automatic desktop bundled resolution local across relaunch on %s', (platform) => {
|
|
135
|
+
setPlatform(platform);
|
|
136
|
+
process.env.AIWORKER_RUNTIME = 'desktop';
|
|
137
|
+
delete process.env.OPENCODE_BINARY;
|
|
138
|
+
delete process.env.AIWORKER_BUNDLED_OPENCODE_CLI_DIR;
|
|
139
|
+
|
|
140
|
+
const oldBinary = createBundledCli();
|
|
141
|
+
const oldRuntime = createRuntime({});
|
|
142
|
+
expect(oldRuntime.runtime.ensureOpencodeCliEnv()).toBe(oldBinary);
|
|
143
|
+
expect(oldRuntime.runtime.ensureOpencodeCliEnv()).toBe(oldBinary);
|
|
144
|
+
expect(oldRuntime.state.resolvedOpencodeBinarySource).toBe('bundled');
|
|
145
|
+
expect(process.env.OPENCODE_BINARY).toBeUndefined();
|
|
146
|
+
expect(process.env.PATH.split(path.delimiter)[0]).toBe(path.dirname(oldBinary));
|
|
147
|
+
|
|
148
|
+
// The old binary remains executable and its directory is still in PATH.
|
|
149
|
+
const newBinary = createBundledCli();
|
|
150
|
+
const newRuntime = createRuntime({});
|
|
151
|
+
expect(newRuntime.runtime.ensureOpencodeCliEnv()).toBe(newBinary);
|
|
152
|
+
expect(newRuntime.state.resolvedOpencodeBinarySource).toBe('bundled');
|
|
153
|
+
expect(newRuntime.runtime.isBundledOpenCodeCliPath(newBinary)).toBe(true);
|
|
154
|
+
expect(process.env.OPENCODE_BINARY).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it.each(['web', 'ssh-remote'])('preserves automatic binary export outside desktop in %s', (runtimeName) => {
|
|
158
|
+
process.env.AIWORKER_RUNTIME = runtimeName;
|
|
159
|
+
delete process.env.OPENCODE_BINARY;
|
|
160
|
+
delete process.env.AIWORKER_BUNDLED_OPENCODE_CLI_DIR;
|
|
161
|
+
const binary = createBundledCli();
|
|
162
|
+
const { runtime } = createRuntime({});
|
|
163
|
+
expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
|
|
164
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it.each(['env', 'settings'])('preserves explicit desktop %s selection even when it points at a bundled CLI', async (source) => {
|
|
168
|
+
process.env.AIWORKER_RUNTIME = 'desktop';
|
|
169
|
+
delete process.env.OPENCODE_BINARY;
|
|
170
|
+
delete process.env.AIWORKER_BUNDLED_OPENCODE_CLI_DIR;
|
|
171
|
+
const binary = createBundledCli();
|
|
172
|
+
if (source === 'env') process.env.OPENCODE_BINARY = binary;
|
|
173
|
+
const { runtime, state } = createRuntime(source === 'settings' ? { opencodeBinary: binary } : {});
|
|
174
|
+
await runtime.applyOpencodeBinaryFromSettings({ strict: true });
|
|
175
|
+
expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
|
|
176
|
+
expect(state.resolvedOpencodeBinarySource).toBe(source);
|
|
177
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('preserves automatic PATH binary export for desktop without a bundled CLI', () => {
|
|
181
|
+
process.env.AIWORKER_RUNTIME = 'desktop';
|
|
182
|
+
delete process.env.OPENCODE_BINARY;
|
|
183
|
+
delete process.env.AIWORKER_BUNDLED_OPENCODE_CLI_DIR;
|
|
184
|
+
const binary = createBundledCli();
|
|
185
|
+
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: undefined });
|
|
186
|
+
process.env.PATH = path.dirname(binary);
|
|
187
|
+
const { runtime, state } = createRuntime({});
|
|
188
|
+
expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
|
|
189
|
+
expect(state.resolvedOpencodeBinarySource).toBe('path');
|
|
190
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
191
|
+
});
|
|
192
|
+
|
|
121
193
|
it('searches an explicit PATH without mutating the process environment', () => {
|
|
122
194
|
const defaultDir = createTempDir('openchamber-default-path-');
|
|
123
195
|
const explicitDir = createTempDir('openchamber-explicit-path-');
|
|
@@ -10,6 +10,7 @@ import { registerDevServerRoutes } from '../dev-servers/routes.js';
|
|
|
10
10
|
import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
|
|
11
11
|
import { registerSessionFoldersRoutes } from '../session-folders/routes.js';
|
|
12
12
|
import { registerProjectContextRoutes } from '../project-context/routes.js';
|
|
13
|
+
import { registerProjectSetupRoutes } from '../projects/routes.js';
|
|
13
14
|
import { registerAgentMemoryRoutes } from '../agent-memory/routes.js';
|
|
14
15
|
import { registerSessionKnowledgeRoutes } from '../session-knowledge/routes.js';
|
|
15
16
|
import { registerPermissionAutoAcceptRoutes } from '../permission-auto-accept/runtime.js';
|
|
@@ -306,7 +307,21 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
306
307
|
registerSessionGoalRoutes(app);
|
|
307
308
|
registerGitHubRoutes(app);
|
|
308
309
|
registerLinearRoutes(app);
|
|
309
|
-
registerGitRoutes(app
|
|
310
|
+
registerGitRoutes(app, {
|
|
311
|
+
emitWorktreeChanged: ({ directories, at }) => {
|
|
312
|
+
const clients = getAiWorkerEventClients();
|
|
313
|
+
for (const client of clients) {
|
|
314
|
+
try {
|
|
315
|
+
writeSseEvent(client, {
|
|
316
|
+
type: 'aiworker:worktree-changed',
|
|
317
|
+
properties: { directories, at },
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
clients.delete(client);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
});
|
|
310
325
|
registerDevServerRoutes(app, { scanner: devServerScanner, getOwnPorts });
|
|
311
326
|
registerMagicPromptRoutes(app, {
|
|
312
327
|
fsPromises,
|
|
@@ -314,6 +329,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
314
329
|
aiworkerDataDir,
|
|
315
330
|
});
|
|
316
331
|
registerProjectContextRoutes(app, { projectContextRuntime });
|
|
332
|
+
registerProjectSetupRoutes(app, { projectConfigRuntime });
|
|
317
333
|
registerAgentMemoryRoutes(app, { agentMemoryRuntime, isAgentMemoryEnabled });
|
|
318
334
|
registerSessionKnowledgeRoutes(app, { sessionKnowledgeRuntime });
|
|
319
335
|
|
|
@@ -377,8 +377,8 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
377
377
|
return parts.length > 0 ? parts.join('\n\n') : 'No stdout/stderr captured';
|
|
378
378
|
};
|
|
379
379
|
|
|
380
|
-
const createManagedOpenCodeServerProcess = async ({ hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
|
|
381
|
-
let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
|
|
380
|
+
const createManagedOpenCodeServerProcess = async ({ resolvedBinary, hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
|
|
381
|
+
let binary = (resolvedBinary || process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
|
|
382
382
|
const sourceBinary = binary;
|
|
383
383
|
let args = ['serve', '--hostname', hostname, '--port', String(port)];
|
|
384
384
|
let launchWrapperType = null;
|
|
@@ -694,7 +694,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
694
694
|
);
|
|
695
695
|
|
|
696
696
|
await applyOpencodeBinaryFromSettings({ strict: true });
|
|
697
|
-
ensureOpencodeCliEnv();
|
|
697
|
+
const resolvedBinary = ensureOpencodeCliEnv();
|
|
698
698
|
recordStartupPerformance('opencode.binary.ready', {
|
|
699
699
|
attempt,
|
|
700
700
|
durationMs: performance.now() - phaseStartedAt,
|
|
@@ -721,6 +721,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
721
721
|
|
|
722
722
|
try {
|
|
723
723
|
const serverInstance = await createManagedOpenCodeServerProcess({
|
|
724
|
+
resolvedBinary,
|
|
724
725
|
hostname: env.ENV_CONFIGURED_OPENCODE_HOSTNAME,
|
|
725
726
|
port: spawnPort,
|
|
726
727
|
timeout: 30000,
|
|
@@ -126,6 +126,35 @@ const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) =
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
describe('OpenCode lifecycle', () => {
|
|
129
|
+
it('uses the resolved binary directly on startup and managed restart without an env override', async () => {
|
|
130
|
+
delete process.env.OPENCODE_BINARY;
|
|
131
|
+
const binaries = ['/bundle one/opencode-cli/opencode', '/bundle two/opencode-cli/opencode'];
|
|
132
|
+
const ensureOpencodeCliEnv = vi.fn()
|
|
133
|
+
.mockReturnValueOnce(binaries[0])
|
|
134
|
+
.mockReturnValueOnce(binaries[1]);
|
|
135
|
+
spawnMock.mockImplementation(() => {
|
|
136
|
+
const child = createMockChild();
|
|
137
|
+
queueMicrotask(() => child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n'));
|
|
138
|
+
return child;
|
|
139
|
+
});
|
|
140
|
+
globalThis.fetch = vi.fn(async () => ({ ok: false }));
|
|
141
|
+
const runtime = createRuntime({ ensureOpencodeCliEnv });
|
|
142
|
+
const firstServer = await runtime.startOpenCode();
|
|
143
|
+
runtime.testState.openCodeProcess = firstServer;
|
|
144
|
+
try {
|
|
145
|
+
await runtime.restartOpenCode();
|
|
146
|
+
expect(firstServer.signalCode).toBe('SIGTERM');
|
|
147
|
+
expect(spawnMock.mock.calls.map(([binary]) => binary)).toEqual(binaries);
|
|
148
|
+
expect(runtime.testState.lastOpenCodeLaunchDiagnostics.sourceBinary).toBe(binaries[1]);
|
|
149
|
+
expect(process.env.OPENCODE_BINARY).toBeUndefined();
|
|
150
|
+
for (const [, , options] of spawnMock.mock.calls) {
|
|
151
|
+
expect(options.env).not.toHaveProperty('OPENCODE_BINARY');
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
await runtime.testState.openCodeProcess.close();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
129
158
|
it('records an authoritative ready terminal event for external startup', async () => {
|
|
130
159
|
globalThis.fetch = vi.fn(async () => ({
|
|
131
160
|
ok: true,
|
|
@@ -8,7 +8,7 @@ const isJsonObject = (value) => value !== null && value !== undefined && Object.
|
|
|
8
8
|
*
|
|
9
9
|
* Existing entries are preserved; an earlier entry for the same URL is dropped
|
|
10
10
|
* so a restart never registers the same plugin twice. Every managed plugin
|
|
11
|
-
* (agent tools, system prompt optimizer
|
|
11
|
+
* (agent tools, system prompt optimizer) goes through this one
|
|
12
12
|
* merge so they compose in any order.
|
|
13
13
|
*
|
|
14
14
|
* @param {string | undefined} rawConfig the current `OPENCODE_CONFIG_CONTENT`, JSONC or unset
|