@vantran-se/openchamber-web 1.23.1 → 1.24.4
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.test.js +29 -6
- package/bin/lib/cli-http.js +5 -1
- package/bin/lib/commands-status.js +5 -0
- package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
- package/dist/assets/DiffView-B2CD9wC8.js +4 -0
- package/dist/assets/FilesView-C4vMFclc.js +13 -0
- package/dist/assets/GitView-Byu2DY7s.js +6 -0
- package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
- package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.js} +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +1 -0
- package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
- package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
- package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
- package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
- package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
- package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
- package/dist/assets/SettingsView-CSinuH-1.js +29 -0
- package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
- package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
- package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
- package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
- package/dist/assets/de-Bro6_zyl.js +10 -0
- package/dist/assets/es-Bb9XQZvd.js +16 -0
- package/dist/assets/fr-Dgfzn-IL.js +14 -0
- package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/index-Dqc6USl3.js +40 -0
- package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
- package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
- package/dist/assets/ja-BYsakGFh.js +16 -0
- package/dist/assets/ko-BSaQz9aN.js +16 -0
- package/dist/assets/main-B62_pjL5.js +2 -0
- package/dist/assets/main-BEdcOI2P.js +896 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-CK48LG0z.js +5 -0
- package/dist/assets/miniChat-LT8Rqofr.js +2 -0
- package/dist/assets/mobile-CqarEynh.js +2 -0
- package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
- package/dist/assets/pl-OIviPdoj.js +16 -0
- package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
- package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
- package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
- package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
- package/dist/assets/tr-CQI-KPre.js +7 -0
- package/dist/assets/uk-CqW7R4DA.js +16 -0
- package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
- package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
- package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
- package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
- package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
- package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
- package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
- package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
- package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
- package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
- package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
- package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
- package/dist/index.html +6 -4
- package/dist/mini-chat.html +6 -4
- package/dist/mobile.html +6 -4
- package/package.json +12 -9
- package/server/built-in-extensions/registry.json +4 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +44 -10
- package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
- package/server/lib/agent-tool/runtime.js +62 -15
- package/server/lib/agent-tool/runtime.test.js +88 -2
- package/server/lib/changelog/update-notes.js +2 -1
- package/server/lib/changelog/update-notes.test.js +19 -0
- package/server/lib/event-stream/DOCUMENTATION.md +23 -0
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +292 -0
- package/server/lib/event-stream/global-hub.js +53 -23
- package/server/lib/event-stream/global-hub.test.js +160 -0
- package/server/lib/event-stream/global-ws-bridge.js +1 -0
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/fs/DOCUMENTATION.md +10 -1
- package/server/lib/fs/git-process.test.js +77 -0
- package/server/lib/fs/routes.js +58 -5
- package/server/lib/fs/routes.test.js +116 -0
- package/server/lib/fs/search.js +2 -1
- package/server/lib/git/DOCUMENTATION.md +20 -5
- package/server/lib/git/routes.js +16 -3
- package/server/lib/git/serial-refresh.js +93 -0
- package/server/lib/git/serial-refresh.test.js +112 -0
- package/server/lib/git/service.js +429 -70
- package/server/lib/git/service.test.js +418 -0
- package/server/lib/github/pr-status.js +5 -5
- package/server/lib/github/pr-status.test.js +1 -1
- package/server/lib/github/routes.js +4 -4
- package/server/lib/guests/DOCUMENTATION.md +68 -0
- package/server/lib/guests/auth-store.js +184 -0
- package/server/lib/guests/auth-store.test.js +112 -0
- package/server/lib/guests/background.test.js +83 -0
- package/server/lib/guests/builtins.js +34 -0
- package/server/lib/guests/builtins.test.js +174 -0
- package/server/lib/guests/catalog.js +477 -0
- package/server/lib/guests/catalog.test.js +390 -0
- package/server/lib/guests/clone.js +297 -0
- package/server/lib/guests/extract-zip.js +136 -0
- package/server/lib/guests/extract-zip.test.js +109 -0
- package/server/lib/guests/files.js +308 -0
- package/server/lib/guests/files.test.js +262 -0
- package/server/lib/guests/grant-scope.js +81 -0
- package/server/lib/guests/grant-scope.test.js +75 -0
- package/server/lib/guests/host-session.js +73 -0
- package/server/lib/guests/host-session.test.js +30 -0
- package/server/lib/guests/html-styles.js +6 -0
- package/server/lib/guests/html-styles.test.js +45 -0
- package/server/lib/guests/html-tokens.js +73 -0
- package/server/lib/guests/html-tokens.test.js +53 -0
- package/server/lib/guests/install.js +406 -0
- package/server/lib/guests/install.test.js +338 -0
- package/server/lib/guests/oauth.js +501 -0
- package/server/lib/guests/oauth.test.js +481 -0
- package/server/lib/guests/persist.js +336 -0
- package/server/lib/guests/persist.test.js +288 -0
- package/server/lib/guests/request.js +112 -0
- package/server/lib/guests/request.test.js +206 -0
- package/server/lib/guests/routes.js +838 -0
- package/server/lib/guests/service.js +538 -0
- package/server/lib/guests/service.test.js +277 -0
- package/server/lib/guests/sockets.js +129 -0
- package/server/lib/guests/sockets.test.js +110 -0
- package/server/lib/guests/ssh-install.test.js +188 -0
- package/server/lib/guests/storage.js +68 -0
- package/server/lib/guests/storage.test.js +59 -0
- package/server/lib/guests/updates.js +294 -0
- package/server/lib/guests/updates.test.js +334 -0
- package/server/lib/guests/upload.js +50 -0
- package/server/lib/guests/upload.test.js +131 -0
- package/server/lib/linear/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/DOCUMENTATION.md +10 -1
- package/server/lib/message-queue/runtime.js +21 -1
- package/server/lib/message-queue/runtime.test.js +52 -5
- package/server/lib/notifications/routes.js +5 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
- package/server/lib/openchamber-sessions/routes.js +62 -29
- package/server/lib/openchamber-sessions/routes.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +55 -1
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/feature-routes-runtime.js +15 -0
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +54 -67
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/session-runtime.js +68 -0
- package/server/lib/opencode/session-runtime.test.js +43 -0
- package/server/lib/opencode/settings-helpers.js +8 -0
- package/server/lib/opencode/settings-helpers.test.js +13 -1
- package/server/lib/opencode/settings-normalization-runtime.js +2 -0
- package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
- package/server/lib/opencode/settings-registry.json +15 -0
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/shutdown-runtime.test.js +24 -0
- package/server/lib/opencode/theme-archive.js +53 -0
- package/server/lib/opencode/theme-catalog.js +171 -0
- package/server/lib/opencode/theme-catalog.test.js +125 -0
- package/server/lib/opencode/theme-import.test.js +144 -0
- package/server/lib/opencode/theme-runtime.js +100 -28
- package/server/lib/opencode/theme-runtime.test.js +15 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
- package/server/lib/permission-auto-accept/runtime.js +13 -0
- package/server/lib/permission-auto-accept/runtime.test.js +37 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +0 -1
- package/server/lib/quota/providers/index.js +0 -7
- package/server/lib/realtime-proxy-terminal.test.js +150 -0
- package/server/lib/realtime-proxy.js +2 -0
- package/server/lib/routing/DOCUMENTATION.md +83 -0
- package/server/lib/routing/defaults.js +98 -0
- package/server/lib/routing/feature-flag.js +11 -0
- package/server/lib/routing/history.js +34 -0
- package/server/lib/routing/jev.js +98 -0
- package/server/lib/routing/routes.http.test.js +100 -0
- package/server/lib/routing/routes.js +81 -0
- package/server/lib/routing/runtime.js +217 -0
- package/server/lib/routing/runtime.test.js +188 -0
- package/server/lib/routing/store.js +249 -0
- package/server/lib/routing/store.test.js +96 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -2
- package/server/lib/scheduled-tasks/runtime.test.js +14 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/small-model/DOCUMENTATION.md +14 -2
- package/server/lib/small-model/call.js +37 -6
- package/server/lib/small-model/call.test.js +182 -0
- package/server/lib/terminal/DOCUMENTATION.md +23 -2
- package/server/lib/terminal/runtime.js +27 -5
- package/server/lib/terminal/runtime.test.js +67 -0
- package/server/lib/terminal/shutdown.js +106 -0
- package/server/lib/terminal/shutdown.test.js +84 -0
- package/server/lib/tts/service.test.js +78 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
- package/server/lib/ui-auth/session-cookie.js +51 -0
- package/server/lib/ui-auth/session-cookie.test.js +46 -0
- package/server/lib/ui-auth/ui-auth.js +56 -17
- package/server/lib/ui-auth/ui-auth.test.js +96 -0
- package/dist/assets/DiffView-CyLmb-wF.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
- package/dist/assets/FilesView-WtJB0y_l.js +0 -13
- package/dist/assets/GitView-DkNrV0BK.js +0 -6
- package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
- package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
- package/dist/assets/PlanView-CAFziKH5.js +0 -5
- package/dist/assets/SettingsView-BRittavZ.js +0 -28
- package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-c7CYLCAW.js +0 -10
- package/dist/assets/es-BKAVPQmQ.js +0 -16
- package/dist/assets/fr-Bj1glqfV.js +0 -14
- package/dist/assets/index-BYFyOu_D.css +0 -1
- package/dist/assets/index-BfwRClDe.js +0 -40
- package/dist/assets/ja-5jwSIbK0.js +0 -16
- package/dist/assets/ko-BaDbg-Eb.js +0 -16
- package/dist/assets/main-CGG2_uHq.js +0 -2
- package/dist/assets/main-CcrSaL4J.js +0 -930
- package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
- package/dist/assets/miniChat-DEaAhP4b.js +0 -2
- package/dist/assets/mobile-BC3rsvv0.js +0 -2
- package/dist/assets/pl-BFKVBDFu.js +0 -16
- package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
- package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
- package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
- package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
- package/dist/assets/tr-DYXPoEDG.js +0 -7
- package/dist/assets/uk-CwAoloR8.js +0 -16
- package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
- package/dist/assets/useGitComparison-CpxioduU.js +0 -1
- package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
- package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
- package/dist/assets/zh-TW-1HNK7shh.js +0 -16
- package/server/lib/quota/providers/crof.js +0 -96
- package/server/lib/quota/providers/crof.test.js +0 -98
- /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
|
@@ -58,7 +58,7 @@ const createOpenCode = () => {
|
|
|
58
58
|
return { state, fetchImpl };
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs } = {}) => {
|
|
61
|
+
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolvePromptBody, now } = {}) => {
|
|
62
62
|
let eventHandler = () => {};
|
|
63
63
|
let statusHandler = () => {};
|
|
64
64
|
const broadcasts = [];
|
|
@@ -79,6 +79,8 @@ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), k
|
|
|
79
79
|
abortHoldMs: 50,
|
|
80
80
|
};
|
|
81
81
|
if (retryDelayMs) options.retryDelayMs = retryDelayMs;
|
|
82
|
+
if (resolvePromptBody) options.resolvePromptBody = resolvePromptBody;
|
|
83
|
+
if (now) options.now = now;
|
|
82
84
|
const runtime = createMessageQueueRuntime(options);
|
|
83
85
|
return {
|
|
84
86
|
runtime,
|
|
@@ -95,6 +97,36 @@ const settle = async (ms = 30) => {
|
|
|
95
97
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
96
98
|
};
|
|
97
99
|
|
|
100
|
+
describe('auto routing', () => {
|
|
101
|
+
it('lets the routing hook rewrite the model of a queued prompt and a queued command', async () => {
|
|
102
|
+
const resolvePromptBody = vi.fn(async (body) => {
|
|
103
|
+
if (body.model?.modelID === 'auto') body.model = { providerID: 'openai', modelID: 'gpt-6-astra' };
|
|
104
|
+
if (body.model === 'openchamber/auto') body.model = 'openai/gpt-6-astra';
|
|
105
|
+
return null;
|
|
106
|
+
});
|
|
107
|
+
const { runtime, openCode, emit } = createRuntime({ resolvePromptBody });
|
|
108
|
+
runtime.start();
|
|
109
|
+
openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
110
|
+
openCode.state.commands = [{ name: 'review', template: 'Review $ARGUMENTS' }];
|
|
111
|
+
const auto = { providerID: 'openchamber', modelID: 'auto', agent: 'build' };
|
|
112
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'plain', text: 'plain', sendConfig: auto }));
|
|
113
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '/review src', text: '/review src', sendConfig: auto }));
|
|
114
|
+
|
|
115
|
+
openCode.state.statuses = {};
|
|
116
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
117
|
+
await settle();
|
|
118
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
119
|
+
await settle();
|
|
120
|
+
|
|
121
|
+
expect(openCode.state.sent.map((entry) => entry.body.model)).toEqual([
|
|
122
|
+
{ providerID: 'openai', modelID: 'gpt-6-astra' },
|
|
123
|
+
'openai/gpt-6-astra',
|
|
124
|
+
]);
|
|
125
|
+
expect(resolvePromptBody).toHaveBeenCalledTimes(2);
|
|
126
|
+
expect(resolvePromptBody.mock.calls[0][1]).toEqual({ sessionId: SESSION, directory: DIRECTORY });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
98
130
|
describe('parseQueuedItemInput', () => {
|
|
99
131
|
it('rejects an item the server could not deliver later', () => {
|
|
100
132
|
expect(() => parseQueuedItemInput({ content: 'x' })).toThrow(TypeError);
|
|
@@ -175,9 +207,10 @@ describe('message queue runtime', () => {
|
|
|
175
207
|
});
|
|
176
208
|
|
|
177
209
|
it('does not send into a running turn even when the status event says idle', async () => {
|
|
178
|
-
const { runtime, openCode, emit } = createRuntime();
|
|
210
|
+
const { runtime, openCode, emit } = createRuntime({ now: () => 10_000 });
|
|
179
211
|
runtime.start();
|
|
180
|
-
|
|
212
|
+
// Live unfinished turn: created after this runtime started.
|
|
213
|
+
openCode.state.tail = [{ info: { role: 'assistant', time: { created: 10_001 } } }];
|
|
181
214
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
182
215
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
183
216
|
await settle();
|
|
@@ -185,10 +218,24 @@ describe('message queue runtime', () => {
|
|
|
185
218
|
|
|
186
219
|
// The reply completes: that alone drains the queue (a missed idle event
|
|
187
220
|
// must not strand it).
|
|
188
|
-
openCode.state.tail = [{ info: { role: 'assistant', time: { created:
|
|
189
|
-
emit({ type: 'message.updated', properties: { info: { role: 'assistant', sessionID: SESSION, time: { created:
|
|
221
|
+
openCode.state.tail = [{ info: { role: 'assistant', time: { created: 10_001, completed: 10_002 } } }];
|
|
222
|
+
emit({ type: 'message.updated', properties: { info: { role: 'assistant', sessionID: SESSION, time: { created: 10_001, completed: 10_002 } } } });
|
|
223
|
+
await settle();
|
|
224
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('delivers past an unfinished tail that predates this runtime (dead pre-restart run)', async () => {
|
|
228
|
+
const { runtime, openCode, emit } = createRuntime({ now: () => 10_000 });
|
|
229
|
+
runtime.start();
|
|
230
|
+
// Assistant reply interrupted by a server restart: unfinished, but older
|
|
231
|
+
// than this runtime — no completion event will ever arrive for it, so it
|
|
232
|
+
// must not block a restored queue forever.
|
|
233
|
+
openCode.state.tail = [{ info: { role: 'assistant', time: { created: 1 } } }];
|
|
234
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
235
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
190
236
|
await settle();
|
|
191
237
|
expect(openCode.state.sent).toHaveLength(1);
|
|
238
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/prompt_async`);
|
|
192
239
|
});
|
|
193
240
|
|
|
194
241
|
it('treats an unreachable OpenCode as unknown, not idle', async () => {
|
|
@@ -44,6 +44,7 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
44
44
|
writeSseEvent,
|
|
45
45
|
getSessionActivitySnapshot,
|
|
46
46
|
getSessionStateSnapshot,
|
|
47
|
+
getPendingBlockingRequestsSnapshot,
|
|
47
48
|
getSessionAttentionSnapshot,
|
|
48
49
|
getSessionState,
|
|
49
50
|
getSessionAttentionState,
|
|
@@ -295,11 +296,15 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
295
296
|
});
|
|
296
297
|
});
|
|
297
298
|
|
|
299
|
+
// Cross-project seed for clients that do not initialize every directory:
|
|
300
|
+
// live status per session plus the permission and question requests still
|
|
301
|
+
// waiting for an answer. Both come from the server's single upstream stream.
|
|
298
302
|
app.get('/api/sessions/status', async (_req, res) => {
|
|
299
303
|
await ensureSessionWatcher();
|
|
300
304
|
const snapshot = getSessionStateSnapshot();
|
|
301
305
|
res.json({
|
|
302
306
|
sessions: snapshot,
|
|
307
|
+
pending: typeof getPendingBlockingRequestsSnapshot === 'function' ? getPendingBlockingRequestsSnapshot() : {},
|
|
303
308
|
serverTime: Date.now(),
|
|
304
309
|
});
|
|
305
310
|
});
|
|
@@ -44,6 +44,12 @@ other.
|
|
|
44
44
|
- Send and fork dispatches without an explicit model/agent/variant reuse the
|
|
45
45
|
target session's last user-message selection before falling back to the
|
|
46
46
|
configured defaults; only session creation resolves defaults directly.
|
|
47
|
+
- Default agents resolve from the owning project before global settings and
|
|
48
|
+
OpenCode defaults. Directory-based requests identify the project before
|
|
49
|
+
creating a worktree; existing linked worktrees resolve through Git's primary
|
|
50
|
+
worktree root. Send/fork fallback uses that same owner. Configured model IDs
|
|
51
|
+
and effort preferences survive missing catalog entries rather than silently
|
|
52
|
+
dispatching with a different model.
|
|
47
53
|
- Usage errors name the missing or conflicting input so CLI and agent-tool
|
|
48
54
|
callers can correct an invalid request without an upfront usage manual.
|
|
49
55
|
- Explicit `projectId` or `directory` scope takes precedence over the managed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
3
|
-
import { createWorktree, getWorktreeBootstrapStatus } from '../git/index.js';
|
|
3
|
+
import { createWorktree, getWorktreeBootstrapStatus, resolvePrimaryWorktreeRoot } from '../git/index.js';
|
|
4
4
|
import { expandSnippets } from '../opencode/snippets.js';
|
|
5
5
|
import { expandCommandGoalObjective, parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
|
|
6
6
|
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
|
|
@@ -75,6 +75,7 @@ const resolveVariant = (providers, providerID, modelID, variant) => {
|
|
|
75
75
|
if (!normalized) return undefined;
|
|
76
76
|
const provider = providers.find((entry) => entry?.id === providerID);
|
|
77
77
|
const model = providerModels(provider).find((entry) => entry?.id === modelID);
|
|
78
|
+
if (!model) return normalized;
|
|
78
79
|
return model?.variants && Object.prototype.hasOwnProperty.call(model.variants, normalized)
|
|
79
80
|
? normalized
|
|
80
81
|
: undefined;
|
|
@@ -82,6 +83,18 @@ const resolveVariant = (providers, providerID, modelID, variant) => {
|
|
|
82
83
|
|
|
83
84
|
const parseConfigModel = (value) => splitModel(value);
|
|
84
85
|
|
|
86
|
+
const resolveProjectDefaults = (settings, directory, projectId) => {
|
|
87
|
+
const projects = Array.isArray(settings?.projects) ? settings.projects : [];
|
|
88
|
+
const matchedProject = projectId
|
|
89
|
+
? projects.find((entry) => entry?.id === projectId) || null
|
|
90
|
+
: projects.find((entry) => entry?.path === directory) || null;
|
|
91
|
+
return {
|
|
92
|
+
defaultAgent: asNonEmptyString(matchedProject?.defaultAgent),
|
|
93
|
+
defaultModel: asNonEmptyString(matchedProject?.defaultModel),
|
|
94
|
+
defaultVariant: asNonEmptyString(matchedProject?.defaultVariant),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
85
98
|
const buildDirectoryHeaders = (directory) => ({
|
|
86
99
|
// OpenCode rejects non-ASCII header values; the official SDK sends this
|
|
87
100
|
// header percent-encoded, so match that wire format (non-ASCII checkout
|
|
@@ -121,11 +134,15 @@ const fetchSelectionInputs = async ({ buildOpenCodeUrl, authHeaders, directory,
|
|
|
121
134
|
};
|
|
122
135
|
};
|
|
123
136
|
|
|
124
|
-
const resolveDefaultSelection = ({ agents, providers, settings, opencodeDefaultAgent, opencodeDefaultModel }) => {
|
|
137
|
+
const resolveDefaultSelection = ({ agents, providers, settings, projectDefaults, opencodeDefaultAgent, opencodeDefaultModel }) => {
|
|
125
138
|
const primaryAgents = agents.filter((agent) => isPrimaryAgentMode(agent?.mode) && agent?.hidden !== true);
|
|
126
139
|
let resolvedAgent = null;
|
|
140
|
+
const projectDefaultAgent = asNonEmptyString(projectDefaults?.defaultAgent);
|
|
127
141
|
const settingsDefaultAgent = asNonEmptyString(settings?.defaultAgent);
|
|
128
|
-
if (
|
|
142
|
+
if (projectDefaultAgent) {
|
|
143
|
+
resolvedAgent = agents.find((agent) => agent?.name === projectDefaultAgent) || null;
|
|
144
|
+
}
|
|
145
|
+
if (!resolvedAgent && settingsDefaultAgent) {
|
|
129
146
|
resolvedAgent = agents.find((agent) => agent?.name === settingsDefaultAgent) || null;
|
|
130
147
|
}
|
|
131
148
|
if (!resolvedAgent && opencodeDefaultAgent) {
|
|
@@ -140,20 +157,24 @@ const resolveDefaultSelection = ({ agents, providers, settings, opencodeDefaultA
|
|
|
140
157
|
|
|
141
158
|
let model = null;
|
|
142
159
|
let variant;
|
|
160
|
+
const projectDefaultModel = parseConfigModel(projectDefaults?.defaultModel);
|
|
143
161
|
const settingsDefaultModel = parseConfigModel(settings?.defaultModel);
|
|
144
|
-
if (
|
|
162
|
+
if (projectDefaultModel) {
|
|
163
|
+
model = projectDefaultModel;
|
|
164
|
+
variant = resolveVariant(providers, model.providerID, model.modelID, projectDefaults?.defaultVariant);
|
|
165
|
+
}
|
|
166
|
+
if (!model && settingsDefaultModel) {
|
|
145
167
|
model = settingsDefaultModel;
|
|
146
168
|
variant = resolveVariant(providers, model.providerID, model.modelID, settings?.defaultVariant);
|
|
147
169
|
}
|
|
148
170
|
|
|
149
|
-
if (!model && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID
|
|
150
|
-
&& hasProviderModel(providers, resolvedAgent.model.providerID, resolvedAgent.model.modelID)) {
|
|
171
|
+
if (!model && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID) {
|
|
151
172
|
model = { providerID: resolvedAgent.model.providerID, modelID: resolvedAgent.model.modelID };
|
|
152
173
|
variant = resolveVariant(providers, model.providerID, model.modelID, resolvedAgent.variant);
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
const opencodeModel = parseConfigModel(opencodeDefaultModel);
|
|
156
|
-
if (!model && opencodeModel
|
|
177
|
+
if (!model && opencodeModel) {
|
|
157
178
|
model = opencodeModel;
|
|
158
179
|
}
|
|
159
180
|
|
|
@@ -304,9 +325,15 @@ const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated
|
|
|
304
325
|
|
|
305
326
|
const directory = asNonEmptyString(payload?.directory);
|
|
306
327
|
const validated = await validateDirectoryPath(directory);
|
|
307
|
-
return validated.
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
if (!validated.ok) return { ok: false, status: 400, error: validated.error || 'Invalid directory' };
|
|
329
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
330
|
+
const projects = sanitizeProjects(settings?.projects || []);
|
|
331
|
+
let project = projects.find((entry) => entry.path === validated.directory);
|
|
332
|
+
if (!project && projects.length > 0) {
|
|
333
|
+
const { root } = await resolvePrimaryWorktreeRoot(validated.directory);
|
|
334
|
+
project = projects.find((entry) => entry.path === root);
|
|
335
|
+
}
|
|
336
|
+
return { ok: true, directory: validated.directory, ...(project ? { projectId: project.id } : {}) };
|
|
310
337
|
};
|
|
311
338
|
|
|
312
339
|
const PROMPT_LANDED_TIMEOUT_MS = 5_000;
|
|
@@ -395,6 +422,10 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
395
422
|
emitSessionCreatedEvent,
|
|
396
423
|
createSessionGoal: createSessionGoalOverride,
|
|
397
424
|
sessionKnowledgeRuntime = null,
|
|
425
|
+
// Auto routing. Prompts dispatched here go straight to OpenCode, not
|
|
426
|
+
// through the proxy that rewrites the Auto sentinel, so the same hook runs
|
|
427
|
+
// on the body before it is sent. Null when routing is not wired in.
|
|
428
|
+
resolvePromptBody = null,
|
|
398
429
|
} = dependencies;
|
|
399
430
|
|
|
400
431
|
// Last user message of an existing session, as a selection to reuse. Returns
|
|
@@ -468,6 +499,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
468
499
|
authHeaders,
|
|
469
500
|
sessionID,
|
|
470
501
|
directory,
|
|
502
|
+
projectId,
|
|
471
503
|
prompt,
|
|
472
504
|
goalInput,
|
|
473
505
|
requestedModel,
|
|
@@ -495,7 +527,10 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
495
527
|
directory,
|
|
496
528
|
readSettingsFromDiskMigrated,
|
|
497
529
|
});
|
|
498
|
-
const defaults = resolveDefaultSelection(
|
|
530
|
+
const defaults = resolveDefaultSelection({
|
|
531
|
+
...inputs,
|
|
532
|
+
projectDefaults: resolveProjectDefaults(inputs.settings, directory, projectId),
|
|
533
|
+
});
|
|
499
534
|
if (!model) {
|
|
500
535
|
model = defaults.model;
|
|
501
536
|
if (variant == null) variant = defaults.variant;
|
|
@@ -565,25 +600,21 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
565
600
|
? await sessionKnowledgeRuntime.resolvePendingForSession(sessionID, directory)
|
|
566
601
|
.catch(() => ({ text: '', signature: '' }))
|
|
567
602
|
: { text: '', signature: '' };
|
|
603
|
+
const payload = {
|
|
604
|
+
model,
|
|
605
|
+
...(agent ? { agent } : {}),
|
|
606
|
+
...(variant ? { variant } : {}),
|
|
607
|
+
parts: [
|
|
608
|
+
...(knowledge.text ? [{ type: 'text', text: knowledge.text, synthetic: true }] : []),
|
|
609
|
+
{ type: 'text', text: expandedPrompt },
|
|
610
|
+
...(goalInput.enabled
|
|
611
|
+
? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }]
|
|
612
|
+
: []),
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
await resolvePromptBody?.(payload, { sessionId: sessionID, directory });
|
|
568
616
|
try {
|
|
569
|
-
await runPromptAsync({
|
|
570
|
-
baseUrl,
|
|
571
|
-
authHeaders,
|
|
572
|
-
sessionID,
|
|
573
|
-
directory,
|
|
574
|
-
payload: {
|
|
575
|
-
model,
|
|
576
|
-
...(agent ? { agent } : {}),
|
|
577
|
-
...(variant ? { variant } : {}),
|
|
578
|
-
parts: [
|
|
579
|
-
...(knowledge.text ? [{ type: 'text', text: knowledge.text, synthetic: true }] : []),
|
|
580
|
-
{ type: 'text', text: expandedPrompt },
|
|
581
|
-
...(goalInput.enabled
|
|
582
|
-
? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }]
|
|
583
|
-
: []),
|
|
584
|
-
],
|
|
585
|
-
},
|
|
586
|
-
});
|
|
617
|
+
await runPromptAsync({ baseUrl, authHeaders, sessionID, directory, payload });
|
|
587
618
|
} catch (error) {
|
|
588
619
|
throw markGoalPartial(error);
|
|
589
620
|
}
|
|
@@ -735,6 +766,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
735
766
|
authHeaders,
|
|
736
767
|
sessionID,
|
|
737
768
|
directory: sessionDirectory,
|
|
769
|
+
projectId: resolvedDirectory.projectId,
|
|
738
770
|
prompt,
|
|
739
771
|
goalInput,
|
|
740
772
|
requestedModel: model,
|
|
@@ -838,6 +870,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
838
870
|
authHeaders,
|
|
839
871
|
sessionID: targetSessionID,
|
|
840
872
|
directory,
|
|
873
|
+
projectId: resolvedDirectory.projectId,
|
|
841
874
|
prompt,
|
|
842
875
|
goalInput,
|
|
843
876
|
requestedModel,
|
|
@@ -90,6 +90,7 @@ vi.mock('@opencode-ai/sdk/v2', () => ({
|
|
|
90
90
|
vi.mock('../git/index.js', () => ({
|
|
91
91
|
createWorktree: (...args) => globalThis.__openchamberCreateWorktreeMock(...args),
|
|
92
92
|
getWorktreeBootstrapStatus: (...args) => globalThis.__openchamberGetWorktreeBootstrapStatusMock(...args),
|
|
93
|
+
resolvePrimaryWorktreeRoot: async (directory) => ({ root: directory === '/repo/worktrees/side-task' ? '/repo/app' : directory }),
|
|
93
94
|
}));
|
|
94
95
|
|
|
95
96
|
const createApp = (overrides = {}, options = {}) => {
|
|
@@ -360,6 +361,101 @@ describe('openchamber session routes', () => {
|
|
|
360
361
|
}
|
|
361
362
|
});
|
|
362
363
|
|
|
364
|
+
it('lets the routing hook replace an Auto default before the prompt leaves', async () => {
|
|
365
|
+
const originalFetch = globalThis.fetch;
|
|
366
|
+
const fetchMock = vi.fn(async (url) => {
|
|
367
|
+
const text = String(url);
|
|
368
|
+
if (text.includes('/prompt_async')) {
|
|
369
|
+
return { ok: true, text: async () => '' };
|
|
370
|
+
}
|
|
371
|
+
if (text.includes('/config/providers')) {
|
|
372
|
+
return { ok: true, json: async () => ({ providers: [{ id: 'openai', models: { 'gpt-5.5': { id: 'gpt-5.5' } } }] }) };
|
|
373
|
+
}
|
|
374
|
+
if (text.includes('/agent')) {
|
|
375
|
+
return { ok: true, json: async () => [{ name: 'build', mode: 'primary' }] };
|
|
376
|
+
}
|
|
377
|
+
if (text.includes('/config')) {
|
|
378
|
+
return { ok: true, json: async () => ({}) };
|
|
379
|
+
}
|
|
380
|
+
return { ok: true, json: async () => ({ id: 'ses_123' }) };
|
|
381
|
+
});
|
|
382
|
+
globalThis.fetch = fetchMock;
|
|
383
|
+
const seen = [];
|
|
384
|
+
const resolvePromptBody = vi.fn(async (body, target) => {
|
|
385
|
+
seen.push({ model: body.model, target });
|
|
386
|
+
body.model = { providerID: 'openai', modelID: 'gpt-5.5' };
|
|
387
|
+
});
|
|
388
|
+
const { app } = createApp({
|
|
389
|
+
readSettingsFromDiskMigrated: async () => ({
|
|
390
|
+
defaultModel: 'openchamber/auto',
|
|
391
|
+
defaultAgent: 'build',
|
|
392
|
+
projects: [{ id: 'proj_1', path: '/repo/app' }],
|
|
393
|
+
}),
|
|
394
|
+
resolvePromptBody,
|
|
395
|
+
});
|
|
396
|
+
try {
|
|
397
|
+
await request(app)
|
|
398
|
+
.post('/api/openchamber/sessions')
|
|
399
|
+
.send({ directory: '/repo/app', prompt: 'Run this' })
|
|
400
|
+
.expect(200);
|
|
401
|
+
|
|
402
|
+
expect(seen).toEqual([{
|
|
403
|
+
model: { providerID: 'openchamber', modelID: 'auto' },
|
|
404
|
+
target: { sessionId: 'ses_123', directory: '/repo/app' },
|
|
405
|
+
}]);
|
|
406
|
+
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
|
|
407
|
+
expect(JSON.parse(promptCall?.[1]?.body).model).toEqual({ providerID: 'openai', modelID: 'gpt-5.5' });
|
|
408
|
+
} finally {
|
|
409
|
+
globalThis.fetch = originalFetch;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it.each([
|
|
414
|
+
['', { projectId: 'proj_1' }],
|
|
415
|
+
['', { directory: '/repo/app', worktree: { name: 'side-task' } }],
|
|
416
|
+
['', { directory: '/repo/worktrees/side-task' }],
|
|
417
|
+
['/ses_existing/send', { directory: '/repo/worktrees/side-task' }],
|
|
418
|
+
['/ses_existing/fork', { directory: '/repo/worktrees/side-task' }],
|
|
419
|
+
])('prefers project defaults for %s with %j', async (endpoint, scope) => {
|
|
420
|
+
const originalFetch = globalThis.fetch;
|
|
421
|
+
const fetchMock = vi.fn(async (url) => {
|
|
422
|
+
const text = String(url);
|
|
423
|
+
if (text.includes('/prompt_async')) {
|
|
424
|
+
return { ok: true, text: async () => '' };
|
|
425
|
+
}
|
|
426
|
+
if (text.includes('/config/providers')) {
|
|
427
|
+
return { ok: true, json: async () => ({ providers: [{ id: 'openai', models: { 'gpt-5.5': { id: 'gpt-5.5' } } }] }) };
|
|
428
|
+
}
|
|
429
|
+
if (text.includes('/agent')) {
|
|
430
|
+
return { ok: true, json: async () => [{ name: 'build', mode: 'primary' }, { name: 'plan', mode: 'primary' }] };
|
|
431
|
+
}
|
|
432
|
+
if (text.includes('/config')) {
|
|
433
|
+
return { ok: true, json: async () => ({}) };
|
|
434
|
+
}
|
|
435
|
+
return { ok: true, json: async () => ({ id: 'ses_123' }) };
|
|
436
|
+
});
|
|
437
|
+
globalThis.fetch = fetchMock;
|
|
438
|
+
const { app } = createApp({
|
|
439
|
+
readSettingsFromDiskMigrated: async () => ({
|
|
440
|
+
defaultModel: 'openai/gpt-5.5',
|
|
441
|
+
defaultAgent: 'build',
|
|
442
|
+
projects: [{ id: 'proj_1', path: '/repo/app', defaultAgent: 'plan' }],
|
|
443
|
+
}),
|
|
444
|
+
});
|
|
445
|
+
try {
|
|
446
|
+
const response = await request(app)
|
|
447
|
+
.post(`/api/openchamber/sessions${endpoint}`)
|
|
448
|
+
.send({ ...scope, prompt: 'Run this' })
|
|
449
|
+
.expect(200);
|
|
450
|
+
|
|
451
|
+
expect(response.body.agent).toBe('plan');
|
|
452
|
+
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
|
|
453
|
+
expect(JSON.parse(promptCall?.[1]?.body)).toMatchObject({ agent: 'plan' });
|
|
454
|
+
} finally {
|
|
455
|
+
globalThis.fetch = originalFetch;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
363
459
|
it('dispatches an initial prompt when model is provided', async () => {
|
|
364
460
|
const originalFetch = globalThis.fetch;
|
|
365
461
|
const fetchMock = vi.fn(async (url) => {
|
|
@@ -32,7 +32,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
32
32
|
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service/routes composition runtime and active-port wiring for main server startup.
|
|
33
33
|
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
|
|
34
34
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
35
|
-
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection,
|
|
35
|
+
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, same-machine authentication (loopback, or the bound address for a concrete bind), and fixed CLI action dispatch.
|
|
36
36
|
- `packages/web/server/lib/system-prompt/runtime.js`: opt-in managed OpenCode system-prompt optimizer materialization and plugin injection.
|
|
37
37
|
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer) appends itself through.
|
|
38
38
|
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
|
|
@@ -44,6 +44,28 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
44
44
|
- `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep.
|
|
45
45
|
- `packages/web/server/lib/opencode/settings-normalization-runtime.js`: path/settings/tunnel normalization and sanitization helpers runtime used by settings/routes/config wiring.
|
|
46
46
|
- `packages/web/server/lib/opencode/theme-runtime.js`: custom theme JSON validation and theme directory loading runtime for settings utility routes.
|
|
47
|
+
|
|
48
|
+
`POST /api/config/themes` saves a converted VS Code palette. The runtime validates
|
|
49
|
+
literal colors and required authored roles, assigns a content-derived filename,
|
|
50
|
+
and publishes through a same-directory hard link so partial files and overwrites
|
|
51
|
+
are impossible. Identical retries reuse the existing file; a manually edited
|
|
52
|
+
collision returns 409. Temporary files are ignored by the loader and removed
|
|
53
|
+
after publication or failure. Non-missing-directory read failures propagate to
|
|
54
|
+
the route instead of returning an authoritative empty library.
|
|
55
|
+
|
|
56
|
+
The common request middleware parses theme POST bodies before these routes;
|
|
57
|
+
integration tests must use that middleware rather than an unrestricted test parser.
|
|
58
|
+
`DELETE /api/config/themes/:id` finds a valid regular JSON file by its metadata ID
|
|
59
|
+
inside the custom themes directory. IDs are never used as filenames. Hand-added
|
|
60
|
+
themes are supported; symlinks and bundled themes are outside deletion ownership.
|
|
61
|
+
Duplicate matching IDs fail explicitly. Missing themes are an idempotent success;
|
|
62
|
+
filesystem failures remain errors.
|
|
63
|
+
`theme-catalog.js` owns POST catalog search/package routes under
|
|
64
|
+
`/api/config/themes/catalog/`. It fetches only Open VSX and its Eclipse CDN over
|
|
65
|
+
HTTPS, validates redirects and checksums, and verifies packaged identity.
|
|
66
|
+
`theme-archive.js` reads selected JSON entries in memory with bounded decompression.
|
|
67
|
+
JSON includes and token references stay inside the package. Each failed variant
|
|
68
|
+
is reported separately so valid siblings remain available. No extension code runs.
|
|
47
69
|
- `packages/web/server/lib/opencode/proxy.js`: OpenCode API/SSE forwarding and readiness-gate route registration.
|
|
48
70
|
- `packages/web/server/lib/opencode/session-runtime.js`: session status/attention/activity runtime for OpenCode SSE events.
|
|
49
71
|
- `packages/web/server/lib/opencode/watcher.js`: global SSE watcher runtime for push/session event fanout.
|
|
@@ -156,6 +178,23 @@ Transport-triggered health checks share the periodic monitor's failure accountin
|
|
|
156
178
|
|
|
157
179
|
Managed health failures are classified as `timeout`, `connection_refused`, `connection_reset`, `invalid_response`, or `error`. The lifecycle retains the latest counted failure with a bounded detail string and source. Managed process wrappers continue capturing a sanitized, bounded stderr tail after readiness and retain exit code/signal. Before replacing a managed process, lifecycle snapshots the reason, latest health failure, process diagnostics/aliveness, busy-session count, and timestamp into `lastOpenCodeRestartDiagnostics`; successful startup does not clear this snapshot, and `/health` exposes it for post-restart diagnosis without process environment or credentials.
|
|
158
180
|
|
|
181
|
+
Managed process ownership starts at spawn. The registry and runtime process
|
|
182
|
+
handle include children that have not announced readiness yet, so shutdown can
|
|
183
|
+
stop an in-flight startup. Readiness timeout, malformed startup output, and
|
|
184
|
+
health-probe errors close that child before retrying. Shutdown cancels further
|
|
185
|
+
startup attempts. Closing a process is single-flight and unregisters it only
|
|
186
|
+
after it exits.
|
|
187
|
+
|
|
188
|
+
On Windows, managed teardown invokes the existing tree termination command
|
|
189
|
+
before terminating the root. Calling `child.kill()` first loses the ancestry
|
|
190
|
+
needed to find Git, shell, and MCP descendants. On POSIX, the managed child
|
|
191
|
+
starts in its own process group and teardown escalates against that group even
|
|
192
|
+
if the root has already exited. A tool ignoring SIGTERM must not survive just
|
|
193
|
+
because the server closed its own pipes. The
|
|
194
|
+
`lifecycle-process.test.js` regressions launch real parent/child fixtures and
|
|
195
|
+
check PID exit plus registry cleanup. macOS results do not validate Windows
|
|
196
|
+
ConPTY or Console Window Host behavior.
|
|
197
|
+
|
|
159
198
|
## Public exports (env-runtime.js)
|
|
160
199
|
- `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state.
|
|
161
200
|
- OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery.
|
|
@@ -332,6 +371,7 @@ Managed health failures are classified as `timeout`, `connection_refused`, `conn
|
|
|
332
371
|
|
|
333
372
|
## Public exports (shutdown-runtime.js)
|
|
334
373
|
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown runtime for managed OpenCode and web server teardown sequencing.
|
|
374
|
+
- After stopping owned runtimes and OpenCode, HTTP shutdown closes active connections as well as the listener. A remaining SSE response must not hold Desktop open until its fallback deadline. Upgraded sockets remain the responsibility of their owning runtime.
|
|
335
375
|
- Returned API:
|
|
336
376
|
- `gracefulShutdown(options?)`
|
|
337
377
|
|
|
@@ -389,6 +429,20 @@ within a ten-minute overall deadline.
|
|
|
389
429
|
- Foreground servers running under a systemd user unit queue installation in
|
|
390
430
|
a separate transient unit and restart the configured service afterwards.
|
|
391
431
|
`OPENCHAMBER_SYSTEMD_UNIT` overrides the default `openchamber.service`.
|
|
432
|
+
- On Windows the install-and-restart script is written to
|
|
433
|
+
`<data dir>/update-install.cmd` before the response and run with
|
|
434
|
+
`cmd.exe /c <file>`. A newline ends a `cmd.exe /c` command line, so the
|
|
435
|
+
same script passed as an argument ran nothing and exited 0; the batch
|
|
436
|
+
file keeps every line. The package-manager line is `call`ed because
|
|
437
|
+
npm, pnpm and yarn are `.cmd` shims that would otherwise end the script,
|
|
438
|
+
the pre-install pause is a loopback `ping` because `timeout` rejects a
|
|
439
|
+
detached child's stdin, and the file deletes itself on its last line
|
|
440
|
+
because the restart command carries the server's flags. If the file
|
|
441
|
+
cannot be written the route answers 500 and the server keeps running.
|
|
442
|
+
The listener is closed before the batch is spawned: on Windows the
|
|
443
|
+
detached child inherits the listening socket and would hold the port
|
|
444
|
+
for the whole batch, so the restart inside it failed with "port already
|
|
445
|
+
in use" and the update ended with no server.
|
|
392
446
|
- `GET /api/openchamber/models-metadata`
|
|
393
447
|
- `GET /api/zen/models`
|
|
394
448
|
|
|
@@ -131,6 +131,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
131
131
|
writeSseEvent,
|
|
132
132
|
getSessionActivitySnapshot: sessionRuntime.getSessionActivitySnapshot,
|
|
133
133
|
getSessionStateSnapshot: sessionRuntime.getSessionStateSnapshot,
|
|
134
|
+
getPendingBlockingRequestsSnapshot: sessionRuntime.getPendingBlockingRequestsSnapshot,
|
|
134
135
|
getSessionAttentionSnapshot: sessionRuntime.getSessionAttentionSnapshot,
|
|
135
136
|
getSessionState: sessionRuntime.getSessionState,
|
|
136
137
|
getSessionAttentionState: sessionRuntime.getSessionAttentionState,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { buildExternalManualRestartResponse } from './config-mutation-response.js';
|
|
2
|
+
import { ThemeImportStorageError } from './theme-runtime.js';
|
|
3
|
+
import { registerThemeCatalogRoutes } from './theme-catalog.js';
|
|
2
4
|
|
|
3
5
|
const parseLoopbackUrl = (rawUrl) => {
|
|
4
6
|
if (typeof rawUrl !== 'string') {
|
|
@@ -589,7 +591,15 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
|
|
|
589
591
|
res.status(statusCode).json({ error: 'Invalid or expired pairing session' });
|
|
590
592
|
};
|
|
591
593
|
|
|
594
|
+
const isGuestOauthCallback = (req) => (
|
|
595
|
+
req.method === 'GET'
|
|
596
|
+
&& /^\/guests\/[a-z][a-z0-9-]*\/oauth\/callback$/.test(req.path || '')
|
|
597
|
+
);
|
|
598
|
+
|
|
592
599
|
const requireApiAuth = async (req, res, next) => {
|
|
600
|
+
if (isGuestOauthCallback(req)) {
|
|
601
|
+
return next();
|
|
602
|
+
}
|
|
593
603
|
const requestScope = tunnelAuthController.classifyRequestScope(req);
|
|
594
604
|
if (requestScope === 'tunnel' || requestScope === 'unknown-public') {
|
|
595
605
|
return tunnelAuthController.requireTunnelSession(req, res, next);
|
|
@@ -995,6 +1005,8 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
|
|
|
995
1005
|
export const registerSettingsUtilityRoutes = (app, dependencies) => {
|
|
996
1006
|
const {
|
|
997
1007
|
readCustomThemesFromDisk,
|
|
1008
|
+
saveImportedTheme,
|
|
1009
|
+
deleteImportedTheme,
|
|
998
1010
|
refreshOpenCodeAfterConfigChange,
|
|
999
1011
|
clientReloadDelayMs,
|
|
1000
1012
|
} = dependencies;
|
|
@@ -1009,6 +1021,30 @@ export const registerSettingsUtilityRoutes = (app, dependencies) => {
|
|
|
1009
1021
|
}
|
|
1010
1022
|
});
|
|
1011
1023
|
|
|
1024
|
+
app.post('/api/config/themes', async (req, res) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const theme = await saveImportedTheme(req.body?.theme);
|
|
1027
|
+
res.status(201).json({ theme });
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (error instanceof ThemeImportStorageError) {
|
|
1030
|
+
res.status(error.status).json({ error: error.code });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
console.error('[themes] Failed to save imported theme');
|
|
1034
|
+
res.status(500).json({ error: 'save' });
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
registerThemeCatalogRoutes(app);
|
|
1039
|
+
app.delete('/api/config/themes/:id', async (req, res) => {
|
|
1040
|
+
try {
|
|
1041
|
+
await deleteImportedTheme(req.params.id);
|
|
1042
|
+
res.json({ success: true });
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
res.status(error instanceof ThemeImportStorageError ? error.status : 500).json({ error: 'delete' });
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1012
1048
|
app.post('/api/config/reload', async (_req, res) => {
|
|
1013
1049
|
try {
|
|
1014
1050
|
console.log('[Server] Manual configuration reload requested');
|
|
@@ -1041,7 +1077,9 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1041
1077
|
const { express, verboseRequestLogs = false } = dependencies;
|
|
1042
1078
|
|
|
1043
1079
|
app.use((req, res, next) => {
|
|
1044
|
-
if (req.path.startsWith('/api/
|
|
1080
|
+
if (req.path === '/api/config/themes' || req.path.startsWith('/api/config/themes/')) {
|
|
1081
|
+
express.json({ limit: '1mb' })(req, res, next);
|
|
1082
|
+
} else if (req.path.startsWith('/api/behavior')) {
|
|
1045
1083
|
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
1046
1084
|
if (contentLength > 1024 * 1024) {
|
|
1047
1085
|
return res.status(413).json({ error: 'Content exceeds maximum size of 1048576 bytes' });
|
|
@@ -5,6 +5,9 @@ import { registerWalkthroughRoutes } from '../walkthrough/routes.js';
|
|
|
5
5
|
import { registerSessionGoalRoutes } from '../session-goal/routes.js';
|
|
6
6
|
import { registerGitHubRoutes } from '../github/routes.js';
|
|
7
7
|
import { registerLinearRoutes } from '../linear/routes.js';
|
|
8
|
+
import { registerGuestRoutes } from '../guests/routes.js';
|
|
9
|
+
import { registerBuiltInGuests } from '../guests/catalog.js';
|
|
10
|
+
import { extensionsPersistPath } from '../guests/persist.js';
|
|
8
11
|
import { registerGitRoutes } from '../git/routes.js';
|
|
9
12
|
import { registerDevServerRoutes } from '../dev-servers/routes.js';
|
|
10
13
|
import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
|
|
@@ -15,6 +18,7 @@ import { registerAgentMemoryRoutes } from '../agent-memory/routes.js';
|
|
|
15
18
|
import { registerSessionKnowledgeRoutes } from '../session-knowledge/routes.js';
|
|
16
19
|
import { registerPermissionAutoAcceptRoutes } from '../permission-auto-accept/runtime.js';
|
|
17
20
|
import { registerMessageQueueRoutes } from '../message-queue/runtime.js';
|
|
21
|
+
import { registerRoutingPromptRewrite, registerRoutingRoutes } from '../routing/routes.js';
|
|
18
22
|
import { registerConfigEntityRoutes } from './config-entity-routes.js';
|
|
19
23
|
import { registerSettingsUtilityRoutes } from './core-routes.js';
|
|
20
24
|
import { registerProjectIconRoutes } from './project-icon-routes.js';
|
|
@@ -105,6 +109,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
105
109
|
resolveOptionalProjectDirectory,
|
|
106
110
|
validateDirectoryPath,
|
|
107
111
|
readCustomThemesFromDisk,
|
|
112
|
+
saveImportedTheme,
|
|
113
|
+
deleteImportedTheme,
|
|
108
114
|
refreshOpenCodeAfterConfigChange,
|
|
109
115
|
getOpenCodeResolutionSnapshot,
|
|
110
116
|
getOpenCodeUpgradeCapability,
|
|
@@ -136,16 +142,23 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
136
142
|
emitSessionCreatedEvent,
|
|
137
143
|
permissionAutoAcceptRuntime,
|
|
138
144
|
messageQueueRuntime,
|
|
145
|
+
routingRuntime,
|
|
146
|
+
openchamberVersion,
|
|
139
147
|
} = routeDependencies;
|
|
140
148
|
|
|
141
149
|
registerSettingsUtilityRoutes(app, {
|
|
142
150
|
readCustomThemesFromDisk,
|
|
151
|
+
saveImportedTheme,
|
|
152
|
+
deleteImportedTheme,
|
|
143
153
|
refreshOpenCodeAfterConfigChange,
|
|
144
154
|
clientReloadDelayMs,
|
|
145
155
|
});
|
|
146
156
|
|
|
147
157
|
registerPermissionAutoAcceptRoutes(app, permissionAutoAcceptRuntime);
|
|
148
158
|
registerMessageQueueRoutes(app, messageQueueRuntime);
|
|
159
|
+
registerRoutingRoutes(app, routingRuntime);
|
|
160
|
+
// Before the generic OpenCode proxy: turns `openchamber/auto` into a real model.
|
|
161
|
+
registerRoutingPromptRewrite(app, routingRuntime);
|
|
149
162
|
|
|
150
163
|
registerOpenCodeRoutes(app, {
|
|
151
164
|
crypto,
|
|
@@ -307,6 +320,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
307
320
|
registerSessionGoalRoutes(app);
|
|
308
321
|
registerGitHubRoutes(app);
|
|
309
322
|
registerLinearRoutes(app);
|
|
323
|
+
await registerBuiltInGuests({ persistPath: extensionsPersistPath(openchamberDataDir), root: routeDependencies.builtInExtensionsDir });
|
|
324
|
+
registerGuestRoutes(app, { openchamberDataDir, openchamberVersion, resolveGitBinaryForSpawn, resolveOptionalProjectDirectory, getSmallModelService });
|
|
310
325
|
registerGitRoutes(app, {
|
|
311
326
|
emitWorktreeChanged: ({ directories, at }) => {
|
|
312
327
|
const clients = getOpenChamberEventClients();
|