@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,320 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { useLocation, useMatch, useNavigate } from "react-router";
|
|
3
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { SessionListItem } from "@shared/protocol.ts";
|
|
5
|
+
import { api } from "../api.ts";
|
|
6
|
+
import { relativeTime, useOverview, useProjects, useSessions } from "../hooks.ts";
|
|
7
|
+
import { Link } from "react-router";
|
|
8
|
+
import { useLive } from "../live.ts";
|
|
9
|
+
import { CloseIcon, MenuIcon, PlusIcon } from "./icons.tsx";
|
|
10
|
+
import { Button } from "../ui/Button.tsx";
|
|
11
|
+
import { BranchIcon, FolderIcon, PinIcon, PlusIcon as UiPlusIcon, SearchIcon } from "../ui/icons.tsx";
|
|
12
|
+
import { Select } from "../ui/Select.tsx";
|
|
13
|
+
import { ResizablePane } from "../ui/ResizablePane.tsx";
|
|
14
|
+
import { ProjectTabs } from "./ProjectNav.tsx";
|
|
15
|
+
import { OpenTabs, OpenTabsTracker } from "./OpenTabs.tsx";
|
|
16
|
+
import { openTabs, useSidebarView } from "../open-tabs.ts";
|
|
17
|
+
import { Segmented } from "../ui/Form.tsx";
|
|
18
|
+
import { SettingsLink } from "./SettingsLink.tsx";
|
|
19
|
+
import { NotificationEntry } from "./NotificationEntry.tsx";
|
|
20
|
+
import { NotificationToasts } from "./NotificationToasts.tsx";
|
|
21
|
+
import { DirectoryPicker } from "./DirectoryPicker.tsx";
|
|
22
|
+
import { ImagePreview } from "./ImagePreview.tsx";
|
|
23
|
+
import { useRemoveProject } from "../project-actions.ts";
|
|
24
|
+
import { CopyButton } from "./CopyButton.tsx";
|
|
25
|
+
|
|
26
|
+
const DrawerContext = createContext<() => void>(() => {});
|
|
27
|
+
|
|
28
|
+
/** Opens the navigation drawer on phones. */
|
|
29
|
+
export function useOpenDrawer() {
|
|
30
|
+
return useContext(DrawerContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Shell({ children }: { children: ReactNode }) {
|
|
34
|
+
const [drawer, setDrawer] = useState(false);
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
useEffect(() => setDrawer(false), [location.pathname, location.search, location.hash]);
|
|
37
|
+
// Session pages carry their own header with the menu button.
|
|
38
|
+
const inSession = !!useMatch("/p/:pid/s/:sid");
|
|
39
|
+
const projectHome = useMatch("/p/:pid"), sessionPage = useMatch("/p/:pid/s/*");
|
|
40
|
+
const navigationPid = projectHome?.params.pid ?? sessionPage?.params.pid;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="app-height flex overflow-hidden">
|
|
44
|
+
<OpenTabsTracker />
|
|
45
|
+
{/* Desktop sidebar */}
|
|
46
|
+
<ResizablePane storageKey="claude-ide:sidebar-width" label="调整导航栏宽度" defaultWidth={288} minWidth={240} maxWidth={520} remainingWidth={480} className="hidden border-r border-border bg-surface-2/50 md:flex md:flex-col">
|
|
47
|
+
<Sidebar />
|
|
48
|
+
</ResizablePane>
|
|
49
|
+
{/* Mobile drawer */}
|
|
50
|
+
{drawer && (
|
|
51
|
+
<div className="fixed inset-0 z-40 md:hidden" role="dialog" aria-label="导航">
|
|
52
|
+
<div className="absolute inset-0 bg-black/30" onClick={() => setDrawer(false)} />
|
|
53
|
+
<aside className="absolute inset-y-0 left-0 flex w-[85%] max-w-80 flex-col bg-bg shadow-xl" onClick={(event) => {
|
|
54
|
+
// Same-document links still dismiss the drawer; internal history
|
|
55
|
+
// state updates must not close a drawer the user just reopened.
|
|
56
|
+
if (event.target instanceof Element && event.target.closest("a[href],button[aria-current='page']")) setDrawer(false);
|
|
57
|
+
}}>
|
|
58
|
+
<div className="flex justify-end p-2">
|
|
59
|
+
<button className="rounded-md p-2.5 text-muted hover:bg-surface-2" onClick={() => setDrawer(false)} aria-label="关闭导航">
|
|
60
|
+
<CloseIcon />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<Sidebar onNavigate={() => setDrawer(false)} />
|
|
64
|
+
</aside>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
68
|
+
{!inSession && <MobileBar onMenu={() => setDrawer(true)} />}
|
|
69
|
+
{navigationPid && <ProjectTabs projectId={navigationPid} />}
|
|
70
|
+
<DrawerContext.Provider value={() => setDrawer(true)}>{children}</DrawerContext.Provider>
|
|
71
|
+
</div>
|
|
72
|
+
<NotificationToasts />
|
|
73
|
+
<ImagePreview />
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function MobileBar({ onMenu }: { onMenu: () => void }) {
|
|
79
|
+
const connected = useLive((s) => s.connected);
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center gap-2 border-b border-border px-2 py-1.5 md:hidden">
|
|
82
|
+
<button className="rounded-md p-2.5 hover:bg-surface-2" onClick={onMenu} aria-label="打开导航">
|
|
83
|
+
<MenuIcon />
|
|
84
|
+
</button>
|
|
85
|
+
<span className="font-serif text-lg">Claude IDE</span>
|
|
86
|
+
{!connected && <span className="ml-auto text-xs text-muted">连接中…</span>}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
|
92
|
+
// Shell renders outside <Routes>, so useParams() is empty here; match the location instead.
|
|
93
|
+
const location = useLocation();
|
|
94
|
+
// Settings pages carry the project in ?p= so the sidebar keeps showing it.
|
|
95
|
+
const pid = useMatch("/p/:pid/*")?.params.pid ?? (location.pathname.startsWith("/settings") ? (new URLSearchParams(location.search).get("p") ?? undefined) : undefined);
|
|
96
|
+
const sid = useMatch("/p/:pid/s/:sid/*")?.params.sid;
|
|
97
|
+
const view = useSidebarView();
|
|
98
|
+
const navigate = useNavigate();
|
|
99
|
+
const projects = useProjects();
|
|
100
|
+
const [adding, setAdding] = useState(false);
|
|
101
|
+
const current = projects.data?.find((p) => p.id === pid);
|
|
102
|
+
const selectedProject = useRef(pid);
|
|
103
|
+
selectedProject.current = pid;
|
|
104
|
+
const remove = useRemoveProject((project) => {
|
|
105
|
+
if (selectedProject.current === project.id) navigate("/overview");
|
|
106
|
+
});
|
|
107
|
+
const overview = useOverview();
|
|
108
|
+
const busy = overview.data?.filter((o) => o.state !== "idle" || o.waitingFor.length).length ?? 0;
|
|
109
|
+
const waiting = overview.data?.filter((o) => o.waitingFor.length || o.state === "requires_action").length ?? 0;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex min-h-0 flex-1 flex-col" data-testid="sidebar">
|
|
113
|
+
<div className="space-y-2 px-3 pt-3">
|
|
114
|
+
<Link to="/overview" className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-surface" data-testid="overview-link">
|
|
115
|
+
<span>运行总览</span>
|
|
116
|
+
{waiting > 0 && <span className="rounded-full bg-amber-500 px-1.5 text-xs text-white">{waiting} 待处理</span>}
|
|
117
|
+
{busy - waiting > 0 && <span className="rounded-full bg-accent/15 px-1.5 text-xs text-accent">{busy - waiting} 运行中</span>}
|
|
118
|
+
</Link>
|
|
119
|
+
<div className="flex gap-1.5">
|
|
120
|
+
<Select
|
|
121
|
+
label="项目"
|
|
122
|
+
value={pid ?? ""}
|
|
123
|
+
placeholder="选择项目…"
|
|
124
|
+
emptyText={
|
|
125
|
+
<>
|
|
126
|
+
<span className="block text-text">暂无项目</span>
|
|
127
|
+
<span className="mt-1 block text-xs">点击右侧 + 添加项目</span>
|
|
128
|
+
</>
|
|
129
|
+
}
|
|
130
|
+
onChange={(v) => v && navigate(`/p/${v}`)}
|
|
131
|
+
options={(projects.data ?? []).map((p) => ({ value: p.id, label: p.name, text: p.name, hint: <span className="font-mono">{p.path}</span> }))}
|
|
132
|
+
icon={<FolderIcon className="shrink-0 text-muted" />}
|
|
133
|
+
className="min-w-0 flex-1"
|
|
134
|
+
testId="project-select"
|
|
135
|
+
/>
|
|
136
|
+
<Button iconOnly onClick={() => setAdding(true)} aria-label="添加项目" title="添加项目" aria-expanded={adding}>
|
|
137
|
+
<UiPlusIcon />
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
{current && (
|
|
141
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
142
|
+
<div className="flex min-w-0 flex-1 items-center gap-1">
|
|
143
|
+
<p className="min-w-0 truncate px-0.5 font-mono text-[11px] text-muted" title={current.path}>{current.path}</p>
|
|
144
|
+
<CopyButton key={current.path} text={current.path} label="复制项目路径" />
|
|
145
|
+
</div>
|
|
146
|
+
<Button variant="ghost" size="sm" aria-label="移除项目" disabled={remove.isPending} onClick={() => remove.mutate(current)}>移除</Button>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
{remove.error && <p role="alert" className="break-words text-xs text-danger">{remove.error.message}</p>}
|
|
150
|
+
{adding && <AddProject onDone={() => setAdding(false)} onCancel={() => setAdding(false)} />}
|
|
151
|
+
</div>
|
|
152
|
+
{pid ? <>
|
|
153
|
+
<div className="shrink-0 px-3 pt-3">
|
|
154
|
+
<Segmented label="侧边栏内容" value={view} onChange={openTabs.setView} size="sm" stretch options={[
|
|
155
|
+
{ value: "sessions", label: "会话列表" }, { value: "tabs", label: "活跃 Tab" },
|
|
156
|
+
]} />
|
|
157
|
+
</div>
|
|
158
|
+
{view === "sessions" ? <SessionList key={pid} projectId={pid} activeId={sid} /> : <OpenTabs key={pid} pid={pid} onNavigate={onNavigate} />}
|
|
159
|
+
</> : <div className="flex-1" />}
|
|
160
|
+
<div className="flex shrink-0 items-center gap-1 border-t border-border px-2 py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
|
|
161
|
+
<SettingsLink />
|
|
162
|
+
<div className="min-w-0 flex-1">
|
|
163
|
+
<NotificationEntry />
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function AddProject({ onDone, onCancel }: { onDone?: () => void; onCancel?: () => void }) {
|
|
171
|
+
const qc = useQueryClient();
|
|
172
|
+
const navigate = useNavigate();
|
|
173
|
+
return (
|
|
174
|
+
<DirectoryPicker
|
|
175
|
+
onClose={() => (onCancel ?? onDone)?.()}
|
|
176
|
+
onSelect={async (path) => {
|
|
177
|
+
const project = await api<{ id: string }>("/api/projects", { method: "POST", json: { path } });
|
|
178
|
+
void qc.invalidateQueries({ queryKey: ["projects"] });
|
|
179
|
+
onDone?.();
|
|
180
|
+
navigate(`/p/${project.id}`);
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const DAY = 86_400_000;
|
|
187
|
+
|
|
188
|
+
/** Group sessions the way people remember them: pinned, then by recency. */
|
|
189
|
+
function groupSessions(items: SessionListItem[]) {
|
|
190
|
+
const startOfToday = new Date().setHours(0, 0, 0, 0);
|
|
191
|
+
const groups: { name: string; items: SessionListItem[] }[] = [
|
|
192
|
+
{ name: "置顶", items: [] },
|
|
193
|
+
{ name: "今天", items: [] },
|
|
194
|
+
{ name: "昨天", items: [] },
|
|
195
|
+
{ name: "最近 7 天", items: [] },
|
|
196
|
+
{ name: "更早", items: [] },
|
|
197
|
+
];
|
|
198
|
+
for (const s of items) {
|
|
199
|
+
const g = s.pinned ? 0 : s.lastModified >= startOfToday ? 1 : s.lastModified >= startOfToday - DAY ? 2 : s.lastModified >= startOfToday - 6 * DAY ? 3 : 4;
|
|
200
|
+
groups[g]!.items.push(s);
|
|
201
|
+
}
|
|
202
|
+
return groups.filter((g) => g.items.length);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function SessionList({ projectId, activeId }: { projectId: string; activeId?: string }) {
|
|
206
|
+
const sessions = useSessions(projectId);
|
|
207
|
+
const navigate = useNavigate();
|
|
208
|
+
const qc = useQueryClient();
|
|
209
|
+
const [q, setQ] = useState("");
|
|
210
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
211
|
+
const create = useMutation({
|
|
212
|
+
mutationFn: () => api<{ id: string }>(`/api/projects/${projectId}/sessions`, { method: "POST" }),
|
|
213
|
+
onSuccess: ({ id }) => {
|
|
214
|
+
qc.invalidateQueries({ queryKey: ["sessions", projectId] });
|
|
215
|
+
navigate(`/p/${projectId}/s/${id}`);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const all = sessions.data ?? [];
|
|
219
|
+
const archivedCount = all.filter((s) => s.archived).length;
|
|
220
|
+
const items = all.filter((s) => s.archived === showArchived && (!q || s.title.toLowerCase().includes(q.toLowerCase())));
|
|
221
|
+
const groups = groupSessions(items);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex min-h-0 flex-1 flex-col pt-3">
|
|
225
|
+
<div className="flex gap-1.5 px-3">
|
|
226
|
+
<div className="relative min-w-0 flex-1">
|
|
227
|
+
<SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted" />
|
|
228
|
+
<input
|
|
229
|
+
aria-label="搜索会话"
|
|
230
|
+
placeholder="搜索会话"
|
|
231
|
+
value={q}
|
|
232
|
+
onChange={(e) => setQ(e.target.value)}
|
|
233
|
+
className="h-10 w-full rounded-md border border-border bg-surface pl-8 pr-2.5 text-base outline-none transition-colors placeholder:text-muted focus:border-accent/60 focus:ring-2 focus:ring-accent/20 md:h-8 md:text-sm"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<Button variant="primary" onClick={() => create.mutate()} loading={create.isPending} icon={<PlusIcon />} title="新会话">
|
|
237
|
+
新会话
|
|
238
|
+
</Button>
|
|
239
|
+
</div>
|
|
240
|
+
<nav className="sidebar-session-list mr-1 mt-2 min-h-0 flex-1 overflow-y-auto px-2 pb-4" aria-label="会话列表">
|
|
241
|
+
{showArchived && <div className="px-2.5 pb-1 pt-2 text-xs font-medium text-muted">已归档</div>}
|
|
242
|
+
{groups.map((g) => (
|
|
243
|
+
<div key={g.name} className="mt-1 first:mt-0">
|
|
244
|
+
{!showArchived && (
|
|
245
|
+
<div className="flex items-center justify-between px-2.5 pb-1 pt-2 text-[11px] font-medium uppercase tracking-wide text-muted">
|
|
246
|
+
<span>{g.name}</span>
|
|
247
|
+
<span className="tabular-nums">{g.items.length}</span>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
{g.items.map((s) => (
|
|
251
|
+
<SessionRow key={s.id} s={s} active={s.id === activeId} onClick={() => navigate(`/p/${projectId}/s/${s.id}`)} />
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
))}
|
|
255
|
+
{sessions.isSuccess && items.length === 0 && (
|
|
256
|
+
<div className="px-2.5 py-6 text-center text-sm text-muted">
|
|
257
|
+
{q ? "没有匹配的会话" : showArchived ? "没有已归档的会话" : "没有会话"}
|
|
258
|
+
{!q && !showArchived && <p className="mt-1 text-xs">点"新会话"开始,或在终端里用 claude 开一个,这里会自动出现。</p>}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
{(archivedCount > 0 || showArchived) && (
|
|
262
|
+
<button
|
|
263
|
+
className="mt-3 flex w-full items-center gap-1.5 rounded-md px-2.5 py-2 text-left text-xs text-muted hover:bg-surface hover:text-text"
|
|
264
|
+
onClick={() => setShowArchived((v) => !v)}
|
|
265
|
+
>
|
|
266
|
+
{showArchived ? "← 返回会话列表" : `已归档(${archivedCount})`}
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
</nav>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const STATE_TEXT: Partial<Record<SessionListItem["state"], string>> = { running: "运行中", requires_action: "等你处理" };
|
|
275
|
+
|
|
276
|
+
function SessionRow({ s, active, onClick }: { s: SessionListItem; active: boolean; onClick: () => void }) {
|
|
277
|
+
const state = STATE_TEXT[s.state];
|
|
278
|
+
return (
|
|
279
|
+
<button
|
|
280
|
+
onClick={onClick}
|
|
281
|
+
aria-current={active ? "page" : undefined}
|
|
282
|
+
className={`group mt-px block w-full rounded-md px-2.5 py-2 text-left transition-colors ${active ? "bg-surface shadow-sm ring-1 ring-border" : "hover:bg-surface/70"}`}
|
|
283
|
+
>
|
|
284
|
+
<div className="flex items-center gap-2">
|
|
285
|
+
<span className={`min-w-0 flex-1 truncate text-sm ${active ? "font-medium" : ""}`}>{s.title || (s.draft ? "新会话" : "未命名会话")}</span>
|
|
286
|
+
{s.pinned && (
|
|
287
|
+
<span className="shrink-0 text-accent" title="已置顶">
|
|
288
|
+
<PinIcon />
|
|
289
|
+
<span className="sr-only">置顶</span>
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
|
|
294
|
+
{state && (
|
|
295
|
+
<span className={`flex shrink-0 items-center gap-1 ${s.state === "requires_action" ? "text-amber-600 dark:text-amber-400" : "text-accent"}`}>
|
|
296
|
+
<StateDot state={s.state} />
|
|
297
|
+
{state}
|
|
298
|
+
</span>
|
|
299
|
+
)}
|
|
300
|
+
<span className="shrink-0">{relativeTime(s.lastModified)}</span>
|
|
301
|
+
{s.forkedFrom && (
|
|
302
|
+
<span className="flex shrink-0 items-center" title="fork 出的会话">
|
|
303
|
+
<BranchIcon size={12} />
|
|
304
|
+
</span>
|
|
305
|
+
)}
|
|
306
|
+
{s.worktree && (
|
|
307
|
+
<span className="min-w-0 truncate rounded bg-surface-2 px-1 font-mono text-[11px]" title={`worktree:${s.cwd}`} data-testid="session-worktree-badge">
|
|
308
|
+
⎇ {s.worktree}
|
|
309
|
+
</span>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</button>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function StateDot({ state }: { state: SessionListItem["state"] }) {
|
|
317
|
+
const cls = state === "running" ? "bg-accent animate-pulse" : state === "requires_action" ? "bg-amber-500" : "bg-transparent";
|
|
318
|
+
const label = state === "running" ? "运行中" : state === "requires_action" ? "等待处理" : "空闲";
|
|
319
|
+
return <span className={`inline-block size-2 shrink-0 rounded-full ${cls}`} title={label} aria-label={label} data-state={state} />;
|
|
320
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** SVGs stay in an image document: scripts/events are never inserted into the
|
|
4
|
+
* application DOM. Blob URLs also work on ordinary HTTP and are released. */
|
|
5
|
+
export function SvgPreview({ markup, alt = "SVG 图像" }: { markup: string; alt?: string }) {
|
|
6
|
+
const [url, setUrl] = useState<string>();
|
|
7
|
+
const [failed, setFailed] = useState(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const svg = markup.replace(/<svg\b([^>]*)>/i, (tag, attrs: string) => /\bxmlns\s*=/.test(attrs) ? tag : tag.replace(/<svg/i, '<svg xmlns="http://www.w3.org/2000/svg"'));
|
|
10
|
+
const next = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
|
|
11
|
+
setUrl(next); setFailed(false);
|
|
12
|
+
return () => URL.revokeObjectURL(next);
|
|
13
|
+
}, [markup]);
|
|
14
|
+
return <span className="inline-block max-w-full align-middle" data-testid="inline-svg-preview">
|
|
15
|
+
{url && !failed && <img src={url} alt={alt} className="max-h-[70dvh] max-w-full object-contain" onError={() => setFailed(true)} />}
|
|
16
|
+
{failed && <span className="text-sm text-danger">SVG 无法显示,请检查 SVG 内容。</span>}
|
|
17
|
+
</span>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Todo } from "../transcript.ts";
|
|
3
|
+
import { ChevronIcon } from "./icons.tsx";
|
|
4
|
+
|
|
5
|
+
/** Claude's task list, kept above the composer so progress is always visible. */
|
|
6
|
+
export function TodoPanel({ todos }: { todos: Todo[] }) {
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
if (todos.length === 0) return null;
|
|
9
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
10
|
+
const current = todos.find((t) => t.status === "in_progress");
|
|
11
|
+
return (
|
|
12
|
+
<div className="mb-2 rounded-xl border border-border bg-surface/80 text-sm" data-testid="todo-panel">
|
|
13
|
+
<button className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
|
|
14
|
+
<ChevronIcon open={open} />
|
|
15
|
+
<span className="shrink-0 font-medium">
|
|
16
|
+
待办 {done}/{todos.length}
|
|
17
|
+
</span>
|
|
18
|
+
<span className="min-w-0 truncate text-muted">{current ? current.activeForm || current.subject : done === todos.length ? "全部完成" : ""}</span>
|
|
19
|
+
<span className="ml-auto h-1.5 w-16 shrink-0 overflow-hidden rounded-full bg-surface-2">
|
|
20
|
+
<span className="block h-full bg-accent" style={{ width: `${(done / todos.length) * 100}%` }} />
|
|
21
|
+
</span>
|
|
22
|
+
</button>
|
|
23
|
+
{open && (
|
|
24
|
+
<ul className="max-h-60 space-y-1 overflow-y-auto border-t border-border px-3 py-2">
|
|
25
|
+
{todos.map((t) => (
|
|
26
|
+
<li key={t.id} className="flex items-start gap-2" data-status={t.status}>
|
|
27
|
+
<span className={`mt-0.5 shrink-0 ${t.status === "completed" ? "text-ok" : t.status === "in_progress" ? "text-accent" : "text-muted"}`}>
|
|
28
|
+
{t.status === "completed" ? "✓" : t.status === "in_progress" ? "◐" : "○"}
|
|
29
|
+
</span>
|
|
30
|
+
<span className={t.status === "completed" ? "text-muted line-through" : ""}>{t.subject}</span>
|
|
31
|
+
</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { loadedBuildVersion, useLive } from "../live.ts";
|
|
3
|
+
import { Button } from "../ui/Button.tsx";
|
|
4
|
+
import { useConfirm } from "./Confirmation.tsx";
|
|
5
|
+
import { allowNextUnload, hasPendingAttachments, unsavedEditors } from "../unsaved.tsx";
|
|
6
|
+
|
|
7
|
+
/** Stay visible until the user refreshes or postpones this particular deployment. */
|
|
8
|
+
export function VersionNotice() {
|
|
9
|
+
const confirm = useConfirm();
|
|
10
|
+
const version = useLive((s) => s.distVersion);
|
|
11
|
+
const [dismissed, setDismissed] = useState<string>();
|
|
12
|
+
if (!loadedBuildVersion || !version || version === loadedBuildVersion || version === dismissed) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="pointer-events-none fixed inset-x-0 bottom-[max(0.75rem,env(safe-area-inset-bottom))] z-50 flex justify-center pl-[max(0.75rem,env(safe-area-inset-left))] pr-[max(0.75rem,env(safe-area-inset-right))]" aria-live="polite">
|
|
16
|
+
<div role="status" className="pointer-events-auto flex w-full max-w-md flex-wrap items-center gap-2 rounded-xl border border-accent/30 bg-surface px-4 py-3 shadow-lg" data-testid="version-notice">
|
|
17
|
+
<span className="min-w-0 flex-1 text-sm">新版本已部署,刷新页面即可生效。</span>
|
|
18
|
+
<Button size="sm" variant="ghost" onClick={() => setDismissed(version)}>稍后</Button>
|
|
19
|
+
<Button size="sm" variant="primary" onClick={async () => {
|
|
20
|
+
const editors = unsavedEditors();
|
|
21
|
+
const attachments = hasPendingAttachments();
|
|
22
|
+
if ((editors.length || attachments) && !(await confirm({
|
|
23
|
+
title: "刷新并放弃未保存内容?",
|
|
24
|
+
description: [
|
|
25
|
+
editors.length ? `尚未保存:${editors.join("、")}。` : "",
|
|
26
|
+
attachments ? "未发送的图片附件和正在读取的图片会丢失。" : "",
|
|
27
|
+
"文字草稿会保留。",
|
|
28
|
+
].join(""),
|
|
29
|
+
confirmLabel: "放弃并刷新",
|
|
30
|
+
cancelLabel: "暂不刷新",
|
|
31
|
+
danger: true,
|
|
32
|
+
})).confirmed) return;
|
|
33
|
+
if ("serviceWorker" in navigator) void navigator.serviceWorker.getRegistration().then((registration) => registration?.update()).catch(() => {});
|
|
34
|
+
allowNextUnload();
|
|
35
|
+
location.reload();
|
|
36
|
+
}}>刷新页面</Button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
function Svg(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden {...props} />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MenuIcon = () => (
|
|
8
|
+
<Svg>
|
|
9
|
+
<path d="M4 6h16M4 12h16M4 18h16" />
|
|
10
|
+
</Svg>
|
|
11
|
+
);
|
|
12
|
+
export const PlusIcon = () => (
|
|
13
|
+
<Svg>
|
|
14
|
+
<path d="M12 5v14M5 12h14" />
|
|
15
|
+
</Svg>
|
|
16
|
+
);
|
|
17
|
+
export const SendIcon = () => (
|
|
18
|
+
<Svg>
|
|
19
|
+
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
20
|
+
</Svg>
|
|
21
|
+
);
|
|
22
|
+
export const StopIcon = () => (
|
|
23
|
+
<Svg>
|
|
24
|
+
<rect x="6" y="6" width="12" height="12" rx="1.5" fill="currentColor" stroke="none" />
|
|
25
|
+
</Svg>
|
|
26
|
+
);
|
|
27
|
+
export const ChevronIcon = ({ open }: { open?: boolean }) => (
|
|
28
|
+
<Svg width="14" height="14" style={{ transform: open ? "rotate(90deg)" : undefined, transition: "transform 120ms" }}>
|
|
29
|
+
<path d="M9 6l6 6-6 6" />
|
|
30
|
+
</Svg>
|
|
31
|
+
);
|
|
32
|
+
export const CloseIcon = ({ size }: { size?: number }) => (
|
|
33
|
+
<Svg {...(size ? { width: size, height: size } : {})}>
|
|
34
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
35
|
+
</Svg>
|
|
36
|
+
);
|
|
37
|
+
export const ImageIcon = () => (
|
|
38
|
+
<Svg>
|
|
39
|
+
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
|
40
|
+
<circle cx="9" cy="10" r="1.5" />
|
|
41
|
+
<path d="M20.5 16l-5-5-8.5 8.5" />
|
|
42
|
+
</Svg>
|
|
43
|
+
);
|
package/web/src/diff.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type DiffLine = { type: " " | "+" | "-"; text: string; oldNo?: number; newNo?: number } | { type: "gap"; count: number };
|
|
2
|
+
|
|
3
|
+
/** Line diff via LCS. Falls back to remove-all/add-all for very large inputs. */
|
|
4
|
+
export function lineDiff(a: string, b: string, context = 3): DiffLine[] {
|
|
5
|
+
const A = a.split("\n");
|
|
6
|
+
const B = b.split("\n");
|
|
7
|
+
const rows: Exclude<DiffLine, { type: "gap" }>[] = [];
|
|
8
|
+
if (A.length * B.length > 4_000_000) {
|
|
9
|
+
A.forEach((t, i) => rows.push({ type: "-", text: t, oldNo: i + 1 }));
|
|
10
|
+
B.forEach((t, i) => rows.push({ type: "+", text: t, newNo: i + 1 }));
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
13
|
+
const n = A.length;
|
|
14
|
+
const m = B.length;
|
|
15
|
+
const lcs = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
16
|
+
for (let i = n - 1; i >= 0; i--)
|
|
17
|
+
for (let j = m - 1; j >= 0; j--) lcs[i]![j] = A[i] === B[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!);
|
|
18
|
+
let i = 0;
|
|
19
|
+
let j = 0;
|
|
20
|
+
while (i < n || j < m) {
|
|
21
|
+
if (i < n && j < m && A[i] === B[j]) rows.push({ type: " ", text: A[i]!, oldNo: ++i, newNo: ++j });
|
|
22
|
+
else if (j < m && (i >= n || lcs[i]![j + 1]! >= lcs[i + 1]![j]!)) rows.push({ type: "+", text: B[j]!, newNo: ++j });
|
|
23
|
+
else rows.push({ type: "-", text: A[i]!, oldNo: ++i });
|
|
24
|
+
}
|
|
25
|
+
return collapse(rows, context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Hide long runs of unchanged lines, keeping `context` lines around changes. */
|
|
29
|
+
export function collapse(rows: DiffLine[], context = 3): DiffLine[] {
|
|
30
|
+
const keep = rows.map(() => false);
|
|
31
|
+
rows.forEach((r, idx) => {
|
|
32
|
+
if (r.type === "+" || r.type === "-") for (let k = Math.max(0, idx - context); k <= Math.min(rows.length - 1, idx + context); k++) keep[k] = true;
|
|
33
|
+
});
|
|
34
|
+
const out: DiffLine[] = [];
|
|
35
|
+
let hidden = 0;
|
|
36
|
+
rows.forEach((r, idx) => {
|
|
37
|
+
if (keep[idx] || r.type === "gap") {
|
|
38
|
+
if (hidden) out.push({ type: "gap", count: hidden });
|
|
39
|
+
hidden = 0;
|
|
40
|
+
out.push(r);
|
|
41
|
+
} else hidden++;
|
|
42
|
+
});
|
|
43
|
+
if (hidden) out.push({ type: "gap", count: hidden });
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Rows from the CLI's structuredPatch hunks. */
|
|
48
|
+
export function fromHunks(hunks: { oldStart: number; newStart: number; lines: string[] }[]): DiffLine[] {
|
|
49
|
+
const out: DiffLine[] = [];
|
|
50
|
+
for (const h of hunks) {
|
|
51
|
+
if (out.length) out.push({ type: "gap", count: 0 });
|
|
52
|
+
let o = h.oldStart;
|
|
53
|
+
let nn = h.newStart;
|
|
54
|
+
for (const l of h.lines) {
|
|
55
|
+
const t = l[0];
|
|
56
|
+
if (t === "+") out.push({ type: "+", text: l.slice(1), newNo: nn++ });
|
|
57
|
+
else if (t === "-") out.push({ type: "-", text: l.slice(1), oldNo: o++ });
|
|
58
|
+
else out.push({ type: " ", text: l.slice(1), oldNo: o++, newNo: nn++ });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { addDraftImages, clearDraft, editDraft, getDraft, restoreRewindDraft, setDraft, useDrafts, type Attached } from "./drafts.ts";
|
|
4
|
+
|
|
5
|
+
const storage = new Map<string, string>();
|
|
6
|
+
Object.defineProperty(globalThis, "localStorage", { value: { getItem: (key: string) => storage.get(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), removeItem: (key: string) => storage.delete(key) }, configurable: true });
|
|
7
|
+
const image: Attached = { id: "image", mediaType: "image/png", data: "AA==", url: "data:image/png;base64,AA==" };
|
|
8
|
+
function deferred<T>() {
|
|
9
|
+
let resolve!: (value: T) => void;
|
|
10
|
+
const promise = new Promise<T>((done) => { resolve = done; });
|
|
11
|
+
return { promise, resolve };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("rewind replay preserves edits across navigation and reload", () => {
|
|
15
|
+
setDraft("rewind", "original", "r1");
|
|
16
|
+
editDraft("rewind", { text: "edited" });
|
|
17
|
+
restoreRewindDraft("rewind", "original", "r1");
|
|
18
|
+
assert.equal(getDraft("rewind").text, "edited");
|
|
19
|
+
useDrafts.setState({ drafts: {} }); // browser refresh, text and rewind identity remain persisted
|
|
20
|
+
restoreRewindDraft("rewind", "original", "r1");
|
|
21
|
+
assert.equal(getDraft("rewind").text, "edited");
|
|
22
|
+
editDraft("rewind", { text: "" });
|
|
23
|
+
useDrafts.setState({ drafts: {} });
|
|
24
|
+
restoreRewindDraft("rewind", "original", "r1");
|
|
25
|
+
assert.equal(getDraft("rewind").text, "", "intentional empty draft stays empty");
|
|
26
|
+
setDraft("rewind", "original", "r2");
|
|
27
|
+
assert.equal(getDraft("rewind").text, "original", "an explicit new rewind applies");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("restoring a new rewind never overwrites an existing user draft", () => {
|
|
31
|
+
editDraft("existing", { text: "my draft" });
|
|
32
|
+
restoreRewindDraft("existing", "original", "r1");
|
|
33
|
+
assert.equal(getDraft("existing").text, "my draft");
|
|
34
|
+
assert.equal(getDraft("existing").rewindId, "r1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("image completion updates source draft after another session opens", async () => {
|
|
38
|
+
const pending = deferred<Attached[]>();
|
|
39
|
+
const loading = addDraftImages("A", 1, () => pending.promise);
|
|
40
|
+
editDraft("B", { text: "other draft" });
|
|
41
|
+
assert.equal(getDraft("A").processing, 1);
|
|
42
|
+
pending.resolve([image]);
|
|
43
|
+
await loading;
|
|
44
|
+
assert.deepEqual(getDraft("A").images, [image]);
|
|
45
|
+
assert.equal(getDraft("A").processing, 0);
|
|
46
|
+
assert.equal(getDraft("B").text, "other draft");
|
|
47
|
+
assert.equal(getDraft("B").images.length, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const replace of ["clear", "rewind"] as const) {
|
|
51
|
+
test(`image finishing after ${replace} cannot resurrect old attachments`, async () => {
|
|
52
|
+
const pending = deferred<Attached[]>();
|
|
53
|
+
const sid = `cancel-${replace}`;
|
|
54
|
+
const loading = addDraftImages(sid, 1, () => pending.promise);
|
|
55
|
+
if (replace === "clear") clearDraft(sid);
|
|
56
|
+
else setDraft(sid, "new rewind", "r-new");
|
|
57
|
+
pending.resolve([image]);
|
|
58
|
+
await loading;
|
|
59
|
+
assert.equal(getDraft(sid).images.length, 0);
|
|
60
|
+
assert.equal(getDraft(sid).processing, 0);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("late successful submission does not clear a newer edit", () => {
|
|
65
|
+
editDraft("submit", { text: "first" });
|
|
66
|
+
const submitted = getDraft("submit").revision;
|
|
67
|
+
editDraft("submit", { text: "second" });
|
|
68
|
+
clearDraft("submit", submitted);
|
|
69
|
+
assert.equal(getDraft("submit").text, "second");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
test("a delayed recovery response cannot undo a newer explicit rewind", () => {
|
|
74
|
+
const generation = getDraft("late-recovery").generation;
|
|
75
|
+
setDraft("late-recovery", "new rewind", "r-new");
|
|
76
|
+
editDraft("late-recovery", { text: "" });
|
|
77
|
+
restoreRewindDraft("late-recovery", "old rewind", "r-old", generation);
|
|
78
|
+
assert.equal(getDraft("late-recovery").rewindId, "r-new");
|
|
79
|
+
assert.equal(getDraft("late-recovery").text, "");
|
|
80
|
+
});
|