termdock 1.4.295 → 1.4.297

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 (367) hide show
  1. package/.desktop-dist/main.js +35 -2
  2. package/README.en.md +257 -0
  3. package/README.md +284 -133
  4. package/dist/client/assets/App-BbqtNY2x.js +177 -0
  5. package/dist/client/assets/App-BbqtNY2x.js.br +0 -0
  6. package/dist/client/assets/App-BbqtNY2x.js.gz +0 -0
  7. package/dist/client/assets/{ChangeWalkthroughPanel-Dv5XRtRh.js → ChangeWalkthroughPanel-CvQJIp_C.js} +1 -1
  8. package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.br +0 -0
  9. package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.gz +0 -0
  10. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js +1 -0
  11. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.br +0 -0
  12. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.gz +0 -0
  13. package/dist/client/assets/CollaborationTaskContent-C01GXOEq.js +2 -0
  14. package/dist/client/assets/ComputerControlView-CW1uA133.js +4 -0
  15. package/dist/client/assets/ComputerControlView-CW1uA133.js.br +0 -0
  16. package/dist/client/assets/ComputerControlView-CW1uA133.js.gz +0 -0
  17. package/dist/client/assets/{DagPlayground-mq69dKyr.js → DagPlayground-CdE3tQsl.js} +1 -1
  18. package/dist/client/assets/DagPlayground-CdE3tQsl.js.br +0 -0
  19. package/dist/client/assets/DagPlayground-CdE3tQsl.js.gz +0 -0
  20. package/dist/client/assets/{DiffLab-BqdHYGi5.js → DiffLab-BLGNnazd.js} +1 -1
  21. package/dist/client/assets/DiffLab-BLGNnazd.js.br +0 -0
  22. package/dist/client/assets/DiffLab-BLGNnazd.js.gz +0 -0
  23. package/dist/client/assets/{DiffReview-BvzGxpuc.js → DiffReview-CYw3ASI6.js} +1 -1
  24. package/dist/client/assets/DiffReview-CYw3ASI6.js.br +0 -0
  25. package/dist/client/assets/DiffReview-CYw3ASI6.js.gz +0 -0
  26. package/dist/client/assets/{DiffReviewLab-DrfddB7x.js → DiffReviewLab-CySIuWq_.js} +1 -1
  27. package/dist/client/assets/DiffReviewLab-CySIuWq_.js.br +0 -0
  28. package/dist/client/assets/DiffReviewLab-CySIuWq_.js.gz +0 -0
  29. package/dist/client/assets/{DiffViewer-3bJV_A5s.js → DiffViewer-WsYvN75z.js} +13 -13
  30. package/dist/client/assets/DiffViewer-WsYvN75z.js.br +0 -0
  31. package/dist/client/assets/DiffViewer-WsYvN75z.js.gz +0 -0
  32. package/dist/client/assets/{ModelPreview-CX7VtoJB.js → ModelPreview-Dnlx9ByX.js} +1 -1
  33. package/dist/client/assets/ModelPreview-Dnlx9ByX.js.br +0 -0
  34. package/dist/client/assets/ModelPreview-Dnlx9ByX.js.gz +0 -0
  35. package/dist/client/assets/RightSidebar-Cp5b_y0u.js +1678 -0
  36. package/dist/client/assets/RightSidebar-Cp5b_y0u.js.br +0 -0
  37. package/dist/client/assets/RightSidebar-Cp5b_y0u.js.gz +0 -0
  38. package/dist/client/assets/{abnfDiagram-VCTEODGH-PJwhLTPH.js → abnfDiagram-VCTEODGH-CK1mGFRQ.js} +1 -1
  39. package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.br +0 -0
  40. package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.gz +0 -0
  41. package/dist/client/assets/app-DfACMixk.js +35 -0
  42. package/dist/client/assets/app-DfACMixk.js.br +0 -0
  43. package/dist/client/assets/app-DfACMixk.js.gz +0 -0
  44. package/dist/client/assets/{arc-l3FXjCEl.js → arc-yZE1gqDl.js} +1 -1
  45. package/dist/client/assets/arc-yZE1gqDl.js.br +0 -0
  46. package/dist/client/assets/arc-yZE1gqDl.js.gz +0 -0
  47. package/dist/client/assets/{architectureDiagram-5GKGNRK7-DdNbKi-4.js → architectureDiagram-5GKGNRK7-U5XSC7cY.js} +1 -1
  48. package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.br +0 -0
  49. package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.gz +0 -0
  50. package/dist/client/assets/{blockDiagram-I7D4REHJ-Cu63oaxR.js → blockDiagram-I7D4REHJ-pVJnntSG.js} +1 -1
  51. package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.br +0 -0
  52. package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.gz +0 -0
  53. package/dist/client/assets/{c4Diagram-7LVT6UL2-DQiV_yk6.js → c4Diagram-7LVT6UL2-Bwz0AJJV.js} +1 -1
  54. package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.br +0 -0
  55. package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.gz +0 -0
  56. package/dist/client/assets/channel-qqjDJtcZ.js +1 -0
  57. package/dist/client/assets/{chunk-2Q5K7J3B-COKJ79Ld.js → chunk-2Q5K7J3B-CXJ1kP4X.js} +1 -1
  58. package/dist/client/assets/{chunk-5VM5RSS4-BfV-m7oa.js → chunk-5VM5RSS4-Dpdum_2z.js} +1 -1
  59. package/dist/client/assets/{chunk-F27PBJKO-sLDCEnU8.js → chunk-F27PBJKO-BCG5Zi_-.js} +1 -1
  60. package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.br +0 -0
  61. package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.gz +0 -0
  62. package/dist/client/assets/{chunk-IMKFNOWR-DYu5qS7g.js → chunk-IMKFNOWR-BM-GcNcv.js} +1 -1
  63. package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.br +0 -0
  64. package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.gz +0 -0
  65. package/dist/client/assets/{chunk-JWPE2WC7-DixkILcl.js → chunk-JWPE2WC7-BWiXpQT9.js} +1 -1
  66. package/dist/client/assets/{chunk-POPQ4Y6H-CfW5Vm_-.js → chunk-POPQ4Y6H-vQSslMNv.js} +1 -1
  67. package/dist/client/assets/{chunk-SVP7TREG-nJnbUFzs.js → chunk-SVP7TREG-Aqfz6nPA.js} +1 -1
  68. package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.br +0 -0
  69. package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.gz +0 -0
  70. package/dist/client/assets/{chunk-TICWLB2K-BwyiSQgx.js → chunk-TICWLB2K-DDjekMd0.js} +1 -1
  71. package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.br +0 -0
  72. package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.gz +0 -0
  73. package/dist/client/assets/{chunk-XXDRQBXY-TCk4c2XN.js → chunk-XXDRQBXY-BegIBcFg.js} +1 -1
  74. package/dist/client/assets/classDiagram-ZZMXUADV-BDuBrfNz.js +1 -0
  75. package/dist/client/assets/classDiagram-v2-VYDZK3BY-BDuBrfNz.js +1 -0
  76. package/dist/client/assets/{cose-bilkent-JH36ORCC-rroo-EOb.js → cose-bilkent-JH36ORCC-B2izFsBY.js} +1 -1
  77. package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.br +0 -0
  78. package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.gz +0 -0
  79. package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js → cynefin-OW5HDTMX-C7HA99Zc.js} +1 -1
  80. package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.br +0 -0
  81. package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js.gz → cynefin-OW5HDTMX-C7HA99Zc.js.gz} +0 -0
  82. package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-hBwpU_bW.js → cynefinDiagram-5FMLGOSQ-vNIzcCYl.js} +1 -1
  83. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.br +0 -0
  84. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.gz +0 -0
  85. package/dist/client/assets/{dagre-GXQ25YYZ-B321-hdp.js → dagre-GXQ25YYZ-Bv9WHNT9.js} +1 -1
  86. package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.br +0 -0
  87. package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.gz +0 -0
  88. package/dist/client/assets/{diagram-S7CK7UJ4-bY87_ptA.js → diagram-S7CK7UJ4-DFg5UNic.js} +1 -1
  89. package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.br +0 -0
  90. package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.gz +0 -0
  91. package/dist/client/assets/{diagram-UQ7AKVKN--cez0H_o.js → diagram-UQ7AKVKN-BFQsCAs7.js} +1 -1
  92. package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.br +0 -0
  93. package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.gz +0 -0
  94. package/dist/client/assets/{diagram-VSXAHHWV-BupH_VRK.js → diagram-VSXAHHWV-C4R8TKEv.js} +1 -1
  95. package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.br +0 -0
  96. package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.gz +0 -0
  97. package/dist/client/assets/{diagram-VX7I27RA-DewqaxvF.js → diagram-VX7I27RA-CL75HIf0.js} +1 -1
  98. package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.br +0 -0
  99. package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.gz +0 -0
  100. package/dist/client/assets/{diagram-Z3DM3KII-wDumcTuR.js → diagram-Z3DM3KII-CfJampuO.js} +1 -1
  101. package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.br +0 -0
  102. package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.gz +0 -0
  103. package/dist/client/assets/{ebnfDiagram-PWID7BFC-C0OY91We.js → ebnfDiagram-PWID7BFC-Ct53jhNd.js} +1 -1
  104. package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.br +0 -0
  105. package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.gz +0 -0
  106. package/dist/client/assets/{erDiagram-RLTQ6QDP-Cw9aNqvJ.js → erDiagram-RLTQ6QDP-BgL2xOOa.js} +1 -1
  107. package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.br +0 -0
  108. package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.gz +0 -0
  109. package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js → flowDiagram-HODETNUW-Cp4Nu5jY.js} +1 -1
  110. package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js.br → flowDiagram-HODETNUW-Cp4Nu5jY.js.br} +0 -0
  111. package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.gz +0 -0
  112. package/dist/client/assets/{ganttDiagram-EL5Y4UJY-DovhJIz5.js → ganttDiagram-EL5Y4UJY-DI0rdyPX.js} +1 -1
  113. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.br +0 -0
  114. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.gz +0 -0
  115. package/dist/client/assets/{gestureArbiter-wjVok2Lg.js → gestureArbiter-Belsq4xq.js} +1 -1
  116. package/dist/client/assets/gestureArbiter-Belsq4xq.js.br +0 -0
  117. package/dist/client/assets/gestureArbiter-Belsq4xq.js.gz +0 -0
  118. package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-BmrbbyW1.js → gitGraphDiagram-WWUBYQGX-C0H4loQP.js} +1 -1
  119. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.br +0 -0
  120. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.gz +0 -0
  121. package/dist/client/assets/index-CYmI1EsU.css +1 -0
  122. package/dist/client/assets/index-CYmI1EsU.css.br +0 -0
  123. package/dist/client/assets/index-CYmI1EsU.css.gz +0 -0
  124. package/dist/client/assets/{infoDiagram-27XIBGKW-CQEu4Jwy.js → infoDiagram-27XIBGKW-BN4RpQTL.js} +1 -1
  125. package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BPqibqsm.js → ishikawaDiagram-5VMMS53U-BZQ5qZB_.js} +1 -1
  126. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.br +0 -0
  127. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.gz +0 -0
  128. package/dist/client/assets/{journeyDiagram-3NMN7TZE-DIkhubnf.js → journeyDiagram-3NMN7TZE-Q8bx0_N4.js} +1 -1
  129. package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.br +0 -0
  130. package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.gz +0 -0
  131. package/dist/client/assets/{kanban-definition-UXKFOSKX-CBT0DIUR.js → kanban-definition-UXKFOSKX-D7JhGtpO.js} +1 -1
  132. package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.br +0 -0
  133. package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.gz +0 -0
  134. package/dist/client/assets/{layout-C1CMlOIU.js → layout-DERUg0xU.js} +1 -1
  135. package/dist/client/assets/layout-DERUg0xU.js.br +0 -0
  136. package/dist/client/assets/layout-DERUg0xU.js.gz +0 -0
  137. package/dist/client/assets/{linear-C4gyqor0.js → linear-lNTe4x00.js} +1 -1
  138. package/dist/client/assets/linear-lNTe4x00.js.br +0 -0
  139. package/dist/client/assets/linear-lNTe4x00.js.gz +0 -0
  140. package/dist/client/assets/{mermaid.core-CMfa4ncd.js → mermaid.core-WHCwLQDQ.js} +5 -5
  141. package/dist/client/assets/mermaid.core-WHCwLQDQ.js.br +0 -0
  142. package/dist/client/assets/mermaid.core-WHCwLQDQ.js.gz +0 -0
  143. package/dist/client/assets/{mindmap-definition-YA3MSWOX-C-JmgKOI.js → mindmap-definition-YA3MSWOX-BouapA4v.js} +1 -1
  144. package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.br +0 -0
  145. package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.gz +0 -0
  146. package/dist/client/assets/{passwordBootstrap-BmUG2IwA.js → passwordBootstrap-E0piP814.js} +1 -1
  147. package/dist/client/assets/passwordBootstrap-E0piP814.js.br +0 -0
  148. package/dist/client/assets/passwordBootstrap-E0piP814.js.gz +0 -0
  149. package/dist/client/assets/{pegDiagram-XKGWAZYB-IY-AEKQa.js → pegDiagram-XKGWAZYB-Curpo5Rq.js} +1 -1
  150. package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.br +0 -0
  151. package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.gz +0 -0
  152. package/dist/client/assets/{pieDiagram-E7YTZNPT-9Qq43qED.js → pieDiagram-E7YTZNPT-BLQCHaOG.js} +1 -1
  153. package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.br +0 -0
  154. package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.gz +0 -0
  155. package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BjQncx5i.js → quadrantDiagram-AXDQQJYC-PViFqGao.js} +1 -1
  156. package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.br +0 -0
  157. package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.gz +0 -0
  158. package/dist/client/assets/{railroadDiagram-O6MQD6OU-Cr1rL0wZ.js → railroadDiagram-O6MQD6OU-BtHo1LpT.js} +1 -1
  159. package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.br +0 -0
  160. package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.gz +0 -0
  161. package/dist/client/assets/rdpSession-O_f9ejOI.js +1 -0
  162. package/dist/client/assets/rdpSession-O_f9ejOI.js.br +0 -0
  163. package/dist/client/assets/rdpSession-O_f9ejOI.js.gz +0 -0
  164. package/dist/client/assets/{referenceLongPress-ConC5-ym.js → referenceLongPress-BZr7Lqzh.js} +1 -1
  165. package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.br +0 -0
  166. package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.gz +0 -0
  167. package/dist/client/assets/{requirementDiagram-BXWQKSXE-DU2q_iZg.js → requirementDiagram-BXWQKSXE-BUm81HUU.js} +1 -1
  168. package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.br +0 -0
  169. package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.gz +0 -0
  170. package/dist/client/assets/rfb-CxsXsMUX.js +5 -0
  171. package/dist/client/assets/rfb-CxsXsMUX.js.br +0 -0
  172. package/dist/client/assets/rfb-CxsXsMUX.js.gz +0 -0
  173. package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BYlx9kXC.js → sankeyDiagram-P5KCCOFB-B-_-aZ_y.js} +1 -1
  174. package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.br +0 -0
  175. package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.gz +0 -0
  176. package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D_uAKme-.js → sequenceDiagram-WJ2MYXX4-DT6LDIfx.js} +1 -1
  177. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.br +0 -0
  178. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.gz +0 -0
  179. package/dist/client/assets/{sizeCapture-INFHLROL-H_uqscf4.js → sizeCapture-INFHLROL-CTYerd9j.js} +1 -1
  180. package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.br +0 -0
  181. package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.gz +0 -0
  182. package/dist/client/assets/{sparkles-D2z4Ue6G.js → sparkles-CgaUBIwH.js} +1 -1
  183. package/dist/client/assets/{stateDiagram-D77RDMKH-BvsQjIIW.js → stateDiagram-D77RDMKH-CQbb5x2_.js} +1 -1
  184. package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.br +0 -0
  185. package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.gz +0 -0
  186. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CJAVXcx1.js +1 -0
  187. package/dist/client/assets/{swimlanes-42K2YHIH-LmUJag-p.js → swimlanes-42K2YHIH-BHZ4X2Fd.js} +1 -1
  188. package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.br +0 -0
  189. package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.gz +0 -0
  190. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-VL2nyI27.js +8 -0
  191. package/dist/client/assets/{timeline-definition-24CTP7MA-DZ5n0Pim.js → timeline-definition-24CTP7MA-BrjH6R6w.js} +1 -1
  192. package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.br +0 -0
  193. package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.gz +0 -0
  194. package/dist/client/assets/{triangle-alert-Ct6iIBmv.js → triangle-alert-C9IHD_eF.js} +1 -1
  195. package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js → vennDiagram-4TSXK5OY-C9f5jYgk.js} +1 -1
  196. package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js.br → vennDiagram-4TSXK5OY-C9f5jYgk.js.br} +0 -0
  197. package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.gz +0 -0
  198. package/dist/client/assets/{videoCompression-VZIoFnyJ.js → videoCompression-DJaU8exd.js} +1 -1
  199. package/dist/client/assets/videoCompression-DJaU8exd.js.br +0 -0
  200. package/dist/client/assets/videoCompression-DJaU8exd.js.gz +0 -0
  201. package/dist/client/assets/{wardleyDiagram-VM6X3IG4-7hzU176j.js → wardleyDiagram-VM6X3IG4-6ljFvtiC.js} +1 -1
  202. package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.br +0 -0
  203. package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.gz +0 -0
  204. package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Bx4P96Bb.js → xychartDiagram-S5SC5T6Z-BaR2hm3e.js} +1 -1
  205. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.br +0 -0
  206. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.gz +0 -0
  207. package/dist/client/index.html +2 -2
  208. package/dist/client/index.html.br +0 -0
  209. package/dist/client/index.html.gz +0 -0
  210. package/dist/client/sw.js +1 -1
  211. package/dist/client/sw.js.br +0 -0
  212. package/dist/client/sw.js.gz +0 -0
  213. package/dist/server/agent/collaborationCli.js +45 -4
  214. package/dist/server/agent/collaborationDeliveryWorker.js +1 -1
  215. package/dist/server/agent/collaborationGroupRoutes.js +0 -2
  216. package/dist/server/agent/collaborationPeerTransport.js +8 -2
  217. package/dist/server/agent/collaborationResultPresentation.js +14 -0
  218. package/dist/server/agent/collaborationRoutes.js +18 -2
  219. package/dist/server/agent/collaborationService.js +46 -2
  220. package/dist/server/agent/collaborationStore.js +12 -21
  221. package/dist/server/agent/collaborationTaskBundles.js +121 -0
  222. package/dist/server/agent/collaborationTaskCli.js +54 -0
  223. package/dist/server/agent/collaborationTaskRoutes.js +58 -0
  224. package/dist/server/agent/collaborationTaskService.js +355 -0
  225. package/dist/server/agent/collaborationTaskStore.js +835 -0
  226. package/dist/server/agent/collaborationTaskTypes.js +1 -0
  227. package/dist/server/agent/collaborationTaskWorkspace.js +165 -0
  228. package/dist/server/agent/collaborationTeam.js +16 -0
  229. package/dist/server/agent/executionArchives.js +52 -0
  230. package/dist/server/entry.js +17 -2
  231. package/dist/server/federation/runtime.js +30 -7
  232. package/dist/server/routes/computer.js +198 -0
  233. package/dist/server/routes/computerTarget.js +23 -0
  234. package/dist/server/routes/guacamoleProtocol.js +55 -0
  235. package/dist/server/routes/rdp.js +162 -0
  236. package/dist/server/routes/terminal.js +441 -362
  237. package/dist/server/shared/computerPreferences.js +29 -0
  238. package/dist/server/utils/computerCredentials.js +88 -0
  239. package/dist/server/utils/sharedSampling.js +93 -0
  240. package/dist/server/utils/terminalOutputDelivery.js +8 -1
  241. package/dist/server/utils/tmuxMetadataWriter.js +61 -0
  242. package/package.json +4 -1
  243. package/runtime-manifest.json +4 -4
  244. package/dist/client/assets/App-D-cerXsD.js +0 -158
  245. package/dist/client/assets/App-D-cerXsD.js.br +0 -0
  246. package/dist/client/assets/App-D-cerXsD.js.gz +0 -0
  247. package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.br +0 -0
  248. package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.gz +0 -0
  249. package/dist/client/assets/DagPlayground-mq69dKyr.js.br +0 -0
  250. package/dist/client/assets/DagPlayground-mq69dKyr.js.gz +0 -0
  251. package/dist/client/assets/DiffLab-BqdHYGi5.js.br +0 -0
  252. package/dist/client/assets/DiffLab-BqdHYGi5.js.gz +0 -0
  253. package/dist/client/assets/DiffReview-BvzGxpuc.js.br +0 -0
  254. package/dist/client/assets/DiffReview-BvzGxpuc.js.gz +0 -0
  255. package/dist/client/assets/DiffReviewLab-DrfddB7x.js.br +0 -0
  256. package/dist/client/assets/DiffReviewLab-DrfddB7x.js.gz +0 -0
  257. package/dist/client/assets/DiffViewer-3bJV_A5s.js.br +0 -0
  258. package/dist/client/assets/DiffViewer-3bJV_A5s.js.gz +0 -0
  259. package/dist/client/assets/ModelPreview-CX7VtoJB.js.br +0 -0
  260. package/dist/client/assets/ModelPreview-CX7VtoJB.js.gz +0 -0
  261. package/dist/client/assets/RightSidebar-BK6vWNw3.js +0 -1678
  262. package/dist/client/assets/RightSidebar-BK6vWNw3.js.br +0 -0
  263. package/dist/client/assets/RightSidebar-BK6vWNw3.js.gz +0 -0
  264. package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.br +0 -0
  265. package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.gz +0 -0
  266. package/dist/client/assets/app-C7pXMvSE.js +0 -35
  267. package/dist/client/assets/app-C7pXMvSE.js.br +0 -0
  268. package/dist/client/assets/app-C7pXMvSE.js.gz +0 -0
  269. package/dist/client/assets/arc-l3FXjCEl.js.br +0 -0
  270. package/dist/client/assets/arc-l3FXjCEl.js.gz +0 -0
  271. package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.br +0 -0
  272. package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.gz +0 -0
  273. package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.br +0 -0
  274. package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.gz +0 -0
  275. package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.br +0 -0
  276. package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.gz +0 -0
  277. package/dist/client/assets/channel-CwC957gi.js +0 -1
  278. package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.br +0 -0
  279. package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.gz +0 -0
  280. package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.br +0 -0
  281. package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.gz +0 -0
  282. package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.br +0 -0
  283. package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.gz +0 -0
  284. package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.br +0 -0
  285. package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.gz +0 -0
  286. package/dist/client/assets/classDiagram-ZZMXUADV-CmoJ7-rE.js +0 -1
  287. package/dist/client/assets/classDiagram-v2-VYDZK3BY-CmoJ7-rE.js +0 -1
  288. package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.br +0 -0
  289. package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.gz +0 -0
  290. package/dist/client/assets/cynefin-OW5HDTMX-CkFeV85k.js.br +0 -0
  291. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.br +0 -0
  292. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.gz +0 -0
  293. package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.br +0 -0
  294. package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.gz +0 -0
  295. package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.br +0 -0
  296. package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.gz +0 -0
  297. package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.br +0 -0
  298. package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.gz +0 -0
  299. package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.br +0 -0
  300. package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.gz +0 -0
  301. package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.br +0 -0
  302. package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.gz +0 -0
  303. package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.br +0 -0
  304. package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.gz +0 -0
  305. package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.br +0 -0
  306. package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.gz +0 -0
  307. package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.br +0 -0
  308. package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.gz +0 -0
  309. package/dist/client/assets/flowDiagram-HODETNUW-BC7XUXeI.js.gz +0 -0
  310. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.br +0 -0
  311. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.gz +0 -0
  312. package/dist/client/assets/gestureArbiter-wjVok2Lg.js.br +0 -0
  313. package/dist/client/assets/gestureArbiter-wjVok2Lg.js.gz +0 -0
  314. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.br +0 -0
  315. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.gz +0 -0
  316. package/dist/client/assets/index-BeD4iJ6L.css +0 -1
  317. package/dist/client/assets/index-BeD4iJ6L.css.br +0 -0
  318. package/dist/client/assets/index-BeD4iJ6L.css.gz +0 -0
  319. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.br +0 -0
  320. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.gz +0 -0
  321. package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.br +0 -0
  322. package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.gz +0 -0
  323. package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.br +0 -0
  324. package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.gz +0 -0
  325. package/dist/client/assets/layout-C1CMlOIU.js.br +0 -0
  326. package/dist/client/assets/layout-C1CMlOIU.js.gz +0 -0
  327. package/dist/client/assets/linear-C4gyqor0.js.br +0 -0
  328. package/dist/client/assets/linear-C4gyqor0.js.gz +0 -0
  329. package/dist/client/assets/mermaid.core-CMfa4ncd.js.br +0 -0
  330. package/dist/client/assets/mermaid.core-CMfa4ncd.js.gz +0 -0
  331. package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.br +0 -0
  332. package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.gz +0 -0
  333. package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.br +0 -0
  334. package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.gz +0 -0
  335. package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.br +0 -0
  336. package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.gz +0 -0
  337. package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.br +0 -0
  338. package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.gz +0 -0
  339. package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.br +0 -0
  340. package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.gz +0 -0
  341. package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.br +0 -0
  342. package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.gz +0 -0
  343. package/dist/client/assets/referenceLongPress-ConC5-ym.js.br +0 -0
  344. package/dist/client/assets/referenceLongPress-ConC5-ym.js.gz +0 -0
  345. package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.br +0 -0
  346. package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.gz +0 -0
  347. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.br +0 -0
  348. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.gz +0 -0
  349. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.br +0 -0
  350. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.gz +0 -0
  351. package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.br +0 -0
  352. package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.gz +0 -0
  353. package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.br +0 -0
  354. package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.gz +0 -0
  355. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-DNgoExtD.js +0 -1
  356. package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.br +0 -0
  357. package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.gz +0 -0
  358. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BRuArMR5.js +0 -8
  359. package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.br +0 -0
  360. package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.gz +0 -0
  361. package/dist/client/assets/vennDiagram-4TSXK5OY-BkTA_Ciz.js.gz +0 -0
  362. package/dist/client/assets/videoCompression-VZIoFnyJ.js.br +0 -0
  363. package/dist/client/assets/videoCompression-VZIoFnyJ.js.gz +0 -0
  364. package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.br +0 -0
  365. package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.gz +0 -0
  366. package/dist/client/assets/xychartDiagram-S5SC5T6Z-Bx4P96Bb.js.br +0 -0
  367. package/dist/client/assets/xychartDiagram-S5SC5T6Z-Bx4P96Bb.js.gz +0 -0
