@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
|
@@ -36,6 +36,12 @@ interface WatchEntry {
|
|
|
36
36
|
refCount: number;
|
|
37
37
|
timer?: ReturnType<typeof setTimeout>;
|
|
38
38
|
pending: Set<string>;
|
|
39
|
+
/** Resolves once the first walk has attached its watchers. Never rejects. */
|
|
40
|
+
ready: Promise<void>;
|
|
41
|
+
/** The cap this tree was given — what it counts as while it is still walking. */
|
|
42
|
+
maxDirs: number;
|
|
43
|
+
/** True until the first walk lands, successfully or not. */
|
|
44
|
+
walking: boolean;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const watchers = new Map<string, WatchEntry>();
|
|
@@ -47,9 +53,21 @@ export function onFileChange(cb: ChangeCallback): void {
|
|
|
47
53
|
changeCallbacks.push(cb);
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* How many directories the live trees account for, for sizing the next project's budget.
|
|
58
|
+
*
|
|
59
|
+
* A tree that is still walking counts as the whole cap it was given rather than as what it has
|
|
60
|
+
* reached so far. The walk hands the event loop back every few hundred directories, so its
|
|
61
|
+
* count climbs for seconds — and a second project opened during one used to size its budget
|
|
62
|
+
* against that unfinished number, which is how two projects starting together could each be
|
|
63
|
+
* told there was room and overshoot `MAX_DIRS_TOTAL` between them. Over-counting is the safe
|
|
64
|
+
* direction: the reservation is released the moment the walk lands and reports its real size.
|
|
65
|
+
*/
|
|
50
66
|
function totalCoveredDirs(): number {
|
|
51
67
|
let total = 0;
|
|
52
|
-
for (const entry of watchers.values())
|
|
68
|
+
for (const entry of watchers.values()) {
|
|
69
|
+
total += entry.walking ? entry.maxDirs : entry.tree.stats().dirs;
|
|
70
|
+
}
|
|
53
71
|
return total;
|
|
54
72
|
}
|
|
55
73
|
|
|
@@ -71,12 +89,25 @@ function queue(entry: WatchEntry, projectName: string, relPath: string): void {
|
|
|
71
89
|
}, DEBOUNCE_MS);
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Start watching a project directory (ref-counted — safe to call multiple times).
|
|
94
|
+
*
|
|
95
|
+
* The returned promise resolves once the tree's watchers are attached, which is
|
|
96
|
+
* no longer the moment this returns: covering yields the event loop every few
|
|
97
|
+
* hundred directories, so a change made in between is not reported. Callers that
|
|
98
|
+
* act on the filesystem immediately afterwards have to await it. It never
|
|
99
|
+
* rejects — a failed walk is logged and leaves the project unwatched.
|
|
100
|
+
*
|
|
101
|
+
* What those yields cost is *events*, not coverage: a directory created while the
|
|
102
|
+
* walk is running would otherwise end up in neither the snapshot nor under a live
|
|
103
|
+
* watcher and stay unwatched for the session, so `WatchTree.start()` makes a
|
|
104
|
+
* reconciliation pass before resolving. Both halves are why awaiting this matters.
|
|
105
|
+
*/
|
|
106
|
+
export function startWatching(projectName: string, projectPath: string): Promise<void> {
|
|
76
107
|
const existing = watchers.get(projectName);
|
|
77
108
|
if (existing) {
|
|
78
109
|
existing.refCount++;
|
|
79
|
-
return;
|
|
110
|
+
return existing.ready;
|
|
80
111
|
}
|
|
81
112
|
|
|
82
113
|
const maxDirs = Math.max(
|
|
@@ -95,28 +126,47 @@ export function startWatching(projectName: string, projectPath: string): void {
|
|
|
95
126
|
}),
|
|
96
127
|
refCount: 1,
|
|
97
128
|
pending: new Set(),
|
|
129
|
+
ready: Promise.resolve(),
|
|
130
|
+
maxDirs,
|
|
131
|
+
walking: true,
|
|
98
132
|
};
|
|
99
133
|
watchers.set(projectName, entry);
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
// The entry is already in `watchers`, so a second caller arriving while the
|
|
136
|
+
// walk is still running finds it and bumps the ref count rather than starting
|
|
137
|
+
// a second tree. Covering is asynchronous now — it hands the event loop back
|
|
138
|
+
// every few hundred directories instead of holding it for the whole walk — so
|
|
139
|
+
// the count and the truncation warning can only be reported once it lands.
|
|
140
|
+
entry.ready = entry.tree
|
|
141
|
+
.start()
|
|
142
|
+
.then(() => {
|
|
143
|
+
entry.walking = false;
|
|
144
|
+
// Stopped while the walk was still running: `stopWatching` closed this tree, so the
|
|
145
|
+
// stats are now zeroes and the line would read "Started watching: proj (0 dirs, 0
|
|
146
|
+
// handles)" for a project nothing is watching.
|
|
147
|
+
if (watchers.get(projectName) !== entry) return;
|
|
148
|
+
const { dirs, watchers: handles, truncated } = entry.tree.stats();
|
|
149
|
+
console.log(
|
|
150
|
+
`[file-watcher] Started watching: ${projectName} (${dirs} dirs, ${handles} handles${truncated ? ", capped" : ""})`,
|
|
151
|
+
);
|
|
152
|
+
if (truncated) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`[file-watcher] ${projectName} exceeded the ${maxDirs}-directory budget — ` +
|
|
155
|
+
`parts of the tree are not watched. Add build/cache directories to the ignore list.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.catch((e) => {
|
|
160
|
+
entry.walking = false;
|
|
161
|
+
entry.tree.close(); // release whatever attached before the failure
|
|
162
|
+
// By identity: a stop-then-start of the same project while this walk was in flight put
|
|
163
|
+
// a *new* entry in the map, and deleting unconditionally would drop that one — leaving
|
|
164
|
+
// its handles unclosed and letting the next `startWatching` build a second tree over
|
|
165
|
+
// the same directories.
|
|
166
|
+
if (watchers.get(projectName) === entry) watchers.delete(projectName);
|
|
167
|
+
console.warn(`[file-watcher] Failed to watch ${projectPath}: ${(e as Error).message}`);
|
|
168
|
+
});
|
|
169
|
+
return entry.ready;
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
/** Decrement ref count — stops watcher when no clients remain */
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing for a whole-branch diff: `git diff --numstat -z` for the line counts
|
|
3
|
+
* and `git diff --raw -z` for the blob ids and change kinds, merged per path.
|
|
4
|
+
*
|
|
5
|
+
* Two commands rather than one because git concatenates the two formats when
|
|
6
|
+
* both are asked for, and telling them apart afterwards is guesswork. They are
|
|
7
|
+
* both cheap and run in parallel.
|
|
8
|
+
*
|
|
9
|
+
* `-z` throughout: without it a path containing a tab, a quote or a newline is
|
|
10
|
+
* escaped and the record becomes ambiguous, and a rename is emitted as a single
|
|
11
|
+
* `old => new` field that cannot be split reliably.
|
|
12
|
+
*
|
|
13
|
+
* (`packages/ext-git-graph/src/compare-args.ts` has its own numstat parser. It
|
|
14
|
+
* stays separate on purpose: an extension is installed from npm into
|
|
15
|
+
* `~/.ppm/extensions`, so it cannot import from `src/`.)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { BranchDiffFile } from "../../types/git.ts";
|
|
19
|
+
|
|
20
|
+
type BranchDiffStatus = BranchDiffFile["status"];
|
|
21
|
+
|
|
22
|
+
interface RawEntry {
|
|
23
|
+
status: BranchDiffStatus;
|
|
24
|
+
blob: string;
|
|
25
|
+
oldPath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One NUL-terminated record at a time, dropping the empty trailing field. */
|
|
29
|
+
function records(stdout: string): string[] {
|
|
30
|
+
const parts = stdout.split("\0");
|
|
31
|
+
if (parts[parts.length - 1] === "") parts.pop();
|
|
32
|
+
return parts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `git diff --numstat -z` → additions/deletions per path.
|
|
37
|
+
*
|
|
38
|
+
* A rename is three records: the counts with an empty path field, then the old
|
|
39
|
+
* path, then the new one.
|
|
40
|
+
*/
|
|
41
|
+
export function parseNumstatZ(stdout: string): Map<string, { additions: number; deletions: number; binary: boolean; oldPath?: string }> {
|
|
42
|
+
const parts = records(stdout);
|
|
43
|
+
const out = new Map<string, { additions: number; deletions: number; binary: boolean; oldPath?: string }>();
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < parts.length; i++) {
|
|
46
|
+
const record = parts[i];
|
|
47
|
+
if (!record) continue;
|
|
48
|
+
const tab1 = record.indexOf("\t");
|
|
49
|
+
if (tab1 === -1) continue;
|
|
50
|
+
const tab2 = record.indexOf("\t", tab1 + 1);
|
|
51
|
+
if (tab2 === -1) continue;
|
|
52
|
+
|
|
53
|
+
const addedRaw = record.slice(0, tab1);
|
|
54
|
+
const deletedRaw = record.slice(tab1 + 1, tab2);
|
|
55
|
+
const pathPart = record.slice(tab2 + 1);
|
|
56
|
+
const binary = addedRaw === "-" && deletedRaw === "-";
|
|
57
|
+
const counts = {
|
|
58
|
+
additions: binary ? 0 : Number(addedRaw) || 0,
|
|
59
|
+
deletions: binary ? 0 : Number(deletedRaw) || 0,
|
|
60
|
+
binary,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (pathPart === "") {
|
|
64
|
+
const oldPath = parts[++i] ?? "";
|
|
65
|
+
const newPath = parts[++i] ?? "";
|
|
66
|
+
out.set(newPath, { ...counts, oldPath });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
out.set(pathPart, counts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `git diff --raw -z --no-abbrev` → status and head-side blob id per path.
|
|
77
|
+
*
|
|
78
|
+
* Record shape: `:<srcmode> <dstmode> <srcsha> <dstsha> <status>` then the path,
|
|
79
|
+
* or two paths when the status is R or C. The status carries a similarity score
|
|
80
|
+
* there (`R100`), which is dropped — the letter is the part anything acts on.
|
|
81
|
+
*
|
|
82
|
+
* `--no-abbrev` matters: the default abbreviates the ids to whatever length is
|
|
83
|
+
* currently unambiguous in the repository, so the same file yields a different
|
|
84
|
+
* string as the repository grows, and every stored review flag would clear.
|
|
85
|
+
*/
|
|
86
|
+
export function parseRawZ(stdout: string): Map<string, RawEntry> {
|
|
87
|
+
const parts = records(stdout);
|
|
88
|
+
const out = new Map<string, RawEntry>();
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < parts.length; i++) {
|
|
91
|
+
const record = parts[i];
|
|
92
|
+
if (!record || !record.startsWith(":")) continue;
|
|
93
|
+
const fields = record.slice(1).split(" ");
|
|
94
|
+
if (fields.length < 5) continue;
|
|
95
|
+
const blob = fields[3] ?? "";
|
|
96
|
+
const letter = (fields[4] ?? "M").charAt(0).toUpperCase();
|
|
97
|
+
const status = (["A", "M", "D", "R", "C", "T"].includes(letter) ? letter : "M") as BranchDiffStatus;
|
|
98
|
+
|
|
99
|
+
if (status === "R" || status === "C") {
|
|
100
|
+
const oldPath = parts[++i] ?? "";
|
|
101
|
+
const newPath = parts[++i] ?? "";
|
|
102
|
+
out.set(newPath, { status, blob, oldPath });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const path = parts[++i] ?? "";
|
|
106
|
+
if (path) out.set(path, { status, blob });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Join the two, keyed by head-side path.
|
|
114
|
+
*
|
|
115
|
+
* The raw output is the spine rather than numstat's: it is the one that reports
|
|
116
|
+
* a mode-only change (`T`), which numstat omits entirely, and a file missing
|
|
117
|
+
* from it has no blob id to remember a review against.
|
|
118
|
+
*/
|
|
119
|
+
export function mergeBranchDiff(
|
|
120
|
+
numstat: ReturnType<typeof parseNumstatZ>,
|
|
121
|
+
raw: ReturnType<typeof parseRawZ>,
|
|
122
|
+
): BranchDiffFile[] {
|
|
123
|
+
const files: BranchDiffFile[] = [];
|
|
124
|
+
for (const [path, entry] of raw) {
|
|
125
|
+
const counts = numstat.get(path);
|
|
126
|
+
files.push({
|
|
127
|
+
path,
|
|
128
|
+
...(entry.oldPath ? { oldPath: entry.oldPath } : {}),
|
|
129
|
+
status: entry.status,
|
|
130
|
+
additions: counts?.additions ?? 0,
|
|
131
|
+
deletions: counts?.deletions ?? 0,
|
|
132
|
+
binary: counts?.binary ?? false,
|
|
133
|
+
blob: entry.blob,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Refs reach git as argv, so a shell is never involved — but an argument
|
|
142
|
+
* starting with `-` is still read as an option, and `..` would turn one field
|
|
143
|
+
* into a range and defeat the caller's choice of comparison.
|
|
144
|
+
*/
|
|
145
|
+
export function assertRef(value: string | undefined, label: string): string {
|
|
146
|
+
const s = (value ?? "").trim();
|
|
147
|
+
if (!s || s.startsWith("-") || s.includes("..") || /[\x00-\x1f\x7f~^:?*[\]\\]/.test(s)) {
|
|
148
|
+
throw new Error(`Invalid git ref for ${label}: "${s}"`);
|
|
149
|
+
}
|
|
150
|
+
return s;
|
|
151
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole of a branch's work in one list: every file that differs between a
|
|
3
|
+
* base ref and a head ref, with the base the per-file diffs must also use.
|
|
4
|
+
*
|
|
5
|
+
* The point of returning `mergeBase` rather than just the file list is that the
|
|
6
|
+
* caller opens each file separately. In three-dot mode the list is computed
|
|
7
|
+
* against where the branches diverged, so a viewer handed `base` instead would
|
|
8
|
+
* show the base branch's own later commits as deletions inside a review of
|
|
9
|
+
* head — the file list and the file view disagreeing about what is being
|
|
10
|
+
* reviewed. Every diff here is run against one resolved commit so the two
|
|
11
|
+
* cannot drift.
|
|
12
|
+
*/
|
|
13
|
+
import simpleGit from "simple-git";
|
|
14
|
+
import type { BranchDiff } from "../../types/git.ts";
|
|
15
|
+
import { assertRef, mergeBranchDiff, parseNumstatZ, parseRawZ } from "./branch-diff-parse.ts";
|
|
16
|
+
|
|
17
|
+
export type BranchDiffMode = "three-dot" | "two-dot";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How many files one comparison may answer with.
|
|
21
|
+
*
|
|
22
|
+
* Both git outputs are buffered as strings and the whole list is returned in
|
|
23
|
+
* one JSON array, which the review tab then renders un-virtualized in a 288px
|
|
24
|
+
* sidebar. A branch that vendors a dependency or lands a formatter pass is
|
|
25
|
+
* 20,000 files — a multi-megabyte response and 20,000 DOM rows for a list
|
|
26
|
+
* nobody is going to read to the end. Reviewing a branch that large one file at
|
|
27
|
+
* a time is not the workflow this exists for, so the honest answer is to say
|
|
28
|
+
* how many were left out.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_FILES = 2000;
|
|
31
|
+
|
|
32
|
+
export async function branchDiff(
|
|
33
|
+
repoPath: string,
|
|
34
|
+
baseRef: string | undefined,
|
|
35
|
+
headRef: string | undefined,
|
|
36
|
+
mode: BranchDiffMode = "three-dot",
|
|
37
|
+
// A parameter only so a test can trip the cap without committing 2001 files.
|
|
38
|
+
maxFiles: number = MAX_FILES,
|
|
39
|
+
): Promise<BranchDiff> {
|
|
40
|
+
const base = assertRef(baseRef, "base");
|
|
41
|
+
const head = assertRef(headRef, "head");
|
|
42
|
+
const git = simpleGit(repoPath);
|
|
43
|
+
|
|
44
|
+
let mergeBase = base;
|
|
45
|
+
if (mode === "three-dot") {
|
|
46
|
+
// `git merge-base` exits non-zero on unrelated histories, where a three-dot
|
|
47
|
+
// diff has no meaning at all. Saying so beats silently answering with the
|
|
48
|
+
// two-dot list under a three-dot label.
|
|
49
|
+
try {
|
|
50
|
+
mergeBase = (await git.raw(["merge-base", base, head])).trim();
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(`"${base}" and "${head}" have no common ancestor.`);
|
|
53
|
+
}
|
|
54
|
+
if (!mergeBase) throw new Error(`"${base}" and "${head}" have no common ancestor.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [numstatOut, rawOut, headCommit] = await Promise.all([
|
|
58
|
+
git.raw(["diff", "--numstat", "-z", "-M", mergeBase, head]),
|
|
59
|
+
git.raw(["diff", "--raw", "-z", "-M", "--no-abbrev", mergeBase, head]),
|
|
60
|
+
// Resolved once, here, rather than by each caller opening a file: `head` is
|
|
61
|
+
// a ref name and a commit landing mid-review would move it, so the viewer
|
|
62
|
+
// would render a tip the row's counts and blob id do not describe. This
|
|
63
|
+
// file's own header promises one resolved commit; `mergeBase` was the only
|
|
64
|
+
// half of that promise being kept.
|
|
65
|
+
git.raw(["rev-parse", head]).then((s) => s.trim()),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const all = mergeBranchDiff(parseNumstatZ(numstatOut), parseRawZ(rawOut));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
base,
|
|
72
|
+
head,
|
|
73
|
+
mode,
|
|
74
|
+
mergeBase,
|
|
75
|
+
headCommit,
|
|
76
|
+
omitted: Math.max(0, all.length - maxFiles),
|
|
77
|
+
files: all.slice(0, maxFiles),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* Staging, unstaging and discarding at hunk / line granularity.
|
|
3
3
|
*
|
|
4
4
|
* Each operation is a patch fed to `git apply` on stdin. `simple-git` has no
|
|
@@ -51,7 +51,7 @@ interface GitResult {
|
|
|
51
51
|
*
|
|
52
52
|
* Chunks are collected as `Buffer` and decoded **once**. Decoding each chunk as
|
|
53
53
|
* it arrives (`stdout += c.toString()`) splits any multi-byte sequence that
|
|
54
|
-
* straddles a chunk boundary into two invalid halves, which become U+FFFD
|
|
54
|
+
* straddles a chunk boundary into two invalid halves, which become U+FFFD — and
|
|
55
55
|
* because a patch built from that text still *applies*, the corruption lands in
|
|
56
56
|
* the index with no error anywhere. Measured on a 2 MB all-Japanese diff: 43
|
|
57
57
|
* mangled lines.
|
|
@@ -59,7 +59,7 @@ interface GitResult {
|
|
|
59
59
|
* The decode is **latin1**, not utf8, so it is a lossless byte round-trip: the
|
|
60
60
|
* patch written back to `git apply` is the bytes git gave us, whatever the
|
|
61
61
|
* file's encoding actually is (a Latin-1 file could not be hunk-staged at all
|
|
62
|
-
* before). Everything the parser looks at
|
|
62
|
+
* before). Everything the parser looks at — `@@`, `+`, `-`, `\` — is ASCII and
|
|
63
63
|
* reads the same either way. Only text leaving for the browser is turned back
|
|
64
64
|
* into UTF-8, by `toDisplay` below.
|
|
65
65
|
*/
|
|
@@ -136,7 +136,7 @@ class GitHunksService {
|
|
|
136
136
|
* is describing a file rather than changing one: doing it to the user's index
|
|
137
137
|
* turns `?? file` into `A file` behind their back, on nothing more than
|
|
138
138
|
* opening a dialog, and the exit code went unread so a failure showed up as
|
|
139
|
-
* "no changes" instead. It goes into a throwaway index instead
|
|
139
|
+
* "no changes" instead. It goes into a throwaway index instead — git reads the
|
|
140
140
|
* same config and the same `.gitattributes`, normalises line endings the same
|
|
141
141
|
* way and produces the same bytes, while the real index is never opened for
|
|
142
142
|
* writing. `git apply --cached` needs no entry of its own to add a new file,
|
|
@@ -181,7 +181,7 @@ class GitHunksService {
|
|
|
181
181
|
// Belt and braces: a future flag or a repository config that reintroduces
|
|
182
182
|
// rename headers must fail loudly rather than reach `git apply --reverse`.
|
|
183
183
|
if (/^rename (from|to) /m.test(res.stdout)) {
|
|
184
|
-
throw new Error("This file was renamed
|
|
184
|
+
throw new Error("This file was renamed — stage or unstage it whole rather than by hunk.");
|
|
185
185
|
}
|
|
186
186
|
return res.stdout;
|
|
187
187
|
} finally {
|
|
@@ -213,20 +213,23 @@ class GitHunksService {
|
|
|
213
213
|
filePath: string,
|
|
214
214
|
scope: HunkScope,
|
|
215
215
|
requested: HunkRequest[],
|
|
216
|
-
apply: { cached: boolean; reverse: boolean },
|
|
216
|
+
apply: { cached: boolean; reverse: boolean; refuseMoved?: boolean },
|
|
217
217
|
): Promise<void> {
|
|
218
218
|
assertSafeFilePath(filePath);
|
|
219
219
|
if (requested.length === 0) throw new Error("No hunks were selected.");
|
|
220
220
|
|
|
221
221
|
const parsed = parseUnifiedDiff(await this.rawDiff(projectPath, filePath, scope));
|
|
222
|
-
if (parsed.binary) throw new Error("A binary file cannot be staged by hunk
|
|
222
|
+
if (parsed.binary) throw new Error("A binary file cannot be staged by hunk — stage the whole file.");
|
|
223
223
|
if (parsed.hunks.length === 0) throw new Error("This file has no changes to apply.");
|
|
224
224
|
|
|
225
225
|
// The client's indexes describe the diff it was *shown*; this diff was read
|
|
226
226
|
// again just now. Resolving by content is what makes the two the same list
|
|
227
|
-
//
|
|
227
|
+
// — or refuses. `git apply` cannot be the guard here, because the patch is
|
|
228
228
|
// built from this fresh parse and therefore always applies cleanly.
|
|
229
|
-
const selection = selectionFromRequest(
|
|
229
|
+
const selection = selectionFromRequest(
|
|
230
|
+
parsed,
|
|
231
|
+
resolveRequestedHunks(parsed, requested, { refuseMoved: apply.refuseMoved }),
|
|
232
|
+
);
|
|
230
233
|
const patch = buildPatch(parsed, selection, { reverse: apply.reverse });
|
|
231
234
|
if (!patch) throw new Error("The selection contains no actual change.");
|
|
232
235
|
|
|
@@ -255,10 +258,22 @@ class GitHunksService {
|
|
|
255
258
|
await this.applySelection(projectPath, filePath, "index", hunks, { cached: true, reverse: true });
|
|
256
259
|
}
|
|
257
260
|
|
|
258
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Throw away the selected worktree changes. Not recoverable.
|
|
263
|
+
*
|
|
264
|
+
* `refuseMoved` is what separates this from the other two: they can be undone
|
|
265
|
+
* by staging or unstaging again, so following a hunk that shifted position is
|
|
266
|
+
* a convenience. Here there is no reflog and no undo, so a hunk that is not
|
|
267
|
+
* where the client saw it is refused rather than guessed at.
|
|
268
|
+
*/
|
|
259
269
|
async discard(projectPath: string, filePath: string, hunks: HunkRequest[]): Promise<void> {
|
|
260
|
-
await this.applySelection(projectPath, filePath, "worktree", hunks, {
|
|
270
|
+
await this.applySelection(projectPath, filePath, "worktree", hunks, {
|
|
271
|
+
cached: false,
|
|
272
|
+
reverse: true,
|
|
273
|
+
refuseMoved: true,
|
|
274
|
+
});
|
|
261
275
|
}
|
|
262
276
|
}
|
|
263
277
|
|
|
264
278
|
export const gitHunksService = new GitHunksService();
|
|
279
|
+
|
|
@@ -334,6 +334,7 @@ export function hunkFingerprint(hunk: DiffHunk): string {
|
|
|
334
334
|
export function resolveRequestedHunks(
|
|
335
335
|
parsed: ParsedDiff,
|
|
336
336
|
requested: { hunk: number; id: string; lines?: number[] }[],
|
|
337
|
+
opts?: { refuseMoved?: boolean },
|
|
337
338
|
): { hunk: number; lines?: number[] }[] {
|
|
338
339
|
const fingerprints = parsed.hunks.map(hunkFingerprint);
|
|
339
340
|
const taken = new Set<number>();
|
|
@@ -352,7 +353,20 @@ export function resolveRequestedHunks(
|
|
|
352
353
|
}
|
|
353
354
|
// The same edit can appear twice in one file; prefer the position the
|
|
354
355
|
// client actually saw it at before falling back to the first free match.
|
|
355
|
-
const
|
|
356
|
+
const atClientIndex = candidates.includes(entry.hunk);
|
|
357
|
+
if (!atClientIndex && opts?.refuseMoved) {
|
|
358
|
+
// The fingerprint is content only, so two identical edits in one file are
|
|
359
|
+
// the same value here. That makes "the hunk you ticked moved down the
|
|
360
|
+
// list" and "the copy you ticked is gone, this is the other one"
|
|
361
|
+
// indistinguishable — both leave one free candidate at a different index.
|
|
362
|
+
// Following it is right when the change can be undone and wrong when it
|
|
363
|
+
// cannot, so the caller with no reflog behind it asks the user to look
|
|
364
|
+
// again instead.
|
|
365
|
+
throw new Error(
|
|
366
|
+
"This change moved in the file since it was listed, and the same edit can appear more than once. Reload and pick it again.",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const index = atClientIndex ? entry.hunk : candidates[0]!;
|
|
356
370
|
taken.add(index);
|
|
357
371
|
return entry.lines ? { hunk: index, lines: entry.lines } : { hunk: index };
|
|
358
372
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import simpleGit, { type SimpleGit } from "simple-git";
|
|
3
|
+
import { isBinaryContent } from "./binary-content.ts";
|
|
3
4
|
import type {
|
|
5
|
+
FileFullDiff,
|
|
4
6
|
GitStatus,
|
|
5
7
|
GitFileChange,
|
|
6
8
|
GitCommit,
|
|
@@ -9,6 +11,23 @@ import type {
|
|
|
9
11
|
GitWorktree,
|
|
10
12
|
} from "../types/git.ts";
|
|
11
13
|
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `filePath` resolved inside `projectPath`, or null when it escapes.
|
|
17
|
+
*
|
|
18
|
+
* Containment is asked of `relative`, not of a string prefix: a prefix compare
|
|
19
|
+
* built with a forward slash rejects every path on Windows, where `resolve`
|
|
20
|
+
* answers with backslashes — the same trap `assertSafeFilePaths` in the git
|
|
21
|
+
* graph extension documents.
|
|
22
|
+
*/
|
|
23
|
+
function insideProject(projectPath: string, filePath: string): string | null {
|
|
24
|
+
if (!filePath || path.isAbsolute(filePath) || filePath.includes("\0")) return null;
|
|
25
|
+
const resolved = path.resolve(projectPath, filePath);
|
|
26
|
+
const rel = path.relative(projectPath, resolved);
|
|
27
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
class GitService {
|
|
13
32
|
private git(projectPath: string): SimpleGit {
|
|
14
33
|
return simpleGit(projectPath);
|
|
@@ -125,48 +144,79 @@ class GitService {
|
|
|
125
144
|
return files;
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The file at a revision, as bytes — null when it does not exist there.
|
|
149
|
+
*
|
|
150
|
+
* `showBuffer` rather than `show`: the string form is stdout decoded as
|
|
151
|
+
* UTF-8, which destroys every byte of a binary file before anything can even
|
|
152
|
+
* tell that it is one.
|
|
153
|
+
*/
|
|
154
|
+
async fileBlob(projectPath: string, filePath: string, ref: string): Promise<Uint8Array | null> {
|
|
155
|
+
try {
|
|
156
|
+
return await this.git(projectPath).showBuffer([`${ref}:${filePath}`]);
|
|
157
|
+
} catch {
|
|
158
|
+
// Absent at that revision (added, renamed, or a ref that resolves to
|
|
159
|
+
// nothing), which is not an error — the other side still has content.
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
/**
|
|
129
165
|
* Returns full file contents for both sides of a diff (VSCode-style).
|
|
130
166
|
* - original: file at HEAD (empty if new/untracked/ref missing)
|
|
131
167
|
* - modified: working tree content (empty if deleted on disk)
|
|
132
168
|
* Monaco DiffEditor will compute/render the diff from these full contents.
|
|
169
|
+
*
|
|
170
|
+
* Both sides are read as bytes so a binary file can be *recognised* as one,
|
|
171
|
+
* and its content is then withheld unless `opts.text` asks for it — see
|
|
172
|
+
* `FileFullDiff`.
|
|
133
173
|
*/
|
|
134
174
|
async fileFullDiff(
|
|
135
175
|
projectPath: string,
|
|
136
176
|
filePath: string,
|
|
137
177
|
ref: string = "HEAD",
|
|
138
178
|
ref2?: string,
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
opts: { text?: boolean } = {},
|
|
180
|
+
): Promise<FileFullDiff> {
|
|
181
|
+
const original = await this.fileBlob(projectPath, filePath, ref);
|
|
141
182
|
|
|
142
|
-
let
|
|
143
|
-
try {
|
|
144
|
-
original = await git.show([`${ref}:${filePath}`]);
|
|
145
|
-
} catch {
|
|
146
|
-
// File does not exist at ref (new/untracked/added) → empty original
|
|
147
|
-
original = "";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let modified = "";
|
|
183
|
+
let modified: Uint8Array | null = null;
|
|
151
184
|
if (ref2) {
|
|
152
185
|
// Commit-to-commit diff: read modified from git object store
|
|
153
|
-
|
|
154
|
-
modified = await git.show([`${ref2}:${filePath}`]);
|
|
155
|
-
} catch {
|
|
156
|
-
modified = "";
|
|
157
|
-
}
|
|
186
|
+
modified = await this.fileBlob(projectPath, filePath, ref2);
|
|
158
187
|
} else {
|
|
159
|
-
// Working tree diff: read from disk
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
// Working tree diff: read from disk.
|
|
189
|
+
//
|
|
190
|
+
// The only side of this function that touches the filesystem directly,
|
|
191
|
+
// and therefore the only one needing a containment check: git refuses
|
|
192
|
+
// `HEAD:../secret.txt` itself, but `path.resolve` is happy to answer with
|
|
193
|
+
// anything the caller asks for and `filePath` arrives from a query
|
|
194
|
+
// string. `fileFullDiff(repo, "../secret.txt", "HEAD")` returned that
|
|
195
|
+
// file's contents. PPM is routinely reached through a public tunnel URL,
|
|
196
|
+
// so "behind auth" is not the whole story.
|
|
197
|
+
const onDisk = insideProject(projectPath, filePath);
|
|
198
|
+
if (onDisk) {
|
|
199
|
+
try {
|
|
200
|
+
const f = Bun.file(onDisk);
|
|
201
|
+
if (await f.exists()) modified = await f.bytes();
|
|
202
|
+
} catch {
|
|
203
|
+
modified = null;
|
|
204
|
+
}
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
207
|
|
|
169
|
-
|
|
208
|
+
const binary = isBinaryContent(original) || isBinaryContent(modified);
|
|
209
|
+
const withhold = binary && !opts.text;
|
|
210
|
+
const decode = (bytes: Uint8Array | null) =>
|
|
211
|
+
bytes && !withhold ? new TextDecoder().decode(bytes) : "";
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
original: decode(original),
|
|
215
|
+
modified: decode(modified),
|
|
216
|
+
binary,
|
|
217
|
+
originalSize: original?.length ?? null,
|
|
218
|
+
modifiedSize: modified?.length ?? null,
|
|
219
|
+
};
|
|
170
220
|
}
|
|
171
221
|
|
|
172
222
|
async fileDiff(
|