@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,279 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { clearDraft, editDraft, restoreRewindDraft, setDraft, useDrafts } from "./drafts.ts";
|
|
4
|
+
import type { AnnotationSource, DraftAnnotation } from "./annotations.ts";
|
|
5
|
+
|
|
6
|
+
type Annotations = typeof import("./annotations.ts");
|
|
7
|
+
const storageKey = "cide.annotations.v1";
|
|
8
|
+
let moduleId = 0;
|
|
9
|
+
function memoryStorage() {
|
|
10
|
+
const values = new Map<string, string>();
|
|
11
|
+
const failures: { read?: Error; write?: Error } = {};
|
|
12
|
+
return {
|
|
13
|
+
values,
|
|
14
|
+
failures,
|
|
15
|
+
getItem(key: string) {
|
|
16
|
+
if (failures.read) throw failures.read;
|
|
17
|
+
return values.get(key) ?? null;
|
|
18
|
+
},
|
|
19
|
+
setItem(key: string, value: string) {
|
|
20
|
+
if (failures.write) throw failures.write;
|
|
21
|
+
values.set(key, value);
|
|
22
|
+
},
|
|
23
|
+
removeItem(key: string) { values.delete(key); },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function installStorage(storage: ReturnType<typeof memoryStorage> | undefined) {
|
|
27
|
+
Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true });
|
|
28
|
+
}
|
|
29
|
+
async function reload(): Promise<Annotations> {
|
|
30
|
+
// A fresh module models a browser refresh without adding a product reset API.
|
|
31
|
+
return import(`./annotations.ts?annotations-test=${++moduleId}`);
|
|
32
|
+
}
|
|
33
|
+
function input(index = 0, source: AnnotationSource = { kind: "file", path: `src/file-${index}.ts`, startLine: 2, endLine: 4 }) {
|
|
34
|
+
return { projectId: "project-a", targetSessionId: "session-a", source, quote: `quoted text ${index}`, comment: `comment ${index}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("SSR import is safe and browser initialization hydrates once", async () => {
|
|
38
|
+
installStorage(undefined);
|
|
39
|
+
const annotations = await reload();
|
|
40
|
+
assert.deepEqual(annotations.useAnnotations.getState(), { items: [], mode: false, recentSessions: {} });
|
|
41
|
+
const storage = memoryStorage();
|
|
42
|
+
storage.values.set(storageKey, JSON.stringify({ version: 1, items: [], mode: true, recentSessions: { p: "s" } }));
|
|
43
|
+
installStorage(storage);
|
|
44
|
+
annotations.ensureAnnotationsLoaded();
|
|
45
|
+
assert.equal(annotations.useAnnotations.getState().mode, true);
|
|
46
|
+
assert.deepEqual(annotations.useAnnotations.getState().recentSessions, { p: "s" });
|
|
47
|
+
storage.values.set(storageKey, JSON.stringify({ version: 1, items: [], mode: false, recentSessions: {} }));
|
|
48
|
+
annotations.ensureAnnotationsLoaded();
|
|
49
|
+
assert.equal(annotations.useAnnotations.getState().mode, true, "repeated initialization cannot replace active state");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("navigation and module reload preserve every annotation, mode, and project target", async () => {
|
|
53
|
+
const storage = memoryStorage();
|
|
54
|
+
installStorage(storage);
|
|
55
|
+
const annotations = await reload();
|
|
56
|
+
for (let index = 0; index < 128; index++) {
|
|
57
|
+
const source: AnnotationSource = index % 2
|
|
58
|
+
? { kind: "message", sessionId: "source-session", itemKey: `item:${index}`, agentToolUseId: "agent-tool" }
|
|
59
|
+
: { kind: "file", path: `文件/${index}.tsx`, startLine: 1, endLine: 2 };
|
|
60
|
+
annotations.addAnnotation({ ...input(index, source), targetSessionId: index % 3 ? "session-a" : "session-b", quote: "第一行\n\nlast line\n" });
|
|
61
|
+
}
|
|
62
|
+
annotations.setAnnotationMode(true);
|
|
63
|
+
annotations.rememberAnnotationSession("project-a", "session-b");
|
|
64
|
+
annotations.rememberAnnotationSession("project-b", "session-c");
|
|
65
|
+
const before = annotations.useAnnotations.getState();
|
|
66
|
+
assert.equal(before.items.length, 128, "annotations have no cache-size eviction");
|
|
67
|
+
assert.equal(annotations.snapshotAnnotations("other-project", "session-a").length, 0);
|
|
68
|
+
assert.equal(annotations.snapshotAnnotations("project-a", "session-b").length, 43);
|
|
69
|
+
// Navigating away and back changes the visible slice, never the backing cards.
|
|
70
|
+
annotations.snapshotAnnotations("project-b", "session-c");
|
|
71
|
+
assert.equal(annotations.snapshotAnnotations("project-a", "session-a").length, 85);
|
|
72
|
+
const restored = await reload();
|
|
73
|
+
assert.deepEqual(restored.useAnnotations.getState().items, before.items);
|
|
74
|
+
assert.equal(restored.useAnnotations.getState().mode, true);
|
|
75
|
+
assert.deepEqual(restored.useAnnotations.getState().recentSessions, { "project-a": "session-b", "project-b": "session-c" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("draft clearing, rewind, and draft store replacement cannot remove annotation data", async () => {
|
|
79
|
+
const storage = memoryStorage();
|
|
80
|
+
installStorage(storage);
|
|
81
|
+
const annotations = await reload();
|
|
82
|
+
const item = annotations.addAnnotation(input());
|
|
83
|
+
const persisted = storage.values.get(storageKey);
|
|
84
|
+
editDraft("session-a", { text: "draft" });
|
|
85
|
+
clearDraft("session-a");
|
|
86
|
+
setDraft("session-a", "rewound", "rewind-a");
|
|
87
|
+
restoreRewindDraft("session-a", "older", "rewind-b");
|
|
88
|
+
useDrafts.setState({ drafts: {} });
|
|
89
|
+
assert.deepEqual(annotations.snapshotAnnotations("project-a", "session-a"), [item]);
|
|
90
|
+
assert.equal(storage.values.get(storageKey), persisted);
|
|
91
|
+
assert.notEqual(storageKey, "cide.draft.session-a");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("successful send consumes exact snapshots while preserving later edits and additions", async () => {
|
|
95
|
+
installStorage(memoryStorage());
|
|
96
|
+
const annotations = await reload();
|
|
97
|
+
const unchanged = annotations.addAnnotation(input(0));
|
|
98
|
+
const changed = annotations.addAnnotation(input(1));
|
|
99
|
+
const deleted = annotations.addAnnotation(input(2));
|
|
100
|
+
const otherSession = annotations.addAnnotation({ ...input(3), targetSessionId: "session-b" });
|
|
101
|
+
const submitted = annotations.snapshotAnnotations("project-a", "session-a");
|
|
102
|
+
annotations.updateAnnotation(changed.id, "newer edit");
|
|
103
|
+
annotations.updateAnnotation(changed.id, changed.comment);
|
|
104
|
+
annotations.deleteAnnotation(deleted.id);
|
|
105
|
+
const added = annotations.addAnnotation(input(4));
|
|
106
|
+
annotations.consumeAnnotations(submitted);
|
|
107
|
+
const remaining = annotations.useAnnotations.getState().items;
|
|
108
|
+
assert.equal(remaining.some((item) => item.id === unchanged.id), false);
|
|
109
|
+
assert.equal(remaining.some((item) => item.id === deleted.id), false);
|
|
110
|
+
assert.deepEqual(remaining.map((item) => item.id), [changed.id, otherSession.id, added.id]);
|
|
111
|
+
assert.equal(remaining[0].revision, 3, "an edit remains newer even after restoring its submitted text");
|
|
112
|
+
annotations.consumeAnnotations(submitted);
|
|
113
|
+
assert.equal(annotations.useAnnotations.getState().items.length, 3, "a duplicate acknowledgement is harmless");
|
|
114
|
+
const restored = await reload();
|
|
115
|
+
assert.deepEqual(restored.useAnnotations.getState().items, remaining);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("submission consumption obeys the same preservation property across interleaved edits", async () => {
|
|
119
|
+
installStorage(memoryStorage());
|
|
120
|
+
const annotations = await reload();
|
|
121
|
+
for (let round = 0; round < 32; round++) {
|
|
122
|
+
annotations.useAnnotations.setState({ items: [] });
|
|
123
|
+
const submittedItems = Array.from({ length: 8 }, (_, index) => annotations.addAnnotation(input(index)));
|
|
124
|
+
const snapshot = annotations.snapshotAnnotations("project-a", "session-a");
|
|
125
|
+
const survivors = new Set<string>();
|
|
126
|
+
submittedItems.forEach((item, index) => {
|
|
127
|
+
if ((round >> (index % 5)) & 1) {
|
|
128
|
+
annotations.updateAnnotation(item.id, `edited ${round}/${index}`);
|
|
129
|
+
survivors.add(item.id);
|
|
130
|
+
}
|
|
131
|
+
if ((round + index) % 7 === 0) {
|
|
132
|
+
annotations.deleteAnnotation(item.id);
|
|
133
|
+
survivors.delete(item.id);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
for (let index = 0; index < round % 4; index++) survivors.add(annotations.addAnnotation(input(index + 8)).id);
|
|
137
|
+
annotations.consumeAnnotations(snapshot);
|
|
138
|
+
assert.deepEqual(new Set(annotations.useAnnotations.getState().items.map((item) => item.id)), survivors, `round ${round}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("returned cards and snapshots do not alias mutable source data", async () => {
|
|
143
|
+
installStorage(memoryStorage());
|
|
144
|
+
const annotations = await reload();
|
|
145
|
+
const original = input();
|
|
146
|
+
const created = annotations.addAnnotation(original);
|
|
147
|
+
if (original.source.kind === "file") original.source.path = "mutated input";
|
|
148
|
+
if (created.source.kind === "file") created.source.path = "mutated return value";
|
|
149
|
+
const snapshot = annotations.snapshotAnnotations("project-a", "session-a");
|
|
150
|
+
snapshot[0].comment = "mutated snapshot";
|
|
151
|
+
if (snapshot[0].source.kind === "file") snapshot[0].source.path = "mutated snapshot source";
|
|
152
|
+
const current = annotations.useAnnotations.getState().items[0];
|
|
153
|
+
assert.equal(current.source.kind === "file" && current.source.path, "src/file-0.ts");
|
|
154
|
+
assert.equal(current.comment, "comment 0");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("serialization preserves normal text, quote blank lines, Unicode, and comments", async () => {
|
|
158
|
+
installStorage(memoryStorage());
|
|
159
|
+
const annotations = await reload();
|
|
160
|
+
const file = annotations.addAnnotation({ ...input(), source: { kind: "file", path: "文件/实现.ts" }, quote: "\nα\n\n z \n", comment: "说明🙂\nsecond comment line" });
|
|
161
|
+
const message = annotations.addAnnotation({ ...input(1), source: { kind: "message", sessionId: "origin", itemKey: "uuid:2", agentToolUseId: "agent-1" }, quote: "first\r\nsecond", comment: "Fix this" });
|
|
162
|
+
assert.equal(annotations.formatAnnotatedMessage("normal text\n ", [file, message]),
|
|
163
|
+
"normal text\n \n\nUser comments on File 文件/实现.ts:\n> \n> α\n> \n> z \n> \n\n说明🙂\nsecond comment line\n\nUser comments:\n> first\r\n> second\n\nFix this");
|
|
164
|
+
assert.equal(annotations.formatAnnotatedMessage("!command\n", []), "!command\n");
|
|
165
|
+
assert.equal(annotations.formatAnnotatedMessage("", [{ ...file, quote: "", comment: "" }]), "User comments on File 文件/实现.ts:\n> \n\n");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("message annotation context is independent of client identifiers while stored source metadata survives", async () => {
|
|
169
|
+
installStorage(memoryStorage());
|
|
170
|
+
const annotations = await reload();
|
|
171
|
+
const source: AnnotationSource = { kind: "message", sessionId: "source-session-id", itemKey: "1f385b18-1ed1-463c-96a9-7300c3e3f367:0", agentToolUseId: "agent-tool-id" };
|
|
172
|
+
const item = annotations.addAnnotation({ ...input(0, source), quote: "你要过滤后的音频,还是目标人的文字?", comment: "直接给文字最好。" });
|
|
173
|
+
const expected = "User comments:\n> 你要过滤后的音频,还是目标人的文字?\n\n直接给文字最好。";
|
|
174
|
+
assert.equal(annotations.formatAnnotatedMessage("", [item]), expected);
|
|
175
|
+
assert.equal(annotations.formatAnnotatedMessage("", [{ ...item, id: "other-card", projectId: "other-project", targetSessionId: "other-session", source: { kind: "message", sessionId: "other-source", itemKey: "other-message:9" } }]), expected);
|
|
176
|
+
const restored = await reload();
|
|
177
|
+
const saved = restored.snapshotAnnotations("project-a", "session-a");
|
|
178
|
+
assert.deepEqual(saved[0].source, source, "client selection metadata stays available after reload");
|
|
179
|
+
assert.equal(restored.formatAnnotatedMessage("", saved), expected, "previously stored drafts use the new serialization too");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("attached annotations prevent leading slash or shell text from dispatching as SDK commands", async () => {
|
|
183
|
+
installStorage(memoryStorage());
|
|
184
|
+
const annotations = await reload();
|
|
185
|
+
const item = annotations.addAnnotation(input());
|
|
186
|
+
assert.equal(annotations.formatAnnotatedMessage("/clear", [item]),
|
|
187
|
+
"User message:\n/clear\n\nUser comments on File src/file-0.ts:\n> quoted text 0\n\ncomment 0");
|
|
188
|
+
assert.equal(annotations.formatAnnotatedMessage(" \n!echo hello", [item]),
|
|
189
|
+
"User message:\n \n!echo hello\n\nUser comments on File src/file-0.ts:\n> quoted text 0\n\ncomment 0");
|
|
190
|
+
assert.equal(annotations.formatAnnotatedMessage("/clear", []), "/clear");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("invalid stored data reports an error without crashing or modifying storage", async () => {
|
|
194
|
+
const valid: DraftAnnotation = { ...input(), id: "saved", revision: 1, createdAt: 1, updatedAt: 2 };
|
|
195
|
+
const cases: unknown[] = [
|
|
196
|
+
null,
|
|
197
|
+
{ version: 2, items: [valid], mode: false, recentSessions: {} },
|
|
198
|
+
{ version: 1, items: null, mode: false, recentSessions: {} },
|
|
199
|
+
{ version: 1, items: [{ ...valid, source: { kind: "file", path: "a", startLine: -1 } }], mode: false, recentSessions: {} },
|
|
200
|
+
{ version: 1, items: [{ ...valid, revision: 0 }], mode: false, recentSessions: {} },
|
|
201
|
+
{ version: 1, items: [valid, valid], mode: false, recentSessions: {} },
|
|
202
|
+
{ version: 1, items: [valid], mode: false, recentSessions: { project: 42 } },
|
|
203
|
+
];
|
|
204
|
+
for (const value of cases) {
|
|
205
|
+
const storage = memoryStorage();
|
|
206
|
+
const serialized = JSON.stringify(value);
|
|
207
|
+
storage.values.set(storageKey, serialized);
|
|
208
|
+
installStorage(storage);
|
|
209
|
+
const annotations = await reload();
|
|
210
|
+
assert.equal(annotations.useAnnotations.getState().items.length, 0);
|
|
211
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /格式或版本无效/);
|
|
212
|
+
annotations.rememberAnnotationSession("project-a", "session-b");
|
|
213
|
+
annotations.setAnnotationMode(true);
|
|
214
|
+
const added = annotations.addAnnotation(input(5));
|
|
215
|
+
annotations.updateAnnotation(added.id, "still in memory");
|
|
216
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /原数据已保留/);
|
|
217
|
+
assert.equal(storage.values.get(storageKey), serialized);
|
|
218
|
+
}
|
|
219
|
+
const storage = memoryStorage();
|
|
220
|
+
storage.values.set(storageKey, "{broken-json");
|
|
221
|
+
installStorage(storage);
|
|
222
|
+
const annotations = await reload();
|
|
223
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /无法读取已保存的批注/);
|
|
224
|
+
assert.equal(storage.values.get(storageKey), "{broken-json");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("a storage read failure cannot overwrite previously saved cards after an automatic visit", async () => {
|
|
228
|
+
const storage = memoryStorage();
|
|
229
|
+
const saved: DraftAnnotation = { ...input(), id: "saved", revision: 1, createdAt: 1, updatedAt: 2 };
|
|
230
|
+
const persisted = JSON.stringify({ version: 1, items: [saved], mode: false, recentSessions: {} });
|
|
231
|
+
storage.values.set(storageKey, persisted);
|
|
232
|
+
storage.failures.read = new Error("read denied");
|
|
233
|
+
installStorage(storage);
|
|
234
|
+
const annotations = await reload();
|
|
235
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /read denied/);
|
|
236
|
+
delete storage.failures.read;
|
|
237
|
+
annotations.rememberAnnotationSession("project-a", "session-a");
|
|
238
|
+
annotations.setAnnotationMode(true);
|
|
239
|
+
annotations.addAnnotation(input(1));
|
|
240
|
+
assert.equal(annotations.useAnnotations.getState().items.length, 1, "new cards still work in memory");
|
|
241
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /原数据已保留/);
|
|
242
|
+
assert.equal(storage.values.get(storageKey), persisted);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("storage write failures are visible while edits remain usable and later saves recover", async () => {
|
|
246
|
+
const storage = memoryStorage();
|
|
247
|
+
installStorage(storage);
|
|
248
|
+
const annotations = await reload();
|
|
249
|
+
const first = annotations.addAnnotation(input());
|
|
250
|
+
const persisted = storage.values.get(storageKey);
|
|
251
|
+
storage.failures.write = new Error("quota exceeded");
|
|
252
|
+
annotations.updateAnnotation(first.id, "unsaved update");
|
|
253
|
+
const second = annotations.addAnnotation(input(1));
|
|
254
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /quota exceeded/);
|
|
255
|
+
assert.equal(annotations.useAnnotations.getState().items[0].comment, "unsaved update");
|
|
256
|
+
assert.equal(annotations.useAnnotations.getState().items.length, 2);
|
|
257
|
+
assert.equal(storage.values.get(storageKey), persisted, "a failed write leaves previously saved data intact");
|
|
258
|
+
delete storage.failures.write;
|
|
259
|
+
annotations.setAnnotationMode(true);
|
|
260
|
+
assert.equal(annotations.useAnnotations.getState().storageError, undefined);
|
|
261
|
+
const restored = await reload();
|
|
262
|
+
assert.deepEqual(restored.useAnnotations.getState().items.map((item) => item.id), [first.id, second.id]);
|
|
263
|
+
assert.equal(restored.useAnnotations.getState().items[0].comment, "unsaved update");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("storage becoming available never replaces edits made while it was unavailable", async () => {
|
|
267
|
+
installStorage(undefined);
|
|
268
|
+
const annotations = await reload();
|
|
269
|
+
const created = annotations.addAnnotation(input());
|
|
270
|
+
assert.match(annotations.useAnnotations.getState().storageError ?? "", /浏览器存储不可用/);
|
|
271
|
+
const storage = memoryStorage();
|
|
272
|
+
storage.values.set(storageKey, JSON.stringify({ version: 1, items: [], mode: true, recentSessions: {} }));
|
|
273
|
+
installStorage(storage);
|
|
274
|
+
annotations.ensureAnnotationsLoaded();
|
|
275
|
+
assert.deepEqual(annotations.useAnnotations.getState().items, [created]);
|
|
276
|
+
annotations.setAnnotationMode(true);
|
|
277
|
+
const restored = await reload();
|
|
278
|
+
assert.deepEqual(restored.useAnnotations.getState().items, [created]);
|
|
279
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export type AnnotationSource =
|
|
4
|
+
| { kind: "file"; path: string; startLine?: number; endLine?: number }
|
|
5
|
+
| { kind: "message"; sessionId: string; itemKey: string; agentToolUseId?: string };
|
|
6
|
+
|
|
7
|
+
export type DraftAnnotation = {
|
|
8
|
+
id: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
targetSessionId: string;
|
|
11
|
+
source: AnnotationSource;
|
|
12
|
+
quote: string;
|
|
13
|
+
comment: string;
|
|
14
|
+
revision: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AnnotationState = {
|
|
20
|
+
items: DraftAnnotation[];
|
|
21
|
+
mode: boolean;
|
|
22
|
+
recentSessions: Record<string, string>;
|
|
23
|
+
storageError?: string;
|
|
24
|
+
};
|
|
25
|
+
type SavedAnnotations = Pick<AnnotationState, "items" | "mode" | "recentSessions"> & { version: 1 };
|
|
26
|
+
|
|
27
|
+
// This namespace deliberately has no relationship to composer drafts or message caches.
|
|
28
|
+
export const ANNOTATIONS_STORAGE_KEY = "cide.annotations.v1";
|
|
29
|
+
export const useAnnotations = create<AnnotationState>(() => ({ items: [], mode: false, recentSessions: {} }));
|
|
30
|
+
let loaded = false;
|
|
31
|
+
let loadFailed = false;
|
|
32
|
+
let fallbackId = 0;
|
|
33
|
+
|
|
34
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function nonempty(value: unknown): value is string {
|
|
38
|
+
return typeof value === "string" && value.length > 0;
|
|
39
|
+
}
|
|
40
|
+
function line(value: unknown): value is number {
|
|
41
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
42
|
+
}
|
|
43
|
+
function source(value: unknown): value is AnnotationSource {
|
|
44
|
+
if (!record(value)) return false;
|
|
45
|
+
if (value.kind === "file") {
|
|
46
|
+
return nonempty(value.path)
|
|
47
|
+
&& (value.startLine === undefined || line(value.startLine))
|
|
48
|
+
&& (value.endLine === undefined || line(value.endLine))
|
|
49
|
+
&& (value.startLine === undefined || value.endLine === undefined || value.endLine >= value.startLine);
|
|
50
|
+
}
|
|
51
|
+
return value.kind === "message" && nonempty(value.sessionId) && nonempty(value.itemKey)
|
|
52
|
+
&& (value.agentToolUseId === undefined || nonempty(value.agentToolUseId));
|
|
53
|
+
}
|
|
54
|
+
function annotation(value: unknown): value is DraftAnnotation {
|
|
55
|
+
return record(value) && nonempty(value.id) && nonempty(value.projectId) && nonempty(value.targetSessionId)
|
|
56
|
+
&& source(value.source) && typeof value.quote === "string" && typeof value.comment === "string"
|
|
57
|
+
&& typeof value.revision === "number" && Number.isSafeInteger(value.revision) && value.revision > 0
|
|
58
|
+
&& typeof value.createdAt === "number" && Number.isFinite(value.createdAt) && value.createdAt >= 0
|
|
59
|
+
&& typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && value.updatedAt >= value.createdAt;
|
|
60
|
+
}
|
|
61
|
+
function savedAnnotations(value: unknown): value is SavedAnnotations {
|
|
62
|
+
if (!record(value) || value.version !== 1 || !Array.isArray(value.items)
|
|
63
|
+
|| !value.items.every(annotation) || typeof value.mode !== "boolean" || !record(value.recentSessions)
|
|
64
|
+
|| !Object.values(value.recentSessions).every(nonempty)) return false;
|
|
65
|
+
return new Set(value.items.map((item) => item.id)).size === value.items.length;
|
|
66
|
+
}
|
|
67
|
+
function copy(item: DraftAnnotation): DraftAnnotation {
|
|
68
|
+
return { ...item, source: { ...item.source } };
|
|
69
|
+
}
|
|
70
|
+
function errorMessage(action: "load" | "save", error: unknown): string {
|
|
71
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return action === "load"
|
|
73
|
+
? `无法读取已保存的批注。原数据已保留,本次修改仅保存在内存中。${detail}`
|
|
74
|
+
: `批注未能保存,当前修改仅保存在内存中。${detail}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Safe at module import without browser storage; subsequent browser calls hydrate once. */
|
|
78
|
+
export function ensureAnnotationsLoaded(): void {
|
|
79
|
+
if (loaded) return;
|
|
80
|
+
try {
|
|
81
|
+
if (typeof localStorage === "undefined") return;
|
|
82
|
+
const serialized = localStorage.getItem(ANNOTATIONS_STORAGE_KEY);
|
|
83
|
+
loaded = true;
|
|
84
|
+
if (serialized === null) return;
|
|
85
|
+
const saved: unknown = JSON.parse(serialized);
|
|
86
|
+
if (!savedAnnotations(saved)) throw new Error("保存的批注数据格式或版本无效。");
|
|
87
|
+
useAnnotations.setState({
|
|
88
|
+
items: saved.items.map(copy),
|
|
89
|
+
mode: saved.mode,
|
|
90
|
+
recentSessions: { ...saved.recentSessions },
|
|
91
|
+
storageError: undefined,
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
loaded = true;
|
|
95
|
+
loadFailed = true;
|
|
96
|
+
useAnnotations.setState({ storageError: errorMessage("load", error) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function persist(): void {
|
|
101
|
+
// Once an edit exists, later storage availability must not overwrite it by hydrating.
|
|
102
|
+
loaded = true;
|
|
103
|
+
// A visit can update recents or mode. It must never replace unreadable saved cards.
|
|
104
|
+
if (loadFailed) return;
|
|
105
|
+
const { items, mode, recentSessions } = useAnnotations.getState();
|
|
106
|
+
try {
|
|
107
|
+
if (typeof localStorage === "undefined") throw new Error("浏览器存储不可用。");
|
|
108
|
+
const saved: SavedAnnotations = { version: 1, items, mode, recentSessions };
|
|
109
|
+
localStorage.setItem(ANNOTATIONS_STORAGE_KEY, JSON.stringify(saved));
|
|
110
|
+
useAnnotations.setState({ storageError: undefined });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// The in-memory edit is retained and the UI can explain why it is not saved.
|
|
113
|
+
useAnnotations.setState({ storageError: errorMessage("save", error) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function addAnnotation(input: Omit<DraftAnnotation, "id" | "revision" | "createdAt" | "updatedAt">): DraftAnnotation {
|
|
118
|
+
ensureAnnotationsLoaded();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const id = globalThis.crypto?.randomUUID?.() ?? `annotation-${now}-${++fallbackId}-${Math.random().toString(36).slice(2)}`;
|
|
121
|
+
const item: DraftAnnotation = { ...input, source: { ...input.source }, id, revision: 1, createdAt: now, updatedAt: now };
|
|
122
|
+
useAnnotations.setState((state) => ({ items: [...state.items, item] }));
|
|
123
|
+
persist();
|
|
124
|
+
return copy(item);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function updateAnnotation(id: string, comment: string): void {
|
|
128
|
+
ensureAnnotationsLoaded();
|
|
129
|
+
const item = useAnnotations.getState().items.find((candidate) => candidate.id === id);
|
|
130
|
+
if (!item || item.comment === comment) return;
|
|
131
|
+
useAnnotations.setState((state) => ({
|
|
132
|
+
items: state.items.map((candidate) => candidate.id === id
|
|
133
|
+
? { ...candidate, comment, revision: candidate.revision + 1, updatedAt: Math.max(Date.now(), candidate.updatedAt) }
|
|
134
|
+
: candidate),
|
|
135
|
+
}));
|
|
136
|
+
persist();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function deleteAnnotation(id: string): void {
|
|
140
|
+
ensureAnnotationsLoaded();
|
|
141
|
+
const items = useAnnotations.getState().items;
|
|
142
|
+
if (!items.some((item) => item.id === id)) return;
|
|
143
|
+
useAnnotations.setState({ items: items.filter((item) => item.id !== id) });
|
|
144
|
+
persist();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function setAnnotationMode(mode: boolean): void {
|
|
148
|
+
ensureAnnotationsLoaded();
|
|
149
|
+
useAnnotations.setState({ mode });
|
|
150
|
+
persist();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function rememberAnnotationSession(projectId: string, sessionId: string): void {
|
|
154
|
+
ensureAnnotationsLoaded();
|
|
155
|
+
useAnnotations.setState((state) => ({ recentSessions: { ...state.recentSessions, [projectId]: sessionId } }));
|
|
156
|
+
persist();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A detached submission snapshot cannot change while the composer is dispatching. */
|
|
160
|
+
export function snapshotAnnotations(projectId: string, sessionId: string): DraftAnnotation[] {
|
|
161
|
+
ensureAnnotationsLoaded();
|
|
162
|
+
return useAnnotations.getState().items
|
|
163
|
+
.filter((item) => item.projectId === projectId && item.targetSessionId === sessionId)
|
|
164
|
+
.map(copy);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Remove only the submitted revisions; new cards and edits belong to the next send. */
|
|
168
|
+
export function consumeAnnotations(snapshot: readonly DraftAnnotation[]): void {
|
|
169
|
+
ensureAnnotationsLoaded();
|
|
170
|
+
const revisions = new Map(snapshot.map((item) => [item.id, item.revision]));
|
|
171
|
+
const items = useAnnotations.getState().items;
|
|
172
|
+
const remaining = items.filter((item) => revisions.get(item.id) !== item.revision);
|
|
173
|
+
if (remaining.length === items.length) return;
|
|
174
|
+
useAnnotations.setState({ items: remaining });
|
|
175
|
+
persist();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function formatAnnotatedMessage(text: string, items: readonly DraftAnnotation[]): string {
|
|
179
|
+
if (!items.length) return text;
|
|
180
|
+
const comments = items.map((item) => {
|
|
181
|
+
// Message keys locate the selection in the client; they are not model context.
|
|
182
|
+
const heading = item.source.kind === "file" ? `User comments on File ${item.source.path}:` : "User comments:";
|
|
183
|
+
const quote = item.quote.split("\n").map((line) => `> ${line}`).join("\n");
|
|
184
|
+
return `${heading}\n${quote}\n\n${item.comment}`;
|
|
185
|
+
});
|
|
186
|
+
// The SDK also recognizes leading commands inside ordinary send messages.
|
|
187
|
+
const normalText = /^[!/]/.test(text.trimStart()) ? `User message:\n${text}` : text;
|
|
188
|
+
return (normalText ? [normalText, ...comments] : comments).join("\n\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
ensureAnnotationsLoaded();
|
package/web/src/api.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { invalidateAuth } from "./auth-events.ts";
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(public status: number, message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Shared by JSON requests and multipart uploads, before any response-body parsing. */
|
|
10
|
+
export async function authFetch(path: string, init?: RequestInit): Promise<Response> {
|
|
11
|
+
const res = await fetch(path, { ...init, credentials: "same-origin" });
|
|
12
|
+
// A wrong login password is a form error, not the expiry of an existing login.
|
|
13
|
+
if (res.status === 401 && path.split("?")[0] !== "/api/login") invalidateAuth();
|
|
14
|
+
return res;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function api<T>(path: string, init?: RequestInit & { json?: unknown }): Promise<T> {
|
|
18
|
+
const { json, ...rest } = init ?? {};
|
|
19
|
+
const res = await authFetch(path, {
|
|
20
|
+
...rest,
|
|
21
|
+
headers: { ...(json !== undefined ? { "content-type": "application/json" } : {}), ...rest.headers },
|
|
22
|
+
body: json !== undefined ? JSON.stringify(json) : rest.body,
|
|
23
|
+
});
|
|
24
|
+
const data = await res.json().catch(() => ({}));
|
|
25
|
+
if (!res.ok) throw new ApiError(res.status, (data as { error?: string }).error ?? res.statusText);
|
|
26
|
+
return data as T;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Transport-independent signal: importing the REST client must not start browser live state.
|
|
2
|
+
const listeners = new Set<() => void>();
|
|
3
|
+
|
|
4
|
+
export function onAuthInvalidated(listener: () => void) {
|
|
5
|
+
listeners.add(listener);
|
|
6
|
+
return () => { listeners.delete(listener); };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function invalidateAuth() {
|
|
10
|
+
for (const listener of listeners) listener();
|
|
11
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { api, ApiError, authFetch } from "./api.ts";
|
|
4
|
+
import { invalidateAuth, onAuthInvalidated } from "./auth-events.ts";
|
|
5
|
+
|
|
6
|
+
test("REST and multipart 401 signal expiry while login errors remain form errors", async () => {
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
8
|
+
let invalidations = 0;
|
|
9
|
+
const unsubscribe = onAuthInvalidated(() => { invalidations++; });
|
|
10
|
+
globalThis.fetch = async () => Response.json({ error: "密码不正确" }, { status: 401 });
|
|
11
|
+
try {
|
|
12
|
+
await assert.rejects(api("/api/login", { method: "POST", json: { password: "test-only" } }), (error: unknown) => error instanceof ApiError && error.status === 401 && error.message === "密码不正确");
|
|
13
|
+
assert.equal(invalidations, 0);
|
|
14
|
+
await assert.rejects(api("/api/projects"), (error: unknown) => error instanceof ApiError && error.status === 401);
|
|
15
|
+
assert.equal(invalidations, 1);
|
|
16
|
+
// Uploads use the same transport; expiry is signalled before parsing any JSON.
|
|
17
|
+
globalThis.fetch = async () => new Response("unauthorized", { status: 401 });
|
|
18
|
+
const response = await authFetch("/api/projects/p/files/upload", { method: "POST", body: new FormData() });
|
|
19
|
+
assert.equal(response.status, 401);
|
|
20
|
+
assert.equal(invalidations, 2);
|
|
21
|
+
assert.equal(await response.text(), "unauthorized");
|
|
22
|
+
} finally { unsubscribe(); globalThis.fetch = originalFetch; }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("expiry stops reconnects and discards offline and blocked commands before login resumes", async () => {
|
|
26
|
+
const originalFetch = globalThis.fetch;
|
|
27
|
+
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
|
|
28
|
+
const originalLocation = Object.getOwnPropertyDescriptor(globalThis, "location");
|
|
29
|
+
const originalSocket = Object.getOwnPropertyDescriptor(globalThis, "WebSocket");
|
|
30
|
+
const sockets: Socket[] = [];
|
|
31
|
+
const visibilityListeners: (() => void)[] = [];
|
|
32
|
+
class Socket {
|
|
33
|
+
static OPEN = 1;
|
|
34
|
+
static CONNECTING = 0;
|
|
35
|
+
readyState = 0;
|
|
36
|
+
sent: string[] = [];
|
|
37
|
+
onopen?: () => void;
|
|
38
|
+
onclose?: (event: { code: number }) => void;
|
|
39
|
+
constructor(_url: string) { sockets.push(this); }
|
|
40
|
+
send(message: string) { this.sent.push(message); }
|
|
41
|
+
open() { this.readyState = 1; this.onopen?.(); }
|
|
42
|
+
close(code = 1000) { this.readyState = 3; this.onclose?.({ code }); }
|
|
43
|
+
}
|
|
44
|
+
Object.defineProperty(globalThis, "document", { configurable: true, value: { querySelector: () => null, visibilityState: "visible", addEventListener: (_type: string, listener: () => void) => visibilityListeners.push(listener) } });
|
|
45
|
+
Object.defineProperty(globalThis, "location", { configurable: true, value: { protocol: "http:", host: "test.invalid" } });
|
|
46
|
+
Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: Socket });
|
|
47
|
+
const { startLive, sendClient, useLive } = await import("./live.ts");
|
|
48
|
+
const { queryClient } = await import("./queryClient.ts");
|
|
49
|
+
const command = { t: "interrupt" as const, sessionId: "test-session" };
|
|
50
|
+
const commands = (socket: Socket) => socket.sent.map((message) => JSON.parse(message)).filter((message) => message.t === "interrupt");
|
|
51
|
+
try {
|
|
52
|
+
queryClient.setQueryData(["auth"], { authed: true });
|
|
53
|
+
startLive();
|
|
54
|
+
const first = sockets.at(-1)!;
|
|
55
|
+
first.open();
|
|
56
|
+
first.close(); // Ordinary network loss schedules a reconnect.
|
|
57
|
+
sendClient(command); // Queued while offline.
|
|
58
|
+
globalThis.fetch = async () => Response.json({ error: "unauthorized" }, { status: 401 });
|
|
59
|
+
await assert.rejects(api("/api/projects"), ApiError);
|
|
60
|
+
assert.equal(useLive.getState().connected, false);
|
|
61
|
+
assert.deepEqual(queryClient.getQueryData(["auth"]), { authed: false });
|
|
62
|
+
sendClient(command); // Must not accumulate after expiry.
|
|
63
|
+
for (const listener of visibilityListeners) listener();
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
65
|
+
assert.equal(sockets.length, 1, "neither retry timer nor visibility may reconnect");
|
|
66
|
+
|
|
67
|
+
startLive(); // A successful login confirmed by App restores live transport.
|
|
68
|
+
const second = sockets.at(-1)!;
|
|
69
|
+
second.open();
|
|
70
|
+
assert.deepEqual(commands(second), [], "REST expiry discarded both old queues");
|
|
71
|
+
sendClient(command);
|
|
72
|
+
assert.equal(commands(second).length, 1, "new authenticated commands work");
|
|
73
|
+
second.close();
|
|
74
|
+
sendClient(command);
|
|
75
|
+
startLive();
|
|
76
|
+
const denied = sockets.at(-1)!;
|
|
77
|
+
denied.close(4401);
|
|
78
|
+
sendClient(command);
|
|
79
|
+
assert.deepEqual(queryClient.getQueryData(["auth"]), { authed: false });
|
|
80
|
+
const count = sockets.length;
|
|
81
|
+
for (const listener of visibilityListeners) listener();
|
|
82
|
+
assert.equal(sockets.length, count);
|
|
83
|
+
startLive();
|
|
84
|
+
const restored = sockets.at(-1)!;
|
|
85
|
+
restored.open();
|
|
86
|
+
assert.deepEqual(commands(restored), [], "WS expiry discarded both old queues");
|
|
87
|
+
|
|
88
|
+
// A REST expiry must also close a currently OPEN socket. Late callbacks are inert.
|
|
89
|
+
await assert.rejects(api("/api/projects"), ApiError);
|
|
90
|
+
assert.equal(restored.readyState, 3);
|
|
91
|
+
const beforeLateCallback = restored.sent.length;
|
|
92
|
+
restored.onopen?.();
|
|
93
|
+
restored.onclose?.({ code: 1000 });
|
|
94
|
+
assert.equal(restored.sent.length, beforeLateCallback);
|
|
95
|
+
assert.equal(useLive.getState().connected, false);
|
|
96
|
+
} finally {
|
|
97
|
+
invalidateAuth();
|
|
98
|
+
queryClient.clear();
|
|
99
|
+
globalThis.fetch = originalFetch;
|
|
100
|
+
for (const [name, descriptor] of [["document", originalDocument], ["location", originalLocation], ["WebSocket", originalSocket]] as const) {
|
|
101
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
|
102
|
+
else Reflect.deleteProperty(globalThis, name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Copy text; plain-http LAN pages have no clipboard API, so fall back to a hidden textarea. */
|
|
2
|
+
export async function copyText(text: string): Promise<void> {
|
|
3
|
+
try {
|
|
4
|
+
await navigator.clipboard.writeText(text);
|
|
5
|
+
} catch {
|
|
6
|
+
const focused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
7
|
+
const selection = window.getSelection();
|
|
8
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, index) => selection.getRangeAt(index).cloneRange()) : [];
|
|
9
|
+
const ta = document.createElement("textarea");
|
|
10
|
+
ta.value = text;
|
|
11
|
+
ta.readOnly = true;
|
|
12
|
+
ta.style.position = "fixed";
|
|
13
|
+
ta.style.top = "0";
|
|
14
|
+
ta.style.left = "0";
|
|
15
|
+
ta.style.fontSize = "16px";
|
|
16
|
+
ta.style.opacity = "0";
|
|
17
|
+
document.body.appendChild(ta);
|
|
18
|
+
try {
|
|
19
|
+
ta.focus({ preventScroll: true });
|
|
20
|
+
ta.select();
|
|
21
|
+
ta.setSelectionRange(0, text.length);
|
|
22
|
+
if (!document.execCommand("copy")) throw new Error("复制失败,请检查浏览器的剪贴板权限或手动复制。");
|
|
23
|
+
} finally {
|
|
24
|
+
ta.remove();
|
|
25
|
+
focused?.focus({ preventScroll: true });
|
|
26
|
+
if (selection && ranges.every((range) => range.startContainer.isConnected && range.endContainer.isConnected)) {
|
|
27
|
+
selection.removeAllRanges();
|
|
28
|
+
ranges.forEach((range) => selection.addRange(range));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|