@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,253 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { historyPage, historyMessage } from "../../server/history-pages.ts";
|
|
7
|
+
import type { SdkMessageLike } from "@shared/protocol.ts";
|
|
8
|
+
import { buildItems } from "./transcript.ts";
|
|
9
|
+
import { MESSAGE_LIMIT } from "@shared/message-window.ts";
|
|
10
|
+
|
|
11
|
+
(globalThis as any).document = { querySelector: () => null };
|
|
12
|
+
const { useLive, hydrateSessionMessage, loadSessionHistory } = await import("./live.ts");
|
|
13
|
+
const snapshot = { state: "idle" as const, pending: [], planMode: false, crons: [], tasks: [] };
|
|
14
|
+
const structured = { structuredPatch: [{ oldStart: 80, newStart: 80, lines: [" context", "-old", "+new"] }] };
|
|
15
|
+
const call = (): SdkMessageLike => ({ type: "assistant", uuid: "call", parent_tool_use_id: "agent-tool", message: { content: [
|
|
16
|
+
{ type: "tool_use", id: "edit", name: "Edit", input: { file_path: "/a", old_string: "old", new_string: "new" } },
|
|
17
|
+
] }, linkedToolResults: { edit: { content: "deferred", isError: false, deferredUuid: "result" } } });
|
|
18
|
+
const result = () => ({ type: "user", uuid: "result", parent_tool_use_id: null,
|
|
19
|
+
message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "edited" }] }, tool_use_result: structured });
|
|
20
|
+
const child = (): SdkMessageLike => ({ type: "assistant", uuid: "child", parent_tool_use_id: "agent-tool", _streamBlocks: [0],
|
|
21
|
+
_deferred: { bytes: 300000 }, message: { content: [{ type: "text", text: "preview" }] } });
|
|
22
|
+
const fullChild = () => ({ type: "assistant", uuid: "child", parent_tool_use_id: null, message: { content: [{ type: "text", text: "full child answer" }] } });
|
|
23
|
+
function seed(items: SdkMessageLike[], events: SdkMessageLike[] = []) {
|
|
24
|
+
useLive.setState({ sessions: { s: { epoch: "e", seq: 1, resetCount: 1, snapshot, events: events.map((m) => ({ k: "sdk", m })),
|
|
25
|
+
history: { source: "agent", mode: "history", generation: "g", request: 1, older: "yes", items } } } });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("linked-only hydration admits its owner and projects complete structured Edit results", async () => {
|
|
29
|
+
const original = globalThis.fetch;
|
|
30
|
+
globalThis.fetch = async () => Response.json(result());
|
|
31
|
+
try {
|
|
32
|
+
seed([call()]);
|
|
33
|
+
await hydrateSessionMessage("s", "p", "result", "agent");
|
|
34
|
+
const current = useLive.getState().sessions.s!;
|
|
35
|
+
const tool = buildItems(current.history!.items, [], { parent: "agent-tool" }).find((item) => item.kind === "tool");
|
|
36
|
+
assert.equal(tool?.kind, "tool");
|
|
37
|
+
if (tool?.kind !== "tool") throw new Error("missing tool");
|
|
38
|
+
assert.deepEqual(tool.result, { content: "edited", isError: false, structured });
|
|
39
|
+
assert.deepEqual(current.history!.items[0]!._hydratedMessageUuids, ["result"]);
|
|
40
|
+
} finally { globalThis.fetch = original; }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("REST body hydration retains live child envelope and stream settlement metadata", async () => {
|
|
44
|
+
const original = globalThis.fetch;
|
|
45
|
+
globalThis.fetch = async () => Response.json(fullChild());
|
|
46
|
+
try {
|
|
47
|
+
seed([], [child()]);
|
|
48
|
+
await hydrateSessionMessage("s", "p", "child", "agent");
|
|
49
|
+
const current = useLive.getState().sessions.s!;
|
|
50
|
+
const event = current.events[0]!;
|
|
51
|
+
assert.equal(event.k, "sdk");
|
|
52
|
+
if (event.k !== "sdk") throw new Error("missing event");
|
|
53
|
+
assert.equal(event.m.parent_tool_use_id, "agent-tool");
|
|
54
|
+
assert.deepEqual(event.m._streamBlocks, [0]);
|
|
55
|
+
assert.equal(event.m._deferred, undefined);
|
|
56
|
+
assert.ok(buildItems(undefined, current.events, { parent: "agent-tool" }).some((item) => item.kind === "text" && item.text === "full child answer"));
|
|
57
|
+
assert.equal(buildItems(undefined, current.events).length, 0);
|
|
58
|
+
} finally { globalThis.fetch = original; }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("pending older page adopts hydrated linked/child bodies and protects them through trimming", async () => {
|
|
62
|
+
const original = globalThis.fetch;
|
|
63
|
+
let resolve!: (response: Response) => void;
|
|
64
|
+
globalThis.fetch = async (input) => {
|
|
65
|
+
const url = String(input);
|
|
66
|
+
if (url.includes("/messages/result")) return Response.json(result());
|
|
67
|
+
if (url.includes("/messages/child")) return Response.json(fullChild());
|
|
68
|
+
return new Promise((r) => { resolve = r; });
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
seed([call(), child()], [child()]);
|
|
72
|
+
const pending = loadSessionHistory("s", "p", "older", "agent");
|
|
73
|
+
await hydrateSessionMessage("s", "p", "result", "agent");
|
|
74
|
+
await hydrateSessionMessage("s", "p", "child", "agent");
|
|
75
|
+
// The response overlaps both frozen records with stale previews and adds enough
|
|
76
|
+
// older records that ordinary backwards trimming would remove the hydrated tail.
|
|
77
|
+
resolve(Response.json({ generation: "g", items: [
|
|
78
|
+
...Array.from({ length: MESSAGE_LIMIT }, (_, i) => ({ type: "user", uuid: `old-${i}`, message: { content: `older ${i}` } })), call(), child(),
|
|
79
|
+
] }));
|
|
80
|
+
await pending;
|
|
81
|
+
const current = useLive.getState().sessions.s!;
|
|
82
|
+
assert.equal(current.history?.error, undefined);
|
|
83
|
+
assert.ok(current.history!.items.length + current.events.length <= MESSAGE_LIMIT);
|
|
84
|
+
assert.equal(current.evicted, true);
|
|
85
|
+
assert.deepEqual(current.history!.items.slice(-2).map((m) => m.uuid), ["call", "child"]);
|
|
86
|
+
const items = buildItems(current.history!.items, current.events, { parent: "agent-tool" });
|
|
87
|
+
const tool = items.find((item) => item.kind === "tool");
|
|
88
|
+
assert.equal(tool?.kind, "tool");
|
|
89
|
+
if (tool?.kind !== "tool") throw new Error("missing tool");
|
|
90
|
+
assert.deepEqual(tool.result, { content: "edited", isError: false, structured });
|
|
91
|
+
assert.ok(items.some((item) => item.kind === "text" && item.text === "full child answer" && !item.deferred));
|
|
92
|
+
const event = current.events.find((event) => event.k === "sdk" && event.m.uuid === "child");
|
|
93
|
+
assert.ok(event?.k === "sdk" && event.m.parent_tool_use_id === "agent-tool");
|
|
94
|
+
} finally { globalThis.fetch = original; }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("unretainable linked hydration rejects without mutating the previous cached preview", async () => {
|
|
98
|
+
const original = globalThis.fetch;
|
|
99
|
+
globalThis.fetch = async () => Response.json({ ...result(), message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "x".repeat(7 * 1024 * 1024) }] } });
|
|
100
|
+
try {
|
|
101
|
+
const owner = call();
|
|
102
|
+
owner.message = { content: "x".repeat(14 * 1024 * 1024) };
|
|
103
|
+
owner._hydrated = true;
|
|
104
|
+
seed([owner]);
|
|
105
|
+
const before = useLive.getState().sessions.s;
|
|
106
|
+
await assert.rejects(hydrateSessionMessage("s", "p", "result", "agent"), /cannot fit/);
|
|
107
|
+
assert.equal(useLive.getState().sessions.s, before);
|
|
108
|
+
} finally { globalThis.fetch = original; }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("hydration rejected after branch change or missing target cannot report success", async () => {
|
|
112
|
+
const original = globalThis.fetch;
|
|
113
|
+
let resolve!: (response: Response) => void;
|
|
114
|
+
globalThis.fetch = () => new Promise((r) => { resolve = r; });
|
|
115
|
+
try {
|
|
116
|
+
seed([child()]);
|
|
117
|
+
const pending = hydrateSessionMessage("s", "p", "child", "agent");
|
|
118
|
+
useLive.setState((state) => ({ sessions: { s: { ...state.sessions.s!, historyRevision: "new-branch" } } }));
|
|
119
|
+
resolve(Response.json(fullChild()));
|
|
120
|
+
await assert.rejects(pending, /会话已更新/);
|
|
121
|
+
seed([]);
|
|
122
|
+
globalThis.fetch = async () => Response.json(fullChild());
|
|
123
|
+
await assert.rejects(hydrateSessionMessage("s", "p", "child", "agent"), /cannot fit/);
|
|
124
|
+
} finally { globalThis.fetch = original; }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("paging introduces standalone previews without losing hydrated results in main or agent views", async () => {
|
|
128
|
+
const original = globalThis.fetch;
|
|
129
|
+
try {
|
|
130
|
+
for (const agentId of [undefined, "agent"]) {
|
|
131
|
+
const source = agentId ?? "main";
|
|
132
|
+
const owner = { ...call(), parent_tool_use_id: null };
|
|
133
|
+
useLive.setState({ sessions: { s: { epoch: "e", seq: 1, resetCount: 1, snapshot, events: [],
|
|
134
|
+
history: { source, mode: "history", generation: "g", request: 1, older: "yes", items: [owner] } } } });
|
|
135
|
+
globalThis.fetch = async (input) => String(input).includes("/messages/result") ? Response.json(result())
|
|
136
|
+
: Response.json({ generation: "g", items: [owner, { ...result(), _deferred: { bytes: 300000 },
|
|
137
|
+
message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "short" }] } }] });
|
|
138
|
+
await hydrateSessionMessage("s", "p", "result", agentId);
|
|
139
|
+
await loadSessionHistory("s", "p", "latest", agentId);
|
|
140
|
+
const current = useLive.getState().sessions.s!;
|
|
141
|
+
const visible = buildItems(current.history!.items, [], agentId ? { parent: "agent-tool" } : {});
|
|
142
|
+
const tool = visible.find((item) => item.kind === "tool");
|
|
143
|
+
assert.equal(tool?.kind, "tool");
|
|
144
|
+
if (tool?.kind !== "tool") throw new Error("missing tool");
|
|
145
|
+
assert.deepEqual(tool.result, { content: "edited", isError: false, structured });
|
|
146
|
+
const standalone = current.history!.items.find((m) => m.uuid === "result")!;
|
|
147
|
+
assert.equal((standalone.linkedToolResults as Record<string, { content: unknown }>).edit!.content, "edited");
|
|
148
|
+
}
|
|
149
|
+
} finally { globalThis.fetch = original; }
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("authoritative branch replacement never adopts hydrated results from the previous view", async () => {
|
|
153
|
+
const original = globalThis.fetch;
|
|
154
|
+
try {
|
|
155
|
+
seed([call()]);
|
|
156
|
+
globalThis.fetch = async () => Response.json(result());
|
|
157
|
+
await hydrateSessionMessage("s", "p", "result", "agent");
|
|
158
|
+
useLive.setState((state) => ({ sessions: { s: { ...state.sessions.s!, historyRevision: "old" } } }));
|
|
159
|
+
globalThis.fetch = async () => Response.json({ generation: "new", historyRevision: "new", items: [
|
|
160
|
+
call(), { ...result(), _deferred: { bytes: 300000 }, message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "new branch preview" }] } },
|
|
161
|
+
] });
|
|
162
|
+
await loadSessionHistory("s", "p", "latest", "agent");
|
|
163
|
+
const current = useLive.getState().sessions.s!;
|
|
164
|
+
assert.equal(current.historyRevision, "new");
|
|
165
|
+
const tool = buildItems(current.history!.items, [], { parent: "agent-tool" }).find((item) => item.kind === "tool");
|
|
166
|
+
assert.equal(tool?.kind === "tool" && tool.result?.content, "new branch preview");
|
|
167
|
+
assert.equal(current.history!.items.some((m) => m._hydrated), false);
|
|
168
|
+
} finally { globalThis.fetch = original; }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("result hydration survives paging away its owner and returning to the tool call", async () => {
|
|
172
|
+
const original = globalThis.fetch;
|
|
173
|
+
try {
|
|
174
|
+
seed([call()]);
|
|
175
|
+
globalThis.fetch = async () => Response.json(result());
|
|
176
|
+
await hydrateSessionMessage("s", "p", "result", "agent");
|
|
177
|
+
globalThis.fetch = async () => Response.json({ generation: "g", older: "yes", items: [
|
|
178
|
+
{ ...result(), _deferred: { bytes: 300000 }, message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "short" }] } },
|
|
179
|
+
] });
|
|
180
|
+
await loadSessionHistory("s", "p", "latest", "agent");
|
|
181
|
+
let current = useLive.getState().sessions.s!;
|
|
182
|
+
assert.deepEqual(current.history!.items.map((m) => m.uuid), ["result"]);
|
|
183
|
+
assert.equal((current.history!.items[0]!.linkedToolResults as Record<string, { content: unknown }>).edit!.content, "edited");
|
|
184
|
+
globalThis.fetch = async () => Response.json({ generation: "g", items: [call()] });
|
|
185
|
+
await loadSessionHistory("s", "p", "older", "agent");
|
|
186
|
+
current = useLive.getState().sessions.s!;
|
|
187
|
+
const tool = buildItems(current.history!.items, [], { parent: "agent-tool" }).find((item) => item.kind === "tool");
|
|
188
|
+
assert.equal(tool?.kind === "tool" && tool.result?.content, "edited");
|
|
189
|
+
} finally { globalThis.fetch = original; }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("three page transitions keep provenance when a previously deferred linked result fits normally", async () => {
|
|
193
|
+
const original = globalThis.fetch;
|
|
194
|
+
try {
|
|
195
|
+
seed([call()]);
|
|
196
|
+
globalThis.fetch = async () => Response.json(result());
|
|
197
|
+
await hydrateSessionMessage("s", "p", "result", "agent");
|
|
198
|
+
const completeCall = { ...call(), linkedToolResults: { edit: { content: "edited", isError: false, structured } }, _toolResultUuids: { edit: "result" } };
|
|
199
|
+
globalThis.fetch = async () => Response.json({ generation: "g", items: [completeCall] });
|
|
200
|
+
await loadSessionHistory("s", "p", "latest", "agent");
|
|
201
|
+
assert.deepEqual(useLive.getState().sessions.s!.history!.items[0]!._hydratedToolResultUuids, { edit: "result" });
|
|
202
|
+
globalThis.fetch = async () => Response.json({ generation: "g", items: [call(),
|
|
203
|
+
{ ...result(), _deferred: { bytes: 300000 }, message: { content: [{ type: "tool_result", tool_use_id: "edit", content: "short" }] } },
|
|
204
|
+
] });
|
|
205
|
+
await loadSessionHistory("s", "p", "latest", "agent");
|
|
206
|
+
const current = useLive.getState().sessions.s!;
|
|
207
|
+
const tool = buildItems(current.history!.items, [], { parent: "agent-tool" }).find((item) => item.kind === "tool");
|
|
208
|
+
assert.equal(tool?.kind === "tool" && tool.result?.content, "edited");
|
|
209
|
+
} finally { globalThis.fetch = original; }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
test("real server preview truncation preserves every hydrated result when its owner leaves and returns", async (t) => {
|
|
214
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cide-result-blocks-"));
|
|
215
|
+
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
|
216
|
+
const file = path.join(directory, "session.jsonl");
|
|
217
|
+
const text = "complete ".repeat(2000);
|
|
218
|
+
const ids = Array.from({ length: 21 }, (_, index) => `tool-${index}`);
|
|
219
|
+
const records: Record<string, unknown>[] = [{ type: "assistant", uuid: "owner", parentUuid: null,
|
|
220
|
+
message: { role: "assistant", content: ids.map((id) => ({ type: "tool_use", id, name: "Read", input: { file_path: `/${id}` } })) } }];
|
|
221
|
+
for (let index = 0; index < 110; index++) records.push({ type: "user", uuid: `padding-${index}`, parentUuid: index ? `padding-${index - 1}` : "owner",
|
|
222
|
+
message: { role: "user", content: `padding ${index}` } });
|
|
223
|
+
records.push({ type: "user", uuid: "result", parentUuid: "padding-109",
|
|
224
|
+
message: { role: "user", content: ids.map((tool_use_id) => ({ type: "tool_result", tool_use_id, content: text })) } });
|
|
225
|
+
await fs.writeFile(file, records.map((record) => JSON.stringify(record)).join("\n") + "\n");
|
|
226
|
+
const initial = await historyPage(file, { before: "padding-0" });
|
|
227
|
+
useLive.setState({ sessions: { s: { epoch: "e", seq: 1, resetCount: 1, snapshot, events: [],
|
|
228
|
+
history: { ...initial, source: "main", mode: "history", request: 1 } } } });
|
|
229
|
+
const original = globalThis.fetch;
|
|
230
|
+
globalThis.fetch = async (input) => {
|
|
231
|
+
const url = new URL(String(input), "http://fixture.invalid");
|
|
232
|
+
if (url.pathname.endsWith("/messages/result")) return Response.json(await historyMessage(file, "result"));
|
|
233
|
+
return Response.json(await historyPage(file, { before: url.searchParams.get("before") ?? undefined }));
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
await hydrateSessionMessage("s", "p", "result");
|
|
237
|
+
let current = useLive.getState().sessions.s!;
|
|
238
|
+
assert.equal(buildItems(current.history!.items, []).filter((item) => item.kind === "tool" && item.result?.content === text).length, ids.length);
|
|
239
|
+
await loadSessionHistory("s", "p", "latest");
|
|
240
|
+
current = useLive.getState().sessions.s!;
|
|
241
|
+
assert.equal(current.history!.items.some((message) => message.uuid === "owner"), false);
|
|
242
|
+
const standalone = current.history!.items.find((message) => message.uuid === "result")!;
|
|
243
|
+
assert.ok(standalone._deferred, "other preview content remains deferred");
|
|
244
|
+
const preview = (standalone.message as { content: { type?: string }[] }).content;
|
|
245
|
+
assert.ok(preview.filter((block) => block?.type === "tool_result").length < ids.length, "fixture must really truncate whole blocks");
|
|
246
|
+
assert.equal(Object.keys(standalone.linkedToolResults as object).length, ids.length);
|
|
247
|
+
await loadSessionHistory("s", "p", "older");
|
|
248
|
+
current = useLive.getState().sessions.s!;
|
|
249
|
+
const tools = buildItems(current.history!.items, []).filter((item) => item.kind === "tool");
|
|
250
|
+
assert.equal(tools.length, ids.length);
|
|
251
|
+
assert.ok(tools.every((item) => item.result?.content === text && !item.result.deferredUuid));
|
|
252
|
+
} finally { globalThis.fetch = original; }
|
|
253
|
+
});
|
package/web/src/live.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { ClientMsg, HistoryPage, LiveEvent, LiveSnapshot, SdkMessageLike, ServerMsg } from "@shared/protocol.ts";
|
|
3
|
+
import { buildItems, deriveTodos } from "./transcript.ts";
|
|
4
|
+
import { api } from "./api.ts";
|
|
5
|
+
import { invalidateAuth, onAuthInvalidated } from "./auth-events.ts";
|
|
6
|
+
import { boundSession, boundSessions, browsingBase, compactEvents, edgeMessage, mergePage } from "./message-cache.ts";
|
|
7
|
+
import { SESSION_BYTES, contentBytes, displayEvent } from "@shared/message-window.ts";
|
|
8
|
+
import { queryClient } from "./queryClient.ts";
|
|
9
|
+
import { mergeSnapshot, updateTodosFromEvents, type SessionLive } from "./live-merge.ts";
|
|
10
|
+
import { hydrateMessageBody, preserveHydratedBodies } from "./message-bodies.ts";
|
|
11
|
+
import { mergeAgentOwners } from "./agent-owners.ts";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const emptySnapshot: LiveSnapshot = { state: "idle", pending: [], planMode: false, crons: [], tasks: [] };
|
|
15
|
+
|
|
16
|
+
type LiveStore = {
|
|
17
|
+
connected: boolean;
|
|
18
|
+
distVersion?: string;
|
|
19
|
+
sessions: Record<string, SessionLive>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const useLive = create<LiveStore>(() => ({ connected: false, sessions: {} }));
|
|
23
|
+
export const loadedBuildVersion = document.querySelector<HTMLMetaElement>('meta[name="cide-build"]')?.content;
|
|
24
|
+
|
|
25
|
+
export function useSessionLive(sessionId: string | undefined): SessionLive | undefined {
|
|
26
|
+
return useLive((s) => (sessionId ? s.sessions[sessionId] : undefined));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Apply one event to the snapshot so UI state (status, pending…) is derived in one place. */
|
|
30
|
+
function applyToSnapshot(snap: LiveSnapshot, ev: LiveEvent): LiveSnapshot {
|
|
31
|
+
switch (ev.k) {
|
|
32
|
+
case "state":
|
|
33
|
+
return { ...snap, state: ev.state };
|
|
34
|
+
case "pending":
|
|
35
|
+
return { ...snap, pending: [...snap.pending.filter((p) => p.id !== ev.req.id), ev.req] };
|
|
36
|
+
case "resolved":
|
|
37
|
+
return { ...snap, pending: snap.pending.filter((p) => p.id !== ev.id) };
|
|
38
|
+
case "crons":
|
|
39
|
+
return { ...snap, crons: ev.crons, cronsStale: false };
|
|
40
|
+
case "tasks":
|
|
41
|
+
return { ...snap, tasks: ev.tasks };
|
|
42
|
+
case "agent_tasks":
|
|
43
|
+
return { ...snap, agentTasks: ev.tasks };
|
|
44
|
+
case "settings":
|
|
45
|
+
return { ...snap, model: ev.model, effort: ev.effort, planMode: ev.planMode, askQuestionsEnabled: ev.askQuestionsEnabled ?? snap.askQuestionsEnabled, writeEnabled: ev.writeEnabled ?? snap.writeEnabled };
|
|
46
|
+
case "sdk":
|
|
47
|
+
if (ev.m.type === "result" && typeof ev.m.total_cost_usd === "number") return { ...snap, costUsd: ev.m.total_cost_usd };
|
|
48
|
+
return snap;
|
|
49
|
+
default:
|
|
50
|
+
return snap;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------- connection ----------
|
|
55
|
+
|
|
56
|
+
type Sub = { projectId: string };
|
|
57
|
+
|
|
58
|
+
let ws: WebSocket | undefined;
|
|
59
|
+
let retry = 0;
|
|
60
|
+
let authBlocked = false;
|
|
61
|
+
const subs = new Map<string, Sub>();
|
|
62
|
+
const outbox: ClientMsg[] = [];
|
|
63
|
+
|
|
64
|
+
function wsUrl() {
|
|
65
|
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
66
|
+
return `${proto}//${location.host}/ws`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rawSend(msg: ClientMsg) {
|
|
70
|
+
if (authBlocked) return;
|
|
71
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
72
|
+
// Subscriptions are re-sent from `subs` on (re)connect; queueing them would subscribe twice.
|
|
73
|
+
else if (msg.t !== "sub" && msg.t !== "unsub") outbox.push(msg);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function subscribeMsg(sessionId: string, sub: Sub): ClientMsg {
|
|
77
|
+
flushLive();
|
|
78
|
+
const cur = useLive.getState().sessions[sessionId];
|
|
79
|
+
return { t: "sub", sessionId, projectId: sub.projectId, epoch: cur?.epoch, seq: cur?.seq, bounded: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
83
|
+
|
|
84
|
+
const visible = () => document.visibilityState === "visible";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Open the socket unless one is already open or opening. Exactly one reconnect timer exists,
|
|
88
|
+
* and callbacks of a superseded socket are ignored, so a phone waking up while a retry is
|
|
89
|
+
* pending never ends up with two sockets or a "disconnected" badge on a live one.
|
|
90
|
+
*/
|
|
91
|
+
function connect() {
|
|
92
|
+
if (authBlocked) return;
|
|
93
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
94
|
+
retryTimer = undefined;
|
|
95
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
96
|
+
const sock = new WebSocket(wsUrl());
|
|
97
|
+
ws = sock;
|
|
98
|
+
sock.onopen = () => {
|
|
99
|
+
if (ws !== sock) return;
|
|
100
|
+
retry = 0;
|
|
101
|
+
useLive.setState({ connected: true });
|
|
102
|
+
if (loadedBuildVersion) sock.send(JSON.stringify({ t: "clientVersion", version: loadedBuildVersion } satisfies ClientMsg));
|
|
103
|
+
sock.send(JSON.stringify({ t: "visibility", visible: visible() } satisfies ClientMsg));
|
|
104
|
+
for (const [id, sub] of subs) sock.send(JSON.stringify(subscribeMsg(id, sub)));
|
|
105
|
+
for (const m of outbox.splice(0)) sock.send(JSON.stringify(m));
|
|
106
|
+
};
|
|
107
|
+
sock.onmessage = (e) => {
|
|
108
|
+
if (ws === sock) onServerMsg(JSON.parse(e.data) as ServerMsg);
|
|
109
|
+
};
|
|
110
|
+
sock.onclose = (e) => {
|
|
111
|
+
if (ws !== sock) return;
|
|
112
|
+
useLive.setState({ connected: false });
|
|
113
|
+
ws = undefined;
|
|
114
|
+
if (e.code === 4401) {
|
|
115
|
+
invalidateAuth();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const delay = Math.min(10_000, 500 * 2 ** retry++);
|
|
119
|
+
retryTimer = setTimeout(connect, delay);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onServerMsg(msg: ServerMsg) {
|
|
124
|
+
if (msg.t === "version") {
|
|
125
|
+
useLive.setState({ distVersion: msg.version });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (msg.t === "pong") {
|
|
129
|
+
if (pongTimer) clearTimeout(pongTimer);
|
|
130
|
+
pongTimer = undefined;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (msg.t === "sessions") {
|
|
134
|
+
queryClient.invalidateQueries({ queryKey: ["sessions", msg.projectId] });
|
|
135
|
+
queryClient.invalidateQueries({ queryKey: ["overview"] });
|
|
136
|
+
queryClient.invalidateQueries({ queryKey: ["btw", msg.projectId] });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (msg.t === "activity") {
|
|
140
|
+
for (const fn of activityListeners) fn(msg);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (msg.t === "error") {
|
|
144
|
+
console.warn("server error:", msg.message);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (msg.t === "ev") {
|
|
148
|
+
queue(msg.sessionId, msg.seq, msg.ev);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// A snapshot follows our own `sub`: commit what is queued first so the order stays intact.
|
|
152
|
+
flushLive();
|
|
153
|
+
useLive.setState((st) => {
|
|
154
|
+
const cur = st.sessions[msg.sessionId];
|
|
155
|
+
const next = mergeSnapshot(cur, msg);
|
|
156
|
+
return { sessions: boundSessions({ ...st.sessions, [msg.sessionId]: next }, new Set(subs.keys())) };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------- batched commits ----------
|
|
161
|
+
//
|
|
162
|
+
// Live events are not committed to the store one by one: every commit re-renders the
|
|
163
|
+
// transcript. Streaming text (the server already merges deltas every ~50ms) is committed
|
|
164
|
+
// at most every 100–400ms, slower when the last commit took long to render (slow phones,
|
|
165
|
+
// long sessions); other events go out within a frame. See NOTES.md "性能".
|
|
166
|
+
|
|
167
|
+
const queued = new Map<string, { seq: number; ev: LiveEvent }[]>();
|
|
168
|
+
let flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
169
|
+
let flushDue = Infinity;
|
|
170
|
+
let lastCommitCost = 0;
|
|
171
|
+
|
|
172
|
+
function queue(sessionId: string, seq: number, ev: LiveEvent) {
|
|
173
|
+
let q = queued.get(sessionId);
|
|
174
|
+
if (!q) queued.set(sessionId, (q = []));
|
|
175
|
+
q.push({ seq, ev });
|
|
176
|
+
schedule(ev.k === "delta" ? Math.min(400, Math.max(100, lastCommitCost * 4)) : 16);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function schedule(delay: number) {
|
|
180
|
+
const due = performance.now() + delay;
|
|
181
|
+
if (flushTimer !== undefined && flushDue <= due) return;
|
|
182
|
+
clearTimeout(flushTimer);
|
|
183
|
+
flushDue = due;
|
|
184
|
+
flushTimer = setTimeout(flushLive, delay);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sameDraft = (a: LiveEvent | undefined, b: Extract<LiveEvent, { k: "delta" }>): a is Extract<LiveEvent, { k: "delta" }> =>
|
|
188
|
+
a?.k === "delta" && a.msgId === b.msgId && a.index === b.index && a.kind === b.kind && a.parent === b.parent;
|
|
189
|
+
|
|
190
|
+
/** Commit queued events to the store (merging consecutive text deltas of one block). */
|
|
191
|
+
function flushLive() {
|
|
192
|
+
clearTimeout(flushTimer);
|
|
193
|
+
flushTimer = undefined;
|
|
194
|
+
flushDue = Infinity;
|
|
195
|
+
if (queued.size === 0) return;
|
|
196
|
+
const batch = [...queued];
|
|
197
|
+
queued.clear();
|
|
198
|
+
const t0 = performance.now();
|
|
199
|
+
useLive.setState((st) => {
|
|
200
|
+
let sessions = st.sessions;
|
|
201
|
+
for (const [id, evs] of batch) {
|
|
202
|
+
const cur = sessions[id];
|
|
203
|
+
if (!cur) continue;
|
|
204
|
+
let { seq, snapshot, events, resetCount, historyRevision } = cur;
|
|
205
|
+
let agentOwners = cur.agentOwners;
|
|
206
|
+
const added: LiveEvent[] = [];
|
|
207
|
+
for (const { seq: s, ev: raw } of evs) {
|
|
208
|
+
const ev = displayEvent(raw);
|
|
209
|
+
if (s <= seq) continue;
|
|
210
|
+
seq = s;
|
|
211
|
+
if (ev.k === "reset") {
|
|
212
|
+
events = [];
|
|
213
|
+
added.length = 0;
|
|
214
|
+
resetCount++;
|
|
215
|
+
historyRevision = ev.historyRevision;
|
|
216
|
+
agentOwners = undefined;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
snapshot = applyToSnapshot(snapshot, ev);
|
|
220
|
+
const last = added[added.length - 1];
|
|
221
|
+
if (ev.k === "delta" && sameDraft(last, ev)) added[added.length - 1] = { ...last, text: last.text + ev.text };
|
|
222
|
+
else added.push(ev);
|
|
223
|
+
}
|
|
224
|
+
if (seq === cur.seq) continue;
|
|
225
|
+
const combined = added.length ? [...events, ...added] : events;
|
|
226
|
+
const compact = combined.length > 128 && (combined.length - events.length >= 128 || combined.length % 128 < added.length);
|
|
227
|
+
const reset = resetCount !== cur.resetCount;
|
|
228
|
+
const todos = updateTodosFromEvents(combined, added, reset ? undefined : cur.todos);
|
|
229
|
+
sessions = { ...sessions, [id]: boundSession({ ...cur, seq, snapshot, resetCount, historyRevision, todos,
|
|
230
|
+
agentOwners: mergeAgentOwners(agentOwners, added, snapshot.agentTasks),
|
|
231
|
+
history: reset ? undefined : cur.history, events: compact ? compactEvents(combined) : combined }) };
|
|
232
|
+
}
|
|
233
|
+
return sessions === st.sessions ? st : { sessions: boundSessions(sessions, new Set(subs.keys())) };
|
|
234
|
+
});
|
|
235
|
+
// Time until the frame showing this commit is out: the rendering cost of one commit.
|
|
236
|
+
requestAnimationFrame(() => setTimeout(() => (lastCommitCost = performance.now() - t0), 0));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type Activity = Extract<ServerMsg, { t: "activity" }>;
|
|
240
|
+
const activityListeners = new Set<(a: Activity) => void>();
|
|
241
|
+
|
|
242
|
+
/** Session finished / needs attention, from any project. */
|
|
243
|
+
export function onActivity(fn: (a: Activity) => void) {
|
|
244
|
+
activityListeners.add(fn);
|
|
245
|
+
return () => {
|
|
246
|
+
activityListeners.delete(fn);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let pongTimer: ReturnType<typeof setTimeout> | undefined;
|
|
251
|
+
let started = false;
|
|
252
|
+
|
|
253
|
+
onAuthInvalidated(() => {
|
|
254
|
+
if (authBlocked) return;
|
|
255
|
+
authBlocked = true;
|
|
256
|
+
outbox.length = 0;
|
|
257
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
258
|
+
retryTimer = undefined;
|
|
259
|
+
if (pongTimer) clearTimeout(pongTimer);
|
|
260
|
+
pongTimer = undefined;
|
|
261
|
+
// Detach first: a late close/open callback must not reconnect or flush old commands.
|
|
262
|
+
const sock = ws;
|
|
263
|
+
ws = undefined;
|
|
264
|
+
useLive.setState({ connected: false });
|
|
265
|
+
sock?.close();
|
|
266
|
+
queryClient.setQueryData(["auth"], { authed: false });
|
|
267
|
+
void queryClient.invalidateQueries({ queryKey: ["auth"] });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** A socket can look OPEN after a phone slept although the connection is gone: probe it. */
|
|
271
|
+
function probe() {
|
|
272
|
+
const sock = ws;
|
|
273
|
+
if (!sock || sock.readyState !== WebSocket.OPEN) return connect();
|
|
274
|
+
sock.send(JSON.stringify({ t: "ping" } satisfies ClientMsg));
|
|
275
|
+
if (pongTimer) clearTimeout(pongTimer);
|
|
276
|
+
pongTimer = setTimeout(() => {
|
|
277
|
+
if (ws !== sock) return;
|
|
278
|
+
// No answer: drop it (its onclose would come late or never) and reconnect now.
|
|
279
|
+
ws = undefined;
|
|
280
|
+
useLive.setState({ connected: false });
|
|
281
|
+
sock.close();
|
|
282
|
+
connect();
|
|
283
|
+
}, 3000);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function startLive() {
|
|
287
|
+
authBlocked = false;
|
|
288
|
+
if (!ws) connect();
|
|
289
|
+
if (started) return;
|
|
290
|
+
started = true;
|
|
291
|
+
document.addEventListener("visibilitychange", () => {
|
|
292
|
+
// Tell the server whether anyone is actually looking, so it pushes to background tabs.
|
|
293
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ t: "visibility", visible: visible() } satisfies ClientMsg));
|
|
294
|
+
// Phones drop sockets while locked; check the connection as soon as the page is back.
|
|
295
|
+
if (visible()) probe();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function subscribeSession(sessionId: string, projectId: string) {
|
|
300
|
+
subs.set(sessionId, { projectId });
|
|
301
|
+
useLive.setState((st) => {
|
|
302
|
+
const cur = st.sessions[sessionId];
|
|
303
|
+
return { sessions: boundSessions({ ...st.sessions, [sessionId]: { ...(cur ?? { seq: 0, snapshot: emptySnapshot, events: [], resetCount: 0 }), accessed: Date.now() } }, new Set(subs.keys())) };
|
|
304
|
+
});
|
|
305
|
+
rawSend(subscribeMsg(sessionId, { projectId }));
|
|
306
|
+
return () => {
|
|
307
|
+
subs.delete(sessionId);
|
|
308
|
+
rawSend({ t: "unsub", sessionId });
|
|
309
|
+
historyRequests.get(sessionId)?.abort();
|
|
310
|
+
useLive.setState((st) => ({ sessions: boundSessions(st.sessions, new Set(subs.keys())) }));
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function sendClient(msg: ClientMsg) {
|
|
315
|
+
// Settings changes show immediately; the server's `settings` event confirms them.
|
|
316
|
+
if (msg.t === "setModel" || msg.t === "setEffort" || msg.t === "setPlanMode") {
|
|
317
|
+
const patch = msg.t === "setModel" ? { model: msg.model } : msg.t === "setEffort" ? { effort: msg.effort } : { planMode: msg.on };
|
|
318
|
+
useLive.setState((st) => {
|
|
319
|
+
const cur = st.sessions[msg.sessionId];
|
|
320
|
+
if (!cur) return st;
|
|
321
|
+
return { sessions: { ...st.sessions, [msg.sessionId]: { ...cur, snapshot: { ...cur.snapshot, ...patch } } } };
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
rawSend(msg);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export { emptySnapshot };
|
|
328
|
+
|
|
329
|
+
// History bodies live in the same bounded session store as live replay, not in a second query cache.
|
|
330
|
+
const historyRequests = new Map<string, AbortController>();
|
|
331
|
+
let historyRequestId = 0;
|
|
332
|
+
export async function loadSessionHistory(sessionId: string, projectId: string, direction: "latest" | "older" | "newer" = "latest", agentId?: string) {
|
|
333
|
+
historyRequests.get(sessionId)?.abort();
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
historyRequests.set(sessionId, controller);
|
|
336
|
+
const request = ++historyRequestId;
|
|
337
|
+
const initial = useLive.getState().sessions[sessionId];
|
|
338
|
+
if (!initial) { historyRequests.delete(sessionId); return; }
|
|
339
|
+
const source = agentId ?? "main";
|
|
340
|
+
const base = `/api/projects/${projectId}/sessions/${sessionId}${agentId ? `/subagents/${encodeURIComponent(agentId)}` : ""}/messages`;
|
|
341
|
+
const baseHistory = direction !== "latest" ? browsingBase(initial) : undefined;
|
|
342
|
+
const initialBodies = new Set([...initial.events.flatMap((event) => event.k === "sdk" ? [event.m] : []), ...(initial.history?.items ?? [])]);
|
|
343
|
+
const resetCount = initial.resetCount;
|
|
344
|
+
const historyRevision = initial.historyRevision;
|
|
345
|
+
const edge = initial.history?.source === source ? edgeMessage(initial, direction === "older" ? "first" : "last") : undefined;
|
|
346
|
+
const params = new URLSearchParams({ page: "1" });
|
|
347
|
+
if (direction !== "latest" && edge) params.set(direction === "older" ? "before" : "after", edge);
|
|
348
|
+
useLive.setState((st) => {
|
|
349
|
+
const cur = st.sessions[sessionId];
|
|
350
|
+
if (!cur) return st;
|
|
351
|
+
return { sessions: { ...st.sessions, [sessionId]: { ...cur, history: { items: [], generation: "", mode: "latest", ...(cur.history?.source === source ? cur.history : {}), source, loading: true, error: undefined, request } } } };
|
|
352
|
+
});
|
|
353
|
+
try {
|
|
354
|
+
const page = await api<HistoryPage>(`${base}?${params}`, { signal: controller.signal });
|
|
355
|
+
useLive.setState((st) => {
|
|
356
|
+
const cur = st.sessions[sessionId];
|
|
357
|
+
if (!cur || cur.resetCount !== resetCount || cur.history?.request !== request) return st;
|
|
358
|
+
if (cur.historyRevision !== historyRevision && cur.historyRevision !== page.historyRevision) {
|
|
359
|
+
// The first socket may have established a newer branch while HTTP was in flight.
|
|
360
|
+
// Trigger the view's normal reload instead of leaving its loading flag stranded.
|
|
361
|
+
return { sessions: { ...st.sessions, [sessionId]: { ...cur, history: undefined, resetCount: cur.resetCount + 1 } } };
|
|
362
|
+
}
|
|
363
|
+
// With no intervening WS update, HTTP is authoritative too (e.g. server restart
|
|
364
|
+
// while WS remains unavailable). Adopt its new revision rather than retry forever.
|
|
365
|
+
const changedView = cur.historyRevision !== undefined && page.historyRevision !== undefined && cur.historyRevision !== page.historyRevision;
|
|
366
|
+
const events = changedView ? [] : cur.events;
|
|
367
|
+
const merged = mergePage(changedView ? undefined : baseHistory ?? cur.history, page, changedView ? "latest" : direction, request);
|
|
368
|
+
const currentBodies = !changedView && cur.history?.source === source ? [
|
|
369
|
+
...cur.events.flatMap((event) => event.k === "sdk" ? [event.m] : []), ...cur.history.items,
|
|
370
|
+
] : [];
|
|
371
|
+
const history = { ...merged, items: preserveHydratedBodies(merged.items, currentBodies), source };
|
|
372
|
+
const hydratedDuringRequest = currentBodies.filter((m) => m._hydrated && !initialBodies.has(m));
|
|
373
|
+
const protectedMessages = hydratedDuringRequest.flatMap((m) => [
|
|
374
|
+
...(m._hydratedMessageUuids as string[] ?? []), ...(!m._deferred && m.uuid ? [m.uuid] : []),
|
|
375
|
+
]);
|
|
376
|
+
const todos = source === "main" ? deriveTodos(buildItems(undefined, events), page.todos) : changedView ? undefined : cur.todos;
|
|
377
|
+
const next = boundSession({ ...cur, events, resetCount: cur.resetCount + (changedView ? 1 : 0), historyRevision: page.historyRevision ?? cur.historyRevision, todos, history }, { protectedMessages });
|
|
378
|
+
return { sessions: boundSessions({ ...st.sessions, [sessionId]: next }, new Set(subs.keys())) };
|
|
379
|
+
});
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (controller.signal.aborted) return;
|
|
382
|
+
useLive.setState((st) => {
|
|
383
|
+
const cur = st.sessions[sessionId];
|
|
384
|
+
if (!cur?.history || cur.history.request !== request) return st;
|
|
385
|
+
return { sessions: { ...st.sessions, [sessionId]: { ...cur, history: { ...cur.history, loading: false, error: (error as Error).message } } } };
|
|
386
|
+
});
|
|
387
|
+
} finally { if (historyRequests.get(sessionId) === controller) historyRequests.delete(sessionId); }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function hydrateSessionMessage(sessionId: string, projectId: string, uuid: string, agentId?: string) {
|
|
391
|
+
const start = useLive.getState().sessions[sessionId];
|
|
392
|
+
if (!start) throw new Error("消息所在的会话已不在缓存中,请重新加载");
|
|
393
|
+
const message = await api<SdkMessageLike>(`/api/projects/${projectId}/sessions/${sessionId}${agentId ? `/subagents/${encodeURIComponent(agentId)}` : ""}/messages/${encodeURIComponent(uuid)}`);
|
|
394
|
+
if (contentBytes(message) > SESSION_BYTES / 2) throw new Error("这条消息超过缓存上限,请导出会话查看");
|
|
395
|
+
message._hydrated = true;
|
|
396
|
+
useLive.setState((st) => {
|
|
397
|
+
const cur = st.sessions[sessionId];
|
|
398
|
+
if (!cur || cur.resetCount !== start.resetCount || cur.epoch !== start.epoch || cur.historyRevision !== start.historyRevision) throw new Error("会话已更新,请重新加载这条消息");
|
|
399
|
+
const hydrate = (m: SdkMessageLike) => hydrateMessageBody(m, message, uuid);
|
|
400
|
+
const history = cur.history ? { ...cur.history, items: cur.history.items.map(hydrate) } : undefined;
|
|
401
|
+
const events = cur.events.map((e) => e.k === "sdk" ? { ...e, m: hydrate(e.m) } : e);
|
|
402
|
+
return { sessions: boundSessions({ ...st.sessions, [sessionId]: boundSession({ ...cur, history, events }, { protectedMessage: uuid }) }, new Set([...subs.keys(), sessionId])) };
|
|
403
|
+
});
|
|
404
|
+
}
|