@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,288 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { HttpBindingOverrides, HttpChannelBinding, HttpChannelDirection, HttpChannelsInfo, HttpExtraBody, HttpSource, HttpSourceDetails, HttpSourceInput, HttpSubscribeMethod, HttpSourceKind } from "@shared/http-channels.ts";
|
|
5
|
+
import { api } from "../api.ts";
|
|
6
|
+
import { Modal } from "./Modal.tsx";
|
|
7
|
+
import { useConfirm } from "./Confirmation.tsx";
|
|
8
|
+
import { useUnsavedChanges } from "../unsaved.tsx";
|
|
9
|
+
import { Button } from "../ui/Button.tsx";
|
|
10
|
+
import { Select } from "../ui/Select.tsx";
|
|
11
|
+
import { Field, inputCls, monoInputCls, monoTextareaCls, Segmented, Tag } from "../ui/Form.tsx";
|
|
12
|
+
|
|
13
|
+
type SourceDraft = { id?: string; kind: HttpSourceKind; brainId: string; brainVoice: string; name: string; url: string; timeout: string; subscribeMethod: HttpSubscribeMethod; headers: string; hasHeaders: boolean; extraBody: string };
|
|
14
|
+
const emptySource = (): SourceDraft => ({ kind: "http", brainId: "", brainVoice: "", name: "", url: "", timeout: "120", subscribeMethod: "GET", headers: "", hasHeaders: false, extraBody: "" });
|
|
15
|
+
type BrainDraft = { brainId: string; brainVoice: string; inheritVoice: boolean };
|
|
16
|
+
const emptyBrain = (): BrainDraft => ({ brainId: "", brainVoice: "", inheritVoice: true });
|
|
17
|
+
const brainOverrides = (draft: BrainDraft): HttpBindingOverrides => ({ brainId: draft.brainId.trim() || null, brainVoice: draft.inheritVoice ? null : draft.brainVoice });
|
|
18
|
+
|
|
19
|
+
function BrainBindingFields({ source, draft, onChange }: { source: HttpSource; draft: BrainDraft; onChange: (next: BrainDraft) => void }) {
|
|
20
|
+
return <div className="space-y-3">
|
|
21
|
+
<Field label="brain_id" hint={source.brainId ? "留空继承 HTTP 源配置。" : "请在激活前填写。"}>
|
|
22
|
+
<input aria-label="会话 brain_id" className={monoInputCls} value={draft.brainId} maxLength={256} placeholder="填写 brain_id" onChange={(event) => onChange({ ...draft, brainId: event.target.value })} />
|
|
23
|
+
</Field>
|
|
24
|
+
<Field label="x-brain-voice" group hint="仅用于推送。取消勾选后留空则不发送 voice。">
|
|
25
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
26
|
+
<input aria-label="会话 x-brain-voice" value={draft.brainVoice} maxLength={1024} disabled={draft.inheritVoice} className={`${monoInputCls} flex-1 disabled:cursor-not-allowed disabled:bg-surface-2`} onChange={(event) => onChange({ ...draft, brainVoice: event.target.value })} />
|
|
27
|
+
<label className="flex min-h-10 shrink-0 cursor-pointer items-center gap-2 whitespace-nowrap text-xs md:min-h-8">
|
|
28
|
+
<input type="checkbox" checked={draft.inheritVoice} onChange={(event) => onChange({ ...draft, inheritVoice: event.target.checked })} className="size-4 accent-accent" />
|
|
29
|
+
使用源配置
|
|
30
|
+
</label>
|
|
31
|
+
</div>
|
|
32
|
+
</Field>
|
|
33
|
+
</div>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function EditBrainBinding({ binding, source, onClose }: { binding: HttpChannelBinding; source: HttpSource; onClose: () => void }) {
|
|
37
|
+
const [draft, setDraft] = useState<BrainDraft>({ brainId: binding.brainId ?? "", brainVoice: binding.brainVoice ?? "", inheritVoice: binding.brainVoice == null });
|
|
38
|
+
const [original] = useState(() => JSON.stringify(draft));
|
|
39
|
+
const { discard } = useUnsavedChanges(JSON.stringify(draft) !== original, "会话 brain 配置");
|
|
40
|
+
const qc = useQueryClient();
|
|
41
|
+
const save = useMutation({
|
|
42
|
+
mutationFn: () => api(`/api/http-channels/bindings/${binding.id}`, { method: "PATCH", json: brainOverrides(draft) }),
|
|
43
|
+
onSuccess: () => { void qc.invalidateQueries({ queryKey: ["http-channels"] }); onClose(); },
|
|
44
|
+
});
|
|
45
|
+
return <Modal title="会话 brain 配置" onClose={() => { if (!save.isPending) void discard().then((ok) => { if (ok) onClose(); }); }} testId="brain-binding-dialog">
|
|
46
|
+
<form className="space-y-4" onSubmit={(event) => { event.preventDefault(); save.mutate(); }}>
|
|
47
|
+
<p className="break-words text-sm text-muted">{source.name} · 配置仅影响这条注册。</p>
|
|
48
|
+
<fieldset disabled={save.isPending} className="min-w-0"><BrainBindingFields source={source} draft={draft} onChange={setDraft} /></fieldset>
|
|
49
|
+
{save.error && <p role="alert" className="text-sm text-danger">{save.error.message}</p>}
|
|
50
|
+
<div className="flex justify-end gap-2"><Button disabled={save.isPending} onClick={() => void discard().then((ok) => { if (ok) onClose(); })}>取消</Button><Button type="submit" variant="primary" loading={save.isPending}>保存配置</Button></div>
|
|
51
|
+
</form>
|
|
52
|
+
</Modal>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Configuration persists on the server; closing this panel never stops an active binding. */
|
|
56
|
+
export function SessionChannelsDialog({ pid, sid, onClose }: { pid: string; sid: string; onClose: () => void }) {
|
|
57
|
+
const qc = useQueryClient();
|
|
58
|
+
const confirm = useConfirm();
|
|
59
|
+
const info = useQuery({ queryKey: ["http-channels"], queryFn: () => api<HttpChannelsInfo>("/api/http-channels"), refetchInterval: 2000 });
|
|
60
|
+
const [tab, setTab] = useState<"session" | "all" | "sources">("session");
|
|
61
|
+
const [sourceId, setSourceId] = useState("");
|
|
62
|
+
const [direction, setDirection] = useState<HttpChannelDirection>("subscribe");
|
|
63
|
+
const [brainDraft, setBrainDraft] = useState<BrainDraft>(emptyBrain);
|
|
64
|
+
const [editingBinding, setEditingBinding] = useState<HttpChannelBinding | null>(null);
|
|
65
|
+
const [editor, setEditor] = useState<SourceDraft | null>(null);
|
|
66
|
+
const [opening, setOpening] = useState(false);
|
|
67
|
+
const [openError, setOpenError] = useState<string>();
|
|
68
|
+
const editorEpoch = useRef(0);
|
|
69
|
+
const [original, setOriginal] = useState("");
|
|
70
|
+
const [notice, setNotice] = useState<string>();
|
|
71
|
+
const { discard } = useUnsavedChanges(!!editor && JSON.stringify(editor) !== original, "HTTP 源配置");
|
|
72
|
+
const close = async () => { if (await discard()) { editorEpoch.current++; setOpening(false); onClose(); } };
|
|
73
|
+
const cancelEditor = async () => { if (await discard()) { editorEpoch.current++; setOpening(false); setEditor(null); } };
|
|
74
|
+
const changeTab = async (next: typeof tab) => {
|
|
75
|
+
if (next === tab) return;
|
|
76
|
+
if (await discard()) { editorEpoch.current++; setOpening(false); setEditor(null); setTab(next); }
|
|
77
|
+
};
|
|
78
|
+
const editSource = async (source?: HttpSource) => {
|
|
79
|
+
if (!await discard()) return;
|
|
80
|
+
const epoch = ++editorEpoch.current;
|
|
81
|
+
setEditor(null);
|
|
82
|
+
setTab("sources");
|
|
83
|
+
setOpening(true);
|
|
84
|
+
setOpenError(undefined);
|
|
85
|
+
action.reset();
|
|
86
|
+
try {
|
|
87
|
+
const details = source ? await api<HttpSourceDetails>(`/api/http-channels/sources/${source.id}`, { cache: "no-store" }) : undefined;
|
|
88
|
+
if (editorEpoch.current !== epoch) return;
|
|
89
|
+
const draft = details ? {
|
|
90
|
+
id: details.id, kind: details.kind, brainId: details.brainId ?? "", brainVoice: details.brainVoice ?? "", name: details.name, url: details.url, timeout: String(details.timeoutMs / 1000), subscribeMethod: details.subscribeMethod,
|
|
91
|
+
headers: details.hasHeaders ? JSON.stringify(details.headers, null, 2) : "", hasHeaders: details.hasHeaders,
|
|
92
|
+
extraBody: details.hasExtraBody ? JSON.stringify(details.extraBody, null, 2) : "",
|
|
93
|
+
} : emptySource();
|
|
94
|
+
setEditor(draft);
|
|
95
|
+
setOriginal(JSON.stringify(draft));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (editorEpoch.current === epoch) setOpenError(error instanceof Error ? error.message : String(error));
|
|
98
|
+
} finally {
|
|
99
|
+
if (editorEpoch.current === epoch) setOpening(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const action = useMutation({
|
|
103
|
+
mutationFn: (fn: () => Promise<unknown>) => fn(),
|
|
104
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ["http-channels"] }),
|
|
105
|
+
});
|
|
106
|
+
const sources = info.data?.sources ?? [];
|
|
107
|
+
const selectedSource = sources.find((source) => source.id === sourceId);
|
|
108
|
+
const selectedBrain = selectedSource?.kind === "brain-api";
|
|
109
|
+
const isBrain = (binding: HttpChannelBinding) => sources.find((source) => source.id === binding.sourceId)?.kind === "brain-api";
|
|
110
|
+
const bindingType = (binding: HttpChannelBinding) => isBrain(binding) ? "brain-api 订阅" : binding.direction === "subscribe" ? "订阅" : "推送";
|
|
111
|
+
const bindings = info.data?.bindings.filter((binding) => tab === "all" || binding.sessionId === sid) ?? [];
|
|
112
|
+
const sourceName = (id: string) => sources.find((source) => source.id === id)?.name ?? "HTTP 源";
|
|
113
|
+
const error = action.error ?? openError ?? info.error;
|
|
114
|
+
|
|
115
|
+
const activate = (binding: HttpChannelBinding) => action.mutate(async () => {
|
|
116
|
+
if (binding.active && !(await confirm({
|
|
117
|
+
title: "反激活这条注册?",
|
|
118
|
+
description: `将停止“${sourceName(binding.sourceId)}”的${isBrain(binding) ? "订阅与推送" : bindingType(binding)},清除尚未投递的内容。已交给会话的消息继续处理。`,
|
|
119
|
+
confirmLabel: "反激活",
|
|
120
|
+
})).confirmed) return;
|
|
121
|
+
const result = await api<{ deactivatedId: string | null }>(`/api/http-channels/bindings/${binding.id}/activation`, { method: "POST", json: { active: !binding.active } });
|
|
122
|
+
const replaced = info.data?.bindings.find((entry) => entry.id === result.deactivatedId);
|
|
123
|
+
setNotice(replaced ? `已激活;最早激活的${bindingType(replaced)}“${sourceName(replaced.sourceId)}”已反激活。` : undefined);
|
|
124
|
+
});
|
|
125
|
+
const removeBinding = (binding: HttpChannelBinding) => action.mutate(async () => {
|
|
126
|
+
if (!(await confirm({ title: "删除这条注册?", description: `会停止“${sourceName(binding.sourceId)}”的${isBrain(binding) ? "订阅与推送" : bindingType(binding)}并删除注册配置。`, confirmLabel: "删除注册", danger: true })).confirmed) return;
|
|
127
|
+
await api(`/api/http-channels/bindings/${binding.id}`, { method: "DELETE" });
|
|
128
|
+
});
|
|
129
|
+
const removeSource = (source: HttpSource) => action.mutate(async () => {
|
|
130
|
+
if (!(await confirm({ title: "删除 HTTP 源?", description: `“${source.name}”以及所有会话对它的注册都会删除,正在执行的请求会取消。`, confirmLabel: "删除源", danger: true })).confirmed) return;
|
|
131
|
+
await api(`/api/http-channels/sources/${source.id}`, { method: "DELETE" });
|
|
132
|
+
if (sourceId === source.id) setSourceId("");
|
|
133
|
+
});
|
|
134
|
+
const saveSource = () => action.mutate(async () => {
|
|
135
|
+
if (!editor) return;
|
|
136
|
+
const submitted = editor;
|
|
137
|
+
const epoch = editorEpoch.current;
|
|
138
|
+
let headers: Record<string, string> | undefined = editor.hasHeaders ? {} : undefined;
|
|
139
|
+
if (editor.headers.trim()) {
|
|
140
|
+
try { headers = JSON.parse(editor.headers); } catch { throw new Error("请求头不是有效 JSON"); }
|
|
141
|
+
}
|
|
142
|
+
let extraBody: HttpExtraBody = {};
|
|
143
|
+
if (editor.kind === "http" && editor.extraBody.trim()) {
|
|
144
|
+
try { extraBody = JSON.parse(editor.extraBody); } catch { throw new Error("Extra body 不是有效 JSON"); }
|
|
145
|
+
}
|
|
146
|
+
const input: HttpSourceInput = { name: editor.name, url: editor.url, kind: editor.kind, brainId: editor.brainId, ...(editor.kind === "brain-api" ? { brainVoice: editor.brainVoice } : {}), timeoutMs: Number(editor.timeout) * 1000, subscribeMethod: editor.subscribeMethod, extraBody, ...(headers !== undefined ? { headers } : {}) };
|
|
147
|
+
const source = await api<HttpSource>(`/api/http-channels/sources${editor.id ? `/${editor.id}` : ""}`, { method: editor.id ? "PUT" : "POST", json: input });
|
|
148
|
+
if (editorEpoch.current !== epoch) return;
|
|
149
|
+
setSourceId(source.id);
|
|
150
|
+
if (source.id !== sourceId) setBrainDraft(emptyBrain());
|
|
151
|
+
setNotice(`HTTP 源“${source.name}”已保存。`);
|
|
152
|
+
setOriginal(JSON.stringify({ ...submitted, id: source.id, hasHeaders: source.hasHeaders }));
|
|
153
|
+
setEditor((current) => {
|
|
154
|
+
if (editorEpoch.current !== epoch || !current) return current;
|
|
155
|
+
// Retain edits made while saving; a newly created source becomes an update next time.
|
|
156
|
+
return current === submitted ? null : { ...current, id: source.id, hasHeaders: source.hasHeaders };
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Modal title="推送与订阅" onClose={() => void close()} testId="http-channels-dialog">
|
|
162
|
+
<div className="space-y-4">
|
|
163
|
+
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted">
|
|
164
|
+
<span>已激活 {info.data?.activeCount ?? 0} / {info.data?.activeLimit ?? 32}</span>
|
|
165
|
+
<span>待投递 {info.data?.inboxSize ?? 0} / {info.data?.inboxLimit ?? 1024}</span>
|
|
166
|
+
</div>
|
|
167
|
+
<Segmented label="通道管理" value={tab} onChange={(next) => void changeTab(next)} stretch options={[
|
|
168
|
+
{ value: "session", label: "本会话" }, { value: "all", label: "全部注册" }, { value: "sources", label: "HTTP 源" },
|
|
169
|
+
]} />
|
|
170
|
+
{error && <p role="alert" className="whitespace-pre-wrap break-words text-sm text-danger">{error instanceof Error ? error.message : String(error)}</p>}
|
|
171
|
+
{notice && <p role="status" className="rounded-lg bg-surface-2 px-3 py-2 text-sm">{notice}</p>}
|
|
172
|
+
{info.isPending && <p className="text-sm text-muted">正在加载…</p>}
|
|
173
|
+
{tab !== "sources" && (
|
|
174
|
+
<>
|
|
175
|
+
<p className="text-xs leading-relaxed text-muted">推送和订阅共用 32 个激活名额,超出时自动反激活最早的一条。关闭网页不影响服务端运行。</p>
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
{bindings.map((binding) => (
|
|
178
|
+
<div key={binding.id} className="rounded-lg border border-border bg-surface px-3 py-2.5" data-testid="http-binding" data-active={binding.active}>
|
|
179
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
180
|
+
<Tag tone={binding.active ? "accent" : "neutral"}>{bindingType(binding)}</Tag>
|
|
181
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium">{sourceName(binding.sourceId)}</span>
|
|
182
|
+
<Tag tone={binding.active ? "ok" : "neutral"}>{binding.active ? "已激活" : "未激活"}</Tag>
|
|
183
|
+
</div>
|
|
184
|
+
{tab === "all" && <Link to={`/p/${binding.projectId}/s/${binding.sessionId}`} onClick={onClose} className="mt-1 block text-xs text-accent">{binding.sessionId === sid ? "当前会话" : `会话 ${binding.sessionId.slice(0, 8)}`}</Link>}
|
|
185
|
+
{isBrain(binding) && <p className="mt-1 break-words text-xs text-muted">brain_id: {binding.brainId ?? sources.find((source) => source.id === binding.sourceId)?.brainId ?? "未设置"}{binding.brainId ? "(会话覆写)" : "(继承源)"} · voice: {binding.brainVoice === "" ? "不发送" : binding.brainVoice ?? sources.find((source) => source.id === binding.sourceId)?.brainVoice ?? "未设置"}</p>}
|
|
186
|
+
{binding.stats ? (
|
|
187
|
+
<p className="mt-1.5 text-xs text-muted">{isBrain(binding) ? `已接收 ${binding.stats.received} · 已推送 ${binding.stats.pushed}` : `已${binding.direction === "subscribe" ? "投递" : "推送"} ${binding.stats.delivered}`} · 待处理 {binding.stats.pending} · 去重 {binding.stats.duplicates}{binding.stats.dropped > 0 ? ` · 未发送 ${binding.stats.dropped}` : ""}</p>
|
|
188
|
+
) : binding.inactiveReason && <p className="mt-1.5 text-xs text-muted">{binding.inactiveReason}</p>}
|
|
189
|
+
{isBrain(binding) ? <>
|
|
190
|
+
{binding.stats?.lastPullError && <p className="mt-1.5 break-words text-xs text-danger">订阅:{binding.stats.lastPullError}</p>}
|
|
191
|
+
{binding.stats?.lastPushError && <p className="mt-1.5 break-words text-xs text-danger">推送:{binding.stats.lastPushError}</p>}
|
|
192
|
+
</> : binding.stats?.lastError && <p className="mt-1.5 break-words text-xs text-danger">{binding.stats.lastError}</p>}
|
|
193
|
+
<div className="mt-2 flex justify-end gap-2">
|
|
194
|
+
{isBrain(binding) && <Button size="sm" disabled={action.isPending} onClick={() => setEditingBinding(binding)}>配置</Button>}
|
|
195
|
+
<Button size="sm" variant="ghost" disabled={action.isPending} onClick={() => removeBinding(binding)}>删除</Button>
|
|
196
|
+
<Button size="sm" variant={binding.active ? "secondary" : "primary"} disabled={action.isPending} onClick={() => activate(binding)}>{binding.active ? "反激活" : "激活"}</Button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
{!bindings.length && <p className="rounded-lg border border-dashed border-border py-5 text-center text-sm text-muted">暂无注册</p>}
|
|
201
|
+
</div>
|
|
202
|
+
{tab === "session" && (
|
|
203
|
+
<form className="space-y-3 border-t border-border pt-3" onSubmit={(event) => {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
action.mutate(async () => {
|
|
206
|
+
await api(`/api/projects/${pid}/sessions/${sid}/http-bindings`, { method: "POST", json: { sourceId, direction: selectedBrain ? "subscribe" : direction, ...(selectedBrain ? brainOverrides(brainDraft) : {}) } });
|
|
207
|
+
});
|
|
208
|
+
}}>
|
|
209
|
+
<Field label="HTTP 源" group>
|
|
210
|
+
<Select label="HTTP 源" value={sourceId} onChange={(id) => { setSourceId(id); setBrainDraft(emptyBrain()); }} className="w-full" emptyText="请先添加 HTTP 源" options={[
|
|
211
|
+
{ value: "", label: "选择 HTTP 源…" }, ...sources.map((source) => ({ value: source.id, label: source.name, hint: source.url })),
|
|
212
|
+
]} />
|
|
213
|
+
</Field>
|
|
214
|
+
{!selectedBrain && <Segmented label="注册方向" value={direction} onChange={setDirection} stretch options={[{ value: "subscribe", label: "订阅 → 会话" }, { value: "push", label: "会话 → 推送" }]} />}
|
|
215
|
+
{selectedBrain && <BrainBindingFields key={sourceId} source={selectedSource!} draft={brainDraft} onChange={setBrainDraft} />}
|
|
216
|
+
<p className="text-xs leading-relaxed text-muted">{selectedBrain ? "brain-api 订阅:同时接收消息和推送会话正文,占 1 个激活名额。" : direction === "subscribe" ? "接收外部消息,发送到当前会话。" : "将会话正文发送到 HTTP 源。"}</p>
|
|
217
|
+
<div className="flex justify-between gap-2">
|
|
218
|
+
<Button variant="ghost" onClick={() => void editSource()}>添加源</Button>
|
|
219
|
+
<Button type="submit" variant="primary" disabled={!sourceId} loading={action.isPending}>注册</Button>
|
|
220
|
+
</div>
|
|
221
|
+
<p className="text-xs text-muted">注册后点击“激活”开始运行。</p>
|
|
222
|
+
</form>
|
|
223
|
+
)}
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
{tab === "sources" && (
|
|
227
|
+
<>
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
{sources.map((source) => (
|
|
230
|
+
<div key={source.id} className="rounded-lg border border-border bg-surface px-3 py-2.5">
|
|
231
|
+
<div className="flex flex-wrap items-center gap-2"><span className="min-w-0 flex-1 truncate text-sm font-medium">{source.name}</span>{source.kind === "brain-api" && <Tag>brain-api 订阅</Tag>}{source.hasHeaders && <Tag>已设请求头</Tag>}{source.hasExtraBody && <Tag>Extra body</Tag>}</div>
|
|
232
|
+
<p className="mt-1 break-all font-mono text-xs text-muted">{source.url}</p>
|
|
233
|
+
{source.kind === "brain-api" && <p className="mt-1 break-all font-mono text-xs text-muted">默认 brain_id: {source.brainId ?? "未设置"}</p>}
|
|
234
|
+
<div className="mt-2 flex justify-end gap-2">
|
|
235
|
+
<Button variant="ghost" size="sm" disabled={action.isPending} onClick={() => removeSource(source)}>删除</Button>
|
|
236
|
+
<Button size="sm" disabled={action.isPending || opening} onClick={() => void editSource(source)}>编辑</Button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
{opening && <p className="text-sm text-muted" role="status">正在加载源配置…</p>}
|
|
242
|
+
{!editor && <Button className="w-full" disabled={opening} onClick={() => void editSource()}>添加源</Button>}
|
|
243
|
+
{editor && (
|
|
244
|
+
<form className="space-y-3 border-t border-border pt-3" onSubmit={(event) => { event.preventDefault(); saveSource(); }}>
|
|
245
|
+
<p className="text-sm font-medium">{editor.id ? "编辑源" : "添加源"}</p>
|
|
246
|
+
{!editor.id ? <Segmented label="源类型" value={editor.kind} disabled={action.isPending} onChange={(kind) => { if (!action.isPending) setEditor({ ...editor, kind }); }} stretch options={[{ value: "http", label: "HTTP 源" }, { value: "brain-api", label: "brain-api 订阅" }]} /> : <Tag>{editor.kind === "brain-api" ? "brain-api 订阅" : "HTTP 源"}</Tag>}
|
|
247
|
+
<Field label={editor.kind === "brain-api" ? "名称(可选)" : "名称"}><input aria-label="源名称" value={editor.name} onChange={(event) => setEditor({ ...editor, name: event.target.value })} className={inputCls} maxLength={100} /></Field>
|
|
248
|
+
<Field label={editor.kind === "brain-api" ? "服务 URL" : "HTTP 地址"} hint={editor.kind === "brain-api" ? "填写服务根地址,自动连接两个收发端点。" : undefined}><input aria-label="源地址" value={editor.url} onChange={(event) => setEditor({ ...editor, url: event.target.value })} className={monoInputCls} placeholder={editor.kind === "brain-api" ? "http://localhost:8000" : "https://example.com/messages"} required /></Field>
|
|
249
|
+
{editor.kind === "brain-api" && <Field label="默认 brain_id(可选)" hint="也可以在每个会话注册时填写。"><input aria-label="brain_id" value={editor.brainId} onChange={(event) => setEditor({ ...editor, brainId: event.target.value })} className={monoInputCls} maxLength={256} placeholder="留空,在会话中配置" /></Field>}
|
|
250
|
+
{editor.kind === "brain-api" && <Field label="默认 x-brain-voice(可选)" hint="仅用于推送请求头,会话可以覆写。"><input aria-label="x-brain-voice" value={editor.brainVoice} onChange={(event) => setEditor({ ...editor, brainVoice: event.target.value })} className={monoInputCls} maxLength={1024} /></Field>}
|
|
251
|
+
{editor.kind === "http" && <Field label="订阅请求方式" group hint="仅影响订阅;推送始终使用 POST。">
|
|
252
|
+
<Segmented label="订阅请求方式" value={editor.subscribeMethod} onChange={(subscribeMethod) => setEditor({ ...editor, subscribeMethod })} options={[{ value: "GET", label: "GET" }, { value: "POST", label: "POST" }]} />
|
|
253
|
+
</Field>}
|
|
254
|
+
<Field label="请求超时(秒)" hint="订阅使用长轮询,按外部服务的等待时间设置。"><input aria-label="源超时" type="number" min={1} max={600} value={editor.timeout} onChange={(event) => setEditor({ ...editor, timeout: event.target.value })} className={inputCls} /></Field>
|
|
255
|
+
<Field label="请求头 JSON(可选)" hint="如鉴权 Token。留空清除,Content-Type 固定为 application/json。">
|
|
256
|
+
<textarea aria-label="源请求头" value={editor.headers} onChange={(event) => setEditor({ ...editor, headers: event.target.value })} className={monoTextareaCls} rows={3} placeholder={'{"Authorization":"Bearer …"}'} spellCheck={false} />
|
|
257
|
+
</Field>
|
|
258
|
+
{editor.kind === "http" && <Field label="Extra body JSON(可选)" hint="POST 订阅按原样发送;推送支持 {{text}}。留空清除。">
|
|
259
|
+
<textarea aria-label="源额外请求体" value={editor.extraBody} onChange={(event) => setEditor({ ...editor, extraBody: event.target.value })} className={monoTextareaCls} rows={4} placeholder={editor.subscribeMethod === "POST" ? '{"brain_id":"你的ID"}' : '{"target_id":"频道ID","content":"{{text}}"}'} spellCheck={false} />
|
|
260
|
+
</Field>}
|
|
261
|
+
{error && <p role="alert" className="whitespace-pre-wrap break-words text-sm text-danger">{error instanceof Error ? error.message : String(error)}</p>}
|
|
262
|
+
<div className="flex justify-end gap-2"><Button variant="ghost" onClick={() => void cancelEditor()}>取消</Button><Button variant="primary" type="submit" disabled={!editor.url.trim() || (editor.kind === "http" && !editor.name.trim())} loading={action.isPending}>保存源</Button></div>
|
|
263
|
+
</form>
|
|
264
|
+
)}
|
|
265
|
+
<div className="space-y-2 rounded-lg bg-surface-2 p-3 text-xs leading-relaxed text-muted">
|
|
266
|
+
<p>订阅接收消息,推送发送会话正文。</p>
|
|
267
|
+
<p>brain-api 自动收发。源可设置默认 brain_id 和 voice,会话注册可覆写;激活前需有 brain_id。</p>
|
|
268
|
+
<details>
|
|
269
|
+
<summary className="cursor-pointer">查看消息格式</summary>
|
|
270
|
+
<div className="mt-2 space-y-2">
|
|
271
|
+
<p>订阅无消息时返回 <code>{'{"kind":"empty"}'}</code>,有消息时返回:</p>
|
|
272
|
+
<pre className="whitespace-pre-wrap break-words">{'{"kind":"text","text":"正文"}'}</pre>
|
|
273
|
+
<p>可附带唯一 <code>id</code> 去重;省略时每次响应都作为新消息。正文原样投递。</p>
|
|
274
|
+
<p>订阅可选 GET 或 POST。POST 请求体为 Extra body 原文,不附加推送字段。</p>
|
|
275
|
+
<p>推送使用 POST JSON,Extra body 合并到以下内容:</p>
|
|
276
|
+
<pre className="whitespace-pre-wrap break-words">{'{"kind":"text","id":"消息ID","text":"正文","sessionId":"会话ID"}'}</pre>
|
|
277
|
+
<p>Extra body 的字符串可使用 <code>{'{{text}}'}</code>、<code>{'{{id}}'}</code>、<code>{'{{sessionId}}'}</code>,不能覆盖上述四个字段。</p>
|
|
278
|
+
<p>服务重启后,待投递消息和去重记录会清空。</p>
|
|
279
|
+
</div>
|
|
280
|
+
</details>
|
|
281
|
+
</div>
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
{editingBinding && sources.find((source) => source.id === editingBinding.sourceId) && <EditBrainBinding key={editingBinding.id} binding={editingBinding} source={sources.find((source) => source.id === editingBinding.sourceId)!} onClose={() => setEditingBinding(null)} />}
|
|
286
|
+
</Modal>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { Popover, usePopover } from "../ui/Popover.tsx";
|
|
2
|
+
import { Button } from "../ui/Button.tsx";
|
|
3
|
+
import { BrainIcon } from "../ui/icons.tsx";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Link, useNavigate } from "react-router";
|
|
6
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { BtwInfo, ContextInfo } from "@shared/protocol.ts";
|
|
8
|
+
import { api } from "../api.ts";
|
|
9
|
+
import { useSessionLive } from "../live.ts";
|
|
10
|
+
import { Markdown } from "./Markdown.tsx";
|
|
11
|
+
import { StateDot } from "./Shell.tsx";
|
|
12
|
+
import { ChevronIcon } from "./icons.tsx";
|
|
13
|
+
import { useCompactSession } from "../session-confirmations.ts";
|
|
14
|
+
|
|
15
|
+
// ---------- /btw ----------
|
|
16
|
+
|
|
17
|
+
export function useBtw(pid: string, sid: string) {
|
|
18
|
+
return useQuery({ queryKey: ["btw", pid, sid], queryFn: ({ signal }) => api<BtwInfo>(`/api/projects/${pid}/sessions/${sid}/btw`, { signal }) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useAskBtw(pid: string, sid: string, onAsked?: (id: string) => void) {
|
|
22
|
+
const qc = useQueryClient();
|
|
23
|
+
const current = useRef({ pid, sid });
|
|
24
|
+
current.current = { pid, sid };
|
|
25
|
+
const mounted = useRef(false);
|
|
26
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
27
|
+
return async (question: string) => {
|
|
28
|
+
const { id } = await api<{ id: string }>(`/api/projects/${pid}/sessions/${sid}/btw`, { method: "POST", json: { question } });
|
|
29
|
+
// Opening the selected branch must not wait for every preview to refresh.
|
|
30
|
+
void qc.invalidateQueries({ queryKey: ["btw", pid, sid] });
|
|
31
|
+
if (mounted.current && current.current.pid === pid && current.current.sid === sid) onAsked?.(id);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function usePromote(pid: string) {
|
|
36
|
+
const qc = useQueryClient();
|
|
37
|
+
return useMutation({
|
|
38
|
+
mutationFn: (id: string) => api(`/api/projects/${pid}/sessions/${id}/promote`, { method: "POST" }),
|
|
39
|
+
onSuccess: async (_data, id) => {
|
|
40
|
+
// The server has committed the promotion. Reflect it immediately, before
|
|
41
|
+
// revalidating previews, and prevent old in-flight lists restoring it.
|
|
42
|
+
await qc.cancelQueries({ queryKey: ["btw", pid] });
|
|
43
|
+
qc.setQueriesData<BtwInfo>({ queryKey: ["btw", pid] }, (previous) => previous ? {
|
|
44
|
+
parent: previous.parent,
|
|
45
|
+
children: previous.children.filter((child) => child.id !== id),
|
|
46
|
+
} : previous);
|
|
47
|
+
qc.setQueryData<BtwInfo>(["btw", pid, id], (previous) => ({ parent: null, children: previous?.children ?? [] }));
|
|
48
|
+
qc.invalidateQueries({ queryKey: ["btw", pid] });
|
|
49
|
+
qc.invalidateQueries({ queryKey: ["sessions", pid] });
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Shown on a btw session: where it came from, and the way to make it a normal session. */
|
|
55
|
+
export function BtwBanner({ pid, sid }: { pid: string; sid: string }) {
|
|
56
|
+
const btw = useBtw(pid, sid);
|
|
57
|
+
const promote = usePromote(pid);
|
|
58
|
+
const parent = btw.data?.parent;
|
|
59
|
+
if (!parent) return null;
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border bg-surface-2/60 px-4 py-2 text-sm md:px-6" data-testid="btw-banner">
|
|
62
|
+
<span className="text-muted">btw 旁支 · 后续消息留在此会话</span>
|
|
63
|
+
<Link to={`/p/${pid}/s/${parent}`} className="text-accent underline underline-offset-2">
|
|
64
|
+
返回主会话
|
|
65
|
+
</Link>
|
|
66
|
+
<Button size="sm" className="ml-auto" onClick={() => promote.mutate(sid)} loading={promote.isPending}>
|
|
67
|
+
转为普通会话
|
|
68
|
+
</Button>
|
|
69
|
+
{promote.error && <p role="alert" className="basis-full text-xs text-danger">转换失败:{promote.error.message}</p>}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The side questions asked from this session, listed inside it (not in the session list). */
|
|
75
|
+
export function BtwPanel({ pid, sid, open, setOpen }: { pid: string; sid: string; open: boolean; setOpen: (v: boolean) => void }) {
|
|
76
|
+
const btw = useBtw(pid, sid);
|
|
77
|
+
const promote = usePromote(pid);
|
|
78
|
+
const navigate = useNavigate();
|
|
79
|
+
const ask = useAskBtw(pid, sid, (id) => navigate(`/p/${pid}/s/${id}`));
|
|
80
|
+
const [q, setQ] = useState("");
|
|
81
|
+
const [err, setErr] = useState<string>();
|
|
82
|
+
const [asking, setAsking] = useState(false);
|
|
83
|
+
const submitting = useRef(false);
|
|
84
|
+
const children = btw.data?.children ?? [];
|
|
85
|
+
if (children.length === 0 && !open) return null;
|
|
86
|
+
const runningCount = children.filter((c) => c.state !== "idle").length;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mb-2 rounded-xl border border-border bg-surface" data-testid="btw-panel">
|
|
90
|
+
<button className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm" onClick={() => setOpen(!open)} aria-expanded={open}>
|
|
91
|
+
<ChevronIcon open={open} />
|
|
92
|
+
<span>旁支问答</span>
|
|
93
|
+
<span className="text-muted">{children.length}</span>
|
|
94
|
+
{runningCount > 0 && <span className="size-1.5 animate-pulse rounded-full bg-accent" />}
|
|
95
|
+
</button>
|
|
96
|
+
{open && (
|
|
97
|
+
<div className="max-h-[40dvh] space-y-2 overflow-y-auto overscroll-contain border-t border-border px-3 py-2">
|
|
98
|
+
{children.map((c) => (
|
|
99
|
+
<div key={c.id} className="rounded-lg border border-border px-3 py-2" data-testid="btw-item" data-state={c.state}>
|
|
100
|
+
<div className="flex items-start gap-2">
|
|
101
|
+
<StateDot state={c.state} />
|
|
102
|
+
<div className="min-w-0 flex-1 break-words text-sm font-medium">{c.question}</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="mt-1 text-sm" data-testid="btw-answer">
|
|
105
|
+
{c.answer ? <Markdown text={c.answer} /> : <span className="text-muted">{c.state === "idle" ? "暂无回答" : "正在回答…"}</span>}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="mt-2 flex flex-wrap gap-2 text-xs">
|
|
108
|
+
<Button size="sm" onClick={() => navigate(`/p/${pid}/s/${c.id}`)}>
|
|
109
|
+
继续对话
|
|
110
|
+
</Button>
|
|
111
|
+
<Button size="sm" onClick={() => promote.mutate(c.id)} loading={promote.isPending && promote.variables === c.id} disabled={promote.isPending}>
|
|
112
|
+
转为普通会话
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
{children.length === 0 && <form
|
|
118
|
+
className="flex gap-2"
|
|
119
|
+
onSubmit={async (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
if (!q.trim() || submitting.current) return;
|
|
122
|
+
submitting.current = true; setAsking(true);
|
|
123
|
+
setErr(undefined);
|
|
124
|
+
try {
|
|
125
|
+
await ask(q);
|
|
126
|
+
setQ("");
|
|
127
|
+
} catch (error) {
|
|
128
|
+
setErr(error instanceof Error ? error.message : String(error));
|
|
129
|
+
} finally {
|
|
130
|
+
submitting.current = false; setAsking(false);
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<input
|
|
135
|
+
aria-label="旁支问题"
|
|
136
|
+
placeholder="创建一个旁支,单独讨论这个问题"
|
|
137
|
+
value={q}
|
|
138
|
+
onChange={(e) => setQ(e.target.value)}
|
|
139
|
+
className="h-10 min-w-0 flex-1 rounded-md border border-border bg-bg px-3 text-base outline-none focus:border-accent/60 focus:ring-2 focus:ring-accent/20 md:h-8 md:text-sm"
|
|
140
|
+
/>
|
|
141
|
+
<Button type="submit" variant="primary" disabled={!q.trim()} loading={asking}>
|
|
142
|
+
创建旁支
|
|
143
|
+
</Button>
|
|
144
|
+
</form>}
|
|
145
|
+
<p className="text-xs text-muted">每个主会话绑定一个 btw 旁支。继续对话不会新建;转为普通会话后,主会话才能创建新的旁支。</p>
|
|
146
|
+
{err && <p className="text-xs text-danger" role="alert">{err}</p>}
|
|
147
|
+
{promote.error && <p className="text-xs text-danger" role="alert">转换失败:{promote.error.message}</p>}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------- context usage ----------
|
|
155
|
+
|
|
156
|
+
function fmtTokens(n: number) {
|
|
157
|
+
return n >= 1000 ? `${(n / 1000).toFixed(n >= 100_000 ? 0 : 1)}k` : String(n);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Context usage in the session header; opens the /context breakdown with compact and clear actions. */
|
|
161
|
+
export function ContextMeter({ pid, sid, onClear }: { pid: string; sid: string; onClear: () => Promise<boolean> }) {
|
|
162
|
+
const compact = useCompactSession(pid, sid);
|
|
163
|
+
const live = useSessionLive(sid);
|
|
164
|
+
const results = live?.events.filter((e) => e.k === "sdk" && e.m.type === "result").length ?? 0;
|
|
165
|
+
const state = live?.snapshot.state;
|
|
166
|
+
const ctx = useQuery({
|
|
167
|
+
queryKey: ["context", sid, results, state === "idle"],
|
|
168
|
+
queryFn: () => api<ContextInfo | null>(`/api/projects/${pid}/sessions/${sid}/context`),
|
|
169
|
+
placeholderData: (prev) => prev,
|
|
170
|
+
});
|
|
171
|
+
const pop = usePopover();
|
|
172
|
+
const [instructions, setInstructions] = useState("");
|
|
173
|
+
const [actionError, setActionError] = useState<string>();
|
|
174
|
+
|
|
175
|
+
const data = ctx.data;
|
|
176
|
+
const pct = data ? Math.round(data.percentage) : undefined;
|
|
177
|
+
const used = data?.categories.filter((c) => c.kind !== "deferred") ?? [];
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<Button
|
|
182
|
+
ref={pop.triggerRef}
|
|
183
|
+
iconOnly
|
|
184
|
+
icon={<BrainIcon size={18} className={pct !== undefined && pct >= 80 ? "text-danger" : "text-muted"} />}
|
|
185
|
+
onClick={pop.toggle}
|
|
186
|
+
aria-haspopup="dialog"
|
|
187
|
+
aria-expanded={pop.open}
|
|
188
|
+
aria-label="上下文用量"
|
|
189
|
+
title={pct !== undefined ? `上下文用量:${pct}%` : "上下文用量"}
|
|
190
|
+
data-testid="context-meter"
|
|
191
|
+
/>
|
|
192
|
+
<Popover open={pop.open} onClose={pop.close} anchor={pop.triggerRef} align="end" width={340} label="上下文用量" testId="context-details">
|
|
193
|
+
<div className="px-4 pb-3 pt-2 text-sm">
|
|
194
|
+
<div className="flex items-baseline justify-between">
|
|
195
|
+
<h3 className="text-base">上下文</h3>
|
|
196
|
+
{live?.snapshot.costUsd !== undefined && <span className="text-xs tabular-nums text-muted">本会话 ${live.snapshot.costUsd.toFixed(4)}</span>}
|
|
197
|
+
</div>
|
|
198
|
+
{data ? (
|
|
199
|
+
<>
|
|
200
|
+
<div className="mt-2 h-2 overflow-hidden rounded-full bg-surface-2">
|
|
201
|
+
<div className={`h-full ${pct !== undefined && pct >= 80 ? "bg-danger" : "bg-accent"}`} style={{ width: `${pct ?? 0}%` }} />
|
|
202
|
+
</div>
|
|
203
|
+
<p className="mt-1.5 text-xs text-muted">
|
|
204
|
+
{fmtTokens(data.totalTokens)} / {fmtTokens(data.maxTokens)} tokens · {pct}% · {data.model}
|
|
205
|
+
</p>
|
|
206
|
+
<table className="mt-2 w-full text-xs">
|
|
207
|
+
<tbody>
|
|
208
|
+
{used.map((c) => (
|
|
209
|
+
<tr key={c.name} className="border-t border-border" data-testid="context-row">
|
|
210
|
+
<td className="py-1.5 pr-2">{c.name}</td>
|
|
211
|
+
<td className="py-1.5 pr-2 text-right font-mono tabular-nums">{fmtTokens(c.tokens)}</td>
|
|
212
|
+
<td className="w-12 py-1.5 text-right font-mono tabular-nums text-muted">{data.maxTokens ? ((c.tokens / data.maxTokens) * 100).toFixed(1) : "0"}%</td>
|
|
213
|
+
</tr>
|
|
214
|
+
))}
|
|
215
|
+
</tbody>
|
|
216
|
+
</table>
|
|
217
|
+
</>
|
|
218
|
+
) : (
|
|
219
|
+
<p className="mt-1 text-muted">发出第一条消息后可见。</p>
|
|
220
|
+
)}
|
|
221
|
+
<div className="mt-3 space-y-2 border-t border-border pt-3">
|
|
222
|
+
<input
|
|
223
|
+
aria-label="压缩说明"
|
|
224
|
+
placeholder="压缩说明(可选),如:保留接口设计的讨论"
|
|
225
|
+
value={instructions}
|
|
226
|
+
onChange={(e) => setInstructions(e.target.value)}
|
|
227
|
+
className="h-10 w-full rounded-md border border-border bg-bg px-3 text-base outline-none focus:border-accent/60 focus:ring-2 focus:ring-accent/20 md:h-8 md:text-sm"
|
|
228
|
+
/>
|
|
229
|
+
<div className="flex gap-2">
|
|
230
|
+
<Button
|
|
231
|
+
className="flex-1"
|
|
232
|
+
onClick={async () => {
|
|
233
|
+
pop.close();
|
|
234
|
+
if (await compact(instructions)) setInstructions("");
|
|
235
|
+
else pop.setOpen(true);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
压缩
|
|
239
|
+
</Button>
|
|
240
|
+
<Button
|
|
241
|
+
className="flex-1"
|
|
242
|
+
title="新建一个会话并跳转过去,原会话保留"
|
|
243
|
+
onClick={async () => {
|
|
244
|
+
pop.close();
|
|
245
|
+
setActionError(undefined);
|
|
246
|
+
try {
|
|
247
|
+
if (!await onClear()) pop.setOpen(true);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
setActionError(error instanceof Error ? error.message : String(error));
|
|
250
|
+
pop.setOpen(true);
|
|
251
|
+
}
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
新开会话
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
{actionError && <p role="alert" className="text-xs text-danger">{actionError}</p>}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</Popover>
|
|
261
|
+
</>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NavLink, useLocation } from "react-router";
|
|
2
|
+
|
|
3
|
+
/** Sidebar entry to the global settings area; carries the current project along. */
|
|
4
|
+
export function SettingsLink() {
|
|
5
|
+
const { pathname, search } = useLocation();
|
|
6
|
+
const pid = /^\/p\/([^/]+)/.exec(pathname)?.[1] ?? (pathname.startsWith("/settings") ? new URLSearchParams(search).get("p") : null);
|
|
7
|
+
return (
|
|
8
|
+
<NavLink
|
|
9
|
+
to={`/settings/general${pid ? `?p=${encodeURIComponent(pid)}` : ""}`}
|
|
10
|
+
className={() =>
|
|
11
|
+
`flex items-center gap-2 rounded-md px-2.5 py-2 text-sm ${pathname.startsWith("/settings") ? "bg-surface font-medium text-text" : "text-muted hover:bg-surface"}`
|
|
12
|
+
}
|
|
13
|
+
>
|
|
14
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
15
|
+
<circle cx="12" cy="12" r="3" />
|
|
16
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
17
|
+
</svg>
|
|
18
|
+
设置
|
|
19
|
+
</NavLink>
|
|
20
|
+
);
|
|
21
|
+
}
|