@thaut/claude-ide 0.0.0-stage → 1.0.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/bin/claude-ide.js +23 -0
- package/package.json +75 -4
- package/server/add-dirs.ts +126 -0
- package/server/auth.test.ts +148 -0
- package/server/auth.ts +235 -0
- package/server/bang.ts +54 -0
- package/server/btw-instructions.ts +19 -0
- package/server/btw-policy.ts +17 -0
- package/server/btw.ts +143 -0
- package/server/child-env.ts +29 -0
- package/server/cli-control.ts +255 -0
- package/server/cli-files.ts +55 -0
- package/server/cli-info.ts +73 -0
- package/server/config/account.ts +65 -0
- package/server/config/agents.ts +186 -0
- package/server/config/mcp.ts +86 -0
- package/server/config/memory.ts +193 -0
- package/server/config/paths.ts +156 -0
- package/server/config/plugins.ts +204 -0
- package/server/config/routes.ts +95 -0
- package/server/config/settings.ts +246 -0
- package/server/config.ts +46 -0
- package/server/db.ts +121 -0
- package/server/directories.test.ts +180 -0
- package/server/directories.ts +87 -0
- package/server/dist-version.ts +69 -0
- package/server/durable-tasks.ts +61 -0
- package/server/file-transfers.ts +79 -0
- package/server/files.test.ts +127 -0
- package/server/files.ts +386 -0
- package/server/fs-queue.ts +14 -0
- package/server/fsutil.ts +144 -0
- package/server/git.ts +86 -0
- package/server/goal-state.test.ts +64 -0
- package/server/goal-state.ts +78 -0
- package/server/history-pages.test.ts +619 -0
- package/server/history-pages.ts +513 -0
- package/server/http-channel-manager.ts +468 -0
- package/server/http-channel-pull.test.ts +314 -0
- package/server/http-channel-push.test.ts +364 -0
- package/server/http-channel-registration.test.ts +291 -0
- package/server/http-channel-routes.ts +73 -0
- package/server/http-channel-store.ts +142 -0
- package/server/http-channel-test-fixture.ts +103 -0
- package/server/ide-messages-mcp.test.ts +82 -0
- package/server/ide-messages-mcp.ts +47 -0
- package/server/idecron-mcp.ts +65 -0
- package/server/idecron.test.ts +79 -0
- package/server/idecron.ts +271 -0
- package/server/index.ts +336 -0
- package/server/live-session.test.ts +1126 -0
- package/server/live-session.ts +1165 -0
- package/server/project-routes.ts +46 -0
- package/server/projects.test.ts +28 -0
- package/server/push.ts +315 -0
- package/server/report-cache.ts +28 -0
- package/server/session-aliases.test.ts +57 -0
- package/server/session-aliases.ts +50 -0
- package/server/session-branch.ts +71 -0
- package/server/session-dirs.ts +189 -0
- package/server/session-file.test.ts +134 -0
- package/server/session-file.ts +113 -0
- package/server/session-messaging.test.ts +297 -0
- package/server/session-messaging.ts +165 -0
- package/server/sessions.ts +638 -0
- package/server/subagents.test.ts +43 -0
- package/server/subagents.ts +58 -0
- package/server/tool-images.test.ts +76 -0
- package/server/tool-images.ts +45 -0
- package/server/turn-changes.ts +308 -0
- package/server/worktree-git.ts +87 -0
- package/server/worktrees.ts +138 -0
- package/server/ws.test.ts +195 -0
- package/server/ws.ts +197 -0
- package/shared/agent-model.ts +15 -0
- package/shared/agent-tasks.ts +12 -0
- package/shared/config.ts +175 -0
- package/shared/directories.ts +12 -0
- package/shared/files.ts +51 -0
- package/shared/http-channels.ts +63 -0
- package/shared/message-window.ts +189 -0
- package/shared/project.ts +77 -0
- package/shared/protocol.ts +203 -0
- package/shared/task-types.ts +4 -0
- package/shared/todos.ts +40 -0
- package/shared/write-policy.ts +9 -0
- package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
- package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
- package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
- package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
- package/web/dist/assets/Form-BKTwiYyw.js +1 -0
- package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
- package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
- package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
- package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
- package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
- package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
- package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
- package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
- package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
- package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
- package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-xBchdRhy.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-1q6DGuN3.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
- package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
- package/web/dist/assets/c-D9gIxSfx.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DRAZz75c.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-CuseGurG.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/core-BxI4V4-T.js +12 -0
- package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
- package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-DpFbYmRh.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-DXQWmXTB.js +1 -0
- package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
- package/web/dist/assets/elm-BslFEg8K.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
- package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
- package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/format-DrBC7w0A.js +1 -0
- package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
- package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
- package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
- package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
- package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-CV_puxbB.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-C5GPjiLV.js +1 -0
- package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
- package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CmHiEKV9.js +1 -0
- package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
- package/web/dist/assets/http-DPjjqtcL.js +1 -0
- package/web/dist/assets/hurl-AMBpSloO.js +1 -0
- package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DNPnl4sh.css +2 -0
- package/web/dist/assets/index-DQh72lEx.js +14 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-CEM7Tdw_.js +1 -0
- package/web/dist/assets/javascript-CU3PLowd.js +1 -0
- package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
- package/web/dist/assets/jison-BoKsxs5M.js +1 -0
- package/web/dist/assets/json-BM2FWsRB.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-B5fTecQP.js +1 -0
- package/web/dist/assets/julia-DGyDG39_.js +1 -0
- package/web/dist/assets/just-B-RyLcSR.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/langs-CciPKqaF.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-CV1pQelm.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/lib-DlcVMUqT.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-CIPVh0S-.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-h2h6YUMS.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-jWINucNT.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
- package/web/dist/assets/nix-Cl_7024t.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-ClD5huOa.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-BFegxyeJ.js +1 -0
- package/web/dist/assets/php-B-qOA3Y4.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-CYUsa87k.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-4xJhgbOf.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-BlHaOHx8.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/react-B4UGpcWC.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-zjfguEh7.js +1 -0
- package/web/dist/assets/ruby-4oR9waBK.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-Do4V_2i9.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CW6JPtag.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
- package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
- package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-DVtw20Yn.js +1 -0
- package/web/dist/assets/sparql-C0ia5K60.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-DGCM-jag.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-DfuMOXna.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
- package/web/dist/assets/svelte-g_uCUem5.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-mx6xDwuK.js +1 -0
- package/web/dist/assets/themes-CNElGaXw.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-z04y7qR9.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-Bz57G_nl.js +1 -0
- package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CQgvQytb.js +1 -0
- package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
- package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-B49klYQ3.js +1 -0
- package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
- package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/icons/apple-touch-icon.png +0 -0
- package/web/dist/icons/badge-96.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon.svg +4 -0
- package/web/dist/icons/maskable-192.png +0 -0
- package/web/dist/icons/maskable-512.png +0 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +20 -0
- package/web/dist/sw.js +176 -0
- package/web/dist/version.json +1 -0
- package/web/src/App.tsx +97 -0
- package/web/src/agent-history-flow.test.ts +191 -0
- package/web/src/agent-history-flow.ts +58 -0
- package/web/src/agent-owners.ts +30 -0
- package/web/src/annotation-selection.test.ts +36 -0
- package/web/src/annotation-selection.ts +123 -0
- package/web/src/annotations.test.ts +279 -0
- package/web/src/annotations.ts +191 -0
- package/web/src/api.ts +27 -0
- package/web/src/auth-events.ts +11 -0
- package/web/src/auth-lifecycle.test.ts +105 -0
- package/web/src/clipboard.ts +32 -0
- package/web/src/components/ActivityNotifier.tsx +22 -0
- package/web/src/components/AnnotationEditor.tsx +135 -0
- package/web/src/components/AnnotationSelection.tsx +122 -0
- package/web/src/components/Automation.tsx +390 -0
- package/web/src/components/CodeView.tsx +121 -0
- package/web/src/components/Composer.tsx +552 -0
- package/web/src/components/ComposerAnnotations.tsx +45 -0
- package/web/src/components/Confirmation.tsx +91 -0
- package/web/src/components/CopyButton.tsx +23 -0
- package/web/src/components/DiffView.tsx +94 -0
- package/web/src/components/DirectoryPicker.tsx +230 -0
- package/web/src/components/FileCopyMenu.tsx +58 -0
- package/web/src/components/FileEntryActions.tsx +95 -0
- package/web/src/components/ImagePreview.tsx +68 -0
- package/web/src/components/Items.tsx +608 -0
- package/web/src/components/Markdown.tsx +73 -0
- package/web/src/components/MessageList.tsx +458 -0
- package/web/src/components/Modal.tsx +72 -0
- package/web/src/components/NotificationEntry.tsx +31 -0
- package/web/src/components/NotificationSettings.tsx +226 -0
- package/web/src/components/NotificationToasts.tsx +59 -0
- package/web/src/components/OpenTabs.tsx +112 -0
- package/web/src/components/Pending.tsx +175 -0
- package/web/src/components/ProjectExtras.tsx +299 -0
- package/web/src/components/ProjectNav.tsx +47 -0
- package/web/src/components/SessionActions.tsx +271 -0
- package/web/src/components/SessionChannels.tsx +288 -0
- package/web/src/components/SessionExtras.tsx +263 -0
- package/web/src/components/SettingsLink.tsx +21 -0
- package/web/src/components/Shell.tsx +320 -0
- package/web/src/components/SvgPreview.tsx +18 -0
- package/web/src/components/TodoPanel.tsx +37 -0
- package/web/src/components/VersionNotice.tsx +40 -0
- package/web/src/components/icons.tsx +43 -0
- package/web/src/diff.ts +62 -0
- package/web/src/drafts.test.ts +80 -0
- package/web/src/drafts.ts +64 -0
- package/web/src/format.ts +5 -0
- package/web/src/highlight.worker.ts +49 -0
- package/web/src/hooks.ts +59 -0
- package/web/src/item-view-state.test.ts +51 -0
- package/web/src/item-view-state.ts +43 -0
- package/web/src/live-history-flow.test.ts +77 -0
- package/web/src/live-merge.test.ts +64 -0
- package/web/src/live-merge.ts +50 -0
- package/web/src/live-message-hydration.test.ts +253 -0
- package/web/src/live.ts +404 -0
- package/web/src/main.tsx +27 -0
- package/web/src/markdown-assets.test.ts +70 -0
- package/web/src/markdown-assets.ts +78 -0
- package/web/src/markdown-html.test.ts +84 -0
- package/web/src/markdown-html.ts +60 -0
- package/web/src/markdown-svg.test.ts +37 -0
- package/web/src/markdown-svg.ts +42 -0
- package/web/src/markdown-tables.test.ts +34 -0
- package/web/src/markdown-tables.ts +17 -0
- package/web/src/message-bodies.ts +117 -0
- package/web/src/message-cache.test.ts +208 -0
- package/web/src/message-cache.ts +158 -0
- package/web/src/message-font-size.test.ts +41 -0
- package/web/src/message-font-size.ts +57 -0
- package/web/src/message-time.test.ts +32 -0
- package/web/src/message-time.ts +26 -0
- package/web/src/notify.ts +132 -0
- package/web/src/open-tabs.test.ts +69 -0
- package/web/src/open-tabs.ts +110 -0
- package/web/src/pages/AgentView.tsx +138 -0
- package/web/src/pages/ChangesView.tsx +193 -0
- package/web/src/pages/FilesView.tsx +406 -0
- package/web/src/pages/HomePage.tsx +96 -0
- package/web/src/pages/LoginPage.tsx +56 -0
- package/web/src/pages/OverviewPage.tsx +124 -0
- package/web/src/pages/ProjectHome.tsx +136 -0
- package/web/src/pages/SessionView.tsx +238 -0
- package/web/src/pages/WorktreesView.tsx +197 -0
- package/web/src/pages/settings/AccountPage.tsx +149 -0
- package/web/src/pages/settings/AgentsPage.tsx +182 -0
- package/web/src/pages/settings/ConfigPage.tsx +181 -0
- package/web/src/pages/settings/GeneralPage.tsx +27 -0
- package/web/src/pages/settings/HooksPage.tsx +244 -0
- package/web/src/pages/settings/McpPage.tsx +310 -0
- package/web/src/pages/settings/MemoryPage.tsx +309 -0
- package/web/src/pages/settings/PermissionsPage.tsx +160 -0
- package/web/src/pages/settings/PluginsPage.tsx +251 -0
- package/web/src/pages/settings/SettingsPage.tsx +141 -0
- package/web/src/pages/settings/SkillsPage.tsx +142 -0
- package/web/src/pages/settings/ui.tsx +165 -0
- package/web/src/preference-document.ts +133 -0
- package/web/src/project-actions.ts +18 -0
- package/web/src/project-files-state.test.ts +49 -0
- package/web/src/project-files-state.ts +64 -0
- package/web/src/project-images.ts +13 -0
- package/web/src/pwa.ts +33 -0
- package/web/src/queryClient.ts +5 -0
- package/web/src/reasoning-preference.test.ts +39 -0
- package/web/src/reasoning-preference.ts +42 -0
- package/web/src/session-confirmations.ts +36 -0
- package/web/src/stream-replay.test.ts +163 -0
- package/web/src/styles.css +231 -0
- package/web/src/tool-result-body.test.ts +115 -0
- package/web/src/transcript.test.ts +186 -0
- package/web/src/transcript.ts +583 -0
- package/web/src/turns.test.ts +338 -0
- package/web/src/turns.ts +178 -0
- package/web/src/ui/Button.tsx +60 -0
- package/web/src/ui/Controls.tsx +227 -0
- package/web/src/ui/Form.tsx +203 -0
- package/web/src/ui/MenuPopover.tsx +144 -0
- package/web/src/ui/Popover.tsx +147 -0
- package/web/src/ui/ResizablePane.tsx +110 -0
- package/web/src/ui/Select.tsx +150 -0
- package/web/src/ui/icons.tsx +270 -0
- package/web/src/unsaved.tsx +99 -0
- package/web/src/user-text.test.ts +60 -0
- package/web/src/user-text.ts +35 -0
- package/web/src/uuid.ts +12 -0
- package/README.md +0 -3
package/web/src/main.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { createBrowserRouter, RouterProvider } from "react-router";
|
|
5
|
+
import { App } from "./App.tsx";
|
|
6
|
+
import { queryClient } from "./queryClient.ts";
|
|
7
|
+
import "./styles.css";
|
|
8
|
+
import { registerServiceWorker } from "./pwa.ts";
|
|
9
|
+
import { ConfirmationProvider } from "./components/Confirmation.tsx";
|
|
10
|
+
import { UnsavedNavigationGuard } from "./unsaved.tsx";
|
|
11
|
+
|
|
12
|
+
const router = createBrowserRouter([{ path: "*", element: (
|
|
13
|
+
<ConfirmationProvider>
|
|
14
|
+
<UnsavedNavigationGuard />
|
|
15
|
+
<App />
|
|
16
|
+
</ConfirmationProvider>
|
|
17
|
+
) }]);
|
|
18
|
+
|
|
19
|
+
createRoot(document.getElementById("root")!).render(
|
|
20
|
+
<StrictMode>
|
|
21
|
+
<QueryClientProvider client={queryClient}>
|
|
22
|
+
<RouterProvider router={router} />
|
|
23
|
+
</QueryClientProvider>
|
|
24
|
+
</StrictMode>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
void registerServiceWorker();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveMarkdownAsset, type MarkdownAssetContext } from "./markdown-assets.ts";
|
|
4
|
+
|
|
5
|
+
const context: MarkdownAssetContext = { projectId: "project 1", filePath: "docs/readme.md", root: "/home/me/project" };
|
|
6
|
+
function resolved(src: string, overrides?: Partial<MarkdownAssetContext>) {
|
|
7
|
+
const value = resolveMarkdownAsset(src, { ...context, ...overrides });
|
|
8
|
+
return value ? new URL(value, "https://ide.test") : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("Markdown images resolve relative to their document or project root", () => {
|
|
12
|
+
assert.equal(resolved("../assets/a.png")?.searchParams.get("path"), "assets/a.png");
|
|
13
|
+
assert.equal(resolved("./images/a.png")?.searchParams.get("path"), "docs/images/a.png");
|
|
14
|
+
assert.equal(resolved("/assets/a.png")?.searchParams.get("path"), "assets/a.png");
|
|
15
|
+
assert.equal(resolved("a.png", { filePath: "readme.md" })?.searchParams.get("path"), "a.png");
|
|
16
|
+
assert.equal(resolved("a.png")?.pathname, "/api/projects/project%201/file");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("paths retain Unicode, spaces, encoded filename delimiters, query and fragment", () => {
|
|
20
|
+
const url = resolved("../assets/图 片%23v%3F1.png?download=1#preview", { version: 4 })!;
|
|
21
|
+
assert.equal(url.searchParams.get("path"), "assets/图 片#v?1.png");
|
|
22
|
+
assert.equal(url.searchParams.get("download"), "1");
|
|
23
|
+
assert.equal(url.searchParams.get("v"), "4");
|
|
24
|
+
assert.equal(url.hash, "#preview");
|
|
25
|
+
assert.equal(resolved("100%25.png")?.searchParams.get("path"), "docs/100%.png");
|
|
26
|
+
assert.equal(resolved("a.png?path=outside&v=1", { version: 5 })?.searchParams.get("path"), "docs/a.png");
|
|
27
|
+
assert.equal(resolved("a.png?path=outside&v=1", { version: 5 })?.searchParams.get("v"), "5");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("filesystem paths and file URLs map only to the supplied project root", () => {
|
|
31
|
+
for (const src of ["/home/me/project/assets/a.png", "file:///home/me/project/assets/a.png", "file://localhost/home/me/project/assets/a.png"]) {
|
|
32
|
+
assert.equal(resolved(src)?.searchParams.get("path"), "assets/a.png");
|
|
33
|
+
}
|
|
34
|
+
assert.equal(resolved("file:///home/me/project/图%20片%23a.png")?.searchParams.get("path"), "图 片#a.png");
|
|
35
|
+
for (const src of ["file:///etc/a.png", "file:///home/me/project-other/a.png", "file://server/home/me/project/a.png", "file:assets/a.png"]) {
|
|
36
|
+
assert.equal(resolved(src), undefined, src);
|
|
37
|
+
}
|
|
38
|
+
assert.equal(resolved("file:///home/me/project/a.png", { root: undefined }), undefined);
|
|
39
|
+
assert.equal(resolveMarkdownAsset("file:///home/me/project/a.png"), undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("local traversal, malformed encoding and ambiguous separators are rejected", () => {
|
|
43
|
+
for (const src of ["../../outside.png", "/../outside.png", "%2e%2e/%2e%2e/outside.png", "/home/me/project/../outside.png", "file:///home/me/project/../outside.png", "a%00.png", "a%5cb.png", "bad%zz.png"]) {
|
|
44
|
+
assert.equal(resolved(src), undefined, src);
|
|
45
|
+
}
|
|
46
|
+
assert.equal(resolved("../assets/../a.png")?.searchParams.get("path"), "a.png");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("network images and context-free relative images retain their URLs", () => {
|
|
50
|
+
for (const src of ["https://example.com/a.png?x=1#y", "http://example.com/a.png", "//example.com/a.png"]) {
|
|
51
|
+
assert.equal(resolveMarkdownAsset(src, context), src);
|
|
52
|
+
}
|
|
53
|
+
assert.equal(resolveMarkdownAsset("../assets/a.png"), "../assets/a.png");
|
|
54
|
+
assert.equal(resolveMarkdownAsset("/assets/a.png"), "/assets/a.png");
|
|
55
|
+
assert.equal(resolveMarkdownAsset(undefined), undefined);
|
|
56
|
+
assert.equal(resolveMarkdownAsset(""), undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("only recognized image data URLs and permitted network schemes are accepted", () => {
|
|
60
|
+
for (const type of ["png", "jpeg", "gif", "webp", "avif", "bmp", "ico", "svg+xml"]) {
|
|
61
|
+
const src = `data:image/${type};base64,PHN2Zy8+`;
|
|
62
|
+
assert.equal(resolveMarkdownAsset(src, context), src);
|
|
63
|
+
}
|
|
64
|
+
const svg = "data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3C%2Fsvg%3E";
|
|
65
|
+
assert.equal(resolveMarkdownAsset(svg), svg);
|
|
66
|
+
for (const src of ["javascript:alert(1)", "JaVaScRiPt:alert(1)", "java\nscript:alert(1)", "blob:https://example.com/1", "ftp://example.com/a.png", "custom:a.png", "data:text/html;base64,PHN2Zy8+", "data:image/unknown;base64,AAAA", "data:image/png,abcd", "data:image/png;base64,***", "data:image/svg+xml,%zz", "data:image/svg+xml,%3Cscript%3E"]) {
|
|
67
|
+
assert.equal(resolveMarkdownAsset(src, context), undefined, src);
|
|
68
|
+
assert.equal(resolveMarkdownAsset(src), undefined, src);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type MarkdownAssetContext = {
|
|
2
|
+
projectId: string;
|
|
3
|
+
filePath: string;
|
|
4
|
+
root?: string;
|
|
5
|
+
version?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Normalize project paths without silently clamping attempts to escape their root. */
|
|
9
|
+
function normalizePath(path: string): string | undefined {
|
|
10
|
+
const parts: string[] = [];
|
|
11
|
+
for (const part of path.split("/")) {
|
|
12
|
+
if (!part || part === ".") continue;
|
|
13
|
+
if (part === "..") {
|
|
14
|
+
if (!parts.length) return undefined;
|
|
15
|
+
parts.pop();
|
|
16
|
+
} else parts.push(part);
|
|
17
|
+
}
|
|
18
|
+
return parts.join("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function imageData(src: string): boolean {
|
|
22
|
+
if (/^data:image\/(?:png|jpeg|gif|webp|avif|bmp|ico|x-icon|vnd\.microsoft\.icon|svg\+xml);base64,[a-z0-9+/]+={0,2}$/i.test(src)) return true;
|
|
23
|
+
const svg = /^data:image\/svg\+xml(?:;charset=utf-8)?,(.+)$/i.exec(src);
|
|
24
|
+
if (!svg) return false;
|
|
25
|
+
try {
|
|
26
|
+
return /^\s*(?:<\?xml[^>]*>\s*)?<svg\b/i.test(decodeURIComponent(svg[1]!));
|
|
27
|
+
} catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Map image URLs in a file preview to the project's authenticated file endpoint. */
|
|
31
|
+
export function resolveMarkdownAsset(src: string | undefined, context?: MarkdownAssetContext): string | undefined {
|
|
32
|
+
if (!src) return undefined;
|
|
33
|
+
const value = src.trim();
|
|
34
|
+
if (!value || /[\u0000-\u001f\u007f]/.test(value)) return undefined;
|
|
35
|
+
if (/^https?:/i.test(value) || value.startsWith("//")) return value;
|
|
36
|
+
if (/^data:/i.test(value)) return imageData(value) ? value : undefined;
|
|
37
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
38
|
+
if (scheme && scheme !== "file") return undefined;
|
|
39
|
+
if (scheme === "file" && (!context?.root || !/^file:\/\//i.test(value))) return undefined;
|
|
40
|
+
if (!context) return value;
|
|
41
|
+
|
|
42
|
+
// Split URL syntax before decoding so encoded # and ? remain filename characters.
|
|
43
|
+
const hashAt = value.indexOf("#");
|
|
44
|
+
const hash = hashAt < 0 ? "" : value.slice(hashAt);
|
|
45
|
+
const withoutHash = hashAt < 0 ? value : value.slice(0, hashAt);
|
|
46
|
+
const queryAt = withoutHash.indexOf("?");
|
|
47
|
+
const query = queryAt < 0 ? "" : withoutHash.slice(queryAt + 1);
|
|
48
|
+
let pathname = queryAt < 0 ? withoutHash : withoutHash.slice(0, queryAt);
|
|
49
|
+
if (scheme === "file") {
|
|
50
|
+
// Remote file hosts are not project-local paths (localhost is conventional).
|
|
51
|
+
const file = /^file:\/\/([^/]*)(\/.*)$/i.exec(pathname);
|
|
52
|
+
if (!file || (file[1] && file[1].toLowerCase() !== "localhost")) return undefined;
|
|
53
|
+
pathname = file[2]!;
|
|
54
|
+
}
|
|
55
|
+
try { pathname = decodeURIComponent(pathname); } catch { return undefined; }
|
|
56
|
+
if (!pathname || /[\\\u0000-\u001f\u007f]/.test(pathname)) return undefined;
|
|
57
|
+
|
|
58
|
+
const root = context.root?.startsWith("/") ? normalizePath(context.root) : undefined;
|
|
59
|
+
const absoluteRoot = root ? `/${root}` : context.root === "/" ? "" : undefined;
|
|
60
|
+
let relative: string | undefined;
|
|
61
|
+
if (absoluteRoot !== undefined && pathname.startsWith("/") &&
|
|
62
|
+
(pathname === absoluteRoot || pathname.startsWith(`${absoluteRoot}/`))) {
|
|
63
|
+
// Strip first, so /project/../outside cannot be mistaken for a project path.
|
|
64
|
+
relative = normalizePath(pathname.slice(absoluteRoot.length));
|
|
65
|
+
} else if (scheme === "file") return undefined;
|
|
66
|
+
else if (pathname.startsWith("/")) relative = normalizePath(pathname);
|
|
67
|
+
else {
|
|
68
|
+
const document = normalizePath(context.filePath);
|
|
69
|
+
if (document === undefined) return undefined;
|
|
70
|
+
const directory = document.includes("/") ? document.slice(0, document.lastIndexOf("/") + 1) : "";
|
|
71
|
+
relative = normalizePath(directory + pathname);
|
|
72
|
+
}
|
|
73
|
+
if (!relative) return undefined;
|
|
74
|
+
const params = new URLSearchParams(query);
|
|
75
|
+
params.set("path", relative);
|
|
76
|
+
if (context.version !== undefined) params.set("v", String(context.version));
|
|
77
|
+
return `/api/projects/${encodeURIComponent(context.projectId)}/file?${params}${hash}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
5
|
+
import { Markdown } from "./components/Markdown.tsx";
|
|
6
|
+
import { markdownUrlTransform } from "./markdown-html.ts";
|
|
7
|
+
|
|
8
|
+
function render(text: string) {
|
|
9
|
+
return renderToStaticMarkup(createElement(Markdown, { text }));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test("common HTML renders real semantic elements with table layout and details", () => {
|
|
13
|
+
const html = render('<details open><summary>More</summary><table><caption>Totals</caption><thead><tr><th colspan="2">Header</th></tr></thead><tbody><tr><td rowspan="2" align="right">Cell</td><td><strong>Bold</strong><br>Next</td></tr></tbody></table></details>');
|
|
14
|
+
assert.match(html, /<details open=""><summary>More<\/summary>/);
|
|
15
|
+
assert.match(html, /<caption>Totals<\/caption>/);
|
|
16
|
+
assert.match(html, /<th colSpan="2">Header<\/th>/);
|
|
17
|
+
assert.match(html, /<td rowSpan="2" style="text-align:right">Cell<\/td>/);
|
|
18
|
+
assert.match(html, /<strong>Bold<\/strong><br\/>Next/);
|
|
19
|
+
assert.match(html, /data-testid="markdown-table-block"/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("Markdown and HTML images preserve network sources and common size attributes", () => {
|
|
23
|
+
const html = render('\n\n<div><img src="https://example.com/b.png" width="160" height="80" alt="HTML"></div>');
|
|
24
|
+
assert.match(html, /src="https:\/\/example.com\/a.png"/);
|
|
25
|
+
assert.match(html, /src="https:\/\/example.com\/b.png" width="160" height="80" alt="HTML"/);
|
|
26
|
+
assert.match(html, /loading="lazy"/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("both Markdown and HTML image paths resolve once against the document context", () => {
|
|
30
|
+
const html = renderToStaticMarkup(createElement(Markdown, {
|
|
31
|
+
text: '\n\n<div><img src="file:///project/assets/b.png" alt="HTML"></div>',
|
|
32
|
+
assetContext: { projectId: "p1", filePath: "docs/readme.md", root: "/project", version: 7 },
|
|
33
|
+
}));
|
|
34
|
+
assert.match(html, /src="\/api\/projects\/p1\/file\?path=assets%2Fa.png&v=7"/);
|
|
35
|
+
assert.match(html, /src="\/api\/projects\/p1\/file\?path=assets%2Fb.png&v=7"/);
|
|
36
|
+
assert.doesNotMatch(html, /path=api|file:\/\/\//);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("scripts, events, styles, embedded documents and srcset never reach the rendered document", () => {
|
|
40
|
+
const html = render('<div onclick="alert(1)" style="position:fixed"><script>alert(2)</script><iframe src="https://example.com">secret</iframe><object data="https://example.com">secret</object><embed src="https://example.com"><style>body{display:none}</style><img src="javascript:alert(3)" onerror="alert(4)" srcset="https://example.com/tracker.png 2x"><a href="javascript:alert(5)">Unsafe</a><p>Visible</p></div>');
|
|
41
|
+
assert.doesNotMatch(html, /onclick|onerror|style=|<script|<iframe|<object|<embed|<style|srcSet|srcset|javascript:|alert\(|secret/);
|
|
42
|
+
assert.match(html, /<p>Visible<\/p>/);
|
|
43
|
+
const alignment = render('<table><tr><td align="right;position:fixed">Cell</td></tr></table>');
|
|
44
|
+
assert.doesNotMatch(alignment, /position:fixed|style=/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("standalone, inline and HTML-nested SVG all stay on the isolated image preview route", () => {
|
|
48
|
+
for (const source of [
|
|
49
|
+
'<svg width="20"><rect width="10" height="10"/></svg>',
|
|
50
|
+
'Before <svg><rect/></svg> after',
|
|
51
|
+
'<div><svg onload="alert(1)"><script>alert(2)</script><foreignObject><iframe src="https://example.com"/></foreignObject><svg><circle r="2"/></svg></svg></div>',
|
|
52
|
+
]) {
|
|
53
|
+
const html = render(source);
|
|
54
|
+
assert.match(html, /data-testid="inline-svg-preview"/);
|
|
55
|
+
assert.doesNotMatch(html, /<svg|<rect|<circle|<script|<iframe|onload|alert\(/);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("GFM table copy metadata, task lists and SVG/code block previews survive sanitizing", () => {
|
|
60
|
+
const html = render('| Name | Value |\n| --- | --- |\n| One | Two |\n\n- [x] Done\n\n```svg\n<svg><rect/></svg>\n```\n\n```ts\nconst answer = 42;\n```');
|
|
61
|
+
assert.match(html, /aria-label="复制表格"/);
|
|
62
|
+
assert.match(html, /class="contains-task-list"/);
|
|
63
|
+
assert.match(html, /type="checkbox"/);
|
|
64
|
+
assert.match(html, /data-testid="inline-svg-preview"/);
|
|
65
|
+
assert.match(html, /SVG 源码/);
|
|
66
|
+
assert.match(html, /class="language-ts"/);
|
|
67
|
+
assert.match(html, /const answer = 42;/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("allowed image data survives while dangerous link and image protocols are rejected", () => {
|
|
71
|
+
const html = render('<img src="data:image/png;base64,iVBORw0KGgo="><img src="data:text/html;base64,PHNjcmlwdD4="><a href="data:image/png;base64,eA==">Link</a>');
|
|
72
|
+
assert.match(html, /src="data:image\/png;base64,iVBORw0KGgo="/);
|
|
73
|
+
assert.doesNotMatch(html, /data:text|href="data:/);
|
|
74
|
+
assert.equal(markdownUrlTransform("javascript:alert(1)", "src"), "");
|
|
75
|
+
assert.equal(markdownUrlTransform("file:///tmp/image.png", "src"), "file:///tmp/image.png");
|
|
76
|
+
assert.equal(markdownUrlTransform("file:///tmp/image.png", "href"), "");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("code examples remain text and unknown XML never becomes executable DOM", () => {
|
|
80
|
+
const html = render('```html\n<script>alert(1)</script>\n```\n\n<custom-widget onclick="run()">Readable</custom-widget>');
|
|
81
|
+
assert.match(html, /<script>alert\(1\)<\/script>/);
|
|
82
|
+
assert.match(html, /Readable/);
|
|
83
|
+
assert.doesNotMatch(html, /<script|<custom-widget|onclick/);
|
|
84
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Element, Root } from "hast";
|
|
2
|
+
import { toHtml } from "hast-util-to-html";
|
|
3
|
+
import { defaultSchema } from "rehype-sanitize";
|
|
4
|
+
import { defaultUrlTransform } from "react-markdown";
|
|
5
|
+
|
|
6
|
+
/** Keep SVGs in an isolated image document, including SVGs nested in raw HTML.
|
|
7
|
+
* Run after the HTML parser and before sanitizing the surrounding document. */
|
|
8
|
+
export function rehypeSvgImages() {
|
|
9
|
+
return (tree: Root) => {
|
|
10
|
+
const visit = (parent: Root | Element) => {
|
|
11
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
12
|
+
const child = parent.children[i]!;
|
|
13
|
+
if (child.type !== "element") continue;
|
|
14
|
+
if (child.tagName === "svg") {
|
|
15
|
+
parent.children[i] = {
|
|
16
|
+
type: "element", tagName: "img", children: [],
|
|
17
|
+
properties: { alt: "SVG 图像", dataInlineSvg: toHtml(child) },
|
|
18
|
+
};
|
|
19
|
+
} else visit(child);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
visit(tree);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** GitHub's HTML allowlist, with layout attributes and our private copy/image
|
|
27
|
+
* metadata. CSS, event handlers, srcset and active embedded documents stay out. */
|
|
28
|
+
export const markdownHtmlSchema = {
|
|
29
|
+
...defaultSchema,
|
|
30
|
+
tagNames: [
|
|
31
|
+
...defaultSchema.tagNames!.filter((tag) => tag !== "picture" && tag !== "source"),
|
|
32
|
+
"caption", "col", "colgroup", "figure", "figcaption", "mark", "u", "time",
|
|
33
|
+
],
|
|
34
|
+
attributes: {
|
|
35
|
+
...defaultSchema.attributes,
|
|
36
|
+
"*": [
|
|
37
|
+
...defaultSchema.attributes!["*"]!.filter((attribute) => attribute !== "align"),
|
|
38
|
+
// ReactMarkdown turns table alignment into CSS, so constrain the values.
|
|
39
|
+
["align", "left", "center", "right", "justify", "char"],
|
|
40
|
+
],
|
|
41
|
+
img: [...defaultSchema.attributes!.img!, "dataInlineSvg", "data-inline-svg"],
|
|
42
|
+
table: [...defaultSchema.attributes!.table!, "dataCopyMarkdown", "data-copy-markdown"],
|
|
43
|
+
},
|
|
44
|
+
protocols: {
|
|
45
|
+
...defaultSchema.protocols,
|
|
46
|
+
// Image URLs pass through markdownUrlTransform and the asset resolver too.
|
|
47
|
+
src: ["http", "https", "data", "file"],
|
|
48
|
+
},
|
|
49
|
+
strip: [...defaultSchema.strip!, "iframe", "object", "embed", "style", "svg", "math", "template"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** ReactMarkdown's defaults for links; image-only exceptions are checked again
|
|
53
|
+
* by the asset resolver before a URL can reach an actual img element. */
|
|
54
|
+
export function markdownUrlTransform(value: string, key: string): string | undefined {
|
|
55
|
+
if (key === "src") {
|
|
56
|
+
if (/^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|ico|x-icon|vnd\.microsoft\.icon|svg\+xml)(?:;[^,]*)?,/i.test(value)) return value;
|
|
57
|
+
if (/^file:/i.test(value)) return value;
|
|
58
|
+
}
|
|
59
|
+
return defaultUrlTransform(value);
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
import remarkParse from "remark-parse";
|
|
5
|
+
import type { Nodes, Image, Root } from "mdast";
|
|
6
|
+
import { remarkInlineSvg } from "./markdown-svg.ts";
|
|
7
|
+
|
|
8
|
+
const parser = unified().use(remarkParse).use(remarkInlineSvg);
|
|
9
|
+
async function images(text: string) {
|
|
10
|
+
const tree = await parser.run(parser.parse(text), { value: text }) as Root;
|
|
11
|
+
const found: Image[] = [];
|
|
12
|
+
const visit = (node: Nodes) => {
|
|
13
|
+
if (node.type === "image") found.push(node);
|
|
14
|
+
else if ("children" in node) node.children.forEach(visit);
|
|
15
|
+
};
|
|
16
|
+
visit(tree);
|
|
17
|
+
return { found, tree };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("embedded SVGs retain their exact markup across Markdown blocks and nested SVG tags", async () => {
|
|
21
|
+
const svg = '<svg width="40" height="40">\n\n<svg><rect width="10" height="10" /></svg>\n</svg>';
|
|
22
|
+
const { found, tree } = await images(`# Test\n\n${svg}\n\nAfter SVG.`);
|
|
23
|
+
assert.equal(found.length, 1);
|
|
24
|
+
assert.equal(found[0]!.data?.hProperties?.["data-inline-svg"], svg);
|
|
25
|
+
assert.equal(tree.children.at(-1)?.type, "paragraph");
|
|
26
|
+
assert.match(JSON.stringify(tree.children.at(-1)), /After SVG/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("inline SVGs remain inline, ordinary HTML is not enabled, and code is not reinterpreted", async () => {
|
|
30
|
+
const svg = '<svg width="20"><rect /></svg>';
|
|
31
|
+
const { found, tree } = await images(`Before ${svg} after.\n\n<script>alert(1)</script>\n\n\`\`\`xml\n${svg}\n\`\`\``);
|
|
32
|
+
assert.equal(found.length, 1);
|
|
33
|
+
assert.equal(tree.children[0]?.type, "paragraph");
|
|
34
|
+
assert.match(JSON.stringify(tree.children[0]), /Before/);
|
|
35
|
+
assert.match(JSON.stringify(tree.children[0]), /after/);
|
|
36
|
+
assert.equal(tree.children.at(-1)?.type, "code");
|
|
37
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Image, Nodes, Root } from "mdast";
|
|
2
|
+
|
|
3
|
+
/** Find the complete outer SVG, including nested SVGs and blank lines that
|
|
4
|
+
* Markdown may otherwise split into multiple nodes. This plugin only converts
|
|
5
|
+
* SVGs; common HTML goes through the subsequent raw parsing and sanitizing chain. */
|
|
6
|
+
function svgLength(text: string): number | undefined {
|
|
7
|
+
if (!/^<svg(?:\s|>|\/)/i.test(text)) return;
|
|
8
|
+
const tags = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<\/?svg\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (const match of text.matchAll(tags)) {
|
|
11
|
+
const tag = match[0];
|
|
12
|
+
if (tag.startsWith("<!")) continue;
|
|
13
|
+
if (/^<\//.test(tag)) depth--;
|
|
14
|
+
else if (!/\/\s*>$/.test(tag)) depth++;
|
|
15
|
+
if (depth === 0) return match.index! + tag.length;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function remarkInlineSvg() {
|
|
20
|
+
return (tree: Root, file: { value: unknown }) => {
|
|
21
|
+
const source = String(file.value);
|
|
22
|
+
const visit = (parent: Nodes) => {
|
|
23
|
+
if (!("children" in parent)) return;
|
|
24
|
+
const children = parent.children as Nodes[];
|
|
25
|
+
for (let i = 0; i < children.length; i++) {
|
|
26
|
+
const node = children[i]!;
|
|
27
|
+
const start = node.type === "html" ? node.position?.start.offset : undefined;
|
|
28
|
+
const length = start === undefined ? undefined : svgLength(source.slice(start));
|
|
29
|
+
if (start === undefined || length === undefined) { visit(node); continue; }
|
|
30
|
+
const end = start + length;
|
|
31
|
+
let last = i;
|
|
32
|
+
while (last + 1 < children.length && (children[last + 1]!.position?.start.offset ?? Infinity) < end) last++;
|
|
33
|
+
const tailEnd = children[last]!.position?.end.offset ?? end;
|
|
34
|
+
const image: Image = { type: "image", url: "", alt: "SVG 图像", data: { hProperties: { "data-inline-svg": source.slice(start, end) } } };
|
|
35
|
+
const replacement: Nodes = parent.type === "paragraph" ? image : { type: "paragraph", children: [image] };
|
|
36
|
+
const tail: Nodes[] = tailEnd > end ? [{ type: "html", value: source.slice(end, tailEnd) }] : [];
|
|
37
|
+
children.splice(i, last - i + 1, replacement, ...tail);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
visit(tree);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
import remarkParse from "remark-parse";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import type { Root, Table, Nodes } from "mdast";
|
|
7
|
+
import { remarkCopyTables } from "./markdown-tables.ts";
|
|
8
|
+
|
|
9
|
+
const parser = unified().use(remarkParse).use(remarkGfm);
|
|
10
|
+
function findTables(node: Nodes): Table[] {
|
|
11
|
+
return node.type === "table" ? [node] : "children" in node ? node.children.flatMap(findTables) : [];
|
|
12
|
+
}
|
|
13
|
+
function shape(node: unknown): unknown {
|
|
14
|
+
if (Array.isArray(node)) return node.map(shape);
|
|
15
|
+
if (!node || typeof node !== "object") return node;
|
|
16
|
+
return Object.fromEntries(Object.entries(node).filter(([key]) => !["position", "data"].includes(key)).map(([key, value]) => [key, shape(value)]));
|
|
17
|
+
}
|
|
18
|
+
const table = "| Name | Value |\n| :--- | ---: |\n| `a` | escaped \\| pipe, **bold**, ~~old~~, [link](https://example.org) |";
|
|
19
|
+
for (const [name, text] of [
|
|
20
|
+
["plain", table],
|
|
21
|
+
["blockquote", table.split("\n").map((line) => `> ${line}`).join("\n")],
|
|
22
|
+
["list", table.split("\n").map((line, i) => `${i ? " " : "- "}${line}`).join("\n")],
|
|
23
|
+
["quoted list", table.split("\n").map((line, i) => `> ${i ? " " : "- "}${line}`).join("\n")],
|
|
24
|
+
]) test(`copied ${name} table remains a standalone Markdown table with intact cells/alignment`, () => {
|
|
25
|
+
const tree = parser.parse(text!) as Root;
|
|
26
|
+
const original = findTables(tree)[0]!;
|
|
27
|
+
remarkCopyTables()(tree);
|
|
28
|
+
const copy = original.data?.hProperties?.["data-copy-markdown"];
|
|
29
|
+
assert.equal(typeof copy, "string");
|
|
30
|
+
const parsed = parser.parse(copy as string);
|
|
31
|
+
assert.equal(parsed.children.length, 1);
|
|
32
|
+
assert.equal(parsed.children[0]?.type, "table");
|
|
33
|
+
assert.deepEqual(shape(parsed.children[0]), shape(original));
|
|
34
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Nodes, Root } from "mdast";
|
|
2
|
+
import { toMarkdown } from "mdast-util-to-markdown";
|
|
3
|
+
import { gfmToMarkdown } from "mdast-util-gfm";
|
|
4
|
+
|
|
5
|
+
/** Serialize the table itself. Source offsets include quote/list markers on
|
|
6
|
+
* continuation lines, so slicing the original Markdown is not a standalone table. */
|
|
7
|
+
export function remarkCopyTables() {
|
|
8
|
+
return (tree: Root) => {
|
|
9
|
+
const visit = (node: Nodes) => {
|
|
10
|
+
if (node.type === "table") {
|
|
11
|
+
const text = toMarkdown(node, { extensions: [gfmToMarkdown()] }).trimEnd();
|
|
12
|
+
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, "data-copy-markdown": text } };
|
|
13
|
+
} else if ("children" in node) node.children.forEach(visit);
|
|
14
|
+
};
|
|
15
|
+
visit(tree);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { SdkMessageLike } from "@shared/protocol.ts";
|
|
2
|
+
import type { ToolResult } from "./transcript.ts";
|
|
3
|
+
|
|
4
|
+
/** REST normalizes attribution. Replace presentation bodies without changing the
|
|
5
|
+
* retained live envelope (including parent ownership and stream settlement). */
|
|
6
|
+
export function mergeMessageBody(retained: SdkMessageLike, full: SdkMessageLike): SdkMessageLike {
|
|
7
|
+
const next: SdkMessageLike = { ...retained, message: full.message, _hydrated: true };
|
|
8
|
+
for (const key of ["content", "tool_use_result"]) {
|
|
9
|
+
if (key in full) next[key] = full[key];
|
|
10
|
+
else delete next[key];
|
|
11
|
+
}
|
|
12
|
+
delete next._deferred;
|
|
13
|
+
return next;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hydrateMessageBody(retained: SdkMessageLike, full: SdkMessageLike, uuid: string): SdkMessageLike {
|
|
17
|
+
if (retained.uuid === uuid) return mergeMessageBody(retained, full);
|
|
18
|
+
const linked = retained.linkedToolResults as Record<string, ToolResult> | undefined;
|
|
19
|
+
if (!linked || !Object.values(linked).some((result) => result.deferredUuid === uuid)) return retained;
|
|
20
|
+
const results = { ...linked };
|
|
21
|
+
const origins = { ...retained._hydratedToolResultUuids as Record<string, string> | undefined };
|
|
22
|
+
let changed = false;
|
|
23
|
+
const blocks = (full.message as { content?: unknown[] })?.content;
|
|
24
|
+
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
25
|
+
const b = block as { type: string; tool_use_id: string; content: unknown; is_error?: boolean };
|
|
26
|
+
if (b.type === "tool_result" && results[b.tool_use_id]?.deferredUuid === uuid) {
|
|
27
|
+
results[b.tool_use_id] = { content: b.content, isError: !!b.is_error, structured: full.tool_use_result };
|
|
28
|
+
origins[b.tool_use_id] = uuid;
|
|
29
|
+
changed = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!changed) return retained;
|
|
33
|
+
return { ...retained, _hydrated: true, linkedToolResults: results, _hydratedToolResultUuids: origins,
|
|
34
|
+
_hydratedMessageUuids: [...new Set([...(retained._hydratedMessageUuids as string[] ?? []), uuid])] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function matchesMessageTarget(message: SdkMessageLike, uuid: string): boolean {
|
|
38
|
+
return message.uuid === uuid || (message._hydratedMessageUuids as string[] | undefined)?.includes(uuid) === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A result can live on its tool call or on its original user record. Keep its
|
|
42
|
+
* own identity after loading; the owner's UUID cannot identify several results. */
|
|
43
|
+
export function toolResultSourceUuid(message: SdkMessageLike, id: string, result: ToolResult): string | undefined {
|
|
44
|
+
return result.deferredUuid ?? (message._toolResultUuids as Record<string, string> | undefined)?.[id]
|
|
45
|
+
?? (message._hydratedToolResultUuids as Record<string, string> | undefined)?.[id];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resultBlocks(message: SdkMessageLike): { tool_use_id: string; content: unknown; is_error?: boolean }[] {
|
|
49
|
+
const blocks = (message.message as { content?: unknown[] } | undefined)?.content;
|
|
50
|
+
return Array.isArray(blocks) ? blocks.filter((b): b is { type: string; tool_use_id: string; content: unknown; is_error?: boolean } =>
|
|
51
|
+
!!b && typeof b === "object" && (b as { type?: string }).type === "tool_result") : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Keep the frozen browsing range/order while adopting bodies hydrated during
|
|
55
|
+
* the request. Full tool results follow their UUID across linked/standalone
|
|
56
|
+
* representations, including when paging trims away their original tool call. */
|
|
57
|
+
export function preserveHydratedBodies(items: SdkMessageLike[], current: SdkMessageLike[]): SdkMessageLike[] {
|
|
58
|
+
const hydrated = new Map(current.filter((m) => m.uuid && m._hydrated).map((m) => [m.uuid, m]));
|
|
59
|
+
const results = new Map<string, Map<string, ToolResult>>();
|
|
60
|
+
const remember = (uuid: string, id: string, result: ToolResult) => {
|
|
61
|
+
let byTool = results.get(uuid);
|
|
62
|
+
if (!byTool) results.set(uuid, byTool = new Map());
|
|
63
|
+
byTool.set(id, result);
|
|
64
|
+
};
|
|
65
|
+
for (const message of current) {
|
|
66
|
+
if (!message._hydrated) continue;
|
|
67
|
+
if (message.uuid && !message._deferred) {
|
|
68
|
+
for (const block of resultBlocks(message)) remember(message.uuid, block.tool_use_id,
|
|
69
|
+
{ content: block.content, isError: !!block.is_error, structured: message.tool_use_result });
|
|
70
|
+
}
|
|
71
|
+
for (const [id, result] of Object.entries(message.linkedToolResults as Record<string, ToolResult> ?? {})) {
|
|
72
|
+
const uuid = toolResultSourceUuid(message, id, result);
|
|
73
|
+
if (uuid && !result.deferredUuid) remember(uuid, id, result);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return items.map((item) => {
|
|
77
|
+
const full = hydrated.get(item.uuid);
|
|
78
|
+
// Linked hydration fills just a result; its owner's body may still be deferred.
|
|
79
|
+
let next = full && !full._deferred ? mergeMessageBody(item, full) : item;
|
|
80
|
+
let linked = next.linkedToolResults as Record<string, ToolResult> | undefined;
|
|
81
|
+
let origins = next._hydratedToolResultUuids as Record<string, string> | undefined;
|
|
82
|
+
let copied = false;
|
|
83
|
+
const adopt = (id: string, uuid: string, result: ToolResult) => {
|
|
84
|
+
if (!copied) { linked = { ...linked }; origins = { ...origins }; copied = true; }
|
|
85
|
+
linked![id] = result;
|
|
86
|
+
origins![id] = uuid;
|
|
87
|
+
};
|
|
88
|
+
// Overlapping owner records can omit a previously linked result altogether.
|
|
89
|
+
// Retain only exact hydrated results, and never replace a different result UUID.
|
|
90
|
+
for (const [id, result] of Object.entries(full?.linkedToolResults as Record<string, ToolResult> ?? {})) {
|
|
91
|
+
const uuid = full && toolResultSourceUuid(full, id, result);
|
|
92
|
+
if (uuid && !result.deferredUuid && !linked?.[id]) adopt(id, uuid, result);
|
|
93
|
+
}
|
|
94
|
+
for (const [id, result] of Object.entries(linked ?? {})) {
|
|
95
|
+
const uuid = toolResultSourceUuid(next, id, result);
|
|
96
|
+
const body = uuid && results.get(uuid)?.get(id);
|
|
97
|
+
// An ordinary complete page response is authoritative too. Keep its exact
|
|
98
|
+
// provenance so a later deferred page can still find the loaded result.
|
|
99
|
+
if (body) adopt(id, uuid!, result.deferredUuid ? body : result);
|
|
100
|
+
}
|
|
101
|
+
const uuid = next.uuid;
|
|
102
|
+
if (uuid && next._deferred) {
|
|
103
|
+
// Display previews can omit whole blocks. The complete results already have
|
|
104
|
+
// exact provenance, so transfer every known result for this record UUID.
|
|
105
|
+
for (const [id, body] of results.get(uuid) ?? []) adopt(id, uuid, body);
|
|
106
|
+
} else if (uuid && !next._hydrated && resultBlocks(next).some((block) => results.get(uuid)?.has(block.tool_use_id))) {
|
|
107
|
+
next = { ...next, _hydrated: true };
|
|
108
|
+
}
|
|
109
|
+
if (copied) {
|
|
110
|
+
// The rest of a deferred user record (text/images/other results) is still a
|
|
111
|
+
// preview. Store complete results in the overlay without claiming a full body.
|
|
112
|
+
next = { ...next, _hydrated: true, linkedToolResults: linked, _hydratedToolResultUuids: origins,
|
|
113
|
+
_hydratedMessageUuids: [...new Set(Object.values(origins!))] };
|
|
114
|
+
}
|
|
115
|
+
return next;
|
|
116
|
+
});
|
|
117
|
+
}
|