@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,297 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import fsPromises from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cide-peer-")));
|
|
10
|
+
process.env.CLAUDE_IDE_DATA_DIR = path.join(root, "data");
|
|
11
|
+
process.env.CLAUDE_CONFIG_DIR = path.join(root, "claude");
|
|
12
|
+
process.env.CLAUDE_IDE_PASSWORD = "peer-test";
|
|
13
|
+
const { sessionMessaging, configureSessionMessaging, beginSessionDeletion } = await import("./session-messaging.ts");
|
|
14
|
+
const aliases = await import("./session-aliases.ts");
|
|
15
|
+
const { db } = await import("./db.ts");
|
|
16
|
+
const { sessions, createDraftSession, removeSession, resolveMessagingSession } = await import("./sessions.ts");
|
|
17
|
+
|
|
18
|
+
function deferred() {
|
|
19
|
+
let resolve!: () => void;
|
|
20
|
+
const promise = new Promise<void>((r) => { resolve = r; });
|
|
21
|
+
return { promise, resolve };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fixture() {
|
|
25
|
+
const a = crypto.randomUUID(), b = crypto.randomUUID(), c = crypto.randomUUID();
|
|
26
|
+
const targets = new Map<string, { sessionId: string; projectPath: string; cwd: string }>([a, b, c].map((id) => [id, { sessionId: id, projectPath: `/project/${id}`, cwd: `/project/${id}` }]));
|
|
27
|
+
const deliveries: { id: string; text: string }[] = [];
|
|
28
|
+
const created: string[] = [];
|
|
29
|
+
let writable = true;
|
|
30
|
+
const deps = {
|
|
31
|
+
resolveExistingSession: async (id: string) => targets.get(id),
|
|
32
|
+
assertAllowed: () => { if (!writable) throw new Error("readonly"); },
|
|
33
|
+
getOrCreate: async (id: string) => {
|
|
34
|
+
created.push(id);
|
|
35
|
+
return {
|
|
36
|
+
cwd: targets.get(id)!.cwd,
|
|
37
|
+
sendPeerText: async (_uuid: string, text: string, active: () => boolean) => {
|
|
38
|
+
if (!active()) return false;
|
|
39
|
+
deliveries.push({ id, text });
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
configureSessionMessaging(deps);
|
|
46
|
+
return { a, b, c, targets, deliveries, created, deps, readonly: () => { writable = false; } };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("send resolves caller's alias and marks source using receiver's independent address book", async () => {
|
|
50
|
+
const f = fixture();
|
|
51
|
+
await sessionMessaging.setAlias(f.a, f.b, "reviewer");
|
|
52
|
+
await sessionMessaging.setAlias(f.b, f.a, "planner");
|
|
53
|
+
await sessionMessaging.setAlias(f.c, f.a, "reviewer");
|
|
54
|
+
assert.deepEqual(await sessionMessaging.send(f.a, "reviewer", "/btw literal text"), { sessionId: f.b, status: "queued" });
|
|
55
|
+
assert.deepEqual(f.created, [f.b]);
|
|
56
|
+
assert.equal(f.deliveries[0]!.text, `Message from session ${f.a}\nSource alias: "planner"\n\n/btw literal text`);
|
|
57
|
+
await sessionMessaging.removeAlias(f.b, "planner");
|
|
58
|
+
await sessionMessaging.send(f.a, f.b.toUpperCase(), "second");
|
|
59
|
+
assert.match(f.deliveries[1]!.text, /Source alias: ""/);
|
|
60
|
+
await assert.rejects(sessionMessaging.send(f.a, f.a.toUpperCase(), "self"), /自身/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("unknown targets never create sessions; malformed and oversized messages are rejected", async () => {
|
|
64
|
+
const f = fixture();
|
|
65
|
+
await assert.rejects(sessionMessaging.send(f.a, crypto.randomUUID(), "missing"), /不存在/);
|
|
66
|
+
await assert.rejects(sessionMessaging.send(f.a, "unknown-alias", "missing"), /不存在/);
|
|
67
|
+
await assert.rejects(sessionMessaging.send(f.a, f.b, " "), /不能为空/);
|
|
68
|
+
await assert.rejects(sessionMessaging.send(f.a, f.b, "a".repeat(262145)), /256/);
|
|
69
|
+
assert.equal(f.created.length, 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("accepted alias address is frozen while lookups run", async () => {
|
|
73
|
+
const f = fixture();
|
|
74
|
+
aliases.setAlias(f.a, f.b, "peer");
|
|
75
|
+
const gate = deferred();
|
|
76
|
+
configureSessionMessaging({ ...f.deps, resolveExistingSession: async (id) => { await gate.promise; return f.targets.get(id); } });
|
|
77
|
+
const sending = sessionMessaging.send(f.a, "peer", "frozen");
|
|
78
|
+
aliases.removeAlias(f.a, "peer");
|
|
79
|
+
aliases.setAlias(f.a, f.c, "peer");
|
|
80
|
+
gate.resolve();
|
|
81
|
+
await sending;
|
|
82
|
+
assert.equal(f.deliveries[0]!.id, f.b);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("deletion invalidates in-flight work even if deletion finishes before lookup returns", async () => {
|
|
86
|
+
const f = fixture();
|
|
87
|
+
const gate = deferred();
|
|
88
|
+
configureSessionMessaging({ ...f.deps, resolveExistingSession: async (id) => { await gate.promise; return f.targets.get(id); } });
|
|
89
|
+
const sending = sessionMessaging.send(f.a, f.b, "stale");
|
|
90
|
+
const deleted = beginSessionDeletion(f.b);
|
|
91
|
+
deleted();
|
|
92
|
+
gate.resolve();
|
|
93
|
+
await assert.rejects(sending, /删除/);
|
|
94
|
+
assert.equal(f.created.length, 0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("readonly change during target lookup prevents both alias mutation and delivery", async () => {
|
|
98
|
+
const f = fixture();
|
|
99
|
+
const gate = deferred();
|
|
100
|
+
configureSessionMessaging({ ...f.deps, resolveExistingSession: async (id) => { await gate.promise; return f.targets.get(id); } });
|
|
101
|
+
const alias = sessionMessaging.setAlias(f.a, f.b, "blocked");
|
|
102
|
+
const sending = sessionMessaging.send(f.a, f.b, "blocked");
|
|
103
|
+
f.readonly();
|
|
104
|
+
gate.resolve();
|
|
105
|
+
await assert.rejects(alias, /readonly/);
|
|
106
|
+
await assert.rejects(sending, /readonly/);
|
|
107
|
+
assert.equal(aliases.aliasTarget(f.a, "blocked"), undefined);
|
|
108
|
+
assert.equal(f.created.length, 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a live session started in another existing directory is rejected after startup", async () => {
|
|
112
|
+
const f = fixture();
|
|
113
|
+
const destination = fs.mkdtempSync(path.join(root, "destination-"));
|
|
114
|
+
const moved = fs.mkdtempSync(path.join(root, "moved-"));
|
|
115
|
+
configureSessionMessaging({
|
|
116
|
+
...f.deps,
|
|
117
|
+
resolveExistingSession: async (id) => (id === f.b ? { sessionId: f.b, projectPath: destination, cwd: destination } : f.targets.get(id)),
|
|
118
|
+
getOrCreate: async () => ({ cwd: moved, sendPeerText: async () => true }),
|
|
119
|
+
});
|
|
120
|
+
await assert.rejects(sessionMessaging.send(f.a, f.b, "wrong directory"), /位置发生变化/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a project registered through a symlink can receive messages from a transcript that records the real path", async () => {
|
|
124
|
+
const real = fs.mkdtempSync(path.join(root, "real-"));
|
|
125
|
+
const link = path.join(root, "link-");
|
|
126
|
+
fs.symlinkSync(real, link);
|
|
127
|
+
db.prepare("INSERT INTO projects(path, added_at) VALUES (?, ?)").run(link, Date.now());
|
|
128
|
+
const receiver = crypto.randomUUID();
|
|
129
|
+
db.prepare("INSERT INTO session_view (session_id, project_path) VALUES (?, ?)").run(receiver, link);
|
|
130
|
+
const sender = createDraftSession(real);
|
|
131
|
+
// The CLI writes the resolved cwd (not the symlink the project was registered with).
|
|
132
|
+
const dir = path.join(process.env.CLAUDE_CONFIG_DIR!, "projects", real.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
133
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(dir, `${receiver}.jsonl`), JSON.stringify({ type: "user", sessionId: receiver, cwd: real, uuid: crypto.randomUUID(), parentUuid: null, timestamp: new Date().toISOString(), message: { role: "user", content: "symlinked project" } }) + "\n");
|
|
135
|
+
const deliveries: string[] = [];
|
|
136
|
+
configureSessionMessaging({
|
|
137
|
+
resolveExistingSession: resolveMessagingSession,
|
|
138
|
+
getOrCreate: async (id, project, stillActive) => {
|
|
139
|
+
const live = await sessions.getOrCreate(id, project, stillActive);
|
|
140
|
+
return {
|
|
141
|
+
cwd: live.cwd,
|
|
142
|
+
sendPeerText: async (_uuid: string, text: string, active: () => boolean) => {
|
|
143
|
+
if (!active()) return false;
|
|
144
|
+
deliveries.push(text);
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
assertAllowed: () => {},
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
assert.deepEqual(await resolveMessagingSession(receiver), { sessionId: receiver, projectPath: link, cwd: real });
|
|
153
|
+
// The live wrapper runs in the registered (symlink) project path, so the two
|
|
154
|
+
// spellings name the same directory and must not be treated as a move.
|
|
155
|
+
assert.equal((await sessions.getOrCreate(receiver, link)).cwd, link);
|
|
156
|
+
assert.deepEqual(await sessionMessaging.send(sender, receiver, "through the link"), { sessionId: receiver, status: "queued" });
|
|
157
|
+
assert.equal(deliveries.length, 1);
|
|
158
|
+
} finally {
|
|
159
|
+
await sessions.drop(receiver);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("filesystem identity keeps the final target guard from reporting a move between two spellings of one project", async () => {
|
|
164
|
+
const f = fixture();
|
|
165
|
+
const real = fs.mkdtempSync(path.join(root, "target-"));
|
|
166
|
+
const link = path.join(root, "target-link-");
|
|
167
|
+
fs.symlinkSync(real, link);
|
|
168
|
+
let lookups = 0;
|
|
169
|
+
configureSessionMessaging({
|
|
170
|
+
...f.deps,
|
|
171
|
+
resolveExistingSession: async (id) => {
|
|
172
|
+
if (id !== f.b) return f.targets.get(id);
|
|
173
|
+
lookups += 1;
|
|
174
|
+
// The first lookup resolves through the registered project path; the re-check
|
|
175
|
+
// reports the project's real path. Both name the same directory.
|
|
176
|
+
return { sessionId: f.b, projectPath: lookups === 1 ? link : real, cwd: real };
|
|
177
|
+
},
|
|
178
|
+
getOrCreate: async () => ({ cwd: real, sendPeerText: async () => true }),
|
|
179
|
+
});
|
|
180
|
+
assert.deepEqual(await sessionMessaging.send(f.a, f.b, "canonical"), { sessionId: f.b, status: "queued" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a move to another existing directory is still rejected", async () => {
|
|
184
|
+
const f = fixture();
|
|
185
|
+
const destination = fs.mkdtempSync(path.join(root, "destination-"));
|
|
186
|
+
// Simulates /cd or worktree removal racing the live-session startup.
|
|
187
|
+
const moved = fs.mkdtempSync(path.join(root, "moved-"));
|
|
188
|
+
configureSessionMessaging({
|
|
189
|
+
...f.deps,
|
|
190
|
+
resolveExistingSession: async (id) => (id === f.b ? { sessionId: f.b, projectPath: destination, cwd: destination } : f.targets.get(id)),
|
|
191
|
+
getOrCreate: async () => ({ cwd: moved, sendPeerText: async () => true }),
|
|
192
|
+
});
|
|
193
|
+
await assert.rejects(sessionMessaging.send(f.a, f.b, "really moved"), /位置发生变化/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a relative cwd containing .. names the same destination", async () => {
|
|
197
|
+
const f = fixture();
|
|
198
|
+
const real = fs.mkdtempSync(path.join(root, "relative-"));
|
|
199
|
+
fs.mkdirSync(path.join(real, "sub"));
|
|
200
|
+
const spelling = `${path.relative(process.cwd(), real)}${path.sep}sub${path.sep}..`;
|
|
201
|
+
configureSessionMessaging({
|
|
202
|
+
...f.deps,
|
|
203
|
+
resolveExistingSession: async (id) => (id === f.b ? { sessionId: f.b, projectPath: real, cwd: real } : f.targets.get(id)),
|
|
204
|
+
getOrCreate: async () => ({ cwd: spelling, sendPeerText: async () => true }),
|
|
205
|
+
});
|
|
206
|
+
assert.deepEqual(await sessionMessaging.send(f.a, f.b, "relative"), { sessionId: f.b, status: "queued" });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("missing directories compare lexically and never surface stat errors", async () => {
|
|
210
|
+
const f = fixture();
|
|
211
|
+
const gone = path.join(root, "gone", "project");
|
|
212
|
+
const spelling = `${gone}${path.sep}nested${path.sep}..`;
|
|
213
|
+
configureSessionMessaging({
|
|
214
|
+
...f.deps,
|
|
215
|
+
resolveExistingSession: async (id) => (id === f.b ? { sessionId: f.b, projectPath: gone, cwd: gone } : f.targets.get(id)),
|
|
216
|
+
getOrCreate: async () => ({ cwd: spelling, sendPeerText: async () => true }),
|
|
217
|
+
});
|
|
218
|
+
assert.deepEqual(await sessionMessaging.send(f.a, f.b, "gone"), { sessionId: f.b, status: "queued" });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
async function sendWithLiveCwd(f: ReturnType<typeof fixture>, project: string, liveCwd: string) {
|
|
222
|
+
configureSessionMessaging({
|
|
223
|
+
...f.deps,
|
|
224
|
+
resolveExistingSession: async (id) => (id === f.b ? { sessionId: f.b, projectPath: project, cwd: project } : f.targets.get(id)),
|
|
225
|
+
getOrCreate: async () => ({ cwd: liveCwd, sendPeerText: async () => true }),
|
|
226
|
+
});
|
|
227
|
+
return sessionMessaging.send(f.a, f.b, "identity");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
test("zero inodes fall back to lexical comparison instead of trusting identity", async (t) => {
|
|
231
|
+
// Windows filesystems and some network mounts report ino 0 for every entry. The mocked
|
|
232
|
+
// paths share an inode, so trusting it would wrongly accept two lexical spellings.
|
|
233
|
+
t.mock.method(fsPromises, "stat", (async () => ({ dev: 1, ino: 0 })) as unknown as typeof fsPromises.stat);
|
|
234
|
+
const real = fs.mkdtempSync(path.join(root, "zero-real-"));
|
|
235
|
+
fs.mkdirSync(path.join(real, "sub"));
|
|
236
|
+
const link = path.join(root, "zero-link-");
|
|
237
|
+
fs.symlinkSync(real, link);
|
|
238
|
+
const f = fixture();
|
|
239
|
+
await assert.rejects(sendWithLiveCwd(f, real, link), /位置发生变化/);
|
|
240
|
+
const g = fixture();
|
|
241
|
+
assert.deepEqual(await sendWithLiveCwd(g, real, `${real}${path.sep}sub${path.sep}..`), { sessionId: g.b, status: "queued" });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("real resolver recognizes drafts and imported transcripts, rejects idle phantom wrappers and missing worktrees", async () => {
|
|
245
|
+
const project = fs.mkdtempSync(path.join(root, "project-"));
|
|
246
|
+
db.prepare("INSERT INTO projects(path, added_at) VALUES (?, ?)").run(project, Date.now());
|
|
247
|
+
const draft = createDraftSession(project);
|
|
248
|
+
assert.deepEqual(await resolveMessagingSession(draft), { sessionId: draft, projectPath: project, cwd: project });
|
|
249
|
+
const phantom = crypto.randomUUID();
|
|
250
|
+
await sessions.getOrCreate(phantom, project);
|
|
251
|
+
assert.equal(await resolveMessagingSession(phantom), undefined);
|
|
252
|
+
const imported = crypto.randomUUID();
|
|
253
|
+
const dir = path.join(process.env.CLAUDE_CONFIG_DIR!, "projects", project.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
254
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
255
|
+
fs.writeFileSync(path.join(dir, `${imported}.jsonl`), JSON.stringify({ type: "user", sessionId: imported, cwd: project, uuid: crypto.randomUUID(), parentUuid: null, timestamp: new Date().toISOString(), message: { role: "user", content: "imported test session" } }) + "\n");
|
|
256
|
+
assert.deepEqual(await resolveMessagingSession(imported), { sessionId: imported, projectPath: project, cwd: project });
|
|
257
|
+
const missing = createDraftSession(project);
|
|
258
|
+
db.prepare("INSERT INTO session_cwd(session_id,cwd) VALUES (?, ?)").run(missing, path.join(project, ".claude/worktrees/deleted"));
|
|
259
|
+
await assert.rejects(resolveMessagingSession(missing), /工作目录不存在/);
|
|
260
|
+
await sessions.drop(phantom);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("stopping a process retains aliases; successful session deletion removes inbound and owned mappings", async () => {
|
|
264
|
+
const project = fs.mkdtempSync(path.join(root, "deletion-"));
|
|
265
|
+
const a = createDraftSession(project), b = createDraftSession(project), c = createDraftSession(project);
|
|
266
|
+
aliases.setAlias(a, b, "b");
|
|
267
|
+
aliases.setAlias(b, c, "c");
|
|
268
|
+
await sessions.drop(b);
|
|
269
|
+
assert.equal(aliases.aliasTarget(a, "b"), b);
|
|
270
|
+
const dir = path.join(process.env.CLAUDE_CONFIG_DIR!, "projects", project.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
271
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
272
|
+
const transcript = path.join(dir, `${b}.jsonl`);
|
|
273
|
+
// A transcript containing only system records has no session-list summary.
|
|
274
|
+
fs.writeFileSync(transcript, JSON.stringify({ type: "system", sessionId: b, cwd: project, uuid: crypto.randomUUID(), subtype: "init" }) + "\n");
|
|
275
|
+
await removeSession(b, project);
|
|
276
|
+
assert.equal(fs.existsSync(transcript), false);
|
|
277
|
+
assert.equal(aliases.aliasTarget(a, "b"), undefined);
|
|
278
|
+
assert.deepEqual(aliases.aliasesFor(b), []);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("transcript access errors do not release aliases or forget the session", async () => {
|
|
282
|
+
const project = fs.mkdtempSync(path.join(root, "unreadable-"));
|
|
283
|
+
const a = createDraftSession(project), b = createDraftSession(project);
|
|
284
|
+
aliases.setAlias(a, b, "keep");
|
|
285
|
+
const dir = path.join(process.env.CLAUDE_CONFIG_DIR!, "projects");
|
|
286
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
287
|
+
// A non-directory at the project store path yields ENOTDIR. Permissive history
|
|
288
|
+
// lookup returns no record; strict deletion must retain state rather than infer absence.
|
|
289
|
+
const blocker = path.join(dir, project.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
290
|
+
fs.writeFileSync(blocker, "blocked");
|
|
291
|
+
await assert.rejects(removeSession(b, project), (err: NodeJS.ErrnoException) => err.code === "ENOTDIR");
|
|
292
|
+
assert.equal(aliases.aliasTarget(a, "keep"), b);
|
|
293
|
+
assert.ok(db.prepare("SELECT session_id FROM session_view WHERE session_id = ?").get(b));
|
|
294
|
+
fs.unlinkSync(blocker);
|
|
295
|
+
await removeSession(b, project);
|
|
296
|
+
assert.equal(aliases.aliasTarget(a, "keep"), undefined);
|
|
297
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { aliasForTarget, aliasTarget, removeAlias, setAlias, validateAlias, SESSION_ID_PATTERN } from "./session-aliases.ts";
|
|
5
|
+
|
|
6
|
+
export type MessagingTarget = { sessionId: string; projectPath: string; cwd: string };
|
|
7
|
+
type MessagingLive = { cwd: string; sendPeerText(uuid: string, text: string, stillActive: () => boolean): Promise<boolean> };
|
|
8
|
+
type MessagingDependencies = {
|
|
9
|
+
resolveExistingSession(id: string): Promise<MessagingTarget | undefined>;
|
|
10
|
+
getOrCreate(id: string, projectPath: string, stillActive: () => boolean): Promise<MessagingLive>;
|
|
11
|
+
assertAllowed(sender: string, action: "send" | "alias"): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let dependencies: MessagingDependencies | undefined;
|
|
15
|
+
export function configureSessionMessaging(value: MessagingDependencies) { dependencies = value; }
|
|
16
|
+
|
|
17
|
+
// Only operations currently in flight occupy memory. Deletion invalidates their guards
|
|
18
|
+
// before its first await, so an old lookup cannot resurrect a deleted conversation.
|
|
19
|
+
const deleting = new Set<string>();
|
|
20
|
+
const operations = new Map<string, Set<{ valid: boolean }>>();
|
|
21
|
+
export function beginSessionDeletion(id: string): () => void {
|
|
22
|
+
if (deleting.has(id)) throw new Error("会话正在删除,请稍后重试。");
|
|
23
|
+
deleting.add(id);
|
|
24
|
+
for (const operation of operations.get(id) ?? []) operation.valid = false;
|
|
25
|
+
return () => { deleting.delete(id); };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function operationFor(ids: string[]) {
|
|
29
|
+
const operation = { valid: true };
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
if (deleting.has(id)) operation.valid = false;
|
|
32
|
+
const active = operations.get(id) ?? new Set();
|
|
33
|
+
active.add(operation);
|
|
34
|
+
operations.set(id, active);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
active: () => operation.valid && ids.every((id) => !deleting.has(id)),
|
|
38
|
+
release: () => {
|
|
39
|
+
for (const id of ids) {
|
|
40
|
+
const active = operations.get(id);
|
|
41
|
+
active?.delete(operation);
|
|
42
|
+
if (!active?.size) operations.delete(id);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function service() {
|
|
49
|
+
if (!dependencies) throw new Error("跨会话消息服务尚未初始化。");
|
|
50
|
+
return dependencies;
|
|
51
|
+
}
|
|
52
|
+
function callerId(id: string) {
|
|
53
|
+
if (!SESSION_ID_PATTERN.test(id)) throw new Error("当前会话的 Session ID 无效。");
|
|
54
|
+
return id.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
function targetId(sender: string, to: string) {
|
|
57
|
+
const address = to.trim();
|
|
58
|
+
const id = SESSION_ID_PATTERN.test(address) ? address.toLowerCase() : aliasTarget(sender, address);
|
|
59
|
+
if (!id) throw new Error("Alias 不存在,请使用当前会话的通讯录昵称或 Session ID。");
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Filesystem identity of a path, following symlinks. Two spellings of one directory
|
|
65
|
+
* (symlink, `..`, case-insensitive mount, hard link, bind mount) share `dev:ino`.
|
|
66
|
+
* Missing or inaccessible paths, and filesystems that cannot report an inode, yield
|
|
67
|
+
* null so callers fall back to lexical comparison.
|
|
68
|
+
*/
|
|
69
|
+
async function pathIdentity(p: string): Promise<string | null> {
|
|
70
|
+
try {
|
|
71
|
+
const s = await fs.stat(p);
|
|
72
|
+
return s.ino === 0 ? null : `${s.dev}:${s.ino}`;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Compare by filesystem identity when both sides have one, otherwise lexically. */
|
|
79
|
+
function sameIdentity(aPath: string, aId: string | null, bPath: string, bId: string | null) {
|
|
80
|
+
if (aId !== null && bId !== null) return aId === bId;
|
|
81
|
+
return path.resolve(aPath) === path.resolve(bPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function samePath(a: string, b: string) {
|
|
85
|
+
const [aId, bId] = await Promise.all([pathIdentity(a), pathIdentity(b)]);
|
|
86
|
+
return sameIdentity(a, aId, b, bId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function sameTarget(a: MessagingTarget, b: MessagingTarget | undefined) {
|
|
90
|
+
if (!b || a.sessionId !== b.sessionId) return false;
|
|
91
|
+
const [projectSame, cwdSame] = await Promise.all([samePath(a.projectPath, b.projectPath), samePath(a.cwd, b.cwd)]);
|
|
92
|
+
return projectSame && cwdSame;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const sessionMessaging = {
|
|
96
|
+
async removeAlias(sender: string, alias: string): Promise<boolean> {
|
|
97
|
+
sender = callerId(sender);
|
|
98
|
+
alias = validateAlias(alias);
|
|
99
|
+
const deps = service();
|
|
100
|
+
const operation = operationFor([sender]);
|
|
101
|
+
try {
|
|
102
|
+
deps.assertAllowed(sender, "alias");
|
|
103
|
+
if (!(await deps.resolveExistingSession(sender))) throw new Error("会话不存在。");
|
|
104
|
+
if (!operation.active()) throw new Error("会话已删除或正在删除。");
|
|
105
|
+
deps.assertAllowed(sender, "alias");
|
|
106
|
+
return removeAlias(sender, alias);
|
|
107
|
+
} finally { operation.release(); }
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async setAlias(sender: string, target: string, alias: string): Promise<void> {
|
|
111
|
+
sender = callerId(sender);
|
|
112
|
+
target = target.toLowerCase();
|
|
113
|
+
alias = validateAlias(alias);
|
|
114
|
+
if (!SESSION_ID_PATTERN.test(target)) throw new Error("请填写有效的目标 Session ID。");
|
|
115
|
+
const deps = service();
|
|
116
|
+
const operation = operationFor([sender, target]);
|
|
117
|
+
try {
|
|
118
|
+
deps.assertAllowed(sender, "alias");
|
|
119
|
+
const [source, destination] = await Promise.all([
|
|
120
|
+
deps.resolveExistingSession(sender), deps.resolveExistingSession(target),
|
|
121
|
+
]);
|
|
122
|
+
if (!operation.active()) throw new Error("会话已删除或正在删除。");
|
|
123
|
+
if (!source || !destination) throw new Error("会话不存在。");
|
|
124
|
+
deps.assertAllowed(sender, "alias");
|
|
125
|
+
setAlias(sender, target, alias);
|
|
126
|
+
} finally { operation.release(); }
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async send(sender: string, to: string, text: string): Promise<{ sessionId: string; status: "queued" }> {
|
|
130
|
+
sender = callerId(sender);
|
|
131
|
+
if (!text.trim()) throw new Error("消息不能为空。");
|
|
132
|
+
if (Buffer.byteLength(text, "utf8") > 256 * 1024) throw new Error("消息不能超过 256 KiB。");
|
|
133
|
+
const id = targetId(sender, to);
|
|
134
|
+
if (id === sender) throw new Error("不能给当前会话自身发送消息。");
|
|
135
|
+
const deps = service();
|
|
136
|
+
const operation = operationFor([sender, id]);
|
|
137
|
+
const active = () => {
|
|
138
|
+
if (!operation.active()) return false;
|
|
139
|
+
deps.assertAllowed(sender, "send");
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
if (!active()) throw new Error("会话已删除或正在删除。");
|
|
144
|
+
const [source, destination] = await Promise.all([
|
|
145
|
+
deps.resolveExistingSession(sender), deps.resolveExistingSession(id),
|
|
146
|
+
]);
|
|
147
|
+
if (!active()) throw new Error("会话已删除或正在删除。");
|
|
148
|
+
if (!source || !destination) throw new Error("会话不存在。");
|
|
149
|
+
const destinationCwd = destination.cwd;
|
|
150
|
+
const destinationId = await pathIdentity(destinationCwd);
|
|
151
|
+
if (!active()) throw new Error("会话已删除或正在删除。");
|
|
152
|
+
const live = await deps.getOrCreate(id, destination.projectPath, active);
|
|
153
|
+
// /cd and worktree removal can race startup. Do not silently send into a
|
|
154
|
+
// different directory from the one validated above.
|
|
155
|
+
if (!sameIdentity(live.cwd, await pathIdentity(live.cwd), destinationCwd, destinationId)) throw new Error("目标会话位置发生变化,请重试。");
|
|
156
|
+
if (!active()) throw new Error("会话已删除或正在删除。");
|
|
157
|
+
if (!(await sameTarget(destination, await deps.resolveExistingSession(id)))) throw new Error("目标会话位置发生变化,请重试。");
|
|
158
|
+
if (!active()) throw new Error("会话已删除或正在删除。");
|
|
159
|
+
const alias = aliasForTarget(id, sender) ?? "";
|
|
160
|
+
const body = `Message from session ${sender}\nSource alias: ${JSON.stringify(alias)}\n\n${text}`;
|
|
161
|
+
if (!(await live.sendPeerText(crypto.randomUUID(), body, active))) throw new Error("消息未入队,会话已停止或删除,请重试。");
|
|
162
|
+
return { sessionId: id, status: "queued" };
|
|
163
|
+
} finally { operation.release(); }
|
|
164
|
+
},
|
|
165
|
+
};
|