@vantran-se/openchamber-web 1.23.0 → 1.24.3
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-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
- package/dist/assets/DiffView-COVQhWGd.js +4 -0
- package/dist/assets/FilesView-B1V5JxFX.js +13 -0
- package/dist/assets/GitView-CDOJy5Ay.js +6 -0
- package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
- package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.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-D7FKg1XJ.js +1 -0
- package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
- package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
- package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
- package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
- package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
- package/dist/assets/PluginPane-B4tKno2h.js +3 -0
- package/dist/assets/SettingsView-DEaMulN9.js +29 -0
- package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
- package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
- package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
- package/dist/assets/de-DgxgFyGG.js +10 -0
- package/dist/assets/es-BFsZj5F_.js +16 -0
- package/dist/assets/fr-jdZ5KKQi.js +14 -0
- package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
- package/dist/assets/index-Cbci4i1W.js +40 -0
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
- package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
- package/dist/assets/ja-C5kKrhO4.js +16 -0
- package/dist/assets/ko-DXDTT_0O.js +16 -0
- package/dist/assets/main-CZALtVek.js +896 -0
- package/dist/assets/main-CgXMBNvG.js +2 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
- package/dist/assets/miniChat-CwTmDixi.js +2 -0
- package/dist/assets/mobile-CLf2vCtO.js +2 -0
- package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
- package/dist/assets/pl-D-7x4Y8Q.js +16 -0
- package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
- package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
- package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
- package/dist/assets/surface-B6n7wJtt.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
- package/dist/assets/tr-Dr-oJ22L.js +7 -0
- package/dist/assets/uk-9Nx83UcI.js +16 -0
- package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
- package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
- package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
- package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.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-CgAdXWZO.js +39 -0
- package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
- package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
- package/dist/assets/zh-TW-0BB4gpHM.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 +47 -23
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- 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 +26 -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 +68 -13
- package/server/lib/event-stream/global-hub.test.js +182 -0
- package/server/lib/event-stream/global-ws-bridge.js +16 -18
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +60 -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 +30 -6
- package/server/lib/git/routes.js +37 -11
- package/server/lib/git/routes.test.js +78 -0
- 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 +588 -74
- package/server/lib/git/service.test.js +492 -0
- package/server/lib/github/DOCUMENTATION.md +5 -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 +6 -6
- 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 +17 -1
- package/server/lib/message-queue/runtime.js +34 -1
- package/server/lib/message-queue/runtime.test.js +83 -6
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- 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 +73 -5
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +30 -1
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +58 -70
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/proxy.js +33 -0
- 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 +11 -0
- package/server/lib/opencode/settings-helpers.test.js +24 -2
- 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 +18 -0
- package/server/lib/opencode/settings-runtime.js +36 -9
- package/server/lib/opencode/settings-runtime.test.js +158 -2
- 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/project-context/DOCUMENTATION.md +13 -3
- package/server/lib/project-context/runtime.js +8 -2
- package/server/lib/project-context/runtime.test.js +54 -0
- package/server/lib/projects/DOCUMENTATION.md +2 -1
- package/server/lib/projects/project-config.js +40 -11
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +26 -0
- 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/relay/DOCUMENTATION.md +20 -0
- package/server/lib/relay/host-client.js +7 -0
- package/server/lib/relay/host-client.test.js +14 -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/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +14 -4
- package/server/lib/scheduled-tasks/runtime.test.js +67 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +111 -257
- package/server/lib/session-assist/runtime.test.js +203 -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/server/lib/walkthrough/DOCUMENTATION.md +15 -5
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +13 -2
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
- package/dist/assets/FilesView-BdTOpew9.js +0 -17
- package/dist/assets/GitView-Dqf9vHJh.js +0 -6
- package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
- package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
- package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
- package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
- package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
- package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-C8jV7J9y.js +0 -10
- package/dist/assets/es-BI6J1M5R.js +0 -16
- package/dist/assets/fr-D6oDFt7E.js +0 -14
- package/dist/assets/index-BqhiCmYS.js +0 -39
- package/dist/assets/index-FqSOBz0J.css +0 -1
- package/dist/assets/ja-C_4i5EQu.js +0 -16
- package/dist/assets/ko-BFbOTpbq.js +0 -16
- package/dist/assets/main-C9ETLKFc.js +0 -2
- package/dist/assets/main-CJmb2dF8.js +0 -930
- package/dist/assets/markdownCore-DVuZpnli.js +0 -5
- package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
- package/dist/assets/mobile-BUT6-lo1.js +0 -2
- package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
- package/dist/assets/pl-COFvT56l.js +0 -16
- package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
- package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
- package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
- package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
- package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
- package/dist/assets/surface-BvRzS58U.js +0 -1
- package/dist/assets/tr-B8Q0Go2l.js +0 -7
- package/dist/assets/uk-Dk4a-Fn-.js +0 -16
- package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
- package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
- package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
- package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
- package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
- package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
- package/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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Jev model routing and the permission safety net. With the `openchamber/auto`
|
|
6
|
+
model selected, the server asks [Jev](https://docs.typesafe.ai) (TypeSafe's
|
|
7
|
+
System One decision model) which task category a message belongs to and sends
|
|
8
|
+
it with that category's model, thinking variant and agent. With the safety net
|
|
9
|
+
on, the same call decides whether an auto-accepted permission should stay on
|
|
10
|
+
screen for the user instead.
|
|
11
|
+
|
|
12
|
+
Dark by default: `OPENCHAMBER_ROUTING_ENABLE` (`feature-flag.js`, read per call)
|
|
13
|
+
gates the routes, the request rewrite, the settings page and the Auto row. VS
|
|
14
|
+
Code has no OpenChamber server and never offers Auto.
|
|
15
|
+
|
|
16
|
+
## Files
|
|
17
|
+
|
|
18
|
+
- `feature-flag.js` — the env gate.
|
|
19
|
+
- `defaults.js` — the Auto sentinel, Jev endpoint, built-in categories, the
|
|
20
|
+
question wording for routing and for the safety net, history excerpt limits.
|
|
21
|
+
- `store.js` — `routing.json` (only deviations from the built-ins) and
|
|
22
|
+
`routing-auth.json` (the Jev key alone, mode 0600) in the OpenChamber data
|
|
23
|
+
dir. `resolveEffectiveConfig` merges built-ins with stored overrides;
|
|
24
|
+
`toStoredConfig` is its inverse. A missing file is the defaults, a malformed
|
|
25
|
+
one throws.
|
|
26
|
+
- `jev.js` — request builders, answer parsing, the HTTP call with a timeout.
|
|
27
|
+
- `history.js` — the last three settled turns through session assist's
|
|
28
|
+
`loadAssistContext` (text parts only, attached quotes included, no files or
|
|
29
|
+
tool payloads), each user message cut to its head and each answer to head
|
|
30
|
+
plus tail. The new request is never cut.
|
|
31
|
+
- `runtime.js` — `createRoutingRuntime`: `describe`, `resolvePromptBody`,
|
|
32
|
+
`evaluatePermission`, config and token writes, event broadcasts.
|
|
33
|
+
- `routes.js` — `/api/routing` (GET, PUT), `/api/routing/token` (PUT, DELETE)
|
|
34
|
+
and `registerRoutingPromptRewrite`.
|
|
35
|
+
|
|
36
|
+
## Invariants
|
|
37
|
+
|
|
38
|
+
- The sentinel never reaches OpenCode. `resolvePromptBody` rewrites
|
|
39
|
+
`body.model` in place for `POST /api/session/:id/{prompt_async,prompt,command}`
|
|
40
|
+
ahead of the generic proxy (which replays a parsed body), inside the
|
|
41
|
+
message queue's `sendItem`, and in the OpenChamber session service's own
|
|
42
|
+
dispatch (`openchamber-sessions/routes.js`), which posts to OpenCode directly
|
|
43
|
+
and can pick Auto up from Session Defaults. Without a fallback model it
|
|
44
|
+
throws 400 rather than forwarding.
|
|
45
|
+
- Every failure keeps the user's own behaviour. A Jev error, timeout, unknown
|
|
46
|
+
category or low confidence routes to the fallback model; the decision carries
|
|
47
|
+
the reason. A safety-net failure accepts the permission exactly as auto-accept
|
|
48
|
+
would have and broadcasts `openchamber:routing.safety-skipped` with the error.
|
|
49
|
+
- A category without a model uses the fallback model *and* variant; a variant
|
|
50
|
+
only travels with the model it was chosen for. A category agent replaces the
|
|
51
|
+
composer's agent; an empty one keeps it.
|
|
52
|
+
- Auto is offered (`autoReady`) only with the flag, `enabled`, a saved key, a
|
|
53
|
+
fallback model and at least two enabled categories.
|
|
54
|
+
- Held permission decisions are cached for 15 minutes per request id so
|
|
55
|
+
reconnect reconciliation in `permission-auto-accept` does not re-ask Jev;
|
|
56
|
+
`permission.replied` forgets them.
|
|
57
|
+
- The rewrite parses JSON only while the flag is set, so a build without it
|
|
58
|
+
leaves the proxy stream untouched.
|
|
59
|
+
|
|
60
|
+
## Events
|
|
61
|
+
|
|
62
|
+
Broadcast on the OpenChamber control stream: `openchamber:routing.updated`
|
|
63
|
+
(availability), `openchamber:routing.decision` (per send),
|
|
64
|
+
`openchamber:routing.permission-held`, `openchamber:routing.safety-skipped`.
|
|
65
|
+
|
|
66
|
+
## UI
|
|
67
|
+
|
|
68
|
+
`packages/ui/src/stores/useRoutingStore.ts` projects `/api/routing` and these
|
|
69
|
+
events; `hooks/useRoutingSync.ts` keeps it current and shows the skipped-check
|
|
70
|
+
toast. `lib/routing/autoModel.ts` owns the sentinel; `useConfigStore` accepts it
|
|
71
|
+
as a valid selection while `autoReady`. `ModelPickerList` renders it as the
|
|
72
|
+
pinned `leadingEntry`; `ModelControls` hides the agent and thinking controls
|
|
73
|
+
while Auto is selected. `PermissionCard` shows the hold reason. Settings →
|
|
74
|
+
Routing (`components/sections/routing/RoutingPage.tsx`) edits the config with
|
|
75
|
+
debounced saves and manages the key.
|
|
76
|
+
|
|
77
|
+
## Tests
|
|
78
|
+
|
|
79
|
+
`store.test.js` (defaults, deviation round-trip, deleted built-ins, malformed
|
|
80
|
+
file, token file mode), `runtime.test.js` (request text, excerpts, decisions,
|
|
81
|
+
rewrite and fallback paths, safety net hold/skip/off), `routes.http.test.js`
|
|
82
|
+
(rewrite ahead of a stand-in proxy, routes, flag off). The queue and
|
|
83
|
+
auto-accept tests cover their hooks.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in routing configuration. Everything here is the shipped default; the
|
|
3
|
+
* user's `routing.json` stores only deviations from it, so a later change to a
|
|
4
|
+
* built-in category reaches everyone who did not edit that category.
|
|
5
|
+
*
|
|
6
|
+
* Category descriptions are what Jev reads, verbatim, as the criteria of one
|
|
7
|
+
* choice question. Keep them about the reasoning a request demands, never about
|
|
8
|
+
* the length of the reply: that wording moved a long mechanical rename from
|
|
9
|
+
* "hard" to "trivial" in the lab runs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
const AUTO_PROVIDER_ID = 'openchamber';
|
|
15
|
+
const AUTO_MODEL_ID = 'auto';
|
|
16
|
+
|
|
17
|
+
const autoModelSchema = z.object({ providerID: z.literal(AUTO_PROVIDER_ID), modelID: z.literal(AUTO_MODEL_ID) });
|
|
18
|
+
const autoModelStringSchema = z.literal(`${AUTO_PROVIDER_ID}/${AUTO_MODEL_ID}`);
|
|
19
|
+
|
|
20
|
+
/** The command route carries the model as `provider/model`; the prompt routes as an object. */
|
|
21
|
+
export const isAutoModel = (model) => autoModelSchema.safeParse(model).success || autoModelStringSchema.safeParse(model).success;
|
|
22
|
+
|
|
23
|
+
export const JEV_API_ORIGIN = 'https://api.typesafe.ai';
|
|
24
|
+
export const JEV_API_PATH = '/v1/systemone';
|
|
25
|
+
export const JEV_MODEL = 'jev-latest';
|
|
26
|
+
|
|
27
|
+
/** Per-attempt timeout; the lab measured 250–700 ms warm, ~1 s on a cold TLS handshake. */
|
|
28
|
+
export const JEV_TIMEOUT_MS = 4000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The third line keeps a hard turn from dragging its follow-ups up. With a
|
|
32
|
+
* plain "classify request only" wording, "дякую, все працює" after a hard fix
|
|
33
|
+
* came back hard at ~40% and "запуш" trivial at ~50%, both under the threshold,
|
|
34
|
+
* so the fallback model took them. Saying that history only resolves what the
|
|
35
|
+
* request refers to, and that finished work carries no weight, put them at
|
|
36
|
+
* 97% and ~75% trivial while "ok, do it" after a plan stayed implement and a
|
|
37
|
+
* regression report stayed hard (jev-router lab, test/follow-up-instructions.js).
|
|
38
|
+
*/
|
|
39
|
+
export const ROUTING_INSTRUCTIONS = [
|
|
40
|
+
'Pick the task category that best describes this coding request, judging the reasoning it demands rather than the length of the reply it asks for.',
|
|
41
|
+
'A request that wants a one-line answer to a hard debugging or design question is still hard; a long but mechanical edit is still trivial.',
|
|
42
|
+
'The thing to classify is `request`, the latest user message. Read `history` (earlier conversation, oldest first) only to understand what `request` refers to, then judge the reasoning `request` itself demands. Do not let the difficulty of work already done in `history` raise or lower the category.',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Measured on 31 mixed bash/edit/mcp/webfetch permissions: this "should the agent
|
|
47
|
+
* ask" wording scored 28/31 at threshold 0.6 with every miss on the safe side,
|
|
48
|
+
* while "is it destructive" scored 23/31 and let through `brew install`, closing
|
|
49
|
+
* a PR, and edits to ~/.zshrc and ~/.ssh.
|
|
50
|
+
*/
|
|
51
|
+
export const SAFETY_INSTRUCTIONS = [
|
|
52
|
+
'An autonomous coding agent is about to perform this action inside a software project, and the user has allowed routine actions to proceed without asking.',
|
|
53
|
+
'Should the agent stop and ask the user first, instead of proceeding automatically?',
|
|
54
|
+
'Yes when the action is hard to undo, or reaches outside the project working tree (the system, the home directory, remote services, other people), or changes shared state others can see, or discards uncommitted work.',
|
|
55
|
+
"No for reading, searching, building, testing, installing project dependencies, ordinary commits, pushes and rebases of the agent's own branch, and file edits inside the project.",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** Effect kinds reported alongside the hold decision; the UI shows the chosen one as the reason. */
|
|
59
|
+
export const SAFETY_KINDS = {
|
|
60
|
+
read_only: 'Reads, lists, searches, builds, tests, or fetches public information; changes nothing.',
|
|
61
|
+
writes_project: 'Creates or edits files inside the project in a way version control can undo.',
|
|
62
|
+
git_history: 'Rewrites, deletes, or force-pushes git history or branches, or discards uncommitted work.',
|
|
63
|
+
deletes_data: 'Deletes files or data outside version control, drops databases, or wipes directories.',
|
|
64
|
+
system_change: 'Installs, removes, or reconfigures software, services, or system settings.',
|
|
65
|
+
external_side_effect: 'Changes state in an external system: posts, sends, deploys, pays, closes, or deletes something remotely.',
|
|
66
|
+
data_exfiltration: 'Sends local files, secrets, or private data to an external host, or fetches and executes remote code.',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** History excerpts: a user message keeps its head, an answer keeps head and tail. */
|
|
70
|
+
export const HISTORY_LIMITS = { turns: 3, user: 600, answerHead: 300, answerTail: 300 };
|
|
71
|
+
|
|
72
|
+
export const THINKING_LEVELS = ['off', 'low', 'medium', 'high'];
|
|
73
|
+
|
|
74
|
+
export const BUILTIN_CATEGORIES = [
|
|
75
|
+
{
|
|
76
|
+
id: 'trivial',
|
|
77
|
+
name: 'Trivial',
|
|
78
|
+
description: 'Trivial, mechanical, or purely factual work: rename a symbol, fix a typo, reformat, add a comment, answer a short factual question about a known file, run one obvious command and report the output. Not for anything requiring design judgement or multi-file reasoning.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'research',
|
|
82
|
+
name: 'Research',
|
|
83
|
+
description: 'Read and explain existing code or behaviour without changing it: explain how a module works, find where something is implemented, summarise what a change does. Not for requests that ask for edits or a plan.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'implement',
|
|
87
|
+
name: 'Implement',
|
|
88
|
+
description: 'Ordinary engineering with a clear, bounded shape: implement a well-specified function, endpoint, or component; write or fix tests for existing behaviour; a localised bug fix where the cause is already understood. Not for open-ended architecture, subtle concurrency, or unknown-cause debugging.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'hard',
|
|
92
|
+
name: 'Hard',
|
|
93
|
+
description: 'Hard reasoning, ambiguity, planning, or high blast radius: debug a failure whose cause is unknown, plan or design a change across several modules, security, auth, concurrency, data-migration, or money-handling logic. Not for work a competent mid-level engineer would finish without thinking hard.',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export const DEFAULT_MIN_CONFIDENCE = 0.6;
|
|
98
|
+
export const DEFAULT_SAFETY_THRESHOLD = 0.6;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Jev model routing exists at all in this build.
|
|
3
|
+
*
|
|
4
|
+
* The feature ships dark, the same way agent memory does: with the flag unset
|
|
5
|
+
* there is no Auto row in the model picker, no settings page, no routes and no
|
|
6
|
+
* request rewriting. Read per call so only the process environment decides.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
10
|
+
|
|
11
|
+
export const isRoutingFeatureAvailable = () => TRUTHY.has((process.env.OPENCHAMBER_ROUTING_ENABLE ?? '').trim().toLowerCase());
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation Jev sees next to the new request: the last settled turns,
|
|
3
|
+
* read the same way session assist reads them (text parts only, attached
|
|
4
|
+
* quotes included, tool payloads and files never), then cut down per message.
|
|
5
|
+
* A user message keeps its head, where the task usually sits; an answer keeps
|
|
6
|
+
* head and tail, where the finding and the outcome sit. The new request itself
|
|
7
|
+
* is never cut.
|
|
8
|
+
*/
|
|
9
|
+
import { loadAssistContext } from '../session-assist/context.js';
|
|
10
|
+
import { HISTORY_LIMITS } from './defaults.js';
|
|
11
|
+
|
|
12
|
+
export const excerptHead = (text, limit) => (text.length <= limit ? text : `${text.slice(0, limit).trimEnd()} […]`);
|
|
13
|
+
|
|
14
|
+
export const excerptHeadTail = (text, head, tail) =>
|
|
15
|
+
(text.length <= head + tail ? text : `${text.slice(0, head).trimEnd()} […] ${text.slice(-tail).trimStart()}`);
|
|
16
|
+
|
|
17
|
+
export const turnsToHistory = (turns, limits = HISTORY_LIMITS) => {
|
|
18
|
+
const history = [];
|
|
19
|
+
for (const turn of turns.slice(-limits.turns)) {
|
|
20
|
+
if (turn.user?.text) history.push({ role: 'user', text: excerptHead(turn.user.text, limits.user) });
|
|
21
|
+
if (turn.assistant?.text) history.push({ role: 'assistant', text: excerptHeadTail(turn.assistant.text, limits.answerHead, limits.answerTail) });
|
|
22
|
+
}
|
|
23
|
+
return history;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Empty when the session has no settled answer yet (a new session, or one
|
|
28
|
+
* interrupted mid-turn) — routing then judges the request on its own. A read
|
|
29
|
+
* failure is thrown so the caller can decide; it is not an empty history.
|
|
30
|
+
*/
|
|
31
|
+
export const loadRoutingHistory = async ({ readPage, signal }) => {
|
|
32
|
+
const context = await loadAssistContext({ readPage, signal });
|
|
33
|
+
return context ? turnsToHistory(context.turns) : [];
|
|
34
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jev side of routing: request shapes and the HTTP call. Pure builders are
|
|
3
|
+
* exported so the decision logic can be tested without a network.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import {
|
|
7
|
+
JEV_API_ORIGIN,
|
|
8
|
+
JEV_API_PATH,
|
|
9
|
+
JEV_MODEL,
|
|
10
|
+
JEV_TIMEOUT_MS,
|
|
11
|
+
ROUTING_INSTRUCTIONS,
|
|
12
|
+
SAFETY_INSTRUCTIONS,
|
|
13
|
+
SAFETY_KINDS,
|
|
14
|
+
} from './defaults.js';
|
|
15
|
+
|
|
16
|
+
export const buildRoutingRequest = ({ categories, history, request }) => {
|
|
17
|
+
const criteria = {};
|
|
18
|
+
for (const category of categories) criteria[category.id] = category.description;
|
|
19
|
+
return {
|
|
20
|
+
model: JEV_MODEL,
|
|
21
|
+
state: { history, request },
|
|
22
|
+
questions: { category: { type: 'choice', instructions: ROUTING_INSTRUCTIONS, criteria } },
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** `permission` is what OpenCode reported: the tool kind, its patterns and its metadata. */
|
|
27
|
+
export const buildPermissionRequest = (permission) => ({
|
|
28
|
+
model: JEV_MODEL,
|
|
29
|
+
state: {
|
|
30
|
+
permission: {
|
|
31
|
+
type: permission.permission,
|
|
32
|
+
patterns: permission.patterns,
|
|
33
|
+
metadata: permission.metadata,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
questions: {
|
|
37
|
+
ask: { type: 'noul', instructions: SAFETY_INSTRUCTIONS },
|
|
38
|
+
kind: { type: 'choice', instructions: 'What is the most significant effect of this action?', criteria: SAFETY_KINDS },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const choiceAnswerSchema = z.object({ choice: z.string(), confidence: z.number() });
|
|
43
|
+
const noulAnswerSchema = z.object({ noul: z.number() });
|
|
44
|
+
const permissionAnswersSchema = z.object({
|
|
45
|
+
ask: noulAnswerSchema,
|
|
46
|
+
kind: z.object({ choice: z.string() }).partial().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Maps a Jev answer onto the category to use. Anything short of a confident,
|
|
51
|
+
* known, enabled category is the fallback; the reason says which.
|
|
52
|
+
*/
|
|
53
|
+
export const decideRouting = (answer, { categories, minConfidence }) => {
|
|
54
|
+
const parsed = choiceAnswerSchema.safeParse(answer);
|
|
55
|
+
if (!parsed.success) return { category: null, reason: 'unknown-category', confidence: 0 };
|
|
56
|
+
const category = categories.find((c) => c.id === parsed.data.choice) ?? null;
|
|
57
|
+
if (!category) return { category: null, reason: 'unknown-category', confidence: parsed.data.confidence };
|
|
58
|
+
if (parsed.data.confidence < minConfidence) return { category: null, reason: 'low-confidence', confidence: parsed.data.confidence };
|
|
59
|
+
return { category, reason: 'routed', confidence: parsed.data.confidence };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const decidePermission = (answers, { threshold }) => {
|
|
63
|
+
const parsed = permissionAnswersSchema.safeParse(answers);
|
|
64
|
+
if (!parsed.success) throw new Error('Jev answer is missing the ask score');
|
|
65
|
+
const score = parsed.data.ask.noul;
|
|
66
|
+
return { hold: score >= threshold, score, kind: parsed.data.kind?.choice ?? null };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const responseSchema = z.object({ answers: z.record(z.string(), z.unknown()) });
|
|
70
|
+
|
|
71
|
+
export const createJevClient = ({ fetchImpl = fetch, timeoutMs = JEV_TIMEOUT_MS } = {}) => ({
|
|
72
|
+
/** Resolves to the parsed answers; throws with `status` on an HTTP error and `code: 'timeout'` on abort. */
|
|
73
|
+
ask: async (request, token) => {
|
|
74
|
+
const abort = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
76
|
+
const started = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchImpl(JEV_API_ORIGIN + JEV_API_PATH, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
81
|
+
body: JSON.stringify(request),
|
|
82
|
+
signal: abort.signal,
|
|
83
|
+
});
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw Object.assign(new Error(`Jev responded ${response.status}`), { status: response.status });
|
|
87
|
+
}
|
|
88
|
+
const body = responseSchema.safeParse(JSON.parse(text));
|
|
89
|
+
if (!body.success) throw new Error('Jev response has no answers');
|
|
90
|
+
return { answers: body.data.answers, ms: Date.now() - started };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error?.name === 'AbortError') throw Object.assign(new Error(`Jev timed out after ${timeoutMs}ms`), { code: 'timeout' });
|
|
93
|
+
throw error;
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { registerRoutingPromptRewrite, registerRoutingRoutes } from './routes.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The prompt rewrite sits in front of the generic OpenCode proxy, which
|
|
9
|
+
* replays `req.body` when a middleware parsed it. These tests mount the
|
|
10
|
+
* rewrite ahead of a stand-in proxy that records what it would forward.
|
|
11
|
+
*/
|
|
12
|
+
const createApp = ({ flag = '1', resolvePromptBody } = {}) => {
|
|
13
|
+
process.env.OPENCHAMBER_ROUTING_ENABLE = flag;
|
|
14
|
+
const forwarded = [];
|
|
15
|
+
const runtime = {
|
|
16
|
+
resolvePromptBody: resolvePromptBody ?? vi.fn(async (body) => {
|
|
17
|
+
if (body.model?.modelID === 'auto') body.model = { providerID: 'openai', modelID: 'gpt-6-astra' };
|
|
18
|
+
return null;
|
|
19
|
+
}),
|
|
20
|
+
describe: async () => ({ available: true, autoReady: true, tokenPresent: true, config: null, builtins: [] }),
|
|
21
|
+
heldPermissions: () => [],
|
|
22
|
+
updateConfig: vi.fn(async () => ({ available: true })),
|
|
23
|
+
setToken: vi.fn(async () => ({ available: true, tokenPresent: true })),
|
|
24
|
+
clearToken: vi.fn(async () => ({ available: true, tokenPresent: false })),
|
|
25
|
+
};
|
|
26
|
+
const app = express();
|
|
27
|
+
registerRoutingRoutes(app, runtime);
|
|
28
|
+
registerRoutingPromptRewrite(app, runtime);
|
|
29
|
+
// Stand-in for the OpenCode proxy: a parsed body arrives as `req.body`, an
|
|
30
|
+
// untouched stream arrives as raw bytes.
|
|
31
|
+
app.use('/api', (req, res) => {
|
|
32
|
+
if (req.body !== undefined) {
|
|
33
|
+
forwarded.push({ path: req.path, parsed: true, body: req.body });
|
|
34
|
+
return res.status(204).end();
|
|
35
|
+
}
|
|
36
|
+
let raw = '';
|
|
37
|
+
req.on('data', (chunk) => { raw += chunk; });
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
forwarded.push({ path: req.path, parsed: false, raw });
|
|
40
|
+
res.status(204).end();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return { app, runtime, forwarded };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
delete process.env.OPENCHAMBER_ROUTING_ENABLE;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('routing prompt rewrite', () => {
|
|
51
|
+
it('rewrites the Auto sentinel on prompt_async and passes the directory along', async () => {
|
|
52
|
+
const { app, runtime, forwarded } = createApp();
|
|
53
|
+
await request(app)
|
|
54
|
+
.post('/api/session/s1/prompt_async?directory=%2Frepo')
|
|
55
|
+
.send({ model: { providerID: 'openchamber', modelID: 'auto' }, parts: [{ type: 'text', text: 'hi' }] })
|
|
56
|
+
.expect(204);
|
|
57
|
+
expect(forwarded).toEqual([{ path: '/session/s1/prompt_async', parsed: true, body: { model: { providerID: 'openai', modelID: 'gpt-6-astra' }, parts: [{ type: 'text', text: 'hi' }] } }]);
|
|
58
|
+
expect(runtime.resolvePromptBody).toHaveBeenCalledWith(expect.anything(), { sessionId: 's1', directory: '/repo' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('leaves the stream untouched when the flag is off or the body is not JSON', async () => {
|
|
62
|
+
const off = createApp({ flag: '' });
|
|
63
|
+
await request(off.app).post('/api/session/s1/prompt').send({ model: { providerID: 'openchamber', modelID: 'auto' } }).expect(204);
|
|
64
|
+
expect(off.forwarded[0].parsed).toBe(false);
|
|
65
|
+
expect(off.runtime.resolvePromptBody).not.toHaveBeenCalled();
|
|
66
|
+
|
|
67
|
+
const text = createApp();
|
|
68
|
+
await request(text.app).post('/api/session/s1/command').set('content-type', 'text/plain').send('raw').expect(204);
|
|
69
|
+
expect(text.forwarded[0]).toEqual({ path: '/session/s1/command', parsed: false, raw: 'raw' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('answers with the runtime error instead of forwarding an unresolved sentinel', async () => {
|
|
73
|
+
const { app, forwarded } = createApp({
|
|
74
|
+
resolvePromptBody: vi.fn(async () => { throw Object.assign(new Error('no fallback'), { status: 400 }); }),
|
|
75
|
+
});
|
|
76
|
+
const response = await request(app).post('/api/session/s1/prompt_async').send({ model: { providerID: 'openchamber', modelID: 'auto' } });
|
|
77
|
+
expect(response.status).toBe(400);
|
|
78
|
+
expect(response.body).toEqual({ error: 'no fallback' });
|
|
79
|
+
expect(forwarded).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('routing routes', () => {
|
|
84
|
+
it('serves state, saves config and manages the token', async () => {
|
|
85
|
+
const { app, runtime } = createApp();
|
|
86
|
+
const state = await request(app).get('/api/routing').expect(200);
|
|
87
|
+
expect(state.body).toMatchObject({ available: true, autoReady: true, heldPermissions: [] });
|
|
88
|
+
await request(app).put('/api/routing').send({ config: { enabled: true } }).expect(200);
|
|
89
|
+
expect(runtime.updateConfig).toHaveBeenCalledWith({ enabled: true });
|
|
90
|
+
await request(app).put('/api/routing/token').send({ token: 'ts-key' }).expect(200);
|
|
91
|
+
expect(runtime.setToken).toHaveBeenCalledWith('ts-key');
|
|
92
|
+
await request(app).delete('/api/routing/token').expect(200);
|
|
93
|
+
expect(runtime.clearToken).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('is absent without the feature flag', async () => {
|
|
97
|
+
const { app } = createApp({ flag: '' });
|
|
98
|
+
await request(app).put('/api/routing/token').send({ token: 'x' }).expect(404);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/routing` — configuration and the Jev key. Normal authenticated
|
|
3
|
+
* OpenChamber routes: do not add them to browser URL-token allowlists.
|
|
4
|
+
*
|
|
5
|
+
* `/api/session/:id/{prompt_async,prompt,command}` — the rewrite that turns
|
|
6
|
+
* `openchamber/auto` into a real model before the generic OpenCode proxy
|
|
7
|
+
* forwards the request. Registered ahead of the proxy; it parses the JSON body
|
|
8
|
+
* only while Auto can actually be selected, so a build without the flag pays
|
|
9
|
+
* nothing on the send path.
|
|
10
|
+
*/
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import { isRoutingFeatureAvailable } from './feature-flag.js';
|
|
13
|
+
|
|
14
|
+
const AUTO_SESSION_PATHS = [
|
|
15
|
+
'/api/session/:sessionId/prompt_async',
|
|
16
|
+
'/api/session/:sessionId/prompt',
|
|
17
|
+
'/api/session/:sessionId/command',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const sendError = (res, error) => {
|
|
21
|
+
const status = Number.isInteger(error?.status) ? error.status : 500;
|
|
22
|
+
res.status(status).json({ error: error?.message ?? 'Routing request failed' });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function registerRoutingRoutes(app, runtime) {
|
|
26
|
+
const unavailable = (res) => res.status(404).json({ error: 'Routing is not available in this build' });
|
|
27
|
+
|
|
28
|
+
app.get('/api/routing', async (_req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const state = await runtime.describe();
|
|
31
|
+
if (!state.available) return unavailable(res);
|
|
32
|
+
res.json({ ...state, heldPermissions: runtime.heldPermissions() });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
sendError(res, error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.put('/api/routing', express.json({ limit: '256kb' }), async (req, res) => {
|
|
39
|
+
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
40
|
+
try {
|
|
41
|
+
res.json(await runtime.updateConfig(req.body?.config));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
sendError(res, error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.put('/api/routing/token', express.json({ limit: '16kb' }), async (req, res) => {
|
|
48
|
+
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
49
|
+
try {
|
|
50
|
+
res.json(await runtime.setToken(req.body?.token));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
sendError(res, error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.delete('/api/routing/token', async (_req, res) => {
|
|
57
|
+
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
58
|
+
try {
|
|
59
|
+
res.json(await runtime.clearToken());
|
|
60
|
+
} catch (error) {
|
|
61
|
+
sendError(res, error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerRoutingPromptRewrite(app, runtime) {
|
|
67
|
+
const parseJson = express.json({ limit: '50mb' });
|
|
68
|
+
app.post(AUTO_SESSION_PATHS, (req, res, next) => {
|
|
69
|
+
if (!isRoutingFeatureAvailable()) return next();
|
|
70
|
+
const contentType = String(req.headers['content-type'] ?? '').toLowerCase();
|
|
71
|
+
if (!contentType.includes('application/json')) return next();
|
|
72
|
+
parseJson(req, res, (parseError) => {
|
|
73
|
+
if (parseError) return next(parseError);
|
|
74
|
+
const url = new URL(req.url, 'http://localhost');
|
|
75
|
+
const directory = url.searchParams.get('directory') || req.get('x-opencode-directory') || undefined;
|
|
76
|
+
runtime.resolvePromptBody(req.body, { sessionId: req.params.sessionId, directory })
|
|
77
|
+
.then(() => next())
|
|
78
|
+
.catch((error) => sendError(res, error));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|