cx-viewer 1.0.3 → 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.
Files changed (1440) hide show
  1. package/README.md +49 -20
  2. package/build.js +15 -0
  3. package/cli.js +102 -18
  4. package/concepts/ar/BodyDiffJSON.md +1 -1
  5. package/concepts/ar/BodyFields.md +30 -33
  6. package/concepts/ar/GlobalSettings.md +165 -167
  7. package/concepts/ar/MainAgent.md +3 -4
  8. package/concepts/ar/ProxySwitch.md +3 -3
  9. package/concepts/ar/QRCode.md +1 -1
  10. package/concepts/ar/ResponseFields.md +29 -41
  11. package/concepts/ar/SubAgent-Search.md +12 -12
  12. package/concepts/ar/Teammate.md +16 -16
  13. package/concepts/ar/ToolsFirst.md +42 -48
  14. package/concepts/ar/TranslateContextPollution.md +9 -9
  15. package/concepts/ar/UltraPlan.md +38 -38
  16. package/concepts/da/BodyDiffJSON.md +2 -2
  17. package/concepts/da/BodyFields.md +30 -33
  18. package/concepts/da/GlobalSettings.md +165 -167
  19. package/concepts/da/MainAgent.md +3 -4
  20. package/concepts/da/ProxySwitch.md +3 -3
  21. package/concepts/da/QRCode.md +1 -1
  22. package/concepts/da/ResponseFields.md +29 -41
  23. package/concepts/da/SubAgent-Search.md +11 -11
  24. package/concepts/da/Teammate.md +16 -16
  25. package/concepts/da/ToolsFirst.md +42 -48
  26. package/concepts/da/TranslateContextPollution.md +9 -9
  27. package/concepts/da/UltraPlan.md +38 -38
  28. package/concepts/de/BodyDiffJSON.md +2 -2
  29. package/concepts/de/BodyFields.md +30 -33
  30. package/concepts/de/GlobalSettings.md +165 -167
  31. package/concepts/de/MainAgent.md +3 -4
  32. package/concepts/de/ProxySwitch.md +3 -3
  33. package/concepts/de/QRCode.md +1 -1
  34. package/concepts/de/ResponseFields.md +29 -41
  35. package/concepts/de/SubAgent-Search.md +11 -11
  36. package/concepts/de/Teammate.md +15 -15
  37. package/concepts/de/ToolsFirst.md +41 -47
  38. package/concepts/de/TranslateContextPollution.md +9 -9
  39. package/concepts/de/UltraPlan.md +38 -38
  40. package/concepts/en/BodyDiffJSON.md +5 -5
  41. package/concepts/en/BodyFields.md +17 -20
  42. package/concepts/en/GlobalSettings.md +49 -51
  43. package/concepts/en/MainAgent.md +8 -8
  44. package/concepts/en/ProxySwitch.md +3 -3
  45. package/concepts/en/QRCode.md +6 -6
  46. package/concepts/en/ResponseFields.md +7 -19
  47. package/concepts/en/SubAgent-Search.md +2 -2
  48. package/concepts/en/Teammate.md +4 -4
  49. package/concepts/en/Tool-apply_patch.md +11 -0
  50. package/concepts/en/Tool-create_goal.md +8 -0
  51. package/concepts/en/Tool-exec.md +88 -0
  52. package/concepts/en/Tool-followup_task.md +10 -0
  53. package/concepts/en/Tool-get_goal.md +5 -0
  54. package/concepts/en/Tool-image_generation.md +5 -0
  55. package/concepts/en/Tool-interrupt_agent.md +9 -0
  56. package/concepts/en/Tool-list_agents.md +9 -0
  57. package/concepts/en/Tool-list_mcp_resource_templates.md +8 -0
  58. package/concepts/en/Tool-list_mcp_resources.md +8 -0
  59. package/concepts/en/Tool-read_mcp_resource.md +10 -0
  60. package/concepts/en/Tool-request_user_input.md +9 -0
  61. package/concepts/en/Tool-send_message.md +10 -0
  62. package/concepts/en/Tool-shell_command.md +12 -0
  63. package/concepts/en/Tool-spawn_agent.md +11 -0
  64. package/concepts/en/Tool-tool_search.md +12 -0
  65. package/concepts/en/Tool-update_goal.md +9 -0
  66. package/concepts/en/Tool-update_plan.md +9 -0
  67. package/concepts/en/Tool-view_image.md +10 -0
  68. package/concepts/en/Tool-wait.md +12 -0
  69. package/concepts/en/Tool-wait_agent.md +9 -0
  70. package/concepts/en/Tool-web_search.md +11 -0
  71. package/concepts/en/Tools.md +29 -58
  72. package/concepts/en/ToolsFirst.md +24 -30
  73. package/concepts/en/TranslateContextPollution.md +10 -38
  74. package/concepts/en/UltraPlan.md +7 -7
  75. package/concepts/es/BodyDiffJSON.md +1 -1
  76. package/concepts/es/BodyFields.md +35 -38
  77. package/concepts/es/GlobalSettings.md +165 -167
  78. package/concepts/es/MainAgent.md +3 -4
  79. package/concepts/es/ProxySwitch.md +3 -3
  80. package/concepts/es/QRCode.md +1 -1
  81. package/concepts/es/ResponseFields.md +28 -40
  82. package/concepts/es/SubAgent-Search.md +12 -12
  83. package/concepts/es/Teammate.md +17 -17
  84. package/concepts/es/ToolsFirst.md +41 -47
  85. package/concepts/es/TranslateContextPollution.md +9 -9
  86. package/concepts/es/UltraPlan.md +38 -38
  87. package/concepts/fr/BodyDiffJSON.md +2 -2
  88. package/concepts/fr/BodyFields.md +35 -38
  89. package/concepts/fr/GlobalSettings.md +165 -167
  90. package/concepts/fr/MainAgent.md +3 -4
  91. package/concepts/fr/ProxySwitch.md +3 -3
  92. package/concepts/fr/QRCode.md +1 -1
  93. package/concepts/fr/ResponseFields.md +25 -37
  94. package/concepts/fr/SubAgent-Search.md +12 -12
  95. package/concepts/fr/Teammate.md +16 -16
  96. package/concepts/fr/ToolsFirst.md +40 -46
  97. package/concepts/fr/TranslateContextPollution.md +9 -9
  98. package/concepts/fr/UltraPlan.md +38 -38
  99. package/concepts/it/BodyDiffJSON.md +2 -2
  100. package/concepts/it/BodyFields.md +35 -38
  101. package/concepts/it/GlobalSettings.md +165 -167
  102. package/concepts/it/MainAgent.md +3 -4
  103. package/concepts/it/ProxySwitch.md +3 -3
  104. package/concepts/it/QRCode.md +1 -1
  105. package/concepts/it/ResponseFields.md +28 -40
  106. package/concepts/it/SubAgent-Search.md +12 -12
  107. package/concepts/it/Teammate.md +17 -17
  108. package/concepts/it/ToolsFirst.md +41 -47
  109. package/concepts/it/TranslateContextPollution.md +9 -9
  110. package/concepts/it/UltraPlan.md +38 -38
  111. package/concepts/ja/BodyDiffJSON.md +1 -1
  112. package/concepts/ja/BodyFields.md +30 -33
  113. package/concepts/ja/GlobalSettings.md +165 -167
  114. package/concepts/ja/MainAgent.md +3 -4
  115. package/concepts/ja/ProxySwitch.md +3 -3
  116. package/concepts/ja/QRCode.md +1 -1
  117. package/concepts/ja/ResponseFields.md +29 -41
  118. package/concepts/ja/SubAgent-Search.md +12 -12
  119. package/concepts/ja/Teammate.md +17 -17
  120. package/concepts/ja/ToolsFirst.md +41 -47
  121. package/concepts/ja/TranslateContextPollution.md +9 -9
  122. package/concepts/ja/UltraPlan.md +38 -38
  123. package/concepts/ko/BodyDiffJSON.md +1 -1
  124. package/concepts/ko/BodyFields.md +30 -33
  125. package/concepts/ko/GlobalSettings.md +165 -167
  126. package/concepts/ko/MainAgent.md +3 -4
  127. package/concepts/ko/ProxySwitch.md +3 -3
  128. package/concepts/ko/QRCode.md +1 -1
  129. package/concepts/ko/ResponseFields.md +29 -41
  130. package/concepts/ko/SubAgent-Search.md +12 -12
  131. package/concepts/ko/Teammate.md +17 -17
  132. package/concepts/ko/ToolsFirst.md +41 -47
  133. package/concepts/ko/TranslateContextPollution.md +9 -9
  134. package/concepts/ko/UltraPlan.md +38 -38
  135. package/concepts/no/BodyDiffJSON.md +2 -2
  136. package/concepts/no/BodyFields.md +30 -33
  137. package/concepts/no/GlobalSettings.md +165 -167
  138. package/concepts/no/MainAgent.md +3 -4
  139. package/concepts/no/ProxySwitch.md +3 -3
  140. package/concepts/no/QRCode.md +1 -1
  141. package/concepts/no/ResponseFields.md +29 -41
  142. package/concepts/no/SubAgent-Search.md +12 -12
  143. package/concepts/no/Teammate.md +17 -17
  144. package/concepts/no/ToolsFirst.md +42 -48
  145. package/concepts/no/TranslateContextPollution.md +9 -9
  146. package/concepts/no/UltraPlan.md +38 -38
  147. package/concepts/pl/BodyDiffJSON.md +2 -2
  148. package/concepts/pl/BodyFields.md +30 -33
  149. package/concepts/pl/GlobalSettings.md +165 -167
  150. package/concepts/pl/MainAgent.md +3 -4
  151. package/concepts/pl/ProxySwitch.md +3 -3
  152. package/concepts/pl/QRCode.md +1 -1
  153. package/concepts/pl/ResponseFields.md +29 -41
  154. package/concepts/pl/SubAgent-Search.md +12 -12
  155. package/concepts/pl/Teammate.md +17 -17
  156. package/concepts/pl/ToolsFirst.md +42 -48
  157. package/concepts/pl/TranslateContextPollution.md +9 -9
  158. package/concepts/pl/UltraPlan.md +38 -38
  159. package/concepts/pt-BR/BodyDiffJSON.md +2 -2
  160. package/concepts/pt-BR/BodyFields.md +30 -33
  161. package/concepts/pt-BR/GlobalSettings.md +165 -167
  162. package/concepts/pt-BR/MainAgent.md +3 -4
  163. package/concepts/pt-BR/ProxySwitch.md +3 -3
  164. package/concepts/pt-BR/QRCode.md +1 -1
  165. package/concepts/pt-BR/ResponseFields.md +29 -41
  166. package/concepts/pt-BR/SubAgent-Search.md +12 -12
  167. package/concepts/pt-BR/Teammate.md +17 -17
  168. package/concepts/pt-BR/ToolsFirst.md +41 -47
  169. package/concepts/pt-BR/TranslateContextPollution.md +9 -9
  170. package/concepts/pt-BR/UltraPlan.md +38 -38
  171. package/concepts/ru/BodyDiffJSON.md +1 -1
  172. package/concepts/ru/BodyFields.md +30 -33
  173. package/concepts/ru/GlobalSettings.md +165 -167
  174. package/concepts/ru/MainAgent.md +3 -4
  175. package/concepts/ru/ProxySwitch.md +3 -3
  176. package/concepts/ru/QRCode.md +1 -1
  177. package/concepts/ru/ResponseFields.md +29 -41
  178. package/concepts/ru/SubAgent-Search.md +12 -12
  179. package/concepts/ru/Teammate.md +17 -17
  180. package/concepts/ru/ToolsFirst.md +41 -47
  181. package/concepts/ru/TranslateContextPollution.md +9 -9
  182. package/concepts/ru/UltraPlan.md +38 -38
  183. package/concepts/th/BodyDiffJSON.md +1 -1
  184. package/concepts/th/BodyFields.md +30 -33
  185. package/concepts/th/GlobalSettings.md +165 -167
  186. package/concepts/th/MainAgent.md +3 -4
  187. package/concepts/th/ProxySwitch.md +3 -3
  188. package/concepts/th/QRCode.md +1 -1
  189. package/concepts/th/ResponseFields.md +29 -41
  190. package/concepts/th/SubAgent-Search.md +12 -12
  191. package/concepts/th/Teammate.md +17 -17
  192. package/concepts/th/ToolsFirst.md +42 -48
  193. package/concepts/th/TranslateContextPollution.md +9 -9
  194. package/concepts/th/UltraPlan.md +38 -38
  195. package/concepts/tr/BodyDiffJSON.md +1 -1
  196. package/concepts/tr/BodyFields.md +30 -33
  197. package/concepts/tr/GlobalSettings.md +165 -167
  198. package/concepts/tr/MainAgent.md +3 -4
  199. package/concepts/tr/ProxySwitch.md +3 -3
  200. package/concepts/tr/QRCode.md +1 -1
  201. package/concepts/tr/ResponseFields.md +29 -41
  202. package/concepts/tr/SubAgent-Search.md +12 -12
  203. package/concepts/tr/Teammate.md +16 -16
  204. package/concepts/tr/ToolsFirst.md +42 -48
  205. package/concepts/tr/TranslateContextPollution.md +9 -9
  206. package/concepts/tr/UltraPlan.md +38 -38
  207. package/concepts/uk/BodyDiffJSON.md +1 -1
  208. package/concepts/uk/BodyFields.md +30 -33
  209. package/concepts/uk/GlobalSettings.md +165 -167
  210. package/concepts/uk/MainAgent.md +3 -4
  211. package/concepts/uk/ProxySwitch.md +3 -3
  212. package/concepts/uk/QRCode.md +1 -1
  213. package/concepts/uk/ResponseFields.md +29 -41
  214. package/concepts/uk/SubAgent-Search.md +12 -12
  215. package/concepts/uk/Teammate.md +17 -17
  216. package/concepts/uk/ToolsFirst.md +42 -48
  217. package/concepts/uk/TranslateContextPollution.md +9 -9
  218. package/concepts/uk/UltraPlan.md +38 -38
  219. package/concepts/zh/BodyDiffJSON.md +4 -4
  220. package/concepts/zh/BodyFields.md +17 -20
  221. package/concepts/zh/GlobalSettings.md +26 -28
  222. package/concepts/zh/MainAgent.md +6 -6
  223. package/concepts/zh/ProxySwitch.md +1 -1
  224. package/concepts/zh/ResponseFields.md +7 -18
  225. package/concepts/zh/SubAgent-Search.md +2 -2
  226. package/concepts/zh/Teammate.md +4 -4
  227. package/concepts/zh/Tool-apply_patch.md +11 -0
  228. package/concepts/zh/Tool-create_goal.md +8 -0
  229. package/concepts/zh/Tool-exec.md +88 -0
  230. package/concepts/zh/Tool-followup_task.md +10 -0
  231. package/concepts/zh/Tool-get_goal.md +5 -0
  232. package/concepts/zh/Tool-image_generation.md +5 -0
  233. package/concepts/zh/Tool-interrupt_agent.md +9 -0
  234. package/concepts/zh/Tool-list_agents.md +9 -0
  235. package/concepts/zh/Tool-list_mcp_resource_templates.md +8 -0
  236. package/concepts/zh/Tool-list_mcp_resources.md +8 -0
  237. package/concepts/zh/Tool-read_mcp_resource.md +10 -0
  238. package/concepts/zh/Tool-request_user_input.md +9 -0
  239. package/concepts/zh/Tool-send_message.md +10 -0
  240. package/concepts/zh/Tool-shell_command.md +12 -0
  241. package/concepts/zh/Tool-spawn_agent.md +11 -0
  242. package/concepts/zh/Tool-tool_search.md +12 -0
  243. package/concepts/zh/Tool-update_goal.md +9 -0
  244. package/concepts/zh/Tool-update_plan.md +9 -0
  245. package/concepts/zh/Tool-view_image.md +10 -0
  246. package/concepts/zh/Tool-wait.md +12 -0
  247. package/concepts/zh/Tool-wait_agent.md +9 -0
  248. package/concepts/zh/Tool-web_search.md +11 -0
  249. package/concepts/zh/Tools.md +29 -58
  250. package/concepts/zh/ToolsFirst.md +21 -27
  251. package/concepts/zh/TranslateContextPollution.md +10 -38
  252. package/concepts/zh/UltraPlan.md +7 -7
  253. package/concepts/zh-TW/BodyFields.md +27 -30
  254. package/concepts/zh-TW/GlobalSettings.md +26 -28
  255. package/concepts/zh-TW/MainAgent.md +0 -1
  256. package/concepts/zh-TW/ProxySwitch.md +22 -22
  257. package/concepts/zh-TW/ResponseFields.md +29 -40
  258. package/concepts/zh-TW/SubAgent-Search.md +12 -12
  259. package/concepts/zh-TW/Teammate.md +16 -16
  260. package/concepts/zh-TW/ToolsFirst.md +41 -47
  261. package/concepts/zh-TW/UltraPlan.md +38 -38
  262. package/dist/assets/App-BZ9oclBj.js +2 -0
  263. package/dist/assets/App-rWON7PuS.css +1 -0
  264. package/dist/assets/MdxEditorPanel-B4lZ7Z_A.js +1 -0
  265. package/dist/assets/MdxEditorPanel-Da_ceLFo.css +1 -0
  266. package/dist/assets/Mobile-B5V_-6Hc.css +1 -0
  267. package/dist/assets/Mobile-C7Hy-TIM.js +1 -0
  268. package/dist/assets/abnfDiagram-VRR7QNED-DYYppIQV.js +1 -0
  269. package/dist/assets/apl-B8ZmNFqK.js +1 -0
  270. package/dist/assets/arc-BT3um7r4.js +1 -0
  271. package/dist/assets/architectureDiagram-ZJ3FMSHR-uweBqnh-.js +1 -0
  272. package/dist/assets/asciiarmor-CIte6N-S.js +1 -0
  273. package/dist/assets/asn1-Ax1-z1nk.js +1 -0
  274. package/dist/assets/asterisk-CLHf-iRz.js +1 -0
  275. package/dist/assets/blockDiagram-677ZJIJ3-DzeI4GyV.js +1 -0
  276. package/dist/assets/brainfuck-CdLnmYpe.js +1 -0
  277. package/dist/assets/c4Diagram-LMCZKHZV-DBI0K7wS.js +1 -0
  278. package/dist/assets/channel-BrN6yx0e.js +1 -0
  279. package/dist/assets/chunk-2Q5K7J3B-DK9JA8Ct.js +1 -0
  280. package/dist/assets/chunk-32BRIVSS-VNg3Ylzs.js +1 -0
  281. package/dist/assets/chunk-5VM5RSS4-DFhufY9o.js +1 -0
  282. package/dist/assets/chunk-EX3LRPZG-B93MHNBT.js +1 -0
  283. package/dist/assets/chunk-JWPE2WC7-C54OW1iN.js +1 -0
  284. package/dist/assets/chunk-MOJQB5TN-lKhWWIDe.js +1 -0
  285. package/dist/assets/chunk-RYQCIY6F-BK2jDXwB.js +1 -0
  286. package/dist/assets/chunk-V7JOEXUC-aLHBmfOc.js +1 -0
  287. package/dist/assets/chunk-VR4S4FIN-Bomuor1h.js +1 -0
  288. package/dist/assets/chunk-XXDRQBXY-BPj05P7g.js +1 -0
  289. package/dist/assets/classDiagram-OUVF2IWQ-COm0X4uJ.js +1 -0
  290. package/dist/assets/classDiagram-v2-EOCWNBFH-COm0X4uJ.js +1 -0
  291. package/dist/assets/clike-BDgEuk0n.js +1 -0
  292. package/dist/assets/clojure-CrPxgQZl.js +1 -0
  293. package/dist/assets/cmake-BknZiLBu.js +1 -0
  294. package/dist/assets/cobol-DN1mHbgw.js +1 -0
  295. package/dist/assets/coffeescript-DTAwRC7M.js +1 -0
  296. package/dist/assets/commonlisp-B4DJ--Bo.js +1 -0
  297. package/dist/assets/cose-bilkent-JH36ORCC-DU44QHSb.js +1 -0
  298. package/dist/assets/crystal-DoipYOOv.js +1 -0
  299. package/dist/assets/css-BeQRP3p5.js +1 -0
  300. package/dist/assets/cynefin-VYW2F7L2-C0dowxsF.js +15 -0
  301. package/dist/assets/cynefinDiagram-TSTJHNR4-C6vNWPUV.js +1 -0
  302. package/dist/assets/cypher-BzELKutH.js +1 -0
  303. package/dist/assets/cytoscape.esm-D0POlJZ1.js +9 -0
  304. package/dist/assets/d-Mh2Rawoc.js +1 -0
  305. package/dist/assets/dagre-VKFMJZFB-aLG4uEb-.js +1 -0
  306. package/dist/assets/defaultLocale-DZ7E7S9R.js +1 -0
  307. package/dist/assets/diagram-FQU43EPY-D5C8GfdU.js +1 -0
  308. package/dist/assets/diagram-G47NLZAW-C9tRw_xi.js +1 -0
  309. package/dist/assets/diagram-NH7WQ7WH-AYxTxwJ2.js +1 -0
  310. package/dist/assets/diagram-OA4YK3LP-BuBE5n9n.js +1 -0
  311. package/dist/assets/diagram-WEI45ONY-Cda7zYeO.js +1 -0
  312. package/dist/assets/diff-B1OtBbfz.js +1 -0
  313. package/dist/assets/dockerfile-Du7niAGi.js +1 -0
  314. package/dist/assets/dtd-So5hjSa8.js +1 -0
  315. package/dist/assets/dylan-Co15g-RX.js +1 -0
  316. package/dist/assets/ebnf-DWrAnno4.js +1 -0
  317. package/dist/assets/ebnfDiagram-CCIWWBDH-DC_OGyoh.js +1 -0
  318. package/dist/assets/ecl-8tbf0rUT.js +1 -0
  319. package/dist/assets/eiffel-BXtlUtEg.js +1 -0
  320. package/dist/assets/elm-BwiUluiH.js +1 -0
  321. package/dist/assets/erDiagram-Q63AITRT-D2G21PVY.js +1 -0
  322. package/dist/assets/erlang-CrHBLi-m.js +1 -0
  323. package/dist/assets/factor-C7nC06Ta.js +1 -0
  324. package/dist/assets/fcl-DKWfryFT.js +1 -0
  325. package/dist/assets/flowDiagram-23GEKE2U-CEQPYHrk.js +1 -0
  326. package/dist/assets/forth-Cg8ASoTj.js +1 -0
  327. package/dist/assets/fortran-BHf_qfFx.js +1 -0
  328. package/dist/assets/ganttDiagram-NO4QXBWP-JStxUtFR.js +1 -0
  329. package/dist/assets/gas-cj_2N_z1.js +1 -0
  330. package/dist/assets/gherkin-BiuTaH40.js +1 -0
  331. package/dist/assets/gitGraphDiagram-IHSO6WYX-BiojPH-V.js +1 -0
  332. package/dist/assets/graph-CnJdn47S.js +1 -0
  333. package/dist/assets/groovy-Bq8cXYUU.js +1 -0
  334. package/dist/assets/haskell-1ANuaeie.js +1 -0
  335. package/dist/assets/haxe-BUNSvWf_.js +1 -0
  336. package/dist/assets/html2canvas.esm-HFdlUwfn.js +20 -0
  337. package/dist/assets/http-DPC2IRQA.js +1 -0
  338. package/dist/assets/idl-DDbIciu_.js +1 -0
  339. package/dist/assets/index-BEyKG3O5.css +1 -0
  340. package/dist/assets/index-BduJoLQ5.js +1 -0
  341. package/dist/assets/index-BqwR8F0v.js +1 -0
  342. package/dist/assets/index-BsycQyrv.js +1 -0
  343. package/dist/assets/index-CFSDxZyA.js +1 -0
  344. package/dist/assets/index-CunNLTHc.js +1 -0
  345. package/dist/assets/index-OGkDFNTK.js +1 -0
  346. package/dist/assets/index-oxOyrL07.js +2 -0
  347. package/dist/assets/index-wvaycTGT.js +1 -0
  348. package/dist/assets/infoDiagram-FWYZ7A6U-Csl_aBIC.js +1 -0
  349. package/dist/assets/init-DMZMl1PR.js +1 -0
  350. package/dist/assets/ishikawaDiagram-FXEZZL3T-CjgGuMwr.js +1 -0
  351. package/dist/assets/javascript-Cet1BUca.js +1 -0
  352. package/dist/assets/journeyDiagram-5HDEW3XC-DUOy1PCq.js +1 -0
  353. package/dist/assets/jszip.min-BxIXdRg4.js +12 -0
  354. package/dist/assets/julia-CdxLCr68.js +1 -0
  355. package/dist/assets/kanban-definition-HUTT4EX6-Cx7GSrSj.js +1 -0
  356. package/dist/assets/katex-CfXf84Fz.js +1 -0
  357. package/dist/assets/layout-CCUE9ZRw.js +1 -0
  358. package/dist/assets/linear-GeBi6jJp.js +1 -0
  359. package/dist/assets/livescript-Bw1Vw7Ae.js +1 -0
  360. package/dist/assets/lua-DM1wbcnc.js +1 -0
  361. package/dist/assets/map-DQeg2hXV.js +1 -0
  362. package/dist/assets/mathematica-BXSv1_O1.js +1 -0
  363. package/dist/assets/mbox-CWY0keg3.js +1 -0
  364. package/dist/assets/mermaid.core-Cy9iVxov.js +7 -0
  365. package/dist/assets/mindmap-definition-LN4V7U3C-CUVxVkK5.js +1 -0
  366. package/dist/assets/mirc-CMLrLVMT.js +1 -0
  367. package/dist/assets/mllike-naqhAFf1.js +1 -0
  368. package/dist/assets/modelica-BvadsaHY.js +1 -0
  369. package/dist/assets/mscgen-DOr-Q10p.js +1 -0
  370. package/dist/assets/mumps-DKTl5Nyj.js +1 -0
  371. package/dist/assets/nginx-DWR90-d1.js +1 -0
  372. package/dist/assets/nsis-ooFGduGn.js +1 -0
  373. package/dist/assets/ntriples-q5jkU4X1.js +1 -0
  374. package/dist/assets/octave-Deu9Ci7_.js +1 -0
  375. package/dist/assets/orbiting-BCD4Vuij.svg +7 -0
  376. package/dist/assets/ordinal-DA-_8pMy.js +1 -0
  377. package/dist/assets/oz-cTQm1aoB.js +1 -0
  378. package/dist/assets/pascal-Oh3VmczP.js +1 -0
  379. package/dist/assets/pegDiagram-2B236MQR-BCF1byLY.js +1 -0
  380. package/dist/assets/perl-BddWF1HQ.js +1 -0
  381. package/dist/assets/pieDiagram-ENE6RG2P-D2cZTo8-.js +1 -0
  382. package/dist/assets/pig-g5ZQNIDr.js +1 -0
  383. package/dist/assets/powershell-CNmQNFVJ.js +1 -0
  384. package/dist/assets/properties-B41DXfv6.js +1 -0
  385. package/dist/assets/protobuf-eEBl31OJ.js +1 -0
  386. package/dist/assets/pug-DRSo4eCK.js +1 -0
  387. package/dist/assets/puppet-ClfRubAD.js +1 -0
  388. package/dist/assets/python-BSflc812.js +1 -0
  389. package/dist/assets/q-BBDx3N9Y.js +1 -0
  390. package/dist/assets/quadrantDiagram-ABIIQ3AL-Bwsn2--F.js +1 -0
  391. package/dist/assets/r-CByC1eO8.js +1 -0
  392. package/dist/assets/railroadDiagram-RFXS5EU6-CLGLpWHs.js +1 -0
  393. package/dist/assets/requirementDiagram-TGXJPOKE-BfmBYwZl.js +1 -0
  394. package/dist/assets/rpm-8t935XkD.js +1 -0
  395. package/dist/assets/ruby-CAgbkLZh.js +1 -0
  396. package/dist/assets/sankeyDiagram-HTMAVEWB-CqZhbyFn.js +1 -0
  397. package/dist/assets/sas-C-JWcRvo.js +1 -0
  398. package/dist/assets/scheme-D7o9RjUX.js +1 -0
  399. package/dist/assets/seqResourceLoaders-Bkh2Rb9i.js +2 -0
  400. package/dist/assets/seqResourceLoaders-Do7Kqg3B.css +41 -0
  401. package/dist/assets/sequenceDiagram-DBY2YBRQ-CcOdXkN2.js +1 -0
  402. package/dist/assets/shell-BXA79DV6.js +1 -0
  403. package/dist/assets/shimmer-BBDejmDf.svg +7 -0
  404. package/dist/assets/sieve-DNwyl0rd.js +1 -0
  405. package/dist/assets/simple-mode-CoQRusFX.js +1 -0
  406. package/dist/assets/sizeCapture-X5ZJPWSS-B4OVmPqu.js +1 -0
  407. package/dist/assets/smalltalk-DggktV6S.js +1 -0
  408. package/dist/assets/solr-D7NRuIPM.js +1 -0
  409. package/dist/assets/sparql-Cr71deJQ.js +1 -0
  410. package/dist/assets/spreadsheet-BMD3tYqw.js +1 -0
  411. package/dist/assets/sql-CbOjXfBR.js +1 -0
  412. package/dist/assets/stateDiagram-2N3HPSRC-Dysun0oO.js +1 -0
  413. package/dist/assets/stateDiagram-v2-6OUMAXLB-BFet8xAz.js +1 -0
  414. package/dist/assets/stex-iDPIkRmr.js +1 -0
  415. package/dist/assets/stylus-nWZizI-c.js +1 -0
  416. package/dist/assets/swift-DOhLK5SG.js +1 -0
  417. package/dist/assets/swimlanes-5IMT3BWC-_30OXwXy.js +2 -0
  418. package/dist/assets/swimlanesDiagram-G3AALYLV-C19viY0G.js +1 -0
  419. package/dist/assets/tcl-CQhJd1WY.js +1 -0
  420. package/dist/assets/textile-D9XqfVtL.js +1 -0
  421. package/dist/assets/tiddlywiki-DfOYgVky.js +1 -0
  422. package/dist/assets/tiki-a0_YLWvV.js +1 -0
  423. package/dist/assets/timeline-definition-FHXFAJF6-X1Xk3onD.js +1 -0
  424. package/dist/assets/toml-ekl3F1YB.js +1 -0
  425. package/dist/assets/troff-Bh_pcXF1.js +1 -0
  426. package/dist/assets/ttcn-M6363IRW.js +1 -0
  427. package/dist/assets/ttcn-cfg-7I2DgJEN.js +1 -0
  428. package/dist/assets/turtle-B5jdxH6D.js +1 -0
  429. package/dist/assets/vb-DM-xPmVv.js +1 -0
  430. package/dist/assets/vbscript-B0qu41Yc.js +1 -0
  431. package/dist/assets/velocity-FCgsuVHf.js +1 -0
  432. package/dist/assets/vendor-antd-Bzi1WuBb.js +17 -0
  433. package/dist/assets/vendor-codemirror-CKTrJd47.js +1 -0
  434. package/dist/assets/vendor-highlight-IFTJ9nxW.js +1 -0
  435. package/dist/assets/vendor-markdown-BN3QLsBu.js +3 -0
  436. package/dist/assets/vendor-mdxeditor-4PbKH6K7.js +2 -0
  437. package/dist/assets/vendor-qrcode-Bnjm0rcu.js +6 -0
  438. package/dist/assets/vendor-react-l0sNRNKZ.js +1 -0
  439. package/dist/assets/vendor-virtuoso-2P4SowHC.js +1 -0
  440. package/dist/assets/vendor-xterm-DckPrCx1.js +37 -0
  441. package/dist/assets/vennDiagram-L72KCM5P-D3fQnGq7.js +1 -0
  442. package/dist/assets/verilog-nrfFvR12.js +1 -0
  443. package/dist/assets/vhdl-WlDXtV-W.js +1 -0
  444. package/dist/assets/wardleyDiagram-EHGQE667-CRhqI6-g.js +1 -0
  445. package/dist/assets/webidl-RIQoSXDx.js +1 -0
  446. package/dist/assets/xquery-BN45G3lJ.js +1 -0
  447. package/dist/assets/xychartDiagram-FW5EYKEG-Cmtgmmtg.js +1 -0
  448. package/dist/assets/yacas-U6gaIVhD.js +1 -0
  449. package/dist/assets/z80-CFe5BHbK.js +1 -0
  450. package/dist/index.html +25 -6
  451. package/dist/voice-packs/default/A_choice_for_your_consideration_sir.MP3 +0 -0
  452. package/dist/voice-packs/default/It_is_done_sir.MP3 +0 -0
  453. package/dist/voice-packs/default/The_plan_awaits_your_approval_sir.MP3 +0 -0
  454. package/dist/voice-packs/default/pack.json +24 -0
  455. package/dist/voice-packs/sanguo/ask.MP3 +0 -0
  456. package/dist/voice-packs/sanguo/end.MP3 +0 -0
  457. package/dist/voice-packs/sanguo/pack.json +24 -0
  458. package/dist/voice-packs/sanguo/plan.MP3 +0 -0
  459. package/electron/diag.js +110 -0
  460. package/electron/main.js +1432 -0
  461. package/electron/menu-model.js +107 -0
  462. package/electron/tab-bar.html +575 -0
  463. package/electron/tab-content-preload.js +66 -0
  464. package/electron/tab-preload.js +24 -0
  465. package/electron/tab-worker.js +142 -0
  466. package/electron/window-state.js +60 -0
  467. package/electron/workspace-preload.js +19 -0
  468. package/findcx.js +4 -0
  469. package/i18n.js +3 -423
  470. package/index.html +30 -0
  471. package/interceptor.js +144 -70
  472. package/jsconfig.json +25 -0
  473. package/lib/approval-reviewer.js +23 -0
  474. package/lib/appserver-bridge.js +1753 -237
  475. package/lib/ask-bridge.js +5 -5
  476. package/lib/cli-args.js +1 -0
  477. package/lib/codex-config.js +157 -18
  478. package/lib/context-watcher.js +6 -65
  479. package/lib/delta-reconstructor.js +74 -52
  480. package/lib/ensure-hooks.js +37 -30
  481. package/lib/interceptor-core.js +490 -43
  482. package/lib/log-watcher.js +3 -8
  483. package/lib/main-agent-entry.js +68 -0
  484. package/lib/openai-body.js +95 -0
  485. package/lib/otel-adapter.js +3 -2
  486. package/lib/otel-receiver.js +1 -2
  487. package/lib/perm-bridge.js +84 -44
  488. package/lib/proxy-env.js +45 -1
  489. package/lib/sdk-adapter.js +241 -51
  490. package/lib/sdk-manager.js +523 -370
  491. package/lib/skills-api.js +364 -0
  492. package/lib/stats-worker.js +21 -21
  493. package/lib/token-usage.js +29 -0
  494. package/lib/updater.js +3 -7
  495. package/package.json +27 -6
  496. package/plugins/.gitkeep +0 -0
  497. package/plugins/README.md +8 -0
  498. package/proxy.js +187 -241
  499. package/pty-manager.js +17 -1
  500. package/public/favicon.ico +0 -0
  501. package/public/voice-packs/default/A_choice_for_your_consideration_sir.MP3 +0 -0
  502. package/public/voice-packs/default/It_is_done_sir.MP3 +0 -0
  503. package/public/voice-packs/default/The_plan_awaits_your_approval_sir.MP3 +0 -0
  504. package/public/voice-packs/default/pack.json +24 -0
  505. package/public/voice-packs/sanguo/ask.MP3 +0 -0
  506. package/public/voice-packs/sanguo/end.MP3 +0 -0
  507. package/public/voice-packs/sanguo/pack.json +24 -0
  508. package/public/voice-packs/sanguo/plan.MP3 +0 -0
  509. package/scripts/check-no-control-bytes.js +130 -0
  510. package/scripts/check-sign-config.sh +85 -0
  511. package/scripts/gen-default-voicepack.js +176 -0
  512. package/scripts/mac-sign.sh +47 -0
  513. package/scripts/npm-pack-dry-run.mjs +20 -0
  514. package/server/lib/approval-modal-prefs.js +72 -0
  515. package/server/lib/context-rules.js +70 -0
  516. package/server/lib/delta-reconstructor.js +403 -0
  517. package/server/lib/tools-xml-formatter.js +91 -0
  518. package/server/lib/voice-pack-events.js +79 -0
  519. package/server.js +420 -86
  520. package/src/App.jsx +638 -0
  521. package/src/App.module.css +1088 -0
  522. package/src/AppBase.jsx +2803 -0
  523. package/src/Mobile.jsx +1348 -0
  524. package/src/components/approval/ApprovalModal.jsx +319 -0
  525. package/src/components/approval/ApprovalModal.module.css +160 -0
  526. package/src/components/approval/ToolApprovalPanel.jsx +111 -0
  527. package/src/components/approval/ToolApprovalPanel.module.css +250 -0
  528. package/src/components/chat/ApprovalPortalContext.js +26 -0
  529. package/src/components/chat/AskQuestionForm.jsx +279 -0
  530. package/src/components/chat/AskTimeoutCountdown.jsx +113 -0
  531. package/src/components/chat/ChatInputBar.jsx +584 -0
  532. package/src/components/chat/ChatInputBar.module.css +656 -0
  533. package/src/components/chat/ChatMessage.jsx +1587 -0
  534. package/src/components/chat/ChatMessage.module.css +1560 -0
  535. package/src/components/chat/ChatView.jsx +3533 -0
  536. package/src/components/chat/ChatView.module.css +418 -0
  537. package/src/components/chat/DiffView.jsx +111 -0
  538. package/src/components/chat/DiffView.module.css +141 -0
  539. package/src/components/chat/RoleFilterBar.jsx +35 -0
  540. package/src/components/chat/RoleFilterBar.module.css +62 -0
  541. package/src/components/chat/controllers/askFlowController.js +789 -0
  542. package/src/components/chat/controllers/permissionController.js +84 -0
  543. package/src/components/chat/controllers/ptyPromptController.js +166 -0
  544. package/src/components/chat/controllers/scrollHighlightController.js +50 -0
  545. package/src/components/chat/controllers/splitDragController.js +135 -0
  546. package/src/components/chat/controllers/toolFileChangeController.js +216 -0
  547. package/src/components/chat/interactionOwnership.js +106 -0
  548. package/src/components/chat/uploadDeferLogic.js +46 -0
  549. package/src/components/common/ConceptHelp.jsx +97 -0
  550. package/src/components/common/ConceptHelp.module.css +151 -0
  551. package/src/components/common/ConfirmRemoveButton.jsx +78 -0
  552. package/src/components/common/CountryFlag.jsx +105 -0
  553. package/src/components/common/CountryFlag.module.css +36 -0
  554. package/src/components/common/DingTalkIcon.jsx +20 -0
  555. package/src/components/common/DiscordIcon.jsx +21 -0
  556. package/src/components/common/FeishuIcon.jsx +23 -0
  557. package/src/components/common/HoverPreviewTip.jsx +62 -0
  558. package/src/components/common/HoverPreviewTip.module.css +21 -0
  559. package/src/components/common/ImageLightbox.jsx +244 -0
  560. package/src/components/common/ImageLightbox.module.css +120 -0
  561. package/src/components/common/MemoryDetailModal.jsx +75 -0
  562. package/src/components/common/OpenFolderIcon.jsx +53 -0
  563. package/src/components/common/OpenFolderIcon.module.css +7 -0
  564. package/src/components/common/PanelResizer.jsx +48 -0
  565. package/src/components/common/PanelResizer.module.css +11 -0
  566. package/src/components/common/QuickAutoApproveRows.jsx +66 -0
  567. package/src/components/common/RefreshIcon.jsx +50 -0
  568. package/src/components/common/RefreshIcon.module.css +26 -0
  569. package/src/components/common/SnapLineOverlay.jsx +47 -0
  570. package/src/components/common/SnapLineOverlay.module.css +31 -0
  571. package/src/components/common/ToolsHelp.jsx +113 -0
  572. package/src/components/common/ToolsHelp.module.css +85 -0
  573. package/src/components/common/WeComIcon.jsx +22 -0
  574. package/src/components/common/quickMenuIcons.jsx +99 -0
  575. package/src/components/common/sharedChrome.module.css +274 -0
  576. package/src/components/dashboard/AppHeader.jsx +2085 -0
  577. package/src/components/dashboard/AppHeader.module.css +676 -0
  578. package/src/components/dashboard/CachePopoverContent.jsx +538 -0
  579. package/src/components/dashboard/CachePopoverContent.module.css +191 -0
  580. package/src/components/dashboard/ContextTab.jsx +769 -0
  581. package/src/components/dashboard/ContextTab.module.css +565 -0
  582. package/src/components/dashboard/DetailPanel.jsx +813 -0
  583. package/src/components/dashboard/DetailPanel.module.css +513 -0
  584. package/src/components/dashboard/LiveTagPopover.jsx +89 -0
  585. package/src/components/dashboard/LiveTagPopover.module.css +67 -0
  586. package/src/components/dashboard/RequestList.jsx +146 -0
  587. package/src/components/dashboard/RequestList.module.css +146 -0
  588. package/src/components/dashboard/TeamSessionPanel.jsx +584 -0
  589. package/src/components/dashboard/TeamSessionPanel.module.css +457 -0
  590. package/src/components/dashboard/UsageWindowPill.jsx +166 -0
  591. package/src/components/dashboard/UsageWindowPill.module.css +140 -0
  592. package/src/components/dashboard/WorkflowRunsPanel.jsx +108 -0
  593. package/src/components/dashboard/WorkflowRunsPanel.module.css +72 -0
  594. package/src/components/dashboard/WorkspaceList.jsx +410 -0
  595. package/src/components/dashboard/WorkspaceList.module.css +254 -0
  596. package/src/components/files/FileContentView.jsx +1013 -0
  597. package/src/components/files/FileContentView.module.css +418 -0
  598. package/src/components/files/FileExplorer.jsx +937 -0
  599. package/src/components/files/FileExplorer.module.css +173 -0
  600. package/src/components/files/MdxEditorPanel.base.css +9 -0
  601. package/src/components/files/MdxEditorPanel.jsx +181 -0
  602. package/src/components/files/MdxEditorPanel.module.css +917 -0
  603. package/src/components/git/DiffMiniMap.jsx +98 -0
  604. package/src/components/git/DiffMiniMap.module.css +17 -0
  605. package/src/components/git/FullFileDiffView.jsx +265 -0
  606. package/src/components/git/FullFileDiffView.module.css +176 -0
  607. package/src/components/git/GitChanges.jsx +461 -0
  608. package/src/components/git/GitChanges.module.css +337 -0
  609. package/src/components/git/GitDiffView.jsx +161 -0
  610. package/src/components/git/GitDiffView.module.css +205 -0
  611. package/src/components/mobile/MobileDrawerCloseButton.jsx +18 -0
  612. package/src/components/mobile/MobileFileExplorer.jsx +231 -0
  613. package/src/components/mobile/MobileFileExplorer.module.css +171 -0
  614. package/src/components/mobile/MobileGitDiff.jsx +293 -0
  615. package/src/components/mobile/MobileGitDiff.module.css +317 -0
  616. package/src/components/mobile/MobileStats.jsx +211 -0
  617. package/src/components/mobile/MobileStats.module.css +75 -0
  618. package/src/components/settings/ImAppendSystemModal.jsx +104 -0
  619. package/src/components/settings/ImConversationModal.jsx +328 -0
  620. package/src/components/settings/ImConversationModal.module.css +49 -0
  621. package/src/components/settings/ImPlatformSettings.jsx +453 -0
  622. package/src/components/settings/ImPlatformSettings.module.css +110 -0
  623. package/src/components/settings/ImSkillsModal.jsx +94 -0
  624. package/src/components/settings/ImStatusChip.jsx +80 -0
  625. package/src/components/settings/ImStatusChip.module.css +36 -0
  626. package/src/components/settings/MessagingModal.jsx +65 -0
  627. package/src/components/settings/MessagingModal.module.css +95 -0
  628. package/src/components/settings/ModelPromptTabs.jsx +118 -0
  629. package/src/components/settings/PluginModal.jsx +423 -0
  630. package/src/components/settings/PluginModal.module.css +136 -0
  631. package/src/components/settings/PreferencesForm.jsx +104 -0
  632. package/src/components/settings/PreferencesForm.module.css +27 -0
  633. package/src/components/settings/ProcessModal.jsx +175 -0
  634. package/src/components/settings/ProcessModal.module.css +21 -0
  635. package/src/components/settings/ProjectAliasEditor.jsx +138 -0
  636. package/src/components/settings/ProjectAliasEditor.module.css +87 -0
  637. package/src/components/settings/ProjectPrefsManagerModal.jsx +123 -0
  638. package/src/components/settings/ProjectPrefsManagerModal.module.css +52 -0
  639. package/src/components/settings/ProxyModal.jsx +286 -0
  640. package/src/components/settings/ProxyModal.module.css +141 -0
  641. package/src/components/settings/SkillsManagerModal.jsx +143 -0
  642. package/src/components/settings/SkillsManagerModal.module.css +143 -0
  643. package/src/components/settings/SystemTextModal.jsx +312 -0
  644. package/src/components/settings/SystemTextModal.module.css +331 -0
  645. package/src/components/settings/VoicePackSettings.jsx +319 -0
  646. package/src/components/settings/VoicePackSettings.module.css +151 -0
  647. package/src/components/settings/imPlatforms.js +177 -0
  648. package/src/components/terminal/CustomUltraplanEditModal.jsx +227 -0
  649. package/src/components/terminal/CustomUltraplanEditModal.module.css +189 -0
  650. package/src/components/terminal/PresetExpertPickerModal.jsx +116 -0
  651. package/src/components/terminal/PresetExpertPickerModal.module.css +90 -0
  652. package/src/components/terminal/PresetModal.jsx +118 -0
  653. package/src/components/terminal/ScratchTerminal.jsx +359 -0
  654. package/src/components/terminal/TerminalPanel.jsx +2021 -0
  655. package/src/components/terminal/TerminalPanel.module.css +872 -0
  656. package/src/components/terminal/TerminalWsContext.jsx +226 -0
  657. package/src/components/terminal/UltraPlanModal.jsx +328 -0
  658. package/src/components/terminal/UltraPlanModal.module.css +527 -0
  659. package/src/components/terminal/UltraplanExpertManagerModal.jsx +114 -0
  660. package/src/components/terminal/UltraplanExpertManagerModal.module.css +91 -0
  661. package/src/components/terminal/UltraplanPanel.jsx +307 -0
  662. package/src/components/terminal/UltraplanPanel.module.css +540 -0
  663. package/src/components/terminal/terminalThemes.js +33 -0
  664. package/src/components/viewers/ImageViewer.jsx +288 -0
  665. package/src/components/viewers/ImageViewer.module.css +203 -0
  666. package/src/components/viewers/JsonViewer.jsx +40 -0
  667. package/src/components/viewers/JsonViewer.module.css +9 -0
  668. package/src/components/viewers/LogTable.jsx +131 -0
  669. package/src/components/viewers/MarkdownBlock.jsx +112 -0
  670. package/src/components/viewers/MarkdownBlock.module.css +157 -0
  671. package/src/components/viewers/ToolResultView.jsx +389 -0
  672. package/src/components/viewers/ToolResultView.module.css +108 -0
  673. package/src/components/viewers/WebResultsView.jsx +74 -0
  674. package/src/components/viewers/WebResultsView.module.css +85 -0
  675. package/src/components/viewers/WorkflowLiveHud.jsx +153 -0
  676. package/src/components/viewers/WorkflowLiveHud.module.css +256 -0
  677. package/src/components/viewers/WorkflowPanel.jsx +243 -0
  678. package/src/components/viewers/WorkflowPanel.module.css +265 -0
  679. package/src/components/viewers/WorkflowTimeline.jsx +83 -0
  680. package/src/components/viewers/WorkflowTimeline.module.css +116 -0
  681. package/src/contexts/SettingsContext.jsx +140 -0
  682. package/src/env.js +54 -0
  683. package/src/global.css +607 -0
  684. package/src/hooks/useMarkdownExport.js +146 -0
  685. package/src/hooks/useMermaidRender.js +193 -0
  686. package/src/hooks/useProjectAlias.js +29 -0
  687. package/src/hooks/useSessionStoragePersistedSet.js +41 -0
  688. package/src/i18n/mdxTranslations.js +472 -0
  689. package/src/i18n.js +12409 -0
  690. package/src/img/ccv-logo.png +0 -0
  691. package/src/img/codex/entrance.svg +7 -0
  692. package/src/img/codex/exit.svg +7 -0
  693. package/src/img/codex/orbiting.svg +7 -0
  694. package/src/img/codex/shimmer.svg +7 -0
  695. package/src/img/codex/thinking.svg +7 -0
  696. package/src/img/codex/tickle.svg +7 -0
  697. package/src/img/codex/waiting.svg +7 -0
  698. package/src/img/codex/writing.svg +7 -0
  699. package/src/img/default-avatar.svg +1 -0
  700. package/src/img/default-model-avatar.svg +1 -0
  701. package/src/img/loading-pet.gif +0 -0
  702. package/src/img/model-codex.svg +2 -0
  703. package/src/img/model-deepseek.svg +2 -0
  704. package/src/img/model-gemini.svg +2 -0
  705. package/src/img/model-glm.svg +2 -0
  706. package/src/img/model-kimi.svg +2 -0
  707. package/src/img/model-minimax.svg +2 -0
  708. package/src/img/model-openai.svg +2 -0
  709. package/src/img/model-qwen.svg +2 -0
  710. package/src/img/teammates/DESIGN.md +165 -0
  711. package/src/img/teammates/analyst.svg +32 -0
  712. package/src/img/teammates/auditor.svg +28 -0
  713. package/src/img/teammates/builder.svg +33 -0
  714. package/src/img/teammates/default.svg +25 -0
  715. package/src/img/teammates/designer.svg +25 -0
  716. package/src/img/teammates/executor.svg +31 -0
  717. package/src/img/teammates/expert.svg +30 -0
  718. package/src/img/teammates/explorer.svg +30 -0
  719. package/src/img/teammates/implementer.svg +34 -0
  720. package/src/img/teammates/investigator.svg +29 -0
  721. package/src/img/teammates/marvel/analyst.svg +26 -0
  722. package/src/img/teammates/marvel/auditor.svg +28 -0
  723. package/src/img/teammates/marvel/builder.svg +15 -0
  724. package/src/img/teammates/marvel/default.svg +25 -0
  725. package/src/img/teammates/marvel/designer.svg +21 -0
  726. package/src/img/teammates/marvel/executor.svg +25 -0
  727. package/src/img/teammates/marvel/expert.svg +31 -0
  728. package/src/img/teammates/marvel/explorer.svg +22 -0
  729. package/src/img/teammates/marvel/implementer.svg +30 -0
  730. package/src/img/teammates/marvel/investigator.svg +21 -0
  731. package/src/img/teammates/marvel/researcher.svg +30 -0
  732. package/src/img/teammates/marvel/reviewer.svg +21 -0
  733. package/src/img/teammates/marvel/scanner.svg +23 -0
  734. package/src/img/teammates/marvel/security.svg +30 -0
  735. package/src/img/teammates/marvel/tracer.svg +19 -0
  736. package/src/img/teammates/marvel/translator.svg +24 -0
  737. package/src/img/teammates/marvel/worker.svg +16 -0
  738. package/src/img/teammates/researcher.svg +35 -0
  739. package/src/img/teammates/reviewer.svg +23 -0
  740. package/src/img/teammates/scanner.svg +31 -0
  741. package/src/img/teammates/security.svg +31 -0
  742. package/src/img/teammates/tracer.svg +30 -0
  743. package/src/img/teammates/translator.svg +32 -0
  744. package/src/img/teammates/worker.svg +29 -0
  745. package/src/main.jsx +30 -0
  746. package/src/tokenInterceptor.js +60 -0
  747. package/src/utils/apiUrl.js +26 -0
  748. package/src/utils/approvalReviewerOptions.js +27 -0
  749. package/src/utils/askFallback.js +21 -0
  750. package/src/utils/askOptionDesc.js +55 -0
  751. package/src/utils/askPortalMatcher.js +54 -0
  752. package/src/utils/autoApproveOptions.js +23 -0
  753. package/src/utils/avatarAnimationPostPass.js +46 -0
  754. package/src/utils/builtinPresets.js +18 -0
  755. package/src/utils/clearCheckpoint.js +154 -0
  756. package/src/utils/commandValidator.js +45 -0
  757. package/src/utils/contentFilter.js +675 -0
  758. package/src/utils/contextRaw.js +17 -0
  759. package/src/utils/contextSidebarNavigation.js +16 -0
  760. package/src/utils/conversationEntryNormalize.js +497 -0
  761. package/src/utils/displayScaleHelper.js +47 -0
  762. package/src/utils/entry-slim.js +594 -0
  763. package/src/utils/entryCache.js +179 -0
  764. package/src/utils/errorReport.js +40 -0
  765. package/src/utils/fileExpandedPathsStorage.js +70 -0
  766. package/src/utils/fileIcons.jsx +36 -0
  767. package/src/utils/fileOpen.js +37 -0
  768. package/src/utils/formatters.js +48 -0
  769. package/src/utils/gitApi.js +50 -0
  770. package/src/utils/gitTreeBuilder.js +19 -0
  771. package/src/utils/helpers.js +462 -0
  772. package/src/utils/identityHeal.js +83 -0
  773. package/src/utils/imOrigin.js +24 -0
  774. package/src/utils/imTr.js +7 -0
  775. package/src/utils/imageCompress.js +83 -0
  776. package/src/utils/imageDownscale.js +28 -0
  777. package/src/utils/imageResize.js +112 -0
  778. package/src/utils/ingestPipeline.js +47 -0
  779. package/src/utils/lazyWithReload.js +53 -0
  780. package/src/utils/markdown.js +30 -0
  781. package/src/utils/markdownIncremental.js +39 -0
  782. package/src/utils/markdownProfiler.js +113 -0
  783. package/src/utils/mdExtensionDetect.js +64 -0
  784. package/src/utils/memoryLinkParser.js +44 -0
  785. package/src/utils/modalMask.js +15 -0
  786. package/src/utils/monotime.js +6 -0
  787. package/src/utils/pinnedMenu.js +55 -0
  788. package/src/utils/presetShortcuts.js +14 -0
  789. package/src/utils/projectAlias.js +135 -0
  790. package/src/utils/promptClassifier.js +111 -0
  791. package/src/utils/promptDetect.js +196 -0
  792. package/src/utils/promptNav.js +60 -0
  793. package/src/utils/ptyChunkBuilder.js +251 -0
  794. package/src/utils/quickMenuHoverIntent.js +35 -0
  795. package/src/utils/rateLimitParser.js +249 -0
  796. package/src/utils/readResultPool.js +106 -0
  797. package/src/utils/refreshCachedItemProp.js +32 -0
  798. package/src/utils/requestTools.js +19 -0
  799. package/src/utils/requestType.js +284 -0
  800. package/src/utils/resizeCalc.js +21 -0
  801. package/src/utils/resolveLocalized.js +17 -0
  802. package/src/utils/seqResourceLoaders.js +79 -0
  803. package/src/utils/sessionManager.js +694 -0
  804. package/src/utils/sessionMerge.js +290 -0
  805. package/src/utils/skillModalController.js +111 -0
  806. package/src/utils/skillsParser.js +116 -0
  807. package/src/utils/slashCommandLabels.js +80 -0
  808. package/src/utils/splitDragCalc.js +73 -0
  809. package/src/utils/stickyBottomController.js +643 -0
  810. package/src/utils/svgSanitize.js +48 -0
  811. package/src/utils/systemTags.js +47 -0
  812. package/src/utils/tCodex.js +20 -0
  813. package/src/utils/teamModalBuilder.js +347 -0
  814. package/src/utils/teamSessionParser.js +186 -0
  815. package/src/utils/teammateAvatars.js +194 -0
  816. package/src/utils/teammateDetector.js +106 -0
  817. package/src/utils/termDiag.js +114 -0
  818. package/src/utils/terminalClipboard.js +117 -0
  819. package/src/utils/terminalWriteQueue.js +398 -0
  820. package/src/utils/thinkingMerge.js +49 -0
  821. package/src/utils/tokenUsage.js +1 -0
  822. package/src/utils/toolCatalog.js +31 -0
  823. package/src/utils/toolNameAliases.js +13 -0
  824. package/src/utils/toolResultBuilder.js +312 -0
  825. package/src/utils/toolResultClassifier.js +16 -0
  826. package/src/utils/toolResultCore.js +161 -0
  827. package/src/utils/toolsDiff.js +44 -0
  828. package/src/utils/toolsXmlFormatter.js +3 -0
  829. package/src/utils/ultraplanController.js +109 -0
  830. package/src/utils/ultraplanExperts.js +86 -0
  831. package/src/utils/ultraplanTemplates.js +152 -0
  832. package/src/utils/userImageRefs.js +49 -0
  833. package/src/utils/voicePackPlayer.js +233 -0
  834. package/src/utils/webResultGrouping.js +111 -0
  835. package/src/utils/workflowFormat.js +52 -0
  836. package/src/utils/workflowRuns.js +118 -0
  837. package/src/utils/workflowStore.js +94 -0
  838. package/vite.config.js +106 -0
  839. package/concepts/ar/CacheRebuild.md +0 -36
  840. package/concepts/ar/KVCacheContent.md +0 -49
  841. package/concepts/ar/Tool-Agent.md +0 -125
  842. package/concepts/ar/Tool-AskUserQuestion.md +0 -75
  843. package/concepts/ar/Tool-Bash.md +0 -167
  844. package/concepts/ar/Tool-Edit.md +0 -46
  845. package/concepts/ar/Tool-EnterPlanMode.md +0 -128
  846. package/concepts/ar/Tool-EnterWorktree.md +0 -56
  847. package/concepts/ar/Tool-ExitPlanMode.md +0 -62
  848. package/concepts/ar/Tool-Glob.md +0 -39
  849. package/concepts/ar/Tool-Grep.md +0 -57
  850. package/concepts/ar/Tool-NotebookEdit.md +0 -33
  851. package/concepts/ar/Tool-Read.md +0 -56
  852. package/concepts/ar/Tool-SendMessage.md +0 -41
  853. package/concepts/ar/Tool-Skill.md +0 -56
  854. package/concepts/ar/Tool-Task.md +0 -51
  855. package/concepts/ar/Tool-TaskCreate.md +0 -78
  856. package/concepts/ar/Tool-TaskGet.md +0 -56
  857. package/concepts/ar/Tool-TaskList.md +0 -56
  858. package/concepts/ar/Tool-TaskOutput.md +0 -40
  859. package/concepts/ar/Tool-TaskStop.md +0 -32
  860. package/concepts/ar/Tool-TaskUpdate.md +0 -123
  861. package/concepts/ar/Tool-TeamCreate.md +0 -38
  862. package/concepts/ar/Tool-TeamDelete.md +0 -30
  863. package/concepts/ar/Tool-WebFetch.md +0 -55
  864. package/concepts/ar/Tool-WebSearch.md +0 -58
  865. package/concepts/ar/Tool-Write.md +0 -39
  866. package/concepts/ar/Tool-executeCode.md +0 -41
  867. package/concepts/ar/Tool-getDiagnostics.md +0 -32
  868. package/concepts/ar/Tools.md +0 -67
  869. package/concepts/da/CacheRebuild.md +0 -36
  870. package/concepts/da/KVCacheContent.md +0 -49
  871. package/concepts/da/Tool-Agent.md +0 -125
  872. package/concepts/da/Tool-AskUserQuestion.md +0 -75
  873. package/concepts/da/Tool-Bash.md +0 -167
  874. package/concepts/da/Tool-Edit.md +0 -46
  875. package/concepts/da/Tool-EnterPlanMode.md +0 -128
  876. package/concepts/da/Tool-EnterWorktree.md +0 -56
  877. package/concepts/da/Tool-ExitPlanMode.md +0 -62
  878. package/concepts/da/Tool-Glob.md +0 -39
  879. package/concepts/da/Tool-Grep.md +0 -57
  880. package/concepts/da/Tool-NotebookEdit.md +0 -33
  881. package/concepts/da/Tool-Read.md +0 -56
  882. package/concepts/da/Tool-SendMessage.md +0 -41
  883. package/concepts/da/Tool-Skill.md +0 -56
  884. package/concepts/da/Tool-Task.md +0 -51
  885. package/concepts/da/Tool-TaskCreate.md +0 -78
  886. package/concepts/da/Tool-TaskGet.md +0 -56
  887. package/concepts/da/Tool-TaskList.md +0 -56
  888. package/concepts/da/Tool-TaskOutput.md +0 -40
  889. package/concepts/da/Tool-TaskStop.md +0 -32
  890. package/concepts/da/Tool-TaskUpdate.md +0 -123
  891. package/concepts/da/Tool-TeamCreate.md +0 -38
  892. package/concepts/da/Tool-TeamDelete.md +0 -30
  893. package/concepts/da/Tool-WebFetch.md +0 -55
  894. package/concepts/da/Tool-WebSearch.md +0 -58
  895. package/concepts/da/Tool-Write.md +0 -39
  896. package/concepts/da/Tool-executeCode.md +0 -41
  897. package/concepts/da/Tool-getDiagnostics.md +0 -32
  898. package/concepts/da/Tools.md +0 -67
  899. package/concepts/de/CacheRebuild.md +0 -36
  900. package/concepts/de/KVCacheContent.md +0 -49
  901. package/concepts/de/Tool-Agent.md +0 -125
  902. package/concepts/de/Tool-AskUserQuestion.md +0 -75
  903. package/concepts/de/Tool-Bash.md +0 -167
  904. package/concepts/de/Tool-Edit.md +0 -46
  905. package/concepts/de/Tool-EnterPlanMode.md +0 -128
  906. package/concepts/de/Tool-EnterWorktree.md +0 -56
  907. package/concepts/de/Tool-ExitPlanMode.md +0 -62
  908. package/concepts/de/Tool-Glob.md +0 -39
  909. package/concepts/de/Tool-Grep.md +0 -57
  910. package/concepts/de/Tool-NotebookEdit.md +0 -33
  911. package/concepts/de/Tool-Read.md +0 -56
  912. package/concepts/de/Tool-SendMessage.md +0 -41
  913. package/concepts/de/Tool-Skill.md +0 -56
  914. package/concepts/de/Tool-Task.md +0 -51
  915. package/concepts/de/Tool-TaskCreate.md +0 -78
  916. package/concepts/de/Tool-TaskGet.md +0 -56
  917. package/concepts/de/Tool-TaskList.md +0 -56
  918. package/concepts/de/Tool-TaskOutput.md +0 -40
  919. package/concepts/de/Tool-TaskStop.md +0 -32
  920. package/concepts/de/Tool-TaskUpdate.md +0 -123
  921. package/concepts/de/Tool-TeamCreate.md +0 -38
  922. package/concepts/de/Tool-TeamDelete.md +0 -30
  923. package/concepts/de/Tool-WebFetch.md +0 -55
  924. package/concepts/de/Tool-WebSearch.md +0 -58
  925. package/concepts/de/Tool-Write.md +0 -39
  926. package/concepts/de/Tool-executeCode.md +0 -41
  927. package/concepts/de/Tool-getDiagnostics.md +0 -32
  928. package/concepts/de/Tools.md +0 -67
  929. package/concepts/en/CacheRebuild.md +0 -36
  930. package/concepts/en/KVCacheContent.md +0 -49
  931. package/concepts/en/Tool-Agent.md +0 -125
  932. package/concepts/en/Tool-AskUserQuestion.md +0 -75
  933. package/concepts/en/Tool-Bash.md +0 -167
  934. package/concepts/en/Tool-Edit.md +0 -46
  935. package/concepts/en/Tool-EnterPlanMode.md +0 -128
  936. package/concepts/en/Tool-EnterWorktree.md +0 -56
  937. package/concepts/en/Tool-ExitPlanMode.md +0 -62
  938. package/concepts/en/Tool-Glob.md +0 -39
  939. package/concepts/en/Tool-Grep.md +0 -57
  940. package/concepts/en/Tool-NotebookEdit.md +0 -33
  941. package/concepts/en/Tool-Read.md +0 -56
  942. package/concepts/en/Tool-SendMessage.md +0 -41
  943. package/concepts/en/Tool-Skill.md +0 -56
  944. package/concepts/en/Tool-Task.md +0 -51
  945. package/concepts/en/Tool-TaskCreate.md +0 -78
  946. package/concepts/en/Tool-TaskGet.md +0 -56
  947. package/concepts/en/Tool-TaskList.md +0 -56
  948. package/concepts/en/Tool-TaskOutput.md +0 -40
  949. package/concepts/en/Tool-TaskStop.md +0 -32
  950. package/concepts/en/Tool-TaskUpdate.md +0 -123
  951. package/concepts/en/Tool-TeamCreate.md +0 -38
  952. package/concepts/en/Tool-TeamDelete.md +0 -30
  953. package/concepts/en/Tool-WebFetch.md +0 -55
  954. package/concepts/en/Tool-WebSearch.md +0 -58
  955. package/concepts/en/Tool-Write.md +0 -39
  956. package/concepts/en/Tool-executeCode.md +0 -41
  957. package/concepts/en/Tool-getDiagnostics.md +0 -32
  958. package/concepts/es/CacheRebuild.md +0 -36
  959. package/concepts/es/KVCacheContent.md +0 -49
  960. package/concepts/es/Tool-Agent.md +0 -125
  961. package/concepts/es/Tool-AskUserQuestion.md +0 -75
  962. package/concepts/es/Tool-Bash.md +0 -167
  963. package/concepts/es/Tool-Edit.md +0 -46
  964. package/concepts/es/Tool-EnterPlanMode.md +0 -128
  965. package/concepts/es/Tool-EnterWorktree.md +0 -56
  966. package/concepts/es/Tool-ExitPlanMode.md +0 -62
  967. package/concepts/es/Tool-Glob.md +0 -39
  968. package/concepts/es/Tool-Grep.md +0 -57
  969. package/concepts/es/Tool-NotebookEdit.md +0 -33
  970. package/concepts/es/Tool-Read.md +0 -56
  971. package/concepts/es/Tool-SendMessage.md +0 -41
  972. package/concepts/es/Tool-Skill.md +0 -56
  973. package/concepts/es/Tool-Task.md +0 -51
  974. package/concepts/es/Tool-TaskCreate.md +0 -78
  975. package/concepts/es/Tool-TaskGet.md +0 -56
  976. package/concepts/es/Tool-TaskList.md +0 -56
  977. package/concepts/es/Tool-TaskOutput.md +0 -40
  978. package/concepts/es/Tool-TaskStop.md +0 -32
  979. package/concepts/es/Tool-TaskUpdate.md +0 -123
  980. package/concepts/es/Tool-TeamCreate.md +0 -38
  981. package/concepts/es/Tool-TeamDelete.md +0 -30
  982. package/concepts/es/Tool-WebFetch.md +0 -55
  983. package/concepts/es/Tool-WebSearch.md +0 -58
  984. package/concepts/es/Tool-Write.md +0 -39
  985. package/concepts/es/Tool-executeCode.md +0 -41
  986. package/concepts/es/Tool-getDiagnostics.md +0 -32
  987. package/concepts/es/Tools.md +0 -67
  988. package/concepts/fr/CacheRebuild.md +0 -36
  989. package/concepts/fr/KVCacheContent.md +0 -49
  990. package/concepts/fr/Tool-Agent.md +0 -125
  991. package/concepts/fr/Tool-AskUserQuestion.md +0 -75
  992. package/concepts/fr/Tool-Bash.md +0 -167
  993. package/concepts/fr/Tool-Edit.md +0 -46
  994. package/concepts/fr/Tool-EnterPlanMode.md +0 -128
  995. package/concepts/fr/Tool-EnterWorktree.md +0 -56
  996. package/concepts/fr/Tool-ExitPlanMode.md +0 -62
  997. package/concepts/fr/Tool-Glob.md +0 -39
  998. package/concepts/fr/Tool-Grep.md +0 -57
  999. package/concepts/fr/Tool-NotebookEdit.md +0 -33
  1000. package/concepts/fr/Tool-Read.md +0 -56
  1001. package/concepts/fr/Tool-SendMessage.md +0 -41
  1002. package/concepts/fr/Tool-Skill.md +0 -56
  1003. package/concepts/fr/Tool-Task.md +0 -51
  1004. package/concepts/fr/Tool-TaskCreate.md +0 -78
  1005. package/concepts/fr/Tool-TaskGet.md +0 -56
  1006. package/concepts/fr/Tool-TaskList.md +0 -56
  1007. package/concepts/fr/Tool-TaskOutput.md +0 -40
  1008. package/concepts/fr/Tool-TaskStop.md +0 -32
  1009. package/concepts/fr/Tool-TaskUpdate.md +0 -123
  1010. package/concepts/fr/Tool-TeamCreate.md +0 -38
  1011. package/concepts/fr/Tool-TeamDelete.md +0 -30
  1012. package/concepts/fr/Tool-WebFetch.md +0 -55
  1013. package/concepts/fr/Tool-WebSearch.md +0 -58
  1014. package/concepts/fr/Tool-Write.md +0 -39
  1015. package/concepts/fr/Tool-executeCode.md +0 -41
  1016. package/concepts/fr/Tool-getDiagnostics.md +0 -32
  1017. package/concepts/fr/Tools.md +0 -67
  1018. package/concepts/it/CacheRebuild.md +0 -36
  1019. package/concepts/it/KVCacheContent.md +0 -49
  1020. package/concepts/it/Tool-Agent.md +0 -125
  1021. package/concepts/it/Tool-AskUserQuestion.md +0 -75
  1022. package/concepts/it/Tool-Bash.md +0 -167
  1023. package/concepts/it/Tool-Edit.md +0 -46
  1024. package/concepts/it/Tool-EnterPlanMode.md +0 -128
  1025. package/concepts/it/Tool-EnterWorktree.md +0 -56
  1026. package/concepts/it/Tool-ExitPlanMode.md +0 -62
  1027. package/concepts/it/Tool-Glob.md +0 -39
  1028. package/concepts/it/Tool-Grep.md +0 -57
  1029. package/concepts/it/Tool-NotebookEdit.md +0 -33
  1030. package/concepts/it/Tool-Read.md +0 -56
  1031. package/concepts/it/Tool-SendMessage.md +0 -41
  1032. package/concepts/it/Tool-Skill.md +0 -56
  1033. package/concepts/it/Tool-Task.md +0 -51
  1034. package/concepts/it/Tool-TaskCreate.md +0 -78
  1035. package/concepts/it/Tool-TaskGet.md +0 -56
  1036. package/concepts/it/Tool-TaskList.md +0 -56
  1037. package/concepts/it/Tool-TaskOutput.md +0 -40
  1038. package/concepts/it/Tool-TaskStop.md +0 -32
  1039. package/concepts/it/Tool-TaskUpdate.md +0 -123
  1040. package/concepts/it/Tool-TeamCreate.md +0 -38
  1041. package/concepts/it/Tool-TeamDelete.md +0 -30
  1042. package/concepts/it/Tool-WebFetch.md +0 -55
  1043. package/concepts/it/Tool-WebSearch.md +0 -58
  1044. package/concepts/it/Tool-Write.md +0 -39
  1045. package/concepts/it/Tool-executeCode.md +0 -41
  1046. package/concepts/it/Tool-getDiagnostics.md +0 -32
  1047. package/concepts/it/Tools.md +0 -67
  1048. package/concepts/ja/CacheRebuild.md +0 -36
  1049. package/concepts/ja/KVCacheContent.md +0 -49
  1050. package/concepts/ja/Tool-Agent.md +0 -125
  1051. package/concepts/ja/Tool-AskUserQuestion.md +0 -75
  1052. package/concepts/ja/Tool-Bash.md +0 -167
  1053. package/concepts/ja/Tool-Edit.md +0 -46
  1054. package/concepts/ja/Tool-EnterPlanMode.md +0 -128
  1055. package/concepts/ja/Tool-EnterWorktree.md +0 -56
  1056. package/concepts/ja/Tool-ExitPlanMode.md +0 -62
  1057. package/concepts/ja/Tool-Glob.md +0 -39
  1058. package/concepts/ja/Tool-Grep.md +0 -57
  1059. package/concepts/ja/Tool-NotebookEdit.md +0 -33
  1060. package/concepts/ja/Tool-Read.md +0 -56
  1061. package/concepts/ja/Tool-SendMessage.md +0 -41
  1062. package/concepts/ja/Tool-Skill.md +0 -56
  1063. package/concepts/ja/Tool-Task.md +0 -51
  1064. package/concepts/ja/Tool-TaskCreate.md +0 -78
  1065. package/concepts/ja/Tool-TaskGet.md +0 -56
  1066. package/concepts/ja/Tool-TaskList.md +0 -56
  1067. package/concepts/ja/Tool-TaskOutput.md +0 -40
  1068. package/concepts/ja/Tool-TaskStop.md +0 -32
  1069. package/concepts/ja/Tool-TaskUpdate.md +0 -123
  1070. package/concepts/ja/Tool-TeamCreate.md +0 -38
  1071. package/concepts/ja/Tool-TeamDelete.md +0 -30
  1072. package/concepts/ja/Tool-WebFetch.md +0 -55
  1073. package/concepts/ja/Tool-WebSearch.md +0 -58
  1074. package/concepts/ja/Tool-Write.md +0 -39
  1075. package/concepts/ja/Tool-executeCode.md +0 -41
  1076. package/concepts/ja/Tool-getDiagnostics.md +0 -32
  1077. package/concepts/ja/Tools.md +0 -67
  1078. package/concepts/ko/CacheRebuild.md +0 -36
  1079. package/concepts/ko/KVCacheContent.md +0 -49
  1080. package/concepts/ko/Tool-Agent.md +0 -125
  1081. package/concepts/ko/Tool-AskUserQuestion.md +0 -75
  1082. package/concepts/ko/Tool-Bash.md +0 -167
  1083. package/concepts/ko/Tool-Edit.md +0 -46
  1084. package/concepts/ko/Tool-EnterPlanMode.md +0 -128
  1085. package/concepts/ko/Tool-EnterWorktree.md +0 -56
  1086. package/concepts/ko/Tool-ExitPlanMode.md +0 -62
  1087. package/concepts/ko/Tool-Glob.md +0 -39
  1088. package/concepts/ko/Tool-Grep.md +0 -57
  1089. package/concepts/ko/Tool-NotebookEdit.md +0 -33
  1090. package/concepts/ko/Tool-Read.md +0 -56
  1091. package/concepts/ko/Tool-SendMessage.md +0 -41
  1092. package/concepts/ko/Tool-Skill.md +0 -56
  1093. package/concepts/ko/Tool-Task.md +0 -51
  1094. package/concepts/ko/Tool-TaskCreate.md +0 -78
  1095. package/concepts/ko/Tool-TaskGet.md +0 -56
  1096. package/concepts/ko/Tool-TaskList.md +0 -56
  1097. package/concepts/ko/Tool-TaskOutput.md +0 -40
  1098. package/concepts/ko/Tool-TaskStop.md +0 -32
  1099. package/concepts/ko/Tool-TaskUpdate.md +0 -123
  1100. package/concepts/ko/Tool-TeamCreate.md +0 -38
  1101. package/concepts/ko/Tool-TeamDelete.md +0 -30
  1102. package/concepts/ko/Tool-WebFetch.md +0 -55
  1103. package/concepts/ko/Tool-WebSearch.md +0 -58
  1104. package/concepts/ko/Tool-Write.md +0 -39
  1105. package/concepts/ko/Tool-executeCode.md +0 -41
  1106. package/concepts/ko/Tool-getDiagnostics.md +0 -32
  1107. package/concepts/ko/Tools.md +0 -67
  1108. package/concepts/no/CacheRebuild.md +0 -36
  1109. package/concepts/no/KVCacheContent.md +0 -49
  1110. package/concepts/no/Tool-Agent.md +0 -125
  1111. package/concepts/no/Tool-AskUserQuestion.md +0 -75
  1112. package/concepts/no/Tool-Bash.md +0 -167
  1113. package/concepts/no/Tool-Edit.md +0 -46
  1114. package/concepts/no/Tool-EnterPlanMode.md +0 -128
  1115. package/concepts/no/Tool-EnterWorktree.md +0 -56
  1116. package/concepts/no/Tool-ExitPlanMode.md +0 -62
  1117. package/concepts/no/Tool-Glob.md +0 -39
  1118. package/concepts/no/Tool-Grep.md +0 -57
  1119. package/concepts/no/Tool-NotebookEdit.md +0 -33
  1120. package/concepts/no/Tool-Read.md +0 -56
  1121. package/concepts/no/Tool-SendMessage.md +0 -41
  1122. package/concepts/no/Tool-Skill.md +0 -56
  1123. package/concepts/no/Tool-Task.md +0 -51
  1124. package/concepts/no/Tool-TaskCreate.md +0 -78
  1125. package/concepts/no/Tool-TaskGet.md +0 -56
  1126. package/concepts/no/Tool-TaskList.md +0 -56
  1127. package/concepts/no/Tool-TaskOutput.md +0 -40
  1128. package/concepts/no/Tool-TaskStop.md +0 -32
  1129. package/concepts/no/Tool-TaskUpdate.md +0 -123
  1130. package/concepts/no/Tool-TeamCreate.md +0 -38
  1131. package/concepts/no/Tool-TeamDelete.md +0 -30
  1132. package/concepts/no/Tool-WebFetch.md +0 -55
  1133. package/concepts/no/Tool-WebSearch.md +0 -58
  1134. package/concepts/no/Tool-Write.md +0 -39
  1135. package/concepts/no/Tool-executeCode.md +0 -41
  1136. package/concepts/no/Tool-getDiagnostics.md +0 -32
  1137. package/concepts/no/Tools.md +0 -67
  1138. package/concepts/pl/CacheRebuild.md +0 -36
  1139. package/concepts/pl/KVCacheContent.md +0 -49
  1140. package/concepts/pl/Tool-Agent.md +0 -125
  1141. package/concepts/pl/Tool-AskUserQuestion.md +0 -75
  1142. package/concepts/pl/Tool-Bash.md +0 -167
  1143. package/concepts/pl/Tool-Edit.md +0 -46
  1144. package/concepts/pl/Tool-EnterPlanMode.md +0 -128
  1145. package/concepts/pl/Tool-EnterWorktree.md +0 -56
  1146. package/concepts/pl/Tool-ExitPlanMode.md +0 -62
  1147. package/concepts/pl/Tool-Glob.md +0 -39
  1148. package/concepts/pl/Tool-Grep.md +0 -57
  1149. package/concepts/pl/Tool-NotebookEdit.md +0 -33
  1150. package/concepts/pl/Tool-Read.md +0 -56
  1151. package/concepts/pl/Tool-SendMessage.md +0 -41
  1152. package/concepts/pl/Tool-Skill.md +0 -56
  1153. package/concepts/pl/Tool-Task.md +0 -51
  1154. package/concepts/pl/Tool-TaskCreate.md +0 -78
  1155. package/concepts/pl/Tool-TaskGet.md +0 -56
  1156. package/concepts/pl/Tool-TaskList.md +0 -56
  1157. package/concepts/pl/Tool-TaskOutput.md +0 -40
  1158. package/concepts/pl/Tool-TaskStop.md +0 -32
  1159. package/concepts/pl/Tool-TaskUpdate.md +0 -123
  1160. package/concepts/pl/Tool-TeamCreate.md +0 -38
  1161. package/concepts/pl/Tool-TeamDelete.md +0 -30
  1162. package/concepts/pl/Tool-WebFetch.md +0 -55
  1163. package/concepts/pl/Tool-WebSearch.md +0 -58
  1164. package/concepts/pl/Tool-Write.md +0 -39
  1165. package/concepts/pl/Tool-executeCode.md +0 -41
  1166. package/concepts/pl/Tool-getDiagnostics.md +0 -32
  1167. package/concepts/pl/Tools.md +0 -67
  1168. package/concepts/pt-BR/CacheRebuild.md +0 -36
  1169. package/concepts/pt-BR/KVCacheContent.md +0 -49
  1170. package/concepts/pt-BR/Tool-Agent.md +0 -125
  1171. package/concepts/pt-BR/Tool-AskUserQuestion.md +0 -75
  1172. package/concepts/pt-BR/Tool-Bash.md +0 -167
  1173. package/concepts/pt-BR/Tool-Edit.md +0 -46
  1174. package/concepts/pt-BR/Tool-EnterPlanMode.md +0 -128
  1175. package/concepts/pt-BR/Tool-EnterWorktree.md +0 -56
  1176. package/concepts/pt-BR/Tool-ExitPlanMode.md +0 -62
  1177. package/concepts/pt-BR/Tool-Glob.md +0 -39
  1178. package/concepts/pt-BR/Tool-Grep.md +0 -57
  1179. package/concepts/pt-BR/Tool-NotebookEdit.md +0 -33
  1180. package/concepts/pt-BR/Tool-Read.md +0 -56
  1181. package/concepts/pt-BR/Tool-SendMessage.md +0 -41
  1182. package/concepts/pt-BR/Tool-Skill.md +0 -56
  1183. package/concepts/pt-BR/Tool-Task.md +0 -51
  1184. package/concepts/pt-BR/Tool-TaskCreate.md +0 -78
  1185. package/concepts/pt-BR/Tool-TaskGet.md +0 -56
  1186. package/concepts/pt-BR/Tool-TaskList.md +0 -56
  1187. package/concepts/pt-BR/Tool-TaskOutput.md +0 -40
  1188. package/concepts/pt-BR/Tool-TaskStop.md +0 -32
  1189. package/concepts/pt-BR/Tool-TaskUpdate.md +0 -123
  1190. package/concepts/pt-BR/Tool-TeamCreate.md +0 -38
  1191. package/concepts/pt-BR/Tool-TeamDelete.md +0 -30
  1192. package/concepts/pt-BR/Tool-WebFetch.md +0 -55
  1193. package/concepts/pt-BR/Tool-WebSearch.md +0 -58
  1194. package/concepts/pt-BR/Tool-Write.md +0 -39
  1195. package/concepts/pt-BR/Tool-executeCode.md +0 -41
  1196. package/concepts/pt-BR/Tool-getDiagnostics.md +0 -32
  1197. package/concepts/pt-BR/Tools.md +0 -67
  1198. package/concepts/ru/CacheRebuild.md +0 -36
  1199. package/concepts/ru/KVCacheContent.md +0 -49
  1200. package/concepts/ru/Tool-Agent.md +0 -125
  1201. package/concepts/ru/Tool-AskUserQuestion.md +0 -75
  1202. package/concepts/ru/Tool-Bash.md +0 -167
  1203. package/concepts/ru/Tool-Edit.md +0 -46
  1204. package/concepts/ru/Tool-EnterPlanMode.md +0 -128
  1205. package/concepts/ru/Tool-EnterWorktree.md +0 -56
  1206. package/concepts/ru/Tool-ExitPlanMode.md +0 -62
  1207. package/concepts/ru/Tool-Glob.md +0 -39
  1208. package/concepts/ru/Tool-Grep.md +0 -57
  1209. package/concepts/ru/Tool-NotebookEdit.md +0 -33
  1210. package/concepts/ru/Tool-Read.md +0 -56
  1211. package/concepts/ru/Tool-SendMessage.md +0 -41
  1212. package/concepts/ru/Tool-Skill.md +0 -56
  1213. package/concepts/ru/Tool-Task.md +0 -51
  1214. package/concepts/ru/Tool-TaskCreate.md +0 -78
  1215. package/concepts/ru/Tool-TaskGet.md +0 -56
  1216. package/concepts/ru/Tool-TaskList.md +0 -56
  1217. package/concepts/ru/Tool-TaskOutput.md +0 -40
  1218. package/concepts/ru/Tool-TaskStop.md +0 -32
  1219. package/concepts/ru/Tool-TaskUpdate.md +0 -123
  1220. package/concepts/ru/Tool-TeamCreate.md +0 -38
  1221. package/concepts/ru/Tool-TeamDelete.md +0 -30
  1222. package/concepts/ru/Tool-WebFetch.md +0 -55
  1223. package/concepts/ru/Tool-WebSearch.md +0 -58
  1224. package/concepts/ru/Tool-Write.md +0 -39
  1225. package/concepts/ru/Tool-executeCode.md +0 -41
  1226. package/concepts/ru/Tool-getDiagnostics.md +0 -32
  1227. package/concepts/ru/Tools.md +0 -67
  1228. package/concepts/th/CacheRebuild.md +0 -36
  1229. package/concepts/th/KVCacheContent.md +0 -49
  1230. package/concepts/th/Tool-Agent.md +0 -125
  1231. package/concepts/th/Tool-AskUserQuestion.md +0 -75
  1232. package/concepts/th/Tool-Bash.md +0 -167
  1233. package/concepts/th/Tool-Edit.md +0 -46
  1234. package/concepts/th/Tool-EnterPlanMode.md +0 -128
  1235. package/concepts/th/Tool-EnterWorktree.md +0 -56
  1236. package/concepts/th/Tool-ExitPlanMode.md +0 -62
  1237. package/concepts/th/Tool-Glob.md +0 -39
  1238. package/concepts/th/Tool-Grep.md +0 -57
  1239. package/concepts/th/Tool-NotebookEdit.md +0 -33
  1240. package/concepts/th/Tool-Read.md +0 -56
  1241. package/concepts/th/Tool-SendMessage.md +0 -41
  1242. package/concepts/th/Tool-Skill.md +0 -56
  1243. package/concepts/th/Tool-Task.md +0 -51
  1244. package/concepts/th/Tool-TaskCreate.md +0 -78
  1245. package/concepts/th/Tool-TaskGet.md +0 -56
  1246. package/concepts/th/Tool-TaskList.md +0 -56
  1247. package/concepts/th/Tool-TaskOutput.md +0 -40
  1248. package/concepts/th/Tool-TaskStop.md +0 -32
  1249. package/concepts/th/Tool-TaskUpdate.md +0 -123
  1250. package/concepts/th/Tool-TeamCreate.md +0 -38
  1251. package/concepts/th/Tool-TeamDelete.md +0 -30
  1252. package/concepts/th/Tool-WebFetch.md +0 -55
  1253. package/concepts/th/Tool-WebSearch.md +0 -58
  1254. package/concepts/th/Tool-Write.md +0 -39
  1255. package/concepts/th/Tool-executeCode.md +0 -41
  1256. package/concepts/th/Tool-getDiagnostics.md +0 -32
  1257. package/concepts/th/Tools.md +0 -67
  1258. package/concepts/tr/CacheRebuild.md +0 -36
  1259. package/concepts/tr/KVCacheContent.md +0 -49
  1260. package/concepts/tr/Tool-Agent.md +0 -125
  1261. package/concepts/tr/Tool-AskUserQuestion.md +0 -75
  1262. package/concepts/tr/Tool-Bash.md +0 -167
  1263. package/concepts/tr/Tool-Edit.md +0 -46
  1264. package/concepts/tr/Tool-EnterPlanMode.md +0 -128
  1265. package/concepts/tr/Tool-EnterWorktree.md +0 -56
  1266. package/concepts/tr/Tool-ExitPlanMode.md +0 -62
  1267. package/concepts/tr/Tool-Glob.md +0 -39
  1268. package/concepts/tr/Tool-Grep.md +0 -57
  1269. package/concepts/tr/Tool-NotebookEdit.md +0 -33
  1270. package/concepts/tr/Tool-Read.md +0 -56
  1271. package/concepts/tr/Tool-SendMessage.md +0 -41
  1272. package/concepts/tr/Tool-Skill.md +0 -56
  1273. package/concepts/tr/Tool-Task.md +0 -51
  1274. package/concepts/tr/Tool-TaskCreate.md +0 -78
  1275. package/concepts/tr/Tool-TaskGet.md +0 -56
  1276. package/concepts/tr/Tool-TaskList.md +0 -56
  1277. package/concepts/tr/Tool-TaskOutput.md +0 -40
  1278. package/concepts/tr/Tool-TaskStop.md +0 -32
  1279. package/concepts/tr/Tool-TaskUpdate.md +0 -123
  1280. package/concepts/tr/Tool-TeamCreate.md +0 -38
  1281. package/concepts/tr/Tool-TeamDelete.md +0 -30
  1282. package/concepts/tr/Tool-WebFetch.md +0 -55
  1283. package/concepts/tr/Tool-WebSearch.md +0 -58
  1284. package/concepts/tr/Tool-Write.md +0 -39
  1285. package/concepts/tr/Tool-executeCode.md +0 -41
  1286. package/concepts/tr/Tool-getDiagnostics.md +0 -32
  1287. package/concepts/tr/Tools.md +0 -67
  1288. package/concepts/uk/CacheRebuild.md +0 -36
  1289. package/concepts/uk/KVCacheContent.md +0 -49
  1290. package/concepts/uk/Tool-Agent.md +0 -125
  1291. package/concepts/uk/Tool-AskUserQuestion.md +0 -75
  1292. package/concepts/uk/Tool-Bash.md +0 -167
  1293. package/concepts/uk/Tool-Edit.md +0 -46
  1294. package/concepts/uk/Tool-EnterPlanMode.md +0 -128
  1295. package/concepts/uk/Tool-EnterWorktree.md +0 -56
  1296. package/concepts/uk/Tool-ExitPlanMode.md +0 -62
  1297. package/concepts/uk/Tool-Glob.md +0 -39
  1298. package/concepts/uk/Tool-Grep.md +0 -57
  1299. package/concepts/uk/Tool-NotebookEdit.md +0 -33
  1300. package/concepts/uk/Tool-Read.md +0 -56
  1301. package/concepts/uk/Tool-SendMessage.md +0 -41
  1302. package/concepts/uk/Tool-Skill.md +0 -56
  1303. package/concepts/uk/Tool-Task.md +0 -51
  1304. package/concepts/uk/Tool-TaskCreate.md +0 -78
  1305. package/concepts/uk/Tool-TaskGet.md +0 -56
  1306. package/concepts/uk/Tool-TaskList.md +0 -56
  1307. package/concepts/uk/Tool-TaskOutput.md +0 -40
  1308. package/concepts/uk/Tool-TaskStop.md +0 -32
  1309. package/concepts/uk/Tool-TaskUpdate.md +0 -123
  1310. package/concepts/uk/Tool-TeamCreate.md +0 -38
  1311. package/concepts/uk/Tool-TeamDelete.md +0 -30
  1312. package/concepts/uk/Tool-WebFetch.md +0 -55
  1313. package/concepts/uk/Tool-WebSearch.md +0 -58
  1314. package/concepts/uk/Tool-Write.md +0 -39
  1315. package/concepts/uk/Tool-executeCode.md +0 -41
  1316. package/concepts/uk/Tool-getDiagnostics.md +0 -32
  1317. package/concepts/uk/Tools.md +0 -67
  1318. package/concepts/zh/CacheRebuild.md +0 -36
  1319. package/concepts/zh/KVCacheContent.md +0 -49
  1320. package/concepts/zh/Tool-Agent.md +0 -125
  1321. package/concepts/zh/Tool-AskUserQuestion.md +0 -75
  1322. package/concepts/zh/Tool-Bash.md +0 -167
  1323. package/concepts/zh/Tool-Edit.md +0 -46
  1324. package/concepts/zh/Tool-EnterPlanMode.md +0 -128
  1325. package/concepts/zh/Tool-EnterWorktree.md +0 -56
  1326. package/concepts/zh/Tool-ExitPlanMode.md +0 -62
  1327. package/concepts/zh/Tool-Glob.md +0 -39
  1328. package/concepts/zh/Tool-Grep.md +0 -57
  1329. package/concepts/zh/Tool-NotebookEdit.md +0 -33
  1330. package/concepts/zh/Tool-Read.md +0 -56
  1331. package/concepts/zh/Tool-SendMessage.md +0 -41
  1332. package/concepts/zh/Tool-Skill.md +0 -56
  1333. package/concepts/zh/Tool-Task.md +0 -51
  1334. package/concepts/zh/Tool-TaskCreate.md +0 -78
  1335. package/concepts/zh/Tool-TaskGet.md +0 -56
  1336. package/concepts/zh/Tool-TaskList.md +0 -56
  1337. package/concepts/zh/Tool-TaskOutput.md +0 -40
  1338. package/concepts/zh/Tool-TaskStop.md +0 -32
  1339. package/concepts/zh/Tool-TaskUpdate.md +0 -123
  1340. package/concepts/zh/Tool-TeamCreate.md +0 -38
  1341. package/concepts/zh/Tool-TeamDelete.md +0 -30
  1342. package/concepts/zh/Tool-WebFetch.md +0 -55
  1343. package/concepts/zh/Tool-WebSearch.md +0 -58
  1344. package/concepts/zh/Tool-Write.md +0 -39
  1345. package/concepts/zh/Tool-executeCode.md +0 -41
  1346. package/concepts/zh/Tool-getDiagnostics.md +0 -32
  1347. package/concepts/zh-TW/CacheRebuild.md +0 -36
  1348. package/concepts/zh-TW/KVCacheContent.md +0 -49
  1349. package/concepts/zh-TW/Tool-Agent.md +0 -125
  1350. package/concepts/zh-TW/Tool-AskUserQuestion.md +0 -75
  1351. package/concepts/zh-TW/Tool-Bash.md +0 -167
  1352. package/concepts/zh-TW/Tool-Edit.md +0 -46
  1353. package/concepts/zh-TW/Tool-EnterPlanMode.md +0 -128
  1354. package/concepts/zh-TW/Tool-EnterWorktree.md +0 -56
  1355. package/concepts/zh-TW/Tool-ExitPlanMode.md +0 -62
  1356. package/concepts/zh-TW/Tool-Glob.md +0 -39
  1357. package/concepts/zh-TW/Tool-Grep.md +0 -57
  1358. package/concepts/zh-TW/Tool-NotebookEdit.md +0 -33
  1359. package/concepts/zh-TW/Tool-Read.md +0 -56
  1360. package/concepts/zh-TW/Tool-SendMessage.md +0 -41
  1361. package/concepts/zh-TW/Tool-Skill.md +0 -56
  1362. package/concepts/zh-TW/Tool-Task.md +0 -51
  1363. package/concepts/zh-TW/Tool-TaskCreate.md +0 -78
  1364. package/concepts/zh-TW/Tool-TaskGet.md +0 -56
  1365. package/concepts/zh-TW/Tool-TaskList.md +0 -56
  1366. package/concepts/zh-TW/Tool-TaskOutput.md +0 -40
  1367. package/concepts/zh-TW/Tool-TaskStop.md +0 -32
  1368. package/concepts/zh-TW/Tool-TaskUpdate.md +0 -123
  1369. package/concepts/zh-TW/Tool-TeamCreate.md +0 -38
  1370. package/concepts/zh-TW/Tool-TeamDelete.md +0 -30
  1371. package/concepts/zh-TW/Tool-WebFetch.md +0 -55
  1372. package/concepts/zh-TW/Tool-WebSearch.md +0 -58
  1373. package/concepts/zh-TW/Tool-Write.md +0 -39
  1374. package/concepts/zh-TW/Tool-executeCode.md +0 -41
  1375. package/concepts/zh-TW/Tool-getDiagnostics.md +0 -32
  1376. package/concepts/zh-TW/Tools.md +0 -67
  1377. package/dist/assets/App-BN2YmKSu.js +0 -41
  1378. package/dist/assets/App-fG2r3jFq.css +0 -1
  1379. package/dist/assets/AppHeader-BqUGBbBx.css +0 -41
  1380. package/dist/assets/AppHeader.module-CvH9GQQ2.js +0 -1250
  1381. package/dist/assets/Mobile-BnDkLcBv.css +0 -1
  1382. package/dist/assets/Mobile-CbC8YD3O.js +0 -10
  1383. package/dist/assets/_baseUniq-B2ddYY2A.js +0 -1
  1384. package/dist/assets/arc-DFPOHgcd.js +0 -1
  1385. package/dist/assets/architectureDiagram-Q4EWVU46-1tuXixQv.js +0 -36
  1386. package/dist/assets/blockDiagram-DXYQGD6D-BfVKQPAV.js +0 -132
  1387. package/dist/assets/c4Diagram-AHTNJAMY-Bua8O4-1.js +0 -10
  1388. package/dist/assets/channel-Cf9uPxYW.js +0 -1
  1389. package/dist/assets/chunk-4BX2VUAB-YOIkSNn_.js +0 -1
  1390. package/dist/assets/chunk-4TB4RGXK-CjZt-To5.js +0 -206
  1391. package/dist/assets/chunk-55IACEB6-BAEfMF5S.js +0 -1
  1392. package/dist/assets/chunk-EDXVE4YY-BUYDmLXU.js +0 -1
  1393. package/dist/assets/chunk-FMBD7UC4-Cw_LDap-.js +0 -15
  1394. package/dist/assets/chunk-OYMX7WX6-PlfZFFxd.js +0 -231
  1395. package/dist/assets/chunk-QZHKN3VN-C2ReFZl_.js +0 -1
  1396. package/dist/assets/chunk-YZCP3GAM-B7IVGkPB.js +0 -1
  1397. package/dist/assets/classDiagram-6PBFFD2Q-DdwVeNLZ.js +0 -1
  1398. package/dist/assets/classDiagram-v2-HSJHXN6E-DdwVeNLZ.js +0 -1
  1399. package/dist/assets/clone-C8Lz0121.js +0 -1
  1400. package/dist/assets/cose-bilkent-S5V4N54A-M6rBKf0y.js +0 -1
  1401. package/dist/assets/cytoscape.esm-DxGcaOPV.js +0 -331
  1402. package/dist/assets/dagre-KV5264BT-UPacTWnL.js +0 -4
  1403. package/dist/assets/defaultLocale-DX6XiGOO.js +0 -1
  1404. package/dist/assets/diagram-5BDNPKRD-Cjjtnh0e.js +0 -10
  1405. package/dist/assets/diagram-G4DWMVQ6-CjXILMP0.js +0 -24
  1406. package/dist/assets/diagram-MMDJMWI5-C_-XZ9gc.js +0 -43
  1407. package/dist/assets/diagram-TYMM5635-Dxe2GxUR.js +0 -24
  1408. package/dist/assets/erDiagram-SMLLAGMA-1w-wtgMd.js +0 -85
  1409. package/dist/assets/flowDiagram-DWJPFMVM-B9naMGCj.js +0 -162
  1410. package/dist/assets/ganttDiagram-T4ZO3ILL-BPZBdhPv.js +0 -292
  1411. package/dist/assets/gitGraphDiagram-UUTBAWPF-_P9PdiGG.js +0 -106
  1412. package/dist/assets/graph-CjOY8lRz.js +0 -1
  1413. package/dist/assets/html2canvas.esm-QH1iLAAe.js +0 -22
  1414. package/dist/assets/index-BcCW9op4.js +0 -41
  1415. package/dist/assets/index-DCtZ2KSX.css +0 -1
  1416. package/dist/assets/infoDiagram-42DDH7IO-Ch8SOTtq.js +0 -2
  1417. package/dist/assets/init-Gi6I4Gst.js +0 -1
  1418. package/dist/assets/ishikawaDiagram-UXIWVN3A-XLscQrJT.js +0 -70
  1419. package/dist/assets/journeyDiagram-VCZTEJTY-C3FQ_QPk.js +0 -139
  1420. package/dist/assets/kanban-definition-6JOO6SKY-DIQDTOi3.js +0 -89
  1421. package/dist/assets/katex-DkKDou_j.js +0 -257
  1422. package/dist/assets/layout-DO9W0X_b.js +0 -1
  1423. package/dist/assets/linear-ClCJqwn1.js +0 -1
  1424. package/dist/assets/mermaid.core-BW02B1TS.js +0 -308
  1425. package/dist/assets/min-BuzClhio.js +0 -1
  1426. package/dist/assets/mindmap-definition-QFDTVHPH-DuCaTeZu.js +0 -96
  1427. package/dist/assets/ordinal-Cboi1Yqb.js +0 -1
  1428. package/dist/assets/pieDiagram-DEJITSTG-BZoZ6CI1.js +0 -30
  1429. package/dist/assets/quadrantDiagram-34T5L4WZ-DFNwvtmc.js +0 -7
  1430. package/dist/assets/requirementDiagram-MS252O5E-DPG-7raV.js +0 -84
  1431. package/dist/assets/sankeyDiagram-XADWPNL6-Bp92e1ie.js +0 -10
  1432. package/dist/assets/sequenceDiagram-FGHM5R23-C6ADwqJY.js +0 -157
  1433. package/dist/assets/stateDiagram-FHFEXIEX-BfrwZGmF.js +0 -1
  1434. package/dist/assets/stateDiagram-v2-QKLJ7IA2-4gBW7EPI.js +0 -1
  1435. package/dist/assets/timeline-definition-GMOUNBTQ-Bb-TxYx2.js +0 -120
  1436. package/dist/assets/vennDiagram-DHZGUBPP-DTRiNXJu.js +0 -34
  1437. package/dist/assets/wardley-RL74JXVD-C8j_kjXS.js +0 -162
  1438. package/dist/assets/wardleyDiagram-NUSXRM2D-CvICMc3N.js +0 -20
  1439. package/dist/assets/xychartDiagram-5P7HB3ND-DI0bBGwq.js +0 -7
  1440. package/lib/kv-cache-analyzer.js +0 -164
@@ -0,0 +1,3533 @@
1
+ import React from 'react';
2
+ import { Empty, Typography, Divider, Spin, Popover, Modal, message } from 'antd';
3
+ import ChatMessage from './ChatMessage';
4
+ import TerminalPanel, { uploadFileAndGetPath } from '../terminal/TerminalPanel';
5
+ import FileExplorer from '../files/FileExplorer';
6
+ import FileContentView from '../files/FileContentView';
7
+ import ImageViewer from '../viewers/ImageViewer';
8
+ import ImageLightbox from '../common/ImageLightbox';
9
+ import GitChanges from '../git/GitChanges';
10
+ import GitDiffView from '../git/GitDiffView';
11
+ import ToolApprovalPanel from '../approval/ToolApprovalPanel';
12
+ import { getModelInfo, getEffectiveModel, resolveProducerModelInfo, AUTO_APPROVE_INSTANT } from '../../utils/helpers';
13
+ import { formatPromptNavTime } from '../../utils/formatters';
14
+ import { buildPromptNavItems } from '../../utils/promptNav';
15
+ import { getTeammateAvatar } from '../../utils/teammateAvatars';
16
+ import { applyAvatarAnimationTargets } from '../../utils/avatarAnimationPostPass';
17
+ import { isSystemText, classifyUserContent, isMainAgent, isTeammate, resolveTeammateNames, extractDisplayText } from '../../utils/contentFilter';
18
+ import { classifyRequest, formatRequestTag, formatTeammateLabel } from '../../utils/requestType';
19
+ import { shouldExcludeFromConversation } from '../../utils/conversationEntryNormalize';
20
+ import { playEvent as playVoiceEvent } from '../../utils/voicePackPlayer';
21
+ import { buildChunksForAnswer, buildBracketPasteSubmitChunks, BRACKET_PASTE_SUBMIT_SETTLE_MS } from '../../utils/ptyChunkBuilder';
22
+ import { isPlanApprovalPrompt, isDangerousOperationPrompt, pickPlanApproveOptionNumber } from '../../utils/promptClassifier';
23
+ import { reportSwallowed } from '../../utils/errorReport';
24
+ import { isImageFile } from '../../utils/commandValidator';
25
+ import { loadExpandedPaths, saveExpandedPaths } from '../../utils/fileExpandedPathsStorage';
26
+ import { createEmptyToolState, appendToolResultMap, cachedBuildToolResultMap, getToolResultCache, setToolResultCache, buildSubAgentResultMap, createEmptyGlobalIndexState, appendToGlobalToolResultIndex } from '../../utils/toolResultBuilder';
27
+ import { refreshCachedItemProp } from '../../utils/refreshCachedItemProp';
28
+ import { ASK_TOOL_NAMES, CODEX_PLAN_TOOL_NAME, PLAN_TOOL_NAMES, isPlanToolName } from '../../utils/toolNameAliases.js';
29
+ import { refreshResolvedModelInfo, healUnresolvedTeammateEntries, needsFullReqRescan } from '../../utils/identityHeal';
30
+ import { resolveBubbleProducerTs } from '../../utils/sessionManager';
31
+ import { TeamButton, TeamModal } from '../dashboard/TeamSessionPanel';
32
+ import { WorkflowButton, WorkflowRunsModal } from '../dashboard/WorkflowRunsPanel';
33
+ import SnapLineOverlay from '../common/SnapLineOverlay';
34
+ import RoleFilterBar from './RoleFilterBar';
35
+ import ChatInputBar from './ChatInputBar';
36
+ import WorkflowLiveHud from '../viewers/WorkflowLiveHud';
37
+ import PresetModal from '../terminal/PresetModal';
38
+ import UltraPlanModal from '../terminal/UltraPlanModal';
39
+ import UltraplanPanel, { readUltraplanPopoverSize, ultraplanOverlayInnerStyle } from '../terminal/UltraplanPanel';
40
+ import UltraplanExpertManagerModal from '../terminal/UltraplanExpertManagerModal';
41
+ import { visibleExpertKeys } from '../../utils/ultraplanExperts';
42
+ import { TerminalWsContext } from '../terminal/TerminalWsContext';
43
+ import CustomUltraplanEditModal from '../terminal/CustomUltraplanEditModal';
44
+ import { buildLocalUltraplan } from '../../utils/ultraplanTemplates';
45
+ import { Virtuoso } from 'react-virtuoso';
46
+ import { StickyBottomController } from '../../utils/stickyBottomController';
47
+ import { AskFlowController, ASK_KIND, LEGACY_ASK_PLACEHOLDER_ID } from './controllers/askFlowController';
48
+ import { UltraplanController } from '../../utils/ultraplanController';
49
+ import { shouldDeferSend, reduceUploading } from './uploadDeferLogic';
50
+ import { computeMessagesPending, healStalePendingIds } from './interactionOwnership';
51
+ import { ScrollHighlightController } from './controllers/scrollHighlightController';
52
+ import { PermissionController } from './controllers/permissionController';
53
+ import { ToolFileChangeController } from './controllers/toolFileChangeController';
54
+ import { SplitDragController, TERMINAL_WIDTH_STORAGE_KEY, SIDEBAR_WIDTH_STORAGE_KEY } from './controllers/splitDragController';
55
+ import { PtyPromptController } from './controllers/ptyPromptController';
56
+ import { TERMINAL_CHAR_WIDTH, RESIZER_WIDTH_PX } from '../../utils/splitDragCalc';
57
+ import { isMobile, isIOS, isPad } from '../../env';
58
+ import { t } from '../../i18n';
59
+ import { apiUrl } from '../../utils/apiUrl';
60
+ import { tryOpenWithSystem } from '../../utils/fileOpen';
61
+ import { BUILTIN_PRESETS } from '../../utils/builtinPresets';
62
+ import defaultAvatarUrl from '../../img/default-avatar.svg';
63
+ import loadingPetUrl from '../../img/loading-pet.gif';
64
+ // 用 <object type="image/svg+xml"> 替代 <img>:WeChat / Android WebView 在 <img> 的 image 路径
65
+ // 下经常把 SMIL <animate> 当 raster 处理只渲染第 0 帧;<object> 走 SVG document 路径,所有 WebView
66
+ // 正确播放,且不依赖 dangerouslySetInnerHTML。
67
+ import shimmerUrl from '../../img/codex/shimmer.svg';
68
+ import orbitingUrl from '../../img/codex/orbiting.svg';
69
+ import styles from './ChatView.module.css';
70
+
71
+ const { Text } = Typography;
72
+
73
+ const QUEUE_THRESHOLD = 20;
74
+
75
+ // 乐观停止兜底超时:到点仍未收到真实停止信号则强制清 stopOptimistic。必须 > AppBase 关闭路径
76
+ // (streaming_status{active:false} 的 SSE + 2s _streamingOffTimer),否则会在 isStreaming 仍为 true
77
+ // 时过早清除、让按钮回跳停止态。正常路径由 componentDidUpdate 的 isStreaming 下降沿先清除。
78
+ const STOP_OPTIMISTIC_FALLBACK_MS = 4000;
79
+ // CLI/PTY 停止:focus-in(\x1b[I) 与 ESC(\x1b) 之间的间隔——给 Codex 一次独立 PTY read 先更新聚焦态
80
+ // 再判定 ESC。对齐 _sendUserMessageImmediate 里 input→\r 的既有 50ms 约定(避免引入新魔数)。
81
+ const STOP_FOCUS_IN_ESC_DELAY_MS = 50;
82
+ // 图片上传在途时按发送 → 缓发(defer)等待上传 resolve 后自动带图发送的超时兜底:
83
+ // 到点仍未完成则提示重试,绝不静默发纯文字。
84
+ const UPLOAD_DEFER_TIMEOUT_MS = 10000;
85
+
86
+ // 免审批下 PTY 子代理 prompt 去重时窗:同一 prompt 在该窗口内被反复检测时只放行一次,
87
+ // 挡住 PTY 慢回显/重绘导致的二次自动放行(_promptSubmitting 仅 500ms,不足以覆盖)。
88
+
89
+ const MOBILE_ITEM_LIMIT = 240;
90
+ const IOS_ITEM_LIMIT = 150;
91
+ const MOBILE_LOAD_MORE_STEP = 100;
92
+ // 桌面端初始渲染上限。桌面不走虚拟化(useVirtuoso 仅 isMobile),长任务会把整段对话全量渲染成
93
+ // DOM,中后段 reconcile/layout 成本随条目数线性增长 → 主线程卡死(Windows 比 Mac 先撞上上限)。
94
+ // 与移动端一致:只渲染最近 N 条 item,更早的用「加载更早」按需展开。桌面给更大窗口。
95
+ const DESKTOP_ITEM_LIMIT = 400;
96
+ // 当前平台基础渲染上限。isMobile/isIOS 在模块加载时即固定(见 env.js),故可一次性求值。
97
+ const ITEM_LIMIT = isMobile ? (isIOS ? IOS_ITEM_LIMIT : MOBILE_ITEM_LIMIT) : DESKTOP_ITEM_LIMIT;
98
+ const useVirtuoso = isMobile && !isIOS && !isPad;
99
+
100
+ // 稳定空对象引用,避免每次 render 创建新 {} 导致子组件重渲染
101
+ const EMPTY_OBJ = {};
102
+ const EMPTY_MAP = {};
103
+
104
+ // ASK_KIND / LEGACY_ASK_PLACEHOLDER_ID 现由 ./controllers/askFlowController 定义并 import(见顶部)。
105
+
106
+ // Virtuoso custom Scroller — 定义在类外部,避免每次 render 创建新组件引用
107
+ const VirtuosoScroller = React.forwardRef((props, ref) => (
108
+ <div ref={ref} {...props} className={styles.container} />
109
+ ));
110
+
111
+ function randomInterval() {
112
+ return 100 + Math.random() * 50;
113
+ }
114
+
115
+ // UltraPlan modal 拖拽尺寸持久化 helper —— 单值 key,与 PanelResizer 风格一致。
116
+ // 手机模式(isMobile && !isPad)不读 localStorage,初值 null,保留 modal CSS 默认尺寸。
117
+ const _ULTRAPLAN_W_KEY = 'cx-viewer-ultraplan-modal-width';
118
+ const _ULTRAPLAN_H_KEY = 'cx-viewer-ultraplan-modal-height';
119
+ function _readUltraplanModalSize() {
120
+ if (isMobile && !isPad) return null;
121
+ try {
122
+ const w = parseFloat(localStorage.getItem(_ULTRAPLAN_W_KEY));
123
+ const h = parseFloat(localStorage.getItem(_ULTRAPLAN_H_KEY));
124
+ if (Number.isFinite(w) || Number.isFinite(h)) {
125
+ return { w: Number.isFinite(w) ? w : null, h: Number.isFinite(h) ? h : null };
126
+ }
127
+ } catch {}
128
+ return null;
129
+ }
130
+ function _writeUltraplanModalSize(size) {
131
+ if (isMobile && !isPad) return;
132
+ try {
133
+ if (size?.w) localStorage.setItem(_ULTRAPLAN_W_KEY, String(size.w));
134
+ if (size?.h) localStorage.setItem(_ULTRAPLAN_H_KEY, String(size.h));
135
+ } catch {}
136
+ }
137
+
138
+ class ChatView extends React.Component {
139
+ // 通过 Context 共享 App 层的单条 /ws/terminal,this.context = { send, isOpen, addMessageHandler, addStateListener }
140
+ static contextType = TerminalWsContext;
141
+
142
+ // 兼容 stub:历史代码大量使用 `this._inputWs.send(JSON.stringify(...))` 和
143
+ // `this._inputWs.readyState === WebSocket.OPEN`。getter 返回一个轻量对象映射到 context API,
144
+ // 这样现有发送/状态检查代码无需逐处替换(只删掉 connectInputWs/onclose/close 路径)。
145
+ // ws 实例由 Provider 持有,readyState 仅区分 OPEN(1) / CLOSED(3),不暴露 CONNECTING/CLOSING 中间态。
146
+ get _inputWs() {
147
+ const ctx = this.context;
148
+ if (!ctx || typeof ctx.send !== 'function') return null;
149
+ return {
150
+ get readyState() { return ctx.isOpen && ctx.isOpen() ? WebSocket.OPEN : WebSocket.CLOSED; },
151
+ send: (s) => {
152
+ let obj;
153
+ try { obj = JSON.parse(s); } catch { return false; }
154
+ return ctx.send(obj);
155
+ },
156
+ };
157
+ }
158
+
159
+ constructor(props) {
160
+ super(props);
161
+ this.containerRef = React.createRef();
162
+ this.virtuosoRef = React.createRef();
163
+ this.splitContainerRef = React.createRef();
164
+ this.innerSplitRef = React.createRef();
165
+
166
+ // 增量 tool result 状态
167
+ this._incToolState = null;
168
+ this._incToolProcessedCount = 0;
169
+ this._incToolSessionIdx = -1;
170
+ this._prevSessions = null;
171
+
172
+ // requests 扫描增量缓存。按职责分 3 类,增量扫描时只追加不回溯:
173
+ //
174
+ // ── 扫描游标 ──
175
+ // processedCount : requests 已扫到的位置(下次从此继续)
176
+ // subAgentProcessedCount: subAgentEntries 的扫描游标(由 buildAllItems 写入)
177
+ // tsToIndex : req.timestamp → requests 数组下标;per-message modelInfo 解析的中转
178
+ //
179
+ // ── 模型状态机(区分"活跃 vs 已完成")──
180
+ // modelName : 最后扫到的 MainAgent req.body.model。即使 req 还在流式(没 response)也更新。
181
+ // 用于侧边栏角色菜单、权限模态等"需要最新态"的消费者。
182
+ // completedModelName : 仅当 req.response 存在时才更新的 model。在流式进行中永远指向"上一次已完成的模型",
183
+ // 用于 SSE streaming avatar —— 继承已完成而非推测 in-flight。避免头像闪烁。
184
+ // modelNameByReqIdx[i] : 每个 request 下标"当时活跃"的模型名(carry-over:无 body.model 的 req 继承前一个)。
185
+ // per-message avatar 解析:tsToIndex[ts] → modelNameByReqIdx[idx] → getModelInfo。
186
+ //
187
+ // ── SubAgent/Teammate 渲染入口 ──
188
+ // subAgentEntries : 非 MainAgent 的 Sub/Teammate 消息渲染数据(时序插入到主列表里)
189
+ this._reqScanCache = { tsToIndex: {}, modelName: null, completedModelName: null, modelNameByReqIdx: [], subAgentEntries: [], processedCount: 0, subAgentProcessedCount: 0, globalIndexState: createEmptyGlobalIndexState(), globalIndexProcessedCount: 0 };
190
+
191
+ // buildAllItems session 级缓存
192
+ // 每项: { session, msgsLen, subCount, items, tsEntries, lastPendingAskId, lastPendingPlanId }
193
+ this._sessionItemCache = [];
194
+ this._itemCacheToggleSig = null;
195
+
196
+ // 文件详情滚动位置快照(写/编辑触发 fileVersion remount 时跨实例传递)。
197
+ // 故意用 instance ref 而非 React state:onScroll throttle 100ms 期间会高频写入,
198
+ // 走 setState 会反复 re-render ChatView/FileContentView 拖累滚动顺滑度。
199
+ // FileContentView 通过 props.getFileScrollSnapshot()(闭包稳定)按需读取。
200
+ // 形态:{ path, viewerType: 'code'|'markdown'|'mdx', line?: number, percent?: number } | null
201
+ this._fileScrollSnapshot = null;
202
+
203
+
204
+ // 从 localStorage 读取用户偏好的终端宽度(像素)
205
+ const savedWidth = localStorage.getItem(TERMINAL_WIDTH_STORAGE_KEY);
206
+ const initialTerminalWidth = savedWidth ? parseFloat(savedWidth) : null;
207
+
208
+ this.state = {
209
+ visibleCount: 0,
210
+ loading: false,
211
+ allItems: [],
212
+ highlightTs: null,
213
+ highlightFading: false,
214
+ highlightVisibleIdx: -1,
215
+ sidebarWidth: parseInt(localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY), 10) || 240,
216
+ terminalWidth: initialTerminalWidth || 624, // 默认 80cols * 7.8px
217
+ needsInitialSnap: initialTerminalWidth === null, // 标记是否需要初始化吸附
218
+ inputEmpty: true,
219
+ pendingInput: null,
220
+ stickyBottom: true,
221
+ userScrolling: false, // 用户滚动意图暂停窗口(wheel/touch/pointer 拖动进行中或空窗内)
222
+ ptyPrompt: null,
223
+ ptyPromptHistory: [],
224
+ inputSuggestion: null,
225
+ fileExplorerOpen: !isMobile
226
+ ? localStorage.getItem('cxv_fileExplorerOpen') !== 'false'
227
+ : (isPad ? localStorage.getItem('cxv_fileExplorerOpen') === 'true' : false),
228
+ currentFile: null,
229
+ currentGitDiff: null,
230
+ scrollToLine: null,
231
+ fileExplorerExpandedPaths: loadExpandedPaths(props.projectName),
232
+ gitChangesOpen: false,
233
+ hasGit: true,
234
+ snapLines: [],
235
+ activeSnapLine: null,
236
+ isDragging: false,
237
+ fileVersion: 0, // 用于强制 FileContentView 重新挂载
238
+ editorSessionId: null, // active $EDITOR session
239
+ editorFilePath: null,
240
+ fileExplorerRefresh: 0,
241
+ gitChangesRefresh: 0,
242
+ roleFilterOpen: false,
243
+ roleFilterSelected: new Set(),
244
+ teamModalSession: null,
245
+ workflowModalRun: null,
246
+ mdLightboxSrc: null,
247
+ streamingFading: false,
248
+ stopOptimistic: false, // 点「停止」后乐观置 true:按钮/指示器立即切非运行态,真实 isStreaming 翻 false 或兜底超时后清除
249
+ presetItems: [],
250
+ mobilePresetModalVisible: false,
251
+ localAskAnswers: {}, // 提交后的本地答案映射,用于 request_user_input 立即切换到已回答状态
252
+ pendingPermission: null, // { id, toolName, input } — active permission approval request
253
+ permissionQueue: [], // queued permission requests when one is already active
254
+ pendingPlanApproval: null, // { id, input } — active plan approval in SDK mode
255
+ pendingAsk: null, // { id, questions } — mirrored React state for global modal. _askHookQuestions / _sdkAskId 仍是提交路径权威源(handleAskQuestionSubmit 用于路由 SDK / hook bridge / PTY 三条提交路径)。
256
+ askQueue: [], // queued asks ({ id, questions, kind: ASK_KIND.HOOK|SDK }) when one is already active — mirrors permissionQueue. server.js pendingAskHooks Map 来源;hook bridge 现已 id 多路复用,sub-agent 并发 / 上一轮没答的 ask 不再阻塞下一轮。
257
+ askMetaMap: {}, // { [askId]: { startedAt, timeoutMs } } — 倒计时元数据。ask-pending 时 add,
258
+ // ask resolve/cancel/timeout 时 delete,确保内存随 ask 生命周期回收(不会随会话增长无界累积)。
259
+ pendingPtyPlan: null, // { id, prompt } — active plan approval. id 与 plan tool_use id (lastPendingPlanId) 同源,由 componentDidUpdate 从 _currentLastPendingPlanId 派生(cliMode 守卫 + _resolvedPlanIds 短暂窗口守卫)。
260
+ planAutoApproveCountdown: null, // number|null — 「Plan 自动审批」开启时当前 pendingPtyPlan 的剩余秒数;null=未在倒计时。下发给 inline 卡片显示「{n}s 后自动批准 · 取消」。
261
+ pendingImages: [], // [{ path, source }] — images uploaded/pasted, shown as previews in chat input
262
+ uploadingItems: [], // [{ id, name, previewUrl }] — 上传在途的占位项(路径还没 resolve);预览条只从 pendingImages 渲染缩略图,故占位需独立 state
263
+ sendDeferred: false, // 仅渲染用:有上传在途时按了发送 → 缓发态(发送按钮 spinner)。权威双发标志是实例字段 _sendDeferred
264
+ ultraplanModalOpen: false, // 移动端专用(桌面走 ultraplanPopoverOpen 的终端同款弹层)
265
+ ultraplanVariant: 'codeExpert',
266
+ ultraplanPrompt: '',
267
+ ultraplanFiles: [],
268
+ // UltraPlan modal 拖拽 resize 后的尺寸,持久化到 localStorage 两个 key
269
+ // (与 PanelResizer 单值风格一致);手机模式不读不写,初值固定 null。
270
+ ultraplanModalSize: _readUltraplanModalSize(),
271
+ customUltraplanExperts: [],
272
+ customUltraplanEditOpen: false,
273
+ customUltraplanEditing: null,
274
+ // 受控的 token 统计 hover 弹层:点开「所有工具」(?) 目录时需主动收起本弹层
275
+ tokenStatsPopoverOpen: false,
276
+ // 桌面隐藏终端的输入栏 UltraPlan 弹层(与终端工具栏共用 UltraplanPanel)
277
+ ultraplanPopoverOpen: false,
278
+ ultraplanPopoverSize: readUltraplanPopoverSize(), // 与终端弹层共享同一对 localStorage key
279
+ ultraplanExpertOrder: [],
280
+ ultraplanExpertHidden: [],
281
+ ultraplanManagerOpen: false,
282
+ ultraplanLightbox: null,
283
+ ultraplanConfirming: false,
284
+ // plan input.planFilePath 异步读盘缓存:{ [planFilePath]: content }
285
+ planFileContents: {},
286
+ };
287
+ this._projectDirCache = null; // 缓存项目目录绝对路径(toolFileChangeController 经 host 读)
288
+ // 关闭期间累积的修改信号;下次面板打开时消费一次(与 setState 一并触发刷新计数 +1)。
289
+ // toolFileChangeController 经 host.setPendingXxxRefresh 写,_setFileExplorerOpen / render Git 开关消费。
290
+ this._pendingFileRefresh = false;
291
+ this._pendingGitRefresh = false;
292
+ this._queueTimer = null;
293
+ this._prevItemsLen = 0;
294
+ this._scrollTargetIdx = null;
295
+ this._scrollTargetRef = React.createRef();
296
+ // _inputWs 现在是 getter(挂在原型),不在 constructor 上设字段,避免覆盖 getter
297
+ this._unsubWsHandler = null;
298
+ this._unsubWsState = null;
299
+ this._inputRef = React.createRef();
300
+ this._stopOptimisticTimer = null; // 乐观停止兜底定时器
301
+ // PTY byte-stream state (buffer/carry/debounce/current-prompt mirror/auto-allow
302
+ // dedupe) is owned by this._ptyPrompt (PtyPromptController), see end of constructor.
303
+ // 全部 ask 状态机字段 + 方法由 this._askFlow(AskFlowController)持有,见构造末尾。
304
+ this._mobileExtraItems = 0;
305
+ this._mobileSliceOffset = 0;
306
+ this._totalItemCount = 0;
307
+ this._autoFillRafId = null; // logfile 只读模式自动渐进扩窗的 rAF 句柄(_maybeScheduleLocalLogAutoFill)
308
+
309
+ // PTY plan modal 触发:用 lastPendingPlanId(plan tool_use id)作权威信号源(与 inline 卡片同源)。
310
+ // _currentLastPendingPlanId 由 buildAllItems 末尾镜像;componentDidUpdate 派生 pendingPtyPlan。
311
+ // _resolvedPlanIds 守卫用户已操作但 JSONL 还没回写 planApprovalMap 的短暂窗口(防 modal 闪回)。
312
+ this._currentLastPendingPlanId = null;
313
+ this._resolvedPlanIds = new Set();
314
+ this._lastObservedLpid = null;
315
+ // 「Plan 自动审批」倒计时:_planAutoTimer 当前 setInterval 句柄;_planAutoCancelled 记录用户手动取消过的 plan id(避免取消后又重启)。
316
+ this._planAutoTimer = null;
317
+ this._planAutoCancelled = new Set();
318
+ // Plan V2 文件型 plan 的异步内容缓存(input.planFilePath → 文件正文)
319
+ this._planFileFetches = new Set();
320
+ this._streamSpinnerUrl = props.isStreaming
321
+ ? (Math.random() < 0.5 ? orbitingUrl : shimmerUrl)
322
+ : null;
323
+ this._unmounted = false;
324
+ // 流式吸底控制器:收敛 7 处 scrollTop 写入、3 套并行机制、引用计数 lock、用户滚动意图暂停窗口
325
+ // 详见 src/utils/stickyBottomController.js + plan modular-floating-hopper.md (v2.1)
326
+ this._stickyController = new StickyBottomController({
327
+ getSticky: () => this.state.stickyBottom,
328
+ setSticky: (v) => {
329
+ if (this._unmounted) return;
330
+ if (this.state.stickyBottom === v) return;
331
+ this.setState({ stickyBottom: v });
332
+ },
333
+ getMode: () => useVirtuoso ? 'virtuoso' : 'desktop',
334
+ // 用户滚动窗口开/关沿 → state.userScrolling,驱动 Virtuoso followOutput 门控
335
+ onUserScrollChange: (active) => {
336
+ if (this._unmounted) return;
337
+ if (this.state.userScrolling === active) return;
338
+ this.setState({ userScrolling: active });
339
+ },
340
+ });
341
+
342
+ // ── Ask 问答流状态机控制器(逻辑从 ChatView 抽出,见 ./controllers/askFlowController.js)──
343
+ // host 适配器把 ChatView 的 state / props / ws / PTY 字段桥接给控制器;ChatView 的 ask 方法
344
+ // 退化为一行委托。state 仍留在 ChatView.state,行为不变。
345
+ this._askFlow = new AskFlowController({
346
+ getState: () => this.state,
347
+ setState: (updater, cb) => this.setState(updater, cb),
348
+ getProps: () => this.props,
349
+ ws: () => this._inputWs,
350
+ ctxSend: (obj) => this.context?.send?.(obj),
351
+ ctxIsOpen: () => this.context?.isOpen?.(),
352
+ addMessageHandler: (fn) => this.context.addMessageHandler(fn),
353
+ getCurrentPtyPrompt: () => this._ptyPrompt.getCurrent(),
354
+ setCurrentPtyPrompt: (v) => this._ptyPrompt.setCurrent(v),
355
+ clearPtyDebounce: () => this._ptyPrompt.clearDebounce(),
356
+ sendUserMessageImmediate: (text, ta, skip) => this._sendUserMessageImmediate(text, ta, skip),
357
+ takePendingFlush: (askId) => {
358
+ if (!this._pendingFlushQueue || this._pendingFlushQueue.length === 0) return null;
359
+ const idx = this._pendingFlushQueue.findIndex(e => e.askId === askId);
360
+ if (idx < 0) return null;
361
+ const entry = this._pendingFlushQueue.splice(idx, 1)[0];
362
+ if (entry?.tid) clearTimeout(entry.tid);
363
+ return entry;
364
+ },
365
+ isUnmounted: () => this._unmounted,
366
+ fetchPendingAsks: () => fetch(apiUrl('/api/pending-asks')).then(r => r.ok ? r.json() : null),
367
+ notifyAskResolved: (payload) => {
368
+ try {
369
+ if (typeof window !== 'undefined' && window.tabBridge?.notifyAskResolved) {
370
+ window.tabBridge.notifyAskResolved({ tabId: this.props.ownTabId ?? null, ...payload });
371
+ }
372
+ } catch {}
373
+ },
374
+ // PTY 提交失败的用户提示(antd Modal + i18n + JSX 留在 ChatView,控制器保持 node:test 可 import)
375
+ warnSubmitRetry: (reason) => {
376
+ try {
377
+ Modal.warning({
378
+ title: t('ui.askSubmitRetryHint'),
379
+ content: (
380
+ <div>
381
+ <div style={{ whiteSpace: 'pre-line' }}>{t('ui.askSubmitFailedDetail')}</div>
382
+ <div style={{ marginTop: 8, fontSize: 11, color: 'var(--text-muted)' }}>[reason] {String(reason || 'unknown')}</div>
383
+ </div>
384
+ ),
385
+ });
386
+ } catch {
387
+ try { message.warning(t('ui.askSubmitRetryHint')); } catch {}
388
+ }
389
+ },
390
+ });
391
+
392
+ // UltraPlan 文件 / 自定义专家纯逻辑(与 TerminalPanel 共享,见 ../utils/ultraplanController)。
393
+ // host 适配器桥接 state / 上传 / 提示 / 偏好 / 关闭编辑器;ChatView 的 6 个方法退化为委托。
394
+ this._ultraplan = new UltraplanController({
395
+ getState: () => this.state,
396
+ setState: (updater) => this.setState(updater),
397
+ onUpdatePreferences: (p) => this.props.onUpdatePreferences?.(p),
398
+ uploadFile: (file) => uploadFileAndGetPath(file),
399
+ messageError: (msg) => message.error(msg),
400
+ closeEditor: () => this._closeCustomUltraplanEditor(),
401
+ });
402
+
403
+ // 跳转高亮「滚动即褪色」(见 ./controllers/scrollHighlightController)。
404
+ this._scrollHighlight = new ScrollHighlightController({
405
+ getScrollContainer: () => this._getScrollContainer(),
406
+ setState: (updater) => this.setState(updater),
407
+ });
408
+
409
+ // 权限审批队列(见 ./controllers/permissionController);state 仍留 ChatView.state。
410
+ this._permission = new PermissionController({
411
+ getState: () => this.state,
412
+ setState: (updater) => this.setState(updater),
413
+ ws: () => this._inputWs,
414
+ promptOptionClick: (n) => this.handlePromptOptionClick(n),
415
+ });
416
+
417
+ // 工具文件变更监听 + LRU 去重(见 ./controllers/toolFileChangeController)。
418
+ this._toolFileMonitor = new ToolFileChangeController({
419
+ getState: () => this.state,
420
+ setState: (updater) => this.setState(updater),
421
+ getProps: () => this.props,
422
+ getProjectDir: () => this._projectDirCache,
423
+ setPendingFileRefresh: () => { this._pendingFileRefresh = true; },
424
+ setPendingGitRefresh: () => { this._pendingGitRefresh = true; },
425
+ });
426
+
427
+ // Terminal/sidebar drag-resize lifecycle (see ./controllers/splitDragController;
428
+ // pure snap geometry in utils/splitDragCalc.js).
429
+ this._splitDrag = new SplitDragController({
430
+ getState: () => this.state,
431
+ setState: (update) => this.setState(update),
432
+ getSplitRect: () => this.innerSplitRef.current?.getBoundingClientRect() ?? null,
433
+ persistWidth: (key, px) => this._persistWidth(key, px),
434
+ });
435
+
436
+ // PTY prompt detection (buffer/carry/debounce/dedupe — see ./controllers/ptyPromptController).
437
+ this._ptyPrompt = new PtyPromptController({
438
+ getState: () => this.state,
439
+ setState: (update, cb) => this.setState(update, cb),
440
+ isAskSubmitting: () => this._askFlow._askSubmitting,
441
+ scrollToBottom: () => this.scrollToBottom(),
442
+ now: () => Date.now(),
443
+ });
444
+ }
445
+
446
+ _setFileExplorerOpen(open) {
447
+ localStorage.setItem('cxv_fileExplorerOpen', String(open));
448
+ if (open && this._pendingFileRefresh) {
449
+ // 关闭期间累积的修改信号在打开瞬间消费一次
450
+ this._pendingFileRefresh = false;
451
+ this.setState(prev => ({
452
+ fileExplorerOpen: true,
453
+ fileExplorerRefresh: prev.fileExplorerRefresh + 1,
454
+ }));
455
+ } else {
456
+ this.setState({ fileExplorerOpen: open });
457
+ }
458
+ }
459
+
460
+ componentDidMount() {
461
+ this.startRender();
462
+ // 注册 ws 消息 handler。Provider 本身根据 cliMode/terminalVisible 决定何时建立 ws,
463
+ // ChatView 不再自己 connect/close;handler 在 ws 重连后会自动继续收到新消息。
464
+ if (this.context && this.context.addMessageHandler) {
465
+ this._unsubWsHandler = this.context.addMessageHandler(this._onTerminalWsMessage);
466
+ }
467
+ if (this.context && this.context.addStateListener) {
468
+ this._unsubWsState = this.context.addStateListener(this._onTerminalWsState);
469
+ }
470
+ // 检测项目是否有 git(优先多仓库 API,回退旧 API)
471
+ fetch(apiUrl('/api/git-repos')).then(r => r.ok ? r.json() : Promise.reject()).then(data => {
472
+ if (!data.repos?.length) this.setState({ hasGit: false, gitChangesOpen: false });
473
+ }).catch(() => {
474
+ fetch(apiUrl('/api/git-status')).then(r => {
475
+ if (!r.ok) this.setState({ hasGit: false, gitChangesOpen: false });
476
+ }).catch(() => this.setState({ hasGit: false, gitChangesOpen: false }));
477
+ });
478
+ // 桌面模式:containerRef 在 first render 后就绪,cdU 第一帧调 controller.bind
479
+ // virtuoso 模式:scrollerRef 回调里 controller.bind
480
+ // touch 守卫与 ResizeObserver 由 controller 内部管理(首次 bind 时统一注册 document touch)
481
+ if (!useVirtuoso && this.containerRef.current) {
482
+ this._stickyController.bind(this.containerRef.current);
483
+ }
484
+ // 初始化时吸附到 60cols
485
+ if (this.state.needsInitialSnap && this.props.cliMode && this.props.terminalVisible) {
486
+ this._snapToInitialPosition();
487
+ }
488
+ // 加载 Agent Team 预置项 (props.preferences 已 ready 时立即加载,
489
+ // 否则 componentDidUpdate 接力。preset 写入由 SettingsContext.updatePreferences
490
+ // setState 触发 props 变化,无需 window event)
491
+ this._loadPresets();
492
+ }
493
+
494
+ shouldComponentUpdate(nextProps, nextState) {
495
+ return (
496
+ nextProps.requests !== this.props.requests ||
497
+ nextProps.mainAgentSessions !== this.props.mainAgentSessions ||
498
+ nextProps.sessionUpperBoundTs !== this.props.sessionUpperBoundTs ||
499
+ nextProps.streamingLatest !== this.props.streamingLatest ||
500
+ nextProps.collapseToolResults !== this.props.collapseToolResults ||
501
+ nextProps.expandThinking !== this.props.expandThinking ||
502
+ nextProps.showFullToolContent !== this.props.showFullToolContent ||
503
+ nextProps.onlyCurrentSession !== this.props.onlyCurrentSession ||
504
+ nextProps.isLocalLog !== this.props.isLocalLog ||
505
+ nextProps.scrollToTimestamp !== this.props.scrollToTimestamp ||
506
+ nextProps.cliMode !== this.props.cliMode ||
507
+ nextProps.terminalVisible !== this.props.terminalVisible ||
508
+ nextProps.userProfile !== this.props.userProfile ||
509
+ nextProps.pendingUploadPaths !== this.props.pendingUploadPaths ||
510
+ nextProps.isStreaming !== this.props.isStreaming ||
511
+ nextProps.hasMoreHistory !== this.props.hasMoreHistory ||
512
+ nextProps.loadingMore !== this.props.loadingMore ||
513
+ nextProps.loadingSessionId !== this.props.loadingSessionId ||
514
+ nextProps.lang !== this.props.lang ||
515
+ nextProps.showThinkingSummaries !== this.props.showThinkingSummaries ||
516
+ nextProps.fileLoading !== this.props.fileLoading ||
517
+ nextProps.preferences !== this.props.preferences ||
518
+ // 审批代理要直达终端工具栏快捷菜单
519
+ nextProps.approvalsReviewer !== this.props.approvalsReviewer ||
520
+ nextProps.planAutoApproveSeconds !== this.props.planAutoApproveSeconds ||
521
+ nextState !== this.state
522
+ );
523
+ }
524
+
525
+ componentDidUpdate(prevProps, prevState) {
526
+ if (prevProps.isStreaming !== this.props.isStreaming) {
527
+ this._streamSpinnerUrl = this.props.isStreaming
528
+ ? (Math.random() < 0.5 ? orbitingUrl : shimmerUrl)
529
+ : null;
530
+ }
531
+ // workspace 切换:projectName 变了 → 按新 key 重新 hydrate;scroll 快照同样作废。
532
+ // ChatView 是 class 组件不能直接用 useSessionStoragePersistedSet hook,这里手抄同款
533
+ // 守卫语义:`空 → 非空` 是 /api/project-name 异步到达的延迟初始化,**跳过 rehydrate**
534
+ // 保留 ctor lazy load 后用户在此窗口内的内存操作;非空 → 非空 / 非空 → 空 才是真切换。
535
+ // (scroll 快照无所谓延迟初始化,工作流上用户必须先打开文件才有快照,每次都清安全。)
536
+ if (prevProps.projectName !== this.props.projectName) {
537
+ if (prevProps.projectName) {
538
+ this.setState({ fileExplorerExpandedPaths: loadExpandedPaths(this.props.projectName) });
539
+ }
540
+ this._fileScrollSnapshot = null;
541
+ }
542
+ // currentFile 变(含切到 null)→ 上一文件的 scroll 快照失效,集中在 cdU 一处清,
543
+ // 比在 8 个 setState({currentFile:...}) 站点各加一行更稳。fileVersion bump 走的是同
544
+ // currentFile 的 remount 路径,currentFile 不变 → 快照保留 → 用于 restore,正合需求。
545
+ if (prevState.currentFile !== this.state.currentFile) {
546
+ this._fileScrollSnapshot = null;
547
+ }
548
+ // SettingsContext 异步 fetch 完成后,props.preferences 才到达;这里接力加载 AgentTeam 预置。
549
+ if (prevProps.preferences !== this.props.preferences) {
550
+ this._loadPresets();
551
+ }
552
+ // 扫描 messages 中所有 plan tool_use 的 input.planFilePath,按需异步拉取磁盘内容
553
+ // 仅在 messages 引用变化时遍历(O(N) 低频),fetch 去重 + _unmounted 守卫
554
+ if (prevProps.messages !== this.props.messages) {
555
+ const messages = this.props.messages || [];
556
+ const cache = this.state.planFileContents || {};
557
+ for (const msg of messages) {
558
+ if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
559
+ for (const blk of msg.content) {
560
+ if (!blk || blk.type !== 'tool_use' || !isPlanToolName(blk.name)) continue;
561
+ const inp = blk.input || {};
562
+ // 已有内联 plan 不需要读盘(input.plan 是首选)
563
+ if (typeof inp.plan === 'string' && inp.plan.trim()) continue;
564
+ const fp = inp.planFilePath;
565
+ if (typeof fp !== 'string' || !fp) continue;
566
+ if (cache[fp] !== undefined) continue;
567
+ if (this._planFileFetches.has(fp)) continue;
568
+ this._planFileFetches.add(fp);
569
+ fetch(apiUrl(`/api/plan-file?path=${encodeURIComponent(fp)}`))
570
+ .then(r => r.ok ? r.json() : null)
571
+ .then(j => {
572
+ if (this._unmounted) return;
573
+ if (j && j.ok && typeof j.content === 'string') {
574
+ this.setState(s => ({ planFileContents: { ...(s.planFileContents || {}), [fp]: j.content } }));
575
+ }
576
+ })
577
+ .catch(() => { /* 文件不存在/拒绝 → 静默回退到其他源 */ });
578
+ }
579
+ }
580
+ }
581
+ // 派生 pendingPtyPlan from _currentLastPendingPlanId(与 inline 卡片同源)。
582
+ // _currentLastPendingPlanId 由 buildAllItems 末尾镜像;只在 cliMode 下生效(红线 2:sdkMode 走 inline-only)。
583
+ // _resolvedPlanIds 守用户已操作但 JSONL 还没回写 planApprovalMap 的短暂窗口(提交后到 tool_result 落盘之间),
584
+ // 防 CDU 把 modal 重弹;过期项在 lpid 变化时(即旧 plan 真正 resolve)下面分支自动清出。
585
+ {
586
+ const lpid = this._currentLastPendingPlanId || null;
587
+ const curPtyPlan = this.state.pendingPtyPlan;
588
+ const skipped = lpid && this._resolvedPlanIds.has(lpid);
589
+ if (this.props.cliMode && lpid && !skipped) {
590
+ if (!curPtyPlan || curPtyPlan.id !== lpid) {
591
+ this.setState({ pendingPtyPlan: { id: lpid, prompt: null } });
592
+ }
593
+ } else if (curPtyPlan) {
594
+ this.setState({ pendingPtyPlan: null });
595
+ }
596
+ // lpid 变化才更新 _lastObservedLpid + 清出过期项;不变时 no-op 节省每帧无谓写
597
+ if (this._lastObservedLpid !== lpid) {
598
+ if (this._lastObservedLpid) {
599
+ this._resolvedPlanIds.delete(this._lastObservedLpid);
600
+ }
601
+ this._lastObservedLpid = lpid;
602
+ }
603
+ }
604
+ // 通知父组件权限审批状态变化(用于移动端全局浮层)
605
+ if (prevState.pendingPermission !== this.state.pendingPermission && this.props.onPendingPermission) {
606
+ if (this.state.pendingPermission) {
607
+ this.props.onPendingPermission({
608
+ permission: this.state.pendingPermission,
609
+ modelName: this._reqScanCache?.modelName,
610
+ handlers: {
611
+ allow: this.handlePermissionAllow,
612
+ allowSession: this.props.sdkMode ? this.handlePermissionAllowSession : null,
613
+ deny: this.handlePermissionDeny,
614
+ },
615
+ });
616
+ } else {
617
+ this.props.onPendingPermission(null);
618
+ }
619
+ }
620
+ if (prevState.pendingPlanApproval !== this.state.pendingPlanApproval && this.props.onPendingPlanApproval) {
621
+ if (this.state.pendingPlanApproval) {
622
+ this.props.onPendingPlanApproval({
623
+ plan: this.state.pendingPlanApproval,
624
+ handlers: { approve: this.handlePlanApprove, reject: this.handlePlanReject },
625
+ });
626
+ // SDK plan approval lives in an inline card, not the global ApprovalModal, so
627
+ // ApprovalModal's sound effect never fires for this path. Hook the voice pack
628
+ // here directly( — SDK plan was silent before this).
629
+ try {
630
+ const vp = this.props.preferences?.approvalModal?.voicePack;
631
+ if (vp && vp.enabled && vp.events && vp.events.planApproval) {
632
+ playVoiceEvent('planApproval', vp, { dedupeKey: `planApproval:${this.state.pendingPlanApproval.id}` });
633
+ }
634
+ } catch { /* never throw from componentDidUpdate */ }
635
+ } else {
636
+ this.props.onPendingPlanApproval(null);
637
+ }
638
+ }
639
+ if (prevState.pendingAsk !== this.state.pendingAsk) {
640
+ if (this.props.onPendingAsk) {
641
+ if (this.state.pendingAsk) {
642
+ this.props.onPendingAsk({
643
+ ask: this.state.pendingAsk,
644
+ handlers: { submit: this.handleAskQuestionSubmit, cancel: this.handleAskCancel },
645
+ });
646
+ } else {
647
+ this.props.onPendingAsk(null);
648
+ }
649
+ }
650
+ // ask transition 时显式通知 main 清聚合状态。覆盖两种迁移:
651
+ // (a) prev 有 → 当前 null(resolve / unmount / WS 断连 兜底)
652
+ // (b) prev 有 → 当前也有但 id 变了(head swap,promote 队列下一个 ask 上头时旧 id 必须先释放)
653
+ // 正常 resolve 路径 server 已 process.send 过 (a),(b) 是新增的多并发场景下 Electron pendingByTab
654
+ // Map<id> 同步必需,否则 main 进程 dock badge / flashFrame 仍认为旧 ask 在挂着。
655
+ try {
656
+ if (typeof window !== 'undefined' && window.tabBridge?.notifyAskResolved
657
+ && prevState.pendingAsk
658
+ && (this.state.pendingAsk == null
659
+ || this.state.pendingAsk.id !== prevState.pendingAsk.id)) {
660
+ window.tabBridge.notifyAskResolved({
661
+ id: prevState.pendingAsk.id,
662
+ tabId: this.props.ownTabId ?? null,
663
+ });
664
+ }
665
+ } catch {}
666
+ }
667
+ // PTY plan transitions — bubble up so the global modal can render, AND notify the Electron main
668
+ // process via tabBridge so it can flashFrame / setBadgeCount / fire OS Notification (parallel to
669
+ // the server.js -> tab-worker process.send route used for ask).
670
+ if (prevState.pendingPtyPlan !== this.state.pendingPtyPlan) {
671
+ if (this.props.onPendingPtyPlan) {
672
+ if (this.state.pendingPtyPlan) {
673
+ this.props.onPendingPtyPlan({
674
+ ptyPlan: this.state.pendingPtyPlan,
675
+ handlers: {
676
+ approve: this.handlePromptOptionClick,
677
+ feedbackSubmit: this.handlePlanFeedbackSubmit,
678
+ },
679
+ });
680
+ } else {
681
+ this.props.onPendingPtyPlan(null);
682
+ }
683
+ }
684
+ try {
685
+ if (typeof window !== 'undefined' && window.tabBridge) {
686
+ if (this.state.pendingPtyPlan) {
687
+ window.tabBridge.notifyPtyPlanPending?.({
688
+ id: this.state.pendingPtyPlan.id,
689
+ payload: { projectName: this.props.projectName || '' },
690
+ tabId: this.props.ownTabId ?? null,
691
+ });
692
+ } else if (prevState.pendingPtyPlan) {
693
+ window.tabBridge.notifyPtyPlanResolved?.({
694
+ id: prevState.pendingPtyPlan.id,
695
+ tabId: this.props.ownTabId ?? null,
696
+ });
697
+ }
698
+ }
699
+ } catch {}
700
+ }
701
+ // 「Plan 自动审批」倒计时生命周期:新 plan 出现(id 变化)→ 启动倒计时;plan 消失/开关关闭 → 清理。
702
+ // 守卫 planChanged||enableChanged 保证每次 CDU 只在相关变化时才介入,开销可忽略。
703
+ {
704
+ const curPlanId = this.state.pendingPtyPlan?.id ?? null;
705
+ const prevPlanId = prevState.pendingPtyPlan?.id ?? null;
706
+ const planChanged = prevPlanId !== curPlanId;
707
+ const enableChanged = prevProps.planAutoApproveSeconds !== this.props.planAutoApproveSeconds;
708
+ // plan 切换时清掉旧 id 的取消记录,避免 _planAutoCancelled 无界增长(同 _resolvedPlanIds 的清理思路)。
709
+ if (planChanged && prevPlanId) this._planAutoCancelled.delete(prevPlanId);
710
+ if (planChanged || enableChanged) {
711
+ if (!curPlanId || !this.props.planAutoApproveSeconds) {
712
+ this._clearPlanAutoApprove();
713
+ } else if (planChanged) {
714
+ this._clearPlanAutoApprove();
715
+ this._startPlanAutoApprove(curPlanId);
716
+ } else if (enableChanged && !this._planAutoTimer) {
717
+ // 用户在 plan 已 pending 时把开关打开 → 补启动倒计时。
718
+ this._startPlanAutoApprove(curPlanId);
719
+ }
720
+ }
721
+ }
722
+ // 同样:Live streaming overlay 变化时也要重新吸底(丝滑缓动,避免每个 chunk 画面硬跳)。
723
+ // pin 锁在更早会话时(sessionUpperBoundTs != null),streaming 来自未展示的更新会话,不应抢滚动。
724
+ if (prevProps.streamingLatest !== this.props.streamingLatest && this.state.stickyBottom && this.props.sessionUpperBoundTs == null) {
725
+ const el = useVirtuoso ? this._virtuosoScrollerEl : this.containerRef.current;
726
+ if (el) this._stickyController.startSmoothFollow(el);
727
+ }
728
+ // Streaming border fade-out: when isStreaming goes from true to false, trigger fade
729
+ if (prevProps.isStreaming && !this.props.isStreaming) {
730
+ this.setState({ streamingFading: true });
731
+ clearTimeout(this._streamingFadeTimer);
732
+ this._streamingFadeTimer = setTimeout(() => {
733
+ this.setState({ streamingFading: false });
734
+ }, 500);
735
+ // 真实停止落地:清除乐观标志与兜底定时器(正常路径,先于 4s 兜底)
736
+ if (this.state.stopOptimistic) this._clearStopOptimistic();
737
+ }
738
+ // 如果 streaming 在 fade-out 期间恢复,立即取消 fade 避免 spinner 以 opacity:0 显示
739
+ if (!prevProps.isStreaming && this.props.isStreaming && this.state.streamingFading) {
740
+ clearTimeout(this._streamingFadeTimer);
741
+ this.setState({ streamingFading: false });
742
+ }
743
+ // 新一轮在「本端 handleInputSend 之外」发起(中断后队列消息自动续发 / 其他 client / 直接 PTY 键入):
744
+ // isStreaming 升起的上升沿即视为陈旧乐观标志失效,立即清除——否则 uiStreaming 会被旧 stopOptimistic
745
+ // 压住,最长到 4s 兜底才恢复,期间无 spinner、无停止按钮。
746
+ if (!prevProps.isStreaming && this.props.isStreaming && this.state.stopOptimistic) {
747
+ this._clearStopOptimistic();
748
+ }
749
+ // Handle files dropped onto the app — add to pendingImages, send at submit time
750
+ if (this.props.pendingUploadPaths && this.props.pendingUploadPaths.length > 0
751
+ && this.props.pendingUploadPaths !== prevProps.pendingUploadPaths) {
752
+ for (const p of this.props.pendingUploadPaths) {
753
+ const raw = p.replace(/^"|"$/g, '');
754
+ this._addPendingImage(raw, 'drop');
755
+ }
756
+ if (this.props.onUploadPathsConsumed) this.props.onUploadPathsConsumed();
757
+ }
758
+ // 拖拽上传在途占位:AppBase 在 _onDrop 里维护 uploadingDrop([{id,name,url}]),与 pendingUploadPaths
759
+ // 同款 prop-diff 调谐进本端 uploadingItems(按 id 增删,移除时撤销 objectURL)。
760
+ if (this.props.uploadingDrop !== prevProps.uploadingDrop) {
761
+ const cur = this.props.uploadingDrop || [];
762
+ const prevDrop = prevProps.uploadingDrop || [];
763
+ const curIds = new Set(cur.map(d => d.id));
764
+ for (const d of cur) {
765
+ if (!this.state.uploadingItems.some(u => u.id === d.id)) {
766
+ this._applyUploading({ type: 'add', item: { id: d.id, name: d.name, previewUrl: d.url } });
767
+ }
768
+ }
769
+ for (const d of prevDrop) {
770
+ if (!curIds.has(d.id)) this._applyUploading({ type: 'remove', id: d.id });
771
+ }
772
+ }
773
+ // 缓发 drain:在途上传清零(且确为「从有到无」的下降沿)→ 自动重跑 handleInputSend(此时 pendingImages 已含路径)。
774
+ // 超时已先把 _sendDeferred 置 false,故超时后才 resolve 不会触发偷偷重发。
775
+ if (this._sendDeferred
776
+ && this.state.uploadingItems.length === 0
777
+ && prevState.uploadingItems.length > 0) {
778
+ this._sendDeferred = false;
779
+ clearTimeout(this._sendDeferTimer);
780
+ this.setState({ sendDeferred: false }, () => {
781
+ if (this._unmounted) return;
782
+ this.handleInputSend();
783
+ });
784
+ }
785
+ if (prevProps.mainAgentSessions !== this.props.mainAgentSessions) {
786
+ // sessions 引用变化 → 仅在 session 对象真正变化时重置增量状态
787
+ // Plan 1 的 push 模式下,外层数组是浅拷贝(新引用),但 session 对象不变 → 保留增量状态
788
+ if (this.props.mainAgentSessions !== this._prevSessions) {
789
+ const prev = this._prevSessions || [];
790
+ const next = this.props.mainAgentSessions || [];
791
+ const sessionsActuallyChanged = prev.length !== next.length ||
792
+ prev.some((s, i) => s !== next[i]);
793
+ if (sessionsActuallyChanged) {
794
+ this._incToolState = null;
795
+ this._incToolProcessedCount = 0;
796
+ this._incToolSessionIdx = -1;
797
+ this._reqScanCache = { tsToIndex: {}, modelName: null, completedModelName: null, modelNameByReqIdx: [], subAgentEntries: [], processedCount: 0, subAgentProcessedCount: 0, globalIndexState: createEmptyGlobalIndexState(), globalIndexProcessedCount: 0 };
798
+ this._sessionItemCache = [];
799
+ // 会话/工作区切换:清除乐观停止标志,避免陈旧 true 泄漏到新会话
800
+ if (this.state.stopOptimistic) this._clearStopOptimistic();
801
+ }
802
+ this._prevSessions = this.props.mainAgentSessions;
803
+ }
804
+ // 会话/工作区切换:复位「加载更早」展开量,新会话从最近窗口开始(移动端+桌面端一致)
805
+ this._mobileExtraItems = 0;
806
+ this.startRender();
807
+ if (this.state.pendingInput) {
808
+ this.setState({ pendingInput: null });
809
+ }
810
+ this._clearPendingImages();
811
+ this._updateSuggestion();
812
+ this._toolFileMonitor.check();
813
+ } else if (prevProps.requests !== this.props.requests) {
814
+ // requests(filteredRequests)变化但 mainAgentSessions 未变 → 重建 tsToIndex 避免索引偏移
815
+ this._reqScanCache.tsToIndex = {};
816
+ this._reqScanCache.modelName = null;
817
+ this._reqScanCache.completedModelName = null;
818
+ this._reqScanCache.modelNameByReqIdx = [];
819
+ this._reqScanCache.processedCount = 0;
820
+ this._reqScanCache.subAgentEntries = [];
821
+ this._reqScanCache.subAgentProcessedCount = 0;
822
+ this._reqScanCache.globalIndexState = createEmptyGlobalIndexState();
823
+ this._reqScanCache.globalIndexProcessedCount = 0;
824
+ this.startRender();
825
+ // subAgent / teammate 的 tool_result 只走 requests 路径(不进 mainAgentSessions),
826
+ // 必须在这里也调一次刷新检查,否则它们的文件修改完全感知不到
827
+ this._toolFileMonitor.check();
828
+ } else if (prevProps.collapseToolResults !== this.props.collapseToolResults
829
+ || prevProps.expandThinking !== this.props.expandThinking
830
+ || prevProps.showFullToolContent !== this.props.showFullToolContent
831
+ || prevProps.showThinkingSummaries !== this.props.showThinkingSummaries
832
+ || prevProps.onlyCurrentSession !== this.props.onlyCurrentSession
833
+ || prevProps.sessionUpperBoundTs !== this.props.sessionUpperBoundTs) {
834
+ // 这些显示开关只改 buildAllItems 的输出,不改 mainAgentSessions/requests 引用,故上面两条增量
835
+ // 分支不会触发。allItems 缓存在 state(render 读 state.allItems),必须在此显式重建,否则切换开关
836
+ // 后 SCU 虽放行 re-render,画面仍是旧 allItems([对话] 与派生它的 [用户 Prompt 导航] 都不刷新)。
837
+ const rawItems = this.buildAllItems();
838
+ const allItems = this._applyMobileSlice(rawItems);
839
+ this.setState({ allItems, visibleCount: allItems.length });
840
+ }
841
+ // localAskAnswers 变化时重建消息,使交互表单切换到已回答的静态视图
842
+ if (prevState.localAskAnswers !== this.state.localAskAnswers &&
843
+ prevProps.mainAgentSessions === this.props.mainAgentSessions &&
844
+ prevProps.requests === this.props.requests) {
845
+ this.startRender();
846
+ }
847
+ // scrollToTimestamp 变化时(如从 raw 模式切回 chat),重建 items 并滚动定位
848
+ if (!prevProps.scrollToTimestamp && this.props.scrollToTimestamp) {
849
+ // 跳转目标若落在被裁剪(未渲染)的更早区域,先展开 _mobileExtraItems 把它纳入可见窗口,
850
+ // 否则跳转/搜索定位会失败。移动端与桌面端统一处理(桌面端启用渲染窗口裁剪后同样需要)。
851
+ const rawItems = this.buildAllItems();
852
+ const targetIdx = this._scrollTargetIdx;
853
+ if (targetIdx != null) {
854
+ const limit = ITEM_LIMIT + this._mobileExtraItems;
855
+ const offset = rawItems.length > limit ? rawItems.length - limit : 0;
856
+ if (targetIdx < offset) {
857
+ this._mobileExtraItems = rawItems.length - targetIdx - ITEM_LIMIT;
858
+ if (this._mobileExtraItems < 0) this._mobileExtraItems = 0;
859
+ }
860
+ }
861
+ const allItems = this._applyMobileSlice(rawItems);
862
+ this.setState({ allItems, visibleCount: allItems.length }, () => this.scrollToBottom());
863
+ }
864
+ // mobileChatVisible: scroll to bottom when becoming visible
865
+ if (isMobile && this.props.mobileChatVisible && !prevProps.mobileChatVisible) {
866
+ // 显式动作(面板切显示):清除用户滚动窗口,贴底不被残留窗口抑制
867
+ this._stickyController.resetUserScrollState();
868
+ requestAnimationFrame(() => {
869
+ if (this.virtuosoRef.current) {
870
+ this.virtuosoRef.current.scrollToIndex({ index: 'LAST' });
871
+ } else {
872
+ const el = this.containerRef.current;
873
+ if (el) this._stickyController.writeUnderLock(el, el.scrollHeight);
874
+ }
875
+ });
876
+ }
877
+ // 不再在此建立 ws — Provider 通过 props.open 派生(cliMode || terminalVisible)集中管理
878
+ if (!useVirtuoso) this._stickyController.bind(this.containerRef.current);
879
+ // logfile 只读模式:非虚拟化平台自动渐进扩窗(替代手工「加载更多」),每步 setState 后
880
+ // 经本钩子接力调度下一步,直到 _mobileSliceOffset 归零。
881
+ this._maybeScheduleLocalLogAutoFill();
882
+ }
883
+
884
+ componentWillUnmount() {
885
+ this._unmounted = true;
886
+ this._splitDrag.dispose();
887
+ if (this._planAutoTimer) { clearInterval(this._planAutoTimer); this._planAutoTimer = null; }
888
+ // 缓发超时 timer + 残留上传占位的 objectURL 清扫(卸载中 drain/超时回调已各自 _unmounted 守卫)。
889
+ clearTimeout(this._sendDeferTimer);
890
+ for (const u of (this.state.uploadingItems || [])) { if (u.previewUrl) { try { URL.revokeObjectURL(u.previewUrl); } catch {} } }
891
+ // 清理全局审批/通知 — 切 session/关 tab 时让 modal 同步消失,main 进程 badge 归零
892
+ if (this.props.onPendingPermission) this.props.onPendingPermission(null);
893
+ if (this.props.onPendingPlanApproval) this.props.onPendingPlanApproval(null);
894
+ if (this.props.onPendingAsk) this.props.onPendingAsk(null);
895
+ if (this.props.onPendingPtyPlan) this.props.onPendingPtyPlan(null);
896
+ try {
897
+ if (this.state.pendingPtyPlan && typeof window !== 'undefined' && window.tabBridge?.notifyPtyPlanResolved) {
898
+ window.tabBridge.notifyPtyPlanResolved({
899
+ id: this.state.pendingPtyPlan.id,
900
+ tabId: this.props.ownTabId ?? null,
901
+ });
902
+ }
903
+ if (typeof window !== 'undefined' && window.tabBridge?.notifyAskResolved) {
904
+ const tabId = this.props.ownTabId ?? null;
905
+ if (this.state.pendingAsk) {
906
+ try { window.tabBridge.notifyAskResolved({ id: this.state.pendingAsk.id, tabId }); } catch {}
907
+ }
908
+ // Drain queued asks too — Electron main keeps pendingByTab[tabId].ask as Map<id>,
909
+ // so each must be removed individually or the badge / dock count goes stale.
910
+ // Per-item try-catch:单条 IPC throw 不中断后续清理。
911
+ for (const q of (this.state.askQueue || [])) {
912
+ try { window.tabBridge.notifyAskResolved({ id: q.id, tabId }); } catch {}
913
+ }
914
+ }
915
+ } catch {}
916
+ if (this._queueTimer) clearTimeout(this._queueTimer);
917
+ if (this._autoFillRafId) { cancelAnimationFrame(this._autoFillRafId); this._autoFillRafId = null; }
918
+ this._ptyPrompt.dispose();
919
+ this._toolFileMonitor.dispose();
920
+ if (this._wsReconnectTimer) clearTimeout(this._wsReconnectTimer);
921
+ if (this._planFeedbackTimer) clearTimeout(this._planFeedbackTimer);
922
+ if (this._streamingFadeTimer) clearTimeout(this._streamingFadeTimer);
923
+ if (this._stopOptimisticTimer) clearTimeout(this._stopOptimisticTimer);
924
+ // ask 流计时器 / _pendingHookAnswers / _pendingCancelIds 的清理收口到控制器 dispose()
925
+ this._askFlow.dispose();
926
+ // ask-cancel ack 协议清理:_pendingFlushQueue 留在 ChatView(生产者 handleInputSend),
927
+ // clearTimeout 所有 in-flight 500ms 兜底 timer,否则 unmount 后还会 fire → 被新 session 收到
928
+ if (this._pendingFlushQueue) {
929
+ for (const entry of this._pendingFlushQueue) clearTimeout(entry.tid);
930
+ this._pendingFlushQueue.length = 0;
931
+ }
932
+ this._scrollHighlight.dispose();
933
+ // 流式吸底统一清理:dispose 内会卸 RO + scroll listener + document touch + cancel 全部 rAF
934
+ if (this._stickyController) this._stickyController.dispose();
935
+ if (this._unsubWsHandler) { try { this._unsubWsHandler(); } catch {} this._unsubWsHandler = null; }
936
+ if (this._unsubWsState) { try { this._unsubWsState(); } catch {} this._unsubWsState = null; }
937
+ }
938
+
939
+ startRender() {
940
+ if (this._queueTimer) clearTimeout(this._queueTimer);
941
+
942
+ const rawItems = this.buildAllItems();
943
+ const allItems = this._applyMobileSlice(rawItems);
944
+ this._prevItemsLen = allItems.length;
945
+
946
+ this.setState({ allItems, visibleCount: allItems.length, loading: false },
947
+ () => {
948
+ // (a) 跳转语义优先(scrollToTimestamp / _scrollTargetIdx)
949
+ if (this._scrollTargetIdx != null || this.props.scrollToTimestamp) {
950
+ this.scrollToBottom();
951
+ return;
952
+ }
953
+ // (b) sticky 时同步写到底(避免 React 18 batched commit 后一帧"先看顶后瞬移")
954
+ // 先 refreshFollowTarget 复用一次 forced layout,writeUnderLock 直接用缓存值(修补 1)
955
+ // 用户滚动窗口内不硬贴底(用户意志优先,停手终判会补追)
956
+ if (this.state.stickyBottom && !this._stickyController.isUserScrolling()) {
957
+ const el = useVirtuoso ? this._virtuosoScrollerEl : this.containerRef.current;
958
+ if (el) {
959
+ this._stickyController.refreshFollowTarget(el);
960
+ this._stickyController.writeUnderLock(el, el.scrollHeight);
961
+ }
962
+ }
963
+ });
964
+ }
965
+
966
+ queueNext(current, total) {
967
+ if (current >= total) return;
968
+ this._queueTimer = setTimeout(() => {
969
+ this.setState({ visibleCount: current + 1 }, () => {
970
+ this.scrollToBottom();
971
+ this.queueNext(current + 1, total);
972
+ });
973
+ }, randomInterval());
974
+ }
975
+
976
+ _isNearBottom() {
977
+ const el = this.containerRef.current;
978
+ if (!el) return true;
979
+ return el.scrollHeight - el.scrollTop - el.clientHeight <= 30;
980
+ }
981
+
982
+ // 保留方法名(queueNext L759 仍在调);常规吸底与跳转分流,常规吸底走 controller.writeUnderLock
983
+ scrollToBottom() {
984
+ // 单点守卫:用户滚动窗口内常规吸底分支全部停摆(含 Virtuoso scrollToIndex 兜底),
985
+ // 跳转分支(scrollToTimestamp)不受影响
986
+ const shouldStick = this.state.stickyBottom && !this._stickyController.isUserScrolling();
987
+ // scrollToTimestamp 的目标不在当前渲染集合内(典型:被「仅展示当前会话」隐藏到更早 session —
988
+ // tsItemMap 不含该 ts → _scrollTargetIdx 为 null、目标 ref 也未绑定):下面两个平台跳转分支都不会
989
+ // 命中,父层 chatScrollToTs 便永远清不掉,后续对任意 ts 的跳转全部失效。这里兜底清除该请求
990
+ // (不跳转,继续按常规吸底渲染当前会话)。
991
+ if (this.props.scrollToTimestamp && this._scrollTargetIdx == null && !this._scrollTargetRef.current) {
992
+ if (this.props.onScrollTsDone) this.props.onScrollTsDone();
993
+ }
994
+ // 移动端 Virtuoso:跳转分支
995
+ if (useVirtuoso && this.virtuosoRef.current) {
996
+ if (this._scrollTargetIdx != null) {
997
+ this.virtuosoRef.current.scrollToIndex({ index: this._scrollTargetIdx, align: 'center' });
998
+ const targetTs = this.props.scrollToTimestamp;
999
+ this._scrollTargetRef = React.createRef();
1000
+ if (targetTs) {
1001
+ this.setState({ highlightTs: targetTs, highlightFading: false });
1002
+ this._scrollHighlight.bind();
1003
+ }
1004
+ if (this.props.onScrollTsDone) this.props.onScrollTsDone();
1005
+ return;
1006
+ }
1007
+ if (shouldStick) {
1008
+ const scroller = this._virtuosoScrollerEl;
1009
+ if (scroller) {
1010
+ this._stickyController.writeUnderLock(scroller, scroller.scrollHeight);
1011
+ } else {
1012
+ this.virtuosoRef.current.scrollToIndex({ index: 'LAST', behavior: 'auto' });
1013
+ }
1014
+ }
1015
+ return;
1016
+ }
1017
+ // 桌面端/iPad/iPhone:scrollToTimestamp 跳转分支
1018
+ if (this._scrollTargetRef.current && this.props.scrollToTimestamp) {
1019
+ const targetEl = this._scrollTargetRef.current;
1020
+ const container = this.containerRef.current;
1021
+ if (container && targetEl.offsetHeight > container.clientHeight) {
1022
+ targetEl.scrollIntoView({ block: 'start', behavior: 'instant' });
1023
+ } else {
1024
+ targetEl.scrollIntoView({ block: 'center', behavior: 'instant' });
1025
+ }
1026
+ const targetTs = this.props.scrollToTimestamp;
1027
+ this._scrollTargetRef = React.createRef();
1028
+ if (targetTs) {
1029
+ this.setState({ highlightTs: targetTs, highlightFading: false });
1030
+ this._scrollHighlight.bind();
1031
+ }
1032
+ if (this.props.onScrollTsDone) this.props.onScrollTsDone();
1033
+ return;
1034
+ }
1035
+ // 常规吸底:走 controller writeUnderLock(lock 引用计数 + 防 onScroll 翻 sticky)
1036
+ if (shouldStick) {
1037
+ const el = this.containerRef.current;
1038
+ if (el) this._stickyController.writeUnderLock(el, el.scrollHeight);
1039
+ }
1040
+ }
1041
+
1042
+ // 流式吸底状态机已收敛进 src/utils/stickyBottomController.js(StickyBottomController 实例):
1043
+ // - bind/unbind/dispose、scroll/RO 监听、引用计数 lock、双 rAF 缓动、用户滚动意图暂停窗口、决策去重
1044
+ // - notifyAtBottom(Virtuoso 接管)、suppressOnce(handleLoadMore 用)、writeUnderLock(唯一写入)
1045
+ // 本类仅持 controller 实例(this._stickyController),所有 scrollTop 写入走 controller.writeUnderLock。
1046
+
1047
+ // 稳定引用:传给被 AppHeader.renderTokenStats 缓存的 <ToolsHelp closeParent>,
1048
+ // 避免每次渲染新建闭包导致缓存子树持有过期引用(memo key 不含 closeParent)。
1049
+ _closeTokenStatsPopover = () => this.setState({ tokenStatsPopoverOpen: false });
1050
+
1051
+ handleStickToBottom = () => {
1052
+ // 显式动作清除用户滚动窗口:在 setState 之前同步调,让 userScrolling:false 与
1053
+ // stickyBottom:true 同一 React 批次 commit,followOutput 不出现中间帧
1054
+ this._stickyController.resetUserScrollState();
1055
+ this.setState({ stickyBottom: true }, () => {
1056
+ if (useVirtuoso && this.virtuosoRef.current) {
1057
+ this.virtuosoRef.current.scrollToIndex({ index: 'LAST', behavior: 'smooth' });
1058
+ } else {
1059
+ const el = this.containerRef.current;
1060
+ if (el) this._stickyController.writeUnderLock(el, el.scrollHeight);
1061
+ }
1062
+ });
1063
+ };
1064
+
1065
+ _addPendingImage = (path, source) => {
1066
+ if (!path) return;
1067
+ this.setState(prev => {
1068
+ if (prev.pendingImages.length >= 20) return null; // cap
1069
+ if (prev.pendingImages.some(img => img.path === path)) return null; // dedup
1070
+ return { pendingImages: [...prev.pendingImages, { path, source }] };
1071
+ });
1072
+ };
1073
+
1074
+ _removePendingImage = (index) => {
1075
+ this.setState(prev => {
1076
+ const img = prev.pendingImages[index];
1077
+ if (!img) return null;
1078
+ const next = prev.pendingImages.filter((_, i) => i !== index);
1079
+ // Notify other clients to remove the same file
1080
+ if (img.path && this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
1081
+ this._inputWs.send(JSON.stringify({ type: 'image-remove-notify', path: img.path }));
1082
+ }
1083
+ return { pendingImages: next };
1084
+ });
1085
+ };
1086
+
1087
+ _clearPendingImages = () => {
1088
+ if (this.state.pendingImages.length > 0) {
1089
+ this.setState({ pendingImages: [] });
1090
+ }
1091
+ };
1092
+
1093
+ // ===== 上传在途占位 + 缓发(defer) =====
1094
+ // 上传一开始(ChatInputBar 粘贴/选图、AppBase 拖拽)就登记占位,resolve/reject 时撤销;
1095
+ // 撤销 objectURL 的副作用由本方法执行(reduceUploading 只计算需 revoke 的 url 列表)。
1096
+ _applyUploading = (action) => {
1097
+ this.setState(prev => {
1098
+ const { next, revoke } = reduceUploading(prev.uploadingItems, action);
1099
+ if (next === prev.uploadingItems) return null;
1100
+ for (const url of revoke) { try { URL.revokeObjectURL(url); } catch {} }
1101
+ return { uploadingItems: next };
1102
+ });
1103
+ };
1104
+
1105
+ // ChatInputBar 上传开始/结束回调(选图 + 粘贴)。onUploadEnd 的 path 仅用于配对结束,
1106
+ // 真正落 pendingImages 仍走既有 onUploadPath→handleUploadPath(保持广播 image-upload-notify)。
1107
+ handleUploadStart = (id, name, previewUrl) => this._applyUploading({ type: 'add', item: { id, name, previewUrl } });
1108
+ handleUploadEnd = (id) => this._applyUploading({ type: 'remove', id });
1109
+
1110
+ // 缓发:有上传在途时被调用。幂等(挡双 Enter / IME 合成结束双触发);10s 超时兜底只提示、不发纯文字。
1111
+ _deferSend = () => {
1112
+ if (this._sendDeferred) return;
1113
+ this._sendDeferred = true;
1114
+ this.setState({ sendDeferred: true });
1115
+ clearTimeout(this._sendDeferTimer);
1116
+ this._sendDeferTimer = setTimeout(() => {
1117
+ if (this._unmounted || !this._sendDeferred) return;
1118
+ this._sendDeferred = false;
1119
+ this.setState({ sendDeferred: false });
1120
+ message.error(t('ui.chatInput.uploadTimeout'));
1121
+ }, UPLOAD_DEFER_TIMEOUT_MS);
1122
+ };
1123
+
1124
+ _clearDeferSend = () => {
1125
+ if (!this._sendDeferred && !this.state.sendDeferred) return;
1126
+ this._sendDeferred = false;
1127
+ clearTimeout(this._sendDeferTimer);
1128
+ this.setState({ sendDeferred: false });
1129
+ };
1130
+
1131
+ // logfile 只读模式(非虚拟化平台:桌面/iOS/iPad)自动渐进扩窗——替代手工点击「加载更多」。
1132
+ // 每个 rAF 帧执行一步 handleLoadMore(帧间让出主线程给绘制/输入,避免单次全量挂 DOM 的长任务卡死,
1133
+ // 见 DESKTOP_ITEM_LIMIT 注释),沿用其滚动补偿保持视口稳定;componentDidUpdate 接力续步直到全量渲染完成。
1134
+ _maybeScheduleLocalLogAutoFill() {
1135
+ if (!this.props.isLocalLog || useVirtuoso) return;
1136
+ if (this._mobileSliceOffset <= 0 || this._autoFillRafId || this._unmounted) return;
1137
+ this._autoFillRafId = requestAnimationFrame(() => {
1138
+ this._autoFillRafId = null;
1139
+ if (this._unmounted || this._mobileSliceOffset <= 0) return;
1140
+ this.handleLoadMore();
1141
+ });
1142
+ }
1143
+
1144
+ handleLoadMore = () => {
1145
+ this._mobileExtraItems += MOBILE_LOAD_MORE_STEP;
1146
+ const prevLen = this.state.allItems?.length || 0;
1147
+ const rawItems = this.buildAllItems();
1148
+ const allItems = this._applyMobileSlice(rawItems);
1149
+ const addedCount = allItems.length - prevLen;
1150
+ if (useVirtuoso && this.virtuosoRef.current) {
1151
+ this.setState({ allItems, visibleCount: allItems.length }, () => {
1152
+ if (this.virtuosoRef.current && addedCount > 0) {
1153
+ this.virtuosoRef.current.scrollToIndex({ index: addedCount, align: 'start' });
1154
+ }
1155
+ });
1156
+ } else {
1157
+ const el = this.containerRef.current;
1158
+ const prevScrollHeight = el ? el.scrollHeight : 0;
1159
+ const prevScrollTop = el ? el.scrollTop : 0;
1160
+ this.setState({ allItems, visibleCount: allItems.length }, () => {
1161
+ if (el) {
1162
+ // suppressOnce:随后 RO fire(DOM 长高)期间锁短路,防止"维持位置"被吸底覆盖
1163
+ this._stickyController.suppressOnce();
1164
+ const newScrollHeight = el.scrollHeight;
1165
+ el.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight);
1166
+ }
1167
+ });
1168
+ }
1169
+ };
1170
+
1171
+ _getScrollContainer() {
1172
+ return useVirtuoso ? this._virtuosoScrollerEl : this.containerRef.current;
1173
+ }
1174
+
1175
+
1176
+ // 派生 per-keyPrefix 的 _mergedAskAnswerMap:cached.askAnswerMap 是原地 mutate 的,引用永远不变;
1177
+ // 用 _askDirty + localAsk 引用 + 上一轮 cache 引用做三信号失效判断。
1178
+ // localAsk(用户乐观更新缓存)每次提交都换新引用,要并入失效信号,否则用户答题后到 server ack 之间 UI 会闪回 pending。
1179
+ // 永远 spread 创建新引用,让下游 ChatMessage SCU 检测到 askAnswerMap 变化(修老 bug:local 空时复用 cached 引用导致 SCU 不命中)。
1180
+ _getMergedAskAnswerMap(messages, keyPrefix, localAsk) {
1181
+ const cached = getToolResultCache(messages);
1182
+ const askMap = cached?.askAnswerMap || EMPTY_MAP;
1183
+ const askDirty = cached?._askDirty || 0;
1184
+ if (!this._mergedAskAnswerMapByKey) this._mergedAskAnswerMapByKey = {};
1185
+ if (!this._prevAskCacheByKey) this._prevAskCacheByKey = {};
1186
+ if (!this._prevAskDirtyByKey) this._prevAskDirtyByKey = {};
1187
+ if (!this._prevAskLocalByKey) this._prevAskLocalByKey = {};
1188
+ if (this._prevAskCacheByKey[keyPrefix] !== askMap
1189
+ || this._prevAskDirtyByKey[keyPrefix] !== askDirty
1190
+ || this._prevAskLocalByKey[keyPrefix] !== localAsk) {
1191
+ const hasLocal = localAsk && Object.keys(localAsk).length > 0;
1192
+ this._mergedAskAnswerMapByKey[keyPrefix] = hasLocal
1193
+ ? { ...askMap, ...localAsk }
1194
+ : { ...askMap };
1195
+ this._prevAskCacheByKey[keyPrefix] = askMap;
1196
+ this._prevAskDirtyByKey[keyPrefix] = askDirty;
1197
+ this._prevAskLocalByKey[keyPrefix] = localAsk;
1198
+ }
1199
+ return this._mergedAskAnswerMapByKey[keyPrefix];
1200
+ }
1201
+
1202
+ // 派生 per-session 的 _mergedPlanApprovalMap:cached.planApprovalMap 是原地 mutate 的,引用永远不变;
1203
+ // 用 _planDirty + 上一轮 cache 引用做失效判断,每次内容变化时创建一个新引用 {} 覆盖。
1204
+ // keyPrefix 区分 main session(s${si})vs sub-agent(tm${si}),避免互相覆盖。
1205
+ // FULL HIT 路径(_sessionItemCache 命中)和内部 renderSessionMessages 都调用此方法,引用保持一致。
1206
+ _getMergedPlanApprovalMap(messages, keyPrefix) {
1207
+ const cached = getToolResultCache(messages);
1208
+ if (!cached) return EMPTY_MAP;
1209
+ const planDirty = cached._planDirty || 0;
1210
+ if (!this._mergedPlanApprovalMapByKey) this._mergedPlanApprovalMapByKey = {};
1211
+ if (!this._prevPlanCacheByKey) this._prevPlanCacheByKey = {};
1212
+ if (!this._prevPlanDirtyByKey) this._prevPlanDirtyByKey = {};
1213
+ if (this._prevPlanCacheByKey[keyPrefix] !== cached.planApprovalMap
1214
+ || this._prevPlanDirtyByKey[keyPrefix] !== planDirty) {
1215
+ this._mergedPlanApprovalMapByKey[keyPrefix] = { ...cached.planApprovalMap };
1216
+ this._prevPlanCacheByKey[keyPrefix] = cached.planApprovalMap;
1217
+ this._prevPlanDirtyByKey[keyPrefix] = planDirty;
1218
+ }
1219
+ return this._mergedPlanApprovalMapByKey[keyPrefix];
1220
+ }
1221
+
1222
+ // 浏览本地历史日志时为 true;CLI / SDK 实时会话为 false。
1223
+ // 用作 ChatMessage 的 isHistoryLog prop,控制时间戳 compact 显示。
1224
+ _getIsHistoryLog() {
1225
+ return !this.props.cliMode && !this.props.sdkMode;
1226
+ }
1227
+
1228
+ // teammateIdentity: only set by _buildTeammateFallbackItems (teammate session
1229
+ // logs). Assistant rows then carry the TEAMMATE's identity (label + portrait
1230
+ // via ChatMessage's teammate branch) instead of the model identity — a model
1231
+ // logo + model name on those rows reads as the MainAgent speaking in the
1232
+ // teammate's own log. animateAvatar:false keeps a long historic transcript
1233
+ // from mass-playing draw-ins (the fallback path returns before the
1234
+ // avatar-animation post-pass, and raw body.input carry no timestamps).
1235
+ renderSessionMessages(messages, keyPrefix, resolveModelInfo, tsToIndex, startIdx = 0, teammateIdentity = null) {
1236
+ const { userProfile, collapseToolResults, expandThinking, showFullToolContent, showThinkingSummaries, onViewRequest } = this.props;
1237
+ const isHistoryLog = this._getIsHistoryLog();
1238
+ // 增量 / WeakMap 缓存
1239
+ let cached = getToolResultCache(messages);
1240
+ if (cached && messages.length > this._incToolProcessedCount) {
1241
+ // WeakMap 命中但 messages 增长了(push 模式增量追加)→ 只处理新增消息的 tool 映射
1242
+ appendToolResultMap(cached, messages, this._incToolProcessedCount);
1243
+ this._incToolProcessedCount = messages.length;
1244
+ }
1245
+ if (!cached) {
1246
+ const si = parseInt(keyPrefix.slice(1), 10);
1247
+ if (this._incToolSessionIdx === si && messages.length >= this._incToolProcessedCount && this._incToolProcessedCount > 0) {
1248
+ appendToolResultMap(this._incToolState, messages, this._incToolProcessedCount);
1249
+ } else {
1250
+ this._incToolState = createEmptyToolState();
1251
+ appendToolResultMap(this._incToolState, messages, 0);
1252
+ this._incToolSessionIdx = si;
1253
+ }
1254
+ this._incToolProcessedCount = messages.length;
1255
+ cached = this._incToolState;
1256
+ setToolResultCache(messages, cached);
1257
+ }
1258
+ const { toolUseMap, toolResultMap, readContentMap, editSnapshotMap, askAnswerMap, latestPlanContent } = cached;
1259
+ // planApprovalMap 派生统一走 _getMergedPlanApprovalMap(per keyPrefix),保证 FULL HIT 路径与 cache miss 路径
1260
+ // 拿到同一引用,prop diff 才能正确触发 plan 卡片重渲。
1261
+ const planApprovalMap = this._getMergedPlanApprovalMap(messages, keyPrefix);
1262
+
1263
+ const activePlanPrompt = this.props.cliMode
1264
+ ? this.state.ptyPromptHistory.slice().reverse().find(p => isPlanApprovalPrompt(p) && p.status === 'active') || null
1265
+ : null;
1266
+ const activeDangerousPrompt = this.props.cliMode && !(this.state.pendingPermission?.source === 'pty')
1267
+ ? this.state.ptyPromptHistory.slice().reverse().find(p => isDangerousOperationPrompt(p) && p.status === 'active') || null
1268
+ : null;
1269
+
1270
+ // 合并 localAskAnswers 到历史 askAnswerMap,使提交后立即显示已回答。
1271
+ // 派生统一走 _getMergedAskAnswerMap(per keyPrefix),保证 FULL HIT 路径与 cache miss 路径
1272
+ // 拿到同一引用,prop diff 才能正确触发 request_user_input 卡片重渲。
1273
+ const _localAsk = this.state.localAskAnswers || {};
1274
+ const mergedAskAnswerMap = this._getMergedAskAnswerMap(messages, keyPrefix, _localAsk);
1275
+ // Pending ask/plan arbitration is centralized in interactionOwnership.js
1276
+ // (last-assistant-only rule + owner-index locking so exactly one bubble
1277
+ // gets the pending id). MERGED maps by design — see the module header.
1278
+ const _pending = computeMessagesPending({ messages, planApprovalMap, askAnswerMap: mergedAskAnswerMap });
1279
+ let lastPendingAskId = _pending.lastPendingAskId;
1280
+ let lastPendingPlanId = _pending.lastPendingPlanId;
1281
+ const lastPendingAskOwnerIdx = _pending.askOwnerIdx;
1282
+ const lastPendingPlanOwnerIdx = _pending.planOwnerIdx;
1283
+
1284
+ const renderedMessages = [];
1285
+
1286
+ for (let mi = startIdx; mi < messages.length; mi++) {
1287
+ const msg = messages[mi];
1288
+ const content = msg.content;
1289
+ const ts = msg._timestamp || null; // carrier ts —— timestamp prop / resolveModelInfo / SubAgent 时间排序用
1290
+ // lookupTs:assistant 用 _generatedTs(producer 的 request ts),其他 role 用 _timestamp(即 carrier)。
1291
+ // 让 "查看请求" 按钮跳到真正产出该 bubble 内容的 mainAgent request,而不是下一次 carrier。
1292
+ const lookupTs = resolveBubbleProducerTs(msg);
1293
+ const reqIdx = lookupTs ? tsToIndex[lookupTs] : undefined;
1294
+ const hasViewRequest = reqIdx != null && onViewRequest;
1295
+ const modelInfo = resolveModelInfo(ts, msg.role);
1296
+
1297
+ if (msg.role === 'user') {
1298
+ if (Array.isArray(content)) {
1299
+ const suggestionText = content.find(b => b.type === 'text' && /^\[SUGGESTION MODE:/i.test((b.text || '').trim()));
1300
+ const toolResults = content.filter(b => b.type === 'tool_result');
1301
+
1302
+ if (suggestionText && toolResults.length > 0) {
1303
+ // request_user_input 的用户回复:跳过渲染(答案已在 assistant 侧问卷卡片上显示)
1304
+ } else {
1305
+ const { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks } = classifyUserContent(content);
1306
+ // 渲染 slash command 作为独立用户输入
1307
+ for (let ci = 0; ci < commands.length; ci++) {
1308
+ renderedMessages.push(
1309
+ <ChatMessage key={`${keyPrefix}-cmd-${mi}-${ci}`} role="user" text={commands[ci]} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1310
+ );
1311
+ }
1312
+ // 渲染 skill 加载块
1313
+ for (const sb of skillBlocks) {
1314
+ const nameMatch = sb.text.match(/^#\s+(.+)$/m);
1315
+ const skillName = nameMatch ? nameMatch[1] : 'Skill';
1316
+ renderedMessages.push(
1317
+ <ChatMessage key={`${keyPrefix}-skill-${mi}`} role="skill-loaded" text={sb.text} skillName={skillName} timestamp={ts} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1318
+ );
1319
+ }
1320
+ // 渲染普通用户文本块
1321
+ for (let ti = 0; ti < textBlocks.length; ti++) {
1322
+ const isPlan = /Implement the following plan:/i.test(textBlocks[ti].text || '');
1323
+ renderedMessages.push(
1324
+ <ChatMessage key={`${keyPrefix}-user-${mi}-${ti}`} role={isPlan ? 'plan-prompt' : 'user'} text={textBlocks[ti].text} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1325
+ );
1326
+ }
1327
+ // 渲染 teammate-message 块
1328
+ for (let tmi = 0; tmi < teammateBlocks.length; tmi++) {
1329
+ const tm = teammateBlocks[tmi];
1330
+ renderedMessages.push(
1331
+ <ChatMessage
1332
+ key={`${keyPrefix}-teammate-${mi}-${tmi}`}
1333
+ role={tm.status ? 'teammate-status' : 'teammate-message'}
1334
+ text={tm.content}
1335
+ label={tm.status ? (tm.statusFrom || tm.id) : tm.id}
1336
+ toolName={tm.status || null}
1337
+ timestamp={ts}
1338
+ modelInfo={modelInfo}
1339
+ requestIndex={hasViewRequest ? reqIdx : undefined}
1340
+ onViewRequest={hasViewRequest ? onViewRequest : undefined}
1341
+ isHistoryLog={isHistoryLog}
1342
+ />
1343
+ );
1344
+ }
1345
+ // 渲染 task-notification 块
1346
+ for (let tni = 0; tni < taskNotificationBlocks.length; tni++) {
1347
+ const tn = taskNotificationBlocks[tni];
1348
+ renderedMessages.push(
1349
+ <ChatMessage
1350
+ key={`${keyPrefix}-tasknotif-${mi}-${tni}`}
1351
+ role="task-notification"
1352
+ taskNotification={tn}
1353
+ timestamp={ts}
1354
+ modelInfo={modelInfo}
1355
+ requestIndex={hasViewRequest ? reqIdx : undefined}
1356
+ onViewRequest={hasViewRequest ? onViewRequest : undefined}
1357
+ isHistoryLog={isHistoryLog}
1358
+ />
1359
+ );
1360
+ }
1361
+ }
1362
+ } else if (typeof content === 'string') {
1363
+ // 复用 classifyUserContent 解析 task-notification(避免重复正则)
1364
+ if (/<task-notification>/i.test(content)) {
1365
+ const { taskNotificationBlocks: strTnBlocks } = classifyUserContent([{ type: 'text', text: content }]);
1366
+ for (let tni = 0; tni < strTnBlocks.length; tni++) {
1367
+ renderedMessages.push(
1368
+ <ChatMessage
1369
+ key={`${keyPrefix}-tasknotif-str-${mi}-${tni}`}
1370
+ role="task-notification"
1371
+ taskNotification={strTnBlocks[tni]}
1372
+ timestamp={ts}
1373
+ modelInfo={modelInfo}
1374
+ requestIndex={hasViewRequest ? reqIdx : undefined}
1375
+ onViewRequest={hasViewRequest ? onViewRequest : undefined}
1376
+ isHistoryLog={isHistoryLog}
1377
+ />
1378
+ );
1379
+ }
1380
+ } else {
1381
+ const dispText = extractDisplayText(content);
1382
+ if (dispText) {
1383
+ const isPlan = /Implement the following plan:/i.test(dispText);
1384
+ renderedMessages.push(
1385
+ <ChatMessage key={`${keyPrefix}-user-${mi}`} role={isPlan ? 'plan-prompt' : 'user'} text={dispText} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1386
+ );
1387
+ }
1388
+ }
1389
+ }
1390
+ } else if (msg.role === 'assistant') {
1391
+ // 定向传递 lastPendingAskId/PlanId:只传给"owner message"(即 streaming 中最新一份),
1392
+ // 避免同 toolId 出现在多条 message 时让多个 ChatMessage 都进 isInteractive 路径并双重 portal
1393
+ const msgLastAskId = (mi === lastPendingAskOwnerIdx) ? lastPendingAskId : null;
1394
+ const msgLastPlanId = (mi === lastPendingPlanOwnerIdx) ? lastPendingPlanId : null;
1395
+ // Normalize the two content shapes into one block list (null → skip):
1396
+ // array content drops system-text blocks (e.g. SUGGESTION MODE); string
1397
+ // content renders only if text survives chrome-stripping.
1398
+ let asstContent = null;
1399
+ if (Array.isArray(content)) {
1400
+ const filteredContent = content.filter(block =>
1401
+ block.type !== 'text' || !isSystemText(block.text)
1402
+ );
1403
+ if (filteredContent.length > 0) asstContent = filteredContent;
1404
+ } else if (typeof content === 'string') {
1405
+ const dispText = extractDisplayText(content);
1406
+ if (dispText) asstContent = [{ type: 'text', text: dispText }];
1407
+ }
1408
+ if (asstContent) {
1409
+ renderedMessages.push(
1410
+ <ChatMessage key={`${keyPrefix}-asst-${mi}`} role="assistant" isTeammate={teammateIdentity ? true : undefined} label={teammateIdentity?.label} animateAvatar={teammateIdentity ? false : undefined} content={asstContent} toolResultMap={toolResultMap} readContentMap={readContentMap} editSnapshotMap={editSnapshotMap} askAnswerMap={mergedAskAnswerMap} planApprovalMap={planApprovalMap} latestPlanContent={latestPlanContent} planFileContents={this.state.planFileContents} timestamp={ts} displayTs={msg._generatedTs} modelInfo={modelInfo} collapseToolResults={collapseToolResults} expandThinking={expandThinking} showFullToolContent={showFullToolContent} showThinkingSummaries={showThinkingSummaries} ptyPrompt={this.state.ptyPrompt} activePlanPrompt={activePlanPrompt} activePtyPlanId={this.state.pendingPtyPlan?.id ?? null} planAutoApproveCountdown={this.state.planAutoApproveCountdown} onCancelPlanAutoApprove={this.cancelPlanAutoApprove} activeDangerousPrompt={activeDangerousPrompt} lastPendingPlanId={msgLastPlanId} lastPendingAskId={msgLastAskId} onPlanApprovalClick={this.handlePromptOptionClick} onPlanFeedbackSubmit={this.handlePlanFeedbackSubmit} onDangerousApprovalClick={this.handlePromptOptionClick} onAskQuestionSubmit={this.handleAskQuestionSubmit} onAskQuestionCancel={this.handleAskCancel} pendingAsk={this.state.pendingAsk} askMetaMap={this.state.askMetaMap} cliMode={this.props.cliMode} onOpenFile={this.handleOpenToolFilePath} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1411
+ );
1412
+ }
1413
+ }
1414
+ }
1415
+
1416
+ // ownerInRange:owner-idx 是否落在本轮渲染的范围(>= startIdx);
1417
+ // 增量路径上层据此判断旧 sc.items 里 stale 的 lastPendingAskId/PlanId 是否需要清空。
1418
+ const askOwnerInRange = lastPendingAskOwnerIdx >= startIdx;
1419
+ const planOwnerInRange = lastPendingPlanOwnerIdx >= startIdx;
1420
+ return { items: renderedMessages, lastPendingAskId, lastPendingPlanId, askOwnerInRange, planOwnerInRange };
1421
+ }
1422
+
1423
+ /**
1424
+ * Fallback: 当 mainAgentSessions 为空时,从 requests 中提取 teammate entries 渲染。
1425
+ * 解决 JSONL 截断后只剩 teammate entries 导致界面空白的问题。
1426
+ */
1427
+ _buildTeammateFallbackItems() {
1428
+ const { requests } = this.props;
1429
+ if (!requests || requests.length === 0) return [];
1430
+
1431
+ // Teammate 名称解析
1432
+ resolveTeammateNames(requests);
1433
+ // 按 teammate 名称分组,保持时间顺序,取最后一条(最完整)的 input
1434
+ const teammateMap = new Map(); // name → { messages, response, timestamp }
1435
+ for (const req of requests) {
1436
+ if (!isTeammate(req) || !req.body?.input?.length) continue;
1437
+ const name = req.teammate || 'teammate';
1438
+ const existing = teammateMap.get(name);
1439
+ // 同名 teammate 后到的 entry input 更完整(增量累积),取最后一条
1440
+ if (!existing || req.body.input.length >= existing.messages.length) {
1441
+ teammateMap.set(name, {
1442
+ messages: req.body.input,
1443
+ response: req.response,
1444
+ timestamp: req.timestamp,
1445
+ model: getEffectiveModel(req),
1446
+ });
1447
+ }
1448
+ }
1449
+
1450
+ if (teammateMap.size === 0) return [];
1451
+
1452
+ const allItems = [];
1453
+ let si = 0;
1454
+ for (const [name, session] of teammateMap) {
1455
+ allItems.push(
1456
+ <Divider key={`tm-div-${si}`} className={styles.sessionDivider}>
1457
+ <Text className={styles.sessionDividerText}>{name}</Text>
1458
+ </Divider>
1459
+ );
1460
+ // Assistant rows carry the TEAMMATE's identity: label (name + model
1461
+ // short-name via formatTeammateLabel) and teammate portrait, rendered by
1462
+ // ChatMessage's assistant teammate branch. modelInfo is supplied only as
1463
+ // the label fallback — rendering the model logo/name directly made these
1464
+ // rows read as the MainAgent speaking in the teammate's own log.
1465
+ // (Replaces both the v1.6.171 null resolver, which showed "MainAgent"
1466
+ // everywhere, and the first fix iteration, which showed model identity.)
1467
+ // Raw body.input carry no _timestamp, so the resolver ignores ts.
1468
+ const tmModelInfo = getModelInfo(session.model);
1469
+ const tmModelResolver = (ts, role) => (role === 'assistant' ? tmModelInfo : null);
1470
+ const tmLabel = formatTeammateLabel(name, session.model);
1471
+ const { items: msgs } = this.renderSessionMessages(session.messages, `tm${si}`, tmModelResolver, {}, 0, { label: tmLabel });
1472
+ allItems.push(...msgs);
1473
+ si++;
1474
+ }
1475
+
1476
+ return allItems;
1477
+ }
1478
+
1479
+ buildAllItems() {
1480
+ const { mainAgentSessions, requests, collapseToolResults, expandThinking, showFullToolContent, onlyCurrentSession, onViewRequest } = this.props;
1481
+ const isHistoryLog = this._getIsHistoryLog();
1482
+ if (!mainAgentSessions || mainAgentSessions.length === 0) {
1483
+ // Fallback: 无 MainAgent 时,从 requests 提取 teammate entries 渲染其对话历史,
1484
+ // 避免 JSONL 截断只剩 teammate 时界面完全空白。
1485
+ this._currentLastPendingPlanId = null;
1486
+ return this._buildTeammateFallbackItems();
1487
+ }
1488
+
1489
+ // 增量扫描 requests(tsToIndex + modelName 增量,subAgentEntries 可按需全量重扫)
1490
+ const cache = this._reqScanCache;
1491
+ if (requests) {
1492
+ // Identity guard: a mid-array INSERTION (an in-flight producer turn
1493
+ // completing enters the filtered array below the cursor) shifts every
1494
+ // index after it — the incremental scan would then build stale
1495
+ // tsToIndex/modelNameByReqIdx and subAgentEntries with wrong
1496
+ // requestIndex values, and the inserted turn's own row would never be
1497
+ // built. Detect via object identity of the last-scanned request and
1498
+ // force a full rescan. Accepted residual: a mid-array REPLACEMENT with
1499
+ // an unchanged element at processedCount-1 does not fire the guard —
1500
+ // worst case a stale token badge, not the identity bug.
1501
+ const fullRescan = needsFullReqRescan(requests, cache.processedCount, cache.lastScannedReq);
1502
+ // tsToIndex / modelName: 只追加不修改,增量扫描
1503
+ const startIdx = (!fullRescan && requests.length >= cache.processedCount) ? cache.processedCount : 0;
1504
+ if (startIdx === 0) {
1505
+ cache.tsToIndex = {};
1506
+ cache.modelName = null;
1507
+ cache.completedModelName = null;
1508
+ cache.modelNameByReqIdx = [];
1509
+ cache.subAgentEntries = [];
1510
+ cache.subAgentProcessedCount = 0;
1511
+ cache.globalIndexState = createEmptyGlobalIndexState();
1512
+ cache.globalIndexProcessedCount = 0;
1513
+ }
1514
+ // carry-over 初值:从上轮末态继承,保证流式追加时非 MainAgent 或无 body.model 的 req 也能拿到"最近活跃模型"
1515
+ let lastModelName = cache.modelName;
1516
+ for (let i = startIdx; i < requests.length; i++) {
1517
+ const req = requests[i];
1518
+ const ma = isMainAgent(req);
1519
+ if (ma && req.timestamp) {
1520
+ cache.tsToIndex[req.timestamp] = i;
1521
+ }
1522
+ const effectiveModel = getEffectiveModel(req);
1523
+ if (ma && effectiveModel) {
1524
+ lastModelName = effectiveModel;
1525
+ cache.modelName = lastModelName;
1526
+ // 只在 response 已到达时更新 completedModelName。
1527
+ // 流式进行中的 request 尚无 response,不应污染"上一次已完成的模型"。
1528
+ if (req.response) cache.completedModelName = lastModelName;
1529
+ }
1530
+ cache.modelNameByReqIdx[i] = lastModelName;
1531
+ }
1532
+ cache.processedCount = requests.length;
1533
+ cache.lastScannedReq = requests.length > 0 ? requests[requests.length - 1] : null;
1534
+
1535
+ // Teammate 名称解析:在 classifyRequest 之前注入 req.teammate(prompt 内容匹配)
1536
+ resolveTeammateNames(requests);
1537
+ // Heal labels baked into surviving entries before the registry resolved
1538
+ // (or baked from a raw id) — entries carry the request object reference,
1539
+ // which survives filtered-array rebuilds and insertions.
1540
+ healUnresolvedTeammateEntries(cache.subAgentEntries);
1541
+
1542
+ // subAgentEntries: response 可能被原地更新,从 subAgentProcessedCount 开始扫描
1543
+ // 回退一位重扫尾项:上一轮尾项的 classifyRequest(req, undefined) 可能因缺少 nextReq 而误判
1544
+ let subStart = cache.subAgentProcessedCount || 0;
1545
+ if (subStart > 0 && subStart < requests.length) {
1546
+ subStart--;
1547
+ // 移除上一轮尾项可能已推入的错误条目
1548
+ while (cache.subAgentEntries.length > 0 && cache.subAgentEntries[cache.subAgentEntries.length - 1].requestIndex >= subStart) {
1549
+ cache.subAgentEntries.pop();
1550
+ }
1551
+ }
1552
+ // 全局 tool_result 索引:并行 SubAgent / Teammate 的请求互相穿插,K+1 不可
1553
+ // 预测,需 id → result 全局映射。增量追加新请求,避免每次 setState 全量重扫。
1554
+ // 与 subStart 同样回退一位:上轮尾项的 response 可能刚到达,需补扫其 response.content。
1555
+ // `!(id in index)` 守卫保证幂等。
1556
+ let globalIndexStart = cache.globalIndexProcessedCount || 0;
1557
+ if (globalIndexStart > 0 && globalIndexStart < requests.length) globalIndexStart--;
1558
+ if (globalIndexStart < requests.length) {
1559
+ appendToGlobalToolResultIndex(cache.globalIndexState, requests, globalIndexStart);
1560
+ cache.globalIndexProcessedCount = requests.length;
1561
+ }
1562
+ const globalToolResultIndex = cache.globalIndexState.index;
1563
+ for (let i = subStart; i < requests.length; i++) {
1564
+ const req = requests[i];
1565
+ if (!req.timestamp) continue;
1566
+ // Direct OpenAI Responses transport entries remain inspectable in the
1567
+ // request list, but must not re-enter the transcript through this
1568
+ // auxiliary SubAgent/Teammate rendering path. The main-session ingest
1569
+ // applies the same predicate in AppBase; without this guard a slimmed
1570
+ // transport entry can be reclassified as `SubAgent: OpenAI Responses`
1571
+ // and render the completed answer a second time.
1572
+ if (shouldExcludeFromConversation(req)) continue;
1573
+ const cls = classifyRequest(req, requests[i + 1]);
1574
+ if (cls.type === 'SubAgent' || cls.type === 'Teammate') {
1575
+ const respContent = req.response?.body?.content;
1576
+ if (Array.isArray(respContent) && respContent.length > 0) {
1577
+ const subToolResultMap = buildSubAgentResultMap(req, globalToolResultIndex);
1578
+ const isTeammateEntry = cls.type === 'Teammate';
1579
+ cache.subAgentEntries.push({
1580
+ timestamp: req.timestamp,
1581
+ content: respContent,
1582
+ toolResultMap: subToolResultMap,
1583
+ label: isTeammateEntry
1584
+ ? formatTeammateLabel(cls.subType, req.body?.model)
1585
+ : formatRequestTag(cls.type, cls.subType),
1586
+ isTeammate: isTeammateEntry,
1587
+ requestIndex: i,
1588
+ // All teammate entries stay healable: a truthy cls.subType can
1589
+ // be a raw id upgraded to a real name later. Object reference,
1590
+ // not index — indices shift on mid-array insertion.
1591
+ unresolved: isTeammateEntry,
1592
+ req,
1593
+ });
1594
+ }
1595
+ }
1596
+ }
1597
+ cache.subAgentProcessedCount = requests.length;
1598
+ }
1599
+ const tsToIndex = cache.tsToIndex;
1600
+ // Per-message resolver:1v1 严格遵从,未匹配返回 null(ChatMessage 显示中性 'MainAgent')。
1601
+ // assistant message 的 _timestamp 实际指向【下一轮 entry】的 ts(详见
1602
+ // src/AppBase.jsx:184-186, src/utils/sessionMerge.js:44/50),所以 producer
1603
+ // req idx = idx - 1;user message producer idx = idx。
1604
+ const resolveModelInfo = (ts, role) => resolveProducerModelInfo(ts, role, tsToIndex, cache.modelNameByReqIdx);
1605
+ const subAgentEntries = cache.subAgentEntries;
1606
+
1607
+ const allItems = [];
1608
+ const tsItemMap = {};
1609
+
1610
+ // === session item 缓存:toggle 签名检查 ===
1611
+ const { showThinkingSummaries } = this.props;
1612
+ const activePromptIds = (this.state.ptyPromptHistory || []).filter(p => p.status === 'active').map(p => p.id).join(',');
1613
+ const toggleSig = `${collapseToolResults}|${expandThinking}|${showFullToolContent}|${showThinkingSummaries}|${this.props.cliMode}|${this.state.ptyPrompt?.id || ''}|${activePromptIds}|${this.props.userProfile?.name || ''}|${this.props.lang || ''}|${Object.keys(this.state.localAskAnswers || {}).join(',')}`;
1614
+ if (toggleSig !== this._itemCacheToggleSig) {
1615
+ this._sessionItemCache = [];
1616
+ this._itemCacheToggleSig = toggleSig;
1617
+ }
1618
+ if (this._sessionItemCache.length > mainAgentSessions.length) {
1619
+ this._sessionItemCache.length = mainAgentSessions.length;
1620
+ }
1621
+ // 清理 _getMergedXxxMap 的 byKey 字典中已不存在的 main session 条目(s${si}),
1622
+ // 否则用户切换/删除 session 后旧 keyPrefix 永不释放(每条 entry 占 ~8 引用,重 session 用户累积可见内存)。
1623
+ // tm${si} (sub-agent) 的清理由各自 render 周期短,不在此处处理。
1624
+ const _validS = mainAgentSessions.length;
1625
+ const _byKeyDicts = [
1626
+ this._mergedPlanApprovalMapByKey, this._prevPlanCacheByKey, this._prevPlanDirtyByKey,
1627
+ this._mergedAskAnswerMapByKey, this._prevAskCacheByKey, this._prevAskDirtyByKey, this._prevAskLocalByKey,
1628
+ ];
1629
+ for (const dict of _byKeyDicts) {
1630
+ if (!dict) continue;
1631
+ for (const k of Object.keys(dict)) {
1632
+ if (!k.startsWith('s')) continue;
1633
+ const idx = parseInt(k.slice(1), 10);
1634
+ if (Number.isFinite(idx) && idx >= _validS) delete dict[k];
1635
+ }
1636
+ }
1637
+
1638
+ // Server-side pagination: "load earlier conversations" button
1639
+ // 仅展示当前会话时隐藏跨会话的「加载更早」按钮(更早内容本就不展示)。
1640
+ if (!onlyCurrentSession && (this.props.hasMoreHistory || this.props.loadingMore)) {
1641
+ allItems.push(
1642
+ <div key="load-more-history" className={styles.loadMoreWrap}>
1643
+ {this.props.loadingMore ? (
1644
+ <div className={`${styles.loadMoreBtn} ${styles.loadMoreBtnLoading}`}>
1645
+ <span className={styles.loadMoreSpinner} />
1646
+ {t('ui.loadingMoreHistory')}
1647
+ </div>
1648
+ ) : (
1649
+ <button className={styles.loadMoreBtn} onClick={() => this.props.onLoadMoreHistory && this.props.onLoadMoreHistory()}>
1650
+ {t('ui.loadEarlierConversations')}
1651
+ </button>
1652
+ )}
1653
+ </div>
1654
+ );
1655
+ }
1656
+
1657
+ let subIdx = 0;
1658
+ // 仅展示当前会话:跳过更早 session 的 SubAgent entries,避免它们 bleed 进当前 session 顶部
1659
+ //(下方 forEach 对更早 session 直接 return,不会推进 subIdx,故这里先把游标快进到当前 session 起点)。
1660
+ if (onlyCurrentSession && mainAgentSessions.length > 1) {
1661
+ const shown = mainAgentSessions[mainAgentSessions.length - 1];
1662
+ const shownStart = shown && shown.messages && shown.messages[0] && shown.messages[0]._timestamp;
1663
+ if (shownStart) {
1664
+ while (subIdx < subAgentEntries.length && subAgentEntries[subIdx].timestamp < shownStart) subIdx++;
1665
+ }
1666
+ }
1667
+ // 跨 session 跟踪当前活跃的 plan tool_use id(最末非 null 即为当前 pending)。
1668
+ // 写入 this._currentLastPendingPlanId 供 componentDidUpdate 派生 pendingPtyPlan 用。
1669
+ let buildLpid = null;
1670
+
1671
+ const startSi = onlyCurrentSession ? mainAgentSessions.length - 1 : 0;
1672
+ mainAgentSessions.forEach((session, si) => {
1673
+ // 仅展示当前会话:跳过当前(最末)session 之前的全部 session(含其冷占位「加载」按钮,见下方 _cold 分支)。
1674
+ if (si < startSi) return;
1675
+ if (si > startSi) {
1676
+ allItems.push(
1677
+ <Divider key={`session-div-${si}`} className={styles.sessionDivider}>
1678
+ <Text className={styles.sessionDividerText}>{t('ui.session')}</Text>
1679
+ </Divider>
1680
+ );
1681
+ }
1682
+
1683
+ // 冷 session 占位符
1684
+ if (session._cold) {
1685
+ const isLoading = this.props.loadingSessionId === session.sessionId;
1686
+ allItems.push(
1687
+ <div key={`cold-session-${si}`} className={styles.loadMoreWrap}>
1688
+ {isLoading ? (
1689
+ <div className={`${styles.loadMoreBtn} ${styles.loadMoreBtnLoading}`}>
1690
+ <span className={styles.loadMoreSpinner} />
1691
+ {t('ui.loadingMoreHistory')}
1692
+ </div>
1693
+ ) : (
1694
+ <button className={styles.loadMoreBtn}
1695
+ onClick={() => this.props.onLoadSession && this.props.onLoadSession(session.sessionId)}>
1696
+ {t('ui.loadSessionPlaceholder', { count: session.msgCount })}
1697
+ </button>
1698
+ )}
1699
+ </div>
1700
+ );
1701
+ return; // 跳过 renderSessionMessages
1702
+ }
1703
+
1704
+ // === session 级缓存判断 ===
1705
+ const sc = this._sessionItemCache[si];
1706
+ let msgs, lastPendingAskId, lastPendingPlanId;
1707
+ // hoist 当前 session 的 planApprovalMap,供下方增量 cache 合并守卫使用。
1708
+ // renderSessionMessages 内 L906 也基于 cached 派生 planApprovalMap,但其作用域只在 renderSessionMessages,
1709
+ // 这里的引用必须独立从 toolResultCache 取,供 healStalePendingIds 检查旧 pending plan。
1710
+ const sessionPlanApprovalMap = (getToolResultCache(session.messages) || {}).planApprovalMap || {};
1711
+ // FULL HIT / INCREMENTAL 路径下用本派生 map 做 prop diff,与 renderSessionMessages 内 L955 同源(同一 keyPrefix)。
1712
+ const mergedPlanApprovalMap = this._getMergedPlanApprovalMap(session.messages, `s${si}`);
1713
+ const _localAskForSession = this.state.localAskAnswers || {};
1714
+ const mergedAskAnswerMap = this._getMergedAskAnswerMap(session.messages, `s${si}`, _localAskForSession);
1715
+
1716
+ if (sc && sc.session === session && sc.msgsLen === session.messages.length) {
1717
+ // 完全命中:session 对象不变且消息数不变 → 直接复用。
1718
+ // 但 planApprovalMap / askAnswerMap 引用变化时(plan 审批落盘 / request_user_input 答完),
1719
+ // 刷新持有相应 tool_use 的旧 element 的 prop,避免 React 因 element 引用未变跳过 SCU 让卡片永远停在 pending 视图。
1720
+ msgs = refreshCachedItemProp(sc.items, sc.planApprovalMap, mergedPlanApprovalMap, PLAN_TOOL_NAMES, 'planApprovalMap');
1721
+ msgs = refreshCachedItemProp(msgs, sc.askAnswerMap, mergedAskAnswerMap, ASK_TOOL_NAMES, 'askAnswerMap');
1722
+ // Heal rows whose modelInfo was baked null before the request scan
1723
+ // could resolve the producer (post-refresh race). The resolver closes
1724
+ // over caches rebuilt earlier in THIS call; the write-back below
1725
+ // persists healed elements, so later FULL HITs are same-ref and free.
1726
+ msgs = refreshResolvedModelInfo(msgs, resolveModelInfo);
1727
+ lastPendingAskId = sc.lastPendingAskId;
1728
+ lastPendingPlanId = sc.lastPendingPlanId;
1729
+ } else if (sc && sc.session === session && session.messages.length > sc.msgsLen) {
1730
+ // 增量:session 对象不变但消息增长 → 只渲染新消息,拼接到缓存
1731
+ const result = this.renderSessionMessages(session.messages, `s${si}`, resolveModelInfo, tsToIndex, sc.msgsLen);
1732
+ // 旧段同样要刷新 planApprovalMap / askAnswerMap prop(同 FULL HIT 理由)
1733
+ msgs = refreshCachedItemProp(sc.items, sc.planApprovalMap, mergedPlanApprovalMap, PLAN_TOOL_NAMES, 'planApprovalMap').slice();
1734
+ msgs = refreshCachedItemProp(msgs, sc.askAnswerMap, mergedAskAnswerMap, ASK_TOOL_NAMES, 'askAnswerMap');
1735
+ // Same modelInfo healing for the reused old segment (see FULL HIT above).
1736
+ msgs = refreshResolvedModelInfo(msgs, resolveModelInfo);
1737
+ // 增量 result 范围内若无新 pending plan/ask,但 sc 旧值仍未 resolved → 保留 sc 值,
1738
+ // 否则会让 modal 在 streaming 间隙短暂关闭再重弹(闪烁)。Heal logic shared
1739
+ // via interactionOwnership.healStalePendingIds (plan checks the RAW map,
1740
+ // ask checks the MERGED map — deliberate asymmetry).
1741
+ ({ lastPendingAskId, lastPendingPlanId } = healStalePendingIds({
1742
+ resultAskId: result.lastPendingAskId,
1743
+ resultPlanId: result.lastPendingPlanId,
1744
+ prevAskId: sc.lastPendingAskId,
1745
+ prevPlanId: sc.lastPendingPlanId,
1746
+ sessionPlanApprovalMap,
1747
+ mergedAskAnswerMap,
1748
+ }));
1749
+ // 如果 lastPendingAskId 迁移了,修补旧缓存中持有旧 id 的 ChatMessage
1750
+ if (sc.lastPendingAskId && sc.lastPendingAskId !== lastPendingAskId) {
1751
+ for (let i = 0; i < msgs.length; i++) {
1752
+ if (msgs[i].props.lastPendingAskId === sc.lastPendingAskId) {
1753
+ msgs[i] = React.cloneElement(msgs[i], { lastPendingAskId: null });
1754
+ break;
1755
+ }
1756
+ }
1757
+ }
1758
+ if (sc.lastPendingPlanId && sc.lastPendingPlanId !== lastPendingPlanId) {
1759
+ for (let i = 0; i < msgs.length; i++) {
1760
+ if (msgs[i].props.lastPendingPlanId === sc.lastPendingPlanId) {
1761
+ msgs[i] = React.cloneElement(msgs[i], { lastPendingPlanId: null });
1762
+ break;
1763
+ }
1764
+ }
1765
+ }
1766
+ // streaming 期间同一 toolId 可能在多条 message 里出现(增量 push 而非 mutate)。
1767
+ // owner-idx 算法保证 result.items 内只有一条 ChatMessage 拿到 lastPendingAskId。
1768
+ // 但 sc.items 里上一轮赋的 owner 可能 stale —— 当本轮 owner 落在 result 范围内时,
1769
+ // 必须清掉 sc.items 中所有持同一 id 的 ChatMessage,否则会出现两份 portal 到 modal askSlot。
1770
+ if (result.lastPendingAskId && result.askOwnerInRange) {
1771
+ for (let i = 0; i < msgs.length; i++) {
1772
+ if (msgs[i].props.lastPendingAskId === result.lastPendingAskId) {
1773
+ msgs[i] = React.cloneElement(msgs[i], { lastPendingAskId: null });
1774
+ }
1775
+ }
1776
+ }
1777
+ if (result.lastPendingPlanId && result.planOwnerInRange) {
1778
+ for (let i = 0; i < msgs.length; i++) {
1779
+ if (msgs[i].props.lastPendingPlanId === result.lastPendingPlanId) {
1780
+ msgs[i] = React.cloneElement(msgs[i], { lastPendingPlanId: null });
1781
+ }
1782
+ }
1783
+ }
1784
+ msgs = msgs.concat(result.items);
1785
+ } else {
1786
+ // 缓存未命中 → 全量渲染
1787
+ const result = this.renderSessionMessages(session.messages, `s${si}`, resolveModelInfo, tsToIndex);
1788
+ msgs = result.items;
1789
+ lastPendingAskId = result.lastPendingAskId;
1790
+ lastPendingPlanId = result.lastPendingPlanId;
1791
+ }
1792
+
1793
+ // 更新缓存。planApprovalMap / askAnswerMap 引用用于下轮 FULL HIT / INCREMENTAL prop 刷新判断。
1794
+ this._sessionItemCache[si] = {
1795
+ session, msgsLen: session.messages.length,
1796
+ items: msgs, lastPendingAskId, lastPendingPlanId,
1797
+ // 记下本轮 planApprovalMap / askAnswerMap 引用,下轮 FULL HIT / INCREMENTAL 据此判断是否要刷新旧 element 的 prop。
1798
+ planApprovalMap: mergedPlanApprovalMap,
1799
+ askAnswerMap: mergedAskAnswerMap,
1800
+ };
1801
+ if (lastPendingPlanId) buildLpid = lastPendingPlanId;
1802
+
1803
+ // 将 SubAgent entries 按时间戳插入到 session 消息之间
1804
+ for (const m of msgs) {
1805
+ // 拆两个 ts 语义:
1806
+ // msgWallTs = m.props.timestamp (carrier) —— SubAgent 按 wall-clock 顺序穿插用
1807
+ // msgLookupTs = m.props.displayTs || m.props.timestamp —— tsItemMap 反向跳转 key
1808
+ // (assistant 已经收 displayTs={msg._generatedTs},自动走 generation ts;
1809
+ // 其他 role displayTs=undefined 自动 fallback 到 carrier)
1810
+ const msgWallTs = m.props.timestamp;
1811
+ const msgLookupTs = m.props.displayTs || m.props.timestamp;
1812
+ // 插入时间戳 <= 当前消息时间戳的 SubAgent entries
1813
+ while (subIdx < subAgentEntries.length && msgWallTs && subAgentEntries[subIdx].timestamp <= msgWallTs) {
1814
+ const sa = subAgentEntries[subIdx];
1815
+ if (sa.timestamp) tsItemMap[sa.timestamp] = allItems.length;
1816
+ allItems.push(
1817
+ <ChatMessage key={`sub-${sa.requestIndex}-${sa.timestamp}`} role="sub-agent-chat" content={sa.content} toolResultMap={sa.toolResultMap} label={sa.label} isTeammate={sa.isTeammate} timestamp={sa.timestamp} collapseToolResults={collapseToolResults} expandThinking={expandThinking} showFullToolContent={showFullToolContent} requestIndex={sa.requestIndex} onViewRequest={onViewRequest} onOpenFile={this.handleOpenToolFilePath} isHistoryLog={isHistoryLog} />
1818
+ );
1819
+ subIdx++;
1820
+ }
1821
+ if (msgLookupTs) tsItemMap[msgLookupTs] = allItems.length;
1822
+ allItems.push(m);
1823
+ }
1824
+ // 插入剩余的 SubAgent entries(时间戳在最后一条消息之后)
1825
+ while (subIdx < subAgentEntries.length) {
1826
+ const sa = subAgentEntries[subIdx];
1827
+ // 只插入属于当前 session 时间范围内的(下一个 session 之前的)
1828
+ const nextSessionStart = si < mainAgentSessions.length - 1 && mainAgentSessions[si + 1].messages?.[0]?._timestamp;
1829
+ // pin 锁在更早会话时,传入的会话已被切到「以 pin 会话结尾」,nextSessionStart 失效(无 si+1),
1830
+ // 改用 sessionUpperBoundTs(= 下一个未展示会话的起点)截断,避免更晚会话的 sub-agent 渗入。
1831
+ const bound = nextSessionStart || this.props.sessionUpperBoundTs;
1832
+ if (bound && sa.timestamp > bound) break;
1833
+ if (sa.timestamp) tsItemMap[sa.timestamp] = allItems.length;
1834
+ allItems.push(
1835
+ <ChatMessage key={`sub-${sa.requestIndex}-${sa.timestamp}`} role="sub-agent-chat" content={sa.content} toolResultMap={sa.toolResultMap} label={sa.label} isTeammate={sa.isTeammate} timestamp={sa.timestamp} collapseToolResults={collapseToolResults} expandThinking={expandThinking} showFullToolContent={showFullToolContent} requestIndex={sa.requestIndex} onViewRequest={onViewRequest} onOpenFile={this.handleOpenToolFilePath} isHistoryLog={isHistoryLog} />
1836
+ );
1837
+ subIdx++;
1838
+ }
1839
+
1840
+ });
1841
+
1842
+ // 记录滚动目标 item index
1843
+ const { scrollToTimestamp } = this.props;
1844
+ this._scrollTargetIdx = scrollToTimestamp && tsItemMap[scrollToTimestamp] != null
1845
+ ? tsItemMap[scrollToTimestamp] : null;
1846
+ this._tsItemMap = tsItemMap;
1847
+ // 镜像本轮最末活跃 lpid 给 componentDidUpdate 派生 pendingPtyPlan 用
1848
+ this._currentLastPendingPlanId = buildLpid;
1849
+
1850
+ // Avatar animation loading strategy: stale teammate rows are cloned to
1851
+ // static so a refresh of a long session does not start hundreds of SMIL
1852
+ // timelines at once (see avatarAnimationPostPass.js for the policy).
1853
+ applyAvatarAnimationTargets(allItems);
1854
+
1855
+ return allItems;
1856
+ }
1857
+
1858
+ // 渲染窗口裁剪:只保留最近 ITEM_LIMIT(+已展开) 条 item。移动端与桌面端统一处理
1859
+ // (桌面端启用裁剪以避免长任务全量渲染卡死,见 DESKTOP_ITEM_LIMIT 注释)。
1860
+ _applyMobileSlice(allItems) {
1861
+ this._totalItemCount = allItems.length;
1862
+ // logfile 只读模式:虚拟化平台(Android,渲染 O(1))直接一次性全量渲染;
1863
+ // 桌面/iOS/iPad 不走虚拟化,单次全量挂 DOM 会重现 DESKTOP_ITEM_LIMIT 注释里点名的长任务卡死,
1864
+ // 故保留 ITEM_LIMIT 窗口裁剪,由 _maybeScheduleLocalLogAutoFill 逐帧自动扩窗至全量(无需手工点击)。
1865
+ if (this.props.isLocalLog && useVirtuoso) {
1866
+ this._mobileSliceOffset = 0;
1867
+ return allItems;
1868
+ }
1869
+ const limit = ITEM_LIMIT + this._mobileExtraItems;
1870
+ if (allItems.length <= limit) {
1871
+ this._mobileSliceOffset = 0;
1872
+ return allItems;
1873
+ }
1874
+ const offset = allItems.length - limit;
1875
+ this._mobileSliceOffset = offset;
1876
+ // Adjust scroll target index
1877
+ if (this._scrollTargetIdx != null) {
1878
+ this._scrollTargetIdx -= offset;
1879
+ if (this._scrollTargetIdx < 0) this._scrollTargetIdx = null;
1880
+ }
1881
+ // Adjust tsItemMap
1882
+ if (this._tsItemMap) {
1883
+ const newMap = {};
1884
+ for (const [ts, idx] of Object.entries(this._tsItemMap)) {
1885
+ const adjusted = idx - offset;
1886
+ if (adjusted >= 0) newMap[ts] = adjusted;
1887
+ }
1888
+ this._tsItemMap = newMap;
1889
+ }
1890
+ return allItems.slice(offset);
1891
+ }
1892
+
1893
+ _extractSuggestion() {
1894
+ const { mainAgentSessions } = this.props;
1895
+ if (!mainAgentSessions?.length) return null;
1896
+ const lastSession = mainAgentSessions[mainAgentSessions.length - 1];
1897
+ const msgs = lastSession?.messages;
1898
+ if (!Array.isArray(msgs) || msgs.length === 0) return null;
1899
+ // 只有 SUGGESTION MODE 请求的响应才是有效建议
1900
+ const lastUserMsg = msgs[msgs.length - 1];
1901
+ if (lastUserMsg?.role !== 'user') return null;
1902
+ const userContent = lastUserMsg.content;
1903
+ const hasSuggestionMode = Array.isArray(userContent)
1904
+ ? userContent.some(b => b.type === 'text' && /^\[SUGGESTION MODE:/i.test((b.text || '').trim()))
1905
+ : typeof userContent === 'string' && /^\[SUGGESTION MODE:/im.test(userContent.trim());
1906
+ if (!hasSuggestionMode) return null;
1907
+ const resp = lastSession?.response;
1908
+ if (!resp) return null;
1909
+ const body = resp.body;
1910
+ if (!body) return null;
1911
+ const stop = body.stop_reason;
1912
+ if (stop !== 'end_turn' && stop !== 'max_tokens') return null;
1913
+ const content = body.content;
1914
+ if (!Array.isArray(content)) return null;
1915
+ for (let i = content.length - 1; i >= 0; i--) {
1916
+ if (content[i].type === 'text' && content[i].text?.trim()) {
1917
+ return content[i].text.trim();
1918
+ }
1919
+ }
1920
+ return null;
1921
+ }
1922
+
1923
+ _updateSuggestion() {
1924
+ const text = this._extractSuggestion();
1925
+ this.setState({ inputSuggestion: text || null });
1926
+ }
1927
+
1928
+ _loadPresets() {
1929
+ // 数据从 props 派生 (SettingsContext 集中 fetch);未 ready 时静默返回,
1930
+ // componentDidUpdate 监听 props.preferences 后重试。
1931
+ const data = this.props.preferences;
1932
+ if (!data) return;
1933
+ const dismissed = Array.isArray(data.dismissedBuiltinPresets) ? new Set(data.dismissedBuiltinPresets) : new Set();
1934
+ let items = [];
1935
+ if (Array.isArray(data.presetShortcuts)) {
1936
+ items = data.presetShortcuts.map((item, i) => {
1937
+ if (typeof item === 'string') return { id: Date.now() + i, teamName: '', description: item };
1938
+ return { id: Date.now() + i, teamName: item.teamName || '', description: item.description || '',
1939
+ ...(item.builtinId ? { builtinId: item.builtinId } : {}), ...(item.modified ? { modified: true } : {}) };
1940
+ });
1941
+ }
1942
+ const existingBuiltinIds = new Set(items.filter(i => i.builtinId).map(i => i.builtinId));
1943
+ for (const bp of BUILTIN_PRESETS) {
1944
+ if (dismissed.has(bp.builtinId) || existingBuiltinIds.has(bp.builtinId)) continue;
1945
+ items.unshift({ id: Date.now() + Math.random(), builtinId: bp.builtinId, teamName: bp.teamName, description: bp.description });
1946
+ }
1947
+ const customExperts = Array.isArray(data.customUltraplanExperts) ? data.customUltraplanExperts : [];
1948
+ const expertOrder = Array.isArray(data.ultraplanExpertOrder) ? data.ultraplanExpertOrder : [];
1949
+ const expertHidden = Array.isArray(data.ultraplanExpertHidden) ? data.ultraplanExpertHidden : [];
1950
+ const next = {
1951
+ presetItems: items,
1952
+ customUltraplanExperts: customExperts,
1953
+ ultraplanExpertOrder: expertOrder,
1954
+ ultraplanExpertHidden: expertHidden,
1955
+ };
1956
+ // 若当前选中的变体已不可见(被另一端删除 / 隐藏),回退到首个可见专家(无可见则 codeExpert)。
1957
+ // 与 TerminalPanel._loadPresetShortcuts 同款语义。
1958
+ const current = this.state.ultraplanVariant;
1959
+ const visible = visibleExpertKeys(customExperts, expertOrder, expertHidden);
1960
+ if (typeof current === 'string' && !visible.includes(current)) {
1961
+ next.ultraplanVariant = visible[0] || 'codeExpert';
1962
+ }
1963
+ this.setState(next);
1964
+ }
1965
+
1966
+ // 清空上下文的直接执行体(无确认弹层):移动端菜单项经 Modal.confirm 包装调用;
1967
+ // 桌面输入栏独立按钮由 ChatInputBar 的 Popconfirm(与终端工具栏同款气泡)确认后调用。
1968
+ _doClearContext = () => {
1969
+ const textarea = this._inputRef.current;
1970
+ if (!textarea) return;
1971
+ textarea.value = '/clear';
1972
+ // /clear 不应被「上传缓发」卡住:先撤销缓发 + 清空上传占位(并 revoke url),
1973
+ // setState 提交后再发 → handleInputSend 守卫看到 uploadingItems 为空,不会 defer。
1974
+ this._clearDeferSend();
1975
+ for (const u of this.state.uploadingItems) { if (u.previewUrl) { try { URL.revokeObjectURL(u.previewUrl); } catch {} } }
1976
+ this.setState({ inputEmpty: false, pendingImages: [], uploadingItems: [] }, () => this.handleInputSend());
1977
+ this.props.onClearContextOptimistic?.();
1978
+ };
1979
+
1980
+ handlePresetSend = (description) => {
1981
+ if (!description) return;
1982
+ const textarea = this._inputRef.current;
1983
+ if (textarea) {
1984
+ textarea.value = description;
1985
+ textarea.style.height = 'auto';
1986
+ textarea.style.height = Math.min(textarea.scrollHeight, (isMobile && !isPad) ? 160 : 120) + 'px';
1987
+ this.setState({ inputEmpty: false });
1988
+ textarea.focus();
1989
+ } else if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
1990
+ // 终端模式下没有 textarea,直接通过 PTY 发送。
1991
+ // 用 bracket-paste 包裹避免 description 含 `/` `!` `\t` 等被 Ink TUI 当特殊键解析。
1992
+ this._inputWs.send(JSON.stringify({
1993
+ type: 'input-sequential',
1994
+ chunks: buildBracketPasteSubmitChunks(description),
1995
+ settleMs: BRACKET_PASTE_SUBMIT_SETTLE_MS,
1996
+ }));
1997
+ }
1998
+ };
1999
+
2000
+ // ─── UltraPlan handlers ─────────────────────────────────
2001
+ _handleUltraplanSend = () => {
2002
+ const trimmed = this.state.ultraplanPrompt.trim();
2003
+ if (!trimmed && this.state.ultraplanFiles.length === 0) return;
2004
+ const filePaths = this.state.ultraplanFiles.map(f => `"${f.path}"`).join(' ');
2005
+ const userInput = filePaths ? (trimmed ? `${filePaths} ${trimmed}` : filePaths) : trimmed;
2006
+ const variant = this.state.ultraplanVariant;
2007
+ let assembled;
2008
+ if (typeof variant === 'string' && variant.startsWith('custom:')) {
2009
+ const id = variant.slice('custom:'.length);
2010
+ const item = this.state.customUltraplanExperts.find(e => e.id === id);
2011
+ if (!item) return;
2012
+ assembled = buildLocalUltraplan(userInput, 'custom', undefined, item.content);
2013
+ } else {
2014
+ assembled = buildLocalUltraplan(userInput, variant);
2015
+ }
2016
+ if (!assembled) return;
2017
+
2018
+ // WS 断开时不直接丢消息:把 assembled 写回 textarea 作草稿,让用户能手动重试。
2019
+ // 不设 pendingInput(没真发出去,不该显示 optimistic bubble)。
2020
+ const wsOpen = this._inputWs && this._inputWs.readyState === WebSocket.OPEN;
2021
+ if (!wsOpen) {
2022
+ const ta = this._inputRef.current;
2023
+ if (ta) {
2024
+ ta.value = assembled;
2025
+ ta.style.height = 'auto';
2026
+ ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
2027
+ }
2028
+ this.setState({
2029
+ ultraplanModalOpen: false,
2030
+ ultraplanPopoverOpen: false,
2031
+ ultraplanPrompt: '',
2032
+ ultraplanVariant: 'codeExpert',
2033
+ ultraplanFiles: [],
2034
+ inputEmpty: ta ? !assembled : true,
2035
+ }, () => {
2036
+ if (ta) ta.focus();
2037
+ });
2038
+ return;
2039
+ }
2040
+
2041
+ this.setState({
2042
+ ultraplanModalOpen: false,
2043
+ ultraplanPopoverOpen: false,
2044
+ ultraplanPrompt: '',
2045
+ ultraplanVariant: 'codeExpert',
2046
+ ultraplanFiles: [],
2047
+ pendingInput: userInput,
2048
+ inputSuggestion: null,
2049
+ }, () => {
2050
+ if (this.props.sdkMode) {
2051
+ this._inputWs.send(JSON.stringify({ type: 'sdk-user-message', text: assembled }));
2052
+ } else {
2053
+ this._inputWs.send(JSON.stringify({
2054
+ type: 'input-sequential',
2055
+ chunks: buildBracketPasteSubmitChunks(assembled),
2056
+ settleMs: BRACKET_PASTE_SUBMIT_SETTLE_MS,
2057
+ }));
2058
+ }
2059
+ this.scrollToBottom();
2060
+ });
2061
+ };
2062
+
2063
+ // UltraPlan 文件 / 专家逻辑委托给共享控制器(见 ../utils/ultraplanController)。方法名保持不变,render 零改动。
2064
+ _handleUltraplanUpload = (...a) => this._ultraplan.handleUpload(...a);
2065
+
2066
+ _handleUltraplanPaste = (...a) => this._ultraplan.handlePaste(...a);
2067
+
2068
+ _handleUltraplanRemoveFile = (...a) => this._ultraplan.handleRemoveFile(...a);
2069
+
2070
+ // UltraPlanModal pointerup 时回调 —— 一次性 setState + 写双 localStorage key。
2071
+ // 拖拽期 UltraPlanModal 已直接改 DOM style,这里只负责落盘 + 下次打开恢复。
2072
+ _handleUltraplanModalSizeChange = (size) => {
2073
+ if (!size) return;
2074
+ this.setState({ ultraplanModalSize: size });
2075
+ _writeUltraplanModalSize(size);
2076
+ };
2077
+
2078
+ _openCustomUltraplanEditor = (item) => {
2079
+ if (isMobile) {
2080
+ // 移动端:打开专家编辑器时关闭父 UltraPlan 弹窗,避免两层 modal 叠加遮挡。
2081
+ // 用快照记录原本的打开状态,close 时按实际状态恢复——防御未来非 UltraPlan 路径调用。
2082
+ this.setState(prev => ({
2083
+ customUltraplanEditOpen: true,
2084
+ customUltraplanEditing: item || null,
2085
+ _ultraplanWasOpenBeforeEdit: prev.ultraplanModalOpen,
2086
+ ultraplanModalOpen: false,
2087
+ }));
2088
+ return;
2089
+ }
2090
+ // 桌面 popover 路径与终端逐字一致:编辑器打开不收面板,
2091
+ // 靠 _ultraplanPopoverOnOpenChange 守卫拦编辑器 mask 的外部点击
2092
+ this.setState({ customUltraplanEditOpen: true, customUltraplanEditing: item || null });
2093
+ };
2094
+
2095
+ _closeCustomUltraplanEditor = () => {
2096
+ if (isMobile) {
2097
+ this.setState(prev => ({
2098
+ customUltraplanEditOpen: false,
2099
+ customUltraplanEditing: null,
2100
+ ultraplanModalOpen: !!prev._ultraplanWasOpenBeforeEdit,
2101
+ _ultraplanWasOpenBeforeEdit: false,
2102
+ }));
2103
+ return;
2104
+ }
2105
+ this.setState({ customUltraplanEditOpen: false, customUltraplanEditing: null });
2106
+ };
2107
+
2108
+ // 桌面输入栏 UltraPlan 弹层的关闭守卫(与 TerminalPanel 的 onOpenChange 同机制):
2109
+ // 编辑器/管理弹窗是 portal Modal,其 mask 对本 Popover 而言是「外部点击」——rc-trigger
2110
+ // 的 capture-phase mousedown 早于 rc-dialog 的 bubble click,守卫读到的状态仍为 true,
2111
+ // 直接拦掉本次关闭;lightbox / ConfirmRemoveButton 气泡同理。
2112
+ _ultraplanPopoverOnOpenChange = (v) => {
2113
+ if (!v && (this.state.ultraplanLightbox || this.state.ultraplanConfirming
2114
+ || this.state.customUltraplanEditOpen || this.state.ultraplanManagerOpen)) return;
2115
+ if (!v) this.setState({ ultraplanPopoverOpen: false });
2116
+ };
2117
+
2118
+ _saveCustomUltraplanExpert = (...a) => this._ultraplan.saveExpert(...a);
2119
+
2120
+ _deleteCustomUltraplanExpert = (...a) => this._ultraplan.deleteExpert(...a);
2121
+
2122
+ // 通过 TerminalWsContext 共享单条 ws,本方法接收消息派发。
2123
+ // 注:`data` 分支不能省 — _ptyPrompt.appendData → detectPrompt 解析出的 ptyPrompt state 被多处引用
2124
+ // (renderDangerApproval / SubAgent 兜底权限面板路由 / handlePlanFeedbackSubmit isDanger 检查 /
2125
+ // _submitViaSequentialQueue 非 danger 类型自检 等)。合并 ws 后 ChatView 仍需要这条解析路径,
2126
+ // CPU 开销保留,但网络层 1 条 ws 是仍有收益(改前 2 条同时收同一份 PTY 流)。
2127
+ _onTerminalWsMessage = (msg) => {
2128
+ try {
2129
+ // ask / sdk-ask 类消息交给 AskFlowController;返回 true 表示已处理 → 短路。
2130
+ if (this._askFlow.handleWsMessage(msg)) return;
2131
+ if (msg.type === 'data' || msg.type === 'data-resync') {
2132
+ // data-resync(反压恢复快照)同样喂给 prompt 检测:洪泛窗口内出现的交互
2133
+ // prompt 仍在快照末尾,控制器内部 buffer 自身 4KB 滚动封顶,无内存风险
2134
+ this._ptyPrompt.appendData(msg.data);
2135
+ } else if (msg.type === 'exit') {
2136
+ this._ptyPrompt.clearPrompt();
2137
+ } else if (msg.type === 'sdk-plan-pending') {
2138
+ // SDK mode: show plan approval UI
2139
+ this.setState({ pendingPlanApproval: { id: msg.id, input: msg.input } });
2140
+ } else if (msg.type === 'approval-reviewer-changed') {
2141
+ this.props.onApprovalsReviewerSynced?.(msg.approvalsReviewer);
2142
+ } else if (msg.type === 'perm-hook-pending') {
2143
+ // Queue support: if a permission panel is already showing, queue the new one.
2144
+ // Codex auto_review requests are deferred by perm-bridge and never arrive here.
2145
+ this.setState(state => {
2146
+ if (state.pendingPermission) {
2147
+ return { permissionQueue: [...state.permissionQueue, { id: msg.id, toolName: msg.toolName, input: msg.input }] };
2148
+ }
2149
+ return { pendingPermission: { id: msg.id, toolName: msg.toolName, input: msg.input } };
2150
+ });
2151
+ } else if (msg.type === 'perm-hook-timeout') {
2152
+ // Timeout carries id — only clear if it matches the active request, or remove from queue
2153
+ this.setState(state => {
2154
+ if (msg.id && state.pendingPermission?.id === msg.id) {
2155
+ const next = state.permissionQueue[0] || null;
2156
+ return { pendingPermission: next, permissionQueue: state.permissionQueue.slice(1) };
2157
+ }
2158
+ if (msg.id) {
2159
+ return { permissionQueue: state.permissionQueue.filter(p => p.id !== msg.id) };
2160
+ }
2161
+ // Legacy timeout without id — clear all
2162
+ return { pendingPermission: null, permissionQueue: [] };
2163
+ });
2164
+ } else if (msg.type === 'perm-hook-resolved') {
2165
+ // 另一端已审批,清除本端面板 or remove from queue
2166
+ this.setState(state => {
2167
+ if (state.pendingPermission?.id === msg.id) {
2168
+ const next = state.permissionQueue[0] || null;
2169
+ return { pendingPermission: next, permissionQueue: state.permissionQueue.slice(1) };
2170
+ }
2171
+ return { permissionQueue: state.permissionQueue.filter(p => p.id !== msg.id) };
2172
+ });
2173
+ } else if (msg.type === 'sdk-plan-resolved') {
2174
+ if (this.state.pendingPlanApproval?.id === msg.id) {
2175
+ this.setState({ pendingPlanApproval: null });
2176
+ }
2177
+ } else if (msg.type === 'image-upload-notify') {
2178
+ // 另一个视图/设备上传了图片,同步到本端 pendingImages
2179
+ this._addPendingImage(msg.path, msg.source);
2180
+ } else if (msg.type === 'image-remove-notify') {
2181
+ // 另一端删除了预览中的文件,同步移除
2182
+ if (msg.path) {
2183
+ this.setState(prev => {
2184
+ const next = prev.pendingImages.filter(img => img.path !== msg.path);
2185
+ if (next.length === prev.pendingImages.length) return null;
2186
+ return { pendingImages: next };
2187
+ });
2188
+ }
2189
+ }
2190
+ } catch (e) { reportSwallowed('ws.terminal-msg', e, { msgType: msg?.type }); }
2191
+ };
2192
+
2193
+ // ws 状态变更监听:close 时清残留审批面板(原 _inputWs.onclose 行为);Provider 内部已自动 2s 重连。
2194
+ // 'open' 的 ask 相关恢复逻辑(重发 _pendingCancelIds + 拉 /api/pending-asks)已搬到 AskFlowController.onWsOpen。
2195
+ _onTerminalWsState = (state) => {
2196
+ if (state === 'open') {
2197
+ this._askFlow.onWsOpen();
2198
+ return;
2199
+ }
2200
+ if (state !== 'close') return;
2201
+ if (this.state.pendingPtyPlan?.id) {
2202
+ this._resolvedPlanIds.add(this.state.pendingPtyPlan.id);
2203
+ }
2204
+ // Drain Electron pendingByTab[tabId].ask Map:清 askQueue 时每条都得逐条 notifyAskResolved,
2205
+ // 否则 dock badge / flashFrame 状态不归零。镜像 componentWillUnmount drain 模式 +
2206
+ // per-item try-catch 防单条 IPC throw 中断后续。
2207
+ try {
2208
+ if (typeof window !== 'undefined' && window.tabBridge?.notifyAskResolved) {
2209
+ const tabId = this.props.ownTabId ?? null;
2210
+ if (this.state.pendingAsk) {
2211
+ try { window.tabBridge.notifyAskResolved({ id: this.state.pendingAsk.id, tabId }); } catch {}
2212
+ }
2213
+ for (const q of (this.state.askQueue || [])) {
2214
+ try { window.tabBridge.notifyAskResolved({ id: q.id, tabId }); } catch {}
2215
+ }
2216
+ }
2217
+ } catch {}
2218
+ if (this.state.pendingPermission || this.state.pendingPlanApproval || this.state.pendingAsk
2219
+ || this.state.pendingPtyPlan || this.state.askQueue?.length) {
2220
+ this.setState({ pendingPermission: null, permissionQueue: [], pendingPlanApproval: null, pendingAsk: null, askQueue: [], askMetaMap: {}, pendingPtyPlan: null });
2221
+ }
2222
+ // ask 实例 flag 由控制器 reset(state 键已在上面那条合并 setState 里清)
2223
+ this._askFlow.resetAskFlagsOnClose();
2224
+ };
2225
+
2226
+ // ── 「Plan 自动审批」:plan 提交后短倒计时,到点自动选中批准项;期间可取消 ──────────────
2227
+ // 倒计时跑在 ChatView(稳定单例,持有 ws / _promptSubmitting / _resolvedPlanIds 守卫与批准入口),
2228
+ // 仅把剩余秒数与取消回调下发给 inline 卡片显示。仅 cliMode(PTY) 路径,SDK 模式不接入。
2229
+ _startPlanAutoApprove = (planId) => {
2230
+ if (this._planAutoTimer) return; // 已在倒计时
2231
+ const seconds = this.props.planAutoApproveSeconds; // 0=关 / N=N 秒倒计时 / -1=立即
2232
+ if (!this.props.cliMode || !seconds) return; // 0/undefined → 关闭
2233
+ if (!planId || this._planAutoCancelled.has(planId)) return; // 已被用户取消的 plan 不再重启
2234
+ const ws = this._inputWs;
2235
+ if (!ws || ws.readyState !== WebSocket.OPEN) return; // ws 未就绪 → 不启动(避免空发)
2236
+ if (seconds === AUTO_APPROVE_INSTANT) { this._firePlanAutoApprove(planId); return; } // 立即批准,无倒计时
2237
+ let remaining = seconds;
2238
+ this.setState({ planAutoApproveCountdown: remaining });
2239
+ this._planAutoTimer = setInterval(() => {
2240
+ remaining -= 1;
2241
+ if (remaining <= 0) {
2242
+ this._clearPlanAutoApprove();
2243
+ this._firePlanAutoApprove(planId);
2244
+ } else {
2245
+ this.setState({ planAutoApproveCountdown: remaining });
2246
+ }
2247
+ }, 1000);
2248
+ };
2249
+
2250
+ _clearPlanAutoApprove = () => {
2251
+ if (this._planAutoTimer) { clearInterval(this._planAutoTimer); this._planAutoTimer = null; }
2252
+ if (!this._unmounted && this.state.planAutoApproveCountdown != null) {
2253
+ this.setState({ planAutoApproveCountdown: null });
2254
+ }
2255
+ };
2256
+
2257
+ _firePlanAutoApprove = (planId) => {
2258
+ // 到点再校验一次:仍是同一个 pending plan、未被取消、ws 仍开。
2259
+ if (this.state.pendingPtyPlan?.id !== planId) return;
2260
+ if (this._planAutoCancelled.has(planId)) return;
2261
+ const ws = this._inputWs;
2262
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2263
+ // 选批准项:优先用检测到的 plan prompt 选项做文本匹配,否则回退 option 1(与卡片按钮判定一致)。
2264
+ const planPrompt = this.state.ptyPromptHistory.slice().reverse().find(p => isPlanApprovalPrompt(p) && p.status === 'active');
2265
+ const approveNum = pickPlanApproveOptionNumber(planPrompt?.options || []);
2266
+ this.handlePromptOptionClick(approveNum); // 复用同一入口:自带 _promptSubmitting / ws / _resolvedPlanIds 守卫
2267
+ };
2268
+
2269
+ // 用户在倒计时期间点「取消」:记下该 plan id 不再重启,并停掉倒计时(保持待手动审批)。
2270
+ cancelPlanAutoApprove = () => {
2271
+ const id = this.state.pendingPtyPlan?.id;
2272
+ if (id) this._planAutoCancelled.add(id);
2273
+ this._clearPlanAutoApprove();
2274
+ };
2275
+
2276
+ handlePromptOptionClick = (number) => {
2277
+ if (this._promptSubmitting) return;
2278
+ const ws = this._inputWs;
2279
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2280
+ // ptyPrompt 可能为 null(plan 卡片渲染后 PTY prompt 尚未检测到),
2281
+ // 回退到 ptyPromptHistory 中最近的 active prompt,或构造默认 prompt(光标在第1项)
2282
+ let prompt = this.state.ptyPrompt;
2283
+ if (!prompt) {
2284
+ prompt = this.state.ptyPromptHistory.slice().reverse().find(p => p.status === 'active')
2285
+ || { options: Array.from({ length: Math.max(number, 3) }, (_, i) => ({ number: i + 1, selected: i === 0 })) };
2286
+ }
2287
+ this._promptSubmitting = true;
2288
+
2289
+ // Codex TUI 使用 Ink SelectInput,需要用箭头键移动光标再回车
2290
+ const options = prompt.options;
2291
+ const targetIdx = options.findIndex(o => o.number === number);
2292
+ let currentIdx = options.findIndex(o => o.selected);
2293
+ if (currentIdx < 0) currentIdx = 0;
2294
+
2295
+ const diff = targetIdx - currentIdx;
2296
+ const arrowKey = diff > 0 ? '\x1b[B' : '\x1b[A';
2297
+ const steps = Math.abs(diff);
2298
+
2299
+ const sendStep = (i) => {
2300
+ if (i < steps) {
2301
+ ws.send(JSON.stringify({ type: 'input', data: arrowKey }));
2302
+ setTimeout(() => sendStep(i + 1), 30);
2303
+ } else {
2304
+ setTimeout(() => {
2305
+ if (ws.readyState === WebSocket.OPEN) {
2306
+ ws.send(JSON.stringify({ type: 'input', data: '\r' }));
2307
+ }
2308
+ }, 50);
2309
+ }
2310
+ };
2311
+ sendStep(0);
2312
+
2313
+ // 标记历史中最后一个 active 为 answered
2314
+ this._ptyPrompt.setCurrent(null);
2315
+ // 用户已提交本轮 plan:把当前 pendingPtyPlan.id 加入 _resolvedPlanIds,
2316
+ // 防 PTY 答案到 JSONL 之间的窗口期 CDU 把 modal 重弹(lpid 仍指向同一 id 直到 tool_result 写入 planApprovalMap)。
2317
+ if (this.state.pendingPtyPlan?.id) {
2318
+ this._resolvedPlanIds.add(this.state.pendingPtyPlan.id);
2319
+ }
2320
+ this.setState(state => {
2321
+ const history = state.ptyPromptHistory.slice();
2322
+ const last = history[history.length - 1];
2323
+ if (last && last.status === 'active') {
2324
+ history[history.length - 1] = { ...last, status: 'answered', selectedNumber: number };
2325
+ }
2326
+ // Atomic clear of pendingPtyPlan so the global modal closes in lockstep with the inline state.
2327
+ return { ptyPrompt: null, ptyPromptHistory: history, pendingPtyPlan: null };
2328
+ });
2329
+ this._ptyPrompt.resetBufferAfterSubmit();
2330
+ setTimeout(() => { this._promptSubmitting = false; }, 500);
2331
+ };
2332
+
2333
+ // Shift the next queued permission request into active position
2334
+ // 权限审批委托给 PermissionController(见 ./controllers/permissionController)。方法名不变,render/cDU 引用不动。
2335
+ handlePermissionAllow = (id) => this._permission.allow(id);
2336
+ handlePermissionAllowSession = (id) => this._permission.allowSession(id);
2337
+ handlePermissionDeny = (id) => this._permission.deny(id);
2338
+
2339
+ handlePlanApprove = (id) => {
2340
+ const ws = this._inputWs;
2341
+ if (ws && ws.readyState === WebSocket.OPEN) {
2342
+ ws.send(JSON.stringify({ type: 'sdk-plan-answer', id, approve: true }));
2343
+ }
2344
+ this.setState({ pendingPlanApproval: null });
2345
+ };
2346
+
2347
+ handlePlanReject = (id, feedback) => {
2348
+ const ws = this._inputWs;
2349
+ if (ws && ws.readyState === WebSocket.OPEN) {
2350
+ ws.send(JSON.stringify({ type: 'sdk-plan-answer', id, approve: false, feedback: feedback || '' }));
2351
+ }
2352
+ this.setState({ pendingPlanApproval: null });
2353
+ };
2354
+
2355
+ handlePlanFeedbackSubmit = (number, text) => {
2356
+ const ws = this._inputWs;
2357
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2358
+ let prompt = this.state.ptyPrompt;
2359
+ if (!prompt) {
2360
+ prompt = this.state.ptyPromptHistory.slice().reverse().find(p => p.status === 'active')
2361
+ || { options: Array.from({ length: Math.max(number, 3) }, (_, i) => ({ number: i + 1, selected: i === 0 })) };
2362
+ }
2363
+
2364
+ const options = prompt.options;
2365
+ const targetIdx = options.findIndex(o => o.number === number);
2366
+ let currentIdx = options.findIndex(o => o.selected);
2367
+ if (currentIdx < 0) currentIdx = 0;
2368
+ const diff = targetIdx - currentIdx;
2369
+ const arrowKey = diff > 0 ? '\x1b[B' : '\x1b[A';
2370
+ const steps = Math.abs(diff);
2371
+
2372
+ const sendStep = (i) => {
2373
+ if (i < steps) {
2374
+ ws.send(JSON.stringify({ type: 'input', data: arrowKey }));
2375
+ setTimeout(() => sendStep(i + 1), 30);
2376
+ } else {
2377
+ // 回车选中选项
2378
+ setTimeout(() => {
2379
+ ws.send(JSON.stringify({ type: 'input', data: '\r' }));
2380
+ // 轮询等待 CLI 进入文本输入模式(buffer 变化说明已响应)
2381
+ const startBuf = this._ptyPrompt.getBuffer();
2382
+ let attempts = 0;
2383
+ const poll = () => {
2384
+ attempts++;
2385
+ if (attempts > 20 || this._ptyPrompt.getBuffer() !== startBuf) {
2386
+ if (ws.readyState === WebSocket.OPEN) {
2387
+ ws.send(JSON.stringify({ type: 'input', data: text }));
2388
+ setTimeout(() => {
2389
+ ws.send(JSON.stringify({ type: 'input', data: '\r' }));
2390
+ }, 50);
2391
+ }
2392
+ return;
2393
+ }
2394
+ setTimeout(poll, 100);
2395
+ };
2396
+ setTimeout(poll, 100);
2397
+ }, 50);
2398
+ }
2399
+ };
2400
+ sendStep(0);
2401
+
2402
+ this._ptyPrompt.setCurrent(null);
2403
+ // 用户已提交 plan + feedback:守 modal 不被 CDU 重弹(同 handlePromptOptionClick 注释)
2404
+ if (this.state.pendingPtyPlan?.id) {
2405
+ this._resolvedPlanIds.add(this.state.pendingPtyPlan.id);
2406
+ }
2407
+ this.setState(state => {
2408
+ const history = state.ptyPromptHistory.slice();
2409
+ const last = history[history.length - 1];
2410
+ if (last && last.status === 'active') {
2411
+ history[history.length - 1] = { ...last, status: 'answered', selectedNumber: number };
2412
+ }
2413
+ // Atomic clear of pendingPtyPlan — modal closes together with inline state.
2414
+ return { ptyPrompt: null, ptyPromptHistory: history, pendingPtyPlan: null };
2415
+ });
2416
+ this._ptyPrompt.resetBufferAfterSubmit();
2417
+ };
2418
+
2419
+ // 委托 → AskFlowController(handleAskCancel 是 public 入口:bubble handlers + ChatMessage props 引用)
2420
+ handleAskCancel = (askId, reason) => this._askFlow.handleAskCancel(askId, reason);
2421
+
2422
+ /**
2423
+ * Send queued user message after ack received (or 500ms timeout best effort).
2424
+ * Extracted from handleInputSend so handleInputSend can short-circuit with ack-wait
2425
+ * when there's a pending ask to cancel first.
2426
+ *
2427
+ * skipUiState=true 用于 typed-interrupt 路径 — handleInputSend 已经先 setState pendingInput
2428
+ * 给用户即时反馈,flush 时不再重复 setState 浪费一次 commit + scrollToBottom 双触发。
2429
+ */
2430
+ _sendUserMessageImmediate = (text, textareaToReset, skipUiState) => {
2431
+ if (!text) return;
2432
+ const ws = this._inputWs;
2433
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2434
+ // /clear 自身不解锁血条 lock(lock 是清空确认后由 AppBase.handleClearContextOptimistic 设置);
2435
+ // 其他正常用户消息(包括 slash 命令)都视为「新请求」并解锁血条。
2436
+ if (!/^\s*\/clear(\s|$)/.test(text)) this.props.onUserMessageSent?.();
2437
+ if (this.props.sdkMode) {
2438
+ ws.send(JSON.stringify({ type: 'sdk-user-message', text }));
2439
+ } else {
2440
+ ws.send(JSON.stringify({ type: 'input', data: text }));
2441
+ setTimeout(() => {
2442
+ if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2443
+ this._inputWs.send(JSON.stringify({ type: 'input', data: '\r' }));
2444
+ }
2445
+ }, 50);
2446
+ }
2447
+ if (textareaToReset) {
2448
+ textareaToReset.value = '';
2449
+ textareaToReset.style.height = 'auto';
2450
+ }
2451
+ if (!skipUiState) {
2452
+ this._clearPendingImages();
2453
+ this.setState({ inputEmpty: true, pendingInput: text, inputSuggestion: null }, () => this.scrollToBottom());
2454
+ }
2455
+ };
2456
+
2457
+ // 委托 → AskFlowController
2458
+ handleAskQuestionSubmit = (answers, askId, questions) => this._askFlow.handleAskQuestionSubmit(answers, askId, questions);
2459
+
2460
+ // 清除乐观停止标志 + 兜底定时器。收口 4 处重复(流式下降沿/上升沿、会话切换、发起新一轮);
2461
+ // 各调用点自带触发条件,这里只统一做 timer + state 清理。
2462
+ _clearStopOptimistic = () => {
2463
+ clearTimeout(this._stopOptimisticTimer);
2464
+ this.setState({ stopOptimistic: false });
2465
+ };
2466
+
2467
+ handleInputStop = () => {
2468
+ // Stop 取消「等上传完成的缓发」必须在 ws 早返之前——否则 ws 不 OPEN 时点停止,缓发不会被撤销。
2469
+ // 上传本身(HTTP)继续 resolve 进 pendingImages,无害,用户可稍后再发。
2470
+ this._clearDeferSend();
2471
+ if (!this._inputWs || this._inputWs.readyState !== WebSocket.OPEN) return;
2472
+ // Stop 应 halt 所有待发:清掉 typed-interrupt 武装的 pending-flush 队列(含 500ms 兜底 timer)。
2473
+ // 否则随后到达的 ask-hook-cancelled ack(本端 handleAskCancel 或服务端 interruptTurn 广播)会
2474
+ // takePendingFlush 把它发出去 —— 等于点了"停止"又自动发消息。与服务端 interruptTurn 清空
2475
+ // _messageQueue 同源(停止即丢弃在途待发)。
2476
+ if (this._pendingFlushQueue && this._pendingFlushQueue.length) {
2477
+ for (const entry of this._pendingFlushQueue) clearTimeout(entry.tid);
2478
+ this._pendingFlushQueue.length = 0;
2479
+ }
2480
+ // 按模式分流中断(与 _sendUserMessageImmediate 的 sdkMode 分流同源):
2481
+ // SDK 模式模型不在 PTY 里,ESC 无效 → 发 sdk-interrupt 让服务端 close 当前 query(保留会话);
2482
+ // CLI/PTY 模式 → ESC 打断 Codex TUI 当前生成。
2483
+ if (this.props.sdkMode) {
2484
+ this._inputWs.send(JSON.stringify({ type: 'sdk-interrupt' }));
2485
+ } else {
2486
+ // CLI/PTY 模式:ESC 打断 Codex TUI 当前生成。
2487
+ // 关键修复:点击 HTML 停止按钮会让隐藏的 xterm 失焦,向 codex 上报 focus-out (\x1b[O),
2488
+ // 之后 codex(Ink) 会忽略 ESC(仅在「聚焦」时把 ESC 当中断)。所以先补一个 focus-in (\x1b[I)
2489
+ // 让 codex 认为终端已聚焦,再发 ESC —— 等价于在聚焦的终端里按 Esc(终端里按 Esc 本就有效)。
2490
+ this._inputWs.send(JSON.stringify({ type: 'input', data: '\x1b[I' }));
2491
+ setTimeout(() => {
2492
+ if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2493
+ this._inputWs.send(JSON.stringify({ type: 'input', data: '\x1b' }));
2494
+ }
2495
+ }, STOP_FOCUS_IN_ESC_DELAY_MS);
2496
+ }
2497
+ // 乐观即时切非运行态:中断已即时发出,按钮无需等 AppBase 的 SSE+2s 才翻 false。
2498
+ this.setState({ stopOptimistic: true });
2499
+ clearTimeout(this._stopOptimisticTimer);
2500
+ // 兜底清除(见 STOP_OPTIMISTIC_FALLBACK_MS 注释)。正常路径由 isStreaming 下降沿/上升沿先清除。
2501
+ this._stopOptimisticTimer = setTimeout(() => this.setState({ stopOptimistic: false }), STOP_OPTIMISTIC_FALLBACK_MS);
2502
+ };
2503
+
2504
+ handleInputSend = () => {
2505
+ const textarea = this._inputRef.current;
2506
+ if (!textarea) return;
2507
+ // 图片上传仍在途 → 不丢图:进入缓发态,等 uploadingItems 清零后由 componentDidUpdate 自动重跑本方法。
2508
+ // _deferSend 自身幂等(已 deferred 不重复武装),故第二次 Enter 也走这里 return,不会落到立即发送丢图。
2509
+ if (shouldDeferSend({ uploadingCount: this.state.uploadingItems.length })) {
2510
+ this._deferSend();
2511
+ return;
2512
+ }
2513
+ const userText = textarea.value.trim();
2514
+ // 拼接 pendingImages 路径到消息前面(发送时才注入,支持用户删除后不发)
2515
+ const imagePaths = this.state.pendingImages.map(img => `"${img.path.replace(/"/g, '')}"`).join(' ');
2516
+ const text = imagePaths ? (userText ? `${imagePaths} ${userText}` : imagePaths) : userText;
2517
+ if (!text) return;
2518
+
2519
+ // 发起新一轮:清除可能仍挂着的乐观停止标志,确保新 run 的按钮正确显示为停止态。
2520
+ if (this.state.stopOptimistic) this._clearStopOptimistic();
2521
+
2522
+ // 打字打断(typed-interrupt):检测到当前有 pending request_user_input 时,先发 ask-cancel
2523
+ // + 等 server ack(ask-hook-cancelled,SDK + Hook 双路统一)后再发 user message。
2524
+ // 等价 terminal Codex 的 screens/REPL.tsx:2137-2141 + handlePromptSubmit 的 abort+enqueue。
2525
+ // ack 协议防 race:cancel 必须在 user message 之前到 server,否则模型可能把 user message
2526
+ // 当 follow-up answer 处理。500ms 超时兜底(best effort,不卡死用户)。
2527
+ if (this.state.pendingAsk?.id && this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2528
+ const askId = this.state.pendingAsk.id;
2529
+ // 先把 textarea 清空 + 收 pending images(与正常路径一致的视觉反馈)
2530
+ textarea.value = '';
2531
+ textarea.style.height = 'auto';
2532
+ this._clearPendingImages();
2533
+ this.setState({ inputEmpty: true, pendingInput: userText || imagePaths, inputSuggestion: null }, () => this.scrollToBottom());
2534
+ this.handleAskCancel(askId, 'Interrupted by user');
2535
+ // 数组队列代替 Map[askId] 索引:连续两次 typed-interrupt < 500ms 时(handleAskCancel 内
2536
+ // setState 还没 commit,第二次 handleInputSend 仍读到旧 askId),避免 Map.set(askId, ...)
2537
+ // 把第一条 prompt 静默覆盖。每个 entry 自带 tid 用于精确 clear / 配对 ack。
2538
+ if (!this._pendingFlushQueue) this._pendingFlushQueue = [];
2539
+ const tid = setTimeout(() => {
2540
+ if (this._unmounted) return;
2541
+ // 500ms 兜底:按 tid 找到自己 entry,flush 后移除(不用 askId 因可能已重复)
2542
+ const idx = this._pendingFlushQueue.findIndex(e => e.tid === tid);
2543
+ if (idx >= 0) {
2544
+ const entry = this._pendingFlushQueue.splice(idx, 1)[0];
2545
+ this._sendUserMessageImmediate(entry.text, null, true);
2546
+ }
2547
+ }, 500);
2548
+ this._pendingFlushQueue.push({ askId, text, tid });
2549
+ return;
2550
+ }
2551
+
2552
+ this._sendUserMessageImmediate(text, textarea);
2553
+ };
2554
+
2555
+ handleInputKeyDown = (e) => {
2556
+ if (e.key === 'Tab' && this.state.inputSuggestion) {
2557
+ e.preventDefault();
2558
+ const textarea = this._inputRef.current;
2559
+ if (textarea) {
2560
+ textarea.value = this.state.inputSuggestion;
2561
+ textarea.style.height = 'auto';
2562
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2563
+ }
2564
+ this.setState({ inputSuggestion: null, inputEmpty: false });
2565
+ return;
2566
+ }
2567
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229) {
2568
+ e.preventDefault();
2569
+ this.handleInputSend();
2570
+ }
2571
+ };
2572
+
2573
+ handleInputChange = (e) => {
2574
+ const textarea = e.target;
2575
+ textarea.style.height = 'auto';
2576
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2577
+ const empty = !textarea.value.trim();
2578
+ this.setState({ inputEmpty: empty });
2579
+ if (this.state.inputSuggestion && !empty) {
2580
+ this.setState({ inputSuggestion: null });
2581
+ }
2582
+ };
2583
+
2584
+ handleSuggestionToTerminal = () => {
2585
+ const text = this.state.inputSuggestion;
2586
+ if (!text || !this._inputWs || this._inputWs.readyState !== WebSocket.OPEN) return;
2587
+ this._inputWs.send(JSON.stringify({ type: 'input', data: text }));
2588
+ setTimeout(() => {
2589
+ if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2590
+ this._inputWs.send(JSON.stringify({ type: 'input', data: '\r' }));
2591
+ }
2592
+ }, 50);
2593
+ this.setState({ inputSuggestion: null, pendingInput: text }, () => this.scrollToBottom());
2594
+ };
2595
+
2596
+ handleUploadPath = (path) => {
2597
+ // 不插入 textarea,不立即注入 PTY — 仅添加到预览条,发送时再拼接路径
2598
+ this._addPendingImage(path, 'chat');
2599
+ if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2600
+ this._inputWs.send(JSON.stringify({ type: 'image-upload-notify', path, source: 'chat' }));
2601
+ }
2602
+ };
2603
+
2604
+ handleSplitMouseDown = (e) => this._splitDrag.onTerminalHandleDown(e);
2605
+
2606
+ handleSidebarMouseDown = (e) => this._splitDrag.onSidebarHandleDown(e);
2607
+
2608
+ // 文件详情 scroll 快照:FileContentView 内部 onScroll throttle 后调用;写 instance ref 不触发 re-render。
2609
+ // path 守卫:throttle 100ms 内用户切文件 / unmount cleanup flush 时旧实例可能发来旧 path 的 snap,
2610
+ // 这里单点拦下,独立于 React 生命周期时序(cdU vs effect cleanup 顺序),消除"跨文件污染"整类风险。
2611
+ handleUpdateFileScroll = (snap) => {
2612
+ if (snap && snap.path !== this.state.currentFile) return;
2613
+ this._fileScrollSnapshot = snap || null;
2614
+ };
2615
+
2616
+ // FileContentView 消费一次后清空,避免下次 mount 又恢复。
2617
+ handleConsumeFileScroll = () => {
2618
+ this._fileScrollSnapshot = null;
2619
+ };
2620
+
2621
+ // 给 FileContentView 拿快照的稳定闭包,避免每次 render 创建新引用。
2622
+ getFileScrollSnapshot = () => this._fileScrollSnapshot;
2623
+
2624
+ handleToggleExpandPath = (path) => {
2625
+ // capture projectName 到闭包:用户毫秒内切 workspace 时,callback 触发的写盘
2626
+ // 应该落到"toggle 发生时"那个项目的 key,不要被切换后的 props 牵走。
2627
+ const projectName = this.props.projectName;
2628
+ this.setState(state => {
2629
+ const newSet = new Set(state.fileExplorerExpandedPaths);
2630
+ if (newSet.has(path)) {
2631
+ newSet.delete(path);
2632
+ } else {
2633
+ newSet.add(path);
2634
+ }
2635
+ return { fileExplorerExpandedPaths: newSet };
2636
+ }, () => {
2637
+ saveExpandedPaths(projectName, this.state.fileExplorerExpandedPaths);
2638
+ });
2639
+ };
2640
+
2641
+ // 点击工具调用中的文件路径,打开文件查看器
2642
+ // 绝对路径需要转为项目相对路径,以便与 FileExplorer 的 TreeNode 匹配
2643
+ handleMdImageClick = (e) => {
2644
+ const img = e.target.closest('.chat-md img');
2645
+ if (img && img.src) {
2646
+ e.preventDefault();
2647
+ this.setState({ mdLightboxSrc: img.src });
2648
+ }
2649
+ };
2650
+
2651
+ handleOpenToolFilePath = async (filePath) => {
2652
+ if (!filePath) return;
2653
+ if (tryOpenWithSystem(filePath, 'chat-message')) return;
2654
+ let resolved = filePath;
2655
+ if (filePath.startsWith('/')) {
2656
+ // 懒加载项目目录(只请求一次,后续用缓存)
2657
+ if (!this._projectDirCache) {
2658
+ try {
2659
+ const r = await fetch(apiUrl('/api/project-dir'));
2660
+ if (r.ok) {
2661
+ const data = await r.json();
2662
+ if (data && data.dir) this._projectDirCache = data.dir;
2663
+ }
2664
+ } catch { /* ignore */ }
2665
+ }
2666
+ if (this._projectDirCache && filePath.startsWith(this._projectDirCache + '/')) {
2667
+ resolved = filePath.slice(this._projectDirCache.length + 1);
2668
+ }
2669
+ }
2670
+ // 计算所有祖先目录路径,加入 expandedPaths 以展开目录树
2671
+ const parts = resolved.split('/');
2672
+ const ancestors = [];
2673
+ for (let i = 1; i < parts.length; i++) {
2674
+ ancestors.push(parts.slice(0, i).join('/'));
2675
+ }
2676
+ // 移动端:通过回调打开 MobileFileExplorer
2677
+ if (this.props.onMobileOpenFile) {
2678
+ this.props.onMobileOpenFile(resolved, ancestors);
2679
+ return;
2680
+ }
2681
+ this._setFileExplorerOpen(true);
2682
+ this.setState(prev => {
2683
+ const newSet = new Set(prev.fileExplorerExpandedPaths);
2684
+ ancestors.forEach(p => newSet.add(p));
2685
+ return {
2686
+ currentFile: resolved,
2687
+ currentGitDiff: null,
2688
+ scrollToLine: null,
2689
+ gitChangesOpen: false,
2690
+ fileExplorerExpandedPaths: newSet,
2691
+ };
2692
+ });
2693
+ };
2694
+
2695
+ _handleInsertPathToChat = (filePath) => {
2696
+ const quoted = `"${filePath}"`;
2697
+ // 终端开启时写入终端,否则写入对话输入框
2698
+ if (this.props.terminalVisible && this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
2699
+ this._inputWs.send(JSON.stringify({ type: 'input', data: quoted }));
2700
+ return;
2701
+ }
2702
+ const textarea = this._inputRef.current;
2703
+ if (!textarea) return;
2704
+ const cur = textarea.value;
2705
+ textarea.value = cur ? `${cur} ${quoted}` : quoted;
2706
+ textarea.style.height = 'auto';
2707
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2708
+ this.setState({ inputEmpty: false });
2709
+ textarea.focus();
2710
+ };
2711
+
2712
+ // Guarded width persistence shared by the drag-controller host and _snapToInitialPosition.
2713
+ _persistWidth(key, px) {
2714
+ try { localStorage.setItem(key, String(px)); } catch {}
2715
+ }
2716
+
2717
+ _snapToInitialPosition() {
2718
+ // 初始化时吸附到 60cols
2719
+ const targetCols = 60;
2720
+ const terminalPx = targetCols * TERMINAL_CHAR_WIDTH; // 468px
2721
+
2722
+ this.setState({ terminalWidth: terminalPx, needsInitialSnap: false });
2723
+ this._persistWidth(TERMINAL_WIDTH_STORAGE_KEY, terminalPx);
2724
+ }
2725
+
2726
+ _renderNavSidebar(showFileExplorerAndGit) {
2727
+ return (
2728
+ <div className={styles.navSidebar}>
2729
+ <button
2730
+ className={this.state.roleFilterOpen ? styles.navBtnActive : styles.navBtn}
2731
+ onClick={() => this.setState(prev => prev.roleFilterOpen ? { roleFilterOpen: false, roleFilterSelected: new Set() } : { roleFilterOpen: true })}
2732
+ title={t('ui.roleFilter')}
2733
+ >
2734
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
2735
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
2736
+ </svg>
2737
+ </button>
2738
+ {showFileExplorerAndGit && (
2739
+ <button
2740
+ className={this.state.fileExplorerOpen ? styles.navBtnActive : styles.navBtn}
2741
+ onClick={() => { this._setFileExplorerOpen(!this.state.fileExplorerOpen); this.setState({ gitChangesOpen: false }); }}
2742
+ title={t('ui.fileExplorer')}
2743
+ >
2744
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
2745
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 2h9a2 2 0 0 1 2 2z"/>
2746
+ </svg>
2747
+ </button>
2748
+ )}
2749
+ {showFileExplorerAndGit && this.state.hasGit && (
2750
+ <button
2751
+ className={this.state.gitChangesOpen ? styles.navBtnActive : styles.navBtn}
2752
+ onClick={() => this.setState(prev => {
2753
+ this._setFileExplorerOpen(false);
2754
+ const opening = !prev.gitChangesOpen;
2755
+ const next = { gitChangesOpen: opening };
2756
+ // 关闭期间累积的修改信号在打开瞬间消费一次(与 fileExplorer 对称)
2757
+ if (opening && this._pendingGitRefresh) {
2758
+ this._pendingGitRefresh = false;
2759
+ next.gitChangesRefresh = (prev.gitChangesRefresh || 0) + 1;
2760
+ }
2761
+ return next;
2762
+ })}
2763
+ title={t('ui.gitChanges')}
2764
+ >
2765
+ <svg width="24" height="24" viewBox="0 0 1024 1024" fill="currentColor">
2766
+ <path d="M759.53332137 326.35000897c0-48.26899766-39.4506231-87.33284994-87.87432908-86.6366625-46.95397689 0.69618746-85.08957923 39.14120645-85.39899588 86.09518335-0.23206249 40.68828971 27.53808201 74.87882971 65.13220519 84.47074592 10.82958281 2.78474987 18.41029078 12.37666607 18.64235327 23.51566553 0.38677082 21.11768647-3.40358317 44.40128953-17.24997834 63.81718442-22.20064476 31.17372767-62.42480948 42.46743545-97.93037026 52.44612248-22.43270724 6.26568719-38.75443563 7.89012462-53.14230994 9.28249954-20.42149901 2.01120825-39.76003975 3.94506233-63.89453858 17.79145747-5.10537475 2.93945818-10.13339535 6.18833303-14.85199928 9.74662453-4.09977063 3.09416652-9.90133285 0.15470833-9.90133286-4.95066641V302.60228095c0-9.43720788 5.26008307-18.17822829 13.69168683-22.3553531 28.69839444-14.23316598 48.42370599-43.93716454 48.19164353-78.20505872-0.38677082-48.57841433-41.15241468-87.71962076-89.730829-86.01782918C338.80402918 117.57112321 301.59667683 155.70672553 301.59667683 202.58334827c0 34.03583169 19.64795738 63.50776777 48.1916435 77.66357958 8.43160375 4.17712479 13.69168685 12.76343689 13.69168684 22.12329062v419.02750058c0 9.43720788-5.26008307 18.17822829-13.69168684 22.3553531-28.69839444 14.23316598-48.42370599 43.93716454-48.1916435 78.20505872 0.30941665 48.57841433 41.07506052 87.6422666 89.65347484 86.01782918C437.74000359 906.42887679 474.87000179 868.2159203 474.87000179 821.41665173c0-34.03583169-19.64795738-63.50776777-48.1916435-77.66357958-8.43160375-4.17712479-13.69168685-12.76343689-13.69168684-22.12329062v-14.85199926c0-32.48874844 15.39347842-63.27570528 42.00331048-81.91805854 2.39797906-1.70179159 4.95066642-3.32622901 7.50335379-4.79595812 14.92935344-8.58631209 25.91364457-9.66927037 44.09187287-11.4484161 15.62554091-1.54708326 35.04143581-3.48093734 61.65126786-10.90693699 39.06385228-10.98429114 92.51557887-25.91364457 124.84961898-71.39789238 18.56499911-26.06835292 27.38337367-58.01562219 26.37776956-95.14562041-0.15470833-5.33743724-0.54147915-10.67487447-1.08295828-16.16702004-0.85089578-8.27689543 2.70739569-16.24437421 9.12779121-21.50445729 19.57060322-15.78024923 32.02462345-39.99210223 32.02462345-67.14341343zM351.1033411 202.58334827c0-20.49885317 16.63114503-37.12999821 37.1299982-37.1299982s37.12999821 16.63114503 37.12999821 37.1299982-16.63114503 37.12999821-37.12999821 37.1299982-37.12999821-16.63114503-37.1299982-37.1299982z m74.25999641 618.83330346c0 20.49885317-16.63114503 37.12999821-37.12999821 37.1299982s-37.12999821-16.63114503-37.1299982-37.1299982 16.63114503-37.12999821 37.1299982-37.1299982 37.12999821 16.63114503 37.12999821 37.1299982z m247.53332139-457.93664456c-20.49885317 0-37.12999821-16.63114503-37.1299982-37.1299982s16.63114503-37.12999821 37.1299982-37.12999821 37.12999821 16.63114503 37.1299982 37.12999821-16.63114503 37.12999821-37.1299982 37.1299982z"/>
2767
+ </svg>
2768
+ </button>
2769
+ )}
2770
+ <div className={styles.navDivider} aria-hidden="true" />
2771
+ <Popover
2772
+ content={() => (
2773
+ // 上下各留 24px:顶部贴标题栏,底部留出工具栏 / chat input 视觉余量,
2774
+ // 避免窄屏 + 长内容时 popup 紧贴底部 footer 区域
2775
+ <div style={{ maxHeight: 'calc(100vh - 48px)', overflowY: 'auto', overflowX: 'hidden' }}>
2776
+ {this.props.getTokenStatsContent?.(this._closeTokenStatsPopover)}
2777
+ </div>
2778
+ )}
2779
+ trigger="hover"
2780
+ open={this.state.tokenStatsPopoverOpen}
2781
+ onOpenChange={(o) => this.setState({ tokenStatsPopoverOpen: o })}
2782
+ placement="right"
2783
+ arrow={{ pointAtCenter: true }}
2784
+ autoAdjustOverflow={false}
2785
+ align={{ overflow: { adjustX: true, shiftY: true } }}
2786
+ overlayInnerStyle={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-hover)', borderRadius: 8, padding: '8px 8px' }}
2787
+ >
2788
+ <button className={styles.navBtn} title={t('ui.tokenStats')}>
2789
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
2790
+ <path d="M12 14l4-4"/>
2791
+ <path d="M3.34 19a10 10 0 1 1 17.32 0"/>
2792
+ </svg>
2793
+ </button>
2794
+ </Popover>
2795
+ <TeamButton requests={this.props.requests} onOpenSession={(session) => this.setState({ teamModalSession: session })} navBtnClass={styles.navBtn} />
2796
+ <WorkflowButton requests={this.props.requests} onOpenRun={(run) => this.setState({ workflowModalRun: run })} navBtnClass={styles.navBtn} />
2797
+ <Popover
2798
+ content={this._buildUserPromptNav()}
2799
+ trigger="hover"
2800
+ placement="right"
2801
+ arrow={{ pointAtCenter: true }}
2802
+ autoAdjustOverflow={false}
2803
+ align={{ overflow: { adjustX: true, shiftY: true } }}
2804
+ overlayStyle={{ maxWidth: 400 }}
2805
+ overlayInnerStyle={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-light)', padding: 0 }}
2806
+ >
2807
+ <button className={styles.navBtn} title={t('ui.userPromptNav')}>
2808
+ <img
2809
+ src={this.props.userProfile?.avatar || defaultAvatarUrl}
2810
+ className={styles.navAvatarImg}
2811
+ alt="User"
2812
+ onError={(e) => { e.target.onerror = null; e.target.src = defaultAvatarUrl; }}
2813
+ />
2814
+ </button>
2815
+ </Popover>
2816
+ </div>
2817
+ );
2818
+ }
2819
+
2820
+ /**
2821
+ * 构建用户 Prompt 导航列表(侧边栏 hover popover 内容)
2822
+ * 基于 _currentVisible(当前可见 items),保证每一项都能精确定位和高亮
2823
+ */
2824
+ _buildUserPromptNav() {
2825
+ const visible = this._currentVisible;
2826
+ if (!visible || visible.length === 0) return null;
2827
+
2828
+ // 缓存:visible 引用未变化时复用上次结果
2829
+ if (this._navCacheVisible === visible && this._navCacheResult) return this._navCacheResult;
2830
+
2831
+ // 纯逻辑(会话边界标记 / 去重 / 图片清理 / 无 ts 容错)抽到 utils/promptNav,便于单测;此处只管缓存与渲染。
2832
+ const prompts = buildPromptNavItems(visible, this.props.mainAgentSessions);
2833
+ if (prompts.length === 0) { this._navCacheVisible = visible; this._navCacheResult = null; return null; }
2834
+
2835
+ const result = (
2836
+ <div className={styles.userPromptNavWrap}>
2837
+ <div className={styles.userPromptNavTitle}>{t('ui.userPromptNav')} ({prompts.length})</div>
2838
+ <div className={styles.userPromptNavList}>
2839
+ {prompts.map((p) => {
2840
+ const timeStr = formatPromptNavTime(p.timestamp);
2841
+ return (
2842
+ <React.Fragment key={p.visibleIdx}>
2843
+ {p.newSession && (
2844
+ <div className={styles.userPromptNavSessionSep}><span>{t('ui.session')}</span></div>
2845
+ )}
2846
+ <div className={styles.userPromptNavItem}
2847
+ onClick={() => this._scrollToUserPrompt(p.visibleIdx, p.timestamp)}>
2848
+ {timeStr && <span className={styles.userPromptNavTime}>{timeStr}</span>}
2849
+ <span className={styles.userPromptNavText}>{p.display}</span>
2850
+ </div>
2851
+ </React.Fragment>
2852
+ );
2853
+ })}
2854
+ </div>
2855
+ </div>
2856
+ );
2857
+ this._navCacheVisible = visible;
2858
+ this._navCacheResult = result;
2859
+ return result;
2860
+ }
2861
+
2862
+ /**
2863
+ * 滚动到指定用户消息,并触发蓝色虚线高亮动画。
2864
+ * @param {number} visibleIdx — visible 数组中的索引(与 containerRef.children 一一对应)
2865
+ * @param {string|null} timestamp — 消息时间戳(用于高亮,遗留消息可能为 null)
2866
+ */
2867
+ _scrollToUserPrompt(visibleIdx, timestamp) {
2868
+ if (visibleIdx == null || visibleIdx < 0) return;
2869
+ // 触发高亮(有 timestamp 时显示蓝色虚线动画)
2870
+ if (timestamp) {
2871
+ this.setState({ highlightTs: timestamp, highlightFading: false, highlightVisibleIdx: visibleIdx }, () => {
2872
+ this._doScrollToVisibleIdx(visibleIdx);
2873
+ this._scrollHighlight.bind();
2874
+ });
2875
+ } else {
2876
+ // 无 timestamp 的遗留消息:仅滚动,不触发高亮
2877
+ this._doScrollToVisibleIdx(visibleIdx);
2878
+ }
2879
+ }
2880
+
2881
+ _doScrollToVisibleIdx(idx) {
2882
+ if (useVirtuoso && this.virtuosoRef.current) {
2883
+ this.virtuosoRef.current.scrollToIndex({ index: idx, align: 'center', behavior: 'smooth' });
2884
+ } else {
2885
+ const el = this.containerRef.current;
2886
+ if (el && el.children[idx]) {
2887
+ el.children[idx].scrollIntoView({ behavior: 'smooth', block: 'center' });
2888
+ }
2889
+ }
2890
+ }
2891
+
2892
+ render() {
2893
+ const { mainAgentSessions, cliMode, terminalVisible, onToggleTerminal } = this.props;
2894
+ const { allItems, visibleCount, loading, terminalWidth, sidebarWidth } = this.state;
2895
+ const streamSpinnerUrl = this._streamSpinnerUrl || shimmerUrl;
2896
+
2897
+ // 计算 SnapLineOverlay 的 currentLeft(侧栏拖拽时用侧栏宽度,终端拖拽时用终端位置)
2898
+ let snapCurrentLeft = 0;
2899
+ if (this.state.isDragging) {
2900
+ if (this._splitDrag.dragTarget() === 'sidebar') {
2901
+ snapCurrentLeft = sidebarWidth;
2902
+ } else if (this._splitDrag.dragTarget() === 'terminal') {
2903
+ const c = this.innerSplitRef.current;
2904
+ if (c) snapCurrentLeft = c.getBoundingClientRect().width - terminalWidth - RESIZER_WIDTH_PX;
2905
+ }
2906
+ }
2907
+
2908
+ const noMainAgent = !mainAgentSessions || mainAgentSessions.length === 0;
2909
+ // 仅展示当前会话开启、且当前 session 暂无可渲染项(如 /clear 后新 session 首条消息尚未到达)时,
2910
+ // 复用既有空/加载占位,避免空白面板(下方 fileLoading 守卫已在初始加载期抑制 Empty→内容 闪烁)。
2911
+ const noData = (!allItems || allItems.length === 0) && (noMainAgent || this.props.onlyCurrentSession);
2912
+
2913
+ if (noData && !cliMode) {
2914
+ // 初始 SSE 加载期间不显示"暂无对话",避免 Empty→内容 的两阶段闪烁
2915
+ if (this.props.fileLoading) {
2916
+ return null;
2917
+ }
2918
+ return (
2919
+ <div className={styles.centerEmpty}>
2920
+ <Empty description={t('ui.noChat')} />
2921
+ </div>
2922
+ );
2923
+ }
2924
+
2925
+ if (loading && !cliMode) {
2926
+ return (
2927
+ <div className={styles.centerEmpty}>
2928
+ <Spin size="large" />
2929
+ </div>
2930
+ );
2931
+ }
2932
+
2933
+ // --- 角色收集 + 筛选 ---
2934
+ const collectedRolesMap = new Map();
2935
+ const userProfile = this.props.userProfile;
2936
+ const modelInfo = this._reqScanCache ? getModelInfo(this._reqScanCache.modelName) : null;
2937
+ for (const item of allItems) {
2938
+ if (!item || !item.props) continue;
2939
+ const role = item.props.role;
2940
+ if (role === 'user' || role === 'plan-prompt') {
2941
+ if (!collectedRolesMap.has('user')) {
2942
+ collectedRolesMap.set('user', { key: 'user', name: userProfile?.name || 'User', avatarType: 'user', color: 'rgba(255,255,255,0.1)', avatarImg: userProfile?.avatar || null });
2943
+ }
2944
+ } else if (role === 'assistant') {
2945
+ if (!collectedRolesMap.has('assistant')) {
2946
+ collectedRolesMap.set('assistant', { key: 'assistant', name: modelInfo?.short || modelInfo?.name || 'Codex', avatarType: 'agent', color: modelInfo?.color || 'rgba(255,255,255,0.1)', avatarSvg: modelInfo?.svg || null });
2947
+ }
2948
+ } else if (role === 'sub-agent-chat') {
2949
+ const label = item.props.label || 'SubAgent';
2950
+ const key = `sub:${label}`;
2951
+ if (!collectedRolesMap.has(key)) {
2952
+ const isTeammate = item.props.isTeammate;
2953
+ let avatarType = 'sub';
2954
+ if (isTeammate) {
2955
+ avatarType = 'teammate';
2956
+ } else {
2957
+ const match = label.match(/SubAgent:\s*(\w+)/i);
2958
+ const st = match ? match[1].toLowerCase() : '';
2959
+ if (st === 'explore' || st === 'search') avatarType = 'sub-search';
2960
+ else if (st === 'plan') avatarType = 'sub-plan';
2961
+ }
2962
+ const tmA = isTeammate ? getTeammateAvatar(label, { animated: false }) : null;
2963
+ collectedRolesMap.set(key, { key, name: label.length > 12 ? label.slice(0, 12) + '…' : label, avatarType, avatarSvg: tmA ? tmA.svg : undefined, color: tmA ? tmA.color : 'rgba(255,255,255,0.1)' });
2964
+ }
2965
+ }
2966
+ }
2967
+ const collectedRoles = Array.from(collectedRolesMap.values());
2968
+
2969
+ let filteredItems = allItems;
2970
+ const _selSize = this.state.roleFilterSelected.size;
2971
+ if (_selSize > 0 && _selSize < collectedRoles.length) {
2972
+ filteredItems = allItems.filter(item => {
2973
+ if (!item || !item.props) return true;
2974
+ const role = item.props.role;
2975
+ if (role === 'user' || role === 'plan-prompt') return this.state.roleFilterSelected.has('user');
2976
+ if (role === 'assistant') return this.state.roleFilterSelected.has('assistant');
2977
+ if (role === 'sub-agent-chat') {
2978
+ const key = `sub:${item.props.label || 'SubAgent'}`;
2979
+ return this.state.roleFilterSelected.has(key);
2980
+ }
2981
+ return false;
2982
+ });
2983
+ }
2984
+
2985
+ const _isFiltering = _selSize > 0 && _selSize < collectedRoles.length;
2986
+
2987
+ // Live streaming overlay: 实时打字机效果,独立于 mainAgentSessions / dedup 路径
2988
+ // 仅显示 text + thinking blocks;工具交互仍由正常消息气泡在会话落地后渲染。
2989
+ // roleFilter 反选 assistant 时跳过 overlay 以遵从过滤语义(否则一边过滤一边仍实时输出自相矛盾)。
2990
+ let streamingLiveItem = null;
2991
+ const _assistantFilteredOut = _isFiltering && !this.state.roleFilterSelected.has('assistant');
2992
+ // 乐观停止:点「停止」后即时停掉实时打字机浮层,避免按钮/页内指示器已切非运行态、
2993
+ // 而中断落地前的残余 SSE chunk 仍在 overlay 里继续吐字(自相矛盾)。
2994
+ // pin 锁在更早会话时(sessionUpperBoundTs != null),streaming 来自未展示的更新会话,
2995
+ // 不应把它的实时浮层叠在 pin 会话底部(会显示用户根本没看的会话内容)。
2996
+ if (this.props.streamingLatest && !_assistantFilteredOut && !this.state.stopOptimistic && this.props.sessionUpperBoundTs == null) {
2997
+ const sl = this.props.streamingLatest;
2998
+ const liveBlocks = (sl.content || []).filter(b =>
2999
+ b.type === 'text' || b.type === 'thinking'
3000
+ );
3001
+ const hasVisibleContent = liveBlocks.some(b => {
3002
+ if (b.type === 'text') return typeof b.text === 'string' && b.text.trim().length > 0;
3003
+ if (b.type === 'thinking') return typeof b.thinking === 'string' && b.thinking.trim().length > 0;
3004
+ return false;
3005
+ });
3006
+ if (hasVisibleContent) {
3007
+ // SSE streaming 消息的 modelInfo:继承"上一次已完成的 MainAgent model",
3008
+ // 而非当前正在进行中的 request.body.model(后者可能还没被扫到或不稳定)。
3009
+ // completedModelName 仅在 req.response 存在时更新,流式期间永远指向上一次已完成模型。
3010
+ // fallback 到 modelName 只是为了冷启动(从未有已完成请求时的边界情况)。
3011
+ const cache = this._reqScanCache;
3012
+ const streamingModelInfo = cache ? getModelInfo(cache.completedModelName || cache.modelName) : null;
3013
+ streamingLiveItem = (
3014
+ <ChatMessage
3015
+ key="streaming-live-msg"
3016
+ role="assistant"
3017
+ content={liveBlocks}
3018
+ timestamp={sl.timestamp}
3019
+ modelInfo={streamingModelInfo}
3020
+ collapseToolResults={this.props.collapseToolResults}
3021
+ expandThinking={this.props.expandThinking}
3022
+ showFullToolContent={this.props.showFullToolContent}
3023
+ showTrailingCursor={true}
3024
+ toolResultMap={EMPTY_MAP}
3025
+ isHistoryLog={this._getIsHistoryLog()}
3026
+ />
3027
+ );
3028
+ }
3029
+ }
3030
+
3031
+ // 仅在 streaming 或淡出期间挂 <img>,避免 ChatView 冷加载就 fetch 76KB 的 shimmer + orbiting。
3032
+ // streamingFading 由 isStreaming true→false 时拉起 500ms(line ~551),覆盖淡出动画窗口。
3033
+ // 乐观停止:点「停止」后立即按非运行态渲染(按钮 + 页内指示器),无需等 AppBase 的 SSE+2s。
3034
+ const uiStreaming = this.props.isStreaming && !this.state.stopOptimistic;
3035
+ const showSpinner = uiStreaming || this.state.streamingFading;
3036
+ const spinnerNode = showSpinner ? (
3037
+ <div className={`${styles.streamingSpinnerWrap}${(!uiStreaming || streamingLiveItem) ? ' ' + styles.streamingSpinnerHidden : ''}`}>
3038
+ <object type="image/svg+xml" data={streamSpinnerUrl} width="20" height="20" aria-hidden="true" tabIndex={-1} />
3039
+ </div>
3040
+ ) : null;
3041
+
3042
+ const targetIdx = this._scrollTargetIdx;
3043
+ const { highlightTs, highlightFading, highlightVisibleIdx } = this.state;
3044
+ const visible = filteredItems.slice(0, _isFiltering ? filteredItems.length : visibleCount);
3045
+ // 缓存 visible,供 _buildUserPromptNav / _scrollToUserPrompt 使用
3046
+ this._currentVisible = visible;
3047
+ // 优先使用精确的 visibleIdx(同一请求的多条消息共享 timestamp,findIndex 会匹配到第一条)
3048
+ // findIndex 用 displayTs ?? timestamp 作 key —— assistant bubble 的 props.timestamp 是 carrier
3049
+ // (= 下一次 entry 的 ts),而 highlightTs 来自 scrollToTimestamp (= request 自身 ts);只看 timestamp
3050
+ // 会让 assistant bubble 永远匹配不上,蓝色虚线选框落到错位的 bubble。displayTs 恰好就是 _generatedTs
3051
+ // (producer request 的 ts),跟 highlightTs 同源。其他 role 没 displayTs 自动 fallback 到 timestamp。
3052
+ const highlightIdx = highlightVisibleIdx >= 0 && highlightVisibleIdx < visible.length
3053
+ ? highlightVisibleIdx
3054
+ : (highlightTs != null ? visible.findIndex(item => (item.props?.displayTs || item.props?.timestamp) === highlightTs) : -1);
3055
+
3056
+ const { pendingInput, stickyBottom, ptyPromptHistory } = this.state;
3057
+
3058
+ const pendingBubble = cliMode && pendingInput ? (
3059
+ <ChatMessage key="pending-input" role="user" text={pendingInput} lang={this.props.lang} timestamp={new Date().toISOString()} userProfile={this.props.userProfile} isHistoryLog={false} />
3060
+ ) : null;
3061
+
3062
+ const stickyBtn = !stickyBottom ? (
3063
+ <button className={styles.stickyBottomBtn} onClick={this.handleStickToBottom}>
3064
+ {uiStreaming && <img src={loadingPetUrl} className={styles.loadingPet} alt="" />}
3065
+ <span>{t('ui.stickyBottom')}</span>
3066
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
3067
+ <polyline points="6 9 12 15 18 9" />
3068
+ </svg>
3069
+ </button>
3070
+ ) : null;
3071
+
3072
+ const loadMoreBtn = this._mobileSliceOffset > 0 ? (
3073
+ <div className={styles.loadMoreWrap}>
3074
+ {this.props.isLocalLog ? (
3075
+ // logfile 只读模式自动渐进扩窗中:展示加载态而非可点按钮(_maybeScheduleLocalLogAutoFill 驱动)
3076
+ <div className={`${styles.loadMoreBtn} ${styles.loadMoreBtnLoading}`}>
3077
+ <span className={styles.loadMoreSpinner} />
3078
+ {t('ui.loadingMoreHistory')}
3079
+ </div>
3080
+ ) : (
3081
+ <button className={styles.loadMoreBtn} onClick={this.handleLoadMore}>
3082
+ {t('ui.loadMoreHistory', { count: this._mobileSliceOffset })}
3083
+ </button>
3084
+ )}
3085
+ </div>
3086
+ ) : null;
3087
+
3088
+ const roleFilterBar = this.state.roleFilterOpen && collectedRoles.length > 0 ? (
3089
+ <RoleFilterBar roles={collectedRoles} selectedRoles={this.state.roleFilterSelected} onToggle={(key) => this.setState(prev => {
3090
+ const next = new Set(prev.roleFilterSelected);
3091
+ next.has(key) ? next.delete(key) : next.add(key);
3092
+ return { roleFilterSelected: next };
3093
+ })} />
3094
+ ) : null;
3095
+
3096
+ const messageList = (noData || loading) ? (
3097
+ <div className={styles.messageListWrap}>
3098
+ <div ref={this.containerRef} className={styles.container}>
3099
+ {(!cliMode || loading) ? (
3100
+ <div className={styles.centerEmpty}>
3101
+ {loading ? <Spin size="large" /> : <Empty description={t('ui.noChat')} />}
3102
+ </div>
3103
+ ) : null}
3104
+ {pendingBubble}
3105
+ </div>
3106
+ {stickyBtn}
3107
+ </div>
3108
+ ) : (
3109
+ <div className={styles.messageListWrap} onClick={this.handleMdImageClick}>
3110
+ {roleFilterBar}
3111
+ {this.state.mdLightboxSrc && (
3112
+ <ImageLightbox src={this.state.mdLightboxSrc} alt="" onClose={() => this.setState({ mdLightboxSrc: null })} />
3113
+ )}
3114
+ {useVirtuoso ? (
3115
+ this._virtuosoHeader = loadMoreBtn,
3116
+ this._virtuosoFooter = <>
3117
+ {spinnerNode}
3118
+ {pendingBubble}
3119
+ {streamingLiveItem && (
3120
+ targetIdx != null && targetIdx >= visible.length
3121
+ ? <div key="stream-resp-anchor" ref={this._scrollTargetRef}>{streamingLiveItem}</div>
3122
+ : streamingLiveItem
3123
+ )}
3124
+ </>,
3125
+ <Virtuoso
3126
+ ref={this.virtuosoRef}
3127
+ className={styles.mobileVirtuoso}
3128
+ data={visible}
3129
+ initialTopMostItemIndex={Math.max(0, visible.length - 1)}
3130
+ followOutput={this.state.stickyBottom && !this.state.userScrolling ? 'auto' : false}
3131
+ atBottomStateChange={(atBottom) => this._stickyController.notifyAtBottom(atBottom)}
3132
+ atBottomThreshold={60}
3133
+ increaseViewportBy={{ top: 200, bottom: 200 }}
3134
+ computeItemKey={(index) => visible[index]?.key || `v-${index}`}
3135
+ itemContent={(index) => {
3136
+ const item = visible[index];
3137
+ const isScrollTarget = index === targetIdx;
3138
+ const needsHighlight = index === highlightIdx;
3139
+ let el = item;
3140
+ if (needsHighlight) el = React.cloneElement(el, { highlight: highlightFading ? 'fading' : 'active' });
3141
+ return isScrollTarget ? <div ref={this._scrollTargetRef}>{el}</div> : el;
3142
+ }}
3143
+ scrollerRef={(ref) => { this._virtuosoScrollerEl = ref; this._stickyController.bind(ref); }}
3144
+ context={{ header: this._virtuosoHeader, footer: this._virtuosoFooter }}
3145
+ components={this._virtuosoComponents || (this._virtuosoComponents = {
3146
+ Scroller: VirtuosoScroller,
3147
+ Header: ({ context }) => context.header,
3148
+ Footer: ({ context }) => context.footer,
3149
+ })}
3150
+ />
3151
+ ) : (
3152
+ <div
3153
+ ref={this.containerRef}
3154
+ className={styles.container}
3155
+ >
3156
+ {loadMoreBtn}
3157
+ {visible.map((item, i) => {
3158
+ const isScrollTarget = i === targetIdx;
3159
+ const needsHighlight = i === highlightIdx;
3160
+ let el = item;
3161
+ if (needsHighlight) {
3162
+ el = React.cloneElement(el, { highlight: highlightFading ? 'fading' : 'active' });
3163
+ }
3164
+ return isScrollTarget
3165
+ ? <div key={item.key + '-anchor'} ref={this._scrollTargetRef}>{el}</div>
3166
+ : el;
3167
+ })}
3168
+ {spinnerNode}
3169
+ {pendingBubble}
3170
+ {streamingLiveItem && (
3171
+ targetIdx != null && targetIdx >= visible.length
3172
+ ? <div key="stream-resp-anchor" ref={this._scrollTargetRef}>{streamingLiveItem}</div>
3173
+ : streamingLiveItem
3174
+ )}
3175
+ </div>
3176
+ )}
3177
+ {stickyBtn}
3178
+ </div>
3179
+ );
3180
+
3181
+ if (!cliMode) {
3182
+ return (<>
3183
+ <div className={styles.splitContainer}>
3184
+ {this._renderNavSidebar(false)}
3185
+ <div className={styles.navSidebarContent}>
3186
+ {messageList}
3187
+ </div>
3188
+ </div>
3189
+ <TeamModal session={this.state.teamModalSession} requests={this.props.requests} mainAgentSessions={this.props.mainAgentSessions} collapseToolResults={this.props.collapseToolResults} expandThinking={this.props.expandThinking} showFullToolContent={this.props.showFullToolContent} userProfile={this.props.userProfile} onViewRequest={this.props.onViewRequest} isHistoryLog={this._getIsHistoryLog()} lang={this.props.lang} onClose={() => this.setState({ teamModalSession: null })} />
3190
+ <WorkflowRunsModal run={this.state.workflowModalRun} onClose={() => this.setState({ workflowModalRun: null })} />
3191
+ </>);
3192
+ }
3193
+
3194
+ return (<>
3195
+ <div ref={this.splitContainerRef} className={styles.splitContainer}>
3196
+ {this._renderNavSidebar(true)}
3197
+ <div className={styles.innerSplitArea} ref={this.innerSplitRef}>
3198
+ <SnapLineOverlay isDragging={this.state.isDragging} activeSnapLine={this.state.activeSnapLine} snapLines={this.state.snapLines} currentLeft={snapCurrentLeft} />
3199
+ {this.state.fileExplorerOpen && (
3200
+ <FileExplorer
3201
+ style={{ width: this.state.sidebarWidth }}
3202
+ refreshTrigger={this.state.fileExplorerRefresh}
3203
+ onManualRefresh={() => this.setState(prev => ({ fileExplorerRefresh: prev.fileExplorerRefresh + 1 }))}
3204
+ onClose={() => this._setFileExplorerOpen(false)}
3205
+ onFileClick={(path) => {
3206
+ if (tryOpenWithSystem(path, 'file-explorer')) return;
3207
+ this.setState({ currentFile: path, currentGitDiff: null, scrollToLine: null });
3208
+ }}
3209
+ expandedPaths={this.state.fileExplorerExpandedPaths}
3210
+ onToggleExpand={this.handleToggleExpandPath}
3211
+ currentFile={this.state.currentFile}
3212
+ onAttachToChat={(filePath) => this._addPendingImage(filePath, 'explorer')}
3213
+ onInsertPathToChat={this._handleInsertPathToChat}
3214
+ onFileRenamed={(oldPath, newPath) => {
3215
+ this.setState(prev => ({
3216
+ currentFile: prev.currentFile === oldPath ? newPath : prev.currentFile,
3217
+ fileExplorerRefresh: prev.fileExplorerRefresh + 1,
3218
+ }));
3219
+ }}
3220
+ />
3221
+ )}
3222
+ {this.state.gitChangesOpen && (
3223
+ <GitChanges
3224
+ style={{ width: this.state.sidebarWidth }}
3225
+ refreshTrigger={this.state.gitChangesRefresh}
3226
+ onManualRefresh={() => this.setState(prev => ({ gitChangesRefresh: prev.gitChangesRefresh + 1 }))}
3227
+ projectName={this.props.projectName}
3228
+ onClose={() => this.setState({ gitChangesOpen: false })}
3229
+ onFileClick={(repoPath, filePath, commitHash) => {
3230
+ const resolvedPath = repoPath && repoPath !== '.' ? `${repoPath}/${filePath}` : filePath;
3231
+ if (tryOpenWithSystem(resolvedPath, 'git-changes')) return;
3232
+ this.setState({ currentGitDiff: { repo: repoPath, file: filePath, commit: commitHash || null }, currentFile: null });
3233
+ }}
3234
+ onOpenFile={(repoPath, filePath) => {
3235
+ const resolvedPath = repoPath && repoPath !== '.' ? `${repoPath}/${filePath}` : filePath;
3236
+ if (tryOpenWithSystem(resolvedPath, 'git-changes')) return;
3237
+ const parts = resolvedPath.split('/');
3238
+ const ancestors = [];
3239
+ for (let i = 1; i < parts.length; i++) ancestors.push(parts.slice(0, i).join('/'));
3240
+ this._setFileExplorerOpen(true);
3241
+ const projectName = this.props.projectName;
3242
+ this.setState(prev => {
3243
+ const newSet = new Set(prev.fileExplorerExpandedPaths);
3244
+ ancestors.forEach(p => newSet.add(p));
3245
+ return { currentGitDiff: null, currentFile: resolvedPath, scrollToLine: null, gitChangesOpen: false, fileExplorerExpandedPaths: newSet };
3246
+ }, () => {
3247
+ saveExpandedPaths(projectName, this.state.fileExplorerExpandedPaths);
3248
+ });
3249
+ }}
3250
+ />
3251
+ )}
3252
+ {(this.state.fileExplorerOpen || this.state.gitChangesOpen) && (
3253
+ <div className={styles.vResizer} onMouseDown={this.handleSidebarMouseDown} />
3254
+ )}
3255
+ <div className={styles.chatSection}>
3256
+ <div className={styles.chatSectionFlex}>
3257
+ {this.state.currentGitDiff && (
3258
+ <div className={styles.overlayPanel}>
3259
+ <GitDiffView
3260
+ filePath={this.state.currentGitDiff.file}
3261
+ repoPath={this.state.currentGitDiff.repo}
3262
+ commitHash={this.state.currentGitDiff.commit || null}
3263
+ onClose={() => this.setState({ currentGitDiff: null })}
3264
+ onOpenFile={(path, line) => {
3265
+ const repo = this.state.currentGitDiff?.repo;
3266
+ const resolvedPath = repo && repo !== '.' ? `${repo}/${path}` : path;
3267
+ if (tryOpenWithSystem(resolvedPath, 'git-diff')) return;
3268
+ const parts = resolvedPath.split('/');
3269
+ const ancestors = [];
3270
+ for (let i = 1; i < parts.length; i++) {
3271
+ ancestors.push(parts.slice(0, i).join('/'));
3272
+ }
3273
+ this._setFileExplorerOpen(true);
3274
+ const projectName = this.props.projectName;
3275
+ this.setState(prev => {
3276
+ const newSet = new Set(prev.fileExplorerExpandedPaths);
3277
+ ancestors.forEach(p => newSet.add(p));
3278
+ return {
3279
+ currentGitDiff: null,
3280
+ currentFile: resolvedPath,
3281
+ scrollToLine: line || 1,
3282
+ gitChangesOpen: false,
3283
+ fileExplorerExpandedPaths: newSet,
3284
+ };
3285
+ }, () => {
3286
+ saveExpandedPaths(projectName, this.state.fileExplorerExpandedPaths);
3287
+ });
3288
+ }}
3289
+ />
3290
+ </div>
3291
+ )}
3292
+ {this.state.currentFile && (
3293
+ <div className={styles.overlayPanel}>
3294
+ {isImageFile(this.state.currentFile) ? (
3295
+ <ImageViewer
3296
+ key={this.state.fileVersion}
3297
+ filePath={this.state.currentFile}
3298
+ editorSession={!!this.state.editorSessionId}
3299
+ onClose={() => {
3300
+ if (this.state.editorSessionId) {
3301
+ fetch(apiUrl('/api/editor-done'), {
3302
+ method: 'POST',
3303
+ headers: { 'Content-Type': 'application/json' },
3304
+ body: JSON.stringify({ sessionId: this.state.editorSessionId }),
3305
+ }).catch(() => {});
3306
+ }
3307
+ this.setState({ currentFile: null, fileVersion: 0, editorSessionId: null, editorFilePath: null });
3308
+ }}
3309
+ />
3310
+ ) : (
3311
+ <FileContentView
3312
+ key={this.state.fileVersion}
3313
+ filePath={this.state.currentFile}
3314
+ scrollToLine={this.state.scrollToLine}
3315
+ editorSession={!!this.state.editorSessionId}
3316
+ onUpdateScroll={this.handleUpdateFileScroll}
3317
+ getRestoreScrollSnapshot={this.getFileScrollSnapshot}
3318
+ onConsumeScrollSnapshot={this.handleConsumeFileScroll}
3319
+ onClose={() => {
3320
+ if (this.state.editorSessionId) {
3321
+ fetch(apiUrl('/api/editor-done'), {
3322
+ method: 'POST',
3323
+ headers: { 'Content-Type': 'application/json' },
3324
+ body: JSON.stringify({ sessionId: this.state.editorSessionId }),
3325
+ }).catch(() => {});
3326
+ }
3327
+ this.setState({ currentFile: null, fileVersion: 0, editorSessionId: null, editorFilePath: null });
3328
+ }}
3329
+ />
3330
+ )}
3331
+ </div>
3332
+ )}
3333
+ {messageList}
3334
+ {/* inputStack 把审批面板 + 输入栏包成同一个定位容器,
3335
+ 面板用 position:absolute; bottom:100% 自动贴在输入栏顶部之上,不遮挡 */}
3336
+ <div className={styles.inputStack}>
3337
+ {/* 如果父组件处理全局渲染(移动端通过 suppressInlineApprovalPanels),跳过本地渲染 */}
3338
+ {!this.props.suppressInlineApprovalPanels && (
3339
+ <ToolApprovalPanel
3340
+ toolName={this.state.pendingPermission?.toolName}
3341
+ toolInput={this.state.pendingPermission?.input}
3342
+ requestId={this.state.pendingPermission?.id}
3343
+ onAllow={this.handlePermissionAllow}
3344
+ onAllowSession={
3345
+ (this.props.sdkMode || (this.state.pendingPermission?.source === 'pty' && this.state.pendingPermission?.ptyPrompt?.options?.length >= 3))
3346
+ ? this.handlePermissionAllowSession
3347
+ : null
3348
+ }
3349
+ onDeny={this.handlePermissionDeny}
3350
+ visible={!!this.state.pendingPermission}
3351
+ source={this.state.pendingPermission?.source}
3352
+ queueDepth={this.state.permissionQueue.length}
3353
+ />
3354
+ )}
3355
+ {!this.props.suppressInlineApprovalPanels && this.state.pendingPlanApproval && (
3356
+ <ToolApprovalPanel
3357
+ toolName={CODEX_PLAN_TOOL_NAME}
3358
+ toolInput={this.state.pendingPlanApproval.input}
3359
+ requestId={this.state.pendingPlanApproval.id}
3360
+ onAllow={this.handlePlanApprove}
3361
+ onDeny={(id) => this.handlePlanReject(id, '')}
3362
+ visible={true}
3363
+ />
3364
+ )}
3365
+ <WorkflowLiveHud />
3366
+ <ChatInputBar
3367
+ inputRef={this._inputRef}
3368
+ inputEmpty={this.state.inputEmpty}
3369
+ inputSuggestion={this.state.inputSuggestion}
3370
+ terminalVisible={terminalVisible}
3371
+ onKeyDown={this.handleInputKeyDown}
3372
+ onChange={this.handleInputChange}
3373
+ onSend={this.handleInputSend}
3374
+ onStop={this.handleInputStop}
3375
+ onSuggestionClick={this.handleSuggestionToTerminal}
3376
+ onUploadPath={this.handleUploadPath}
3377
+ presetItems={this.state.presetItems}
3378
+ onPresetSend={this.handlePresetSend}
3379
+ onOpenPresetModal={() => this.setState({ mobilePresetModalVisible: true })}
3380
+ onOpenUltraPlan={this.props.cliMode
3381
+ ? (isMobile ? () => this.setState({ ultraplanModalOpen: true }) : () => this.setState({ ultraplanPopoverOpen: true }))
3382
+ : null}
3383
+ ultraplanPopover={(!isMobile && this.props.cliMode) ? {
3384
+ open: this.state.ultraplanPopoverOpen,
3385
+ onOpenChange: this._ultraplanPopoverOnOpenChange,
3386
+ overlayInnerStyle: ultraplanOverlayInnerStyle(this.state.ultraplanPopoverSize),
3387
+ content: (
3388
+ <UltraplanPanel
3389
+ variant={this.state.ultraplanVariant}
3390
+ prompt={this.state.ultraplanPrompt}
3391
+ files={this.state.ultraplanFiles}
3392
+ customExperts={this.state.customUltraplanExperts}
3393
+ expertOrder={this.state.ultraplanExpertOrder}
3394
+ expertHidden={this.state.ultraplanExpertHidden}
3395
+ onVariantChange={(v) => this.setState({ ultraplanVariant: v })}
3396
+ onPromptChange={(p) => this.setState({ ultraplanPrompt: p })}
3397
+ onSend={this._handleUltraplanSend}
3398
+ onUpload={this._handleUltraplanUpload}
3399
+ onPaste={this._handleUltraplanPaste}
3400
+ onRemoveFile={this._handleUltraplanRemoveFile}
3401
+ onClose={() => this.setState({ ultraplanPopoverOpen: false })}
3402
+ onOpenManager={() => this.setState({ ultraplanManagerOpen: true })}
3403
+ onOpenCustomEditor={this._openCustomUltraplanEditor}
3404
+ onPreviewImage={(lb) => this.setState({ ultraplanLightbox: lb })}
3405
+ onConfirmingChange={(open) => this.setState({ ultraplanConfirming: open })}
3406
+ onSizeChange={(size) => this.setState({ ultraplanPopoverSize: size })}
3407
+ />
3408
+ ),
3409
+ } : null}
3410
+ onClearContext={this.props.cliMode ? () => {
3411
+ Modal.confirm({
3412
+ title: t('ui.chatInput.clearContextConfirm'),
3413
+ okType: 'danger',
3414
+ okText: t('ui.chatInput.clearContext'),
3415
+ onOk: this._doClearContext,
3416
+ });
3417
+ } : null}
3418
+ onClearContextNow={this.props.cliMode ? this._doClearContext : null}
3419
+ isStreaming={uiStreaming}
3420
+ pendingImages={this.state.pendingImages}
3421
+ onRemovePendingImage={this._removePendingImage}
3422
+ uploadingItems={this.state.uploadingItems}
3423
+ sendDeferred={this.state.sendDeferred}
3424
+ onUploadStart={this.handleUploadStart}
3425
+ onUploadEnd={this.handleUploadEnd}
3426
+ setContextBarSlot={this.props.setContextBarSlot}
3427
+ approvalsReviewer={this.props.approvalsReviewer}
3428
+ onApprovalsReviewerChange={this.props.onApprovalsReviewerChange}
3429
+ planAutoApproveSeconds={this.props.planAutoApproveSeconds}
3430
+ onPlanAutoApproveChange={this.props.onPlanAutoApproveChange}
3431
+ />
3432
+ </div>
3433
+ <UltraPlanModal
3434
+ open={this.state.ultraplanModalOpen}
3435
+ variant={this.state.ultraplanVariant}
3436
+ prompt={this.state.ultraplanPrompt}
3437
+ files={this.state.ultraplanFiles}
3438
+ customExperts={this.state.customUltraplanExperts}
3439
+ expertOrder={this.state.ultraplanExpertOrder}
3440
+ expertHidden={this.state.ultraplanExpertHidden}
3441
+ onClose={() => this.setState({ ultraplanModalOpen: false })}
3442
+ onVariantChange={(v) => this.setState({ ultraplanVariant: v })}
3443
+ onPromptChange={(t) => this.setState({ ultraplanPrompt: t })}
3444
+ onSend={this._handleUltraplanSend}
3445
+ onUpload={this._handleUltraplanUpload}
3446
+ onPaste={this._handleUltraplanPaste}
3447
+ onRemoveFile={this._handleUltraplanRemoveFile}
3448
+ onOpenCustomEditor={this._openCustomUltraplanEditor}
3449
+ onOpenManager={() => this.setState({ ultraplanManagerOpen: true })}
3450
+ modalSize={this.state.ultraplanModalSize}
3451
+ onModalSizeChange={this._handleUltraplanModalSizeChange}
3452
+ />
3453
+ <CustomUltraplanEditModal
3454
+ open={this.state.customUltraplanEditOpen}
3455
+ initial={this.state.customUltraplanEditing}
3456
+ onSave={this._saveCustomUltraplanExpert}
3457
+ onDelete={this._deleteCustomUltraplanExpert}
3458
+ onClose={this._closeCustomUltraplanEditor}
3459
+ />
3460
+ <UltraplanExpertManagerModal
3461
+ open={this.state.ultraplanManagerOpen}
3462
+ customExperts={this.state.customUltraplanExperts}
3463
+ order={this.state.ultraplanExpertOrder}
3464
+ hidden={this.state.ultraplanExpertHidden}
3465
+ onPersist={({ order, hidden }) => this._ultraplan.persistExpertLayout({ order, hidden })}
3466
+ onClose={() => this.setState({ ultraplanManagerOpen: false })}
3467
+ />
3468
+ {this.state.ultraplanLightbox && (
3469
+ <ImageLightbox
3470
+ src={this.state.ultraplanLightbox.src}
3471
+ alt={this.state.ultraplanLightbox.alt}
3472
+ zIndex={1200}
3473
+ onClose={() => this.setState({ ultraplanLightbox: null })}
3474
+ />
3475
+ )}
3476
+ </div>
3477
+ </div>
3478
+ {cliMode && !this.props.sdkMode && onToggleTerminal && (
3479
+ <div
3480
+ className={styles.terminalToggle}
3481
+ onClick={onToggleTerminal}
3482
+ title={terminalVisible ? t('ui.collapseTerminal') : t('ui.expandTerminal')}
3483
+ >
3484
+ <svg viewBox="0 0 8 24" width="8" height="24">
3485
+ {terminalVisible
3486
+ ? <path d="M4 8 L7 12 L4 16" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
3487
+ : <path d="M4 8 L1 12 L4 16" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
3488
+ }
3489
+ </svg>
3490
+ </div>
3491
+ )}
3492
+ {terminalVisible && (
3493
+ <>
3494
+ <div className={styles.vResizer} onMouseDown={this.handleSplitMouseDown} />
3495
+ <div className={styles.terminalPanelWrap} style={{ width: terminalWidth }}>
3496
+ <TerminalPanel preferences={this.props.preferences} onUpdatePreferences={this.props.onUpdatePreferences} onEditorOpen={(sessionId, filePath) => {
3497
+ this.setState({
3498
+ editorSessionId: sessionId,
3499
+ editorFilePath: filePath,
3500
+ currentFile: filePath,
3501
+ currentGitDiff: null,
3502
+ scrollToLine: null,
3503
+ fileVersion: (this.state.fileVersion || 0) + 1,
3504
+ });
3505
+ }} onFilePath={(path) => {
3506
+ // 加入 pendingImages,在终端 Enter 时统一注入 PTY
3507
+ this._addPendingImage(path, 'terminal');
3508
+ }}
3509
+ pendingImages={this.state.pendingImages}
3510
+ onRemovePendingImage={this._removePendingImage}
3511
+ onClearPendingImages={this._clearPendingImages}
3512
+ modelName={this._reqScanCache?.modelName}
3513
+ getChatScroller={() => this._getScrollContainer()}
3514
+ onClearContextOptimistic={this.props.onClearContextOptimistic}
3515
+ setContextBarSlot={this.props.setContextBarSlot}
3516
+ approvalsReviewer={this.props.approvalsReviewer}
3517
+ onApprovalsReviewerChange={this.props.onApprovalsReviewerChange}
3518
+ planAutoApproveSeconds={this.props.planAutoApproveSeconds}
3519
+ onPlanAutoApproveChange={this.props.onPlanAutoApproveChange}
3520
+ />
3521
+ </div>
3522
+ </>
3523
+ )}
3524
+ </div>
3525
+ </div>
3526
+ <TeamModal session={this.state.teamModalSession} requests={this.props.requests} mainAgentSessions={this.props.mainAgentSessions} collapseToolResults={this.props.collapseToolResults} expandThinking={this.props.expandThinking} showFullToolContent={this.props.showFullToolContent} userProfile={this.props.userProfile} onViewRequest={this.props.onViewRequest} isHistoryLog={this._getIsHistoryLog()} lang={this.props.lang} onClose={() => this.setState({ teamModalSession: null })} />
3527
+ <WorkflowRunsModal run={this.state.workflowModalRun} onClose={() => this.setState({ workflowModalRun: null })} />
3528
+ <PresetModal open={this.state.mobilePresetModalVisible} onClose={() => this.setState({ mobilePresetModalVisible: false })} items={this.state.presetItems} onItemsChange={(items) => this.setState({ presetItems: items })} onSavePresets={(payload) => { if (this.props.onUpdatePreferences) this.props.onUpdatePreferences(payload); }} />
3529
+ </>);
3530
+ }
3531
+ }
3532
+
3533
+ export default ChatView;