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.
- package/.desktop-dist/main.js +35 -2
- package/README.en.md +257 -0
- package/README.md +284 -133
- package/dist/client/assets/App-BbqtNY2x.js +177 -0
- package/dist/client/assets/App-BbqtNY2x.js.br +0 -0
- package/dist/client/assets/App-BbqtNY2x.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-Dv5XRtRh.js → ChangeWalkthroughPanel-CvQJIp_C.js} +1 -1
- package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js +1 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-C01GXOEq.js +2 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js +4 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js.br +0 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-mq69dKyr.js → DagPlayground-CdE3tQsl.js} +1 -1
- package/dist/client/assets/DagPlayground-CdE3tQsl.js.br +0 -0
- package/dist/client/assets/DagPlayground-CdE3tQsl.js.gz +0 -0
- package/dist/client/assets/{DiffLab-BqdHYGi5.js → DiffLab-BLGNnazd.js} +1 -1
- package/dist/client/assets/DiffLab-BLGNnazd.js.br +0 -0
- package/dist/client/assets/DiffLab-BLGNnazd.js.gz +0 -0
- package/dist/client/assets/{DiffReview-BvzGxpuc.js → DiffReview-CYw3ASI6.js} +1 -1
- package/dist/client/assets/DiffReview-CYw3ASI6.js.br +0 -0
- package/dist/client/assets/DiffReview-CYw3ASI6.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-DrfddB7x.js → DiffReviewLab-CySIuWq_.js} +1 -1
- package/dist/client/assets/DiffReviewLab-CySIuWq_.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CySIuWq_.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-3bJV_A5s.js → DiffViewer-WsYvN75z.js} +13 -13
- package/dist/client/assets/DiffViewer-WsYvN75z.js.br +0 -0
- package/dist/client/assets/DiffViewer-WsYvN75z.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-CX7VtoJB.js → ModelPreview-Dnlx9ByX.js} +1 -1
- package/dist/client/assets/ModelPreview-Dnlx9ByX.js.br +0 -0
- package/dist/client/assets/ModelPreview-Dnlx9ByX.js.gz +0 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js +1678 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js.br +0 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-PJwhLTPH.js → abnfDiagram-VCTEODGH-CK1mGFRQ.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.gz +0 -0
- package/dist/client/assets/app-DfACMixk.js +35 -0
- package/dist/client/assets/app-DfACMixk.js.br +0 -0
- package/dist/client/assets/app-DfACMixk.js.gz +0 -0
- package/dist/client/assets/{arc-l3FXjCEl.js → arc-yZE1gqDl.js} +1 -1
- package/dist/client/assets/arc-yZE1gqDl.js.br +0 -0
- package/dist/client/assets/arc-yZE1gqDl.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-DdNbKi-4.js → architectureDiagram-5GKGNRK7-U5XSC7cY.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-Cu63oaxR.js → blockDiagram-I7D4REHJ-pVJnntSG.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DQiV_yk6.js → c4Diagram-7LVT6UL2-Bwz0AJJV.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.gz +0 -0
- package/dist/client/assets/channel-qqjDJtcZ.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-COKJ79Ld.js → chunk-2Q5K7J3B-CXJ1kP4X.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-BfV-m7oa.js → chunk-5VM5RSS4-Dpdum_2z.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-sLDCEnU8.js → chunk-F27PBJKO-BCG5Zi_-.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-DYu5qS7g.js → chunk-IMKFNOWR-BM-GcNcv.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-DixkILcl.js → chunk-JWPE2WC7-BWiXpQT9.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-CfW5Vm_-.js → chunk-POPQ4Y6H-vQSslMNv.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-nJnbUFzs.js → chunk-SVP7TREG-Aqfz6nPA.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-BwyiSQgx.js → chunk-TICWLB2K-DDjekMd0.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-TCk4c2XN.js → chunk-XXDRQBXY-BegIBcFg.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BDuBrfNz.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BDuBrfNz.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-rroo-EOb.js → cose-bilkent-JH36ORCC-B2izFsBY.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js → cynefin-OW5HDTMX-C7HA99Zc.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.br +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js.gz → cynefin-OW5HDTMX-C7HA99Zc.js.gz} +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-hBwpU_bW.js → cynefinDiagram-5FMLGOSQ-vNIzcCYl.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-B321-hdp.js → dagre-GXQ25YYZ-Bv9WHNT9.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-bY87_ptA.js → diagram-S7CK7UJ4-DFg5UNic.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN--cez0H_o.js → diagram-UQ7AKVKN-BFQsCAs7.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-BupH_VRK.js → diagram-VSXAHHWV-C4R8TKEv.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-DewqaxvF.js → diagram-VX7I27RA-CL75HIf0.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-wDumcTuR.js → diagram-Z3DM3KII-CfJampuO.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-C0OY91We.js → ebnfDiagram-PWID7BFC-Ct53jhNd.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-Cw9aNqvJ.js → erDiagram-RLTQ6QDP-BgL2xOOa.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js → flowDiagram-HODETNUW-Cp4Nu5jY.js} +1 -1
- package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js.br → flowDiagram-HODETNUW-Cp4Nu5jY.js.br} +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-DovhJIz5.js → ganttDiagram-EL5Y4UJY-DI0rdyPX.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-wjVok2Lg.js → gestureArbiter-Belsq4xq.js} +1 -1
- package/dist/client/assets/gestureArbiter-Belsq4xq.js.br +0 -0
- package/dist/client/assets/gestureArbiter-Belsq4xq.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-BmrbbyW1.js → gitGraphDiagram-WWUBYQGX-C0H4loQP.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.gz +0 -0
- package/dist/client/assets/index-CYmI1EsU.css +1 -0
- package/dist/client/assets/index-CYmI1EsU.css.br +0 -0
- package/dist/client/assets/index-CYmI1EsU.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-CQEu4Jwy.js → infoDiagram-27XIBGKW-BN4RpQTL.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BPqibqsm.js → ishikawaDiagram-5VMMS53U-BZQ5qZB_.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-DIkhubnf.js → journeyDiagram-3NMN7TZE-Q8bx0_N4.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-CBT0DIUR.js → kanban-definition-UXKFOSKX-D7JhGtpO.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.gz +0 -0
- package/dist/client/assets/{layout-C1CMlOIU.js → layout-DERUg0xU.js} +1 -1
- package/dist/client/assets/layout-DERUg0xU.js.br +0 -0
- package/dist/client/assets/layout-DERUg0xU.js.gz +0 -0
- package/dist/client/assets/{linear-C4gyqor0.js → linear-lNTe4x00.js} +1 -1
- package/dist/client/assets/linear-lNTe4x00.js.br +0 -0
- package/dist/client/assets/linear-lNTe4x00.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-CMfa4ncd.js → mermaid.core-WHCwLQDQ.js} +5 -5
- package/dist/client/assets/mermaid.core-WHCwLQDQ.js.br +0 -0
- package/dist/client/assets/mermaid.core-WHCwLQDQ.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-C-JmgKOI.js → mindmap-definition-YA3MSWOX-BouapA4v.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-BmUG2IwA.js → passwordBootstrap-E0piP814.js} +1 -1
- package/dist/client/assets/passwordBootstrap-E0piP814.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-E0piP814.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-IY-AEKQa.js → pegDiagram-XKGWAZYB-Curpo5Rq.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-9Qq43qED.js → pieDiagram-E7YTZNPT-BLQCHaOG.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BjQncx5i.js → quadrantDiagram-AXDQQJYC-PViFqGao.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-Cr1rL0wZ.js → railroadDiagram-O6MQD6OU-BtHo1LpT.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.gz +0 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js +1 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js.br +0 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-ConC5-ym.js → referenceLongPress-BZr7Lqzh.js} +1 -1
- package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.br +0 -0
- package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-DU2q_iZg.js → requirementDiagram-BXWQKSXE-BUm81HUU.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.gz +0 -0
- package/dist/client/assets/rfb-CxsXsMUX.js +5 -0
- package/dist/client/assets/rfb-CxsXsMUX.js.br +0 -0
- package/dist/client/assets/rfb-CxsXsMUX.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BYlx9kXC.js → sankeyDiagram-P5KCCOFB-B-_-aZ_y.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D_uAKme-.js → sequenceDiagram-WJ2MYXX4-DT6LDIfx.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-H_uqscf4.js → sizeCapture-INFHLROL-CTYerd9j.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.gz +0 -0
- package/dist/client/assets/{sparkles-D2z4Ue6G.js → sparkles-CgaUBIwH.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-BvsQjIIW.js → stateDiagram-D77RDMKH-CQbb5x2_.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CJAVXcx1.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-LmUJag-p.js → swimlanes-42K2YHIH-BHZ4X2Fd.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-VL2nyI27.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-DZ5n0Pim.js → timeline-definition-24CTP7MA-BrjH6R6w.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-Ct6iIBmv.js → triangle-alert-C9IHD_eF.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js → vennDiagram-4TSXK5OY-C9f5jYgk.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js.br → vennDiagram-4TSXK5OY-C9f5jYgk.js.br} +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.gz +0 -0
- package/dist/client/assets/{videoCompression-VZIoFnyJ.js → videoCompression-DJaU8exd.js} +1 -1
- package/dist/client/assets/videoCompression-DJaU8exd.js.br +0 -0
- package/dist/client/assets/videoCompression-DJaU8exd.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-7hzU176j.js → wardleyDiagram-VM6X3IG4-6ljFvtiC.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Bx4P96Bb.js → xychartDiagram-S5SC5T6Z-BaR2hm3e.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.gz +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +45 -4
- package/dist/server/agent/collaborationDeliveryWorker.js +1 -1
- package/dist/server/agent/collaborationGroupRoutes.js +0 -2
- package/dist/server/agent/collaborationPeerTransport.js +8 -2
- package/dist/server/agent/collaborationResultPresentation.js +14 -0
- package/dist/server/agent/collaborationRoutes.js +18 -2
- package/dist/server/agent/collaborationService.js +46 -2
- package/dist/server/agent/collaborationStore.js +12 -21
- package/dist/server/agent/collaborationTaskBundles.js +121 -0
- package/dist/server/agent/collaborationTaskCli.js +54 -0
- package/dist/server/agent/collaborationTaskRoutes.js +58 -0
- package/dist/server/agent/collaborationTaskService.js +355 -0
- package/dist/server/agent/collaborationTaskStore.js +835 -0
- package/dist/server/agent/collaborationTaskTypes.js +1 -0
- package/dist/server/agent/collaborationTaskWorkspace.js +165 -0
- package/dist/server/agent/collaborationTeam.js +16 -0
- package/dist/server/agent/executionArchives.js +52 -0
- package/dist/server/entry.js +17 -2
- package/dist/server/federation/runtime.js +30 -7
- package/dist/server/routes/computer.js +198 -0
- package/dist/server/routes/computerTarget.js +23 -0
- package/dist/server/routes/guacamoleProtocol.js +55 -0
- package/dist/server/routes/rdp.js +162 -0
- package/dist/server/routes/terminal.js +441 -362
- package/dist/server/shared/computerPreferences.js +29 -0
- package/dist/server/utils/computerCredentials.js +88 -0
- package/dist/server/utils/sharedSampling.js +93 -0
- package/dist/server/utils/terminalOutputDelivery.js +8 -1
- package/dist/server/utils/tmuxMetadataWriter.js +61 -0
- package/package.json +4 -1
- package/runtime-manifest.json +4 -4
- package/dist/client/assets/App-D-cerXsD.js +0 -158
- package/dist/client/assets/App-D-cerXsD.js.br +0 -0
- package/dist/client/assets/App-D-cerXsD.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.gz +0 -0
- package/dist/client/assets/DagPlayground-mq69dKyr.js.br +0 -0
- package/dist/client/assets/DagPlayground-mq69dKyr.js.gz +0 -0
- package/dist/client/assets/DiffLab-BqdHYGi5.js.br +0 -0
- package/dist/client/assets/DiffLab-BqdHYGi5.js.gz +0 -0
- package/dist/client/assets/DiffReview-BvzGxpuc.js.br +0 -0
- package/dist/client/assets/DiffReview-BvzGxpuc.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-DrfddB7x.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DrfddB7x.js.gz +0 -0
- package/dist/client/assets/DiffViewer-3bJV_A5s.js.br +0 -0
- package/dist/client/assets/DiffViewer-3bJV_A5s.js.gz +0 -0
- package/dist/client/assets/ModelPreview-CX7VtoJB.js.br +0 -0
- package/dist/client/assets/ModelPreview-CX7VtoJB.js.gz +0 -0
- package/dist/client/assets/RightSidebar-BK6vWNw3.js +0 -1678
- package/dist/client/assets/RightSidebar-BK6vWNw3.js.br +0 -0
- package/dist/client/assets/RightSidebar-BK6vWNw3.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.gz +0 -0
- package/dist/client/assets/app-C7pXMvSE.js +0 -35
- package/dist/client/assets/app-C7pXMvSE.js.br +0 -0
- package/dist/client/assets/app-C7pXMvSE.js.gz +0 -0
- package/dist/client/assets/arc-l3FXjCEl.js.br +0 -0
- package/dist/client/assets/arc-l3FXjCEl.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.gz +0 -0
- package/dist/client/assets/channel-CwC957gi.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-CmoJ7-rE.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CmoJ7-rE.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CkFeV85k.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BC7XUXeI.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-wjVok2Lg.js.br +0 -0
- package/dist/client/assets/gestureArbiter-wjVok2Lg.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.gz +0 -0
- package/dist/client/assets/index-BeD4iJ6L.css +0 -1
- package/dist/client/assets/index-BeD4iJ6L.css.br +0 -0
- package/dist/client/assets/index-BeD4iJ6L.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.gz +0 -0
- package/dist/client/assets/layout-C1CMlOIU.js.br +0 -0
- package/dist/client/assets/layout-C1CMlOIU.js.gz +0 -0
- package/dist/client/assets/linear-C4gyqor0.js.br +0 -0
- package/dist/client/assets/linear-C4gyqor0.js.gz +0 -0
- package/dist/client/assets/mermaid.core-CMfa4ncd.js.br +0 -0
- package/dist/client/assets/mermaid.core-CMfa4ncd.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-ConC5-ym.js.br +0 -0
- package/dist/client/assets/referenceLongPress-ConC5-ym.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-DNgoExtD.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BRuArMR5.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BkTA_Ciz.js.gz +0 -0
- package/dist/client/assets/videoCompression-VZIoFnyJ.js.br +0 -0
- package/dist/client/assets/videoCompression-VZIoFnyJ.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Bx4P96Bb.js.br +0 -0
- 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,
|
|
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
|
|
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:
|
|
1765
|
-
name:
|
|
1766
|
-
sessionIds: [...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3612
|
-
|
|
3613
|
-
.
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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
|
-
//
|
|
3753
|
-
//
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
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
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8530
|
-
|
|
8531
|
-
if (
|
|
8532
|
-
if (entry.
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
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
|
}
|