@aiworker/web 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/cli.js +14 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +19 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +19 -17
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +15 -18
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +91 -30
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/DiffView-DluiRSla.js +0 -4
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
2
|
+
import {
|
|
3
|
+
buildPreferencesFields,
|
|
4
|
+
flattenPreferences,
|
|
5
|
+
instancePartOf,
|
|
6
|
+
legacySettingsDocumentOf,
|
|
7
|
+
profilePartOf,
|
|
8
|
+
isDeviceSettingsKey,
|
|
9
|
+
isProfileSettingsKey,
|
|
10
|
+
normalizeSettingsSurface,
|
|
11
|
+
parsePreferencesDocument,
|
|
12
|
+
preferencesFilePathFor,
|
|
13
|
+
seedPreferencesFrom,
|
|
14
|
+
serializePreferencesDocument,
|
|
15
|
+
} from './settings-files.js';
|
|
2
16
|
|
|
3
17
|
const DEFAULT_NOTIFICATION_TEMPLATES = {
|
|
4
18
|
completion: { title: '{agent_name} is ready', message: '{model_name} completed the task' },
|
|
@@ -48,6 +62,13 @@ export const createSettingsRuntime = (deps) => {
|
|
|
48
62
|
|
|
49
63
|
let persistSettingsLock = Promise.resolve();
|
|
50
64
|
|
|
65
|
+
const PREFERENCES_FILE_PATH = preferencesFilePathFor(SETTINGS_FILE_PATH, path);
|
|
66
|
+
// True while preferences.json exists but cannot be read. Profile writes are
|
|
67
|
+
// refused meanwhile so a corrupt file is never overwritten with a seed or a
|
|
68
|
+
// partial document; clients keep the values they hold.
|
|
69
|
+
let preferencesUnavailable = false;
|
|
70
|
+
let preferencesFailureLogged = false;
|
|
71
|
+
|
|
51
72
|
// Orphan recovery is a one-shot best-effort scan: when orphans can't be
|
|
52
73
|
// matched on first pass they stay on disk and every subsequent settings
|
|
53
74
|
// read would re-scan them. In-process (Electron) this runs in the main
|
|
@@ -472,7 +493,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
472
493
|
}
|
|
473
494
|
};
|
|
474
495
|
|
|
475
|
-
const
|
|
496
|
+
const readInstanceSettingsFromDisk = async () => {
|
|
476
497
|
try {
|
|
477
498
|
const raw = await fsPromises.readFile(SETTINGS_FILE_PATH, 'utf8');
|
|
478
499
|
const parsed = JSON.parse(raw);
|
|
@@ -489,6 +510,67 @@ export const createSettingsRuntime = (deps) => {
|
|
|
489
510
|
}
|
|
490
511
|
};
|
|
491
512
|
|
|
513
|
+
/**
|
|
514
|
+
* `{ status: 'missing' }` when the file does not exist, `{ status: 'ok',
|
|
515
|
+
* fields }` when it parsed, `{ status: 'failed' }` for anything else. Only
|
|
516
|
+
* "missing" may be seeded; "failed" must leave the file alone.
|
|
517
|
+
*/
|
|
518
|
+
const readPreferenceFields = async () => {
|
|
519
|
+
let raw;
|
|
520
|
+
try {
|
|
521
|
+
raw = await fsPromises.readFile(PREFERENCES_FILE_PATH, 'utf8');
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
524
|
+
return { status: 'missing' };
|
|
525
|
+
}
|
|
526
|
+
if (!preferencesFailureLogged) {
|
|
527
|
+
preferencesFailureLogged = true;
|
|
528
|
+
console.warn('Failed to read preferences file:', error);
|
|
529
|
+
}
|
|
530
|
+
return { status: 'failed' };
|
|
531
|
+
}
|
|
532
|
+
const parsed = parsePreferencesDocument(raw);
|
|
533
|
+
if (!parsed.ok) {
|
|
534
|
+
if (!preferencesFailureLogged) {
|
|
535
|
+
preferencesFailureLogged = true;
|
|
536
|
+
console.warn(`Preferences file is unreadable (${parsed.reason}); profile writes are paused until it is fixed or removed.`);
|
|
537
|
+
}
|
|
538
|
+
return { status: 'failed' };
|
|
539
|
+
}
|
|
540
|
+
preferencesFailureLogged = false;
|
|
541
|
+
return { status: 'ok', fields: parsed.fields };
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const writePreferencesToDisk = async (fields) => {
|
|
545
|
+
await writeJsonFileAtomic(PREFERENCES_FILE_PATH, serializePreferencesDocument(fields));
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// The merged document every consumer sees: instance facts from settings.json
|
|
549
|
+
// plus the profile from preferences.json. On the first read of an install
|
|
550
|
+
// that predates the split, the profile keys still sitting in settings.json
|
|
551
|
+
// seed preferences.json. settings.json keeps a copy of the profile's base
|
|
552
|
+
// values on every write too, so an older build (which reads only that file)
|
|
553
|
+
// still finds everything where it used to be.
|
|
554
|
+
const readSettingsFromDisk = async ({ surface = null } = {}) => {
|
|
555
|
+
const instance = await readInstanceSettingsFromDisk();
|
|
556
|
+
const preferences = await readPreferenceFields();
|
|
557
|
+
if (preferences.status === 'failed') {
|
|
558
|
+
preferencesUnavailable = true;
|
|
559
|
+
return instance;
|
|
560
|
+
}
|
|
561
|
+
preferencesUnavailable = false;
|
|
562
|
+
if (preferences.status === 'missing') {
|
|
563
|
+
const seeded = seedPreferencesFrom(instance, Date.now());
|
|
564
|
+
try {
|
|
565
|
+
await writePreferencesToDisk(seeded);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.warn('Failed to seed preferences file:', error);
|
|
568
|
+
}
|
|
569
|
+
return instance;
|
|
570
|
+
}
|
|
571
|
+
return { ...instance, ...flattenPreferences(preferences.fields, normalizeSettingsSurface(surface)) };
|
|
572
|
+
};
|
|
573
|
+
|
|
492
574
|
// Strict variant for callers that REGENERATE persisted identity when a key is
|
|
493
575
|
// absent (relay signing/encryption keys). The lenient reader above maps every
|
|
494
576
|
// failure — corrupt JSON, EACCES, transient I/O — to `{}`, which such callers
|
|
@@ -558,7 +640,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
558
640
|
try {
|
|
559
641
|
const entries = await fsPromises.readdir(directory, { withFileTypes: true });
|
|
560
642
|
const cleanupTasks = entries
|
|
561
|
-
.filter((entry) => entry.isFile() && entry.name.startsWith('settings.json.tmp-'))
|
|
643
|
+
.filter((entry) => entry.isFile() && (entry.name.startsWith('settings.json.tmp-') || entry.name.startsWith('preferences.json.tmp-')))
|
|
562
644
|
.map((entry) => fsPromises.rm(path.join(directory, entry.name), { force: true }).catch(() => {}));
|
|
563
645
|
await Promise.all(cleanupTasks);
|
|
564
646
|
} catch {
|
|
@@ -566,27 +648,54 @@ export const createSettingsRuntime = (deps) => {
|
|
|
566
648
|
}
|
|
567
649
|
};
|
|
568
650
|
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
await fsPromises.mkdir(
|
|
572
|
-
if (process.platform !== 'win32') await fsPromises.chmod(
|
|
573
|
-
// Atomic write: Electron main and ssh-manager read
|
|
651
|
+
const writeJsonFileAtomic = async (filePath, text) => {
|
|
652
|
+
const directory = path.dirname(filePath);
|
|
653
|
+
await fsPromises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
654
|
+
if (process.platform !== 'win32') await fsPromises.chmod(directory, 0o700);
|
|
655
|
+
// Atomic write: Electron main and ssh-manager read these files via plain
|
|
574
656
|
// readFile + JSON.parse and silently coerce parse errors to {}. A
|
|
575
657
|
// partial read during a non-atomic writeFile would make their next
|
|
576
|
-
// read-modify-write wipe the
|
|
577
|
-
const tmp = `${
|
|
658
|
+
// read-modify-write wipe the file.
|
|
659
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
578
660
|
try {
|
|
579
|
-
await fsPromises.writeFile(tmp,
|
|
661
|
+
await fsPromises.writeFile(tmp, text, { encoding: 'utf8', mode: 0o600 });
|
|
580
662
|
if (process.platform !== 'win32') await fsPromises.chmod(tmp, 0o600);
|
|
581
|
-
await replaceFile(tmp,
|
|
582
|
-
if (process.platform !== 'win32') await fsPromises.chmod(
|
|
663
|
+
await replaceFile(tmp, filePath);
|
|
664
|
+
if (process.platform !== 'win32') await fsPromises.chmod(filePath, 0o600);
|
|
583
665
|
} catch (error) {
|
|
584
666
|
await fsPromises.rm(tmp, { force: true }).catch(() => {});
|
|
585
|
-
console.warn(
|
|
667
|
+
console.warn(`Failed to write ${path.basename(filePath)}:`, error);
|
|
586
668
|
throw error;
|
|
587
669
|
}
|
|
588
670
|
};
|
|
589
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Persist a merged document: profile keys go to preferences.json (stamped
|
|
674
|
+
* when their value changed), everything else to settings.json. While
|
|
675
|
+
* preferences.json is unreadable its part is skipped rather than replaced.
|
|
676
|
+
*/
|
|
677
|
+
const writeSettingsToDisk = async (settings, { surface = null, changedKeys = null } = {}) => {
|
|
678
|
+
const current = preferencesUnavailable ? { status: 'failed' } : await readPreferenceFields();
|
|
679
|
+
if (current.status === 'failed') {
|
|
680
|
+
// The profile part is not saved; settings.json keeps whatever legacy
|
|
681
|
+
// profile copy it already holds rather than losing it too.
|
|
682
|
+
preferencesUnavailable = true;
|
|
683
|
+
const onDisk = await readInstanceSettingsFromDisk();
|
|
684
|
+
await writeJsonFileAtomic(SETTINGS_FILE_PATH, JSON.stringify({
|
|
685
|
+
...instancePartOf(settings),
|
|
686
|
+
...profilePartOf(onDisk),
|
|
687
|
+
}, null, 2));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const previousFields = current.status === 'ok' ? current.fields : {};
|
|
691
|
+
const nextFields = buildPreferencesFields(previousFields, settings, Date.now(), {
|
|
692
|
+
surface: normalizeSettingsSurface(surface),
|
|
693
|
+
changedKeys,
|
|
694
|
+
});
|
|
695
|
+
await writeJsonFileAtomic(SETTINGS_FILE_PATH, JSON.stringify(legacySettingsDocumentOf(settings, nextFields), null, 2));
|
|
696
|
+
await writePreferencesToDisk(nextFields);
|
|
697
|
+
};
|
|
698
|
+
|
|
590
699
|
const validateProjectEntries = async (projects) => {
|
|
591
700
|
if (!Array.isArray(projects)) {
|
|
592
701
|
return [];
|
|
@@ -692,8 +801,8 @@ export const createSettingsRuntime = (deps) => {
|
|
|
692
801
|
return { settings, changed: false };
|
|
693
802
|
}
|
|
694
803
|
|
|
695
|
-
const defaultLight = '
|
|
696
|
-
const defaultDark = '
|
|
804
|
+
const defaultLight = 'openchamber-light';
|
|
805
|
+
const defaultDark = 'openchamber-dark';
|
|
697
806
|
|
|
698
807
|
let nextLightThemeId = hasLight ? settings.lightThemeId : undefined;
|
|
699
808
|
let nextDarkThemeId = hasDark ? settings.darkThemeId : undefined;
|
|
@@ -872,7 +981,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
872
981
|
|
|
873
982
|
let hasCleanedOrphanedTempFiles = false;
|
|
874
983
|
|
|
875
|
-
const readSettingsFromDiskMigrated = async () => {
|
|
984
|
+
const readSettingsFromDiskMigrated = async ({ surface = null } = {}) => {
|
|
876
985
|
if (!hasCleanedOrphanedTempFiles) {
|
|
877
986
|
hasCleanedOrphanedTempFiles = true;
|
|
878
987
|
await cleanupOrphanedSettingsTempFiles(path.dirname(SETTINGS_FILE_PATH));
|
|
@@ -889,16 +998,28 @@ export const createSettingsRuntime = (deps) => {
|
|
|
889
998
|
if (migration1.changed || migration2.changed || migration3.changed || migration4.changed || migration5.changed || migration6.changed || migration7.changed || migration8.changed) {
|
|
890
999
|
await writeSettingsToDisk(migration8.settings);
|
|
891
1000
|
}
|
|
892
|
-
|
|
1001
|
+
// Migrations run on the base view; a surface asks for its own resolution
|
|
1002
|
+
// of the per-surface keys on top of the migrated files.
|
|
1003
|
+
return normalizeSettingsSurface(surface) ? readSettingsFromDisk({ surface }) : migration8.settings;
|
|
893
1004
|
};
|
|
894
1005
|
|
|
895
|
-
const persistSettings = async (changes) => {
|
|
1006
|
+
const persistSettings = async (changes, { surface = null } = {}) => {
|
|
896
1007
|
persistSettingsLock = persistSettingsLock.then(async () => {
|
|
897
1008
|
// Log field names only — changes can carry credentials (UI password,
|
|
898
1009
|
// client tokens, tunnel tokens) that must never reach the log file.
|
|
899
1010
|
console.log('[persistSettings] Updating fields:', Object.keys(changes || {}).join(', ') || '(none)');
|
|
900
|
-
const current = await readSettingsFromDisk();
|
|
1011
|
+
const current = await readSettingsFromDisk({ surface });
|
|
901
1012
|
const sanitized = sanitizeSettingsUpdate(changes);
|
|
1013
|
+
for (const key of Object.keys(sanitized)) {
|
|
1014
|
+
// Device state belongs to the install in front of the user, never to
|
|
1015
|
+
// the instance; a client that still sends it is simply ignored.
|
|
1016
|
+
if (isDeviceSettingsKey(key)) {
|
|
1017
|
+
delete sanitized[key];
|
|
1018
|
+
} else if (preferencesUnavailable && isProfileSettingsKey(key)) {
|
|
1019
|
+
console.warn(`[persistSettings] Dropping ${key}: preferences file is unreadable`);
|
|
1020
|
+
delete sanitized[key];
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
902
1023
|
let next = mergePersistedSettings(current, sanitized);
|
|
903
1024
|
|
|
904
1025
|
const normalizedState = normalizeSettingsPaths(next);
|
|
@@ -962,7 +1083,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
962
1083
|
}
|
|
963
1084
|
}
|
|
964
1085
|
|
|
965
|
-
await writeSettingsToDisk(next);
|
|
1086
|
+
await writeSettingsToDisk(next, { surface, changedKeys: Object.keys(sanitized) });
|
|
966
1087
|
return formatSettingsResponse(next);
|
|
967
1088
|
});
|
|
968
1089
|
|
|
@@ -6,7 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
7
7
|
import { createSettingsRuntime } from './settings-runtime.js';
|
|
8
8
|
|
|
9
|
-
const createRuntime = async () => {
|
|
9
|
+
const createRuntime = async ({ mergePersistedSettings = (_current, changes) => changes } = {}) => {
|
|
10
10
|
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
|
|
11
11
|
const settingsFilePath = path.join(tempRoot, 'settings.json');
|
|
12
12
|
const runtime = createSettingsRuntime({
|
|
@@ -16,7 +16,7 @@ const createRuntime = async () => {
|
|
|
16
16
|
SETTINGS_FILE_PATH: settingsFilePath,
|
|
17
17
|
sanitizeProjects: (projects) => Array.isArray(projects) ? projects : [],
|
|
18
18
|
sanitizeSettingsUpdate: (settings) => settings,
|
|
19
|
-
mergePersistedSettings
|
|
19
|
+
mergePersistedSettings,
|
|
20
20
|
normalizeSettingsPaths: (settings) => ({ settings, changed: false }),
|
|
21
21
|
normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [],
|
|
22
22
|
formatSettingsResponse: (settings) => settings,
|
|
@@ -39,8 +39,37 @@ const createRuntime = async () => {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
describe('settings runtime', () => {
|
|
42
|
-
it('
|
|
42
|
+
it('uses AiWorker themes when a new install has no theme preferences', async () => {
|
|
43
|
+
const { runtime, cleanup } = await createRuntime();
|
|
44
|
+
try {
|
|
45
|
+
await expect(runtime.readSettingsFromDiskMigrated()).resolves.toMatchObject({
|
|
46
|
+
lightThemeId: 'openchamber-light',
|
|
47
|
+
darkThemeId: 'openchamber-dark',
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
await cleanup();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('preserves existing theme preferences during theme migration', async () => {
|
|
43
55
|
const { runtime, settingsFilePath, cleanup } = await createRuntime();
|
|
56
|
+
try {
|
|
57
|
+
await fsPromises.writeFile(settingsFilePath, JSON.stringify({
|
|
58
|
+
lightThemeId: 'flexoki-light',
|
|
59
|
+
darkThemeId: 'flexoki-dark',
|
|
60
|
+
}), 'utf8');
|
|
61
|
+
|
|
62
|
+
await expect(runtime.readSettingsFromDiskMigrated()).resolves.toMatchObject({
|
|
63
|
+
lightThemeId: 'flexoki-light',
|
|
64
|
+
darkThemeId: 'flexoki-dark',
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
await cleanup();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('round-trips shared sidebar preferences through preferences.json', async () => {
|
|
72
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
44
73
|
const preferences = {
|
|
45
74
|
sidebarProjectDisplayMode: 'single',
|
|
46
75
|
sidebarSessionGroupingMode: 'flat',
|
|
@@ -51,7 +80,10 @@ describe('settings runtime', () => {
|
|
|
51
80
|
await runtime.persistSettings(preferences);
|
|
52
81
|
|
|
53
82
|
await expect(runtime.readSettingsFromDisk()).resolves.toEqual(preferences);
|
|
54
|
-
|
|
83
|
+
// Profile keys live in preferences.json; settings.json keeps a legacy copy for older builds.
|
|
84
|
+
expect(JSON.parse(await fsPromises.readFile(settingsFilePath, 'utf8'))).toEqual(preferences);
|
|
85
|
+
const stored = JSON.parse(await fsPromises.readFile(path.join(tempRoot, 'preferences.json'), 'utf8'));
|
|
86
|
+
expect(Object.fromEntries(Object.entries(stored.fields).map(([key, entry]) => [key, entry.value]))).toEqual(preferences);
|
|
55
87
|
} finally {
|
|
56
88
|
await cleanup();
|
|
57
89
|
}
|
|
@@ -219,3 +251,158 @@ describe('settings runtime', () => {
|
|
|
219
251
|
}
|
|
220
252
|
});
|
|
221
253
|
});
|
|
254
|
+
|
|
255
|
+
describe('settings runtime: preferences.json split', () => {
|
|
256
|
+
const readJson = async (filePath) => JSON.parse(await fsPromises.readFile(filePath, 'utf8'));
|
|
257
|
+
|
|
258
|
+
it('seeds preferences.json from the profile keys of an existing settings.json and leaves that file intact', async () => {
|
|
259
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
260
|
+
try {
|
|
261
|
+
const legacy = { projects: [], fontSize: 110, themeId: 'openchamber-dark', desktopLanAccessEnabled: true };
|
|
262
|
+
await fsPromises.writeFile(settingsFilePath, JSON.stringify(legacy));
|
|
263
|
+
|
|
264
|
+
const merged = await runtime.readSettingsFromDisk();
|
|
265
|
+
expect(merged).toMatchObject(legacy);
|
|
266
|
+
|
|
267
|
+
const preferences = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
268
|
+
expect(preferences.version).toBe(1);
|
|
269
|
+
expect(Object.keys(preferences.fields).sort()).toEqual(['fontSize', 'themeId']);
|
|
270
|
+
expect(preferences.fields.fontSize.value).toBe(110);
|
|
271
|
+
expect(typeof preferences.fields.fontSize.updatedAt).toBe('number');
|
|
272
|
+
expect(await readJson(settingsFilePath)).toEqual(legacy);
|
|
273
|
+
} finally {
|
|
274
|
+
await cleanup();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('routes profile keys to preferences.json, keeps a legacy copy of them in settings.json, and drops device keys', async () => {
|
|
279
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
280
|
+
try {
|
|
281
|
+
await runtime.persistSettings({ fontSize: 120, desktopLanAccessEnabled: true, mobileKeyboardMode: 'native' });
|
|
282
|
+
|
|
283
|
+
const settings = await readJson(settingsFilePath);
|
|
284
|
+
expect(settings.desktopLanAccessEnabled).toBe(true);
|
|
285
|
+
// Older builds read only settings.json: the profile's base values stay there as a copy.
|
|
286
|
+
expect(settings.fontSize).toBe(120);
|
|
287
|
+
expect(settings).not.toHaveProperty('mobileKeyboardMode');
|
|
288
|
+
|
|
289
|
+
const preferences = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
290
|
+
expect(preferences.fields.fontSize.value).toBe(120);
|
|
291
|
+
expect(preferences.fields).not.toHaveProperty('mobileKeyboardMode');
|
|
292
|
+
expect(preferences.fields).not.toHaveProperty('desktopLanAccessEnabled');
|
|
293
|
+
|
|
294
|
+
expect(await runtime.readSettingsFromDisk()).toMatchObject({ fontSize: 120, desktopLanAccessEnabled: true });
|
|
295
|
+
} finally {
|
|
296
|
+
await cleanup();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('keeps the timestamp of an unchanged profile key and restamps a changed one', async () => {
|
|
301
|
+
const { runtime, tempRoot, cleanup } = await createRuntime();
|
|
302
|
+
try {
|
|
303
|
+
const preferencesPath = path.join(tempRoot, 'preferences.json');
|
|
304
|
+
await runtime.persistSettings({ fontSize: 100, padding: 100 });
|
|
305
|
+
const first = await readJson(preferencesPath);
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
307
|
+
await runtime.persistSettings({ fontSize: 100, padding: 120 });
|
|
308
|
+
const second = await readJson(preferencesPath);
|
|
309
|
+
|
|
310
|
+
expect(second.fields.fontSize.updatedAt).toBe(first.fields.fontSize.updatedAt);
|
|
311
|
+
expect(second.fields.padding.updatedAt).toBeGreaterThan(first.fields.padding.updatedAt);
|
|
312
|
+
expect(second.fields.padding.value).toBe(120);
|
|
313
|
+
} finally {
|
|
314
|
+
await cleanup();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('treats an unreadable preferences.json as failure: no seed, no overwrite, profile writes refused, instance still served', async () => {
|
|
319
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
320
|
+
try {
|
|
321
|
+
const preferencesPath = path.join(tempRoot, 'preferences.json');
|
|
322
|
+
await fsPromises.writeFile(settingsFilePath, JSON.stringify({ desktopLanAccessEnabled: true }));
|
|
323
|
+
await fsPromises.writeFile(preferencesPath, '{ not json');
|
|
324
|
+
|
|
325
|
+
expect(await runtime.readSettingsFromDisk()).toEqual({ desktopLanAccessEnabled: true });
|
|
326
|
+
|
|
327
|
+
await runtime.persistSettings({ fontSize: 130, desktopKeepAwakeEnabled: true });
|
|
328
|
+
|
|
329
|
+
expect(await fsPromises.readFile(preferencesPath, 'utf8')).toBe('{ not json');
|
|
330
|
+
const settings = await readJson(settingsFilePath);
|
|
331
|
+
expect(settings.desktopKeepAwakeEnabled).toBe(true);
|
|
332
|
+
// The refused profile write must not land in the legacy copy either.
|
|
333
|
+
expect(settings).not.toHaveProperty('fontSize');
|
|
334
|
+
} finally {
|
|
335
|
+
await cleanup();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('settings runtime: per-surface profile keys', () => {
|
|
341
|
+
const readJson = async (filePath) => JSON.parse(await fsPromises.readFile(filePath, 'utf8'));
|
|
342
|
+
// These sequences persist several times; the default stub replaces the
|
|
343
|
+
// document with the changes, the real merge keeps the current document.
|
|
344
|
+
const createMergingRuntime = () => createRuntime({ mergePersistedSettings: (current, changes) => ({ ...current, ...changes }) });
|
|
345
|
+
|
|
346
|
+
it('stores a per-surface key under the writing surface and leaves the base alone', async () => {
|
|
347
|
+
const { runtime, tempRoot, cleanup } = await createMergingRuntime();
|
|
348
|
+
try {
|
|
349
|
+
await runtime.persistSettings({ fontSize: 100 }); // base (no surface): migrations and legacy callers
|
|
350
|
+
await runtime.persistSettings({ fontSize: 130, showReasoningTraces: false }, { surface: 'mobile' });
|
|
351
|
+
|
|
352
|
+
const stored = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
353
|
+
expect(stored.fields.fontSize.value).toBe(100);
|
|
354
|
+
expect(stored.fields.fontSize.surfaces.mobile.value).toBe(130);
|
|
355
|
+
// Not per-surface: written to the base regardless of the surface.
|
|
356
|
+
expect(stored.fields.showReasoningTraces.value).toBe(false);
|
|
357
|
+
expect(stored.fields.showReasoningTraces.surfaces).toBeUndefined();
|
|
358
|
+
|
|
359
|
+
expect((await runtime.readSettingsFromDisk({ surface: 'mobile' })).fontSize).toBe(130);
|
|
360
|
+
expect((await runtime.readSettingsFromDisk({ surface: 'desktop' })).fontSize).toBe(100);
|
|
361
|
+
expect((await runtime.readSettingsFromDisk()).fontSize).toBe(100);
|
|
362
|
+
expect((await runtime.readSettingsFromDiskMigrated({ surface: 'mobile' })).fontSize).toBe(130);
|
|
363
|
+
} finally {
|
|
364
|
+
await cleanup();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('a per-surface key set only from one surface has no base and stays absent elsewhere', async () => {
|
|
369
|
+
const { runtime, tempRoot, cleanup } = await createMergingRuntime();
|
|
370
|
+
try {
|
|
371
|
+
await runtime.persistSettings({ stickyUserHeader: false }, { surface: 'mobile' });
|
|
372
|
+
const stored = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
373
|
+
expect(stored.fields.stickyUserHeader).not.toHaveProperty('value');
|
|
374
|
+
expect(stored.fields.stickyUserHeader.surfaces.mobile.value).toBe(false);
|
|
375
|
+
expect((await runtime.readSettingsFromDisk({ surface: 'desktop' })).stickyUserHeader).toBeUndefined();
|
|
376
|
+
expect((await runtime.readSettingsFromDisk({ surface: 'mobile' })).stickyUserHeader).toBe(false);
|
|
377
|
+
} finally {
|
|
378
|
+
await cleanup();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('a surface write of an unrelated key does not copy the resolved per-surface view into the file', async () => {
|
|
383
|
+
const { runtime, tempRoot, cleanup } = await createMergingRuntime();
|
|
384
|
+
try {
|
|
385
|
+
await runtime.persistSettings({ fontSize: 100 });
|
|
386
|
+
await runtime.persistSettings({ fontSize: 130 }, { surface: 'mobile' });
|
|
387
|
+
await runtime.persistSettings({ showReasoningTraces: true }, { surface: 'mobile' });
|
|
388
|
+
const stored = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
389
|
+
expect(stored.fields.fontSize.value).toBe(100);
|
|
390
|
+
expect(stored.fields.fontSize.surfaces.mobile.value).toBe(130);
|
|
391
|
+
expect(stored.fields.fontSize.surfaces.desktop).toBeUndefined();
|
|
392
|
+
} finally {
|
|
393
|
+
await cleanup();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('ignores an unknown surface header value and writes the base', async () => {
|
|
398
|
+
const { runtime, tempRoot, cleanup } = await createMergingRuntime();
|
|
399
|
+
try {
|
|
400
|
+
await runtime.persistSettings({ fontSize: 90 }, { surface: 'toaster' });
|
|
401
|
+
const stored = await readJson(path.join(tempRoot, 'preferences.json'));
|
|
402
|
+
expect(stored.fields.fontSize.value).toBe(90);
|
|
403
|
+
expect(stored.fields.fontSize.surfaces).toBeUndefined();
|
|
404
|
+
} finally {
|
|
405
|
+
await cleanup();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
});
|
|
@@ -6,7 +6,10 @@ import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
|
|
|
6
6
|
|
|
7
7
|
// ============== PATH CONSTANTS ==============
|
|
8
8
|
|
|
9
|
-
const OPENCODE_CONFIG_DIR = path.join(
|
|
9
|
+
const OPENCODE_CONFIG_DIR = path.join(
|
|
10
|
+
process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'),
|
|
11
|
+
'opencode',
|
|
12
|
+
);
|
|
10
13
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
|
11
14
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
|
12
15
|
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
|
@@ -320,7 +323,7 @@ function writeConfig(config, filePath = CONFIG_FILE) {
|
|
|
320
323
|
parseConfigObject(existing, filePath);
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
const backupFile = `${filePath}.
|
|
326
|
+
const backupFile = `${filePath}.aiworker.backup`;
|
|
324
327
|
fs.copyFileSync(filePath, backupFile);
|
|
325
328
|
console.log(`Created config backup: ${backupFile}`);
|
|
326
329
|
}
|
|
@@ -49,7 +49,7 @@ describe('parseMdFile', () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('parses frontmatter whose closing --- is at end-of-file without a trailing newline', () => {
|
|
52
|
-
// gray-matter (used by OpenCode) accepts this shape;
|
|
52
|
+
// gray-matter (used by OpenCode) accepts this shape; AiWorker must too,
|
|
53
53
|
// otherwise a later save duplicates the YAML block.
|
|
54
54
|
const file = writeFixture('eof-close.md', [
|
|
55
55
|
'---',
|
|
@@ -147,7 +147,7 @@ describe('updateAgent frontmatter preservation', () => {
|
|
|
147
147
|
|
|
148
148
|
it('updates the model in place without duplicating YAML for a file with EOF-closed frontmatter', () => {
|
|
149
149
|
// Repro of OPE-178: the file's closing --- sits at EOF (no trailing
|
|
150
|
-
// newline). OpenCode parses it;
|
|
150
|
+
// newline). OpenCode parses it; AiWorker previously treated the whole
|
|
151
151
|
// file as the prompt body and prepended a second frontmatter block on save.
|
|
152
152
|
const projectDir = path.join(FIXTURE_DIR, 'project');
|
|
153
153
|
const agentPath = path.join(projectDir, '.opencode', 'agents', 'strateg.md');
|
|
@@ -305,7 +305,7 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
305
305
|
/cannot be loaded safely/,
|
|
306
306
|
);
|
|
307
307
|
expect(fs.readFileSync(file, 'utf8')).toBe(PARTIAL_PARSE_CONFIG);
|
|
308
|
-
expect(fs.existsSync(`${file}.
|
|
308
|
+
expect(fs.existsSync(`${file}.aiworker.backup`)).toBe(false);
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
it('preserves a valid config across MCP updates', () => {
|
|
@@ -318,7 +318,7 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
318
318
|
expect(rewritten.plugin).toEqual(['opencode-see-image']);
|
|
319
319
|
expect(rewritten.provider['ollama-cloud'].name).toBe('Ollama Cloud');
|
|
320
320
|
expect(rewritten.mcp.openproject.enabled).toBe(false);
|
|
321
|
-
expect(fs.readFileSync(`${file}.
|
|
321
|
+
expect(fs.readFileSync(`${file}.aiworker.backup`, 'utf8')).toBe(VALID_CONFIG);
|
|
322
322
|
});
|
|
323
323
|
|
|
324
324
|
it('does not wipe an unparseable user config during MCP mutation attempts', () => {
|
|
@@ -331,7 +331,7 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
331
331
|
/cannot be loaded safely/,
|
|
332
332
|
);
|
|
333
333
|
expect(fs.readFileSync(file, 'utf8')).toBe(PARTIAL_PARSE_CONFIG);
|
|
334
|
-
expect(fs.existsSync(`${file}.
|
|
334
|
+
expect(fs.existsSync(`${file}.aiworker.backup`)).toBe(false);
|
|
335
335
|
} finally {
|
|
336
336
|
if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
|
|
337
337
|
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
|
|
@@ -350,7 +350,7 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
350
350
|
/cannot be loaded safely/,
|
|
351
351
|
);
|
|
352
352
|
expect(fs.readFileSync(yamlish, 'utf8')).toBe('mcp:\n openproject:\n type: remote\n');
|
|
353
|
-
expect(fs.existsSync(`${yamlish}.
|
|
353
|
+
expect(fs.existsSync(`${yamlish}.aiworker.backup`)).toBe(false);
|
|
354
354
|
});
|
|
355
355
|
|
|
356
356
|
it('keeps a valid custom layer readable when a project layer is unparseable', () => {
|
|
@@ -377,7 +377,7 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
377
377
|
expect(rewritten.plugin).toEqual(['opencode-see-image']);
|
|
378
378
|
expect(rewritten.mcp.openproject.enabled).toBe(false);
|
|
379
379
|
expect(fs.readFileSync(projectFile, 'utf8')).toBe(PARTIAL_PARSE_CONFIG);
|
|
380
|
-
expect(fs.existsSync(`${projectFile}.
|
|
380
|
+
expect(fs.existsSync(`${projectFile}.aiworker.backup`)).toBe(false);
|
|
381
381
|
} finally {
|
|
382
382
|
if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
|
|
383
383
|
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
|
|
@@ -11,6 +11,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
11
11
|
sessionAssistRuntime,
|
|
12
12
|
sessionGoalRuntime,
|
|
13
13
|
contextObligatoryRuntime,
|
|
14
|
+
messageQueueRuntime,
|
|
14
15
|
scheduledTasksRuntime,
|
|
15
16
|
getHealthCheckInterval,
|
|
16
17
|
clearHealthCheckInterval,
|
|
@@ -47,6 +48,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
47
48
|
sessionAssistRuntime?.stop?.();
|
|
48
49
|
sessionGoalRuntime?.stop?.();
|
|
49
50
|
contextObligatoryRuntime?.stop?.();
|
|
51
|
+
messageQueueRuntime?.stop?.();
|
|
50
52
|
scheduledTasksRuntime?.stop?.();
|
|
51
53
|
|
|
52
54
|
const healthCheckInterval = getHealthCheckInterval();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
2
|
import { buildDeferredRestartResponse } from './config-mutation-response.js';
|
|
3
|
+
import { OPENCODE_CONFIG_DIR } from './shared.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Matches how OpenCode reads its own boolean env flags: any value other than
|
|
@@ -110,7 +111,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
const userRoots = [
|
|
113
|
-
|
|
114
|
+
OPENCODE_CONFIG_DIR,
|
|
114
115
|
path.join(home, '.opencode'),
|
|
115
116
|
path.join(home, '.claude', 'skills'),
|
|
116
117
|
path.join(home, '.agents', 'skills'),
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import yaml from 'yaml';
|
|
4
|
+
import { OPENCODE_CONFIG_DIR } from './shared.js';
|
|
5
5
|
|
|
6
|
-
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
7
6
|
const GLOBAL_SNIPPET_DIR = path.join(OPENCODE_CONFIG_DIR, 'snippet');
|
|
8
7
|
const GLOBAL_SNIPPET_DIR_ALT = path.join(OPENCODE_CONFIG_DIR, 'snippets');
|
|
9
8
|
const SNIPPET_EXTENSION = '.md';
|
|
@@ -21,7 +21,7 @@ const finiteNonNegative = (value) => Number.isFinite(value) && value >= 0 ? valu
|
|
|
21
21
|
const nonNegativeInteger = (value) => Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
22
22
|
|
|
23
23
|
const isStartupPerformanceEnabled = () => (
|
|
24
|
-
ENABLED_VALUES.has(String(process.env.
|
|
24
|
+
ENABLED_VALUES.has(String(process.env.AIWORKER_STARTUP_PERF ?? '').toLowerCase())
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
export const recordStartupPerformance = (phase, details = {}) => {
|
|
@@ -3,16 +3,16 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { recordStartupPerformance } from './startup-performance.js';
|
|
4
4
|
|
|
5
5
|
describe('startup performance diagnostics', () => {
|
|
6
|
-
const previousValue = process.env.
|
|
6
|
+
const previousValue = process.env.AIWORKER_STARTUP_PERF;
|
|
7
7
|
|
|
8
8
|
afterEach(() => {
|
|
9
|
-
if (previousValue === undefined) delete process.env.
|
|
10
|
-
else process.env.
|
|
9
|
+
if (previousValue === undefined) delete process.env.AIWORKER_STARTUP_PERF;
|
|
10
|
+
else process.env.AIWORKER_STARTUP_PERF = previousValue;
|
|
11
11
|
vi.restoreAllMocks();
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('is disabled by default', () => {
|
|
15
|
-
delete process.env.
|
|
15
|
+
delete process.env.AIWORKER_STARTUP_PERF;
|
|
16
16
|
const info = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
17
17
|
|
|
18
18
|
recordStartupPerformance('opencode.health.ready', { durationMs: 5 });
|
|
@@ -21,7 +21,7 @@ describe('startup performance diagnostics', () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('records only approved labels and numeric metadata', () => {
|
|
24
|
-
process.env.
|
|
24
|
+
process.env.AIWORKER_STARTUP_PERF = '1';
|
|
25
25
|
const info = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
26
26
|
|
|
27
27
|
recordStartupPerformance('proxy.readiness-hold', {
|
|
@@ -50,7 +50,7 @@ describe('startup performance diagnostics', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('rejects unknown phases and invalid field values', () => {
|
|
53
|
-
process.env.
|
|
53
|
+
process.env.AIWORKER_STARTUP_PERF = 'true';
|
|
54
54
|
const info = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
55
55
|
|
|
56
56
|
recordStartupPerformance('secret.phase', { durationMs: 1 });
|