@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,73 @@
|
|
|
1
|
+
import { Children, cloneElement, isValidElement, memo, useState, type ComponentProps, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import rehypeRaw from "rehype-raw";
|
|
5
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
6
|
+
import { markdownHtmlSchema, markdownUrlTransform, rehypeSvgImages } from "../markdown-html.ts";
|
|
7
|
+
import { resolveMarkdownAsset, type MarkdownAssetContext } from "../markdown-assets.ts";
|
|
8
|
+
import { CopyButton } from "./CopyButton.tsx";
|
|
9
|
+
import { remarkCopyTables } from "../markdown-tables.ts";
|
|
10
|
+
import { remarkInlineSvg } from "../markdown-svg.ts";
|
|
11
|
+
import { SvgPreview } from "./SvgPreview.tsx";
|
|
12
|
+
|
|
13
|
+
/** Code blocks longer than this show their first lines until "show all". */
|
|
14
|
+
const CODE_LINES = 150;
|
|
15
|
+
|
|
16
|
+
/** A fenced code block: long ones render their start first. */
|
|
17
|
+
function Pre({ children, node: _node, ...rest }: ComponentProps<"pre"> & { node?: unknown }) {
|
|
18
|
+
const [all, setAll] = useState(false);
|
|
19
|
+
const kids = Children.toArray(children);
|
|
20
|
+
const code = kids.length === 1 ? kids[0] : undefined;
|
|
21
|
+
const text = isValidElement(code) ? (code as ReactElement<{ children?: ReactNode }>).props.children : undefined;
|
|
22
|
+
const language = isValidElement(code) ? (code as ReactElement<{ className?: string }>).props.className : undefined;
|
|
23
|
+
const svg = typeof text === "string" && language === "language-svg" && /^\s*<svg\b/i.test(text);
|
|
24
|
+
let at = -1;
|
|
25
|
+
if (!all && typeof text === "string") {
|
|
26
|
+
for (let n = 0; n < CODE_LINES; n++) {
|
|
27
|
+
at = text.indexOf("\n", at + 1);
|
|
28
|
+
if (at === -1) break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const clipped = typeof text === "string" && at !== -1 && at < text.trimEnd().length;
|
|
32
|
+
return (
|
|
33
|
+
<div className="markdown-copy-block markdown-code-block" data-testid="markdown-code-block">
|
|
34
|
+
{typeof text === "string" && <CopyButton text={text} label="复制代码" className="markdown-copy-button" />}
|
|
35
|
+
{svg && <div className="rounded-lg border border-border p-4 pt-10"><SvgPreview markup={text} /></div>}
|
|
36
|
+
{svg ? <details className="mt-2"><summary className="cursor-pointer text-xs text-muted">SVG 源码</summary><pre {...rest}>{children}</pre></details>
|
|
37
|
+
: <pre {...rest}>{clipped ? cloneElement(code as ReactElement<{ children?: ReactNode }>, { children: text.slice(0, at) }) : children}</pre>}
|
|
38
|
+
{clipped && !svg && <button className="mt-1 text-xs text-accent hover:underline" onClick={() => setAll(true)} data-testid="show-all">
|
|
39
|
+
显示全部(共 {text.trimEnd().split("\n").length} 行)
|
|
40
|
+
</button>}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Table({ children, node: _node, "data-copy-markdown": text, ...rest }: ComponentProps<"table"> & { node?: unknown; "data-copy-markdown"?: string }) {
|
|
46
|
+
return <div className="markdown-copy-block markdown-table-block" data-testid="markdown-table-block">
|
|
47
|
+
{text !== undefined && <CopyButton text={text} label="复制表格" className="markdown-copy-button" />}
|
|
48
|
+
<div className="overflow-x-auto"><table {...rest}>{children}</table></div>
|
|
49
|
+
</div>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Image({ node: _node, "data-inline-svg": svg, assetContext, ...rest }: ComponentProps<"img"> & { node?: unknown; "data-inline-svg"?: string; assetContext?: MarkdownAssetContext }) {
|
|
53
|
+
if (svg) return <SvgPreview markup={svg} alt={rest.alt} />;
|
|
54
|
+
const src = resolveMarkdownAsset(rest.src ?? "", assetContext);
|
|
55
|
+
return <img {...rest} src={src} loading="lazy" />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const components: Components = { pre: Pre, table: Table };
|
|
59
|
+
|
|
60
|
+
export const Markdown = memo(function Markdown({ text, assetContext }: { text: string; assetContext?: MarkdownAssetContext }) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="prose-claude">
|
|
63
|
+
<ReactMarkdown
|
|
64
|
+
remarkPlugins={[remarkGfm, remarkCopyTables, remarkInlineSvg]}
|
|
65
|
+
rehypePlugins={[rehypeRaw, rehypeSvgImages, [rehypeSanitize, markdownHtmlSchema]]}
|
|
66
|
+
urlTransform={markdownUrlTransform}
|
|
67
|
+
components={{ ...components, img: (props) => <Image {...props} assetContext={assetContext} /> }}
|
|
68
|
+
>
|
|
69
|
+
{text}
|
|
70
|
+
</ReactMarkdown>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
|
|
2
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
+
import type { Item } from "../transcript.ts";
|
|
4
|
+
import { ItemView, TranscriptContext } from "./Items.tsx";
|
|
5
|
+
import type { useTurnChangeSlots } from "./ProjectExtras.tsx";
|
|
6
|
+
import { Button } from "../ui/Button.tsx";
|
|
7
|
+
import { ChevronDownIcon } from "../ui/icons.tsx";
|
|
8
|
+
import { ChevronIcon } from "./icons.tsx";
|
|
9
|
+
import { defaultTurnCollapsed, groupTurns, visibleTurnItems, turnRebases, rebaseTurnRow, rebaseTurnFolds, setTurnFold, type TranscriptTurn, type TurnFold } from "../turns.ts";
|
|
10
|
+
import { MESSAGE_FONT_OPTIONS, useMessageFontSize, type MessageFontSize } from "../message-font-size.ts";
|
|
11
|
+
import { formatMessageTime, isNewMessageTime } from "../message-time.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The session transcript. Short transcripts render in normal flow; long ones are
|
|
15
|
+
* virtualized (only rows near the viewport are in the DOM, heights measured as they
|
|
16
|
+
* render). Either way the list sticks to the bottom while new output arrives, unless
|
|
17
|
+
* the user has scrolled up.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Above this many rows the list is virtualized. */
|
|
21
|
+
const VIRTUALIZE_FROM = 150;
|
|
22
|
+
/** Distance from the bottom (px) that still counts as "at the bottom". */
|
|
23
|
+
const STICK_SLACK = 80;
|
|
24
|
+
|
|
25
|
+
type Row = { key: string; item?: Item; showTime?: boolean; before?: ReactNode; node?: ReactNode; nested?: boolean; turn?: { key: string; number: number; expanded: boolean } };
|
|
26
|
+
type ScrollAnchor = { key: string; offset: number; focus?: boolean };
|
|
27
|
+
let savedTurnFolds = new Map<string, TurnFold>();
|
|
28
|
+
let foldRevision = 0;
|
|
29
|
+
type HistoryControls = { older: boolean; newer: boolean; historical: boolean; loading: boolean; error?: string; load: (direction: "latest" | "older" | "newer") => Promise<void> };
|
|
30
|
+
|
|
31
|
+
export function MessageList({
|
|
32
|
+
items,
|
|
33
|
+
loading,
|
|
34
|
+
running,
|
|
35
|
+
waiting = false,
|
|
36
|
+
turnSlots,
|
|
37
|
+
history,
|
|
38
|
+
}: {
|
|
39
|
+
items: Item[];
|
|
40
|
+
loading: boolean;
|
|
41
|
+
running: boolean;
|
|
42
|
+
waiting?: boolean;
|
|
43
|
+
turnSlots: ReturnType<typeof useTurnChangeSlots>;
|
|
44
|
+
history?: HistoryControls;
|
|
45
|
+
}) {
|
|
46
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
47
|
+
const stick = useRef(true);
|
|
48
|
+
const scrollAnchor = useRef<ScrollAnchor | null>(null);
|
|
49
|
+
const visibleFrame = useRef<number | null>(null);
|
|
50
|
+
const preserveFontAnchor = useCallback(() => {
|
|
51
|
+
const el = ref.current;
|
|
52
|
+
if (!el || stick.current) return;
|
|
53
|
+
const top = el.getBoundingClientRect().top;
|
|
54
|
+
const row = [...el.querySelectorAll<HTMLElement>("[data-row-key]")].find((row) => row.getBoundingClientRect().bottom > top);
|
|
55
|
+
if (row) scrollAnchor.current = { key: row.dataset.rowKey!, offset: row.getBoundingClientRect().top - top };
|
|
56
|
+
}, []);
|
|
57
|
+
const fontSize = useMessageFontSize(preserveFontAnchor);
|
|
58
|
+
const [visibleTurn, setVisibleTurn] = useState<{ key: string; number: number } | null>(null);
|
|
59
|
+
const [away, setAway] = useState(false);
|
|
60
|
+
const { sessionUrl = "", viewKey } = useContext(TranscriptContext);
|
|
61
|
+
const foldNamespace = viewKey ?? sessionUrl;
|
|
62
|
+
const [folds, setFolds] = useState(() => new Map(savedTurnFolds));
|
|
63
|
+
const atLatest = !history?.newer;
|
|
64
|
+
const turns = useMemo(() => groupTurns(items, running || waiting, atLatest), [items, running, waiting, atLatest]);
|
|
65
|
+
const previousTurns = useRef<{ namespace: string; turns: TranscriptTurn[] }>({ namespace: foldNamespace, turns: [] });
|
|
66
|
+
const rebases = useMemo(() => turnRebases(previousTurns.current.namespace === foldNamespace ? previousTurns.current.turns : [], turns), [turns, foldNamespace]);
|
|
67
|
+
const effectiveFolds = useMemo(() => rebaseTurnFolds(folds, foldNamespace, rebases), [folds, rebases, foldNamespace]);
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
previousTurns.current = { namespace: foldNamespace, turns };
|
|
70
|
+
// Commit each operation once against the saved cache, then render that exact
|
|
71
|
+
// snapshot. Replaying changed keys separately changes eviction order at cap.
|
|
72
|
+
savedTurnFolds = rebaseTurnFolds(savedTurnFolds, foldNamespace, rebases);
|
|
73
|
+
if (savedTurnFolds !== folds) setFolds(savedTurnFolds);
|
|
74
|
+
}, [turns, foldNamespace, effectiveFolds, folds]);
|
|
75
|
+
const loadHistory = (direction: "latest" | "older" | "newer") => {
|
|
76
|
+
const el = ref.current;
|
|
77
|
+
if (direction === "latest") { stick.current = true; scrollAnchor.current = null; }
|
|
78
|
+
else if (el) {
|
|
79
|
+
stick.current = false;
|
|
80
|
+
const top = el.getBoundingClientRect().top;
|
|
81
|
+
const visible = [...el.querySelectorAll<HTMLElement>("[data-row-key]")].find((row) => row.getBoundingClientRect().bottom > top && !row.dataset.rowKey?.startsWith("history-"));
|
|
82
|
+
if (visible) scrollAnchor.current = { key: visible.dataset.rowKey!, offset: visible.getBoundingClientRect().top - top };
|
|
83
|
+
}
|
|
84
|
+
void history?.load(direction);
|
|
85
|
+
};
|
|
86
|
+
const rows = useMemo(() => {
|
|
87
|
+
const out: Row[] = [];
|
|
88
|
+
// Items from one SDK message share its timestamp: label the run once.
|
|
89
|
+
let previousItem: Item | undefined;
|
|
90
|
+
if (history) out.push({ key: "history-controls", node: <div className="flex flex-wrap items-center justify-center gap-2 text-xs text-muted">
|
|
91
|
+
{history.older && <Button size="sm" disabled={history.loading} onClick={() => loadHistory("older")}>加载更早消息</Button>}
|
|
92
|
+
{history.historical && <><span>正在查看历史</span><Button size="sm" disabled={history.loading} onClick={() => loadHistory("latest")}>返回最新消息</Button></>}
|
|
93
|
+
{history.loading && <span>加载中…</span>}
|
|
94
|
+
{history.error && <><span className="text-danger">{history.error}</span><Button size="sm" onClick={() => loadHistory("latest")}>重新加载</Button></>}
|
|
95
|
+
</div> });
|
|
96
|
+
turns.forEach((turn, index) => {
|
|
97
|
+
const key = `${foldNamespace}:${turn.key}`;
|
|
98
|
+
// Follow turn lifecycle until the user explicitly chooses an open/closed state.
|
|
99
|
+
const folded = effectiveFolds.get(key)?.collapsed;
|
|
100
|
+
const collapsed = folded ?? defaultTurnCollapsed(turn);
|
|
101
|
+
const visibleItems = visibleTurnItems(turn, folded);
|
|
102
|
+
// The floating action collapses any visible body, including a final reply
|
|
103
|
+
// whose process is folded by default. The header still toggles the process.
|
|
104
|
+
// An explicit fold hides the whole turn, so there is no body to collapse again.
|
|
105
|
+
const owner = { key: turn.key, number: index + 1, expanded: visibleItems.length > 0 };
|
|
106
|
+
const add = (row: Row) => out.push({ ...row, turn: owner });
|
|
107
|
+
const toggle = (button: HTMLButtonElement) => {
|
|
108
|
+
stick.current = false;
|
|
109
|
+
scrollAnchor.current = { key: `turn-${turn.key}`, offset: Math.max(0, button.getBoundingClientRect().top - (ref.current?.getBoundingClientRect().top ?? 0)), focus: true };
|
|
110
|
+
const choice = { collapsed: !collapsed, revision: ++foldRevision };
|
|
111
|
+
savedTurnFolds = setTurnFold(savedTurnFolds, foldNamespace, key, choice);
|
|
112
|
+
setFolds(savedTurnFolds);
|
|
113
|
+
};
|
|
114
|
+
add({ key: `turn-${turn.key}`, node: <TurnHeader turn={turn} number={index + 1} collapsed={collapsed} waiting={waiting && turn.active && !turn.queued} onToggle={toggle} /> });
|
|
115
|
+
visibleItems.forEach((item, itemIndex) => {
|
|
116
|
+
const showTime = isNewMessageTime(item.ts, previousItem?.ts);
|
|
117
|
+
previousItem = item;
|
|
118
|
+
add({ key: item.key, item, showTime, before: !collapsed && itemIndex ? turnSlots.before(item.key) : null, nested: true });
|
|
119
|
+
});
|
|
120
|
+
if (collapsed) return;
|
|
121
|
+
// Change bars preceding the next prompt belong inside the preceding turn.
|
|
122
|
+
const next = turns[index + 1]?.items[0];
|
|
123
|
+
const changes = next ? turnSlots.before(next.key) : turnSlots.tail;
|
|
124
|
+
if (changes) add({ key: `changes-${turn.key}`, node: changes, nested: true });
|
|
125
|
+
if (turn.active && !turn.queued && running) add({ key: `running-${turn.key}`, node: <RunningIndicator />, nested: true });
|
|
126
|
+
});
|
|
127
|
+
if (!turns.length && running) out.push({ key: "running", node: <RunningIndicator /> });
|
|
128
|
+
if (history?.historical) out.push({ key: "history-newer", node: <div className="flex justify-center gap-2">
|
|
129
|
+
{history.newer && <Button size="sm" disabled={history.loading} onClick={() => loadHistory("newer")}>加载后续消息</Button>}
|
|
130
|
+
<Button size="sm" disabled={history.loading} onClick={() => loadHistory("latest")}>返回最新消息</Button>
|
|
131
|
+
</div> });
|
|
132
|
+
return out;
|
|
133
|
+
}, [turns, effectiveFolds, foldNamespace, turnSlots, running, waiting, history]);
|
|
134
|
+
const virtual = rows.length > VIRTUALIZE_FROM;
|
|
135
|
+
|
|
136
|
+
// Read the turn near the top of the reading viewport, including a single
|
|
137
|
+
// very tall row. Only mounted rows are inspected when the list is virtualized.
|
|
138
|
+
const scheduleVisibleTurn = useCallback(() => {
|
|
139
|
+
if (visibleFrame.current !== null) return;
|
|
140
|
+
visibleFrame.current = requestAnimationFrame(() => {
|
|
141
|
+
visibleFrame.current = null;
|
|
142
|
+
const el = ref.current;
|
|
143
|
+
if (!el) return;
|
|
144
|
+
const viewport = el.getBoundingClientRect();
|
|
145
|
+
const probe = viewport.top + Math.min(80, viewport.height / 4);
|
|
146
|
+
let next: { key: string; number: number } | null = null;
|
|
147
|
+
for (const row of el.querySelectorAll<HTMLElement>("[data-turn-key]")) {
|
|
148
|
+
const rect = row.getBoundingClientRect();
|
|
149
|
+
if (rect.bottom <= probe || rect.top >= viewport.bottom) continue;
|
|
150
|
+
if (row.dataset.turnExpanded === "true") next = { key: row.dataset.turnKey!, number: Number(row.dataset.turnNumber) };
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
setVisibleTurn((current) => current?.key === next?.key && current?.number === next?.number ? current : next);
|
|
154
|
+
});
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
useLayoutEffect(scheduleVisibleTurn, [rows, fontSize, scheduleVisibleTurn]);
|
|
158
|
+
useEffect(() => () => {
|
|
159
|
+
if (visibleFrame.current !== null) cancelAnimationFrame(visibleFrame.current);
|
|
160
|
+
visibleFrame.current = null;
|
|
161
|
+
}, []);
|
|
162
|
+
|
|
163
|
+
// Collapsing removes the content around the reader. Keep the folded
|
|
164
|
+
// header in view rather than leaving the reader somewhere in the next turn.
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
if (virtual || history?.loading || !scrollAnchor.current || !ref.current) return;
|
|
167
|
+
const el = ref.current;
|
|
168
|
+
const anchor = scrollAnchor.current;
|
|
169
|
+
const row = el.querySelector<HTMLElement>(`[data-row-key="${CSS.escape(rebaseTurnRow(anchor.key, rebases))}"]`);
|
|
170
|
+
if (!row) { scrollAnchor.current = null; return; }
|
|
171
|
+
scrollAnchor.current = null;
|
|
172
|
+
el.scrollTop += row.getBoundingClientRect().top - el.getBoundingClientRect().top - anchor.offset;
|
|
173
|
+
if (anchor.focus) row.querySelector<HTMLButtonElement>("button")?.focus({ preventScroll: true });
|
|
174
|
+
}, [rows, virtual, history?.loading, rebases, fontSize]);
|
|
175
|
+
|
|
176
|
+
useLayoutEffect(() => {
|
|
177
|
+
const el = ref.current;
|
|
178
|
+
if (el && stick.current) el.scrollTop = el.scrollHeight;
|
|
179
|
+
}, [items, running, fontSize]);
|
|
180
|
+
|
|
181
|
+
useLayoutEffect(() => {
|
|
182
|
+
const el = ref.current;
|
|
183
|
+
if (!el) return;
|
|
184
|
+
const observer = new ResizeObserver(() => {
|
|
185
|
+
if (stick.current) el.scrollTop = el.scrollHeight;
|
|
186
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLACK;
|
|
187
|
+
stick.current = atBottom;
|
|
188
|
+
setAway(!atBottom);
|
|
189
|
+
scheduleVisibleTurn();
|
|
190
|
+
});
|
|
191
|
+
observer.observe(el);
|
|
192
|
+
if (el.firstElementChild) observer.observe(el.firstElementChild);
|
|
193
|
+
return () => observer.disconnect();
|
|
194
|
+
}, [virtual, scheduleVisibleTurn]);
|
|
195
|
+
|
|
196
|
+
const scrollToBottom = () => {
|
|
197
|
+
const el = ref.current;
|
|
198
|
+
if (!el) return;
|
|
199
|
+
stick.current = true;
|
|
200
|
+
setAway(false);
|
|
201
|
+
el.scrollTop = el.scrollHeight;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const collapseVisibleTurn = () => {
|
|
205
|
+
if (!visibleTurn) return;
|
|
206
|
+
const turn = turns.find((turn) => turn.key === visibleTurn.key);
|
|
207
|
+
const key = `${foldNamespace}:${visibleTurn.key}`;
|
|
208
|
+
const folded = effectiveFolds.get(key)?.collapsed;
|
|
209
|
+
if (!turn || !visibleTurnItems(turn, folded).length) return;
|
|
210
|
+
stick.current = false;
|
|
211
|
+
scrollAnchor.current = { key: `turn-${turn.key}`, offset: 12, focus: true };
|
|
212
|
+
const choice = { collapsed: true, revision: ++foldRevision };
|
|
213
|
+
savedTurnFolds = setTurnFold(savedTurnFolds, foldNamespace, key, choice);
|
|
214
|
+
setFolds(savedTurnFolds);
|
|
215
|
+
setVisibleTurn(null);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div className="relative flex min-h-0 flex-1 flex-col">
|
|
220
|
+
<div
|
|
221
|
+
ref={ref}
|
|
222
|
+
className={`message-transcript min-h-0 flex-1 overflow-y-auto overflow-x-hidden ${virtual ? "[overflow-anchor:none]" : ""}`}
|
|
223
|
+
onScroll={(e) => {
|
|
224
|
+
scheduleVisibleTurn();
|
|
225
|
+
const el = e.currentTarget;
|
|
226
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLACK;
|
|
227
|
+
if (!history?.loading) {
|
|
228
|
+
const top = el.getBoundingClientRect().top;
|
|
229
|
+
const row = [...el.querySelectorAll<HTMLElement>("[data-row-key]")].find((r) => r.getBoundingClientRect().bottom > top);
|
|
230
|
+
scrollAnchor.current = !atBottom && row ? { key: row.dataset.rowKey!, offset: row.getBoundingClientRect().top - top } : null;
|
|
231
|
+
}
|
|
232
|
+
if (atBottom !== stick.current) {
|
|
233
|
+
stick.current = atBottom;
|
|
234
|
+
setAway(!atBottom);
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
data-testid="message-list"
|
|
238
|
+
data-virtual={virtual ? "true" : undefined}
|
|
239
|
+
data-message-font-size={fontSize}
|
|
240
|
+
>
|
|
241
|
+
{virtual ? (
|
|
242
|
+
<VirtualRows rows={rows} scrollRef={ref} stick={stick} scrollAnchor={scrollAnchor} anchoringPaused={history?.loading} rebases={rebases} fontSize={fontSize} />
|
|
243
|
+
) : (
|
|
244
|
+
<div className="mx-auto max-w-4xl space-y-4 pb-20 pl-12 pr-4 pt-6 md:pl-14 md:pr-6">
|
|
245
|
+
{loading && <p className="text-sm text-muted">加载中…</p>}
|
|
246
|
+
{!loading && items.length === 0 && !running && <EmptyConversation />}
|
|
247
|
+
{rows.map((row) => <div key={row.key} data-row-key={row.key} data-turn-key={row.turn?.key} data-turn-number={row.turn?.number} data-turn-expanded={row.turn?.expanded} className={`relative ${row.nested ? "ml-2 border-l border-border/70 pl-4" : ""}`}>
|
|
248
|
+
{row.item && row.showTime && <MessageTime item={row.item} />}
|
|
249
|
+
{row.item ? <ItemRow item={row.item} before={row.before} /> : row.node}
|
|
250
|
+
</div>)}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
{(visibleTurn || away) && <div className="pointer-events-none absolute inset-x-3 bottom-3 z-10 flex flex-wrap justify-center gap-2">
|
|
255
|
+
{visibleTurn && <Button icon={<ChevronDownIcon className="rotate-180" />} className="pointer-events-auto gap-1.5! rounded-full! bg-bg/95 font-normal! shadow-lg backdrop-blur-sm" aria-label={`收起第 ${visibleTurn.number} 轮`} data-testid="turn-collapse" onClick={collapseVisibleTurn}>
|
|
256
|
+
<span className="inline-flex items-baseline gap-2"><span>收起</span><span className="text-xs text-muted">第 {visibleTurn.number} 轮</span></span>
|
|
257
|
+
</Button>}
|
|
258
|
+
{away && <Button icon={<ChevronDownIcon />} className="pointer-events-auto rounded-full! bg-bg/95 shadow-lg backdrop-blur-sm" aria-label="回到底部" onClick={scrollToBottom}>回到底部</Button>}
|
|
259
|
+
</div>}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function VirtualRows({
|
|
265
|
+
rows,
|
|
266
|
+
scrollRef,
|
|
267
|
+
stick,
|
|
268
|
+
scrollAnchor,
|
|
269
|
+
anchoringPaused,
|
|
270
|
+
rebases,
|
|
271
|
+
fontSize,
|
|
272
|
+
}: {
|
|
273
|
+
rows: Row[];
|
|
274
|
+
scrollRef: RefObject<HTMLDivElement | null>;
|
|
275
|
+
stick: RefObject<boolean>;
|
|
276
|
+
scrollAnchor: RefObject<ScrollAnchor | null>;
|
|
277
|
+
anchoringPaused?: boolean;
|
|
278
|
+
rebases: ReadonlyMap<string, string>;
|
|
279
|
+
fontSize: MessageFontSize;
|
|
280
|
+
}) {
|
|
281
|
+
const fontScale = MESSAGE_FONT_OPTIONS.find((option) => option.value === fontSize)!.bodyRem;
|
|
282
|
+
const v = useVirtualizer({
|
|
283
|
+
count: rows.length,
|
|
284
|
+
getScrollElement: () => scrollRef.current,
|
|
285
|
+
estimateSize: (i) => estimateRow(rows[i]!) * fontScale,
|
|
286
|
+
getItemKey: (i) => rows[i]!.key,
|
|
287
|
+
overscan: 6,
|
|
288
|
+
paddingStart: 24,
|
|
289
|
+
paddingEnd: 80,
|
|
290
|
+
gap: 16,
|
|
291
|
+
// Opened at the bottom: start with the last rows instead of measuring the first ones.
|
|
292
|
+
initialOffset: () => (stick.current ? Number.MAX_SAFE_INTEGER : 0),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
useLayoutEffect(() => {
|
|
296
|
+
// Cached heights belong to the previous font. Read mounted rows immediately;
|
|
297
|
+
// offscreen rows get the new estimates until they are measured again.
|
|
298
|
+
v.measure();
|
|
299
|
+
scrollRef.current?.querySelectorAll<HTMLElement>("[data-index]").forEach((row) => v.measureElement(row));
|
|
300
|
+
// Track the changing maximum while the virtualizer replaces its estimates;
|
|
301
|
+
// one direct scrollTop write would target the previous total height.
|
|
302
|
+
if (stick.current) v.scrollToEnd();
|
|
303
|
+
}, [fontSize, v, scrollRef, stick]);
|
|
304
|
+
|
|
305
|
+
useLayoutEffect(() => {
|
|
306
|
+
if (anchoringPaused || !scrollAnchor.current) return;
|
|
307
|
+
const anchor = scrollAnchor.current;
|
|
308
|
+
const key = rebaseTurnRow(anchor.key, rebases);
|
|
309
|
+
const index = rows.findIndex((row) => row.key === key);
|
|
310
|
+
if (index < 0) { scrollAnchor.current = null; return; }
|
|
311
|
+
const offset = v.getOffsetForIndex(index, "start");
|
|
312
|
+
if (!offset) return;
|
|
313
|
+
scrollAnchor.current = { ...anchor, key };
|
|
314
|
+
v.scrollToOffset(Math.max(0, offset[0] - anchor.offset));
|
|
315
|
+
const frame = requestAnimationFrame(() => {
|
|
316
|
+
const el = scrollRef.current;
|
|
317
|
+
const row = el?.querySelector<HTMLElement>(`[data-row-key="${CSS.escape(key)}"]`);
|
|
318
|
+
if (!el || !row) return;
|
|
319
|
+
scrollAnchor.current = null;
|
|
320
|
+
el.scrollTop += row.getBoundingClientRect().top - el.getBoundingClientRect().top - anchor.offset;
|
|
321
|
+
if (anchor.focus) row.querySelector<HTMLButtonElement>("button")?.focus({ preventScroll: true });
|
|
322
|
+
});
|
|
323
|
+
return () => cancelAnimationFrame(frame);
|
|
324
|
+
}, [rows, v, scrollAnchor, scrollRef, anchoringPaused, rebases, fontSize]);
|
|
325
|
+
|
|
326
|
+
// Heights settle after rows are measured (and grow while text streams): stay at the bottom.
|
|
327
|
+
const inner = useRef<HTMLDivElement>(null);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const box = inner.current;
|
|
330
|
+
const el = scrollRef.current;
|
|
331
|
+
if (!box || !el) return;
|
|
332
|
+
const ro = new ResizeObserver(() => {
|
|
333
|
+
if (stick.current) el.scrollTop = el.scrollHeight;
|
|
334
|
+
});
|
|
335
|
+
ro.observe(box);
|
|
336
|
+
return () => ro.disconnect();
|
|
337
|
+
}, [scrollRef, stick]);
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div className="mx-auto max-w-4xl pl-12 pr-4 md:pl-14 md:pr-6">
|
|
341
|
+
<div ref={inner} className="relative w-full" style={{ height: v.getTotalSize() }}>
|
|
342
|
+
{v.getVirtualItems().map((vi) => {
|
|
343
|
+
const row = rows[vi.index]!;
|
|
344
|
+
return (
|
|
345
|
+
<div key={vi.key} data-row-key={row.key} data-turn-key={row.turn?.key} data-turn-number={row.turn?.number} data-turn-expanded={row.turn?.expanded} data-index={vi.index} ref={v.measureElement} className={`absolute top-0 ${row.nested ? "left-2 w-[calc(100%-0.5rem)] border-l border-border/70 pl-4" : "left-0 w-full"}`} style={{ transform: `translateY(${vi.start}px)` }}>
|
|
346
|
+
{row.item && row.showTime && <MessageTime item={row.item} />}
|
|
347
|
+
{row.item ? <ItemRow item={row.item} before={row.before} /> : row.node}
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
})}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function TurnHeader({ turn, number, collapsed, waiting, onToggle }: { turn: TranscriptTurn; number: number; collapsed: boolean; waiting: boolean; onToggle: (button: HTMLButtonElement) => void }) {
|
|
357
|
+
const tools = turn.items.filter((item) => item.kind === "tool").length;
|
|
358
|
+
const result = turn.result;
|
|
359
|
+
const status = turn.error ? result?.subtype === "error_during_execution" ? "已中断" : "失败" : turn.active ? turn.queued ? "排队中" : waiting ? "等待回答" : "执行中" : "已结束";
|
|
360
|
+
return (
|
|
361
|
+
<button type="button" className="flex w-full min-w-0 items-center gap-2 rounded-lg border border-border bg-surface/70 px-3 py-2 text-left text-sm hover:bg-surface-2" aria-expanded={!collapsed} aria-label={`${collapsed ? "展开" : "折叠"}第 ${number} 轮`} onClick={(event) => onToggle(event.currentTarget)} data-testid="turn-header">
|
|
362
|
+
<ChevronIcon open={!collapsed} />
|
|
363
|
+
<span className="shrink-0 font-medium">第 {number} 轮</span>
|
|
364
|
+
<span className="min-w-0 flex-1 truncate text-muted" title={turn.summary}>{turn.summary}</span>
|
|
365
|
+
{tools > 0 && <span className="hidden shrink-0 text-xs text-muted sm:inline">{tools} 次工具调用</span>}
|
|
366
|
+
{collapsed && result?.durationMs !== undefined && <span className="hidden shrink-0 text-xs text-muted sm:inline">{(result.durationMs / 1000).toFixed(1)}s</span>}
|
|
367
|
+
<span className={`shrink-0 text-xs ${turn.error ? "text-danger" : turn.active ? "text-accent" : "text-muted"}`}>{status}</span>
|
|
368
|
+
</button>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const MessageTime = memo(function MessageTime({ item }: { item: Item }) {
|
|
373
|
+
const time = formatMessageTime(item.ts);
|
|
374
|
+
if (!time) return null;
|
|
375
|
+
return (
|
|
376
|
+
<time
|
|
377
|
+
className="absolute top-0 flex w-max select-none flex-col items-end gap-0.5 text-right text-[10px] leading-none tabular-nums text-muted/80"
|
|
378
|
+
style={{ right: "calc(100% + 0.5rem)" }}
|
|
379
|
+
dateTime={time.dateTime}
|
|
380
|
+
title={time.title}
|
|
381
|
+
data-testid="message-time"
|
|
382
|
+
>
|
|
383
|
+
{time.lines.map((line, index) => <span key={index} className="whitespace-nowrap">{line}</span>)}
|
|
384
|
+
</time>
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const ItemRow = memo(function ItemRow({ item, before }: { item: Item; before?: ReactNode }) {
|
|
389
|
+
if (!before) return <ItemView item={item} />;
|
|
390
|
+
return (
|
|
391
|
+
<div className="space-y-4">
|
|
392
|
+
{before}
|
|
393
|
+
<ItemView item={item} />
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
/** A rough height before a row is measured; closer guesses mean less movement while scrolling. */
|
|
399
|
+
function estimateRow(row: Row): number {
|
|
400
|
+
const it = row.item;
|
|
401
|
+
const extra = row.before ? 56 : 0;
|
|
402
|
+
if (!it) return 40;
|
|
403
|
+
const lines = (text: string, perLine: number) => text.split("\n").reduce((n, l) => n + Math.max(1, Math.ceil(l.length / perLine)), 0);
|
|
404
|
+
switch (it.kind) {
|
|
405
|
+
case "user":
|
|
406
|
+
return extra + 48 + 24 * lines(it.text, 60);
|
|
407
|
+
case "text":
|
|
408
|
+
return extra + 32 + 26 * lines(it.text, 80);
|
|
409
|
+
case "thinking":
|
|
410
|
+
return extra + 40;
|
|
411
|
+
case "tool": {
|
|
412
|
+
if (it.name === "Agent" || it.name === "Task") return extra + 40;
|
|
413
|
+
if (it.name.startsWith("Task") || it.name === "TodoWrite") return extra + 20;
|
|
414
|
+
return extra + 40;
|
|
415
|
+
}
|
|
416
|
+
case "result":
|
|
417
|
+
return extra + 28;
|
|
418
|
+
case "bash":
|
|
419
|
+
return extra + 80;
|
|
420
|
+
case "local":
|
|
421
|
+
return extra + 16 + 18 * lines(it.text, 100);
|
|
422
|
+
case "notice":
|
|
423
|
+
return extra + 20;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function RunningIndicator() {
|
|
428
|
+
return (
|
|
429
|
+
<div className="flex items-center gap-2 text-sm text-muted" data-testid="running-indicator">
|
|
430
|
+
<span className="inline-block size-2 animate-pulse rounded-full bg-accent" />
|
|
431
|
+
Claude 正在工作…
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const TIPS: [string, string][] = [
|
|
437
|
+
["/", "斜杠命令和技能,例如 /init、/review、/goal"],
|
|
438
|
+
["@", "引用项目里的文件"],
|
|
439
|
+
["!", "直接在项目目录执行一条命令,输出带进对话"],
|
|
440
|
+
["图片", "粘贴、拖进来,或点输入框左下角的按钮"],
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
function EmptyConversation() {
|
|
444
|
+
return (
|
|
445
|
+
<div className="mx-auto max-w-md py-[10vh] text-center" data-testid="empty-conversation">
|
|
446
|
+
<h2 className="text-2xl">有什么要做的?</h2>
|
|
447
|
+
<p className="mt-2 text-sm text-muted">描述任务,Claude 会在这个项目里读代码、改文件、跑命令。关掉页面也会继续运行。</p>
|
|
448
|
+
<ul className="mt-6 space-y-2 text-left text-sm">
|
|
449
|
+
{TIPS.map(([k, v]) => (
|
|
450
|
+
<li key={k} className="flex items-baseline gap-3 rounded-lg border border-border bg-surface px-3 py-2">
|
|
451
|
+
<kbd className="min-w-8 shrink-0 rounded border border-border bg-surface-2 px-1.5 text-center font-mono text-xs">{k}</kbd>
|
|
452
|
+
<span className="text-muted">{v}</span>
|
|
453
|
+
</li>
|
|
454
|
+
))}
|
|
455
|
+
</ul>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import { ActionMenu, MenuItem as UiMenuItem } from "../ui/Controls.tsx";
|
|
3
|
+
import { Button } from "../ui/Button.tsx";
|
|
4
|
+
import { XIcon } from "../ui/icons.tsx";
|
|
5
|
+
|
|
6
|
+
export function Modal({ title, onClose, children, testId, scrollBody = true, panelClassName = "" }: { title: string; onClose: () => void; children: ReactNode; testId?: string; scrollBody?: boolean; panelClassName?: string }) {
|
|
7
|
+
const root = useRef<HTMLDivElement>(null);
|
|
8
|
+
const returnFocus = useRef(document.activeElement);
|
|
9
|
+
const close = useRef(onClose);
|
|
10
|
+
useEffect(() => { close.current = onClose; }, [onClose]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const dialog = root.current!;
|
|
13
|
+
const previous = returnFocus.current;
|
|
14
|
+
if (!dialog.contains(document.activeElement)) dialog.focus({ preventScroll: true });
|
|
15
|
+
const onKey = (e: KeyboardEvent) => {
|
|
16
|
+
if (e.defaultPrevented) return;
|
|
17
|
+
const dialogs = document.querySelectorAll('[role="dialog"][aria-modal="true"]');
|
|
18
|
+
if (dialogs[dialogs.length - 1] !== dialog) return;
|
|
19
|
+
if (e.key === "Escape") {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
close.current();
|
|
22
|
+
} else if (e.key === "Tab") {
|
|
23
|
+
// A portalled picker has its own dialog layer. Let it handle navigation
|
|
24
|
+
// while open instead of moving focus back behind that picker.
|
|
25
|
+
const layers = document.querySelectorAll('[role="dialog"]');
|
|
26
|
+
const top = layers[layers.length - 1];
|
|
27
|
+
if (top !== dialog && !dialog.contains(top)) return;
|
|
28
|
+
const controls = [...dialog.querySelectorAll<HTMLElement>('button:not([disabled]), a[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')]
|
|
29
|
+
.filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest("[inert]")
|
|
30
|
+
&& element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden");
|
|
31
|
+
const first = controls[0], last = controls.at(-1), active = document.activeElement;
|
|
32
|
+
if (!first || active === dialog || !dialog.contains(active) || (e.shiftKey ? active === first : active === last)) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
(e.shiftKey ? last : first)?.focus({ preventScroll: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener("keydown", onKey);
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener("keydown", onKey);
|
|
41
|
+
// StrictMode replays effects while the same DOM remains mounted. Only a
|
|
42
|
+
// real unmount should return focus, or replay would erase child autofocus.
|
|
43
|
+
if (!dialog.isConnected && previous instanceof HTMLElement && previous.isConnected
|
|
44
|
+
&& (dialog.contains(document.activeElement) || document.activeElement === document.body)) previous.focus({ preventScroll: true });
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
return (
|
|
48
|
+
<div ref={root} tabIndex={-1} className="safe-overlay fixed inset-0 z-50 flex items-end justify-center outline-none sm:items-center" role="dialog" aria-modal="true" aria-label={title} data-testid={testId}>
|
|
49
|
+
<div className="absolute inset-0 bg-black/30 backdrop-blur-[1px]" onClick={onClose} />
|
|
50
|
+
<div className={`safe-overlay-panel relative flex w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-border bg-bg shadow-2xl sm:rounded-xl ${panelClassName}`}>
|
|
51
|
+
<header className="flex shrink-0 items-center gap-2 border-b border-border py-2.5 pl-5 pr-2.5">
|
|
52
|
+
<h2 className="min-w-0 flex-1 truncate text-lg">{title}</h2>
|
|
53
|
+
<Button variant="ghost" size="sm" iconOnly onClick={onClose} aria-label="关闭">
|
|
54
|
+
<XIcon />
|
|
55
|
+
</Button>
|
|
56
|
+
</header>
|
|
57
|
+
<div className={`min-h-0 px-4 py-4 sm:px-5 ${scrollBody ? "overflow-y-auto overscroll-contain" : "flex flex-1 flex-col overflow-hidden"}`}>{children}</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** "⋯" menu; shared MenuItem owns closing before running an action. */
|
|
64
|
+
export function Menu({ label, children, testId }: { label: string; children: ReactNode; testId?: string }) {
|
|
65
|
+
return (
|
|
66
|
+
<ActionMenu label={label} testId={testId}>
|
|
67
|
+
{() => children}
|
|
68
|
+
</ActionMenu>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const MenuItem = UiMenuItem;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lazy, Suspense, useState } from "react";
|
|
2
|
+
import { Modal } from "./Modal.tsx";
|
|
3
|
+
import { Button } from "../ui/Button.tsx";
|
|
4
|
+
import { BellIcon } from "../ui/icons.tsx";
|
|
5
|
+
|
|
6
|
+
const NotificationSettings = lazy(() => import("./NotificationSettings.tsx").then((m) => ({ default: m.NotificationSettings })));
|
|
7
|
+
|
|
8
|
+
/** Sidebar footer button that opens the notification settings. */
|
|
9
|
+
export function NotificationEntry() {
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<button
|
|
14
|
+
onClick={() => setOpen(true)}
|
|
15
|
+
className="flex h-10 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm text-muted transition-colors hover:bg-surface hover:text-text md:h-8"
|
|
16
|
+
>
|
|
17
|
+
<BellIcon size={16} /> 通知
|
|
18
|
+
</button>
|
|
19
|
+
{open && (
|
|
20
|
+
<Modal title="通知" onClose={() => setOpen(false)}>
|
|
21
|
+
<Suspense fallback={<p className="py-6 text-center text-sm text-muted">加载中…</p>}>
|
|
22
|
+
<NotificationSettings />
|
|
23
|
+
</Suspense>
|
|
24
|
+
<div className="mt-5 flex justify-end">
|
|
25
|
+
<Button onClick={() => setOpen(false)}>完成</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</Modal>
|
|
28
|
+
)}
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|