@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Fragment, useMemo, useState, useSyncExternalStore, type ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { ExtraDirSource, ExtraDirsInfo, TurnChanges, TurnChangesInfo, TurnFile, TurnFileDiff } from "@shared/project.ts";
|
|
5
|
+
import type { SessionListItem } from "@shared/protocol.ts";
|
|
6
|
+
import type { Item } from "../transcript.ts";
|
|
7
|
+
import { api, ApiError } from "../api.ts";
|
|
8
|
+
import { fromHunks, lineDiff } from "../diff.ts";
|
|
9
|
+
import { DiffView } from "./Items.tsx";
|
|
10
|
+
import { Modal } from "./Modal.tsx";
|
|
11
|
+
import { Button, cx } from "../ui/Button.tsx";
|
|
12
|
+
import { SwitchRow, Tag, monoInputCls } from "../ui/Form.tsx";
|
|
13
|
+
import { BranchIcon, ChevronRightIcon, FolderIcon, PlusSmIcon } from "../ui/icons.tsx";
|
|
14
|
+
|
|
15
|
+
// ---------- files changed by each turn ----------
|
|
16
|
+
|
|
17
|
+
const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-turn change bars, placed at the end of each turn (just before the next turn's prompt,
|
|
21
|
+
* or at the end of the transcript). Refetched whenever an edit finishes or a turn ends.
|
|
22
|
+
*/
|
|
23
|
+
export function useTurnChangeSlots(pid: string, sid: string, items: Item[], running: boolean) {
|
|
24
|
+
const edits = items.filter((it) => it.kind === "tool" && FILE_TOOLS.has(it.name) && it.result).length;
|
|
25
|
+
const results = items.filter((it) => it.kind === "result").length;
|
|
26
|
+
const q = useQuery({
|
|
27
|
+
queryKey: ["turn-changes", pid, sid, edits, results, running],
|
|
28
|
+
queryFn: () => api<TurnChangesInfo>(`/api/projects/${pid}/sessions/${sid}/turn-changes`),
|
|
29
|
+
placeholderData: (prev) => prev,
|
|
30
|
+
});
|
|
31
|
+
return useMemo(() => {
|
|
32
|
+
const byPrompt = new Map<string, TurnChanges[]>();
|
|
33
|
+
for (const t of q.data?.turns ?? []) byPrompt.set(t.prompt, [...(byPrompt.get(t.prompt) ?? []), t]);
|
|
34
|
+
const before = new Map<string, ReactNode>();
|
|
35
|
+
let open: TurnChanges[] = [];
|
|
36
|
+
const render = (turns: TurnChanges[]) =>
|
|
37
|
+
turns.length ? (
|
|
38
|
+
<>
|
|
39
|
+
{turns.map((t) => (
|
|
40
|
+
<TurnChangesBar key={t.id} pid={pid} sid={sid} turn={t} />
|
|
41
|
+
))}
|
|
42
|
+
</>
|
|
43
|
+
) : null;
|
|
44
|
+
for (const it of items) {
|
|
45
|
+
if (it.kind !== "user") continue;
|
|
46
|
+
// A turn ends where the next prompt (any user message, even one that changed nothing) starts.
|
|
47
|
+
if (open.length) before.set(it.key, render(open));
|
|
48
|
+
open = byPrompt.get(it.key) ?? [];
|
|
49
|
+
}
|
|
50
|
+
return { before: (key: string) => before.get(key) ?? null, tail: render(open) };
|
|
51
|
+
}, [q.data, items, pid, sid]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Render items with the per-turn change bars in between. */
|
|
55
|
+
export function WithTurnChanges({ items, slots, render }: { items: Item[]; slots: ReturnType<typeof useTurnChangeSlots>; render: (it: Item) => ReactNode }) {
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{items.map((it) => (
|
|
59
|
+
<Fragment key={it.key}>
|
|
60
|
+
{slots.before(it.key)}
|
|
61
|
+
{render(it)}
|
|
62
|
+
</Fragment>
|
|
63
|
+
))}
|
|
64
|
+
{slots.tail}
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const STATUS_STYLE: Record<TurnFile["status"], string> = { A: "text-ok", M: "text-accent", D: "text-danger" };
|
|
70
|
+
const STATUS_LABEL: Record<TurnFile["status"], string> = { A: "新增", M: "修改", D: "删除" };
|
|
71
|
+
|
|
72
|
+
function TurnChangesBar({ pid, sid, turn }: { pid: string; sid: string; turn: TurnChanges }) {
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const [file, setFile] = useState<string>();
|
|
75
|
+
const adds = turn.files.reduce((n, f) => n + f.additions, 0);
|
|
76
|
+
const dels = turn.files.reduce((n, f) => n + f.deletions, 0);
|
|
77
|
+
return (
|
|
78
|
+
<div className="rounded-lg border border-border bg-surface/60 text-sm" data-testid="turn-changes" data-turn={turn.id}>
|
|
79
|
+
<button className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left text-muted" onClick={() => setOpen((v) => !v)} aria-expanded={open}>
|
|
80
|
+
<ChevronRightIcon size={12} className={cx("shrink-0 transition-transform", open && "rotate-90")} />
|
|
81
|
+
<span className="text-text">本轮改动:{turn.files.length} 个文件</span>
|
|
82
|
+
<span className="text-xs">
|
|
83
|
+
<span className="text-ok">+{adds}</span> <span className="text-danger">-{dels}</span>
|
|
84
|
+
</span>
|
|
85
|
+
</button>
|
|
86
|
+
{open && (
|
|
87
|
+
<div className="border-t border-border">
|
|
88
|
+
<ul className="py-1">
|
|
89
|
+
{turn.files.map((f) => (
|
|
90
|
+
<li key={f.path}>
|
|
91
|
+
<button
|
|
92
|
+
className={`flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left md:py-1.5 ${file === f.path ? "bg-surface-2" : "hover:bg-surface-2/70"}`}
|
|
93
|
+
onClick={() => setFile(file === f.path ? undefined : f.path)}
|
|
94
|
+
aria-expanded={file === f.path}
|
|
95
|
+
data-testid="turn-file"
|
|
96
|
+
data-path={f.path}
|
|
97
|
+
>
|
|
98
|
+
<span className={`w-4 shrink-0 font-mono text-xs font-semibold ${STATUS_STYLE[f.status]}`} title={STATUS_LABEL[f.status]}>
|
|
99
|
+
{f.status}
|
|
100
|
+
</span>
|
|
101
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[13px]" title={f.path}>
|
|
102
|
+
{f.path}
|
|
103
|
+
</span>
|
|
104
|
+
{f.unchanged ? (
|
|
105
|
+
<span className="shrink-0 text-xs text-muted">无净变化</span>
|
|
106
|
+
) : (
|
|
107
|
+
<span className="shrink-0 text-xs">
|
|
108
|
+
<span className="text-ok">+{f.additions}</span> <span className="text-danger">-{f.deletions}</span>
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
{file === f.path && <TurnFileDiffView pid={pid} sid={sid} turn={turn.id} path={f.path} />}
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
<p className="border-t border-border px-3 py-1.5 text-xs text-muted">
|
|
117
|
+
根据 CLI 的代码检查点和本轮的 Edit/Write 调用统计;Bash 命令和子 agent 改动的文件追踪不到,不在此列。
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function TurnFileDiffView({ pid, sid, turn, path }: { pid: string; sid: string; turn: string; path: string }) {
|
|
126
|
+
const diff = useQuery({
|
|
127
|
+
queryKey: ["turn-diff", pid, sid, turn, path],
|
|
128
|
+
queryFn: () => api<TurnFileDiff>(`/api/projects/${pid}/sessions/${sid}/turn-changes/diff?${new URLSearchParams({ turn, path })}`),
|
|
129
|
+
});
|
|
130
|
+
const rows = useMemo(() => {
|
|
131
|
+
const d = diff.data;
|
|
132
|
+
if (!d || d.binary) return [];
|
|
133
|
+
if (d.patches) return fromHunks(d.patches);
|
|
134
|
+
return lineDiff(d.before ?? "", d.after ?? "");
|
|
135
|
+
}, [diff.data]);
|
|
136
|
+
if (diff.isPending) return <p className="px-3 py-2 text-xs text-muted">加载中…</p>;
|
|
137
|
+
if (diff.isError) return <p className="px-3 py-2 text-xs text-danger">{diff.error instanceof ApiError ? diff.error.message : "读取失败"}</p>;
|
|
138
|
+
if (diff.data.binary) return <p className="px-3 py-2 text-xs text-muted">二进制文件,无法显示差异。</p>;
|
|
139
|
+
return (
|
|
140
|
+
<div className="border-y border-border" data-testid="turn-diff">
|
|
141
|
+
{diff.data.patches && <p className="bg-surface-2/60 px-3 py-1 text-xs text-muted">没有检查点,按本轮各次编辑的补丁显示。</p>}
|
|
142
|
+
{diff.data.truncated && <p className="bg-surface-2/60 px-3 py-1 text-xs text-muted">文件太大,只比较了前 1 MiB。</p>}
|
|
143
|
+
<DiffView rows={rows} />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------- extra directories (/add-dir) ----------
|
|
149
|
+
|
|
150
|
+
let dirsDialogFor: string | undefined;
|
|
151
|
+
const dirsListeners = new Set<() => void>();
|
|
152
|
+
const setDirsDialog = (sid: string | undefined) => {
|
|
153
|
+
dirsDialogFor = sid;
|
|
154
|
+
for (const l of dirsListeners) l();
|
|
155
|
+
};
|
|
156
|
+
/** Open the extra-directories dialog of a session (e.g. from the session menu). */
|
|
157
|
+
export const openDirsDialog = (sid: string) => setDirsDialog(sid);
|
|
158
|
+
|
|
159
|
+
function useDirsDialog(sid: string) {
|
|
160
|
+
return useSyncExternalStore(
|
|
161
|
+
(l) => {
|
|
162
|
+
dirsListeners.add(l);
|
|
163
|
+
return () => dirsListeners.delete(l);
|
|
164
|
+
},
|
|
165
|
+
() => dirsDialogFor === sid,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function useExtraDirs(pid: string, sid: string) {
|
|
170
|
+
return useQuery({ queryKey: ["dirs", pid, sid], queryFn: () => api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs`) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `/add-dir <path>` from the composer. */
|
|
174
|
+
export function useAddDir(pid: string, sid: string) {
|
|
175
|
+
const qc = useQueryClient();
|
|
176
|
+
return async (path: string, remember = false) => {
|
|
177
|
+
const info = await api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs`, { method: "POST", json: { path, remember } });
|
|
178
|
+
qc.setQueryData(["dirs", pid, sid], info);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SOURCE_LABEL: Record<ExtraDirSource, string> = { session: "本会话", local: "已记住", project: "项目设置", user: "用户设置" };
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Under the session header: the worktree the session runs in, and the directories outside the
|
|
186
|
+
* project it may access. Hidden when there is neither.
|
|
187
|
+
*/
|
|
188
|
+
export function SessionContextBar({ pid, sid, meta }: { pid: string; sid: string; meta?: SessionListItem }) {
|
|
189
|
+
const dirs = useExtraDirs(pid, sid);
|
|
190
|
+
const open = useDirsDialog(sid);
|
|
191
|
+
const list = dirs.data?.dirs ?? [];
|
|
192
|
+
if (!meta?.worktree && list.length === 0 && !open) return null;
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-4 py-1.5 text-xs text-muted md:px-6" data-testid="session-context">
|
|
195
|
+
{meta?.worktree && (
|
|
196
|
+
<Link to={`/p/${pid}/worktrees`} className="flex min-w-0 items-center gap-1 hover:text-accent" title={meta.cwd} data-testid="session-worktree">
|
|
197
|
+
<BranchIcon size={12} className="shrink-0" />
|
|
198
|
+
<span className="truncate">
|
|
199
|
+
worktree <span className="font-mono text-text">{meta.worktree}</span>
|
|
200
|
+
</span>
|
|
201
|
+
</Link>
|
|
202
|
+
)}
|
|
203
|
+
{list.length > 0 && (
|
|
204
|
+
<button className="flex min-w-0 items-center gap-1 hover:text-accent" onClick={() => setDirsDialog(sid)} data-testid="extra-dirs-button">
|
|
205
|
+
<FolderIcon size={12} className="shrink-0" />
|
|
206
|
+
<span className="truncate">额外目录 {list.length}</span>
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
{open && <ExtraDirsDialog pid={pid} sid={sid} info={dirs.data} onClose={() => setDirsDialog(undefined)} />}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ExtraDirsDialog({ pid, sid, info, onClose }: { pid: string; sid: string; info?: ExtraDirsInfo; onClose: () => void }) {
|
|
215
|
+
const qc = useQueryClient();
|
|
216
|
+
const addDir = useAddDir(pid, sid);
|
|
217
|
+
const [path, setPath] = useState("");
|
|
218
|
+
const [remember, setRemember] = useState(false);
|
|
219
|
+
const add = useMutation({
|
|
220
|
+
mutationFn: () => addDir(path, remember),
|
|
221
|
+
onSuccess: () => setPath(""),
|
|
222
|
+
});
|
|
223
|
+
const remove = useMutation({
|
|
224
|
+
mutationFn: (dir: string) => api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs?${new URLSearchParams({ path: dir })}`, { method: "DELETE" }),
|
|
225
|
+
onSuccess: (data) => qc.setQueryData(["dirs", pid, sid], data),
|
|
226
|
+
});
|
|
227
|
+
return (
|
|
228
|
+
<Modal title="额外目录" onClose={onClose} testId="dirs-dialog">
|
|
229
|
+
<p className="text-sm text-muted">
|
|
230
|
+
除了会话的工作目录{info && <span className="font-mono"> {info.cwd}</span>},Claude 还可以读写这些目录(相当于 CLI 的 /add-dir)。运行中的会话立即生效。
|
|
231
|
+
</p>
|
|
232
|
+
<ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-surface empty:hidden" aria-label="额外目录列表">
|
|
233
|
+
{info?.dirs.map((d) => {
|
|
234
|
+
const removable = d.sources.includes("session") || d.sources.includes("local");
|
|
235
|
+
return (
|
|
236
|
+
<li key={d.path} className="flex min-w-0 items-center gap-2 py-2 pl-3 pr-1.5" data-testid="extra-dir" data-path={d.path}>
|
|
237
|
+
<FolderIcon className="shrink-0 text-muted" />
|
|
238
|
+
<div className="min-w-0 flex-1">
|
|
239
|
+
<div className={cx("truncate font-mono text-[13px]", !d.exists && "text-danger line-through")} title={d.path}>
|
|
240
|
+
{d.path}
|
|
241
|
+
</div>
|
|
242
|
+
<div className="mt-1 flex flex-wrap gap-1">
|
|
243
|
+
{d.sources.map((s) => (
|
|
244
|
+
<Tag key={s} tone={s === "session" ? "accent" : "neutral"}>
|
|
245
|
+
{SOURCE_LABEL[s]}
|
|
246
|
+
</Tag>
|
|
247
|
+
))}
|
|
248
|
+
{!d.exists && <Tag tone="danger">目录不存在</Tag>}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
{removable ? (
|
|
252
|
+
<Button variant="ghost" size="sm" className="text-danger hover:text-danger" onClick={() => remove.mutate(d.path)} disabled={remove.isPending}>
|
|
253
|
+
移除
|
|
254
|
+
</Button>
|
|
255
|
+
) : (
|
|
256
|
+
<span className="shrink-0 px-2 text-xs text-muted" title="在设置文件里,请在设置页修改">
|
|
257
|
+
只读
|
|
258
|
+
</span>
|
|
259
|
+
)}
|
|
260
|
+
</li>
|
|
261
|
+
);
|
|
262
|
+
})}
|
|
263
|
+
</ul>
|
|
264
|
+
{info && info.dirs.length === 0 && <p className="mt-3 rounded-lg border border-dashed border-border px-3 py-4 text-center text-sm text-muted">还没有额外目录。在下面填一个绝对路径即可添加。</p>}
|
|
265
|
+
<form
|
|
266
|
+
className="mt-4 space-y-2"
|
|
267
|
+
onSubmit={(e) => {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
if (path.trim()) add.mutate();
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<input aria-label="目录路径" placeholder="/home/you/other-repo" value={path} onChange={(e) => setPath(e.target.value)} className={monoInputCls} />
|
|
273
|
+
<SwitchRow
|
|
274
|
+
checked={remember}
|
|
275
|
+
onChange={setRemember}
|
|
276
|
+
label="记住这个目录"
|
|
277
|
+
desc={
|
|
278
|
+
<>
|
|
279
|
+
写进 <span className="font-mono">.claude/settings.local.json</span>,以后这个目录下的会话(包括 CLI)都能访问
|
|
280
|
+
</>
|
|
281
|
+
}
|
|
282
|
+
/>
|
|
283
|
+
{(add.error || remove.error) && (
|
|
284
|
+
<p role="alert" className="text-xs text-danger">
|
|
285
|
+
{((add.error ?? remove.error) as Error).message}
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
288
|
+
<div className="flex justify-end gap-2 border-t border-border pt-3">
|
|
289
|
+
<Button variant="ghost" onClick={onClose}>
|
|
290
|
+
完成
|
|
291
|
+
</Button>
|
|
292
|
+
<Button type="submit" variant="primary" icon={<PlusSmIcon />} disabled={!path.trim()} loading={add.isPending}>
|
|
293
|
+
添加
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</form>
|
|
297
|
+
</Modal>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Link, useLocation } from "react-router";
|
|
3
|
+
import { cx } from "../ui/Button.tsx";
|
|
4
|
+
import { BranchIcon, ChatIcon, DiffIcon, FileIcon } from "../ui/icons.tsx";
|
|
5
|
+
import { projectFilesUrl, useProjectFilesState } from "../project-files-state.ts";
|
|
6
|
+
|
|
7
|
+
const tabs = (pid: string, file: string) => [
|
|
8
|
+
{ to: `/p/${pid}`, base: `/p/${pid}`, label: "项目概览", end: true, icon: <ChatIcon /> },
|
|
9
|
+
{ to: projectFilesUrl(pid, file), base: `/p/${pid}/files`, label: "文件", end: false, icon: <FileIcon /> },
|
|
10
|
+
{ to: `/p/${pid}/changes`, base: `/p/${pid}/changes`, label: "变更", end: false, icon: <DiffIcon /> },
|
|
11
|
+
{ to: `/p/${pid}/worktrees`, base: `/p/${pid}/worktrees`, label: "工作区", end: false, icon: <BranchIcon /> },
|
|
12
|
+
];
|
|
13
|
+
const activeTab = (pathname: string, tab: { base: string; end: boolean }) => pathname === tab.base || !tab.end && pathname.startsWith(`${tab.base}/`);
|
|
14
|
+
|
|
15
|
+
/** Project navigation, kept in the main page instead of duplicated in the sidebar. */
|
|
16
|
+
export function ProjectTabs({ projectId, children }: { projectId: string; children?: ReactNode }) {
|
|
17
|
+
const { pathname } = useLocation();
|
|
18
|
+
const state = useProjectFilesState(projectId);
|
|
19
|
+
return (
|
|
20
|
+
<header className="flex min-w-0 items-center gap-2 border-b border-border px-2 py-1.5 md:px-4">
|
|
21
|
+
<nav aria-label="项目页面" className="flex min-w-0 flex-1 gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
22
|
+
{tabs(projectId, state.file).map((t) => (
|
|
23
|
+
<Link
|
|
24
|
+
key={t.base}
|
|
25
|
+
to={t.to}
|
|
26
|
+
aria-current={activeTab(pathname, t) ? "page" : undefined}
|
|
27
|
+
className={cx(
|
|
28
|
+
"flex h-10 shrink-0 items-center gap-1.5 rounded-md px-3 text-sm transition-colors md:h-8",
|
|
29
|
+
activeTab(pathname, t) ? "bg-surface-2 font-medium text-text" : "text-muted hover:bg-surface-2/70 hover:text-text",
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<span aria-hidden className="hidden sm:inline">
|
|
33
|
+
{t.icon}
|
|
34
|
+
</span>
|
|
35
|
+
{t.label}
|
|
36
|
+
</Link>
|
|
37
|
+
))}
|
|
38
|
+
</nav>
|
|
39
|
+
{children && <div className="flex shrink-0 items-center gap-1.5">{children}</div>}
|
|
40
|
+
</header>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const encodePath = (p: string) => p.split("/").map(encodeURIComponent).join("/");
|
|
45
|
+
|
|
46
|
+
export const fileApi = (pid: string, endpoint: string, params: Record<string, string>) =>
|
|
47
|
+
`/api/projects/${pid}/${endpoint}?${new URLSearchParams(params).toString()}`;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { SessionFileInfo, SessionListItem } from "@shared/protocol.ts";
|
|
5
|
+
import { api, ApiError } from "../api.ts";
|
|
6
|
+
import { setDraft } from "../drafts.ts";
|
|
7
|
+
import { Menu, MenuItem, Modal } from "./Modal.tsx";
|
|
8
|
+
import { CopyButton } from "./CopyButton.tsx";
|
|
9
|
+
import { Button } from "../ui/Button.tsx";
|
|
10
|
+
import { RadioCards } from "../ui/Controls.tsx";
|
|
11
|
+
import { copyText } from "../clipboard.ts";
|
|
12
|
+
import { openDirsDialog } from "./ProjectExtras.tsx";
|
|
13
|
+
import { SessionChannelsDialog } from "./SessionChannels.tsx";
|
|
14
|
+
|
|
15
|
+
type RewindMode = "conversation" | "code" | "both";
|
|
16
|
+
type FilesPreview = { canRewind: boolean; error?: string; filesChanged?: string[]; insertions?: number; deletions?: number };
|
|
17
|
+
|
|
18
|
+
function useInvalidateSessions(pid: string) {
|
|
19
|
+
const qc = useQueryClient();
|
|
20
|
+
return () => qc.invalidateQueries({ queryKey: ["sessions", pid] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Header menu: rename, pin, archive, fork, export, delete. */
|
|
24
|
+
export function SessionMenu({ pid, sid, meta }: { pid: string; sid: string; meta?: SessionListItem }) {
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
const invalidate = useInvalidateSessions(pid);
|
|
27
|
+
const [renaming, setRenaming] = useState(false);
|
|
28
|
+
const [deleting, setDeleting] = useState(false);
|
|
29
|
+
const [channels, setChannels] = useState(false);
|
|
30
|
+
const [info, setInfo] = useState(false);
|
|
31
|
+
const patch = (body: object) => api(`/api/projects/${pid}/sessions/${sid}`, { method: "PATCH", json: body }).then(invalidate);
|
|
32
|
+
const fork = useMutation({
|
|
33
|
+
mutationFn: () => api<{ id: string }>(`/api/projects/${pid}/sessions/${sid}/fork`, { method: "POST", json: {} }),
|
|
34
|
+
onSuccess: ({ id }) => {
|
|
35
|
+
invalidate();
|
|
36
|
+
navigate(`/p/${pid}/s/${id}`);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<Menu label="会话操作" testId="session-menu">
|
|
43
|
+
<MenuItem onClick={() => setRenaming(true)}>重命名</MenuItem>
|
|
44
|
+
<MenuItem onClick={() => patch({ pinned: !meta?.pinned })}>{meta?.pinned ? "取消置顶" : "置顶"}</MenuItem>
|
|
45
|
+
<MenuItem onClick={() => patch({ archived: !meta?.archived })}>{meta?.archived ? "取消归档" : "归档"}</MenuItem>
|
|
46
|
+
<MenuItem onClick={() => fork.mutate()}>Fork 整个会话</MenuItem>
|
|
47
|
+
<MenuItem onClick={() => openDirsDialog(sid)}>额外目录…</MenuItem>
|
|
48
|
+
<MenuItem onClick={() => setChannels(true)}>推送与订阅…</MenuItem>
|
|
49
|
+
<MenuItem onClick={() => window.open(`/api/projects/${pid}/sessions/${sid}/export`, "_blank")}>导出对话</MenuItem>
|
|
50
|
+
<MenuItem onClick={() => void copyText(sid)}>复制会话 ID</MenuItem>
|
|
51
|
+
<MenuItem onClick={() => setInfo(true)}>会话文件…</MenuItem>
|
|
52
|
+
<MenuItem onClick={() => void copyText(`claude --resume ${sid}`)}>复制 CLI 续聊命令</MenuItem>
|
|
53
|
+
<MenuItem danger onClick={() => setDeleting(true)}>
|
|
54
|
+
删除
|
|
55
|
+
</MenuItem>
|
|
56
|
+
</Menu>
|
|
57
|
+
{renaming && <RenameDialog current={meta?.title ?? ""} onClose={() => setRenaming(false)} onSave={(title) => patch({ title }).then(() => setRenaming(false))} />}
|
|
58
|
+
{channels && <SessionChannelsDialog key={sid} pid={pid} sid={sid} onClose={() => setChannels(false)} />}
|
|
59
|
+
{info && <SessionFileDialog pid={pid} sid={sid} onClose={() => setInfo(false)} />}
|
|
60
|
+
{deleting && (
|
|
61
|
+
<Modal title="删除会话?" onClose={() => setDeleting(false)} testId="delete-dialog">
|
|
62
|
+
<p className="text-sm text-muted">会话文件会从磁盘删除,CLI 里也将看不到它。这个操作无法撤销。</p>
|
|
63
|
+
<div className="mt-5 flex justify-end gap-2">
|
|
64
|
+
<Button variant="ghost" onClick={() => setDeleting(false)}>
|
|
65
|
+
取消
|
|
66
|
+
</Button>
|
|
67
|
+
<Button
|
|
68
|
+
variant="danger"
|
|
69
|
+
onClick={async () => {
|
|
70
|
+
await api(`/api/projects/${pid}/sessions/${sid}`, { method: "DELETE" });
|
|
71
|
+
invalidate();
|
|
72
|
+
navigate(`/p/${pid}`);
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
确认删除
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</Modal>
|
|
79
|
+
)}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function RenameDialog({ current, onClose, onSave }: { current: string; onClose: () => void; onSave: (t: string) => void }) {
|
|
85
|
+
const [title, setTitle] = useState(current);
|
|
86
|
+
return (
|
|
87
|
+
<Modal title="重命名会话" onClose={onClose}>
|
|
88
|
+
<form
|
|
89
|
+
onSubmit={(e) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
if (title.trim()) onSave(title.trim());
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<input
|
|
95
|
+
aria-label="会话标题"
|
|
96
|
+
autoFocus
|
|
97
|
+
value={title}
|
|
98
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
99
|
+
className="h-10 w-full rounded-md border border-border bg-surface px-3 text-base outline-none focus:border-accent/60 focus:ring-2 focus:ring-accent/20 md:h-9 md:text-sm"
|
|
100
|
+
/>
|
|
101
|
+
<div className="mt-4 flex justify-end gap-2">
|
|
102
|
+
<Button variant="ghost" onClick={onClose}>
|
|
103
|
+
取消
|
|
104
|
+
</Button>
|
|
105
|
+
<Button type="submit" variant="primary">
|
|
106
|
+
保存
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
</form>
|
|
110
|
+
</Modal>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Session ID, working directory and transcript path, each copyable. */
|
|
115
|
+
function SessionFileDialog({ pid, sid, onClose }: { pid: string; sid: string; onClose: () => void }) {
|
|
116
|
+
const info = useQuery({
|
|
117
|
+
queryKey: ["session-info", pid, sid],
|
|
118
|
+
queryFn: () => api<SessionFileInfo>(`/api/projects/${pid}/sessions/${sid}/info`),
|
|
119
|
+
staleTime: 0,
|
|
120
|
+
});
|
|
121
|
+
return (
|
|
122
|
+
<Modal title="会话文件" onClose={onClose} testId="session-info-dialog">
|
|
123
|
+
<dl className="space-y-4 text-sm">
|
|
124
|
+
<InfoRow label="会话 ID" value={sid} testId="session-info-id" />
|
|
125
|
+
{info.isPending ? (
|
|
126
|
+
<p className="text-xs text-muted">正在读取会话文件…</p>
|
|
127
|
+
) : info.error ? (
|
|
128
|
+
<p role="alert" className="text-xs text-danger">读取失败:{info.error instanceof Error ? info.error.message : String(info.error)}</p>
|
|
129
|
+
) : (
|
|
130
|
+
<>
|
|
131
|
+
<InfoRow label="工作目录" value={info.data.cwd} testId="session-info-cwd" />
|
|
132
|
+
<InfoRow label="会话文件" value={info.data.file} empty="还没有写入磁盘。发送第一条消息后,CLI 会创建这个文件。" testId="session-info-file" />
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</dl>
|
|
136
|
+
<p className="mt-5 text-xs leading-relaxed text-muted">会话文件是 JSONL 格式,每行一条消息,可以用文本编辑器直接查看或备份。</p>
|
|
137
|
+
</Modal>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function InfoRow({ label, value, empty, testId }: { label: string; value: string | null; empty?: string; testId: string }) {
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
<dt className="mb-1 text-xs text-muted">{label}</dt>
|
|
145
|
+
{value ? (
|
|
146
|
+
<dd className="flex items-start gap-2">
|
|
147
|
+
<code className="min-w-0 flex-1 break-all rounded-md bg-surface-2 px-2 py-1.5 font-mono text-xs" data-testid={testId}>{value}</code>
|
|
148
|
+
<CopyButton text={value} label={`复制${label}`} className="shrink-0" />
|
|
149
|
+
</dd>
|
|
150
|
+
) : (
|
|
151
|
+
<dd className="text-xs text-muted" data-testid={testId}>{empty}</dd>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** "Forked from …" link back to the origin session. */
|
|
158
|
+
export function Lineage({ pid, meta, sessions }: { pid: string; meta?: SessionListItem; sessions?: SessionListItem[] }) {
|
|
159
|
+
if (!meta?.forkedFrom) return null;
|
|
160
|
+
const origin = sessions?.find((s) => s.id === meta.forkedFrom);
|
|
161
|
+
return (
|
|
162
|
+
<Link to={`/p/${pid}/s/${meta.forkedFrom}`} className="truncate text-xs text-muted hover:text-accent" data-testid="lineage">
|
|
163
|
+
⑂ 分支自:{origin?.title || meta.forkedFrom.slice(0, 8)}
|
|
164
|
+
</Link>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Actions under a user message: fork from before it, or rewind to it. */
|
|
169
|
+
export function UserMessageActions({ pid, sid, uuid }: { pid: string; sid: string; uuid: string }) {
|
|
170
|
+
const navigate = useNavigate();
|
|
171
|
+
const invalidate = useInvalidateSessions(pid);
|
|
172
|
+
const [rewinding, setRewinding] = useState(false);
|
|
173
|
+
const [error, setError] = useState<string | null>(null);
|
|
174
|
+
async function fork() {
|
|
175
|
+
try {
|
|
176
|
+
const { id } = await api<{ id: string }>(`/api/projects/${pid}/sessions/${sid}/fork`, { method: "POST", json: { beforeUuid: uuid } });
|
|
177
|
+
invalidate();
|
|
178
|
+
navigate(`/p/${pid}/s/${id}`);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
setError(e instanceof ApiError ? e.message : "fork 失败");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<div className="flex justify-end">
|
|
185
|
+
<Menu label="消息操作" testId="message-menu">
|
|
186
|
+
<MenuItem onClick={fork}>从这条消息之前 fork</MenuItem>
|
|
187
|
+
<MenuItem onClick={() => setRewinding(true)}>回退到这里…</MenuItem>
|
|
188
|
+
</Menu>
|
|
189
|
+
{error && <span className="text-xs text-danger">{error}</span>}
|
|
190
|
+
{rewinding && <RewindDialog pid={pid} sid={sid} uuid={uuid} onClose={() => setRewinding(false)} />}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const MODES: { value: RewindMode; title: string; detail: string }[] = [
|
|
196
|
+
{
|
|
197
|
+
value: "conversation",
|
|
198
|
+
title: "只回退对话",
|
|
199
|
+
detail: "这条消息和它之后的对话从当前会话移除,消息内容放回输入框,可以修改后重新发送。项目文件不变。原来的对话仍保存在会话文件里,可以 fork 找回。",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
value: "code",
|
|
203
|
+
title: "只回退代码",
|
|
204
|
+
detail: "把 Claude 从这条消息起用 Edit/Write 改过的文件恢复到发送前的状态。对话不变。用 Bash 命令改动的文件不会恢复。",
|
|
205
|
+
},
|
|
206
|
+
{ value: "both", title: "对话和代码都回退", detail: "先恢复文件,再回退对话。" },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
function RewindDialog({ pid, sid, uuid, onClose }: { pid: string; sid: string; uuid: string; onClose: () => void }) {
|
|
210
|
+
const navigate = useNavigate();
|
|
211
|
+
const qc = useQueryClient();
|
|
212
|
+
const [mode, setMode] = useState<RewindMode>("conversation");
|
|
213
|
+
const [error, setError] = useState<string | null>(null);
|
|
214
|
+
const [busy, setBusy] = useState(false);
|
|
215
|
+
const preview = useQuery({
|
|
216
|
+
queryKey: ["rewind-preview", sid, uuid],
|
|
217
|
+
queryFn: () => api<{ files?: FilesPreview }>(`/api/projects/${pid}/sessions/${sid}/rewind`, { method: "POST", json: { userUuid: uuid, mode: "code", dryRun: true } }),
|
|
218
|
+
enabled: mode !== "conversation",
|
|
219
|
+
staleTime: 0,
|
|
220
|
+
});
|
|
221
|
+
const files = preview.data?.files;
|
|
222
|
+
|
|
223
|
+
async function confirm() {
|
|
224
|
+
setBusy(true);
|
|
225
|
+
setError(null);
|
|
226
|
+
try {
|
|
227
|
+
const r = await api<{ text: string; rewindId?: string; files?: FilesPreview; newSessionId?: string }>(`/api/projects/${pid}/sessions/${sid}/rewind`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
json: { userUuid: uuid, mode },
|
|
230
|
+
});
|
|
231
|
+
if (r.files && !r.files.canRewind) throw new ApiError(400, r.files.error || "无法回退代码");
|
|
232
|
+
qc.invalidateQueries({ queryKey: ["sessions", pid] });
|
|
233
|
+
if (mode !== "code") {
|
|
234
|
+
const target = r.newSessionId ?? sid;
|
|
235
|
+
setDraft(target, r.text, r.rewindId);
|
|
236
|
+
if (r.newSessionId) navigate(`/p/${pid}/s/${r.newSessionId}`);
|
|
237
|
+
}
|
|
238
|
+
onClose();
|
|
239
|
+
} catch (e) {
|
|
240
|
+
setError(e instanceof ApiError ? e.message : "回退失败");
|
|
241
|
+
} finally {
|
|
242
|
+
setBusy(false);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<Modal title="回退到这条消息" onClose={onClose} testId="rewind-dialog">
|
|
248
|
+
<RadioCards label="回退方式" value={mode} onChange={setMode} options={MODES.map((m) => ({ value: m.value, title: m.title, detail: m.detail }))} />
|
|
249
|
+
{mode !== "conversation" && (
|
|
250
|
+
<div className="mt-3 rounded-lg bg-surface-2 px-3 py-2 text-xs" data-testid="rewind-preview">
|
|
251
|
+
{preview.isPending
|
|
252
|
+
? "正在计算会恢复哪些文件…"
|
|
253
|
+
: files?.canRewind
|
|
254
|
+
? files.filesChanged?.length
|
|
255
|
+
? `将恢复 ${files.filesChanged.length} 个文件(+${files.insertions ?? 0} −${files.deletions ?? 0}):${files.filesChanged.join("、")}`
|
|
256
|
+
: "没有需要恢复的文件。"
|
|
257
|
+
: `无法回退代码:${files?.error ?? "没有检查点"}`}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
{error && <p className="mt-3 text-sm text-danger">{error}</p>}
|
|
261
|
+
<div className="mt-5 flex justify-end gap-2">
|
|
262
|
+
<Button variant="ghost" onClick={onClose}>
|
|
263
|
+
取消
|
|
264
|
+
</Button>
|
|
265
|
+
<Button variant="primary" loading={busy} onClick={confirm}>
|
|
266
|
+
回退
|
|
267
|
+
</Button>
|
|
268
|
+
</div>
|
|
269
|
+
</Modal>
|
|
270
|
+
);
|
|
271
|
+
}
|