@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,338 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { defaultTurnCollapsed, groupTurns, visibleTurnItems, turnRebases, rebaseTurnRow, rebaseTurnFolds, setTurnFold, type TurnFold } from "./turns.ts";
|
|
4
|
+
import type { Item } from "./transcript.ts";
|
|
5
|
+
|
|
6
|
+
const user = (key: string, text = key): Item => ({ kind: "user", key, text, images: 0 });
|
|
7
|
+
const text = (key: string): Item => ({ kind: "text", key, text: key });
|
|
8
|
+
const result = (key: string, userMessageIds?: string[]): Item => ({ kind: "result", key, ok: true, subtype: "success", userMessageIds });
|
|
9
|
+
const choice = (collapsed: boolean, revision: number): TurnFold => ({ collapsed, revision });
|
|
10
|
+
|
|
11
|
+
test("history without result records groups by prompt and preserves every item in order", () => {
|
|
12
|
+
const items = [user("u1"), text("a1"), user("u2"), text("a2")];
|
|
13
|
+
const turns = groupTurns(items, false);
|
|
14
|
+
assert.deepEqual(turns.map((turn) => turn.key), ["u1", "u2"]);
|
|
15
|
+
assert.deepEqual(turns.flatMap((turn) => turn.items), items);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("SDK results never merge input boundaries even when one run consumes several inputs", () => {
|
|
19
|
+
const items: Item[] = [user("u1"), { kind: "thinking", key: "think", text: "thought" }, { kind: "tool", key: "tool", id: "tool", name: "Read", input: {} }, user("u2"), text("reply"), result("r", ["u1", "u2"])];
|
|
20
|
+
const turns = groupTurns(items, false);
|
|
21
|
+
assert.equal(turns.length, 2);
|
|
22
|
+
assert.deepEqual(turns.map((turn) => turn.key), ["u1", "u2"]);
|
|
23
|
+
assert.ok(turns.every(defaultTurnCollapsed));
|
|
24
|
+
assert.equal(turns[0]!.result, undefined);
|
|
25
|
+
assert.equal(turns[1]!.result?.key, "r");
|
|
26
|
+
assert.deepEqual(turns.flatMap((turn) => turn.items), items);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("consecutive queued inputs stay together when an earlier result arrives between them", () => {
|
|
30
|
+
const items = [user("A"), text("reply-A"), user("B"), result("rA", ["A"]), user("C"), user("D"), text("reply-BCD"), result("rBCD", ["B", "C", "D"])];
|
|
31
|
+
const turns = groupTurns(items, false);
|
|
32
|
+
assert.deepEqual(turns.map((turn) => turn.key), ["A", "B"]);
|
|
33
|
+
assert.deepEqual(turns[0]!.items.map((item) => item.key), ["A", "reply-A", "rA"]);
|
|
34
|
+
assert.deepEqual(turns[1]!.items.map((item) => item.key), ["B", "C", "D", "reply-BCD", "rBCD"]);
|
|
35
|
+
assert.equal(new Set(turns.flatMap((turn) => turn.items)).size, items.length);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a result for A cannot complete or steal queued B and C", () => {
|
|
39
|
+
const items = [user("A"), text("reply-A"), user("B"), user("C"), result("rA", ["A"])];
|
|
40
|
+
const turns = groupTurns(items, true);
|
|
41
|
+
assert.deepEqual(turns.map((turn) => [turn.key, turn.active]), [["A", false], ["B", true]]);
|
|
42
|
+
assert.deepEqual(turns[1]!.items.map((item) => item.key), ["B", "C"]);
|
|
43
|
+
assert.equal(turns[1]!.result, undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a still-running foreground tool stays open while consecutive later inputs show queued", () => {
|
|
47
|
+
const bash: Item = { kind: "tool", key: "bash", id: "bash", name: "Bash", input: { command: "long command" } };
|
|
48
|
+
const turns = groupTurns([user("A"), bash, user("B"), user("C")], true);
|
|
49
|
+
assert.deepEqual(turns.map((turn) => [turn.key, turn.active, turn.queued]), [["A", true, false], ["B", true, true]]);
|
|
50
|
+
assert.ok(turns.every((turn) => !defaultTurnCollapsed(turn)));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a new input after agent content starts another turn, but adjacent inputs do not", () => {
|
|
54
|
+
const turns = groupTurns([user("A"), text("reply-A"), user("B"), user("C"), text("reply-BC"), user("D")], true);
|
|
55
|
+
assert.deepEqual(turns.map((turn) => turn.items.filter((item) => item.kind === "user").map((item) => item.key)), [["A"], ["B", "C"], ["D"]]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("startup errors without result records stay expanded and expose failure metadata", () => {
|
|
59
|
+
const turns = groupTurns([{ kind: "notice", key: "error", tone: "error", text: "SDK 启动失败" }], false);
|
|
60
|
+
assert.equal(turns[0]!.summary, "执行失败");
|
|
61
|
+
assert.equal(turns[0]!.error, "SDK 启动失败");
|
|
62
|
+
assert.equal(defaultTurnCollapsed(turns[0]!), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("failed results remain visible while successful completion still folds automatically", () => {
|
|
66
|
+
const failure: Item = { kind: "result", key: "failed", ok: false, subtype: "error_during_execution", error: "命令失败", userMessageIds: ["A"] };
|
|
67
|
+
const turns = groupTurns([user("A"), text("reply-A"), failure, user("B"), text("reply-B"), result("ok", ["B"])], false);
|
|
68
|
+
assert.equal(turns[0]!.error, "命令失败");
|
|
69
|
+
assert.equal(defaultTurnCollapsed(turns[0]!), false);
|
|
70
|
+
assert.equal(defaultTurnCollapsed(turns[1]!), true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("older history stays separate from the live turn, and only the unfinished tail is active", () => {
|
|
74
|
+
const items = [user("old"), text("old-reply"), user("live"), text("live-reply"), result("r", ["live"]), user("next"), text("streaming")];
|
|
75
|
+
const turns = groupTurns(items, true);
|
|
76
|
+
assert.deepEqual(turns.map((turn) => [turn.key, turn.active]), [["old", false], ["live", false], ["next", true]]);
|
|
77
|
+
assert.deepEqual(turns.flatMap((turn) => turn.items), items);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("consecutive inputs and images are kept within one turn without SDK prompt IDs", () => {
|
|
81
|
+
const image: Item = { kind: "user", key: "image", text: "", images: 1 };
|
|
82
|
+
const turns = groupTurns([image, user("second"), text("reply"), result("r")], false);
|
|
83
|
+
assert.equal(turns.length, 1);
|
|
84
|
+
assert.equal(turns[0]!.summary, "图片输入");
|
|
85
|
+
assert.equal(turns[0]!.items.length, 4);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("queued speech can supply a turn preview when earlier inputs contain only playback metadata", () => {
|
|
89
|
+
const metadata = user("playback", '<unheard_text reason="用户打断讲话">旧回复。</unheard_text>');
|
|
90
|
+
const speech = user("voice", "<asr-recognized-text>请查看下载的文件。</asr-recognized-text>");
|
|
91
|
+
const items = [metadata, speech, user("next", "后续排队输入")];
|
|
92
|
+
const turns = groupTurns(items, true);
|
|
93
|
+
assert.equal(turns.length, 1);
|
|
94
|
+
assert.equal(turns[0]!.summary, "请查看下载的文件。");
|
|
95
|
+
assert.deepEqual(turns[0]!.items, items, "preview selection cannot change queued input grouping or content");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("background turns without prompts and empty transcripts remain visible and valid", () => {
|
|
99
|
+
const items = [text("background"), result("r"), text("next")];
|
|
100
|
+
const turns = groupTurns(items, true);
|
|
101
|
+
assert.equal(turns.length, 2);
|
|
102
|
+
assert.equal(turns[0]!.summary, "后台调用");
|
|
103
|
+
assert.equal(turns[1]!.active, true);
|
|
104
|
+
assert.deepEqual(groupTurns([], true), []);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("default completed folds preserve all final-frame text blocks once with their original identities", () => {
|
|
108
|
+
for (const withResult of [false, true]) {
|
|
109
|
+
for (let blocks = 1; blocks <= 4; blocks++) {
|
|
110
|
+
// Colons in a frame key must not split the reply at an arbitrary block.
|
|
111
|
+
const reply = Array.from({ length: blocks }, (_, index) => text(`final:frame:${index + 1}`));
|
|
112
|
+
const items: Item[] = [user("u"), text("commentary:0"),
|
|
113
|
+
{ kind: "tool", key: "tool", id: "tool", name: "Read", input: {}, result: { content: "file", isError: false } },
|
|
114
|
+
text("previous-frame:0"), ...reply, ...(withResult ? [result("r", ["u"])] : [])];
|
|
115
|
+
const turn = groupTurns(items, false)[0]!;
|
|
116
|
+
assert.equal(defaultTurnCollapsed(turn), true);
|
|
117
|
+
const visible = visibleTurnItems(turn);
|
|
118
|
+
assert.deepEqual(visible, reply);
|
|
119
|
+
visible.forEach((item, index) => assert.equal(item, reply[index]));
|
|
120
|
+
assert.equal(new Set(visible.map((item) => item.key)).size, reply.length);
|
|
121
|
+
// Expanded rows use the same input sequence, without appending a second
|
|
122
|
+
// copy of the preview. This is shared by normal and virtual rendering.
|
|
123
|
+
assert.equal(visibleTurnItems(turn, false), turn.items);
|
|
124
|
+
assert.equal(new Set(visibleTurnItems(turn, false).map((item) => item.key)).size, items.length);
|
|
125
|
+
assert.deepEqual(visibleTurnItems(turn, true), []);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("completed folds never promote commentary when the final output is not settled assistant text", () => {
|
|
131
|
+
const tails: Item[] = [
|
|
132
|
+
{ kind: "tool", key: "tool", id: "tool", name: "Read", input: {}, result: { content: "file", isError: false } },
|
|
133
|
+
{ kind: "thinking", key: "thinking", text: "still reasoning" },
|
|
134
|
+
{ kind: "notice", key: "notice", tone: "info", text: "notice" },
|
|
135
|
+
{ kind: "local", key: "local", text: "local output" },
|
|
136
|
+
{ kind: "bash", key: "bash", command: "ls", stdout: "file", running: false },
|
|
137
|
+
{ kind: "text", key: "draft-msg:1", text: "unfinished", streaming: true },
|
|
138
|
+
];
|
|
139
|
+
for (const tail of tails) {
|
|
140
|
+
for (const withResult of [false, true]) {
|
|
141
|
+
const turn = groupTurns([user("u"), text("commentary:0"), tail, ...(withResult ? [result("r")] : [])], false)[0]!;
|
|
142
|
+
assert.deepEqual(visibleTurnItems(turn), [], tail.kind);
|
|
143
|
+
assert.equal(visibleTurnItems(turn, false), turn.items);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const items of [[user("u")], [result("r")]]) {
|
|
147
|
+
assert.deepEqual(visibleTurnItems(groupTurns(items, false)[0]!), []);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a same-frame tool or thinking block bounds the final text suffix", () => {
|
|
152
|
+
for (const boundary of [
|
|
153
|
+
{ kind: "thinking", key: "frame:1", text: "thinking" } as Item,
|
|
154
|
+
{ kind: "tool", key: "tool", id: "tool", name: "Read", input: {} } as Item,
|
|
155
|
+
]) {
|
|
156
|
+
const reply = [text("frame:2"), text("frame:3")];
|
|
157
|
+
const turn = groupTurns([user("u"), text("frame:0"), boundary, ...reply, result("r")], false)[0]!;
|
|
158
|
+
assert.deepEqual(visibleTurnItems(turn), reply);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("completion keeps the final reply visible while automatic and explicit folds follow the lifecycle", () => {
|
|
163
|
+
const reply = [text("reply:0"), text("reply:1")];
|
|
164
|
+
const items: Item[] = [user("u"), { kind: "thinking", key: "thought", text: "thinking" }, ...reply];
|
|
165
|
+
const active = groupTurns(items, true)[0]!;
|
|
166
|
+
assert.equal(visibleTurnItems(active), active.items);
|
|
167
|
+
const completed = groupTurns([...items, result("r", ["u"])], true)[0]!;
|
|
168
|
+
assert.equal(completed.active, false);
|
|
169
|
+
assert.deepEqual(visibleTurnItems(completed), reply);
|
|
170
|
+
for (const turn of [active, completed]) {
|
|
171
|
+
assert.equal(visibleTurnItems(turn, false), turn.items);
|
|
172
|
+
assert.deepEqual(visibleTurnItems(turn, true), []);
|
|
173
|
+
}
|
|
174
|
+
const failed = groupTurns([...items, { kind: "result", key: "failed", ok: false, subtype: "error_during_execution", error: "failed", userMessageIds: ["u"] }], false)[0]!;
|
|
175
|
+
assert.equal(visibleTurnItems(failed), failed.items);
|
|
176
|
+
assert.deepEqual(visibleTurnItems(failed, true), []);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("the latest turn stays expanded after completion while a manual fold hides the whole turn", () => {
|
|
180
|
+
const items: Item[] = [user("u"), { kind: "thinking", key: "thought", text: "thinking" }, text("reply:0")];
|
|
181
|
+
const active = groupTurns(items, true, true)[0]!;
|
|
182
|
+
const completedItems = [...items, result("r", ["u"])];
|
|
183
|
+
for (const running of [true, false]) {
|
|
184
|
+
const completed = groupTurns(completedItems, running, true)[0]!;
|
|
185
|
+
assert.equal(completed.active, false);
|
|
186
|
+
assert.equal(defaultTurnCollapsed(completed), false);
|
|
187
|
+
assert.equal(visibleTurnItems(completed), completed.items);
|
|
188
|
+
assert.deepEqual(visibleTurnItems(completed, true), [], "the latest manual fold hides the final reply too");
|
|
189
|
+
}
|
|
190
|
+
assert.equal(visibleTurnItems(active), active.items);
|
|
191
|
+
|
|
192
|
+
const next = groupTurns([...completedItems, user("next")], true, true);
|
|
193
|
+
assert.equal(defaultTurnCollapsed(next[0]!), true, "the previous turn follows the usual default once a new turn begins");
|
|
194
|
+
assert.equal(visibleTurnItems(next[0]!, false), next[0]!.items, "manual expansion remains honored");
|
|
195
|
+
assert.equal(defaultTurnCollapsed(next[1]!), false);
|
|
196
|
+
|
|
197
|
+
const olderWindow = groupTurns(completedItems, false, false)[0]!;
|
|
198
|
+
assert.equal(defaultTurnCollapsed(olderWindow), true, "an older page's last visible turn is not the conversation tail");
|
|
199
|
+
assert.deepEqual(visibleTurnItems(olderWindow).map((item) => item.key), ["reply:0"]);
|
|
200
|
+
assert.deepEqual(groupTurns([], false, true), []);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a manual fold hides the latest turn with and without a final text", () => {
|
|
204
|
+
// A trailing reply would survive a default fold; only the explicit branch hides it.
|
|
205
|
+
const withReply = groupTurns([user("u"), text("reply:0"), result("r")], false, true)[0]!;
|
|
206
|
+
assert.deepEqual(visibleTurnItems(withReply, true), []);
|
|
207
|
+
|
|
208
|
+
const tool: Item = { kind: "tool", key: "tool", id: "tool", name: "Read", input: {} };
|
|
209
|
+
const latest = groupTurns([user("u"), tool, result("r")], false, true)[0]!;
|
|
210
|
+
assert.deepEqual(visibleTurnItems(latest, true), []);
|
|
211
|
+
|
|
212
|
+
const promptOnly = groupTurns([user("only")], false, true)[0]!;
|
|
213
|
+
assert.deepEqual(visibleTurnItems(promptOnly, true), []);
|
|
214
|
+
|
|
215
|
+
const older = groupTurns([user("u"), tool, result("r")], false, false)[0]!;
|
|
216
|
+
assert.deepEqual(visibleTurnItems(older, true), [], "older manual folds may still hide a non-text tail");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("default final replies have a collapsible body even while the process is folded", () => {
|
|
220
|
+
for (const length of [100, 10_000]) {
|
|
221
|
+
const reply: Item = { kind: "text", key: "reply:0", text: "x".repeat(length) };
|
|
222
|
+
const completed = groupTurns([user("u"), reply, result("r")], false)[0]!;
|
|
223
|
+
assert.equal(defaultTurnCollapsed(completed), true);
|
|
224
|
+
// Both normal and virtual row owners and the floating-collapse guard use
|
|
225
|
+
// visible body length, independently of the header's process-fold state.
|
|
226
|
+
assert.ok(visibleTurnItems(completed).length > 0);
|
|
227
|
+
assert.equal(visibleTurnItems(completed)[0]!.key, reply.key);
|
|
228
|
+
assert.equal(visibleTurnItems(completed, true).length, 0);
|
|
229
|
+
assert.ok(visibleTurnItems(completed, false).length > 0);
|
|
230
|
+
}
|
|
231
|
+
const toolOnly = groupTurns([user("u"), { kind: "tool", key: "tool", id: "tool", name: "Read", input: {} }, result("r")], false)[0]!;
|
|
232
|
+
assert.equal(visibleTurnItems(toolOnly).length, 0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
test("prepending a partial turn carries explicit folds and header anchors to its prompt", () => {
|
|
237
|
+
const previous = groupTurns([text("a5"), text("a6")], false);
|
|
238
|
+
const current = groupTurns([user("older"), text("old-reply"), result("old-result"), user("prompt"), text("a5"), text("a6")], false);
|
|
239
|
+
const rebases = turnRebases(previous, current);
|
|
240
|
+
assert.equal(rebases.get("a5"), "prompt");
|
|
241
|
+
for (const collapsed of [false, true]) {
|
|
242
|
+
const value = choice(collapsed, 1);
|
|
243
|
+
const folds = rebaseTurnFolds(new Map([["main:a5", value], ["agent:a5", choice(!collapsed, 2)]]), "main", rebases);
|
|
244
|
+
assert.equal(folds.get("main:prompt"), value);
|
|
245
|
+
assert.equal(folds.get("agent:prompt"), undefined);
|
|
246
|
+
}
|
|
247
|
+
// Both normal DOM lookup and virtual index lookup use the same rebase; focus
|
|
248
|
+
// and pixel offset stay on the original anchor object at the call site.
|
|
249
|
+
assert.equal(rebaseTurnRow("turn-a5", rebases), "turn-prompt");
|
|
250
|
+
assert.equal(rebaseTurnRow("changes-a5", rebases), "changes-prompt");
|
|
251
|
+
assert.equal(rebaseTurnRow("a6", rebases), "a6");
|
|
252
|
+
const newer = choice(true, 2);
|
|
253
|
+
assert.equal(rebaseTurnFolds(new Map([["main:a5", choice(false, 1)], ["main:prompt", newer]]), "main", rebases).get("main:prompt"), newer);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("CACHE-021 latest explicit choice survives page round trips and migration chains", () => {
|
|
257
|
+
let folds = new Map([["view:tail", choice(true, 1)]]);
|
|
258
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["tail", "prompt"]]));
|
|
259
|
+
const opened = choice(false, 2);
|
|
260
|
+
folds = setTurnFold(folds, "view", "view:prompt", opened);
|
|
261
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["prompt", "tail"]]));
|
|
262
|
+
assert.equal(folds.get("view:tail"), opened);
|
|
263
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["tail", "middle"]]));
|
|
264
|
+
const closed = choice(true, 3);
|
|
265
|
+
folds = setTurnFold(folds, "view", "view:middle", closed);
|
|
266
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["middle", "prompt"]]));
|
|
267
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["prompt", "tail"]]));
|
|
268
|
+
assert.equal(folds.get("view:tail"), closed);
|
|
269
|
+
assert.ok(folds.has("view:prompt") && folds.has("view:middle"));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("an explicit action updates all known aliases before leaving the component", () => {
|
|
273
|
+
for (const initial of [new Map<string, TurnFold>(), new Map([["view:tail", choice(false, 1)]])]) {
|
|
274
|
+
let folds = rebaseTurnFolds(initial, "view", new Map([["tail", "prompt"]]));
|
|
275
|
+
const closed = choice(true, 2);
|
|
276
|
+
folds = setTurnFold(folds, "view", "view:prompt", closed);
|
|
277
|
+
// Remounting at the tail has no previous turns and therefore no rebases.
|
|
278
|
+
assert.equal(rebaseTurnFolds(folds, "view", new Map()).get("view:tail"), closed);
|
|
279
|
+
assert.equal(folds.get("view:prompt"), closed);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("aliases do not acquire fake recency, and merged turns choose the latest explicit action", () => {
|
|
284
|
+
for (const pairs of [[["a", "merged"], ["b", "merged"]], [["b", "merged"], ["a", "merged"]]] as const) {
|
|
285
|
+
const newer = choice(false, 2);
|
|
286
|
+
let folds = new Map([["view:a", choice(true, 1)], ["view:b", newer], ["other:merged", choice(true, 99)]]);
|
|
287
|
+
folds = rebaseTurnFolds(folds, "view", new Map([["a", "alias"]]));
|
|
288
|
+
assert.equal(folds.get("view:alias")?.revision, 1);
|
|
289
|
+
folds = rebaseTurnFolds(folds, "view", new Map(pairs.map(([from, to]) => [from === "a" ? "alias" : from, to])));
|
|
290
|
+
assert.equal(folds.get("view:merged"), newer);
|
|
291
|
+
assert.equal(folds.get("other:merged")?.revision, 99);
|
|
292
|
+
const closed = choice(true, 3);
|
|
293
|
+
folds = setTurnFold(folds, "view", "view:merged", closed);
|
|
294
|
+
for (const alias of ["a", "b", "alias", "merged"]) assert.equal(folds.get(`view:${alias}`), closed);
|
|
295
|
+
assert.equal(folds.get("other:merged")?.revision, 99);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("alias relationships stay inside the bounded fold cache", () => {
|
|
300
|
+
let folds = new Map<string, TurnFold>();
|
|
301
|
+
for (let index = 0; index < 7500; index++) folds.set(`view:${index}`, choice(false, index + 1));
|
|
302
|
+
const rebased = rebaseTurnFolds(folds, "view", new Map([["0", "alias"]]));
|
|
303
|
+
assert.equal(folds.size, 7500, "rebasing does not mutate the prior state");
|
|
304
|
+
assert.equal(rebased.size, 7500);
|
|
305
|
+
assert.equal(rebased.has("view:0"), false);
|
|
306
|
+
folds = setTurnFold(rebased, "view", "view:alias", choice(true, 8000));
|
|
307
|
+
assert.equal(folds.size, 7500);
|
|
308
|
+
assert.equal(folds.has("view:0"), false, "updating an alias never resurrects evicted keys");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("at the fold budget, successive merges keep one cache order and the latest alias choice", () => {
|
|
312
|
+
const older = choice(true, 1);
|
|
313
|
+
const newer = choice(false, 2);
|
|
314
|
+
const unrelated = choice(true, 99);
|
|
315
|
+
let saved = new Map<string, TurnFold>([["view:a", older], ["view:b", newer], ["other:unchanged", unrelated], ["view:b-alias", newer]]);
|
|
316
|
+
for (let index = 4; index < 7500; index++) saved.set(`view:unrelated-${index}`, { revision: 0 });
|
|
317
|
+
let local = new Map(saved);
|
|
318
|
+
const commit = (rebases: [string, string][]) => {
|
|
319
|
+
// MessageList commits once to the saved cache and adopts its exact snapshot.
|
|
320
|
+
saved = rebaseTurnFolds(saved, "view", new Map(rebases));
|
|
321
|
+
local = saved;
|
|
322
|
+
};
|
|
323
|
+
commit([["a", "b"]]);
|
|
324
|
+
commit([["unrelated-4", "new-alias"]]);
|
|
325
|
+
assert.equal(saved.size, 7500);
|
|
326
|
+
assert.equal(saved.has("view:a"), false);
|
|
327
|
+
assert.equal(saved.get("other:unchanged"), unrelated);
|
|
328
|
+
assert.equal(saved.has("view:b"), true);
|
|
329
|
+
const chosen = choice(true, 3);
|
|
330
|
+
saved = setTurnFold(saved, "view", "view:b", chosen);
|
|
331
|
+
local = saved;
|
|
332
|
+
assert.equal(local.get("view:b-alias"), chosen);
|
|
333
|
+
assert.equal(new Map(saved).get("view:b-alias"), chosen, "remount sees the same latest choice");
|
|
334
|
+
assert.deepEqual([...local], [...saved]);
|
|
335
|
+
assert.equal(saved.size, 7500);
|
|
336
|
+
assert.equal(saved.has("view:a"), false, "choosing a retained alias does not restore an evicted one");
|
|
337
|
+
assert.equal(saved.get("other:unchanged"), unrelated);
|
|
338
|
+
});
|
package/web/src/turns.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Item } from "./transcript.ts";
|
|
2
|
+
import { userTextSummary } from "./user-text.ts";
|
|
3
|
+
|
|
4
|
+
export type TranscriptTurn = {
|
|
5
|
+
key: string;
|
|
6
|
+
items: Item[];
|
|
7
|
+
summary: string;
|
|
8
|
+
result?: Extract<Item, { kind: "result" }>;
|
|
9
|
+
active: boolean;
|
|
10
|
+
queued: boolean;
|
|
11
|
+
latest?: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Input boundaries define UI turns; SDK results update their owners without regrouping inputs. */
|
|
16
|
+
export function groupTurns(items: Item[], active: boolean, atLatest = false): TranscriptTurn[] {
|
|
17
|
+
const turns: TranscriptTurn[] = [];
|
|
18
|
+
const prompts = new Map<string, TranscriptTurn>();
|
|
19
|
+
const completed = new Set<TranscriptTurn>();
|
|
20
|
+
const replied = new Set<TranscriptTurn>();
|
|
21
|
+
const summarized = new Set<TranscriptTurn>();
|
|
22
|
+
let current: TranscriptTurn | undefined;
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
if (item.kind === "result") {
|
|
25
|
+
const owners = new Set((item.userMessageIds ?? []).map((id) => prompts.get(id)).filter((turn) => turn !== undefined));
|
|
26
|
+
// One SDK run may consume several UI turns. Keep one fee/result row, on
|
|
27
|
+
// the latest consumed input, and never close a still-queued later input.
|
|
28
|
+
const owner = turns.findLast((turn) => owners.has(turn)) ?? current;
|
|
29
|
+
if (owner) {
|
|
30
|
+
if (!owners.size) owners.add(owner);
|
|
31
|
+
for (const turn of owners) {
|
|
32
|
+
completed.add(turn);
|
|
33
|
+
turn.error = item.ok ? undefined : item.error || "本轮执行失败";
|
|
34
|
+
}
|
|
35
|
+
owner.result = item;
|
|
36
|
+
owner.items.push(item);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!current || (item.kind === "user" ? replied.has(current) || completed.has(current) : completed.has(current))) {
|
|
41
|
+
current = { key: item.key, items: [], summary: "后台调用", active: false, queued: false };
|
|
42
|
+
turns.push(current);
|
|
43
|
+
}
|
|
44
|
+
current.items.push(item);
|
|
45
|
+
if (item.kind === "user") {
|
|
46
|
+
if (!summarized.has(current)) {
|
|
47
|
+
const summary = userTextSummary(item.text);
|
|
48
|
+
if (summary) {
|
|
49
|
+
current.summary = summary.slice(0, 120);
|
|
50
|
+
summarized.add(current);
|
|
51
|
+
} else if (item.images) {
|
|
52
|
+
current.summary = "图片输入";
|
|
53
|
+
summarized.add(current);
|
|
54
|
+
}
|
|
55
|
+
else if (current.items.length === 1) current.summary = "会话输入";
|
|
56
|
+
}
|
|
57
|
+
prompts.set(item.key, current);
|
|
58
|
+
} else if (item.kind === "result") {
|
|
59
|
+
current.result = item;
|
|
60
|
+
current.error = item.ok ? undefined : item.error || "本轮执行失败";
|
|
61
|
+
completed.add(current);
|
|
62
|
+
} else {
|
|
63
|
+
replied.add(current);
|
|
64
|
+
if (item.kind === "notice" && item.tone === "error") {
|
|
65
|
+
current.error = item.text;
|
|
66
|
+
if (!current.items.some((other) => other.kind === "user")) current.summary = "执行失败";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (active) {
|
|
71
|
+
// A foreground tool can still be running above newer queued inputs.
|
|
72
|
+
const executing = turns.findLast((turn) => !completed.has(turn) && turn.items.some((item) =>
|
|
73
|
+
((item.kind === "text" || item.kind === "thinking") && item.streaming)
|
|
74
|
+
|| (item.kind === "tool" && !item.result && (!item.task || item.task.status === "running"))));
|
|
75
|
+
const start = executing ? turns.indexOf(executing) : turns.length - 1;
|
|
76
|
+
for (let index = start; index >= 0 && index < turns.length; index++) {
|
|
77
|
+
const turn = turns[index]!;
|
|
78
|
+
if (completed.has(turn) || turn.error) continue;
|
|
79
|
+
turn.active = true;
|
|
80
|
+
turn.queued = index > start && !replied.has(turn);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Only the real conversation tail stays open after completion. An older
|
|
84
|
+
// history page's last visible turn is not necessarily the latest turn.
|
|
85
|
+
if (atLatest && turns.length) turns[turns.length - 1]!.latest = true;
|
|
86
|
+
return turns;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function defaultTurnCollapsed(turn: TranscriptTurn): boolean {
|
|
90
|
+
return !turn.active && !turn.latest && !turn.error;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Default folds hide the process but retain the final assistant reply. An
|
|
94
|
+
* explicit fold always hides the whole turn, including the latest reply.
|
|
95
|
+
* Return original items so row/annotation identities stay the same when the
|
|
96
|
+
* process is expanded. */
|
|
97
|
+
export function visibleTurnItems(turn: TranscriptTurn, folded?: boolean): Item[] {
|
|
98
|
+
const collapsed = folded ?? defaultTurnCollapsed(turn);
|
|
99
|
+
if (!collapsed) return turn.items;
|
|
100
|
+
if (folded === true) return [];
|
|
101
|
+
return trailingReply(turn);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The trailing text suffix of a turn, with result footers skipped. */
|
|
105
|
+
function trailingReply(turn: TranscriptTurn): Item[] {
|
|
106
|
+
let end = turn.items.length;
|
|
107
|
+
while (end && turn.items[end - 1]!.kind === "result") end--;
|
|
108
|
+
const last = turn.items[end - 1];
|
|
109
|
+
// Do not pull an earlier commentary forward after a tool-only completion,
|
|
110
|
+
// notice, or unfinished draft. Result rows are the only footer to skip.
|
|
111
|
+
if (last?.kind !== "text" || last.streaming) return [];
|
|
112
|
+
let start = end - 1;
|
|
113
|
+
// Transcript text keys are `${frameUuid}:${blockIndex}`. A reply can contain
|
|
114
|
+
// several text blocks; keep its entire trailing text suffix, not just one.
|
|
115
|
+
const frame = /^(.*):\d+$/.exec(last.key)?.[1];
|
|
116
|
+
while (frame && start > 0) {
|
|
117
|
+
const item = turn.items[start - 1]!;
|
|
118
|
+
if (item.kind !== "text" || item.streaming || /^(.*):\d+$/.exec(item.key)?.[1] !== frame) break;
|
|
119
|
+
start--;
|
|
120
|
+
}
|
|
121
|
+
return turn.items.slice(start, end);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A paged window can start inside a turn. Match its surviving items when an
|
|
125
|
+
* earlier prompt arrives so explicit folds and header anchors follow that turn. */
|
|
126
|
+
export function turnRebases(previous: TranscriptTurn[], current: TranscriptTurn[]): Map<string, string> {
|
|
127
|
+
const owners = new Map(current.flatMap((turn) => turn.items.map((item) => [item.key, turn.key] as const)));
|
|
128
|
+
const rebases = new Map<string, string>();
|
|
129
|
+
for (const turn of previous) {
|
|
130
|
+
const next = turn.items.map((item) => owners.get(item.key)).find((key) => key !== undefined);
|
|
131
|
+
if (next && next !== turn.key) rebases.set(turn.key, next);
|
|
132
|
+
}
|
|
133
|
+
return rebases;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function rebaseTurnRow(key: string, rebases: ReadonlyMap<string, string>): string {
|
|
137
|
+
for (const prefix of ["turn-", "changes-", "running-"]) {
|
|
138
|
+
if (key.startsWith(prefix)) return prefix + (rebases.get(key.slice(prefix.length)) ?? key.slice(prefix.length));
|
|
139
|
+
}
|
|
140
|
+
return key;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Aliases retain the revision of the user's choice. Creating an alias is not a
|
|
144
|
+
* new choice: a later merge must still prefer the most recent explicit action. */
|
|
145
|
+
export type TurnFold = { collapsed?: boolean; revision: number };
|
|
146
|
+
|
|
147
|
+
/** All known aliases share one choice object, including default-only aliases.
|
|
148
|
+
* Update the whole group before a view unmounts; a remount may start at any alias. */
|
|
149
|
+
export function setTurnFold(folds: Map<string, TurnFold>, namespace: string, key: string, choice: TurnFold): Map<string, TurnFold> {
|
|
150
|
+
const previous = folds.get(key);
|
|
151
|
+
const next = new Map(folds);
|
|
152
|
+
if (previous) for (const [alias, value] of folds) {
|
|
153
|
+
if (alias.startsWith(`${namespace}:`) && value === previous) next.set(alias, choice);
|
|
154
|
+
}
|
|
155
|
+
next.delete(key); next.set(key, choice);
|
|
156
|
+
while (next.size > 7500) next.delete(next.keys().next().value!);
|
|
157
|
+
return next;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function rebaseTurnFolds(folds: Map<string, TurnFold>, namespace: string, rebases: ReadonlyMap<string, string>): Map<string, TurnFold> {
|
|
161
|
+
let next = folds;
|
|
162
|
+
for (const [oldKey, newKey] of rebases) {
|
|
163
|
+
const old = `${namespace}:${oldKey}`;
|
|
164
|
+
const key = `${namespace}:${newKey}`;
|
|
165
|
+
const value = next.get(old);
|
|
166
|
+
const target = next.get(key);
|
|
167
|
+
if (value && value === target) continue;
|
|
168
|
+
const winner = value && target ? value.revision > target.revision ? value : target : value ?? target ?? { revision: 0 };
|
|
169
|
+
if (next === folds) next = new Map(folds);
|
|
170
|
+
for (const [alias, choice] of next) {
|
|
171
|
+
if (alias.startsWith(`${namespace}:`) && (choice === value || choice === target)) next.set(alias, winner);
|
|
172
|
+
}
|
|
173
|
+
next.set(old, winner); next.set(key, winner);
|
|
174
|
+
}
|
|
175
|
+
if (next.size > 7500 && next === folds) next = new Map(folds);
|
|
176
|
+
while (next.size > 7500) next.delete(next.keys().next().value!);
|
|
177
|
+
return next;
|
|
178
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function cx(...parts: (string | false | null | undefined)[]) {
|
|
4
|
+
return parts.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "outline-danger";
|
|
8
|
+
export type ButtonSize = "sm" | "md";
|
|
9
|
+
|
|
10
|
+
/** Shared control geometry: every button, select trigger and input uses these heights. */
|
|
11
|
+
export const CONTROL = {
|
|
12
|
+
sm: "h-8 md:h-7 px-2.5 text-xs gap-1.5",
|
|
13
|
+
md: "h-10 md:h-8 px-3 text-sm gap-2",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const VARIANT: Record<ButtonVariant, string> = {
|
|
17
|
+
primary: "bg-accent text-accent-text hover:bg-accent-hover border border-transparent shadow-sm",
|
|
18
|
+
secondary: "bg-surface text-text border border-border hover:bg-surface-2 hover:border-muted/40",
|
|
19
|
+
ghost: "text-muted hover:text-text hover:bg-surface-2 border border-transparent",
|
|
20
|
+
danger: "bg-danger text-bg border border-transparent hover:opacity-90",
|
|
21
|
+
"outline-danger": "text-danger border border-border bg-surface hover:border-danger/60 hover:bg-danger/5",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
25
|
+
variant?: ButtonVariant;
|
|
26
|
+
size?: ButtonSize;
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
/** Square, icon-only button. */
|
|
29
|
+
iconOnly?: boolean;
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
|
|
34
|
+
{ variant = "secondary", size = "md", icon, iconOnly, loading, className, children, disabled, type = "button", ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
ref={ref}
|
|
40
|
+
type={type}
|
|
41
|
+
disabled={disabled || loading}
|
|
42
|
+
className={cx(
|
|
43
|
+
"inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors [&>svg]:shrink-0",
|
|
44
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:pointer-events-none disabled:opacity-45",
|
|
45
|
+
CONTROL[size],
|
|
46
|
+
iconOnly && (size === "sm" ? "w-8 px-0! md:w-7" : "w-10 px-0! md:w-8"),
|
|
47
|
+
VARIANT[variant],
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
{...rest}
|
|
51
|
+
>
|
|
52
|
+
{loading ? <Spinner /> : icon}
|
|
53
|
+
{children}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export function Spinner({ className }: { className?: string }) {
|
|
59
|
+
return <span className={cx("inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent", className)} aria-hidden />;
|
|
60
|
+
}
|