@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
package/dist/index.html
CHANGED
|
@@ -509,16 +509,16 @@
|
|
|
509
509
|
pointer-events: none;
|
|
510
510
|
}
|
|
511
511
|
</style>
|
|
512
|
-
<script type="module" crossorigin src="/assets/main-
|
|
512
|
+
<script type="module" crossorigin src="/assets/main-D3ZDQglc.js"></script>
|
|
513
513
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
514
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
|
|
514
515
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
515
516
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
516
517
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
517
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
518
518
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
519
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
520
|
-
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-
|
|
521
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
519
|
+
<link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
|
|
520
|
+
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-BdA6bdky.js">
|
|
521
|
+
<link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
|
|
522
522
|
</head>
|
|
523
523
|
<body class="h-full bg-background text-foreground">
|
|
524
524
|
<div id="root" class="h-full">
|
|
@@ -603,44 +603,6 @@
|
|
|
603
603
|
}, 10000);
|
|
604
604
|
</script>
|
|
605
605
|
|
|
606
|
-
<!-- CSS Font Loading API for reliable Nerd Font loading. The ~2 MB of
|
|
607
|
-
Nerd Fonts are only used by the terminal viewport, so they are no
|
|
608
|
-
longer downloaded on every cold start: the terminal requests them on
|
|
609
|
-
first mount through this idempotent hook. -->
|
|
610
|
-
<script>
|
|
611
|
-
(function() {
|
|
612
|
-
let nerdFontsPromise = null;
|
|
613
|
-
window.__aiworkerEnsureNerdFonts = function() {
|
|
614
|
-
if (nerdFontsPromise) {
|
|
615
|
-
return nerdFontsPromise;
|
|
616
|
-
}
|
|
617
|
-
const fonts = [
|
|
618
|
-
{
|
|
619
|
-
name: 'JetBrainsMono Nerd Font',
|
|
620
|
-
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2'
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
name: 'FiraCode Nerd Font',
|
|
624
|
-
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2'
|
|
625
|
-
}
|
|
626
|
-
];
|
|
627
|
-
|
|
628
|
-
const fontPromises = fonts.map(font => {
|
|
629
|
-
const fontFace = new FontFace(font.name, `url(${font.url}) format('woff2')`);
|
|
630
|
-
document.fonts.add(fontFace);
|
|
631
|
-
return fontFace.load().catch(err => {
|
|
632
|
-
console.warn(`Failed to load font: ${font.name}`, err);
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
nerdFontsPromise = Promise.allSettled(fontPromises).then(() => {
|
|
637
|
-
document.documentElement.classList.add('fonts-loaded');
|
|
638
|
-
});
|
|
639
|
-
return nerdFontsPromise;
|
|
640
|
-
};
|
|
641
|
-
})();
|
|
642
|
-
</script>
|
|
643
|
-
|
|
644
606
|
<!-- Polyfill for process before loading React -->
|
|
645
607
|
<script>
|
|
646
608
|
if (typeof process === 'undefined') {
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-d01k801U.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
79
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
|
|
79
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
80
81
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
81
82
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
85
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
|
|
85
|
+
<link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
|
|
86
86
|
</head>
|
|
87
87
|
<body class="h-full bg-background text-foreground">
|
|
88
88
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,15 +63,15 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-D7VV-Ufz.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
68
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
|
|
68
69
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
69
70
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
70
71
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
71
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
73
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
74
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
73
|
+
<link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
|
|
74
|
+
<link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
|
|
75
75
|
</head>
|
|
76
76
|
<body class="h-full bg-background text-foreground">
|
|
77
77
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiworker/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.1.0",
|
|
27
27
|
"@octokit/rest": "^22.0.1",
|
|
28
|
-
"@opencode-ai/sdk": "1.18.
|
|
28
|
+
"@opencode-ai/sdk": "1.18.30",
|
|
29
29
|
"@simplewebauthn/server": "13.3.1",
|
|
30
30
|
"bun-pty": "^0.4.5",
|
|
31
31
|
"compression": "^1.8.1",
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
"eslint": "^9.33.0",
|
|
78
78
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
79
79
|
"eslint-plugin-react-refresh": "^0.5.0",
|
|
80
|
-
"ghostty-web": "0.4.0",
|
|
81
80
|
"globals": "^16.3.0",
|
|
82
81
|
"next-themes": "^0.4.6",
|
|
83
82
|
"nodemon": "^3.1.7",
|
package/server/index.d.ts
CHANGED
|
@@ -11,12 +11,27 @@ export interface WebUiServerController {
|
|
|
11
11
|
stop: (options?: { exitProcess?: boolean }) => Promise<void>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface DesktopUpdateInfo {
|
|
15
|
+
available: boolean;
|
|
16
|
+
currentVersion?: string;
|
|
17
|
+
version?: string | null;
|
|
18
|
+
body?: string | null;
|
|
19
|
+
date?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DesktopUpdater {
|
|
23
|
+
check: () => Promise<DesktopUpdateInfo>;
|
|
24
|
+
install: () => Promise<DesktopUpdateInfo>;
|
|
25
|
+
restart: () => Promise<void> | void;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
export interface StartWebUiServerOptions {
|
|
15
29
|
port?: number;
|
|
16
30
|
host?: string;
|
|
17
31
|
attachSignals?: boolean;
|
|
18
32
|
exitOnShutdown?: boolean;
|
|
19
33
|
uiPassword?: string | null;
|
|
34
|
+
desktopUpdater?: DesktopUpdater;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export declare function startWebUiServer(
|
package/server/index.js
CHANGED
|
@@ -94,6 +94,7 @@ import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/
|
|
|
94
94
|
import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
|
|
95
95
|
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
|
|
96
96
|
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
|
|
97
|
+
import { migrateLegacyUserDirs } from './lib/data-dir-migration.js';
|
|
97
98
|
import { createProjectContextRuntime } from './lib/project-context/runtime.js';
|
|
98
99
|
import { createAgentMemoryRuntime } from './lib/agent-memory/runtime.js';
|
|
99
100
|
import { createAgentMemoryActions } from './lib/agent-memory/actions.js';
|
|
@@ -111,12 +112,15 @@ import { createDevServerScanner } from './lib/dev-servers/routes.js';
|
|
|
111
112
|
import { createDevTunnelRuntime } from './lib/dev-tunnel/runtime.js';
|
|
112
113
|
import { registerBrowserControlRoutes } from './lib/browser-control/routes.js';
|
|
113
114
|
import { createSystemPromptRuntime } from './lib/system-prompt/runtime.js';
|
|
114
|
-
import { createMcpReconnectRuntime } from './lib/mcp-reconnect/runtime.js';
|
|
115
115
|
import { createAiWorkerSessionService } from './lib/openchamber-sessions/routes.js';
|
|
116
116
|
import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
|
|
117
117
|
import { createAiWorkerControlService } from './lib/openchamber-control/service.js';
|
|
118
118
|
import { AiWorkerControlError } from './lib/openchamber-control/error.js';
|
|
119
119
|
import webPush from 'web-push';
|
|
120
|
+
import { applyConnectAttemptTimeout } from './lib/network-defaults.js';
|
|
121
|
+
|
|
122
|
+
// Background CLI launches enter here in a fresh process, without CLI defaults.
|
|
123
|
+
applyConnectAttemptTimeout();
|
|
120
124
|
|
|
121
125
|
const __filename = fileURLToPath(import.meta.url);
|
|
122
126
|
const __dirname = path.dirname(__filename);
|
|
@@ -259,14 +263,20 @@ const normalizeManagedRemoteTunnelPresets = (...args) =>
|
|
|
259
263
|
const normalizeManagedRemoteTunnelPresetTokens = (...args) =>
|
|
260
264
|
settingsNormalizationRuntime.normalizeManagedRemoteTunnelPresetTokens(...args);
|
|
261
265
|
const isUnsafeSkillRelativePath = (...args) => settingsNormalizationRuntime.isUnsafeSkillRelativePath(...args);
|
|
262
|
-
const sanitizeTypographySizesPartial = (...args) =>
|
|
263
|
-
settingsNormalizationRuntime.sanitizeTypographySizesPartial(...args);
|
|
264
266
|
const normalizeStringArray = (...args) => settingsNormalizationRuntime.normalizeStringArray(...args);
|
|
265
267
|
const sanitizeModelRefs = (...args) => settingsNormalizationRuntime.sanitizeModelRefs(...args);
|
|
266
268
|
const sanitizeSkillCatalogs = (...args) => settingsNormalizationRuntime.sanitizeSkillCatalogs(...args);
|
|
267
269
|
const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProjects(...args);
|
|
268
270
|
|
|
269
|
-
|
|
271
|
+
// Every AiWorker-owned file and folder hangs off one root: the default
|
|
272
|
+
// `~/.config/openchamber`, or `AIWORKER_DATA_DIR` when set. The user
|
|
273
|
+
// folders (`projects/`, `themes/`, `speech-models/`) are copied into a custom
|
|
274
|
+
// root once at startup (`migrateLegacyUserDirs`), because they used to ignore
|
|
275
|
+
// the variable.
|
|
276
|
+
const AIWORKER_DEFAULT_CONFIG_ROOT = path.join(os.homedir(), '.config', 'aiworker');
|
|
277
|
+
const AIWORKER_USER_CONFIG_ROOT = process.env.AIWORKER_DATA_DIR
|
|
278
|
+
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
279
|
+
: AIWORKER_DEFAULT_CONFIG_ROOT;
|
|
270
280
|
const AIWORKER_USER_THEMES_DIR = path.join(AIWORKER_USER_CONFIG_ROOT, 'themes');
|
|
271
281
|
const AIWORKER_PROJECTS_CONFIG_DIR = path.join(AIWORKER_USER_CONFIG_ROOT, 'projects');
|
|
272
282
|
// AIWORKER_CHATS_DIR relocates managed chat worktrees — needed when the
|
|
@@ -291,7 +301,6 @@ const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromD
|
|
|
291
301
|
let notificationTemplateRuntime = null;
|
|
292
302
|
let agentToolRuntime = null;
|
|
293
303
|
let systemPromptRuntime = null;
|
|
294
|
-
let mcpReconnectRuntime = null;
|
|
295
304
|
|
|
296
305
|
const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
|
|
297
306
|
const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
|
|
@@ -305,9 +314,7 @@ const maybeCacheSessionInfoFromEvent = (...args) => notificationTemplateRuntime.
|
|
|
305
314
|
const buildTemplateVariables = (...args) => notificationTemplateRuntime.buildTemplateVariables(...args);
|
|
306
315
|
const getCachedZenModels = (...args) => notificationTemplateRuntime.getCachedZenModels(...args);
|
|
307
316
|
|
|
308
|
-
const AIWORKER_DATA_DIR =
|
|
309
|
-
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
310
|
-
: path.join(os.homedir(), '.config', 'aiworker');
|
|
317
|
+
const AIWORKER_DATA_DIR = AIWORKER_USER_CONFIG_ROOT;
|
|
311
318
|
const SETTINGS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'settings.json');
|
|
312
319
|
const PUSH_SUBSCRIPTIONS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'push-subscriptions.json');
|
|
313
320
|
const APNS_TOKENS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'apns-tokens.json');
|
|
@@ -345,7 +352,6 @@ const settingsHelpers = createSettingsHelpers({
|
|
|
345
352
|
normalizeManagedRemoteTunnelHostname,
|
|
346
353
|
normalizeManagedRemoteTunnelPresets,
|
|
347
354
|
normalizeManagedRemoteTunnelPresetTokens,
|
|
348
|
-
sanitizeTypographySizesPartial,
|
|
349
355
|
normalizeStringArray,
|
|
350
356
|
sanitizeModelRefs,
|
|
351
357
|
sanitizeSkillCatalogs,
|
|
@@ -484,6 +490,17 @@ const getUpstreamStallTimeoutMs = () => (
|
|
|
484
490
|
: DEFAULT_UPSTREAM_STALL_TIMEOUT_MS
|
|
485
491
|
);
|
|
486
492
|
|
|
493
|
+
const movedUserDirs = await migrateLegacyUserDirs({
|
|
494
|
+
fsPromises,
|
|
495
|
+
path,
|
|
496
|
+
dataDir: AIWORKER_USER_CONFIG_ROOT,
|
|
497
|
+
legacyRoot: AIWORKER_DEFAULT_CONFIG_ROOT,
|
|
498
|
+
warn: (message) => console.warn(`[data-dir] ${message}`),
|
|
499
|
+
});
|
|
500
|
+
if (movedUserDirs.length > 0) {
|
|
501
|
+
console.log(`[data-dir] Copied ${movedUserDirs.join(', ')} into ${AIWORKER_USER_CONFIG_ROOT}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
487
504
|
const projectConfigRuntime = createProjectConfigRuntime({
|
|
488
505
|
fsPromises,
|
|
489
506
|
path,
|
|
@@ -494,6 +511,7 @@ const projectContextRuntime = createProjectContextRuntime({
|
|
|
494
511
|
fsPromises,
|
|
495
512
|
path,
|
|
496
513
|
projectsDirPath: AIWORKER_PROJECTS_CONFIG_DIR,
|
|
514
|
+
resolveSharedPlansDir: (projectId) => projectConfigRuntime.resolveSharedPlansDir(projectId),
|
|
497
515
|
});
|
|
498
516
|
|
|
499
517
|
const agentMemoryRuntime = createAgentMemoryRuntime({
|
|
@@ -893,7 +911,13 @@ const messageQueueRuntime = createMessageQueueRuntime({
|
|
|
893
911
|
buildOpenCodeUrl,
|
|
894
912
|
getOpenCodeAuthHeaders,
|
|
895
913
|
sessionKnowledgeRuntime,
|
|
896
|
-
|
|
914
|
+
// OpenCode's /global/event SSE proxy cannot carry AiWorker-owned events.
|
|
915
|
+
// Use the shared control stream for SSE clients and the existing WS fan-out.
|
|
916
|
+
broadcastGlobalUiEvent: createGlobalUiEventBroadcaster({
|
|
917
|
+
sseClients: uiAiWorkerEventClients,
|
|
918
|
+
wsClients: uiNotificationWsClients,
|
|
919
|
+
writeSseEvent,
|
|
920
|
+
}),
|
|
897
921
|
onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
|
|
898
922
|
dataDir: AIWORKER_DATA_DIR,
|
|
899
923
|
});
|
|
@@ -1230,14 +1254,11 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
|
|
|
1230
1254
|
? await (agentToolRuntime?.prepareManagedOpenCodeEnv({ includeControl, includeWeb, includeMemory }) || {})
|
|
1231
1255
|
: {};
|
|
1232
1256
|
|
|
1233
|
-
// Each managed plugin appends itself to the config the previous one produced.
|
|
1234
|
-
let configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
|
|
1235
1257
|
if (settings?.optimizeSystemPrompt === true) {
|
|
1236
|
-
|
|
1258
|
+
const configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
|
|
1259
|
+
Object.assign(managedEnv, await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent));
|
|
1237
1260
|
}
|
|
1238
|
-
|
|
1239
|
-
const mcpReconnectEnv = await mcpReconnectRuntime.prepareManagedOpenCodeEnv(configContent);
|
|
1240
|
-
return { ...managedEnv, ...mcpReconnectEnv };
|
|
1261
|
+
return managedEnv;
|
|
1241
1262
|
},
|
|
1242
1263
|
});
|
|
1243
1264
|
|
|
@@ -1511,11 +1532,6 @@ async function main(options = {}) {
|
|
|
1511
1532
|
path,
|
|
1512
1533
|
dataDir: AIWORKER_DATA_DIR,
|
|
1513
1534
|
});
|
|
1514
|
-
mcpReconnectRuntime = createMcpReconnectRuntime({
|
|
1515
|
-
fsPromises,
|
|
1516
|
-
path,
|
|
1517
|
-
dataDir: AIWORKER_DATA_DIR,
|
|
1518
|
-
});
|
|
1519
1535
|
|
|
1520
1536
|
// Pairing transports advertised to the create-device dialog. LAN reachability is
|
|
1521
1537
|
// derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
|
|
@@ -1642,6 +1658,12 @@ async function main(options = {}) {
|
|
|
1642
1658
|
const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
|
|
1643
1659
|
? options.getDesktopRuntimeConfig
|
|
1644
1660
|
: null;
|
|
1661
|
+
const desktopUpdater = options.desktopUpdater
|
|
1662
|
+
&& typeof options.desktopUpdater.check === 'function'
|
|
1663
|
+
&& typeof options.desktopUpdater.install === 'function'
|
|
1664
|
+
&& typeof options.desktopUpdater.restart === 'function'
|
|
1665
|
+
? options.desktopUpdater
|
|
1666
|
+
: null;
|
|
1645
1667
|
|
|
1646
1668
|
console.log(`Starting AiWorker on port ${port === 0 ? 'auto' : port}`);
|
|
1647
1669
|
|
|
@@ -1676,7 +1698,10 @@ async function main(options = {}) {
|
|
|
1676
1698
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1677
1699
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
1678
1700
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
1679
|
-
|
|
1701
|
+
// The packaged desktop UI (openchamber-ui://) and the dev UI sit on a
|
|
1702
|
+
// different origin, so every custom request header must be listed here or
|
|
1703
|
+
// the browser refuses the request at preflight, before it reaches a route.
|
|
1704
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Requested-With,Cache-Control,X-OpenCode-Directory,X-OpenCode-Directory-Encoding,Ngrok-Skip-Browser-Warning,X-AiWorker-Surface');
|
|
1680
1705
|
res.setHeader('Access-Control-Expose-Headers', 'x-next-cursor');
|
|
1681
1706
|
res.setHeader('Vary', 'Origin');
|
|
1682
1707
|
if (req.method === 'OPTIONS') {
|
|
@@ -1817,6 +1842,7 @@ async function main(options = {}) {
|
|
|
1817
1842
|
getCachedZenModels,
|
|
1818
1843
|
setAutoAcceptSession,
|
|
1819
1844
|
agentToolRuntime,
|
|
1845
|
+
desktopUpdater,
|
|
1820
1846
|
});
|
|
1821
1847
|
uiAuthController = bootstrapResult.uiAuthController;
|
|
1822
1848
|
realtimeProxyRuntime = attachRealtimeProxy({
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* What the agent has learned and chose to keep, in two scopes:
|
|
5
5
|
*
|
|
6
6
|
* - **project** — `<projectsDir>/<projectId>/memory.json`. How this codebase
|
|
7
|
-
* works, what was decided, where things live.
|
|
7
|
+
* works, what was decided, where things live. `<projectId>` is the bounded
|
|
8
|
+
* stem `projectConfigFileStemOf` gives the id (the id itself up to 200
|
|
9
|
+
* characters, `path_sha256_<digest>` beyond), the same folder that holds
|
|
10
|
+
* `project-context`'s `context.json` and sits beside the config file.
|
|
8
11
|
* - **global** — `<userConfigRoot>/memory.json`. Who the user is and how they
|
|
9
12
|
* want to be worked with. It belongs to no project, so it cannot live under
|
|
10
13
|
* one.
|
|
@@ -51,6 +54,7 @@ const PROJECT_MEMORY_MAX_ITEMS = 200;
|
|
|
51
54
|
*/
|
|
52
55
|
const MEMORY_TYPES = new Set(['fact', 'preference', 'reference']);
|
|
53
56
|
|
|
57
|
+
import { projectConfigFileStemOf } from '../projects/project-id.js';
|
|
54
58
|
import { findThreatPattern } from './threat-patterns.js';
|
|
55
59
|
|
|
56
60
|
const PROJECT_ID_PATTERN = /^[a-zA-Z0-9._:-]+$/;
|
|
@@ -211,7 +215,8 @@ export const createAgentMemoryRuntime = (deps) => {
|
|
|
211
215
|
return {
|
|
212
216
|
scope: 'project',
|
|
213
217
|
key: `project:${projectId}`,
|
|
214
|
-
|
|
218
|
+
// Same bounded folder name as `project-context` and the config file.
|
|
219
|
+
filePath: path.join(projectsDirPath, projectConfigFileStemOf(projectId), 'memory.json'),
|
|
215
220
|
};
|
|
216
221
|
}
|
|
217
222
|
throw new Error('scope is required');
|
|
@@ -3,6 +3,7 @@ import fsPromises from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
|
|
6
7
|
import { createAgentMemoryRuntime } from './runtime.js';
|
|
7
8
|
|
|
8
9
|
const PROJECT_ID = 'path_dGVzdA';
|
|
@@ -48,6 +49,27 @@ describe('scope resolution', () => {
|
|
|
48
49
|
await fsPromises.access(projectPath());
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
test('a project id too long for a folder name is stored under the bounded stem', async () => {
|
|
53
|
+
// A checkout nested deep enough that its `path_<base64url>` id is longer
|
|
54
|
+
// than a folder name may be; the store lands under the bounded stem the
|
|
55
|
+
// config file and project context use, instead of failing with ENAMETOOLONG.
|
|
56
|
+
const longProjectId = createProjectIdFromPath(`/private/tmp/claude-501/${'segment-'.repeat(18)}/scratchpad/evidence/demo-repo`);
|
|
57
|
+
const stem = projectConfigFileStemOf(longProjectId);
|
|
58
|
+
expect(longProjectId.length).toBeGreaterThan(240);
|
|
59
|
+
expect(stem.startsWith('path_sha256_')).toBe(true);
|
|
60
|
+
const target = { scope: 'project', projectId: longProjectId };
|
|
61
|
+
|
|
62
|
+
expect((await runtime.read(target)).entries).toEqual([]);
|
|
63
|
+
await runtime.create(target, { title: 'Uses bun', body: 'Tests run with bun test.' });
|
|
64
|
+
|
|
65
|
+
expect((await runtime.read(target)).entries.map((e) => e.title)).toEqual(['Uses bun']);
|
|
66
|
+
expect((await runtime.readAll(longProjectId)).project.map((e) => e.title)).toEqual(['Uses bun']);
|
|
67
|
+
await fsPromises.access(path.join(rootDir, 'config', 'projects', stem, 'memory.json'));
|
|
68
|
+
expect(await fsPromises.readdir(path.join(rootDir, 'config', 'projects'))).toEqual([stem]);
|
|
69
|
+
// A short id keeps naming its folder itself.
|
|
70
|
+
expect(runtime.resolveTarget(PROJECT).filePath).toBe(projectPath());
|
|
71
|
+
});
|
|
72
|
+
|
|
51
73
|
test('rejects an unknown scope', async () => {
|
|
52
74
|
await expect(runtime.read({ scope: 'nope' })).rejects.toThrow('scope is required');
|
|
53
75
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// One-time copy of the user-authored folders into a custom data directory.
|
|
2
|
+
//
|
|
3
|
+
// `AIWORKER_DATA_DIR` is documented as "the AiWorker data directory",
|
|
4
|
+
// but for a long time only the flat files (settings, auth, push tokens)
|
|
5
|
+
// followed it while `projects/`, `themes/`, and `speech-models/` stayed under
|
|
6
|
+
// `~/.config/openchamber`. Now every folder hangs off the data directory, so an
|
|
7
|
+
// instance that already ran with a custom directory finds those folders in
|
|
8
|
+
// the old place. This copies each one over once: only when the new location
|
|
9
|
+
// does not exist yet and the old one does. It copies rather than moves
|
|
10
|
+
// because a second instance on the same machine (a custom directory next to
|
|
11
|
+
// the default one) must not strip the default instance of its projects.
|
|
12
|
+
|
|
13
|
+
const USER_DIR_ENTRIES = Object.freeze(['projects', 'themes', 'speech-models']);
|
|
14
|
+
|
|
15
|
+
const exists = async (fsPromises, target) => fsPromises.access(target).then(() => true, () => false);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy the user folders from `legacyRoot` into `dataDir`. Returns the entries
|
|
19
|
+
* copied. A no-op when the two roots are the same directory. A folder whose
|
|
20
|
+
* copy fails is reported through `warn` and its partial copy removed; the
|
|
21
|
+
* server keeps starting, reading the (empty) new location.
|
|
22
|
+
*/
|
|
23
|
+
export const migrateLegacyUserDirs = async ({ fsPromises, path, dataDir, legacyRoot, warn = () => {}, entries = USER_DIR_ENTRIES }) => {
|
|
24
|
+
if (path.resolve(dataDir) === path.resolve(legacyRoot)) return [];
|
|
25
|
+
const moved = [];
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const from = path.join(legacyRoot, entry);
|
|
28
|
+
const to = path.join(dataDir, entry);
|
|
29
|
+
if (await exists(fsPromises, to) || !(await exists(fsPromises, from))) continue;
|
|
30
|
+
try {
|
|
31
|
+
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
32
|
+
await fsPromises.cp(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
33
|
+
moved.push(entry);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await fsPromises.rm(to, { recursive: true, force: true }).catch(() => {});
|
|
36
|
+
warn(`Failed to copy ${from} to ${to}: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return moved;
|
|
40
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fsPromises from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
import { migrateLegacyUserDirs } from './data-dir-migration.js';
|
|
7
|
+
|
|
8
|
+
const setup = async () => {
|
|
9
|
+
const root = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-data-dir-'));
|
|
10
|
+
const legacyRoot = path.join(root, 'legacy');
|
|
11
|
+
const dataDir = path.join(root, 'custom');
|
|
12
|
+
await fsPromises.mkdir(path.join(legacyRoot, 'projects'), { recursive: true });
|
|
13
|
+
await fsPromises.writeFile(path.join(legacyRoot, 'projects', 'p.json'), '{"a":1}');
|
|
14
|
+
await fsPromises.mkdir(path.join(legacyRoot, 'themes'), { recursive: true });
|
|
15
|
+
return { root, legacyRoot, dataDir, cleanup: () => fsPromises.rm(root, { recursive: true, force: true }) };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('migrateLegacyUserDirs', () => {
|
|
19
|
+
it('copies the user folders once into a custom data dir and leaves the originals', async () => {
|
|
20
|
+
const { legacyRoot, dataDir, cleanup } = await setup();
|
|
21
|
+
try {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
const moved = await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot, warn: (message) => warnings.push(message) });
|
|
24
|
+
expect(moved).toEqual(['projects', 'themes']);
|
|
25
|
+
expect(warnings).toEqual([]);
|
|
26
|
+
expect(await fsPromises.readFile(path.join(dataDir, 'projects', 'p.json'), 'utf8')).toBe('{"a":1}');
|
|
27
|
+
// The default instance keeps its own copy: a second instance must not strip it.
|
|
28
|
+
expect(await fsPromises.readFile(path.join(legacyRoot, 'projects', 'p.json'), 'utf8')).toBe('{"a":1}');
|
|
29
|
+
// A second start copies nothing more.
|
|
30
|
+
expect(await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot })).toEqual([]);
|
|
31
|
+
} finally {
|
|
32
|
+
await cleanup();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('never merges into a folder that already exists in the data dir', async () => {
|
|
37
|
+
const { legacyRoot, dataDir, cleanup } = await setup();
|
|
38
|
+
try {
|
|
39
|
+
await fsPromises.mkdir(path.join(dataDir, 'projects'), { recursive: true });
|
|
40
|
+
await fsPromises.writeFile(path.join(dataDir, 'projects', 'q.json'), '{}');
|
|
41
|
+
const moved = await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot });
|
|
42
|
+
expect(moved).toEqual(['themes']);
|
|
43
|
+
expect(await fsPromises.readdir(path.join(dataDir, 'projects'))).toEqual(['q.json']);
|
|
44
|
+
expect(await fsPromises.readdir(path.join(legacyRoot, 'projects'))).toEqual(['p.json']);
|
|
45
|
+
} finally {
|
|
46
|
+
await cleanup();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is a no-op when the data dir is the default root', async () => {
|
|
51
|
+
const { legacyRoot, cleanup } = await setup();
|
|
52
|
+
try {
|
|
53
|
+
expect(await migrateLegacyUserDirs({ fsPromises, path, dataDir: legacyRoot, legacyRoot })).toEqual([]);
|
|
54
|
+
expect(await fsPromises.readdir(path.join(legacyRoot, 'projects'))).toEqual(['p.json']);
|
|
55
|
+
} finally {
|
|
56
|
+
await cleanup();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -24,6 +24,7 @@ This module contains the AiWorker message-stream WebSocket protocol and runtime
|
|
|
24
24
|
- `parseSseEventEnvelope(block)`: parses an SSE block into `{ eventId, directory, payload }`.
|
|
25
25
|
- `sendMessageStreamWsFrame(socket, payload)`: serializes and sends a JSON WS frame.
|
|
26
26
|
- `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`.
|
|
27
|
+
- `serializeMessageStreamWsEvent(payload, options)` and `sendSerializedMessageStreamWsFrame(socket, frame)` separate encoding from per-socket delivery. Delivery retains ready-state and backpressure checks.
|
|
27
28
|
|
|
28
29
|
### Runtime helpers
|
|
29
30
|
- `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay.
|
|
@@ -40,12 +41,14 @@ This module contains the AiWorker message-stream WebSocket protocol and runtime
|
|
|
40
41
|
- AiWorker still fetches OpenCode upstream event streams over SSE.
|
|
41
42
|
- The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
|
|
42
43
|
- The global hub keeps a bounded replay buffer keyed by SSE `eventId` so reconnecting browser clients can receive buffered events after their requested `Last-Event-ID`.
|
|
44
|
+
- Replay retains at most 2,048 events and 8 MiB of UTF-8 wire frames. It stores encoded frames rather than retaining parsed payloads as well. Encoding is shared with live WS fanout. An oversized event still reaches live clients in full, but clears the retained suffix so replay cannot cross its gap. A missing replay cursor returns `null`, distinct from a complete empty tail; the bridge sends `ready` with `replayReset: true` and no partial replay. The client retires its cursor and requests authoritative repair, including during the early-boot reconnect grace period. A stopped hub retains its bounded suffix for clients reconnecting after the last socket closed.
|
|
43
45
|
- Directory WS clients still attach one upstream `/event?directory=...` SSE reader per connection because directory streams are scoped.
|
|
44
46
|
- If an upstream SSE stream stalls after the browser WS is already ready, the reader aborts that upstream fetch and reconnects upstream with `Last-Event-ID`, keeping the browser WS alive when recovery is fast.
|
|
45
47
|
- When the shared global upstream reconnects after it was previously ready, the global WS bridge sends a fresh `ready` frame to already-ready browser clients. The browser treats this as a reconnect edge and can run scoped state repair without requiring the browser WS to close.
|
|
46
48
|
- Health checks are reserved for initial upstream connect failures and explicit upstream-unavailable responses, not for ordinary stall recovery on an already-established stream.
|
|
47
49
|
- Global synthetic events such as `aiworker:session-status`, `aiworker:session-activity`, `aiworker:notification`, and `aiworker:heartbeat` are preserved on the WS path, but heartbeat frames are emitted only while an upstream SSE stream is actively attached.
|
|
48
50
|
- Global UI broadcasts are fan-out capable across both SSE and WS clients.
|
|
51
|
+
- Global UI broadcasts serialize once per wire format, irrespective of client count. The SSE writer accepts an optional pre-serialized payload; isolated callers keep the two-argument contract. Failed or backpressured clients cannot block delivery to healthy clients.
|
|
49
52
|
- The reusable upstream reader centralizes SSE fetch/parsing/reconnect behavior for the WS runtime and OpenCode watcher. Additional event consumers should move to it only with parity tests for their lifecycle and error semantics.
|
|
50
53
|
- Browser transport concerns live in the WS bridge modules; server-side global stream ownership lives in `global-hub.js`.
|
|
51
54
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createUpstreamSseReader } from './upstream-reader.js';
|
|
2
|
+
import { serializeMessageStreamWsEvent } from './protocol.js';
|
|
2
3
|
|
|
3
4
|
// Raised from 512 → 2048 to improve recovery after brief disconnects during
|
|
4
5
|
// long-running agent sessions where many events accumulate quickly.
|
|
5
6
|
const MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT = 2048;
|
|
7
|
+
const MESSAGE_STREAM_GLOBAL_REPLAY_BYTES = 8 * 1024 * 1024;
|
|
6
8
|
|
|
7
9
|
export function createGlobalMessageStreamHub({
|
|
8
10
|
buildOpenCodeUrl,
|
|
@@ -11,10 +13,16 @@ export function createGlobalMessageStreamHub({
|
|
|
11
13
|
upstreamStallTimeoutMs,
|
|
12
14
|
upstreamReconnectDelayMs,
|
|
13
15
|
replayLimit = MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT,
|
|
16
|
+
replayByteLimit = MESSAGE_STREAM_GLOBAL_REPLAY_BYTES,
|
|
14
17
|
}) {
|
|
18
|
+
if (!Number.isSafeInteger(replayLimit) || replayLimit < 0 || !Number.isSafeInteger(replayByteLimit) || replayByteLimit < 0) {
|
|
19
|
+
throw new RangeError('Replay limits must be nonnegative safe integers');
|
|
20
|
+
}
|
|
15
21
|
const eventSubscribers = new Set();
|
|
16
22
|
const statusSubscribers = new Set();
|
|
17
23
|
const replay = [];
|
|
24
|
+
let replayBytes = 0;
|
|
25
|
+
let latestEventId;
|
|
18
26
|
|
|
19
27
|
let controller = null;
|
|
20
28
|
let reader = null;
|
|
@@ -45,11 +53,16 @@ export function createGlobalMessageStreamHub({
|
|
|
45
53
|
const directory =
|
|
46
54
|
typeof envelope?.directory === 'string' && envelope.directory.length > 0 ? envelope.directory : 'global';
|
|
47
55
|
const eventId = typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined;
|
|
56
|
+
let serializedFrame;
|
|
48
57
|
return {
|
|
49
58
|
envelope,
|
|
50
59
|
payload,
|
|
51
60
|
directory,
|
|
52
61
|
eventId,
|
|
62
|
+
serialize() {
|
|
63
|
+
serializedFrame ??= serializeMessageStreamWsEvent(payload, { directory, eventId });
|
|
64
|
+
return serializedFrame;
|
|
65
|
+
},
|
|
53
66
|
};
|
|
54
67
|
};
|
|
55
68
|
|
|
@@ -87,9 +100,20 @@ export function createGlobalMessageStreamHub({
|
|
|
87
100
|
onEvent(event) {
|
|
88
101
|
const normalized = normalizeEvent(event);
|
|
89
102
|
if (normalized.eventId) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
latestEventId = normalized.eventId;
|
|
104
|
+
const serializedFrame = normalized.serialize();
|
|
105
|
+
const bytes = Buffer.byteLength(serializedFrame);
|
|
106
|
+
if (bytes > replayByteLimit) {
|
|
107
|
+
// An oversized live event creates a hole: retain only a contiguous
|
|
108
|
+
// suffix after it, never replay an older prefix across the gap.
|
|
109
|
+
replay.length = 0;
|
|
110
|
+
replayBytes = 0;
|
|
111
|
+
} else {
|
|
112
|
+
replay.push({ eventId: normalized.eventId, serializedFrame, bytes });
|
|
113
|
+
replayBytes += bytes;
|
|
114
|
+
while (replay.length > replayLimit || replayBytes > replayByteLimit) {
|
|
115
|
+
replayBytes -= replay.shift().bytes;
|
|
116
|
+
}
|
|
93
117
|
}
|
|
94
118
|
}
|
|
95
119
|
|
|
@@ -152,7 +176,8 @@ export function createGlobalMessageStreamHub({
|
|
|
152
176
|
}
|
|
153
177
|
|
|
154
178
|
const index = replay.findIndex((entry) => entry.eventId === eventId);
|
|
155
|
-
|
|
179
|
+
if (eventId === latestEventId) return [];
|
|
180
|
+
return index === -1 ? null : replay.slice(index + 1);
|
|
156
181
|
},
|
|
157
182
|
};
|
|
158
183
|
}
|
|
@@ -2,6 +2,28 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
4
4
|
|
|
5
|
+
it('bounds a contiguous replay suffix by UTF-8 bytes and event count', async () => {
|
|
6
|
+
const blocks = Array.from({ length: 8 }, (_, i) => `id: e${i}\ndata: ${JSON.stringify({ type: 'message', properties: { text: '界'.repeat(40) } })}\n\n`);
|
|
7
|
+
const received = [];
|
|
8
|
+
const hub = createGlobalMessageStreamHub({
|
|
9
|
+
buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
|
|
10
|
+
getOpenCodeAuthHeaders: () => ({}), replayLimit: 3, replayByteLimit: 550,
|
|
11
|
+
upstreamReconnectDelayMs: 60_000,
|
|
12
|
+
fetchImpl: async () => createSseResponse({ blocks }),
|
|
13
|
+
});
|
|
14
|
+
hub.subscribeEvent(event => received.push(event.eventId));
|
|
15
|
+
try {
|
|
16
|
+
hub.start();
|
|
17
|
+
await waitForAssertion(() => expect(received).toHaveLength(8));
|
|
18
|
+
expect(hub.replayAfter('e0')).toBeNull();
|
|
19
|
+
expect(hub.replayAfter('e5')).toBeNull();
|
|
20
|
+
const tail = hub.replayAfter('e6');
|
|
21
|
+
expect(tail.map(entry => entry.eventId)).toEqual(['e7']);
|
|
22
|
+
expect(Buffer.byteLength(tail[0].serializedFrame) * 2).toBeLessThanOrEqual(550);
|
|
23
|
+
expect(Buffer.byteLength(tail[0].serializedFrame) * 3).toBeGreaterThan(550);
|
|
24
|
+
} finally { hub.stop(); }
|
|
25
|
+
});
|
|
26
|
+
|
|
5
27
|
function createSseResponse({ blocks = [] } = {}) {
|
|
6
28
|
const encoder = new TextEncoder();
|
|
7
29
|
let index = 0;
|