@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
|
@@ -2,6 +2,28 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
4
4
|
|
|
5
|
+
it('bounds a contiguous replay suffix by UTF-8 bytes and event count', async () => {
|
|
6
|
+
const blocks = Array.from({ length: 8 }, (_, i) => `id: e${i}\ndata: ${JSON.stringify({ type: 'message', properties: { text: '界'.repeat(40) } })}\n\n`);
|
|
7
|
+
const received = [];
|
|
8
|
+
const hub = createGlobalMessageStreamHub({
|
|
9
|
+
buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
|
|
10
|
+
getOpenCodeAuthHeaders: () => ({}), replayLimit: 3, replayByteLimit: 550,
|
|
11
|
+
upstreamReconnectDelayMs: 60_000,
|
|
12
|
+
fetchImpl: async () => createSseResponse({ blocks }),
|
|
13
|
+
});
|
|
14
|
+
hub.subscribeEvent(event => received.push(event.eventId));
|
|
15
|
+
try {
|
|
16
|
+
hub.start();
|
|
17
|
+
await waitForAssertion(() => expect(received).toHaveLength(8));
|
|
18
|
+
expect(hub.replayAfter('e0')).toBeNull();
|
|
19
|
+
expect(hub.replayAfter('e5')).toBeNull();
|
|
20
|
+
const tail = hub.replayAfter('e6');
|
|
21
|
+
expect(tail.map(entry => entry.eventId)).toEqual(['e7']);
|
|
22
|
+
expect(Buffer.byteLength(tail[0].serializedFrame) * 2).toBeLessThanOrEqual(550);
|
|
23
|
+
expect(Buffer.byteLength(tail[0].serializedFrame) * 3).toBeGreaterThan(550);
|
|
24
|
+
} finally { hub.stop(); }
|
|
25
|
+
});
|
|
26
|
+
|
|
5
27
|
function createSseResponse({ blocks = [] } = {}) {
|
|
6
28
|
const encoder = new TextEncoder();
|
|
7
29
|
let index = 0;
|
|
@@ -40,6 +62,166 @@ async function waitForAssertion(assertion) {
|
|
|
40
62
|
throw lastError;
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
const deltaBlock = (id, text, partID = 'prt_a') => `id: ${id}\ndata: ${JSON.stringify({
|
|
66
|
+
id, type: 'message.part.delta',
|
|
67
|
+
properties: { sessionID: 'ses_1', messageID: 'msg_1', partID, field: 'text', delta: text },
|
|
68
|
+
})}\n\n`;
|
|
69
|
+
|
|
70
|
+
const createDeltaHub = ({ blocks, deltaCoalesceWindowMs }) => createGlobalMessageStreamHub({
|
|
71
|
+
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
72
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
73
|
+
upstreamReconnectDelayMs: 60_000,
|
|
74
|
+
deltaCoalesceWindowMs,
|
|
75
|
+
fetchImpl: async () => createSseResponse({ blocks }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// What a browser holds after applying frames in order: text per part, and the
|
|
79
|
+
// text each part had when a snapshot barrier passed.
|
|
80
|
+
const applyFrames = (frames) => {
|
|
81
|
+
const text = {};
|
|
82
|
+
const barriers = [];
|
|
83
|
+
for (const frame of frames) {
|
|
84
|
+
const payload = frame.payload;
|
|
85
|
+
if (payload.type === 'message.part.delta') {
|
|
86
|
+
text[payload.properties.partID] = (text[payload.properties.partID] ?? '') + payload.properties.delta;
|
|
87
|
+
} else {
|
|
88
|
+
barriers.push({ id: payload.id, seen: { ...text } });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { text, barriers };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
describe('delta coalescing in the global hub', () => {
|
|
95
|
+
it('delivers the same text in far fewer frames', async () => {
|
|
96
|
+
const words = Array.from({ length: 120 }, (_, index) => `w${index} `);
|
|
97
|
+
const hub = createDeltaHub({ blocks: words.map((word, index) => deltaBlock(`e${String(index).padStart(4, '0')}`, word)) });
|
|
98
|
+
const received = [];
|
|
99
|
+
hub.subscribeEvent((event) => received.push(event));
|
|
100
|
+
try {
|
|
101
|
+
hub.start();
|
|
102
|
+
await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(words.join('')));
|
|
103
|
+
expect(received.length).toBeLessThanOrEqual(3);
|
|
104
|
+
expect(received.at(-1).eventId).toBe('e0119');
|
|
105
|
+
} finally { hub.stop(); }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('resumes from any cursor without losing or repeating text', async () => {
|
|
109
|
+
const blocks = [];
|
|
110
|
+
let id = 0;
|
|
111
|
+
const nextId = () => `e${String(id++).padStart(4, '0')}`;
|
|
112
|
+
for (let index = 0; index < 40; index += 1) blocks.push(deltaBlock(nextId(), `a${index}.`, index % 3 === 0 ? 'prt_b' : 'prt_a'));
|
|
113
|
+
const snapshotId = nextId();
|
|
114
|
+
blocks.push(`id: ${snapshotId}\ndata: ${JSON.stringify({ id: snapshotId, type: 'message.part.updated', properties: { part: { id: 'prt_a', messageID: 'msg_1' } } })}\n\n`);
|
|
115
|
+
for (let index = 0; index < 40; index += 1) blocks.push(deltaBlock(nextId(), `b${index}.`));
|
|
116
|
+
|
|
117
|
+
const hub = createDeltaHub({ blocks });
|
|
118
|
+
const received = [];
|
|
119
|
+
hub.subscribeEvent((event) => received.push(event));
|
|
120
|
+
try {
|
|
121
|
+
hub.start();
|
|
122
|
+
const expectedA = [...Array(40).keys()].filter((index) => index % 3 !== 0).map((index) => `a${index}.`).join('')
|
|
123
|
+
+ [...Array(40).keys()].map((index) => `b${index}.`).join('');
|
|
124
|
+
await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(expectedA));
|
|
125
|
+
expect(received.length).toBeLessThan(blocks.length / 4);
|
|
126
|
+
|
|
127
|
+
const complete = applyFrames(received);
|
|
128
|
+
// The snapshot barrier saw exactly the text that arrived before it.
|
|
129
|
+
expect(complete.barriers).toHaveLength(1);
|
|
130
|
+
expect(complete.barriers[0].seen.prt_a).toBe([...Array(40).keys()].filter((index) => index % 3 !== 0).map((index) => `a${index}.`).join(''));
|
|
131
|
+
|
|
132
|
+
// A socket that drops after any frame reconnects with that frame's id.
|
|
133
|
+
for (let cut = 0; cut < received.length; cut += 1) {
|
|
134
|
+
const tail = hub.replayAfter(received[cut].eventId);
|
|
135
|
+
expect(tail).not.toBeNull();
|
|
136
|
+
const replayed = tail.map((entry) => JSON.parse(entry.serializedFrame));
|
|
137
|
+
expect(applyFrames([...received.slice(0, cut + 1), ...replayed])).toEqual(complete);
|
|
138
|
+
}
|
|
139
|
+
} finally { hub.stop(); }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// OpenCode 1.18 sends no SSE ids at all. Before the hub numbered such events
|
|
143
|
+
// itself the replay buffer stayed empty and every reconnect lost its gap.
|
|
144
|
+
it('numbers id-less upstream events so a reconnect resumes from any cursor', async () => {
|
|
145
|
+
const idless = (type, properties) => `data: ${JSON.stringify({ type, properties })}\n\n`;
|
|
146
|
+
const blocks = [];
|
|
147
|
+
for (let index = 0; index < 30; index += 1) {
|
|
148
|
+
blocks.push(idless('message.part.delta', { sessionID: 'ses_1', messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: `a${index}.` }));
|
|
149
|
+
}
|
|
150
|
+
blocks.push(idless('message.part.updated', { part: { id: 'prt_a', messageID: 'msg_1' } }));
|
|
151
|
+
for (let index = 0; index < 30; index += 1) {
|
|
152
|
+
blocks.push(idless('message.part.delta', { sessionID: 'ses_1', messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: `b${index}.` }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hub = createDeltaHub({ blocks });
|
|
156
|
+
const received = [];
|
|
157
|
+
hub.subscribeEvent((event) => received.push(event));
|
|
158
|
+
try {
|
|
159
|
+
hub.start();
|
|
160
|
+
const expected = [...Array(30).keys()].map((index) => `a${index}.`).join('') + [...Array(30).keys()].map((index) => `b${index}.`).join('');
|
|
161
|
+
await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(expected));
|
|
162
|
+
|
|
163
|
+
const ids = received.map((event) => event.eventId);
|
|
164
|
+
expect(ids.every((eventId) => typeof eventId === 'string' && eventId.length > 0)).toBe(true);
|
|
165
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
166
|
+
expect(ids).toEqual([...ids].sort());
|
|
167
|
+
|
|
168
|
+
const complete = applyFrames(received);
|
|
169
|
+
for (let cut = 0; cut < received.length; cut += 1) {
|
|
170
|
+
const tail = hub.replayAfter(received[cut].eventId);
|
|
171
|
+
expect(tail).not.toBeNull();
|
|
172
|
+
const replayed = tail.map((entry) => JSON.parse(entry.serializedFrame));
|
|
173
|
+
expect(replayed.every((frame) => typeof frame.eventId === 'string')).toBe(true);
|
|
174
|
+
expect(applyFrames([...received.slice(0, cut + 1), ...replayed])).toEqual(complete);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A cursor minted by another server process must miss, never match by
|
|
178
|
+
// sequence number: the bridge then reports replayReset and the client
|
|
179
|
+
// repairs from HTTP instead of trusting a wrong tail.
|
|
180
|
+
const foreign = ids[0].replace(/^oc-[^-]+-/, 'oc-00000000-');
|
|
181
|
+
expect(foreign).not.toBe(ids[0]);
|
|
182
|
+
expect(hub.replayAfter(foreign)).toBeNull();
|
|
183
|
+
} finally { hub.stop(); }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('keeps pending text for replay when the hub stops', async () => {
|
|
187
|
+
const hub = createDeltaHub({
|
|
188
|
+
deltaCoalesceWindowMs: 60_000,
|
|
189
|
+
blocks: ['one ', 'two ', 'three'].map((text, index) => deltaBlock(`e${index}`, text)),
|
|
190
|
+
});
|
|
191
|
+
const received = [];
|
|
192
|
+
hub.subscribeEvent((event) => received.push(event));
|
|
193
|
+
hub.start();
|
|
194
|
+
// The window is a minute, so only the leading delta has been delivered.
|
|
195
|
+
await waitForAssertion(() => expect(received).toHaveLength(1));
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
197
|
+
|
|
198
|
+
hub.stop();
|
|
199
|
+
|
|
200
|
+
const tail = hub.replayAfter('e0').map((entry) => JSON.parse(entry.serializedFrame));
|
|
201
|
+
expect(applyFrames(tail).text.prt_a).toBe('two three');
|
|
202
|
+
expect(tail.at(-1).eventId).toBe('e2');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('commits pending text on demand, so a client readied later starts after it', async () => {
|
|
206
|
+
const hub = createDeltaHub({
|
|
207
|
+
deltaCoalesceWindowMs: 60_000,
|
|
208
|
+
blocks: ['one ', 'two ', 'three'].map((text, index) => deltaBlock(`e${index}`, text)),
|
|
209
|
+
});
|
|
210
|
+
const received = [];
|
|
211
|
+
hub.subscribeEvent((event) => received.push(event));
|
|
212
|
+
try {
|
|
213
|
+
hub.start();
|
|
214
|
+
await waitForAssertion(() => expect(received).toHaveLength(1));
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
216
|
+
|
|
217
|
+
hub.flushPending();
|
|
218
|
+
|
|
219
|
+
expect(applyFrames(received).text.prt_a).toBe('one two three');
|
|
220
|
+
expect(hub.replayAfter('e2')).toEqual([]);
|
|
221
|
+
} finally { hub.stop(); }
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
43
225
|
describe('createGlobalMessageStreamHub', () => {
|
|
44
226
|
it('continues fanout when an event subscriber throws', async () => {
|
|
45
227
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sendMessageStreamWsEvent, sendMessageStreamWsFrame } from './protocol.js';
|
|
1
|
+
import { sendMessageStreamWsEvent, sendMessageStreamWsFrame, sendSerializedMessageStreamWsFrame, serializeMessageStreamWsEvent } from './protocol.js';
|
|
2
2
|
|
|
3
3
|
function shouldTriggerUpstreamHealthCheck(upstream) {
|
|
4
4
|
if (!upstream) {
|
|
@@ -31,12 +31,9 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
31
31
|
wsClients.delete(socket);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
const replayEvents = (socket,
|
|
35
|
-
for (const entry of
|
|
36
|
-
const sent =
|
|
37
|
-
directory: entry.directory,
|
|
38
|
-
eventId: entry.eventId,
|
|
39
|
-
});
|
|
34
|
+
const replayEvents = (socket, entries) => {
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const sent = sendSerializedMessageStreamWsFrame(socket, entry.serializedFrame);
|
|
40
37
|
if (!sent) {
|
|
41
38
|
removeClient(socket);
|
|
42
39
|
return;
|
|
@@ -49,10 +46,11 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
globalHub.flushPending();
|
|
50
|
+
const replay = globalHub.replayAfter(requestedLastEventId);
|
|
51
|
+
const ready = { type: 'ready', scope: 'global' };
|
|
52
|
+
if (replay === null) ready.replayReset = true;
|
|
53
|
+
const sent = sendMessageStreamWsFrame(socket, ready);
|
|
56
54
|
if (!sent) {
|
|
57
55
|
removeClient(socket);
|
|
58
56
|
return;
|
|
@@ -60,7 +58,7 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
60
58
|
|
|
61
59
|
readyClients.add(socket);
|
|
62
60
|
wsClients.add(socket);
|
|
63
|
-
replayEvents(socket,
|
|
61
|
+
if (replay !== null) replayEvents(socket, replay);
|
|
64
62
|
};
|
|
65
63
|
|
|
66
64
|
const stopHubIfUnused = () => {
|
|
@@ -88,26 +86,26 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
88
86
|
}
|
|
89
87
|
};
|
|
90
88
|
|
|
91
|
-
const unsubscribeEvent = globalHub.subscribeEvent((
|
|
89
|
+
const unsubscribeEvent = globalHub.subscribeEvent((event) => {
|
|
90
|
+
const { payload } = event;
|
|
92
91
|
for (const socket of Array.from(clients)) {
|
|
93
92
|
if (!readyClients.has(socket)) {
|
|
94
93
|
continue;
|
|
95
94
|
}
|
|
96
|
-
const sent =
|
|
97
|
-
directory,
|
|
98
|
-
eventId,
|
|
99
|
-
});
|
|
95
|
+
const sent = sendSerializedMessageStreamWsFrame(socket, event.serialize());
|
|
100
96
|
if (!sent) {
|
|
101
97
|
removeClient(socket);
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
processForwardedEventPayload(payload, (syntheticPayload) => {
|
|
102
|
+
if (readyClients.size === 0) return;
|
|
103
|
+
const serializedFrame = serializeMessageStreamWsEvent(syntheticPayload, { directory: 'global' });
|
|
106
104
|
for (const socket of Array.from(clients)) {
|
|
107
105
|
if (!readyClients.has(socket)) {
|
|
108
106
|
continue;
|
|
109
107
|
}
|
|
110
|
-
const sent =
|
|
108
|
+
const sent = sendSerializedMessageStreamWsFrame(socket, serializedFrame);
|
|
111
109
|
if (!sent) {
|
|
112
110
|
removeClient(socket);
|
|
113
111
|
}
|
|
@@ -71,6 +71,14 @@ export function parseSseEventEnvelope(block) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function sendMessageStreamWsFrame(socket, payload) {
|
|
74
|
+
try {
|
|
75
|
+
return sendSerializedMessageStreamWsFrame(socket, JSON.stringify(payload));
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sendSerializedMessageStreamWsFrame(socket, serializedFrame) {
|
|
74
82
|
if (!socket || socket.readyState !== 1) {
|
|
75
83
|
return false;
|
|
76
84
|
}
|
|
@@ -86,7 +94,7 @@ export function sendMessageStreamWsFrame(socket, payload) {
|
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
try {
|
|
89
|
-
socket.send(
|
|
97
|
+
socket.send(serializedFrame);
|
|
90
98
|
const bufferedAfter = typeof socket.bufferedAmount === 'number' ? socket.bufferedAmount : 0;
|
|
91
99
|
if (bufferedAfter > MESSAGE_STREAM_WS_MAX_BUFFERED_BYTES) {
|
|
92
100
|
try {
|
|
@@ -124,10 +132,16 @@ export function sendMessageStreamWsFrame(socket, payload) {
|
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
export function sendMessageStreamWsEvent(socket, payload, options = {}) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
try {
|
|
136
|
+
return sendSerializedMessageStreamWsFrame(socket, serializeMessageStreamWsEvent(payload, options));
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function serializeMessageStreamWsEvent(payload, options = {}) {
|
|
143
|
+
const frame = { type: 'event', payload };
|
|
144
|
+
if (typeof options.eventId === 'string' && options.eventId.length > 0) frame.eventId = options.eventId;
|
|
145
|
+
if (typeof options.directory === 'string' && options.directory.length > 0) frame.directory = options.directory;
|
|
146
|
+
return JSON.stringify(frame);
|
|
133
147
|
}
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
MESSAGE_STREAM_DIRECTORY_WS_PATH,
|
|
6
6
|
MESSAGE_STREAM_GLOBAL_WS_PATH,
|
|
7
7
|
MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
|
|
8
|
-
|
|
8
|
+
serializeMessageStreamWsEvent,
|
|
9
|
+
sendSerializedMessageStreamWsFrame,
|
|
9
10
|
} from './protocol.js';
|
|
10
11
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
11
12
|
import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
|
|
@@ -28,20 +29,22 @@ export function createGlobalUiEventBroadcaster({
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
if (hasSseClients) {
|
|
32
|
+
const serializedPayload = JSON.stringify(payload);
|
|
31
33
|
for (const res of sseClients) {
|
|
32
34
|
try {
|
|
33
|
-
writeSseEvent(res, payload);
|
|
35
|
+
writeSseEvent(res, payload, serializedPayload);
|
|
34
36
|
} catch {
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
if (hasWsClients) {
|
|
42
|
+
const serializedFrame = serializeMessageStreamWsEvent(payload, {
|
|
43
|
+
directory: typeof options.directory === 'string' && options.directory.length > 0 ? options.directory : 'global',
|
|
44
|
+
eventId: typeof options.eventId === 'string' && options.eventId.length > 0 ? options.eventId : undefined,
|
|
45
|
+
});
|
|
40
46
|
for (const socket of Array.from(wsClients)) {
|
|
41
|
-
const sent =
|
|
42
|
-
directory: typeof options.directory === 'string' && options.directory.length > 0 ? options.directory : 'global',
|
|
43
|
-
eventId: typeof options.eventId === 'string' && options.eventId.length > 0 ? options.eventId : undefined,
|
|
44
|
-
});
|
|
47
|
+
const sent = sendSerializedMessageStreamWsFrame(socket, serializedFrame);
|
|
45
48
|
if (!sent) {
|
|
46
49
|
wsClients.delete(socket);
|
|
47
50
|
}
|
|
@@ -2,6 +2,66 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { createGlobalUiEventBroadcaster, createMessageStreamWsRuntime } from './runtime.js';
|
|
5
|
+
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
6
|
+
import { createNotificationEmitterRuntime } from '../notifications/emitter-runtime.js';
|
|
7
|
+
|
|
8
|
+
it('serializes a UI broadcast once per wire format and isolates failed clients', () => {
|
|
9
|
+
let serializations = 0;
|
|
10
|
+
const payload = { toJSON() { serializations++; return { type: 'openchamber:test', properties: { value: 'same' } }; } };
|
|
11
|
+
const writes = [];
|
|
12
|
+
const sseClients = new Set(Array.from({ length: 4 }, () => ({ write: text => writes.push(text) })));
|
|
13
|
+
const sockets = Array.from({ length: 4 }, () => ({ readyState: 1, bufferedAmount: 0, send: text => writes.push(text) }));
|
|
14
|
+
const failed = { readyState: 1, bufferedAmount: 0, send() { throw new Error('closed'); } };
|
|
15
|
+
const wsClients = new Set([...sockets, failed]);
|
|
16
|
+
const { writeSseEvent } = createNotificationEmitterRuntime({});
|
|
17
|
+
const broadcast = createGlobalUiEventBroadcaster({ sseClients, wsClients, writeSseEvent });
|
|
18
|
+
broadcast(payload);
|
|
19
|
+
expect(serializations).toBe(2);
|
|
20
|
+
expect(writes).toHaveLength(8);
|
|
21
|
+
expect(wsClients.has(failed)).toBe(false);
|
|
22
|
+
expect(wsClients.size).toBe(4);
|
|
23
|
+
expect(writes[0]).toBe(writes[3]);
|
|
24
|
+
expect(writes[4]).toBe(writes[7]);
|
|
25
|
+
sseClients.clear(); wsClients.clear();
|
|
26
|
+
broadcast(payload);
|
|
27
|
+
expect(serializations).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('delivers oversized events live, but reports a replay gap instead of replaying across it', async () => {
|
|
31
|
+
const hub = createGlobalMessageStreamHub({
|
|
32
|
+
buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
|
|
33
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
34
|
+
replayByteLimit: 512,
|
|
35
|
+
fetchImpl: async (_url, options) => createSseResponse({
|
|
36
|
+
signal: options.signal, holdOpen: true,
|
|
37
|
+
blocks: [
|
|
38
|
+
'id: first\ndata: {"type":"first","properties":{}}\n\n',
|
|
39
|
+
`id: large\ndata: ${JSON.stringify({ type: 'large', properties: { text: '界'.repeat(1024) } })}\n\n`,
|
|
40
|
+
'id: last\ndata: {"type":"last","properties":{}}\n\n',
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
const runtime = createMessageStreamWsRuntime({
|
|
45
|
+
server: new EventEmitter(), globalEventHub: hub, wsClients: new Set(),
|
|
46
|
+
uiAuthController: null, isRequestOriginAllowed: async () => true,
|
|
47
|
+
rejectWebSocketUpgrade() {}, buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
|
|
48
|
+
getOpenCodeAuthHeaders: () => ({}), processForwardedEventPayload() {},
|
|
49
|
+
});
|
|
50
|
+
const first = new FakeSocket();
|
|
51
|
+
const reconnect = new FakeSocket();
|
|
52
|
+
try {
|
|
53
|
+
runtime.wsServer.emit('connection', first, { url: '/api/global/event/ws' });
|
|
54
|
+
await expect.poll(() => first.sent.filter(frame => frame.type === 'event').length).toBe(3);
|
|
55
|
+
expect(first.sent.find(frame => frame.eventId === 'large').payload.properties.text).toHaveLength(1024);
|
|
56
|
+
expect(hub.replayAfter('first')).toBeNull();
|
|
57
|
+
expect(hub.replayAfter('last')).toEqual([]);
|
|
58
|
+
runtime.wsServer.emit('connection', reconnect, { url: '/api/global/event/ws?lastEventId=first' });
|
|
59
|
+
await expect.poll(() => reconnect.sent.length).toBeGreaterThan(0);
|
|
60
|
+
expect(reconnect.sent).toEqual([{ type: 'ready', scope: 'global', replayReset: true }]);
|
|
61
|
+
} finally {
|
|
62
|
+
first.close(); reconnect.close(); hub.stop(); await runtime.close();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
5
65
|
|
|
6
66
|
class FakeSocket extends EventEmitter {
|
|
7
67
|
constructor() {
|
|
@@ -36,6 +36,14 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
|
|
|
36
36
|
- Returns `{ searchFilesystemFiles(rootPath, options) }`.
|
|
37
37
|
- Supports fuzzy matching, hidden-file handling, and optional `git check-ignore` filtering.
|
|
38
38
|
|
|
39
|
+
Both search and directory listing discard `git check-ignore` stderr at spawn.
|
|
40
|
+
They consume stdout for ignore matches. Never create an unread stderr pipe:
|
|
41
|
+
Git diagnostics can fill it and block the child indefinitely, so repeated
|
|
42
|
+
searches accumulate live Git processes. `git-process.test.js` exercises both
|
|
43
|
+
paths with real OS pipes, twelve concurrent checks, and 2 MiB of diagnostics
|
|
44
|
+
per child. Successful completion must preserve ignore filtering and reap all
|
|
45
|
+
twelve children.
|
|
46
|
+
|
|
39
47
|
## Composition contract with `index.js`
|
|
40
48
|
- `index.js` provides composition-time dependencies only (platform primitives + callbacks such as `resolveProjectDirectory`, `normalizeDirectoryPath`, and `buildAugmentedPath`).
|
|
41
49
|
- `index.js` no longer owns FS route handlers or FS exec job state.
|
|
@@ -43,7 +51,8 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
|
|
|
43
51
|
## Notes for contributors
|
|
44
52
|
- Keep filesystem policy (workspace root checks, error mapping, exec timeout behavior) inside this module, not in the composition root.
|
|
45
53
|
- Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
|
|
46
|
-
- `GET /api/fs/home`
|
|
54
|
+
- `GET /api/fs/home` preserves `{ home, chatsRoot }` and adds `canonicalChatsRoot` and `canonicalLegacyChatsRoot`, resolved by the server's filesystem. If a root does not exist yet, it resolves the nearest existing ancestor and appends the missing segments, without creating directories. Errors resolving the configured root fail the request. A failed legacy lookup warns and omits only that optional alias, retaining exact legacy matching without blocking an accessible relocated root. Clients use confirmed aliases for exact membership while keeping the original roots as folder/scope identities. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
|
|
55
|
+
- Workspace authorization accepts both the configured managed paths and their filesystem-confirmed canonical roots. Canonical lookup runs only when lexical workspace/managed-root checks fail. One failed managed-root lookup cannot reject an independently authorized root or worktree. Path comparisons remain case-sensitive on POSIX: distinct Linux directories named `chats` and `Chats` never become aliases through string folding. Shared UI keeps exact root membership for classification and directory cleanup.
|
|
47
56
|
- Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
|
|
48
57
|
- `GET /api/fs/directory-stat?path=...` uses one `stat` without listing contents or resolving project topology. It follows the same authenticated directory-discovery path policy as `/api/fs/list`, including targets outside the active workspace. A directory returns `{ isDirectory: true }`; `ENOENT` returns `not-found`, and a file or `ENOTDIR` returns `not-directory`. Permission and other failures remain distinct from a missing path. VS Code explicitly returns 501, so the shared client treats its probe as unknown.
|
|
49
58
|
- Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { createFsSearchRuntime } from './search.js';
|
|
8
|
+
import { registerFsRoutes } from './routes.js';
|
|
9
|
+
|
|
10
|
+
// Use real OS pipes. A mocked data event cannot reproduce a full stderr pipe.
|
|
11
|
+
const noisyGit = `
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const chunk = Buffer.alloc(65536, 'x');
|
|
14
|
+
for (let i = 0; i < 32; i++) fs.writeSync(2, chunk);
|
|
15
|
+
fs.writeSync(1, 'ignored.txt\\n');
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
describe('filesystem Git process ownership', () => {
|
|
19
|
+
for (const operation of ['search', 'list']) {
|
|
20
|
+
it(`${operation} completes repeated checks with more diagnostics than an OS pipe can hold`, async () => {
|
|
21
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-git-pipes-'));
|
|
22
|
+
const children = [];
|
|
23
|
+
const exits = [];
|
|
24
|
+
const launch = (binary, args, options) => {
|
|
25
|
+
expect(binary).toBe('git');
|
|
26
|
+
expect(args).toContain('check-ignore');
|
|
27
|
+
expect(args).toContain('ignored.txt');
|
|
28
|
+
const child = spawn(process.execPath, ['-e', noisyGit], options);
|
|
29
|
+
children.push(child);
|
|
30
|
+
exits.push(new Promise((resolve) => child.once('close', (code) => resolve(code))));
|
|
31
|
+
return child;
|
|
32
|
+
};
|
|
33
|
+
let deadline;
|
|
34
|
+
try {
|
|
35
|
+
await fs.writeFile(path.join(root, 'ignored.txt'), 'ignored');
|
|
36
|
+
await fs.writeFile(path.join(root, 'visible.txt'), 'visible');
|
|
37
|
+
const dependencies = { fsPromises: fs, path, spawn: launch, resolveGitBinaryForSpawn: () => 'git' };
|
|
38
|
+
const search = createFsSearchRuntime(dependencies);
|
|
39
|
+
const routes = new Map();
|
|
40
|
+
registerFsRoutes({
|
|
41
|
+
get(route, handler) { routes.set(route, handler); }, post() {}, put() {}, delete() {},
|
|
42
|
+
}, {
|
|
43
|
+
...dependencies, os, crypto, normalizeDirectoryPath: (value) => value,
|
|
44
|
+
resolveProjectDirectory: async () => ({ directory: root }),
|
|
45
|
+
buildAugmentedPath: () => process.env.PATH,
|
|
46
|
+
openchamberUserConfigRoot: root,
|
|
47
|
+
gitCheckIgnoreTimeoutMs: 10_000,
|
|
48
|
+
});
|
|
49
|
+
const work = Array.from({ length: 12 }, async () => {
|
|
50
|
+
if (operation === 'search') {
|
|
51
|
+
const files = await search.searchFilesystemFiles(root, { query: '', limit: 10, respectGitignore: true });
|
|
52
|
+
expect(files.map((file) => file.name)).toEqual(['visible.txt']);
|
|
53
|
+
} else {
|
|
54
|
+
const response = { status() { return this; }, json(value) { this.body = value; } };
|
|
55
|
+
await routes.get('/api/fs/list')({ query: { path: root, respectGitignore: 'true' } }, response);
|
|
56
|
+
expect(response.body.entries.map((entry) => entry.name)).toEqual(['visible.txt']);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
await Promise.race([
|
|
60
|
+
Promise.all(work),
|
|
61
|
+
new Promise((_, reject) => { deadline = setTimeout(() => reject(new Error('Git blocked on unread diagnostics')), 5000); }),
|
|
62
|
+
]);
|
|
63
|
+
expect(children).toHaveLength(12);
|
|
64
|
+
expect(await Promise.all(exits)).toEqual(Array(12).fill(0));
|
|
65
|
+
for (const child of children) expect(child.exitCode).toBe(0);
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(deadline);
|
|
68
|
+
for (const child of children) {
|
|
69
|
+
child.stderr?.resume();
|
|
70
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
71
|
+
}
|
|
72
|
+
await Promise.all(exits);
|
|
73
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}, 15_000);
|
|
76
|
+
}
|
|
77
|
+
});
|