@@ -1,3 +1,4 @@
1
+ import { ensureTeam } from '../agent/collaborationTeam.js';
1
2
  import { androidRecordings } from '../android/recording.js';
2
3
  import { assertPeerRegistrationAuthority } from '../agent/collaborationPeerTransport.js';
3
4
  import { progressRoutes } from '../notifications/progressRoutes.js';
@@ -5,6 +6,8 @@ import { ensureNodePty } from '../utils/ensureNodePty.js';
5
6
  import { PtySpawnBackoff, PtySpawnDeferredError } from '../utils/ptySpawnBackoff.js';
6
7
  import { collaborationGroupRoutes } from '../agent/collaborationGroupRoutes.js';
7
8
  import { TerminalClientAttachment } from '../utils/terminalClientAttachment.js';
9
+ import { SharedSessionSampler, SharedSnapshotCache } from '../utils/sharedSampling.js';
10
+ import { batchTmuxOptions, TmuxMetadataWriter } from '../utils/tmuxMetadataWriter.js';
8
11
  import { redrawTmuxClient } from '../utils/tmuxClientRedraw.js';
9
12
  import { TmuxInitialScreen } from '../utils/tmuxInitialScreen.js';
10
13
  import { TerminalOutputDelivery, resolveTerminalReplayCursor } from '../utils/terminalOutputDelivery.js';
