@bud.sh/app 0.1.0 → 0.3.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/dist/client/assets/{index-sj6mWw9-.js → index-GPPa1XPO.js} +2 -2
- package/dist/client/assets/workspace-CznMN7F6.js +2 -0
- package/dist/client/index.html +1 -1
- package/dist/hosted/hosted.js +233 -38
- package/dist/node/index.js +95 -0
- package/dist/node-host/assets/abap-LtYK6g-v.js +3 -0
- package/dist/node-host/assets/actionscript-3-CfsDl31I.js +3 -0
- package/dist/node-host/assets/ada-CvTdxb4T.js +3 -0
- package/dist/node-host/assets/ahk-HN_I_y_3.js +3 -0
- package/dist/node-host/assets/ahk2-bJ5Wm9-K.js +3 -0
- package/dist/node-host/assets/andromeeda-BASpqCeJ.js +4 -0
- package/dist/node-host/assets/angular-html-C4SmZqv0.js +33 -0
- package/dist/node-host/assets/angular-ts-X1i_WOxI.js +27 -0
- package/dist/node-host/assets/apache-BIl2qPOf.js +3 -0
- package/dist/node-host/assets/apex-CybrBQ0z.js +3 -0
- package/dist/node-host/assets/api_vite-DSBC5Nzy.js +5 -0
- package/dist/node-host/assets/apl-CDTyNgE2.js +17 -0
- package/dist/node-host/assets/applescript-CJ80B5XJ.js +3 -0
- package/dist/node-host/assets/ara-IxnQjO6j.js +3 -0
- package/dist/node-host/assets/asciidoc-xKuVlNk6.js +3 -0
- package/dist/node-host/assets/asm-wBDSgBbA.js +3 -0
- package/dist/node-host/assets/astro-BiZFCPSt.js +19 -0
- package/dist/node-host/assets/aurora-x-DugsLcii.js +4 -0
- package/dist/node-host/assets/awk-D6dA05Ye.js +3 -0
- package/dist/node-host/assets/ayu-dark-BT3qdi3o.js +4 -0
- package/dist/node-host/assets/ayu-light-BVZyHoh9.js +4 -0
- package/dist/node-host/assets/ayu-mirage-XFwUVZQZ.js +4 -0
- package/dist/node-host/assets/ballerina-DIdXD2KH.js +3 -0
- package/dist/node-host/assets/bat-Dx8nnxXE.js +3 -0
- package/dist/node-host/assets/beancount-BG2UQN2D.js +3 -0
- package/dist/node-host/assets/berry-CgA2ea0_.js +3 -0
- package/dist/node-host/assets/bibtex-BzgdqG3s.js +3 -0
- package/dist/node-host/assets/bicep-CZ338dSk.js +3 -0
- package/dist/node-host/assets/bird2-BMprQDNV.js +3 -0
- package/dist/node-host/assets/blade-cZjv4Qad.js +21 -0
- package/dist/node-host/assets/bsl-Bh8bzcoy.js +6 -0
- package/dist/node-host/assets/build-Dn-L17Oj.js +5372 -0
- package/dist/node-host/assets/c-BXsbGxlp.js +6 -0
- package/dist/node-host/assets/c3-u0qNrniV.js +3 -0
- package/dist/node-host/assets/cadence-B1ELWULD.js +3 -0
- package/dist/node-host/assets/cairo-BsQhEq3R.js +6 -0
- package/dist/node-host/assets/canvas-BZhzizzt.js +1066 -0
- package/dist/node-host/assets/catppuccin-frappe-D20KODDN.js +4 -0
- package/dist/node-host/assets/catppuccin-latte-eGLVHIUJ.js +4 -0
- package/dist/node-host/assets/catppuccin-macchiato-bQGch4PY.js +4 -0
- package/dist/node-host/assets/catppuccin-mocha-Bes8-0Mr.js +4 -0
- package/dist/node-host/assets/chapel-cEZNBFmv.js +6 -0
- package/dist/node-host/assets/clarity-BkOxTQMf.js +3 -0
- package/dist/node-host/assets/clojure-DDsQ0FaV.js +3 -0
- package/dist/node-host/assets/cloneFileDiffMetadata-C1BcNpxN.js +3075 -0
- package/dist/node-host/assets/cmake-DPsCB_V6.js +3 -0
- package/dist/node-host/assets/cobol-BpMD18Rr.js +11 -0
- package/dist/node-host/assets/codeowners-CxEKPWu0.js +3 -0
- package/dist/node-host/assets/codeql-BwdvwJnv.js +3 -0
- package/dist/node-host/assets/coffee-BX_u-2R1.js +6 -0
- package/dist/node-host/assets/comment-data-nUlI0szN.js +92501 -0
- package/dist/node-host/assets/common-lisp-DJtwvW4V.js +3 -0
- package/dist/node-host/assets/coq-Byd42hNw.js +3 -0
- package/dist/node-host/assets/cpp-DjJRL98I.js +22 -0
- package/dist/node-host/assets/crystal-Cz-dKioR.js +19 -0
- package/dist/node-host/assets/csharp-Cx443ZdC.js +3 -0
- package/dist/node-host/assets/css-CWNyyZRg.js +6 -0
- package/dist/node-host/assets/csv-DqIVFkK7.js +3 -0
- package/dist/node-host/assets/cue-B5KneM5p.js +3 -0
- package/dist/node-host/assets/cypher-D8Bb8Cq2.js +3 -0
- package/dist/node-host/assets/d-BQY8TDgQ.js +3 -0
- package/dist/node-host/assets/dark-plus-BGuiqa1B.js +4 -0
- package/dist/node-host/assets/dart-BZ3YsgDB.js +3 -0
- package/dist/node-host/assets/dax-S-Noo-ku.js +3 -0
- package/dist/node-host/assets/desktop-BgOb7Aj4.js +3 -0
- package/dist/node-host/assets/diff-Blb4MjsC.js +3 -0
- package/dist/node-host/assets/dist-CVXhIZqO.js +6659 -0
- package/dist/node-host/assets/docker-PC1tuYpa.js +3 -0
- package/dist/node-host/assets/dotenv-BNWDqzYB.js +3 -0
- package/dist/node-host/assets/dracula-0CVA2XY5.js +4 -0
- package/dist/node-host/assets/dracula-soft-Dk9bpbVU.js +4 -0
- package/dist/node-host/assets/dream-maker-CngiPd8U.js +3 -0
- package/dist/node-host/assets/edge-D1_nghia.js +13 -0
- package/dist/node-host/assets/edit-DjpTbnEc.js +9788 -0
- package/dist/node-host/assets/elixir-x6BMZ6zW.js +6 -0
- package/dist/node-host/assets/elm-CiAjb3qf.js +6 -0
- package/dist/node-host/assets/emacs-lisp-DPCJASlD.js +3 -0
- package/dist/node-host/assets/erb-DzXTnEFa.js +11 -0
- package/dist/node-host/assets/erlang-DALBVNgL.js +6 -0
- package/dist/node-host/assets/everforest-dark-CRjTbhh5.js +4 -0
- package/dist/node-host/assets/everforest-light-CEeYFLn8.js +4 -0
- package/dist/node-host/assets/fennel-B3wtZRne.js +3 -0
- package/dist/node-host/assets/files-CTzcWGjb.js +3771 -0
- package/dist/node-host/assets/fish-D7qAeWQz.js +3 -0
- package/dist/node-host/assets/fluent-BjfpB34g.js +3 -0
- package/dist/node-host/assets/fortran-fixed-form-BbzsUHx4.js +6 -0
- package/dist/node-host/assets/fortran-free-form-yxJ7igsv.js +3 -0
- package/dist/node-host/assets/fsharp-D2o1Vyx0.js +6 -0
- package/dist/node-host/assets/gdresource-aTcOcjSd.js +11 -0
- package/dist/node-host/assets/gdscript-CSSSWtkL.js +3 -0
- package/dist/node-host/assets/gdshader-D7JfdFV3.js +3 -0
- package/dist/node-host/assets/genie-DnK2KWIP.js +3 -0
- package/dist/node-host/assets/gherkin-D6Lr12YQ.js +3 -0
- package/dist/node-host/assets/git-commit-BU1ZeqU-.js +6 -0
- package/dist/node-host/assets/git-rebase-BBwLmuqc.js +6 -0
- package/dist/node-host/assets/github-dark-BXgKmOw_.js +4 -0
- package/dist/node-host/assets/github-dark-default-DzJQXksd.js +4 -0
- package/dist/node-host/assets/github-dark-dimmed-B1pEmGtS.js +4 -0
- package/dist/node-host/assets/github-dark-high-contrast-BkHIa6jc.js +4 -0
- package/dist/node-host/assets/github-light-BKRlUbaB.js +4 -0
- package/dist/node-host/assets/github-light-default-BU3eYkzs.js +4 -0
- package/dist/node-host/assets/github-light-high-contrast-B3MumDcI.js +4 -0
- package/dist/node-host/assets/gleam-CTllUdfC.js +3 -0
- package/dist/node-host/assets/glimmer-js-Cr4KUylv.js +15 -0
- package/dist/node-host/assets/glimmer-ts-W_3bhuUU.js +15 -0
- package/dist/node-host/assets/glsl-CV8OarLe.js +8 -0
- package/dist/node-host/assets/gn-CpIn4Dcw.js +3 -0
- package/dist/node-host/assets/gnuplot-DgWvVXI8.js +3 -0
- package/dist/node-host/assets/go-aXtTCanQ.js +3 -0
- package/dist/node-host/assets/graphql-BzQEBhBd.js +17 -0
- package/dist/node-host/assets/groovy-Bw2Ku4bU.js +3 -0
- package/dist/node-host/assets/gruvbox-dark-hard-B0ecF443.js +4 -0
- package/dist/node-host/assets/gruvbox-dark-medium-BQnpC39k.js +4 -0
- package/dist/node-host/assets/gruvbox-dark-soft-DFZUgESr.js +4 -0
- package/dist/node-host/assets/gruvbox-light-hard-CuB_LOJr.js +4 -0
- package/dist/node-host/assets/gruvbox-light-medium-idDHgxjs.js +4 -0
- package/dist/node-host/assets/gruvbox-light-soft-CfWH49MY.js +4 -0
- package/dist/node-host/assets/hack-DhYZEMlu.js +11 -0
- package/dist/node-host/assets/haml-CRid5kR3.js +13 -0
- package/dist/node-host/assets/handlebars-Sm6kCGmL.js +15 -0
- package/dist/node-host/assets/haskell-CfGsE1Cf.js +3 -0
- package/dist/node-host/assets/haxe-QaqXWmIB.js +3 -0
- package/dist/node-host/assets/hcl-DHg_Osgo.js +3 -0
- package/dist/node-host/assets/hjson-Cuaphql8.js +3 -0
- package/dist/node-host/assets/hlsl-BJLUNfne.js +3 -0
- package/dist/node-host/assets/horizon-bright-Ba6M8QSV.js +4 -0
- package/dist/node-host/assets/horizon-h92C5OlZ.js +4 -0
- package/dist/node-host/assets/houston-EQnPegWq.js +4 -0
- package/dist/node-host/assets/html-derivative-Cm8plTfc.js +6 -0
- package/dist/node-host/assets/html-j1vihRKA.js +13 -0
- package/dist/node-host/assets/http-BZNc4yhO.js +15 -0
- package/dist/node-host/assets/hurl-O8ckTHPH.js +13 -0
- package/dist/node-host/assets/hxml-9hvSHCPQ.js +6 -0
- package/dist/node-host/assets/hy-CJKxDzbh.js +3 -0
- package/dist/node-host/assets/imba-96qKoJCa.js +3 -0
- package/dist/node-host/assets/ini-C24lZfdM.js +3 -0
- package/dist/node-host/assets/java-CFxSaKyu.js +6 -0
- package/dist/node-host/assets/javascript-D44WgUBn.js +6 -0
- package/dist/node-host/assets/jinja-BkVH86If.js +10 -0
- package/dist/node-host/assets/jison-DOInKsmT.js +6 -0
- package/dist/node-host/assets/jiti-CjBwKumX.js +5461 -0
- package/dist/node-host/assets/json-CnGl9idC.js +6 -0
- package/dist/node-host/assets/json5-xULSgPQI.js +3 -0
- package/dist/node-host/assets/jsonc-Cu6rqcrJ.js +3 -0
- package/dist/node-host/assets/jsonl-Bp0qqr10.js +3 -0
- package/dist/node-host/assets/jsonnet-BafgE6cE.js +3 -0
- package/dist/node-host/assets/jssm-Dx5A5sBR.js +3 -0
- package/dist/node-host/assets/jsx-Dv6553c2.js +6 -0
- package/dist/node-host/assets/julia-CnoY-eZu.js +17 -0
- package/dist/node-host/assets/just-rCgUmVkD.js +19 -0
- package/dist/node-host/assets/kanagawa-dragon-DgDvwi3B.js +4 -0
- package/dist/node-host/assets/kanagawa-lotus-BgoQro20.js +4 -0
- package/dist/node-host/assets/kanagawa-wave-DgXEowHs.js +4 -0
- package/dist/node-host/assets/kdl-DkTyxPcj.js +3 -0
- package/dist/node-host/assets/kotlin-BHE8CoV9.js +3 -0
- package/dist/node-host/assets/kusto-6FdaWTzR.js +3 -0
- package/dist/node-host/assets/laserwave-IxBH_3yW.js +4 -0
- package/dist/node-host/assets/latex-DTvJHHg_.js +6 -0
- package/dist/node-host/assets/lean-BwOKScIt.js +3 -0
- package/dist/node-host/assets/less-BzbdhFUi.js +3 -0
- package/dist/node-host/assets/lib-BzDu3tbz.js +17484 -0
- package/dist/node-host/assets/light-plus-S8JBx6is.js +4 -0
- package/dist/node-host/assets/liquid-praBF2DO.js +15 -0
- package/dist/node-host/assets/llvm-ZNG_yAr9.js +3 -0
- package/dist/node-host/assets/log-Dad54gsi.js +3 -0
- package/dist/node-host/assets/logo-DTXV8SKw.js +3 -0
- package/dist/node-host/assets/lru-C0_oUet4.js +134 -0
- package/dist/node-host/assets/lua-CiYoSQn2.js +8 -0
- package/dist/node-host/assets/luau-DlImmvOM.js +3 -0
- package/dist/node-host/assets/main-D0ab5M4V.js +2353 -0
- package/dist/node-host/assets/make-B2ukzWs3.js +3 -0
- package/dist/node-host/assets/markdown-CkmxmzqY.js +3 -0
- package/dist/node-host/assets/marko-Din1W3ot.js +15 -0
- package/dist/node-host/assets/material-theme-D1f3HBq-.js +4 -0
- package/dist/node-host/assets/material-theme-darker-ExzYhT0j.js +4 -0
- package/dist/node-host/assets/material-theme-lighter-D6gH9nnA.js +4 -0
- package/dist/node-host/assets/material-theme-ocean-D1w5BPAa.js +4 -0
- package/dist/node-host/assets/material-theme-palenight-CkqWmVpF.js +4 -0
- package/dist/node-host/assets/matlab-CfRK6IMC.js +3 -0
- package/dist/node-host/assets/mdc-DrUS0kOP.js +13 -0
- package/dist/node-host/assets/mdx-BK9ZufRc.js +3 -0
- package/dist/node-host/assets/mermaid-ItFqCK9s.js +3 -0
- package/dist/node-host/assets/min-dark-npsJH98j.js +4 -0
- package/dist/node-host/assets/min-light-DUk1XsVt.js +4 -0
- package/dist/node-host/assets/mipsasm-EL46Y4Xg.js +3 -0
- package/dist/node-host/assets/mojo-CsUSYzsu.js +3 -0
- package/dist/node-host/assets/monokai-CfFRWUNl.js +4 -0
- package/dist/node-host/assets/moonbit-BuY4M1CG.js +3 -0
- package/dist/node-host/assets/move-B-o8dn0N.js +3 -0
- package/dist/node-host/assets/narrat-D9rrR0H5.js +3 -0
- package/dist/node-host/assets/nextflow-ByPelpfh.js +6 -0
- package/dist/node-host/assets/nextflow-groovy-kiH-pxwE.js +3 -0
- package/dist/node-host/assets/nginx-CqxQjy7t.js +6 -0
- package/dist/node-host/assets/night-owl-BS7V-av_.js +4 -0
- package/dist/node-host/assets/night-owl-light-CZtRawYE.js +4 -0
- package/dist/node-host/assets/nim-BJfyW_Eo.js +21 -0
- package/dist/node-host/assets/nix-NHxlegoK.js +7 -0
- package/dist/node-host/assets/node-CaLaDAar.js +783 -0
- package/dist/node-host/assets/nord-BFh54Nqz.js +4 -0
- package/dist/node-host/assets/nsis-C3tUpeAt.js +3 -0
- package/dist/node-host/assets/nushell-Dx3871HW.js +3 -0
- package/dist/node-host/assets/objective-c-6dJF64Dt.js +3 -0
- package/dist/node-host/assets/objective-cpp-_ooRQ-PY.js +3 -0
- package/dist/node-host/assets/ocaml-vm1Y8An6.js +3 -0
- package/dist/node-host/assets/odin-vjOJSyfV.js +3 -0
- package/dist/node-host/assets/one-dark-pro-zDovGosB.js +4 -0
- package/dist/node-host/assets/one-light-gmHK3iK6.js +4 -0
- package/dist/node-host/assets/openscad-jn-f1Ocl.js +3 -0
- package/dist/node-host/assets/org-kbyZnkNe.js +101 -0
- package/dist/node-host/assets/pages-0aWH7z1j.js +578 -0
- package/dist/node-host/assets/pascal-D8-gNZ2I.js +3 -0
- package/dist/node-host/assets/perl-Ck21ZEIA.js +17 -0
- package/dist/node-host/assets/php-BI2xxNig.js +19 -0
- package/dist/node-host/assets/pierre-dark-DDWDl8g6.js +4 -0
- package/dist/node-host/assets/pierre-dark-protanopia-deuteranopia-C35UMuvO.js +4 -0
- package/dist/node-host/assets/pierre-dark-soft-Bb-4-biO.js +4 -0
- package/dist/node-host/assets/pierre-dark-tritanopia-CIJISGWF.js +4 -0
- package/dist/node-host/assets/pierre-dark-vibrant-8oZzB3gw.js +4 -0
- package/dist/node-host/assets/pierre-light-CWhW4NrH.js +4 -0
- package/dist/node-host/assets/pierre-light-protanopia-deuteranopia-u0U7N4Hn.js +4 -0
- package/dist/node-host/assets/pierre-light-soft-BCU6fuhO.js +4 -0
- package/dist/node-host/assets/pierre-light-tritanopia-3c8Yx3co.js +4 -0
- package/dist/node-host/assets/pierre-light-vibrant-Cwh332o_.js +4 -0
- package/dist/node-host/assets/pkl-C7lWzykI.js +3 -0
- package/dist/node-host/assets/plastic-DtdaCjub.js +4 -0
- package/dist/node-host/assets/plsql-DCNXypXO.js +3 -0
- package/dist/node-host/assets/po-IjpR1yHO.js +3 -0
- package/dist/node-host/assets/poimandres-Hdda6aHK.js +4 -0
- package/dist/node-host/assets/polar-4pN1JrJ8.js +3 -0
- package/dist/node-host/assets/postcss-0FnF2XR3.js +5833 -0
- package/dist/node-host/assets/postcss-D1gKt4z_.js +3 -0
- package/dist/node-host/assets/postcss-import-D5DErX4i.js +445 -0
- package/dist/node-host/assets/powerquery-SrvS_NBM.js +3 -0
- package/dist/node-host/assets/powershell-DvPkO2jl.js +3 -0
- package/dist/node-host/assets/prisma-4G_BaF_i.js +3 -0
- package/dist/node-host/assets/project-branches-1zs_0J_y.js +600 -0
- package/dist/node-host/assets/project-thread-links-LpUAGcDr.js +241 -0
- package/dist/node-host/assets/prolog-Bacii3Ue.js +3 -0
- package/dist/node-host/assets/prompt-CH6TK0bC-ZEO69UbU.js +885 -0
- package/dist/node-host/assets/proto-ClkOcAW0.js +3 -0
- package/dist/node-host/assets/pug-Du-dOfS3.js +13 -0
- package/dist/node-host/assets/puppet-CQlPp728.js +3 -0
- package/dist/node-host/assets/purescript-CLpaGMbw.js +3 -0
- package/dist/node-host/assets/python-BjnOIxPe.js +3 -0
- package/dist/node-host/assets/qml-BcFatdsQ.js +6 -0
- package/dist/node-host/assets/qmldir-Dz8ttxjf.js +3 -0
- package/dist/node-host/assets/qss-Iltvkn1C.js +3 -0
- package/dist/node-host/assets/r-DvDYa_8Q.js +6 -0
- package/dist/node-host/assets/racket-DRv7j6nv.js +3 -0
- package/dist/node-host/assets/raku-DI9yGpeW.js +3 -0
- package/dist/node-host/assets/razor-vGjCiVYw.js +11 -0
- package/dist/node-host/assets/rbs-D_4FkXDS.js +3 -0
- package/dist/node-host/assets/red-VqZFRByZ.js +4 -0
- package/dist/node-host/assets/reg-DpJf71oO.js +3 -0
- package/dist/node-host/assets/regexp-CE_3vZDh.js +6 -0
- package/dist/node-host/assets/rel-BT0iIKKw.js +3 -0
- package/dist/node-host/assets/riscv-CvzikGLb.js +3 -0
- package/dist/node-host/assets/rolldown-runtime-DktXgydw.js +35 -0
- package/dist/node-host/assets/ron-DzAo6FTJ.js +3 -0
- package/dist/node-host/assets/rose-pine-DiTkEueR.js +4 -0
- package/dist/node-host/assets/rose-pine-dawn-tfo5y2X0.js +4 -0
- package/dist/node-host/assets/rose-pine-moon-C0gON9XN.js +4 -0
- package/dist/node-host/assets/rosmsg-CyYq6XdT.js +3 -0
- package/dist/node-host/assets/rst-BqrTc4by.js +23 -0
- package/dist/node-host/assets/ruby-DLXfYriJ.js +31 -0
- package/dist/node-host/assets/rust-CaJo2YYS.js +3 -0
- package/dist/node-host/assets/sas-DJAJgMrq.js +6 -0
- package/dist/node-host/assets/sass-DI2zJwx7.js +3 -0
- package/dist/node-host/assets/scala-pMGZAs9j.js +3 -0
- package/dist/node-host/assets/scheme-D3A2QBtM.js +3 -0
- package/dist/node-host/assets/screen-DQoTOWYR.js +494 -0
- package/dist/node-host/assets/scss-n4nz8dJI.js +8 -0
- package/dist/node-host/assets/sdbl-CdQg5gu1.js +3 -0
- package/dist/node-host/assets/shaderlab-5KHT7nQ3.js +6 -0
- package/dist/node-host/assets/shellscript-BfuVOb-a.js +6 -0
- package/dist/node-host/assets/shellsession-hT9eQUDu.js +6 -0
- package/dist/node-host/assets/slack-dark-v8P_eXr6.js +4 -0
- package/dist/node-host/assets/slack-ochin-D8SyEdKz.js +4 -0
- package/dist/node-host/assets/smalltalk-D4IpSogw.js +3 -0
- package/dist/node-host/assets/smithy-BRyN83Yn.js +3 -0
- package/dist/node-host/assets/snazzy-light-BtkQkvXb.js +4 -0
- package/dist/node-host/assets/solarized-dark-B8nAdMfM.js +4 -0
- package/dist/node-host/assets/solarized-light-B-3F1INj.js +4 -0
- package/dist/node-host/assets/solidity-DPVu0wkJ.js +3 -0
- package/dist/node-host/assets/soy-Cbnz3v4z.js +6 -0
- package/dist/node-host/assets/sparql-cTZV4yOr.js +6 -0
- package/dist/node-host/assets/splunk-xP_ZjUH2.js +3 -0
- package/dist/node-host/assets/sql-DEvbt_rU.js +6 -0
- package/dist/node-host/assets/ssh-config-DXFPABYp.js +3 -0
- package/dist/node-host/assets/stata-0wnAnxv2.js +6 -0
- package/dist/node-host/assets/stylus-ZZDn1yUO.js +3 -0
- package/dist/node-host/assets/surrealql-70--UPGf.js +6 -0
- package/dist/node-host/assets/svelte-COI8WXVC.js +15 -0
- package/dist/node-host/assets/swift-DWmMn3lF.js +3 -0
- package/dist/node-host/assets/synthwave-84-DcsXCF_c.js +4 -0
- package/dist/node-host/assets/system-verilog-DW0KSMVO.js +3 -0
- package/dist/node-host/assets/systemd-DFbUltlW.js +3 -0
- package/dist/node-host/assets/talonscript-CLxaFu-b.js +3 -0
- package/dist/node-host/assets/tasks-C0WqnM7Q.js +644 -0
- package/dist/node-host/assets/tasks-request-Bwky5ORf.js +114 -0
- package/dist/node-host/assets/tasl-_AAbKiWM.js +3 -0
- package/dist/node-host/assets/tcl-DLe0Tsfm.js +3 -0
- package/dist/node-host/assets/templ-Bd0jzThC.js +13 -0
- package/dist/node-host/assets/terraform-v0KN9QQh.js +3 -0
- package/dist/node-host/assets/tex-C1Wtngrr.js +8 -0
- package/dist/node-host/assets/tokyo-night-Bvpqduda.js +4 -0
- package/dist/node-host/assets/toml-B7BHZIqX.js +3 -0
- package/dist/node-host/assets/ts-tags-BSGaPI18.js +59 -0
- package/dist/node-host/assets/tsv-BorGVq2Z.js +3 -0
- package/dist/node-host/assets/tsx-BxnCmyTp.js +6 -0
- package/dist/node-host/assets/turtle-BEG5g_eT.js +3 -0
- package/dist/node-host/assets/twig-BJG2k0MA.js +19 -0
- package/dist/node-host/assets/typescript-Cr5OHcug.js +6 -0
- package/dist/node-host/assets/typespec--jp0a8Qj.js +3 -0
- package/dist/node-host/assets/typst-BAvHLpwp.js +121 -0
- package/dist/node-host/assets/v-DBTjbKF9.js +3 -0
- package/dist/node-host/assets/vala-BnnASFsX.js +3 -0
- package/dist/node-host/assets/vb-eGVhCUXq.js +3 -0
- package/dist/node-host/assets/verilog-Ma7yBE30.js +3 -0
- package/dist/node-host/assets/vesper-CfxmMOy3.js +4 -0
- package/dist/node-host/assets/vhdl-Co2XFKY7.js +3 -0
- package/dist/node-host/assets/viml-BReTXeDA.js +3 -0
- package/dist/node-host/assets/vitesse-black-D9-_1mjV.js +4 -0
- package/dist/node-host/assets/vitesse-dark-DlSMLlr9.js +4 -0
- package/dist/node-host/assets/vitesse-light-KRWWWztf.js +4 -0
- package/dist/node-host/assets/vue-C3fWU2fi.js +31 -0
- package/dist/node-host/assets/vue-html-DxOM9wex.js +6 -0
- package/dist/node-host/assets/vue-vine-OKzSGBlX.js +19 -0
- package/dist/node-host/assets/vyper-C6KGx0YU.js +3 -0
- package/dist/node-host/assets/wasm-CvHAs-FG.js +7 -0
- package/dist/node-host/assets/wasm-gNIKwGlD.js +3 -0
- package/dist/node-host/assets/wenyan-BeysbCrW.js +3 -0
- package/dist/node-host/assets/wgsl-1s5rV5xS.js +3 -0
- package/dist/node-host/assets/wikitext-wjhjmmzD.js +3 -0
- package/dist/node-host/assets/wit-CeQiCbz_.js +3 -0
- package/dist/node-host/assets/wolfram-CcZPfnLd.js +3 -0
- package/dist/node-host/assets/worker-factory-C80HGMWf.js +14 -0
- package/dist/node-host/assets/xml-BsBjJrlv.js +8 -0
- package/dist/node-host/assets/xsl-BYwWzvT_.js +6 -0
- package/dist/node-host/assets/yaml-BRgACZP0.js +6 -0
- package/dist/node-host/assets/zenscript-BoijL7rl.js +3 -0
- package/dist/node-host/assets/zig-lTQvxhLD.js +3 -0
- package/dist/node-host/node-host.js +41511 -0
- package/dist/server/assets/{branch-previews-C_TVt_HQ.js → branch-previews-BryeA_kA.js} +112 -3
- package/dist/server/assets/{listener-BVbFsnmX.js → listener-oKd5ETMj.js} +3 -3
- package/dist/server/assets/{server-DUBjmaBw.js → server-DN5uwE4k.js} +1 -1
- package/dist/server/assets/{services-Og5rcF50.js → services-Ber8opFE.js} +2 -2
- package/dist/server/capture.js +1 -1
- package/dist/server/migration.js +3 -3
- package/dist/server/runtime.js +4 -4
- package/dist/server/server.js +3 -3
- package/dist/types/assets/{config-BzzRLcL-.d.ts → config-Cuauq8xY.d.ts} +24 -1
- package/dist/types/assets/contract-BsCQbbxD.d.ts +111 -0
- package/dist/types/companion.d.ts +1 -1
- package/dist/types/hosted.d.ts +3 -99
- package/dist/types/index.d.ts +1 -1
- package/dist/types/node-host.d.ts +46 -0
- package/dist/types/screen.d.ts +1 -1
- package/package.json +6 -2
- package/dist/client/assets/workspace-9KxFcfyV.js +0 -2
|
@@ -0,0 +1,3771 @@
|
|
|
1
|
+
import { i as __toESM } from "./rolldown-runtime-DktXgydw.js";
|
|
2
|
+
import { $ as FileSearchPanel, $n as resolveLanguage, A as parseEditorPreferences, An as reviewConflictIdentity, At as defaultReviewView, Bn as markWorkerErrorReported, Bt as fileHref, D as ReviewActionDialog, E as FileConflictPanel, F as FileWorkspace, Fn as areDiffTargetsEqual, Ft as reviewComparison, Gn as areThemesEqual, Gt as fileRoutes, Hn as renderFileWithHighlighter, Ht as readFileLocation, I as defaultFileDestination, In as WorkerPoolTaskCanceledError, It as reviewContextKey, Jn as validateResolvedThemeName, Kn as getSharedHighlighter, L as fileActionIntent, Ln as WorkerPoolTerminatedError, Lt as reviewHref, M as FileMutationDialog, Mn as renderDiffWithHighlighter, Mt as parseReviewSession, N as FileReviewPanel, Nn as areDiffRenderOptionsEqual, Nt as readReviewLocation, O as FileEditorPanel, On as createCodeAnchor, P as codeThemes, Pn as areFilesEqual, Pt as rememberReviewPosition, Qn as getResolvedOrResolveLanguage, R as mutationFileView, Rn as WorkerPoolWorkerError, Rt as closeFileTab, St as isCodeConversation, Tt as readConversationLocation, Un as areFileRenderOptionsEqual, Ut as reconcileFileView, Vn as isFilePlainText, Vt as parseFileView, Wn as hasResolvedThemes, Wt as rememberFile, Xn as themeResolver, Yn as attachResolvedThemes, Zn as getThemes, _r as parseFileSearchResult, ar as parseFileSaveRequest, br as parseFileIndex, bt as conversationHref, cn as toast, cr as parseMutationPlan, dn as Text, dr as parseReviewDiff, er as areLanguagesAttached, f as storeDocument, fr as parseReviewHistory, gr as parseFileSearchQuery, hr as parseReviewSource, ir as parseReviewActionResult, j as FileRecoveryDialog, jn as isDiffPlainText, k as defaultEditorPreferences, kn as parseFileUpdate, kt as defaultReviewPosition, l as projectStore, ln as readPreference, lr as parseMutationResult, m as storedFiles, mr as parseReviewSnapshot, nr as parseReviewActionPlan, or as parseFileSaveResult, p as storedDocument, pr as parseReviewRepository, qn as prepareThemeResolution, rr as parseReviewActionRecoveries, s as applyFileUpdate, sr as maxFileSaveBytes, t as commentsUnavailable, tr as ResolvedLanguages, tt as ConversationSidebar, u as saveFiles, un as writePreference, ur as parseRecoveries, v as projectLive, vr as parseFileChange, vt as codeCommentCounts, wt as locateCodeConversations, xr as parseFilePath, y as routes, yr as parseFileDocument, z as remainingMutationIntent, zn as isHandledWorkerPoolError, zt as defaultFileView } from "./comment-data-nUlI0szN.js";
|
|
3
|
+
import { B as DEFAULT_THEMES, C as getFiletypeFromFileName, S as getCustomExtensionsVersion, x as getCustomExtensionsMap } from "./cloneFileDiffMetadata-C1BcNpxN.js";
|
|
4
|
+
import { n as createProjectComments, t as defineScreen } from "./screen-DQoTOWYR.js";
|
|
5
|
+
import { t as require_lru } from "./lru-C0_oUet4.js";
|
|
6
|
+
import { t as ProjectThreadLinks } from "./project-thread-links-LpUAGcDr.js";
|
|
7
|
+
import { ref } from "remix/component";
|
|
8
|
+
import { jsx, jsxs } from "remix/component/jsx-runtime";
|
|
9
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/highlighter/languages/getResolvedLanguages.js
|
|
10
|
+
function getResolvedLanguages(languages) {
|
|
11
|
+
const resolvedLanguages = [];
|
|
12
|
+
for (const language of languages) {
|
|
13
|
+
const resolvedLanguage = ResolvedLanguages.get(language);
|
|
14
|
+
if (resolvedLanguage == null) throw new Error(`getResolvedLanguages: ${language} is not resolved. Please resolve languages before calling getResolvedLanguages`);
|
|
15
|
+
resolvedLanguages.push(resolvedLanguage);
|
|
16
|
+
}
|
|
17
|
+
return resolvedLanguages;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/highlighter/languages/hasResolvedLanguages.js
|
|
21
|
+
function hasResolvedLanguages(languages) {
|
|
22
|
+
for (const language of Array.isArray(languages) ? languages : [languages]) if (!ResolvedLanguages.has(language)) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/highlighter/languages/resolveLanguages.js
|
|
27
|
+
async function resolveLanguages(languages) {
|
|
28
|
+
const resolvedLanguages = [];
|
|
29
|
+
const languagesToResolve = [];
|
|
30
|
+
for (const language of languages) {
|
|
31
|
+
if (language === "text" || language === "ansi") continue;
|
|
32
|
+
const maybeResolvedLanguage = getResolvedOrResolveLanguage(language) ?? resolveLanguage(language);
|
|
33
|
+
if ("then" in maybeResolvedLanguage) languagesToResolve.push(maybeResolvedLanguage);
|
|
34
|
+
else resolvedLanguages.push(maybeResolvedLanguage);
|
|
35
|
+
}
|
|
36
|
+
if (languagesToResolve.length > 0) await Promise.all(languagesToResolve).then((_resolvedLanguages) => {
|
|
37
|
+
for (const resolvedLanguage of _resolvedLanguages) {
|
|
38
|
+
if (resolvedLanguage == null) throw new Error("resolvedLanguages: unable to resolve language");
|
|
39
|
+
resolvedLanguages.push(resolvedLanguage);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return resolvedLanguages;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/highlighter/themes/getResolvedThemes.js
|
|
46
|
+
function getResolvedThemes(themeNames) {
|
|
47
|
+
return themeResolver.getResolvedThemes(themeNames);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/highlighter/themes/resolveThemes.js
|
|
51
|
+
async function resolveThemes(themes) {
|
|
52
|
+
for (const themeName of themes) prepareThemeResolution(themeName);
|
|
53
|
+
const resolvedThemes = await themeResolver.resolveThemes(themes);
|
|
54
|
+
for (let i = 0; i < themes.length; i++) validateResolvedThemeName(themes[i], resolvedThemes[i]);
|
|
55
|
+
return resolvedThemes;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.5.2_@shikijs+themes@4.5.0_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/@pierre/diffs/dist/worker/WorkerPoolManager.js
|
|
59
|
+
var import_lru = /* @__PURE__ */ __toESM(require_lru(), 1);
|
|
60
|
+
var IGNORE_RESPONSE = Symbol("IGNORE_RESPONSE");
|
|
61
|
+
var DEFAULT_WORKER_INITIALIZATION_TIMEOUT = 1e4;
|
|
62
|
+
var WorkerPoolManager = class {
|
|
63
|
+
options;
|
|
64
|
+
highlighter;
|
|
65
|
+
preferredHighlighter;
|
|
66
|
+
renderOptions;
|
|
67
|
+
renderOptionsRequestVersion = 0;
|
|
68
|
+
renderOptionsVersion = 0;
|
|
69
|
+
initialized = false;
|
|
70
|
+
workers = [];
|
|
71
|
+
queuedTasks = [];
|
|
72
|
+
queuedTaskByInstance = /* @__PURE__ */ new Map();
|
|
73
|
+
taskByHighlightKey = /* @__PURE__ */ new Map();
|
|
74
|
+
activeTaskById = /* @__PURE__ */ new Map();
|
|
75
|
+
activeRequestByInstance = /* @__PURE__ */ new Map();
|
|
76
|
+
nextRequestId = 0;
|
|
77
|
+
themeSubscribers = /* @__PURE__ */ new Set();
|
|
78
|
+
workersFailed = false;
|
|
79
|
+
statSubscribers = /* @__PURE__ */ new Set();
|
|
80
|
+
fileCache;
|
|
81
|
+
diffCache;
|
|
82
|
+
_queuedBroadcast;
|
|
83
|
+
lifecycleGeneration = 0;
|
|
84
|
+
constructor(options, { langs, theme = DEFAULT_THEMES, useTokenTransformer = false, lineDiffType = "word-alt", maxLineDiffLength = 1e3, tokenizeMaxLineLength = 1e3, preferredHighlighter = "shiki-js" }) {
|
|
85
|
+
this.options = options;
|
|
86
|
+
this.preferredHighlighter = preferredHighlighter;
|
|
87
|
+
this.renderOptions = {
|
|
88
|
+
theme,
|
|
89
|
+
useTokenTransformer,
|
|
90
|
+
lineDiffType,
|
|
91
|
+
maxLineDiffLength,
|
|
92
|
+
tokenizeMaxLineLength
|
|
93
|
+
};
|
|
94
|
+
this.fileCache = new import_lru.default.LRUMap(options.totalASTLRUCacheSize ?? 100);
|
|
95
|
+
this.diffCache = new import_lru.default.LRUMap(options.totalASTLRUCacheSize ?? 100);
|
|
96
|
+
this.queueInitialization(langs);
|
|
97
|
+
}
|
|
98
|
+
isWorkingPool() {
|
|
99
|
+
return !this.workersFailed;
|
|
100
|
+
}
|
|
101
|
+
getFileResultCache(file) {
|
|
102
|
+
return file.cacheKey != null ? this.fileCache.get(file.cacheKey) : void 0;
|
|
103
|
+
}
|
|
104
|
+
getDiffResultCache(diff) {
|
|
105
|
+
return diff.cacheKey != null ? this.diffCache.get(diff.cacheKey) : void 0;
|
|
106
|
+
}
|
|
107
|
+
inspectCaches() {
|
|
108
|
+
const { fileCache, diffCache } = this;
|
|
109
|
+
return {
|
|
110
|
+
fileCache,
|
|
111
|
+
diffCache
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
evictFileFromCache(cacheKey) {
|
|
115
|
+
try {
|
|
116
|
+
return this.fileCache.delete(cacheKey) !== void 0;
|
|
117
|
+
} finally {
|
|
118
|
+
this.queueBroadcastStateChanges();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
evictDiffFromCache(cacheKey) {
|
|
122
|
+
try {
|
|
123
|
+
return this.diffCache.delete(cacheKey) !== void 0;
|
|
124
|
+
} finally {
|
|
125
|
+
this.queueBroadcastStateChanges();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async setRenderOptions({ theme = this.renderOptions.theme ?? DEFAULT_THEMES, useTokenTransformer = this.renderOptions.useTokenTransformer ?? false, lineDiffType = this.renderOptions.lineDiffType ?? "word-alt", maxLineDiffLength = this.renderOptions.maxLineDiffLength ?? 1e3, tokenizeMaxLineLength = this.renderOptions.tokenizeMaxLineLength ?? 1e3 }) {
|
|
129
|
+
const { lifecycleGeneration } = this;
|
|
130
|
+
const renderOptionsRequestVersion = ++this.renderOptionsRequestVersion;
|
|
131
|
+
const isCurrentRequest = () => this.isCurrentLifecycle(lifecycleGeneration) && this.renderOptionsRequestVersion === renderOptionsRequestVersion;
|
|
132
|
+
try {
|
|
133
|
+
const newRenderOptions = {
|
|
134
|
+
theme,
|
|
135
|
+
useTokenTransformer,
|
|
136
|
+
lineDiffType,
|
|
137
|
+
maxLineDiffLength,
|
|
138
|
+
tokenizeMaxLineLength
|
|
139
|
+
};
|
|
140
|
+
if (!this.isInitialized()) await this.initialize();
|
|
141
|
+
if (!isCurrentRequest() || areDiffRenderOptionsEqual(newRenderOptions, this.renderOptions)) return;
|
|
142
|
+
const themeNames = getThemes(theme);
|
|
143
|
+
let resolvedThemes = [];
|
|
144
|
+
if (!areThemesEqual(newRenderOptions.theme, this.renderOptions.theme)) {
|
|
145
|
+
if (hasResolvedThemes(themeNames)) resolvedThemes = getResolvedThemes(themeNames);
|
|
146
|
+
else resolvedThemes = await resolveThemes(themeNames);
|
|
147
|
+
}
|
|
148
|
+
if (!isCurrentRequest()) return;
|
|
149
|
+
if (this.highlighter != null) attachResolvedThemes(resolvedThemes, this.highlighter);
|
|
150
|
+
else {
|
|
151
|
+
const highlighter = await getSharedHighlighter({
|
|
152
|
+
themes: themeNames,
|
|
153
|
+
langs: ["text"],
|
|
154
|
+
preferredHighlighter: this.preferredHighlighter
|
|
155
|
+
});
|
|
156
|
+
if (!isCurrentRequest()) return;
|
|
157
|
+
this.highlighter = highlighter;
|
|
158
|
+
}
|
|
159
|
+
const workerSetup = this.setRenderOptionsOnWorkers(newRenderOptions, resolvedThemes);
|
|
160
|
+
this.renderOptions = newRenderOptions;
|
|
161
|
+
this.renderOptionsVersion++;
|
|
162
|
+
this.diffCache.clear();
|
|
163
|
+
this.fileCache.clear();
|
|
164
|
+
this.invalidateRenderTasks();
|
|
165
|
+
for (const instance of this.themeSubscribers) instance.onThemeChange();
|
|
166
|
+
await workerSetup;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof WorkerPoolTerminatedError || !isCurrentRequest()) return;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
getPreferredHighlighter() {
|
|
173
|
+
return this.preferredHighlighter;
|
|
174
|
+
}
|
|
175
|
+
getFileRenderOptions() {
|
|
176
|
+
const { tokenizeMaxLineLength, theme, useTokenTransformer } = this.renderOptions;
|
|
177
|
+
return {
|
|
178
|
+
theme,
|
|
179
|
+
useTokenTransformer,
|
|
180
|
+
tokenizeMaxLineLength
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
getDiffRenderOptions() {
|
|
184
|
+
return { ...this.renderOptions };
|
|
185
|
+
}
|
|
186
|
+
async setRenderOptionsOnWorkers(renderOptions, resolvedThemes) {
|
|
187
|
+
if (this.workersFailed) return;
|
|
188
|
+
if (!this.isInitialized()) await this.initialize();
|
|
189
|
+
const taskPromises = [];
|
|
190
|
+
for (const managedWorker of this.workers) {
|
|
191
|
+
if (!managedWorker.initialized) {
|
|
192
|
+
console.log({ managedWorker });
|
|
193
|
+
throw new Error("setRenderOptionsOnWorkers: Somehow we have an uninitialized worker");
|
|
194
|
+
}
|
|
195
|
+
taskPromises.push(new Promise((resolve, reject) => {
|
|
196
|
+
const id = this.generateRequestId();
|
|
197
|
+
const task = {
|
|
198
|
+
type: "set-render-options",
|
|
199
|
+
id,
|
|
200
|
+
request: {
|
|
201
|
+
type: "set-render-options",
|
|
202
|
+
id,
|
|
203
|
+
renderOptions,
|
|
204
|
+
resolvedThemes
|
|
205
|
+
},
|
|
206
|
+
resolve,
|
|
207
|
+
reject,
|
|
208
|
+
requestStart: Date.now()
|
|
209
|
+
};
|
|
210
|
+
this.activeTaskById.set(id, task);
|
|
211
|
+
managedWorker.pendingSetupRequestId = id;
|
|
212
|
+
managedWorker.worker.postMessage(task.request);
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
await Promise.all(taskPromises);
|
|
216
|
+
}
|
|
217
|
+
subscribeToThemeChanges(instance) {
|
|
218
|
+
this.themeSubscribers.add(instance);
|
|
219
|
+
this.queueBroadcastStateChanges();
|
|
220
|
+
return () => {
|
|
221
|
+
this.unsubscribeToThemeChanges(instance);
|
|
222
|
+
this.queueBroadcastStateChanges();
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
unsubscribeToThemeChanges(instance) {
|
|
226
|
+
this.themeSubscribers.delete(instance);
|
|
227
|
+
this.queueBroadcastStateChanges();
|
|
228
|
+
}
|
|
229
|
+
subscribeToStatChanges(callback) {
|
|
230
|
+
this.statSubscribers.add(callback);
|
|
231
|
+
callback(this.getStats());
|
|
232
|
+
return () => {
|
|
233
|
+
this.statSubscribers.delete(callback);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
queueBroadcastStateChanges() {
|
|
237
|
+
if (this._queuedBroadcast != null) return;
|
|
238
|
+
this._queuedBroadcast = requestAnimationFrame(this._broadcastStateChanges);
|
|
239
|
+
}
|
|
240
|
+
_broadcastStateChanges = () => {
|
|
241
|
+
if (this._queuedBroadcast != null) {
|
|
242
|
+
cancelAnimationFrame(this._queuedBroadcast);
|
|
243
|
+
this._queuedBroadcast = void 0;
|
|
244
|
+
}
|
|
245
|
+
const stats = this.getStats();
|
|
246
|
+
for (const callback of this.statSubscribers) callback(stats);
|
|
247
|
+
};
|
|
248
|
+
cleanUpTasks(instance) {
|
|
249
|
+
this.detachInstanceFromQueuedTasks(instance);
|
|
250
|
+
const requestId = this.activeRequestByInstance.get(instance);
|
|
251
|
+
if (requestId != null) {
|
|
252
|
+
const task = this.activeTaskById.get(requestId);
|
|
253
|
+
if (isRenderTask(task)) {
|
|
254
|
+
this.detachInstanceFromRenderTask(task, instance);
|
|
255
|
+
if (!task.primeCache && task.instances.size === 0) this.removeActiveTask(task);
|
|
256
|
+
} else this.activeTaskById.delete(requestId);
|
|
257
|
+
}
|
|
258
|
+
this.activeRequestByInstance.delete(instance);
|
|
259
|
+
this.queueBroadcastStateChanges();
|
|
260
|
+
}
|
|
261
|
+
isInitialized() {
|
|
262
|
+
return this.initialized === true;
|
|
263
|
+
}
|
|
264
|
+
async initialize(languages = []) {
|
|
265
|
+
if (this.initialized === true) return;
|
|
266
|
+
else if (this.initialized === false) {
|
|
267
|
+
const { lifecycleGeneration } = this;
|
|
268
|
+
const initialization = new Promise((resolve, reject) => {
|
|
269
|
+
(async () => {
|
|
270
|
+
try {
|
|
271
|
+
const themes = getThemes(this.renderOptions.theme);
|
|
272
|
+
let resolvedThemes = [];
|
|
273
|
+
if (hasResolvedThemes(themes)) resolvedThemes = getResolvedThemes(themes);
|
|
274
|
+
else resolvedThemes = await resolveThemes(themes);
|
|
275
|
+
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
|
|
276
|
+
resolve();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
let resolvedLanguages = [];
|
|
280
|
+
if (hasResolvedLanguages(languages)) resolvedLanguages = getResolvedLanguages(languages);
|
|
281
|
+
else resolvedLanguages = await resolveLanguages(languages);
|
|
282
|
+
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
|
|
283
|
+
resolve();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const [highlighter] = await Promise.all([getSharedHighlighter({
|
|
287
|
+
themes,
|
|
288
|
+
langs: ["text", ...languages],
|
|
289
|
+
preferredHighlighter: this.preferredHighlighter
|
|
290
|
+
}), this.initializeWorkers(resolvedThemes, resolvedLanguages)]);
|
|
291
|
+
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
|
|
292
|
+
this.terminateWorkers();
|
|
293
|
+
resolve();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.highlighter = highlighter;
|
|
297
|
+
this.initialized = true;
|
|
298
|
+
this.diffCache.clear();
|
|
299
|
+
this.fileCache.clear();
|
|
300
|
+
this.drainQueue();
|
|
301
|
+
this.queueBroadcastStateChanges();
|
|
302
|
+
resolve();
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if (e instanceof WorkerPoolTerminatedError || !this.isCurrentLifecycle(lifecycleGeneration)) {
|
|
305
|
+
resolve();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.initialized = false;
|
|
309
|
+
this.workersFailed = true;
|
|
310
|
+
this.cancelActiveWorkerTasks();
|
|
311
|
+
this.terminateWorkers();
|
|
312
|
+
this.activeTaskById.clear();
|
|
313
|
+
for (const task of this.queuedTasks) this.rejectRenderTaskCallbacks(task, normalizeWorkerError(e));
|
|
314
|
+
this.queueBroadcastStateChanges();
|
|
315
|
+
reject(e);
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
318
|
+
});
|
|
319
|
+
this.initialized = initialization;
|
|
320
|
+
this.queueBroadcastStateChanges();
|
|
321
|
+
return initialization;
|
|
322
|
+
} else return this.initialized;
|
|
323
|
+
}
|
|
324
|
+
async initializeWorkers(resolvedThemes, resolvedLanguages) {
|
|
325
|
+
this.workersFailed = false;
|
|
326
|
+
const initPromises = [];
|
|
327
|
+
const customExtensionVersion = getCustomExtensionsVersion();
|
|
328
|
+
const customExtensionMap = customExtensionVersion > 0 ? getCustomExtensionsMap() : void 0;
|
|
329
|
+
if (this.workers.length > 0) this.terminateWorkers();
|
|
330
|
+
for (let i = 0; i < (this.options.poolSize ?? 8); i++) {
|
|
331
|
+
const worker = this.options.workerFactory();
|
|
332
|
+
const managedWorker = {
|
|
333
|
+
worker,
|
|
334
|
+
requestId: void 0,
|
|
335
|
+
pendingSetupRequestId: void 0,
|
|
336
|
+
initialized: false,
|
|
337
|
+
langs: /* @__PURE__ */ new Set(["text", ...resolvedLanguages.map(({ name }) => name)]),
|
|
338
|
+
customExtensionsVersion: 0
|
|
339
|
+
};
|
|
340
|
+
worker.addEventListener("message", (event) => {
|
|
341
|
+
this.handleWorkerMessage(managedWorker, event.data);
|
|
342
|
+
});
|
|
343
|
+
worker.addEventListener("error", (error) => this.handleWorkerError(managedWorker, error));
|
|
344
|
+
this.workers.push(managedWorker);
|
|
345
|
+
initPromises.push(new Promise((resolve, reject) => {
|
|
346
|
+
const id = this.generateRequestId();
|
|
347
|
+
const task = {
|
|
348
|
+
type: "initialize",
|
|
349
|
+
id,
|
|
350
|
+
request: {
|
|
351
|
+
type: "initialize",
|
|
352
|
+
id,
|
|
353
|
+
renderOptions: this.renderOptions,
|
|
354
|
+
preferredHighlighter: this.preferredHighlighter,
|
|
355
|
+
resolvedThemes,
|
|
356
|
+
resolvedLanguages,
|
|
357
|
+
customExtensionsVersion: customExtensionMap != null ? customExtensionVersion : void 0,
|
|
358
|
+
customExtensionMap
|
|
359
|
+
},
|
|
360
|
+
resolve() {
|
|
361
|
+
managedWorker.initialized = true;
|
|
362
|
+
resolve();
|
|
363
|
+
},
|
|
364
|
+
reject,
|
|
365
|
+
requestStart: Date.now()
|
|
366
|
+
};
|
|
367
|
+
managedWorker.pendingSetupRequestId = id;
|
|
368
|
+
this.activeTaskById.set(id, task);
|
|
369
|
+
this.executeTask(managedWorker, task);
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
const timeoutMs = this.options.workerInitializationTimeout ?? DEFAULT_WORKER_INITIALIZATION_TIMEOUT;
|
|
373
|
+
let timeoutId;
|
|
374
|
+
try {
|
|
375
|
+
await Promise.race([Promise.all(initPromises), new Promise((_, reject) => {
|
|
376
|
+
timeoutId = setTimeout(() => {
|
|
377
|
+
reject(/* @__PURE__ */ new Error(`WorkerPoolManager: worker initialization timed out after ${timeoutMs}ms`));
|
|
378
|
+
}, timeoutMs);
|
|
379
|
+
})]);
|
|
380
|
+
} finally {
|
|
381
|
+
if (timeoutId != null) clearTimeout(timeoutId);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Pass worker errors to onWorkerError, or log them if no callback is set.
|
|
386
|
+
*
|
|
387
|
+
* A startup error rejects initialization so components can highlight on the
|
|
388
|
+
* main thread. Other requests wait for their own responses: worker.ts catches
|
|
389
|
+
* request errors, so this event may be unrelated to the current request.
|
|
390
|
+
* A worker that stops responding remains busy and receives no further work.
|
|
391
|
+
*/
|
|
392
|
+
handleWorkerError(managedWorker, event) {
|
|
393
|
+
const { onWorkerError } = this.options;
|
|
394
|
+
if (onWorkerError != null) try {
|
|
395
|
+
onWorkerError(event, managedWorker.worker);
|
|
396
|
+
} catch (hookError) {
|
|
397
|
+
console.error("WorkerPoolManager: onWorkerError threw:", hookError);
|
|
398
|
+
}
|
|
399
|
+
else console.error("Worker error:", event, managedWorker);
|
|
400
|
+
const { pendingSetupRequestId } = managedWorker;
|
|
401
|
+
if (pendingSetupRequestId == null) return;
|
|
402
|
+
const task = this.activeTaskById.get(pendingSetupRequestId);
|
|
403
|
+
if (task?.type !== "initialize") return;
|
|
404
|
+
const error = errorFromWorkerErrorEvent(event);
|
|
405
|
+
if (onWorkerError != null) markWorkerErrorReported(error);
|
|
406
|
+
task.reject(error);
|
|
407
|
+
this.cleanWorkerAndTask(managedWorker, task);
|
|
408
|
+
}
|
|
409
|
+
drainQueue = () => {
|
|
410
|
+
this._queuedDrain = void 0;
|
|
411
|
+
if (this.initialized !== true || this.queuedTasks.length === 0) return;
|
|
412
|
+
for (let i = 0; i < this.queuedTasks.length;) {
|
|
413
|
+
const task = this.queuedTasks[i];
|
|
414
|
+
if (this.hasActiveRequest(task)) {
|
|
415
|
+
i++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const langs = getLangsFromTask(task);
|
|
419
|
+
const availableWorker = this.getAvailableWorker(langs);
|
|
420
|
+
if (availableWorker == null) break;
|
|
421
|
+
this.queuedTasks.splice(i, 1);
|
|
422
|
+
this.assignWorkerToTask(task, availableWorker);
|
|
423
|
+
this.resolveLanguagesAndExecuteTask(availableWorker, task, langs);
|
|
424
|
+
}
|
|
425
|
+
this.queueBroadcastStateChanges();
|
|
426
|
+
};
|
|
427
|
+
highlightFileAST(instance, file) {
|
|
428
|
+
const cachedResult = this.getFileResultCache(file);
|
|
429
|
+
if (isFilePlainText(file) || cachedResult != null && areFileRenderOptionsEqual(cachedResult.options, this.getFileRenderOptions())) return;
|
|
430
|
+
if (!this.hasMatchingFileInstanceTask(instance, file)) this.submitTask(instance, {
|
|
431
|
+
type: "file",
|
|
432
|
+
file
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
primeFileHighlightCache(file) {
|
|
436
|
+
if (!this.isWorkingPool()) return Promise.reject(/* @__PURE__ */ new Error("WorkerPoolManager.primeFileHighlightCache: worker pool is not working"));
|
|
437
|
+
const task = this.getOrCreateFileHighlightCacheTask(file);
|
|
438
|
+
return task != null ? this.createRenderTaskCallbacks(task) : Promise.resolve();
|
|
439
|
+
}
|
|
440
|
+
getOrCreateFileHighlightCacheTask(file) {
|
|
441
|
+
if (file.cacheKey == null) {
|
|
442
|
+
console.warn(`WorkerPoolManager.primeFileHighlightCache: priming highlight cache requires file.cacheKey; skipping "${file.name}".`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const cachedResult = this.getFileResultCache(file);
|
|
446
|
+
const highlightKey = this.getFileHighlightKey(file);
|
|
447
|
+
if (highlightKey == null || isFilePlainText(file) || cachedResult != null && areFileRenderOptionsEqual(cachedResult.options, this.getFileRenderOptions())) return;
|
|
448
|
+
const existingTask = this.getTaskByHighlightKey(highlightKey);
|
|
449
|
+
if (existingTask != null) {
|
|
450
|
+
existingTask.primeCache = true;
|
|
451
|
+
return existingTask.type === "file" ? existingTask : void 0;
|
|
452
|
+
} else {
|
|
453
|
+
const task = this.submitCacheTask({
|
|
454
|
+
type: "file",
|
|
455
|
+
file
|
|
456
|
+
}, highlightKey);
|
|
457
|
+
return task.type === "file" ? task : void 0;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
getPlainFileAST(file, startingLine, totalLines, lines) {
|
|
461
|
+
if (this.highlighter == null) {
|
|
462
|
+
this.queueInitialization();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
return renderFileWithHighlighter(file, this.highlighter, this.renderOptions, {
|
|
466
|
+
forcePlainText: true,
|
|
467
|
+
startingLine,
|
|
468
|
+
totalLines,
|
|
469
|
+
lines
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
highlightDiffAST(instance, diff) {
|
|
473
|
+
const cachedResult = this.getDiffResultCache(diff);
|
|
474
|
+
if (isDiffPlainText(diff) || cachedResult != null && areDiffRenderOptionsEqual(cachedResult.options, this.getDiffRenderOptions())) return;
|
|
475
|
+
if (!this.hasMatchingDiffInstanceTask(instance, diff)) this.submitTask(instance, {
|
|
476
|
+
type: "diff",
|
|
477
|
+
diff
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
primeDiffHighlightCache(diff) {
|
|
481
|
+
if (!this.isWorkingPool()) return Promise.reject(/* @__PURE__ */ new Error("WorkerPoolManager.primeDiffHighlightCache: worker pool is not working"));
|
|
482
|
+
const task = this.getOrCreateDiffHighlightCacheTask(diff);
|
|
483
|
+
return task != null ? this.createRenderTaskCallbacks(task) : Promise.resolve();
|
|
484
|
+
}
|
|
485
|
+
getOrCreateDiffHighlightCacheTask(diff) {
|
|
486
|
+
if (diff.cacheKey == null) {
|
|
487
|
+
console.warn(`WorkerPoolManager.primeDiffHighlightCache: priming highlight cache requires diff.cacheKey; skipping "${diff.prevName ?? diff.name}" -> "${diff.name}".`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const cachedResult = this.getDiffResultCache(diff);
|
|
491
|
+
const highlightKey = this.getDiffHighlightKey(diff);
|
|
492
|
+
if (highlightKey == null || isDiffPlainText(diff) || cachedResult != null && areDiffRenderOptionsEqual(cachedResult.options, this.getDiffRenderOptions())) return;
|
|
493
|
+
const existingTask = this.getTaskByHighlightKey(highlightKey);
|
|
494
|
+
if (existingTask != null) {
|
|
495
|
+
existingTask.primeCache = true;
|
|
496
|
+
return existingTask.type === "diff" ? existingTask : void 0;
|
|
497
|
+
} else {
|
|
498
|
+
const task = this.submitCacheTask({
|
|
499
|
+
type: "diff",
|
|
500
|
+
diff
|
|
501
|
+
}, highlightKey);
|
|
502
|
+
return task.type === "diff" ? task : void 0;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
getPlainDiffAST(diff, startingLine, totalLines, expandedHunks, collapsedContextThreshold) {
|
|
506
|
+
return this.highlighter != null ? renderDiffWithHighlighter(diff, this.highlighter, this.renderOptions, {
|
|
507
|
+
forcePlainText: true,
|
|
508
|
+
startingLine,
|
|
509
|
+
totalLines,
|
|
510
|
+
expandedHunks,
|
|
511
|
+
collapsedContextThreshold
|
|
512
|
+
}) : void 0;
|
|
513
|
+
}
|
|
514
|
+
terminate() {
|
|
515
|
+
this.lifecycleGeneration++;
|
|
516
|
+
this.cancelActiveWorkerTasks();
|
|
517
|
+
this.terminateWorkers();
|
|
518
|
+
const error = new WorkerPoolTerminatedError();
|
|
519
|
+
for (const task of this.queuedTasks) this.rejectRenderTaskCallbacks(task, error);
|
|
520
|
+
this.fileCache.clear();
|
|
521
|
+
this.diffCache.clear();
|
|
522
|
+
this.activeRequestByInstance.clear();
|
|
523
|
+
this.queuedTasks.length = 0;
|
|
524
|
+
this.queuedTaskByInstance.clear();
|
|
525
|
+
this.taskByHighlightKey.clear();
|
|
526
|
+
this.activeTaskById.clear();
|
|
527
|
+
this.highlighter = void 0;
|
|
528
|
+
this.initialized = false;
|
|
529
|
+
this.workersFailed = false;
|
|
530
|
+
this.queueBroadcastStateChanges();
|
|
531
|
+
}
|
|
532
|
+
isCurrentLifecycle(lifecycleGeneration) {
|
|
533
|
+
return this.lifecycleGeneration === lifecycleGeneration;
|
|
534
|
+
}
|
|
535
|
+
queueInitialization(languages) {
|
|
536
|
+
this.initialize(languages).catch((error) => {
|
|
537
|
+
if (isHandledWorkerPoolError(error)) return;
|
|
538
|
+
console.error(error);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
cancelActiveWorkerTasks() {
|
|
542
|
+
const error = new WorkerPoolTerminatedError();
|
|
543
|
+
for (const task of this.activeTaskById.values()) if ("reject" in task) task.reject(error);
|
|
544
|
+
else if (isRenderTask(task)) this.rejectRenderTaskCallbacks(task, error);
|
|
545
|
+
}
|
|
546
|
+
terminateWorkers() {
|
|
547
|
+
for (const managedWorker of this.workers) managedWorker.worker.terminate();
|
|
548
|
+
this.workers.length = 0;
|
|
549
|
+
}
|
|
550
|
+
getStats() {
|
|
551
|
+
return {
|
|
552
|
+
managerState: (() => {
|
|
553
|
+
if (this.initialized === false) return "waiting";
|
|
554
|
+
if (this.initialized !== true) return "initializing";
|
|
555
|
+
return "initialized";
|
|
556
|
+
})(),
|
|
557
|
+
totalWorkers: this.workers.length,
|
|
558
|
+
workersFailed: this.workersFailed,
|
|
559
|
+
busyWorkers: this.workers.filter((w) => w.requestId != null || w.pendingSetupRequestId != null).length,
|
|
560
|
+
queuedTasks: this.queuedTasks.length,
|
|
561
|
+
activeTasks: this.activeTaskById.size,
|
|
562
|
+
themeSubscribers: this.themeSubscribers.size,
|
|
563
|
+
fileCacheSize: this.fileCache.size,
|
|
564
|
+
diffCacheSize: this.diffCache.size
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
submitTask(instance, request) {
|
|
568
|
+
if (this.initialized === false) this.queueInitialization();
|
|
569
|
+
const highlightKey = this.getHighlightKeyForRequest(request);
|
|
570
|
+
const existingTask = highlightKey != null ? this.getTaskByHighlightKey(highlightKey) : void 0;
|
|
571
|
+
if (existingTask != null) {
|
|
572
|
+
this.detachInstanceFromQueuedTasks(instance, existingTask);
|
|
573
|
+
this.addInstanceToTask(existingTask, instance);
|
|
574
|
+
this.queueBroadcastStateChanges();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.detachInstanceFromQueuedTasks(instance);
|
|
578
|
+
const id = this.generateRequestId();
|
|
579
|
+
const requestStart = Date.now();
|
|
580
|
+
const { renderOptionsVersion } = this;
|
|
581
|
+
const task = (() => {
|
|
582
|
+
switch (request.type) {
|
|
583
|
+
case "file": return {
|
|
584
|
+
type: "file",
|
|
585
|
+
id,
|
|
586
|
+
request: {
|
|
587
|
+
...request,
|
|
588
|
+
id
|
|
589
|
+
},
|
|
590
|
+
instances: /* @__PURE__ */ new Set([instance]),
|
|
591
|
+
primeCache: false,
|
|
592
|
+
highlightKey,
|
|
593
|
+
callbacks: /* @__PURE__ */ new Set(),
|
|
594
|
+
renderOptionsVersion,
|
|
595
|
+
requestStart
|
|
596
|
+
};
|
|
597
|
+
case "diff": return {
|
|
598
|
+
type: "diff",
|
|
599
|
+
id,
|
|
600
|
+
request: {
|
|
601
|
+
...request,
|
|
602
|
+
id
|
|
603
|
+
},
|
|
604
|
+
instances: /* @__PURE__ */ new Set([instance]),
|
|
605
|
+
primeCache: false,
|
|
606
|
+
highlightKey,
|
|
607
|
+
callbacks: /* @__PURE__ */ new Set(),
|
|
608
|
+
renderOptionsVersion,
|
|
609
|
+
requestStart
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
this.enqueueRenderTask(task, instance);
|
|
614
|
+
}
|
|
615
|
+
submitCacheTask(request, highlightKey) {
|
|
616
|
+
if (this.initialized === false) this.queueInitialization();
|
|
617
|
+
const id = this.generateRequestId();
|
|
618
|
+
const requestStart = Date.now();
|
|
619
|
+
const { renderOptionsVersion } = this;
|
|
620
|
+
const task = (() => {
|
|
621
|
+
switch (request.type) {
|
|
622
|
+
case "file": return {
|
|
623
|
+
type: "file",
|
|
624
|
+
id,
|
|
625
|
+
request: {
|
|
626
|
+
...request,
|
|
627
|
+
id
|
|
628
|
+
},
|
|
629
|
+
instances: /* @__PURE__ */ new Set(),
|
|
630
|
+
primeCache: true,
|
|
631
|
+
highlightKey,
|
|
632
|
+
callbacks: /* @__PURE__ */ new Set(),
|
|
633
|
+
renderOptionsVersion,
|
|
634
|
+
requestStart
|
|
635
|
+
};
|
|
636
|
+
case "diff": return {
|
|
637
|
+
type: "diff",
|
|
638
|
+
id,
|
|
639
|
+
request: {
|
|
640
|
+
...request,
|
|
641
|
+
id
|
|
642
|
+
},
|
|
643
|
+
instances: /* @__PURE__ */ new Set(),
|
|
644
|
+
primeCache: true,
|
|
645
|
+
highlightKey,
|
|
646
|
+
callbacks: /* @__PURE__ */ new Set(),
|
|
647
|
+
renderOptionsVersion,
|
|
648
|
+
requestStart
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
this.enqueueRenderTask(task);
|
|
653
|
+
return task;
|
|
654
|
+
}
|
|
655
|
+
enqueueRenderTask(task, instance) {
|
|
656
|
+
this.queuedTasks.push(task);
|
|
657
|
+
if (instance != null) this.queuedTaskByInstance.set(instance, task);
|
|
658
|
+
if (task.highlightKey != null) this.taskByHighlightKey.set(task.highlightKey, task);
|
|
659
|
+
this.queueDrain();
|
|
660
|
+
}
|
|
661
|
+
async resolveLanguagesAndExecuteTask(availableWorker, task, langs) {
|
|
662
|
+
try {
|
|
663
|
+
const mainThreadLangs = langs.filter((lang) => !areLanguagesAttached(lang));
|
|
664
|
+
if (mainThreadLangs.length > 0) getSharedHighlighter({
|
|
665
|
+
themes: getThemes(this.renderOptions.theme),
|
|
666
|
+
langs: ["text", ...mainThreadLangs],
|
|
667
|
+
preferredHighlighter: this.preferredHighlighter
|
|
668
|
+
}).catch((error) => {
|
|
669
|
+
console.error(error);
|
|
670
|
+
});
|
|
671
|
+
const workerMissingLangs = langs.filter((lang) => !availableWorker.langs.has(lang));
|
|
672
|
+
if (workerMissingLangs.length > 0) {
|
|
673
|
+
if (hasResolvedLanguages(workerMissingLangs)) task.request.resolvedLanguages = getResolvedLanguages(workerMissingLangs);
|
|
674
|
+
else task.request.resolvedLanguages = await resolveLanguages(workerMissingLangs);
|
|
675
|
+
}
|
|
676
|
+
if (!this.activeTaskById.has(task.id)) {
|
|
677
|
+
if (availableWorker.requestId === task.id) {
|
|
678
|
+
this.cleanWorkerAndTask(availableWorker, task);
|
|
679
|
+
this.queueBroadcastStateChanges();
|
|
680
|
+
if (this.queuedTasks.length > 0) this.queueDrain();
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.executeTask(availableWorker, task);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
this.rejectRenderTaskCallbacks(task, normalizeWorkerError(error));
|
|
687
|
+
this.cleanWorkerAndTask(availableWorker, task);
|
|
688
|
+
this.queueBroadcastStateChanges();
|
|
689
|
+
if (this.queuedTasks.length > 0) this.queueDrain();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
handleWorkerMessage(managedWorker, response) {
|
|
693
|
+
const task = this.activeTaskById.get(response.id);
|
|
694
|
+
try {
|
|
695
|
+
if (task == null) throw IGNORE_RESPONSE;
|
|
696
|
+
else if (response.type === "error") {
|
|
697
|
+
const error = new Error(response.error);
|
|
698
|
+
if (response.stack) error.stack = response.stack;
|
|
699
|
+
if ("reject" in task) task.reject(error);
|
|
700
|
+
else if (isRenderTask(task)) {
|
|
701
|
+
this.notifyHighlightError(task, error);
|
|
702
|
+
this.rejectRenderTaskCallbacks(task, error);
|
|
703
|
+
} else throw new Error("handleWorkerMessage: unknown task type");
|
|
704
|
+
throw error;
|
|
705
|
+
} else {
|
|
706
|
+
if (isRenderTask(task)) for (const { name } of task.request.resolvedLanguages ?? []) managedWorker.langs.add(name);
|
|
707
|
+
switch (response.requestType) {
|
|
708
|
+
case "initialize":
|
|
709
|
+
if (task.type !== "initialize") throw new Error("handleWorkerMessage: task/response dont match");
|
|
710
|
+
this.syncCustomExtensionVersion(managedWorker, task.request);
|
|
711
|
+
task.resolve();
|
|
712
|
+
break;
|
|
713
|
+
case "set-render-options":
|
|
714
|
+
if (task.type !== "set-render-options") throw new Error("handleWorkerMessage: task/response dont match");
|
|
715
|
+
task.resolve();
|
|
716
|
+
break;
|
|
717
|
+
case "file": {
|
|
718
|
+
if (task.type !== "file") throw new Error("handleWorkerMessage: task/response dont match");
|
|
719
|
+
const { result, options } = response;
|
|
720
|
+
if (!this.isCurrentRenderTask(task) || !areFileRenderOptionsEqual(options, this.getFileRenderOptions())) throw IGNORE_RESPONSE;
|
|
721
|
+
const { request } = task;
|
|
722
|
+
this.syncCustomExtensionVersion(managedWorker, request);
|
|
723
|
+
if (task.cacheKeyAtDispatch != null) this.fileCache.set(task.cacheKeyAtDispatch, {
|
|
724
|
+
result,
|
|
725
|
+
options
|
|
726
|
+
});
|
|
727
|
+
this.resolveRenderTaskCallbacks(task);
|
|
728
|
+
this.notifyFileInstances(task, result, options);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "diff": {
|
|
732
|
+
if (task.type !== "diff") throw new Error("handleWorkerMessage: task/response dont match");
|
|
733
|
+
const { result, options } = response;
|
|
734
|
+
if (!this.isCurrentRenderTask(task) || !areDiffRenderOptionsEqual(options, this.getDiffRenderOptions())) throw IGNORE_RESPONSE;
|
|
735
|
+
const { request } = task;
|
|
736
|
+
this.syncCustomExtensionVersion(managedWorker, request);
|
|
737
|
+
if (task.cacheKeyAtDispatch != null) this.diffCache.set(task.cacheKeyAtDispatch, {
|
|
738
|
+
result,
|
|
739
|
+
options
|
|
740
|
+
});
|
|
741
|
+
this.resolveRenderTaskCallbacks(task);
|
|
742
|
+
this.notifyDiffInstances(task, result, options);
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
if (error !== IGNORE_RESPONSE) console.error(error, task, response);
|
|
749
|
+
}
|
|
750
|
+
this.cleanWorkerAndTask(managedWorker, task, response.id);
|
|
751
|
+
this.queueBroadcastStateChanges();
|
|
752
|
+
if (this.queuedTasks.length > 0) this.queueDrain();
|
|
753
|
+
}
|
|
754
|
+
_queuedDrain;
|
|
755
|
+
queueDrain() {
|
|
756
|
+
if (this._queuedDrain != null) return;
|
|
757
|
+
this._queuedDrain = Promise.resolve().then(this.drainQueue);
|
|
758
|
+
this.queueBroadcastStateChanges();
|
|
759
|
+
}
|
|
760
|
+
assignWorkerToTask(task, managedWorker) {
|
|
761
|
+
managedWorker.requestId = task.id;
|
|
762
|
+
if (isRenderTask(task)) {
|
|
763
|
+
this.clearQueuedInstanceRequests(task);
|
|
764
|
+
this.trackInstanceRequests(task);
|
|
765
|
+
}
|
|
766
|
+
this.activeTaskById.set(task.id, task);
|
|
767
|
+
}
|
|
768
|
+
cleanWorkerAndTask(managedWorker, task, requestId = task?.id) {
|
|
769
|
+
if (managedWorker.requestId === requestId) managedWorker.requestId = void 0;
|
|
770
|
+
if (managedWorker.pendingSetupRequestId === requestId) managedWorker.pendingSetupRequestId = void 0;
|
|
771
|
+
if (task != null) {
|
|
772
|
+
if (isRenderTask(task)) {
|
|
773
|
+
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
|
|
774
|
+
this.clearInstanceRequests(task);
|
|
775
|
+
this.clearHighlightKey(task);
|
|
776
|
+
}
|
|
777
|
+
this.activeTaskById.delete(task.id);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
executeTask(managedWorker, task) {
|
|
781
|
+
if (shouldSyncCustomExtensions(task.request)) this.maybeAttachCustomExtensions(managedWorker, task.request);
|
|
782
|
+
if (!this.activeTaskById.has(task.id)) this.assignWorkerToTask(task, managedWorker);
|
|
783
|
+
try {
|
|
784
|
+
if (task.type === "file") task.cacheKeyAtDispatch = task.request.file.cacheKey;
|
|
785
|
+
else if (task.type === "diff") task.cacheKeyAtDispatch = task.request.diff.cacheKey;
|
|
786
|
+
managedWorker.worker.postMessage(task.request);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
console.error("Failed to post message to worker:", error);
|
|
789
|
+
if (isRenderTask(task)) {
|
|
790
|
+
this.notifyHighlightError(task, error);
|
|
791
|
+
this.rejectRenderTaskCallbacks(task, normalizeWorkerError(error));
|
|
792
|
+
} else if ("reject" in task) task.reject(error);
|
|
793
|
+
this.cleanWorkerAndTask(managedWorker, task);
|
|
794
|
+
if (this.queuedTasks.length > 0) this.queueDrain();
|
|
795
|
+
}
|
|
796
|
+
this.queueBroadcastStateChanges();
|
|
797
|
+
}
|
|
798
|
+
maybeAttachCustomExtensions(managedWorker, request) {
|
|
799
|
+
if (request.customExtensionsVersion != null) return;
|
|
800
|
+
const version = getCustomExtensionsVersion();
|
|
801
|
+
if (managedWorker.customExtensionsVersion >= version) return;
|
|
802
|
+
request.customExtensionsVersion = version;
|
|
803
|
+
request.customExtensionMap = getCustomExtensionsMap();
|
|
804
|
+
}
|
|
805
|
+
syncCustomExtensionVersion(managedWorker, request) {
|
|
806
|
+
if (request.customExtensionsVersion == null) return;
|
|
807
|
+
managedWorker.customExtensionsVersion = request.customExtensionsVersion;
|
|
808
|
+
}
|
|
809
|
+
getAvailableWorker(langs) {
|
|
810
|
+
let worker;
|
|
811
|
+
for (const managedWorker of this.workers) {
|
|
812
|
+
if (managedWorker.requestId != null || managedWorker.pendingSetupRequestId != null || !managedWorker.initialized) continue;
|
|
813
|
+
worker = managedWorker;
|
|
814
|
+
if (langs.length === 0) break;
|
|
815
|
+
let hasEveryLang = true;
|
|
816
|
+
for (const lang of langs) if (!managedWorker.langs.has(lang)) {
|
|
817
|
+
hasEveryLang = false;
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
if (hasEveryLang) break;
|
|
821
|
+
}
|
|
822
|
+
return worker;
|
|
823
|
+
}
|
|
824
|
+
getFileHighlightKey(file) {
|
|
825
|
+
if (file.cacheKey == null) return;
|
|
826
|
+
return `file:${file.cacheKey}:${this.renderOptionsVersion}`;
|
|
827
|
+
}
|
|
828
|
+
getDiffHighlightKey(diff) {
|
|
829
|
+
if (diff.cacheKey == null) return;
|
|
830
|
+
return `diff:${diff.cacheKey}:${this.renderOptionsVersion}`;
|
|
831
|
+
}
|
|
832
|
+
getHighlightKeyForRequest(request) {
|
|
833
|
+
switch (request.type) {
|
|
834
|
+
case "file": return this.getFileHighlightKey(request.file);
|
|
835
|
+
case "diff": return this.getDiffHighlightKey(request.diff);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
hasActiveRequest(task) {
|
|
839
|
+
for (const instance of getInstances(task)) if (this.activeRequestByInstance.has(instance)) return true;
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
addInstanceToTask(task, instance) {
|
|
843
|
+
if (task.type === "file") task.instances.add(instance);
|
|
844
|
+
else task.instances.add(instance);
|
|
845
|
+
if (this.activeTaskById.has(task.id)) this.activeRequestByInstance.set(instance, task.id);
|
|
846
|
+
else this.queuedTaskByInstance.set(instance, task);
|
|
847
|
+
}
|
|
848
|
+
detachInstanceFromQueuedTasks(instance, exceptTask) {
|
|
849
|
+
const task = this.queuedTaskByInstance.get(instance);
|
|
850
|
+
if (task == null || task === exceptTask) return;
|
|
851
|
+
this.queuedTaskByInstance.delete(instance);
|
|
852
|
+
this.detachInstanceFromRenderTask(task, instance);
|
|
853
|
+
if (!task.primeCache && task.instances.size === 0) this.removeQueuedTask(task);
|
|
854
|
+
}
|
|
855
|
+
detachInstanceFromRenderTask(task, instance) {
|
|
856
|
+
if (task.type === "file") task.instances.delete(instance);
|
|
857
|
+
else task.instances.delete(instance);
|
|
858
|
+
}
|
|
859
|
+
removeQueuedTask(task) {
|
|
860
|
+
const index = this.queuedTasks.indexOf(task);
|
|
861
|
+
if (index !== -1) this.queuedTasks.splice(index, 1);
|
|
862
|
+
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
|
|
863
|
+
this.clearQueuedInstanceRequests(task);
|
|
864
|
+
this.clearHighlightKey(task);
|
|
865
|
+
}
|
|
866
|
+
removeActiveTask(task) {
|
|
867
|
+
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
|
|
868
|
+
this.clearInstanceRequests(task);
|
|
869
|
+
this.clearHighlightKey(task);
|
|
870
|
+
this.activeTaskById.delete(task.id);
|
|
871
|
+
}
|
|
872
|
+
createRenderTaskCallbacks(task) {
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
task.callbacks.add({
|
|
875
|
+
resolve,
|
|
876
|
+
reject
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
resolveRenderTaskCallbacks(task) {
|
|
881
|
+
for (const callbacks of task.callbacks) callbacks.resolve();
|
|
882
|
+
task.callbacks.clear();
|
|
883
|
+
}
|
|
884
|
+
rejectRenderTaskCallbacks(task, error) {
|
|
885
|
+
for (const callbacks of task.callbacks) callbacks.reject(error);
|
|
886
|
+
task.callbacks.clear();
|
|
887
|
+
}
|
|
888
|
+
invalidateRenderTasks() {
|
|
889
|
+
for (let index = this.queuedTasks.length - 1; index >= 0; index--) {
|
|
890
|
+
const task = this.queuedTasks[index];
|
|
891
|
+
if (task.renderOptionsVersion !== this.renderOptionsVersion) this.removeQueuedTask(task);
|
|
892
|
+
}
|
|
893
|
+
for (const task of Array.from(this.activeTaskById.values())) if (isRenderTask(task) && task.renderOptionsVersion !== this.renderOptionsVersion) this.removeActiveTask(task);
|
|
894
|
+
}
|
|
895
|
+
clearQueuedInstanceRequests(task) {
|
|
896
|
+
for (const instance of getInstances(task)) if (this.queuedTaskByInstance.get(instance) === task) this.queuedTaskByInstance.delete(instance);
|
|
897
|
+
}
|
|
898
|
+
clearHighlightKey(task) {
|
|
899
|
+
if (task.highlightKey != null && this.taskByHighlightKey.get(task.highlightKey) === task) this.taskByHighlightKey.delete(task.highlightKey);
|
|
900
|
+
}
|
|
901
|
+
trackInstanceRequests(task) {
|
|
902
|
+
for (const instance of getInstances(task)) this.activeRequestByInstance.set(instance, task.id);
|
|
903
|
+
}
|
|
904
|
+
clearInstanceRequests(task) {
|
|
905
|
+
for (const instance of getInstances(task)) if (this.activeRequestByInstance.get(instance) === task.id) this.activeRequestByInstance.delete(instance);
|
|
906
|
+
}
|
|
907
|
+
notifyFileInstances(task, result, options) {
|
|
908
|
+
for (const instance of task.instances) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightSuccess(task.request.file, result, options);
|
|
909
|
+
}
|
|
910
|
+
notifyDiffInstances(task, result, options) {
|
|
911
|
+
for (const instance of task.instances) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightSuccess(task.request.diff, result, options);
|
|
912
|
+
}
|
|
913
|
+
notifyHighlightError(task, error) {
|
|
914
|
+
for (const instance of getInstances(task)) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightError(error);
|
|
915
|
+
}
|
|
916
|
+
hasMatchingFileInstanceTask(instance, file) {
|
|
917
|
+
for (const task of this.iterateRenderTasks()) if (task.type === "file" && this.isCurrentRenderTask(task) && task.instances.has(instance) && areFilesEqual(file, task.request.file)) return true;
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
hasMatchingDiffInstanceTask(instance, diff) {
|
|
921
|
+
for (const task of this.iterateRenderTasks()) if (task.type === "diff" && this.isCurrentRenderTask(task) && task.instances.has(instance) && areDiffTargetsEqual(task.request.diff, diff)) return true;
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
getTaskByHighlightKey(highlightKey) {
|
|
925
|
+
const task = this.taskByHighlightKey.get(highlightKey);
|
|
926
|
+
return task != null && this.isCurrentRenderTask(task) ? task : void 0;
|
|
927
|
+
}
|
|
928
|
+
isCurrentRenderTask(task) {
|
|
929
|
+
return task.renderOptionsVersion === this.renderOptionsVersion;
|
|
930
|
+
}
|
|
931
|
+
*iterateRenderTasks() {
|
|
932
|
+
for (const task of this.queuedTasks) yield task;
|
|
933
|
+
for (const task of this.activeTaskById.values()) if (isRenderTask(task)) yield task;
|
|
934
|
+
}
|
|
935
|
+
generateRequestId() {
|
|
936
|
+
return `req_${++this.nextRequestId}`;
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
function shouldSyncCustomExtensions(request) {
|
|
940
|
+
return request.type === "initialize" || request.type === "file" || request.type === "diff";
|
|
941
|
+
}
|
|
942
|
+
function getLangsFromTask(task) {
|
|
943
|
+
const langs = /* @__PURE__ */ new Set();
|
|
944
|
+
if (task.type === "initialize" || task.type === "set-render-options") return [];
|
|
945
|
+
switch (task.type) {
|
|
946
|
+
case "file":
|
|
947
|
+
langs.add(task.request.file.lang ?? getFiletypeFromFileName(task.request.file.name));
|
|
948
|
+
break;
|
|
949
|
+
case "diff":
|
|
950
|
+
langs.add(task.request.diff.lang ?? getFiletypeFromFileName(task.request.diff.name));
|
|
951
|
+
langs.add(task.request.diff.lang ?? getFiletypeFromFileName(task.request.diff.prevName ?? "-"));
|
|
952
|
+
}
|
|
953
|
+
langs.delete("text");
|
|
954
|
+
return Array.from(langs);
|
|
955
|
+
}
|
|
956
|
+
function isRenderTask(task) {
|
|
957
|
+
return task?.type === "file" || task?.type === "diff";
|
|
958
|
+
}
|
|
959
|
+
function getInstances(task) {
|
|
960
|
+
return task.instances;
|
|
961
|
+
}
|
|
962
|
+
function normalizeWorkerError(error) {
|
|
963
|
+
if (error instanceof Error) return error;
|
|
964
|
+
if (error != null && typeof error === "object") {
|
|
965
|
+
if ("error" in error && error.error instanceof Error) return error.error;
|
|
966
|
+
if ("message" in error && typeof error.message === "string") return new Error(error.message);
|
|
967
|
+
}
|
|
968
|
+
return new Error(String(error));
|
|
969
|
+
}
|
|
970
|
+
function errorFromWorkerErrorEvent(event) {
|
|
971
|
+
const cause = "error" in event ? event.error : void 0;
|
|
972
|
+
const message = "message" in event && typeof event.message === "string" && event.message !== "" ? event.message : cause instanceof Error ? cause.message : "the worker script failed to load (the error event carries no message)";
|
|
973
|
+
return new WorkerPoolWorkerError(`WorkerPoolManager: worker failed: ${message}`, cause instanceof Error ? { cause } : void 0);
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region ../ui/src/lib/files/code-workers.ts
|
|
977
|
+
/**
|
|
978
|
+
* Create one pool per workspace in a browser lifecycle, then lend it to its viewers.
|
|
979
|
+
* @param options - Pool configuration and the owning workspace's lifetime signal.
|
|
980
|
+
* @returns The borrowed manager, observable initialization, and idempotent disposal.
|
|
981
|
+
*/
|
|
982
|
+
async function createCodeWorkerPool(options = {}) {
|
|
983
|
+
let { signal, renderOptions, ...poolOptions } = options;
|
|
984
|
+
signal?.throwIfAborted();
|
|
985
|
+
let workerFactory = options.workerFactory ?? (await import("./worker-factory-C80HGMWf.js")).createCodeWorker;
|
|
986
|
+
signal?.throwIfAborted();
|
|
987
|
+
let pool = new WorkerPoolManager({
|
|
988
|
+
poolSize: 2,
|
|
989
|
+
...poolOptions,
|
|
990
|
+
workerFactory
|
|
991
|
+
}, renderOptions ?? {});
|
|
992
|
+
let disposed = false;
|
|
993
|
+
function dispose() {
|
|
994
|
+
if (disposed) return;
|
|
995
|
+
disposed = true;
|
|
996
|
+
signal?.removeEventListener("abort", dispose);
|
|
997
|
+
pool.terminate();
|
|
998
|
+
}
|
|
999
|
+
signal?.addEventListener("abort", dispose, { once: true });
|
|
1000
|
+
return {
|
|
1001
|
+
pool,
|
|
1002
|
+
ready: pool.initialize().then(() => {
|
|
1003
|
+
if (disposed) pool.terminate();
|
|
1004
|
+
}),
|
|
1005
|
+
dispose
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region app/client/files-request.ts
|
|
1010
|
+
var endpoints = new Set([
|
|
1011
|
+
routes.filesApi,
|
|
1012
|
+
routes.fileSearchApi,
|
|
1013
|
+
routes.fileReviewApi,
|
|
1014
|
+
routes.fileMutationsApi,
|
|
1015
|
+
routes.fileSavesApi,
|
|
1016
|
+
routes.fileReviewActionsApi
|
|
1017
|
+
].flatMap((group) => Object.values(group).map((route) => route.href())));
|
|
1018
|
+
/** A Files request that failed, with the service's error code and the HTTP status. */
|
|
1019
|
+
var FilesRequestError = class extends Error {
|
|
1020
|
+
code;
|
|
1021
|
+
status;
|
|
1022
|
+
constructor(message, code = "request", status = 0) {
|
|
1023
|
+
super(message);
|
|
1024
|
+
this.name = "FilesRequestError";
|
|
1025
|
+
this.code = code;
|
|
1026
|
+
this.status = status;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
/**
|
|
1030
|
+
* Reads one bounded, same-origin Files endpoint with the project capability.
|
|
1031
|
+
* @param url - Relative endpoint from the application's route contract.
|
|
1032
|
+
* @param token - Current project's capability; never sent to previewed pages or redirects.
|
|
1033
|
+
* @param signal - Owner's request lifetime.
|
|
1034
|
+
* @param parse - Domain DTO validator.
|
|
1035
|
+
* @param fetcher - Injectable transport for browser-free boundary tests.
|
|
1036
|
+
* @param action - JSON payload, accepted only by the explicit mutation POST endpoints.
|
|
1037
|
+
* @returns A validated value or explicit request error, never an invented empty success.
|
|
1038
|
+
*/
|
|
1039
|
+
async function requestFiles(url, token, signal, parse, fetcher = fetch, action) {
|
|
1040
|
+
let parsed = new URL(url, "http://bud.invalid");
|
|
1041
|
+
let mutation = parsed.pathname === "/api/files/mutations/prepare" || parsed.pathname === "/api/files/mutations/apply" || parsed.pathname === "/api/files/review-actions/prepare" || parsed.pathname === "/api/files/review-actions/apply" || parsed.pathname === "/api/files/save";
|
|
1042
|
+
if (!url.startsWith("/api/files") || url.startsWith("//") || parsed.origin !== "http://bud.invalid" || parsed.hash || !endpoints.has(parsed.pathname) || (parsed.pathname === "/api/files/refresh" || parsed.pathname === "/api/files/save" || parsed.pathname.startsWith("/api/files/review-actions/") || parsed.pathname.startsWith("/api/files/mutations/")) && parsed.search || mutation !== Boolean(action)) throw new FilesRequestError("Invalid file endpoint.", "input");
|
|
1043
|
+
signal.throwIfAborted();
|
|
1044
|
+
let response = await fetcher(url, {
|
|
1045
|
+
method: mutation || parsed.pathname === "/api/files/refresh" ? "POST" : "GET",
|
|
1046
|
+
headers: {
|
|
1047
|
+
"X-Bud-Token": token,
|
|
1048
|
+
...mutation ? { "Content-Type": "application/json" } : {}
|
|
1049
|
+
},
|
|
1050
|
+
...action ? { body: JSON.stringify(action.body) } : {},
|
|
1051
|
+
credentials: "same-origin",
|
|
1052
|
+
redirect: "error",
|
|
1053
|
+
cache: "no-store",
|
|
1054
|
+
signal
|
|
1055
|
+
});
|
|
1056
|
+
let reader = response.body?.getReader();
|
|
1057
|
+
if (!reader) throw new FilesRequestError("The file service returned an empty response.", "response", response.status);
|
|
1058
|
+
let chunks = [];
|
|
1059
|
+
let size = 0;
|
|
1060
|
+
let cancel = () => {
|
|
1061
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
1062
|
+
};
|
|
1063
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
1064
|
+
try {
|
|
1065
|
+
while (true) {
|
|
1066
|
+
signal.throwIfAborted();
|
|
1067
|
+
let part = await reader.read();
|
|
1068
|
+
signal.throwIfAborted();
|
|
1069
|
+
if (part.done) break;
|
|
1070
|
+
size += part.value.byteLength;
|
|
1071
|
+
if (size > 16777216) throw new FilesRequestError("This file response exceeds the display limit. Narrow the file view and try again.", "limit", response.status);
|
|
1072
|
+
chunks.push(part.value);
|
|
1073
|
+
}
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
await reader.cancel().catch(() => {});
|
|
1076
|
+
throw error;
|
|
1077
|
+
} finally {
|
|
1078
|
+
signal.removeEventListener("abort", cancel);
|
|
1079
|
+
reader.releaseLock();
|
|
1080
|
+
}
|
|
1081
|
+
let bytes = new Uint8Array(size);
|
|
1082
|
+
let offset = 0;
|
|
1083
|
+
for (let chunk of chunks) {
|
|
1084
|
+
bytes.set(chunk, offset);
|
|
1085
|
+
offset += chunk.byteLength;
|
|
1086
|
+
}
|
|
1087
|
+
let value;
|
|
1088
|
+
try {
|
|
1089
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new FilesRequestError("The file service returned invalid data. Refresh and try again.", "response", response.status);
|
|
1092
|
+
}
|
|
1093
|
+
if (!response.ok) throw new FilesRequestError(value && typeof value === "object" && "error" in value && typeof value.error === "string" ? value.error.slice(0, 1e3) : "Could not load files. Try again.", value && typeof value === "object" && "code" in value && typeof value.code === "string" ? value.code.slice(0, 100) : "request", response.status);
|
|
1094
|
+
try {
|
|
1095
|
+
return parse(value);
|
|
1096
|
+
} catch {
|
|
1097
|
+
throw new FilesRequestError("The file service returned unexpected data. Refresh and try again.", "response", response.status);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region app/client/file-mutation-controller.ts
|
|
1102
|
+
/**
|
|
1103
|
+
* Collects the guards of the open views and runs them, in order, around every file action.
|
|
1104
|
+
* @returns The guard set: register a guard for its lifetime, then run them before and after.
|
|
1105
|
+
*/
|
|
1106
|
+
function createFileMutationGuards() {
|
|
1107
|
+
let guards = /* @__PURE__ */ new Set();
|
|
1108
|
+
return {
|
|
1109
|
+
register(guard, signal) {
|
|
1110
|
+
if (signal.aborted) return;
|
|
1111
|
+
guards.add(guard);
|
|
1112
|
+
signal.addEventListener("abort", () => guards.delete(guard), { once: true });
|
|
1113
|
+
},
|
|
1114
|
+
async beforeApply(intent, plan, signal) {
|
|
1115
|
+
for (let guard of guards) {
|
|
1116
|
+
signal.throwIfAborted();
|
|
1117
|
+
let reason = await guard.beforeApply(intent, plan, signal);
|
|
1118
|
+
signal.throwIfAborted();
|
|
1119
|
+
if (reason) throw new Error(reason);
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
async afterApply(intent, result) {
|
|
1123
|
+
for (let guard of guards) await guard.afterApply(intent, result);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function message$4(cause) {
|
|
1128
|
+
return cause instanceof Error ? cause.message : "The file action could not be completed.";
|
|
1129
|
+
}
|
|
1130
|
+
function assertPlan(intent, plan) {
|
|
1131
|
+
if (intent.kind !== plan.kind) throw new Error("The service returned a different file action.");
|
|
1132
|
+
if (intent.kind === "restore") return;
|
|
1133
|
+
let expected = "entries" in intent ? intent.entries.map((item) => [item.path, item.destination]) : intent.kind === "delete" ? intent.paths.map((source) => [source, null]) : [[null, intent.path]];
|
|
1134
|
+
let actual = plan.items.map((item) => [item.source, item.destination]);
|
|
1135
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) throw new Error("The service returned different file paths. Refresh before continuing.");
|
|
1136
|
+
}
|
|
1137
|
+
function assertResult(plan, result) {
|
|
1138
|
+
if (result.id !== plan.id || result.items.length !== plan.items.length || result.items.some((item, index) => item.source !== plan.items[index].source || item.destination !== plan.items[index].destination)) throw new Error("The file result does not match its confirmed plan. Refresh and check Recovery before retrying.");
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Coordinates validated plans without hiding partial results or retrying writes.
|
|
1142
|
+
* @param options - Scoped transport, draft guards and authoritative refresh callbacks.
|
|
1143
|
+
* @returns The dialog state and deliberate action/recovery commands.
|
|
1144
|
+
*/
|
|
1145
|
+
function createFileMutationController(options) {
|
|
1146
|
+
let state = null;
|
|
1147
|
+
let recoveryOpen = false;
|
|
1148
|
+
let recoveries = null;
|
|
1149
|
+
let recoveryLoading = false;
|
|
1150
|
+
let recoveryError = "";
|
|
1151
|
+
let request;
|
|
1152
|
+
let recoveryRequest;
|
|
1153
|
+
let intent = null;
|
|
1154
|
+
let sequence = 0;
|
|
1155
|
+
function changed() {
|
|
1156
|
+
if (!options.signal.aborted) options.onChange();
|
|
1157
|
+
}
|
|
1158
|
+
function busy() {
|
|
1159
|
+
return state?.phase === "preparing" || state?.phase === "applying";
|
|
1160
|
+
}
|
|
1161
|
+
function close() {
|
|
1162
|
+
if (state?.phase === "applying") return;
|
|
1163
|
+
request?.abort();
|
|
1164
|
+
sequence++;
|
|
1165
|
+
state = null;
|
|
1166
|
+
intent = null;
|
|
1167
|
+
changed();
|
|
1168
|
+
}
|
|
1169
|
+
async function prepare(current, applyImmediately) {
|
|
1170
|
+
if (!state || state.phase === "applying") return;
|
|
1171
|
+
request?.abort();
|
|
1172
|
+
let controller = new AbortController();
|
|
1173
|
+
request = controller;
|
|
1174
|
+
let signal = AbortSignal.any([options.signal, controller.signal]);
|
|
1175
|
+
let turn = ++sequence;
|
|
1176
|
+
intent = current;
|
|
1177
|
+
state = {
|
|
1178
|
+
...state,
|
|
1179
|
+
phase: "preparing",
|
|
1180
|
+
plan: null,
|
|
1181
|
+
result: null,
|
|
1182
|
+
error: ""
|
|
1183
|
+
};
|
|
1184
|
+
changed();
|
|
1185
|
+
try {
|
|
1186
|
+
let plan = await options.transport.prepare(current, signal);
|
|
1187
|
+
signal.throwIfAborted();
|
|
1188
|
+
assertPlan(current, plan);
|
|
1189
|
+
if (turn !== sequence || !state) return;
|
|
1190
|
+
state = {
|
|
1191
|
+
...state,
|
|
1192
|
+
plan,
|
|
1193
|
+
phase: "confirming"
|
|
1194
|
+
};
|
|
1195
|
+
changed();
|
|
1196
|
+
if (applyImmediately) await apply();
|
|
1197
|
+
} catch (cause) {
|
|
1198
|
+
if (!signal.aborted && turn === sequence && state) {
|
|
1199
|
+
state = {
|
|
1200
|
+
...state,
|
|
1201
|
+
phase: "editing",
|
|
1202
|
+
error: message$4(cause)
|
|
1203
|
+
};
|
|
1204
|
+
changed();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function apply() {
|
|
1209
|
+
if (!state?.plan || !intent || busy()) return;
|
|
1210
|
+
let current = intent;
|
|
1211
|
+
let plan = state.plan;
|
|
1212
|
+
if (Date.parse(plan.expiresAt) <= Date.now()) {
|
|
1213
|
+
state = {
|
|
1214
|
+
...state,
|
|
1215
|
+
phase: "editing",
|
|
1216
|
+
plan: null,
|
|
1217
|
+
error: "This confirmation expired. Review the current files again."
|
|
1218
|
+
};
|
|
1219
|
+
changed();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
request?.abort();
|
|
1223
|
+
let controller = new AbortController();
|
|
1224
|
+
request = controller;
|
|
1225
|
+
let signal = AbortSignal.any([options.signal, controller.signal]);
|
|
1226
|
+
let turn = ++sequence;
|
|
1227
|
+
let sent = false;
|
|
1228
|
+
state = {
|
|
1229
|
+
...state,
|
|
1230
|
+
phase: "applying",
|
|
1231
|
+
error: ""
|
|
1232
|
+
};
|
|
1233
|
+
changed();
|
|
1234
|
+
try {
|
|
1235
|
+
await options.guards.beforeApply(current, plan, signal);
|
|
1236
|
+
signal.throwIfAborted();
|
|
1237
|
+
sent = true;
|
|
1238
|
+
let result = await options.transport.apply(plan, signal);
|
|
1239
|
+
signal.throwIfAborted();
|
|
1240
|
+
assertResult(plan, result);
|
|
1241
|
+
if (turn !== sequence || !state) return;
|
|
1242
|
+
state = {
|
|
1243
|
+
...state,
|
|
1244
|
+
phase: "result",
|
|
1245
|
+
result
|
|
1246
|
+
};
|
|
1247
|
+
let draftError = "";
|
|
1248
|
+
try {
|
|
1249
|
+
await options.guards.afterApply(current, result);
|
|
1250
|
+
} catch (cause) {
|
|
1251
|
+
draftError = `Files changed, but retained draft metadata could not be updated. ${message$4(cause)}`;
|
|
1252
|
+
}
|
|
1253
|
+
await options.onApplied(current, result);
|
|
1254
|
+
if (turn !== sequence || !state) return;
|
|
1255
|
+
state = {
|
|
1256
|
+
...state,
|
|
1257
|
+
error: draftError
|
|
1258
|
+
};
|
|
1259
|
+
if (result.status === "applied" && !draftError && !["delete", "restore"].includes(current.kind)) {
|
|
1260
|
+
state = null;
|
|
1261
|
+
intent = null;
|
|
1262
|
+
}
|
|
1263
|
+
changed();
|
|
1264
|
+
} catch (cause) {
|
|
1265
|
+
if (signal.aborted || turn !== sequence || !state) return;
|
|
1266
|
+
state = {
|
|
1267
|
+
...state,
|
|
1268
|
+
phase: "editing",
|
|
1269
|
+
plan: null,
|
|
1270
|
+
error: message$4(cause)
|
|
1271
|
+
};
|
|
1272
|
+
if (sent) {
|
|
1273
|
+
await options.onUncertain().catch(() => {});
|
|
1274
|
+
if (state && turn === sequence) state = {
|
|
1275
|
+
...state,
|
|
1276
|
+
error: `${message$4(cause)} Refresh and check Recovery before retrying.`
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
changed();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function open(next, runImmediately = false) {
|
|
1283
|
+
if (busy()) return;
|
|
1284
|
+
request?.abort();
|
|
1285
|
+
recoveryRequest?.abort();
|
|
1286
|
+
sequence++;
|
|
1287
|
+
recoveryOpen = false;
|
|
1288
|
+
intent = null;
|
|
1289
|
+
let destination = "";
|
|
1290
|
+
let error = "";
|
|
1291
|
+
try {
|
|
1292
|
+
destination = defaultFileDestination(next, options.index());
|
|
1293
|
+
} catch (cause) {
|
|
1294
|
+
error = message$4(cause);
|
|
1295
|
+
}
|
|
1296
|
+
state = {
|
|
1297
|
+
request: next,
|
|
1298
|
+
destination,
|
|
1299
|
+
phase: "editing",
|
|
1300
|
+
plan: null,
|
|
1301
|
+
result: null,
|
|
1302
|
+
error
|
|
1303
|
+
};
|
|
1304
|
+
changed();
|
|
1305
|
+
if (!error && (next.action === "delete" || next.action === "restore" || runImmediately)) submit();
|
|
1306
|
+
}
|
|
1307
|
+
async function submit() {
|
|
1308
|
+
if (!state || busy()) return;
|
|
1309
|
+
if (state.phase === "confirming") {
|
|
1310
|
+
await apply();
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
try {
|
|
1314
|
+
let next = state.result && intent ? remainingMutationIntent(intent, state.result) : fileActionIntent(state.request, state.destination, options.index());
|
|
1315
|
+
if (!next) return;
|
|
1316
|
+
await prepare(next, !["delete", "restore"].includes(next.kind));
|
|
1317
|
+
} catch (cause) {
|
|
1318
|
+
if (state) state = {
|
|
1319
|
+
...state,
|
|
1320
|
+
error: message$4(cause),
|
|
1321
|
+
phase: "editing"
|
|
1322
|
+
};
|
|
1323
|
+
changed();
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function loadRecoveries() {
|
|
1327
|
+
recoveryRequest?.abort();
|
|
1328
|
+
let controller = new AbortController();
|
|
1329
|
+
recoveryRequest = controller;
|
|
1330
|
+
let signal = AbortSignal.any([options.signal, controller.signal]);
|
|
1331
|
+
recoveryLoading = true;
|
|
1332
|
+
recoveryError = "";
|
|
1333
|
+
changed();
|
|
1334
|
+
try {
|
|
1335
|
+
let next = await options.transport.recoveries(signal);
|
|
1336
|
+
if (!signal.aborted) recoveries = next;
|
|
1337
|
+
} catch (cause) {
|
|
1338
|
+
if (!signal.aborted) recoveryError = message$4(cause);
|
|
1339
|
+
} finally {
|
|
1340
|
+
if (recoveryRequest === controller) {
|
|
1341
|
+
recoveryLoading = false;
|
|
1342
|
+
changed();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
options.signal.addEventListener("abort", () => {
|
|
1347
|
+
request?.abort();
|
|
1348
|
+
recoveryRequest?.abort();
|
|
1349
|
+
}, { once: true });
|
|
1350
|
+
return {
|
|
1351
|
+
get state() {
|
|
1352
|
+
return state;
|
|
1353
|
+
},
|
|
1354
|
+
get busy() {
|
|
1355
|
+
return busy();
|
|
1356
|
+
},
|
|
1357
|
+
get recovery() {
|
|
1358
|
+
return {
|
|
1359
|
+
open: recoveryOpen,
|
|
1360
|
+
recoveries,
|
|
1361
|
+
loading: recoveryLoading,
|
|
1362
|
+
error: recoveryError
|
|
1363
|
+
};
|
|
1364
|
+
},
|
|
1365
|
+
open,
|
|
1366
|
+
close,
|
|
1367
|
+
submit,
|
|
1368
|
+
destination(value) {
|
|
1369
|
+
if (!state || busy()) return;
|
|
1370
|
+
intent = null;
|
|
1371
|
+
state = {
|
|
1372
|
+
...state,
|
|
1373
|
+
destination: value,
|
|
1374
|
+
plan: null,
|
|
1375
|
+
result: null,
|
|
1376
|
+
phase: "editing",
|
|
1377
|
+
error: ""
|
|
1378
|
+
};
|
|
1379
|
+
changed();
|
|
1380
|
+
},
|
|
1381
|
+
recoveries() {
|
|
1382
|
+
if (busy()) return;
|
|
1383
|
+
close();
|
|
1384
|
+
recoveryOpen = true;
|
|
1385
|
+
loadRecoveries();
|
|
1386
|
+
changed();
|
|
1387
|
+
},
|
|
1388
|
+
refreshRecoveries: loadRecoveries,
|
|
1389
|
+
closeRecoveries() {
|
|
1390
|
+
recoveryRequest?.abort();
|
|
1391
|
+
recoveryOpen = false;
|
|
1392
|
+
changed();
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region app/client/project-file-review-data.ts
|
|
1398
|
+
/**
|
|
1399
|
+
* Writes a review comparison as URL parameters.
|
|
1400
|
+
* @param comparison - The working tree, the staged changes or two commits.
|
|
1401
|
+
* @returns The parameters.
|
|
1402
|
+
*/
|
|
1403
|
+
function comparisonQuery(comparison) {
|
|
1404
|
+
return new URLSearchParams(comparison.mode === "commits" ? {
|
|
1405
|
+
mode: comparison.mode,
|
|
1406
|
+
base: comparison.base,
|
|
1407
|
+
target: comparison.target
|
|
1408
|
+
} : { mode: comparison.mode });
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Reads a bounded page with at most three in-flight diffs and a shared cancellation signal.
|
|
1412
|
+
* @param snapshot - Pinned comparison metadata returned by the server.
|
|
1413
|
+
* @param paths - A bounded page of paths belonging to that snapshot.
|
|
1414
|
+
* @param token - Current project capability.
|
|
1415
|
+
* @param signal - Owning selection/request lifetime.
|
|
1416
|
+
* @param fetcher - Injectable transport for deterministic concurrency and stale-data checks.
|
|
1417
|
+
* @returns Exact source snapshots plus explicit per-file failures.
|
|
1418
|
+
*/
|
|
1419
|
+
async function readReviewPage(snapshot, paths, token, signal, fetcher = fetch) {
|
|
1420
|
+
if (paths.length > 20 || paths.some((path) => !snapshot.entries.some((entry) => entry.path === path))) throw new FilesRequestError("The selected files are outside this comparison page.", "input");
|
|
1421
|
+
let result = /* @__PURE__ */ new Map();
|
|
1422
|
+
let failures = {};
|
|
1423
|
+
let cursor = 0;
|
|
1424
|
+
let retainedBytes = 0;
|
|
1425
|
+
let controller = new AbortController();
|
|
1426
|
+
let lifetime = AbortSignal.any([signal, controller.signal]);
|
|
1427
|
+
let fatal;
|
|
1428
|
+
async function worker() {
|
|
1429
|
+
try {
|
|
1430
|
+
while (cursor < paths.length) {
|
|
1431
|
+
lifetime.throwIfAborted();
|
|
1432
|
+
let path = paths[cursor++];
|
|
1433
|
+
let query = comparisonQuery(snapshot.comparison);
|
|
1434
|
+
query.set("path", path);
|
|
1435
|
+
query.set("expectedRevision", snapshot.revision);
|
|
1436
|
+
try {
|
|
1437
|
+
let diff = await requestFiles(`${routes.fileReviewApi.diff.href()}?${query}`, token, lifetime, parseReviewDiff, fetcher);
|
|
1438
|
+
if (diff.entry.path !== path || diff.metadataRevision !== snapshot.revision || JSON.stringify(diff.comparison) !== JSON.stringify(snapshot.comparison)) throw new FilesRequestError("The comparison changed while reading. Refresh before continuing.", "changed", 409);
|
|
1439
|
+
let bytes = [
|
|
1440
|
+
diff.oldFile,
|
|
1441
|
+
diff.newFile,
|
|
1442
|
+
diff.conflict?.base,
|
|
1443
|
+
diff.conflict?.ours,
|
|
1444
|
+
diff.conflict?.theirs
|
|
1445
|
+
].reduce((total, file) => total + (file?.contents?.length ?? 0) * 2, 0);
|
|
1446
|
+
if (retainedBytes + bytes > 33554432) failures[path] = "This page reached its memory limit. Open this file individually.";
|
|
1447
|
+
else {
|
|
1448
|
+
retainedBytes += bytes;
|
|
1449
|
+
result.set(path, diff);
|
|
1450
|
+
}
|
|
1451
|
+
} catch (cause) {
|
|
1452
|
+
if (lifetime.aborted || cause instanceof FilesRequestError && (cause.status === 409 || cause.status === 401 || cause.status === 403)) throw cause;
|
|
1453
|
+
failures[path] = cause instanceof Error ? cause.message : "Could not read this comparison.";
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
} catch (cause) {
|
|
1457
|
+
fatal ??= cause;
|
|
1458
|
+
controller.abort(cause);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
await Promise.all(Array.from({ length: Math.min(3, paths.length) }, worker));
|
|
1462
|
+
if (fatal) throw fatal;
|
|
1463
|
+
signal.throwIfAborted();
|
|
1464
|
+
return {
|
|
1465
|
+
diffs: paths.flatMap((path) => result.get(path) ?? []),
|
|
1466
|
+
failures
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region app/client/file-drafts.ts
|
|
1471
|
+
/** Drafts kept as UI preferences, which stay in this browser. */
|
|
1472
|
+
var preferenceDraftStorage = {
|
|
1473
|
+
getItem: (key) => {
|
|
1474
|
+
let value = readPreference(key);
|
|
1475
|
+
return typeof value === "string" ? value : null;
|
|
1476
|
+
},
|
|
1477
|
+
setItem: (key, value) => writePreference(key, value)
|
|
1478
|
+
};
|
|
1479
|
+
var maxDrafts = 16;
|
|
1480
|
+
var maxDraftBytes = maxDrafts * maxFileSaveBytes * 2;
|
|
1481
|
+
var maxStoredCharacters = 3145728;
|
|
1482
|
+
var encoder = new TextEncoder();
|
|
1483
|
+
/**
|
|
1484
|
+
* Whether a draft differs from the contents it was made against.
|
|
1485
|
+
* @param draft - The draft.
|
|
1486
|
+
* @returns True when it has unsaved changes.
|
|
1487
|
+
*/
|
|
1488
|
+
function draftDirty(draft) {
|
|
1489
|
+
return draft.contents !== draft.baseContents;
|
|
1490
|
+
}
|
|
1491
|
+
function bytes(draft) {
|
|
1492
|
+
return encoder.encode(draft.contents).length + encoder.encode(draft.baseContents).length;
|
|
1493
|
+
}
|
|
1494
|
+
function within(path, parent) {
|
|
1495
|
+
return path === parent || path.startsWith(`${parent}/`);
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Strict project-bound draft records never authorize writes without a live server revision.
|
|
1499
|
+
* @param value - Serialized local browser record.
|
|
1500
|
+
* @param projectId - Expected connected project identity.
|
|
1501
|
+
* @returns Validated draft entries.
|
|
1502
|
+
*/
|
|
1503
|
+
function parseDrafts(value, projectId) {
|
|
1504
|
+
if (!value) return [];
|
|
1505
|
+
if (value.length > maxStoredCharacters) throw new Error("The local draft record exceeds its limit.");
|
|
1506
|
+
let parsed = JSON.parse(value);
|
|
1507
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid local draft record.");
|
|
1508
|
+
let record = parsed;
|
|
1509
|
+
if (record.version !== 1 || record.projectId !== projectId || !Array.isArray(record.drafts) || record.drafts.length > maxDrafts || Object.keys(record).some((key) => ![
|
|
1510
|
+
"version",
|
|
1511
|
+
"projectId",
|
|
1512
|
+
"drafts"
|
|
1513
|
+
].includes(key))) throw new Error("Invalid local draft record.");
|
|
1514
|
+
let seen = /* @__PURE__ */ new Set();
|
|
1515
|
+
let total = 0;
|
|
1516
|
+
return record.drafts.map((input) => {
|
|
1517
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Invalid local draft.");
|
|
1518
|
+
let item = input;
|
|
1519
|
+
if (Object.keys(item).some((key) => ![
|
|
1520
|
+
"id",
|
|
1521
|
+
"path",
|
|
1522
|
+
"baseRevision",
|
|
1523
|
+
"baseContents",
|
|
1524
|
+
"contents",
|
|
1525
|
+
"updatedAt"
|
|
1526
|
+
].includes(key)) || typeof item.id !== "string" || !/^[a-zA-Z0-9-]{1,80}$/.test(item.id) || !Number.isSafeInteger(item.updatedAt) || Number(item.updatedAt) < 0) throw new Error("Invalid local draft.");
|
|
1527
|
+
parseFileSaveRequest({
|
|
1528
|
+
path: item.path,
|
|
1529
|
+
expectedRevision: item.baseRevision,
|
|
1530
|
+
contents: item.baseContents
|
|
1531
|
+
});
|
|
1532
|
+
parseFileSaveRequest({
|
|
1533
|
+
path: item.path,
|
|
1534
|
+
expectedRevision: item.baseRevision,
|
|
1535
|
+
contents: item.contents
|
|
1536
|
+
});
|
|
1537
|
+
let draft = item;
|
|
1538
|
+
if (seen.has(draft.path)) throw new Error("Duplicate local draft path.");
|
|
1539
|
+
seen.add(draft.path);
|
|
1540
|
+
total += bytes(draft);
|
|
1541
|
+
if (total > maxDraftBytes) throw new Error("The local drafts exceed their memory limit.");
|
|
1542
|
+
return { ...draft };
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Bounded memory retains drafts even when browser storage fails. Nothing dirty is silently evicted.
|
|
1547
|
+
* @param projectId - Connected project identity.
|
|
1548
|
+
* @param storage - Optional injectable browser persistence.
|
|
1549
|
+
* @param now - Clock for draft modification times.
|
|
1550
|
+
* @returns Draft storage and explicit mutation guards.
|
|
1551
|
+
*/
|
|
1552
|
+
function createFileDrafts(projectId, storage, now = Date.now) {
|
|
1553
|
+
let drafts = /* @__PURE__ */ new Map();
|
|
1554
|
+
let warning = "";
|
|
1555
|
+
let key = `bud:drafts:${projectId}:v1`;
|
|
1556
|
+
try {
|
|
1557
|
+
for (let draft of parseDrafts(storage?.getItem(key) ?? null, projectId)) drafts.set(draft.path, draft);
|
|
1558
|
+
} catch {
|
|
1559
|
+
warning = "Local drafts could not be read. The stored record has been left untouched.";
|
|
1560
|
+
}
|
|
1561
|
+
let unreadable = Boolean(warning);
|
|
1562
|
+
function persist() {
|
|
1563
|
+
if (unreadable) return false;
|
|
1564
|
+
let record = JSON.stringify({
|
|
1565
|
+
version: 1,
|
|
1566
|
+
projectId,
|
|
1567
|
+
drafts: [...drafts.values()].filter(draftDirty)
|
|
1568
|
+
});
|
|
1569
|
+
try {
|
|
1570
|
+
if (!storage || record.length > maxStoredCharacters) throw new Error("Storage unavailable");
|
|
1571
|
+
storage.setItem(key, record);
|
|
1572
|
+
warning = "";
|
|
1573
|
+
return true;
|
|
1574
|
+
} catch {
|
|
1575
|
+
warning = "This draft is kept in this tab only. Browser storage is unavailable or full; save or download it before closing the tab.";
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function put(draft) {
|
|
1580
|
+
if (encoder.encode(draft.contents).length > 2097152 || !draft.contents.isWellFormed() || draft.contents.includes("\0")) throw new Error("This draft exceeds the text editor limit or contains unsupported text. Undo the last edit before saving.");
|
|
1581
|
+
let clean = [...drafts.values()].filter((item) => item.path !== draft.path && !draftDirty(item));
|
|
1582
|
+
for (let item of clean) drafts.delete(item.path);
|
|
1583
|
+
let others = [...drafts.values()].filter((item) => item.path !== draft.path);
|
|
1584
|
+
if (others.length >= maxDrafts || others.reduce((sum, item) => sum + bytes(item), bytes(draft)) > maxDraftBytes) throw new Error("Save or discard another draft before editing more files. Your existing drafts have been kept.");
|
|
1585
|
+
drafts.set(draft.path, draft);
|
|
1586
|
+
persist();
|
|
1587
|
+
return draft;
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
get warning() {
|
|
1591
|
+
return warning;
|
|
1592
|
+
},
|
|
1593
|
+
get dirty() {
|
|
1594
|
+
return [...drafts.values()].filter(draftDirty);
|
|
1595
|
+
},
|
|
1596
|
+
get(path) {
|
|
1597
|
+
return drafts.get(path);
|
|
1598
|
+
},
|
|
1599
|
+
open(path, revision, contents) {
|
|
1600
|
+
let previous = drafts.get(path);
|
|
1601
|
+
if (previous && draftDirty(previous)) return previous;
|
|
1602
|
+
parseFileSaveRequest({
|
|
1603
|
+
path,
|
|
1604
|
+
expectedRevision: revision,
|
|
1605
|
+
contents
|
|
1606
|
+
});
|
|
1607
|
+
return put({
|
|
1608
|
+
id: crypto.randomUUID(),
|
|
1609
|
+
path,
|
|
1610
|
+
baseRevision: revision,
|
|
1611
|
+
baseContents: contents,
|
|
1612
|
+
contents,
|
|
1613
|
+
updatedAt: now()
|
|
1614
|
+
});
|
|
1615
|
+
},
|
|
1616
|
+
change(path, contents) {
|
|
1617
|
+
let draft = drafts.get(path);
|
|
1618
|
+
if (!draft) throw new Error("This draft is no longer open.");
|
|
1619
|
+
return put({
|
|
1620
|
+
...draft,
|
|
1621
|
+
contents,
|
|
1622
|
+
updatedAt: now()
|
|
1623
|
+
});
|
|
1624
|
+
},
|
|
1625
|
+
acknowledge(path, savedContents, revision) {
|
|
1626
|
+
let draft = drafts.get(path);
|
|
1627
|
+
if (!draft) return;
|
|
1628
|
+
return put({
|
|
1629
|
+
...draft,
|
|
1630
|
+
baseRevision: revision,
|
|
1631
|
+
baseContents: savedContents,
|
|
1632
|
+
updatedAt: now()
|
|
1633
|
+
});
|
|
1634
|
+
},
|
|
1635
|
+
rebase(path, revision, contents) {
|
|
1636
|
+
let draft = drafts.get(path);
|
|
1637
|
+
if (!draft) throw new Error("This draft is no longer open.");
|
|
1638
|
+
return put({
|
|
1639
|
+
...draft,
|
|
1640
|
+
id: crypto.randomUUID(),
|
|
1641
|
+
baseRevision: revision,
|
|
1642
|
+
baseContents: contents,
|
|
1643
|
+
updatedAt: now()
|
|
1644
|
+
});
|
|
1645
|
+
},
|
|
1646
|
+
discard(path) {
|
|
1647
|
+
drafts.delete(path);
|
|
1648
|
+
persist();
|
|
1649
|
+
},
|
|
1650
|
+
persist,
|
|
1651
|
+
beforeMutation(intent, plan) {
|
|
1652
|
+
let dirty = [...drafts.values()].filter(draftDirty);
|
|
1653
|
+
if (intent.kind === "delete" && dirty.some((draft) => intent.paths.some((path) => within(draft.path, path)))) return "Save or discard the unsaved drafts in this selection before deleting it.";
|
|
1654
|
+
if (intent.kind === "restore" && dirty.some((draft) => plan.items.some((item) => item.destination && within(draft.path, item.destination)))) return "Save or discard the unsaved drafts at these original paths before restoring them.";
|
|
1655
|
+
if (intent.kind === "move") {
|
|
1656
|
+
if (dirty.some((draft) => intent.entries.some((entry) => within(draft.path, entry.destination) && !within(draft.path, entry.path)))) return "A destination has an unsaved draft. Save or discard it before moving files here.";
|
|
1657
|
+
if (dirty.some((draft) => intent.entries.some((entry) => within(draft.path, entry.path))) && !persist()) return "Save or download your drafts before moving these files; local draft storage is unavailable.";
|
|
1658
|
+
}
|
|
1659
|
+
return null;
|
|
1660
|
+
},
|
|
1661
|
+
afterMutation(intent, result) {
|
|
1662
|
+
if (intent.kind !== "move") return;
|
|
1663
|
+
let changes = result.items.filter((item) => item.status === "applied" && item.source && item.destination);
|
|
1664
|
+
let moved = [];
|
|
1665
|
+
let next = new Map(drafts);
|
|
1666
|
+
for (let draft of drafts.values()) {
|
|
1667
|
+
let item = changes.find((item) => within(draft.path, item.source));
|
|
1668
|
+
if (!item) continue;
|
|
1669
|
+
let path = parseFilePath(item.destination + draft.path.slice(item.source.length));
|
|
1670
|
+
moved.push({
|
|
1671
|
+
draft,
|
|
1672
|
+
path
|
|
1673
|
+
});
|
|
1674
|
+
next.delete(draft.path);
|
|
1675
|
+
}
|
|
1676
|
+
for (let { draft, path } of moved) {
|
|
1677
|
+
let destination = next.get(path);
|
|
1678
|
+
if (destination && draftDirty(destination)) throw new Error("The destination acquired an unsaved draft while the move was running. Both drafts remain available at their original paths; download or review them before continuing.");
|
|
1679
|
+
next.set(path, {
|
|
1680
|
+
...draft,
|
|
1681
|
+
id: crypto.randomUUID(),
|
|
1682
|
+
path
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
drafts = next;
|
|
1686
|
+
persist();
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
var openProjects = /* @__PURE__ */ new Map();
|
|
1691
|
+
var unloadWarningInstalled = false;
|
|
1692
|
+
/**
|
|
1693
|
+
* Mount-time only: retain unsaved data through client route replacement, including storage
|
|
1694
|
+
* failures.
|
|
1695
|
+
* @param projectId - Connected project identity.
|
|
1696
|
+
* @param storage - Browser persistence for this tab.
|
|
1697
|
+
* @returns The bounded shared draft owner for the project.
|
|
1698
|
+
*/
|
|
1699
|
+
function projectFileDrafts(projectId, storage) {
|
|
1700
|
+
if (!unloadWarningInstalled) {
|
|
1701
|
+
unloadWarningInstalled = true;
|
|
1702
|
+
window.addEventListener("beforeunload", (event) => {
|
|
1703
|
+
if ([...openProjects.values()].some((drafts) => drafts.dirty.length)) {
|
|
1704
|
+
for (let drafts of openProjects.values()) drafts.persist();
|
|
1705
|
+
event.preventDefault();
|
|
1706
|
+
event.returnValue = "";
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
let existing = openProjects.get(projectId);
|
|
1711
|
+
if (existing) return existing;
|
|
1712
|
+
for (let [id, drafts] of openProjects) if (!drafts.dirty.length) openProjects.delete(id);
|
|
1713
|
+
if (openProjects.size >= 4) throw new Error("Save or discard drafts in another project before opening more editors.");
|
|
1714
|
+
let drafts = createFileDrafts(projectId, storage);
|
|
1715
|
+
openProjects.set(projectId, drafts);
|
|
1716
|
+
return drafts;
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region app/client/file-editor-controller.ts
|
|
1720
|
+
function message$3(cause) {
|
|
1721
|
+
return cause instanceof Error ? cause.message : "The file could not be saved. Your draft has been kept.";
|
|
1722
|
+
}
|
|
1723
|
+
function readable(document, path) {
|
|
1724
|
+
if (document.entry.path !== path) throw new Error("The source response belongs to another file.");
|
|
1725
|
+
if (document.contents === null || !document.revision) throw new Error("Only regular UTF-8 text files within the editor size limit can be edited.");
|
|
1726
|
+
return {
|
|
1727
|
+
revision: document.revision,
|
|
1728
|
+
contents: document.contents
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function inside(path, parent) {
|
|
1732
|
+
return path === parent || path.startsWith(`${parent}/`);
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Keeps acknowledged bytes separate from live drafts, including edits made during a save.
|
|
1736
|
+
* @param options - Scoped transport, draft owner and application notifications.
|
|
1737
|
+
* @returns Explicit editor, save and conflict commands.
|
|
1738
|
+
*/
|
|
1739
|
+
function createFileEditorController(options) {
|
|
1740
|
+
let state = null;
|
|
1741
|
+
let opening = false;
|
|
1742
|
+
let requestedPath = null;
|
|
1743
|
+
let openError = "";
|
|
1744
|
+
let request;
|
|
1745
|
+
let saves = /* @__PURE__ */ new Set();
|
|
1746
|
+
let generation = 0;
|
|
1747
|
+
function changed() {
|
|
1748
|
+
if (!options.signal.aborted) options.onChange();
|
|
1749
|
+
}
|
|
1750
|
+
function close() {
|
|
1751
|
+
request?.abort();
|
|
1752
|
+
generation++;
|
|
1753
|
+
state = null;
|
|
1754
|
+
requestedPath = null;
|
|
1755
|
+
opening = false;
|
|
1756
|
+
openError = "";
|
|
1757
|
+
options.drafts.persist();
|
|
1758
|
+
changed();
|
|
1759
|
+
}
|
|
1760
|
+
function closeOtherPath(path) {
|
|
1761
|
+
if (requestedPath !== path) close();
|
|
1762
|
+
}
|
|
1763
|
+
function observe(document) {
|
|
1764
|
+
if (!state || state.draft.path !== document.entry.path || state.saving || state.latest) return;
|
|
1765
|
+
if (document.revision !== state.draft.baseRevision) {
|
|
1766
|
+
state = {
|
|
1767
|
+
...state,
|
|
1768
|
+
conflict: true,
|
|
1769
|
+
error: "This file changed outside the editor. Your draft has been kept. Compare with the latest file before saving."
|
|
1770
|
+
};
|
|
1771
|
+
changed();
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function begin(path, mode = "file", baseline) {
|
|
1775
|
+
request?.abort();
|
|
1776
|
+
let controller = new AbortController();
|
|
1777
|
+
request = controller;
|
|
1778
|
+
let signal = AbortSignal.any([controller.signal, options.signal]);
|
|
1779
|
+
let turn = ++generation;
|
|
1780
|
+
requestedPath = path;
|
|
1781
|
+
opening = true;
|
|
1782
|
+
openError = "";
|
|
1783
|
+
changed();
|
|
1784
|
+
try {
|
|
1785
|
+
let document = await options.read(path, signal);
|
|
1786
|
+
signal.throwIfAborted();
|
|
1787
|
+
let current = readable(document, path);
|
|
1788
|
+
let draft = options.drafts.open(path, current.revision, current.contents);
|
|
1789
|
+
let conflict = draft.baseRevision !== current.revision;
|
|
1790
|
+
let useIndex = mode === "diff" && baseline?.workingRevision === current.revision;
|
|
1791
|
+
state = {
|
|
1792
|
+
key: `${draft.id}:${turn}`,
|
|
1793
|
+
draft,
|
|
1794
|
+
file: {
|
|
1795
|
+
name: path,
|
|
1796
|
+
contents: draft.contents
|
|
1797
|
+
},
|
|
1798
|
+
oldFile: useIndex ? baseline.file : {
|
|
1799
|
+
name: path,
|
|
1800
|
+
contents: draft.baseContents
|
|
1801
|
+
},
|
|
1802
|
+
mode,
|
|
1803
|
+
baselineLabel: useIndex ? "Index" : "Opened version",
|
|
1804
|
+
saving: saves.has(path),
|
|
1805
|
+
conflict,
|
|
1806
|
+
latest: null,
|
|
1807
|
+
error: conflict ? "This recovered draft belongs to an older file version. Compare it with the latest file before saving." : "",
|
|
1808
|
+
status: draftDirty(draft) ? "Recovered unsaved draft." : ""
|
|
1809
|
+
};
|
|
1810
|
+
} catch (cause) {
|
|
1811
|
+
if (!signal.aborted) {
|
|
1812
|
+
let retained = options.drafts.get(path);
|
|
1813
|
+
if (retained && draftDirty(retained)) state = {
|
|
1814
|
+
key: `${retained.id}:recovery:${turn}`,
|
|
1815
|
+
draft: retained,
|
|
1816
|
+
file: {
|
|
1817
|
+
name: path,
|
|
1818
|
+
contents: retained.contents
|
|
1819
|
+
},
|
|
1820
|
+
oldFile: {
|
|
1821
|
+
name: path,
|
|
1822
|
+
contents: retained.baseContents
|
|
1823
|
+
},
|
|
1824
|
+
mode: "file",
|
|
1825
|
+
baselineLabel: "Opened version",
|
|
1826
|
+
saving: saves.has(path),
|
|
1827
|
+
conflict: true,
|
|
1828
|
+
latest: null,
|
|
1829
|
+
error: `The current project file could not be read. ${message$3(cause)}`,
|
|
1830
|
+
status: "Your unsaved draft is available below. Download it, or restore the project file and compare with its latest contents before saving."
|
|
1831
|
+
};
|
|
1832
|
+
else openError = message$3(cause);
|
|
1833
|
+
}
|
|
1834
|
+
} finally {
|
|
1835
|
+
if (turn === generation) {
|
|
1836
|
+
opening = false;
|
|
1837
|
+
changed();
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function change(contents) {
|
|
1842
|
+
if (!state) return;
|
|
1843
|
+
try {
|
|
1844
|
+
let draft = options.drafts.change(state.draft.path, contents);
|
|
1845
|
+
state = {
|
|
1846
|
+
...state,
|
|
1847
|
+
draft,
|
|
1848
|
+
status: "",
|
|
1849
|
+
error: state.conflict ? state.error : ""
|
|
1850
|
+
};
|
|
1851
|
+
} catch (cause) {
|
|
1852
|
+
state = {
|
|
1853
|
+
...state,
|
|
1854
|
+
error: message$3(cause)
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
changed();
|
|
1858
|
+
}
|
|
1859
|
+
async function save() {
|
|
1860
|
+
if (!state || state.saving || state.conflict || state.latest || !draftDirty(state.draft)) return;
|
|
1861
|
+
let draft = state.draft;
|
|
1862
|
+
saves.add(draft.path);
|
|
1863
|
+
state = {
|
|
1864
|
+
...state,
|
|
1865
|
+
saving: true,
|
|
1866
|
+
error: "",
|
|
1867
|
+
status: ""
|
|
1868
|
+
};
|
|
1869
|
+
changed();
|
|
1870
|
+
try {
|
|
1871
|
+
let result = await options.save({
|
|
1872
|
+
path: draft.path,
|
|
1873
|
+
expectedRevision: draft.baseRevision,
|
|
1874
|
+
contents: draft.contents
|
|
1875
|
+
}, options.signal);
|
|
1876
|
+
if (result.path !== draft.path || result.previousRevision !== draft.baseRevision) throw new Error("The save response belongs to a different file revision. Check the latest file before retrying.");
|
|
1877
|
+
let next = options.drafts.acknowledge(draft.path, draft.contents, result.revision);
|
|
1878
|
+
if (state?.draft.path === draft.path && next) {
|
|
1879
|
+
state = {
|
|
1880
|
+
...state,
|
|
1881
|
+
draft: next,
|
|
1882
|
+
status: "",
|
|
1883
|
+
error: ""
|
|
1884
|
+
};
|
|
1885
|
+
if (result.refresh === "failed") toast.error("Saved, but the file browser could not refresh", { description: "Refresh it before continuing." });
|
|
1886
|
+
else if (draftDirty(next)) toast.success("Saved to your project", { description: "Newer edits are still unsaved." });
|
|
1887
|
+
else toast.success("Saved to your project");
|
|
1888
|
+
}
|
|
1889
|
+
options.onSaved();
|
|
1890
|
+
} catch (cause) {
|
|
1891
|
+
if (state?.draft.path === draft.path) state = {
|
|
1892
|
+
...state,
|
|
1893
|
+
error: message$3(cause),
|
|
1894
|
+
conflict: true
|
|
1895
|
+
};
|
|
1896
|
+
} finally {
|
|
1897
|
+
saves.delete(draft.path);
|
|
1898
|
+
if (state?.draft.path === draft.path) state = {
|
|
1899
|
+
...state,
|
|
1900
|
+
saving: false
|
|
1901
|
+
};
|
|
1902
|
+
changed();
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function checkLatest() {
|
|
1906
|
+
if (!state || state.saving) return;
|
|
1907
|
+
let previous = state;
|
|
1908
|
+
request?.abort();
|
|
1909
|
+
let controller = new AbortController();
|
|
1910
|
+
request = controller;
|
|
1911
|
+
let signal = AbortSignal.any([controller.signal, options.signal]);
|
|
1912
|
+
let turn = ++generation;
|
|
1913
|
+
try {
|
|
1914
|
+
let document = await options.read(previous.draft.path, signal);
|
|
1915
|
+
signal.throwIfAborted();
|
|
1916
|
+
let current = readable(document, previous.draft.path);
|
|
1917
|
+
if (!state || state.draft.path !== previous.draft.path) return;
|
|
1918
|
+
let draft = options.drafts.get(previous.draft.path);
|
|
1919
|
+
state = {
|
|
1920
|
+
...state,
|
|
1921
|
+
key: `${draft.id}:merge:${turn}`,
|
|
1922
|
+
file: {
|
|
1923
|
+
name: draft.path,
|
|
1924
|
+
contents: draft.contents
|
|
1925
|
+
},
|
|
1926
|
+
oldFile: {
|
|
1927
|
+
name: draft.path,
|
|
1928
|
+
contents: current.contents
|
|
1929
|
+
},
|
|
1930
|
+
mode: "diff",
|
|
1931
|
+
baselineLabel: "Latest file",
|
|
1932
|
+
latest: document,
|
|
1933
|
+
conflict: true,
|
|
1934
|
+
error: "",
|
|
1935
|
+
status: "Your draft is on the new side. No file is changed until you review it and save."
|
|
1936
|
+
};
|
|
1937
|
+
} catch (cause) {
|
|
1938
|
+
if (!signal.aborted && state) state = {
|
|
1939
|
+
...state,
|
|
1940
|
+
error: message$3(cause)
|
|
1941
|
+
};
|
|
1942
|
+
} finally {
|
|
1943
|
+
changed();
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function acceptMerge() {
|
|
1947
|
+
if (!state?.latest) return;
|
|
1948
|
+
let latest = readable(state.latest, state.draft.path);
|
|
1949
|
+
let draft = options.drafts.rebase(state.draft.path, latest.revision, latest.contents);
|
|
1950
|
+
state = {
|
|
1951
|
+
...state,
|
|
1952
|
+
draft,
|
|
1953
|
+
latest: null,
|
|
1954
|
+
conflict: false,
|
|
1955
|
+
error: "",
|
|
1956
|
+
status: "Reviewed against the latest file. Save when your merged draft is ready."
|
|
1957
|
+
};
|
|
1958
|
+
changed();
|
|
1959
|
+
}
|
|
1960
|
+
async function discard() {
|
|
1961
|
+
if (!state || state.saving) return;
|
|
1962
|
+
let path = state.draft.path;
|
|
1963
|
+
options.drafts.discard(path);
|
|
1964
|
+
close();
|
|
1965
|
+
await begin(path);
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
get state() {
|
|
1969
|
+
return state;
|
|
1970
|
+
},
|
|
1971
|
+
get opening() {
|
|
1972
|
+
return opening;
|
|
1973
|
+
},
|
|
1974
|
+
get openError() {
|
|
1975
|
+
return openError;
|
|
1976
|
+
},
|
|
1977
|
+
begin,
|
|
1978
|
+
change,
|
|
1979
|
+
save,
|
|
1980
|
+
close,
|
|
1981
|
+
closeOtherPath,
|
|
1982
|
+
observe,
|
|
1983
|
+
checkLatest,
|
|
1984
|
+
acceptMerge,
|
|
1985
|
+
discard,
|
|
1986
|
+
guard: {
|
|
1987
|
+
async beforeApply(intent, plan, signal) {
|
|
1988
|
+
signal.throwIfAborted();
|
|
1989
|
+
if ([...saves].some((path) => plan.items.some((item) => item.source && inside(path, item.source) || item.destination && inside(path, item.destination)))) return "Wait for the file save to finish before changing these paths.";
|
|
1990
|
+
return options.drafts.beforeMutation(intent, plan);
|
|
1991
|
+
},
|
|
1992
|
+
afterApply(intent, result) {
|
|
1993
|
+
options.drafts.afterMutation(intent, result);
|
|
1994
|
+
if (state && intent.kind === "move" && result.items.some((item) => item.status === "applied" && item.source && inside(state.draft.path, item.source))) close();
|
|
1995
|
+
changed();
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
//#endregion
|
|
2001
|
+
//#region app/client/file-review-actions-controller.ts
|
|
2002
|
+
function message$2(cause) {
|
|
2003
|
+
return cause instanceof Error ? cause.message : "The review action could not be completed. Refresh and inspect retained undo before trying again.";
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Runs review actions, such as staging or discarding changes, one at a time: plans one, applies
|
|
2007
|
+
* it and lists recoveries, dropping the answers of superseded requests.
|
|
2008
|
+
* @param options - The component's lifetime, the Files API calls and the callbacks.
|
|
2009
|
+
* @returns The controller and its dialog state.
|
|
2010
|
+
*/
|
|
2011
|
+
function createFileReviewActionsController(options) {
|
|
2012
|
+
let state = null, request, generation = 0;
|
|
2013
|
+
function changed() {
|
|
2014
|
+
if (!options.signal.aborted) options.onChange();
|
|
2015
|
+
}
|
|
2016
|
+
function start(phase) {
|
|
2017
|
+
request?.abort();
|
|
2018
|
+
request = new AbortController();
|
|
2019
|
+
generation++;
|
|
2020
|
+
state = {
|
|
2021
|
+
phase,
|
|
2022
|
+
plan: null,
|
|
2023
|
+
result: null,
|
|
2024
|
+
recoveries: null,
|
|
2025
|
+
error: ""
|
|
2026
|
+
};
|
|
2027
|
+
changed();
|
|
2028
|
+
return {
|
|
2029
|
+
signal: AbortSignal.any([options.signal, request.signal]),
|
|
2030
|
+
turn: generation
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function close() {
|
|
2034
|
+
if (state?.phase === "applying") return;
|
|
2035
|
+
request?.abort();
|
|
2036
|
+
generation++;
|
|
2037
|
+
state = null;
|
|
2038
|
+
changed();
|
|
2039
|
+
}
|
|
2040
|
+
async function prepare(intent) {
|
|
2041
|
+
if (state?.phase === "applying") return;
|
|
2042
|
+
let { signal, turn } = start("preparing");
|
|
2043
|
+
try {
|
|
2044
|
+
if (intent.kind !== "undo") options.guard([intent.path], intent.kind === "discard" ? "worktree" : "index");
|
|
2045
|
+
let plan = await options.prepare(intent, signal);
|
|
2046
|
+
signal.throwIfAborted();
|
|
2047
|
+
if (plan.kind !== intent.kind || intent.kind !== "undo" && !plan.paths.includes(intent.path)) throw new Error("The prepared review belongs to a different action or file.");
|
|
2048
|
+
options.guard(plan.paths, plan.target);
|
|
2049
|
+
if (turn === generation) state = {
|
|
2050
|
+
phase: "ready",
|
|
2051
|
+
plan,
|
|
2052
|
+
result: null,
|
|
2053
|
+
recoveries: null,
|
|
2054
|
+
error: ""
|
|
2055
|
+
};
|
|
2056
|
+
} catch (cause) {
|
|
2057
|
+
if (!signal.aborted && turn === generation) state = {
|
|
2058
|
+
phase: "error",
|
|
2059
|
+
plan: null,
|
|
2060
|
+
result: null,
|
|
2061
|
+
recoveries: null,
|
|
2062
|
+
error: message$2(cause)
|
|
2063
|
+
};
|
|
2064
|
+
} finally {
|
|
2065
|
+
if (turn === generation) changed();
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
async function apply() {
|
|
2069
|
+
if (state?.phase !== "ready" || !state.plan) return;
|
|
2070
|
+
let plan = state.plan, turn = generation, signal = AbortSignal.any([options.signal, request.signal]);
|
|
2071
|
+
try {
|
|
2072
|
+
options.guard(plan.paths, plan.target);
|
|
2073
|
+
state = {
|
|
2074
|
+
...state,
|
|
2075
|
+
phase: "applying",
|
|
2076
|
+
error: ""
|
|
2077
|
+
};
|
|
2078
|
+
changed();
|
|
2079
|
+
let result = await options.apply({
|
|
2080
|
+
id: plan.id,
|
|
2081
|
+
revision: plan.revision
|
|
2082
|
+
}, signal);
|
|
2083
|
+
signal.throwIfAborted();
|
|
2084
|
+
if (result.id !== plan.id || result.target !== plan.target || result.changedPaths.some((path) => !plan.paths.includes(path))) throw new Error("The write response could not be confirmed. Refresh and inspect retained undo.");
|
|
2085
|
+
if (turn === generation) {
|
|
2086
|
+
let refreshError = "";
|
|
2087
|
+
try {
|
|
2088
|
+
await options.onApplied(result);
|
|
2089
|
+
} catch (cause) {
|
|
2090
|
+
refreshError = message$2(cause);
|
|
2091
|
+
}
|
|
2092
|
+
signal.throwIfAborted();
|
|
2093
|
+
if (turn === generation) state = {
|
|
2094
|
+
...state,
|
|
2095
|
+
phase: "done",
|
|
2096
|
+
result,
|
|
2097
|
+
error: refreshError
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
} catch (cause) {
|
|
2101
|
+
if (!signal.aborted && turn === generation && state) state = {
|
|
2102
|
+
...state,
|
|
2103
|
+
phase: "error",
|
|
2104
|
+
error: message$2(cause)
|
|
2105
|
+
};
|
|
2106
|
+
} finally {
|
|
2107
|
+
if (turn === generation) changed();
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
async function recoveries() {
|
|
2111
|
+
if (state?.phase === "applying") return;
|
|
2112
|
+
let { signal, turn } = start("preparing");
|
|
2113
|
+
try {
|
|
2114
|
+
let items = await options.recoveries(signal);
|
|
2115
|
+
if (!signal.aborted && turn === generation) state = {
|
|
2116
|
+
phase: "recoveries",
|
|
2117
|
+
plan: null,
|
|
2118
|
+
result: null,
|
|
2119
|
+
recoveries: items,
|
|
2120
|
+
error: ""
|
|
2121
|
+
};
|
|
2122
|
+
} catch (cause) {
|
|
2123
|
+
if (!signal.aborted && turn === generation) state = {
|
|
2124
|
+
phase: "error",
|
|
2125
|
+
plan: null,
|
|
2126
|
+
result: null,
|
|
2127
|
+
recoveries: null,
|
|
2128
|
+
error: message$2(cause)
|
|
2129
|
+
};
|
|
2130
|
+
} finally {
|
|
2131
|
+
if (turn === generation) changed();
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
options.signal.addEventListener("abort", () => {
|
|
2135
|
+
request?.abort();
|
|
2136
|
+
state = null;
|
|
2137
|
+
}, { once: true });
|
|
2138
|
+
return {
|
|
2139
|
+
get state() {
|
|
2140
|
+
return state;
|
|
2141
|
+
},
|
|
2142
|
+
prepare,
|
|
2143
|
+
apply,
|
|
2144
|
+
recoveries,
|
|
2145
|
+
close
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
//#endregion
|
|
2149
|
+
//#region app/client/project-code-comments.ts
|
|
2150
|
+
/**
|
|
2151
|
+
* Where a code conversation's lines are in the review, for selecting them.
|
|
2152
|
+
* @param location - The conversation's anchor as it resolves in the current source.
|
|
2153
|
+
* @returns The lines and their revision, or null when the anchor is outdated.
|
|
2154
|
+
*/
|
|
2155
|
+
function codeConversationPosition(location) {
|
|
2156
|
+
if (location.resolution.state === "outdated") return null;
|
|
2157
|
+
return {
|
|
2158
|
+
selection: {
|
|
2159
|
+
path: location.source.viewPath ?? location.source.path,
|
|
2160
|
+
start: location.resolution.startLine,
|
|
2161
|
+
end: location.resolution.endLine,
|
|
2162
|
+
...location.source.side ? { side: location.source.side === "old" ? "deletions" : "additions" } : {}
|
|
2163
|
+
},
|
|
2164
|
+
revision: location.resolution.revision
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* The sources a review can be commented on: the shown source, the open file, or both sides of
|
|
2169
|
+
* every diff.
|
|
2170
|
+
* @param document - The open file, if any.
|
|
2171
|
+
* @param diffs - The shown diffs.
|
|
2172
|
+
* @param source - The shown source at a commit, if any.
|
|
2173
|
+
* @returns The commentable sources.
|
|
2174
|
+
*/
|
|
2175
|
+
function reviewCommentSources(document, diffs, source) {
|
|
2176
|
+
if (source) return [{
|
|
2177
|
+
path: source.path,
|
|
2178
|
+
revision: source.file?.revision ?? null,
|
|
2179
|
+
contents: source.file?.contents ?? null,
|
|
2180
|
+
commit: source.commit
|
|
2181
|
+
}];
|
|
2182
|
+
if (document) return [{
|
|
2183
|
+
path: document.entry.path,
|
|
2184
|
+
revision: document.revision,
|
|
2185
|
+
contents: document.contents,
|
|
2186
|
+
commit: null
|
|
2187
|
+
}];
|
|
2188
|
+
return diffs.flatMap((diff) => [{
|
|
2189
|
+
path: diff.oldFile?.name ?? diff.entry.originalPath ?? diff.entry.path,
|
|
2190
|
+
revision: diff.oldFile?.revision ?? null,
|
|
2191
|
+
contents: diff.oldFile?.contents ?? null,
|
|
2192
|
+
viewPath: diff.entry.path,
|
|
2193
|
+
side: "old",
|
|
2194
|
+
commit: diff.base
|
|
2195
|
+
}, {
|
|
2196
|
+
path: diff.entry.path,
|
|
2197
|
+
previousPath: diff.entry.originalPath,
|
|
2198
|
+
revision: diff.newFile?.revision ?? null,
|
|
2199
|
+
contents: diff.newFile?.contents ?? null,
|
|
2200
|
+
viewPath: diff.entry.path,
|
|
2201
|
+
side: "new",
|
|
2202
|
+
commit: diff.target
|
|
2203
|
+
}]);
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Anchors selected lines to their exact source revision for a new code comment.
|
|
2207
|
+
* @param selection - Consecutive lines on one side.
|
|
2208
|
+
* @param sources - The commentable sources.
|
|
2209
|
+
* @param expectedRevision - The revision the selection was made in, when known.
|
|
2210
|
+
* @returns The code anchor.
|
|
2211
|
+
*/
|
|
2212
|
+
function captureCodeSelection(selection, sources, expectedRevision) {
|
|
2213
|
+
if (selection.endSide && selection.endSide !== selection.side) throw new Error("Select consecutive lines on one side before commenting.");
|
|
2214
|
+
let side = selection.side === "deletions" ? "old" : "new";
|
|
2215
|
+
let source = sources.find((source) => (source.viewPath ?? source.path) === selection.path && (!source.side || source.side === side));
|
|
2216
|
+
if (!source || source.contents === null || !source.revision) throw new Error("Comments require readable text with an exact source revision.");
|
|
2217
|
+
if (expectedRevision && source.revision !== expectedRevision) throw new Error("Those lines belong to an older source. Select them again before commenting.");
|
|
2218
|
+
return createCodeAnchor({
|
|
2219
|
+
path: source.path,
|
|
2220
|
+
revision: source.revision,
|
|
2221
|
+
commit: source.commit ?? null,
|
|
2222
|
+
side,
|
|
2223
|
+
startLine: Math.min(selection.start, selection.end),
|
|
2224
|
+
endLine: Math.max(selection.start, selection.end)
|
|
2225
|
+
}, source.contents);
|
|
2226
|
+
}
|
|
2227
|
+
//#endregion
|
|
2228
|
+
//#region app/client/file-search-controller.ts
|
|
2229
|
+
/**
|
|
2230
|
+
* Writes a search query as the search API's URL parameters.
|
|
2231
|
+
* @param query - The validated query.
|
|
2232
|
+
* @returns The parameters, with one `include` and `exclude` entry for each pattern.
|
|
2233
|
+
*/
|
|
2234
|
+
function fileSearchParameters(query) {
|
|
2235
|
+
let params = new URLSearchParams({
|
|
2236
|
+
query: query.query,
|
|
2237
|
+
regex: String(query.regex),
|
|
2238
|
+
caseSensitive: String(query.caseSensitive),
|
|
2239
|
+
wholeWord: String(query.wholeWord),
|
|
2240
|
+
directory: query.directory,
|
|
2241
|
+
includeDotfiles: String(query.includeDotfiles),
|
|
2242
|
+
includeIgnored: String(query.includeIgnored),
|
|
2243
|
+
limit: String(query.limit)
|
|
2244
|
+
});
|
|
2245
|
+
for (let value of query.include) params.append("include", value);
|
|
2246
|
+
for (let value of query.exclude) params.append("exclude", value);
|
|
2247
|
+
return params;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Superseded and cancelled requests never publish their result or error.
|
|
2251
|
+
* @param options - Component lifetime, validated transport and render notification.
|
|
2252
|
+
* @returns A single request owner and its observable search state.
|
|
2253
|
+
*/
|
|
2254
|
+
function createFileSearchController(options) {
|
|
2255
|
+
let active;
|
|
2256
|
+
let state = {
|
|
2257
|
+
loading: false,
|
|
2258
|
+
result: null,
|
|
2259
|
+
error: "",
|
|
2260
|
+
notice: ""
|
|
2261
|
+
};
|
|
2262
|
+
function notify() {
|
|
2263
|
+
if (!options.signal.aborted) options.onChange();
|
|
2264
|
+
}
|
|
2265
|
+
function cancel(notice = "Search cancelled.") {
|
|
2266
|
+
active?.abort();
|
|
2267
|
+
active = void 0;
|
|
2268
|
+
state = {
|
|
2269
|
+
...state,
|
|
2270
|
+
loading: false,
|
|
2271
|
+
notice,
|
|
2272
|
+
error: ""
|
|
2273
|
+
};
|
|
2274
|
+
notify();
|
|
2275
|
+
}
|
|
2276
|
+
options.signal.addEventListener("abort", () => cancel(""), { once: true });
|
|
2277
|
+
return {
|
|
2278
|
+
get state() {
|
|
2279
|
+
return state;
|
|
2280
|
+
},
|
|
2281
|
+
cancel,
|
|
2282
|
+
async search(input) {
|
|
2283
|
+
cancel("");
|
|
2284
|
+
let owner = new AbortController();
|
|
2285
|
+
active = owner;
|
|
2286
|
+
let signal = AbortSignal.any([owner.signal, options.signal]);
|
|
2287
|
+
state = {
|
|
2288
|
+
loading: true,
|
|
2289
|
+
result: null,
|
|
2290
|
+
error: "",
|
|
2291
|
+
notice: ""
|
|
2292
|
+
};
|
|
2293
|
+
notify();
|
|
2294
|
+
try {
|
|
2295
|
+
let query = parseFileSearchQuery(input);
|
|
2296
|
+
let result = await options.request(query, signal);
|
|
2297
|
+
if (fileSearchParameters(result.query).toString() !== fileSearchParameters(query).toString()) throw new FilesRequestError("The service returned results for a different search.", "response");
|
|
2298
|
+
if (signal.aborted || active !== owner) return;
|
|
2299
|
+
state = {
|
|
2300
|
+
loading: false,
|
|
2301
|
+
result,
|
|
2302
|
+
error: "",
|
|
2303
|
+
notice: ""
|
|
2304
|
+
};
|
|
2305
|
+
} catch (cause) {
|
|
2306
|
+
if (signal.aborted || active !== owner) return;
|
|
2307
|
+
state = {
|
|
2308
|
+
loading: false,
|
|
2309
|
+
result: null,
|
|
2310
|
+
error: cause instanceof Error ? cause.message : "Could not search this project.",
|
|
2311
|
+
notice: ""
|
|
2312
|
+
};
|
|
2313
|
+
} finally {
|
|
2314
|
+
if (active === owner) {
|
|
2315
|
+
active = void 0;
|
|
2316
|
+
notify();
|
|
2317
|
+
}
|
|
2318
|
+
owner.abort();
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Search offsets only navigate the exact on-disk version that produced them.
|
|
2325
|
+
* @param document - Current complete file response.
|
|
2326
|
+
* @param match - Captured search result with revision and coordinates.
|
|
2327
|
+
* @returns A verified source-review position, or an explicit outdated-result error.
|
|
2328
|
+
*/
|
|
2329
|
+
function searchMatchPosition(document, match) {
|
|
2330
|
+
if (document.entry.path !== match.path || document.revision !== match.revision || document.entry.content !== "text" || typeof document.contents !== "string" || match.start < 0 || match.end < match.start || match.end > document.contents.length) throw new Error("This search result is outdated. Search again to locate the current text; any unsaved draft is retained.");
|
|
2331
|
+
let position = (offset) => {
|
|
2332
|
+
let line = 1, start = 0;
|
|
2333
|
+
for (let ending of document.contents.matchAll(/\r\n|\r|\n/g)) {
|
|
2334
|
+
let next = ending.index + ending[0].length;
|
|
2335
|
+
if (next > offset) break;
|
|
2336
|
+
line++;
|
|
2337
|
+
start = next;
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
line,
|
|
2341
|
+
column: offset - start + 1
|
|
2342
|
+
};
|
|
2343
|
+
};
|
|
2344
|
+
let before = position(match.start), end = position(match.end);
|
|
2345
|
+
if (before.line !== match.startLine || before.column !== match.startColumn || end.line !== match.endLine || end.column !== match.endColumn) throw new Error("This search result has invalid coordinates. Search again.");
|
|
2346
|
+
return {
|
|
2347
|
+
page: 0,
|
|
2348
|
+
scroll: 0,
|
|
2349
|
+
revision: match.revision,
|
|
2350
|
+
selection: {
|
|
2351
|
+
path: match.path,
|
|
2352
|
+
start: match.startLine,
|
|
2353
|
+
end: Math.max(match.startLine, match.endLine - Number(match.endColumn === 1 && match.endLine > match.startLine))
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
//#endregion
|
|
2358
|
+
//#region app/client/project-file-review.tsx
|
|
2359
|
+
function message$1(cause) {
|
|
2360
|
+
return cause instanceof Error ? cause.message : "Could not read the project.";
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Owns validated review requests, the worker pool and bounded project view persistence.
|
|
2364
|
+
* @param handle - Selected path and the connected project's public capability.
|
|
2365
|
+
* @returns The review render function.
|
|
2366
|
+
*/
|
|
2367
|
+
function ProjectFileReview(handle) {
|
|
2368
|
+
let project = handle.props.project;
|
|
2369
|
+
let session = {
|
|
2370
|
+
version: 1,
|
|
2371
|
+
projectId: project.id,
|
|
2372
|
+
view: defaultReviewView(),
|
|
2373
|
+
positions: {}
|
|
2374
|
+
};
|
|
2375
|
+
let document = null;
|
|
2376
|
+
let snapshot = null;
|
|
2377
|
+
let diffs = [];
|
|
2378
|
+
let failures = {};
|
|
2379
|
+
let error = "";
|
|
2380
|
+
let loading = false;
|
|
2381
|
+
let history = null;
|
|
2382
|
+
let historyOpen = false;
|
|
2383
|
+
let historyLoading = false;
|
|
2384
|
+
let historyError = "";
|
|
2385
|
+
let theme = "light";
|
|
2386
|
+
let pool;
|
|
2387
|
+
let poolSettled = false;
|
|
2388
|
+
let request;
|
|
2389
|
+
let historyRequest;
|
|
2390
|
+
let persistTimer;
|
|
2391
|
+
let mounted = false;
|
|
2392
|
+
let renderedHref = "";
|
|
2393
|
+
let renderedConversation;
|
|
2394
|
+
let pendingHref;
|
|
2395
|
+
let lastKey = "";
|
|
2396
|
+
let displayedKey = "";
|
|
2397
|
+
let historyKey = "";
|
|
2398
|
+
let refresh = handle.props.refresh;
|
|
2399
|
+
let appliedHighlights = "";
|
|
2400
|
+
let drafts;
|
|
2401
|
+
let editor;
|
|
2402
|
+
let preferences = defaultEditorPreferences();
|
|
2403
|
+
let observedPath = handle.props.path;
|
|
2404
|
+
let actions;
|
|
2405
|
+
let merge;
|
|
2406
|
+
let historical = null;
|
|
2407
|
+
let source = null;
|
|
2408
|
+
let panelOpen = false, selectedThread, draftAnchor;
|
|
2409
|
+
let commentError = "", requestedThread = null, appliedThread = null;
|
|
2410
|
+
let commentSources = [], commentLocations = [];
|
|
2411
|
+
let sourceSignature = "", locationsVersion;
|
|
2412
|
+
let pendingFocus = null;
|
|
2413
|
+
let editorLocations;
|
|
2414
|
+
let locatedEditor;
|
|
2415
|
+
let searchSelectionId = 0, searchNotice = "";
|
|
2416
|
+
let pendingSearch;
|
|
2417
|
+
let commentReturnFocus;
|
|
2418
|
+
let host;
|
|
2419
|
+
let reportedDrafts = "";
|
|
2420
|
+
let reportedComments = false;
|
|
2421
|
+
let store = handle.props.store;
|
|
2422
|
+
if (!store) projectStore(project).then((opened) => {
|
|
2423
|
+
store = opened;
|
|
2424
|
+
});
|
|
2425
|
+
function openComments() {
|
|
2426
|
+
if (!panelOpen) {
|
|
2427
|
+
let active = window.document.activeElement;
|
|
2428
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
2429
|
+
commentReturnFocus = active instanceof HTMLElement ? active : void 0;
|
|
2430
|
+
}
|
|
2431
|
+
panelOpen = true;
|
|
2432
|
+
}
|
|
2433
|
+
function closeComments() {
|
|
2434
|
+
panelOpen = false;
|
|
2435
|
+
draftAnchor = void 0;
|
|
2436
|
+
changed();
|
|
2437
|
+
handle.queueTask(() => {
|
|
2438
|
+
(commentReturnFocus?.isConnected ? commentReturnFocus : host?.querySelector("diffs-container")?.shadowRoot?.querySelector("[contenteditable=true]") ?? host?.querySelector("[data-bud-code-review]"))?.focus({ preventScroll: true });
|
|
2439
|
+
commentReturnFocus = void 0;
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
function changed() {
|
|
2443
|
+
if (!handle.signal.aborted) handle.update();
|
|
2444
|
+
}
|
|
2445
|
+
function context() {
|
|
2446
|
+
return historical ? JSON.stringify([reviewContextKey(session.view, handle.props.path), historical.commit]) : reviewContextKey(session.view, handle.props.path);
|
|
2447
|
+
}
|
|
2448
|
+
function position() {
|
|
2449
|
+
return session.positions[context()] ?? defaultReviewPosition();
|
|
2450
|
+
}
|
|
2451
|
+
function persist() {
|
|
2452
|
+
clearTimeout(persistTimer);
|
|
2453
|
+
persistTimer = setTimeout(() => writePreference(`bud:review:${project.id}:v1`, JSON.stringify(session)), 200);
|
|
2454
|
+
}
|
|
2455
|
+
function savePosition(change, render = false) {
|
|
2456
|
+
let previous = position();
|
|
2457
|
+
let next = {
|
|
2458
|
+
...previous,
|
|
2459
|
+
...change
|
|
2460
|
+
};
|
|
2461
|
+
session = rememberReviewPosition(session, context(), next);
|
|
2462
|
+
persist();
|
|
2463
|
+
if (render || JSON.stringify(previous.selection) !== JSON.stringify(next.selection) || previous.revision !== next.revision) changed();
|
|
2464
|
+
}
|
|
2465
|
+
function location() {
|
|
2466
|
+
let href = reviewHref(handle.props.path, session.view);
|
|
2467
|
+
if (window.location.pathname + window.location.search + window.location.hash !== href) {
|
|
2468
|
+
pendingHref = new URL(href, window.location.origin).href;
|
|
2469
|
+
window.history.pushState({ ...window.history.state }, "", href);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function changeView(change) {
|
|
2473
|
+
historical = null;
|
|
2474
|
+
source = null;
|
|
2475
|
+
session = {
|
|
2476
|
+
...session,
|
|
2477
|
+
view: {
|
|
2478
|
+
...session.view,
|
|
2479
|
+
...change
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2482
|
+
persist();
|
|
2483
|
+
location();
|
|
2484
|
+
changed();
|
|
2485
|
+
}
|
|
2486
|
+
let reading;
|
|
2487
|
+
async function load() {
|
|
2488
|
+
let finishRead;
|
|
2489
|
+
reading = new Promise((resolve) => {
|
|
2490
|
+
finishRead = resolve;
|
|
2491
|
+
});
|
|
2492
|
+
request?.abort();
|
|
2493
|
+
let controller = new AbortController();
|
|
2494
|
+
request = controller;
|
|
2495
|
+
let signal = AbortSignal.any([handle.signal, controller.signal]);
|
|
2496
|
+
let selected = handle.props.path;
|
|
2497
|
+
let settings = session.view;
|
|
2498
|
+
let immutable = historical && historical.path === selected ? historical : null;
|
|
2499
|
+
let key = context();
|
|
2500
|
+
let nextDisplay = JSON.stringify([
|
|
2501
|
+
key,
|
|
2502
|
+
position().page,
|
|
2503
|
+
selected
|
|
2504
|
+
]);
|
|
2505
|
+
let comparison = reviewComparison(settings);
|
|
2506
|
+
loading = true;
|
|
2507
|
+
error = "";
|
|
2508
|
+
if (displayedKey !== nextDisplay) {
|
|
2509
|
+
document = null;
|
|
2510
|
+
snapshot = null;
|
|
2511
|
+
diffs = [];
|
|
2512
|
+
source = null;
|
|
2513
|
+
failures = {};
|
|
2514
|
+
}
|
|
2515
|
+
changed();
|
|
2516
|
+
try {
|
|
2517
|
+
if (immutable) {
|
|
2518
|
+
let next = await requestFiles(`${routes.fileReviewApi.source.href()}?${new URLSearchParams(immutable)}`, project.token, signal, parseReviewSource);
|
|
2519
|
+
if (next.path !== immutable.path || next.commit !== immutable.commit) throw new FilesRequestError("The source response belongs to another commit or file.", "response");
|
|
2520
|
+
if (!signal.aborted) {
|
|
2521
|
+
source = next;
|
|
2522
|
+
displayedKey = nextDisplay;
|
|
2523
|
+
}
|
|
2524
|
+
} else if (!comparison) {
|
|
2525
|
+
if (selected) {
|
|
2526
|
+
let stored = displayedKey !== nextDisplay && store ? storedDocument(store, selected) : null;
|
|
2527
|
+
if (stored) {
|
|
2528
|
+
document = stored;
|
|
2529
|
+
displayedKey = nextDisplay;
|
|
2530
|
+
loading = false;
|
|
2531
|
+
changed();
|
|
2532
|
+
}
|
|
2533
|
+
let next = await requestFiles(`${routes.filesApi.read.href()}?${new URLSearchParams({ path: selected })}`, project.token, signal, parseFileDocument);
|
|
2534
|
+
if (next.entry.path !== selected) throw new FilesRequestError("The source response belongs to another file.", "response");
|
|
2535
|
+
if (store) storeDocument(store, next).catch(() => {});
|
|
2536
|
+
if (!signal.aborted) {
|
|
2537
|
+
document = document?.revision === next.revision && document.entry.path === next.entry.path ? document : next;
|
|
2538
|
+
displayedKey = nextDisplay;
|
|
2539
|
+
editor?.observe(next);
|
|
2540
|
+
if (pendingSearch?.path === selected) {
|
|
2541
|
+
let match = pendingSearch;
|
|
2542
|
+
pendingSearch = void 0;
|
|
2543
|
+
try {
|
|
2544
|
+
savePosition(searchMatchPosition(next, match), true);
|
|
2545
|
+
searchNotice = `Search match at ${match.startLine}:${match.startColumn}. Any unsaved draft is retained.`;
|
|
2546
|
+
} catch (cause) {
|
|
2547
|
+
searchNotice = message$1(cause);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
} else for (let attempt = 0; attempt < 2; attempt++) {
|
|
2553
|
+
let next = await requestFiles(`${routes.fileReviewApi.compare.href()}?${comparisonQuery(comparison)}`, project.token, signal, parseReviewSnapshot);
|
|
2554
|
+
if (next.comparison.mode !== comparison.mode) throw new FilesRequestError("The service returned a different comparison.", "response");
|
|
2555
|
+
let page = Math.min(position().page, Math.max(0, Math.ceil(next.entries.length / 20) - 1));
|
|
2556
|
+
let restoredPath = position().selection?.path;
|
|
2557
|
+
if (restoredPath && settings.multiple && next.entries.some((entry) => entry.path === restoredPath)) page = Math.floor(next.entries.findIndex((entry) => entry.path === restoredPath) / 20);
|
|
2558
|
+
if (page !== position().page) savePosition({
|
|
2559
|
+
...position(),
|
|
2560
|
+
page
|
|
2561
|
+
});
|
|
2562
|
+
let paths = settings.multiple ? next.entries.slice(page * 20, (page + 1) * 20).map((entry) => entry.path) : next.entries.filter((entry) => entry.path === selected).map((entry) => entry.path);
|
|
2563
|
+
try {
|
|
2564
|
+
let result = await readReviewPage(next, paths, project.token, signal);
|
|
2565
|
+
if (!signal.aborted && context() === key) {
|
|
2566
|
+
snapshot = next;
|
|
2567
|
+
diffs = result.diffs;
|
|
2568
|
+
failures = result.failures;
|
|
2569
|
+
displayedKey = JSON.stringify([
|
|
2570
|
+
key,
|
|
2571
|
+
page,
|
|
2572
|
+
selected
|
|
2573
|
+
]);
|
|
2574
|
+
}
|
|
2575
|
+
break;
|
|
2576
|
+
} catch (cause) {
|
|
2577
|
+
if (attempt === 0 && cause instanceof FilesRequestError && cause.status === 409) continue;
|
|
2578
|
+
throw cause;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
} catch (cause) {
|
|
2582
|
+
if (!signal.aborted) error = message$1(cause);
|
|
2583
|
+
} finally {
|
|
2584
|
+
if (request === controller) {
|
|
2585
|
+
loading = false;
|
|
2586
|
+
changed();
|
|
2587
|
+
}
|
|
2588
|
+
finishRead();
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
async function loadHistory(older = false) {
|
|
2592
|
+
historyRequest?.abort();
|
|
2593
|
+
let path = handle.props.path;
|
|
2594
|
+
if (!path) {
|
|
2595
|
+
history = null;
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
let controller = new AbortController();
|
|
2599
|
+
historyRequest = controller;
|
|
2600
|
+
let signal = AbortSignal.any([handle.signal, controller.signal]);
|
|
2601
|
+
let query = new URLSearchParams({
|
|
2602
|
+
path,
|
|
2603
|
+
limit: "25"
|
|
2604
|
+
});
|
|
2605
|
+
if (older && history?.nextCursor && history.revision) {
|
|
2606
|
+
query.set("revision", history.revision);
|
|
2607
|
+
query.set("cursor", history.nextCursor);
|
|
2608
|
+
} else if (session.view.mode === "commits") query.set("revision", session.view.target);
|
|
2609
|
+
historyLoading = true;
|
|
2610
|
+
historyError = "";
|
|
2611
|
+
changed();
|
|
2612
|
+
try {
|
|
2613
|
+
let next = await requestFiles(`${routes.fileReviewApi.history.href()}?${query}`, project.token, signal, parseReviewHistory);
|
|
2614
|
+
if (next.path !== path) throw new FilesRequestError("The history response belongs to another path.", "response");
|
|
2615
|
+
if (!signal.aborted) history = next;
|
|
2616
|
+
} catch (cause) {
|
|
2617
|
+
if (!signal.aborted) historyError = message$1(cause);
|
|
2618
|
+
} finally {
|
|
2619
|
+
if (historyRequest === controller) {
|
|
2620
|
+
historyLoading = false;
|
|
2621
|
+
changed();
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
function selectPath(path) {
|
|
2626
|
+
if (session.view.multiple && snapshot) {
|
|
2627
|
+
let index = snapshot.entries.findIndex((entry) => entry.path === path);
|
|
2628
|
+
savePosition({
|
|
2629
|
+
...defaultReviewPosition(),
|
|
2630
|
+
page: Math.max(0, Math.floor(index / 20))
|
|
2631
|
+
});
|
|
2632
|
+
lastKey = "";
|
|
2633
|
+
}
|
|
2634
|
+
handle.props.onSelect(path);
|
|
2635
|
+
changed();
|
|
2636
|
+
}
|
|
2637
|
+
function historyRevision(commit, action) {
|
|
2638
|
+
let endpoints = action === "parent" && commit.parents[0] ? {
|
|
2639
|
+
base: commit.parents[0],
|
|
2640
|
+
target: commit.commit
|
|
2641
|
+
} : action === "base" ? { base: commit.commit } : { target: commit.commit };
|
|
2642
|
+
historyOpen = false;
|
|
2643
|
+
changeView({
|
|
2644
|
+
mode: "commits",
|
|
2645
|
+
...endpoints
|
|
2646
|
+
});
|
|
2647
|
+
if (commit.path !== handle.props.path) handle.props.onSelect(commit.path);
|
|
2648
|
+
}
|
|
2649
|
+
async function startEditor() {
|
|
2650
|
+
let path = handle.props.path;
|
|
2651
|
+
let mode = session.view.mode;
|
|
2652
|
+
if (!path || !editor) return;
|
|
2653
|
+
if (mode === "working") {
|
|
2654
|
+
if (!snapshot || snapshot.comparison.mode !== "working") await load();
|
|
2655
|
+
while (loading && reading) {
|
|
2656
|
+
await reading;
|
|
2657
|
+
if (handle.signal.aborted || handle.props.path !== path || session.view.mode !== mode) return;
|
|
2658
|
+
}
|
|
2659
|
+
if (handle.signal.aborted || handle.props.path !== path || session.view.mode !== mode) return;
|
|
2660
|
+
}
|
|
2661
|
+
let diff = diffs.find((diff) => diff.entry.path === path);
|
|
2662
|
+
let working = session.view.mode === "working" && diff?.newFile?.contents !== null && diff?.newFile?.revision;
|
|
2663
|
+
editor.begin(path, working ? "diff" : "file", working ? {
|
|
2664
|
+
workingRevision: diff.newFile.revision,
|
|
2665
|
+
file: diff.oldFile?.contents !== null && diff.oldFile ? {
|
|
2666
|
+
name: diff.oldFile.name,
|
|
2667
|
+
contents: diff.oldFile.contents
|
|
2668
|
+
} : null
|
|
2669
|
+
} : void 0);
|
|
2670
|
+
}
|
|
2671
|
+
function setPreferences(value) {
|
|
2672
|
+
preferences = value;
|
|
2673
|
+
writePreference("bud:editor-preferences:v1", JSON.stringify(value));
|
|
2674
|
+
changed();
|
|
2675
|
+
}
|
|
2676
|
+
function downloadDraft() {
|
|
2677
|
+
let draft = editor?.state?.draft;
|
|
2678
|
+
if (!draft) return;
|
|
2679
|
+
let url = URL.createObjectURL(new Blob([new TextEncoder().encode(draft.contents)], { type: "application/octet-stream" }));
|
|
2680
|
+
let link = window.document.createElement("a");
|
|
2681
|
+
link.href = url;
|
|
2682
|
+
link.download = draft.path.split("/").at(-1) ?? "draft.txt";
|
|
2683
|
+
link.click();
|
|
2684
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
2685
|
+
}
|
|
2686
|
+
async function resolveConflict(diff) {
|
|
2687
|
+
let identity = reviewConflictIdentity(diff.conflict);
|
|
2688
|
+
if (!identity || !editor) return;
|
|
2689
|
+
if (diff.entry.path !== handle.props.path) handle.props.onSelect(diff.entry.path);
|
|
2690
|
+
merge = {
|
|
2691
|
+
path: diff.entry.path,
|
|
2692
|
+
conflict: identity
|
|
2693
|
+
};
|
|
2694
|
+
await editor.begin(diff.entry.path);
|
|
2695
|
+
changed();
|
|
2696
|
+
}
|
|
2697
|
+
function selectDiscussion(thread, original = false) {
|
|
2698
|
+
if (!isCodeConversation(thread)) {
|
|
2699
|
+
window.location.assign(conversationHref(thread.id));
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
selectedThread = thread.id;
|
|
2703
|
+
handle.props.comments.loadThread(thread.id);
|
|
2704
|
+
openComments();
|
|
2705
|
+
draftAnchor = void 0;
|
|
2706
|
+
commentError = "";
|
|
2707
|
+
let located = commentLocations.find((entry) => entry.thread.id === thread.id);
|
|
2708
|
+
if (editor?.state?.draft.path === thread.anchor.path && !original) {
|
|
2709
|
+
changed();
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
let needsOldSide = thread.anchor.side === "old" && located?.source.side === void 0 && located?.resolution.state === "outdated";
|
|
2713
|
+
let usable = !original && !needsOldSide ? located : void 0;
|
|
2714
|
+
let path = usable?.source.viewPath ?? usable?.source.path ?? thread.anchor.path;
|
|
2715
|
+
if (thread.anchor.commit && (original || !usable)) {
|
|
2716
|
+
historical = {
|
|
2717
|
+
path: thread.anchor.path,
|
|
2718
|
+
commit: thread.anchor.commit
|
|
2719
|
+
};
|
|
2720
|
+
path = thread.anchor.path;
|
|
2721
|
+
session = {
|
|
2722
|
+
...session,
|
|
2723
|
+
view: {
|
|
2724
|
+
...session.view,
|
|
2725
|
+
mode: "source",
|
|
2726
|
+
multiple: false
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
editor?.close();
|
|
2730
|
+
} else if (!usable) {
|
|
2731
|
+
historical = null;
|
|
2732
|
+
session = {
|
|
2733
|
+
...session,
|
|
2734
|
+
view: {
|
|
2735
|
+
...session.view,
|
|
2736
|
+
mode: thread.anchor.side === "old" ? "working" : "source",
|
|
2737
|
+
multiple: false
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
if (path !== handle.props.path) handle.props.onSelect(path);
|
|
2742
|
+
pendingFocus = thread.id;
|
|
2743
|
+
if (!usable || path !== handle.props.path) lastKey = "";
|
|
2744
|
+
changed();
|
|
2745
|
+
}
|
|
2746
|
+
function startComment(selection, fromEditor = false) {
|
|
2747
|
+
if (!handle.props.comments.ready || !handle.props.comments.snapshot.capabilities.create) return;
|
|
2748
|
+
openComments();
|
|
2749
|
+
appliedThread = requestedThread;
|
|
2750
|
+
selectedThread = void 0;
|
|
2751
|
+
commentError = "";
|
|
2752
|
+
try {
|
|
2753
|
+
let editing = editor?.state;
|
|
2754
|
+
if (fromEditor && editing && (draftDirty(editing.draft) || editing.conflict || editing.saving)) throw new Error("Save this draft before creating a code comment. Existing discussions remain available, and your draft is kept.");
|
|
2755
|
+
draftAnchor = captureCodeSelection(selection, fromEditor && editing && selection.side !== "deletions" ? [{
|
|
2756
|
+
path: editing.draft.path,
|
|
2757
|
+
revision: editing.draft.baseRevision,
|
|
2758
|
+
contents: editing.draft.contents,
|
|
2759
|
+
commit: null
|
|
2760
|
+
}] : commentSources, fromEditor ? void 0 : position().revision);
|
|
2761
|
+
} catch (cause) {
|
|
2762
|
+
draftAnchor = void 0;
|
|
2763
|
+
commentError = message$1(cause);
|
|
2764
|
+
}
|
|
2765
|
+
changed();
|
|
2766
|
+
}
|
|
2767
|
+
async function createComment(body) {
|
|
2768
|
+
if (!draftAnchor) throw new Error("Select code lines again before commenting.");
|
|
2769
|
+
if (editor?.state && draftDirty(editor.state.draft)) throw new Error("Save your draft before sending this code comment. The message is kept.");
|
|
2770
|
+
selectedThread = (await handle.props.comments.write({
|
|
2771
|
+
anchor: draftAnchor,
|
|
2772
|
+
body
|
|
2773
|
+
})).thread.id;
|
|
2774
|
+
draftAnchor = void 0;
|
|
2775
|
+
changed();
|
|
2776
|
+
}
|
|
2777
|
+
let mount = ref((node, signal) => {
|
|
2778
|
+
host = node;
|
|
2779
|
+
mounted = true;
|
|
2780
|
+
try {
|
|
2781
|
+
drafts = projectFileDrafts(project.id, preferenceDraftStorage);
|
|
2782
|
+
let saved = readPreference("bud:editor-preferences:v1");
|
|
2783
|
+
preferences = parseEditorPreferences(JSON.parse(typeof saved === "string" ? saved : "null"));
|
|
2784
|
+
} catch (cause) {
|
|
2785
|
+
error = message$1(cause);
|
|
2786
|
+
}
|
|
2787
|
+
if (drafts) {
|
|
2788
|
+
editor = createFileEditorController({
|
|
2789
|
+
signal,
|
|
2790
|
+
drafts,
|
|
2791
|
+
read: (path, signal) => requestFiles(`${routes.filesApi.read.href()}?${new URLSearchParams({ path })}`, project.token, signal, parseFileDocument),
|
|
2792
|
+
save: (body, signal) => requestFiles(routes.fileSavesApi.save.href(), project.token, signal, parseFileSaveResult, fetch, { body }),
|
|
2793
|
+
onChange: changed,
|
|
2794
|
+
onSaved: () => {
|
|
2795
|
+
lastKey = "";
|
|
2796
|
+
changed();
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
handle.props.registerDraftGuard?.(editor.guard, signal);
|
|
2800
|
+
}
|
|
2801
|
+
actions = createFileReviewActionsController({
|
|
2802
|
+
signal,
|
|
2803
|
+
prepare: (body, signal) => requestFiles(routes.fileReviewActionsApi.prepare.href(), project.token, signal, parseReviewActionPlan, fetch, { body }),
|
|
2804
|
+
apply: (body, signal) => requestFiles(routes.fileReviewActionsApi.apply.href(), project.token, signal, parseReviewActionResult, fetch, { body }),
|
|
2805
|
+
recoveries: (signal) => requestFiles(routes.fileReviewActionsApi.recoveries.href(), project.token, signal, parseReviewActionRecoveries),
|
|
2806
|
+
guard(paths) {
|
|
2807
|
+
if (editor?.state?.saving) throw new Error("Wait for the current save to finish before reviewing changes.");
|
|
2808
|
+
if (paths.some((path) => {
|
|
2809
|
+
let draft = drafts?.get(path);
|
|
2810
|
+
return draft && draftDirty(draft);
|
|
2811
|
+
})) throw new Error("Save or explicitly discard the unsaved draft for this file before applying review actions. Your draft has been kept.");
|
|
2812
|
+
},
|
|
2813
|
+
async onApplied(result) {
|
|
2814
|
+
if (result.target === "worktree") editor?.close();
|
|
2815
|
+
if (merge && result.changedPaths.includes(merge.path) && result.target === "index") {
|
|
2816
|
+
merge = void 0;
|
|
2817
|
+
editor?.close();
|
|
2818
|
+
}
|
|
2819
|
+
lastKey = JSON.stringify([
|
|
2820
|
+
context(),
|
|
2821
|
+
position().page,
|
|
2822
|
+
handle.props.path
|
|
2823
|
+
]);
|
|
2824
|
+
refresh = handle.props.refresh;
|
|
2825
|
+
let current = load();
|
|
2826
|
+
while (true) {
|
|
2827
|
+
let active = reading;
|
|
2828
|
+
await current;
|
|
2829
|
+
if (handle.signal.aborted || reading === active || !reading) break;
|
|
2830
|
+
current = reading;
|
|
2831
|
+
}
|
|
2832
|
+
if (error) throw new Error(`The write completed, but the review could not refresh: ${error}`);
|
|
2833
|
+
},
|
|
2834
|
+
onChange: changed
|
|
2835
|
+
});
|
|
2836
|
+
window.addEventListener("beforeunload", (event) => {
|
|
2837
|
+
drafts?.persist();
|
|
2838
|
+
if (drafts?.dirty.length) {
|
|
2839
|
+
event.preventDefault();
|
|
2840
|
+
event.returnValue = "";
|
|
2841
|
+
}
|
|
2842
|
+
}, { signal });
|
|
2843
|
+
try {
|
|
2844
|
+
session = parseReviewSession(readPreference(`bud:review:${project.id}:v1`), project.id);
|
|
2845
|
+
} catch {}
|
|
2846
|
+
readLocation(handle.props.href);
|
|
2847
|
+
renderedHref = handle.props.href;
|
|
2848
|
+
let readTheme = () => {
|
|
2849
|
+
theme = window.document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
|
2850
|
+
changed();
|
|
2851
|
+
};
|
|
2852
|
+
let observer = new MutationObserver(readTheme);
|
|
2853
|
+
observer.observe(window.document.documentElement, {
|
|
2854
|
+
attributes: true,
|
|
2855
|
+
attributeFilter: ["data-theme"]
|
|
2856
|
+
});
|
|
2857
|
+
readTheme();
|
|
2858
|
+
createCodeWorkerPool({
|
|
2859
|
+
signal,
|
|
2860
|
+
renderOptions: {
|
|
2861
|
+
theme: codeThemes,
|
|
2862
|
+
lineDiffType: session.view.highlights
|
|
2863
|
+
}
|
|
2864
|
+
}).then(async (value) => {
|
|
2865
|
+
await value.ready;
|
|
2866
|
+
if (!signal.aborted) {
|
|
2867
|
+
pool = value;
|
|
2868
|
+
poolSettled = true;
|
|
2869
|
+
changed();
|
|
2870
|
+
}
|
|
2871
|
+
}).catch(() => {
|
|
2872
|
+
poolSettled = true;
|
|
2873
|
+
changed();
|
|
2874
|
+
});
|
|
2875
|
+
signal.addEventListener("abort", () => {
|
|
2876
|
+
request?.abort();
|
|
2877
|
+
historyRequest?.abort();
|
|
2878
|
+
observer.disconnect();
|
|
2879
|
+
clearTimeout(persistTimer);
|
|
2880
|
+
pool?.dispose();
|
|
2881
|
+
drafts?.persist();
|
|
2882
|
+
writePreference(`bud:review:${project.id}:v1`, JSON.stringify(session));
|
|
2883
|
+
}, { once: true });
|
|
2884
|
+
});
|
|
2885
|
+
function readLocation(href) {
|
|
2886
|
+
let url = new URL(href);
|
|
2887
|
+
requestedThread = handle.props.conversation?.id ?? null;
|
|
2888
|
+
appliedThread = null;
|
|
2889
|
+
if (!requestedThread) {
|
|
2890
|
+
historical = null;
|
|
2891
|
+
source = null;
|
|
2892
|
+
}
|
|
2893
|
+
let parsed = readReviewLocation(url, project.id);
|
|
2894
|
+
if (readFileLocation(url, project.id).foreign) parsed = { view: {
|
|
2895
|
+
mode: "source",
|
|
2896
|
+
multiple: false
|
|
2897
|
+
} };
|
|
2898
|
+
if (parsed) {
|
|
2899
|
+
session = {
|
|
2900
|
+
...session,
|
|
2901
|
+
view: {
|
|
2902
|
+
...session.view,
|
|
2903
|
+
...parsed.view
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
if (parsed.position) session = rememberReviewPosition(session, reviewContextKey(session.view, parsed.position.selection?.path ?? handle.props.path), parsed.position);
|
|
2907
|
+
}
|
|
2908
|
+
lastKey = "";
|
|
2909
|
+
}
|
|
2910
|
+
return () => {
|
|
2911
|
+
if (mounted) {
|
|
2912
|
+
if (renderedHref !== handle.props.href || renderedConversation !== handle.props.conversation?.id) {
|
|
2913
|
+
renderedHref = handle.props.href;
|
|
2914
|
+
renderedConversation = handle.props.conversation?.id;
|
|
2915
|
+
if (pendingHref !== renderedHref) readLocation(renderedHref);
|
|
2916
|
+
pendingHref = void 0;
|
|
2917
|
+
}
|
|
2918
|
+
let target = handle.props.searchSelection;
|
|
2919
|
+
if (target && target.id !== searchSelectionId) {
|
|
2920
|
+
searchSelectionId = target.id;
|
|
2921
|
+
pendingSearch = target.match;
|
|
2922
|
+
searchNotice = "";
|
|
2923
|
+
handle.queueTask(() => {
|
|
2924
|
+
editor?.close();
|
|
2925
|
+
merge = void 0;
|
|
2926
|
+
changeView({
|
|
2927
|
+
mode: "source",
|
|
2928
|
+
multiple: false
|
|
2929
|
+
});
|
|
2930
|
+
lastKey = "";
|
|
2931
|
+
changed();
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
if (observedPath !== handle.props.path) {
|
|
2935
|
+
observedPath = handle.props.path;
|
|
2936
|
+
merge = void 0;
|
|
2937
|
+
if (historical?.path !== handle.props.path) {
|
|
2938
|
+
historical = null;
|
|
2939
|
+
source = null;
|
|
2940
|
+
}
|
|
2941
|
+
handle.queueTask(() => editor?.closeOtherPath(handle.props.path));
|
|
2942
|
+
}
|
|
2943
|
+
let key = JSON.stringify([
|
|
2944
|
+
context(),
|
|
2945
|
+
position().page,
|
|
2946
|
+
handle.props.path
|
|
2947
|
+
]);
|
|
2948
|
+
if (key !== lastKey || refresh !== handle.props.refresh) {
|
|
2949
|
+
lastKey = key;
|
|
2950
|
+
refresh = handle.props.refresh;
|
|
2951
|
+
handle.queueTask(() => {
|
|
2952
|
+
load();
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
let nextHistoryKey = JSON.stringify([handle.props.path, session.view.mode === "commits" ? session.view.target : handle.props.repository?.head]);
|
|
2956
|
+
if (historyOpen && nextHistoryKey !== historyKey) {
|
|
2957
|
+
historyKey = nextHistoryKey;
|
|
2958
|
+
handle.queueTask(() => {
|
|
2959
|
+
loadHistory();
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
if (pool && appliedHighlights !== session.view.highlights) {
|
|
2963
|
+
appliedHighlights = session.view.highlights;
|
|
2964
|
+
let owned = pool;
|
|
2965
|
+
handle.queueTask(() => {
|
|
2966
|
+
owned.pool.setRenderOptions({ lineDiffType: session.view.highlights }).catch((cause) => {
|
|
2967
|
+
if (!handle.signal.aborted) {
|
|
2968
|
+
error = `Could not update highlighting. ${message$1(cause)}`;
|
|
2969
|
+
changed();
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
let editing = editor?.state;
|
|
2976
|
+
let comments = handle.props.comments;
|
|
2977
|
+
if (selectedThread && comments.ready) {
|
|
2978
|
+
if (!comments.snapshot.threads.find((thread) => thread.id === selectedThread)?.messagesLoaded && !comments.loadingThread(selectedThread)) {
|
|
2979
|
+
let id = selectedThread;
|
|
2980
|
+
handle.queueTask(() => {
|
|
2981
|
+
comments.loadThread(id);
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
let nextSourceSignature = JSON.stringify([
|
|
2986
|
+
document?.entry.path,
|
|
2987
|
+
document?.revision,
|
|
2988
|
+
source?.path,
|
|
2989
|
+
source?.commit,
|
|
2990
|
+
source?.file?.revision,
|
|
2991
|
+
diffs.map((diff) => diff.revision),
|
|
2992
|
+
editing?.key,
|
|
2993
|
+
editing?.draft.baseRevision
|
|
2994
|
+
]);
|
|
2995
|
+
if (nextSourceSignature !== sourceSignature || locationsVersion !== comments.version) {
|
|
2996
|
+
sourceSignature = nextSourceSignature;
|
|
2997
|
+
locationsVersion = comments.version;
|
|
2998
|
+
commentSources = reviewCommentSources(document, diffs, source);
|
|
2999
|
+
commentLocations = locateCodeConversations(comments.snapshot.threads, commentSources);
|
|
3000
|
+
}
|
|
3001
|
+
if (locatedEditor !== editing?.key) {
|
|
3002
|
+
locatedEditor = editing?.key;
|
|
3003
|
+
editorLocations = void 0;
|
|
3004
|
+
}
|
|
3005
|
+
if (editing && editorLocations) commentLocations = [...editorLocations];
|
|
3006
|
+
if (requestedThread && requestedThread !== appliedThread && comments.loaded && !loading && (document || source || snapshot || error)) {
|
|
3007
|
+
let thread = comments.snapshot.threads.find((thread) => thread.id === requestedThread);
|
|
3008
|
+
if (thread) {
|
|
3009
|
+
appliedThread = requestedThread;
|
|
3010
|
+
handle.queueTask(() => selectDiscussion(thread, handle.props.conversation?.original ?? false));
|
|
3011
|
+
} else {
|
|
3012
|
+
panelOpen = true;
|
|
3013
|
+
let id = requestedThread;
|
|
3014
|
+
handle.queueTask(() => {
|
|
3015
|
+
comments.loadThread(id);
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
if (pendingFocus && !loading) {
|
|
3020
|
+
let location = commentLocations.find((entry) => entry.thread.id === pendingFocus);
|
|
3021
|
+
if (location) {
|
|
3022
|
+
pendingFocus = null;
|
|
3023
|
+
let change = codeConversationPosition(location);
|
|
3024
|
+
if (change) handle.queueTask(() => savePosition(change, true));
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
let commentsShown = panelOpen && comments.ready;
|
|
3028
|
+
if (commentsShown !== reportedComments) {
|
|
3029
|
+
reportedComments = commentsShown;
|
|
3030
|
+
handle.queueTask(() => handle.props.onComments?.(commentsShown));
|
|
3031
|
+
}
|
|
3032
|
+
let dirtyPaths = drafts?.dirty.map((draft) => draft.path) ?? [];
|
|
3033
|
+
if (dirtyPaths.join("\0") !== reportedDrafts) {
|
|
3034
|
+
reportedDrafts = dirtyPaths.join("\0");
|
|
3035
|
+
handle.queueTask(() => handle.props.onDrafts?.(dirtyPaths));
|
|
3036
|
+
}
|
|
3037
|
+
let codeComments = {
|
|
3038
|
+
threads: comments.snapshot.threads,
|
|
3039
|
+
locations: commentLocations,
|
|
3040
|
+
version: comments.version,
|
|
3041
|
+
selected: selectedThread,
|
|
3042
|
+
onShow: () => {
|
|
3043
|
+
openComments();
|
|
3044
|
+
changed();
|
|
3045
|
+
},
|
|
3046
|
+
onOpen: (id) => {
|
|
3047
|
+
let thread = comments.snapshot.threads.find((thread) => thread.id === id);
|
|
3048
|
+
if (thread) selectDiscussion(thread);
|
|
3049
|
+
},
|
|
3050
|
+
onCreate: comments.snapshot.capabilities.create ? (selection) => startComment(selection) : void 0,
|
|
3051
|
+
onLocations: (locations) => {
|
|
3052
|
+
if (editor?.state?.key !== editing?.key) return;
|
|
3053
|
+
editorLocations = locations;
|
|
3054
|
+
changed();
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3058
|
+
class: "flex h-full min-h-0 flex-col",
|
|
3059
|
+
mix: mount,
|
|
3060
|
+
children: [
|
|
3061
|
+
(comments.loaded ? !comments.ready : project.storage.threads?.state !== "ready") ? /* @__PURE__ */ jsx(Text, {
|
|
3062
|
+
as: "p",
|
|
3063
|
+
variant: "ui-caption",
|
|
3064
|
+
tone: "muted",
|
|
3065
|
+
role: "status",
|
|
3066
|
+
class: "shrink-0 border-b border-line px-3 py-1.5",
|
|
3067
|
+
children: commentsUnavailable
|
|
3068
|
+
}) : null,
|
|
3069
|
+
searchNotice ? /* @__PURE__ */ jsx(Text, {
|
|
3070
|
+
as: "p",
|
|
3071
|
+
variant: "ui-caption",
|
|
3072
|
+
role: "status",
|
|
3073
|
+
class: "shrink-0 border-b border-line px-3 py-1.5",
|
|
3074
|
+
children: searchNotice
|
|
3075
|
+
}) : null,
|
|
3076
|
+
editor?.openError ? /* @__PURE__ */ jsx(Text, {
|
|
3077
|
+
as: "p",
|
|
3078
|
+
variant: "ui-caption",
|
|
3079
|
+
role: "alert",
|
|
3080
|
+
class: "shrink-0 border-b border-line px-3 py-1.5 text-red-600 dark:text-red-400",
|
|
3081
|
+
children: editor.openError
|
|
3082
|
+
}) : null,
|
|
3083
|
+
editor?.opening ? /* @__PURE__ */ jsx(Text, {
|
|
3084
|
+
as: "p",
|
|
3085
|
+
variant: "ui-caption",
|
|
3086
|
+
role: "status",
|
|
3087
|
+
class: "sr-only",
|
|
3088
|
+
children: "Reading current file for editing…"
|
|
3089
|
+
}) : null,
|
|
3090
|
+
editing && editor && merge?.path === editing.draft.path ? /* @__PURE__ */ jsx(FileConflictPanel, {
|
|
3091
|
+
chrome: handle.props.chrome,
|
|
3092
|
+
file: {
|
|
3093
|
+
name: editing.draft.path,
|
|
3094
|
+
contents: editing.draft.contents,
|
|
3095
|
+
cacheKey: editing.draft.baseRevision
|
|
3096
|
+
},
|
|
3097
|
+
dirty: draftDirty(editing.draft),
|
|
3098
|
+
saving: editing.saving,
|
|
3099
|
+
stale: editing.conflict,
|
|
3100
|
+
error: editing.error,
|
|
3101
|
+
status: editing.status,
|
|
3102
|
+
theme,
|
|
3103
|
+
workerPool: pool?.pool,
|
|
3104
|
+
onChange: editor.change,
|
|
3105
|
+
onSave: () => {
|
|
3106
|
+
editor?.save();
|
|
3107
|
+
},
|
|
3108
|
+
onMarkResolved: () => {
|
|
3109
|
+
if (merge) actions?.prepare({
|
|
3110
|
+
kind: "mark-resolved",
|
|
3111
|
+
path: merge.path,
|
|
3112
|
+
expectedRevision: editing.draft.baseRevision,
|
|
3113
|
+
conflict: merge.conflict
|
|
3114
|
+
});
|
|
3115
|
+
},
|
|
3116
|
+
onEdit: () => {
|
|
3117
|
+
merge = void 0;
|
|
3118
|
+
editor?.begin(editing.draft.path);
|
|
3119
|
+
changed();
|
|
3120
|
+
},
|
|
3121
|
+
onClose: () => {
|
|
3122
|
+
merge = void 0;
|
|
3123
|
+
editor?.close();
|
|
3124
|
+
lastKey = "";
|
|
3125
|
+
changed();
|
|
3126
|
+
}
|
|
3127
|
+
}, `merge:${editing.key}`) : editing && editor ? /* @__PURE__ */ jsx(FileEditorPanel, {
|
|
3128
|
+
chrome: handle.props.chrome,
|
|
3129
|
+
file: editing.file,
|
|
3130
|
+
oldFile: editing.oldFile,
|
|
3131
|
+
initialMode: editing.mode,
|
|
3132
|
+
sessionKey: editing.key,
|
|
3133
|
+
comments: comments.ready ? {
|
|
3134
|
+
...codeComments,
|
|
3135
|
+
onCreate: comments.snapshot.capabilities.create ? (selection) => startComment(selection, true) : void 0
|
|
3136
|
+
} : void 0,
|
|
3137
|
+
commentRevision: editing.draft.baseRevision,
|
|
3138
|
+
baselineLabel: editing.baselineLabel,
|
|
3139
|
+
dirty: draftDirty(editing.draft),
|
|
3140
|
+
saving: editing.saving,
|
|
3141
|
+
conflict: editing.conflict,
|
|
3142
|
+
reviewingConflict: Boolean(editing.latest),
|
|
3143
|
+
status: editing.status,
|
|
3144
|
+
error: editing.error,
|
|
3145
|
+
storageWarning: drafts?.warning ?? "",
|
|
3146
|
+
preferences,
|
|
3147
|
+
theme,
|
|
3148
|
+
workerPool: pool?.pool,
|
|
3149
|
+
onChange: editor.change,
|
|
3150
|
+
onSave: () => {
|
|
3151
|
+
editor?.save();
|
|
3152
|
+
},
|
|
3153
|
+
onClose: () => {
|
|
3154
|
+
editor?.close();
|
|
3155
|
+
lastKey = "";
|
|
3156
|
+
changed();
|
|
3157
|
+
},
|
|
3158
|
+
onDiscard: () => {
|
|
3159
|
+
editor?.discard();
|
|
3160
|
+
},
|
|
3161
|
+
onDownload: downloadDraft,
|
|
3162
|
+
onCheckLatest: () => {
|
|
3163
|
+
editor?.checkLatest();
|
|
3164
|
+
},
|
|
3165
|
+
onAcceptMerge: editor.acceptMerge,
|
|
3166
|
+
onPreferences: setPreferences
|
|
3167
|
+
}, editing.key) : /* @__PURE__ */ jsx(FileReviewPanel, {
|
|
3168
|
+
chrome: handle.props.chrome,
|
|
3169
|
+
path: handle.props.path,
|
|
3170
|
+
repository: handle.props.repository,
|
|
3171
|
+
contextKey: context(),
|
|
3172
|
+
view: session.view,
|
|
3173
|
+
position: position(),
|
|
3174
|
+
document: poolSettled ? document : null,
|
|
3175
|
+
source: poolSettled ? source : null,
|
|
3176
|
+
comments: comments.ready ? codeComments : void 0,
|
|
3177
|
+
snapshot,
|
|
3178
|
+
diffs: poolSettled ? diffs : [],
|
|
3179
|
+
failures,
|
|
3180
|
+
loading: loading || !poolSettled,
|
|
3181
|
+
error,
|
|
3182
|
+
history,
|
|
3183
|
+
historyOpen,
|
|
3184
|
+
historyLoading,
|
|
3185
|
+
historyError,
|
|
3186
|
+
theme,
|
|
3187
|
+
workerPool: pool?.pool,
|
|
3188
|
+
onEdit: project.files.edit && editor && handle.props.path && !historical ? startEditor : void 0,
|
|
3189
|
+
onReviewAction: actions ? (intent) => {
|
|
3190
|
+
actions?.prepare(intent);
|
|
3191
|
+
} : void 0,
|
|
3192
|
+
onReviewUndo: actions ? () => {
|
|
3193
|
+
actions?.recoveries();
|
|
3194
|
+
} : void 0,
|
|
3195
|
+
onResolve: (diff) => {
|
|
3196
|
+
resolveConflict(diff);
|
|
3197
|
+
},
|
|
3198
|
+
editLabel: handle.props.path && drafts?.get(handle.props.path) && draftDirty(drafts.get(handle.props.path)) ? "Resume draft" : "Edit file",
|
|
3199
|
+
onChange: changeView,
|
|
3200
|
+
onPosition: savePosition,
|
|
3201
|
+
onSelect: selectPath,
|
|
3202
|
+
onPage: (page) => {
|
|
3203
|
+
savePosition({
|
|
3204
|
+
...defaultReviewPosition(),
|
|
3205
|
+
page
|
|
3206
|
+
}, true);
|
|
3207
|
+
},
|
|
3208
|
+
onRetry: () => {
|
|
3209
|
+
load();
|
|
3210
|
+
},
|
|
3211
|
+
onHistory: () => {
|
|
3212
|
+
historyOpen = !historyOpen;
|
|
3213
|
+
if (historyOpen) historyKey = "";
|
|
3214
|
+
else historyRequest?.abort();
|
|
3215
|
+
changed();
|
|
3216
|
+
},
|
|
3217
|
+
onHistoryPage: (older) => {
|
|
3218
|
+
loadHistory(older);
|
|
3219
|
+
},
|
|
3220
|
+
onHistoryRevision: historyRevision,
|
|
3221
|
+
onCopyLink: async (position) => {
|
|
3222
|
+
let path = position.selection?.path ?? handle.props.path;
|
|
3223
|
+
let view = snapshot?.comparison.mode === "commits" ? {
|
|
3224
|
+
...session.view,
|
|
3225
|
+
...snapshot.comparison
|
|
3226
|
+
} : session.view;
|
|
3227
|
+
let href = reviewHref(path, view, position);
|
|
3228
|
+
await navigator.clipboard.writeText(new URL(href, window.location.origin).href);
|
|
3229
|
+
}
|
|
3230
|
+
}),
|
|
3231
|
+
panelOpen && comments.ready ? /* @__PURE__ */ jsx(ConversationSidebar, {
|
|
3232
|
+
threads: comments.snapshot.threads.filter(isCodeConversation),
|
|
3233
|
+
renderThreadLinks: (thread) => /* @__PURE__ */ jsx(ProjectThreadLinks, {
|
|
3234
|
+
part: "links",
|
|
3235
|
+
project,
|
|
3236
|
+
thread,
|
|
3237
|
+
canLink: comments.snapshot.capabilities.link,
|
|
3238
|
+
busy: comments.busy,
|
|
3239
|
+
onChange: async () => {
|
|
3240
|
+
await comments.load();
|
|
3241
|
+
await comments.loadThread(thread.id, true);
|
|
3242
|
+
}
|
|
3243
|
+
}),
|
|
3244
|
+
renderThreadActions: (thread) => /* @__PURE__ */ jsx(ProjectThreadLinks, {
|
|
3245
|
+
part: "action",
|
|
3246
|
+
project,
|
|
3247
|
+
thread,
|
|
3248
|
+
canLink: comments.snapshot.capabilities.link,
|
|
3249
|
+
busy: comments.busy,
|
|
3250
|
+
onChange: async () => {
|
|
3251
|
+
await comments.load();
|
|
3252
|
+
await comments.loadThread(thread.id, true);
|
|
3253
|
+
}
|
|
3254
|
+
}),
|
|
3255
|
+
capabilities: comments.snapshot.capabilities,
|
|
3256
|
+
selected: selectedThread,
|
|
3257
|
+
currentPath: handle.props.path,
|
|
3258
|
+
locations: commentLocations,
|
|
3259
|
+
draft: draftAnchor,
|
|
3260
|
+
loaded: comments.loaded,
|
|
3261
|
+
busy: comments.busy,
|
|
3262
|
+
now: comments.now,
|
|
3263
|
+
error: commentError || comments.error,
|
|
3264
|
+
onSelect: (thread) => selectDiscussion(thread),
|
|
3265
|
+
onCloseThread: () => {
|
|
3266
|
+
selectedThread = void 0;
|
|
3267
|
+
changed();
|
|
3268
|
+
},
|
|
3269
|
+
onClose: closeComments,
|
|
3270
|
+
onWrite: comments.write,
|
|
3271
|
+
onCreate: createComment,
|
|
3272
|
+
onCancelDraft: () => {
|
|
3273
|
+
draftAnchor = void 0;
|
|
3274
|
+
changed();
|
|
3275
|
+
},
|
|
3276
|
+
onReload: () => {
|
|
3277
|
+
comments.load();
|
|
3278
|
+
},
|
|
3279
|
+
onSync: comments.snapshot.capabilities.sync ? () => {
|
|
3280
|
+
comments.sync().catch(() => {});
|
|
3281
|
+
} : void 0,
|
|
3282
|
+
onOriginal: (thread) => selectDiscussion(thread, true),
|
|
3283
|
+
onCopyLink: (thread) => navigator.clipboard.writeText(new URL(conversationHref(thread.id), window.location.origin).href)
|
|
3284
|
+
}) : null,
|
|
3285
|
+
actions?.state ? /* @__PURE__ */ jsx(ReviewActionDialog, {
|
|
3286
|
+
state: actions.state,
|
|
3287
|
+
theme,
|
|
3288
|
+
onApply: () => {
|
|
3289
|
+
actions?.apply();
|
|
3290
|
+
},
|
|
3291
|
+
onClose: actions.close,
|
|
3292
|
+
onUndo: (recoveryId) => {
|
|
3293
|
+
actions?.prepare({
|
|
3294
|
+
kind: "undo",
|
|
3295
|
+
recoveryId
|
|
3296
|
+
});
|
|
3297
|
+
},
|
|
3298
|
+
onRecoveries: () => {
|
|
3299
|
+
actions?.recoveries();
|
|
3300
|
+
}
|
|
3301
|
+
}) : null
|
|
3302
|
+
]
|
|
3303
|
+
});
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
//#endregion
|
|
3307
|
+
//#region app/client/project-files.tsx
|
|
3308
|
+
function message(error) {
|
|
3309
|
+
return error instanceof Error ? error.message : "Could not load project files.";
|
|
3310
|
+
}
|
|
3311
|
+
function requestProjectFiles(token, view, signal) {
|
|
3312
|
+
let params = new URLSearchParams({
|
|
3313
|
+
includeIgnored: String(view.includeIgnored),
|
|
3314
|
+
includeDotfiles: "true"
|
|
3315
|
+
});
|
|
3316
|
+
return Promise.all([requestFiles(`${routes.filesApi.index.href()}?${params}`, token, signal, parseFileIndex), requestFiles(routes.fileReviewApi.repository.href(), token, signal, parseReviewRepository)]);
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Reads the stored view, the file the URL asks for and the first listing for the screen's load.
|
|
3320
|
+
* A listing this browser stored for the same view shows at once.
|
|
3321
|
+
* @param project - The connected project.
|
|
3322
|
+
* @param signal - The screen's lifetime.
|
|
3323
|
+
* @param url - The screen's initial typed location.
|
|
3324
|
+
* @returns The listing, or the error the workspace shows until a refresh succeeds.
|
|
3325
|
+
*/
|
|
3326
|
+
async function loadFiles(project, signal, url) {
|
|
3327
|
+
let view;
|
|
3328
|
+
try {
|
|
3329
|
+
view = parseFileView(readPreference(`bud:files:${project.id}:v1`), project.id);
|
|
3330
|
+
} catch {
|
|
3331
|
+
view = defaultFileView(project.id);
|
|
3332
|
+
}
|
|
3333
|
+
let location = readFileLocation(url, project.id);
|
|
3334
|
+
if (location.explicit) view = rememberFile(view, location.path);
|
|
3335
|
+
let store = await projectStore(project);
|
|
3336
|
+
let stored = storedFiles(store);
|
|
3337
|
+
if (stored && stored.includeIgnored === view.includeIgnored) {
|
|
3338
|
+
let { index, repository } = stored;
|
|
3339
|
+
view = reconcileFileView(view, null, index, repository);
|
|
3340
|
+
return {
|
|
3341
|
+
view,
|
|
3342
|
+
index,
|
|
3343
|
+
repository,
|
|
3344
|
+
error: "",
|
|
3345
|
+
store,
|
|
3346
|
+
stored: true
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
try {
|
|
3350
|
+
let [index, repository] = await requestProjectFiles(project.token, view, signal);
|
|
3351
|
+
await saveFiles(store, index, repository, view.includeIgnored);
|
|
3352
|
+
return {
|
|
3353
|
+
view: reconcileFileView(view, null, index, repository),
|
|
3354
|
+
index,
|
|
3355
|
+
repository,
|
|
3356
|
+
error: "",
|
|
3357
|
+
store,
|
|
3358
|
+
stored: false
|
|
3359
|
+
};
|
|
3360
|
+
} catch (cause) {
|
|
3361
|
+
signal.throwIfAborted();
|
|
3362
|
+
return {
|
|
3363
|
+
view,
|
|
3364
|
+
index: null,
|
|
3365
|
+
repository: null,
|
|
3366
|
+
error: message(cause),
|
|
3367
|
+
store,
|
|
3368
|
+
stored: false
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* The server capabilities and browser persistence stay outside the shared navigator.
|
|
3374
|
+
* @param handle - The project, its first listing and the component's owned lifecycle.
|
|
3375
|
+
* @returns The component render function.
|
|
3376
|
+
*/
|
|
3377
|
+
function ProjectFiles(handle) {
|
|
3378
|
+
let project = handle.props.project;
|
|
3379
|
+
let { view, index, repository, error, store } = handle.props.initial;
|
|
3380
|
+
let listed = () => {
|
|
3381
|
+
let next = storedFiles(store);
|
|
3382
|
+
if (!next || next.includeIgnored !== view.includeIgnored) return;
|
|
3383
|
+
let previous = view.selected;
|
|
3384
|
+
let retained = view.selected !== null && drafted(view.selected);
|
|
3385
|
+
view = reconcileFileView(view, retained ? null : index, next.index, next.repository);
|
|
3386
|
+
index = next.index;
|
|
3387
|
+
repository = next.repository;
|
|
3388
|
+
if (previous !== view.selected) location(true);
|
|
3389
|
+
changed();
|
|
3390
|
+
};
|
|
3391
|
+
function drafted(path) {
|
|
3392
|
+
try {
|
|
3393
|
+
return projectFileDrafts(project.id, preferenceDraftStorage).dirty.some((draft) => draft.path === path);
|
|
3394
|
+
} catch {
|
|
3395
|
+
return false;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
let subscriptions = [
|
|
3399
|
+
store.listings.subscribeChanges(listed),
|
|
3400
|
+
store.files.subscribeChanges(listed),
|
|
3401
|
+
store.repositories.subscribeChanges(listed)
|
|
3402
|
+
];
|
|
3403
|
+
let loading = false;
|
|
3404
|
+
let renderedHref = handle.props.href;
|
|
3405
|
+
let pendingHref;
|
|
3406
|
+
let deferredLocation = false;
|
|
3407
|
+
let notice = "";
|
|
3408
|
+
let listRequest;
|
|
3409
|
+
let storageTimer;
|
|
3410
|
+
let refreshTimer;
|
|
3411
|
+
let mounted = false;
|
|
3412
|
+
let refreshing = false;
|
|
3413
|
+
let trailingRefresh = false;
|
|
3414
|
+
let settling = false;
|
|
3415
|
+
let pendingContentRefresh = false;
|
|
3416
|
+
let contentRefresh = 0;
|
|
3417
|
+
let searchOpen = false, searchSerial = 0;
|
|
3418
|
+
let searchSelection;
|
|
3419
|
+
let dirty = [];
|
|
3420
|
+
let commentsOpen = false;
|
|
3421
|
+
let search = createFileSearchController({
|
|
3422
|
+
signal: handle.signal,
|
|
3423
|
+
onChange: changed,
|
|
3424
|
+
request: (query, signal) => requestFiles(`${routes.fileSearchApi.search.href()}?${fileSearchParameters(query)}`, project.token, signal, parseFileSearchResult)
|
|
3425
|
+
});
|
|
3426
|
+
function closeSearch() {
|
|
3427
|
+
search.cancel("");
|
|
3428
|
+
searchOpen = false;
|
|
3429
|
+
changed();
|
|
3430
|
+
}
|
|
3431
|
+
let guards = createFileMutationGuards();
|
|
3432
|
+
let comments = createProjectComments({
|
|
3433
|
+
store: projectStore(project),
|
|
3434
|
+
token: project.token,
|
|
3435
|
+
signal: handle.signal,
|
|
3436
|
+
onChange: changed
|
|
3437
|
+
});
|
|
3438
|
+
let countedVersion;
|
|
3439
|
+
let commentCounts = {};
|
|
3440
|
+
let mutations = createFileMutationController({
|
|
3441
|
+
signal: handle.signal,
|
|
3442
|
+
index: () => index,
|
|
3443
|
+
guards,
|
|
3444
|
+
onChange() {
|
|
3445
|
+
if (mutations.state?.phase === "applying") {
|
|
3446
|
+
listRequest?.abort();
|
|
3447
|
+
settling = true;
|
|
3448
|
+
}
|
|
3449
|
+
changed();
|
|
3450
|
+
},
|
|
3451
|
+
transport: {
|
|
3452
|
+
prepare: (intent, signal) => requestFiles(routes.fileMutationsApi.prepare.href(), project.token, signal, parseMutationPlan, fetch, { body: intent }),
|
|
3453
|
+
apply: (plan, signal) => requestFiles(routes.fileMutationsApi.apply.href(), project.token, signal, parseMutationResult, fetch, { body: {
|
|
3454
|
+
id: plan.id,
|
|
3455
|
+
revision: plan.revision
|
|
3456
|
+
} }),
|
|
3457
|
+
recoveries: (signal) => requestFiles(routes.fileMutationsApi.recoveries.href(), project.token, signal, parseRecoveries)
|
|
3458
|
+
},
|
|
3459
|
+
async onApplied(intent, result) {
|
|
3460
|
+
view = mutationFileView(view, intent.kind, result);
|
|
3461
|
+
let count = result.items.filter((item) => item.status === "applied").length;
|
|
3462
|
+
if (count) toast.success(`${count} file action${count === 1 ? "" : "s"} completed`, { description: result.recoveryId ? "Original contents are available in Recovery." : void 0 });
|
|
3463
|
+
else toast.error("The file action could not be applied", { description: "Your project was refreshed." });
|
|
3464
|
+
location(true);
|
|
3465
|
+
persist();
|
|
3466
|
+
pendingContentRefresh = true;
|
|
3467
|
+
await load(true);
|
|
3468
|
+
settling = false;
|
|
3469
|
+
},
|
|
3470
|
+
async onUncertain() {
|
|
3471
|
+
pendingContentRefresh = true;
|
|
3472
|
+
await load(true);
|
|
3473
|
+
settling = false;
|
|
3474
|
+
}
|
|
3475
|
+
});
|
|
3476
|
+
function changed() {
|
|
3477
|
+
if (!handle.signal.aborted) handle.update();
|
|
3478
|
+
}
|
|
3479
|
+
function persist() {
|
|
3480
|
+
clearTimeout(storageTimer);
|
|
3481
|
+
storageTimer = setTimeout(() => writePreference(`bud:files:${project.id}:v1`, JSON.stringify(view)), 200);
|
|
3482
|
+
}
|
|
3483
|
+
function location(replace) {
|
|
3484
|
+
let root = fileHref(null);
|
|
3485
|
+
let conversation = readConversationLocation(window.location.href) === handle.props.conversation?.id;
|
|
3486
|
+
if (!handle.props.active || !conversation && window.location.pathname !== root && !window.location.pathname.startsWith(`${root}/`)) {
|
|
3487
|
+
deferredLocation = true;
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
deferredLocation = false;
|
|
3491
|
+
if (conversation && replace) return;
|
|
3492
|
+
let review = readReviewLocation(conversation ? handle.props.href : window.location.href, project.id);
|
|
3493
|
+
let href = review?.view.mode && review.view.mode !== "source" ? reviewHref(view.selected, {
|
|
3494
|
+
...defaultReviewView(),
|
|
3495
|
+
...review.view
|
|
3496
|
+
}) : fileHref(view.selected);
|
|
3497
|
+
if (window.location.pathname + window.location.search + window.location.hash === href) return;
|
|
3498
|
+
pendingHref = new URL(href, window.location.origin).href;
|
|
3499
|
+
window.history[replace ? "replaceState" : "pushState"]({
|
|
3500
|
+
...window.history.state,
|
|
3501
|
+
budFile: view.selected
|
|
3502
|
+
}, "", href);
|
|
3503
|
+
}
|
|
3504
|
+
function updateView(change) {
|
|
3505
|
+
let next = {
|
|
3506
|
+
...view,
|
|
3507
|
+
...change
|
|
3508
|
+
};
|
|
3509
|
+
let includeChanged = next.includeIgnored !== view.includeIgnored;
|
|
3510
|
+
view = next;
|
|
3511
|
+
persist();
|
|
3512
|
+
changed();
|
|
3513
|
+
if (includeChanged) scheduleRefresh();
|
|
3514
|
+
}
|
|
3515
|
+
function select(path, selection) {
|
|
3516
|
+
let previous = view.selected;
|
|
3517
|
+
view = rememberFile(view, path, selection);
|
|
3518
|
+
notice = "";
|
|
3519
|
+
persist();
|
|
3520
|
+
if (previous !== path) location(false);
|
|
3521
|
+
changed();
|
|
3522
|
+
}
|
|
3523
|
+
function closeTab(path) {
|
|
3524
|
+
if (dirty.includes(path)) {
|
|
3525
|
+
select(path, [path]);
|
|
3526
|
+
toast("Save or discard this draft to close its tab");
|
|
3527
|
+
changed();
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
let previous = view.selected;
|
|
3531
|
+
view = closeFileTab(view, path);
|
|
3532
|
+
persist();
|
|
3533
|
+
if (previous !== view.selected) location(false);
|
|
3534
|
+
changed();
|
|
3535
|
+
}
|
|
3536
|
+
async function load(afterMutation = false) {
|
|
3537
|
+
if (!afterMutation && mutations.state?.phase === "applying") {
|
|
3538
|
+
trailingRefresh = true;
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
if (refreshing) {
|
|
3542
|
+
trailingRefresh = true;
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
refreshing = true;
|
|
3546
|
+
loading = true;
|
|
3547
|
+
error = "";
|
|
3548
|
+
let controller = new AbortController();
|
|
3549
|
+
listRequest = controller;
|
|
3550
|
+
let signal = AbortSignal.any([handle.signal, controller.signal]);
|
|
3551
|
+
changed();
|
|
3552
|
+
try {
|
|
3553
|
+
let [nextIndex, nextRepository] = await requestProjectFiles(project.token, view, signal);
|
|
3554
|
+
if (signal.aborted) return;
|
|
3555
|
+
let previous = view.selected;
|
|
3556
|
+
view = reconcileFileView(view, index, nextIndex, nextRepository);
|
|
3557
|
+
index = nextIndex;
|
|
3558
|
+
repository = nextRepository;
|
|
3559
|
+
await saveFiles(store, nextIndex, nextRepository, view.includeIgnored);
|
|
3560
|
+
persist();
|
|
3561
|
+
if (previous !== view.selected) location(true);
|
|
3562
|
+
if (pendingContentRefresh) contentRefresh++;
|
|
3563
|
+
pendingContentRefresh = false;
|
|
3564
|
+
} catch (cause) {
|
|
3565
|
+
if (!signal.aborted) error = message(cause);
|
|
3566
|
+
} finally {
|
|
3567
|
+
controller.abort();
|
|
3568
|
+
refreshing = false;
|
|
3569
|
+
loading = false;
|
|
3570
|
+
changed();
|
|
3571
|
+
if (trailingRefresh && !handle.signal.aborted) {
|
|
3572
|
+
trailingRefresh = false;
|
|
3573
|
+
scheduleRefresh();
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
function scheduleRefresh() {
|
|
3578
|
+
clearTimeout(refreshTimer);
|
|
3579
|
+
refreshTimer = setTimeout(() => {
|
|
3580
|
+
load();
|
|
3581
|
+
}, 100);
|
|
3582
|
+
}
|
|
3583
|
+
async function refresh() {
|
|
3584
|
+
if (loading) return;
|
|
3585
|
+
let controller = new AbortController();
|
|
3586
|
+
try {
|
|
3587
|
+
await requestFiles(routes.filesApi.refresh.href(), project.token, AbortSignal.any([handle.signal, controller.signal]), parseFileChange);
|
|
3588
|
+
pendingContentRefresh = true;
|
|
3589
|
+
await load();
|
|
3590
|
+
} catch (cause) {
|
|
3591
|
+
if (!handle.signal.aborted) {
|
|
3592
|
+
error = message(cause);
|
|
3593
|
+
changed();
|
|
3594
|
+
}
|
|
3595
|
+
} finally {
|
|
3596
|
+
controller.abort();
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
let mount = ref((_node, signal) => {
|
|
3600
|
+
if (mounted) return;
|
|
3601
|
+
mounted = true;
|
|
3602
|
+
let opened = readFileLocation(handle.props.href, project.id);
|
|
3603
|
+
foreignNotice(opened.foreign);
|
|
3604
|
+
if (!opened.foreign && (opened.explicit ? opened.path !== view.selected : !view.selected)) location(true);
|
|
3605
|
+
persist();
|
|
3606
|
+
let stopFiles = projectLive(project.token).subscribe("files", {
|
|
3607
|
+
onEvent(data) {
|
|
3608
|
+
let update = parseFileUpdate(data);
|
|
3609
|
+
pendingContentRefresh = true;
|
|
3610
|
+
if (settling) {
|
|
3611
|
+
trailingRefresh = true;
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (update.change.reset || refreshing || view.includeIgnored) return scheduleRefresh();
|
|
3615
|
+
applyFileUpdate(store, update).then(() => {
|
|
3616
|
+
contentRefresh++;
|
|
3617
|
+
pendingContentRefresh = false;
|
|
3618
|
+
changed();
|
|
3619
|
+
});
|
|
3620
|
+
},
|
|
3621
|
+
onResync() {
|
|
3622
|
+
pendingContentRefresh = true;
|
|
3623
|
+
scheduleRefresh();
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
if (handle.props.initial.stored) scheduleRefresh();
|
|
3627
|
+
comments.load();
|
|
3628
|
+
signal.addEventListener("abort", () => {
|
|
3629
|
+
stopFiles();
|
|
3630
|
+
for (let subscription of subscriptions) subscription.unsubscribe();
|
|
3631
|
+
listRequest?.abort();
|
|
3632
|
+
clearTimeout(storageTimer);
|
|
3633
|
+
clearTimeout(refreshTimer);
|
|
3634
|
+
writePreference(`bud:files:${project.id}:v1`, JSON.stringify(view));
|
|
3635
|
+
}, { once: true });
|
|
3636
|
+
});
|
|
3637
|
+
function foreignNotice(foreign) {
|
|
3638
|
+
notice = foreign ? "This file link belongs to another project. Choose a file from this project to continue." : "";
|
|
3639
|
+
}
|
|
3640
|
+
return () => {
|
|
3641
|
+
if (renderedHref !== handle.props.href) {
|
|
3642
|
+
renderedHref = handle.props.href;
|
|
3643
|
+
if (pendingHref !== renderedHref) {
|
|
3644
|
+
deferredLocation = false;
|
|
3645
|
+
let selected = readFileLocation(renderedHref, project.id);
|
|
3646
|
+
if (selected.explicit) view = rememberFile(view, selected.path);
|
|
3647
|
+
foreignNotice(selected.foreign);
|
|
3648
|
+
}
|
|
3649
|
+
pendingHref = void 0;
|
|
3650
|
+
}
|
|
3651
|
+
if (deferredLocation && handle.props.active) handle.queueTask(() => location(true));
|
|
3652
|
+
if (countedVersion !== comments.version) {
|
|
3653
|
+
countedVersion = comments.version;
|
|
3654
|
+
commentCounts = codeCommentCounts(comments.snapshot.threads);
|
|
3655
|
+
}
|
|
3656
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3657
|
+
class: `relative h-full min-h-0 ${commentsOpen ? "md:pe-80" : ""}`,
|
|
3658
|
+
mix: mount,
|
|
3659
|
+
children: [
|
|
3660
|
+
/* @__PURE__ */ jsx(FileWorkspace, {
|
|
3661
|
+
projectName: project.name,
|
|
3662
|
+
view,
|
|
3663
|
+
index,
|
|
3664
|
+
repository,
|
|
3665
|
+
loading,
|
|
3666
|
+
error,
|
|
3667
|
+
notice,
|
|
3668
|
+
commentCounts,
|
|
3669
|
+
dirty,
|
|
3670
|
+
onViewChange: updateView,
|
|
3671
|
+
onSelect: select,
|
|
3672
|
+
onCloseTab: closeTab,
|
|
3673
|
+
onSearch: () => {
|
|
3674
|
+
searchOpen = true;
|
|
3675
|
+
changed();
|
|
3676
|
+
},
|
|
3677
|
+
onRefresh: () => {
|
|
3678
|
+
refresh();
|
|
3679
|
+
},
|
|
3680
|
+
mutations: project.files.edit ? {
|
|
3681
|
+
busy: mutations.busy,
|
|
3682
|
+
open: (request) => mutations.open(request),
|
|
3683
|
+
move: (entries, source) => mutations.open({
|
|
3684
|
+
action: source === "rename" ? "rename" : "move",
|
|
3685
|
+
paths: entries.map((entry) => entry.path),
|
|
3686
|
+
entries
|
|
3687
|
+
}, true),
|
|
3688
|
+
recoveries: () => mutations.recoveries()
|
|
3689
|
+
} : void 0,
|
|
3690
|
+
sidebar: searchOpen ? /* @__PURE__ */ jsx(FileSearchPanel, {
|
|
3691
|
+
...search.state,
|
|
3692
|
+
onSearch: (query) => {
|
|
3693
|
+
search.search(query);
|
|
3694
|
+
},
|
|
3695
|
+
onCancel: () => search.cancel(),
|
|
3696
|
+
onClose: closeSearch,
|
|
3697
|
+
onSelect: (match) => {
|
|
3698
|
+
searchSelection = {
|
|
3699
|
+
id: ++searchSerial,
|
|
3700
|
+
match
|
|
3701
|
+
};
|
|
3702
|
+
select(match.path, [match.path]);
|
|
3703
|
+
if (!window.matchMedia("(min-width: 768px)").matches) closeSearch();
|
|
3704
|
+
}
|
|
3705
|
+
}) : void 0,
|
|
3706
|
+
content: (chrome) => /* @__PURE__ */ jsx(ProjectFileReview, {
|
|
3707
|
+
project,
|
|
3708
|
+
href: handle.props.href,
|
|
3709
|
+
path: view.selected,
|
|
3710
|
+
repository,
|
|
3711
|
+
refresh: contentRefresh,
|
|
3712
|
+
store,
|
|
3713
|
+
chrome,
|
|
3714
|
+
onSelect: (path) => select(path, [path]),
|
|
3715
|
+
onComments: (open) => {
|
|
3716
|
+
commentsOpen = open;
|
|
3717
|
+
changed();
|
|
3718
|
+
},
|
|
3719
|
+
onDrafts: (paths) => {
|
|
3720
|
+
if (paths.join("\0") === dirty.join("\0")) return;
|
|
3721
|
+
dirty = paths;
|
|
3722
|
+
changed();
|
|
3723
|
+
},
|
|
3724
|
+
registerDraftGuard: guards.register,
|
|
3725
|
+
comments,
|
|
3726
|
+
conversation: handle.props.conversation,
|
|
3727
|
+
searchSelection
|
|
3728
|
+
})
|
|
3729
|
+
}),
|
|
3730
|
+
mutations.state ? /* @__PURE__ */ jsx(FileMutationDialog, {
|
|
3731
|
+
state: mutations.state,
|
|
3732
|
+
onDestination: mutations.destination,
|
|
3733
|
+
onSubmit: () => {
|
|
3734
|
+
mutations.submit();
|
|
3735
|
+
},
|
|
3736
|
+
onClose: mutations.close,
|
|
3737
|
+
onRecovery: mutations.recoveries
|
|
3738
|
+
}) : null,
|
|
3739
|
+
mutations.recovery.open ? /* @__PURE__ */ jsx(FileRecoveryDialog, {
|
|
3740
|
+
recoveries: mutations.recovery.recoveries,
|
|
3741
|
+
loading: mutations.recovery.loading,
|
|
3742
|
+
error: mutations.recovery.error,
|
|
3743
|
+
onClose: mutations.closeRecoveries,
|
|
3744
|
+
onRefresh: () => {
|
|
3745
|
+
mutations.refreshRecoveries();
|
|
3746
|
+
},
|
|
3747
|
+
onRestore: (recoveryId, paths) => mutations.open({
|
|
3748
|
+
action: "restore",
|
|
3749
|
+
recoveryId,
|
|
3750
|
+
paths
|
|
3751
|
+
})
|
|
3752
|
+
}) : null
|
|
3753
|
+
]
|
|
3754
|
+
});
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
//#endregion
|
|
3758
|
+
//#region app/screens/files.tsx
|
|
3759
|
+
var files_default = defineScreen({
|
|
3760
|
+
routes: fileRoutes,
|
|
3761
|
+
load: ({ project, signal, url }) => loadFiles(project, signal, url),
|
|
3762
|
+
render: (files, { project, url, active, conversation }) => /* @__PURE__ */ jsx(ProjectFiles, {
|
|
3763
|
+
project,
|
|
3764
|
+
initial: files,
|
|
3765
|
+
href: url.href,
|
|
3766
|
+
active,
|
|
3767
|
+
conversation
|
|
3768
|
+
})
|
|
3769
|
+
});
|
|
3770
|
+
//#endregion
|
|
3771
|
+
export { files_default as default };
|