@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,638 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { deleteSession, forkSession, getSessionInfo, getSessionMessages, listSessions, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
+
import { decodeProjectId, encodeProjectId, type CronInfo, type ProjectInfo, type SessionFileInfo, type SessionListItem, type SessionState } from "../shared/protocol.ts";
|
|
6
|
+
import { db } from "./db.ts";
|
|
7
|
+
import { LiveSession, type LiveTextPart } from "./live-session.ts";
|
|
8
|
+
import { forkedFrom } from "./cli-files.ts";
|
|
9
|
+
import { localCommandOutputs, sessionFile } from "./session-file.ts";
|
|
10
|
+
import { historyPage, historyMessage, historyReadPath, type PageOptions } from "./history-pages.ts";
|
|
11
|
+
import { draftCwds, historyCwd, sessionCwd, setSessionCwd, worktreeName, worktreeTranscriptCwds } from "./session-dirs.ts";
|
|
12
|
+
import { durableTasksForSession } from "./durable-tasks.ts";
|
|
13
|
+
import { idecron } from "./idecron.ts";
|
|
14
|
+
import { projectWorktrees } from "./worktree-git.ts";
|
|
15
|
+
import { beginSessionDeletion, configureSessionMessaging } from "./session-messaging.ts";
|
|
16
|
+
import { removeSessionAliases } from "./session-aliases.ts";
|
|
17
|
+
|
|
18
|
+
// ---------- projects (view state: which directories the user works in) ----------
|
|
19
|
+
|
|
20
|
+
export async function listProjects(): Promise<ProjectInfo[]> {
|
|
21
|
+
const rows = db.prepare("SELECT path FROM projects ORDER BY sort_order, added_at").all() as { path: string }[];
|
|
22
|
+
return Promise.all(rows.map((r) => projectInfo(r.path)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function projectInfo(p: string): Promise<ProjectInfo> {
|
|
26
|
+
return { id: encodeProjectId(p), path: p, name: path.basename(p) || p, exists: await isDir(p) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function isDir(p: string) {
|
|
30
|
+
try {
|
|
31
|
+
return (await fs.stat(p)).isDirectory();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function pathExists(p: string) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(p);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function addProject(raw: string): Promise<ProjectInfo> {
|
|
47
|
+
const p = path.resolve(raw.trim());
|
|
48
|
+
if (!path.isAbsolute(raw.trim())) throw new Error("请填写绝对路径");
|
|
49
|
+
if (!(await isDir(p))) throw new Error("目录不存在");
|
|
50
|
+
db.prepare("INSERT OR IGNORE INTO projects (path, added_at) VALUES (?, ?)").run(p, Date.now());
|
|
51
|
+
return projectInfo(p);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function removeProject(id: string) {
|
|
55
|
+
db.prepare("DELETE FROM projects WHERE path = ?").run(decodeProjectId(id));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve a project id from the client to a registered project path. */
|
|
59
|
+
export function projectPath(id: string): string {
|
|
60
|
+
const p = decodeProjectId(id);
|
|
61
|
+
const row = db.prepare("SELECT path FROM projects WHERE path = ?").get(p);
|
|
62
|
+
if (!row) throw new HttpError(404, "项目不存在");
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class HttpError extends Error {
|
|
67
|
+
constructor(
|
|
68
|
+
public status: 400 | 404 | 409,
|
|
69
|
+
message: string,
|
|
70
|
+
) {
|
|
71
|
+
super(message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
db.exec(`CREATE TABLE IF NOT EXISTS scheduled_sessions (session_id TEXT PRIMARY KEY, project_path TEXT NOT NULL, crons_json TEXT NOT NULL DEFAULT '[]', has_durable INTEGER NOT NULL DEFAULT 0)`);
|
|
76
|
+
// Existing databases predate the cached task list and the durable-only marker.
|
|
77
|
+
const scheduledColumns = db.prepare("PRAGMA table_info(scheduled_sessions)").all() as { name: string }[];
|
|
78
|
+
if (!scheduledColumns.some((c) => c.name === "crons_json")) {
|
|
79
|
+
db.exec(`ALTER TABLE scheduled_sessions ADD COLUMN crons_json TEXT NOT NULL DEFAULT '[]'`);
|
|
80
|
+
}
|
|
81
|
+
if (!scheduledColumns.some((c) => c.name === "has_durable")) {
|
|
82
|
+
db.exec(`ALTER TABLE scheduled_sessions ADD COLUMN has_durable INTEGER NOT NULL DEFAULT 0`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type ScheduledRow = { project_path: string; crons_json: string; has_durable: number };
|
|
86
|
+
function scheduledRow(sessionId: string): ScheduledRow | undefined {
|
|
87
|
+
return db.prepare("SELECT project_path, crons_json, has_durable FROM scheduled_sessions WHERE session_id = ?").get(sessionId) as ScheduledRow | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function storedCrons(raw: string): CronInfo[] {
|
|
91
|
+
try {
|
|
92
|
+
const value = JSON.parse(raw) as unknown;
|
|
93
|
+
if (!Array.isArray(value)) return [];
|
|
94
|
+
return value.flatMap((c) => {
|
|
95
|
+
if (!c || typeof c !== "object") return [];
|
|
96
|
+
const v = c as Record<string, unknown>;
|
|
97
|
+
if (typeof v.id !== "string" || typeof v.schedule !== "string" || typeof v.prompt !== "string") return [];
|
|
98
|
+
return [{ id: v.id, schedule: v.schedule, recurring: v.recurring === true, prompt: v.prompt }];
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------- live sessions ----------
|
|
106
|
+
|
|
107
|
+
type ChangeListener = (projectId: string) => void;
|
|
108
|
+
export type Activity = { projectId: string; sessionId: string; kind: "done" | "attention" | "error"; title: string };
|
|
109
|
+
type ActivityListener = (a: Activity) => void;
|
|
110
|
+
|
|
111
|
+
class SessionManager {
|
|
112
|
+
private live = new Map<string, LiveSession>();
|
|
113
|
+
private changeListeners = new Set<ChangeListener>();
|
|
114
|
+
private textListeners = new Set<(part: LiveTextPart) => void>();
|
|
115
|
+
private dropListeners = new Set<(sessionId: string) => void>();
|
|
116
|
+
|
|
117
|
+
onTextPart(fn: (part: LiveTextPart) => void) {
|
|
118
|
+
this.textListeners.add(fn);
|
|
119
|
+
return () => this.textListeners.delete(fn);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onSessionDropped(fn: (sessionId: string) => void) {
|
|
123
|
+
this.dropListeners.add(fn);
|
|
124
|
+
return () => this.dropListeners.delete(fn);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onSessionsChanged(fn: ChangeListener) {
|
|
128
|
+
this.changeListeners.add(fn);
|
|
129
|
+
return () => this.changeListeners.delete(fn);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private activityListeners = new Set<ActivityListener>();
|
|
133
|
+
|
|
134
|
+
onActivity(fn: ActivityListener) {
|
|
135
|
+
this.activityListeners.add(fn);
|
|
136
|
+
return () => this.activityListeners.delete(fn);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A turn ended or the session started waiting: tell everyone (notifications). */
|
|
140
|
+
/** Project each live session belongs to (its cwd may be a worktree of it). */
|
|
141
|
+
readonly projectOf = new Map<string, string>();
|
|
142
|
+
|
|
143
|
+
private transition(sessionId: string, projectPath: string, cwd: string, from: SessionState, to: SessionState, failed: boolean) {
|
|
144
|
+
const kind = to === "requires_action" ? "attention" : from !== "idle" && to === "idle" ? (failed ? "error" : "done") : undefined;
|
|
145
|
+
if (!kind) return;
|
|
146
|
+
void getSessionInfo(sessionId, { dir: cwd })
|
|
147
|
+
.catch(() => undefined)
|
|
148
|
+
.then((info) => {
|
|
149
|
+
const title = info?.customTitle ?? info?.summary ?? "会话";
|
|
150
|
+
for (const fn of this.activityListeners) fn({ projectId: encodeProjectId(projectPath), sessionId, kind, title });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
notifyChanged(projectPath: string) {
|
|
155
|
+
const id = encodeProjectId(projectPath);
|
|
156
|
+
for (const fn of this.changeListeners) fn(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Stop a session's CLI process and forget it. Waits for the process so it cannot write afterwards. */
|
|
160
|
+
async drop(sessionId: string) {
|
|
161
|
+
for (const listener of this.dropListeners) listener(sessionId);
|
|
162
|
+
db.prepare("DELETE FROM scheduled_sessions WHERE session_id = ?").run(sessionId);
|
|
163
|
+
idecron.cancelSession(sessionId);
|
|
164
|
+
const s = this.live.get(sessionId);
|
|
165
|
+
if (!s) return;
|
|
166
|
+
// Stay registered (closed) until stopped, so a client can't create a fresh instance meanwhile.
|
|
167
|
+
await s.shutdown();
|
|
168
|
+
if (this.live.get(sessionId) === s) this.live.delete(sessionId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get(sessionId: string): LiveSession | undefined {
|
|
172
|
+
return this.live.get(sessionId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** `projectPath` is the project; the CLI runs in the session's own cwd (a worktree of it, or the project). */
|
|
176
|
+
async getOrCreate(sessionId: string, projectPath: string, stillActive?: () => boolean): Promise<LiveSession> {
|
|
177
|
+
if (stillActive && !stillActive()) throw new Error("会话已删除或正在删除。");
|
|
178
|
+
let s = this.live.get(sessionId);
|
|
179
|
+
if (!s) {
|
|
180
|
+
// A stale URL must not reclaim a session moved by /cd before a server restart.
|
|
181
|
+
const scheduled = scheduledRow(sessionId);
|
|
182
|
+
const owner = viewRow(sessionId)?.project_path ?? scheduled?.project_path ?? projectPath;
|
|
183
|
+
const cwd = await sessionCwd(owner, sessionId);
|
|
184
|
+
if (stillActive && !stillActive()) throw new Error("会话已删除或正在删除。");
|
|
185
|
+
// A concurrent call may have created the session while we awaited the transcript.
|
|
186
|
+
s = this.live.get(sessionId);
|
|
187
|
+
if (s) return s;
|
|
188
|
+
s = new LiveSession(
|
|
189
|
+
sessionId,
|
|
190
|
+
cwd,
|
|
191
|
+
() => this.notifyChanged(this.projectOf.get(sessionId) ?? owner),
|
|
192
|
+
(from, to, turn) => !turn.silent && this.transition(sessionId, this.projectOf.get(sessionId) ?? owner, s!.cwd, from, to, turn.failed),
|
|
193
|
+
(part) => { for (const listener of this.textListeners) listener(part); },
|
|
194
|
+
);
|
|
195
|
+
this.projectOf.set(sessionId, owner);
|
|
196
|
+
// Scheduled tasks live in the CLI process: remember which sessions have them, so a server
|
|
197
|
+
// restart can resume those sessions (the CLI restores the tasks from the transcript).
|
|
198
|
+
s.onCronsChanged = (crons, hasDurable) => {
|
|
199
|
+
if (crons.length || hasDurable) {
|
|
200
|
+
db.prepare("INSERT OR REPLACE INTO scheduled_sessions (session_id, project_path, crons_json, has_durable) VALUES (?, ?, ?, ?)")
|
|
201
|
+
.run(sessionId, this.projectOf.get(sessionId) ?? owner, JSON.stringify(crons), hasDurable ? 1 : 0);
|
|
202
|
+
} else {
|
|
203
|
+
db.prepare("DELETE FROM scheduled_sessions WHERE session_id = ?").run(sessionId);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
if (scheduled) {
|
|
207
|
+
// The task list survives a restart; the current process still has to confirm it.
|
|
208
|
+
const cached = storedCrons(scheduled.crons_json);
|
|
209
|
+
if (cached.length) {
|
|
210
|
+
s.hydrateCrons(cached, false);
|
|
211
|
+
} else if (scheduled.has_durable) {
|
|
212
|
+
// A durable-only row: if the file is gone, there is no scheduled work left to protect.
|
|
213
|
+
if (s.hasDurableWork()) {
|
|
214
|
+
s.hydrateCrons([], true, true);
|
|
215
|
+
} else {
|
|
216
|
+
db.prepare("DELETE FROM scheduled_sessions WHERE session_id = ?").run(sessionId);
|
|
217
|
+
s.hydrateCrons([], false);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// A legacy row with no cached list: session-level tasks may still be restored by resume.
|
|
221
|
+
s.hydrateCrons([], true);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// A durable file may be the only evidence that this session owns scheduled work.
|
|
225
|
+
s.noteScheduled();
|
|
226
|
+
if (s.hasDurableWork()) s.markStaleDurable();
|
|
227
|
+
}
|
|
228
|
+
this.live.set(sessionId, s);
|
|
229
|
+
}
|
|
230
|
+
return s;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** CLI config changed on disk (settings pages): let every live session reload it. */
|
|
234
|
+
async configChanged() {
|
|
235
|
+
await Promise.all([...this.live.values()].map((s) => s.reloadConfig().catch(() => {})));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Sessions that are running or waiting for the user, across all projects. */
|
|
239
|
+
active() {
|
|
240
|
+
return [...this.live.values()].filter((s) => {
|
|
241
|
+
const snap = s.snapshot();
|
|
242
|
+
return s.state !== "idle" || snap.pending.length > 0 || snap.crons.length > 0;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** After a restart: bring back sessions that had scheduled tasks, so the tasks keep firing. */
|
|
247
|
+
async resumeScheduled() {
|
|
248
|
+
const rows = db.prepare("SELECT session_id, project_path, crons_json, has_durable FROM scheduled_sessions").all() as (ScheduledRow & { session_id: string })[];
|
|
249
|
+
for (const r of rows) {
|
|
250
|
+
try {
|
|
251
|
+
// Rows written before has_durable existed may be durable-only. Mark them now so a later
|
|
252
|
+
// file deletion can clear the stale state instead of keeping the process alive forever.
|
|
253
|
+
if (!r.has_durable && storedCrons(r.crons_json).length === 0) {
|
|
254
|
+
try {
|
|
255
|
+
if (durableTasksForSession(await sessionCwd(r.project_path, r.session_id), r.session_id).length > 0) {
|
|
256
|
+
db.prepare("UPDATE scheduled_sessions SET has_durable = 1 WHERE session_id = ?").run(r.session_id);
|
|
257
|
+
r.has_durable = 1;
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// A broken cwd must not prevent the other scheduled sessions from resuming.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
await (await this.getOrCreate(r.session_id, r.project_path)).wake();
|
|
264
|
+
console.log(`resumed scheduled session ${r.session_id}`);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.warn(`could not resume scheduled session ${r.session_id}: ${err instanceof Error ? err.message : err}`);
|
|
267
|
+
db.prepare("DELETE FROM scheduled_sessions WHERE session_id = ?").run(r.session_id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Periodically stop CLI processes of sessions that have been idle for a while. */
|
|
273
|
+
startReaper(maxIdleMs: number) {
|
|
274
|
+
const timer = setInterval(() => {
|
|
275
|
+
for (const s of this.live.values()) s.reapIfIdle(maxIdleMs);
|
|
276
|
+
}, 60_000);
|
|
277
|
+
timer.unref();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
closeAll() {
|
|
281
|
+
for (const s of this.live.values()) s.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const sessions = new SessionManager();
|
|
286
|
+
|
|
287
|
+
/** Resolve identity before creating any live wrapper. Unknown IDs must never create sessions. */
|
|
288
|
+
export async function resolveMessagingSession(id: string) {
|
|
289
|
+
const view = viewRow(id);
|
|
290
|
+
const scheduled = scheduledRow(id);
|
|
291
|
+
const live = sessions.get(id);
|
|
292
|
+
const liveProject = live ? sessions.projectOf.get(id) : undefined;
|
|
293
|
+
const knownProject = view?.project_path ?? scheduled?.project_path;
|
|
294
|
+
const candidate = async (projectPath: string, recordedCwd?: string) => {
|
|
295
|
+
const cwd = recordedCwd ?? await historyCwd(projectPath, id);
|
|
296
|
+
if (!(await isDir(cwd))) throw new Error("目标会话的工作目录不存在,无法投递消息。");
|
|
297
|
+
return { sessionId: id, projectPath, cwd };
|
|
298
|
+
};
|
|
299
|
+
if (knownProject) return candidate(knownProject);
|
|
300
|
+
// History reads can create an idle wrapper for arbitrary IDs. Such wrappers are
|
|
301
|
+
// not evidence of a real session; active first prompts, however, are.
|
|
302
|
+
if (live && liveProject) {
|
|
303
|
+
if (live.state !== "idle" || live.queuedMessageCount > 0) return candidate(liveProject, live.cwd);
|
|
304
|
+
const info = await getSessionInfo(id, { dir: await historyCwd(liveProject, id) }).catch(() => undefined);
|
|
305
|
+
if (info) return candidate(liveProject, info.cwd);
|
|
306
|
+
}
|
|
307
|
+
// Imported CLI sessions may have no IDE view row. Search managed projects and
|
|
308
|
+
// their transcript worktrees without reading whole histories or changing state.
|
|
309
|
+
const projects = db.prepare("SELECT path FROM projects ORDER BY sort_order, added_at").all() as { path: string }[];
|
|
310
|
+
for (const { path: project } of projects) {
|
|
311
|
+
const info = await getSessionInfo(id, { dir: project }).catch(() => undefined);
|
|
312
|
+
if (info) return candidate(project, info.cwd ?? project);
|
|
313
|
+
for (const cwd of await worktreeTranscriptCwds(project)) {
|
|
314
|
+
const info = await getSessionInfo(id, { dir: cwd }).catch(() => undefined);
|
|
315
|
+
if (info) return candidate(project, cwd);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
configureSessionMessaging({
|
|
322
|
+
resolveExistingSession: resolveMessagingSession,
|
|
323
|
+
getOrCreate: (id, project, stillActive) => sessions.getOrCreate(id, project, stillActive),
|
|
324
|
+
assertAllowed: (id, action) => {
|
|
325
|
+
const row = db.prepare("SELECT write_enabled, btw_parent FROM session_view WHERE session_id = ?").get(id) as { write_enabled: number; btw_parent: string | null } | undefined;
|
|
326
|
+
if (row?.write_enabled === 0 || sessions.get(id)?.writeEnabled === false) throw new Error("只读会话不能发送跨会话消息或修改 Alias。");
|
|
327
|
+
if (action === "send" && row?.btw_parent) throw new Error("旁支会话不能发送跨会话消息。");
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
idecron.setDelivery({
|
|
332
|
+
canDeliver: (task) => {
|
|
333
|
+
const live = sessions.get(task.sessionId);
|
|
334
|
+
return !live || (live.state === "idle" && live.snapshot().pending.length === 0);
|
|
335
|
+
},
|
|
336
|
+
deliver: async (task) => (await sessions
|
|
337
|
+
.getOrCreate(task.sessionId, task.projectPath))
|
|
338
|
+
.sendScheduled(crypto.randomUUID(), task.prompt, task.mode),
|
|
339
|
+
});
|
|
340
|
+
idecron.onChange = (sessionId) => sessions.get(sessionId)?.emitCronList();
|
|
341
|
+
|
|
342
|
+
// ---------- session list (CLI files are the source of truth) ----------
|
|
343
|
+
|
|
344
|
+
type ViewRow = { session_id: string; pinned: number; archived: number; btw_parent: string | null };
|
|
345
|
+
|
|
346
|
+
export async function listProjectSessions(projectPathStr: string): Promise<SessionListItem[]> {
|
|
347
|
+
// Worktrees are listed explicitly (M4); merging them here would also pull in the
|
|
348
|
+
// parent repo's sessions when a project is a subdirectory of another checkout.
|
|
349
|
+
const infos: Awaited<ReturnType<typeof listSessions>> = await listSessions({ dir: projectPathStr, includeWorktrees: false }).catch(() => []);
|
|
350
|
+
// Sessions in the project's own worktrees (<repo>/.claude/worktrees/<name>), listed with it.
|
|
351
|
+
const wts = await projectWorktrees(projectPathStr).catch(() => undefined);
|
|
352
|
+
// Plus worktrees that were cleaned up: their transcripts remain and stay reachable.
|
|
353
|
+
const wtCwds = new Set<string>();
|
|
354
|
+
for (const w of wts?.worktrees ?? []) if (await pathExists(w.cwd)) wtCwds.add(w.cwd);
|
|
355
|
+
for (const c of await worktreeTranscriptCwds(projectPathStr)) wtCwds.add(c);
|
|
356
|
+
for (const cwd of wtCwds) {
|
|
357
|
+
for (const info of await listSessions({ dir: cwd, includeWorktrees: false }).catch(() => [])) {
|
|
358
|
+
setSessionCwd(info.sessionId, cwd);
|
|
359
|
+
infos.push({ ...info, cwd });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const views = new Map(
|
|
363
|
+
(db.prepare("SELECT session_id, pinned, archived, btw_parent FROM session_view WHERE project_path = ?").all(projectPathStr) as ViewRow[]).map(
|
|
364
|
+
(r) => [r.session_id, r],
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
const items: SessionListItem[] = [];
|
|
368
|
+
const seen = new Set<string>();
|
|
369
|
+
for (const info of infos) {
|
|
370
|
+
seen.add(info.sessionId);
|
|
371
|
+
const v = views.get(info.sessionId);
|
|
372
|
+
if (v?.btw_parent) continue;
|
|
373
|
+
const live = sessions.get(info.sessionId);
|
|
374
|
+
items.push({
|
|
375
|
+
id: info.sessionId,
|
|
376
|
+
title: info.customTitle ?? info.summary ?? "",
|
|
377
|
+
lastModified: info.lastModified,
|
|
378
|
+
createdAt: info.createdAt,
|
|
379
|
+
state: live?.state ?? "idle",
|
|
380
|
+
pinned: !!v?.pinned,
|
|
381
|
+
archived: !!v?.archived,
|
|
382
|
+
draft: false,
|
|
383
|
+
tag: info.tag,
|
|
384
|
+
forkedFrom: (await forkedFrom(projectPathStr, info.sessionId))?.sessionId,
|
|
385
|
+
needsAttention: live?.state === "requires_action",
|
|
386
|
+
...worktreeFields(projectPathStr, info.cwd),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const cwds = draftCwds([...views.keys()]);
|
|
390
|
+
// Sessions created in the web UI that the CLI has not written yet.
|
|
391
|
+
for (const v of views.values()) {
|
|
392
|
+
if (seen.has(v.session_id) || v.btw_parent) continue;
|
|
393
|
+
const live = sessions.get(v.session_id);
|
|
394
|
+
const lastModified = live?.lastActivity ?? Date.now();
|
|
395
|
+
items.push({
|
|
396
|
+
id: v.session_id,
|
|
397
|
+
title: "",
|
|
398
|
+
lastModified,
|
|
399
|
+
state: live?.state ?? "idle",
|
|
400
|
+
pinned: !!v.pinned,
|
|
401
|
+
archived: !!v.archived,
|
|
402
|
+
draft: true,
|
|
403
|
+
needsAttention: live?.state === "requires_action",
|
|
404
|
+
...worktreeFields(projectPathStr, cwds.get(v.session_id)),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
items.sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.lastModified - a.lastModified);
|
|
408
|
+
return items;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function worktreeFields(projectPathStr: string, cwd: string | undefined): { cwd?: string; worktree?: string } {
|
|
412
|
+
if (!cwd || cwd === projectPathStr) return {};
|
|
413
|
+
const name = worktreeName(cwd);
|
|
414
|
+
return name ? { cwd, worktree: name } : {};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function createDraftSession(projectPathStr: string): string {
|
|
418
|
+
const id = crypto.randomUUID();
|
|
419
|
+
db.prepare("INSERT INTO session_view (session_id, project_path) VALUES (?, ?)").run(id, projectPathStr);
|
|
420
|
+
sessions.notifyChanged(projectPathStr);
|
|
421
|
+
return id;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The transcript path (and cwd) shown in the session info dialog; undefined for an unknown session. */
|
|
425
|
+
export async function sessionFileInfo(sessionId: string, projectPathStr: string): Promise<SessionFileInfo | undefined> {
|
|
426
|
+
const dir = await historyCwd(projectPathStr, sessionId);
|
|
427
|
+
const file = (await sessionFile(dir, sessionId)) ?? null;
|
|
428
|
+
if (!file && !viewRow(sessionId)) return undefined;
|
|
429
|
+
return { id: sessionId, cwd: await sessionCwd(projectPathStr, sessionId), file };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function sessionHistory(sessionId: string, projectPathStr: string) {
|
|
433
|
+
const msgs = await rawHistory(sessionId, projectPathStr);
|
|
434
|
+
// getSessionMessages leaves out system/local_command (the output of /cost, /context…): put it back after its command.
|
|
435
|
+
const outputs = await localCommandOutputs(await historyCwd(projectPathStr, sessionId), sessionId);
|
|
436
|
+
if (outputs.size === 0) return msgs;
|
|
437
|
+
return (msgs as unknown[]).flatMap((m) => {
|
|
438
|
+
const extra = outputs.get((m as { uuid?: string }).uuid ?? "") ?? [];
|
|
439
|
+
return [m, ...extra.map((o) => ({ type: "system", subtype: "local_command", uuid: o.uuid, content: o.content, parent_tool_use_id: null }))];
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function sessionHistoryPage(sessionId: string, projectPathStr: string, options: PageOptions = {}) {
|
|
444
|
+
const historyRevision = (await sessions.getOrCreate(sessionId, projectPathStr)).historyRevision;
|
|
445
|
+
const page = await historyPage(await sessionFile(await historyCwd(projectPathStr, sessionId), sessionId), { ...options, anchor: viewRow(sessionId)?.rewind_anchor });
|
|
446
|
+
return { ...page, historyRevision };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function sessionHistoryMessage(sessionId: string, projectPathStr: string, uuid: string) {
|
|
450
|
+
return historyMessage(await sessionFile(await historyCwd(projectPathStr, sessionId), sessionId), uuid, viewRow(sessionId)?.rewind_anchor);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function sessionReadPath(sessionId: string, projectPathStr: string, toolId: string) {
|
|
454
|
+
return historyReadPath(await sessionFile(await historyCwd(projectPathStr, sessionId), sessionId), toolId, viewRow(sessionId)?.rewind_anchor);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Transcript messages of the current branch, cut at a pending conversation rewind. */
|
|
458
|
+
async function rawHistory(sessionId: string, projectPathStr: string) {
|
|
459
|
+
// Read from where the transcript lives (a worktree, possibly already removed), not the project root.
|
|
460
|
+
const msgs = await getSessionMessages(sessionId, { dir: await historyCwd(projectPathStr, sessionId) }).catch(() => []);
|
|
461
|
+
// A pending conversation rewind hides everything after its anchor until the next message.
|
|
462
|
+
const anchor = viewRow(sessionId)?.rewind_anchor;
|
|
463
|
+
if (anchor) {
|
|
464
|
+
const idx = msgs.findIndex((m) => m.uuid === anchor);
|
|
465
|
+
if (idx !== -1) return msgs.slice(0, idx + 1);
|
|
466
|
+
}
|
|
467
|
+
return msgs;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
type FullViewRow = ViewRow & { project_path: string; rewind_anchor: string | null; rewind_text: string | null };
|
|
471
|
+
|
|
472
|
+
function viewRow(sessionId: string): FullViewRow | undefined {
|
|
473
|
+
return db.prepare("SELECT * FROM session_view WHERE session_id = ?").get(sessionId) as FullViewRow | undefined;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function upsertView(sessionId: string, projectPathStr: string, patch: Partial<Record<"pinned" | "archived" | "ask_questions_enabled" | "write_enabled", number>>) {
|
|
477
|
+
db.prepare("INSERT OR IGNORE INTO session_view (session_id, project_path) VALUES (?, ?)").run(sessionId, projectPathStr);
|
|
478
|
+
for (const [k, v] of Object.entries(patch)) db.prepare(`UPDATE session_view SET ${k} = ? WHERE session_id = ?`).run(v, sessionId);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function updateSession(sessionId: string, projectPathStr: string, patch: { title?: string; pinned?: boolean; archived?: boolean; askQuestionsEnabled?: boolean; writeEnabled?: boolean }) {
|
|
482
|
+
if (patch.title !== undefined) {
|
|
483
|
+
const title = patch.title.trim();
|
|
484
|
+
if (!title) throw new HttpError(400, "标题不能为空");
|
|
485
|
+
const renamedLive = await sessions.get(sessionId)?.renameLive(title).catch(() => false);
|
|
486
|
+
// The transcript may live in a worktree of the project, not under the project root.
|
|
487
|
+
if (!renamedLive) await renameSession(sessionId, title, { dir: await historyCwd(projectPathStr, sessionId) });
|
|
488
|
+
}
|
|
489
|
+
const view: Partial<Record<"pinned" | "archived" | "ask_questions_enabled" | "write_enabled", number>> = {};
|
|
490
|
+
if (patch.pinned !== undefined) view.pinned = patch.pinned ? 1 : 0;
|
|
491
|
+
if (patch.archived !== undefined) view.archived = patch.archived ? 1 : 0;
|
|
492
|
+
if (patch.askQuestionsEnabled !== undefined) view.ask_questions_enabled = patch.askQuestionsEnabled ? 1 : 0;
|
|
493
|
+
if (patch.writeEnabled !== undefined) view.write_enabled = patch.writeEnabled ? 1 : 0;
|
|
494
|
+
if (Object.keys(view).length) upsertView(sessionId, projectPathStr, view);
|
|
495
|
+
if (patch.askQuestionsEnabled !== undefined) sessions.get(sessionId)?.setAskQuestionsEnabled(patch.askQuestionsEnabled);
|
|
496
|
+
if (patch.writeEnabled !== undefined) sessions.get(sessionId)?.setWriteEnabled(patch.writeEnabled);
|
|
497
|
+
sessions.notifyChanged(projectPathStr);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function removeSession(sessionId: string, projectPathStr: string) {
|
|
501
|
+
const finishDeletion = beginSessionDeletion(sessionId);
|
|
502
|
+
try {
|
|
503
|
+
await sessions.drop(sessionId);
|
|
504
|
+
const cwd = await historyCwd(projectPathStr, sessionId);
|
|
505
|
+
// Metadata may omit a transcript without a usable summary. Delete by file
|
|
506
|
+
// existence, and keep aliases if the CLI deletion fails. Drafts have no file.
|
|
507
|
+
if (await sessionFile(cwd, sessionId, true)) await deleteSession(sessionId, { dir: cwd });
|
|
508
|
+
if (await sessionFile(cwd, sessionId, true)) throw new Error("会话记录仍然存在,未完成删除。");
|
|
509
|
+
db.prepare("DELETE FROM session_view WHERE session_id = ?").run(sessionId);
|
|
510
|
+
removeSessionAliases(sessionId);
|
|
511
|
+
sessions.projectOf.delete(sessionId);
|
|
512
|
+
sessions.notifyChanged(projectPathStr);
|
|
513
|
+
} finally { finishDeletion(); }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** The transcript record just before a user message: the last record of the previous turn. */
|
|
517
|
+
async function recordBefore(sessionId: string, projectPathStr: string, userUuid: string) {
|
|
518
|
+
const msgs = await rawHistory(sessionId, projectPathStr);
|
|
519
|
+
const idx = msgs.findIndex((m) => m.uuid === userUuid);
|
|
520
|
+
if (idx === -1) throw new HttpError(404, "找不到这条消息(可能还没写入会话文件)");
|
|
521
|
+
const msg = msgs[idx]!.message as { content?: unknown };
|
|
522
|
+
const content = msg?.content;
|
|
523
|
+
const text =
|
|
524
|
+
typeof content === "string"
|
|
525
|
+
? content
|
|
526
|
+
: Array.isArray(content)
|
|
527
|
+
? content.filter((b: { type?: string }) => b?.type === "text").map((b: { text?: string }) => b.text ?? "").join("\n")
|
|
528
|
+
: "";
|
|
529
|
+
return { before: msgs[idx - 1]?.uuid, last: msgs[msgs.length - 1]?.uuid, text };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Fork into a new independent session. `beforeUuid` = fork from just before that user message; omitted = whole session. */
|
|
533
|
+
export async function forkAt(sessionId: string, projectPathStr: string, beforeUuid?: string): Promise<string> {
|
|
534
|
+
let upTo: string | undefined;
|
|
535
|
+
if (beforeUuid) {
|
|
536
|
+
const r = await recordBefore(sessionId, projectPathStr, beforeUuid);
|
|
537
|
+
if (!r.before) throw new HttpError(400, "第一条消息之前没有内容可以 fork,请直接新建会话");
|
|
538
|
+
upTo = r.before;
|
|
539
|
+
}
|
|
540
|
+
// The fork is written next to its source (the project, or the worktree the source runs in).
|
|
541
|
+
const { sessionId: id } = await forkSession(sessionId, { dir: await historyCwd(projectPathStr, sessionId), ...(upTo ? { upToMessageId: upTo } : {}) });
|
|
542
|
+
sessions.notifyChanged(projectPathStr);
|
|
543
|
+
return id;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export type RewindMode = "conversation" | "code" | "both";
|
|
547
|
+
|
|
548
|
+
export async function rewind(sessionId: string, projectPathStr: string, userUuid: string, mode: RewindMode, dryRun: boolean) {
|
|
549
|
+
const live = await sessions.getOrCreate(sessionId, projectPathStr);
|
|
550
|
+
const { before, text } = await recordBefore(sessionId, projectPathStr, userUuid);
|
|
551
|
+
const result: { text: string; rewindId?: string; files?: Awaited<ReturnType<LiveSession["rewindFiles"]>>; newSessionId?: string } = { text };
|
|
552
|
+
if (mode !== "conversation") result.files = await live.rewindFiles(userUuid, dryRun);
|
|
553
|
+
if (dryRun || mode === "code") return result;
|
|
554
|
+
if (before) {
|
|
555
|
+
result.rewindId = await live.rewindConversation(projectPathStr, before, text);
|
|
556
|
+
} else {
|
|
557
|
+
// Rewinding to the very first message: nothing to keep, so continue in a fresh session (the old one stays).
|
|
558
|
+
result.newSessionId = createDraftSession(projectPathStr);
|
|
559
|
+
result.rewindId = crypto.randomUUID();
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function pendingRewindText(sessionId: string): string | undefined {
|
|
565
|
+
const row = viewRow(sessionId);
|
|
566
|
+
return row?.rewind_anchor ? (row.rewind_text ?? "") : undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function pendingRewindDraft(sessionId: string): { rewindText: string | null; rewindId: string | null } {
|
|
570
|
+
const row = db.prepare("SELECT rewind_anchor, rewind_text, rewind_id FROM session_view WHERE session_id = ?").get(sessionId) as { rewind_anchor: string | null; rewind_text: string | null; rewind_id: string | null } | undefined;
|
|
571
|
+
return row?.rewind_anchor ? { rewindText: row.rewind_text ?? "", rewindId: row.rewind_id ?? row.rewind_anchor } : { rewindText: null, rewindId: null };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function exportSession(sessionId: string, projectPathStr: string) {
|
|
575
|
+
return (await sessions.getOrCreate(sessionId, projectPathStr)).exportText();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export type OverviewItem = {
|
|
579
|
+
projectId: string;
|
|
580
|
+
projectName: string;
|
|
581
|
+
sessionId: string;
|
|
582
|
+
title: string;
|
|
583
|
+
state: SessionState;
|
|
584
|
+
waitingFor: ("ask" | "plan")[];
|
|
585
|
+
crons: number;
|
|
586
|
+
/** Task IDs shown for this session; the overview total dedupes shared project tasks. */
|
|
587
|
+
cronIds: string[];
|
|
588
|
+
tasks: number;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
/** The "running overview": every session that is busy, waiting for the user, or has scheduled work. */
|
|
592
|
+
export async function overview(): Promise<OverviewItem[]> {
|
|
593
|
+
const out: OverviewItem[] = [];
|
|
594
|
+
const managed = new Set((db.prepare("SELECT path FROM projects").all() as { path: string }[]).map((row) => row.path));
|
|
595
|
+
for (const s of sessions.active()) {
|
|
596
|
+
const project = sessions.projectOf.get(s.sessionId) ?? s.cwd;
|
|
597
|
+
if (!managed.has(project)) continue;
|
|
598
|
+
const projectId = encodeProjectId(project);
|
|
599
|
+
const snap = s.snapshot();
|
|
600
|
+
const info = await getSessionInfo(s.sessionId, { dir: s.cwd }).catch(() => undefined);
|
|
601
|
+
out.push({
|
|
602
|
+
projectId,
|
|
603
|
+
projectName: path.basename(project),
|
|
604
|
+
sessionId: s.sessionId,
|
|
605
|
+
title: info?.customTitle ?? info?.summary ?? "新会话",
|
|
606
|
+
state: snap.state,
|
|
607
|
+
waitingFor: snap.pending.map((p) => p.kind),
|
|
608
|
+
crons: snap.crons.length,
|
|
609
|
+
cronIds: snap.crons.map((c) => `${projectId}:${c.id}`),
|
|
610
|
+
tasks: snap.tasks.filter((t) => t.status === "running").length,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const rank = (o: OverviewItem) => (o.waitingFor.length || o.state === "requires_action" ? 0 : o.state === "running" ? 1 : 2);
|
|
614
|
+
return out.sort((a, b) => rank(a) - rank(b));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** /cd: the session continues in another directory, which becomes (or already is) a project. */
|
|
618
|
+
export async function changeSessionDir(sessionId: string, projectPathStr: string, dir: string) {
|
|
619
|
+
const target = (await addProject(dir)).path;
|
|
620
|
+
const live = await sessions.getOrCreate(sessionId, projectPathStr);
|
|
621
|
+
await live.changeDir(target, () => {
|
|
622
|
+
db.exec("BEGIN");
|
|
623
|
+
try {
|
|
624
|
+
setSessionCwd(sessionId, target);
|
|
625
|
+
db.prepare("INSERT INTO session_view (session_id, project_path) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET project_path = excluded.project_path").run(sessionId, target);
|
|
626
|
+
db.prepare("UPDATE scheduled_sessions SET project_path = ? WHERE session_id = ?").run(target, sessionId);
|
|
627
|
+
db.prepare("UPDATE idecron_schedules SET project_path = ? WHERE session_id = ?").run(target, sessionId);
|
|
628
|
+
db.exec("COMMIT");
|
|
629
|
+
} catch (err) {
|
|
630
|
+
db.exec("ROLLBACK");
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
633
|
+
sessions.projectOf.set(sessionId, target);
|
|
634
|
+
});
|
|
635
|
+
sessions.notifyChanged(projectPathStr);
|
|
636
|
+
sessions.notifyChanged(target);
|
|
637
|
+
return { projectId: encodeProjectId(target) };
|
|
638
|
+
}
|