@@ -30,11 +33,13 @@ import { getOnboardingServerUrl } from '../onboardingServer.js';
30
33
  import { inspectLinuxInotifyUsage, isWatchResourceExhaustion } from '../utils/fileWatchPolicy.js';
31
34
  import { writeResponseChunk } from '../utils/httpResponse.js';
32
35
  import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
33
- import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, tmuxMetadataChanged, } from '../utils/tmuxProgramDetection.js';
36
+ import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, } from '../utils/tmuxProgramDetection.js';
34
37
  import { agentBySlug, buildResumeCommand, detectAgentFromCommand, inferResumeSessionId, listAgents, } from '../agent/registry.js';
35
38
  import { applyAgentEvent, agentEventMatchesCurrentAgent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
36
39
  import { canRestoreDeadAgentShell, isConfirmedAgentResumeProcess, normalizePersistedAgentResumeInfo, } from '../agent/resumePersistence.js';
37
40
  import { AgentResumeHistoryStore } from '../agent/resumeHistory.js';
41
+ import { completedExecution, ExecutionArchiveStore } from '../agent/executionArchives.js';
42
+ import { archiveTaskWorkspace, restoreTaskWorkspace, snapshotTaskWorkspace } from '../agent/collaborationTaskWorkspace.js';
38
43
  import { isTmuxRecoveryCandidate } from '../utils/tmuxRecoveryCandidate.js';
39
44
  import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
40
45
  import { buildBracketedSubmitBytes, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
@@ -69,10 +74,15 @@ import { TermdockAutoUpdateManager } from '../utils/autoUpdate.js';
69
74
  import { AUTO_TITLE_LONG_RUNNING_DELAY_MS, AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, hasSubstantiveAutoTitleContext, isLongRunningAutoTitleTurnEligible, isNewAgentSessionId, isAutoTitleReevaluationDue, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
70
75
  import { getTitleNamerCatalog, invalidateTitleNamerCatalog, probePluginTitleNamer } from '../agent/titleNamerCatalog.js';
71
76
  import { RenderedTerminalContext } from '../agent/renderedTerminalContext.js';
77
+ import { CollaborationTaskStore } from '../agent/collaborationTaskStore.js';
78
+ import { collaborationTaskRoutes } from '../agent/collaborationTaskRoutes.js';
79
+ import { prepareTaskWorkspace } from '../agent/collaborationTaskWorkspace.js';
80
+ import { importTaskDependency } from '../agent/collaborationTaskBundles.js';
72
81
  const router = express.Router();
73
82
  const execFileAsync = promisify(execFile);
74
83
  const TERMDOCK_DIR = `${os.homedir()}/.termdock`;
75
84
  const automationStore = new AutomationStore(`${TERMDOCK_DIR}/automations.json`);
85
+ export const collaborationTaskStore = new CollaborationTaskStore(`${TERMDOCK_DIR}/collaboration-tasks.json`);
76
86
  export const collaborationStore = new CollaborationStore(`${TERMDOCK_DIR}/collaboration-groups.json`);
77
87
  const collaborationRouting = new CollaborationRoutingStore(`${TERMDOCK_DIR}/collaboration-routing.json`);
78
88
  const collaborationDeliveryWorker = new CollaborationDeliveryWorker({
@@ -176,6 +186,8 @@ function generateFrontendSessionId() {
176
186
  const GLOBAL_SESSION_STATE_FILE = `${TERMDOCK_DIR}/global-session-state.json`;
177
187
  const CLIENT_STATES_FILE = `${TERMDOCK_DIR}/client-states.json`; // 保留用于迁移
178
188
  const agentResumeHistory = new AgentResumeHistoryStore(`${TERMDOCK_DIR}/agent-resume-history.json`);
189
+ const executionArchives = new ExecutionArchiveStore(`${TERMDOCK_DIR}/execution-archives.json`);
190
+ let executionArchiveMutation = Promise.resolve();
179
191
  const TMUX_RECOVERY_STATE_FILE = `${TERMDOCK_DIR}/tmux-recovery-state.json`;
180
192
  const tmuxLifecycle = new TmuxLifecycleCoordinator();
181
193
  const intentionallyDeletingTmuxSessions = new Set();
@@ -189,6 +201,8 @@ let globalSessionStateReloadTimer = null;
189
201
  // reconciliation, etc.). Replaces the 5-second poll on the front-end. ──
190
202
  const controlClients = new Map();
191
203
  let latestSessionInventory = null;
204
+ // Only authoritative observations may remove persisted tmux sessions.
205
+ const inventoryTmuxLiveness = new WeakMap();
192
206
  let latestSessionInventoryAt = 0;
193
207
  let sessionInventoryBuildPromise = null;
194
208
  let broadcastInventorySeq = 0;
@@ -200,13 +214,12 @@ const inventoryOpenLocks = new Map();
200
214
  const CONTROL_BROADCAST_COALESCE_MS = 50;
201
215
  const SESSION_INVENTORY_CACHE_TTL_MS = 1500;
202
216
  async function getSessionInventorySnapshot(options = {}) {
217
+ if (sessionInventoryBuildPromise)
218
+ return sessionInventoryBuildPromise;
203
219
  const now = Date.now();
204
220
  if (!options.refresh && latestSessionInventory && now - latestSessionInventoryAt < SESSION_INVENTORY_CACHE_TTL_MS) {
205
221
  return latestSessionInventory;
206
222
  }
207
- if (!options.refresh && sessionInventoryBuildPromise) {
208
- return sessionInventoryBuildPromise;
209
- }
210
223
  const promise = buildSessionInventory()
211
224
  .then((inventory) => {
212
225
  latestSessionInventory = inventory;
@@ -977,22 +990,6 @@ function buildRuntimeTmuxMetadata(input) {
977
990
  });
978
991
  return { program, cwd, label, rawArgs: input.rawArgs ?? null };
979
992
  }
980
- function maybeRepairTmuxOptions(sessionName, current, next) {
981
- const currentSnapshot = {
982
- program: current.program ?? null,
983
- cwd: current.cwd ?? null,
984
- label: current.label ?? '',
985
- };
986
- if (!tmuxMetadataChanged(currentSnapshot, next)) {
987
- return;
988
- }
989
- void setTmuxOptions(sessionName, {
990
- '@termdock-label': next.label,
991
- '@termdock-program': next.program ?? '',
992
- '@termdock-cwd': next.cwd ?? '',
993
- '@termdock-last-active-at': String(Date.now()),
994
- });
995
- }
996
993
  function makeTerminalSessionPayload(backendSessionId, session, cols = 80, rows = 24) {
997
994
  return {
998
995
  sessionId: backendSessionId,
@@ -1387,10 +1384,13 @@ function orchestrationSessionSnapshot(record) {
1387
1384
  const backendId = binding?.backendSessionId ?? record.backendSessionId;
1388
1385
  const backend = backendId ? terminalSessions.get(backendId) : null;
1389
1386
  const route = collaborationDeliveryWorker.state(record.sessionId);
1387
+ // A live terminal backend is observable even before the delivery worker has
1388
+ // inspected this session. It does not prove a pinned Agent target is writable.
1389
+ const terminalConnected = Boolean(backend);
1390
1390
  const slug = binding?.pane?.agentSlug ?? binding?.agentSlug ?? record.agentResume?.slug;
1391
1391
  const agent = slug ? agentBySlug(slug) : backend?.agent;
1392
1392
  return {
1393
- ...collaborationStore.sessionFacts(record.sessionId, route.state === 'ready'),
1393
+ ...collaborationStore.sessionFacts(record.sessionId, terminalConnected || route.state === 'ready' || route.state === 'shell'),
1394
1394
  last_terminal_output_at: backend?.lastOutputAt || null,
1395
1395
  activity_observed_at: backend ? Date.now() : null,
1396
1396
  activity_source: backend ? 'terminal_output' : 'unavailable',
@@ -1404,7 +1404,7 @@ function orchestrationSessionSnapshot(record) {
1404
1404
  name: record.name,
1405
1405
  cwd: backend?.cwd ?? record.cwd ?? '',
1406
1406
  agent: agent ? { slug: agent.slug, displayName: agent.displayName } : null,
1407
- status: route.state,
1407
+ status: route.state === 'unchecked' && terminalConnected ? 'terminal-connected' : route.state,
1408
1408
  capability: agent
1409
1409
  ? [agent.displayName, ...(agent.capabilities ?? []), backend?.activeProgram?.command || record.activeProgram || 'Agent 会话'].join(' · ')
1410
1410
  : (backend?.activeProgram?.command || record.activeProgram || 'Shell 终端'),
@@ -1711,7 +1711,7 @@ async function resolveCollaborationRoute(frontendSessionId) {
1711
1711
  async function refreshCollaborationAgentIdentity(backendSessionId, session) {
1712
1712
  let activeProgram = null;
1713
1713
  if (session.mode === 'shell') {
1714
- activeProgram = await detectShellActiveProgram(session);
1714
+ activeProgram = await getCachedShellActiveProgram(backendSessionId, session);
1715
1715
  }
1716
1716
  else if (session.tmuxSessionName) {
1717
1717
  const layout = await getCachedTmuxLayout(session.tmuxSessionName);
@@ -1728,7 +1728,7 @@ async function refreshCollaborationAgentIdentity(backendSessionId, session) {
1728
1728
  function deliverCollaborationInboxWhenAgentReady(frontendSessionId) {
1729
1729
  collaborationDeliveryWorker.wake(frontendSessionId);
1730
1730
  }
1731
- async function spawnCollaborationAgentSession(req, group, sourceSessionId, input) {
1731
+ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input, bootstrapRole) {
1732
1732
  const agentSlug = typeof input.agentSlug === 'string' ? input.agentSlug.trim().toLowerCase() : '';
1733
1733
  const launchers = await listDetectedAgentLaunchers();
1734
1734
  const launcher = launchers.find((candidate) => candidate.slug === agentSlug);
@@ -1760,11 +1760,26 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
1760
1760
  const backend = terminalSessions.get(opened.terminalSession.sessionId);
1761
1761
  if (!backend)
1762
1762
  throw new HttpStatusError(500, 'Agent Session 创建失败', 'COLLAB_AGENT_SESSION_FAILED');
1763
+ const latestGroup = collaborationStore.getGroup(group.id);
1764
+ if (!latestGroup || latestGroup.deleted) {
1765
+ const created = globalSessionState.sessions.find(record => record.sessionId === frontendSessionId);
1766
+ if (created?.tmuxSessionName)
1767
+ await destroyTmuxSessionSafely(created.tmuxSessionName);
1768
+ else {
1769
+ cleanupSession(opened.terminalSession.sessionId, { killProcess: true });
1770
+ globalSessionState.sessions = globalSessionState.sessions.filter(record => record.sessionId !== frontendSessionId);
1771
+ }
1772
+ await persistGlobalStateNow();
1773
+ broadcastClientState();
1774
+ throw new HttpStatusError(409, '协作组已删除,未启动 Agent', 'COLLAB_GROUP_CHANGED');
1775
+ }
1763
1776
  const updatedGroup = collaborationStore.save({
1764
- id: group.id,
1765
- name: group.name,
1766
- sessionIds: [...group.sessionIds, frontendSessionId],
1777
+ id: latestGroup.id,
1778
+ name: latestGroup.name,
1779
+ sessionIds: [...latestGroup.sessionIds, frontendSessionId],
1767
1780
  });
1781
+ if (bootstrapRole)
1782
+ collaborationStore.setRole({ groupId: group.id, sessionId: frontendSessionId, role: bootstrapRole });
1768
1783
  writeTerminalInput(backend, `${buildCollaborationSpawnCommand({ slug: launcher.slug, command: launcher.command })}\r`);
1769
1784
  const requestedTask = typeof input.task === 'string' ? input.task.trim().slice(0, 20_000) : '';
1770
1785
  collaborationStore.send({
@@ -1779,6 +1794,60 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
1779
1794
  }, 300).unref?.();
1780
1795
  return { group: updatedGroup, session: orchestrationSessionSnapshot(globalSessionState.sessions.find((candidate) => candidate.sessionId === frontendSessionId)) };
1781
1796
  }
1797
+ const preparingTaskWorkers = new Map();
1798
+ /** Dedicated persistent terminals leave existing member sessions and the user's checkout untouched. */
1799
+ export function prepareCollaborationTaskWorker(task, template, dependencies, service, readDependency) {
1800
+ const key = `${task.id}:${task.events.filter(e => e.kind === 'scheduled').at(-1)?.id}`;
1801
+ const pending = preparingTaskWorkers.get(key);
1802
+ if (pending)
1803
+ return pending;
1804
+ const operation = (async () => {
1805
+ const source = globalSessionState.sessions.find(s => s.sessionId === template.sessionId);
1806
+ if (!source || template.serviceId !== service.descriptor().serviceId)
1807
+ throw new Error('执行成员当前不可用,请协调者重新分派');
1808
+ const snapshot = orchestrationSessionSnapshot(source);
1809
+ const slug = snapshot.agent?.slug ?? source.agentResume?.slug;
1810
+ const launcher = (await listDetectedAgentLaunchers()).find(a => a.slug === slug);
1811
+ if (!launcher || !source.cwd)
1812
+ throw new Error('独立代码任务需要有项目目录的 Agent 成员,请选择可用成员');
1813
+ const workspace = await prepareTaskWorkspace(task, source.cwd, dependencies, (dependency, repository) => importTaskDependency(task, dependency, repository, service, readDependency));
1814
+ await pathValidator.allowSessionCwd(workspace.cwd);
1815
+ const existing = globalSessionState.sessions.find(s => s.cwd === workspace.cwd);
1816
+ const req = {};
1817
+ const opened = await openInventorySession(req, { preferredFrontendSessionId: existing?.sessionId,
1818
+ name: `执行 ${task.title.slice(0, 70)}`, customName: true, mode: 'tmux', cwd: workspace.cwd });
1819
+ const record = globalSessionState.sessions.find(s => s.sessionId === opened.session.sessionId);
1820
+ const backend = terminalSessions.get(opened.terminalSession.sessionId);
1821
+ if (!backend)
1822
+ throw new Error('独立执行终端创建失败,目录已保留');
1823
+ const currentAgent = orchestrationSessionSnapshot(record).agent;
1824
+ const launchFile = path.join(path.dirname(workspace.cwd), `${path.basename(workspace.cwd)}.worker.json`);
1825
+ let launch = null;
1826
+ try {
1827
+ launch = JSON.parse(await fs.promises.readFile(launchFile, 'utf8'));
1828
+ }
1829
+ catch (error) {
1830
+ if (error.code !== 'ENOENT')
1831
+ throw error;
1832
+ }
1833
+ if (!currentAgent && (!launch || launch.backendId !== opened.terminalSession.sessionId)) {
1834
+ // Persist before typing: a retry must never type the launcher into a newly booted Agent's prompt.
1835
+ const temporary = `${launchFile}.${process.pid}.tmp`;
1836
+ await fs.promises.writeFile(temporary, JSON.stringify({ sessionId: record.sessionId, backendId: opened.terminalSession.sessionId }), { mode: 0o600 });
1837
+ await fs.promises.rename(temporary, launchFile);
1838
+ writeTerminalInput(backend, `${buildCollaborationSpawnCommand({ slug: launcher.slug, command: launcher.command })}\r`);
1839
+ }
1840
+ const group = collaborationStore.getGroup(task.groupId);
1841
+ if (!group || group.deleted)
1842
+ throw new Error('协作组已删除,执行目录已保留');
1843
+ if (!group.sessionIds.includes(record.sessionId))
1844
+ await service.save({ id: group.id, name: group.name,
1845
+ sessionIds: [...group.sessionIds, record.sessionId], expectedUpdatedAt: group.updatedAt });
1846
+ return { sessionId: record.sessionId, workspace };
1847
+ })().finally(() => preparingTaskWorkers.delete(key));
1848
+ preparingTaskWorkers.set(key, operation);
1849
+ return operation;
1850
+ }
1782
1851
  function getTmuxBinary() {
1783
1852
  if (process.env.TMUX_BIN)
1784
1853
  return process.env.TMUX_BIN;
@@ -2354,9 +2423,11 @@ async function observeTmuxServerGeneration(liveTmuxSessions) {
2354
2423
  async function buildSessionInventory() {
2355
2424
  const tmuxStatus = await getTmuxStatus();
2356
2425
  let liveTmuxSessions = [];
2426
+ let authoritativeTmuxList = false;
2357
2427
  if (tmuxStatus.available) {
2358
2428
  try {
2359
2429
  liveTmuxSessions = await listLiveTmuxInventorySessions();
2430
+ authoritativeTmuxList = true;
2360
2431
  }
2361
2432
  catch (error) {
2362
2433
  const errorMessage = getErrorMessage(error);
@@ -2372,12 +2443,12 @@ async function buildSessionInventory() {
2372
2443
  return tmux;
2373
2444
  }
2374
2445
  try {
2375
- const metadata = await resolveLiveTmuxMetadata(tmux.name);
2446
+ const metadata = await resolveLiveTmuxMetadata(tmux.name, true);
2376
2447
  if (!metadata) {
2377
2448
  return tmux;
2378
2449
  }
2379
2450
  backfillAgentResumeFromTmuxProcess(tmux.name, metadata.rawArgs);
2380
- maybeRepairTmuxOptions(tmux.name, tmux, metadata);
2451
+ syncDynamicTmuxMetadata({ tmuxSessionName: tmux.name, program: metadata.program, cwd: metadata.cwd });
2381
2452
  return {
2382
2453
  ...tmux,
2383
2454
  program: metadata.program,
@@ -2523,13 +2594,19 @@ async function buildSessionInventory() {
2523
2594
  lastActiveAt: tmux.lastActiveAt,
2524
2595
  };
2525
2596
  });
2526
- return {
2597
+ const inventory = {
2527
2598
  clientSessions,
2528
2599
  tmuxSessions,
2529
2600
  tmuxStatus,
2530
2601
  tmuxRecovery: getTmuxRecoveryView(),
2531
2602
  updatedAt: Date.now(),
2532
2603
  };
2604
+ if (authoritativeTmuxList) {
2605
+ const names = new Set(liveTmuxSessions.map(session => session.name));
2606
+ inventoryTmuxLiveness.set(inventory, names);
2607
+ dynamicTmuxMetadata.retain(names);
2608
+ }
2609
+ return inventory;
2533
2610
  }
2534
2611
  // ── end tmux user-option helpers ──
2535
2612
  async function captureTmuxPane(sessionName) {
@@ -3541,6 +3618,8 @@ try {
3541
3618
  catch { /* plugin loading must never block startup */ }
3542
3619
  let processSnapshot = null;
3543
3620
  let processSnapshotPromise = null;
3621
+ const ttyProcessSnapshots = new SharedSnapshotCache(PROCESS_SNAPSHOT_CACHE_TTL_MS);
3622
+ const shellProgramSnapshots = new SharedSnapshotCache(ACTIVE_PROGRAM_POLL_INTERVAL);
3544
3623
  function parseProcessSnapshot(stdout) {
3545
3624
  return stdout
3546
3625
  .split('\n')
@@ -3608,26 +3687,28 @@ async function resolveTmuxPaneProgram(pane) {
3608
3687
  const psArgs = pane.tty
3609
3688
  ? ['-t', pane.tty.replace(/^\/dev\//, ''), '-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args=']
3610
3689
  : ['-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args='];
3611
- const { stdout } = await execFileAsync('ps', psArgs, { timeout: 3000, maxBuffer: 512 * 1024 });
3612
- rows = stdout
3613
- .split('\n')
3614
- .map((line) => line.trim())
3615
- .filter(Boolean)
3616
- .map((line) => {
3617
- const match = line.match(/^(\d+)\s+(\d+)\s+(-?\d+)\s+(-?\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
3618
- if (!match)
3619
- return null;
3620
- return {
3621
- pid: Number.parseInt(match[1] || '0', 10),
3622
- ppid: Number.parseInt(match[2] || '0', 10),
3623
- pgid: Number.parseInt(match[3] || '0', 10),
3624
- tpgid: Number.parseInt(match[4] || '0', 10),
3625
- stat: match[5] || '',
3626
- comm: match[6] || '',
3627
- args: match[7]?.trim() || '',
3628
- };
3629
- })
3630
- .filter((row) => row !== null);
3690
+ rows = await ttyProcessSnapshots.get(pane.tty || '*', async () => {
3691
+ const result = await execFileAsync('ps', psArgs, { timeout: 3000, maxBuffer: 512 * 1024 });
3692
+ return result.stdout
3693
+ .split('\n')
3694
+ .map((line) => line.trim())
3695
+ .filter(Boolean)
3696
+ .map((line) => {
3697
+ const match = line.match(/^(\d+)\s+(\d+)\s+(-?\d+)\s+(-?\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
3698
+ if (!match)
3699
+ return null;
3700
+ return {
3701
+ pid: Number.parseInt(match[1] || '0', 10),
3702
+ ppid: Number.parseInt(match[2] || '0', 10),
3703
+ pgid: Number.parseInt(match[3] || '0', 10),
3704
+ tpgid: Number.parseInt(match[4] || '0', 10),
3705
+ stat: match[5] || '',
3706
+ comm: match[6] || '',
3707
+ args: match[7]?.trim() || '',
3708
+ };
3709
+ })
3710
+ .filter((row) => row !== null);
3711
+ });
3631
3712
  }
3632
3713
  const selected = selectTmuxForegroundProgram({
3633
3714
  panePid: pane.pid,
@@ -3683,8 +3764,8 @@ function getCwdFromTmuxLayout(layout) {
3683
3764
  const activePane = activeWindow?.panes.find((pane) => pane.id === layout.activePaneId);
3684
3765
  return activePane?.currentPath || null;
3685
3766
  }
3686
- async function resolveLiveTmuxMetadata(tmuxSessionName) {
3687
- const layout = await getTmuxLayout(tmuxSessionName);
3767
+ async function resolveLiveTmuxMetadata(tmuxSessionName, cached = false) {
3768
+ const layout = cached ? await getCachedTmuxLayout(tmuxSessionName) : await getTmuxLayout(tmuxSessionName);
3688
3769
  const activePane = getActivePaneFromLayout(layout);
3689
3770
  if (!activePane) {
3690
3771
  return null;
@@ -3749,31 +3830,87 @@ function findFriendlyNameForTmuxSession(tmuxSessionName) {
3749
3830
  }
3750
3831
  return null;
3751
3832
  }
3752
- // Push the latest dynamic metadata (program / cwd / label / last-active-at)
3753
- // onto the tmux session as user options. Caller passes the previous metadata
3754
- // snapshot and last-active-write timestamp so repeated polls with no change
3755
- // skip the tmux write entirely. Returns the new snapshot + write timestamp.
3756
- const TERMDOCK_LAST_ACTIVE_REFRESH_MS = 30_000;
3833
+ // Metadata belongs to the tmux session, including writes from inventory scans.
3834
+ // Batch options into one tmux process rather than four parallel child processes.
3835
+ const dynamicTmuxMetadata = new TmuxMetadataWriter(async (name, options) => {
3836
+ await runTmux(batchTmuxOptions(name, options));
3837
+ });
3757
3838
  function syncDynamicTmuxMetadata(input) {
3758
- const { tmuxSessionName, program, cwd, previousMetadata, lastActiveWriteAt } = input;
3759
- const metadata = buildRuntimeTmuxMetadata({ tmuxSessionName, program, cwd });
3760
- const now = Date.now();
3761
- if (!tmuxMetadataChanged(previousMetadata, metadata)) {
3762
- // Cheap path: refresh last-active-at at most every 30 s so external
3763
- // tools see the session as alive without flooding tmux every 500 ms.
3764
- if (now - lastActiveWriteAt >= TERMDOCK_LAST_ACTIVE_REFRESH_MS) {
3765
- void setTmuxOption(tmuxSessionName, '@termdock-last-active-at', String(now));
3766
- return { ...metadata, lastActiveWriteAt: now };
3767
- }
3768
- return { ...metadata, lastActiveWriteAt };
3769
- }
3770
- void setTmuxOptions(tmuxSessionName, {
3771
- '@termdock-label': metadata.label,
3772
- '@termdock-program': metadata.program ?? '',
3773
- '@termdock-cwd': metadata.cwd ?? '',
3774
- '@termdock-last-active-at': String(now),
3839
+ const metadata = buildRuntimeTmuxMetadata(input);
3840
+ void dynamicTmuxMetadata.sync(input.tmuxSessionName, {
3841
+ program: metadata.program, cwd: metadata.cwd, label: metadata.label,
3842
+ }).catch(error => {
3843
+ console.warn(`[tmux] metadata write failed for ${input.tmuxSessionName}: ${getErrorMessage(error)}`);
3775
3844
  });
3776
- return { ...metadata, lastActiveWriteAt: now };
3845
+ }
3846
+ function getCachedShellActiveProgram(sessionId, session) {
3847
+ return shellProgramSnapshots.get(`${sessionId}:${session.ptyProcess.pid}`, () => detectShellActiveProgram(session));
3848
+ }
3849
+ const terminalSamplers = new Map();
3850
+ const sampledProgramSessions = new WeakSet();
3851
+ function subscribeTerminalSampling(sessionId, session, observer) {
3852
+ let shared = terminalSamplers.get(sessionId);
3853
+ if (!shared || shared.session !== session) {
3854
+ shared?.sampler.stop();
3855
+ shared = {
3856
+ session,
3857
+ sampler: new SharedSessionSampler(() => sampleTerminalSession(sessionId, session), () => session.mode === 'tmux' ? TMUX_POLL_INTERVAL : ACTIVE_PROGRAM_POLL_INTERVAL, error => console.warn(`[terminal-sampling] ${sessionId}: ${getErrorMessage(error)}`)),
3858
+ };
3859
+ terminalSamplers.set(sessionId, shared);
3860
+ }
3861
+ return shared.sampler.subscribe(observer);
3862
+ }
3863
+ async function sampleTerminalSession(sessionId, session) {
3864
+ const tmuxName = session.tmuxSessionName;
3865
+ const layout = session.mode === 'tmux' && tmuxName ? await getCachedTmuxLayout(tmuxName) : null;
3866
+ const activePane = layout ? getActivePaneFromLayout(layout) : null;
3867
+ const resolved = activePane ? await resolveTmuxPaneProgram(activePane) : null;
3868
+ const activeProgram = layout
3869
+ ? (resolved ? { ...resolved, updatedAt: Date.now() } : getActiveProgramFromTmuxLayout(layout))
3870
+ : await getCachedShellActiveProgram(sessionId, session);
3871
+ // A deletion or task switch during a slow subprocess invalidates its result.
3872
+ if (terminalSessions.get(sessionId) !== session || session.tmuxSessionName !== tmuxName)
3873
+ return [];
3874
+ const programSignature = (ap) => JSON.stringify(ap
3875
+ ? { command: ap.command, source: ap.source, rawArgs: ap.rawArgs } : null);
3876
+ const programChanged = programSignature(activeProgram) !== programSignature(session.activeProgram);
3877
+ session.activeProgram = activeProgram;
3878
+ if (programChanged)
3879
+ persistActiveProgramBinding(sessionId, activeProgram?.command);
3880
+ if (programChanged || !sampledProgramSessions.has(session) || session.agentLeftAt !== null)
3881
+ syncAgentIdentity(sessionId, session);
3882
+ sampledProgramSessions.add(session);
3883
+ const events = [{
3884
+ type: 'active-program',
3885
+ activeProgram: activeProgram?.command ?? null,
3886
+ activeProgramRaw: activeProgram?.rawArgs ?? null,
3887
+ activeProgramSource: activeProgram?.source ?? null,
3888
+ }];
3889
+ if (!layout)
3890
+ return events;
3891
+ const cwd = getCwdFromTmuxLayout(layout);
3892
+ if (cwd && cwd !== session.cwd) {
3893
+ session.cwd = cwd;
3894
+ if (updateGlobalBindingForBackendSession(sessionId, { cwd, lastActivity: session.lastActivity }))
3895
+ schedulePersistGlobalState();
3896
+ refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
3897
+ }
3898
+ if (activePane) {
3899
+ if (activePane.title)
3900
+ session.lastOscTitle = activePane.title;
3901
+ const state = activePane.command && !shellNamesBackend.has(activePane.command) ? 'running' : 'idle';
3902
+ if (session.lastPromptState === 'running' && state === 'idle')
3903
+ refreshGitStatus(sessionId, session);
3904
+ session.lastPromptState = state;
3905
+ }
3906
+ syncDynamicTmuxMetadata({ tmuxSessionName: tmuxName, program: activeProgram?.command ?? null, cwd: session.cwd ?? null });
3907
+ events.push({ type: 'cwd', cwd: session.cwd ?? null });
3908
+ if (session.lastOscTitle)
3909
+ events.push({ type: 'shell-title', title: session.lastOscTitle });
3910
+ if (session.lastPromptState)
3911
+ events.push({ type: 'prompt-state', state: session.lastPromptState });
3912
+ events.push({ type: 'tmux-layout', layout });
3913
+ return events;
3777
3914
  }
3778
3915
  async function detectShellActiveProgram(session) {
3779
3916
  const pid = getPtyProcessPid(session.ptyProcess);
@@ -4358,6 +4495,9 @@ function cleanupSession(sessionId, options) {
4358
4495
  if (!session) {
4359
4496
  return;
4360
4497
  }
4498
+ const sampler = terminalSamplers.get(sessionId);
4499
+ sampler?.sampler.stop();
4500
+ terminalSamplers.delete(sessionId);
4361
4501
  session.dataDisposable?.dispose();
4362
4502
  session.exitDisposable?.dispose();
4363
4503
  cancelLongRunningAutoTitle(session);
@@ -5368,6 +5508,7 @@ router.delete('/tmux/recovery', async (_req, res) => {
5368
5508
  async function destroyTmuxSessionSafely(rawName) {
5369
5509
  return tmuxLifecycle.run(`destroy:${rawName}`, async () => {
5370
5510
  intentionallyDeletingTmuxSessions.add(rawName);
5511
+ dynamicTmuxMetadata.forget(rawName);
5371
5512
  try {
5372
5513
  const affectedSessionIds = [];
5373
5514
  for (const [sessionId, session] of terminalSessions.entries()) {
@@ -5631,6 +5772,8 @@ export function collaborationLocalActivity() {
5631
5772
  });
5632
5773
  }
5633
5774
  export function deliverPeerCollaboration(sessionId) { void tryDeliverCollaborationInbox(sessionId); }
5775
+ router.use('/operations/collaboration-tasks', collaborationTaskRoutes({ agent: false, store: collaborationStore, resolveSession: resolveFrontendSessionId }));
5776
+ router.use('/operations/orchestration/tasks', collaborationTaskRoutes({ agent: true, store: collaborationStore, resolveSession: resolveFrontendSessionId }));
5634
5777
  router.use('/operations', collaborationGroupRoutes({ store: collaborationStore,
5635
5778
  sessions: () => globalSessionState.sessions.map(orchestrationSessionSnapshot),
5636
5779
  save: (req, input) => {
@@ -5648,6 +5791,53 @@ function resolveGroupParam(id) {
5648
5791
  return { group: resolved.status === 'ok' ? collaborationStore.getGroup(resolved.id) : null, ambiguous: 0 };
5649
5792
  }
5650
5793
  const ambiguousGroupResponse = (res, count) => res.status(409).json({ error: ambiguousIdMessage('协作组', count), code: 'GROUP_ID_AMBIGUOUS' });
5794
+ const preparingTeams = new Map();
5795
+ router.post('/operations/collaboration-groups/:groupId/team', async (req, res) => {
5796
+ try {
5797
+ assertPeerRegistrationAuthority(req);
5798
+ const { group, ambiguous } = resolveGroupParam(req.params.groupId);
5799
+ if (ambiguous)
5800
+ return ambiguousGroupResponse(res, ambiguous);
5801
+ if (!group || group.deleted)
5802
+ return res.status(404).json({ error: '协作组不存在' });
5803
+ if (group.federated)
5804
+ throw new Error('跨服务组请先从成员设置添加 Agent');
5805
+ const agentSlug = typeof req.body?.agentSlug === 'string' ? req.body.agentSlug : '';
5806
+ if (!(await listDetectedAgentLaunchers()).some(agent => agent.slug === agentSlug))
5807
+ throw new Error('所选 Agent 当前不可用');
5808
+ if (typeof req.body?.cwd !== 'string' || !req.body.cwd.trim())
5809
+ throw new Error('请选择工作目录');
5810
+ const cwd = await resolveWorkingDirectory(req, req.body.cwd);
5811
+ const previous = preparingTeams.get(group.id) ?? Promise.resolve();
5812
+ const operation = previous.catch(() => { }).then(() => ensureTeam({ agentSlug, cwd,
5813
+ members: () => {
5814
+ const current = collaborationStore.getGroup(group.id);
5815
+ if (!current || current.deleted)
5816
+ throw new Error('协作组已删除');
5817
+ return globalSessionState.sessions.filter(record => current.sessionIds.includes(record.sessionId))
5818
+ .map(record => ({ id: record.sessionId, cwd: record.cwd, role: current.roles?.[record.sessionId], agentSlug: orchestrationSessionSnapshot(record).agent?.slug }));
5819
+ },
5820
+ spawn: async (role) => {
5821
+ const current = collaborationStore.getGroup(group.id);
5822
+ if (!current || current.deleted)
5823
+ throw new Error('协作组已删除');
5824
+ const result = await spawnCollaborationAgentSession(req, current, null, { agentSlug, cwd, mode: 'tmux', name: role.startsWith('自动协调者') ? `协调 ${group.name}` : `执行与评审 ${group.name}` }, role);
5825
+ return result.session.sessionId;
5826
+ },
5827
+ }));
5828
+ preparingTeams.set(group.id, operation);
5829
+ try {
5830
+ res.json(await operation);
5831
+ }
5832
+ finally {
5833
+ if (preparingTeams.get(group.id) === operation)
5834
+ preparingTeams.delete(group.id);
5835
+ }
5836
+ }
5837
+ catch (error) {
5838
+ res.status(getErrorMessage(error) === 'AUTHORIZATION_DENIED' ? 403 : 409).json({ error: getErrorMessage(error) });
5839
+ }
5840
+ });
5651
5841
  router.post('/operations/collaboration-groups/:groupId/spawn', async (req, res) => {
5652
5842
  const { group, ambiguous } = resolveGroupParam(req.params.groupId);
5653
5843
  if (ambiguous)
@@ -5760,8 +5950,8 @@ router.post('/operations/orchestration/members', (req, res) => {
5760
5950
  const nextSessionIds = action === 'add'
5761
5951
  ? Array.from(new Set([...group.sessionIds, targetSessionId]))
5762
5952
  : group.sessionIds.filter((sessionId) => sessionId !== targetSessionId);
5763
- if (nextSessionIds.length < 2)
5764
- return res.status(400).json({ error: '协作组至少需要两个会话' });
5953
+ if (group.federated && nextSessionIds.length < 2)
5954
+ return res.status(400).json({ error: '跨服务组仍需两个成员;移出操作未保存' });
5765
5955
  const updated = collaborationStore.save({ id: group.id, name: group.name, sessionIds: nextSessionIds });
5766
5956
  res.json({ ok: true, group: updated });
5767
5957
  });
@@ -5831,7 +6021,7 @@ router.post('/operations/orchestration/cleanup', async (req, res) => {
5831
6021
  const remaining = group.sessionIds.filter((member) => !requestedSet.has(member)).length;
5832
6022
  groupEffects.push({
5833
6023
  id: group.id, name: group.name ?? null, sizeBefore: group.sessionIds.length,
5834
- sizeAfter: remaining < 2 ? 0 : remaining, dissolves: remaining < 2,
6024
+ sizeAfter: group.federated ? group.sessionIds.length : remaining, dissolves: false,
5835
6025
  includesCaller: group.sessionIds.includes(sourceSessionId),
5836
6026
  });
5837
6027
  }
@@ -6097,6 +6287,140 @@ router.post('/operations/session-search/prepare', async (req, res) => {
6097
6287
  res.status(400).json({ error: `无法恢复会话:${getErrorMessage(error)}` });
6098
6288
  }
6099
6289
  });
6290
+ router.get('/operations/execution-archives', (req, res) => {
6291
+ try {
6292
+ assertPeerRegistrationAuthority(req);
6293
+ res.json({ entries: executionArchives.list().filter(entry => !globalSessionState.sessions.some(record => record.sessionId === entry.sessionId)).map(entry => entry.restoredAt ? { ...entry, cleanup: { ...entry.cleanup, state: 'retained', reason: '恢复后关闭的会话,执行目录保留' } } : entry) });
6294
+ }
6295
+ catch (error) {
6296
+ res.status(403).json({ error: getErrorMessage(error) });
6297
+ }
6298
+ });
6299
+ function executionArchiveAction(action) {
6300
+ return async (req, res) => {
6301
+ try {
6302
+ assertPeerRegistrationAuthority(req);
6303
+ const operation = executionArchiveMutation.then(() => action(req));
6304
+ executionArchiveMutation = operation.then(() => undefined, () => undefined);
6305
+ res.json(await operation);
6306
+ }
6307
+ catch (error) {
6308
+ res.status(getErrorMessage(error) === 'AUTHORIZATION_DENIED' ? 403 : 409).json({ error: getErrorMessage(error) });
6309
+ }
6310
+ };
6311
+ }
6312
+ router.post('/operations/execution-archives/:sessionId/archive', executionArchiveAction(async (req) => {
6313
+ const sessionId = req.params.sessionId;
6314
+ const record = globalSessionState.sessions.find(record => record.sessionId === sessionId);
6315
+ if (!record) {
6316
+ const entry = executionArchives.get(sessionId);
6317
+ if (entry && !entry.restoredAt)
6318
+ return { entry };
6319
+ throw new Error('执行会话已变化,请刷新列表');
6320
+ }
6321
+ const service = req.app.locals.collaborationService;
6322
+ if (!service)
6323
+ throw new Error('协作服务正在启动');
6324
+ const groups = collaborationStore.groupsForSession(sessionId);
6325
+ const tasks = collaborationTaskStore.list(collaborationStore.list().map(group => group.id));
6326
+ const completed = completedExecution(tasks, service.descriptor().serviceId, sessionId);
6327
+ const workspace = completed.find(task => task.workspace?.cwd === record.cwd)?.workspace;
6328
+ if (!workspace)
6329
+ throw new Error('执行目录与任务记录不符,终端已保留');
6330
+ if (globalSessionState.sessions.some(other => other.sessionId !== sessionId && (other.cwd === workspace.cwd
6331
+ || record.tmuxSessionName && other.tmuxSessionName === record.tmuxSessionName
6332
+ || record.backendSessionId && other.backendSessionId === record.backendSessionId)))
6333
+ throw new Error('此执行目录或终端仍被其他会话使用,不能归档');
6334
+ const observed = orchestrationSessionSnapshot(record);
6335
+ const agent = normalizePersistedAgentResumeInfo({ slug: observed.agent?.slug, sessionId: observed.agentNativeSessionId,
6336
+ launchArgv: record.agentResume && record.agentResume.slug === observed.agent?.slug ? record.agentResume.launchArgv : null, updatedAt: Date.now() });
6337
+ if (!agent)
6338
+ throw new Error('尚未取得 Agent 原生会话 ID,无法保证恢复;终端已保留');
6339
+ const launcher = agentBySlug(agent.slug);
6340
+ if (!launcher || !buildResumeCommand(launcher, agent.sessionId, agent.launchArgv))
6341
+ throw new Error('此 Agent 没有可用的恢复命令,终端已保留');
6342
+ const commit = await snapshotTaskWorkspace(workspace);
6343
+ // Persist the conversation and branch before stopping anything. Pending
6344
+ // cleanup can also reconstruct a removed directory after an interrupted save.
6345
+ const entry = executionArchives.save({ sessionId, title: record.name, cwd: workspace.cwd, agent,
6346
+ groups: groups.map(group => ({ id: group.id, role: group.roles?.[sessionId] })), taskIds: completed.map(task => task.id),
6347
+ workspace, archivedAt: Date.now(), cleanup: { state: 'pending', commit } });
6348
+ if (record.mode === 'tmux' && record.tmuxSessionName)
6349
+ await destroyTmuxSessionSafely(record.tmuxSessionName);
6350
+ else {
6351
+ if (record.backendSessionId)
6352
+ cleanupSession(record.backendSessionId, { killProcess: true });
6353
+ globalSessionState = { sessions: globalSessionState.sessions.filter(item => item.sessionId !== sessionId), updatedAt: Date.now() };
6354
+ }
6355
+ collaborationStore.archiveSession(sessionId);
6356
+ collaborationRouting.remove(sessionId);
6357
+ await persistGlobalStateNow();
6358
+ broadcastClientState();
6359
+ const cleanup = await archiveTaskWorkspace(workspace);
6360
+ return { entry: executionArchives.save({ ...entry, cleanup: { ...cleanup, commit: cleanup.commit ?? commit } }) };
6361
+ }));
6362
+ function restoreArchivedMembership(entry, sessionId) {
6363
+ for (const saved of entry.groups) {
6364
+ const group = collaborationStore.getGroup(saved.id);
6365
+ if (!group || group.deleted)
6366
+ continue;
6367
+ collaborationStore.save({ id: group.id, name: group.name, sessionIds: [...new Set([...group.sessionIds, sessionId])] });
6368
+ if (saved.role)
6369
+ collaborationStore.setRole({ groupId: group.id, sessionId: sessionId, role: saved.role });
6370
+ }
6371
+ }
6372
+ router.post('/operations/execution-archives/:sessionId/restore', executionArchiveAction(async (req) => {
6373
+ const entry = executionArchives.get(req.params.sessionId);
6374
+ if (!entry)
6375
+ throw new Error('执行归档不存在');
6376
+ const existing = globalSessionState.sessions.find(record => record.sessionId === entry.sessionId);
6377
+ if (existing) {
6378
+ if (!entry.restoredAt)
6379
+ throw new Error('原会话仍在运行,请先完成归档');
6380
+ restoreArchivedMembership(entry, existing.sessionId);
6381
+ return { session: orchestrationSessionSnapshot(existing) };
6382
+ }
6383
+ // A newly reopened task may already have another active execution. Resume is
6384
+ // an explicit conversation launch; it never reopens or reassigns the task.
6385
+ const launcher = agentBySlug(entry.agent.slug);
6386
+ const command = launcher ? buildResumeCommand(launcher, entry.agent.sessionId, entry.agent.launchArgv) : null;
6387
+ if (!command)
6388
+ throw new Error('此 Agent 当前没有可用的恢复命令,归档记录已保留');
6389
+ const target = { slug: entry.agent.slug, nativeSessionId: entry.agent.sessionId, command };
6390
+ if (findActiveAgentResumeOwner('', target) || await findExternalCodexWriter(target))
6391
+ throw new Error('此 Agent 会话已在其他终端打开,请切换到已有会话');
6392
+ await restoreTaskWorkspace(entry.workspace, entry.cleanup);
6393
+ await pathValidator.allowSessionCwd(entry.cwd);
6394
+ const opened = await openInventorySession(req, { preferredFrontendSessionId: entry.sessionId, name: entry.title, customName: true, mode: 'tmux', cwd: entry.cwd });
6395
+ const record = globalSessionState.sessions.find(record => record.sessionId === opened.session.sessionId);
6396
+ const backend = terminalSessions.get(opened.terminalSession.sessionId);
6397
+ if (!backend)
6398
+ throw new Error('恢复终端失败,归档记录已保留');
6399
+ upsertGlobalSessionRecord({ ...record, agentResume: entry.agent });
6400
+ // One serialized restore per native conversation. Never type into an existing Agent.
6401
+ try {
6402
+ executionArchives.save({ ...entry, restoredAt: Date.now() });
6403
+ }
6404
+ catch (error) {
6405
+ // The newly created shell has not received a resume command. Roll it back
6406
+ // so the retained archive remains safely retryable after a disk failure.
6407
+ if (record.tmuxSessionName)
6408
+ await destroyTmuxSessionSafely(record.tmuxSessionName);
6409
+ else {
6410
+ cleanupSession(opened.terminalSession.sessionId, { killProcess: true });
6411
+ globalSessionState.sessions = globalSessionState.sessions.filter(item => item.sessionId !== record.sessionId);
6412
+ }
6413
+ collaborationStore.archiveSession(record.sessionId);
6414
+ await persistGlobalStateNow();
6415
+ broadcastClientState();
6416
+ throw error;
6417
+ }
6418
+ writeTerminalInput(backend, `${command}\r`);
6419
+ restoreArchivedMembership(entry, record.sessionId);
6420
+ await persistGlobalStateNow();
6421
+ broadcastClientState();
6422
+ return { session: orchestrationSessionSnapshot(globalSessionState.sessions.find(item => item.sessionId === record.sessionId)) };
6423
+ }));
6100
6424
  router.get('/agent-resume-history', (_req, res) => {
6101
6425
  const entries = agentResumeHistory.list().flatMap((entry) => {
6102
6426
  const agent = agentBySlug(entry.agent.slug);
@@ -7144,7 +7468,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7144
7468
  // 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
7145
7469
  try {
7146
7470
  if (session.mode === 'shell') {
7147
- const ap = await detectShellActiveProgram(session);
7471
+ const ap = await getCachedShellActiveProgram(sessionId, session);
7148
7472
  if (ap)
7149
7473
  session.activeProgram = ap;
7150
7474
  }
@@ -7197,131 +7521,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7197
7521
  if (session.gitStatus) {
7198
7522
  writeSse(res, { type: 'git-status', gitStatus: session.gitStatus });
7199
7523
  }
7200
- let tmuxInterval = null;
7201
- let activeProgramInterval = null;
7202
- let lastTmuxLayoutSnapshot = '';
7203
- let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
7204
- let lastTmuxMetadata = null;
7205
- let lastTmuxMetaWriteAt = 0;
7206
- const maybeWriteActiveProgram = (activeProgram) => {
7207
- const snapshot = JSON.stringify(activeProgram ? { command: activeProgram.command, source: activeProgram.source } : null);
7208
- if (snapshot === lastActiveProgramSnapshot) {
7209
- return;
7210
- }
7211
- lastActiveProgramSnapshot = snapshot;
7212
- session.activeProgram = activeProgram;
7213
- persistActiveProgramBinding(sessionId, activeProgram?.command);
7214
- // Agent status: react to AI tool start/exit
7215
- syncAgentIdentity(sessionId, session);
7216
- console.log(`[active-program][shell-sse] session=${sessionId} client=${clientId} cmd=${activeProgram?.command ?? null} source=${activeProgram?.source ?? null}`);
7217
- writeSse(res, {
7218
- type: 'active-program',
7219
- activeProgram: activeProgram?.command ?? null,
7220
- activeProgramRaw: activeProgram?.rawArgs ?? null,
7221
- activeProgramSource: activeProgram?.source ?? null,
7222
- });
7223
- };
7224
- const sendTmuxLayout = async () => {
7225
- if (session.mode !== 'tmux' || !session.tmuxSessionName) {
7226
- return;
7227
- }
7228
- try {
7229
- const layout = await getCachedTmuxLayout(session.tmuxSessionName);
7230
- // Resolve the active program — try ps-based detection for generic commands
7231
- const activePane = getActivePaneFromLayout(layout);
7232
- if (activePane) {
7233
- const resolved = await resolveTmuxPaneProgram(activePane);
7234
- if (resolved) {
7235
- maybeWriteActiveProgram({
7236
- command: resolved.command,
7237
- source: resolved.source,
7238
- rawArgs: resolved.rawArgs,
7239
- updatedAt: Date.now(),
7240
- });
7241
- }
7242
- else {
7243
- maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
7244
- }
7245
- }
7246
- else {
7247
- maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
7248
- }
7249
- const newCwd = getCwdFromTmuxLayout(layout);
7250
- if (newCwd && newCwd !== session.cwd) {
7251
- session.cwd = newCwd;
7252
- console.log(`[tmux-cwd][sse] session=${sessionId} cwd=${newCwd}`);
7253
- if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
7254
- schedulePersistGlobalState();
7255
- }
7256
- writeSse(res, { type: 'cwd', cwd: newCwd });
7257
- refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
7258
- }
7259
- // tmux 消费了 inner shell 发的 OSC 2/133,不透传到外层 PTY。
7260
- // 从 tmux layout 提取 active pane 的 title 和 command 来推导。
7261
- if (activePane) {
7262
- const paneTitle = activePane.title || '';
7263
- if (paneTitle && paneTitle !== session.lastOscTitle) {
7264
- session.lastOscTitle = paneTitle;
7265
- writeSse(res, { type: 'shell-title', title: paneTitle });
7266
- }
7267
- const paneCmd = activePane.command || '';
7268
- const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
7269
- if (inferredState !== session.lastPromptState) {
7270
- const wasRunning = session.lastPromptState === 'running';
7271
- session.lastPromptState = inferredState;
7272
- writeSse(res, { type: 'prompt-state', state: inferredState });
7273
- if (wasRunning && inferredState === 'idle') {
7274
- refreshGitStatus(sessionId, session);
7275
- }
7276
- }
7277
- }
7278
- // Mirror dynamic metadata onto tmux user options (cheap when nothing
7279
- // changed thanks to the full metadata snapshot cache).
7280
- const meta = syncDynamicTmuxMetadata({
7281
- tmuxSessionName: session.tmuxSessionName,
7282
- program: session.activeProgram?.command ?? null,
7283
- cwd: session.cwd ?? null,
7284
- previousMetadata: lastTmuxMetadata,
7285
- lastActiveWriteAt: lastTmuxMetaWriteAt,
7286
- });
7287
- lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
7288
- lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
7289
- const snapshot = JSON.stringify(layout);
7290
- if (snapshot === lastTmuxLayoutSnapshot) {
7291
- return;
7292
- }
7293
- lastTmuxLayoutSnapshot = snapshot;
7294
- writeSse(res, { type: 'tmux-layout', layout });
7295
- }
7296
- catch (error) {
7297
- const errorMessage = error instanceof Error ? error.message : String(error);
7298
- console.warn(`Failed to fetch tmux layout for ${session.tmuxSessionName}: ${errorMessage}`);
7299
- }
7300
- };
7301
- const sendShellActiveProgram = async () => {
7302
- if (session.mode !== 'shell') {
7303
- return;
7304
- }
7305
- try {
7306
- maybeWriteActiveProgram(await detectShellActiveProgram(session));
7307
- }
7308
- catch (error) {
7309
- const errorMessage = error instanceof Error ? error.message : String(error);
7310
- console.warn(`Failed to detect active shell program for ${sessionId}: ${errorMessage}`);
7311
- }
7312
- };
7313
- if (session.mode === 'tmux' && session.tmuxSessionName) {
7314
- void sendTmuxLayout();
7315
- tmuxInterval = setInterval(() => {
7316
- void sendTmuxLayout();
7317
- }, TMUX_POLL_INTERVAL);
7318
- }
7319
- else {
7320
- void sendShellActiveProgram();
7321
- activeProgramInterval = setInterval(() => {
7322
- void sendShellActiveProgram();
7323
- }, ACTIVE_PROGRAM_POLL_INTERVAL);
7324
- }
7524
+ const stopSampling = subscribeTerminalSampling(sessionId, session, event => writeSse(res, event));
7325
7525
  const heartbeatInterval = setInterval(() => {
7326
7526
  if (!writeResponseChunk(res, ': heartbeat\n\n')) {
7327
7527
  clearInterval(heartbeatInterval);
@@ -7337,12 +7537,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7337
7537
  }
7338
7538
  const cleanup = () => {
7339
7539
  clearInterval(heartbeatInterval);
7340
- if (tmuxInterval) {
7341
- clearInterval(tmuxInterval);
7342
- }
7343
- if (activeProgramInterval) {
7344
- clearInterval(activeProgramInterval);
7345
- }
7540
+ stopSampling();
7346
7541
  closeClient(session, sessionId, clientId);
7347
7542
  console.log(`Client ${clientId} disconnected from terminal session ${sessionId}`);
7348
7543
  };
@@ -7900,7 +8095,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
7900
8095
  // 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
7901
8096
  try {
7902
8097
  if (session.mode === 'shell') {
7903
- const ap = await detectShellActiveProgram(session);
8098
+ const ap = await getCachedShellActiveProgram(sessionId, session);
7904
8099
  if (ap)
7905
8100
  session.activeProgram = ap;
7906
8101
  }
@@ -7930,7 +8125,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
7930
8125
  if (attachment) {
7931
8126
  if (ws.readyState !== ws.OPEN)
7932
8127
  return;
7933
- delivery.setActive(outputWanted && !clientPaused);
8128
+ delivery.setActive(outputWanted);
7934
8129
  if (delivery.active) {
7935
8130
  const screen = new TmuxInitialScreen();
7936
8131
  initialScreen = screen;
@@ -8050,129 +8245,10 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8050
8245
  await sendReplay(0);
8051
8246
  });
8052
8247
  void sendReplay();
8053
- // Tmux layout polling (per-client, like the SSE stream does)
8054
- let tmuxInterval = null;
8055
- let activeProgramInterval = null;
8056
- if (session.mode === 'tmux' && session.tmuxSessionName) {
8057
- let lastTmuxLayoutSnapshot = '';
8058
- let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
8059
- let lastTmuxMetadata = null;
8060
- let lastTmuxMetaWriteAt = 0;
8061
- const sendTmuxLayout = async () => {
8062
- if (ws.readyState !== ws.OPEN)
8063
- return;
8064
- try {
8065
- const layout = await getCachedTmuxLayout(session.tmuxSessionName);
8066
- // Update active program — try ps-based detection for generic commands
8067
- const activePane = getActivePaneFromLayout(layout);
8068
- let ap = null;
8069
- if (activePane) {
8070
- const resolved = await resolveTmuxPaneProgram(activePane);
8071
- if (resolved) {
8072
- ap = { command: resolved.command, source: resolved.source, rawArgs: resolved.rawArgs, updatedAt: Date.now() };
8073
- }
8074
- else {
8075
- ap = getActiveProgramFromTmuxLayout(layout);
8076
- }
8077
- }
8078
- else {
8079
- ap = getActiveProgramFromTmuxLayout(layout);
8080
- }
8081
- const apSnapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
8082
- if (apSnapshot !== lastActiveProgramSnapshot) {
8083
- lastActiveProgramSnapshot = apSnapshot;
8084
- session.activeProgram = ap;
8085
- persistActiveProgramBinding(sessionId, ap?.command);
8086
- // Agent status: react to AI tool start/exit
8087
- syncAgentIdentity(sessionId, session);
8088
- console.log(`[active-program][ws] session=${sessionId} cmd=${ap?.command ?? null} source=${ap?.source ?? null}`);
8089
- ws.send(JSON.stringify({
8090
- type: 'active-program',
8091
- activeProgram: ap?.command ?? null,
8092
- activeProgramRaw: ap?.rawArgs ?? null,
8093
- activeProgramSource: ap?.source ?? null,
8094
- }));
8095
- }
8096
- // Update cwd from active pane
8097
- const newCwd = getCwdFromTmuxLayout(layout);
8098
- if (newCwd && newCwd !== session.cwd) {
8099
- session.cwd = newCwd;
8100
- console.log(`[tmux-cwd][ws] session=${sessionId} cwd=${newCwd}`);
8101
- if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
8102
- schedulePersistGlobalState();
8103
- }
8104
- ws.send(JSON.stringify({ type: 'cwd', cwd: newCwd }));
8105
- refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
8106
- }
8107
- // tmux 消费了 inner shell 发的 OSC 2(存入 pane_title)和 OSC 133,
8108
- // 不透传到外层 PTY。因此从 tmux layout 提取 active pane 的 title
8109
- // 和 command 来推导 shell-title / prompt-state。
8110
- if (activePane) {
8111
- const paneTitle = activePane.title || '';
8112
- if (paneTitle && paneTitle !== session.lastOscTitle) {
8113
- session.lastOscTitle = paneTitle;
8114
- ws.send(JSON.stringify({ type: 'shell-title', title: paneTitle }));
8115
- }
8116
- // prompt-state: command 是 shell 名 → idle;否则 → running
8117
- const paneCmd = activePane.command || '';
8118
- const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
8119
- if (inferredState !== session.lastPromptState) {
8120
- const wasRunning = session.lastPromptState === 'running';
8121
- session.lastPromptState = inferredState;
8122
- ws.send(JSON.stringify({ type: 'prompt-state', state: inferredState }));
8123
- if (wasRunning && inferredState === 'idle') {
8124
- refreshGitStatus(sessionId, session);
8125
- }
8126
- }
8127
- }
8128
- // Mirror dynamic metadata onto tmux user options.
8129
- const meta = syncDynamicTmuxMetadata({
8130
- tmuxSessionName: session.tmuxSessionName,
8131
- program: session.activeProgram?.command ?? null,
8132
- cwd: session.cwd ?? null,
8133
- previousMetadata: lastTmuxMetadata,
8134
- lastActiveWriteAt: lastTmuxMetaWriteAt,
8135
- });
8136
- lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
8137
- lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
8138
- const snapshot = JSON.stringify(layout);
8139
- if (snapshot !== lastTmuxLayoutSnapshot) {
8140
- lastTmuxLayoutSnapshot = snapshot;
8141
- ws.send(JSON.stringify({ type: 'tmux-layout', layout }));
8142
- }
8143
- }
8144
- catch { /* ignore polling errors */ }
8145
- };
8146
- sendTmuxLayout();
8147
- tmuxInterval = setInterval(sendTmuxLayout, TMUX_POLL_INTERVAL);
8148
- }
8149
- // Active program polling (shell mode)
8150
- if (session.mode === 'shell') {
8151
- let lastApSnapshot = JSON.stringify(session.activeProgram ? { command: session.activeProgram.command, source: session.activeProgram.source } : null);
8152
- const pollActiveProgram = async () => {
8153
- if (ws.readyState !== ws.OPEN)
8154
- return;
8155
- try {
8156
- const ap = await detectShellActiveProgram(session);
8157
- const snapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
8158
- if (snapshot !== lastApSnapshot) {
8159
- lastApSnapshot = snapshot;
8160
- session.activeProgram = ap;
8161
- persistActiveProgramBinding(sessionId, ap?.command);
8162
- // Agent status: react to AI tool start/exit
8163
- syncAgentIdentity(sessionId, session);
8164
- ws.send(JSON.stringify({
8165
- type: 'active-program',
8166
- activeProgram: ap?.command ?? null,
8167
- activeProgramRaw: ap?.rawArgs ?? null,
8168
- activeProgramSource: ap?.source ?? null,
8169
- }));
8170
- }
8171
- }
8172
- catch { /* ignore */ }
8173
- };
8174
- activeProgramInterval = setInterval(pollActiveProgram, ACTIVE_PROGRAM_POLL_INTERVAL);
8175
- }
8248
+ const stopSampling = subscribeTerminalSampling(sessionId, session, event => {
8249
+ if (ws.readyState === ws.OPEN)
8250
+ ws.send(JSON.stringify(event));
8251
+ });
8176
8252
  // Handle client → server messages
8177
8253
  ws.on('message', async (raw) => {
8178
8254
  const messageReceivedAt = performance.now();
@@ -8306,7 +8382,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8306
8382
  if (!options.flowControl || typeof msg.active !== 'boolean')
8307
8383
  break;
8308
8384
  outputWanted = msg.active;
8309
- const resumed = delivery.setActive(outputWanted && !clientPaused);
8385
+ const resumed = delivery.setActive(outputWanted);
8310
8386
  if (!delivery.active)
8311
8387
  attachment?.close();
8312
8388
  if (resumed && delivery.active) {
@@ -8327,12 +8403,9 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8327
8403
  if (typeof msg.paused === 'boolean') {
8328
8404
  const reason = typeof msg.reason === 'string' ? msg.reason : 'client-flow-control';
8329
8405
  if (options.flowControl) {
8330
- const wasPaused = clientPaused;
8331
8406
  clientPaused = msg.paused;
8332
- delivery.setActive(outputWanted && !clientPaused);
8333
- if (!delivery.active)
8334
- attachment?.close();
8335
- if (wasPaused && delivery.active)
8407
+ delivery.setPaused(clientPaused);
8408
+ if (!clientPaused && delivery.needsReplay && delivery.active)
8336
8409
  void sendReplay(0);
8337
8410
  }
8338
8411
  else
@@ -8373,10 +8446,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8373
8446
  initialScreen?.cancel();
8374
8447
  initialScreen = null;
8375
8448
  attachment?.close();
8376
- if (tmuxInterval)
8377
- clearInterval(tmuxInterval);
8378
- if (activeProgramInterval)
8379
- clearInterval(activeProgramInterval);
8449
+ stopSampling();
8380
8450
  const clients = wsClients.get(sessionId);
8381
8451
  if (clients) {
8382
8452
  clients.delete(clientId);
@@ -8484,7 +8554,9 @@ export function handleControlWebSocket(ws, clientId) {
8484
8554
  // session/tmux server is no longer alive; broadcast the cleaned list.
8485
8555
  const CLIENT_STATE_RECONCILE_INTERVAL_MS = 30_000;
8486
8556
  const reconcileTimer = setInterval(() => {
8487
- void reconcileClientState();
8557
+ void reconcileClientState().catch(error => {
8558
+ console.warn('[reconcile] failed:', getErrorMessage(error));
8559
+ });
8488
8560
  }, CLIENT_STATE_RECONCILE_INTERVAL_MS);
8489
8561
  // Don't keep the event loop alive for housekeeping.
8490
8562
  reconcileTimer.unref?.();
@@ -8496,17 +8568,30 @@ const automationTimer = setInterval(() => {
8496
8568
  }
8497
8569
  }, 30_000);
8498
8570
  automationTimer.unref?.();
8499
- async function reconcileClientState() {
8571
+ let reconcileInFlight = null;
8572
+ function reconcileClientState() {
8573
+ if (reconcileInFlight)
8574
+ return reconcileInFlight;
8575
+ reconcileInFlight = reconcileClientStateOnce().finally(() => { reconcileInFlight = null; });
8576
+ return reconcileInFlight;
8577
+ }
8578
+ async function reconcileClientStateOnce() {
8500
8579
  if (globalSessionState.sessions.length === 0)
8501
8580
  return;
8581
+ const observedEntries = new Map(globalSessionState.sessions.map(entry => [entry.sessionId, entry]));
8502
8582
  // Refresh first so a shared tmux crash is recorded before orphan cleanup
8503
8583
  // can discard the metadata needed to recover Agent conversations.
8504
- await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
8584
+ const inventory = await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
8505
8585
  console.warn('[tmux-recovery] reconcile observation failed:', getErrorMessage(error));
8586
+ return null;
8506
8587
  });
8588
+ const liveTmuxNames = inventory ? inventoryTmuxLiveness.get(inventory) : undefined;
8507
8589
  const toRemove = [];
8508
8590
  const toDetach = [];
8509
8591
  for (const entry of globalSessionState.sessions) {
8592
+ // Do not delete a session created/rebound while the inventory was loading.
8593
+ if (observedEntries.get(entry.sessionId) !== entry)
8594
+ continue;
8510
8595
  if (entry.mode === 'shell') {
8511
8596
  // Shell wrapper: backendSessionId must map to a live terminal session.
8512
8597
  // A resumable Agent record survives with a detached backend so the tab
@@ -8526,21 +8611,15 @@ async function reconcileClientState() {
8526
8611
  // own this session name. We deliberately do NOT require a live
8527
8612
  // terminal wrapper here — detaching from the wrapper doesn't kill
8528
8613
  // the tmux session.
8529
- try {
8530
- const alive = await tmuxSessionExists(entry.tmuxSessionName);
8531
- if (!alive) {
8532
- if (entry.cwd && entry.agentResume?.sessionId) {
8533
- if (entry.backendSessionId)
8534
- toDetach.push(entry.sessionId);
8535
- }
8536
- else {
8537
- toRemove.push(entry.sessionId);
8538
- }
8614
+ // A failed list is unknown, never proof that every session disappeared.
8615
+ if (liveTmuxNames && !liveTmuxNames.has(entry.tmuxSessionName)) {
8616
+ if (entry.cwd && entry.agentResume?.sessionId) {
8617
+ if (entry.backendSessionId)
8618
+ toDetach.push(entry.sessionId);
8619
+ }
8620
+ else {
8621
+ toRemove.push(entry.sessionId);
8539
8622
  }
8540
- }
8541
- catch {
8542
- // tmux itself is down — leave the entry alone; the next tick (or
8543
- // boot-time prune) will clean it up once tmux is back.
8544
8623
  }
8545
8624
  }
8546
8625
  }