@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,334 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { findInstalledGuest, listInstalledGuests } from './catalog.js';
|
|
8
|
+
import { isGitRef, parseGitInstallUrl } from './clone.js';
|
|
9
|
+
import { installGuestFromGitSource } from './install.js';
|
|
10
|
+
import { readExtensionStore } from './persist.js';
|
|
11
|
+
import {
|
|
12
|
+
UPDATE_CHECK_TTL_MS,
|
|
13
|
+
checkAllGuestUpdates,
|
|
14
|
+
checkGuestUpdate,
|
|
15
|
+
clearGuestUpdateCache,
|
|
16
|
+
compareSemver,
|
|
17
|
+
getCachedGuestUpdate,
|
|
18
|
+
updateGuest,
|
|
19
|
+
} from './updates.js';
|
|
20
|
+
|
|
21
|
+
const git = (cwd, args) => {
|
|
22
|
+
const result = spawnSync('git', args, {
|
|
23
|
+
cwd,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0) {
|
|
28
|
+
throw new Error(result.stderr || `git ${args.join(' ')} failed`);
|
|
29
|
+
}
|
|
30
|
+
return result.stdout;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const manifest = (id, version, extra = {}) => JSON.stringify({
|
|
34
|
+
name: `@openchamber/${id}`,
|
|
35
|
+
version,
|
|
36
|
+
openchamber: {
|
|
37
|
+
apiVersion: 1,
|
|
38
|
+
contributes: {
|
|
39
|
+
panel: { id, name: id, icon: 'window', entry: 'panel/index.html' },
|
|
40
|
+
...extra,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const writeGuest = async (root, id, version, extra) => {
|
|
46
|
+
await fs.mkdir(path.join(root, 'panel'), { recursive: true });
|
|
47
|
+
await fs.writeFile(path.join(root, 'panel', 'index.html'), '<html></html>');
|
|
48
|
+
await fs.writeFile(path.join(root, 'package.json'), manifest(id, version, extra));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const commit = (cwd, message) => {
|
|
52
|
+
git(cwd, ['add', '.']);
|
|
53
|
+
git(cwd, ['-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', '-q', '-m', message]);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A working repo plus a bare "remote" the install clones from, the way a
|
|
58
|
+
* hosted repository behaves: the bare repo has no working tree and gets new
|
|
59
|
+
* commits pushed to it.
|
|
60
|
+
*/
|
|
61
|
+
const setupRemote = async (dir, id, version = '1.0.0') => {
|
|
62
|
+
const work = path.join(dir, 'work');
|
|
63
|
+
const bare = path.join(dir, 'remote.git');
|
|
64
|
+
await writeGuest(work, id, version);
|
|
65
|
+
git(work, ['init', '-q', '--template=', '-b', 'main']);
|
|
66
|
+
commit(work, 'init');
|
|
67
|
+
// The bare repo's HEAD must name the branch that gets pushed; without -b
|
|
68
|
+
// it follows the machine's init.defaultBranch (master on CI), and a clone
|
|
69
|
+
// of a bare repo whose HEAD points at a missing branch checks out nothing.
|
|
70
|
+
git(dir, ['init', '-q', '--bare', '--template=', '-b', 'main', bare]);
|
|
71
|
+
git(work, ['remote', 'add', 'origin', bare]);
|
|
72
|
+
git(work, ['push', '-q', 'origin', 'main']);
|
|
73
|
+
return { work, bare };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const pushVersion = async (work, id, version, extra) => {
|
|
77
|
+
await fs.writeFile(path.join(work, 'package.json'), manifest(id, version, extra));
|
|
78
|
+
commit(work, `v${version}`);
|
|
79
|
+
git(work, ['push', '-q', 'origin', 'HEAD']);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const installFrom = async (bare, persistPath, ref) => {
|
|
83
|
+
const installed = await installGuestFromGitSource(bare, persistPath, { openchamberVersion: '1.30.0', ref });
|
|
84
|
+
if (!installed.ok) {
|
|
85
|
+
throw new Error(`install failed: ${installed.code}`);
|
|
86
|
+
}
|
|
87
|
+
return installed.guest;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const loadGuest = async (id, persistPath) => {
|
|
91
|
+
const guest = await findInstalledGuest(id, persistPath);
|
|
92
|
+
if (!guest) {
|
|
93
|
+
throw new Error(`guest ${id} missing`);
|
|
94
|
+
}
|
|
95
|
+
return guest;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('compareSemver', () => {
|
|
99
|
+
test('orders releases and puts prereleases below their release', () => {
|
|
100
|
+
expect(compareSemver('1.0.0', '1.0.0')).toBe(0);
|
|
101
|
+
expect(compareSemver('1.0.0', '1.0.1')).toBe(-1);
|
|
102
|
+
expect(compareSemver('1.10.0', '1.9.0')).toBe(1);
|
|
103
|
+
expect(compareSemver('2.0.0', '1.99.99')).toBe(1);
|
|
104
|
+
expect(compareSemver('1.1.0-beta.1', '1.1.0')).toBe(-1);
|
|
105
|
+
expect(compareSemver('1.1.0', '1.1.0-rc.1')).toBe(1);
|
|
106
|
+
expect(compareSemver('1.1.0-alpha', '1.1.0-beta')).toBe(-1);
|
|
107
|
+
expect(compareSemver('1.1.0-beta.2', '1.1.0-beta.10')).toBe(-1);
|
|
108
|
+
expect(compareSemver('1.1.0-beta', '1.1.0-beta.1')).toBe(-1);
|
|
109
|
+
expect(compareSemver('1.0.0+build.1', '1.0.0')).toBe(0);
|
|
110
|
+
expect(compareSemver('v1.0.0', '1.0.0')).toBeNull();
|
|
111
|
+
expect(compareSemver('1.0', '1.0.0')).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('parseGitInstallUrl', () => {
|
|
116
|
+
test('splits a #ref and refuses unusable refs', () => {
|
|
117
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel.git')).toEqual({ url: 'https://github.com/acme/panel.git' });
|
|
118
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel.git#v1.2.0')).toEqual({
|
|
119
|
+
url: 'https://github.com/acme/panel.git',
|
|
120
|
+
ref: 'v1.2.0',
|
|
121
|
+
});
|
|
122
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel#release/2.x')).toEqual({
|
|
123
|
+
url: 'https://github.com/acme/panel',
|
|
124
|
+
ref: 'release/2.x',
|
|
125
|
+
});
|
|
126
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel.git#')).toBeNull();
|
|
127
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel.git#--upload-pack=evil')).toBeNull();
|
|
128
|
+
expect(parseGitInstallUrl('https://github.com/acme/panel.git#a..b')).toBeNull();
|
|
129
|
+
expect(parseGitInstallUrl('https://localhost/panel.git#main')).toBeNull();
|
|
130
|
+
expect(parseGitInstallUrl('http://github.com/acme/panel.git#main')).toBeNull();
|
|
131
|
+
expect(isGitRef('main')).toBe(true);
|
|
132
|
+
expect(isGitRef('-x')).toBe(false);
|
|
133
|
+
expect(isGitRef('feature/')).toBe(false);
|
|
134
|
+
expect(isGitRef('a//b')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('checkGuestUpdate', () => {
|
|
139
|
+
test('reports a newer remote version and stays quiet on the same one', async () => {
|
|
140
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-upd-'));
|
|
141
|
+
const persistPath = path.join(dir, 'data', 'extensions.json');
|
|
142
|
+
const { work, bare } = await setupRemote(dir, 'upd-hello');
|
|
143
|
+
await installFrom(bare, persistPath);
|
|
144
|
+
const guest = await loadGuest('upd-hello', persistPath);
|
|
145
|
+
expect(guest.gitOrigin).toEqual({ url: bare });
|
|
146
|
+
expect((await readExtensionStore(persistPath)).gitOrigins).toEqual({ [guest.packageRoot]: { url: bare } });
|
|
147
|
+
|
|
148
|
+
const same = await checkGuestUpdate({ guest, origin: guest.gitOrigin });
|
|
149
|
+
expect(same).toEqual({ available: false, version: '1.0.0' });
|
|
150
|
+
|
|
151
|
+
await pushVersion(work, 'upd-hello', '1.1.0', { capabilities: ['prompt'], filesystem: ['~/notes/**'] });
|
|
152
|
+
const newer = await checkGuestUpdate({ guest, origin: guest.gitOrigin });
|
|
153
|
+
expect(newer).toEqual({ available: true, version: '1.1.0', requested: ['prompt', 'filesystem'] });
|
|
154
|
+
|
|
155
|
+
await pushVersion(work, 'upd-hello', '1.0.1');
|
|
156
|
+
const older = await checkGuestUpdate({ guest: { ...guest, version: '1.2.0' }, origin: guest.gitOrigin });
|
|
157
|
+
expect(older).toEqual({ available: false, version: '1.0.1' });
|
|
158
|
+
|
|
159
|
+
await pushVersion(work, 'upd-hello', '2.0.0-beta.1');
|
|
160
|
+
const pre = await checkGuestUpdate({ guest, origin: guest.gitOrigin });
|
|
161
|
+
expect(pre).toEqual({ available: true, version: '2.0.0-beta.1', requested: [] });
|
|
162
|
+
|
|
163
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('follows a pinned tag and never throws on a broken remote', async () => {
|
|
167
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-upd-'));
|
|
168
|
+
const persistPath = path.join(dir, 'data', 'extensions.json');
|
|
169
|
+
const { work, bare } = await setupRemote(dir, 'upd-tag');
|
|
170
|
+
git(work, ['tag', 'stable']);
|
|
171
|
+
git(work, ['push', '-q', 'origin', 'stable']);
|
|
172
|
+
await installFrom(bare, persistPath, 'stable');
|
|
173
|
+
const guest = await loadGuest('upd-tag', persistPath);
|
|
174
|
+
expect(guest.gitOrigin).toEqual({ url: bare, ref: 'stable' });
|
|
175
|
+
|
|
176
|
+
// main moves ahead; the tag does not.
|
|
177
|
+
await pushVersion(work, 'upd-tag', '1.5.0');
|
|
178
|
+
expect(await checkGuestUpdate({ guest, origin: guest.gitOrigin })).toEqual({ available: false, version: '1.0.0' });
|
|
179
|
+
|
|
180
|
+
git(work, ['tag', '-f', 'stable']);
|
|
181
|
+
git(work, ['push', '-q', '-f', 'origin', 'stable']);
|
|
182
|
+
expect(await checkGuestUpdate({ guest, origin: guest.gitOrigin })).toEqual({ available: true, version: '1.5.0', requested: [] });
|
|
183
|
+
|
|
184
|
+
await fs.rm(bare, { recursive: true, force: true });
|
|
185
|
+
expect(await checkGuestUpdate({ guest, origin: guest.gitOrigin })).toEqual({ available: false, error: 'fetch-failed' });
|
|
186
|
+
expect(await checkGuestUpdate({ guest, origin: undefined })).toEqual({ available: false, error: 'not-git' });
|
|
187
|
+
expect(await checkGuestUpdate({ guest, origin: { url: bare }, gitBinary: '/nonexistent/git' })).toEqual({ available: false, error: 'fetch-failed' });
|
|
188
|
+
|
|
189
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('checkAllGuestUpdates', () => {
|
|
194
|
+
test('caches per guest for an hour unless forced and feeds the catalog row', async () => {
|
|
195
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-upd-'));
|
|
196
|
+
const persistPath = path.join(dir, 'data', 'extensions.json');
|
|
197
|
+
const { work, bare } = await setupRemote(dir, 'upd-cache');
|
|
198
|
+
await installFrom(bare, persistPath);
|
|
199
|
+
const start = Date.now();
|
|
200
|
+
|
|
201
|
+
expect(await checkAllGuestUpdates({ persistPath, now: start })).toEqual({});
|
|
202
|
+
await pushVersion(work, 'upd-cache', '1.1.0');
|
|
203
|
+
// Inside the hour: the stale answer stands.
|
|
204
|
+
expect(await checkAllGuestUpdates({ persistPath, now: start + UPDATE_CHECK_TTL_MS - 1 })).toEqual({});
|
|
205
|
+
expect(getCachedGuestUpdate(persistPath, 'upd-cache')).toBeNull();
|
|
206
|
+
// Forced: the network is asked again.
|
|
207
|
+
expect(await checkAllGuestUpdates({ persistPath, force: true, now: start + 1 })).toEqual({ 'upd-cache': { version: '1.1.0' } });
|
|
208
|
+
expect(getCachedGuestUpdate(persistPath, 'upd-cache')).toEqual({ version: '1.1.0' });
|
|
209
|
+
// After the hour: refreshed on its own.
|
|
210
|
+
await pushVersion(work, 'upd-cache', '1.2.0');
|
|
211
|
+
expect(await checkAllGuestUpdates({ persistPath, now: start + 1 + UPDATE_CHECK_TTL_MS })).toEqual({ 'upd-cache': { version: '1.2.0' } });
|
|
212
|
+
|
|
213
|
+
clearGuestUpdateCache(persistPath, 'upd-cache');
|
|
214
|
+
expect(getCachedGuestUpdate(persistPath, 'upd-cache')).toBeNull();
|
|
215
|
+
|
|
216
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('updateGuest', () => {
|
|
221
|
+
test('swaps in the new clone and keeps grants, then reports a new version', async () => {
|
|
222
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-upd-'));
|
|
223
|
+
const persistPath = path.join(dir, 'data', 'extensions.json');
|
|
224
|
+
const { work, bare } = await setupRemote(dir, 'upd-swap');
|
|
225
|
+
await installFrom(bare, persistPath);
|
|
226
|
+
const before = await loadGuest('upd-swap', persistPath);
|
|
227
|
+
const store = await readExtensionStore(persistPath);
|
|
228
|
+
await fs.writeFile(persistPath, `${JSON.stringify({
|
|
229
|
+
paths: store.paths,
|
|
230
|
+
sources: store.sources,
|
|
231
|
+
gitOrigins: store.gitOrigins,
|
|
232
|
+
capabilityGrants: { 'upd-swap': ['prompt'] },
|
|
233
|
+
disabledGuests: { 'upd-swap': true },
|
|
234
|
+
})}\n`);
|
|
235
|
+
|
|
236
|
+
await pushVersion(work, 'upd-swap', '1.1.0', { capabilities: ['prompt', 'sessions'] });
|
|
237
|
+
await checkAllGuestUpdates({ persistPath, force: true });
|
|
238
|
+
expect(getCachedGuestUpdate(persistPath, 'upd-swap')).toEqual({ version: '1.1.0' });
|
|
239
|
+
|
|
240
|
+
const updated = await updateGuest({ guest: before, origin: before.gitOrigin, persistPath, openchamberVersion: '1.30.0' });
|
|
241
|
+
expect(updated).toEqual({ ok: true, id: 'upd-swap' });
|
|
242
|
+
|
|
243
|
+
const after = await loadGuest('upd-swap', persistPath);
|
|
244
|
+
expect(after.version).toBe('1.1.0');
|
|
245
|
+
expect(after.packageRoot).toBe(before.packageRoot);
|
|
246
|
+
expect(after.gitOrigin).toEqual({ url: bare });
|
|
247
|
+
expect(after.capabilityGrants).toEqual(['prompt']);
|
|
248
|
+
expect(after.enabled).toBe(false);
|
|
249
|
+
expect(after.capabilities).toEqual(['prompt', 'sessions']);
|
|
250
|
+
expect(getCachedGuestUpdate(persistPath, 'upd-swap')).toBeNull();
|
|
251
|
+
const leftovers = (await fs.readdir(path.dirname(before.packageRoot))).filter((name) => name.startsWith('.'));
|
|
252
|
+
expect(leftovers).toEqual([]);
|
|
253
|
+
expect((await readExtensionStore(persistPath)).paths).toEqual([before.packageRoot]);
|
|
254
|
+
|
|
255
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('a failure before the swap leaves the install untouched', async () => {
|
|
259
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-upd-'));
|
|
260
|
+
const persistPath = path.join(dir, 'data', 'extensions.json');
|
|
261
|
+
const { work, bare } = await setupRemote(dir, 'upd-keep');
|
|
262
|
+
await installFrom(bare, persistPath);
|
|
263
|
+
const guest = await loadGuest('upd-keep', persistPath);
|
|
264
|
+
const copiesDir = path.dirname(guest.packageRoot);
|
|
265
|
+
const noLeftovers = async () => {
|
|
266
|
+
const names = (await fs.readdir(copiesDir)).filter((name) => name.startsWith('.'));
|
|
267
|
+
expect(names).toEqual([]);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Remote now needs a host this build does not have.
|
|
271
|
+
await fs.writeFile(path.join(work, 'package.json'), JSON.stringify({
|
|
272
|
+
name: '@openchamber/upd-keep',
|
|
273
|
+
version: '1.1.0',
|
|
274
|
+
openchamber: {
|
|
275
|
+
apiVersion: 1,
|
|
276
|
+
engines: { openchamber: '>=9.9.9' },
|
|
277
|
+
contributes: { panel: { id: 'upd-keep', name: 'Keep', icon: 'window', entry: 'panel/index.html' } },
|
|
278
|
+
},
|
|
279
|
+
}));
|
|
280
|
+
commit(work, 'needs newer host');
|
|
281
|
+
git(work, ['push', '-q', 'origin', 'HEAD']);
|
|
282
|
+
expect(await updateGuest({ guest, origin: guest.gitOrigin, persistPath, openchamberVersion: '1.30.0' })).toEqual({
|
|
283
|
+
ok: false,
|
|
284
|
+
code: 'host-too-old',
|
|
285
|
+
required: '9.9.9',
|
|
286
|
+
});
|
|
287
|
+
await noLeftovers();
|
|
288
|
+
expect((await loadGuest('upd-keep', persistPath)).version).toBe('1.0.0');
|
|
289
|
+
|
|
290
|
+
// Remote now ships an unbuilt panel.
|
|
291
|
+
await fs.writeFile(path.join(work, 'package.json'), manifest('upd-keep', '1.2.0'));
|
|
292
|
+
await fs.writeFile(path.join(work, 'panel', 'index.html'), '<script src="./main.js"></script>');
|
|
293
|
+
commit(work, 'unbuilt');
|
|
294
|
+
git(work, ['push', '-q', 'origin', 'HEAD']);
|
|
295
|
+
expect(await updateGuest({ guest, origin: guest.gitOrigin, persistPath, openchamberVersion: '1.30.0' })).toEqual({
|
|
296
|
+
ok: false,
|
|
297
|
+
code: 'missing-build',
|
|
298
|
+
});
|
|
299
|
+
await noLeftovers();
|
|
300
|
+
|
|
301
|
+
// Remote renamed the panel: not an update of this extension.
|
|
302
|
+
await fs.writeFile(path.join(work, 'package.json'), manifest('other-id', '1.3.0'));
|
|
303
|
+
await fs.writeFile(path.join(work, 'panel', 'index.html'), '<html></html>');
|
|
304
|
+
commit(work, 'renamed');
|
|
305
|
+
git(work, ['push', '-q', 'origin', 'HEAD']);
|
|
306
|
+
expect(await updateGuest({ guest, origin: guest.gitOrigin, persistPath, openchamberVersion: '1.30.0' })).toEqual({
|
|
307
|
+
ok: false,
|
|
308
|
+
code: 'invalid-manifest',
|
|
309
|
+
});
|
|
310
|
+
await noLeftovers();
|
|
311
|
+
|
|
312
|
+
// Remote is gone.
|
|
313
|
+
await fs.rm(bare, { recursive: true, force: true });
|
|
314
|
+
expect(await updateGuest({ guest, origin: guest.gitOrigin, persistPath, openchamberVersion: '1.30.0' })).toEqual({
|
|
315
|
+
ok: false,
|
|
316
|
+
code: 'clone-failed',
|
|
317
|
+
});
|
|
318
|
+
await noLeftovers();
|
|
319
|
+
|
|
320
|
+
// Folder installs never update.
|
|
321
|
+
expect(await updateGuest({ guest: { ...guest, source: 'path' }, origin: guest.gitOrigin, persistPath })).toEqual({
|
|
322
|
+
ok: false,
|
|
323
|
+
code: 'not-git',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const still = await loadGuest('upd-keep', persistPath);
|
|
327
|
+
expect(still.version).toBe('1.0.0');
|
|
328
|
+
expect(still.packageRoot).toBe(guest.packageRoot);
|
|
329
|
+
expect(await fs.readFile(path.join(guest.packageRoot, 'panel', 'index.html'), 'utf8')).toBe('<html></html>');
|
|
330
|
+
expect((await listInstalledGuests({ persistPath })).map((entry) => entry.id)).toEqual(['upd-keep']);
|
|
331
|
+
|
|
332
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body handling for `POST /api/guests/upload`: a raw zip sent as
|
|
3
|
+
* `application/octet-stream`. The archive is collected into memory (the
|
|
4
|
+
* install path works on a buffer) under a configurable cap, so a remote or
|
|
5
|
+
* web user can add an extension without a path on the server. Nothing from
|
|
6
|
+
* the archive is ever logged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
export const guestUploadMaxBytes = () => {
|
|
12
|
+
const raw = Number(process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES);
|
|
13
|
+
if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
|
|
14
|
+
return DEFAULT_UPLOAD_MAX_BYTES;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isOctetStream = (contentType) => (
|
|
18
|
+
String(contentType || '').toLowerCase().startsWith('application/octet-stream')
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads the upload body. `req` is the Express request (headers plus an
|
|
23
|
+
* async-iterable body). Answers `{ ok: true, buffer }`, or `{ ok: false,
|
|
24
|
+
* status, error }` with 415 for a wrong content type and 413 when either the
|
|
25
|
+
* declared `Content-Length` or the bytes actually received pass `maxBytes`.
|
|
26
|
+
* The declared-size refusal drains the request so the socket stays reusable.
|
|
27
|
+
*/
|
|
28
|
+
export const readGuestUploadBody = async (req, maxBytes) => {
|
|
29
|
+
if (!isOctetStream(req.headers?.['content-type'])) {
|
|
30
|
+
req.resume?.();
|
|
31
|
+
return { ok: false, status: 415, error: 'unsupported-media-type' };
|
|
32
|
+
}
|
|
33
|
+
const declaredSize = Number(req.headers?.['content-length']);
|
|
34
|
+
if (Number.isFinite(declaredSize) && declaredSize > maxBytes) {
|
|
35
|
+
req.resume?.();
|
|
36
|
+
return { ok: false, status: 413, error: 'too-large' };
|
|
37
|
+
}
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let received = 0;
|
|
40
|
+
for await (const chunk of req) {
|
|
41
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
+
received += buffer.length;
|
|
43
|
+
if (received > maxBytes) {
|
|
44
|
+
req.resume?.();
|
|
45
|
+
return { ok: false, status: 413, error: 'too-large' };
|
|
46
|
+
}
|
|
47
|
+
chunks.push(buffer);
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, buffer: Buffer.concat(chunks, received) };
|
|
50
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { listInstalledGuests } from './catalog.js';
|
|
7
|
+
import { buildStoreZip } from './extract-zip.test.js';
|
|
8
|
+
import { installGuestFromZipBuffer } from './install.js';
|
|
9
|
+
import { guestCopiesDir } from './persist.js';
|
|
10
|
+
import { guestUploadMaxBytes, readGuestUploadBody } from './upload.js';
|
|
11
|
+
|
|
12
|
+
const fakeRequest = ({ headers = {}, chunks = [] } = {}) => {
|
|
13
|
+
let resumed = false;
|
|
14
|
+
return {
|
|
15
|
+
headers: { 'content-type': 'application/octet-stream', ...headers },
|
|
16
|
+
resume: () => {
|
|
17
|
+
resumed = true;
|
|
18
|
+
},
|
|
19
|
+
wasResumed: () => resumed,
|
|
20
|
+
async *[Symbol.asyncIterator]() {
|
|
21
|
+
for (const chunk of chunks) {
|
|
22
|
+
yield chunk;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const guestZip = (id) => buildStoreZip([
|
|
29
|
+
{ name: 'package.json', data: JSON.stringify({
|
|
30
|
+
name: `@openchamber/${id}`,
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
openchamber: {
|
|
33
|
+
apiVersion: 1,
|
|
34
|
+
contributes: {
|
|
35
|
+
panel: { id, name: 'Uploaded', icon: 'window', entry: 'panel/index.html' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}) },
|
|
39
|
+
{ name: 'panel/index.html', data: '<html></html>' },
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
describe('readGuestUploadBody', () => {
|
|
43
|
+
test('collects an octet-stream body under the cap', async () => {
|
|
44
|
+
const req = fakeRequest({ chunks: [Buffer.from('ab'), Buffer.from('cd')] });
|
|
45
|
+
const body = await readGuestUploadBody(req, 10);
|
|
46
|
+
expect(body).toEqual({ ok: true, buffer: Buffer.from('abcd') });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('refuses a wrong content type without reading the body', async () => {
|
|
50
|
+
const req = fakeRequest({ headers: { 'content-type': 'multipart/form-data' }, chunks: [Buffer.from('abcd')] });
|
|
51
|
+
const body = await readGuestUploadBody(req, 10);
|
|
52
|
+
expect(body).toEqual({ ok: false, status: 415, error: 'unsupported-media-type' });
|
|
53
|
+
expect(req.wasResumed()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('refuses a declared size over the cap before reading', async () => {
|
|
57
|
+
const req = fakeRequest({ headers: { 'content-length': '11' }, chunks: [Buffer.from('abcd')] });
|
|
58
|
+
const body = await readGuestUploadBody(req, 10);
|
|
59
|
+
expect(body).toEqual({ ok: false, status: 413, error: 'too-large' });
|
|
60
|
+
expect(req.wasResumed()).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('stops a body that grows past the cap without a declared size', async () => {
|
|
64
|
+
const req = fakeRequest({ chunks: [Buffer.from('12345'), Buffer.from('678901')] });
|
|
65
|
+
const body = await readGuestUploadBody(req, 10);
|
|
66
|
+
expect(body).toEqual({ ok: false, status: 413, error: 'too-large' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('reads the cap from the environment', () => {
|
|
70
|
+
const previous = process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES;
|
|
71
|
+
try {
|
|
72
|
+
delete process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES;
|
|
73
|
+
expect(guestUploadMaxBytes()).toBe(50 * 1024 * 1024);
|
|
74
|
+
process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES = '1234';
|
|
75
|
+
expect(guestUploadMaxBytes()).toBe(1234);
|
|
76
|
+
process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES = 'nope';
|
|
77
|
+
expect(guestUploadMaxBytes()).toBe(50 * 1024 * 1024);
|
|
78
|
+
} finally {
|
|
79
|
+
if (previous === undefined) delete process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES;
|
|
80
|
+
else process.env.OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES = previous;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('installGuestFromZipBuffer', () => {
|
|
86
|
+
test('installs from bytes, lists with source zip, and reinstalls only with replace', async () => {
|
|
87
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-upload-'));
|
|
88
|
+
const persistPath = path.join(dir, 'extensions.json');
|
|
89
|
+
const buffer = guestZip('upload-hello');
|
|
90
|
+
|
|
91
|
+
const installed = await installGuestFromZipBuffer(buffer, persistPath);
|
|
92
|
+
expect(installed.ok).toBe(true);
|
|
93
|
+
if (!installed.ok) {
|
|
94
|
+
throw new Error('expected upload install');
|
|
95
|
+
}
|
|
96
|
+
expect(installed.guest.id).toBe('upload-hello');
|
|
97
|
+
expect(installed.guest.source).toBe('zip');
|
|
98
|
+
expect(installed.replaced).toBe(false);
|
|
99
|
+
const copy = path.join(guestCopiesDir(persistPath), 'upload-hello');
|
|
100
|
+
expect(await fs.stat(path.join(copy, 'package.json')).then((stat) => stat.isFile())).toBe(true);
|
|
101
|
+
|
|
102
|
+
const listed = await listInstalledGuests({ persistPath });
|
|
103
|
+
expect(listed.map((guest) => [guest.id, guest.source])).toEqual([['upload-hello', 'zip']]);
|
|
104
|
+
|
|
105
|
+
const again = await installGuestFromZipBuffer(buffer, persistPath);
|
|
106
|
+
expect(again).toEqual({ ok: false, code: 'already-installed', id: 'upload-hello' });
|
|
107
|
+
|
|
108
|
+
const replaced = await installGuestFromZipBuffer(buffer, persistPath, { replace: true });
|
|
109
|
+
expect(replaced.ok).toBe(true);
|
|
110
|
+
if (!replaced.ok) {
|
|
111
|
+
throw new Error('expected replace');
|
|
112
|
+
}
|
|
113
|
+
expect(replaced.replaced).toBe(true);
|
|
114
|
+
expect((await listInstalledGuests({ persistPath })).length).toBe(1);
|
|
115
|
+
|
|
116
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('answers extract-failed for bytes that are not a zip and leaves nothing behind', async () => {
|
|
120
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-upload-'));
|
|
121
|
+
const persistPath = path.join(dir, 'extensions.json');
|
|
122
|
+
|
|
123
|
+
const result = await installGuestFromZipBuffer(Buffer.from('this is a text file, not a zip'), persistPath);
|
|
124
|
+
expect(result).toEqual({ ok: false, code: 'extract-failed' });
|
|
125
|
+
expect(await listInstalledGuests({ persistPath })).toEqual([]);
|
|
126
|
+
const leftovers = await fs.readdir(guestCopiesDir(persistPath)).catch(() => []);
|
|
127
|
+
expect(leftovers).toEqual([]);
|
|
128
|
+
|
|
129
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -28,7 +28,7 @@ VS Code omits Linear (`RuntimeAPIs.linear` is optional). Hide Linear UI when the
|
|
|
28
28
|
- `POST /api/linear/auth/activate`: body `{ organizationId }`. Makes that stored workspace current. 400 if the id is missing, 404 if it is not stored.
|
|
29
29
|
- `DELETE /api/linear/auth`: revokes the current workspace refresh token when present, then drops that workspace only. Other stored workspaces stay. Mapping is kept.
|
|
30
30
|
- `GET /api/linear/issues/list?query=&cursor=&status=&assignee=&teamId=&priority=`: issues from the current workspace. Omitted `status` is incomplete states (same as the chat picker). The panel sends `all`, `backlog`, `todo` (Linear `unstarted`), `started` (In Progress, excluding the In Review name), `inReview` (state name In Review), `completed` (Done), `canceled` (excluding the Duplicate name), or `duplicate` (state type or name Duplicate). `assignee` is `any` (default) or `me`. `teamId` limits the list to that Linear team. `priority` is `all` (default), `none`, `urgent`, `high`, `medium`, or `low`. An identifier or Linear URL returns that issue even if it is completed and ignores the other filters. Each issue includes `state.id` when Linear sends it, plus `priority` (0 none through 4 low) and `labels`. Never returns tokens.
|
|
31
|
-
- `GET /api/linear/issues/get?id=`: one issue by UUID or identifier, including description, comments, team, `state.id`, priority, and labels.
|
|
31
|
+
- `GET /api/linear/issues/get?id=`: one issue by UUID or identifier, including description, comments, team, `state.id`, priority, and labels. The host Linear guest panel uses this route for the issue card so comment images keep public file URLs. Guest request does not add Linear headers.
|
|
32
32
|
- `GET /api/linear/issues/states?teamId=`: workflow states for that Linear team (`id`, `name`, `type`, `position`), ordered like Linear's workflow: type (backlog, unstarted, started, completed, canceled) then position. Missing `teamId` is 400. Linear not-found or validation errors are 400 with Linear's presentable message. Disconnected is `{ connected: false }` with HTTP 200.
|
|
33
33
|
- `POST /api/linear/issues/update`: body `{ id, stateId }`. `id` may be a UUID, identifier, or Linear URL; identifiers are resolved before `issueUpdate` because Linear's mutation requires a UUID. Returns the updated issue. Closing an issue is this same call with the team's first `type: completed` state. Missing `id` or `stateId` is 400. Linear validation (for example a non-UUID `stateId`) is 400 with Linear's presentable message. A GraphQL 401 clears that workspace only. Disconnected is `{ connected: false }` with HTTP 200.
|
|
34
34
|
- `GET /api/linear/mapping`: stored default project plus live Linear teams with their mapped paths. Missing file is empty mapping. Malformed file is 500, not empty success. Disconnected is `{ connected: false }` with HTTP 200.
|
|
@@ -22,6 +22,12 @@ session-knowledge runtime; routes registered in
|
|
|
22
22
|
JSON bodies enabled in `opencode/core-routes.js`; stopped by
|
|
23
23
|
`opencode/shutdown-runtime.js`.
|
|
24
24
|
|
|
25
|
+
## Auto routing
|
|
26
|
+
|
|
27
|
+
`resolvePromptBody` (the routing runtime) runs on the assembled body right
|
|
28
|
+
before the prompt or command send, turning a queued `openchamber/auto` model
|
|
29
|
+
into a real one. The queue captures the sentinel like any other send config.
|
|
30
|
+
|
|
25
31
|
## Item
|
|
26
32
|
|
|
27
33
|
An item is what the UI would have sent itself, captured at queue time so the
|
|
@@ -31,6 +37,7 @@ send never re-resolves mutable UI state:
|
|
|
31
37
|
{
|
|
32
38
|
id, createdAt,
|
|
33
39
|
content, // raw text for display and editing
|
|
40
|
+
contextPreview?, // bounded display-only summary captured by the UI
|
|
34
41
|
text, // text to deliver (agent mention stripped, file mentions resolved); defaults to content
|
|
35
42
|
agentMention?, // delivered as an `agent` part
|
|
36
43
|
attachments: [{ id, filename, mimeType, size, source, serverPath?, dataUrl }],
|
|
@@ -56,6 +63,12 @@ context entry). Public snapshots and broadcasts strip the payloads —
|
|
|
56
63
|
attachment `dataUrl` (megabytes of base64) and `context` (a PR diff, say) —
|
|
57
64
|
so they do not ride every update; the only way to get them back is a `take`.
|
|
58
65
|
|
|
66
|
+
Snapshots retain `contextPreview`, capped at 100 characters plus an ellipsis.
|
|
67
|
+
It carries the attached comment or context label when `content` is empty and
|
|
68
|
+
never replaces editable text or delivered parts. Older items without a summary
|
|
69
|
+
derive one from the attached comment metadata or the first non-instruction
|
|
70
|
+
context text. This optional field needs no queue-file migration.
|
|
71
|
+
|
|
59
72
|
## Persistence
|
|
60
73
|
|
|
61
74
|
`<data-dir>/message-queue.json` (`OPENCHAMBER_DATA_DIR` or
|
|
@@ -91,7 +104,10 @@ persisted "sending" flag would strand a message forever.
|
|
|
91
104
|
next one: `GET /session/status` must not list the session as busy/retry,
|
|
92
105
|
and the trailing message must not be an unfinished assistant reply (the
|
|
93
106
|
status map only lists busy sessions, so a missed busy event leaves no
|
|
94
|
-
entry while a turn still streams).
|
|
107
|
+
entry while a turn still streams). An unfinished reply created before this
|
|
108
|
+
runtime started does not block: its run died with the previous server and
|
|
109
|
+
will never complete, so a restored queue would wait on it forever. A reply
|
|
110
|
+
with no `created` time still blocks. A failed fetch is unknown, never idle:
|
|
95
111
|
the tick re-arms with backoff.
|
|
96
112
|
5. The head is marked in flight (broadcast), then sent:
|
|
97
113
|
- text starting with `/` that names a command in OpenCode's `/command`
|
|
@@ -133,6 +133,8 @@ export const parseQueuedItemInput = (value) => {
|
|
|
133
133
|
if (agentMention) item.agentMention = agentMention;
|
|
134
134
|
item.attachments = attachments;
|
|
135
135
|
item.context = context;
|
|
136
|
+
const contextPreview = asNonEmptyString(raw.contextPreview).slice(0, 103);
|
|
137
|
+
if (contextPreview) item.contextPreview = contextPreview;
|
|
136
138
|
item.sendConfig = sendConfig;
|
|
137
139
|
return item;
|
|
138
140
|
};
|
|
@@ -157,6 +159,17 @@ const toPublicItem = (item) => {
|
|
|
157
159
|
const publicItem = { id: item.id, createdAt: item.createdAt, content: item.content, text: item.text };
|
|
158
160
|
if (item.agentMention) publicItem.agentMention = item.agentMention;
|
|
159
161
|
publicItem.attachments = item.attachments.map(toPublicAttachment);
|
|
162
|
+
// Older persisted items have no UI summary. Prefer their attached comment
|
|
163
|
+
// before falling back to the model-facing context text.
|
|
164
|
+
const contextPreview = item.contextPreview || item.context
|
|
165
|
+
.filter((part) => part.kind !== 'instruction')
|
|
166
|
+
.map((part) => asNonEmptyString(asRecord(part.metadata?.openchamberContext)?.text) || part.text.trim())
|
|
167
|
+
.find(Boolean);
|
|
168
|
+
if (contextPreview) {
|
|
169
|
+
const firstLine = contextPreview.split('\n', 1)[0];
|
|
170
|
+
publicItem.contextPreview = firstLine.slice(0, 100)
|
|
171
|
+
+ (contextPreview.length > firstLine.length || firstLine.length > 100 ? '...' : '');
|
|
172
|
+
}
|
|
160
173
|
publicItem.sendConfig = { ...item.sendConfig };
|
|
161
174
|
return publicItem;
|
|
162
175
|
};
|
|
@@ -198,6 +211,9 @@ export function createMessageQueueRuntime({
|
|
|
198
211
|
sessionKnowledgeRuntime = null,
|
|
199
212
|
broadcastGlobalUiEvent,
|
|
200
213
|
onPromptSent,
|
|
214
|
+
// Turns the `openchamber/auto` model into a real one right before the send;
|
|
215
|
+
// absent means the queue never sees the sentinel.
|
|
216
|
+
resolvePromptBody = null,
|
|
201
217
|
dataDir,
|
|
202
218
|
fetchImpl = fetch,
|
|
203
219
|
now = Date.now,
|
|
@@ -214,6 +230,13 @@ export function createMessageQueueRuntime({
|
|
|
214
230
|
let writePromise = Promise.resolve();
|
|
215
231
|
let stopped = false;
|
|
216
232
|
|
|
233
|
+
// An unfinished assistant message older than this marker is a run that died
|
|
234
|
+
// with the previous server, not a streaming turn: no completion event will
|
|
235
|
+
// ever arrive for it, so treating it as live strands restored queue items
|
|
236
|
+
// forever. A run that outlived the restart (external OpenCode) is still
|
|
237
|
+
// caught by the live status check, which runs first.
|
|
238
|
+
const runtimeStartedAt = now();
|
|
239
|
+
|
|
217
240
|
/** In-memory only — a restart has no in-flight sends. */
|
|
218
241
|
const sending = new Map(); // sessionId → itemId
|
|
219
242
|
const timers = new Map(); // sessionId → timeout
|
|
@@ -380,7 +403,15 @@ export function createMessageQueueRuntime({
|
|
|
380
403
|
}).catch(() => null));
|
|
381
404
|
if (!messages) return null;
|
|
382
405
|
const last = asRecord(asRecord(messages[messages.length - 1])?.info);
|
|
383
|
-
|
|
406
|
+
const lastTime = asRecord(last?.time);
|
|
407
|
+
if (last?.role === 'assistant' && asCount(lastTime?.completed) === null) {
|
|
408
|
+
const created = asCount(lastTime?.created);
|
|
409
|
+
if (created === null || created >= runtimeStartedAt) return false;
|
|
410
|
+
// Unfinished tail from before this runtime started: its run died with
|
|
411
|
+
// the previous server, so it must not block delivery. (A missing
|
|
412
|
+
// created timestamp stays conservative and blocks, as before.)
|
|
413
|
+
console.log(`[message-queue] ignoring pre-boot unfinished tail for ${sessionId}`);
|
|
414
|
+
}
|
|
384
415
|
return true;
|
|
385
416
|
};
|
|
386
417
|
|
|
@@ -456,6 +487,7 @@ export function createMessageQueueRuntime({
|
|
|
456
487
|
if (agent) body.agent = agent;
|
|
457
488
|
if (variant) body.variant = variant;
|
|
458
489
|
if (fileParts.length > 0) body.parts = fileParts;
|
|
490
|
+
await resolvePromptBody?.(body, { sessionId, directory });
|
|
459
491
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/command`, { directory, method: 'POST', body });
|
|
460
492
|
return;
|
|
461
493
|
}
|
|
@@ -490,6 +522,7 @@ export function createMessageQueueRuntime({
|
|
|
490
522
|
if (agent) body.agent = agent;
|
|
491
523
|
if (variant) body.variant = variant;
|
|
492
524
|
body.parts = parts;
|
|
525
|
+
await resolvePromptBody?.(body, { sessionId, directory });
|
|
493
526
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/prompt_async`, { directory, method: 'POST', body });
|
|
494
527
|
if (knowledge.text && sessionKnowledgeRuntime) {
|
|
495
528
|
// After the prompt is accepted, so a rejected dispatch carries it again.
|