@hienlh/ppm 0.21.0 → 0.22.0
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/CHANGELOG.md +41 -0
- package/CLAUDE.md +29 -4
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
- package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
- package/dist/web/assets/ai-settings-section-X2UOfSaS.js +1 -0
- package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
- package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
- package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
- package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
- package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
- package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
- package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-DLoPK0LD.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
- package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
- package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
- package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
- package/dist/web/assets/branch-review-tab-G84Ojzae.js +1 -0
- package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
- package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
- package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
- package/dist/web/assets/chat-tab-DQJfjm8E.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.js} +1 -1
- package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
- package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-C4ay676e.js} +2 -2
- package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
- package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
- package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
- package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-DbwEmG7o.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
- package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-S6LsALjX.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-3oBA7vgQ.js +4 -0
- package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
- package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
- package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-DJmaKVY0.js} +1 -1
- package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
- package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
- package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
- package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
- package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
- package/dist/web/assets/explorer-body-BQ0jyAGv.js +2 -0
- package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
- package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.js} +1 -1
- package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
- package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
- package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
- package/dist/web/assets/files-settings-section-Gnyd7han.js +1 -0
- package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
- package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
- package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
- package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-V6Khh15V.js} +1 -1
- package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
- package/dist/web/assets/git-log-panel-Bi513qjT.js +1 -0
- package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
- package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
- package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-DnQwYSmQ.js} +7 -7
- package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
- package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
- package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
- package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
- package/dist/web/assets/group-chat-tab-DpczdwyV.js +1 -0
- package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
- package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
- package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
- package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
- package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
- package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
- package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
- package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
- package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
- package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
- package/dist/web/assets/index-BH99TIE0.css +1 -0
- package/dist/web/assets/index-DU68xk06.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
- package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
- package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-f2HUbGDY.js} +1 -1
- package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
- package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
- package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
- package/dist/web/assets/keybindings-store-DCRsnKg5.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.js} +1 -1
- package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
- package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
- package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
- package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +1 -0
- package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -0
- package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
- package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
- package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-Bol3XAO1.js +1 -0
- package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
- package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
- package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
- package/dist/web/assets/notification-store-BXt54VIn.js +1 -0
- package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
- package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
- package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
- package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
- package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
- package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
- package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
- package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
- package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-DKJUA3Kq.js} +1 -1
- package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.js} +1 -1
- package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
- package/dist/web/assets/project-store-B87hLRSk.js +1 -0
- package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-B7qRVl8w.js} +1 -1
- package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
- package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
- package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
- package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-0WDYsyCA.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-UodhsF7W.js +1 -0
- package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
- package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
- package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-bG2lYfGv.js} +1 -1
- package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
- package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
- package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
- package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
- package/dist/web/assets/settings-body-C06bFarv.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
- package/dist/web/assets/settings-window-content-Dr_2lmHH.js +1 -0
- package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
- package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
- package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
- package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
- package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-BgRH0cHD.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.js} +1 -1
- package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
- package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
- package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
- package/dist/web/assets/system-monitor-body-IZuLn7Qh.js +2 -0
- package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.js} +1 -1
- package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
- package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-4ypP0NDY.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-Dd6E7Q3k.js +9 -0
- package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
- package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
- package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
- package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
- package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-vrfFHuaR.js} +1 -1
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
- package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-BMbufhX7.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
- package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
- package/dist/web/assets/video-preview-Byj8P_jA.js +1 -0
- package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
- package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
- package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
- package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
- package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
- package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
- package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
- package/dist/web/index.html +27 -26
- package/dist/web/sw.js +1 -1
- package/docs/architecture/plugins-and-tracing.md +285 -0
- package/docs/lessons-learned.md +31 -0
- package/docs/system-architecture.md +1 -0
- package/package.json +2 -1
- package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
- package/packages/ext-git-graph/src/compare-view.ts +22 -3
- package/packages/ext-git-graph/src/extension.ts +6 -0
- package/packages/ext-git-graph/src/types.ts +19 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +134 -1
- package/packages/ext-git-graph/src/webview-html.ts +277 -27
- package/scripts/gen-file-icons.ts +493 -242
- package/src/providers/claude-agent-sdk.ts +42 -1
- package/src/providers/codex-app-server/codex-history.ts +77 -23
- package/src/server/index.ts +42 -7
- package/src/server/routes/chat.ts +15 -11
- package/src/server/routes/git.ts +83 -3
- package/src/server/routes/remote-desktop.ts +7 -2
- package/src/server/ws/global.ts +1 -1
- package/src/server/ws/remote-desktop.ts +12 -5
- package/src/services/binary-content.ts +25 -0
- package/src/services/chat-search.service.ts +1 -1
- package/src/services/chat.service.ts +9 -0
- package/src/services/event-loop-lag.ts +208 -0
- package/src/services/file-lines.ts +114 -0
- package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
- package/src/services/file-watcher/watch-tree.ts +131 -10
- package/src/services/file-watcher.service.ts +73 -23
- package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
- package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
- package/src/services/git-hunks/git-hunks.service.ts +26 -11
- package/src/services/git-hunks/unified-diff.ts +15 -1
- package/src/services/git.service.ts +74 -24
- package/src/services/jsonl-transcript-parser.ts +138 -6
- package/src/services/log-rotate.ts +142 -0
- package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
- package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
- package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
- package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
- package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
- package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
- package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
- package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
- package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
- package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
- package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
- package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
- package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
- package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
- package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
- package/src/services/supervisor.ts +47 -4
- package/src/shared/remote-desktop-custom-quality.ts +117 -0
- package/src/shared/remote-desktop-quality.ts +127 -0
- package/src/types/chat.ts +4 -0
- package/src/types/git.ts +52 -0
- package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
- package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
- package/src/web/components/chat/attachment-chips.tsx +30 -6
- package/src/web/components/chat/chat-tab.tsx +12 -1
- package/src/web/components/chat/message-events.tsx +273 -0
- package/src/web/components/chat/message-list.tsx +107 -861
- package/src/web/components/chat/message-markdown.tsx +34 -0
- package/src/web/components/chat/message-media.tsx +174 -0
- package/src/web/components/chat/message-user-bubble.tsx +399 -0
- package/src/web/components/chat/provider-selector.tsx +30 -22
- package/src/web/components/chat/session-history-list.tsx +2 -2
- package/src/web/components/chat/session-list-panel.tsx +3 -2
- package/src/web/components/editor/binary-diff-view.tsx +212 -0
- package/src/web/components/editor/diff-viewer.tsx +76 -3
- package/src/web/components/explorer/file-tree.tsx +235 -134
- package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
- package/src/web/components/explorer/tree-node.tsx +69 -69
- package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
- package/src/web/components/git/git-status-panel.tsx +25 -0
- package/src/web/components/layout/draggable-tab.tsx +4 -2
- package/src/web/components/layout/nav-section-rail.tsx +12 -3
- package/src/web/components/layout/status-bar.tsx +10 -6
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +1 -0
- package/src/web/components/layout/upgrade-button.tsx +5 -1
- package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
- package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
- package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
- package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
- package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
- package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
- package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
- package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
- package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
- package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
- package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
- package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
- package/src/web/components/settings/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/hooks/use-delayed-flag.ts +35 -0
- package/src/web/lib/branch-review-state.ts +175 -0
- package/src/web/lib/file-icon-name.ts +62 -0
- package/src/web/lib/file-icons.generated.ts +3287 -25
- package/src/web/lib/file-icons.tsx +71 -73
- package/src/web/lib/flatten-expansions.ts +18 -5
- package/src/web/lib/message-ordinals.ts +23 -0
- package/src/web/lib/provider-logos.tsx +143 -0
- package/src/web/lib/tab-type-icons.ts +11 -0
- package/src/web/stores/file-store.ts +73 -35
- package/src/web/stores/panel-utils.ts +5 -0
- package/src/web/stores/project-framework-store.ts +82 -0
- package/src/web/stores/project-store.ts +7 -1
- package/src/web/stores/settings-store.ts +128 -0
- package/src/web/stores/tab-store.ts +1 -0
- package/src/web/stores/window-panel-persistence.ts +1 -0
- package/src/web/styles/file-icons.generated.css +3282 -234
- package/src/web/styles/globals.css +26 -0
- package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
- package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
- package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
- package/dist/web/assets/api-client-BUCo1lms.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
- package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
- package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
- package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
- package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
- package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
- package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
- package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
- package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
- package/dist/web/assets/file-store-DMABxOkV.js +0 -1
- package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
- package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
- package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
- package/dist/web/assets/index-D7DbXD3G.css +0 -1
- package/dist/web/assets/index-ZtfxZOyb.js +0 -52
- package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
- package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
- package/dist/web/assets/markdown-renderer-CvL1pWeK.js +0 -3
- package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
- package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
- package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
- package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
- package/dist/web/assets/project-store-BhMbVln3.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
- package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
- package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
- package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
- package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
- package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
- package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
- package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
- package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
- package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
- package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
- package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
- package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
- /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
- /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
- /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
- /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
- /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
- /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
- /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
- /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
- /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
- /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
- /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
- /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
- /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
- /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
- /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
- /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
- /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
- /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
- /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
- /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
- /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
- /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
- /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
- /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
- /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
- /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
- /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
- /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
- /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
- /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
- /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
- /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
- /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
- /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
- /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
- /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
- /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
- /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
- /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
- /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
- /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
- /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
- /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
- /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
- /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
- /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
- /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
- /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
- /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
- /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
- /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
- /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
- /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
- /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
- /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
- /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
- /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
- /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
- /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
- /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
- /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
- /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
- /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
- /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
- /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
- /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
- /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
- /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
- /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
- /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
- /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
- /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
- /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
- /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
- /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
- /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
- /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
- /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
- /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
- /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
- /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
- /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
- /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
- /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
- /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
- /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
- /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
- /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
- /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
- /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
- /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
- /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
- /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
- /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
- /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
- /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
- /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
- /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
- /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
- /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
- /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
- /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
- /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
- /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
- /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
- /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
- /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
- /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
- /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
- /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
- /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
- /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
- /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
- /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
- /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
- /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
- /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
- /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
- /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
- /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
- /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
- /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
- /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
- /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
- /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
- /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
- /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
- /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
- /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
- /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
- /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
- /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
- /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
- /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
- /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
- /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
- /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
- /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
- /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
- /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
- /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
- /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
- /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
- /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
- /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
- /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
- /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
- /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
- /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
- /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
- /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
- /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
- /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
- /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
- /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
- /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
- /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
- /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
- /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
- /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
- /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
- /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
- /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
- /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
- /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
- /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
- /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
- /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
- /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
- /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
- /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
- /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
- /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
- /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
- /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
- /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
- /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
- /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
- /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
- /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
- /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
- /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
- /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
- /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
- /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
- /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
- /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
- /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
- /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
- /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
- /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
- /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
- /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
- /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
- /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
- /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
- /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
- /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
- /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
- /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
- /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
- /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
- /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
- /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
- /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
- /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
- /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
- /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
- /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
- /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
- /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
- /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-loop lag, and — the part that decides what to do about it — whether the
|
|
3
|
+
* time was ours.
|
|
4
|
+
*
|
|
5
|
+
* PPM serves every HTTP request, every WebSocket frame and all of the chat work
|
|
6
|
+
* from one Bun process with one event loop, so anything synchronous holds up
|
|
7
|
+
* everything else for exactly as long as it runs. That is measurable from
|
|
8
|
+
* outside (`/api/health`, which returns a literal, has been seen taking 18.9s on
|
|
9
|
+
* a busy instance against 0.12-0.65ms on an idle one of the same binary), but
|
|
10
|
+
* from outside it names no culprit.
|
|
11
|
+
*
|
|
12
|
+
* A late tick means one of two things, and they are fixed in different places:
|
|
13
|
+
*
|
|
14
|
+
* - **self** — the loop was busy with our own synchronous work, or with GC.
|
|
15
|
+
* Worth hunting: a blocking call, a whole-file read, a synchronous DB write.
|
|
16
|
+
* - **starved** — the OS gave the CPU to something else. PPM shares a machine,
|
|
17
|
+
* and when a chat session spawns them a *cgroup*, with compilers, browsers
|
|
18
|
+
* and one `claude` process per session. Nothing in this repository fixes
|
|
19
|
+
* that; moving work to another thread least of all, since the CPU was never
|
|
20
|
+
* ours to move it to.
|
|
21
|
+
*
|
|
22
|
+
* Sampling `process.cpuUsage()` across the same gap tells them apart: time we
|
|
23
|
+
* burned is ours, time we did not is somebody else's. Without that, "the server
|
|
24
|
+
* stalled" is a fact with two opposite conclusions.
|
|
25
|
+
*
|
|
26
|
+
* The monitor costs one timer and two counter reads per tick. It holds its
|
|
27
|
+
* samples in memory and writes nothing to disk — `~/.ppm/ppm.log` is already
|
|
28
|
+
* 261 MB with 54% of its lines duplicated, and a diagnostic that makes the
|
|
29
|
+
* problem it diagnoses worse is not one.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** How often the monitor checks in. */
|
|
33
|
+
export const TICK_MS = 250;
|
|
34
|
+
|
|
35
|
+
/** A tick at least this late is recorded. One whole interval of slip. */
|
|
36
|
+
export const REPORT_THRESHOLD_MS = 250;
|
|
37
|
+
|
|
38
|
+
/** Samples kept. At one stall per tick this is ~50s of solid stalling. */
|
|
39
|
+
export const MAX_SAMPLES = 240;
|
|
40
|
+
|
|
41
|
+
/** CPU/wall at or above this during the gap: the loop was busy with our work. */
|
|
42
|
+
export const SELF_RATIO = 0.7;
|
|
43
|
+
|
|
44
|
+
/** CPU/wall at or below this: the process was not running for most of the gap. */
|
|
45
|
+
export const STARVED_RATIO = 0.25;
|
|
46
|
+
|
|
47
|
+
export type LagCause = "self" | "starved" | "mixed";
|
|
48
|
+
|
|
49
|
+
export interface LagSample {
|
|
50
|
+
/** Epoch ms at which the late tick finally ran. */
|
|
51
|
+
at: number;
|
|
52
|
+
/** How much later than scheduled it ran. */
|
|
53
|
+
lagMs: number;
|
|
54
|
+
/** Process CPU (user + system) consumed across the same gap. */
|
|
55
|
+
cpuMs: number;
|
|
56
|
+
/** Resident set at that moment, to spot a stall that is really GC pressure. */
|
|
57
|
+
rssMb: number;
|
|
58
|
+
cause: LagCause;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LagReport {
|
|
62
|
+
/** Whether the monitor is running. */
|
|
63
|
+
running: boolean;
|
|
64
|
+
/** Ticks observed since it started. */
|
|
65
|
+
ticks: number;
|
|
66
|
+
/** Ticks that slipped past the threshold. */
|
|
67
|
+
stalls: number;
|
|
68
|
+
/** Total slip across those, in ms. */
|
|
69
|
+
blockedMs: number;
|
|
70
|
+
/** The single worst slip seen, in ms. */
|
|
71
|
+
worstMs: number;
|
|
72
|
+
/** Wall time the monitor has been running, in ms. */
|
|
73
|
+
uptimeMs: number;
|
|
74
|
+
/** Share of wall time the loop was unavailable, 0-1. */
|
|
75
|
+
blockedFraction: number;
|
|
76
|
+
/** How the stalls broke down. */
|
|
77
|
+
byCause: Record<LagCause, number>;
|
|
78
|
+
/** Most recent stalls, newest last. */
|
|
79
|
+
samples: LagSample[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Which of the two stories the numbers tell.
|
|
84
|
+
*
|
|
85
|
+
* The ratio can exceed 1: `cpuUsage` counts every thread, and Bun runs file
|
|
86
|
+
* system work on a pool, so a gap spent in parallel IO bills more CPU than wall
|
|
87
|
+
* time passed. That is still our own work, which is what the answer says.
|
|
88
|
+
*/
|
|
89
|
+
export function classifyLag(lagMs: number, cpuMs: number): LagCause {
|
|
90
|
+
if (lagMs <= 0) return "self";
|
|
91
|
+
const ratio = cpuMs / lagMs;
|
|
92
|
+
if (ratio >= SELF_RATIO) return "self";
|
|
93
|
+
if (ratio <= STARVED_RATIO) return "starved";
|
|
94
|
+
return "mixed";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fold one observation into a sample, or return null when the tick was on time.
|
|
99
|
+
*
|
|
100
|
+
* Split out from the timer so the arithmetic — which is the whole of the
|
|
101
|
+
* measurement — can be tested without waiting for real stalls to happen.
|
|
102
|
+
*/
|
|
103
|
+
export function sampleFromTick(args: {
|
|
104
|
+
now: number;
|
|
105
|
+
elapsedMs: number;
|
|
106
|
+
cpuMicros: number;
|
|
107
|
+
rssBytes: number;
|
|
108
|
+
thresholdMs?: number;
|
|
109
|
+
}): LagSample | null {
|
|
110
|
+
const lagMs = args.elapsedMs - TICK_MS;
|
|
111
|
+
if (lagMs < (args.thresholdMs ?? REPORT_THRESHOLD_MS)) return null;
|
|
112
|
+
const cpuMs = args.cpuMicros / 1000;
|
|
113
|
+
return {
|
|
114
|
+
at: args.now,
|
|
115
|
+
lagMs: Math.round(lagMs),
|
|
116
|
+
cpuMs: Math.round(cpuMs),
|
|
117
|
+
rssMb: Math.round(args.rssBytes / 1048576),
|
|
118
|
+
cause: classifyLag(lagMs, cpuMs),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Monitor ───────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
125
|
+
let samples: LagSample[] = [];
|
|
126
|
+
let startedAt = 0;
|
|
127
|
+
let lastAt = 0;
|
|
128
|
+
let lastCpu: { user: number; system: number } | null = null;
|
|
129
|
+
let ticks = 0;
|
|
130
|
+
let stalls = 0;
|
|
131
|
+
let blockedMs = 0;
|
|
132
|
+
let worstMs = 0;
|
|
133
|
+
|
|
134
|
+
function tick(): void {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const cpu = process.cpuUsage();
|
|
137
|
+
const elapsedMs = now - lastAt;
|
|
138
|
+
const cpuMicros = lastCpu
|
|
139
|
+
? cpu.user - lastCpu.user + (cpu.system - lastCpu.system)
|
|
140
|
+
: 0;
|
|
141
|
+
lastAt = now;
|
|
142
|
+
lastCpu = cpu;
|
|
143
|
+
ticks++;
|
|
144
|
+
|
|
145
|
+
const sample = sampleFromTick({
|
|
146
|
+
now,
|
|
147
|
+
elapsedMs,
|
|
148
|
+
cpuMicros,
|
|
149
|
+
rssBytes: process.memoryUsage.rss(),
|
|
150
|
+
});
|
|
151
|
+
if (!sample) return;
|
|
152
|
+
|
|
153
|
+
stalls++;
|
|
154
|
+
blockedMs += sample.lagMs;
|
|
155
|
+
if (sample.lagMs > worstMs) worstMs = sample.lagMs;
|
|
156
|
+
samples.push(sample);
|
|
157
|
+
if (samples.length > MAX_SAMPLES) samples.shift();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Idempotent: calling it twice does not start a second timer. */
|
|
161
|
+
export function startLagMonitor(): void {
|
|
162
|
+
if (timer) return;
|
|
163
|
+
startedAt = Date.now();
|
|
164
|
+
lastAt = startedAt;
|
|
165
|
+
lastCpu = process.cpuUsage();
|
|
166
|
+
ticks = 0;
|
|
167
|
+
stalls = 0;
|
|
168
|
+
blockedMs = 0;
|
|
169
|
+
worstMs = 0;
|
|
170
|
+
samples = [];
|
|
171
|
+
timer = setInterval(tick, TICK_MS);
|
|
172
|
+
// The monitor must never be the reason the process stays alive.
|
|
173
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function stopLagMonitor(): void {
|
|
177
|
+
if (!timer) return;
|
|
178
|
+
clearInterval(timer);
|
|
179
|
+
timer = null;
|
|
180
|
+
lastCpu = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function lagReport(): LagReport {
|
|
184
|
+
const uptimeMs = startedAt ? Date.now() - startedAt : 0;
|
|
185
|
+
const byCause: Record<LagCause, number> = { self: 0, starved: 0, mixed: 0 };
|
|
186
|
+
for (const s of samples) byCause[s.cause]++;
|
|
187
|
+
return {
|
|
188
|
+
running: timer !== null,
|
|
189
|
+
ticks,
|
|
190
|
+
stalls,
|
|
191
|
+
blockedMs,
|
|
192
|
+
worstMs,
|
|
193
|
+
uptimeMs,
|
|
194
|
+
blockedFraction: uptimeMs > 0 ? blockedMs / uptimeMs : 0,
|
|
195
|
+
byCause,
|
|
196
|
+
samples: [...samples],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Test seam: drop every counter and sample without touching the timer. */
|
|
201
|
+
export function resetLagMonitorForTest(): void {
|
|
202
|
+
samples = [];
|
|
203
|
+
ticks = 0;
|
|
204
|
+
stalls = 0;
|
|
205
|
+
blockedMs = 0;
|
|
206
|
+
worstMs = 0;
|
|
207
|
+
startedAt = 0;
|
|
208
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-oriented reads of a file that may be enormous, without holding it.
|
|
3
|
+
*
|
|
4
|
+
* Both functions here replace a `readFileSync(path, "utf-8")` that was correct
|
|
5
|
+
* when the files were small and became a stall when they were not. Measured on
|
|
6
|
+
* this machine:
|
|
7
|
+
*
|
|
8
|
+
* - `~/.ppm/ppm.log` at 276 MB: 264 ms to read + 92 ms to `split("\n")` =
|
|
9
|
+
* **356 ms** of blocked event loop and a 644 MB resident spike, to answer
|
|
10
|
+
* with the last 30 lines. The route doing it (`/api/logs/recent`) is
|
|
11
|
+
* registered *before* `authMiddleware`.
|
|
12
|
+
* - A 35 MB transcript: 34 ms to read as a string + 18 ms to walk it with
|
|
13
|
+
* `charCodeAt`, to arrive at a line count.
|
|
14
|
+
*
|
|
15
|
+
* Neither cost was bounded: both grow with the file, and both were paid on the
|
|
16
|
+
* one thread that also serves every other request.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How much of the end of a file is read to find the last few lines. */
|
|
20
|
+
export const DEFAULT_TAIL_BYTES = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The last `count` lines out of a slice taken from the end of a file.
|
|
24
|
+
*
|
|
25
|
+
* `startedMidFile` is what makes this safe to call on a slice: the first line
|
|
26
|
+
* of one is the tail of a line whose beginning was never read, so a report
|
|
27
|
+
* would open mid-sentence. It is dropped. That also absorbs the other hazard
|
|
28
|
+
* of slicing at an arbitrary byte — a cut through a multi-byte character
|
|
29
|
+
* decodes to U+FFFD, and the replacement character is inside the line being
|
|
30
|
+
* discarded.
|
|
31
|
+
*/
|
|
32
|
+
export function lastLinesFromTail(tail: string, count: number, startedMidFile: boolean): string {
|
|
33
|
+
const lines = tail.split("\n");
|
|
34
|
+
if (startedMidFile && lines.length > 0) lines.shift();
|
|
35
|
+
// A file that ends in a newline splits to a trailing empty string, and it
|
|
36
|
+
// counts against `count` like any other element — asking for the last 1 line
|
|
37
|
+
// of a well-formed log returned that empty string and nothing else. The
|
|
38
|
+
// previous `readFileSync` spelling had the same flaw and got away with it
|
|
39
|
+
// only because it always asked for 30 and then trimmed, quietly answering
|
|
40
|
+
// with 29.
|
|
41
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
42
|
+
return lines.slice(-count).join("\n").trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The last `count` lines of a file, at a cost that does not grow with it. */
|
|
46
|
+
export async function tailLines(
|
|
47
|
+
filePath: string,
|
|
48
|
+
count: number,
|
|
49
|
+
tailBytes: number = DEFAULT_TAIL_BYTES,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const file = Bun.file(filePath);
|
|
52
|
+
const size = file.size;
|
|
53
|
+
if (size <= 0) return "";
|
|
54
|
+
const start = Math.max(0, size - tailBytes);
|
|
55
|
+
const tail = await file.slice(start).text();
|
|
56
|
+
return lastLinesFromTail(tail, count, start > 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Bytes counted between two yields back to the event loop. */
|
|
60
|
+
export const YIELD_EVERY_BYTES = 2 * 1024 * 1024;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How many lines a file has, read as a stream of bytes.
|
|
64
|
+
*
|
|
65
|
+
* Counting `0x0A` over raw bytes rather than characters is not an
|
|
66
|
+
* approximation: UTF-8 gives every byte of a multi-byte sequence the high bit,
|
|
67
|
+
* so no byte of any non-ASCII character can collide with a newline. It also
|
|
68
|
+
* builds no string at all, so nothing the size of the file is held.
|
|
69
|
+
*
|
|
70
|
+
* This is **not faster** than the `readFileSync` walk it replaces — both are
|
|
71
|
+
* 53 ms on a 35 MB transcript. What changes is that the event loop gets the
|
|
72
|
+
* thread back during it, and that is the entire point.
|
|
73
|
+
*
|
|
74
|
+
* The explicit yield is the part that is easy to get wrong, and was. Awaiting
|
|
75
|
+
* the stream is *not* enough: its chunks resolve as **microtasks**, and a
|
|
76
|
+
* microtask queue drains without ever letting a timer or a socket run, so a
|
|
77
|
+
* `for await` over an already-buffered file blocks exactly as hard as the
|
|
78
|
+
* synchronous version. Measured with a 5 ms interval running alongside, over
|
|
79
|
+
* the same file:
|
|
80
|
+
*
|
|
81
|
+
* readFileSync + charCodeAt 53 ms wall, 0 timer fires
|
|
82
|
+
* stream, no explicit yield 37 ms wall, 0 timer fires
|
|
83
|
+
* stream + setTimeout yield 53 ms wall, 9 timer fires, worst gap 11.8 ms
|
|
84
|
+
*
|
|
85
|
+
* (control: an equivalent `Bun.sleep` allows 12.) Only a macrotask hands the
|
|
86
|
+
* thread back, hence the `setTimeout`.
|
|
87
|
+
*/
|
|
88
|
+
export async function countLines(filePath: string): Promise<number> {
|
|
89
|
+
let count = 0;
|
|
90
|
+
let lastByte = -1;
|
|
91
|
+
let sinceYield = 0;
|
|
92
|
+
// `getReader()` rather than `for await`: Bun's ReadableStream is async
|
|
93
|
+
// iterable at runtime, but the DOM typings it is declared against do not say
|
|
94
|
+
// so, and a cast to paper over that would outlive the reason for it.
|
|
95
|
+
const reader = Bun.file(filePath).stream().getReader();
|
|
96
|
+
try {
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done || !value) break;
|
|
100
|
+
for (let i = 0; i < value.length; i++) if (value[i] === 0x0a) count++;
|
|
101
|
+
if (value.length > 0) lastByte = value[value.length - 1]!;
|
|
102
|
+
sinceYield += value.length;
|
|
103
|
+
if (sinceYield >= YIELD_EVERY_BYTES) {
|
|
104
|
+
sinceYield = 0;
|
|
105
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
}
|
|
111
|
+
if (lastByte === -1) return 0; // empty file
|
|
112
|
+
if (lastByte !== 0x0a) count++; // last line carries no newline
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* there.
|
|
23
23
|
*/
|
|
24
24
|
import { lstatSync, readdirSync } from "node:fs";
|
|
25
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
25
26
|
import { join } from "node:path";
|
|
26
27
|
|
|
27
28
|
/** Slow enough to be invisible next to inotify, fast enough for a save-and-see loop. */
|
|
@@ -33,6 +34,15 @@ const POLL_INTERVAL_MS = 1000;
|
|
|
33
34
|
* blowup once.
|
|
34
35
|
*/
|
|
35
36
|
const MAX_POLLED_DIRS = 64;
|
|
37
|
+
/**
|
|
38
|
+
* Entries stat'ed per turn of the loop.
|
|
39
|
+
*
|
|
40
|
+
* A recreated `dist` or `node_modules` holds tens of thousands of files, and statting them in
|
|
41
|
+
* one go is the stall this whole release is about — once a second, forever, for as long as the
|
|
42
|
+
* directory stays polled. The reads are async so they run off the loop thread anyway; the batch
|
|
43
|
+
* is what stops their *completions* arriving as one unbroken run of callbacks.
|
|
44
|
+
*/
|
|
45
|
+
const STAT_BATCH = 256;
|
|
36
46
|
|
|
37
47
|
export interface RecreatedDirPollerOptions {
|
|
38
48
|
/** Absolute path of every entry that appeared, vanished or changed. */
|
|
@@ -50,6 +60,8 @@ export class RecreatedDirPoller {
|
|
|
50
60
|
private readonly intervalMs: number;
|
|
51
61
|
private readonly maxDirs: number;
|
|
52
62
|
private droppedForBudget = false;
|
|
63
|
+
/** True while a sweep is in flight, so the interval cannot stack them. */
|
|
64
|
+
private sweeping = false;
|
|
53
65
|
|
|
54
66
|
constructor(private readonly options: RecreatedDirPollerOptions) {
|
|
55
67
|
this.intervalMs = options.intervalMs ?? POLL_INTERVAL_MS;
|
|
@@ -109,24 +121,114 @@ export class RecreatedDirPoller {
|
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
private tick(): void {
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
// A sweep that outlasts the interval must not be joined by the next one: two sweeps over
|
|
125
|
+
// the same directories would double the syscalls and race each other's snapshot writes,
|
|
126
|
+
// and the slower the host the worse it would get — the opposite of what polling is for.
|
|
127
|
+
if (this.sweeping) return;
|
|
128
|
+
this.sweeping = true;
|
|
129
|
+
void this.sweep().finally(() => {
|
|
130
|
+
this.sweeping = false;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async sweep(): Promise<void> {
|
|
135
|
+
// Copied, because a directory can be added or removed while this awaits.
|
|
136
|
+
let first = true;
|
|
137
|
+
for (const dir of [...this.snapshots.keys()]) {
|
|
138
|
+
if (!first) await new Promise((resolve) => setImmediate(resolve));
|
|
139
|
+
first = false;
|
|
140
|
+
const previous = this.snapshots.get(dir);
|
|
141
|
+
if (!previous) continue; // removed while we were reading something else
|
|
142
|
+
const current = await this.readDirAsync(dir);
|
|
143
|
+
// Identity, not presence. A directory can be removed *and added back* while this reads,
|
|
144
|
+
// which is the ordinary rebuild path on Linux — `scheduleRebuild` calls `closeSubtree`
|
|
145
|
+
// (which removes) and then `cover` (which adds), and the new `add` installs a fresh
|
|
146
|
+
// baseline synchronously. Diffing against the baseline that has since been replaced
|
|
147
|
+
// reports changes the new one already accounts for.
|
|
148
|
+
//
|
|
149
|
+
// Reasoned rather than measured: the window is inside the await above, and no test here
|
|
150
|
+
// reaches it. The severe half of the same hazard — storing the stale read over the new
|
|
151
|
+
// baseline, which makes the *next* sweep report the whole directory — is the check below,
|
|
152
|
+
// and that one is covered.
|
|
153
|
+
if (this.snapshots.get(dir) !== previous) continue;
|
|
114
154
|
|
|
115
155
|
for (const [name, mtime] of current) {
|
|
116
156
|
const before = previous.get(name);
|
|
117
157
|
if (before === undefined || before !== mtime) {
|
|
118
|
-
this.
|
|
158
|
+
this.report(join(dir, name));
|
|
119
159
|
}
|
|
120
160
|
}
|
|
121
161
|
for (const name of previous.keys()) {
|
|
122
|
-
if (!current.has(name)) this.
|
|
162
|
+
if (!current.has(name)) this.report(join(dir, name));
|
|
123
163
|
}
|
|
124
164
|
|
|
125
|
-
|
|
165
|
+
// Checked again: `onChange` runs synchronously between the two, and this class is
|
|
166
|
+
// exported with a caller-supplied callback that may close or re-register the directory.
|
|
167
|
+
if (this.snapshots.get(dir) === previous) this.snapshots.set(dir, current);
|
|
126
168
|
}
|
|
127
169
|
}
|
|
128
170
|
|
|
129
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* One change, reported without letting a bad listener take the process down.
|
|
173
|
+
*
|
|
174
|
+
* The sweep runs from a `void`ed promise, so a throwing `onChange` becomes an unhandled
|
|
175
|
+
* rejection — and the server treats three of those in a minute as fatal, which a poller
|
|
176
|
+
* ticking once a second reaches in three. Swallowing is right here: the listener's failure
|
|
177
|
+
* is the listener's business, and the alternative is that one bad path stops the directory
|
|
178
|
+
* being watched at all.
|
|
179
|
+
*/
|
|
180
|
+
private report(absPath: string): void {
|
|
181
|
+
try {
|
|
182
|
+
this.options.onChange(absPath);
|
|
183
|
+
} catch {
|
|
184
|
+
// The caller's problem, not a reason to stop polling.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Direct entries of `absDir` with their mtimes, without holding the loop.
|
|
190
|
+
*
|
|
191
|
+
* Empty when unreadable — deleted again, or permissions.
|
|
192
|
+
*/
|
|
193
|
+
private async readDirAsync(absDir: string): Promise<DirSnapshot> {
|
|
194
|
+
const snapshot: DirSnapshot = new Map();
|
|
195
|
+
let names: string[];
|
|
196
|
+
try {
|
|
197
|
+
names = await readdir(absDir);
|
|
198
|
+
} catch {
|
|
199
|
+
return snapshot;
|
|
200
|
+
}
|
|
201
|
+
for (let i = 0; i < names.length; i += STAT_BATCH) {
|
|
202
|
+
// Measured, not assumed: on 10k entries, async reads alone took the worst loop gap from
|
|
203
|
+
// 341ms to 194ms, because the completions still arrived as one unbroken run. Handing the
|
|
204
|
+
// loop a turn between batches is what takes it to single digits.
|
|
205
|
+
if (i > 0) await new Promise((resolve) => setImmediate(resolve));
|
|
206
|
+
const batch = names.slice(i, i + STAT_BATCH);
|
|
207
|
+
const stats = await Promise.all(
|
|
208
|
+
batch.map(async (name) => {
|
|
209
|
+
try {
|
|
210
|
+
// lstat, not stat: a symlink's own mtime, never its target's, matching
|
|
211
|
+
// the scan that decides coverage.
|
|
212
|
+
return [name, (await lstat(join(absDir, name))).mtimeMs] as const;
|
|
213
|
+
} catch {
|
|
214
|
+
// Vanished between readdir and lstat; the next tick reports it.
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
for (const entry of stats) if (entry) snapshot.set(entry[0], entry[1]);
|
|
220
|
+
}
|
|
221
|
+
return snapshot;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The same read, synchronously, for the baseline taken at registration.
|
|
226
|
+
*
|
|
227
|
+
* It stays sync so the baseline is the directory as it was at the moment `add` was called:
|
|
228
|
+
* a `git checkout` writes into a recreated directory immediately, and those writes are
|
|
229
|
+
* exactly what the poller exists to report. Taking the baseline a turn later would fold them
|
|
230
|
+
* into it and report nothing. It is one read per directory, not one per second.
|
|
231
|
+
*/
|
|
130
232
|
private readDir(absDir: string): DirSnapshot {
|
|
131
233
|
const snapshot: DirSnapshot = new Map();
|
|
132
234
|
let names: string[];
|
|
@@ -43,6 +43,32 @@ import { RecreatedDirPoller } from "./recreated-dir-poller.ts";
|
|
|
43
43
|
/** Coalesce a burst of directory churn into a single subtree rebuild. */
|
|
44
44
|
const REBUILD_DEBOUNCE_MS = 500;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Directories walked between two hand-backs of the event loop.
|
|
48
|
+
*
|
|
49
|
+
* Covering a project used to be one synchronous burst: `readdirSync` down the
|
|
50
|
+
* whole tree, then `fs.watch` on every directory it kept. On a 12,000-directory
|
|
51
|
+
* project that held the loop for **2.95 seconds** at startup — measured, and
|
|
52
|
+
* billed to PPM by the lag monitor at ratio 1.38 across two independent
|
|
53
|
+
* restarts (the ratio is over 1 because Bun runs the `readdirSync` calls on its
|
|
54
|
+
* file system thread pool while the main thread walks). Nothing else in the
|
|
55
|
+
* process could run for any of it.
|
|
56
|
+
*
|
|
57
|
+
* Picked by measurement on a 13,321-directory tree, against a 5 ms timer whose
|
|
58
|
+
* fires are counted. The wall time is allowed to get worse — nothing waits on
|
|
59
|
+
* this walk any more — so the number to read is the worst single pause:
|
|
60
|
+
*
|
|
61
|
+
* sync (before) 2361 ms wall 0 fires gap = the whole 2361 ms
|
|
62
|
+
* every 250 dirs 2653 ms wall 63 fires worst gap 134 ms
|
|
63
|
+
* every 64 dirs 2812 ms wall 183 fires worst gap 41 ms
|
|
64
|
+
* every 32 dirs 4624 ms wall 503 fires worst gap 23 ms
|
|
65
|
+
*
|
|
66
|
+
* 64 is where the curve bends: halving it again buys 18 ms off the worst pause
|
|
67
|
+
* and costs 1.8 seconds of wall time. The residual ~41 ms is one batch, not
|
|
68
|
+
* scheduling overhead — it is the same with either yield primitive below.
|
|
69
|
+
*/
|
|
70
|
+
const YIELD_EVERY_DIRS = 64;
|
|
71
|
+
|
|
46
72
|
/**
|
|
47
73
|
* Whether `{ recursive: true }` is a real subtree watch rather than a per-directory walk.
|
|
48
74
|
* Named platforms rather than `!== "linux"`, so a runtime nobody has measured here —
|
|
@@ -104,6 +130,16 @@ export class WatchTree {
|
|
|
104
130
|
private covered = 0;
|
|
105
131
|
private truncated = false;
|
|
106
132
|
private closed = false;
|
|
133
|
+
private sinceYield = 0;
|
|
134
|
+
/**
|
|
135
|
+
* Covers run one at a time, as they did when they were synchronous.
|
|
136
|
+
*
|
|
137
|
+
* Each one reads `this.covered` to work out its remaining budget, so two
|
|
138
|
+
* interleaving would each believe the other's watchers were still unspent and
|
|
139
|
+
* together overshoot `maxDirs` — which on Linux is the inotify budget, the one
|
|
140
|
+
* number this class exists to respect.
|
|
141
|
+
*/
|
|
142
|
+
private coverChain: Promise<void> = Promise.resolve();
|
|
107
143
|
|
|
108
144
|
constructor(private readonly options: WatchTreeOptions) {
|
|
109
145
|
// Only Bun on Linux has the stale-watch defect; elsewhere re-watching works
|
|
@@ -116,8 +152,9 @@ export class WatchTree {
|
|
|
116
152
|
: null;
|
|
117
153
|
}
|
|
118
154
|
|
|
119
|
-
start(): void {
|
|
120
|
-
this.cover(this.options.root);
|
|
155
|
+
async start(): Promise<void> {
|
|
156
|
+
await this.cover(this.options.root);
|
|
157
|
+
await this.reconcile();
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
close(): void {
|
|
@@ -144,18 +181,54 @@ export class WatchTree {
|
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
/** Walk `absDir` and attach the fewest watchers that cover it without touching ignored dirs. */
|
|
147
|
-
private cover(absDir: string): void {
|
|
184
|
+
private cover(absDir: string): Promise<void> {
|
|
185
|
+
const run = this.coverChain.then(() => this.coverNow(absDir));
|
|
186
|
+
// The chain must survive a failure — one unreadable subtree cannot stop
|
|
187
|
+
// every later cover — but the caller still has to be able to see it, so the
|
|
188
|
+
// swallowing copy and the returned promise are deliberately not the same one.
|
|
189
|
+
this.coverChain = run.catch(() => {});
|
|
190
|
+
return run;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async coverNow(absDir: string): Promise<void> {
|
|
194
|
+
if (this.closed) return;
|
|
148
195
|
const budget = { left: this.options.maxDirs - this.covered };
|
|
149
196
|
if (budget.left <= 0) {
|
|
150
197
|
this.truncated = true;
|
|
151
198
|
return;
|
|
152
199
|
}
|
|
153
|
-
this.
|
|
200
|
+
this.sinceYield = 0;
|
|
201
|
+
await this.attach(await this.scan(absDir, budget));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hand the event loop back every `YIELD_EVERY_DIRS` directories, and say
|
|
206
|
+
* whether the walk should carry on.
|
|
207
|
+
*
|
|
208
|
+
* A **macrotask**, not `await Promise.resolve()`. A microtask queue drains
|
|
209
|
+
* without ever letting a timer or a socket run, so awaiting one would leave
|
|
210
|
+
* the loop exactly as blocked while looking asynchronous — the same trap
|
|
211
|
+
* measured and documented in `file-lines.ts`.
|
|
212
|
+
*
|
|
213
|
+
* `close()` can now land in one of these pauses, which is why the answer is
|
|
214
|
+
* a boolean rather than void: a walk that carries on past it would attach
|
|
215
|
+
* watchers to a tree nobody is going to close.
|
|
216
|
+
*
|
|
217
|
+
* `setImmediate` rather than `setTimeout(…, 0)`: both let timers run, and
|
|
218
|
+
* both leave the same 41 ms worst pause, but a timer costs a real round trip
|
|
219
|
+
* per yield — at 208 yields that measured 3.4 s of wall time against 2.8 s.
|
|
220
|
+
*/
|
|
221
|
+
private async yieldIfDue(): Promise<boolean> {
|
|
222
|
+
if (++this.sinceYield < YIELD_EVERY_DIRS) return !this.closed;
|
|
223
|
+
this.sinceYield = 0;
|
|
224
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
225
|
+
return !this.closed;
|
|
154
226
|
}
|
|
155
227
|
|
|
156
|
-
private scan(absDir: string, budget: { left: number }): ScanNode {
|
|
228
|
+
private async scan(absDir: string, budget: { left: number }): Promise<ScanNode> {
|
|
157
229
|
const node: ScanNode = { path: absDir, dirs: [], hasIgnored: false, size: 1 };
|
|
158
230
|
budget.left--;
|
|
231
|
+
if (!(await this.yieldIfDue())) return node;
|
|
159
232
|
|
|
160
233
|
let entries;
|
|
161
234
|
try {
|
|
@@ -189,7 +262,7 @@ export class WatchTree {
|
|
|
189
262
|
this.truncated = true;
|
|
190
263
|
break;
|
|
191
264
|
}
|
|
192
|
-
const child = this.scan(join(absDir, entry.name), budget);
|
|
265
|
+
const child = await this.scan(join(absDir, entry.name), budget);
|
|
193
266
|
node.dirs.push(child);
|
|
194
267
|
node.size += child.size;
|
|
195
268
|
if (child.hasIgnored) node.hasIgnored = true;
|
|
@@ -198,7 +271,50 @@ export class WatchTree {
|
|
|
198
271
|
return node;
|
|
199
272
|
}
|
|
200
273
|
|
|
201
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Second pass: attach whatever appeared *during* the first one.
|
|
276
|
+
*
|
|
277
|
+
* `scan` takes a snapshot and `attach` then walks it, and both hand the event loop back
|
|
278
|
+
* every `YIELD_EVERY_DIRS` directories — so on a large project seconds pass between "X was
|
|
279
|
+
* read" and "X has a watcher". A directory created inside X in that window is in neither the
|
|
280
|
+
* snapshot nor under a live watcher, and nothing later finds it: `syncChildDir` only fires
|
|
281
|
+
* from a parent watcher that did not exist yet. That subtree would stay unwatched for the
|
|
282
|
+
* whole session — a file change that never arrives, which is the failure the budget comments
|
|
283
|
+
* in this file exist to avoid. Before the walk yielded at all the window was too small to
|
|
284
|
+
* matter; yielding is what opened it.
|
|
285
|
+
*
|
|
286
|
+
* Only non-recursive watchers are re-read, because a recursive handle covers what appears
|
|
287
|
+
* under it by definition. That is one extra `readdir` per *directory* on Linux and one per
|
|
288
|
+
* *subtree* on Windows and macOS.
|
|
289
|
+
*/
|
|
290
|
+
private async reconcile(): Promise<void> {
|
|
291
|
+
if (this.closed) return;
|
|
292
|
+
this.sinceYield = 0;
|
|
293
|
+
for (const [dir, entry] of [...this.attached]) {
|
|
294
|
+
if (this.closed) return;
|
|
295
|
+
if (entry.recursive) continue;
|
|
296
|
+
let entries;
|
|
297
|
+
try {
|
|
298
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
299
|
+
} catch {
|
|
300
|
+
continue; // deleted since it was attached; its watcher will say so
|
|
301
|
+
}
|
|
302
|
+
for (const child of entries) {
|
|
303
|
+
// Same rules as the walk: symlinks are not directories here, which keeps link cycles
|
|
304
|
+
// and doubly-covered pnpm stores out of this pass too.
|
|
305
|
+
if (!child.isDirectory() || isIgnoredDirName(child.name)) continue;
|
|
306
|
+
const abs = join(dir, child.name);
|
|
307
|
+
if (this.attached.has(abs)) continue;
|
|
308
|
+
// The whole subtree under it, budget and truncation accounted for — the same call
|
|
309
|
+
// `syncChildDir` makes for a directory that appears under a live watcher.
|
|
310
|
+
await this.cover(abs);
|
|
311
|
+
}
|
|
312
|
+
if (!(await this.yieldIfDue())) return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async attach(node: ScanNode): Promise<void> {
|
|
317
|
+
if (this.closed) return;
|
|
202
318
|
const fitsWholeSubtree = this.covered + node.size <= this.options.maxDirs;
|
|
203
319
|
|
|
204
320
|
// A path we have watched before is being re-attached, so this directory was deleted
|
|
@@ -221,10 +337,15 @@ export class WatchTree {
|
|
|
221
337
|
return;
|
|
222
338
|
}
|
|
223
339
|
this.addWatcher(node.path, false, 1);
|
|
224
|
-
|
|
340
|
+
if (!(await this.yieldIfDue())) return;
|
|
341
|
+
for (const child of node.dirs) await this.attach(child);
|
|
225
342
|
}
|
|
226
343
|
|
|
227
344
|
private addWatcher(absDir: string, recursive: boolean, covers: number): boolean {
|
|
345
|
+
// The tightest guard against a walk that was still running when `close()`
|
|
346
|
+
// ran: a handle opened after it is one nothing will ever close, because
|
|
347
|
+
// `close()` has already emptied the map it would have been recorded in.
|
|
348
|
+
if (this.closed) return false;
|
|
228
349
|
if (this.attached.has(absDir)) return true;
|
|
229
350
|
try {
|
|
230
351
|
const watcher = watch(absDir, { recursive }, (event, filename) => {
|
|
@@ -307,7 +428,7 @@ export class WatchTree {
|
|
|
307
428
|
// A directory we already watch just got created again: the old handle is attached
|
|
308
429
|
// to the deleted inode, so replace the whole subtree rather than trusting it.
|
|
309
430
|
if (this.attached.has(abs)) this.scheduleRebuild(abs);
|
|
310
|
-
else this.cover(abs);
|
|
431
|
+
else void this.cover(abs);
|
|
311
432
|
}
|
|
312
433
|
|
|
313
434
|
private scheduleRebuild(absDir: string): void {
|
|
@@ -316,7 +437,7 @@ export class WatchTree {
|
|
|
316
437
|
this.rebuildTimers.delete(absDir);
|
|
317
438
|
if (this.closed) return;
|
|
318
439
|
this.closeSubtree(absDir);
|
|
319
|
-
this.cover(absDir);
|
|
440
|
+
void this.cover(absDir);
|
|
320
441
|
}, REBUILD_DEBOUNCE_MS));
|
|
321
442
|
}
|
|
322
443
|
|