cx-viewer 1.0.2 → 1.0.4
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/README.md +75 -28
- package/build.js +15 -0
- package/cli.js +108 -38
- package/concepts/ar/BodyDiffJSON.md +1 -1
- package/concepts/ar/BodyFields.md +30 -33
- package/concepts/ar/GlobalSettings.md +165 -167
- package/concepts/ar/MainAgent.md +3 -4
- package/concepts/ar/ProxySwitch.md +3 -3
- package/concepts/ar/QRCode.md +1 -1
- package/concepts/ar/ResponseFields.md +29 -41
- package/concepts/ar/SubAgent-Search.md +12 -12
- package/concepts/ar/Teammate.md +16 -16
- package/concepts/ar/ToolsFirst.md +42 -48
- package/concepts/ar/TranslateContextPollution.md +9 -9
- package/concepts/ar/UltraPlan.md +38 -38
- package/concepts/da/BodyDiffJSON.md +2 -2
- package/concepts/da/BodyFields.md +30 -33
- package/concepts/da/GlobalSettings.md +165 -167
- package/concepts/da/MainAgent.md +3 -4
- package/concepts/da/ProxySwitch.md +3 -3
- package/concepts/da/QRCode.md +1 -1
- package/concepts/da/ResponseFields.md +29 -41
- package/concepts/da/SubAgent-Search.md +11 -11
- package/concepts/da/Teammate.md +16 -16
- package/concepts/da/ToolsFirst.md +42 -48
- package/concepts/da/TranslateContextPollution.md +9 -9
- package/concepts/da/UltraPlan.md +38 -38
- package/concepts/de/BodyDiffJSON.md +2 -2
- package/concepts/de/BodyFields.md +30 -33
- package/concepts/de/GlobalSettings.md +165 -167
- package/concepts/de/MainAgent.md +3 -4
- package/concepts/de/ProxySwitch.md +3 -3
- package/concepts/de/QRCode.md +1 -1
- package/concepts/de/ResponseFields.md +29 -41
- package/concepts/de/SubAgent-Search.md +11 -11
- package/concepts/de/Teammate.md +15 -15
- package/concepts/de/ToolsFirst.md +41 -47
- package/concepts/de/TranslateContextPollution.md +9 -9
- package/concepts/de/UltraPlan.md +38 -38
- package/concepts/en/BodyDiffJSON.md +5 -5
- package/concepts/en/BodyFields.md +17 -20
- package/concepts/en/GlobalSettings.md +49 -51
- package/concepts/en/MainAgent.md +8 -8
- package/concepts/en/ProxySwitch.md +3 -3
- package/concepts/en/QRCode.md +6 -6
- package/concepts/en/ResponseFields.md +7 -19
- package/concepts/en/SubAgent-Search.md +2 -2
- package/concepts/en/Teammate.md +4 -4
- package/concepts/en/Tool-apply_patch.md +11 -0
- package/concepts/en/Tool-create_goal.md +8 -0
- package/concepts/en/Tool-exec.md +88 -0
- package/concepts/en/Tool-followup_task.md +10 -0
- package/concepts/en/Tool-get_goal.md +5 -0
- package/concepts/en/Tool-image_generation.md +5 -0
- package/concepts/en/Tool-interrupt_agent.md +9 -0
- package/concepts/en/Tool-list_agents.md +9 -0
- package/concepts/en/Tool-list_mcp_resource_templates.md +8 -0
- package/concepts/en/Tool-list_mcp_resources.md +8 -0
- package/concepts/en/Tool-read_mcp_resource.md +10 -0
- package/concepts/en/Tool-request_user_input.md +9 -0
- package/concepts/en/Tool-send_message.md +10 -0
- package/concepts/en/Tool-shell_command.md +12 -0
- package/concepts/en/Tool-spawn_agent.md +11 -0
- package/concepts/en/Tool-tool_search.md +12 -0
- package/concepts/en/Tool-update_goal.md +9 -0
- package/concepts/en/Tool-update_plan.md +9 -0
- package/concepts/en/Tool-view_image.md +10 -0
- package/concepts/en/Tool-wait.md +12 -0
- package/concepts/en/Tool-wait_agent.md +9 -0
- package/concepts/en/Tool-web_search.md +11 -0
- package/concepts/en/Tools.md +29 -58
- package/concepts/en/ToolsFirst.md +24 -30
- package/concepts/en/TranslateContextPollution.md +10 -38
- package/concepts/en/UltraPlan.md +7 -7
- package/concepts/es/BodyDiffJSON.md +1 -1
- package/concepts/es/BodyFields.md +35 -38
- package/concepts/es/GlobalSettings.md +165 -167
- package/concepts/es/MainAgent.md +3 -4
- package/concepts/es/ProxySwitch.md +3 -3
- package/concepts/es/QRCode.md +1 -1
- package/concepts/es/ResponseFields.md +28 -40
- package/concepts/es/SubAgent-Search.md +12 -12
- package/concepts/es/Teammate.md +17 -17
- package/concepts/es/ToolsFirst.md +41 -47
- package/concepts/es/TranslateContextPollution.md +9 -9
- package/concepts/es/UltraPlan.md +38 -38
- package/concepts/fr/BodyDiffJSON.md +2 -2
- package/concepts/fr/BodyFields.md +35 -38
- package/concepts/fr/GlobalSettings.md +165 -167
- package/concepts/fr/MainAgent.md +3 -4
- package/concepts/fr/ProxySwitch.md +3 -3
- package/concepts/fr/QRCode.md +1 -1
- package/concepts/fr/ResponseFields.md +25 -37
- package/concepts/fr/SubAgent-Search.md +12 -12
- package/concepts/fr/Teammate.md +16 -16
- package/concepts/fr/ToolsFirst.md +40 -46
- package/concepts/fr/TranslateContextPollution.md +9 -9
- package/concepts/fr/UltraPlan.md +38 -38
- package/concepts/it/BodyDiffJSON.md +2 -2
- package/concepts/it/BodyFields.md +35 -38
- package/concepts/it/GlobalSettings.md +165 -167
- package/concepts/it/MainAgent.md +3 -4
- package/concepts/it/ProxySwitch.md +3 -3
- package/concepts/it/QRCode.md +1 -1
- package/concepts/it/ResponseFields.md +28 -40
- package/concepts/it/SubAgent-Search.md +12 -12
- package/concepts/it/Teammate.md +17 -17
- package/concepts/it/ToolsFirst.md +41 -47
- package/concepts/it/TranslateContextPollution.md +9 -9
- package/concepts/it/UltraPlan.md +38 -38
- package/concepts/ja/BodyDiffJSON.md +1 -1
- package/concepts/ja/BodyFields.md +30 -33
- package/concepts/ja/GlobalSettings.md +165 -167
- package/concepts/ja/MainAgent.md +3 -4
- package/concepts/ja/ProxySwitch.md +3 -3
- package/concepts/ja/QRCode.md +1 -1
- package/concepts/ja/ResponseFields.md +29 -41
- package/concepts/ja/SubAgent-Search.md +12 -12
- package/concepts/ja/Teammate.md +17 -17
- package/concepts/ja/ToolsFirst.md +41 -47
- package/concepts/ja/TranslateContextPollution.md +9 -9
- package/concepts/ja/UltraPlan.md +38 -38
- package/concepts/ko/BodyDiffJSON.md +1 -1
- package/concepts/ko/BodyFields.md +30 -33
- package/concepts/ko/GlobalSettings.md +165 -167
- package/concepts/ko/MainAgent.md +3 -4
- package/concepts/ko/ProxySwitch.md +3 -3
- package/concepts/ko/QRCode.md +1 -1
- package/concepts/ko/ResponseFields.md +29 -41
- package/concepts/ko/SubAgent-Search.md +12 -12
- package/concepts/ko/Teammate.md +17 -17
- package/concepts/ko/ToolsFirst.md +41 -47
- package/concepts/ko/TranslateContextPollution.md +9 -9
- package/concepts/ko/UltraPlan.md +38 -38
- package/concepts/no/BodyDiffJSON.md +2 -2
- package/concepts/no/BodyFields.md +30 -33
- package/concepts/no/GlobalSettings.md +165 -167
- package/concepts/no/MainAgent.md +3 -4
- package/concepts/no/ProxySwitch.md +3 -3
- package/concepts/no/QRCode.md +1 -1
- package/concepts/no/ResponseFields.md +29 -41
- package/concepts/no/SubAgent-Search.md +12 -12
- package/concepts/no/Teammate.md +17 -17
- package/concepts/no/ToolsFirst.md +42 -48
- package/concepts/no/TranslateContextPollution.md +9 -9
- package/concepts/no/UltraPlan.md +38 -38
- package/concepts/pl/BodyDiffJSON.md +2 -2
- package/concepts/pl/BodyFields.md +30 -33
- package/concepts/pl/GlobalSettings.md +165 -167
- package/concepts/pl/MainAgent.md +3 -4
- package/concepts/pl/ProxySwitch.md +3 -3
- package/concepts/pl/QRCode.md +1 -1
- package/concepts/pl/ResponseFields.md +29 -41
- package/concepts/pl/SubAgent-Search.md +12 -12
- package/concepts/pl/Teammate.md +17 -17
- package/concepts/pl/ToolsFirst.md +42 -48
- package/concepts/pl/TranslateContextPollution.md +9 -9
- package/concepts/pl/UltraPlan.md +38 -38
- package/concepts/pt-BR/BodyDiffJSON.md +2 -2
- package/concepts/pt-BR/BodyFields.md +30 -33
- package/concepts/pt-BR/GlobalSettings.md +165 -167
- package/concepts/pt-BR/MainAgent.md +3 -4
- package/concepts/pt-BR/ProxySwitch.md +3 -3
- package/concepts/pt-BR/QRCode.md +1 -1
- package/concepts/pt-BR/ResponseFields.md +29 -41
- package/concepts/pt-BR/SubAgent-Search.md +12 -12
- package/concepts/pt-BR/Teammate.md +17 -17
- package/concepts/pt-BR/ToolsFirst.md +41 -47
- package/concepts/pt-BR/TranslateContextPollution.md +9 -9
- package/concepts/pt-BR/UltraPlan.md +38 -38
- package/concepts/ru/BodyDiffJSON.md +1 -1
- package/concepts/ru/BodyFields.md +30 -33
- package/concepts/ru/GlobalSettings.md +165 -167
- package/concepts/ru/MainAgent.md +3 -4
- package/concepts/ru/ProxySwitch.md +3 -3
- package/concepts/ru/QRCode.md +1 -1
- package/concepts/ru/ResponseFields.md +29 -41
- package/concepts/ru/SubAgent-Search.md +12 -12
- package/concepts/ru/Teammate.md +17 -17
- package/concepts/ru/ToolsFirst.md +41 -47
- package/concepts/ru/TranslateContextPollution.md +9 -9
- package/concepts/ru/UltraPlan.md +38 -38
- package/concepts/th/BodyDiffJSON.md +1 -1
- package/concepts/th/BodyFields.md +30 -33
- package/concepts/th/GlobalSettings.md +165 -167
- package/concepts/th/MainAgent.md +3 -4
- package/concepts/th/ProxySwitch.md +3 -3
- package/concepts/th/QRCode.md +1 -1
- package/concepts/th/ResponseFields.md +29 -41
- package/concepts/th/SubAgent-Search.md +12 -12
- package/concepts/th/Teammate.md +17 -17
- package/concepts/th/ToolsFirst.md +42 -48
- package/concepts/th/TranslateContextPollution.md +9 -9
- package/concepts/th/UltraPlan.md +38 -38
- package/concepts/tr/BodyDiffJSON.md +1 -1
- package/concepts/tr/BodyFields.md +30 -33
- package/concepts/tr/GlobalSettings.md +165 -167
- package/concepts/tr/MainAgent.md +3 -4
- package/concepts/tr/ProxySwitch.md +3 -3
- package/concepts/tr/QRCode.md +1 -1
- package/concepts/tr/ResponseFields.md +29 -41
- package/concepts/tr/SubAgent-Search.md +12 -12
- package/concepts/tr/Teammate.md +16 -16
- package/concepts/tr/ToolsFirst.md +42 -48
- package/concepts/tr/TranslateContextPollution.md +9 -9
- package/concepts/tr/UltraPlan.md +38 -38
- package/concepts/uk/BodyDiffJSON.md +1 -1
- package/concepts/uk/BodyFields.md +30 -33
- package/concepts/uk/GlobalSettings.md +165 -167
- package/concepts/uk/MainAgent.md +3 -4
- package/concepts/uk/ProxySwitch.md +3 -3
- package/concepts/uk/QRCode.md +1 -1
- package/concepts/uk/ResponseFields.md +29 -41
- package/concepts/uk/SubAgent-Search.md +12 -12
- package/concepts/uk/Teammate.md +17 -17
- package/concepts/uk/ToolsFirst.md +42 -48
- package/concepts/uk/TranslateContextPollution.md +9 -9
- package/concepts/uk/UltraPlan.md +38 -38
- package/concepts/zh/BodyDiffJSON.md +4 -4
- package/concepts/zh/BodyFields.md +17 -20
- package/concepts/zh/GlobalSettings.md +26 -28
- package/concepts/zh/MainAgent.md +6 -6
- package/concepts/zh/ProxySwitch.md +1 -1
- package/concepts/zh/ResponseFields.md +7 -18
- package/concepts/zh/SubAgent-Search.md +2 -2
- package/concepts/zh/Teammate.md +4 -4
- package/concepts/zh/Tool-apply_patch.md +11 -0
- package/concepts/zh/Tool-create_goal.md +8 -0
- package/concepts/zh/Tool-exec.md +88 -0
- package/concepts/zh/Tool-followup_task.md +10 -0
- package/concepts/zh/Tool-get_goal.md +5 -0
- package/concepts/zh/Tool-image_generation.md +5 -0
- package/concepts/zh/Tool-interrupt_agent.md +9 -0
- package/concepts/zh/Tool-list_agents.md +9 -0
- package/concepts/zh/Tool-list_mcp_resource_templates.md +8 -0
- package/concepts/zh/Tool-list_mcp_resources.md +8 -0
- package/concepts/zh/Tool-read_mcp_resource.md +10 -0
- package/concepts/zh/Tool-request_user_input.md +9 -0
- package/concepts/zh/Tool-send_message.md +10 -0
- package/concepts/zh/Tool-shell_command.md +12 -0
- package/concepts/zh/Tool-spawn_agent.md +11 -0
- package/concepts/zh/Tool-tool_search.md +12 -0
- package/concepts/zh/Tool-update_goal.md +9 -0
- package/concepts/zh/Tool-update_plan.md +9 -0
- package/concepts/zh/Tool-view_image.md +10 -0
- package/concepts/zh/Tool-wait.md +12 -0
- package/concepts/zh/Tool-wait_agent.md +9 -0
- package/concepts/zh/Tool-web_search.md +11 -0
- package/concepts/zh/Tools.md +29 -58
- package/concepts/zh/ToolsFirst.md +21 -27
- package/concepts/zh/TranslateContextPollution.md +10 -38
- package/concepts/zh/UltraPlan.md +7 -7
- package/concepts/zh-TW/BodyFields.md +27 -30
- package/concepts/zh-TW/GlobalSettings.md +26 -28
- package/concepts/zh-TW/MainAgent.md +0 -1
- package/concepts/zh-TW/ProxySwitch.md +22 -22
- package/concepts/zh-TW/ResponseFields.md +29 -40
- package/concepts/zh-TW/SubAgent-Search.md +12 -12
- package/concepts/zh-TW/Teammate.md +16 -16
- package/concepts/zh-TW/ToolsFirst.md +41 -47
- package/concepts/zh-TW/UltraPlan.md +38 -38
- package/dist/assets/App-BZ9oclBj.js +2 -0
- package/dist/assets/App-rWON7PuS.css +1 -0
- package/dist/assets/MdxEditorPanel-B4lZ7Z_A.js +1 -0
- package/dist/assets/MdxEditorPanel-Da_ceLFo.css +1 -0
- package/dist/assets/Mobile-B5V_-6Hc.css +1 -0
- package/dist/assets/Mobile-C7Hy-TIM.js +1 -0
- package/dist/assets/abnfDiagram-VRR7QNED-DYYppIQV.js +1 -0
- package/dist/assets/apl-B8ZmNFqK.js +1 -0
- package/dist/assets/arc-BT3um7r4.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-uweBqnh-.js +1 -0
- package/dist/assets/asciiarmor-CIte6N-S.js +1 -0
- package/dist/assets/asn1-Ax1-z1nk.js +1 -0
- package/dist/assets/asterisk-CLHf-iRz.js +1 -0
- package/dist/assets/blockDiagram-677ZJIJ3-DzeI4GyV.js +1 -0
- package/dist/assets/brainfuck-CdLnmYpe.js +1 -0
- package/dist/assets/c4Diagram-LMCZKHZV-DBI0K7wS.js +1 -0
- package/dist/assets/channel-BrN6yx0e.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-DK9JA8Ct.js +1 -0
- package/dist/assets/chunk-32BRIVSS-VNg3Ylzs.js +1 -0
- package/dist/assets/chunk-5VM5RSS4-DFhufY9o.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-B93MHNBT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-C54OW1iN.js +1 -0
- package/dist/assets/chunk-MOJQB5TN-lKhWWIDe.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-BK2jDXwB.js +1 -0
- package/dist/assets/chunk-V7JOEXUC-aLHBmfOc.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-Bomuor1h.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-BPj05P7g.js +1 -0
- package/dist/assets/classDiagram-OUVF2IWQ-COm0X4uJ.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-COm0X4uJ.js +1 -0
- package/dist/assets/clike-BDgEuk0n.js +1 -0
- package/dist/assets/clojure-CrPxgQZl.js +1 -0
- package/dist/assets/cmake-BknZiLBu.js +1 -0
- package/dist/assets/cobol-DN1mHbgw.js +1 -0
- package/dist/assets/coffeescript-DTAwRC7M.js +1 -0
- package/dist/assets/commonlisp-B4DJ--Bo.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-DU44QHSb.js +1 -0
- package/dist/assets/crystal-DoipYOOv.js +1 -0
- package/dist/assets/css-BeQRP3p5.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-C0dowxsF.js +15 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-C6vNWPUV.js +1 -0
- package/dist/assets/cypher-BzELKutH.js +1 -0
- package/dist/assets/cytoscape.esm-D0POlJZ1.js +9 -0
- package/dist/assets/d-Mh2Rawoc.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-aLG4uEb-.js +1 -0
- package/dist/assets/defaultLocale-DZ7E7S9R.js +1 -0
- package/dist/assets/diagram-FQU43EPY-D5C8GfdU.js +1 -0
- package/dist/assets/diagram-G47NLZAW-C9tRw_xi.js +1 -0
- package/dist/assets/diagram-NH7WQ7WH-AYxTxwJ2.js +1 -0
- package/dist/assets/diagram-OA4YK3LP-BuBE5n9n.js +1 -0
- package/dist/assets/diagram-WEI45ONY-Cda7zYeO.js +1 -0
- package/dist/assets/diff-B1OtBbfz.js +1 -0
- package/dist/assets/dockerfile-Du7niAGi.js +1 -0
- package/dist/assets/dtd-So5hjSa8.js +1 -0
- package/dist/assets/dylan-Co15g-RX.js +1 -0
- package/dist/assets/ebnf-DWrAnno4.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-DC_OGyoh.js +1 -0
- package/dist/assets/ecl-8tbf0rUT.js +1 -0
- package/dist/assets/eiffel-BXtlUtEg.js +1 -0
- package/dist/assets/elm-BwiUluiH.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-D2G21PVY.js +1 -0
- package/dist/assets/erlang-CrHBLi-m.js +1 -0
- package/dist/assets/factor-C7nC06Ta.js +1 -0
- package/dist/assets/fcl-DKWfryFT.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-CEQPYHrk.js +1 -0
- package/dist/assets/forth-Cg8ASoTj.js +1 -0
- package/dist/assets/fortran-BHf_qfFx.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-JStxUtFR.js +1 -0
- package/dist/assets/gas-cj_2N_z1.js +1 -0
- package/dist/assets/gherkin-BiuTaH40.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-BiojPH-V.js +1 -0
- package/dist/assets/graph-CnJdn47S.js +1 -0
- package/dist/assets/groovy-Bq8cXYUU.js +1 -0
- package/dist/assets/haskell-1ANuaeie.js +1 -0
- package/dist/assets/haxe-BUNSvWf_.js +1 -0
- package/dist/assets/html2canvas.esm-HFdlUwfn.js +20 -0
- package/dist/assets/http-DPC2IRQA.js +1 -0
- package/dist/assets/idl-DDbIciu_.js +1 -0
- package/dist/assets/index-BEyKG3O5.css +1 -0
- package/dist/assets/index-BduJoLQ5.js +1 -0
- package/dist/assets/index-BqwR8F0v.js +1 -0
- package/dist/assets/index-BsycQyrv.js +1 -0
- package/dist/assets/index-CFSDxZyA.js +1 -0
- package/dist/assets/index-CunNLTHc.js +1 -0
- package/dist/assets/index-OGkDFNTK.js +1 -0
- package/dist/assets/index-oxOyrL07.js +2 -0
- package/dist/assets/index-wvaycTGT.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-Csl_aBIC.js +1 -0
- package/dist/assets/init-DMZMl1PR.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-CjgGuMwr.js +1 -0
- package/dist/assets/javascript-Cet1BUca.js +1 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-DUOy1PCq.js +1 -0
- package/dist/assets/jszip.min-BxIXdRg4.js +12 -0
- package/dist/assets/julia-CdxLCr68.js +1 -0
- package/dist/assets/kanban-definition-HUTT4EX6-Cx7GSrSj.js +1 -0
- package/dist/assets/katex-CfXf84Fz.js +1 -0
- package/dist/assets/layout-CCUE9ZRw.js +1 -0
- package/dist/assets/linear-GeBi6jJp.js +1 -0
- package/dist/assets/livescript-Bw1Vw7Ae.js +1 -0
- package/dist/assets/lua-DM1wbcnc.js +1 -0
- package/dist/assets/map-DQeg2hXV.js +1 -0
- package/dist/assets/mathematica-BXSv1_O1.js +1 -0
- package/dist/assets/mbox-CWY0keg3.js +1 -0
- package/dist/assets/mermaid.core-Cy9iVxov.js +7 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-CUVxVkK5.js +1 -0
- package/dist/assets/mirc-CMLrLVMT.js +1 -0
- package/dist/assets/mllike-naqhAFf1.js +1 -0
- package/dist/assets/modelica-BvadsaHY.js +1 -0
- package/dist/assets/mscgen-DOr-Q10p.js +1 -0
- package/dist/assets/mumps-DKTl5Nyj.js +1 -0
- package/dist/assets/nginx-DWR90-d1.js +1 -0
- package/dist/assets/nsis-ooFGduGn.js +1 -0
- package/dist/assets/ntriples-q5jkU4X1.js +1 -0
- package/dist/assets/octave-Deu9Ci7_.js +1 -0
- package/dist/assets/orbiting-BCD4Vuij.svg +7 -0
- package/dist/assets/ordinal-DA-_8pMy.js +1 -0
- package/dist/assets/oz-cTQm1aoB.js +1 -0
- package/dist/assets/pascal-Oh3VmczP.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-BCF1byLY.js +1 -0
- package/dist/assets/perl-BddWF1HQ.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-D2cZTo8-.js +1 -0
- package/dist/assets/pig-g5ZQNIDr.js +1 -0
- package/dist/assets/powershell-CNmQNFVJ.js +1 -0
- package/dist/assets/properties-B41DXfv6.js +1 -0
- package/dist/assets/protobuf-eEBl31OJ.js +1 -0
- package/dist/assets/pug-DRSo4eCK.js +1 -0
- package/dist/assets/puppet-ClfRubAD.js +1 -0
- package/dist/assets/python-BSflc812.js +1 -0
- package/dist/assets/q-BBDx3N9Y.js +1 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-Bwsn2--F.js +1 -0
- package/dist/assets/r-CByC1eO8.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-CLGLpWHs.js +1 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-BfmBYwZl.js +1 -0
- package/dist/assets/rpm-8t935XkD.js +1 -0
- package/dist/assets/ruby-CAgbkLZh.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-CqZhbyFn.js +1 -0
- package/dist/assets/sas-C-JWcRvo.js +1 -0
- package/dist/assets/scheme-D7o9RjUX.js +1 -0
- package/dist/assets/seqResourceLoaders-Bkh2Rb9i.js +2 -0
- package/dist/assets/seqResourceLoaders-Do7Kqg3B.css +41 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-CcOdXkN2.js +1 -0
- package/dist/assets/shell-BXA79DV6.js +1 -0
- package/dist/assets/shimmer-BBDejmDf.svg +7 -0
- package/dist/assets/sieve-DNwyl0rd.js +1 -0
- package/dist/assets/simple-mode-CoQRusFX.js +1 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-B4OVmPqu.js +1 -0
- package/dist/assets/smalltalk-DggktV6S.js +1 -0
- package/dist/assets/solr-D7NRuIPM.js +1 -0
- package/dist/assets/sparql-Cr71deJQ.js +1 -0
- package/dist/assets/spreadsheet-BMD3tYqw.js +1 -0
- package/dist/assets/sql-CbOjXfBR.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-Dysun0oO.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BFet8xAz.js +1 -0
- package/dist/assets/stex-iDPIkRmr.js +1 -0
- package/dist/assets/stylus-nWZizI-c.js +1 -0
- package/dist/assets/swift-DOhLK5SG.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-_30OXwXy.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-C19viY0G.js +1 -0
- package/dist/assets/tcl-CQhJd1WY.js +1 -0
- package/dist/assets/textile-D9XqfVtL.js +1 -0
- package/dist/assets/tiddlywiki-DfOYgVky.js +1 -0
- package/dist/assets/tiki-a0_YLWvV.js +1 -0
- package/dist/assets/timeline-definition-FHXFAJF6-X1Xk3onD.js +1 -0
- package/dist/assets/toml-ekl3F1YB.js +1 -0
- package/dist/assets/troff-Bh_pcXF1.js +1 -0
- package/dist/assets/ttcn-M6363IRW.js +1 -0
- package/dist/assets/ttcn-cfg-7I2DgJEN.js +1 -0
- package/dist/assets/turtle-B5jdxH6D.js +1 -0
- package/dist/assets/vb-DM-xPmVv.js +1 -0
- package/dist/assets/vbscript-B0qu41Yc.js +1 -0
- package/dist/assets/velocity-FCgsuVHf.js +1 -0
- package/dist/assets/vendor-antd-Bzi1WuBb.js +17 -0
- package/dist/assets/vendor-codemirror-CKTrJd47.js +1 -0
- package/dist/assets/vendor-highlight-IFTJ9nxW.js +1 -0
- package/dist/assets/vendor-markdown-BN3QLsBu.js +3 -0
- package/dist/assets/vendor-mdxeditor-4PbKH6K7.js +2 -0
- package/dist/assets/vendor-qrcode-Bnjm0rcu.js +6 -0
- package/dist/assets/vendor-react-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-virtuoso-2P4SowHC.js +1 -0
- package/dist/assets/vendor-xterm-DckPrCx1.js +37 -0
- package/dist/assets/vennDiagram-L72KCM5P-D3fQnGq7.js +1 -0
- package/dist/assets/verilog-nrfFvR12.js +1 -0
- package/dist/assets/vhdl-WlDXtV-W.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CRhqI6-g.js +1 -0
- package/dist/assets/webidl-RIQoSXDx.js +1 -0
- package/dist/assets/xquery-BN45G3lJ.js +1 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-Cmtgmmtg.js +1 -0
- package/dist/assets/yacas-U6gaIVhD.js +1 -0
- package/dist/assets/z80-CFe5BHbK.js +1 -0
- package/dist/index.html +25 -6
- package/dist/voice-packs/default/A_choice_for_your_consideration_sir.MP3 +0 -0
- package/dist/voice-packs/default/It_is_done_sir.MP3 +0 -0
- package/dist/voice-packs/default/The_plan_awaits_your_approval_sir.MP3 +0 -0
- package/dist/voice-packs/default/pack.json +24 -0
- package/dist/voice-packs/sanguo/ask.MP3 +0 -0
- package/dist/voice-packs/sanguo/end.MP3 +0 -0
- package/dist/voice-packs/sanguo/pack.json +24 -0
- package/dist/voice-packs/sanguo/plan.MP3 +0 -0
- package/electron/diag.js +110 -0
- package/electron/main.js +1432 -0
- package/electron/menu-model.js +107 -0
- package/electron/tab-bar.html +575 -0
- package/electron/tab-content-preload.js +66 -0
- package/electron/tab-preload.js +24 -0
- package/electron/tab-worker.js +142 -0
- package/electron/window-state.js +60 -0
- package/electron/workspace-preload.js +19 -0
- package/findcx.js +4 -0
- package/i18n.js +3 -423
- package/index.html +30 -0
- package/interceptor.js +144 -70
- package/jsconfig.json +25 -0
- package/lib/approval-reviewer.js +23 -0
- package/lib/appserver-bridge.js +1753 -237
- package/lib/ask-bridge.js +5 -5
- package/lib/cli-args.js +57 -0
- package/lib/codex-config.js +157 -18
- package/lib/context-watcher.js +6 -65
- package/lib/delta-reconstructor.js +74 -52
- package/lib/ensure-hooks.js +37 -30
- package/lib/interceptor-core.js +490 -43
- package/lib/log-watcher.js +3 -8
- package/lib/main-agent-entry.js +68 -0
- package/lib/openai-body.js +95 -0
- package/lib/otel-adapter.js +3 -2
- package/lib/otel-receiver.js +1 -2
- package/lib/perm-bridge.js +84 -44
- package/lib/proxy-env.js +45 -1
- package/lib/sdk-adapter.js +241 -51
- package/lib/sdk-manager.js +523 -370
- package/lib/skills-api.js +364 -0
- package/lib/stats-worker.js +21 -21
- package/lib/token-usage.js +29 -0
- package/lib/updater.js +3 -7
- package/package.json +27 -6
- package/plugins/.gitkeep +0 -0
- package/plugins/README.md +8 -0
- package/proxy.js +187 -241
- package/pty-manager.js +17 -1
- package/public/favicon.ico +0 -0
- package/public/voice-packs/default/A_choice_for_your_consideration_sir.MP3 +0 -0
- package/public/voice-packs/default/It_is_done_sir.MP3 +0 -0
- package/public/voice-packs/default/The_plan_awaits_your_approval_sir.MP3 +0 -0
- package/public/voice-packs/default/pack.json +24 -0
- package/public/voice-packs/sanguo/ask.MP3 +0 -0
- package/public/voice-packs/sanguo/end.MP3 +0 -0
- package/public/voice-packs/sanguo/pack.json +24 -0
- package/public/voice-packs/sanguo/plan.MP3 +0 -0
- package/scripts/check-no-control-bytes.js +130 -0
- package/scripts/check-sign-config.sh +85 -0
- package/scripts/gen-default-voicepack.js +176 -0
- package/scripts/mac-sign.sh +47 -0
- package/scripts/npm-pack-dry-run.mjs +20 -0
- package/server/lib/approval-modal-prefs.js +72 -0
- package/server/lib/context-rules.js +70 -0
- package/server/lib/delta-reconstructor.js +403 -0
- package/server/lib/tools-xml-formatter.js +91 -0
- package/server/lib/voice-pack-events.js +79 -0
- package/server.js +420 -86
- package/src/App.jsx +638 -0
- package/src/App.module.css +1088 -0
- package/src/AppBase.jsx +2803 -0
- package/src/Mobile.jsx +1348 -0
- package/src/components/approval/ApprovalModal.jsx +319 -0
- package/src/components/approval/ApprovalModal.module.css +160 -0
- package/src/components/approval/ToolApprovalPanel.jsx +111 -0
- package/src/components/approval/ToolApprovalPanel.module.css +250 -0
- package/src/components/chat/ApprovalPortalContext.js +26 -0
- package/src/components/chat/AskQuestionForm.jsx +279 -0
- package/src/components/chat/AskTimeoutCountdown.jsx +113 -0
- package/src/components/chat/ChatInputBar.jsx +584 -0
- package/src/components/chat/ChatInputBar.module.css +656 -0
- package/src/components/chat/ChatMessage.jsx +1587 -0
- package/src/components/chat/ChatMessage.module.css +1560 -0
- package/src/components/chat/ChatView.jsx +3533 -0
- package/src/components/chat/ChatView.module.css +418 -0
- package/src/components/chat/DiffView.jsx +111 -0
- package/src/components/chat/DiffView.module.css +141 -0
- package/src/components/chat/RoleFilterBar.jsx +35 -0
- package/src/components/chat/RoleFilterBar.module.css +62 -0
- package/src/components/chat/controllers/askFlowController.js +789 -0
- package/src/components/chat/controllers/permissionController.js +84 -0
- package/src/components/chat/controllers/ptyPromptController.js +166 -0
- package/src/components/chat/controllers/scrollHighlightController.js +50 -0
- package/src/components/chat/controllers/splitDragController.js +135 -0
- package/src/components/chat/controllers/toolFileChangeController.js +216 -0
- package/src/components/chat/interactionOwnership.js +106 -0
- package/src/components/chat/uploadDeferLogic.js +46 -0
- package/src/components/common/ConceptHelp.jsx +97 -0
- package/src/components/common/ConceptHelp.module.css +151 -0
- package/src/components/common/ConfirmRemoveButton.jsx +78 -0
- package/src/components/common/CountryFlag.jsx +105 -0
- package/src/components/common/CountryFlag.module.css +36 -0
- package/src/components/common/DingTalkIcon.jsx +20 -0
- package/src/components/common/DiscordIcon.jsx +21 -0
- package/src/components/common/FeishuIcon.jsx +23 -0
- package/src/components/common/HoverPreviewTip.jsx +62 -0
- package/src/components/common/HoverPreviewTip.module.css +21 -0
- package/src/components/common/ImageLightbox.jsx +244 -0
- package/src/components/common/ImageLightbox.module.css +120 -0
- package/src/components/common/MemoryDetailModal.jsx +75 -0
- package/src/components/common/OpenFolderIcon.jsx +53 -0
- package/src/components/common/OpenFolderIcon.module.css +7 -0
- package/src/components/common/PanelResizer.jsx +48 -0
- package/src/components/common/PanelResizer.module.css +11 -0
- package/src/components/common/QuickAutoApproveRows.jsx +66 -0
- package/src/components/common/RefreshIcon.jsx +50 -0
- package/src/components/common/RefreshIcon.module.css +26 -0
- package/src/components/common/SnapLineOverlay.jsx +47 -0
- package/src/components/common/SnapLineOverlay.module.css +31 -0
- package/src/components/common/ToolsHelp.jsx +113 -0
- package/src/components/common/ToolsHelp.module.css +85 -0
- package/src/components/common/WeComIcon.jsx +22 -0
- package/src/components/common/quickMenuIcons.jsx +99 -0
- package/src/components/common/sharedChrome.module.css +274 -0
- package/src/components/dashboard/AppHeader.jsx +2085 -0
- package/src/components/dashboard/AppHeader.module.css +676 -0
- package/src/components/dashboard/CachePopoverContent.jsx +538 -0
- package/src/components/dashboard/CachePopoverContent.module.css +191 -0
- package/src/components/dashboard/ContextTab.jsx +769 -0
- package/src/components/dashboard/ContextTab.module.css +565 -0
- package/src/components/dashboard/DetailPanel.jsx +813 -0
- package/src/components/dashboard/DetailPanel.module.css +513 -0
- package/src/components/dashboard/LiveTagPopover.jsx +89 -0
- package/src/components/dashboard/LiveTagPopover.module.css +67 -0
- package/src/components/dashboard/RequestList.jsx +146 -0
- package/src/components/dashboard/RequestList.module.css +146 -0
- package/src/components/dashboard/TeamSessionPanel.jsx +584 -0
- package/src/components/dashboard/TeamSessionPanel.module.css +457 -0
- package/src/components/dashboard/UsageWindowPill.jsx +166 -0
- package/src/components/dashboard/UsageWindowPill.module.css +140 -0
- package/src/components/dashboard/WorkflowRunsPanel.jsx +108 -0
- package/src/components/dashboard/WorkflowRunsPanel.module.css +72 -0
- package/src/components/dashboard/WorkspaceList.jsx +410 -0
- package/src/components/dashboard/WorkspaceList.module.css +254 -0
- package/src/components/files/FileContentView.jsx +1013 -0
- package/src/components/files/FileContentView.module.css +418 -0
- package/src/components/files/FileExplorer.jsx +937 -0
- package/src/components/files/FileExplorer.module.css +173 -0
- package/src/components/files/MdxEditorPanel.base.css +9 -0
- package/src/components/files/MdxEditorPanel.jsx +181 -0
- package/src/components/files/MdxEditorPanel.module.css +917 -0
- package/src/components/git/DiffMiniMap.jsx +98 -0
- package/src/components/git/DiffMiniMap.module.css +17 -0
- package/src/components/git/FullFileDiffView.jsx +265 -0
- package/src/components/git/FullFileDiffView.module.css +176 -0
- package/src/components/git/GitChanges.jsx +461 -0
- package/src/components/git/GitChanges.module.css +337 -0
- package/src/components/git/GitDiffView.jsx +161 -0
- package/src/components/git/GitDiffView.module.css +205 -0
- package/src/components/mobile/MobileDrawerCloseButton.jsx +18 -0
- package/src/components/mobile/MobileFileExplorer.jsx +231 -0
- package/src/components/mobile/MobileFileExplorer.module.css +171 -0
- package/src/components/mobile/MobileGitDiff.jsx +293 -0
- package/src/components/mobile/MobileGitDiff.module.css +317 -0
- package/src/components/mobile/MobileStats.jsx +211 -0
- package/src/components/mobile/MobileStats.module.css +75 -0
- package/src/components/settings/ImAppendSystemModal.jsx +104 -0
- package/src/components/settings/ImConversationModal.jsx +328 -0
- package/src/components/settings/ImConversationModal.module.css +49 -0
- package/src/components/settings/ImPlatformSettings.jsx +453 -0
- package/src/components/settings/ImPlatformSettings.module.css +110 -0
- package/src/components/settings/ImSkillsModal.jsx +94 -0
- package/src/components/settings/ImStatusChip.jsx +80 -0
- package/src/components/settings/ImStatusChip.module.css +36 -0
- package/src/components/settings/MessagingModal.jsx +65 -0
- package/src/components/settings/MessagingModal.module.css +95 -0
- package/src/components/settings/ModelPromptTabs.jsx +118 -0
- package/src/components/settings/PluginModal.jsx +423 -0
- package/src/components/settings/PluginModal.module.css +136 -0
- package/src/components/settings/PreferencesForm.jsx +104 -0
- package/src/components/settings/PreferencesForm.module.css +27 -0
- package/src/components/settings/ProcessModal.jsx +175 -0
- package/src/components/settings/ProcessModal.module.css +21 -0
- package/src/components/settings/ProjectAliasEditor.jsx +138 -0
- package/src/components/settings/ProjectAliasEditor.module.css +87 -0
- package/src/components/settings/ProjectPrefsManagerModal.jsx +123 -0
- package/src/components/settings/ProjectPrefsManagerModal.module.css +52 -0
- package/src/components/settings/ProxyModal.jsx +286 -0
- package/src/components/settings/ProxyModal.module.css +141 -0
- package/src/components/settings/SkillsManagerModal.jsx +143 -0
- package/src/components/settings/SkillsManagerModal.module.css +143 -0
- package/src/components/settings/SystemTextModal.jsx +312 -0
- package/src/components/settings/SystemTextModal.module.css +331 -0
- package/src/components/settings/VoicePackSettings.jsx +319 -0
- package/src/components/settings/VoicePackSettings.module.css +151 -0
- package/src/components/settings/imPlatforms.js +177 -0
- package/src/components/terminal/CustomUltraplanEditModal.jsx +227 -0
- package/src/components/terminal/CustomUltraplanEditModal.module.css +189 -0
- package/src/components/terminal/PresetExpertPickerModal.jsx +116 -0
- package/src/components/terminal/PresetExpertPickerModal.module.css +90 -0
- package/src/components/terminal/PresetModal.jsx +118 -0
- package/src/components/terminal/ScratchTerminal.jsx +359 -0
- package/src/components/terminal/TerminalPanel.jsx +2021 -0
- package/src/components/terminal/TerminalPanel.module.css +872 -0
- package/src/components/terminal/TerminalWsContext.jsx +226 -0
- package/src/components/terminal/UltraPlanModal.jsx +328 -0
- package/src/components/terminal/UltraPlanModal.module.css +527 -0
- package/src/components/terminal/UltraplanExpertManagerModal.jsx +114 -0
- package/src/components/terminal/UltraplanExpertManagerModal.module.css +91 -0
- package/src/components/terminal/UltraplanPanel.jsx +307 -0
- package/src/components/terminal/UltraplanPanel.module.css +540 -0
- package/src/components/terminal/terminalThemes.js +33 -0
- package/src/components/viewers/ImageViewer.jsx +288 -0
- package/src/components/viewers/ImageViewer.module.css +203 -0
- package/src/components/viewers/JsonViewer.jsx +40 -0
- package/src/components/viewers/JsonViewer.module.css +9 -0
- package/src/components/viewers/LogTable.jsx +131 -0
- package/src/components/viewers/MarkdownBlock.jsx +112 -0
- package/src/components/viewers/MarkdownBlock.module.css +157 -0
- package/src/components/viewers/ToolResultView.jsx +389 -0
- package/src/components/viewers/ToolResultView.module.css +108 -0
- package/src/components/viewers/WebResultsView.jsx +74 -0
- package/src/components/viewers/WebResultsView.module.css +85 -0
- package/src/components/viewers/WorkflowLiveHud.jsx +153 -0
- package/src/components/viewers/WorkflowLiveHud.module.css +256 -0
- package/src/components/viewers/WorkflowPanel.jsx +243 -0
- package/src/components/viewers/WorkflowPanel.module.css +265 -0
- package/src/components/viewers/WorkflowTimeline.jsx +83 -0
- package/src/components/viewers/WorkflowTimeline.module.css +116 -0
- package/src/contexts/SettingsContext.jsx +140 -0
- package/src/env.js +54 -0
- package/src/global.css +607 -0
- package/src/hooks/useMarkdownExport.js +146 -0
- package/src/hooks/useMermaidRender.js +193 -0
- package/src/hooks/useProjectAlias.js +29 -0
- package/src/hooks/useSessionStoragePersistedSet.js +41 -0
- package/src/i18n/mdxTranslations.js +472 -0
- package/src/i18n.js +12409 -0
- package/src/img/ccv-logo.png +0 -0
- package/src/img/codex/entrance.svg +7 -0
- package/src/img/codex/exit.svg +7 -0
- package/src/img/codex/orbiting.svg +7 -0
- package/src/img/codex/shimmer.svg +7 -0
- package/src/img/codex/thinking.svg +7 -0
- package/src/img/codex/tickle.svg +7 -0
- package/src/img/codex/waiting.svg +7 -0
- package/src/img/codex/writing.svg +7 -0
- package/src/img/default-avatar.svg +1 -0
- package/src/img/default-model-avatar.svg +1 -0
- package/src/img/loading-pet.gif +0 -0
- package/src/img/model-codex.svg +2 -0
- package/src/img/model-deepseek.svg +2 -0
- package/src/img/model-gemini.svg +2 -0
- package/src/img/model-glm.svg +2 -0
- package/src/img/model-kimi.svg +2 -0
- package/src/img/model-minimax.svg +2 -0
- package/src/img/model-openai.svg +2 -0
- package/src/img/model-qwen.svg +2 -0
- package/src/img/teammates/DESIGN.md +165 -0
- package/src/img/teammates/analyst.svg +32 -0
- package/src/img/teammates/auditor.svg +28 -0
- package/src/img/teammates/builder.svg +33 -0
- package/src/img/teammates/default.svg +25 -0
- package/src/img/teammates/designer.svg +25 -0
- package/src/img/teammates/executor.svg +31 -0
- package/src/img/teammates/expert.svg +30 -0
- package/src/img/teammates/explorer.svg +30 -0
- package/src/img/teammates/implementer.svg +34 -0
- package/src/img/teammates/investigator.svg +29 -0
- package/src/img/teammates/marvel/analyst.svg +26 -0
- package/src/img/teammates/marvel/auditor.svg +28 -0
- package/src/img/teammates/marvel/builder.svg +15 -0
- package/src/img/teammates/marvel/default.svg +25 -0
- package/src/img/teammates/marvel/designer.svg +21 -0
- package/src/img/teammates/marvel/executor.svg +25 -0
- package/src/img/teammates/marvel/expert.svg +31 -0
- package/src/img/teammates/marvel/explorer.svg +22 -0
- package/src/img/teammates/marvel/implementer.svg +30 -0
- package/src/img/teammates/marvel/investigator.svg +21 -0
- package/src/img/teammates/marvel/researcher.svg +30 -0
- package/src/img/teammates/marvel/reviewer.svg +21 -0
- package/src/img/teammates/marvel/scanner.svg +23 -0
- package/src/img/teammates/marvel/security.svg +30 -0
- package/src/img/teammates/marvel/tracer.svg +19 -0
- package/src/img/teammates/marvel/translator.svg +24 -0
- package/src/img/teammates/marvel/worker.svg +16 -0
- package/src/img/teammates/researcher.svg +35 -0
- package/src/img/teammates/reviewer.svg +23 -0
- package/src/img/teammates/scanner.svg +31 -0
- package/src/img/teammates/security.svg +31 -0
- package/src/img/teammates/tracer.svg +30 -0
- package/src/img/teammates/translator.svg +32 -0
- package/src/img/teammates/worker.svg +29 -0
- package/src/main.jsx +30 -0
- package/src/tokenInterceptor.js +60 -0
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/approvalReviewerOptions.js +27 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +23 -0
- package/src/utils/avatarAnimationPostPass.js +46 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +154 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +675 -0
- package/src/utils/contextRaw.js +17 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/conversationEntryNormalize.js +497 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/entry-slim.js +594 -0
- package/src/utils/entryCache.js +179 -0
- package/src/utils/errorReport.js +40 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +462 -0
- package/src/utils/identityHeal.js +83 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +111 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +249 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +32 -0
- package/src/utils/requestTools.js +19 -0
- package/src/utils/requestType.js +284 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +694 -0
- package/src/utils/sessionMerge.js +290 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tCodex.js +20 -0
- package/src/utils/teamModalBuilder.js +347 -0
- package/src/utils/teamSessionParser.js +186 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +106 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/tokenUsage.js +1 -0
- package/src/utils/toolCatalog.js +31 -0
- package/src/utils/toolNameAliases.js +13 -0
- package/src/utils/toolResultBuilder.js +312 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +161 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +152 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webResultGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/vite.config.js +106 -0
- package/concepts/ar/CacheRebuild.md +0 -36
- package/concepts/ar/KVCacheContent.md +0 -49
- package/concepts/ar/Tool-Agent.md +0 -125
- package/concepts/ar/Tool-AskUserQuestion.md +0 -75
- package/concepts/ar/Tool-Bash.md +0 -167
- package/concepts/ar/Tool-Edit.md +0 -46
- package/concepts/ar/Tool-EnterPlanMode.md +0 -128
- package/concepts/ar/Tool-EnterWorktree.md +0 -56
- package/concepts/ar/Tool-ExitPlanMode.md +0 -62
- package/concepts/ar/Tool-Glob.md +0 -39
- package/concepts/ar/Tool-Grep.md +0 -57
- package/concepts/ar/Tool-NotebookEdit.md +0 -33
- package/concepts/ar/Tool-Read.md +0 -56
- package/concepts/ar/Tool-SendMessage.md +0 -41
- package/concepts/ar/Tool-Skill.md +0 -56
- package/concepts/ar/Tool-Task.md +0 -51
- package/concepts/ar/Tool-TaskCreate.md +0 -78
- package/concepts/ar/Tool-TaskGet.md +0 -56
- package/concepts/ar/Tool-TaskList.md +0 -56
- package/concepts/ar/Tool-TaskOutput.md +0 -40
- package/concepts/ar/Tool-TaskStop.md +0 -32
- package/concepts/ar/Tool-TaskUpdate.md +0 -123
- package/concepts/ar/Tool-TeamCreate.md +0 -38
- package/concepts/ar/Tool-TeamDelete.md +0 -30
- package/concepts/ar/Tool-WebFetch.md +0 -55
- package/concepts/ar/Tool-WebSearch.md +0 -58
- package/concepts/ar/Tool-Write.md +0 -39
- package/concepts/ar/Tool-executeCode.md +0 -41
- package/concepts/ar/Tool-getDiagnostics.md +0 -32
- package/concepts/ar/Tools.md +0 -67
- package/concepts/da/CacheRebuild.md +0 -36
- package/concepts/da/KVCacheContent.md +0 -49
- package/concepts/da/Tool-Agent.md +0 -125
- package/concepts/da/Tool-AskUserQuestion.md +0 -75
- package/concepts/da/Tool-Bash.md +0 -167
- package/concepts/da/Tool-Edit.md +0 -46
- package/concepts/da/Tool-EnterPlanMode.md +0 -128
- package/concepts/da/Tool-EnterWorktree.md +0 -56
- package/concepts/da/Tool-ExitPlanMode.md +0 -62
- package/concepts/da/Tool-Glob.md +0 -39
- package/concepts/da/Tool-Grep.md +0 -57
- package/concepts/da/Tool-NotebookEdit.md +0 -33
- package/concepts/da/Tool-Read.md +0 -56
- package/concepts/da/Tool-SendMessage.md +0 -41
- package/concepts/da/Tool-Skill.md +0 -56
- package/concepts/da/Tool-Task.md +0 -51
- package/concepts/da/Tool-TaskCreate.md +0 -78
- package/concepts/da/Tool-TaskGet.md +0 -56
- package/concepts/da/Tool-TaskList.md +0 -56
- package/concepts/da/Tool-TaskOutput.md +0 -40
- package/concepts/da/Tool-TaskStop.md +0 -32
- package/concepts/da/Tool-TaskUpdate.md +0 -123
- package/concepts/da/Tool-TeamCreate.md +0 -38
- package/concepts/da/Tool-TeamDelete.md +0 -30
- package/concepts/da/Tool-WebFetch.md +0 -55
- package/concepts/da/Tool-WebSearch.md +0 -58
- package/concepts/da/Tool-Write.md +0 -39
- package/concepts/da/Tool-executeCode.md +0 -41
- package/concepts/da/Tool-getDiagnostics.md +0 -32
- package/concepts/da/Tools.md +0 -67
- package/concepts/de/CacheRebuild.md +0 -36
- package/concepts/de/KVCacheContent.md +0 -49
- package/concepts/de/Tool-Agent.md +0 -125
- package/concepts/de/Tool-AskUserQuestion.md +0 -75
- package/concepts/de/Tool-Bash.md +0 -167
- package/concepts/de/Tool-Edit.md +0 -46
- package/concepts/de/Tool-EnterPlanMode.md +0 -128
- package/concepts/de/Tool-EnterWorktree.md +0 -56
- package/concepts/de/Tool-ExitPlanMode.md +0 -62
- package/concepts/de/Tool-Glob.md +0 -39
- package/concepts/de/Tool-Grep.md +0 -57
- package/concepts/de/Tool-NotebookEdit.md +0 -33
- package/concepts/de/Tool-Read.md +0 -56
- package/concepts/de/Tool-SendMessage.md +0 -41
- package/concepts/de/Tool-Skill.md +0 -56
- package/concepts/de/Tool-Task.md +0 -51
- package/concepts/de/Tool-TaskCreate.md +0 -78
- package/concepts/de/Tool-TaskGet.md +0 -56
- package/concepts/de/Tool-TaskList.md +0 -56
- package/concepts/de/Tool-TaskOutput.md +0 -40
- package/concepts/de/Tool-TaskStop.md +0 -32
- package/concepts/de/Tool-TaskUpdate.md +0 -123
- package/concepts/de/Tool-TeamCreate.md +0 -38
- package/concepts/de/Tool-TeamDelete.md +0 -30
- package/concepts/de/Tool-WebFetch.md +0 -55
- package/concepts/de/Tool-WebSearch.md +0 -58
- package/concepts/de/Tool-Write.md +0 -39
- package/concepts/de/Tool-executeCode.md +0 -41
- package/concepts/de/Tool-getDiagnostics.md +0 -32
- package/concepts/de/Tools.md +0 -67
- package/concepts/en/CacheRebuild.md +0 -36
- package/concepts/en/KVCacheContent.md +0 -49
- package/concepts/en/Tool-Agent.md +0 -125
- package/concepts/en/Tool-AskUserQuestion.md +0 -75
- package/concepts/en/Tool-Bash.md +0 -167
- package/concepts/en/Tool-Edit.md +0 -46
- package/concepts/en/Tool-EnterPlanMode.md +0 -128
- package/concepts/en/Tool-EnterWorktree.md +0 -56
- package/concepts/en/Tool-ExitPlanMode.md +0 -62
- package/concepts/en/Tool-Glob.md +0 -39
- package/concepts/en/Tool-Grep.md +0 -57
- package/concepts/en/Tool-NotebookEdit.md +0 -33
- package/concepts/en/Tool-Read.md +0 -56
- package/concepts/en/Tool-SendMessage.md +0 -41
- package/concepts/en/Tool-Skill.md +0 -56
- package/concepts/en/Tool-Task.md +0 -51
- package/concepts/en/Tool-TaskCreate.md +0 -78
- package/concepts/en/Tool-TaskGet.md +0 -56
- package/concepts/en/Tool-TaskList.md +0 -56
- package/concepts/en/Tool-TaskOutput.md +0 -40
- package/concepts/en/Tool-TaskStop.md +0 -32
- package/concepts/en/Tool-TaskUpdate.md +0 -123
- package/concepts/en/Tool-TeamCreate.md +0 -38
- package/concepts/en/Tool-TeamDelete.md +0 -30
- package/concepts/en/Tool-WebFetch.md +0 -55
- package/concepts/en/Tool-WebSearch.md +0 -58
- package/concepts/en/Tool-Write.md +0 -39
- package/concepts/en/Tool-executeCode.md +0 -41
- package/concepts/en/Tool-getDiagnostics.md +0 -32
- package/concepts/es/CacheRebuild.md +0 -36
- package/concepts/es/KVCacheContent.md +0 -49
- package/concepts/es/Tool-Agent.md +0 -125
- package/concepts/es/Tool-AskUserQuestion.md +0 -75
- package/concepts/es/Tool-Bash.md +0 -167
- package/concepts/es/Tool-Edit.md +0 -46
- package/concepts/es/Tool-EnterPlanMode.md +0 -128
- package/concepts/es/Tool-EnterWorktree.md +0 -56
- package/concepts/es/Tool-ExitPlanMode.md +0 -62
- package/concepts/es/Tool-Glob.md +0 -39
- package/concepts/es/Tool-Grep.md +0 -57
- package/concepts/es/Tool-NotebookEdit.md +0 -33
- package/concepts/es/Tool-Read.md +0 -56
- package/concepts/es/Tool-SendMessage.md +0 -41
- package/concepts/es/Tool-Skill.md +0 -56
- package/concepts/es/Tool-Task.md +0 -51
- package/concepts/es/Tool-TaskCreate.md +0 -78
- package/concepts/es/Tool-TaskGet.md +0 -56
- package/concepts/es/Tool-TaskList.md +0 -56
- package/concepts/es/Tool-TaskOutput.md +0 -40
- package/concepts/es/Tool-TaskStop.md +0 -32
- package/concepts/es/Tool-TaskUpdate.md +0 -123
- package/concepts/es/Tool-TeamCreate.md +0 -38
- package/concepts/es/Tool-TeamDelete.md +0 -30
- package/concepts/es/Tool-WebFetch.md +0 -55
- package/concepts/es/Tool-WebSearch.md +0 -58
- package/concepts/es/Tool-Write.md +0 -39
- package/concepts/es/Tool-executeCode.md +0 -41
- package/concepts/es/Tool-getDiagnostics.md +0 -32
- package/concepts/es/Tools.md +0 -67
- package/concepts/fr/CacheRebuild.md +0 -36
- package/concepts/fr/KVCacheContent.md +0 -49
- package/concepts/fr/Tool-Agent.md +0 -125
- package/concepts/fr/Tool-AskUserQuestion.md +0 -75
- package/concepts/fr/Tool-Bash.md +0 -167
- package/concepts/fr/Tool-Edit.md +0 -46
- package/concepts/fr/Tool-EnterPlanMode.md +0 -128
- package/concepts/fr/Tool-EnterWorktree.md +0 -56
- package/concepts/fr/Tool-ExitPlanMode.md +0 -62
- package/concepts/fr/Tool-Glob.md +0 -39
- package/concepts/fr/Tool-Grep.md +0 -57
- package/concepts/fr/Tool-NotebookEdit.md +0 -33
- package/concepts/fr/Tool-Read.md +0 -56
- package/concepts/fr/Tool-SendMessage.md +0 -41
- package/concepts/fr/Tool-Skill.md +0 -56
- package/concepts/fr/Tool-Task.md +0 -51
- package/concepts/fr/Tool-TaskCreate.md +0 -78
- package/concepts/fr/Tool-TaskGet.md +0 -56
- package/concepts/fr/Tool-TaskList.md +0 -56
- package/concepts/fr/Tool-TaskOutput.md +0 -40
- package/concepts/fr/Tool-TaskStop.md +0 -32
- package/concepts/fr/Tool-TaskUpdate.md +0 -123
- package/concepts/fr/Tool-TeamCreate.md +0 -38
- package/concepts/fr/Tool-TeamDelete.md +0 -30
- package/concepts/fr/Tool-WebFetch.md +0 -55
- package/concepts/fr/Tool-WebSearch.md +0 -58
- package/concepts/fr/Tool-Write.md +0 -39
- package/concepts/fr/Tool-executeCode.md +0 -41
- package/concepts/fr/Tool-getDiagnostics.md +0 -32
- package/concepts/fr/Tools.md +0 -67
- package/concepts/it/CacheRebuild.md +0 -36
- package/concepts/it/KVCacheContent.md +0 -49
- package/concepts/it/Tool-Agent.md +0 -125
- package/concepts/it/Tool-AskUserQuestion.md +0 -75
- package/concepts/it/Tool-Bash.md +0 -167
- package/concepts/it/Tool-Edit.md +0 -46
- package/concepts/it/Tool-EnterPlanMode.md +0 -128
- package/concepts/it/Tool-EnterWorktree.md +0 -56
- package/concepts/it/Tool-ExitPlanMode.md +0 -62
- package/concepts/it/Tool-Glob.md +0 -39
- package/concepts/it/Tool-Grep.md +0 -57
- package/concepts/it/Tool-NotebookEdit.md +0 -33
- package/concepts/it/Tool-Read.md +0 -56
- package/concepts/it/Tool-SendMessage.md +0 -41
- package/concepts/it/Tool-Skill.md +0 -56
- package/concepts/it/Tool-Task.md +0 -51
- package/concepts/it/Tool-TaskCreate.md +0 -78
- package/concepts/it/Tool-TaskGet.md +0 -56
- package/concepts/it/Tool-TaskList.md +0 -56
- package/concepts/it/Tool-TaskOutput.md +0 -40
- package/concepts/it/Tool-TaskStop.md +0 -32
- package/concepts/it/Tool-TaskUpdate.md +0 -123
- package/concepts/it/Tool-TeamCreate.md +0 -38
- package/concepts/it/Tool-TeamDelete.md +0 -30
- package/concepts/it/Tool-WebFetch.md +0 -55
- package/concepts/it/Tool-WebSearch.md +0 -58
- package/concepts/it/Tool-Write.md +0 -39
- package/concepts/it/Tool-executeCode.md +0 -41
- package/concepts/it/Tool-getDiagnostics.md +0 -32
- package/concepts/it/Tools.md +0 -67
- package/concepts/ja/CacheRebuild.md +0 -36
- package/concepts/ja/KVCacheContent.md +0 -49
- package/concepts/ja/Tool-Agent.md +0 -125
- package/concepts/ja/Tool-AskUserQuestion.md +0 -75
- package/concepts/ja/Tool-Bash.md +0 -167
- package/concepts/ja/Tool-Edit.md +0 -46
- package/concepts/ja/Tool-EnterPlanMode.md +0 -128
- package/concepts/ja/Tool-EnterWorktree.md +0 -56
- package/concepts/ja/Tool-ExitPlanMode.md +0 -62
- package/concepts/ja/Tool-Glob.md +0 -39
- package/concepts/ja/Tool-Grep.md +0 -57
- package/concepts/ja/Tool-NotebookEdit.md +0 -33
- package/concepts/ja/Tool-Read.md +0 -56
- package/concepts/ja/Tool-SendMessage.md +0 -41
- package/concepts/ja/Tool-Skill.md +0 -56
- package/concepts/ja/Tool-Task.md +0 -51
- package/concepts/ja/Tool-TaskCreate.md +0 -78
- package/concepts/ja/Tool-TaskGet.md +0 -56
- package/concepts/ja/Tool-TaskList.md +0 -56
- package/concepts/ja/Tool-TaskOutput.md +0 -40
- package/concepts/ja/Tool-TaskStop.md +0 -32
- package/concepts/ja/Tool-TaskUpdate.md +0 -123
- package/concepts/ja/Tool-TeamCreate.md +0 -38
- package/concepts/ja/Tool-TeamDelete.md +0 -30
- package/concepts/ja/Tool-WebFetch.md +0 -55
- package/concepts/ja/Tool-WebSearch.md +0 -58
- package/concepts/ja/Tool-Write.md +0 -39
- package/concepts/ja/Tool-executeCode.md +0 -41
- package/concepts/ja/Tool-getDiagnostics.md +0 -32
- package/concepts/ja/Tools.md +0 -67
- package/concepts/ko/CacheRebuild.md +0 -36
- package/concepts/ko/KVCacheContent.md +0 -49
- package/concepts/ko/Tool-Agent.md +0 -125
- package/concepts/ko/Tool-AskUserQuestion.md +0 -75
- package/concepts/ko/Tool-Bash.md +0 -167
- package/concepts/ko/Tool-Edit.md +0 -46
- package/concepts/ko/Tool-EnterPlanMode.md +0 -128
- package/concepts/ko/Tool-EnterWorktree.md +0 -56
- package/concepts/ko/Tool-ExitPlanMode.md +0 -62
- package/concepts/ko/Tool-Glob.md +0 -39
- package/concepts/ko/Tool-Grep.md +0 -57
- package/concepts/ko/Tool-NotebookEdit.md +0 -33
- package/concepts/ko/Tool-Read.md +0 -56
- package/concepts/ko/Tool-SendMessage.md +0 -41
- package/concepts/ko/Tool-Skill.md +0 -56
- package/concepts/ko/Tool-Task.md +0 -51
- package/concepts/ko/Tool-TaskCreate.md +0 -78
- package/concepts/ko/Tool-TaskGet.md +0 -56
- package/concepts/ko/Tool-TaskList.md +0 -56
- package/concepts/ko/Tool-TaskOutput.md +0 -40
- package/concepts/ko/Tool-TaskStop.md +0 -32
- package/concepts/ko/Tool-TaskUpdate.md +0 -123
- package/concepts/ko/Tool-TeamCreate.md +0 -38
- package/concepts/ko/Tool-TeamDelete.md +0 -30
- package/concepts/ko/Tool-WebFetch.md +0 -55
- package/concepts/ko/Tool-WebSearch.md +0 -58
- package/concepts/ko/Tool-Write.md +0 -39
- package/concepts/ko/Tool-executeCode.md +0 -41
- package/concepts/ko/Tool-getDiagnostics.md +0 -32
- package/concepts/ko/Tools.md +0 -67
- package/concepts/no/CacheRebuild.md +0 -36
- package/concepts/no/KVCacheContent.md +0 -49
- package/concepts/no/Tool-Agent.md +0 -125
- package/concepts/no/Tool-AskUserQuestion.md +0 -75
- package/concepts/no/Tool-Bash.md +0 -167
- package/concepts/no/Tool-Edit.md +0 -46
- package/concepts/no/Tool-EnterPlanMode.md +0 -128
- package/concepts/no/Tool-EnterWorktree.md +0 -56
- package/concepts/no/Tool-ExitPlanMode.md +0 -62
- package/concepts/no/Tool-Glob.md +0 -39
- package/concepts/no/Tool-Grep.md +0 -57
- package/concepts/no/Tool-NotebookEdit.md +0 -33
- package/concepts/no/Tool-Read.md +0 -56
- package/concepts/no/Tool-SendMessage.md +0 -41
- package/concepts/no/Tool-Skill.md +0 -56
- package/concepts/no/Tool-Task.md +0 -51
- package/concepts/no/Tool-TaskCreate.md +0 -78
- package/concepts/no/Tool-TaskGet.md +0 -56
- package/concepts/no/Tool-TaskList.md +0 -56
- package/concepts/no/Tool-TaskOutput.md +0 -40
- package/concepts/no/Tool-TaskStop.md +0 -32
- package/concepts/no/Tool-TaskUpdate.md +0 -123
- package/concepts/no/Tool-TeamCreate.md +0 -38
- package/concepts/no/Tool-TeamDelete.md +0 -30
- package/concepts/no/Tool-WebFetch.md +0 -55
- package/concepts/no/Tool-WebSearch.md +0 -58
- package/concepts/no/Tool-Write.md +0 -39
- package/concepts/no/Tool-executeCode.md +0 -41
- package/concepts/no/Tool-getDiagnostics.md +0 -32
- package/concepts/no/Tools.md +0 -67
- package/concepts/pl/CacheRebuild.md +0 -36
- package/concepts/pl/KVCacheContent.md +0 -49
- package/concepts/pl/Tool-Agent.md +0 -125
- package/concepts/pl/Tool-AskUserQuestion.md +0 -75
- package/concepts/pl/Tool-Bash.md +0 -167
- package/concepts/pl/Tool-Edit.md +0 -46
- package/concepts/pl/Tool-EnterPlanMode.md +0 -128
- package/concepts/pl/Tool-EnterWorktree.md +0 -56
- package/concepts/pl/Tool-ExitPlanMode.md +0 -62
- package/concepts/pl/Tool-Glob.md +0 -39
- package/concepts/pl/Tool-Grep.md +0 -57
- package/concepts/pl/Tool-NotebookEdit.md +0 -33
- package/concepts/pl/Tool-Read.md +0 -56
- package/concepts/pl/Tool-SendMessage.md +0 -41
- package/concepts/pl/Tool-Skill.md +0 -56
- package/concepts/pl/Tool-Task.md +0 -51
- package/concepts/pl/Tool-TaskCreate.md +0 -78
- package/concepts/pl/Tool-TaskGet.md +0 -56
- package/concepts/pl/Tool-TaskList.md +0 -56
- package/concepts/pl/Tool-TaskOutput.md +0 -40
- package/concepts/pl/Tool-TaskStop.md +0 -32
- package/concepts/pl/Tool-TaskUpdate.md +0 -123
- package/concepts/pl/Tool-TeamCreate.md +0 -38
- package/concepts/pl/Tool-TeamDelete.md +0 -30
- package/concepts/pl/Tool-WebFetch.md +0 -55
- package/concepts/pl/Tool-WebSearch.md +0 -58
- package/concepts/pl/Tool-Write.md +0 -39
- package/concepts/pl/Tool-executeCode.md +0 -41
- package/concepts/pl/Tool-getDiagnostics.md +0 -32
- package/concepts/pl/Tools.md +0 -67
- package/concepts/pt-BR/CacheRebuild.md +0 -36
- package/concepts/pt-BR/KVCacheContent.md +0 -49
- package/concepts/pt-BR/Tool-Agent.md +0 -125
- package/concepts/pt-BR/Tool-AskUserQuestion.md +0 -75
- package/concepts/pt-BR/Tool-Bash.md +0 -167
- package/concepts/pt-BR/Tool-Edit.md +0 -46
- package/concepts/pt-BR/Tool-EnterPlanMode.md +0 -128
- package/concepts/pt-BR/Tool-EnterWorktree.md +0 -56
- package/concepts/pt-BR/Tool-ExitPlanMode.md +0 -62
- package/concepts/pt-BR/Tool-Glob.md +0 -39
- package/concepts/pt-BR/Tool-Grep.md +0 -57
- package/concepts/pt-BR/Tool-NotebookEdit.md +0 -33
- package/concepts/pt-BR/Tool-Read.md +0 -56
- package/concepts/pt-BR/Tool-SendMessage.md +0 -41
- package/concepts/pt-BR/Tool-Skill.md +0 -56
- package/concepts/pt-BR/Tool-Task.md +0 -51
- package/concepts/pt-BR/Tool-TaskCreate.md +0 -78
- package/concepts/pt-BR/Tool-TaskGet.md +0 -56
- package/concepts/pt-BR/Tool-TaskList.md +0 -56
- package/concepts/pt-BR/Tool-TaskOutput.md +0 -40
- package/concepts/pt-BR/Tool-TaskStop.md +0 -32
- package/concepts/pt-BR/Tool-TaskUpdate.md +0 -123
- package/concepts/pt-BR/Tool-TeamCreate.md +0 -38
- package/concepts/pt-BR/Tool-TeamDelete.md +0 -30
- package/concepts/pt-BR/Tool-WebFetch.md +0 -55
- package/concepts/pt-BR/Tool-WebSearch.md +0 -58
- package/concepts/pt-BR/Tool-Write.md +0 -39
- package/concepts/pt-BR/Tool-executeCode.md +0 -41
- package/concepts/pt-BR/Tool-getDiagnostics.md +0 -32
- package/concepts/pt-BR/Tools.md +0 -67
- package/concepts/ru/CacheRebuild.md +0 -36
- package/concepts/ru/KVCacheContent.md +0 -49
- package/concepts/ru/Tool-Agent.md +0 -125
- package/concepts/ru/Tool-AskUserQuestion.md +0 -75
- package/concepts/ru/Tool-Bash.md +0 -167
- package/concepts/ru/Tool-Edit.md +0 -46
- package/concepts/ru/Tool-EnterPlanMode.md +0 -128
- package/concepts/ru/Tool-EnterWorktree.md +0 -56
- package/concepts/ru/Tool-ExitPlanMode.md +0 -62
- package/concepts/ru/Tool-Glob.md +0 -39
- package/concepts/ru/Tool-Grep.md +0 -57
- package/concepts/ru/Tool-NotebookEdit.md +0 -33
- package/concepts/ru/Tool-Read.md +0 -56
- package/concepts/ru/Tool-SendMessage.md +0 -41
- package/concepts/ru/Tool-Skill.md +0 -56
- package/concepts/ru/Tool-Task.md +0 -51
- package/concepts/ru/Tool-TaskCreate.md +0 -78
- package/concepts/ru/Tool-TaskGet.md +0 -56
- package/concepts/ru/Tool-TaskList.md +0 -56
- package/concepts/ru/Tool-TaskOutput.md +0 -40
- package/concepts/ru/Tool-TaskStop.md +0 -32
- package/concepts/ru/Tool-TaskUpdate.md +0 -123
- package/concepts/ru/Tool-TeamCreate.md +0 -38
- package/concepts/ru/Tool-TeamDelete.md +0 -30
- package/concepts/ru/Tool-WebFetch.md +0 -55
- package/concepts/ru/Tool-WebSearch.md +0 -58
- package/concepts/ru/Tool-Write.md +0 -39
- package/concepts/ru/Tool-executeCode.md +0 -41
- package/concepts/ru/Tool-getDiagnostics.md +0 -32
- package/concepts/ru/Tools.md +0 -67
- package/concepts/th/CacheRebuild.md +0 -36
- package/concepts/th/KVCacheContent.md +0 -49
- package/concepts/th/Tool-Agent.md +0 -125
- package/concepts/th/Tool-AskUserQuestion.md +0 -75
- package/concepts/th/Tool-Bash.md +0 -167
- package/concepts/th/Tool-Edit.md +0 -46
- package/concepts/th/Tool-EnterPlanMode.md +0 -128
- package/concepts/th/Tool-EnterWorktree.md +0 -56
- package/concepts/th/Tool-ExitPlanMode.md +0 -62
- package/concepts/th/Tool-Glob.md +0 -39
- package/concepts/th/Tool-Grep.md +0 -57
- package/concepts/th/Tool-NotebookEdit.md +0 -33
- package/concepts/th/Tool-Read.md +0 -56
- package/concepts/th/Tool-SendMessage.md +0 -41
- package/concepts/th/Tool-Skill.md +0 -56
- package/concepts/th/Tool-Task.md +0 -51
- package/concepts/th/Tool-TaskCreate.md +0 -78
- package/concepts/th/Tool-TaskGet.md +0 -56
- package/concepts/th/Tool-TaskList.md +0 -56
- package/concepts/th/Tool-TaskOutput.md +0 -40
- package/concepts/th/Tool-TaskStop.md +0 -32
- package/concepts/th/Tool-TaskUpdate.md +0 -123
- package/concepts/th/Tool-TeamCreate.md +0 -38
- package/concepts/th/Tool-TeamDelete.md +0 -30
- package/concepts/th/Tool-WebFetch.md +0 -55
- package/concepts/th/Tool-WebSearch.md +0 -58
- package/concepts/th/Tool-Write.md +0 -39
- package/concepts/th/Tool-executeCode.md +0 -41
- package/concepts/th/Tool-getDiagnostics.md +0 -32
- package/concepts/th/Tools.md +0 -67
- package/concepts/tr/CacheRebuild.md +0 -36
- package/concepts/tr/KVCacheContent.md +0 -49
- package/concepts/tr/Tool-Agent.md +0 -125
- package/concepts/tr/Tool-AskUserQuestion.md +0 -75
- package/concepts/tr/Tool-Bash.md +0 -167
- package/concepts/tr/Tool-Edit.md +0 -46
- package/concepts/tr/Tool-EnterPlanMode.md +0 -128
- package/concepts/tr/Tool-EnterWorktree.md +0 -56
- package/concepts/tr/Tool-ExitPlanMode.md +0 -62
- package/concepts/tr/Tool-Glob.md +0 -39
- package/concepts/tr/Tool-Grep.md +0 -57
- package/concepts/tr/Tool-NotebookEdit.md +0 -33
- package/concepts/tr/Tool-Read.md +0 -56
- package/concepts/tr/Tool-SendMessage.md +0 -41
- package/concepts/tr/Tool-Skill.md +0 -56
- package/concepts/tr/Tool-Task.md +0 -51
- package/concepts/tr/Tool-TaskCreate.md +0 -78
- package/concepts/tr/Tool-TaskGet.md +0 -56
- package/concepts/tr/Tool-TaskList.md +0 -56
- package/concepts/tr/Tool-TaskOutput.md +0 -40
- package/concepts/tr/Tool-TaskStop.md +0 -32
- package/concepts/tr/Tool-TaskUpdate.md +0 -123
- package/concepts/tr/Tool-TeamCreate.md +0 -38
- package/concepts/tr/Tool-TeamDelete.md +0 -30
- package/concepts/tr/Tool-WebFetch.md +0 -55
- package/concepts/tr/Tool-WebSearch.md +0 -58
- package/concepts/tr/Tool-Write.md +0 -39
- package/concepts/tr/Tool-executeCode.md +0 -41
- package/concepts/tr/Tool-getDiagnostics.md +0 -32
- package/concepts/tr/Tools.md +0 -67
- package/concepts/uk/CacheRebuild.md +0 -36
- package/concepts/uk/KVCacheContent.md +0 -49
- package/concepts/uk/Tool-Agent.md +0 -125
- package/concepts/uk/Tool-AskUserQuestion.md +0 -75
- package/concepts/uk/Tool-Bash.md +0 -167
- package/concepts/uk/Tool-Edit.md +0 -46
- package/concepts/uk/Tool-EnterPlanMode.md +0 -128
- package/concepts/uk/Tool-EnterWorktree.md +0 -56
- package/concepts/uk/Tool-ExitPlanMode.md +0 -62
- package/concepts/uk/Tool-Glob.md +0 -39
- package/concepts/uk/Tool-Grep.md +0 -57
- package/concepts/uk/Tool-NotebookEdit.md +0 -33
- package/concepts/uk/Tool-Read.md +0 -56
- package/concepts/uk/Tool-SendMessage.md +0 -41
- package/concepts/uk/Tool-Skill.md +0 -56
- package/concepts/uk/Tool-Task.md +0 -51
- package/concepts/uk/Tool-TaskCreate.md +0 -78
- package/concepts/uk/Tool-TaskGet.md +0 -56
- package/concepts/uk/Tool-TaskList.md +0 -56
- package/concepts/uk/Tool-TaskOutput.md +0 -40
- package/concepts/uk/Tool-TaskStop.md +0 -32
- package/concepts/uk/Tool-TaskUpdate.md +0 -123
- package/concepts/uk/Tool-TeamCreate.md +0 -38
- package/concepts/uk/Tool-TeamDelete.md +0 -30
- package/concepts/uk/Tool-WebFetch.md +0 -55
- package/concepts/uk/Tool-WebSearch.md +0 -58
- package/concepts/uk/Tool-Write.md +0 -39
- package/concepts/uk/Tool-executeCode.md +0 -41
- package/concepts/uk/Tool-getDiagnostics.md +0 -32
- package/concepts/uk/Tools.md +0 -67
- package/concepts/zh/CacheRebuild.md +0 -36
- package/concepts/zh/KVCacheContent.md +0 -49
- package/concepts/zh/Tool-Agent.md +0 -125
- package/concepts/zh/Tool-AskUserQuestion.md +0 -75
- package/concepts/zh/Tool-Bash.md +0 -167
- package/concepts/zh/Tool-Edit.md +0 -46
- package/concepts/zh/Tool-EnterPlanMode.md +0 -128
- package/concepts/zh/Tool-EnterWorktree.md +0 -56
- package/concepts/zh/Tool-ExitPlanMode.md +0 -62
- package/concepts/zh/Tool-Glob.md +0 -39
- package/concepts/zh/Tool-Grep.md +0 -57
- package/concepts/zh/Tool-NotebookEdit.md +0 -33
- package/concepts/zh/Tool-Read.md +0 -56
- package/concepts/zh/Tool-SendMessage.md +0 -41
- package/concepts/zh/Tool-Skill.md +0 -56
- package/concepts/zh/Tool-Task.md +0 -51
- package/concepts/zh/Tool-TaskCreate.md +0 -78
- package/concepts/zh/Tool-TaskGet.md +0 -56
- package/concepts/zh/Tool-TaskList.md +0 -56
- package/concepts/zh/Tool-TaskOutput.md +0 -40
- package/concepts/zh/Tool-TaskStop.md +0 -32
- package/concepts/zh/Tool-TaskUpdate.md +0 -123
- package/concepts/zh/Tool-TeamCreate.md +0 -38
- package/concepts/zh/Tool-TeamDelete.md +0 -30
- package/concepts/zh/Tool-WebFetch.md +0 -55
- package/concepts/zh/Tool-WebSearch.md +0 -58
- package/concepts/zh/Tool-Write.md +0 -39
- package/concepts/zh/Tool-executeCode.md +0 -41
- package/concepts/zh/Tool-getDiagnostics.md +0 -32
- package/concepts/zh-TW/CacheRebuild.md +0 -36
- package/concepts/zh-TW/KVCacheContent.md +0 -49
- package/concepts/zh-TW/Tool-Agent.md +0 -125
- package/concepts/zh-TW/Tool-AskUserQuestion.md +0 -75
- package/concepts/zh-TW/Tool-Bash.md +0 -167
- package/concepts/zh-TW/Tool-Edit.md +0 -46
- package/concepts/zh-TW/Tool-EnterPlanMode.md +0 -128
- package/concepts/zh-TW/Tool-EnterWorktree.md +0 -56
- package/concepts/zh-TW/Tool-ExitPlanMode.md +0 -62
- package/concepts/zh-TW/Tool-Glob.md +0 -39
- package/concepts/zh-TW/Tool-Grep.md +0 -57
- package/concepts/zh-TW/Tool-NotebookEdit.md +0 -33
- package/concepts/zh-TW/Tool-Read.md +0 -56
- package/concepts/zh-TW/Tool-SendMessage.md +0 -41
- package/concepts/zh-TW/Tool-Skill.md +0 -56
- package/concepts/zh-TW/Tool-Task.md +0 -51
- package/concepts/zh-TW/Tool-TaskCreate.md +0 -78
- package/concepts/zh-TW/Tool-TaskGet.md +0 -56
- package/concepts/zh-TW/Tool-TaskList.md +0 -56
- package/concepts/zh-TW/Tool-TaskOutput.md +0 -40
- package/concepts/zh-TW/Tool-TaskStop.md +0 -32
- package/concepts/zh-TW/Tool-TaskUpdate.md +0 -123
- package/concepts/zh-TW/Tool-TeamCreate.md +0 -38
- package/concepts/zh-TW/Tool-TeamDelete.md +0 -30
- package/concepts/zh-TW/Tool-WebFetch.md +0 -55
- package/concepts/zh-TW/Tool-WebSearch.md +0 -58
- package/concepts/zh-TW/Tool-Write.md +0 -39
- package/concepts/zh-TW/Tool-executeCode.md +0 -41
- package/concepts/zh-TW/Tool-getDiagnostics.md +0 -32
- package/concepts/zh-TW/Tools.md +0 -67
- package/dist/assets/App-CF-qZ3B6.js +0 -41
- package/dist/assets/App-fG2r3jFq.css +0 -1
- package/dist/assets/AppHeader-BqUGBbBx.css +0 -41
- package/dist/assets/AppHeader.module-BjoBBbCB.js +0 -1250
- package/dist/assets/Mobile-BnDkLcBv.css +0 -1
- package/dist/assets/Mobile-ByhBkyqb.js +0 -10
- package/dist/assets/_baseUniq-5XIuSb_1.js +0 -1
- package/dist/assets/arc-V3mJhKFJ.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-22bjxOE1.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-Brshek1N.js +0 -132
- package/dist/assets/c4Diagram-AHTNJAMY-DQHIuxkd.js +0 -10
- package/dist/assets/channel-SmGaOM6g.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-BQE7pifU.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-DcZwv_PV.js +0 -206
- package/dist/assets/chunk-55IACEB6-Gd_3-vV5.js +0 -1
- package/dist/assets/chunk-EDXVE4YY-BbnYdg9M.js +0 -1
- package/dist/assets/chunk-FMBD7UC4-NSh7z90L.js +0 -15
- package/dist/assets/chunk-OYMX7WX6-DdFwjHsc.js +0 -231
- package/dist/assets/chunk-QZHKN3VN-C-1z3DA3.js +0 -1
- package/dist/assets/chunk-YZCP3GAM-w_NiY6dL.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Bu96zciS.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-Bu96zciS.js +0 -1
- package/dist/assets/clone-u2hQVO7u.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-bHlM-ydV.js +0 -1
- package/dist/assets/cytoscape.esm-DxGcaOPV.js +0 -331
- package/dist/assets/dagre-KV5264BT-2cCXOuNw.js +0 -4
- package/dist/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/dist/assets/diagram-5BDNPKRD-C70P_Nd1.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-C2JvwT8U.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-C9ygMSLV.js +0 -43
- package/dist/assets/diagram-TYMM5635-x-SMPmqb.js +0 -24
- package/dist/assets/erDiagram-SMLLAGMA-BGIbL82W.js +0 -85
- package/dist/assets/flowDiagram-DWJPFMVM-Dg0qwguE.js +0 -162
- package/dist/assets/ganttDiagram-T4ZO3ILL-XX5qCwKD.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-DFil4bpA.js +0 -106
- package/dist/assets/graph-DJ6egHOd.js +0 -1
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +0 -22
- package/dist/assets/index-DCtZ2KSX.css +0 -1
- package/dist/assets/index-DYIYWRaO.js +0 -41
- package/dist/assets/infoDiagram-42DDH7IO-yB7VRrpO.js +0 -2
- package/dist/assets/init-Gi6I4Gst.js +0 -1
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DZ_lPco0.js +0 -70
- package/dist/assets/journeyDiagram-VCZTEJTY-B-FlPR2W.js +0 -139
- package/dist/assets/kanban-definition-6JOO6SKY-mgJwVxKf.js +0 -89
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-BjzpJ8ZU.js +0 -1
- package/dist/assets/linear-CqOq1rA7.js +0 -1
- package/dist/assets/mermaid.core-CZyQhtml.js +0 -308
- package/dist/assets/min-B_tad8Qz.js +0 -1
- package/dist/assets/mindmap-definition-QFDTVHPH-CgBeHk8K.js +0 -96
- package/dist/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-CHENa0KJ.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUwZwCpc.js +0 -7
- package/dist/assets/requirementDiagram-MS252O5E-H7OaNjNn.js +0 -84
- package/dist/assets/sankeyDiagram-XADWPNL6-6UqaFrBf.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-DBgpLSsr.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-D_6TWlLl.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Cg06jbzN.js +0 -1
- package/dist/assets/timeline-definition-GMOUNBTQ-9_saaHvl.js +0 -120
- package/dist/assets/vennDiagram-DHZGUBPP-DMUS1fiT.js +0 -34
- package/dist/assets/wardley-RL74JXVD-Y-_Nnpu2.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-DIh62HY7.js +0 -20
- package/dist/assets/xychartDiagram-5P7HB3ND-B_CLrnpt.js +0 -7
- package/lib/kv-cache-analyzer.js +0 -164
package/src/AppBase.jsx
ADDED
|
@@ -0,0 +1,2803 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ConfigProvider, theme, Modal, Spin, Button, message } from 'antd';
|
|
3
|
+
import { uploadFileAndGetPath } from './components/terminal/TerminalPanel';
|
|
4
|
+
import { DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
5
|
+
import { isMobile, isPad, hasNativeZoom } from './env';
|
|
6
|
+
import WorkspaceList from './components/dashboard/WorkspaceList';
|
|
7
|
+
import OpenFolderIcon from './components/common/OpenFolderIcon';
|
|
8
|
+
import LogTable from './components/viewers/LogTable';
|
|
9
|
+
import { t, getLang, setLang } from './i18n';
|
|
10
|
+
import { SettingsContext } from './contexts/SettingsContext';
|
|
11
|
+
import { filterRelevantRequests, isRelevantRequest, visibleRequests } from './utils/helpers';
|
|
12
|
+
import { snapToPreset, stepPreset } from './utils/displayScaleHelper';
|
|
13
|
+
import { getProjectAlias, subscribeToAlias } from './utils/projectAlias';
|
|
14
|
+
import { isMainAgent, isSessionBoundary, getMainAgentSessionKey, setTeammateNameSeeds, clearTeammateNameSeeds } from './utils/contentFilter';
|
|
15
|
+
import { apiUrl, getBasePath } from './utils/apiUrl';
|
|
16
|
+
import { publish as publishWorkflowUpdate } from './utils/workflowStore';
|
|
17
|
+
import { reportSwallowed } from './utils/errorReport';
|
|
18
|
+
import { playEvent as playVoiceEvent, unlockAudio, setTurnEndCooldownMs } from './utils/voicePackPlayer';
|
|
19
|
+
import { getDefaultBindingsForLocale as vpDefaultBindingsForLocale } from '../server/lib/voice-pack-events';
|
|
20
|
+
import { mergeVoicePackInto } from '../server/lib/approval-modal-prefs';
|
|
21
|
+
import { saveEntries, loadEntries, clearEntries, getCacheMeta, saveSessionEntries, loadSessionEntries } from './utils/entryCache';
|
|
22
|
+
import { buildSessionIndex, splitHotCold, mergeSessionIndices, HOT_SESSION_COUNT, assignMessageTimestamps, applyInPlaceLastMsgReplace, getSessionStableId, resolveDisplaySessions, getLatestSessionByActivity, resolveHydratedPin, runPinHydration, applyBatchEntryTimestamps } from './utils/sessionManager';
|
|
23
|
+
import { mergeMainAgentSessions as _mergeMainAgentSessions, isMergeBlockedEntry } from './utils/sessionMerge';
|
|
24
|
+
import { createConversationEntryNormalizer, shouldExcludeFromConversation, stampConversationMessageCount } from './utils/conversationEntryNormalize';
|
|
25
|
+
import { reconstructEntries, createIncrementalReconstructor } from '../server/lib/delta-reconstructor.js';
|
|
26
|
+
import { createEntrySlimmer, createIncrementalSlimmer, restoreSlimmedEntry, internEntryBigFields } from './utils/entry-slim.js';
|
|
27
|
+
import { yieldToMain, runChunkedPass, INGEST_BATCH_SIZE } from './utils/ingestPipeline.js';
|
|
28
|
+
import { reinitializeMermaid } from './hooks/useMermaidRender';
|
|
29
|
+
import { APPROVALS_REVIEWER_USER, normalizeApprovalsReviewer } from './utils/approvalReviewerOptions';
|
|
30
|
+
import styles from './App.module.css';
|
|
31
|
+
|
|
32
|
+
export { styles };
|
|
33
|
+
|
|
34
|
+
export const MAX_SESSIONS = (isMobile && !isPad) ? 30 : 100;
|
|
35
|
+
// /clear 后乐观水位:把上下文血条压到这个百分比,下一次 context_window SSE 推送会自动覆盖回真实值
|
|
36
|
+
export const OPTIMISTIC_CLEAR_PERCENT = 5;
|
|
37
|
+
|
|
38
|
+
// AntD 主题配置:模块顶层冻结常量。
|
|
39
|
+
// 旧实现是 getter 每次 render 返回新字面量,导致 antd cssinjs useTheme cache 永远 miss、
|
|
40
|
+
// flattenToken 反复跑。顶层常量保证主题不变时引用稳定。
|
|
41
|
+
const LIGHT_THEME_CONFIG = Object.freeze({
|
|
42
|
+
algorithm: theme.defaultAlgorithm,
|
|
43
|
+
token: Object.freeze({
|
|
44
|
+
colorPrimary: '#0969DA',
|
|
45
|
+
colorBgContainer: '#FFFFFF',
|
|
46
|
+
colorBgLayout: '#FAFAFA',
|
|
47
|
+
colorBgElevated: '#FFFFFF',
|
|
48
|
+
colorBorder: '#E0E0E0',
|
|
49
|
+
controlOutline: 'transparent',
|
|
50
|
+
controlOutlineWidth: 0,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const DARK_THEME_CONFIG = Object.freeze({
|
|
55
|
+
algorithm: theme.darkAlgorithm,
|
|
56
|
+
token: Object.freeze({
|
|
57
|
+
colorPrimary: '#1668dc',
|
|
58
|
+
colorBgContainer: '#111',
|
|
59
|
+
colorBgLayout: '#0a0a0a',
|
|
60
|
+
colorBgElevated: '#1e1e1e',
|
|
61
|
+
colorBorder: '#2a2a2a',
|
|
62
|
+
controlOutline: 'transparent',
|
|
63
|
+
controlOutlineWidth: 0,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 共享基类:包含 PC 和 Mobile 通用的状态管理、SSE 通信、数据处理、偏好设置等逻辑。
|
|
69
|
+
* 子类 App (PC) 和 Mobile 各自实现 render() 方法。
|
|
70
|
+
*
|
|
71
|
+
* settings 数据(codex-settings + preferences)集中由 SettingsContext 提供;
|
|
72
|
+
* setLang / setCodexConfigDir 这两个全局副作用已搬到 SettingsProvider 的 fetch 回调。
|
|
73
|
+
* AppBase 仍保留本地 state 副本用于即时 UI 反馈,POST 写入走 this.context.updatePreferences。
|
|
74
|
+
*/
|
|
75
|
+
class AppBase extends React.Component {
|
|
76
|
+
static contextType = SettingsContext;
|
|
77
|
+
|
|
78
|
+
constructor(props) {
|
|
79
|
+
super(props);
|
|
80
|
+
this.state = {
|
|
81
|
+
requests: [],
|
|
82
|
+
selectedIndex: null,
|
|
83
|
+
viewMode: 'raw',
|
|
84
|
+
mainAgentSessions: [], // [{ messages, response }]
|
|
85
|
+
// 「仅展示当前会话」锁定的会话稳定 id(= 会话起点 ts);null = 未锁定。
|
|
86
|
+
// 服务端持久化(按项目 + 可选 --pid 实例隔离),同进程多端经 SSE 实时一致。
|
|
87
|
+
// 命名提示:本字段就是服务端 /api/session-pin 里的 `pinnedSessionId`(同一个值,Ts/Id 同物)。
|
|
88
|
+
pinnedSessionTs: null,
|
|
89
|
+
// 本实例 id(来自 cxv --pid),随 /api/project-name 拿回;null = 默认模式。仅用于标题 项目(id)。
|
|
90
|
+
instanceId: null,
|
|
91
|
+
importModalVisible: false,
|
|
92
|
+
localLogs: {}, // { projectName: [{file, timestamp, size}] }
|
|
93
|
+
localLogsLoading: false,
|
|
94
|
+
refreshingStats: false,
|
|
95
|
+
logShowAllInstances: false, // 「显示全部实例」开关:true 时越过 --pid 硬隔离列出全部实例日志
|
|
96
|
+
showAll: false,
|
|
97
|
+
lang: getLang(),
|
|
98
|
+
userProfile: null, // { name, avatar }
|
|
99
|
+
projectName: '', // 当前监控的项目名称
|
|
100
|
+
// codex 自己存的项目偏好 model(~/.codex.json projects[cwd].lastModelUsage 推断),
|
|
101
|
+
// 用作 AppHeader 血条 calibration 'auto' 启动期的回落 hint(避 haiku init ping 误判 200K)。
|
|
102
|
+
// 初值 null = 还没拿到;/api/codex-settings 与 workspace_started SSE 都会塞值。
|
|
103
|
+
codexProjectModel: null,
|
|
104
|
+
resumeModalVisible: false,
|
|
105
|
+
resumeFileName: '',
|
|
106
|
+
resumeRememberChoice: false,
|
|
107
|
+
resumeAutoChoice: null, // null | "continue" | "new";出厂默认 'continue' 由 GET /api/preferences 注入(键缺失时),这里的 null 只是 pre-hydrate 占位
|
|
108
|
+
approvalsReviewer: APPROVALS_REVIEWER_USER,
|
|
109
|
+
logDir: '',
|
|
110
|
+
themeColor: /Windows/i.test(navigator.userAgent) ? 'dark' : 'light',
|
|
111
|
+
displayScale: 100, // 整体显示缩放百分比(100=原始大小),仅 Electron 桌面经 webFrame.setZoomFactor 原生缩放;浏览器交由原生快捷键
|
|
112
|
+
|
|
113
|
+
codexMissing: false,
|
|
114
|
+
updateModalVisible: false,
|
|
115
|
+
fileLoading: false,
|
|
116
|
+
fileLoadingCount: 0,
|
|
117
|
+
isDragging: false,
|
|
118
|
+
selectedLogs: new Set(), // Set<file>
|
|
119
|
+
githubStars: null,
|
|
120
|
+
cliMode: false,
|
|
121
|
+
sdkMode: false,
|
|
122
|
+
workspaceMode: false,
|
|
123
|
+
updateInfo: null,
|
|
124
|
+
pendingUploadPaths: [],
|
|
125
|
+
uploadingDrop: [], // [{id,name}] — 拖拽上传在途占位(spinner-only),供 ChatView 调谐 uploadingItems 实现「上传未完成时按发送→缓发不漏图」
|
|
126
|
+
contextWindow: null,
|
|
127
|
+
contextBarOptimistic: false, // /clear 后的乐观水位重置,下一次 context_window SSE 自动清除
|
|
128
|
+
contextBarLocked: false, // /clear 触发后强制血条 0K (0%),到用户发出非 /clear 消息时解锁
|
|
129
|
+
isStreaming: false,
|
|
130
|
+
streamingLatest: null, // { timestamp, url, content, model } — Live typewriter overlay for latest assistant message
|
|
131
|
+
hasMoreHistory: false,
|
|
132
|
+
loadingMore: false,
|
|
133
|
+
sessionIndex: [],
|
|
134
|
+
loadingSessionId: null,
|
|
135
|
+
proxyProfiles: [],
|
|
136
|
+
activeProxyId: 'max',
|
|
137
|
+
defaultConfig: null,
|
|
138
|
+
// ─── Approval modal global state ───
|
|
139
|
+
// approvalGlobal: { ptyPlan?, ask? } currently active in the (single) ChatView mounted in this app instance.
|
|
140
|
+
// Each entry carries { id, ..., handlers } as bubbled by ChatView.componentDidUpdate.
|
|
141
|
+
// Permission and SDK plan approval stay inline-only — they do NOT pop the global modal.
|
|
142
|
+
approvalGlobal: { ptyPlan: null, ask: null },
|
|
143
|
+
// approvalDismissedIds: pending ids the user has chosen to minimize. Reopens via bell / chip.
|
|
144
|
+
approvalDismissedIds: new Set(),
|
|
145
|
+
// approvalOtherTabs: aggregated state from other Electron tabs, pushed by main via tabBridge.onApprovalBroadcast.
|
|
146
|
+
approvalOtherTabs: [],
|
|
147
|
+
// approvalOwnPending: 当前 tab 在 main 进程聚合的 pending 计数(来自 approval-broadcast.ownPending)。
|
|
148
|
+
// 仅信息性使用(bell badge 显示「服务端记得有 N 条 pending」),不试图重写 approvalGlobal——
|
|
149
|
+
// approvalGlobal 含 questions / handlers 闭包无法跨 IPC 序列化,权威源是 ChatView 的 pendingAsk / pendingPtyPlan。
|
|
150
|
+
approvalOwnPending: { ask: 0, ptyPlan: 0 },
|
|
151
|
+
// ownTabId: numeric tab id pushed by main once on view init (electron only). null in pure web mode.
|
|
152
|
+
ownTabId: null,
|
|
153
|
+
// approvalPrefs: user toggles persisted to /api/preferences.
|
|
154
|
+
// soundEnabled = 合并后的"审批提示音"主开关(默认 ON),voicePack.enabled 始终 == soundEnabled。
|
|
155
|
+
// hydrate 时如检测到老版本独立两字段不一致,会强制对齐并一次性写回 server。
|
|
156
|
+
// events.turnEnd 仍默认 null(disabled,避免每轮都响)。
|
|
157
|
+
// Locale-aware initial seed: zh / zh-TW 新用户首次拿 sanguo,其它走 default (butler)。
|
|
158
|
+
// getLang() 在 i18n.js 模块加载时已调过 setLang(detectLanguage())(i18n.js:9465),
|
|
159
|
+
// AppBase constructor 进入这里时 currentLang 已就绪 — 单测见 voice-pack-events.test.js。
|
|
160
|
+
// 注意:这是 React state 初始 seed,不是 dynamic 重计算。运行时切语言不会重 seed
|
|
161
|
+
// binding(避免静默改变用户持久化选择 — "no silent migration" P0 规则)。
|
|
162
|
+
approvalPrefs: {
|
|
163
|
+
modalEnabled: true,
|
|
164
|
+
soundEnabled: true,
|
|
165
|
+
notifyOnlyWhenHidden: true,
|
|
166
|
+
planAutoApproveSeconds: 0, // 「Plan 自动审批」:0=关 / N=N 秒后自动批准 / -1=立即;仅 CLI(PTY) 路径
|
|
167
|
+
voicePack: {
|
|
168
|
+
enabled: true,
|
|
169
|
+
volume: 0.3,
|
|
170
|
+
events: { ...vpDefaultBindingsForLocale(getLang()) },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
this.eventSource = null;
|
|
175
|
+
this._liveConversationNormalizer = createConversationEntryNormalizer();
|
|
176
|
+
this._currentSessionId = null;
|
|
177
|
+
// pin 竞态守卫(_maintainPinState/_hydratePin/session_pin SSE 用):
|
|
178
|
+
// _isHydratingPin — 服务端 pin 的 GET 在途;期间禁 lazy-lock + 禁 persist,防抢在真值返回前误锁/回写。
|
|
179
|
+
// _applyingRemotePin — 正在采纳服务端值(hydrate/SSE);期间禁 persist,防把服务端值当本地改动回 POST(防回环)。
|
|
180
|
+
// _hydratePinSeq — monotonic token per hydrate round; a GET that resolves after a
|
|
181
|
+
// newer hydrate started (project switch, another reconnect) is discarded.
|
|
182
|
+
this._isHydratingPin = false;
|
|
183
|
+
this._applyingRemotePin = false;
|
|
184
|
+
this._hydratePinSeq = 0;
|
|
185
|
+
this._approvalsReviewerUpdateSeq = 0;
|
|
186
|
+
this._approvalsReviewerWriteQueue = Promise.resolve();
|
|
187
|
+
this._approvalsReviewerPendingWrites = 0;
|
|
188
|
+
// 跟踪上一次 mainAgent entry 的 timestamp,给新增 assistant msg 赋 _generatedTs(生成时 ts)。
|
|
189
|
+
// 解决 bubble 时间标签晚一拍的 bug:assistant 响应是上一次 API 调用产出的,
|
|
190
|
+
// 被这次 API 调用带进 body.input,旧逻辑统一赋 entry.timestamp 导致显示成"下一次 ts"。
|
|
191
|
+
this._prevMainAgentTs = null;
|
|
192
|
+
this._autoSelectTimer = null;
|
|
193
|
+
this._chunkedEntries = []; // 分段加载缓冲
|
|
194
|
+
this._chunkedTotal = 0;
|
|
195
|
+
this.mainContainerRef = React.createRef();
|
|
196
|
+
this._layoutRef = React.createRef();
|
|
197
|
+
// P0 perf: O(1) request dedup index
|
|
198
|
+
this._requestIndexMap = new Map();
|
|
199
|
+
// P0 perf: rAF batching for SSE messages
|
|
200
|
+
this._pendingEntries = [];
|
|
201
|
+
this._flushRafId = null;
|
|
202
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
203
|
+
// 冷启动分帧摄取管线(_runColdIngestCore)并发控制:
|
|
204
|
+
// - _ingestRunning 在途时 live 条目入 _liveGateBuffer(见 handleEventMessage),
|
|
205
|
+
// 提交后统一泄洪,防止 live 条目与未提交基线交错污染 sessionMerge
|
|
206
|
+
// - _ingestToken 自增令牌:任何 baseline 重置路径(重连/full_reload/workspace 切换/
|
|
207
|
+
// 新管线启动)bump 即废弃在途管线,废弃管线不 setState
|
|
208
|
+
this._ingestRunning = false;
|
|
209
|
+
this._ingestToken = 0;
|
|
210
|
+
this._liveGateBuffer = [];
|
|
211
|
+
this._ingestProgressCount = 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 批量剪枝 entries:清空旧 MainAgent 的 body.input,保留最后一条完整。
|
|
215
|
+
* v3: intern body.tools / body.instructions 让所有 entry 共享 pool 引用 */
|
|
216
|
+
// Centralised document.title writer. All paths that used to do
|
|
217
|
+
// document.title = projectName
|
|
218
|
+
// document.title = `${projectName} - CX Viewer`
|
|
219
|
+
// route through here so a user-configured per-project alias (utils/projectAlias)
|
|
220
|
+
// can override consistently. Without this, the SSE workspace_started handler
|
|
221
|
+
// would clobber alias on every switch.
|
|
222
|
+
// Empty / missing projectName falls back to the literal app name to keep the
|
|
223
|
+
// browser tab from showing a stale name across reloads.
|
|
224
|
+
_applyDocTitle = (projectName) => {
|
|
225
|
+
try {
|
|
226
|
+
if (typeof document === 'undefined') return;
|
|
227
|
+
const alias = getProjectAlias(projectName);
|
|
228
|
+
// --pid 实例:标题加 (id) 后缀,让多开 cxv 时能从标签页一眼分辨是哪个实例。
|
|
229
|
+
const id = this.state.instanceId;
|
|
230
|
+
const suffix = id ? `(${id})` : '';
|
|
231
|
+
if (alias) {
|
|
232
|
+
document.title = `${alias}${suffix}`;
|
|
233
|
+
} else if (projectName) {
|
|
234
|
+
document.title = `${projectName}${suffix}`;
|
|
235
|
+
} else {
|
|
236
|
+
document.title = 'CX Viewer';
|
|
237
|
+
}
|
|
238
|
+
} catch { /* ignore — title is cosmetic, never block */ }
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Subscribe the current projectName to alias mutations (same-tab pubsub +
|
|
242
|
+
// cross-tab storage event). Re-called whenever projectName changes so we
|
|
243
|
+
// don't end up listening to an old project's key.
|
|
244
|
+
_resubscribeAlias = (projectName) => {
|
|
245
|
+
if (typeof this._aliasOff === 'function') {
|
|
246
|
+
try { this._aliasOff(); } catch {}
|
|
247
|
+
this._aliasOff = null;
|
|
248
|
+
}
|
|
249
|
+
if (!projectName) return;
|
|
250
|
+
this._aliasOff = subscribeToAlias(projectName, () => {
|
|
251
|
+
this._applyDocTitle(projectName);
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
_batchSlim(entries) {
|
|
256
|
+
for (let i = 0; i < entries.length; i++) {
|
|
257
|
+
entries[i] = internEntryBigFields(entries[i]);
|
|
258
|
+
stampConversationMessageCount(entries[i]);
|
|
259
|
+
}
|
|
260
|
+
const slimmer = createEntrySlimmer(isMainAgent);
|
|
261
|
+
for (let i = 0; i < entries.length; i++) slimmer.process(entries[i], entries, i);
|
|
262
|
+
slimmer.finalize(entries);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Rebuild the O(1) request dedup index from a full entries array. */
|
|
266
|
+
_rebuildRequestIndex(entries) {
|
|
267
|
+
this._requestIndexMap.clear();
|
|
268
|
+
for (let i = 0; i < entries.length; i++) {
|
|
269
|
+
const e = entries[i];
|
|
270
|
+
this._requestIndexMap.set(`${e.timestamp}|${e.url}`, i);
|
|
271
|
+
}
|
|
272
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 给子组件(ChatView / TerminalPanel)一次性注入 SettingsContext 的所有字段。
|
|
276
|
+
// 不能直接给它们绑 contextType — 它们已绑 TerminalWsContext,class 一次只能一个。
|
|
277
|
+
_settingsProps() {
|
|
278
|
+
const ctx = this.context || {};
|
|
279
|
+
return {
|
|
280
|
+
codexSettings: ctx.codexSettings,
|
|
281
|
+
preferences: ctx.preferences,
|
|
282
|
+
onUpdatePreferences: ctx.updatePreferences,
|
|
283
|
+
onUpdateCodexSettings: ctx.updateCodexSettings,
|
|
284
|
+
// 把 lang 塞进 settings spread,让 App / Mobile 入口都自动拿到,
|
|
285
|
+
// 避免 ChatMessage 切语言时只在桌面端刷新而漏移动端。
|
|
286
|
+
lang: this.state.lang,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 这 5 个偏好的唯一真相源是 SettingsContext(preferences/codexSettings);
|
|
291
|
+
// App/Mobile render 时直接派生往下传 prop,不再镜像进本地 state。
|
|
292
|
+
// context 未就绪(fetch 前)时用与原初始 state 一致的默认值兜底。
|
|
293
|
+
_prefValues() {
|
|
294
|
+
const prefs = (this.context && this.context.preferences) || {};
|
|
295
|
+
const cs = (this.context && this.context.codexSettings) || {};
|
|
296
|
+
return {
|
|
297
|
+
collapseToolResults: prefs.collapseToolResults ?? true,
|
|
298
|
+
expandThinking: !!prefs.expandThinking,
|
|
299
|
+
expandDiff: !!prefs.expandDiff,
|
|
300
|
+
showFullToolContent: !!prefs.showFullToolContent,
|
|
301
|
+
onlyCurrentSession: prefs.onlyCurrentSession !== undefined ? !!prefs.onlyCurrentSession : /Windows/i.test(navigator.userAgent),
|
|
302
|
+
showThinkingSummaries: !!cs.showThinkingSummaries,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 把 /api/preferences 回包水合进散落在 this.state 的偏好字段(approvalsReviewer / approvalPrefs /
|
|
307
|
+
// themeColor / displayScale / resumeAutoChoice 等,区别于 _prefValues() 直接读 context 的那几个)。
|
|
308
|
+
// 初次加载与 refreshAllPrefs(toggle「项目独立配置」后)共用,避免抽屉里这半数控件读到旧的全局值。
|
|
309
|
+
_hydratePrefsFromData = (data) => {
|
|
310
|
+
if (!data) return;
|
|
311
|
+
if (data.lang) this.setState({ lang: data.lang });
|
|
312
|
+
// collapseToolResults / expandThinking / expandDiff / showFullToolContent
|
|
313
|
+
// 不再镜像进 state —— render 经 _prefValues() 直接读 context.preferences。
|
|
314
|
+
if (data.resumeAutoChoice) {
|
|
315
|
+
this.setState({ resumeAutoChoice: data.resumeAutoChoice });
|
|
316
|
+
}
|
|
317
|
+
this.setState({ approvalsReviewer: normalizeApprovalsReviewer(data.approvalsReviewer) });
|
|
318
|
+
// Approval modal preferences (defaults already in initial state — only override when persisted).
|
|
319
|
+
if (data.approvalModal && typeof data.approvalModal === 'object') {
|
|
320
|
+
// setState updater 不做 side effect,先在外层算 next + mismatch,再 setState + POST + IPC。
|
|
321
|
+
const prevPrefs = this.state.approvalPrefs;
|
|
322
|
+
const mergedVP = mergeVoicePackInto(prevPrefs.voicePack, data.approvalModal.voicePack);
|
|
323
|
+
const next = {
|
|
324
|
+
modalEnabled: data.approvalModal.modalEnabled !== undefined ? !!data.approvalModal.modalEnabled : prevPrefs.modalEnabled,
|
|
325
|
+
soundEnabled: data.approvalModal.soundEnabled !== undefined ? !!data.approvalModal.soundEnabled : prevPrefs.soundEnabled,
|
|
326
|
+
notifyOnlyWhenHidden: data.approvalModal.notifyOnlyWhenHidden !== undefined ? !!data.approvalModal.notifyOnlyWhenHidden : prevPrefs.notifyOnlyWhenHidden,
|
|
327
|
+
planAutoApproveSeconds: typeof data.approvalModal.planAutoApproveSeconds === 'number' ? data.approvalModal.planAutoApproveSeconds : prevPrefs.planAutoApproveSeconds,
|
|
328
|
+
voicePack: mergedVP,
|
|
329
|
+
};
|
|
330
|
+
// 合并开关迁移:server 端 soundEnabled !== voicePack.enabled 时以 soundEnabled 为准强制对齐(幂等回写)。
|
|
331
|
+
const mismatch = !!next.voicePack.enabled !== !!next.soundEnabled;
|
|
332
|
+
if (mismatch) {
|
|
333
|
+
next.voicePack = { ...next.voicePack, enabled: next.soundEnabled };
|
|
334
|
+
}
|
|
335
|
+
this.setState({ approvalPrefs: next });
|
|
336
|
+
// updatePreferences 顶层浅 merge:必须传完整 next(含 voicePack 子树),否则 events/volume 被砍。
|
|
337
|
+
if (mismatch) {
|
|
338
|
+
this.context?.updatePreferences?.({ approvalModal: next });
|
|
339
|
+
}
|
|
340
|
+
// 同步给 electron main 进程(voicePack 不发——播放在 renderer)。
|
|
341
|
+
try {
|
|
342
|
+
const { voicePack: _omit, ...forIpc } = next;
|
|
343
|
+
window.tabBridge?.setApprovalPref?.(forIpc);
|
|
344
|
+
} catch (e) { console.warn('[approvalPref IPC] hydrate sync failed:', e); }
|
|
345
|
+
}
|
|
346
|
+
// hydrate:prefs 没存过 themeColor 时回退当前 state(首次安装 'light')。不写回 prefs,但同步 localStorage。
|
|
347
|
+
const effective = (data.themeColor === 'light' || data.themeColor === 'dark')
|
|
348
|
+
? data.themeColor
|
|
349
|
+
: this.state.themeColor;
|
|
350
|
+
this._applyTheme(effective);
|
|
351
|
+
// 整体显示大小:prefs 为准(跨设备),没存过回退当前 state(默认 100)。
|
|
352
|
+
this._applyDisplayScale(data.displayScale ?? this.state.displayScale);
|
|
353
|
+
// filterIrrelevant 默认 true,showAll = !filterIrrelevant
|
|
354
|
+
const filterIrrelevant = data.filterIrrelevant !== undefined ? !!data.filterIrrelevant : true;
|
|
355
|
+
this.setState({ showAll: !filterIrrelevant });
|
|
356
|
+
if (data.logDir) {
|
|
357
|
+
this.setState({ logDir: data.logDir });
|
|
358
|
+
}
|
|
359
|
+
// URL 参数覆盖主题(白名单校验防 XSS)。一次性覆盖,不写回 prefs,但同步 localStorage。
|
|
360
|
+
const urlTheme = new URLSearchParams(window.location.search).get('theme');
|
|
361
|
+
if (urlTheme === 'light' || urlTheme === 'dark') {
|
|
362
|
+
this._applyTheme(urlTheme);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 重新拉取偏好并重跑本地 state 水合(toggle 项目独立配置后、admin 改完他人 fork 后调用)。
|
|
367
|
+
refreshAllPrefs = () => {
|
|
368
|
+
const p = this.context?.refreshPreferences?.();
|
|
369
|
+
if (!p || typeof p.then !== 'function') return Promise.resolve(null);
|
|
370
|
+
return p.then(d => { if (d) this._hydratePrefsFromData(d); return d; });
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// 切换「启动项目独立配置」:开启 = 把当前全局偏好 fork 到本项目;关闭 = 删除该 fork。
|
|
374
|
+
// 服务端按当前项目 key 处理;成功后整刷一遍偏好(GET 会据 fork 解析出有效值)。
|
|
375
|
+
handleToggleProjectScoped = (enabled) => {
|
|
376
|
+
return fetch(apiUrl('/api/project-prefs/toggle'), {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers: { 'Content-Type': 'application/json' },
|
|
379
|
+
body: JSON.stringify({ enabled: !!enabled }),
|
|
380
|
+
}).then(r => (r.ok ? r.json() : null))
|
|
381
|
+
.then((resp) => {
|
|
382
|
+
// 服务端确认后立刻乐观翻 _projectScoped,关掉"确认→refresh 到位"窗口内偏好写误投全局的风险;
|
|
383
|
+
// 随后 refreshAllPrefs 再按 fork 解析出的有效值整体校准。toggle 失败(resp 为 null)则只刷新校准。
|
|
384
|
+
if (resp) this.context?.mergeLocalPreferences?.({ _projectScoped: !!enabled });
|
|
385
|
+
return this.refreshAllPrefs();
|
|
386
|
+
})
|
|
387
|
+
.catch(() => this.refreshAllPrefs());
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ─── 「仅展示当前会话」会话锁定(pin) ──────────────────────────
|
|
391
|
+
// 生效的「仅展示当前会话」值:本地日志模式强制关闭(须看全量历史),否则取 _prefValues()
|
|
392
|
+
// (含 Windows 未设时默认开启),与 App.jsx render 传给 ChatView 的口径一致。
|
|
393
|
+
_effectiveOnlyCurrentSession() {
|
|
394
|
+
if (this._isLocalLog) return false;
|
|
395
|
+
return !!this._prefValues().onlyCurrentSession;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 移动端 splitHotCold 的「强制保热」集合:始终把当前 pin 会话纳入,防其被冷淘汰后
|
|
399
|
+
// 在 [对话] 里退化成「加载」占位(findIndex 也就再找不到它)。可附加额外 id(如刚加载的冷 session)。
|
|
400
|
+
_pinnedSessionIdSet(extra) {
|
|
401
|
+
const s = new Set();
|
|
402
|
+
if (this.state.pinnedSessionTs != null) s.add(this.state.pinnedSessionTs);
|
|
403
|
+
if (Array.isArray(extra)) {
|
|
404
|
+
for (const id of extra) { if (id != null) s.add(id); }
|
|
405
|
+
}
|
|
406
|
+
return s;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Single definition of "the current session's stable id" — used by follow-latest
|
|
410
|
+
// (_maintainPinState), hydrate adoption (_hydratePin), and the session_pin SSE
|
|
411
|
+
// listener. Keep all three on this helper so the derivation can never drift
|
|
412
|
+
// between the paths again (that divergence class is the bug this fixes).
|
|
413
|
+
_derivedLatestId() {
|
|
414
|
+
return getSessionStableId(getLatestSessionByActivity(this.state.mainAgentSessions));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 一次性清理旧版浏览器本地 pin(cxv_pinnedSession_<项目>)。pin 已改服务端存储,这些键不再使用,
|
|
418
|
+
// 历史上每访问一个项目就攒一个、永不回收。mount 时调用一次即可。
|
|
419
|
+
_cleanupLegacyPinKeys() {
|
|
420
|
+
try {
|
|
421
|
+
const stale = [];
|
|
422
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
423
|
+
const k = localStorage.key(i);
|
|
424
|
+
if (k && k.startsWith('cxv_pinnedSession_')) stale.push(k);
|
|
425
|
+
}
|
|
426
|
+
for (const k of stale) localStorage.removeItem(k);
|
|
427
|
+
} catch {}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// pin 持久化 → 服务端(POST /api/session-pin),由 server 按项目 + --pid 实例键落盘并 SSE 广播本进程,
|
|
431
|
+
// 多端实时一致。本地日志模式无 server,短路不发。
|
|
432
|
+
_persistPin() {
|
|
433
|
+
if (this._isLocalLog) return;
|
|
434
|
+
const val = this.state.pinnedSessionTs;
|
|
435
|
+
try {
|
|
436
|
+
fetch(apiUrl('/api/session-pin'), {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers: { 'Content-Type': 'application/json' },
|
|
439
|
+
body: JSON.stringify({ pinnedSessionId: val == null ? null : String(val) }),
|
|
440
|
+
}).catch(() => {});
|
|
441
|
+
} catch {}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 从服务端读回 pin(刷新/切项目/重连后恢复)。异步:hydrate 在途时置 _isHydratingPin,
|
|
445
|
+
// 抑制 _maintainPinState 的 lazy-lock / persist,避免抢在 GET 返回前误锁并 POST 覆盖服务端真值。
|
|
446
|
+
// 采纳服务端值时置 _applyingRemotePin,避免被 persist 分支当成本地改动回写。
|
|
447
|
+
//
|
|
448
|
+
// Orchestration lives in runPinHydration (sessionManager.js) so the race-sensitive
|
|
449
|
+
// ordering is unit-tested: a stale server pin loses to the locally derived latest
|
|
450
|
+
// when the toggle is on (resolveHydratedPin); a superseded GET (newer hydrate
|
|
451
|
+
// started meanwhile) is discarded without touching the gate; and after settling,
|
|
452
|
+
// the gate is cleared BEFORE _maintainPinState(null) re-runs follow-latest — so a
|
|
453
|
+
// reconnect on an idle stream self-heals instead of sticking to a stale pin.
|
|
454
|
+
_hydratePin() {
|
|
455
|
+
if (this._isLocalLog) return;
|
|
456
|
+
const seq = ++this._hydratePinSeq;
|
|
457
|
+
this._isHydratingPin = true;
|
|
458
|
+
runPinHydration({
|
|
459
|
+
fetchPin: () => fetch(apiUrl('/api/session-pin'))
|
|
460
|
+
.then(res => res.ok ? res.json() : null)
|
|
461
|
+
.then(data => data && data.pinnedSessionId),
|
|
462
|
+
// Unmount counts as supersession: a GET resolving after teardown must not
|
|
463
|
+
// setState (adopt/self-heal) or clear a gate nobody owns anymore.
|
|
464
|
+
isCurrent: () => seq === this._hydratePinSeq && !this._unmounted,
|
|
465
|
+
getDerived: () => this._derivedLatestId(),
|
|
466
|
+
effOnly: () => this._effectiveOnlyCurrentSession(),
|
|
467
|
+
getLocalPin: () => this.state.pinnedSessionTs,
|
|
468
|
+
adopt: (val) => {
|
|
469
|
+
this._applyingRemotePin = true;
|
|
470
|
+
this.setState({ pinnedSessionTs: val }, () => { this._applyingRemotePin = false; });
|
|
471
|
+
},
|
|
472
|
+
persistLocal: () => this._persistPin(),
|
|
473
|
+
clearGate: () => { this._isHydratingPin = false; },
|
|
474
|
+
selfHeal: () => this._maintainPinState(null),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// App / Mobile 子类的 componentDidUpdate 都 `super.componentDidUpdate(...)`,故 pin 维护集中在此。
|
|
479
|
+
componentDidUpdate(prevProps, prevState) {
|
|
480
|
+
this._maintainPinState(prevState);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 维护 pin:过滤开启时始终把 pin 跟随到「当前会话」(= 最新会话的稳定 id);切项目重 hydrate;
|
|
484
|
+
// pin 变化持久化。「当前会话」以日志最新条目所属会话为准,不依赖界面 /clear 交互 —— 从新终端
|
|
485
|
+
// (如 Ghostty)启动的会话,重载/实时都能自动切过去(配合 _flushPendingEntries 的实时推进)。
|
|
486
|
+
_maintainPinState(prevState) {
|
|
487
|
+
const effOnly = this._effectiveOnlyCurrentSession();
|
|
488
|
+
|
|
489
|
+
// _isHydratingPin:服务端 pin 的 GET 在途时,不要抢先推进(否则会在真值返回前误锁到最新)。
|
|
490
|
+
if (effOnly && !this._isHydratingPin) {
|
|
491
|
+
// "Current session" = newest ACTIVITY among hot sessions, NOT the last list
|
|
492
|
+
// element: mainAgentSessions is insertion-ordered, and with interleaved
|
|
493
|
+
// multi-terminal sessions or a truncated reconnect replay the tail is often
|
|
494
|
+
// an old session — following it anchored (and persisted) the wrong pin.
|
|
495
|
+
const latestId = this._derivedLatestId() || this._currentSessionId || null;
|
|
496
|
+
// 始终跟随最新会话(不再只在「开关刚打开 / pin 为空」时锁一次):这样即便持久化的 pin
|
|
497
|
+
// 指向旧会话(cx-viewer 关闭期间新终端已启动,重开后 batch 重载出新会话),也会自动切到
|
|
498
|
+
// 最新会话。因为没有「手动锁定任意旧会话」的 UI,「当前会话」恒等于最新会话,此推进安全。
|
|
499
|
+
// 不变式:切项目那一拍这里的乐观锁不会被持久化——下面同一趟的 _hydratePin() 会同步置
|
|
500
|
+
// _isHydratingPin=true,把下一拍 persist 分支挡掉(由 hydrate 的服务端真值收口),故无需在此回写。
|
|
501
|
+
if (latestId && this.state.pinnedSessionTs !== latestId) {
|
|
502
|
+
this.setState({ pinnedSessionTs: latestId });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (prevState && prevState.projectName !== this.state.projectName) {
|
|
507
|
+
// 切项目:从服务端重新 hydrate(旧 pin 已在切换 setState 里清空)。
|
|
508
|
+
this._hydratePin();
|
|
509
|
+
} else if (prevState && prevState.pinnedSessionTs !== this.state.pinnedSessionTs && this.state.projectName
|
|
510
|
+
&& !this._applyingRemotePin && !this._isHydratingPin) {
|
|
511
|
+
// pin 本地变化且 projectName 稳定 → 持久化到服务端。
|
|
512
|
+
// _applyingRemotePin:来自 hydrate/SSE 的服务端值不回写(防回环);_isHydratingPin:hydrate 在途不写。
|
|
513
|
+
this._persistPin();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// App / Mobile render 共用:按生效的「仅展示当前会话」+ pin 切出传给 ChatView 的会话与上界。
|
|
518
|
+
_displaySessionsFor(mainAgentSessions) {
|
|
519
|
+
return resolveDisplaySessions(mainAgentSessions, this.state.pinnedSessionTs, this._effectiveOnlyCurrentSession());
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 单次遍历完成 timestamp 赋值 + session 构建 + 过滤 + index 重建。
|
|
524
|
+
* 合并 assignMessageTimestamps + buildSessionsFromEntries + filterRelevantRequests + _rebuildRequestIndex,
|
|
525
|
+
* 减少 3 次 O(n) 全量扫描。
|
|
526
|
+
*/
|
|
527
|
+
_processEntries(entries) {
|
|
528
|
+
const st = this._initProcessState();
|
|
529
|
+
for (let i = 0; i < entries.length; i++) {
|
|
530
|
+
this._processOneEntry(entries[i], i, st);
|
|
531
|
+
}
|
|
532
|
+
this._liveConversationNormalizer = st.conversationNormalizer;
|
|
533
|
+
this._currentSessionId = st.currentSessionId;
|
|
534
|
+
return { mainAgentSessions: st.sessions, filtered: st.filtered };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** _processEntries 的循环前置:实例状态重置(_rebuildRequestIndex 内联)+ 遍历局部状态对象。
|
|
538
|
+
* 同步 _processEntries 与分帧 _processEntriesChunked 共用,保证两条路径前置完全一致。 */
|
|
539
|
+
_initProcessState() {
|
|
540
|
+
// _rebuildRequestIndex 内联
|
|
541
|
+
this._requestIndexMap.clear();
|
|
542
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
timestamps: [],
|
|
546
|
+
generatedTimestamps: [], // 跟 timestamps 平行:position → _generatedTs(assistant 才有)
|
|
547
|
+
prevMainAgentTs: null, // 上一次 mainAgent entry 的 ts,给本次新增 assistant msg 赋
|
|
548
|
+
prevUserId: null,
|
|
549
|
+
prevSessionKey: null,
|
|
550
|
+
sessions: [],
|
|
551
|
+
filtered: [],
|
|
552
|
+
currentSessionId: null,
|
|
553
|
+
conversationNormalizer: createConversationEntryNormalizer(),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** _processEntries 的循环体原样抽取(局部变量改读写 st.*,其余逐行一致)。
|
|
558
|
+
* 同步与分帧路径共用此方法 —— mergeMainAgentSessions 的调用序列/参数/
|
|
559
|
+
* _sessionId 赋值因此与抽取前完全相同(sessionMerge 脆弱区零语义变化)。 */
|
|
560
|
+
_processOneEntry(entry, i, st) {
|
|
561
|
+
const mergeBlocked = isMergeBlockedEntry(entry, { batch: true });
|
|
562
|
+
const conversationExcluded = shouldExcludeFromConversation(entry);
|
|
563
|
+
const conversationEntry = conversationExcluded
|
|
564
|
+
? entry
|
|
565
|
+
: st.conversationNormalizer(entry, { commit: !mergeBlocked });
|
|
566
|
+
|
|
567
|
+
// Rotation-context sentinel (first frame of a post-rotation segment):
|
|
568
|
+
// capture the carry-forward payload, seed the teammate-name registry, and
|
|
569
|
+
// never treat it as a renderable request (isRelevantRequest also rejects
|
|
570
|
+
// it as belt-and-braces for other filter paths).
|
|
571
|
+
if (entry && entry.cxvRotationContext) {
|
|
572
|
+
this._rotationContext = entry;
|
|
573
|
+
if (Array.isArray(entry.teammateNames)) setTeammateNameSeeds(entry.teammateNames);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// requestIndex
|
|
578
|
+
this._requestIndexMap.set(`${entry.timestamp}|${entry.url}`, i);
|
|
579
|
+
|
|
580
|
+
// filterRelevant
|
|
581
|
+
if (isRelevantRequest(entry)) st.filtered.push(entry);
|
|
582
|
+
|
|
583
|
+
// assignTimestamps + buildSessions(仅 mainAgent)
|
|
584
|
+
if (!conversationExcluded && isMainAgent(conversationEntry) && conversationEntry.body && Array.isArray(conversationEntry.body.input)) {
|
|
585
|
+
// Boundary detection + positional timestamp accumulation extracted to
|
|
586
|
+
// applyBatchEntryTimestamps (sessionManager.js) — shares isSessionBoundary
|
|
587
|
+
// with the live SSE path (_flushPendingEntries) so batch reload and live
|
|
588
|
+
// streaming segment sessions identically (the "only show current session"
|
|
589
|
+
// pin depends on stable ids matching across the two paths).
|
|
590
|
+
// KEEP IN SYNC: test/session-boundary-parity.test.js runBatchLeg mirrors
|
|
591
|
+
// this slim → applyBatchEntryTimestamps → merge call order.
|
|
592
|
+
applyBatchEntryTimestamps(st, conversationEntry);
|
|
593
|
+
|
|
594
|
+
// session 合并(跳过 _slimmed;批量路径额外跳过 stale/broken/inProgress,见谓词 JSDoc)
|
|
595
|
+
if (!conversationEntry._slimmed && !mergeBlocked) {
|
|
596
|
+
st.sessions = this.mergeMainAgentSessions(st.sessions, conversationEntry);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
entry._sessionId = st.currentSessionId;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** _processEntries 的分帧版:同一循环插入让步,调用序列与同步版完全一致。 */
|
|
604
|
+
async _processEntriesChunked(entries, ctl) {
|
|
605
|
+
const st = this._initProcessState();
|
|
606
|
+
const r = await runChunkedPass(entries.length, (i) => this._processOneEntry(entries[i], i, st), ctl);
|
|
607
|
+
if (r.aborted) return { aborted: true };
|
|
608
|
+
this._liveConversationNormalizer = st.conversationNormalizer;
|
|
609
|
+
this._currentSessionId = st.currentSessionId;
|
|
610
|
+
return { aborted: false, mainAgentSessions: st.sessions, filtered: st.filtered };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** _batchSlim 的分帧版:与同步版完全同序 —— intern 全量 pass → slimmer.process 全量 pass
|
|
614
|
+
* → finalize 一次。两个 pass 各自分帧(保持"intern 先全部完成"的既有顺序假设)。 */
|
|
615
|
+
async _batchSlimChunked(entries, ctl) {
|
|
616
|
+
const r1 = await runChunkedPass(entries.length, (i) => {
|
|
617
|
+
entries[i] = internEntryBigFields(entries[i]);
|
|
618
|
+
stampConversationMessageCount(entries[i]);
|
|
619
|
+
}, ctl);
|
|
620
|
+
if (r1.aborted) return { aborted: true };
|
|
621
|
+
const slimmer = createEntrySlimmer(isMainAgent);
|
|
622
|
+
const r2 = await runChunkedPass(entries.length, (i) => { slimmer.process(entries[i], entries, i); }, ctl);
|
|
623
|
+
if (r2.aborted) return { aborted: true };
|
|
624
|
+
slimmer.finalize(entries);
|
|
625
|
+
return { aborted: false };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** 分帧管线的并发控制句柄。progress 经 _loadingCountRafId rAF 节流写 fileLoadingCount。
|
|
629
|
+
* _loadingCountRafId/_ingestProgressCount 跨管线共享 —— onProgress 与 rAF 回调都按
|
|
630
|
+
* token 过滤,防被 supersede 的旧管线最后一批写入陈旧计数(进度数字乱跳)。 */
|
|
631
|
+
_makeIngestCtl(myToken) {
|
|
632
|
+
return {
|
|
633
|
+
shouldAbort: () => this._ingestToken !== myToken || this._unmounted,
|
|
634
|
+
onProgress: (count) => {
|
|
635
|
+
if (this._ingestToken !== myToken) return;
|
|
636
|
+
this._ingestProgressCount = count;
|
|
637
|
+
if (this._loadingCountRafId) return;
|
|
638
|
+
this._loadingCountRafId = requestAnimationFrame(() => {
|
|
639
|
+
this._loadingCountRafId = null;
|
|
640
|
+
if (this._ingestToken === myToken && !this._unmounted) {
|
|
641
|
+
this.setState({ fileLoadingCount: this._ingestProgressCount });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
yieldFn: yieldToMain,
|
|
646
|
+
batchSize: INGEST_BATCH_SIZE,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** 冷启动共享分帧管线:reconstruct(整体一次)→ 分帧 slim → 分帧 process。
|
|
651
|
+
* reconstructEntries 有状态(running accumulated + _compensateBrokenEntries 全数组
|
|
652
|
+
* 前向补偿),不可切片 —— 作为独立任务隔离,算法不动。
|
|
653
|
+
* Delta 重建必须在 entry-slim 之前:delta 条目的 body.input 只有增量部分,
|
|
654
|
+
* 先 slim 会永久丢失增量数据,导致重建后 input 为空。 */
|
|
655
|
+
async _runColdIngestCore(rawEntries, ctl) {
|
|
656
|
+
const entries = Array.isArray(rawEntries) ? reconstructEntries(rawEntries) : rawEntries;
|
|
657
|
+
if (ctl.shouldAbort()) return { aborted: true };
|
|
658
|
+
if (!(Array.isArray(entries) && entries.length > 0)) {
|
|
659
|
+
return { aborted: false, empty: true, entries: Array.isArray(entries) ? entries : [], mainAgentSessions: [], filtered: [] };
|
|
660
|
+
}
|
|
661
|
+
await ctl.yieldFn(); // reconstruct 是长任务,先让出一帧再进分帧 passes
|
|
662
|
+
if (ctl.shouldAbort()) return { aborted: true };
|
|
663
|
+
const s = await this._batchSlimChunked(entries, ctl);
|
|
664
|
+
if (s.aborted) return { aborted: true };
|
|
665
|
+
const p = await this._processEntriesChunked(entries, ctl);
|
|
666
|
+
if (p.aborted) return { aborted: true };
|
|
667
|
+
return { aborted: false, empty: false, entries, mainAgentSessions: p.mainAgentSessions, filtered: p.filtered };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** 管线提交:单次原子 setState;回调里关闸 + 泄洪 live 缓冲(对已提交基线重建)。 */
|
|
671
|
+
_commitColdIngest(myToken, newState, after) {
|
|
672
|
+
if (this._ingestToken !== myToken || this._unmounted) return; // 已被 supersede
|
|
673
|
+
this.setState(newState, () => {
|
|
674
|
+
if (this._ingestToken !== myToken) return; // setState 提交期间又被 supersede
|
|
675
|
+
this._ingestRunning = false;
|
|
676
|
+
const buffered = this._liveGateBuffer;
|
|
677
|
+
this._liveGateBuffer = [];
|
|
678
|
+
if (buffered.length > 0) {
|
|
679
|
+
this._pendingEntries.push(...buffered);
|
|
680
|
+
if (!this._flushRafId) {
|
|
681
|
+
this._flushRafId = requestAnimationFrame(this._flushPendingEntries);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (after) after();
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** 废弃在途分帧管线(baseline 重置路径调用:重连/full_reload/workspace 切换)。
|
|
689
|
+
* drain=true 时把闸门缓冲送回 _pendingEntries 走正常 flush(dedup 兜底重复)。 */
|
|
690
|
+
_abortColdIngest({ drain = false } = {}) {
|
|
691
|
+
this._ingestToken++;
|
|
692
|
+
this._ingestRunning = false;
|
|
693
|
+
const buffered = this._liveGateBuffer;
|
|
694
|
+
this._liveGateBuffer = [];
|
|
695
|
+
if (drain && buffered.length > 0) {
|
|
696
|
+
this._pendingEntries.push(...buffered);
|
|
697
|
+
if (!this._flushRafId) {
|
|
698
|
+
this._flushRafId = requestAnimationFrame(this._flushPendingEntries);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** initSSE load_end 的分帧版主流程(移动端 hot/cold 分层提交原样保留)。 */
|
|
704
|
+
async _runSseColdIngest(rawEntries, { isIncremental, unlockContextBar }) {
|
|
705
|
+
const myToken = ++this._ingestToken;
|
|
706
|
+
this._ingestRunning = true;
|
|
707
|
+
// Seed lifecycle: reset carried teammate-name seeds ONLY on non-incremental
|
|
708
|
+
// baseline loads (workspace switches land here too). Incremental reloads
|
|
709
|
+
// (SSE reconnect ?since=, mobile cache merge) carry no sentinel and must
|
|
710
|
+
// not wipe seeds they cannot re-deliver. The route re-delivers context
|
|
711
|
+
// after this load, and an in-window sentinel re-seeds during processing.
|
|
712
|
+
if (!isIncremental) {
|
|
713
|
+
clearTeammateNameSeeds();
|
|
714
|
+
this._rotationContext = null;
|
|
715
|
+
this._backfillDoneFor = null;
|
|
716
|
+
this._backfillCount = 0;
|
|
717
|
+
}
|
|
718
|
+
const ctl = this._makeIngestCtl(myToken);
|
|
719
|
+
const core = await this._runColdIngestCore(rawEntries, ctl);
|
|
720
|
+
if (core.aborted) return;
|
|
721
|
+
if (core.empty) {
|
|
722
|
+
const st = { fileLoading: false, fileLoadingCount: 0 };
|
|
723
|
+
if (unlockContextBar) st.contextBarLocked = false;
|
|
724
|
+
this._commitColdIngest(myToken, st);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const { entries, mainAgentSessions, filtered } = core;
|
|
728
|
+
|
|
729
|
+
// P1: 移动端 hot/cold 分层
|
|
730
|
+
if (isMobile && mainAgentSessions.length > HOT_SESSION_COUNT) {
|
|
731
|
+
const sessionIndex = buildSessionIndex(entries, mainAgentSessions);
|
|
732
|
+
const fullIndex = isIncremental
|
|
733
|
+
? mergeSessionIndices(this.state.sessionIndex, sessionIndex)
|
|
734
|
+
: sessionIndex;
|
|
735
|
+
const unslimmed = entries.map(e => e._slimmed ? restoreSlimmedEntry(e, entries) : e);
|
|
736
|
+
const { hotEntries, allSessions, coldGroups } = splitHotCold(
|
|
737
|
+
unslimmed, mainAgentSessions, fullIndex, HOT_SESSION_COUNT, this._pinnedSessionIdSet()
|
|
738
|
+
);
|
|
739
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
740
|
+
// 冷 session entries 异步写入 IndexedDB
|
|
741
|
+
const pn = this.state.projectName;
|
|
742
|
+
if (pn) {
|
|
743
|
+
for (const [sid, coldEntries] of coldGroups) {
|
|
744
|
+
saveSessionEntries(pn, sid, coldEntries);
|
|
745
|
+
}
|
|
746
|
+
// 主缓存保存全量 entries(而非 hotEntries),确保下次缓存恢复时有完整数据
|
|
747
|
+
saveEntries(pn, entries);
|
|
748
|
+
}
|
|
749
|
+
// Fix #4: selectedIndex 基于 hotEntries 而非全量 filtered
|
|
750
|
+
const hotFiltered = hotEntries.filter(e => isRelevantRequest(e));
|
|
751
|
+
const newState = {
|
|
752
|
+
requests: hotEntries,
|
|
753
|
+
selectedIndex: hotFiltered.length > 0 ? hotFiltered.length - 1 : null,
|
|
754
|
+
mainAgentSessions: allSessions,
|
|
755
|
+
sessionIndex: fullIndex,
|
|
756
|
+
fileLoading: false,
|
|
757
|
+
fileLoadingCount: 0,
|
|
758
|
+
};
|
|
759
|
+
// 增量模式保留缓存恢复时设的 hasMoreHistory;非增量(limit)模式用服务端的值
|
|
760
|
+
// hasMoreHistory 必须 AND 上 _oldestTs 非空,否则后续 loadMoreHistory() 会拼 before=null 触发 400
|
|
761
|
+
if (!isIncremental) newState.hasMoreHistory = !!this._hasMoreHistory && !!this._oldestTs;
|
|
762
|
+
if (unlockContextBar) newState.contextBarLocked = false;
|
|
763
|
+
this._commitColdIngest(myToken, newState);
|
|
764
|
+
} else {
|
|
765
|
+
const newState = {
|
|
766
|
+
requests: entries,
|
|
767
|
+
selectedIndex: filtered.length > 0 ? filtered.length - 1 : null,
|
|
768
|
+
mainAgentSessions,
|
|
769
|
+
fileLoading: false,
|
|
770
|
+
fileLoadingCount: 0,
|
|
771
|
+
};
|
|
772
|
+
if (!isIncremental) newState.hasMoreHistory = !!this._hasMoreHistory && !!this._oldestTs;
|
|
773
|
+
if (unlockContextBar) newState.contextBarLocked = false;
|
|
774
|
+
this._commitColdIngest(myToken, newState, () => {
|
|
775
|
+
if (isMobile && this.state.projectName) {
|
|
776
|
+
saveEntries(this.state.projectName, entries);
|
|
777
|
+
}
|
|
778
|
+
// Post-rotation teammate backfill — desktop baseline loads only. The
|
|
779
|
+
// route decides whether rotation context exists (the in-band sentinel
|
|
780
|
+
// may sit outside the load window on long post-rotation files), so
|
|
781
|
+
// the call is unconditional up to the guards; "no context" is a
|
|
782
|
+
// silent no-op. Mobile is excluded in v1: persisting pre-split
|
|
783
|
+
// entries would corrupt the _oldestTs paging cursor.
|
|
784
|
+
if (!isIncremental && !isMobile && !this._isLocalLog && !newState.hasMoreHistory) {
|
|
785
|
+
this._fetchPrevSegmentTeammates();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** loadLocalLogFile load_end 的分帧版主流程。 */
|
|
792
|
+
async _runLocalLogIngest(rawEntries) {
|
|
793
|
+
const myToken = ++this._ingestToken;
|
|
794
|
+
this._ingestRunning = true;
|
|
795
|
+
// History-switcher loads must not inherit live-session seeds or fire a
|
|
796
|
+
// stale backfill; a historical post-rotation segment self-seeds from its
|
|
797
|
+
// own head sentinel during processing.
|
|
798
|
+
clearTeammateNameSeeds();
|
|
799
|
+
this._rotationContext = null;
|
|
800
|
+
this._backfillDoneFor = null;
|
|
801
|
+
this._backfillCount = 0;
|
|
802
|
+
const ctl = this._makeIngestCtl(myToken);
|
|
803
|
+
const core = await this._runColdIngestCore(rawEntries, ctl);
|
|
804
|
+
if (core.aborted) return;
|
|
805
|
+
if (core.empty) {
|
|
806
|
+
this._commitColdIngest(myToken, { fileLoading: false, fileLoadingCount: 0 });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
this._commitColdIngest(myToken, {
|
|
810
|
+
requests: core.entries,
|
|
811
|
+
selectedIndex: core.filtered.length > 0 ? core.filtered.length - 1 : null,
|
|
812
|
+
mainAgentSessions: core.mainAgentSessions,
|
|
813
|
+
fileLoading: false,
|
|
814
|
+
fileLoadingCount: 0,
|
|
815
|
+
// logfile 只读模式恒为全量,无「加载更早」分页
|
|
816
|
+
hasMoreHistory: this._isLocalLog ? false : (!!this._hasMoreHistory && !!this._oldestTs),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
componentDidMount() {
|
|
821
|
+
// pin 已改服务端存储:清掉旧版浏览器本地 cxv_pinnedSession_* 残留(一次性)。
|
|
822
|
+
this._cleanupLegacyPinKeys();
|
|
823
|
+
// 全局键盘缩放监听(Cmd/Ctrl +/-/0)仅 Electron 注册——驱动原生 setZoomFactor 并与下拉同步。
|
|
824
|
+
// 纯浏览器**不**注册,把 Cmd/Ctrl +/- 交还浏览器原生缩放(不拦截)。unmount 时按同一 ref 卸载。
|
|
825
|
+
if (hasNativeZoom) window.addEventListener('keydown', this._onScaleKeydown);
|
|
826
|
+
// codex-settings / preferences fetch 由 SettingsProvider 集中触发;
|
|
827
|
+
// 这里仅订阅其 Promise,把字段同步到本地 state(沿用现有 13+ 个 setState 消费链路)。
|
|
828
|
+
this.context._codexSettingsReady.then(data => {
|
|
829
|
+
if (!data) return;
|
|
830
|
+
// showThinkingSummaries 不再镜像进 state —— render 经 _prefValues() 直接读
|
|
831
|
+
// context.codexSettings,fetch 回包触发 Provider 重渲染即生效。勿在此重加 setState。
|
|
832
|
+
if (data.codexAvailable === false) this.setState({ codexMissing: true });
|
|
833
|
+
if (typeof data.codexProjectModel === 'string' && data.codexProjectModel) {
|
|
834
|
+
this.setState({ codexProjectModel: data.codexProjectModel });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// ─── Approval modal: subscribe to electron main → tabBridge ──────────────────
|
|
839
|
+
// No-op when running in pure web mode — window.tabBridge is only injected by tab-content-preload.js.
|
|
840
|
+
// Subscription handles保存到 instance 以便 unmount 时卸载,避免 webContents reload 累加监听。
|
|
841
|
+
this._tabBridgeDisposers = [];
|
|
842
|
+
if (typeof window !== 'undefined' && window.tabBridge) {
|
|
843
|
+
try {
|
|
844
|
+
const offTabId = window.tabBridge.onTabIdInit?.((tabId) => {
|
|
845
|
+
this.setState({ ownTabId: tabId });
|
|
846
|
+
});
|
|
847
|
+
const offBroadcast = window.tabBridge.onApprovalBroadcast?.((payload) => {
|
|
848
|
+
if (!payload) return;
|
|
849
|
+
// ownPending 只取计数(main 进程的 ptyPlan/ask Map 序列化为 [{id, projectName, ...}])。
|
|
850
|
+
// 不重写 approvalGlobal——闭包内的 handlers / questions 无法跨 IPC 还原,
|
|
851
|
+
// 权威源仍是 ChatView 的 pendingAsk / pendingPtyPlan(WS 重连服务端会重放)。
|
|
852
|
+
const op = payload.ownPending;
|
|
853
|
+
const ownPendingCount = (op && typeof op === 'object')
|
|
854
|
+
? { ask: Array.isArray(op.ask) ? op.ask.length : 0, ptyPlan: Array.isArray(op.ptyPlan) ? op.ptyPlan.length : 0 }
|
|
855
|
+
: { ask: 0, ptyPlan: 0 };
|
|
856
|
+
this.setState((prev) => ({
|
|
857
|
+
ownTabId: payload.ownTabId != null ? payload.ownTabId : prev.ownTabId,
|
|
858
|
+
approvalOtherTabs: Array.isArray(payload.others) ? payload.others : [],
|
|
859
|
+
approvalOwnPending: ownPendingCount,
|
|
860
|
+
}));
|
|
861
|
+
});
|
|
862
|
+
if (typeof offTabId === 'function') this._tabBridgeDisposers.push(offTabId);
|
|
863
|
+
if (typeof offBroadcast === 'function') this._tabBridgeDisposers.push(offBroadcast);
|
|
864
|
+
} catch {}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 等 SettingsProvider 完成 /api/preferences fetch,把字段同步到本地 state。
|
|
868
|
+
// setLang / setCodexConfigDir 已由 Provider 处理,这里不再重复。
|
|
869
|
+
// initSSE 仍可读 this._prefsReady(getter 代理到 context),resume_prompt 行为不变。
|
|
870
|
+
this.context._prefsReady.then(data => this._hydratePrefsFromData(data));
|
|
871
|
+
|
|
872
|
+
// 获取系统用户头像和名字
|
|
873
|
+
fetch(apiUrl('/api/user-profile'))
|
|
874
|
+
.then(res => res.json())
|
|
875
|
+
.then(data => this.setState({ userProfile: data }))
|
|
876
|
+
.catch(() => { });
|
|
877
|
+
|
|
878
|
+
// 获取 proxy profile 配置
|
|
879
|
+
fetch(apiUrl('/api/proxy-profiles'))
|
|
880
|
+
.then(res => res.json())
|
|
881
|
+
.then(data => {
|
|
882
|
+
if (!data.profiles) return;
|
|
883
|
+
let activeId = data.active || 'max';
|
|
884
|
+
const dc = data.defaultConfig;
|
|
885
|
+
// 如果当前是 Default 且启动配置匹配了某个 proxy profile(origin + apiKey + model),自动指定到那一项
|
|
886
|
+
if (activeId === 'max' && dc?.origin) {
|
|
887
|
+
const match = data.profiles.find(p => {
|
|
888
|
+
if (p.id === 'max' || !p.baseURL) return false;
|
|
889
|
+
try {
|
|
890
|
+
if (new URL(p.baseURL).origin !== dc.origin) return false;
|
|
891
|
+
} catch { return false; }
|
|
892
|
+
// apiKey 匹配(mask 格式比较:都取后 4 位)
|
|
893
|
+
if (dc.apiKey && p.apiKey) {
|
|
894
|
+
const dcTail = dc.apiKey.slice(-4);
|
|
895
|
+
const pTail = p.apiKey.slice(-4);
|
|
896
|
+
if (dcTail !== pTail) return false;
|
|
897
|
+
}
|
|
898
|
+
// model 匹配;新配置使用 activeModel,旧 ANTHROPIC_* 字段只作迁移兼容。
|
|
899
|
+
const pModel = p.activeModel
|
|
900
|
+
|| p.OPENAI_MODEL
|
|
901
|
+
|| p.ANTHROPIC_MODEL
|
|
902
|
+
|| p.ANTHROPIC_DEFAULT_OPUS_MODEL
|
|
903
|
+
|| p.ANTHROPIC_DEFAULT_SONNET_MODEL
|
|
904
|
+
|| p.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
905
|
+
if (dc.model && pModel && dc.model !== pModel) return false;
|
|
906
|
+
return true;
|
|
907
|
+
});
|
|
908
|
+
if (match) {
|
|
909
|
+
activeId = match.id;
|
|
910
|
+
this.handleProxyProfileChange({ active: match.id, profiles: data.profiles });
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
this.setState({ proxyProfiles: data.profiles, activeProxyId: activeId, defaultConfig: dc || null });
|
|
914
|
+
})
|
|
915
|
+
.catch(() => { });
|
|
916
|
+
|
|
917
|
+
// 获取当前监控的项目名称
|
|
918
|
+
const params = new URLSearchParams(window.location.search);
|
|
919
|
+
const logfile = params.get('logfile');
|
|
920
|
+
fetch(apiUrl('/api/project-name'))
|
|
921
|
+
.then(res => res.json())
|
|
922
|
+
.then(data => {
|
|
923
|
+
const projectName = data.projectName || '';
|
|
924
|
+
const instanceId = data.instanceId || null;
|
|
925
|
+
// instanceId 与 projectName 同批入 state;标题在回调里应用,确保读到已更新的 instanceId。
|
|
926
|
+
this.setState({ projectName, instanceId }, () => this._applyDocTitle(projectName));
|
|
927
|
+
this._resubscribeAlias(projectName);
|
|
928
|
+
// 移动端:从缓存恢复数据,在 SSE 数据到达前立即渲染
|
|
929
|
+
if (isMobile && projectName && !logfile && this.state.requests.length === 0) {
|
|
930
|
+
loadEntries(projectName).then(cached => {
|
|
931
|
+
if (cached && this.state.requests.length === 0) {
|
|
932
|
+
this._batchSlim(cached);
|
|
933
|
+
const { mainAgentSessions, filtered } = this._processEntries(cached);
|
|
934
|
+
// P1: 缓存恢复也做 hot/cold 分层,避免全量数据驻留内存
|
|
935
|
+
if (mainAgentSessions.length > HOT_SESSION_COUNT) {
|
|
936
|
+
const sessionIndex = buildSessionIndex(cached, mainAgentSessions);
|
|
937
|
+
// slimmer 全平台:split 前还原 slimmed entries,确保 IndexedDB / hot 数据完整
|
|
938
|
+
const unslimmed = cached.map(e => e._slimmed ? restoreSlimmedEntry(e, cached) : e);
|
|
939
|
+
const { hotEntries, allSessions } = splitHotCold(
|
|
940
|
+
unslimmed, mainAgentSessions, sessionIndex, HOT_SESSION_COUNT, this._pinnedSessionIdSet()
|
|
941
|
+
);
|
|
942
|
+
this._sseSlimmer = null; this._sseReconstructor = null; // 重置,下帧 SSE 重建
|
|
943
|
+
const hotFiltered = hotEntries.filter(e => isRelevantRequest(e));
|
|
944
|
+
// 计算 _oldestTs 供"加载更多"使用
|
|
945
|
+
this._oldestTs = hotEntries.length > 0 ? hotEntries[0].timestamp : null;
|
|
946
|
+
this.setState({
|
|
947
|
+
requests: hotEntries,
|
|
948
|
+
selectedIndex: hotFiltered.length > 0 ? hotFiltered.length - 1 : null,
|
|
949
|
+
mainAgentSessions: allSessions,
|
|
950
|
+
sessionIndex,
|
|
951
|
+
hasMoreHistory: !!this._oldestTs,
|
|
952
|
+
fileLoading: false,
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
this._oldestTs = cached.length > 0 ? cached[0].timestamp : null;
|
|
956
|
+
this.setState({
|
|
957
|
+
requests: cached,
|
|
958
|
+
selectedIndex: filtered.length > 0 ? filtered.length - 1 : null,
|
|
959
|
+
mainAgentSessions,
|
|
960
|
+
hasMoreHistory: !!this._oldestTs,
|
|
961
|
+
fileLoading: false,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
.catch(() => { });
|
|
969
|
+
|
|
970
|
+
// 获取 GitHub star 数
|
|
971
|
+
fetch('https://api.github.com/repos/weiesky/cx-viewer')
|
|
972
|
+
.then(res => res.json())
|
|
973
|
+
.then(data => { if (data.stargazers_count != null) this.setState({ githubStars: data.stargazers_count }); })
|
|
974
|
+
.catch(() => { });
|
|
975
|
+
|
|
976
|
+
// 检测 CLI 模式 / 工作区模式
|
|
977
|
+
fetch(apiUrl('/api/cli-mode'))
|
|
978
|
+
.then(res => res.json())
|
|
979
|
+
.then(data => {
|
|
980
|
+
if (data.workspaceMode) {
|
|
981
|
+
this.setState({ cliMode: true, workspaceMode: true, isWorkspaceServer: true });
|
|
982
|
+
} else if (data.cliMode) {
|
|
983
|
+
this.setState({ cliMode: true, sdkMode: !!data.sdkMode, viewMode: 'chat' });
|
|
984
|
+
}
|
|
985
|
+
})
|
|
986
|
+
.catch(() => { });
|
|
987
|
+
|
|
988
|
+
// 检查是否是通过 ?logfile= 打开的历史日志
|
|
989
|
+
if (logfile) {
|
|
990
|
+
this.loadLocalLogFile(logfile);
|
|
991
|
+
} else {
|
|
992
|
+
this._scheduleInitSSE();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
componentWillUnmount() {
|
|
997
|
+
window.removeEventListener('keydown', this._onScaleKeydown);
|
|
998
|
+
if (Array.isArray(this._tabBridgeDisposers)) {
|
|
999
|
+
for (const off of this._tabBridgeDisposers) {
|
|
1000
|
+
try { off(); } catch {}
|
|
1001
|
+
}
|
|
1002
|
+
this._tabBridgeDisposers = null;
|
|
1003
|
+
}
|
|
1004
|
+
this._unmounted = true;
|
|
1005
|
+
if (this.eventSource) this.eventSource.close();
|
|
1006
|
+
if (this._localLogES) { this._localLogES.close(); this._localLogES = null; }
|
|
1007
|
+
if (this._autoSelectTimer) clearTimeout(this._autoSelectTimer);
|
|
1008
|
+
if (this._loadingCountTimer) cancelAnimationFrame(this._loadingCountTimer);
|
|
1009
|
+
if (this._loadingCountRafId) cancelAnimationFrame(this._loadingCountRafId);
|
|
1010
|
+
if (this._cacheSaveTimer) clearTimeout(this._cacheSaveTimer);
|
|
1011
|
+
if (this._evictionTimer) clearTimeout(this._evictionTimer);
|
|
1012
|
+
if (this._sseTimeoutTimer) clearTimeout(this._sseTimeoutTimer);
|
|
1013
|
+
if (this._sseReconnectTimer) clearTimeout(this._sseReconnectTimer);
|
|
1014
|
+
if (this._streamingOffTimer) clearTimeout(this._streamingOffTimer);
|
|
1015
|
+
if (this._streamingRaf) { cancelAnimationFrame(this._streamingRaf); this._streamingRaf = null; }
|
|
1016
|
+
if (this._clearOptimisticTimer) clearTimeout(this._clearOptimisticTimer);
|
|
1017
|
+
if (typeof this._aliasOff === 'function') { try { this._aliasOff(); } catch {} this._aliasOff = null; }
|
|
1018
|
+
this._pendingStreamingLatest = null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ─── SSE 通信 ───────────────────────────────────────────
|
|
1022
|
+
|
|
1023
|
+
// SSE 心跳超时检测:45s 内无任何事件则判定连接断开
|
|
1024
|
+
_resetSSETimeout = () => {
|
|
1025
|
+
if (this._sseTimeoutTimer) clearTimeout(this._sseTimeoutTimer);
|
|
1026
|
+
this._sseReconnectCount = 0; // 收到事件说明连接正常,重置重连计数
|
|
1027
|
+
this._sseTimeoutTimer = setTimeout(() => {
|
|
1028
|
+
console.warn('SSE heartbeat timeout, reconnecting...');
|
|
1029
|
+
this._reconnectSSE();
|
|
1030
|
+
}, 45000);
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// 不关闭 EventSource —— 连接是会话级单例,workspace 切换复用同一条连接。
|
|
1034
|
+
_scheduleInitSSE() {
|
|
1035
|
+
const start = () => { if (!this._unmounted) this.initSSE(); };
|
|
1036
|
+
// Windows 冷启动时 V8 需要 3-5 秒编译 ~7MB JS bundle(热启动有 Code Cache 则 <0.5s)。
|
|
1037
|
+
// timeout 设为 5 秒确保编译完成后再建 SSE 连接,避免数据处理与编译竞争导致 tab 崩溃。
|
|
1038
|
+
// 浏览器空闲时会提前触发(不必等满 5 秒),所以对热启动/Mac 无感知延迟。
|
|
1039
|
+
if (typeof requestIdleCallback === 'function') {
|
|
1040
|
+
requestIdleCallback(start, { timeout: 5000 });
|
|
1041
|
+
} else {
|
|
1042
|
+
requestAnimationFrame(() => requestAnimationFrame(start));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
_teardownTransientLiveState = () => {
|
|
1047
|
+
this._pendingEntries = [];
|
|
1048
|
+
if (this._flushRafId) { cancelAnimationFrame(this._flushRafId); this._flushRafId = null; }
|
|
1049
|
+
if (this._streamingOffTimer) { clearTimeout(this._streamingOffTimer); this._streamingOffTimer = null; }
|
|
1050
|
+
if (this._loadingCountRafId) { cancelAnimationFrame(this._loadingCountRafId); this._loadingCountRafId = null; }
|
|
1051
|
+
this._chunkedEntries = [];
|
|
1052
|
+
this._chunkedTotal = 0;
|
|
1053
|
+
this._isIncremental = false;
|
|
1054
|
+
this._sseSlimmer = null;
|
|
1055
|
+
this._sseReconstructor = null;
|
|
1056
|
+
// 分帧管线闸门兜底复位(_pendingEntries 已清空,缓冲不泄洪直接丢弃)
|
|
1057
|
+
this._ingestToken++;
|
|
1058
|
+
this._ingestRunning = false;
|
|
1059
|
+
this._liveGateBuffer = [];
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
_reconnectSSE() {
|
|
1063
|
+
// SSE 连接真死(心跳超时 / 重试上限),清除流式 overlay 避免卡死
|
|
1064
|
+
if (this.state.streamingLatest) this.setState({ streamingLatest: null });
|
|
1065
|
+
if (this._sseReconnectCount >= 10) {
|
|
1066
|
+
console.error('SSE reconnect limit reached');
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
this._sseReconnectCount = (this._sseReconnectCount || 0) + 1;
|
|
1070
|
+
if (this.eventSource) { this.eventSource.close(); this.eventSource = null; }
|
|
1071
|
+
|
|
1072
|
+
// 必须在部分保存之前废弃在途分帧管线(review P1):下方部分保存会同步跑
|
|
1073
|
+
// _processEntries → 清空 _requestIndexMap 等实例状态,若在途管线未先废弃,
|
|
1074
|
+
// 其下一批会基于被污染的状态继续写。
|
|
1075
|
+
// 不 drain 是有意的:闸门缓冲条目已由 interceptor 落盘,重连后 server replay
|
|
1076
|
+
// 必然重发;且下方 _teardownTransientLiveState 会清空 _pendingEntries,
|
|
1077
|
+
// drain 进去也会被立即清掉 —— 泄洪在此既无意义也有合并陈旧基线的风险。
|
|
1078
|
+
this._abortColdIngest();
|
|
1079
|
+
|
|
1080
|
+
// 必须在 _teardownTransientLiveState() 之前,否则 _chunkedEntries 会被清零。
|
|
1081
|
+
if (this._chunkedEntries && this._chunkedEntries.length > 0 && isMobile) {
|
|
1082
|
+
try {
|
|
1083
|
+
const partial = reconstructEntries([...this._chunkedEntries]);
|
|
1084
|
+
if (Array.isArray(partial) && partial.length > 0) {
|
|
1085
|
+
this._batchSlim(partial);
|
|
1086
|
+
const { mainAgentSessions } = this._processEntries(partial);
|
|
1087
|
+
// 保持 fileLoading: true,重连后继续加载
|
|
1088
|
+
this.setState({ requests: partial, mainAgentSessions });
|
|
1089
|
+
if (this.state.projectName) {
|
|
1090
|
+
const meta = getCacheMeta();
|
|
1091
|
+
const existingCount = (meta && meta.projectName === this.state.projectName) ? meta.count : 0;
|
|
1092
|
+
if (partial.length >= existingCount) {
|
|
1093
|
+
saveEntries(this.state.projectName, partial);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
console.warn('Failed to save partial entries on reconnect:', e);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
this._teardownTransientLiveState();
|
|
1103
|
+
this.setState({ isStreaming: false, contextBarLocked: false });
|
|
1104
|
+
if (this._sseReconnectTimer) clearTimeout(this._sseReconnectTimer);
|
|
1105
|
+
const delay = Math.min(2000 * Math.pow(2, (this._sseReconnectCount || 1) - 1), 32000);
|
|
1106
|
+
this._sseReconnectTimer = setTimeout(() => { this.initSSE(); }, delay);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
animateLoadingCount(target, onDone) {
|
|
1110
|
+
if (this._loadingCountTimer) {
|
|
1111
|
+
cancelAnimationFrame(this._loadingCountTimer);
|
|
1112
|
+
this._loadingCountTimer = null;
|
|
1113
|
+
}
|
|
1114
|
+
const duration = Math.min(800, Math.max(300, target * 0.5));
|
|
1115
|
+
const start = performance.now();
|
|
1116
|
+
const step = (now) => {
|
|
1117
|
+
const progress = Math.min((now - start) / duration, 1);
|
|
1118
|
+
const current = Math.round(progress * target);
|
|
1119
|
+
this.setState({ fileLoadingCount: current });
|
|
1120
|
+
if (progress < 1) {
|
|
1121
|
+
this._loadingCountTimer = requestAnimationFrame(step);
|
|
1122
|
+
} else {
|
|
1123
|
+
this._loadingCountTimer = null;
|
|
1124
|
+
onDone();
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
this._loadingCountTimer = requestAnimationFrame(step);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Post-rotation teammate backfill: fetch teammate-only entries from the
|
|
1132
|
+
* previous log segment and prepend them so pre-split teammate rows reappear.
|
|
1133
|
+
* One-shot per rotation context; superseded fetches (workspace switch / new
|
|
1134
|
+
* cold ingest mid-flight) are dropped via the ingest token, mirroring the
|
|
1135
|
+
* reload-token discipline used elsewhere.
|
|
1136
|
+
*/
|
|
1137
|
+
async _fetchPrevSegmentTeammates() {
|
|
1138
|
+
const ctxKey = this._rotationContext?.from || '__probe__';
|
|
1139
|
+
if (this._backfillDoneFor === ctxKey) return;
|
|
1140
|
+
const tok = this._ingestToken;
|
|
1141
|
+
let lines;
|
|
1142
|
+
try {
|
|
1143
|
+
const res = await fetch(apiUrl('/api/prev-segment-teammates'));
|
|
1144
|
+
if (!res.ok) return;
|
|
1145
|
+
const text = await res.text();
|
|
1146
|
+
lines = text.split('\n').filter(Boolean).map((l) => {
|
|
1147
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
1148
|
+
}).filter(Boolean);
|
|
1149
|
+
} catch { return; }
|
|
1150
|
+
if (this._ingestToken !== tok || this._unmounted) return; // superseded mid-flight
|
|
1151
|
+
if (!lines || lines.length === 0) return;
|
|
1152
|
+
this._backfillDoneFor = ctxKey;
|
|
1153
|
+
const ctx = lines[0];
|
|
1154
|
+
const done = lines[lines.length - 1];
|
|
1155
|
+
// The route's context line is the primary seed channel — the in-band
|
|
1156
|
+
// sentinel may be outside the client's load window entirely.
|
|
1157
|
+
if (Array.isArray(ctx?.teammateNames) && ctx.teammateNames.length > 0) {
|
|
1158
|
+
setTeammateNameSeeds(ctx.teammateNames);
|
|
1159
|
+
}
|
|
1160
|
+
if (!done || done.error || !done.prevSegment) return; // not post-rotation / no predecessor
|
|
1161
|
+
const entries = lines.slice(1, -1).filter((e) => e && e.timestamp && e.url && !e.done);
|
|
1162
|
+
const fresh = entries.filter((e) => !this._requestIndexMap.has(`${e.timestamp}|${e.url}`));
|
|
1163
|
+
if (fresh.length === 0) return;
|
|
1164
|
+
// Prepend precedent (loadMoreHistory): merge → slim → reprocess → commit.
|
|
1165
|
+
const reconstructed = reconstructEntries(fresh);
|
|
1166
|
+
const merged = [...reconstructed, ...this.state.requests];
|
|
1167
|
+
this._batchSlim(merged);
|
|
1168
|
+
const { mainAgentSessions } = this._processEntries(merged);
|
|
1169
|
+
if (this._ingestToken !== tok || this._unmounted) return;
|
|
1170
|
+
this._backfillCount = (this._backfillCount || 0) + reconstructed.length;
|
|
1171
|
+
this.setState((prev) => {
|
|
1172
|
+
// Shift by the count of rows the ACTIVE view actually gained —
|
|
1173
|
+
// selectedIndex indexes visibleRequests, which depends on showAll.
|
|
1174
|
+
const addedVisible = visibleRequests(reconstructed, prev.showAll).length;
|
|
1175
|
+
return {
|
|
1176
|
+
requests: merged,
|
|
1177
|
+
mainAgentSessions,
|
|
1178
|
+
// Keep the DetailPanel selection on the same logical row.
|
|
1179
|
+
selectedIndex: prev.selectedIndex == null ? null : prev.selectedIndex + addedVisible,
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async loadMoreHistory() {
|
|
1185
|
+
if (!this.state.hasMoreHistory || this._loadingMore) return;
|
|
1186
|
+
// 防御 _hasMoreHistory=true 而 _oldestTs 为 null 的不一致状态:
|
|
1187
|
+
// 没有锚点时间戳就别去拼 before=null,否则服务端 400。把 hasMoreHistory 同步
|
|
1188
|
+
// 关掉避免上层 loader 反复触发。
|
|
1189
|
+
if (!this._oldestTs) {
|
|
1190
|
+
this.setState({ hasMoreHistory: false });
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this._loadingMore = true;
|
|
1194
|
+
this.setState({ loadingMore: true });
|
|
1195
|
+
try {
|
|
1196
|
+
// logfile 只读模式已全量加载(hasMoreHistory 恒 false),不会触发本函数;此分页逻辑仅供 live 模式使用。
|
|
1197
|
+
const pageUrl = `/api/entries/page?before=${encodeURIComponent(this._oldestTs)}&limit=100`;
|
|
1198
|
+
const res = await fetch(apiUrl(pageUrl));
|
|
1199
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1200
|
+
const data = await res.json();
|
|
1201
|
+
if (Array.isArray(data.entries) && data.entries.length > 0) {
|
|
1202
|
+
const reconstructed = reconstructEntries(data.entries);
|
|
1203
|
+
// Paged current-file entries are OLDER than the current baseline but
|
|
1204
|
+
// NEWER than any backfilled previous-segment block — splice them after
|
|
1205
|
+
// the backfilled head so the array stays time-ordered (the sub-agent
|
|
1206
|
+
// interleave cursor depends on it).
|
|
1207
|
+
const bf = this._backfillCount || 0;
|
|
1208
|
+
const merged = bf > 0
|
|
1209
|
+
? [...this.state.requests.slice(0, bf), ...reconstructed, ...this.state.requests.slice(bf)]
|
|
1210
|
+
: [...reconstructed, ...this.state.requests];
|
|
1211
|
+
this._batchSlim(merged);
|
|
1212
|
+
const { mainAgentSessions } = this._processEntries(merged);
|
|
1213
|
+
this._oldestTs = data.oldestTimestamp;
|
|
1214
|
+
|
|
1215
|
+
// P1: 移动端 hot/cold 分层
|
|
1216
|
+
if (isMobile && mainAgentSessions.length > HOT_SESSION_COUNT) {
|
|
1217
|
+
const sessionIndex = buildSessionIndex(merged, mainAgentSessions);
|
|
1218
|
+
const fullIndex = mergeSessionIndices(this.state.sessionIndex, sessionIndex);
|
|
1219
|
+
const unslimmed = merged.map(e => e._slimmed ? restoreSlimmedEntry(e, merged) : e);
|
|
1220
|
+
const { hotEntries, allSessions, coldGroups } = splitHotCold(
|
|
1221
|
+
unslimmed, mainAgentSessions, fullIndex, HOT_SESSION_COUNT, this._pinnedSessionIdSet()
|
|
1222
|
+
);
|
|
1223
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
1224
|
+
const pn = this.state.projectName;
|
|
1225
|
+
if (pn) {
|
|
1226
|
+
for (const [sid, coldEntries] of coldGroups) {
|
|
1227
|
+
saveSessionEntries(pn, sid, coldEntries);
|
|
1228
|
+
}
|
|
1229
|
+
saveEntries(pn, merged);
|
|
1230
|
+
}
|
|
1231
|
+
this.setState({
|
|
1232
|
+
requests: hotEntries,
|
|
1233
|
+
mainAgentSessions: allSessions,
|
|
1234
|
+
sessionIndex: fullIndex,
|
|
1235
|
+
hasMoreHistory: !!data.hasMore && !!data.oldestTimestamp,
|
|
1236
|
+
loadingMore: false,
|
|
1237
|
+
});
|
|
1238
|
+
} else {
|
|
1239
|
+
this.setState((prev) => {
|
|
1240
|
+
// Count against the ACTIVE view: raw pages contain non-relevant
|
|
1241
|
+
// entries that showAll displays but the default view hides.
|
|
1242
|
+
const addedVisible = visibleRequests(reconstructed, prev.showAll).length;
|
|
1243
|
+
return {
|
|
1244
|
+
requests: merged,
|
|
1245
|
+
mainAgentSessions,
|
|
1246
|
+
hasMoreHistory: !!data.hasMore && !!data.oldestTimestamp,
|
|
1247
|
+
loadingMore: false,
|
|
1248
|
+
// Keep the DetailPanel selection on the same logical row after the
|
|
1249
|
+
// prepend (pre-existing latent flaw, fixed alongside the backfill).
|
|
1250
|
+
selectedIndex: prev.selectedIndex == null ? null : prev.selectedIndex + addedVisible,
|
|
1251
|
+
};
|
|
1252
|
+
});
|
|
1253
|
+
if (isMobile && this.state.projectName) {
|
|
1254
|
+
saveEntries(this.state.projectName, merged);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
this.setState({ hasMoreHistory: false, loadingMore: false });
|
|
1259
|
+
}
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
console.error('loadMoreHistory failed:', e);
|
|
1262
|
+
this.setState({ loadingMore: false });
|
|
1263
|
+
message.error(t('ui.loadMoreHistoryFailed'));
|
|
1264
|
+
}
|
|
1265
|
+
this._loadingMore = false;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
initSSE() {
|
|
1269
|
+
try {
|
|
1270
|
+
// 尝试使用缓存元数据进行增量加载
|
|
1271
|
+
let url = '/events';
|
|
1272
|
+
let hasCache = false;
|
|
1273
|
+
if (isMobile) {
|
|
1274
|
+
const meta = getCacheMeta();
|
|
1275
|
+
if (meta && meta.lastTs && meta.count > 0) {
|
|
1276
|
+
url = `/events?since=${encodeURIComponent(meta.lastTs)}&cc=${meta.count}&project=${encodeURIComponent(meta.projectName || '')}`;
|
|
1277
|
+
hasCache = true;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// 桌面端重连:用最后接收到的时间戳做增量加载,避免全量重载放大卡顿
|
|
1281
|
+
if (!hasCache && !isMobile && this._sseReconnectCount > 0 && this.state.requests.length > 0) {
|
|
1282
|
+
const reqs = this.state.requests;
|
|
1283
|
+
let lastTs = null;
|
|
1284
|
+
for (let i = reqs.length - 1; i >= 0; i--) {
|
|
1285
|
+
if (reqs[i]?.timestamp) { lastTs = reqs[i].timestamp; break; }
|
|
1286
|
+
}
|
|
1287
|
+
if (lastTs && this.state.projectName) {
|
|
1288
|
+
url = `/events?since=${encodeURIComponent(lastTs)}&cc=${reqs.length}&project=${encodeURIComponent(this.state.projectName)}`;
|
|
1289
|
+
hasCache = true;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
// 无缓存时限制首屏加载量,剩余按需分页。
|
|
1293
|
+
// 移动端 200 条;桌面端 400 条(Windows 上 1000 条的同步重建 + React 渲染
|
|
1294
|
+
// 可达 10-15s,超出 Chrome tab kill 阈值导致崩溃)。
|
|
1295
|
+
if (!hasCache) {
|
|
1296
|
+
url = `/events?limit=${isMobile ? 200 : 400}`;
|
|
1297
|
+
}
|
|
1298
|
+
// 只有在无缓存时才显示 loading 遮罩
|
|
1299
|
+
if (!hasCache) {
|
|
1300
|
+
this.setState({ fileLoading: true, fileLoadingCount: 0 });
|
|
1301
|
+
}
|
|
1302
|
+
this.eventSource = new EventSource(apiUrl(url));
|
|
1303
|
+
// 每次收到任何 SSE 事件(包括心跳注释帧触发的隐式活动)都重置超时
|
|
1304
|
+
this.eventSource.onmessage = (event) => { this._resetSSETimeout(); this.handleEventMessage(event); };
|
|
1305
|
+
this.eventSource.onopen = () => {
|
|
1306
|
+
this._resetSSETimeout();
|
|
1307
|
+
// 每次连上都补一次 pin GET 同步:浏览器原生 EventSource 自愈重连复用同一 EventSource、
|
|
1308
|
+
// 不走 _reconnectSSE(不增 _sseReconnectCount),故不能只在 wasReconnect 时同步,否则原生重连
|
|
1309
|
+
// 期间他端改的 pin 会一直 stale 到下次 session_pin 或切项目。GET 幂等,_applyingRemotePin 防回写。
|
|
1310
|
+
this._hydratePin();
|
|
1311
|
+
};
|
|
1312
|
+
// Live streaming overlay: 直接更新 streamingLatest state(不走 reconstructor / dedup)
|
|
1313
|
+
// rAF coalesce + startTransition:每个 SSE chunk 只在下一帧合并成一次 setState,
|
|
1314
|
+
// 并标记为低优先级渲染,避免阻塞用户输入。最终 chunk 经 entry path 交付而非
|
|
1315
|
+
// stream-progress,所以丢掉 trailing stream-progress 是安全的。
|
|
1316
|
+
this.eventSource.addEventListener('stream-progress', (event) => {
|
|
1317
|
+
this._resetSSETimeout();
|
|
1318
|
+
try {
|
|
1319
|
+
const data = JSON.parse(event.data);
|
|
1320
|
+
// 防 stale:若 requests 中已有同 timestamp 的完成条目,说明最终 entry 已到达,
|
|
1321
|
+
// 此 chunk 是乱序/延迟到达的旧包,直接丢弃以免复活已清除的 overlay
|
|
1322
|
+
const existingFinal = this.state.requests.find(r =>
|
|
1323
|
+
r && r.timestamp === data.timestamp && !r.inProgress
|
|
1324
|
+
);
|
|
1325
|
+
if (existingFinal) return;
|
|
1326
|
+
// streamingLatest 生命周期只由两种信号终结(不再用短 timeout 兜底):
|
|
1327
|
+
// 1) 正常:最终 entry 到达时 _flushPendingEntries 原子清除
|
|
1328
|
+
// 2) 异常:SSE 连接真死 (_reconnectSSE)
|
|
1329
|
+
// 避免长 thinking / 网络抖动 / 切 tab 等场景误杀 overlay。
|
|
1330
|
+
this._pendingStreamingLatest = {
|
|
1331
|
+
timestamp: data.timestamp,
|
|
1332
|
+
url: data.url,
|
|
1333
|
+
content: data.content || [],
|
|
1334
|
+
model: data.model,
|
|
1335
|
+
updatedAt: Date.now(),
|
|
1336
|
+
};
|
|
1337
|
+
if (this._streamingRaf) return;
|
|
1338
|
+
this._streamingRaf = requestAnimationFrame(() => {
|
|
1339
|
+
this._streamingRaf = null;
|
|
1340
|
+
const pending = this._pendingStreamingLatest;
|
|
1341
|
+
this._pendingStreamingLatest = null;
|
|
1342
|
+
if (!pending) return;
|
|
1343
|
+
React.startTransition(() => {
|
|
1344
|
+
this.setState({ streamingLatest: pending });
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
} catch (e) { reportSwallowed('sse.stream-progress', e); }
|
|
1348
|
+
});
|
|
1349
|
+
this.eventSource.addEventListener('resume_prompt', (event) => {
|
|
1350
|
+
this._resetSSETimeout();
|
|
1351
|
+
try {
|
|
1352
|
+
const data = JSON.parse(event.data);
|
|
1353
|
+
// 等待偏好加载完成再判断是否跳过弹窗(避免竞态)
|
|
1354
|
+
(this.context._prefsReady || Promise.resolve({})).then((initialPrefs) => {
|
|
1355
|
+
// 优先读 live preferences(本会话内改过开关需立即生效,否则关了开关当次仍自动继承);
|
|
1356
|
+
// provider 尚未 setState 时回落启动快照
|
|
1357
|
+
const prefs = this.context?.preferences || initialPrefs;
|
|
1358
|
+
if (prefs?.resumeAutoChoice) {
|
|
1359
|
+
// 自动跳过:直接发送选择到服务端,不触碰偏好设置(避免 setState 竞态清除偏好)
|
|
1360
|
+
fetch(apiUrl('/api/resume-choice'), {
|
|
1361
|
+
method: 'POST',
|
|
1362
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1363
|
+
body: JSON.stringify({ choice: prefs.resumeAutoChoice }),
|
|
1364
|
+
}).catch(err => console.error('resume-choice failed:', err));
|
|
1365
|
+
} else {
|
|
1366
|
+
this.setState({ resumeModalVisible: true, resumeFileName: data.recentFileName || '' });
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
} catch (e) { reportSwallowed('sse.resume_prompt', e); }
|
|
1370
|
+
});
|
|
1371
|
+
this.eventSource.addEventListener('resume_resolved', () => {
|
|
1372
|
+
this._resetSSETimeout();
|
|
1373
|
+
this.setState({ resumeModalVisible: false, resumeFileName: '', resumeRememberChoice: false });
|
|
1374
|
+
});
|
|
1375
|
+
// update_completed 事件已废弃:自 1.6.203 起后台 detached npm install 负责升级,
|
|
1376
|
+
// 当前进程内存里仍是旧版本,广播"已升级完成"会误导用户。保留 update_major_available
|
|
1377
|
+
// 作为"有新版可用"的统一信号(包含跨大版本提示 + 本版本忙时跳过两种场景)。
|
|
1378
|
+
this.eventSource.addEventListener('update_major_available', (event) => {
|
|
1379
|
+
this._resetSSETimeout();
|
|
1380
|
+
try {
|
|
1381
|
+
const data = JSON.parse(event.data);
|
|
1382
|
+
this.setState({ updateInfo: { type: 'major', version: data.version } });
|
|
1383
|
+
} catch (e) { reportSwallowed('sse.update_major_available', e); }
|
|
1384
|
+
});
|
|
1385
|
+
this.eventSource.addEventListener('load_start', (event) => {
|
|
1386
|
+
this._resetSSETimeout();
|
|
1387
|
+
try {
|
|
1388
|
+
const data = JSON.parse(event.data);
|
|
1389
|
+
this._chunkedEntries = [];
|
|
1390
|
+
this._chunkedTotal = data.total || 0;
|
|
1391
|
+
this._isIncremental = !!data.incremental;
|
|
1392
|
+
this._hasMoreHistory = !!data.hasMore;
|
|
1393
|
+
this._oldestTs = data.oldestTs || null;
|
|
1394
|
+
// 增量模式下已有缓存数据在显示,不需要 loading 遮罩
|
|
1395
|
+
if (!this._isIncremental) {
|
|
1396
|
+
this.setState({ fileLoading: true, fileLoadingCount: 0 });
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e) { reportSwallowed('sse.load_start', e); }
|
|
1399
|
+
});
|
|
1400
|
+
this.eventSource.addEventListener('load_chunk', (event) => {
|
|
1401
|
+
this._resetSSETimeout();
|
|
1402
|
+
try {
|
|
1403
|
+
const chunk = JSON.parse(event.data);
|
|
1404
|
+
if (Array.isArray(chunk)) {
|
|
1405
|
+
this._chunkedEntries.push(...chunk);
|
|
1406
|
+
// 增量模式下静默累积;非增量模式用 rAF 节流,每帧最多更新一次计数
|
|
1407
|
+
if (!this._isIncremental && !this._loadingCountRafId) {
|
|
1408
|
+
this._loadingCountRafId = requestAnimationFrame(() => {
|
|
1409
|
+
this._loadingCountRafId = null;
|
|
1410
|
+
this.setState({ fileLoadingCount: this._chunkedEntries.length });
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
} catch (e) { reportSwallowed('sse.load_chunk', e, { dataLen: event.data?.length }); }
|
|
1415
|
+
});
|
|
1416
|
+
this.eventSource.addEventListener('load_end', () => {
|
|
1417
|
+
this._resetSSETimeout();
|
|
1418
|
+
if (this._loadingCountRafId) { cancelAnimationFrame(this._loadingCountRafId); this._loadingCountRafId = null; }
|
|
1419
|
+
const delta = this._chunkedEntries;
|
|
1420
|
+
this._chunkedEntries = [];
|
|
1421
|
+
this._chunkedTotal = 0;
|
|
1422
|
+
const isIncremental = this._isIncremental;
|
|
1423
|
+
this._isIncremental = false;
|
|
1424
|
+
// 解锁信号:增量模式下出现至少一条**带 body.input 的 mainAgent** 条目,说明
|
|
1425
|
+
// mainAgent 真有新一轮请求落盘。仅看 delta.length>0 会被 SSE 重连时 backlog
|
|
1426
|
+
// replay 的旧 entry(synthetic、post-stop hook 等)误触发;mainAgent + body.input
|
|
1427
|
+
// 才是"用户实际发了内容"的最强信号。覆盖 TerminalPanel /clear 后用户没走 ChatView
|
|
1428
|
+
// 输入框(pty 直接键入 / 外部 hook / Agent 自驱)时血条卡 0% 的场景。
|
|
1429
|
+
// 注:解锁不再单独 setState,并入分帧管线末段的原子提交(避免与主提交分帧)。
|
|
1430
|
+
let unlockContextBar = false;
|
|
1431
|
+
if (isIncremental && this.state.contextBarLocked) {
|
|
1432
|
+
const hasMainAgentTurn = delta.some(e => {
|
|
1433
|
+
if (!e || !e.mainAgent) return false;
|
|
1434
|
+
const input = e.body?.input;
|
|
1435
|
+
return Array.isArray(input) && input.length > 0;
|
|
1436
|
+
});
|
|
1437
|
+
if (hasMainAgentTurn) unlockContextBar = true;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// 增量模式:Map 去重合并(delta 条目覆盖同 key 的缓存条目)
|
|
1441
|
+
let rawEntries;
|
|
1442
|
+
if (isIncremental && isMobile && this.state.requests.length > 0) {
|
|
1443
|
+
if (delta.length === 0) {
|
|
1444
|
+
// 无新数据,缓存已是最新,跳过重建(保留缓存恢复时已设置的 hasMoreHistory)
|
|
1445
|
+
const st = { fileLoading: false, fileLoadingCount: 0 };
|
|
1446
|
+
if (unlockContextBar) st.contextBarLocked = false;
|
|
1447
|
+
this.setState(st);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const eKey = (e, i) => (e.timestamp && e.url) ? `${e.timestamp}|${e.url}` : `__nokey_c${i}`;
|
|
1451
|
+
const map = new Map();
|
|
1452
|
+
this.state.requests.forEach((e, i) => map.set(eKey(e, i), e));
|
|
1453
|
+
delta.forEach((e, i) => map.set((e.timestamp && e.url) ? `${e.timestamp}|${e.url}` : `__nokey_d${i}`, e));
|
|
1454
|
+
// 注意:合并结果含 state.requests 的 live 引用 —— 分帧 slim/process 期间这些对象被
|
|
1455
|
+
// 原地变异(intern/_slimmed),让步间隙的 render 会看到中间态。旧同步代码同样原地
|
|
1456
|
+
// 变异(只是单任务内完成);最终原子提交会以干净引用整体覆盖。
|
|
1457
|
+
rawEntries = Array.from(map.values());
|
|
1458
|
+
} else {
|
|
1459
|
+
rawEntries = delta;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// 分帧管线:reconstruct → 分帧 slim → 分帧 process → 原子提交。
|
|
1463
|
+
// async 不 await(EventSource 回调);在途期间 live 条目入闸门缓冲(handleEventMessage)。
|
|
1464
|
+
this._runSseColdIngest(rawEntries, { isIncremental, unlockContextBar });
|
|
1465
|
+
});
|
|
1466
|
+
this.eventSource.addEventListener('full_reload', (event) => {
|
|
1467
|
+
this._resetSSETimeout();
|
|
1468
|
+
// 服务端要求整体重载 = baseline 重置:废弃在途分帧管线(防其稍后提交陈旧基线),
|
|
1469
|
+
// 闸门缓冲泄回 _pendingEntries(dedup 兜底与重载数据的重复)。
|
|
1470
|
+
this._abortColdIngest({ drain: true });
|
|
1471
|
+
// animateLoadingCount 回调有数百 ms 窗口:期间若新分帧管线启动(token 再 bump),
|
|
1472
|
+
// 本次 full_reload 的延迟 setState 不得覆盖新管线提交 —— 回调内按 token 失配丢弃。
|
|
1473
|
+
const reloadToken = this._ingestToken;
|
|
1474
|
+
try {
|
|
1475
|
+
const entries = JSON.parse(event.data);
|
|
1476
|
+
if (Array.isArray(entries)) {
|
|
1477
|
+
if (entries.length > 0) this._batchSlim(entries);
|
|
1478
|
+
const { mainAgentSessions, filtered } = entries.length > 0 ? this._processEntries(entries) : { mainAgentSessions: [], filtered: [] };
|
|
1479
|
+
if (entries.length > 0) {
|
|
1480
|
+
this.animateLoadingCount(entries.length, () => {
|
|
1481
|
+
if (this._ingestToken !== reloadToken) return; // 已被新管线 supersede
|
|
1482
|
+
this.setState({
|
|
1483
|
+
requests: entries,
|
|
1484
|
+
selectedIndex: filtered.length > 0 ? filtered.length - 1 : null,
|
|
1485
|
+
mainAgentSessions,
|
|
1486
|
+
fileLoading: false,
|
|
1487
|
+
fileLoadingCount: 0,
|
|
1488
|
+
});
|
|
1489
|
+
if (isMobile && this.state.projectName) {
|
|
1490
|
+
saveEntries(this.state.projectName, entries);
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
this.setState({
|
|
1495
|
+
requests: entries,
|
|
1496
|
+
selectedIndex: null,
|
|
1497
|
+
mainAgentSessions,
|
|
1498
|
+
fileLoading: false,
|
|
1499
|
+
fileLoadingCount: 0,
|
|
1500
|
+
});
|
|
1501
|
+
if (isMobile) clearEntries();
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
this.setState({ fileLoading: false, fileLoadingCount: 0 });
|
|
1505
|
+
}
|
|
1506
|
+
} catch (e) {
|
|
1507
|
+
// A silently failed full reload discards the server's baseline — report, then recover.
|
|
1508
|
+
reportSwallowed('sse.full_reload', e);
|
|
1509
|
+
this.setState({ fileLoading: false, fileLoadingCount: 0 });
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
// 工作区模式事件
|
|
1513
|
+
this.eventSource.addEventListener('workspace_started', (event) => {
|
|
1514
|
+
this._resetSSETimeout();
|
|
1515
|
+
try {
|
|
1516
|
+
const data = JSON.parse(event.data);
|
|
1517
|
+
// 取消旧动画,防止旧 full_reload 回调覆盖新数据
|
|
1518
|
+
if (this._loadingCountTimer) {
|
|
1519
|
+
cancelAnimationFrame(this._loadingCountTimer);
|
|
1520
|
+
this._loadingCountTimer = null;
|
|
1521
|
+
}
|
|
1522
|
+
// workspace 切换 = baseline 重置:废弃在途分帧管线,防旧项目的巨型基线
|
|
1523
|
+
// 在切换后才提交、覆盖新项目数据(闸门缓冲属旧项目,直接丢弃不泄洪)
|
|
1524
|
+
this._abortColdIngest();
|
|
1525
|
+
this._rebuildRequestIndex([]);
|
|
1526
|
+
// 切项目要连 _currentSessionId 一并清掉:否则 _maintainPinState 的 lazy-lock 兜底
|
|
1527
|
+
// (getSessionStableId(null) || this._currentSessionId) 会拿旧项目的会话 id 误锁新项目,
|
|
1528
|
+
// 在 hydrate GET 抢先返回前可能把旧 id POST 进新项目的 pin 文件。
|
|
1529
|
+
this._currentSessionId = null;
|
|
1530
|
+
// SSE workspace switch — rebind alias subscription to the new
|
|
1531
|
+
// project before writing the title so the title reflects the new
|
|
1532
|
+
// alias if one exists. _applyDocTitle handles the "no alias"
|
|
1533
|
+
// fallback (used to be `${projectName} - CX Viewer` here; that
|
|
1534
|
+
// suffix is dropped — pure projectName for consistency with the
|
|
1535
|
+
// initial mount path).
|
|
1536
|
+
this._resubscribeAlias(data.projectName || '');
|
|
1537
|
+
this._applyDocTitle(data.projectName || '');
|
|
1538
|
+
// Reset isStreaming alongside streamingLatest — workspace switches happen
|
|
1539
|
+
// between user prompts and shouldn't leave streaming flags stuck. (turnEnd
|
|
1540
|
+
// false-fire on this transition is no longer a concern since we hook
|
|
1541
|
+
// turnEnd to the Stop SSE event, not to isStreaming falling-edge.)
|
|
1542
|
+
this.setState({
|
|
1543
|
+
workspaceMode: false,
|
|
1544
|
+
projectName: data.projectName || '',
|
|
1545
|
+
viewMode: 'chat',
|
|
1546
|
+
cliMode: true,
|
|
1547
|
+
requests: [],
|
|
1548
|
+
mainAgentSessions: [],
|
|
1549
|
+
// 切项目:清空旧项目 pin,由 App.componentDidUpdate 按新 projectName 重新 hydrate
|
|
1550
|
+
pinnedSessionTs: null,
|
|
1551
|
+
selectedIndex: null,
|
|
1552
|
+
streamingLatest: null,
|
|
1553
|
+
isStreaming: false,
|
|
1554
|
+
// workspace 切换 = cwd 切换 → codex 的 lastModelUsage 也要重查;
|
|
1555
|
+
// 后端在 workspace_started 一并塞了新 cwd 对应的 hint,没有就清空。
|
|
1556
|
+
codexProjectModel: (typeof data.codexProjectModel === 'string' && data.codexProjectModel) ? data.codexProjectModel : null,
|
|
1557
|
+
});
|
|
1558
|
+
if (isMobile) clearEntries();
|
|
1559
|
+
} catch (e) { reportSwallowed('sse.workspace_started', e); }
|
|
1560
|
+
});
|
|
1561
|
+
this.eventSource.addEventListener('workspace_stopped', () => {
|
|
1562
|
+
this._resetSSETimeout();
|
|
1563
|
+
this._teardownTransientLiveState();
|
|
1564
|
+
this._rebuildRequestIndex([]);
|
|
1565
|
+
this._currentSessionId = null; // 同 workspace_started:清旧会话 id,避免 lazy-lock 误锁
|
|
1566
|
+
this.setState({
|
|
1567
|
+
workspaceMode: true,
|
|
1568
|
+
requests: [],
|
|
1569
|
+
mainAgentSessions: [],
|
|
1570
|
+
projectName: '',
|
|
1571
|
+
pinnedSessionTs: null,
|
|
1572
|
+
selectedIndex: null,
|
|
1573
|
+
streamingLatest: null,
|
|
1574
|
+
contextBarLocked: false,
|
|
1575
|
+
isStreaming: false,
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
this.eventSource.addEventListener('context_window', (event) => {
|
|
1579
|
+
this._resetSSETimeout();
|
|
1580
|
+
try {
|
|
1581
|
+
const data = JSON.parse(event.data);
|
|
1582
|
+
// 收到新的 context_window 测量 → 同步解锁血条。
|
|
1583
|
+
// 兜底场景:onUserMessageSent / load_end fallback 都没触发解锁时
|
|
1584
|
+
//(WS 抖动、非增量 load、纯外部输入),SSE 推送的真实测量值就是
|
|
1585
|
+
//「会话已推进」的最强信号,避免 lock 永久卡 0%。
|
|
1586
|
+
this.setState({ contextWindow: data, contextBarOptimistic: false, contextBarLocked: false });
|
|
1587
|
+
if (this._clearOptimisticTimer) { clearTimeout(this._clearOptimisticTimer); this._clearOptimisticTimer = null; }
|
|
1588
|
+
} catch (e) { reportSwallowed('sse.context_window', e); }
|
|
1589
|
+
});
|
|
1590
|
+
this.eventSource.addEventListener('workflow_update', (event) => {
|
|
1591
|
+
this._resetSSETimeout();
|
|
1592
|
+
try {
|
|
1593
|
+
publishWorkflowUpdate(JSON.parse(event.data));
|
|
1594
|
+
} catch (e) { reportSwallowed('sse.workflow_update', e); }
|
|
1595
|
+
});
|
|
1596
|
+
this.eventSource.addEventListener('proxy_profile', (event) => {
|
|
1597
|
+
this._resetSSETimeout();
|
|
1598
|
+
try {
|
|
1599
|
+
const data = JSON.parse(event.data);
|
|
1600
|
+
if (data.active) this.setState({ activeProxyId: data.active });
|
|
1601
|
+
if (data.profile) {
|
|
1602
|
+
// 刷新完整列表
|
|
1603
|
+
fetch(apiUrl('/api/proxy-profiles')).then(r => r.json()).then(d => {
|
|
1604
|
+
if (d.profiles) this.setState({ proxyProfiles: d.profiles, activeProxyId: d.active || 'max' });
|
|
1605
|
+
}).catch(() => { });
|
|
1606
|
+
}
|
|
1607
|
+
} catch (e) { reportSwallowed('sse.proxy_profile', e); }
|
|
1608
|
+
});
|
|
1609
|
+
this.eventSource.addEventListener('ping', () => { this._resetSSETimeout(); });
|
|
1610
|
+
// server_config: server 启动时一次性推 turnEnd debounce ms(CXV_TURN_END_DEBOUNCE_MS
|
|
1611
|
+
// 可能改过默认值),前端拿这个值同步 voicePackPlayer 的 turnEnd cooldown,避免硬常数漂移。
|
|
1612
|
+
this.eventSource.addEventListener('server_config', (event) => {
|
|
1613
|
+
this._resetSSETimeout();
|
|
1614
|
+
try {
|
|
1615
|
+
const cfg = JSON.parse(event?.data || '{}');
|
|
1616
|
+
if (typeof cfg.turnEndDebounceMs === 'number') setTurnEndCooldownMs(cfg.turnEndDebounceMs);
|
|
1617
|
+
} catch { /* tolerate parse error */ }
|
|
1618
|
+
});
|
|
1619
|
+
// session_pin SSE — 本进程任一端改了「当前会话」pin 后广播,让同实例多端(电脑+手机)实时一致。
|
|
1620
|
+
// 采纳服务端值时置 _applyingRemotePin,避免 _maintainPinState 把它当本地改动回 POST(防回环)。
|
|
1621
|
+
// A broadcast value that mismatches the locally derived latest is rejected via
|
|
1622
|
+
// resolveHydratedPin (another client may have derived a stale "latest"). No
|
|
1623
|
+
// POST-back on reject — re-persisting inside the broadcast handler could
|
|
1624
|
+
// ping-pong between clients; healing flows through _maintainPinState's
|
|
1625
|
+
// normal persist path instead.
|
|
1626
|
+
this.eventSource.addEventListener('session_pin', (event) => {
|
|
1627
|
+
this._resetSSETimeout();
|
|
1628
|
+
try {
|
|
1629
|
+
const data = JSON.parse(event?.data || '{}');
|
|
1630
|
+
const r = resolveHydratedPin(data.pinnedSessionId, this._derivedLatestId(), this._effectiveOnlyCurrentSession());
|
|
1631
|
+
if (r.adopt && r.value !== this.state.pinnedSessionTs) {
|
|
1632
|
+
this._applyingRemotePin = true;
|
|
1633
|
+
this.setState({ pinnedSessionTs: r.value }, () => { this._applyingRemotePin = false; });
|
|
1634
|
+
}
|
|
1635
|
+
} catch { /* tolerate parse error */ }
|
|
1636
|
+
});
|
|
1637
|
+
// turn_end SSE — broadcast by /api/turn-end-notify whenever Codex's Stop hook
|
|
1638
|
+
// fires (real end of a user-prompt turn). This is the **authoritative** turnEnd
|
|
1639
|
+
// signal — far more accurate than isStreaming falling-edge, which resets per-API-call
|
|
1640
|
+
// and would mis-fire during slow tool execution. 30s cooldown lives in voicePackPlayer.
|
|
1641
|
+
this.eventSource.addEventListener('turn_end', (event) => {
|
|
1642
|
+
// Guard against a teardown race: SSE chunks in flight when _reconnectSSE
|
|
1643
|
+
// closes the current EventSource can still fire here before the listener
|
|
1644
|
+
// unbinds (round-3 quality P1).
|
|
1645
|
+
if (!this.eventSource) return;
|
|
1646
|
+
this._resetSSETimeout();
|
|
1647
|
+
const vp = this.state.approvalPrefs && this.state.approvalPrefs.voicePack;
|
|
1648
|
+
if (vp && vp.enabled && vp.events && vp.events.turnEnd) {
|
|
1649
|
+
let serverTs = null;
|
|
1650
|
+
try { serverTs = (JSON.parse(event?.data || '{}'))?.ts || null; } catch { /* fine */ }
|
|
1651
|
+
try {
|
|
1652
|
+
playVoiceEvent('turnEnd', vp, {
|
|
1653
|
+
// Prefer the server-supplied ts so a re-broadcast (server bug, two
|
|
1654
|
+
// SSE delivery paths) is deduped by the player. Falls back to a
|
|
1655
|
+
// unique key if absent — relies on COOLDOWN_MS.turnEnd to suppress.
|
|
1656
|
+
dedupeKey: `turnEnd:${serverTs || Date.now()}`,
|
|
1657
|
+
});
|
|
1658
|
+
} catch { /* never propagate */ }
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
// im_log_update SSE — 主服务 fs.watch 到某 IM worker 日志目录写入时广播(IM worker 独立端口,
|
|
1662
|
+
// turn_end 落在 worker 自己进程,主服务收不到,故用日志落盘信号驱动「对话记录」自动刷新)。
|
|
1663
|
+
// AppBase 不直接持有 ImConversationModal,转成 window 事件解耦派发,弹窗打开时自行监听并重拉。
|
|
1664
|
+
this.eventSource.addEventListener('im_log_update', (event) => {
|
|
1665
|
+
if (!this.eventSource) return;
|
|
1666
|
+
this._resetSSETimeout();
|
|
1667
|
+
let platform = null;
|
|
1668
|
+
if (typeof event?.data === 'string') {
|
|
1669
|
+
try { platform = JSON.parse(event.data)?.platform || null; } catch { /* tolerate */ }
|
|
1670
|
+
}
|
|
1671
|
+
if (platform) {
|
|
1672
|
+
try { window.dispatchEvent(new CustomEvent('cxv:im-log-update', { detail: { platform } })); } catch { /* noop */ }
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
this.eventSource.addEventListener('streaming_status', (e) => {
|
|
1676
|
+
this._resetSSETimeout();
|
|
1677
|
+
try {
|
|
1678
|
+
const data = JSON.parse(e.data);
|
|
1679
|
+
if (data.active) {
|
|
1680
|
+
// 立即显示 loading
|
|
1681
|
+
clearTimeout(this._streamingOffTimer);
|
|
1682
|
+
// agent 开始响应 = 新一轮已落实 → 顺手解锁血条。
|
|
1683
|
+
// 覆盖 onUserMessageSent 没触发的极端情况(WS 抖动 / 外部输入 /
|
|
1684
|
+
// pty 直接键入),避免 lock 永久卡 0%。
|
|
1685
|
+
const patch = { isStreaming: true };
|
|
1686
|
+
if (this.state.contextBarLocked) patch.contextBarLocked = false;
|
|
1687
|
+
this.setState(patch);
|
|
1688
|
+
} else {
|
|
1689
|
+
// 延迟隐藏,避免工具调用间隙导致 spinner 频繁闪烁
|
|
1690
|
+
clearTimeout(this._streamingOffTimer);
|
|
1691
|
+
this._streamingOffTimer = setTimeout(() => {
|
|
1692
|
+
this.setState({ isStreaming: false });
|
|
1693
|
+
}, 2000);
|
|
1694
|
+
}
|
|
1695
|
+
} catch (err) { console.error('Failed to parse streaming_status:', err); }
|
|
1696
|
+
});
|
|
1697
|
+
this.eventSource.onerror = () => {
|
|
1698
|
+
console.error('SSE连接错误');
|
|
1699
|
+
// 不清 streamingLatest:浏览器会自动 3s 重连,新 chunk 到达会覆盖 state;
|
|
1700
|
+
// 若彻底断连,45s heartbeat 超时触发 _reconnectSSE,那里会清 overlay;
|
|
1701
|
+
// 若流式已完成,最终 entry 的原子清除会收走 overlay。
|
|
1702
|
+
};
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
console.error('EventSource初始化失败:', error);
|
|
1705
|
+
this.setState({ fileLoading: false, fileLoadingCount: 0 });
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
loadLocalLogFile(file) {
|
|
1710
|
+
// 独立 SSE 链路加载历史日志:/api/local-log 返回 event-stream,
|
|
1711
|
+
// 与 /events (CLI 模式) 完全隔离,不会触发 terminal/workspace 等 CLI 行为
|
|
1712
|
+
this._isLocalLog = true;
|
|
1713
|
+
this._localLogFile = file;
|
|
1714
|
+
// 全量加载,无分页:防御上一次状态残留
|
|
1715
|
+
this._hasMoreHistory = false;
|
|
1716
|
+
this._oldestTs = null;
|
|
1717
|
+
this.setState({ fileLoading: true, fileLoadingCount: 0 });
|
|
1718
|
+
|
|
1719
|
+
// 关闭上一次的加载连接(防止快速切换时资源泄漏)
|
|
1720
|
+
if (this._localLogES) { this._localLogES.close(); this._localLogES = null; }
|
|
1721
|
+
|
|
1722
|
+
const entries = [];
|
|
1723
|
+
// logfile 只读模式一次性全量加载(含移动端):走服务端全量流式分支,
|
|
1724
|
+
// 由分帧管线 _runLocalLogIngest 消化大文件,避免「加载更早」手工分页。
|
|
1725
|
+
const es = new EventSource(apiUrl(`/api/local-log?file=${encodeURIComponent(file)}`));
|
|
1726
|
+
this._localLogES = es;
|
|
1727
|
+
|
|
1728
|
+
es.addEventListener('load_start', (event) => {
|
|
1729
|
+
try {
|
|
1730
|
+
const data = JSON.parse(event.data);
|
|
1731
|
+
this._hasMoreHistory = !!data.hasMore;
|
|
1732
|
+
this._oldestTs = data.oldestTs || null;
|
|
1733
|
+
this.setState({ fileLoadingCount: 0 });
|
|
1734
|
+
} catch (e) { reportSwallowed('sse.local-log.load_start', e); }
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
es.addEventListener('load_chunk', (event) => {
|
|
1738
|
+
try {
|
|
1739
|
+
const chunk = JSON.parse(event.data);
|
|
1740
|
+
if (Array.isArray(chunk)) {
|
|
1741
|
+
for (const entry of chunk) {
|
|
1742
|
+
entries.push(entry);
|
|
1743
|
+
}
|
|
1744
|
+
this.setState({ fileLoadingCount: entries.length });
|
|
1745
|
+
}
|
|
1746
|
+
} catch (e) { reportSwallowed('sse.local-log.load_chunk', e, { dataLen: event.data?.length }); }
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
es.addEventListener('load_end', () => {
|
|
1750
|
+
es.close();
|
|
1751
|
+
// 分帧管线(reconstruct → 分帧 slim → 分帧 process → 原子提交):
|
|
1752
|
+
// 历史日志同样可能含巨型 checkpoint,同步管线会卡死主线程。
|
|
1753
|
+
this._runLocalLogIngest(entries);
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
es.onerror = () => {
|
|
1757
|
+
es.close();
|
|
1758
|
+
console.error('加载日志文件 SSE 连接错误');
|
|
1759
|
+
this.setState({ fileLoading: false, fileLoadingCount: 0 });
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
handleEventMessage(event) {
|
|
1764
|
+
try {
|
|
1765
|
+
const entry = JSON.parse(event.data);
|
|
1766
|
+
// 冷启动分帧管线在途:live 条目入闸门缓冲,提交后统一泄洪(_commitColdIngest)。
|
|
1767
|
+
// 否则 live flush 会基于旧 prev.requests 合并、随后被管线的基线提交整体覆盖,
|
|
1768
|
+
// 且 _sseSlimmer/_sseReconstructor 会对错误基线初始化(sessionMerge 脆弱区)。
|
|
1769
|
+
if (this._ingestRunning) {
|
|
1770
|
+
this._liveGateBuffer.push(entry);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
this._pendingEntries.push(entry);
|
|
1774
|
+
if (!this._flushRafId) {
|
|
1775
|
+
this._flushRafId = requestAnimationFrame(this._flushPendingEntries);
|
|
1776
|
+
}
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
console.error('处理事件消息失败:', error);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
_flushPendingEntries = () => {
|
|
1783
|
+
this._flushRafId = null;
|
|
1784
|
+
const batch = this._pendingEntries;
|
|
1785
|
+
this._pendingEntries = [];
|
|
1786
|
+
if (batch.length === 0) return;
|
|
1787
|
+
|
|
1788
|
+
this.setState(prev => {
|
|
1789
|
+
const requests = [...prev.requests]; // one copy per frame, not per message
|
|
1790
|
+
|
|
1791
|
+
let mainAgentSessions = prev.mainAgentSessions;
|
|
1792
|
+
let shouldClearStreaming = false; // 检测到最终 entry 时原子清除 Live overlay
|
|
1793
|
+
// 本窗口实时新会话(/clear、/resume)时推进 pin —— 仅实时追加路径会跟随,
|
|
1794
|
+
// 整体重载(_processEntries)不动 pin,于是多开时 [对话] 不被重载/他实例拖走。
|
|
1795
|
+
let _newPinTs = null;
|
|
1796
|
+
|
|
1797
|
+
// P0 perf: lazy init 增量剪枝器
|
|
1798
|
+
if (!this._sseSlimmer) {
|
|
1799
|
+
this._sseSlimmer = createIncrementalSlimmer(isMainAgent);
|
|
1800
|
+
}
|
|
1801
|
+
// Delta 增量重建器:SSE 逐条到达的 delta entry 只有增量 input,
|
|
1802
|
+
// 需要拼接为完整 input(与批量加载时 reconstructEntries 对应)
|
|
1803
|
+
if (!this._sseReconstructor) {
|
|
1804
|
+
this._sseReconstructor = createIncrementalReconstructor();
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
for (const rawEntry of batch) {
|
|
1808
|
+
// Rotation-context sentinel on the LIVE path (rotation while this
|
|
1809
|
+
// client is connected): capture + seed, never enter state.requests.
|
|
1810
|
+
if (rawEntry && rawEntry.cxvRotationContext) {
|
|
1811
|
+
this._rotationContext = rawEntry;
|
|
1812
|
+
if (Array.isArray(rawEntry.teammateNames)) setTeammateNameSeeds(rawEntry.teammateNames);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
// v3: intern body.tools / body.instructions → pool 共享引用,消除 fullEntry 累积
|
|
1816
|
+
// v5: 同时 intern body.input 内 tool_result block.content(lazy-clone 三层
|
|
1817
|
+
// input/content/block)。下方会 mutate `messages[i]._timestamp`
|
|
1818
|
+
// 的安全前提:浅 clone 仅 spread 顶层字段保留 _timestamp 写位;共享的
|
|
1819
|
+
// block.content 是 string primitive 不可变,跨 entry 共享 ref 不会串扰。
|
|
1820
|
+
const entry = internEntryBigFields(this._sseReconstructor.reconstruct(rawEntry));
|
|
1821
|
+
stampConversationMessageCount(entry);
|
|
1822
|
+
const key = `${entry.timestamp}|${entry.url}`;
|
|
1823
|
+
const existingIndex = this._requestIndexMap.get(key);
|
|
1824
|
+
|
|
1825
|
+
if (existingIndex !== undefined) {
|
|
1826
|
+
requests[existingIndex] = entry;
|
|
1827
|
+
if (this._sseSlimmer) this._sseSlimmer.onDedup(existingIndex);
|
|
1828
|
+
} else {
|
|
1829
|
+
const newIdx = requests.length;
|
|
1830
|
+
if (this._sseSlimmer) this._sseSlimmer.processEntry(entry, requests, newIdx);
|
|
1831
|
+
this._requestIndexMap.set(key, newIdx);
|
|
1832
|
+
requests.push(entry);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Live overlay 原子清除:最终 entry(非 inProgress)到达且 timestamp 匹配 → 同 setState 清除 overlay
|
|
1836
|
+
if (!entry.inProgress && isMainAgent(entry) && prev.streamingLatest
|
|
1837
|
+
&& prev.streamingLatest.timestamp === entry.timestamp) {
|
|
1838
|
+
shouldClearStreaming = true;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// 合并 mainAgent sessions(跳过被剪枝的 entry,其 input 已被清空;
|
|
1842
|
+
// 跳过重建层标记的乱序/断裂条目,防完成序倒置翻倍,见 isMergeBlockedEntry JSDoc)
|
|
1843
|
+
// KEEP IN SYNC: test/delta-reorder.test.js clientMergeSse 镜像本块守卫顺序
|
|
1844
|
+
// (mainAgent 形态 → teammate/blocked → applyInPlaceLastMsgReplace → merge),改动需同步
|
|
1845
|
+
const mergeBlocked = isMergeBlockedEntry(entry);
|
|
1846
|
+
const conversationExcluded = shouldExcludeFromConversation(entry);
|
|
1847
|
+
const conversationEntry = conversationExcluded
|
|
1848
|
+
? entry
|
|
1849
|
+
: this._liveConversationNormalizer(entry, { commit: !mergeBlocked });
|
|
1850
|
+
if (!conversationExcluded && isMainAgent(conversationEntry) && conversationEntry.body && Array.isArray(conversationEntry.body.input) && !conversationEntry._slimmed && !mergeBlocked) {
|
|
1851
|
+
const timestamp = conversationEntry.timestamp || new Date().toISOString();
|
|
1852
|
+
const lastSession = mainAgentSessions.length > 0 ? mainAgentSessions[mainAgentSessions.length - 1] : null;
|
|
1853
|
+
const prevMessages = lastSession?.messages || [];
|
|
1854
|
+
const messages = conversationEntry.body.input;
|
|
1855
|
+
const prevCount = prevMessages.length;
|
|
1856
|
+
const messageCount = conversationEntry._conversationMessageCount ?? messages.length;
|
|
1857
|
+
const messageOffset = conversationEntry._conversationWindowStart ?? 0;
|
|
1858
|
+
|
|
1859
|
+
const userId = conversationEntry.body.metadata?.user_id || null;
|
|
1860
|
+
const sessionKey = getMainAgentSessionKey(conversationEntry);
|
|
1861
|
+
// Session-boundary detection shares isSessionBoundary (clearCheckpoint.js)
|
|
1862
|
+
// with the batch path (applyBatchEntryTimestamps) so live streaming and
|
|
1863
|
+
// reload segment sessions identically: post-/clear checkpoint always
|
|
1864
|
+
// splits; a big count drop splits unless it's a /compact continuation
|
|
1865
|
+
// (same-machine multi-terminal user_id is identical, so the summary
|
|
1866
|
+
// msg[0] is the only reliable /compact-vs-new-terminal discriminator);
|
|
1867
|
+
// a user_id change splits (previously batch-only — without it, merge
|
|
1868
|
+
// appended a new session while timestamps were inherited positionally,
|
|
1869
|
+
// yielding two sessions with the SAME stable id and a mis-resolved pin).
|
|
1870
|
+
// KEEP IN SYNC: test/session-boundary-parity.test.js runLiveLeg mirrors
|
|
1871
|
+
// this boundary → assignMessageTimestamps → in-place/merge call order.
|
|
1872
|
+
const isNewSession = isSessionBoundary(conversationEntry, {
|
|
1873
|
+
prevCount,
|
|
1874
|
+
count: messageCount,
|
|
1875
|
+
prevUserId: lastSession ? lastSession.userId : null,
|
|
1876
|
+
userId,
|
|
1877
|
+
prevSessionKey: lastSession ? lastSession.sessionKey || null : null,
|
|
1878
|
+
sessionKey,
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
// SSE 实时流每条 entry 都是完整 request+response,不存在"中间态";
|
|
1882
|
+
// 历史代码曾在此处 `if (isTransient) continue` 跳过极短 entry 防中间态污染,
|
|
1883
|
+
// 但这会把真实的 /clear → 短对话(如 "hi")也丢掉 —— 交给 mergeMainAgentSessions
|
|
1884
|
+
// 的 skipTransientFilter: true 统一放行,isNewSession 单独驱动 _currentSessionId。
|
|
1885
|
+
if (isNewSession) {
|
|
1886
|
+
this._currentSessionId = timestamp;
|
|
1887
|
+
// 新 session 起点:reset _prevMainAgentTs 防跨 session 串场(旧 session 的末尾 ts
|
|
1888
|
+
// 不应作为新 session 第一条 assistant msg 的"生成时 ts")
|
|
1889
|
+
this._prevMainAgentTs = null;
|
|
1890
|
+
// 开启「仅展示当前会话」时,跟随本窗口新会话:把 pin 推进到新会话起点 ts
|
|
1891
|
+
//(= 新会话 messages[0]._timestamp,与 getSessionStableId 一致)。
|
|
1892
|
+
if (this._effectiveOnlyCurrentSession()) _newPinTs = timestamp;
|
|
1893
|
+
} else if (this._currentSessionId === null) {
|
|
1894
|
+
this._currentSessionId = timestamp;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// 赋 _timestamp 和 _generatedTs(assistant 角色新增 msg 拿 prevMainAgentTs 反映生成时 ts)
|
|
1898
|
+
assignMessageTimestamps(messages, prevMessages, isNewSession, prevCount, timestamp, this._prevMainAgentTs, messageOffset);
|
|
1899
|
+
// 信号驱动短路:服务端已检测到末位替换(_inPlaceReplaceDetected:true)→ 直接 in-place
|
|
1900
|
+
// 替换 lastSession.messages 末位,避开 sessionMerge prefix-overlap 算法在
|
|
1901
|
+
// newLen===currentLen+末位fp异 场景必然 overlap=0 → push 整段 → 翻倍的陷阱。
|
|
1902
|
+
// helper 协议详见 src/utils/sessionManager.js applyInPlaceLastMsgReplace JSDoc。
|
|
1903
|
+
const inPlaceResult = applyInPlaceLastMsgReplace(mainAgentSessions, conversationEntry, timestamp, isNewSession);
|
|
1904
|
+
if (inPlaceResult.applied) {
|
|
1905
|
+
mainAgentSessions = inPlaceResult.sessions;
|
|
1906
|
+
} else {
|
|
1907
|
+
// SSE 实时追加:每条 entry 都已是完整 request+response,不存在中间态,
|
|
1908
|
+
// 跳过 transient 过滤以避免误伤真实的 /clear → 短消息对话。
|
|
1909
|
+
mainAgentSessions = this.mergeMainAgentSessions(mainAgentSessions, conversationEntry, { skipTransientFilter: true });
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// 记录本次 mainAgent entry 的 timestamp,给下一次 entry 处理时
|
|
1913
|
+
// 当作 _generatedTs 赋给新增 assistant msg(反映"生成时刻")。
|
|
1914
|
+
// 必须放在 if (isMainAgent && !_slimmed) 块内 —— timestamp 是该块内的 const
|
|
1915
|
+
this._prevMainAgentTs = timestamp;
|
|
1916
|
+
this._currentMainAgentSessionKey = sessionKey || null;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// 标记 entry 的 _sessionId
|
|
1920
|
+
entry._sessionId = this._currentSessionId;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
let selectedIndex = prev.selectedIndex;
|
|
1924
|
+
|
|
1925
|
+
if (mainAgentSessions.length > MAX_SESSIONS) {
|
|
1926
|
+
mainAgentSessions = mainAgentSessions.slice(-MAX_SESSIONS);
|
|
1927
|
+
}
|
|
1928
|
+
if (selectedIndex === null && requests.length > 0) {
|
|
1929
|
+
if (this._autoSelectTimer) clearTimeout(this._autoSelectTimer);
|
|
1930
|
+
this._autoSelectTimer = setTimeout(() => {
|
|
1931
|
+
this.setState(s => {
|
|
1932
|
+
if (s.selectedIndex === null && s.requests.length > 0) {
|
|
1933
|
+
const filtered = visibleRequests(s.requests, s.showAll);
|
|
1934
|
+
return filtered.length > 0 ? { selectedIndex: filtered.length - 1 } : null;
|
|
1935
|
+
}
|
|
1936
|
+
return null;
|
|
1937
|
+
});
|
|
1938
|
+
}, 200);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
return {
|
|
1942
|
+
requests, mainAgentSessions,
|
|
1943
|
+
...(shouldClearStreaming && { streamingLatest: null }),
|
|
1944
|
+
...(_newPinTs != null && { pinnedSessionTs: _newPinTs }),
|
|
1945
|
+
};
|
|
1946
|
+
}, () => {
|
|
1947
|
+
// 移动端:防抖 5s 批量写入缓存
|
|
1948
|
+
if (isMobile && this.state.projectName) {
|
|
1949
|
+
if (this._cacheSaveTimer) clearTimeout(this._cacheSaveTimer);
|
|
1950
|
+
this._cacheSaveTimer = setTimeout(() => {
|
|
1951
|
+
// hot/cold 分层激活时跳过 saveEntries(state.requests 只有热数据,
|
|
1952
|
+
// 写入会覆盖 load_end 保存的全量缓存)。冷数据已通过 per-session 存储持久化。
|
|
1953
|
+
if (this.state.projectName && this.state.sessionIndex.length === 0) {
|
|
1954
|
+
saveEntries(this.state.projectName, this.state.requests);
|
|
1955
|
+
}
|
|
1956
|
+
}, 5000);
|
|
1957
|
+
// P1: 延迟淘汰冷 session,避免频繁触发
|
|
1958
|
+
if (this.state.mainAgentSessions.length > HOT_SESSION_COUNT + 2) {
|
|
1959
|
+
if (!this._evictionTimer) {
|
|
1960
|
+
this._evictionTimer = setTimeout(() => {
|
|
1961
|
+
this._evictionTimer = null;
|
|
1962
|
+
this._evictColdSessions();
|
|
1963
|
+
}, 10000);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
// ─── P1: cold session 加载 / 淘汰 ──────────────────────────
|
|
1971
|
+
|
|
1972
|
+
async loadSession(sessionId) {
|
|
1973
|
+
if (this._loadingSessionId != null) return;
|
|
1974
|
+
this._loadingSessionId = sessionId;
|
|
1975
|
+
this.setState({ loadingSessionId: sessionId });
|
|
1976
|
+
|
|
1977
|
+
try {
|
|
1978
|
+
// 1. 从 IndexedDB 加载
|
|
1979
|
+
let entries = await loadSessionEntries(this.state.projectName, sessionId);
|
|
1980
|
+
|
|
1981
|
+
// 2. fallback: 从 REST API 加载
|
|
1982
|
+
if (!entries || entries.length === 0) {
|
|
1983
|
+
const meta = (this.state.sessionIndex || []).find(s => s.sessionId === sessionId);
|
|
1984
|
+
if (meta && meta.lastTs) {
|
|
1985
|
+
const res = await fetch(apiUrl(`/api/entries/page?before=${encodeURIComponent(meta.lastTs)}&limit=200`));
|
|
1986
|
+
const data = await res.json();
|
|
1987
|
+
entries = data.entries || [];
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (entries && entries.length > 0) {
|
|
1992
|
+
const reconstructed = reconstructEntries(entries);
|
|
1993
|
+
const merged = [...reconstructed, ...this.state.requests];
|
|
1994
|
+
this._batchSlim(merged);
|
|
1995
|
+
const { mainAgentSessions } = this._processEntries(merged);
|
|
1996
|
+
|
|
1997
|
+
const sessionIndex = buildSessionIndex(merged, mainAgentSessions);
|
|
1998
|
+
const fullIndex = mergeSessionIndices(this.state.sessionIndex, sessionIndex);
|
|
1999
|
+
// Fix #3: pin 加载的 session,防止 splitHotCold 立即淘汰(并入「仅展示当前会话」锁定的 pin)
|
|
2000
|
+
const unslimmed = merged.map(e => e._slimmed ? restoreSlimmedEntry(e, merged) : e);
|
|
2001
|
+
const { hotEntries, allSessions, coldGroups } = splitHotCold(
|
|
2002
|
+
unslimmed, mainAgentSessions, fullIndex, HOT_SESSION_COUNT,
|
|
2003
|
+
this._pinnedSessionIdSet([sessionId])
|
|
2004
|
+
);
|
|
2005
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
2006
|
+
const pn = this.state.projectName;
|
|
2007
|
+
if (pn) {
|
|
2008
|
+
for (const [sid, coldEntries] of coldGroups) {
|
|
2009
|
+
saveSessionEntries(pn, sid, coldEntries);
|
|
2010
|
+
}
|
|
2011
|
+
saveEntries(pn, merged);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
this.setState({
|
|
2015
|
+
requests: hotEntries,
|
|
2016
|
+
mainAgentSessions: allSessions,
|
|
2017
|
+
sessionIndex: fullIndex,
|
|
2018
|
+
loadingSessionId: null,
|
|
2019
|
+
});
|
|
2020
|
+
} else {
|
|
2021
|
+
this.setState({ loadingSessionId: null });
|
|
2022
|
+
}
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
console.error('loadSession failed:', e);
|
|
2025
|
+
this.setState({ loadingSessionId: null });
|
|
2026
|
+
}
|
|
2027
|
+
this._loadingSessionId = null;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
_evictColdSessions() {
|
|
2031
|
+
const { requests, mainAgentSessions, projectName } = this.state;
|
|
2032
|
+
if (!isMobile || mainAgentSessions.length <= HOT_SESSION_COUNT) return;
|
|
2033
|
+
|
|
2034
|
+
const unslimmed = requests.map(e => e._slimmed ? restoreSlimmedEntry(e, requests) : e);
|
|
2035
|
+
const { hotEntries, allSessions, coldGroups } = splitHotCold(
|
|
2036
|
+
unslimmed, mainAgentSessions, this.state.sessionIndex, HOT_SESSION_COUNT, this._pinnedSessionIdSet()
|
|
2037
|
+
);
|
|
2038
|
+
this._sseSlimmer = null; this._sseReconstructor = null;
|
|
2039
|
+
const fullIndex = this.state.sessionIndex;
|
|
2040
|
+
if (projectName) {
|
|
2041
|
+
for (const [sid, coldEntries] of coldGroups) {
|
|
2042
|
+
saveSessionEntries(projectName, sid, coldEntries);
|
|
2043
|
+
}
|
|
2044
|
+
// 不调 saveEntries:state.requests 可能已是 hotEntries,写入会覆盖全量缓存。
|
|
2045
|
+
// 冷数据已通过 saveSessionEntries 持久化,全量缓存由 load_end 维护。
|
|
2046
|
+
}
|
|
2047
|
+
this.setState({
|
|
2048
|
+
requests: hotEntries,
|
|
2049
|
+
mainAgentSessions: allSessions,
|
|
2050
|
+
sessionIndex: fullIndex,
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// ─── 数据处理 ───────────────────────────────────────────
|
|
2055
|
+
|
|
2056
|
+
mergeMainAgentSessions(prevSessions, entry, options) {
|
|
2057
|
+
return _mergeMainAgentSessions(prevSessions, entry, options);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// ─── 选中 & 导航 ───────────────────────────────────────
|
|
2061
|
+
|
|
2062
|
+
handleSelectRequest = (index) => {
|
|
2063
|
+
this.setState({ selectedIndex: index, scrollCenter: false });
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
handleScrollDone = () => { this.setState({ scrollCenter: false }); };
|
|
2067
|
+
handleScrollTsDone = () => { this.setState({ chatScrollToTs: null }); };
|
|
2068
|
+
// 用户点 /clear 时立即把 Header 上下文血条降到 OPTIMISTIC_CLEAR_PERCENT 水位;
|
|
2069
|
+
// 正常路径下一次 context_window SSE 推送会自动取消。
|
|
2070
|
+
// 30s 兜底:SSE 没及时来(PTY 未连接、后端没推、CLI 崩了)时自动清掉,避免血条卡在低位。
|
|
2071
|
+
// 同时进入 locked 状态:忽略 SSE / 其他 re-render,强制血条 0K (0%),直到用户
|
|
2072
|
+
// 通过 _sendUserMessageImmediate 发出一条非 /clear 消息(见 handleUserMessageSent)。
|
|
2073
|
+
handleClearContextOptimistic = () => {
|
|
2074
|
+
this.setState({ contextBarOptimistic: true, contextBarLocked: true });
|
|
2075
|
+
if (this._clearOptimisticTimer) clearTimeout(this._clearOptimisticTimer);
|
|
2076
|
+
this._clearOptimisticTimer = setTimeout(() => {
|
|
2077
|
+
this.setState({ contextBarOptimistic: false });
|
|
2078
|
+
this._clearOptimisticTimer = null;
|
|
2079
|
+
}, 30000);
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// ChatView 在 _sendUserMessageImmediate 里对非 /clear 文本调用本方法解锁血条。
|
|
2083
|
+
handleUserMessageSent = () => {
|
|
2084
|
+
if (this.state.contextBarLocked) this.setState({ contextBarLocked: false });
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// ─── 模式切换 ──────────────────────────────────────────
|
|
2088
|
+
|
|
2089
|
+
handleWorkspaceLaunch = ({ projectName }) => {
|
|
2090
|
+
this._isLocalLog = false;
|
|
2091
|
+
this._localLogFile = null;
|
|
2092
|
+
// 切 project:清掉旧 project 残留的 /clear optimistic 30s timer,避免延迟到新 project 触发。
|
|
2093
|
+
if (this._clearOptimisticTimer) {
|
|
2094
|
+
clearTimeout(this._clearOptimisticTimer);
|
|
2095
|
+
this._clearOptimisticTimer = null;
|
|
2096
|
+
}
|
|
2097
|
+
this.setState({
|
|
2098
|
+
workspaceMode: false,
|
|
2099
|
+
projectName,
|
|
2100
|
+
viewMode: 'chat',
|
|
2101
|
+
cliMode: true,
|
|
2102
|
+
terminalVisible: false,
|
|
2103
|
+
contextBarLocked: false,
|
|
2104
|
+
contextBarOptimistic: false,
|
|
2105
|
+
});
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
handleReturnToWorkspaces = () => {
|
|
2109
|
+
fetch(apiUrl('/api/workspaces/stop'), { method: 'POST' })
|
|
2110
|
+
.then(() => {
|
|
2111
|
+
this._teardownTransientLiveState();
|
|
2112
|
+
this._rebuildRequestIndex([]);
|
|
2113
|
+
this._currentSessionId = null; // 同 workspace_started:清旧会话 id,避免 lazy-lock 误锁
|
|
2114
|
+
this.setState({
|
|
2115
|
+
workspaceMode: true,
|
|
2116
|
+
requests: [],
|
|
2117
|
+
mainAgentSessions: [],
|
|
2118
|
+
projectName: '',
|
|
2119
|
+
pinnedSessionTs: null,
|
|
2120
|
+
selectedIndex: null,
|
|
2121
|
+
streamingLatest: null,
|
|
2122
|
+
contextBarLocked: false,
|
|
2123
|
+
isStreaming: false,
|
|
2124
|
+
});
|
|
2125
|
+
})
|
|
2126
|
+
.catch(() => {});
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
// ─── Proxy Profile ─────────────────────────────────────
|
|
2130
|
+
|
|
2131
|
+
handleProxyProfileChange = (data) => {
|
|
2132
|
+
fetch(apiUrl('/api/proxy-profiles'), {
|
|
2133
|
+
method: 'POST',
|
|
2134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2135
|
+
body: JSON.stringify(data),
|
|
2136
|
+
})
|
|
2137
|
+
.then(r => r.json())
|
|
2138
|
+
.then(() => {
|
|
2139
|
+
this.setState({ proxyProfiles: data.profiles, activeProxyId: data.active });
|
|
2140
|
+
})
|
|
2141
|
+
.catch(() => { });
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
// ─── 偏好设置 ──────────────────────────────────────────
|
|
2145
|
+
|
|
2146
|
+
handleLangChange = () => {
|
|
2147
|
+
const lang = getLang();
|
|
2148
|
+
this.setState({ lang });
|
|
2149
|
+
this.context.updatePreferences({ lang });
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
handleCollapseToolResultsChange = (checked) => {
|
|
2153
|
+
// 单一真相源 = context;updatePreferences 内乐观 setState 即驱动重渲染。
|
|
2154
|
+
this.context.updatePreferences({ collapseToolResults: checked });
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
handleExpandThinkingChange = (checked) => {
|
|
2158
|
+
this.context.updatePreferences({ expandThinking: checked });
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
handleApprovalsReviewerChange = (value) => {
|
|
2162
|
+
const approvalsReviewer = normalizeApprovalsReviewer(value);
|
|
2163
|
+
const previous = this.state.approvalsReviewer;
|
|
2164
|
+
const seq = ++this._approvalsReviewerUpdateSeq;
|
|
2165
|
+
this.setState({ approvalsReviewer });
|
|
2166
|
+
this._approvalsReviewerPendingWrites += 1;
|
|
2167
|
+
const send = () => fetch(apiUrl('/api/approval-reviewer'), {
|
|
2168
|
+
method: 'POST',
|
|
2169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2170
|
+
body: JSON.stringify({ approvalsReviewer }),
|
|
2171
|
+
});
|
|
2172
|
+
const request = this._approvalsReviewerWriteQueue.then(send, send);
|
|
2173
|
+
this._approvalsReviewerWriteQueue = request.catch(() => null);
|
|
2174
|
+
return request.then(async (res) => {
|
|
2175
|
+
const data = await res.json().catch(() => ({}));
|
|
2176
|
+
if (!res.ok) throw new Error(data.error || 'Approval reviewer update failed');
|
|
2177
|
+
if (seq !== this._approvalsReviewerUpdateSeq) return data;
|
|
2178
|
+
const confirmed = normalizeApprovalsReviewer(data.approvalsReviewer);
|
|
2179
|
+
this.setState({ approvalsReviewer: confirmed });
|
|
2180
|
+
this.context?.mergeLocalPreferences?.({ approvalsReviewer: confirmed });
|
|
2181
|
+
message.success(t(data.appliedToRuntime
|
|
2182
|
+
? 'ui.permission.reviewer.applied'
|
|
2183
|
+
: 'ui.permission.reviewer.saved'));
|
|
2184
|
+
return data;
|
|
2185
|
+
}).catch(() => {
|
|
2186
|
+
if (seq !== this._approvalsReviewerUpdateSeq) return null;
|
|
2187
|
+
this.setState({ approvalsReviewer: previous });
|
|
2188
|
+
this.context?.mergeLocalPreferences?.({ approvalsReviewer: previous });
|
|
2189
|
+
message.error(t('ui.permission.reviewer.failed'));
|
|
2190
|
+
return null;
|
|
2191
|
+
}).finally(() => {
|
|
2192
|
+
this._approvalsReviewerPendingWrites = Math.max(0, this._approvalsReviewerPendingWrites - 1);
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
handleApprovalsReviewerSynced = (value) => {
|
|
2197
|
+
if (this._approvalsReviewerPendingWrites > 0) return;
|
|
2198
|
+
const approvalsReviewer = normalizeApprovalsReviewer(value);
|
|
2199
|
+
if (approvalsReviewer !== this.state.approvalsReviewer) {
|
|
2200
|
+
this._approvalsReviewerUpdateSeq += 1;
|
|
2201
|
+
}
|
|
2202
|
+
this.setState({ approvalsReviewer });
|
|
2203
|
+
this.context?.mergeLocalPreferences?.({ approvalsReviewer });
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
// 终端工具栏快捷设置菜单的 Plan 档位回调。稳定引用(类属性而非调用处内联箭头),
|
|
2207
|
+
// 避免 App/Mobile 每次 render 生成新闭包穿透 ChatView.shouldComponentUpdate。
|
|
2208
|
+
handlePlanAutoApproveChange = (seconds) => {
|
|
2209
|
+
this.handleApprovalPrefsChange({ planAutoApproveSeconds: seconds });
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
// ─── Approval modal: ChatView -> AppBase bubbling handlers ───────────────────────
|
|
2213
|
+
// Inject projectName from AppBase state so the modal chip / Notification body have
|
|
2214
|
+
// human-readable session context. ChatView itself doesn't track project name.
|
|
2215
|
+
_injectProjectName = (data, slot) => {
|
|
2216
|
+
if (!data) return data;
|
|
2217
|
+
const projectName = this.state.projectName || '';
|
|
2218
|
+
if (!projectName) return data;
|
|
2219
|
+
const innerKey = slot; // 'ptyPlan' | 'ask'
|
|
2220
|
+
if (data[innerKey] && data[innerKey].projectName === undefined) {
|
|
2221
|
+
return { ...data, [innerKey]: { ...data[innerKey], projectName } };
|
|
2222
|
+
}
|
|
2223
|
+
return data;
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
// Generic transition helper that mirrors a kind in/out of approvalGlobal AND wipes stale
|
|
2227
|
+
// dismissed entries for that kind. Used by both ask (static id reuse) and ptyPlan (timestamp ids
|
|
2228
|
+
// could repeat after long sessions). PTY plan and ask share the same dismiss-on-transition policy.
|
|
2229
|
+
_setApprovalKind = (kind, data) => {
|
|
2230
|
+
const enriched = this._injectProjectName(data, kind);
|
|
2231
|
+
this.setState(prev => {
|
|
2232
|
+
const next = { ...prev.approvalGlobal };
|
|
2233
|
+
if (enriched) next[kind] = enriched;
|
|
2234
|
+
else next[kind] = null;
|
|
2235
|
+
const dismissed = new Set(prev.approvalDismissedIds);
|
|
2236
|
+
let changed = false;
|
|
2237
|
+
for (const id of dismissed) {
|
|
2238
|
+
if (id.startsWith(`${kind}:`)) { dismissed.delete(id); changed = true; }
|
|
2239
|
+
}
|
|
2240
|
+
return changed
|
|
2241
|
+
? { approvalGlobal: next, approvalDismissedIds: dismissed }
|
|
2242
|
+
: { approvalGlobal: next };
|
|
2243
|
+
});
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
handleApprovalAsk = (data) => this._setApprovalKind('ask', data);
|
|
2247
|
+
handleApprovalPtyPlan = (data) => this._setApprovalKind('ptyPlan', data);
|
|
2248
|
+
|
|
2249
|
+
// Modal calls this when user presses ESC / clicks backdrop. Pending state untouched — only UI hides.
|
|
2250
|
+
handleApprovalDismiss = (kind, id) => {
|
|
2251
|
+
if (!kind || !id) return;
|
|
2252
|
+
this.setState(prev => {
|
|
2253
|
+
const next = new Set(prev.approvalDismissedIds);
|
|
2254
|
+
next.add(`${kind}:${id}`);
|
|
2255
|
+
return { approvalDismissedIds: next };
|
|
2256
|
+
});
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// Bell / chip click reopens minimised modal — clear all dismissed entries currently pending.
|
|
2260
|
+
handleApprovalReopen = () => {
|
|
2261
|
+
this.setState({ approvalDismissedIds: new Set() });
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// Cross-tab jump (electron only). Renderer doesn't directly switch — main does it.
|
|
2265
|
+
handleApprovalJumpTab = (tabId) => {
|
|
2266
|
+
if (typeof window !== 'undefined' && window.tabBridge?.jumpToTab && tabId != null) {
|
|
2267
|
+
try { window.tabBridge.jumpToTab(tabId); } catch {}
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
|
|
2271
|
+
handleApprovalPrefsChange = (patch) => {
|
|
2272
|
+
// 同源 next:setState + POST body 都用同一个 next,避免 rapid toggle 下第二次 POST 读到 stale state 漏 patch
|
|
2273
|
+
const next = { ...this.state.approvalPrefs, ...patch };
|
|
2274
|
+
this.setState({ approvalPrefs: next });
|
|
2275
|
+
// 同步给 electron main 进程,maybeNotify 立即用新 notifyOnlyWhenHidden 决策。
|
|
2276
|
+
// voicePack 不发给 main —— renderer 自己播放,main 只关心 OS notification。
|
|
2277
|
+
try {
|
|
2278
|
+
const { voicePack: _omit, ...forIpc } = next;
|
|
2279
|
+
window.tabBridge?.setApprovalPref?.(forIpc);
|
|
2280
|
+
} catch (e) { console.warn('[approvalPref IPC] onChange sync failed:', e); }
|
|
2281
|
+
this.context.updatePreferences({ approvalModal: next });
|
|
2282
|
+
};
|
|
2283
|
+
|
|
2284
|
+
// Deep-merge change handler for the voicePack subtree — patches `events` field-by-field
|
|
2285
|
+
// so e.g. updating only `events.askQuestion` doesn't drop the bindings for other events.
|
|
2286
|
+
// Uses the shared mergeVoicePackInto helper (single source of truth across hydrate /
|
|
2287
|
+
// server POST / this handler — review dedup).
|
|
2288
|
+
handleVoicePackChange = (patch) => {
|
|
2289
|
+
if (!patch || typeof patch !== 'object') return;
|
|
2290
|
+
const nextVP = mergeVoicePackInto(this.state.approvalPrefs?.voicePack, patch);
|
|
2291
|
+
const nextPrefs = { ...this.state.approvalPrefs, voicePack: nextVP };
|
|
2292
|
+
this.setState({ approvalPrefs: nextPrefs });
|
|
2293
|
+
// SettingsContext.updatePreferences 是顶层浅 merge — 必须带完整 approvalModal,否则会把
|
|
2294
|
+
// modalEnabled / soundEnabled / notifyOnlyWhenHidden 抹成 undefined(直到下次 GET 才回来)。
|
|
2295
|
+
this.context.updatePreferences({ approvalModal: nextPrefs });
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// 合并开关「审批提示音」的统一入口:原子地双写 soundEnabled + voicePack.enabled。
|
|
2299
|
+
// updatePreferences patch 带完整 next(含 voicePack.events / volume),因为 SettingsContext 是
|
|
2300
|
+
// 顶层浅 merge — 若只传 voicePack:{enabled} 会擦掉 events,AskTimeoutCountdown 与 ChatView SDK
|
|
2301
|
+
// 直接读 ctx.approvalModal.voicePack.events 立即变 undefined 致静音。
|
|
2302
|
+
// unlockAudio 在用户手势内立即调用,绕过移动浏览器的 autoplay policy(onChange 是 trusted gesture)。
|
|
2303
|
+
handleApprovalSoundToggle = (checked) => {
|
|
2304
|
+
if (checked) {
|
|
2305
|
+
try { unlockAudio(); } catch (e) { /* 内部已 try/catch,理论上 unreachable */ }
|
|
2306
|
+
}
|
|
2307
|
+
const prev = this.state.approvalPrefs;
|
|
2308
|
+
const nextVP = { ...prev.voicePack, enabled: checked };
|
|
2309
|
+
const next = { ...prev, soundEnabled: checked, voicePack: nextVP };
|
|
2310
|
+
this.setState({ approvalPrefs: next });
|
|
2311
|
+
try {
|
|
2312
|
+
const { voicePack: _omit, ...forIpc } = next;
|
|
2313
|
+
window.tabBridge?.setApprovalPref?.(forIpc);
|
|
2314
|
+
} catch (e) { console.warn('[approvalPref IPC] sound toggle sync failed:', e); }
|
|
2315
|
+
this.context.updatePreferences({ approvalModal: next });
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
/**
|
|
2319
|
+
* 主题应用收口:state / <html data-theme> / localStorage 三处镜像同步。
|
|
2320
|
+
* 三个调用方(hydrate / urlTheme / handleThemeColorChange)行为差异收敛到 opts。
|
|
2321
|
+
*
|
|
2322
|
+
* 幂等:setAttribute 只在值变化时调用,避免唤醒 TerminalPanel MutationObserver
|
|
2323
|
+
* 重赋 xterm theme(80×24 cell 重算 1-3ms)。
|
|
2324
|
+
*/
|
|
2325
|
+
_applyTheme = (value, opts = {}) => {
|
|
2326
|
+
const theme = value === 'light' ? 'light' : 'dark';
|
|
2327
|
+
const { persistPref = false, remountMermaid = false } = opts;
|
|
2328
|
+
if (this.state.themeColor !== theme) this.setState({ themeColor: theme });
|
|
2329
|
+
if (document.documentElement.getAttribute('data-theme') !== theme) {
|
|
2330
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
2331
|
+
}
|
|
2332
|
+
try { localStorage.setItem('cxv_themeColor', theme); } catch {}
|
|
2333
|
+
if (remountMermaid) reinitializeMermaid();
|
|
2334
|
+
if (persistPref) this.context.updatePreferences({ themeColor: theme });
|
|
2335
|
+
};
|
|
2336
|
+
|
|
2337
|
+
handleThemeColorChange = (value) => {
|
|
2338
|
+
this._applyTheme(value, { persistPref: true, remountMermaid: true });
|
|
2339
|
+
// 切换主题后让终端获得焦点,便于用户看到 /theme 切换效果
|
|
2340
|
+
window.dispatchEvent(new CustomEvent('cxv-focus-terminal'));
|
|
2341
|
+
};
|
|
2342
|
+
|
|
2343
|
+
/**
|
|
2344
|
+
* 整体显示缩放收口:state / 原生缩放(webFrame.setZoomFactor)/ localStorage 三处同步。
|
|
2345
|
+
* 仅 Electron 桌面生效——用真·原生缩放(等同浏览器 Cmd/Ctrl +/-),避开 CSS zoom 的坐标空间分裂。
|
|
2346
|
+
* 纯浏览器无 JS API 设原生缩放,该档位不渲染下拉而提示用户用浏览器快捷键,故 hasNativeZoom=false 时早返回。
|
|
2347
|
+
* @param {number} pct 目标百分比
|
|
2348
|
+
* @param {{persistPref?: boolean}} opts persistPref=true 时写回 preferences.json
|
|
2349
|
+
*/
|
|
2350
|
+
_applyDisplayScale = (pct, opts = {}) => {
|
|
2351
|
+
// 「显示大小」仅 Electron 桌面有效——经 webFrame.setZoomFactor 做真·原生缩放(不再用 CSS zoom,
|
|
2352
|
+
// 后者会引发 Chromium 128 标准化 zoom 的坐标空间分裂)。纯浏览器无法用 JS 设原生缩放,该档位
|
|
2353
|
+
// 不渲染下拉、改提示用户用浏览器快捷键,故这里直接早返回。
|
|
2354
|
+
if (!hasNativeZoom) return;
|
|
2355
|
+
const { persistPref = false } = opts;
|
|
2356
|
+
const scale = snapToPreset(pct);
|
|
2357
|
+
if (this.state.displayScale !== scale) this.setState({ displayScale: scale });
|
|
2358
|
+
try { window.tabBridge.setZoomFactor(scale / 100); } catch {}
|
|
2359
|
+
try { localStorage.setItem('cxv_displayScale', String(scale)); } catch {}
|
|
2360
|
+
if (persistPref) this.context.updatePreferences({ displayScale: scale });
|
|
2361
|
+
};
|
|
2362
|
+
|
|
2363
|
+
handleDisplayScaleChange = (pct) => {
|
|
2364
|
+
this._applyDisplayScale(pct, { persistPref: true });
|
|
2365
|
+
};
|
|
2366
|
+
|
|
2367
|
+
// 全局键盘缩放:Cmd/Ctrl + "+"/"-" 步进,Cmd/Ctrl + 0 复位 100%。
|
|
2368
|
+
// 行为对齐 Chrome —— 即便焦点在输入框内也生效。stored ref 以便 unmount 卸载。
|
|
2369
|
+
_onScaleKeydown = (e) => {
|
|
2370
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
|
|
2371
|
+
if (isMobile && !isPad) return;
|
|
2372
|
+
const key = e.key;
|
|
2373
|
+
const code = e.code;
|
|
2374
|
+
let next = null;
|
|
2375
|
+
if (key === '=' || key === '+' || code === 'NumpadAdd') {
|
|
2376
|
+
next = stepPreset(this.state.displayScale, +1);
|
|
2377
|
+
} else if (key === '-' || key === '_' || code === 'NumpadSubtract') {
|
|
2378
|
+
next = stepPreset(this.state.displayScale, -1);
|
|
2379
|
+
} else if (key === '0' || code === 'Numpad0') {
|
|
2380
|
+
next = 100;
|
|
2381
|
+
}
|
|
2382
|
+
if (next === null) return;
|
|
2383
|
+
e.preventDefault();
|
|
2384
|
+
this.handleDisplayScaleChange(next);
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
handleLogDirChange = (value) => {
|
|
2388
|
+
if (!value || typeof value !== 'string') return;
|
|
2389
|
+
const trimmed = value.trim();
|
|
2390
|
+
if (!trimmed) return;
|
|
2391
|
+
this.setState({ logDir: trimmed });
|
|
2392
|
+
// logDir 服务端可能 normalize 后回写,read response.logDir 覆盖本地
|
|
2393
|
+
this.context.updatePreferences({ logDir: trimmed }).then(data => {
|
|
2394
|
+
if (data && data.logDir) this.setState({ logDir: data.logDir });
|
|
2395
|
+
});
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
handleShowFullToolContentChange = (checked) => {
|
|
2399
|
+
this.context.updatePreferences({ showFullToolContent: checked });
|
|
2400
|
+
};
|
|
2401
|
+
|
|
2402
|
+
handleOnlyCurrentSessionChange = (checked) => {
|
|
2403
|
+
this.context.updatePreferences({ onlyCurrentSession: checked });
|
|
2404
|
+
};
|
|
2405
|
+
|
|
2406
|
+
handleFilterIrrelevantChange = (checked) => {
|
|
2407
|
+
this.setState(prev => {
|
|
2408
|
+
const newShowAll = !checked;
|
|
2409
|
+
const newFiltered = visibleRequests(prev.requests, newShowAll);
|
|
2410
|
+
return {
|
|
2411
|
+
showAll: newShowAll,
|
|
2412
|
+
selectedIndex: newFiltered.length > 0 ? newFiltered.length - 1 : null,
|
|
2413
|
+
};
|
|
2414
|
+
});
|
|
2415
|
+
this.context.updatePreferences({ filterIrrelevant: checked });
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
// ─── 日志管理 ──────────────────────────────────────────
|
|
2419
|
+
|
|
2420
|
+
// 集中构造 /api/local-logs URL:「显示全部实例」开启时带 ?all=1。打开弹窗 / 刷新统计 /
|
|
2421
|
+
// 合并·归档·删除后这几处 refetch 都经此 helper,开关一改即自动跟随(apiUrl 的 token 追加已正确处理已有 ?)。
|
|
2422
|
+
_localLogsUrl() {
|
|
2423
|
+
return apiUrl(this.state.logShowAllInstances ? '/api/local-logs?all=1' : '/api/local-logs');
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
handleImportLocalLogs = () => {
|
|
2427
|
+
this.setState({ importModalVisible: true, localLogsLoading: true });
|
|
2428
|
+
fetch(this._localLogsUrl())
|
|
2429
|
+
.then(res => res.json())
|
|
2430
|
+
.then(data => {
|
|
2431
|
+
const { _currentProject, ...logs } = data;
|
|
2432
|
+
this.setState({ localLogs: logs, currentProject: _currentProject || '', localLogsLoading: false });
|
|
2433
|
+
})
|
|
2434
|
+
.catch(() => {
|
|
2435
|
+
this.setState({ localLogs: {}, localLogsLoading: false });
|
|
2436
|
+
});
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
// 「显示全部实例」开关:翻转后用新 scope 重新拉取列表(开关状态持久,不随弹窗关闭重置)。
|
|
2440
|
+
handleToggleShowAllLogs = () => {
|
|
2441
|
+
this.setState(prev => ({ logShowAllInstances: !prev.logShowAllInstances }), () => this.handleImportLocalLogs());
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
handleCloseImportModal = () => {
|
|
2445
|
+
this.setState({ importModalVisible: false, selectedLogs: new Set() });
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
handleRefreshStats = () => {
|
|
2449
|
+
this.setState({ refreshingStats: true });
|
|
2450
|
+
fetch(apiUrl('/api/refresh-stats'), { method: 'POST' })
|
|
2451
|
+
.then(res => res.json())
|
|
2452
|
+
.then(data => {
|
|
2453
|
+
if (!data.ok) throw new Error(data.error || 'refresh failed');
|
|
2454
|
+
return fetch(this._localLogsUrl());
|
|
2455
|
+
})
|
|
2456
|
+
.then(res => res.json())
|
|
2457
|
+
.then(data => {
|
|
2458
|
+
const { _currentProject, ...logs } = data;
|
|
2459
|
+
this.setState({ localLogs: logs, refreshingStats: false });
|
|
2460
|
+
message.success(t('ui.refreshStatsSuccess'));
|
|
2461
|
+
})
|
|
2462
|
+
.catch(() => {
|
|
2463
|
+
this.setState({ refreshingStats: false });
|
|
2464
|
+
message.error(t('ui.refreshStatsFailed'));
|
|
2465
|
+
});
|
|
2466
|
+
};
|
|
2467
|
+
|
|
2468
|
+
renderLogTable(logs, mobile) {
|
|
2469
|
+
return (
|
|
2470
|
+
<LogTable
|
|
2471
|
+
logs={logs}
|
|
2472
|
+
mobile={mobile}
|
|
2473
|
+
selectedLogs={this.state.selectedLogs}
|
|
2474
|
+
onToggleSelect={this.handleToggleLogSelect}
|
|
2475
|
+
onOpenLog={this.handleOpenLogFile}
|
|
2476
|
+
onDownloadLog={this.handleDownloadLogFile}
|
|
2477
|
+
/>
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
handleToggleLogSelect = (file, checked) => {
|
|
2482
|
+
this.setState(prev => {
|
|
2483
|
+
const selectedLogs = new Set(prev.selectedLogs);
|
|
2484
|
+
if (checked) selectedLogs.add(file);
|
|
2485
|
+
else selectedLogs.delete(file);
|
|
2486
|
+
return { selectedLogs };
|
|
2487
|
+
});
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
handleMergeLogs = () => {
|
|
2491
|
+
const { selectedLogs, localLogs, currentProject } = this.state;
|
|
2492
|
+
if (selectedLogs.size < 2) return;
|
|
2493
|
+
|
|
2494
|
+
const logs = localLogs[currentProject];
|
|
2495
|
+
if (!logs) return;
|
|
2496
|
+
|
|
2497
|
+
const indices = [];
|
|
2498
|
+
logs.forEach((log, i) => {
|
|
2499
|
+
if (selectedLogs.has(log.file)) indices.push(i);
|
|
2500
|
+
});
|
|
2501
|
+
indices.sort((a, b) => a - b);
|
|
2502
|
+
|
|
2503
|
+
if (selectedLogs.has(logs[0].file)) {
|
|
2504
|
+
message.warning(t('ui.mergeLatestNotAllowed'));
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
for (let i = 1; i < indices.length; i++) {
|
|
2509
|
+
if (indices[i] - indices[i - 1] !== 1) {
|
|
2510
|
+
message.warning(t('ui.mergeNotConsecutive'));
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
const totalSize = indices.reduce((sum, i) => sum + logs[i].size, 0);
|
|
2516
|
+
if (totalSize > 400 * 1024 * 1024) {
|
|
2517
|
+
message.warning(t('ui.mergeTooLarge'));
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
const files = indices.map(i => logs[i].file).reverse();
|
|
2522
|
+
|
|
2523
|
+
fetch(apiUrl('/api/merge-logs'), {
|
|
2524
|
+
method: 'POST',
|
|
2525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2526
|
+
body: JSON.stringify({ files }),
|
|
2527
|
+
})
|
|
2528
|
+
.then(res => res.json())
|
|
2529
|
+
.then(data => {
|
|
2530
|
+
if (data.ok) {
|
|
2531
|
+
message.success(t('ui.mergeSuccess'));
|
|
2532
|
+
this.setState({ selectedLogs: new Set() });
|
|
2533
|
+
this.handleImportLocalLogs();
|
|
2534
|
+
} else {
|
|
2535
|
+
message.error(data.error || 'Merge failed');
|
|
2536
|
+
}
|
|
2537
|
+
})
|
|
2538
|
+
.catch(() => message.error('Merge failed'));
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
handleArchiveLogs = () => {
|
|
2542
|
+
const { selectedLogs, localLogs, currentProject } = this.state;
|
|
2543
|
+
if (selectedLogs.size === 0) return;
|
|
2544
|
+
const logs = localLogs[currentProject];
|
|
2545
|
+
if (!logs) return;
|
|
2546
|
+
const latestFile = logs[0]?.file;
|
|
2547
|
+
const candidates = [...selectedLogs].filter(f => f.endsWith('.jsonl') && f !== latestFile);
|
|
2548
|
+
if (candidates.length === 0) {
|
|
2549
|
+
message.warning(t('ui.mergeLatestNotAllowed'));
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
Modal.confirm({
|
|
2554
|
+
title: t('ui.archiveLogs'),
|
|
2555
|
+
content: t('ui.archiveLogsConfirm', { count: candidates.length }),
|
|
2556
|
+
okText: t('ui.archiveLogs'),
|
|
2557
|
+
cancelText: t('ui.cancel'),
|
|
2558
|
+
onOk: () => {
|
|
2559
|
+
fetch(apiUrl('/api/archive-logs'), {
|
|
2560
|
+
method: 'POST',
|
|
2561
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2562
|
+
body: JSON.stringify({ files: candidates }),
|
|
2563
|
+
})
|
|
2564
|
+
.then(res => res.json())
|
|
2565
|
+
.then(data => {
|
|
2566
|
+
const archived = data.archived?.length || 0;
|
|
2567
|
+
const failed = (data.failed?.length || 0) + (data.skipped?.length || 0);
|
|
2568
|
+
if (archived > 0) message.success(t('ui.archiveSuccess', { count: archived }));
|
|
2569
|
+
if (failed > 0) message.error(t('ui.archiveFailed', { count: failed }));
|
|
2570
|
+
this.setState({ selectedLogs: new Set() });
|
|
2571
|
+
this.handleImportLocalLogs();
|
|
2572
|
+
})
|
|
2573
|
+
.catch(() => message.error(t('ui.archiveFailed', { count: candidates.length })));
|
|
2574
|
+
},
|
|
2575
|
+
});
|
|
2576
|
+
};
|
|
2577
|
+
|
|
2578
|
+
handleDeleteLogs = () => {
|
|
2579
|
+
const { selectedLogs } = this.state;
|
|
2580
|
+
if (selectedLogs.size === 0) return;
|
|
2581
|
+
|
|
2582
|
+
Modal.confirm({
|
|
2583
|
+
title: t('ui.deleteLogs'),
|
|
2584
|
+
content: t('ui.deleteLogsConfirm', { count: selectedLogs.size }),
|
|
2585
|
+
okText: t('ui.deleteLogs'),
|
|
2586
|
+
okButtonProps: { danger: true },
|
|
2587
|
+
cancelText: t('ui.cancel'),
|
|
2588
|
+
onOk: () => {
|
|
2589
|
+
const files = [...selectedLogs];
|
|
2590
|
+
fetch(apiUrl('/api/delete-logs'), {
|
|
2591
|
+
method: 'POST',
|
|
2592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2593
|
+
body: JSON.stringify({ files }),
|
|
2594
|
+
})
|
|
2595
|
+
.then(res => res.json())
|
|
2596
|
+
.then(data => {
|
|
2597
|
+
if (data.results) {
|
|
2598
|
+
const deleted = data.results.filter(r => r.ok).length;
|
|
2599
|
+
const failed = data.results.filter(r => r.error).length;
|
|
2600
|
+
if (deleted > 0) message.success(t('ui.deleteSuccess', { count: deleted }));
|
|
2601
|
+
if (failed > 0) message.error(t('ui.deleteFailed', { count: failed }));
|
|
2602
|
+
this.setState({ selectedLogs: new Set() });
|
|
2603
|
+
this.handleImportLocalLogs();
|
|
2604
|
+
}
|
|
2605
|
+
})
|
|
2606
|
+
.catch(() => message.error('Delete failed'));
|
|
2607
|
+
},
|
|
2608
|
+
});
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
handleOpenLogFile = async (file) => {
|
|
2612
|
+
// 优先使用当前 URL 的 token(远程访问时已有);本地访问时从 /api/local-url 获取带 token 的基础 URL
|
|
2613
|
+
let base = `${window.location.protocol}//${window.location.host}${getBasePath()}`;
|
|
2614
|
+
let token = new URLSearchParams(window.location.search).get('token');
|
|
2615
|
+
if (!token) {
|
|
2616
|
+
try {
|
|
2617
|
+
const r = await fetch(apiUrl('/api/local-url'));
|
|
2618
|
+
if (r.ok) {
|
|
2619
|
+
const data = await r.json();
|
|
2620
|
+
if (data.url) { base = data.url.split('?')[0]; token = new URL(data.url).searchParams.get('token'); }
|
|
2621
|
+
}
|
|
2622
|
+
} catch {}
|
|
2623
|
+
}
|
|
2624
|
+
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
|
2625
|
+
window.open(`${base}?logfile=${encodeURIComponent(file)}${tokenParam}`, '_blank');
|
|
2626
|
+
this.setState({ importModalVisible: false });
|
|
2627
|
+
};
|
|
2628
|
+
|
|
2629
|
+
handleDownloadLogFile = (file) => {
|
|
2630
|
+
const url = apiUrl(`/api/download-log?file=${encodeURIComponent(file)}`);
|
|
2631
|
+
const a = document.createElement('a');
|
|
2632
|
+
a.href = url;
|
|
2633
|
+
a.download = '';
|
|
2634
|
+
document.body.appendChild(a);
|
|
2635
|
+
a.click();
|
|
2636
|
+
document.body.removeChild(a);
|
|
2637
|
+
};
|
|
2638
|
+
|
|
2639
|
+
// ─── 恢复会话 ──────────────────────────────────────────
|
|
2640
|
+
|
|
2641
|
+
handleResumeChoice = (choice) => {
|
|
2642
|
+
if (this.state.resumeRememberChoice) {
|
|
2643
|
+
this.setState({ resumeAutoChoice: choice });
|
|
2644
|
+
this.context.updatePreferences({ resumeAutoChoice: choice });
|
|
2645
|
+
}
|
|
2646
|
+
fetch(apiUrl('/api/resume-choice'), {
|
|
2647
|
+
method: 'POST',
|
|
2648
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2649
|
+
body: JSON.stringify({ choice }),
|
|
2650
|
+
}).catch(err => console.error('resume-choice failed:', err));
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
handleResumeAutoChoiceToggle = (enabled) => {
|
|
2654
|
+
const value = enabled ? 'continue' : null;
|
|
2655
|
+
this.setState({ resumeAutoChoice: value });
|
|
2656
|
+
this.context.updatePreferences({ resumeAutoChoice: value });
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
handleResumeAutoChoiceChange = (value) => {
|
|
2660
|
+
this.setState({ resumeAutoChoice: value });
|
|
2661
|
+
this.context.updatePreferences({ resumeAutoChoice: value });
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
_finishLocalLoad = (entries, fileNames) => {
|
|
2665
|
+
if (entries.length === 0) {
|
|
2666
|
+
message.error(t('ui.noLogs'));
|
|
2667
|
+
this.setState({ fileLoading: false, fileLoadingCount: 0 });
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
this.animateLoadingCount(entries.length, () => {
|
|
2671
|
+
this._batchSlim(entries);
|
|
2672
|
+
const { mainAgentSessions, filtered } = this._processEntries(entries);
|
|
2673
|
+
this._isLocalLog = true;
|
|
2674
|
+
this._localLogFile = fileNames.length === 1 ? fileNames[0] : `${fileNames.length} files`;
|
|
2675
|
+
this._hasMoreHistory = false;
|
|
2676
|
+
this._oldestTs = null;
|
|
2677
|
+
if (this.eventSource) { this.eventSource.close(); this.eventSource = null; }
|
|
2678
|
+
if (this._streamingOffTimer) { clearTimeout(this._streamingOffTimer); this._streamingOffTimer = null; }
|
|
2679
|
+
this.setState({
|
|
2680
|
+
requests: entries,
|
|
2681
|
+
selectedIndex: filtered.length > 0 ? filtered.length - 1 : null,
|
|
2682
|
+
mainAgentSessions,
|
|
2683
|
+
importModalVisible: false,
|
|
2684
|
+
fileLoading: false,
|
|
2685
|
+
fileLoadingCount: 0,
|
|
2686
|
+
hasMoreHistory: false,
|
|
2687
|
+
});
|
|
2688
|
+
});
|
|
2689
|
+
};
|
|
2690
|
+
|
|
2691
|
+
// ─── 拖拽上传(App / Mobile 共享)─────────────────────────
|
|
2692
|
+
// 文件拖入窗口 → 上传 → 落入 pendingUploadPaths。子类用 _captureDropContext()/
|
|
2693
|
+
// _dispatchUploadedFiles() 两个 prototype 钩子定制分发(Mobile 按终端可见性分流)。
|
|
2694
|
+
_isInternalDrag = (e) => e.dataTransfer.types.includes('text/x-preset-reorder');
|
|
2695
|
+
|
|
2696
|
+
_onDragOver = (e) => {
|
|
2697
|
+
e.preventDefault();
|
|
2698
|
+
if (this._isInternalDrag(e)) return;
|
|
2699
|
+
// FileExplorer 区域不显示全屏 overlay,由 FileExplorer 自己处理外部拖入反馈
|
|
2700
|
+
const overFileExplorer = e.target.closest && e.target.closest('[data-file-explorer]');
|
|
2701
|
+
if (overFileExplorer) {
|
|
2702
|
+
if (this.state.isDragging) this.setState({ isDragging: false });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
if (!this.state.isDragging) this.setState({ isDragging: true });
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
_onDragLeave = (e) => {
|
|
2709
|
+
const layout = this._layoutRef.current;
|
|
2710
|
+
if (layout && !layout.contains(e.relatedTarget)) {
|
|
2711
|
+
this.setState({ isDragging: false });
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
|
|
2715
|
+
_onDrop = (e) => {
|
|
2716
|
+
e.preventDefault();
|
|
2717
|
+
if (this._isInternalDrag(e)) return;
|
|
2718
|
+
this.setState({ isDragging: false });
|
|
2719
|
+
const files = Array.from(e.dataTransfer.files);
|
|
2720
|
+
if (!files.length) return;
|
|
2721
|
+
// drop 时刻同步捕获分发上下文(Mobile 需要 mobileTerminalVisible 的当时值,非上传完成后的值)
|
|
2722
|
+
const ctx = this._captureDropContext();
|
|
2723
|
+
// 拖拽上传在途占位:每文件登记 {id,name}(spinner-only,不建 objectURL 以免跨组件 revoke 竞态)。
|
|
2724
|
+
// ChatView 据 uploadingDrop 调谐占位 + 缓发,使图不漏发。
|
|
2725
|
+
const items = files.map(file => ({ id: `drop-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, name: file.name }));
|
|
2726
|
+
this.setState(prev => ({ uploadingDrop: [...(prev.uploadingDrop || []), ...items] }));
|
|
2727
|
+
Promise.all(
|
|
2728
|
+
files.map(file =>
|
|
2729
|
+
uploadFileAndGetPath(file).then(path => ({ name: file.name, path }))
|
|
2730
|
+
.catch(err => { message.error(`${file.name}: ${err.message}`); return null; })
|
|
2731
|
+
)
|
|
2732
|
+
).then(results => {
|
|
2733
|
+
// 关键顺序:先派发已 resolve 路径(落 pendingUploadPaths→pendingImages),再在同一 .then(同 tick,
|
|
2734
|
+
// React 批处理合并)清掉本批占位 —— 保证 ChatView 的 drain 重发在同一次渲染里读到已就绪的 pendingImages,
|
|
2735
|
+
// 不会出现「占位先清空、路径还没到 → drain 发出纯文字」的拖拽路径丢图竞态。
|
|
2736
|
+
this._dispatchUploadedFiles(results, ctx);
|
|
2737
|
+
const ids = new Set(items.map(i => i.id));
|
|
2738
|
+
this.setState(prev => ({ uploadingDrop: (prev.uploadingDrop || []).filter(d => !ids.has(d.id)) }));
|
|
2739
|
+
});
|
|
2740
|
+
};
|
|
2741
|
+
|
|
2742
|
+
// 子类可 override(prototype 方法)。默认=桌面行为:全落入 pendingUploadPaths。
|
|
2743
|
+
_captureDropContext() { return undefined; }
|
|
2744
|
+
|
|
2745
|
+
_dispatchUploadedFiles(results) {
|
|
2746
|
+
const paths = results.filter(Boolean).map(r => `"${r.path}"`);
|
|
2747
|
+
if (paths.length > 0) {
|
|
2748
|
+
this.setState(prev => ({
|
|
2749
|
+
pendingUploadPaths: [...(prev.pendingUploadPaths || []), ...paths],
|
|
2750
|
+
}));
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
handleUploadPathsConsumed = () => {
|
|
2755
|
+
this.setState({ pendingUploadPaths: [] });
|
|
2756
|
+
};
|
|
2757
|
+
|
|
2758
|
+
// ─── 共享渲染辅助 ─────────────────────────────────────
|
|
2759
|
+
|
|
2760
|
+
/** render() 前置计算,子类在 render 开头调用 */
|
|
2761
|
+
renderPrepare() {
|
|
2762
|
+
const { requests, selectedIndex, showAll, fileLoading, fileLoadingCount, mainAgentSessions, viewMode } = this.state;
|
|
2763
|
+
|
|
2764
|
+
// 过滤心跳请求
|
|
2765
|
+
if (this._filteredSource !== requests || this._filteredShowAll !== showAll) {
|
|
2766
|
+
this._filteredSource = requests;
|
|
2767
|
+
this._filteredShowAll = showAll;
|
|
2768
|
+
this._filteredRequests = visibleRequests(requests, showAll);
|
|
2769
|
+
}
|
|
2770
|
+
const filteredRequests = this._filteredRequests;
|
|
2771
|
+
|
|
2772
|
+
const selectedRequest = selectedIndex !== null ? filteredRequests[selectedIndex] : null;
|
|
2773
|
+
|
|
2774
|
+
return { filteredRequests, selectedRequest, fileLoading, fileLoadingCount, mainAgentSessions, viewMode };
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
/** 工作区选择器渲染(PC/Mobile 共用) */
|
|
2778
|
+
renderWorkspaceMode() {
|
|
2779
|
+
return (
|
|
2780
|
+
<ConfigProvider theme={this.themeConfig}>
|
|
2781
|
+
<WorkspaceList onLaunch={this.handleWorkspaceLaunch} />
|
|
2782
|
+
</ConfigProvider>
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/** Ant Design 主题配置 (dark/light)
|
|
2787
|
+
*
|
|
2788
|
+
* 历史尝试 `cssVar: true`(antd 5.14+)想砍 useToken/useGlobalCache 开销,但实测是性能
|
|
2789
|
+
* 负优化:trace3 vs trace2 显示 cssinjs 自身耗时 +170%,`flattenToken` +1426%,GC +56%,
|
|
2790
|
+
* 主线程 idle 从 16% 崩到 0.5%,dropped frames +64%。原因:启用 cssVar 后每个 token 多走
|
|
2791
|
+
* 一层 CSSVarRegister.path + flattenToken;4 处 ConfigProvider + 主题切换 + 大量 antd
|
|
2792
|
+
* 组件叠加,cache miss 路径被放大。antd 文档宣传的 20-35% 收益建立在「单 ConfigProvider
|
|
2793
|
+
* + 主题不切换」理想场景,本仓库不符合。结论:保持 hash style,不要开 cssVar。
|
|
2794
|
+
*
|
|
2795
|
+
* 引用稳定性:返回模块顶层冻结常量(LIGHT_THEME_CONFIG / DARK_THEME_CONFIG),
|
|
2796
|
+
* 主题不变时 React 每次 render 都拿到同一引用 → cssinjs useTheme useMemo 真正命中。
|
|
2797
|
+
*/
|
|
2798
|
+
get themeConfig() {
|
|
2799
|
+
return this.state.themeColor === 'light' ? LIGHT_THEME_CONFIG : DARK_THEME_CONFIG;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
export default AppBase;
|