@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,203 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { createSessionAssistRuntime } from './runtime.js';
|
|
4
|
+
|
|
5
|
+
const resources = [];
|
|
6
|
+
const message = (id, role, text, extra = {}) => ({
|
|
7
|
+
info: { id, role, parentID: 'user', finish: 'stop', time: { completed: 1 }, providerID: 'test-provider', modelID: 'test-model', ...extra },
|
|
8
|
+
parts: [{ type: 'text', text }],
|
|
9
|
+
});
|
|
10
|
+
const output = (recap = 'Зміни готові', suggestion = '') => ({ text: JSON.stringify({ recap, suggestion }), providerID: 'test-provider', modelID: 'test-model' });
|
|
11
|
+
const pause = () => new Promise((resolve) => setTimeout(resolve, 15));
|
|
12
|
+
|
|
13
|
+
async function fixture(generate = async () => output()) {
|
|
14
|
+
const state = {
|
|
15
|
+
messages: [message('user', 'user', 'Виправ помилку'), message('answer', 'assistant', 'Виправлено')],
|
|
16
|
+
session: { id: 'session', directory: '/project', time: {}, metadata: { external: 'keep', openchamber: { note: 'keep' } } },
|
|
17
|
+
targets: { recap: true, suggestion: true },
|
|
18
|
+
gets: 0, failFresh: false, patches: [], requests: [], calls: [],
|
|
19
|
+
};
|
|
20
|
+
const server = createServer(async (request, response) => {
|
|
21
|
+
const url = new URL(request.url, 'http://localhost');
|
|
22
|
+
state.requests.push({ method: request.method, directory: url.searchParams.get('directory'), limit: url.searchParams.get('limit'), auth: request.headers['x-test-auth'] });
|
|
23
|
+
response.setHeader('content-type', 'application/json');
|
|
24
|
+
if (request.method === 'PATCH') {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
27
|
+
state.patches.push(JSON.parse(Buffer.concat(chunks).toString()));
|
|
28
|
+
response.end(JSON.stringify(state.session));
|
|
29
|
+
} else if (url.pathname.endsWith('/message')) {
|
|
30
|
+
response.end(JSON.stringify(url.searchParams.get('limit') === '1' ? state.messages.slice(-1) : state.messages));
|
|
31
|
+
} else {
|
|
32
|
+
state.gets++;
|
|
33
|
+
response.statusCode = state.failFresh && state.gets > 1 ? 500 : 200;
|
|
34
|
+
response.end(JSON.stringify(state.session));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
38
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
39
|
+
state.base = base;
|
|
40
|
+
const runtime = createSessionAssistRuntime({
|
|
41
|
+
buildOpenCodeUrl: (route) => state.base + route,
|
|
42
|
+
getOpenCodeAuthHeaders: () => ({ 'x-test-auth': 'fixture' }),
|
|
43
|
+
getTargets: () => state.targets,
|
|
44
|
+
quietMs: 1,
|
|
45
|
+
getSmallModelService: async () => ({
|
|
46
|
+
describeSmallModel: async () => ({ inputCharBudget: 64_000 }),
|
|
47
|
+
generateSmallModelText: async (args) => { state.calls.push(args); return generate(args, state); },
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
resources.push({ runtime, server });
|
|
51
|
+
const status = (type) => runtime.processPayload({ type: 'session.status', properties: { sessionID: 'session', status: { type } } }, '/project');
|
|
52
|
+
return { state, runtime, status };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
for (const { runtime, server } of resources.splice(0)) {
|
|
57
|
+
runtime.stop();
|
|
58
|
+
server.closeAllConnections();
|
|
59
|
+
await new Promise((resolve) => server.close(resolve));
|
|
60
|
+
}
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('session assist runtime', () => {
|
|
65
|
+
it('uses bounded authenticated SDK reads and preserves metadata with an empty suggestion', async () => {
|
|
66
|
+
const { state, status } = await fixture(async (_args, current) => {
|
|
67
|
+
current.session.metadata.openchamber.concurrent = 'new';
|
|
68
|
+
return output();
|
|
69
|
+
});
|
|
70
|
+
status('idle');
|
|
71
|
+
await vi.waitFor(() => expect(state.patches).toHaveLength(1));
|
|
72
|
+
expect(state.requests.every((r) => r.directory === '/project' && r.auth === 'fixture')).toBe(true);
|
|
73
|
+
expect(state.requests.filter((r) => r.limit).map((r) => r.limit)).toEqual(['50', '1']);
|
|
74
|
+
expect(state.calls[0]).toMatchObject({ restrictToPreferredProvider: true, onOverflow: 'error', preferredProviderID: 'test-provider', preferredModelID: 'test-model', sessionID: 'session' });
|
|
75
|
+
expect(state.patches[0].metadata).toMatchObject({ external: 'keep', openchamber: {
|
|
76
|
+
note: 'keep', concurrent: 'new', assist: { recap: 'Зміни готові', suggestion: '', forMessageID: 'answer' },
|
|
77
|
+
} });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('does no work with both settings off and skips child, archived, or reverted sessions', async () => {
|
|
81
|
+
const { state, status } = await fixture();
|
|
82
|
+
state.targets = { recap: false, suggestion: false };
|
|
83
|
+
status('idle');
|
|
84
|
+
await pause();
|
|
85
|
+
expect(state.requests).toHaveLength(0);
|
|
86
|
+
state.targets.recap = true;
|
|
87
|
+
state.session.parentID = 'parent';
|
|
88
|
+
status('idle');
|
|
89
|
+
await vi.waitFor(() => expect(state.gets).toBe(1));
|
|
90
|
+
delete state.session.parentID;
|
|
91
|
+
state.session.revert = { messageID: 'user' };
|
|
92
|
+
status('idle');
|
|
93
|
+
await vi.waitFor(() => expect(state.gets).toBe(2));
|
|
94
|
+
delete state.session.revert;
|
|
95
|
+
state.session.time.archived = 1;
|
|
96
|
+
status('idle');
|
|
97
|
+
await vi.waitFor(() => expect(state.gets).toBe(3));
|
|
98
|
+
expect(state.calls).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('keeps recent context for recap-only and performs no write for an empty suggestion-only result', async () => {
|
|
102
|
+
const { state, status } = await fixture();
|
|
103
|
+
state.targets.suggestion = false;
|
|
104
|
+
state.messages.unshift(message('previous-user', 'user', 'Попередня задача'), message('previous-answer', 'assistant', 'Зміст зробленого', { parentID: 'previous-user' }));
|
|
105
|
+
status('idle');
|
|
106
|
+
await vi.waitFor(() => expect(state.patches).toHaveLength(1));
|
|
107
|
+
expect(state.calls[0].prompt).toContain('Зміст зробленого');
|
|
108
|
+
expect(state.calls[0].system).not.toContain('suggestion');
|
|
109
|
+
state.targets = { recap: false, suggestion: true };
|
|
110
|
+
status('idle');
|
|
111
|
+
await vi.waitFor(() => expect(state.calls).toHaveLength(2));
|
|
112
|
+
await pause();
|
|
113
|
+
expect(state.patches).toHaveLength(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not write a stale result when the tail moves during generation', async () => {
|
|
117
|
+
const { state, status } = await fixture(async (_args, current) => {
|
|
118
|
+
current.messages.push(message('new-user', 'user', 'Нова задача'));
|
|
119
|
+
return output();
|
|
120
|
+
});
|
|
121
|
+
status('idle');
|
|
122
|
+
await vi.waitFor(() => expect(state.requests.some((r) => r.limit === '1')).toBe(true));
|
|
123
|
+
await pause();
|
|
124
|
+
expect(state.patches).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not fall back to stale metadata when the fresh read fails', async () => {
|
|
128
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
129
|
+
const { state, status } = await fixture();
|
|
130
|
+
state.failFresh = true;
|
|
131
|
+
status('idle');
|
|
132
|
+
await vi.waitFor(() => expect(warning).toHaveBeenCalled());
|
|
133
|
+
expect(state.gets).toBe(2);
|
|
134
|
+
expect(state.patches).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('cancels old work and retains an expired newer idle timer until it can run', async () => {
|
|
138
|
+
const releases = [];
|
|
139
|
+
const { state, status } = await fixture(() => new Promise((resolve) => releases.push(resolve)));
|
|
140
|
+
status('idle');
|
|
141
|
+
await vi.waitFor(() => expect(releases).toHaveLength(1));
|
|
142
|
+
status('busy');
|
|
143
|
+
expect(state.calls[0].signal.aborted).toBe(true);
|
|
144
|
+
state.messages.push(message('next-user', 'user', 'Далі'), message('next-answer', 'assistant', 'Готово', { parentID: 'next-user' }));
|
|
145
|
+
status('idle');
|
|
146
|
+
await pause();
|
|
147
|
+
releases[0](output('Старий результат'));
|
|
148
|
+
await vi.waitFor(() => expect(releases).toHaveLength(2));
|
|
149
|
+
expect(state.patches).toHaveLength(0);
|
|
150
|
+
releases[1](output('Новий результат'));
|
|
151
|
+
await vi.waitFor(() => expect(state.patches).toHaveLength(1));
|
|
152
|
+
expect(state.patches[0].metadata.openchamber.assist).toMatchObject({ recap: 'Новий результат', forMessageID: 'next-answer' });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('aborts on stop and honors settings switched off during generation', async () => {
|
|
156
|
+
let release;
|
|
157
|
+
const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
|
|
158
|
+
status('idle');
|
|
159
|
+
await vi.waitFor(() => expect(state.calls).toHaveLength(1));
|
|
160
|
+
runtime.stop();
|
|
161
|
+
expect(state.calls[0].signal.aborted).toBe(true);
|
|
162
|
+
release(output());
|
|
163
|
+
await pause();
|
|
164
|
+
expect(state.patches).toHaveLength(0);
|
|
165
|
+
const second = await fixture(async (_args, current) => {
|
|
166
|
+
current.targets = { recap: false, suggestion: false };
|
|
167
|
+
return output();
|
|
168
|
+
});
|
|
169
|
+
second.status('idle');
|
|
170
|
+
await vi.waitFor(() => expect(second.state.gets).toBe(2));
|
|
171
|
+
await pause();
|
|
172
|
+
expect(second.state.patches).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('ignores historical user updates but cancels a new request during generation', async () => {
|
|
176
|
+
let release;
|
|
177
|
+
const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
|
|
178
|
+
status('idle');
|
|
179
|
+
await vi.waitFor(() => expect(state.calls).toHaveLength(1));
|
|
180
|
+
const userUpdate = (created) => runtime.processPayload({ type: 'message.updated', properties: {
|
|
181
|
+
info: { id: 'user', sessionID: 'session', role: 'user', time: { created } },
|
|
182
|
+
} });
|
|
183
|
+
userUpdate(1);
|
|
184
|
+
expect(state.calls[0].signal.aborted).toBe(false);
|
|
185
|
+
userUpdate(Date.now());
|
|
186
|
+
expect(state.calls[0].signal.aborted).toBe(true);
|
|
187
|
+
release(output());
|
|
188
|
+
await pause();
|
|
189
|
+
expect(state.patches).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('rejects endpoint changes before writing instead of carrying a session into a new runtime', async () => {
|
|
193
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
194
|
+
const { state, status } = await fixture(async (_args, current) => {
|
|
195
|
+
current.base = 'http://unreachable.invalid';
|
|
196
|
+
return output();
|
|
197
|
+
});
|
|
198
|
+
status('idle');
|
|
199
|
+
await vi.waitFor(() => expect(warning).toHaveBeenCalled());
|
|
200
|
+
expect(state.patches).toHaveLength(0);
|
|
201
|
+
expect(state.gets).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -118,14 +118,26 @@ other runtime API.
|
|
|
118
118
|
provider's base URL, resolved from (1) `provider.<id>.options.baseURL`
|
|
119
119
|
in the OpenCode config, (2) the hardcoded `https://api.openai.com/v1`
|
|
120
120
|
endpoint, (3) the selected model's endpoint OpenCode resolved at runtime,
|
|
121
|
-
(4) the provider-level runtime endpoint,
|
|
122
|
-
from the models.dev catalog
|
|
121
|
+
(4) the provider-level runtime endpoint, (5) the provider's `api` field
|
|
122
|
+
from the models.dev catalog, or (6) `SDK_DEFAULT_BASE_URLS` — providers
|
|
123
|
+
such as Groq, xAI, and Mistral keep their endpoint inside a dedicated AI
|
|
124
|
+
SDK package, so neither the catalog nor OpenCode reports one. The
|
|
125
|
+
credential follows
|
|
123
126
|
the same shape: config `options.apiKey`, then the runtime credential, then
|
|
124
127
|
the auth.json entry. `provider.<id>.options.headers` is sent with the
|
|
125
128
|
request and overrides the bearer default, so gateways that authenticate on
|
|
126
129
|
their own header work here exactly as they do in a chat turn. Configured API
|
|
127
130
|
keys and header values honor OpenCode's `{env:NAME}` and `{file:path}`
|
|
128
131
|
substitutions; file contents and resolved credentials remain server-side.
|
|
132
|
+
- `thinking: { type: 'disabled' }` is sent in two cases, mirroring OpenCode's
|
|
133
|
+
`transform.ts`. The GLM switch belongs to the endpoint, not the model
|
|
134
|
+
family: it goes to a provider whose id contains `zai`/`zhipu` or whose
|
|
135
|
+
base URL is on `api.z.ai`/`bigmodel.cn`, because the same GLM model served
|
|
136
|
+
by another provider (OpenCode Go) rejects the unknown field with a 400.
|
|
137
|
+
The MiniMax M3 switch goes only behind the `@ai-sdk/openai-compatible` and
|
|
138
|
+
`@ai-sdk/anthropic` adapters (runtime model, then catalog model, then
|
|
139
|
+
catalog provider `npm`); a provider that reports no adapter counts as
|
|
140
|
+
OpenAI-compatible.
|
|
129
141
|
- The runtime credential is refused for providers listed in
|
|
130
142
|
`OWN_CREDENTIAL_HANDLING`. Their branches need the stored entry rather than
|
|
131
143
|
a bearer token: the clearest case is the ChatGPT-plan `openai` login, whose
|
|
@@ -20,6 +20,25 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 4_000;
|
|
|
20
20
|
|
|
21
21
|
const USER_AGENT = 'opencode/1.0 openchamber';
|
|
22
22
|
|
|
23
|
+
// Providers whose endpoint lives inside their dedicated AI SDK package, so the
|
|
24
|
+
// models.dev catalog carries no `api` URL and OpenCode reports none at runtime.
|
|
25
|
+
// Each of these serves OpenAI-compatible `/chat/completions` at the URL below.
|
|
26
|
+
const SDK_DEFAULT_BASE_URLS = new Map([
|
|
27
|
+
['groq', 'https://api.groq.com/openai/v1'],
|
|
28
|
+
['xai', 'https://api.x.ai/v1'],
|
|
29
|
+
['mistral', 'https://api.mistral.ai/v1'],
|
|
30
|
+
['cerebras', 'https://api.cerebras.ai/v1'],
|
|
31
|
+
['togetherai', 'https://api.together.xyz/v1'],
|
|
32
|
+
['deepinfra', 'https://api.deepinfra.com/v1/openai'],
|
|
33
|
+
['perplexity', 'https://api.perplexity.ai'],
|
|
34
|
+
['cohere', 'https://api.cohere.ai/compatibility/v1'],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Where `thinking: { type: 'disabled' }` is a real switch rather than an
|
|
38
|
+
// unknown field (mirrors the gates in OpenCode's provider/transform.ts).
|
|
39
|
+
const ZAI_ENDPOINT_HOSTS = ['api.z.ai', 'bigmodel.cn'];
|
|
40
|
+
const MINIMAX_THINKING_ADAPTERS = new Set(['@ai-sdk/openai-compatible', '@ai-sdk/anthropic']);
|
|
41
|
+
|
|
23
42
|
const mergeHeadersCaseInsensitive = (base, overrides) => {
|
|
24
43
|
const merged = { ...base };
|
|
25
44
|
for (const [name, value] of Object.entries(overrides || {})) {
|
|
@@ -765,7 +784,8 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
|
|
|
765
784
|
// selected model's endpoint OpenCode resolved at runtime, then to the
|
|
766
785
|
// provider-level runtime endpoint. For a plugin provider, the runtime listing
|
|
767
786
|
// is the only place those endpoints exist, and several are local proxies the
|
|
768
|
-
// plugin itself runs.
|
|
787
|
+
// plugin itself runs. Last comes SDK_DEFAULT_BASE_URLS, for providers whose
|
|
788
|
+
// endpoint only their SDK package knows. The openai provider also respects
|
|
769
789
|
// provider.openai.options.baseURL — OpenCode itself uses the same config for
|
|
770
790
|
// all providers including openai.
|
|
771
791
|
const provider = getCatalogProvider(catalog, providerID);
|
|
@@ -779,7 +799,7 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
|
|
|
779
799
|
?? runtimeProvider?.baseURL
|
|
780
800
|
?? (typeof provider?.api === 'string' && provider.api
|
|
781
801
|
? provider.api
|
|
782
|
-
: null);
|
|
802
|
+
: SDK_DEFAULT_BASE_URLS.get(providerID) ?? null);
|
|
783
803
|
if (!baseURL) {
|
|
784
804
|
throw new Error(`Provider "${providerID}" has no known API base URL`);
|
|
785
805
|
}
|
|
@@ -790,11 +810,22 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
|
|
|
790
810
|
// parameter: unknown body fields 400 on some providers, so this stays an
|
|
791
811
|
// explicit allowlist. Models without a switch (DeepSeek, Qwen, Kimi, …)
|
|
792
812
|
// just get the generous output budget.
|
|
793
|
-
|
|
794
|
-
|
|
813
|
+
// GLM's switch belongs to the Z.ai/Zhipu endpoints, not to the model: the
|
|
814
|
+
// same GLM served by another provider (OpenCode Go) rejects the field. The
|
|
815
|
+
// host check keeps the switch for a custom-named provider pointed at them.
|
|
816
|
+
const servedByZai = providerID.includes('zai')
|
|
795
817
|
|| providerID.includes('zhipu')
|
|
796
|
-
||
|
|
797
|
-
|
|
818
|
+
|| ZAI_ENDPOINT_HOSTS.some((host) => baseURL.includes(host));
|
|
819
|
+
// MiniMax M3's switch is understood only behind the adapters OpenCode sends
|
|
820
|
+
// it through. An adapter nobody reports (a custom provider) is OpenAI-
|
|
821
|
+
// compatible by OpenCode's own default.
|
|
822
|
+
const adapter = runtimeModel?.api?.npm
|
|
823
|
+
?? provider?.models?.[modelID]?.provider?.npm
|
|
824
|
+
?? provider?.npm
|
|
825
|
+
?? null;
|
|
826
|
+
const minimaxSwitch = modelID.toLowerCase().includes('minimax-m3')
|
|
827
|
+
&& (adapter === null || MINIMAX_THINKING_ADAPTERS.has(adapter));
|
|
828
|
+
const supportsThinkingToggle = servedByZai || minimaxSwitch;
|
|
798
829
|
const extraBody = supportsThinkingToggle ? { thinking: { type: 'disabled' } } : undefined;
|
|
799
830
|
|
|
800
831
|
return callOpenaiCompatible({
|
|
@@ -772,6 +772,188 @@ describe('callSmallModel — Google thinking configuration', () => {
|
|
|
772
772
|
});
|
|
773
773
|
});
|
|
774
774
|
|
|
775
|
+
describe('callSmallModel — OpenAI-compatible thinking toggle', () => {
|
|
776
|
+
let fetchMock;
|
|
777
|
+
let originalFetch;
|
|
778
|
+
|
|
779
|
+
beforeEach(() => {
|
|
780
|
+
fetchMock = vi.fn();
|
|
781
|
+
originalFetch = globalThis.fetch;
|
|
782
|
+
globalThis.fetch = fetchMock;
|
|
783
|
+
readConfig.mockReset();
|
|
784
|
+
readConfig.mockReturnValue({});
|
|
785
|
+
readConfigLayers.mockReset();
|
|
786
|
+
readConfigLayers.mockReturnValue({ mergedConfig: {} });
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
afterEach(() => {
|
|
790
|
+
globalThis.fetch = originalFetch;
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const requestBody = async ({ providerID, modelID }) => {
|
|
794
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
795
|
+
await callSmallModel({
|
|
796
|
+
auth: { [providerID]: { type: 'api', key: 'sk-test' } },
|
|
797
|
+
catalog: { [providerID]: { id: providerID, api: 'https://provider.test/v1', models: {} } },
|
|
798
|
+
workingDirectory: '/proj',
|
|
799
|
+
providerID,
|
|
800
|
+
modelID,
|
|
801
|
+
prompt: 'summarize',
|
|
802
|
+
});
|
|
803
|
+
return JSON.parse(lastCall(fetchMock).init.body);
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
it('does not send the thinking field for a GLM model served by OpenCode Go', async () => {
|
|
807
|
+
const body = await requestBody({ providerID: 'opencode-go', modelID: 'glm-5.3-flash' });
|
|
808
|
+
expect(body.thinking).toBeUndefined();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('disables thinking on the Z.ai and Zhipu endpoints', async () => {
|
|
812
|
+
const zai = await requestBody({ providerID: 'zai-coding-plan', modelID: 'glm-5.3-flash' });
|
|
813
|
+
expect(zai.thinking).toEqual({ type: 'disabled' });
|
|
814
|
+
const zhipu = await requestBody({ providerID: 'zhipuai', modelID: 'glm-5.3-flash' });
|
|
815
|
+
expect(zhipu.thinking).toEqual({ type: 'disabled' });
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('keeps the GLM switch for a custom-named provider pointed at a Z.ai endpoint', async () => {
|
|
819
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
820
|
+
await callSmallModel({
|
|
821
|
+
auth: { 'my-glm': { type: 'api', key: 'sk-test' } },
|
|
822
|
+
catalog: { 'my-glm': { id: 'my-glm', api: 'https://api.z.ai/api/coding/paas/v4', models: {} } },
|
|
823
|
+
workingDirectory: '/proj',
|
|
824
|
+
providerID: 'my-glm',
|
|
825
|
+
modelID: 'glm-5.3-flash',
|
|
826
|
+
prompt: 'summarize',
|
|
827
|
+
});
|
|
828
|
+
expect(JSON.parse(lastCall(fetchMock).init.body).thinking).toEqual({ type: 'disabled' });
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const minimaxBody = async (providerEntry) => {
|
|
832
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
833
|
+
await callSmallModel({
|
|
834
|
+
auth: { host: { type: 'api', key: 'sk-test' } },
|
|
835
|
+
catalog: { host: { id: 'host', api: 'https://provider.test/v1', ...providerEntry } },
|
|
836
|
+
workingDirectory: '/proj',
|
|
837
|
+
providerID: 'host',
|
|
838
|
+
modelID: 'minimax-m3',
|
|
839
|
+
prompt: 'summarize',
|
|
840
|
+
});
|
|
841
|
+
return JSON.parse(lastCall(fetchMock).init.body);
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
it('disables MiniMax M3 thinking behind an OpenAI-compatible adapter', async () => {
|
|
845
|
+
const body = await minimaxBody({ npm: '@ai-sdk/openai-compatible', models: {} });
|
|
846
|
+
expect(body.thinking).toEqual({ type: 'disabled' });
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('honors a model-level adapter over the provider one for MiniMax M3', async () => {
|
|
850
|
+
const body = await minimaxBody({
|
|
851
|
+
npm: '@ai-sdk/deepinfra',
|
|
852
|
+
models: { 'minimax-m3': { provider: { npm: '@ai-sdk/anthropic' } } },
|
|
853
|
+
});
|
|
854
|
+
expect(body.thinking).toEqual({ type: 'disabled' });
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('does not send the MiniMax M3 switch through an adapter that has none', async () => {
|
|
858
|
+
const body = await minimaxBody({ npm: '@ai-sdk/deepinfra', models: {} });
|
|
859
|
+
expect(body.thinking).toBeUndefined();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('treats a provider with no reported adapter as OpenAI-compatible for MiniMax M3', async () => {
|
|
863
|
+
const body = await minimaxBody({ models: {} });
|
|
864
|
+
expect(body.thinking).toEqual({ type: 'disabled' });
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('callSmallModel — providers with an SDK-owned endpoint', () => {
|
|
869
|
+
let fetchMock;
|
|
870
|
+
let originalFetch;
|
|
871
|
+
|
|
872
|
+
beforeEach(() => {
|
|
873
|
+
fetchMock = vi.fn();
|
|
874
|
+
originalFetch = globalThis.fetch;
|
|
875
|
+
globalThis.fetch = fetchMock;
|
|
876
|
+
readConfig.mockReset();
|
|
877
|
+
readConfig.mockReturnValue({});
|
|
878
|
+
readConfigLayers.mockReset();
|
|
879
|
+
readConfigLayers.mockReturnValue({ mergedConfig: {} });
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
afterEach(() => {
|
|
883
|
+
globalThis.fetch = originalFetch;
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it.each([
|
|
887
|
+
['groq', 'https://api.groq.com/openai/v1'],
|
|
888
|
+
['xai', 'https://api.x.ai/v1'],
|
|
889
|
+
['mistral', 'https://api.mistral.ai/v1'],
|
|
890
|
+
['cerebras', 'https://api.cerebras.ai/v1'],
|
|
891
|
+
['togetherai', 'https://api.together.xyz/v1'],
|
|
892
|
+
['deepinfra', 'https://api.deepinfra.com/v1/openai'],
|
|
893
|
+
['perplexity', 'https://api.perplexity.ai'],
|
|
894
|
+
['cohere', 'https://api.cohere.ai/compatibility/v1'],
|
|
895
|
+
])('calls %s at its default endpoint when the catalog has no api URL', async (providerID, baseURL) => {
|
|
896
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
897
|
+
|
|
898
|
+
const text = await callSmallModel({
|
|
899
|
+
auth: { [providerID]: { type: 'api', key: 'key-test' } },
|
|
900
|
+
catalog: { [providerID]: { id: providerID, api: null, models: {} } },
|
|
901
|
+
workingDirectory: '/proj',
|
|
902
|
+
providerID,
|
|
903
|
+
modelID: 'some-model',
|
|
904
|
+
prompt: 'summarize',
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
expect(text).toBe('done');
|
|
908
|
+
expect(lastCall(fetchMock).url).toBe(`${baseURL}/chat/completions`);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('prefers a catalog api URL over the default endpoint', async () => {
|
|
912
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
913
|
+
|
|
914
|
+
await callSmallModel({
|
|
915
|
+
auth: { groq: { type: 'api', key: 'key-test' } },
|
|
916
|
+
catalog: { groq: { id: 'groq', api: 'https://catalog.test/v1', models: {} } },
|
|
917
|
+
workingDirectory: '/proj',
|
|
918
|
+
providerID: 'groq',
|
|
919
|
+
modelID: 'some-model',
|
|
920
|
+
prompt: 'summarize',
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
expect(lastCall(fetchMock).url).toBe('https://catalog.test/v1/chat/completions');
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('prefers a configured baseURL over the default endpoint', async () => {
|
|
927
|
+
readConfig.mockReturnValue({
|
|
928
|
+
provider: { groq: { options: { baseURL: 'https://proxy.test/v1' } } },
|
|
929
|
+
});
|
|
930
|
+
fetchMock.mockResolvedValue(ok('done'));
|
|
931
|
+
|
|
932
|
+
await callSmallModel({
|
|
933
|
+
auth: { groq: { type: 'api', key: 'key-test' } },
|
|
934
|
+
catalog: { groq: { id: 'groq', api: null, models: {} } },
|
|
935
|
+
workingDirectory: '/proj',
|
|
936
|
+
providerID: 'groq',
|
|
937
|
+
modelID: 'some-model',
|
|
938
|
+
prompt: 'summarize',
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
expect(lastCall(fetchMock).url).toBe('https://proxy.test/v1/chat/completions');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('still fails clearly for a provider with no endpoint anywhere', async () => {
|
|
945
|
+
await expect(callSmallModel({
|
|
946
|
+
auth: { mystery: { type: 'api', key: 'sk-test' } },
|
|
947
|
+
catalog: {},
|
|
948
|
+
workingDirectory: '/proj',
|
|
949
|
+
providerID: 'mystery',
|
|
950
|
+
modelID: 'some-model',
|
|
951
|
+
prompt: 'summarize',
|
|
952
|
+
})).rejects.toThrow('has no known API base URL');
|
|
953
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
775
957
|
describe('callSmallModel — GitHub Copilot endpoint routing', () => {
|
|
776
958
|
let fetchMock;
|
|
777
959
|
let originalFetch;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Ownership
|
|
4
4
|
|
|
5
|
-
`runtime.js` owns terminal identity, PTY processes, launch mode, session purpose, ordered output, bounded scrollback, WebSocket attachments, and lifecycle routes. `shells.js` discovers executable shell families, resolves the persisted shell ID, and builds the per-shell argv for interactive versus command launches. Clients own tab arrangement. Interactive terminals use stable IDs; project actions keep a stable UI tab and allocate a fresh terminal ID for each command execution. Electron uses this same runtime in-process; VS Code returns an explicit unsupported error.
|
|
5
|
+
`runtime.js` owns terminal identity, PTY processes, launch mode, session purpose, ordered output, bounded scrollback, WebSocket attachments, and lifecycle routes. `shutdown.js` owns bounded runtime-wide process cleanup. `shells.js` discovers executable shell families, resolves the persisted shell ID, and builds the per-shell argv for interactive versus command launches. Clients own tab arrangement. Interactive terminals use stable IDs; project actions keep a stable UI tab and allocate a fresh terminal ID for each command execution. Electron uses this same runtime in-process; VS Code returns an explicit unsupported error.
|
|
6
6
|
|
|
7
7
|
## Protocol
|
|
8
8
|
|
|
@@ -37,8 +37,11 @@ HTTP remains the authenticated command plane for create, resize, appearance upda
|
|
|
37
37
|
- Deduplicated create responses may describe another client's execution. Cancellation cleanup closes only the terminal ID allocated for the cancelled request; it never closes an adopted peer execution.
|
|
38
38
|
- Create and restart validate the working directory with a real `stat` and answer HTTP 400 `Invalid working directory` when it is not a directory. When the path does not exist at all (`ENOENT`/`ENOTDIR`, a worktree deleted outside OpenChamber) the body also carries `code: "TERMINAL_CWD_MISSING"`. The client shows the failure without moving the session. The runtime never substitutes a parent directory on its own.
|
|
39
39
|
- Restarts are serialized per terminal. Each restart spawns and wires the replacement before terminating the old process, retaining the terminal ID. Command-mode sessions reject restart with HTTP 400 instead of silently turning into interactive shells with stale action metadata.
|
|
40
|
+
- A restart rechecks session ownership before and after PTY creation. Close, force-kill, idle removal, or shutdown can invalidate it while spawn is pending. A late replacement is terminated instead of being attached to a retired session.
|
|
40
41
|
- A delete that arrives while create is still pending leaves a cancellation tombstone. When the PTY arrives, the runtime terminates it immediately, never inserts the session into the live map, and returns a create error while the delete still succeeds.
|
|
41
|
-
- Close uses SIGTERM with bounded SIGKILL escalation. Force-kill
|
|
42
|
+
- Close uses SIGTERM with bounded SIGKILL escalation. Force-kill and idle cleanup terminate process groups immediately where supported. Removal explicitly sends a fatal scoped closure and evicts client projections even when a PTY backend fails to emit `onExit`; attached terminals are not considered idle.
|
|
43
|
+
- Shutdown is single-flight, rejects new creates, cancels pending creates, and waits for pending creates and restarts before clearing live sessions. A PTY that arrives during shutdown follows the same cancellation cleanup as a pending create that was explicitly closed.
|
|
44
|
+
- On POSIX, shutdown disconnects input and gives terminal workloads a shared 20-second SIGTERM grace. Process-table snapshots retain descendant ownership across shell exit and separate job-control groups; PID start times guard against reused PIDs. Linux reads `/proc` directly so minimal containers do not need `ps`. Shells remain open until the workloads exit, then receive SIGHUP. An exec'ed server receives SIGTERM and the full grace even though it occupies the shell PID. Remaining processes receive SIGKILL at the deadline. A process-table failure warns and forces known owned processes closed. Windows retains ConPTY-owned teardown because it has no POSIX signal protocol.
|
|
42
45
|
|
|
43
46
|
## Security And Relay
|
|
44
47
|
|
|
@@ -50,5 +53,23 @@ Run:
|
|
|
50
53
|
|
|
51
54
|
```sh
|
|
52
55
|
bun test packages/web/server/lib/terminal/runtime.test.js packages/web/server/lib/terminal/terminal-ws-protocol.test.js
|
|
56
|
+
bun run --cwd packages/web test server/lib/terminal/shutdown.test.js
|
|
53
57
|
bun test packages/web/server/lib/ui-auth/ui-auth.test.js packages/web/server/lib/relay/cross-compat.test.js
|
|
54
58
|
```
|
|
59
|
+
|
|
60
|
+
The #3389 macOS ARM64 reproduction used real Electron 43.7.0 PTYs and HTTP
|
|
61
|
+
servers with signal handlers that took 15 seconds to finish cleanup. On the
|
|
62
|
+
baseline, File > Restart and Quit sent SIGKILL to the exec'ed server within
|
|
63
|
+
3–4 ms; a foreground child in another process group received SIGHUP and outlived
|
|
64
|
+
Electron. After the fix, both workloads completed after SIGTERM before Electron
|
|
65
|
+
exited, in bundled and HMR runs. A noncooperative workload was killed after the
|
|
66
|
+
shared 20-second deadline.
|
|
67
|
+
|
|
68
|
+
Packaged test builds 1.24.90 and 1.24.91 exercised the real macOS Squirrel install
|
|
69
|
+
with a loopback feed, isolated app identity/profile, and a shared test signing
|
|
70
|
+
requirement. Both the normal update and closing the window during cleanup
|
|
71
|
+
installed and relaunched N+1 after the terminal workloads finished. The installed
|
|
72
|
+
ASAR matched N+1, and OpenCode remained bundled after its own restart. A rejected
|
|
73
|
+
signature also reached the installer error handler after the 15-second cleanup.
|
|
74
|
+
Linux ARM64 process regressions pass without `ps` installed. Native Windows
|
|
75
|
+
ConPTY and Linux Electron installer behavior were not exercised for this fix.
|
|
@@ -11,6 +11,7 @@ import { sanitizeTerminalHistoryChunk } from './history.js';
|
|
|
11
11
|
import { consumeTerminalThemeQueries, terminalThemeModeReport } from './theme-response.js';
|
|
12
12
|
import { buildTerminalShellLaunch, createTerminalShellResolver, normalizeTerminalShell } from './shells.js';
|
|
13
13
|
import { stripAppImageArgv0Leak, resolvePosixPtyLaunch } from '../inherited-env.js';
|
|
14
|
+
import { shutdownTerminalProcesses } from './shutdown.js';
|
|
14
15
|
|
|
15
16
|
const MAX_SESSIONS = 20;
|
|
16
17
|
const MAX_HISTORY_BYTES = 512 * 1024;
|
|
@@ -91,7 +92,7 @@ const trimHistory = (history) => {
|
|
|
91
92
|
export function createTerminalRuntime({
|
|
92
93
|
app, server, fs, path, uiAuthController, buildAugmentedPath, searchPathFor, isExecutable,
|
|
93
94
|
isRequestOriginAllowed, rejectWebSocketUpgrade, TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS,
|
|
94
|
-
loadPtyProvider, terminalTerminationGraceMs = TERMINATION_GRACE_MS,
|
|
95
|
+
loadPtyProvider, terminalTerminationGraceMs = TERMINATION_GRACE_MS, shutdownProcesses = shutdownTerminalProcesses,
|
|
95
96
|
}) {
|
|
96
97
|
const sessions = new Map();
|
|
97
98
|
const pendingSessionCreates = new Map();
|
|
@@ -100,6 +101,7 @@ export function createTerminalRuntime({
|
|
|
100
101
|
const pendingTerminations = new Set();
|
|
101
102
|
const runtime = typeof globalThis.Bun === 'undefined' ? 'node' : 'bun';
|
|
102
103
|
let ptyProviderPromise = null;
|
|
104
|
+
let shutdownPromise = null;
|
|
103
105
|
let wsServer = new WebSocketServer({ noServer: true, maxPayload: TERMINAL_WS_MAX_PAYLOAD_BYTES });
|
|
104
106
|
const shellResolver = createTerminalShellResolver({ fs, path, searchPathFor, isExecutable, buildAugmentedPath });
|
|
105
107
|
|
|
@@ -135,7 +137,7 @@ export function createTerminalRuntime({
|
|
|
135
137
|
const launch = resolvePosixPtyLaunch(shellLaunch.executable, shellLaunch.args);
|
|
136
138
|
const options = { name: 'xterm-256color', cwd, cols, rows, env };
|
|
137
139
|
if (process.platform === 'win32') options.useConpty = true;
|
|
138
|
-
return { process: await provider.spawn(launch.executable, launch.args, options), backend: provider.backend, shell: resolvedShell.id, loginShell };
|
|
140
|
+
return { process: await provider.spawn(launch.executable, launch.args, options), backend: provider.backend, shell: resolvedShell.id, shellExecutable: executable, loginShell };
|
|
139
141
|
} catch (error) { lastError = error; }
|
|
140
142
|
}
|
|
141
143
|
throw lastError ?? new Error('No executable shell found');
|
|
@@ -273,6 +275,7 @@ export function createTerminalRuntime({
|
|
|
273
275
|
if (clear) { session.history = ''; session.pendingHistoryControlSequence = ''; session.pendingThemeControlSequence = ''; session.themeModeEnabled = false; }
|
|
274
276
|
session.cwd = cwd; session.cols = cols; session.rows = rows; session.process = spawned.process;
|
|
275
277
|
session.backend = spawned.backend; session.shell = spawned.shell; session.loginShell = spawned.loginShell; session.status = 'running'; session.exitCode = null; session.signal = null;
|
|
278
|
+
session.shellExecutable = spawned.shellExecutable;
|
|
276
279
|
session.mode = mode; session.command = mode === COMMAND_TERMINAL_MODE ? command : null;
|
|
277
280
|
session.purpose = purpose;
|
|
278
281
|
session.themeMode = themeMode === 'light' ? 'light' : 'dark'; session.terminalBackground = terminalBackground; session.terminalForeground = terminalForeground;
|
|
@@ -282,6 +285,7 @@ export function createTerminalRuntime({
|
|
|
282
285
|
};
|
|
283
286
|
|
|
284
287
|
const createSession = async ({ sessionId, cwd, cols = 80, rows = 24, themeMode, terminalBackground, terminalForeground, shell = 'auto', loginShell = false, mode, command, purpose }) => {
|
|
288
|
+
if (shutdownPromise) throw new Error('Terminal runtime is shutting down');
|
|
285
289
|
if (!validateSize(cols, 1000) || !validateSize(rows, 500)) throw new Error('Invalid terminal dimensions');
|
|
286
290
|
if (typeof loginShell !== 'boolean') throw new Error('Invalid terminal login mode');
|
|
287
291
|
const normalizedShell = normalizeTerminalShell(shell);
|
|
@@ -507,11 +511,17 @@ export function createTerminalRuntime({
|
|
|
507
511
|
const previousRestart = pendingSessionRestarts.get(session.id) ?? Promise.resolve();
|
|
508
512
|
const restart = previousRestart.catch(() => {}).then(async () => {
|
|
509
513
|
await validateCwd(cwd);
|
|
514
|
+
if (shutdownPromise || sessions.get(session.id) !== session) throw new Error('Terminal session was closed during restart');
|
|
510
515
|
if (!validateSize(cols, 1000) || !validateSize(rows, 500)) throw new Error('Invalid terminal dimensions');
|
|
511
516
|
if (typeof loginShell !== 'boolean') throw new Error('Invalid terminal login mode');
|
|
512
517
|
const oldProcess = session.process;
|
|
513
518
|
const spawned = await spawnPty({ cwd, cols, rows, themeMode, shell, loginShell });
|
|
519
|
+
if (shutdownPromise || sessions.get(session.id) !== session) {
|
|
520
|
+
await terminateProcess(spawned.process, true);
|
|
521
|
+
throw new Error('Terminal session was closed during restart');
|
|
522
|
+
}
|
|
514
523
|
session.process = spawned.process; session.backend = spawned.backend; session.shell = spawned.shell; session.loginShell = spawned.loginShell; session.cwd = cwd; session.cols = cols; session.rows = rows;
|
|
524
|
+
session.shellExecutable = spawned.shellExecutable;
|
|
515
525
|
session.history = ''; session.pendingHistoryControlSequence = ''; session.pendingThemeControlSequence = ''; session.themeModeEnabled = false; session.status = 'running'; session.exitCode = null; session.signal = null; session.eventQueue.length = 0;
|
|
516
526
|
session.themeMode = themeMode === 'light' ? 'light' : 'dark'; session.terminalBackground = terminalBackground; session.terminalForeground = terminalForeground;
|
|
517
527
|
wire(session, spawned.process); void terminateProcess(oldProcess); publish(session, { t: 'restarted', history: '' });
|
|
@@ -560,11 +570,19 @@ export function createTerminalRuntime({
|
|
|
560
570
|
}
|
|
561
571
|
}, 5 * 60 * 1000);
|
|
562
572
|
|
|
563
|
-
const
|
|
573
|
+
const stop = async () => {
|
|
564
574
|
server.off('upgrade', upgradeHandler); clearInterval(idleSweep);
|
|
565
|
-
|
|
566
|
-
for (const
|
|
575
|
+
for (const client of wsServer?.clients ?? []) client.terminate();
|
|
576
|
+
for (const pending of pendingSessionCreates.values()) pending.cancelled = true;
|
|
577
|
+
await Promise.allSettled([
|
|
578
|
+
...[...pendingSessionCreates.values()].map((pending) => pending.promise),
|
|
579
|
+
...pendingSessionRestarts.values(),
|
|
580
|
+
]);
|
|
581
|
+
const terminals = [...sessions.values()]
|
|
582
|
+
.filter(session => session.status === 'running' && session.process)
|
|
583
|
+
.map(session => ({ process: session.process, shellExecutable: session.shellExecutable }));
|
|
567
584
|
sessions.clear();
|
|
585
|
+
await shutdownProcesses(terminals);
|
|
568
586
|
await Promise.allSettled([...pendingTerminations]);
|
|
569
587
|
if (!wsServer) return;
|
|
570
588
|
for (const client of wsServer.clients) client.terminate();
|
|
@@ -574,5 +592,9 @@ export function createTerminalRuntime({
|
|
|
574
592
|
]);
|
|
575
593
|
wsServer = null;
|
|
576
594
|
};
|
|
595
|
+
const shutdown = () => {
|
|
596
|
+
if (!shutdownPromise) shutdownPromise = stop();
|
|
597
|
+
return shutdownPromise;
|
|
598
|
+
};
|
|
577
599
|
return { shutdown };
|
|
578
600
|
}
|