termdock 1.4.296 → 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-DXprT-7m.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-DZcUunT5.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-Brv4DCTb.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-BuLu1qCi.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-DSGamuAD.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-BUCYxg27.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-Bnlo-oVE.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-K6Ppkrbt.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-Co2OQqtH.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-BQnl7tVF.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-Bm5D5-qq.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-B9oPpey8.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-TMaWHH0U.js → chunk-2Q5K7J3B-CXJ1kP4X.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CH7XpknV.js → chunk-5VM5RSS4-Dpdum_2z.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-CJ1rotBe.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-UYKZ5uOG.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-BYL8aXjQ.js → chunk-JWPE2WC7-BWiXpQT9.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-C3E8yfU2.js → chunk-POPQ4Y6H-vQSslMNv.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-DCTDYa1E.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-wHiaOsBh.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-D_d479hK.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-DnhlPBLr.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-BsjzNqJZ.js → cynefin-OW5HDTMX-C7HA99Zc.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-sVUb0zY5.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-YErk8S-c.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-BaHbV5ho.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-BocCYx3V.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-ZMTYwWOQ.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-xmk_EY1h.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-DKDY_cG6.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-C_0z-Jx9.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-DHUaG7mt.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-BMNsdMBD.js → flowDiagram-HODETNUW-Cp4Nu5jY.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-BH1AiKuF.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-DvupkPra.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-DAgV0MV-.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-C7iSg0cB.js → infoDiagram-27XIBGKW-BN4RpQTL.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BpZwsQvw.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-DXEeFLvw.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-CV9ivL5u.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-DsKKsJKX.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-Dl9MDNV3.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-CxZFe3as.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-C3ormYdL.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-a8z6jJNY.js → passwordBootstrap-E0piP814.js} +2 -2
- 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-CG0wNylH.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-D2-ydqyX.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-CK3fRC18.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-V4EFlydu.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-BjmYd1vB.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-Di1_tyAX.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-D-qpWNza.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-DmIxafrB.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-4MTh3tTt.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-Dk93lHg0.js → sparkles-CgaUBIwH.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-CgYgneIv.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-OWA9HFVf.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-BAzx5DyO.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-CUIBOwqW.js → triangle-alert-C9IHD_eF.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-CBZ-PhTQ.js → vennDiagram-4TSXK5OY-C9f5jYgk.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.gz +0 -0
- package/dist/client/assets/{videoCompression-BCnqCkRK.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-DbWzC6l5.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-BmApnTWE.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 +3 -3
- package/dist/server/agent/collaborationGroupRoutes.js +0 -2
- package/dist/server/agent/collaborationResultPresentation.js +14 -0
- package/dist/server/agent/collaborationService.js +3 -1
- package/dist/server/agent/collaborationStore.js +12 -21
- package/dist/server/agent/collaborationTaskBundles.js +2 -2
- package/dist/server/agent/collaborationTaskCli.js +4 -4
- package/dist/server/agent/collaborationTaskStore.js +87 -25
- package/dist/server/agent/collaborationTaskWorkspace.js +66 -1
- package/dist/server/agent/collaborationTeam.js +16 -0
- package/dist/server/agent/executionArchives.js +52 -0
- package/dist/server/entry.js +6 -1
- package/dist/server/federation/runtime.js +24 -6
- 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 +375 -360
- 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-BiLjlrk-.js +0 -171
- package/dist/client/assets/App-BiLjlrk-.js.br +0 -0
- package/dist/client/assets/App-BiLjlrk-.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DXprT-7m.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DXprT-7m.js.gz +0 -0
- package/dist/client/assets/DagPlayground-DZcUunT5.js.br +0 -0
- package/dist/client/assets/DagPlayground-DZcUunT5.js.gz +0 -0
- package/dist/client/assets/DiffLab-Brv4DCTb.js.br +0 -0
- package/dist/client/assets/DiffLab-Brv4DCTb.js.gz +0 -0
- package/dist/client/assets/DiffReview-BuLu1qCi.js.br +0 -0
- package/dist/client/assets/DiffReview-BuLu1qCi.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-DSGamuAD.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DSGamuAD.js.gz +0 -0
- package/dist/client/assets/DiffViewer-BUCYxg27.js.br +0 -0
- package/dist/client/assets/DiffViewer-BUCYxg27.js.gz +0 -0
- package/dist/client/assets/ModelPreview-Bnlo-oVE.js.br +0 -0
- package/dist/client/assets/ModelPreview-Bnlo-oVE.js.gz +0 -0
- package/dist/client/assets/RightSidebar-r_3Lk5Rp.js +0 -1678
- package/dist/client/assets/RightSidebar-r_3Lk5Rp.js.br +0 -0
- package/dist/client/assets/RightSidebar-r_3Lk5Rp.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-K6Ppkrbt.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-K6Ppkrbt.js.gz +0 -0
- package/dist/client/assets/app-CATnTUZF.js +0 -35
- package/dist/client/assets/app-CATnTUZF.js.br +0 -0
- package/dist/client/assets/app-CATnTUZF.js.gz +0 -0
- package/dist/client/assets/arc-Co2OQqtH.js.br +0 -0
- package/dist/client/assets/arc-Co2OQqtH.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-BQnl7tVF.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-BQnl7tVF.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Bm5D5-qq.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Bm5D5-qq.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-B9oPpey8.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-B9oPpey8.js.gz +0 -0
- package/dist/client/assets/channel-Czv2W4qA.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-CJ1rotBe.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-CJ1rotBe.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-UYKZ5uOG.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-UYKZ5uOG.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-DCTDYa1E.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-DCTDYa1E.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-wHiaOsBh.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-wHiaOsBh.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-BdvOOL9k.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BdvOOL9k.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-DnhlPBLr.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-DnhlPBLr.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BsjzNqJZ.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BsjzNqJZ.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-sVUb0zY5.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-sVUb0zY5.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-YErk8S-c.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-YErk8S-c.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BaHbV5ho.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BaHbV5ho.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BocCYx3V.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BocCYx3V.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-ZMTYwWOQ.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-ZMTYwWOQ.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-xmk_EY1h.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-xmk_EY1h.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-DKDY_cG6.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-DKDY_cG6.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C_0z-Jx9.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C_0z-Jx9.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DHUaG7mt.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DHUaG7mt.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BMNsdMBD.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BMNsdMBD.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BH1AiKuF.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BH1AiKuF.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-DvupkPra.js.br +0 -0
- package/dist/client/assets/gestureArbiter-DvupkPra.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DAgV0MV-.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DAgV0MV-.js.gz +0 -0
- package/dist/client/assets/index-Bi9A6QUP.css +0 -1
- package/dist/client/assets/index-Bi9A6QUP.css.br +0 -0
- package/dist/client/assets/index-Bi9A6QUP.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BpZwsQvw.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BpZwsQvw.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DXEeFLvw.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DXEeFLvw.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CV9ivL5u.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CV9ivL5u.js.gz +0 -0
- package/dist/client/assets/layout-DsKKsJKX.js.br +0 -0
- package/dist/client/assets/layout-DsKKsJKX.js.gz +0 -0
- package/dist/client/assets/linear-Dl9MDNV3.js.br +0 -0
- package/dist/client/assets/linear-Dl9MDNV3.js.gz +0 -0
- package/dist/client/assets/mermaid.core-CxZFe3as.js.br +0 -0
- package/dist/client/assets/mermaid.core-CxZFe3as.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C3ormYdL.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C3ormYdL.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-a8z6jJNY.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-a8z6jJNY.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-CG0wNylH.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-CG0wNylH.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D2-ydqyX.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D2-ydqyX.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-CK3fRC18.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-CK3fRC18.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-V4EFlydu.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-V4EFlydu.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-BjmYd1vB.js.br +0 -0
- package/dist/client/assets/referenceLongPress-BjmYd1vB.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-Di1_tyAX.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-Di1_tyAX.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-qpWNza.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-qpWNza.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DmIxafrB.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DmIxafrB.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-4MTh3tTt.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-4MTh3tTt.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CgYgneIv.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CgYgneIv.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CvPf0fxf.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-OWA9HFVf.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-OWA9HFVf.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CZ_OauyW.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-BAzx5DyO.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-BAzx5DyO.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CBZ-PhTQ.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CBZ-PhTQ.js.gz +0 -0
- package/dist/client/assets/videoCompression-BCnqCkRK.js.br +0 -0
- package/dist/client/assets/videoCompression-BCnqCkRK.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DbWzC6l5.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DbWzC6l5.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmApnTWE.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmApnTWE.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';
|
|
@@ -181,6 +186,8 @@ function generateFrontendSessionId() {
|
|
|
181
186
|
const GLOBAL_SESSION_STATE_FILE = `${TERMDOCK_DIR}/global-session-state.json`;
|
|
182
187
|
const CLIENT_STATES_FILE = `${TERMDOCK_DIR}/client-states.json`; // 保留用于迁移
|
|
183
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();
|
|
184
191
|
const TMUX_RECOVERY_STATE_FILE = `${TERMDOCK_DIR}/tmux-recovery-state.json`;
|
|
185
192
|
const tmuxLifecycle = new TmuxLifecycleCoordinator();
|
|
186
193
|
const intentionallyDeletingTmuxSessions = new Set();
|
|
@@ -194,6 +201,8 @@ let globalSessionStateReloadTimer = null;
|
|
|
194
201
|
// reconciliation, etc.). Replaces the 5-second poll on the front-end. ──
|
|
195
202
|
const controlClients = new Map();
|
|
196
203
|
let latestSessionInventory = null;
|
|
204
|
+
// Only authoritative observations may remove persisted tmux sessions.
|
|
205
|
+
const inventoryTmuxLiveness = new WeakMap();
|
|
197
206
|
let latestSessionInventoryAt = 0;
|
|
198
207
|
let sessionInventoryBuildPromise = null;
|
|
199
208
|
let broadcastInventorySeq = 0;
|
|
@@ -205,13 +214,12 @@ const inventoryOpenLocks = new Map();
|
|
|
205
214
|
const CONTROL_BROADCAST_COALESCE_MS = 50;
|
|
206
215
|
const SESSION_INVENTORY_CACHE_TTL_MS = 1500;
|
|
207
216
|
async function getSessionInventorySnapshot(options = {}) {
|
|
217
|
+
if (sessionInventoryBuildPromise)
|
|
218
|
+
return sessionInventoryBuildPromise;
|
|
208
219
|
const now = Date.now();
|
|
209
220
|
if (!options.refresh && latestSessionInventory && now - latestSessionInventoryAt < SESSION_INVENTORY_CACHE_TTL_MS) {
|
|
210
221
|
return latestSessionInventory;
|
|
211
222
|
}
|
|
212
|
-
if (!options.refresh && sessionInventoryBuildPromise) {
|
|
213
|
-
return sessionInventoryBuildPromise;
|
|
214
|
-
}
|
|
215
223
|
const promise = buildSessionInventory()
|
|
216
224
|
.then((inventory) => {
|
|
217
225
|
latestSessionInventory = inventory;
|
|
@@ -982,22 +990,6 @@ function buildRuntimeTmuxMetadata(input) {
|
|
|
982
990
|
});
|
|
983
991
|
return { program, cwd, label, rawArgs: input.rawArgs ?? null };
|
|
984
992
|
}
|
|
985
|
-
function maybeRepairTmuxOptions(sessionName, current, next) {
|
|
986
|
-
const currentSnapshot = {
|
|
987
|
-
program: current.program ?? null,
|
|
988
|
-
cwd: current.cwd ?? null,
|
|
989
|
-
label: current.label ?? '',
|
|
990
|
-
};
|
|
991
|
-
if (!tmuxMetadataChanged(currentSnapshot, next)) {
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
void setTmuxOptions(sessionName, {
|
|
995
|
-
'@termdock-label': next.label,
|
|
996
|
-
'@termdock-program': next.program ?? '',
|
|
997
|
-
'@termdock-cwd': next.cwd ?? '',
|
|
998
|
-
'@termdock-last-active-at': String(Date.now()),
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
993
|
function makeTerminalSessionPayload(backendSessionId, session, cols = 80, rows = 24) {
|
|
1002
994
|
return {
|
|
1003
995
|
sessionId: backendSessionId,
|
|
@@ -1719,7 +1711,7 @@ async function resolveCollaborationRoute(frontendSessionId) {
|
|
|
1719
1711
|
async function refreshCollaborationAgentIdentity(backendSessionId, session) {
|
|
1720
1712
|
let activeProgram = null;
|
|
1721
1713
|
if (session.mode === 'shell') {
|
|
1722
|
-
activeProgram = await
|
|
1714
|
+
activeProgram = await getCachedShellActiveProgram(backendSessionId, session);
|
|
1723
1715
|
}
|
|
1724
1716
|
else if (session.tmuxSessionName) {
|
|
1725
1717
|
const layout = await getCachedTmuxLayout(session.tmuxSessionName);
|
|
@@ -1736,7 +1728,7 @@ async function refreshCollaborationAgentIdentity(backendSessionId, session) {
|
|
|
1736
1728
|
function deliverCollaborationInboxWhenAgentReady(frontendSessionId) {
|
|
1737
1729
|
collaborationDeliveryWorker.wake(frontendSessionId);
|
|
1738
1730
|
}
|
|
1739
|
-
async function spawnCollaborationAgentSession(req, group, sourceSessionId, input) {
|
|
1731
|
+
async function spawnCollaborationAgentSession(req, group, sourceSessionId, input, bootstrapRole) {
|
|
1740
1732
|
const agentSlug = typeof input.agentSlug === 'string' ? input.agentSlug.trim().toLowerCase() : '';
|
|
1741
1733
|
const launchers = await listDetectedAgentLaunchers();
|
|
1742
1734
|
const launcher = launchers.find((candidate) => candidate.slug === agentSlug);
|
|
@@ -1768,11 +1760,26 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
1768
1760
|
const backend = terminalSessions.get(opened.terminalSession.sessionId);
|
|
1769
1761
|
if (!backend)
|
|
1770
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
|
+
}
|
|
1771
1776
|
const updatedGroup = collaborationStore.save({
|
|
1772
|
-
id:
|
|
1773
|
-
name:
|
|
1774
|
-
sessionIds: [...
|
|
1777
|
+
id: latestGroup.id,
|
|
1778
|
+
name: latestGroup.name,
|
|
1779
|
+
sessionIds: [...latestGroup.sessionIds, frontendSessionId],
|
|
1775
1780
|
});
|
|
1781
|
+
if (bootstrapRole)
|
|
1782
|
+
collaborationStore.setRole({ groupId: group.id, sessionId: frontendSessionId, role: bootstrapRole });
|
|
1776
1783
|
writeTerminalInput(backend, `${buildCollaborationSpawnCommand({ slug: launcher.slug, command: launcher.command })}\r`);
|
|
1777
1784
|
const requestedTask = typeof input.task === 'string' ? input.task.trim().slice(0, 20_000) : '';
|
|
1778
1785
|
collaborationStore.send({
|
|
@@ -2416,9 +2423,11 @@ async function observeTmuxServerGeneration(liveTmuxSessions) {
|
|
|
2416
2423
|
async function buildSessionInventory() {
|
|
2417
2424
|
const tmuxStatus = await getTmuxStatus();
|
|
2418
2425
|
let liveTmuxSessions = [];
|
|
2426
|
+
let authoritativeTmuxList = false;
|
|
2419
2427
|
if (tmuxStatus.available) {
|
|
2420
2428
|
try {
|
|
2421
2429
|
liveTmuxSessions = await listLiveTmuxInventorySessions();
|
|
2430
|
+
authoritativeTmuxList = true;
|
|
2422
2431
|
}
|
|
2423
2432
|
catch (error) {
|
|
2424
2433
|
const errorMessage = getErrorMessage(error);
|
|
@@ -2434,12 +2443,12 @@ async function buildSessionInventory() {
|
|
|
2434
2443
|
return tmux;
|
|
2435
2444
|
}
|
|
2436
2445
|
try {
|
|
2437
|
-
const metadata = await resolveLiveTmuxMetadata(tmux.name);
|
|
2446
|
+
const metadata = await resolveLiveTmuxMetadata(tmux.name, true);
|
|
2438
2447
|
if (!metadata) {
|
|
2439
2448
|
return tmux;
|
|
2440
2449
|
}
|
|
2441
2450
|
backfillAgentResumeFromTmuxProcess(tmux.name, metadata.rawArgs);
|
|
2442
|
-
|
|
2451
|
+
syncDynamicTmuxMetadata({ tmuxSessionName: tmux.name, program: metadata.program, cwd: metadata.cwd });
|
|
2443
2452
|
return {
|
|
2444
2453
|
...tmux,
|
|
2445
2454
|
program: metadata.program,
|
|
@@ -2585,13 +2594,19 @@ async function buildSessionInventory() {
|
|
|
2585
2594
|
lastActiveAt: tmux.lastActiveAt,
|
|
2586
2595
|
};
|
|
2587
2596
|
});
|
|
2588
|
-
|
|
2597
|
+
const inventory = {
|
|
2589
2598
|
clientSessions,
|
|
2590
2599
|
tmuxSessions,
|
|
2591
2600
|
tmuxStatus,
|
|
2592
2601
|
tmuxRecovery: getTmuxRecoveryView(),
|
|
2593
2602
|
updatedAt: Date.now(),
|
|
2594
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;
|
|
2595
2610
|
}
|
|
2596
2611
|
// ── end tmux user-option helpers ──
|
|
2597
2612
|
async function captureTmuxPane(sessionName) {
|
|
@@ -3603,6 +3618,8 @@ try {
|
|
|
3603
3618
|
catch { /* plugin loading must never block startup */ }
|
|
3604
3619
|
let processSnapshot = null;
|
|
3605
3620
|
let processSnapshotPromise = null;
|
|
3621
|
+
const ttyProcessSnapshots = new SharedSnapshotCache(PROCESS_SNAPSHOT_CACHE_TTL_MS);
|
|
3622
|
+
const shellProgramSnapshots = new SharedSnapshotCache(ACTIVE_PROGRAM_POLL_INTERVAL);
|
|
3606
3623
|
function parseProcessSnapshot(stdout) {
|
|
3607
3624
|
return stdout
|
|
3608
3625
|
.split('\n')
|
|
@@ -3670,26 +3687,28 @@ async function resolveTmuxPaneProgram(pane) {
|
|
|
3670
3687
|
const psArgs = pane.tty
|
|
3671
3688
|
? ['-t', pane.tty.replace(/^\/dev\//, ''), '-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args=']
|
|
3672
3689
|
: ['-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args='];
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
.
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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
|
+
});
|
|
3693
3712
|
}
|
|
3694
3713
|
const selected = selectTmuxForegroundProgram({
|
|
3695
3714
|
panePid: pane.pid,
|
|
@@ -3745,8 +3764,8 @@ function getCwdFromTmuxLayout(layout) {
|
|
|
3745
3764
|
const activePane = activeWindow?.panes.find((pane) => pane.id === layout.activePaneId);
|
|
3746
3765
|
return activePane?.currentPath || null;
|
|
3747
3766
|
}
|
|
3748
|
-
async function resolveLiveTmuxMetadata(tmuxSessionName) {
|
|
3749
|
-
const layout = await getTmuxLayout(tmuxSessionName);
|
|
3767
|
+
async function resolveLiveTmuxMetadata(tmuxSessionName, cached = false) {
|
|
3768
|
+
const layout = cached ? await getCachedTmuxLayout(tmuxSessionName) : await getTmuxLayout(tmuxSessionName);
|
|
3750
3769
|
const activePane = getActivePaneFromLayout(layout);
|
|
3751
3770
|
if (!activePane) {
|
|
3752
3771
|
return null;
|
|
@@ -3811,31 +3830,87 @@ function findFriendlyNameForTmuxSession(tmuxSessionName) {
|
|
|
3811
3830
|
}
|
|
3812
3831
|
return null;
|
|
3813
3832
|
}
|
|
3814
|
-
//
|
|
3815
|
-
//
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
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
|
+
});
|
|
3819
3838
|
function syncDynamicTmuxMetadata(input) {
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
// tools see the session as alive without flooding tmux every 500 ms.
|
|
3826
|
-
if (now - lastActiveWriteAt >= TERMDOCK_LAST_ACTIVE_REFRESH_MS) {
|
|
3827
|
-
void setTmuxOption(tmuxSessionName, '@termdock-last-active-at', String(now));
|
|
3828
|
-
return { ...metadata, lastActiveWriteAt: now };
|
|
3829
|
-
}
|
|
3830
|
-
return { ...metadata, lastActiveWriteAt };
|
|
3831
|
-
}
|
|
3832
|
-
void setTmuxOptions(tmuxSessionName, {
|
|
3833
|
-
'@termdock-label': metadata.label,
|
|
3834
|
-
'@termdock-program': metadata.program ?? '',
|
|
3835
|
-
'@termdock-cwd': metadata.cwd ?? '',
|
|
3836
|
-
'@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)}`);
|
|
3837
3844
|
});
|
|
3838
|
-
|
|
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;
|
|
3839
3914
|
}
|
|
3840
3915
|
async function detectShellActiveProgram(session) {
|
|
3841
3916
|
const pid = getPtyProcessPid(session.ptyProcess);
|
|
@@ -4420,6 +4495,9 @@ function cleanupSession(sessionId, options) {
|
|
|
4420
4495
|
if (!session) {
|
|
4421
4496
|
return;
|
|
4422
4497
|
}
|
|
4498
|
+
const sampler = terminalSamplers.get(sessionId);
|
|
4499
|
+
sampler?.sampler.stop();
|
|
4500
|
+
terminalSamplers.delete(sessionId);
|
|
4423
4501
|
session.dataDisposable?.dispose();
|
|
4424
4502
|
session.exitDisposable?.dispose();
|
|
4425
4503
|
cancelLongRunningAutoTitle(session);
|
|
@@ -5430,6 +5508,7 @@ router.delete('/tmux/recovery', async (_req, res) => {
|
|
|
5430
5508
|
async function destroyTmuxSessionSafely(rawName) {
|
|
5431
5509
|
return tmuxLifecycle.run(`destroy:${rawName}`, async () => {
|
|
5432
5510
|
intentionallyDeletingTmuxSessions.add(rawName);
|
|
5511
|
+
dynamicTmuxMetadata.forget(rawName);
|
|
5433
5512
|
try {
|
|
5434
5513
|
const affectedSessionIds = [];
|
|
5435
5514
|
for (const [sessionId, session] of terminalSessions.entries()) {
|
|
@@ -5712,6 +5791,53 @@ function resolveGroupParam(id) {
|
|
|
5712
5791
|
return { group: resolved.status === 'ok' ? collaborationStore.getGroup(resolved.id) : null, ambiguous: 0 };
|
|
5713
5792
|
}
|
|
5714
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
|
+
});
|
|
5715
5841
|
router.post('/operations/collaboration-groups/:groupId/spawn', async (req, res) => {
|
|
5716
5842
|
const { group, ambiguous } = resolveGroupParam(req.params.groupId);
|
|
5717
5843
|
if (ambiguous)
|
|
@@ -5824,8 +5950,8 @@ router.post('/operations/orchestration/members', (req, res) => {
|
|
|
5824
5950
|
const nextSessionIds = action === 'add'
|
|
5825
5951
|
? Array.from(new Set([...group.sessionIds, targetSessionId]))
|
|
5826
5952
|
: group.sessionIds.filter((sessionId) => sessionId !== targetSessionId);
|
|
5827
|
-
if (nextSessionIds.length < 2)
|
|
5828
|
-
return res.status(400).json({ error: '
|
|
5953
|
+
if (group.federated && nextSessionIds.length < 2)
|
|
5954
|
+
return res.status(400).json({ error: '跨服务组仍需两个成员;移出操作未保存' });
|
|
5829
5955
|
const updated = collaborationStore.save({ id: group.id, name: group.name, sessionIds: nextSessionIds });
|
|
5830
5956
|
res.json({ ok: true, group: updated });
|
|
5831
5957
|
});
|
|
@@ -5895,7 +6021,7 @@ router.post('/operations/orchestration/cleanup', async (req, res) => {
|
|
|
5895
6021
|
const remaining = group.sessionIds.filter((member) => !requestedSet.has(member)).length;
|
|
5896
6022
|
groupEffects.push({
|
|
5897
6023
|
id: group.id, name: group.name ?? null, sizeBefore: group.sessionIds.length,
|
|
5898
|
-
sizeAfter:
|
|
6024
|
+
sizeAfter: group.federated ? group.sessionIds.length : remaining, dissolves: false,
|
|
5899
6025
|
includesCaller: group.sessionIds.includes(sourceSessionId),
|
|
5900
6026
|
});
|
|
5901
6027
|
}
|
|
@@ -6161,6 +6287,140 @@ router.post('/operations/session-search/prepare', async (req, res) => {
|
|
|
6161
6287
|
res.status(400).json({ error: `无法恢复会话:${getErrorMessage(error)}` });
|
|
6162
6288
|
}
|
|
6163
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
|
+
}));
|
|
6164
6424
|
router.get('/agent-resume-history', (_req, res) => {
|
|
6165
6425
|
const entries = agentResumeHistory.list().flatMap((entry) => {
|
|
6166
6426
|
const agent = agentBySlug(entry.agent.slug);
|
|
@@ -7208,7 +7468,7 @@ router.get('/:sessionId/stream', async (req, res) => {
|
|
|
7208
7468
|
// 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
|
|
7209
7469
|
try {
|
|
7210
7470
|
if (session.mode === 'shell') {
|
|
7211
|
-
const ap = await
|
|
7471
|
+
const ap = await getCachedShellActiveProgram(sessionId, session);
|
|
7212
7472
|
if (ap)
|
|
7213
7473
|
session.activeProgram = ap;
|
|
7214
7474
|
}
|
|
@@ -7261,131 +7521,7 @@ router.get('/:sessionId/stream', async (req, res) => {
|
|
|
7261
7521
|
if (session.gitStatus) {
|
|
7262
7522
|
writeSse(res, { type: 'git-status', gitStatus: session.gitStatus });
|
|
7263
7523
|
}
|
|
7264
|
-
|
|
7265
|
-
let activeProgramInterval = null;
|
|
7266
|
-
let lastTmuxLayoutSnapshot = '';
|
|
7267
|
-
let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
|
|
7268
|
-
let lastTmuxMetadata = null;
|
|
7269
|
-
let lastTmuxMetaWriteAt = 0;
|
|
7270
|
-
const maybeWriteActiveProgram = (activeProgram) => {
|
|
7271
|
-
const snapshot = JSON.stringify(activeProgram ? { command: activeProgram.command, source: activeProgram.source } : null);
|
|
7272
|
-
if (snapshot === lastActiveProgramSnapshot) {
|
|
7273
|
-
return;
|
|
7274
|
-
}
|
|
7275
|
-
lastActiveProgramSnapshot = snapshot;
|
|
7276
|
-
session.activeProgram = activeProgram;
|
|
7277
|
-
persistActiveProgramBinding(sessionId, activeProgram?.command);
|
|
7278
|
-
// Agent status: react to AI tool start/exit
|
|
7279
|
-
syncAgentIdentity(sessionId, session);
|
|
7280
|
-
console.log(`[active-program][shell-sse] session=${sessionId} client=${clientId} cmd=${activeProgram?.command ?? null} source=${activeProgram?.source ?? null}`);
|
|
7281
|
-
writeSse(res, {
|
|
7282
|
-
type: 'active-program',
|
|
7283
|
-
activeProgram: activeProgram?.command ?? null,
|
|
7284
|
-
activeProgramRaw: activeProgram?.rawArgs ?? null,
|
|
7285
|
-
activeProgramSource: activeProgram?.source ?? null,
|
|
7286
|
-
});
|
|
7287
|
-
};
|
|
7288
|
-
const sendTmuxLayout = async () => {
|
|
7289
|
-
if (session.mode !== 'tmux' || !session.tmuxSessionName) {
|
|
7290
|
-
return;
|
|
7291
|
-
}
|
|
7292
|
-
try {
|
|
7293
|
-
const layout = await getCachedTmuxLayout(session.tmuxSessionName);
|
|
7294
|
-
// Resolve the active program — try ps-based detection for generic commands
|
|
7295
|
-
const activePane = getActivePaneFromLayout(layout);
|
|
7296
|
-
if (activePane) {
|
|
7297
|
-
const resolved = await resolveTmuxPaneProgram(activePane);
|
|
7298
|
-
if (resolved) {
|
|
7299
|
-
maybeWriteActiveProgram({
|
|
7300
|
-
command: resolved.command,
|
|
7301
|
-
source: resolved.source,
|
|
7302
|
-
rawArgs: resolved.rawArgs,
|
|
7303
|
-
updatedAt: Date.now(),
|
|
7304
|
-
});
|
|
7305
|
-
}
|
|
7306
|
-
else {
|
|
7307
|
-
maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
|
|
7308
|
-
}
|
|
7309
|
-
}
|
|
7310
|
-
else {
|
|
7311
|
-
maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
|
|
7312
|
-
}
|
|
7313
|
-
const newCwd = getCwdFromTmuxLayout(layout);
|
|
7314
|
-
if (newCwd && newCwd !== session.cwd) {
|
|
7315
|
-
session.cwd = newCwd;
|
|
7316
|
-
console.log(`[tmux-cwd][sse] session=${sessionId} cwd=${newCwd}`);
|
|
7317
|
-
if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
|
|
7318
|
-
schedulePersistGlobalState();
|
|
7319
|
-
}
|
|
7320
|
-
writeSse(res, { type: 'cwd', cwd: newCwd });
|
|
7321
|
-
refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
|
|
7322
|
-
}
|
|
7323
|
-
// tmux 消费了 inner shell 发的 OSC 2/133,不透传到外层 PTY。
|
|
7324
|
-
// 从 tmux layout 提取 active pane 的 title 和 command 来推导。
|
|
7325
|
-
if (activePane) {
|
|
7326
|
-
const paneTitle = activePane.title || '';
|
|
7327
|
-
if (paneTitle && paneTitle !== session.lastOscTitle) {
|
|
7328
|
-
session.lastOscTitle = paneTitle;
|
|
7329
|
-
writeSse(res, { type: 'shell-title', title: paneTitle });
|
|
7330
|
-
}
|
|
7331
|
-
const paneCmd = activePane.command || '';
|
|
7332
|
-
const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
|
|
7333
|
-
if (inferredState !== session.lastPromptState) {
|
|
7334
|
-
const wasRunning = session.lastPromptState === 'running';
|
|
7335
|
-
session.lastPromptState = inferredState;
|
|
7336
|
-
writeSse(res, { type: 'prompt-state', state: inferredState });
|
|
7337
|
-
if (wasRunning && inferredState === 'idle') {
|
|
7338
|
-
refreshGitStatus(sessionId, session);
|
|
7339
|
-
}
|
|
7340
|
-
}
|
|
7341
|
-
}
|
|
7342
|
-
// Mirror dynamic metadata onto tmux user options (cheap when nothing
|
|
7343
|
-
// changed thanks to the full metadata snapshot cache).
|
|
7344
|
-
const meta = syncDynamicTmuxMetadata({
|
|
7345
|
-
tmuxSessionName: session.tmuxSessionName,
|
|
7346
|
-
program: session.activeProgram?.command ?? null,
|
|
7347
|
-
cwd: session.cwd ?? null,
|
|
7348
|
-
previousMetadata: lastTmuxMetadata,
|
|
7349
|
-
lastActiveWriteAt: lastTmuxMetaWriteAt,
|
|
7350
|
-
});
|
|
7351
|
-
lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
|
|
7352
|
-
lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
|
|
7353
|
-
const snapshot = JSON.stringify(layout);
|
|
7354
|
-
if (snapshot === lastTmuxLayoutSnapshot) {
|
|
7355
|
-
return;
|
|
7356
|
-
}
|
|
7357
|
-
lastTmuxLayoutSnapshot = snapshot;
|
|
7358
|
-
writeSse(res, { type: 'tmux-layout', layout });
|
|
7359
|
-
}
|
|
7360
|
-
catch (error) {
|
|
7361
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7362
|
-
console.warn(`Failed to fetch tmux layout for ${session.tmuxSessionName}: ${errorMessage}`);
|
|
7363
|
-
}
|
|
7364
|
-
};
|
|
7365
|
-
const sendShellActiveProgram = async () => {
|
|
7366
|
-
if (session.mode !== 'shell') {
|
|
7367
|
-
return;
|
|
7368
|
-
}
|
|
7369
|
-
try {
|
|
7370
|
-
maybeWriteActiveProgram(await detectShellActiveProgram(session));
|
|
7371
|
-
}
|
|
7372
|
-
catch (error) {
|
|
7373
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7374
|
-
console.warn(`Failed to detect active shell program for ${sessionId}: ${errorMessage}`);
|
|
7375
|
-
}
|
|
7376
|
-
};
|
|
7377
|
-
if (session.mode === 'tmux' && session.tmuxSessionName) {
|
|
7378
|
-
void sendTmuxLayout();
|
|
7379
|
-
tmuxInterval = setInterval(() => {
|
|
7380
|
-
void sendTmuxLayout();
|
|
7381
|
-
}, TMUX_POLL_INTERVAL);
|
|
7382
|
-
}
|
|
7383
|
-
else {
|
|
7384
|
-
void sendShellActiveProgram();
|
|
7385
|
-
activeProgramInterval = setInterval(() => {
|
|
7386
|
-
void sendShellActiveProgram();
|
|
7387
|
-
}, ACTIVE_PROGRAM_POLL_INTERVAL);
|
|
7388
|
-
}
|
|
7524
|
+
const stopSampling = subscribeTerminalSampling(sessionId, session, event => writeSse(res, event));
|
|
7389
7525
|
const heartbeatInterval = setInterval(() => {
|
|
7390
7526
|
if (!writeResponseChunk(res, ': heartbeat\n\n')) {
|
|
7391
7527
|
clearInterval(heartbeatInterval);
|
|
@@ -7401,12 +7537,7 @@ router.get('/:sessionId/stream', async (req, res) => {
|
|
|
7401
7537
|
}
|
|
7402
7538
|
const cleanup = () => {
|
|
7403
7539
|
clearInterval(heartbeatInterval);
|
|
7404
|
-
|
|
7405
|
-
clearInterval(tmuxInterval);
|
|
7406
|
-
}
|
|
7407
|
-
if (activeProgramInterval) {
|
|
7408
|
-
clearInterval(activeProgramInterval);
|
|
7409
|
-
}
|
|
7540
|
+
stopSampling();
|
|
7410
7541
|
closeClient(session, sessionId, clientId);
|
|
7411
7542
|
console.log(`Client ${clientId} disconnected from terminal session ${sessionId}`);
|
|
7412
7543
|
};
|
|
@@ -7964,7 +8095,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
7964
8095
|
// 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
|
|
7965
8096
|
try {
|
|
7966
8097
|
if (session.mode === 'shell') {
|
|
7967
|
-
const ap = await
|
|
8098
|
+
const ap = await getCachedShellActiveProgram(sessionId, session);
|
|
7968
8099
|
if (ap)
|
|
7969
8100
|
session.activeProgram = ap;
|
|
7970
8101
|
}
|
|
@@ -7994,7 +8125,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
7994
8125
|
if (attachment) {
|
|
7995
8126
|
if (ws.readyState !== ws.OPEN)
|
|
7996
8127
|
return;
|
|
7997
|
-
delivery.setActive(outputWanted
|
|
8128
|
+
delivery.setActive(outputWanted);
|
|
7998
8129
|
if (delivery.active) {
|
|
7999
8130
|
const screen = new TmuxInitialScreen();
|
|
8000
8131
|
initialScreen = screen;
|
|
@@ -8114,129 +8245,10 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
8114
8245
|
await sendReplay(0);
|
|
8115
8246
|
});
|
|
8116
8247
|
void sendReplay();
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
let lastTmuxLayoutSnapshot = '';
|
|
8122
|
-
let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
|
|
8123
|
-
let lastTmuxMetadata = null;
|
|
8124
|
-
let lastTmuxMetaWriteAt = 0;
|
|
8125
|
-
const sendTmuxLayout = async () => {
|
|
8126
|
-
if (ws.readyState !== ws.OPEN)
|
|
8127
|
-
return;
|
|
8128
|
-
try {
|
|
8129
|
-
const layout = await getCachedTmuxLayout(session.tmuxSessionName);
|
|
8130
|
-
// Update active program — try ps-based detection for generic commands
|
|
8131
|
-
const activePane = getActivePaneFromLayout(layout);
|
|
8132
|
-
let ap = null;
|
|
8133
|
-
if (activePane) {
|
|
8134
|
-
const resolved = await resolveTmuxPaneProgram(activePane);
|
|
8135
|
-
if (resolved) {
|
|
8136
|
-
ap = { command: resolved.command, source: resolved.source, rawArgs: resolved.rawArgs, updatedAt: Date.now() };
|
|
8137
|
-
}
|
|
8138
|
-
else {
|
|
8139
|
-
ap = getActiveProgramFromTmuxLayout(layout);
|
|
8140
|
-
}
|
|
8141
|
-
}
|
|
8142
|
-
else {
|
|
8143
|
-
ap = getActiveProgramFromTmuxLayout(layout);
|
|
8144
|
-
}
|
|
8145
|
-
const apSnapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
|
|
8146
|
-
if (apSnapshot !== lastActiveProgramSnapshot) {
|
|
8147
|
-
lastActiveProgramSnapshot = apSnapshot;
|
|
8148
|
-
session.activeProgram = ap;
|
|
8149
|
-
persistActiveProgramBinding(sessionId, ap?.command);
|
|
8150
|
-
// Agent status: react to AI tool start/exit
|
|
8151
|
-
syncAgentIdentity(sessionId, session);
|
|
8152
|
-
console.log(`[active-program][ws] session=${sessionId} cmd=${ap?.command ?? null} source=${ap?.source ?? null}`);
|
|
8153
|
-
ws.send(JSON.stringify({
|
|
8154
|
-
type: 'active-program',
|
|
8155
|
-
activeProgram: ap?.command ?? null,
|
|
8156
|
-
activeProgramRaw: ap?.rawArgs ?? null,
|
|
8157
|
-
activeProgramSource: ap?.source ?? null,
|
|
8158
|
-
}));
|
|
8159
|
-
}
|
|
8160
|
-
// Update cwd from active pane
|
|
8161
|
-
const newCwd = getCwdFromTmuxLayout(layout);
|
|
8162
|
-
if (newCwd && newCwd !== session.cwd) {
|
|
8163
|
-
session.cwd = newCwd;
|
|
8164
|
-
console.log(`[tmux-cwd][ws] session=${sessionId} cwd=${newCwd}`);
|
|
8165
|
-
if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
|
|
8166
|
-
schedulePersistGlobalState();
|
|
8167
|
-
}
|
|
8168
|
-
ws.send(JSON.stringify({ type: 'cwd', cwd: newCwd }));
|
|
8169
|
-
refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
|
|
8170
|
-
}
|
|
8171
|
-
// tmux 消费了 inner shell 发的 OSC 2(存入 pane_title)和 OSC 133,
|
|
8172
|
-
// 不透传到外层 PTY。因此从 tmux layout 提取 active pane 的 title
|
|
8173
|
-
// 和 command 来推导 shell-title / prompt-state。
|
|
8174
|
-
if (activePane) {
|
|
8175
|
-
const paneTitle = activePane.title || '';
|
|
8176
|
-
if (paneTitle && paneTitle !== session.lastOscTitle) {
|
|
8177
|
-
session.lastOscTitle = paneTitle;
|
|
8178
|
-
ws.send(JSON.stringify({ type: 'shell-title', title: paneTitle }));
|
|
8179
|
-
}
|
|
8180
|
-
// prompt-state: command 是 shell 名 → idle;否则 → running
|
|
8181
|
-
const paneCmd = activePane.command || '';
|
|
8182
|
-
const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
|
|
8183
|
-
if (inferredState !== session.lastPromptState) {
|
|
8184
|
-
const wasRunning = session.lastPromptState === 'running';
|
|
8185
|
-
session.lastPromptState = inferredState;
|
|
8186
|
-
ws.send(JSON.stringify({ type: 'prompt-state', state: inferredState }));
|
|
8187
|
-
if (wasRunning && inferredState === 'idle') {
|
|
8188
|
-
refreshGitStatus(sessionId, session);
|
|
8189
|
-
}
|
|
8190
|
-
}
|
|
8191
|
-
}
|
|
8192
|
-
// Mirror dynamic metadata onto tmux user options.
|
|
8193
|
-
const meta = syncDynamicTmuxMetadata({
|
|
8194
|
-
tmuxSessionName: session.tmuxSessionName,
|
|
8195
|
-
program: session.activeProgram?.command ?? null,
|
|
8196
|
-
cwd: session.cwd ?? null,
|
|
8197
|
-
previousMetadata: lastTmuxMetadata,
|
|
8198
|
-
lastActiveWriteAt: lastTmuxMetaWriteAt,
|
|
8199
|
-
});
|
|
8200
|
-
lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
|
|
8201
|
-
lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
|
|
8202
|
-
const snapshot = JSON.stringify(layout);
|
|
8203
|
-
if (snapshot !== lastTmuxLayoutSnapshot) {
|
|
8204
|
-
lastTmuxLayoutSnapshot = snapshot;
|
|
8205
|
-
ws.send(JSON.stringify({ type: 'tmux-layout', layout }));
|
|
8206
|
-
}
|
|
8207
|
-
}
|
|
8208
|
-
catch { /* ignore polling errors */ }
|
|
8209
|
-
};
|
|
8210
|
-
sendTmuxLayout();
|
|
8211
|
-
tmuxInterval = setInterval(sendTmuxLayout, TMUX_POLL_INTERVAL);
|
|
8212
|
-
}
|
|
8213
|
-
// Active program polling (shell mode)
|
|
8214
|
-
if (session.mode === 'shell') {
|
|
8215
|
-
let lastApSnapshot = JSON.stringify(session.activeProgram ? { command: session.activeProgram.command, source: session.activeProgram.source } : null);
|
|
8216
|
-
const pollActiveProgram = async () => {
|
|
8217
|
-
if (ws.readyState !== ws.OPEN)
|
|
8218
|
-
return;
|
|
8219
|
-
try {
|
|
8220
|
-
const ap = await detectShellActiveProgram(session);
|
|
8221
|
-
const snapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
|
|
8222
|
-
if (snapshot !== lastApSnapshot) {
|
|
8223
|
-
lastApSnapshot = snapshot;
|
|
8224
|
-
session.activeProgram = ap;
|
|
8225
|
-
persistActiveProgramBinding(sessionId, ap?.command);
|
|
8226
|
-
// Agent status: react to AI tool start/exit
|
|
8227
|
-
syncAgentIdentity(sessionId, session);
|
|
8228
|
-
ws.send(JSON.stringify({
|
|
8229
|
-
type: 'active-program',
|
|
8230
|
-
activeProgram: ap?.command ?? null,
|
|
8231
|
-
activeProgramRaw: ap?.rawArgs ?? null,
|
|
8232
|
-
activeProgramSource: ap?.source ?? null,
|
|
8233
|
-
}));
|
|
8234
|
-
}
|
|
8235
|
-
}
|
|
8236
|
-
catch { /* ignore */ }
|
|
8237
|
-
};
|
|
8238
|
-
activeProgramInterval = setInterval(pollActiveProgram, ACTIVE_PROGRAM_POLL_INTERVAL);
|
|
8239
|
-
}
|
|
8248
|
+
const stopSampling = subscribeTerminalSampling(sessionId, session, event => {
|
|
8249
|
+
if (ws.readyState === ws.OPEN)
|
|
8250
|
+
ws.send(JSON.stringify(event));
|
|
8251
|
+
});
|
|
8240
8252
|
// Handle client → server messages
|
|
8241
8253
|
ws.on('message', async (raw) => {
|
|
8242
8254
|
const messageReceivedAt = performance.now();
|
|
@@ -8370,7 +8382,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
8370
8382
|
if (!options.flowControl || typeof msg.active !== 'boolean')
|
|
8371
8383
|
break;
|
|
8372
8384
|
outputWanted = msg.active;
|
|
8373
|
-
const resumed = delivery.setActive(outputWanted
|
|
8385
|
+
const resumed = delivery.setActive(outputWanted);
|
|
8374
8386
|
if (!delivery.active)
|
|
8375
8387
|
attachment?.close();
|
|
8376
8388
|
if (resumed && delivery.active) {
|
|
@@ -8391,12 +8403,9 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
8391
8403
|
if (typeof msg.paused === 'boolean') {
|
|
8392
8404
|
const reason = typeof msg.reason === 'string' ? msg.reason : 'client-flow-control';
|
|
8393
8405
|
if (options.flowControl) {
|
|
8394
|
-
const wasPaused = clientPaused;
|
|
8395
8406
|
clientPaused = msg.paused;
|
|
8396
|
-
delivery.
|
|
8397
|
-
if (!delivery.active)
|
|
8398
|
-
attachment?.close();
|
|
8399
|
-
if (wasPaused && delivery.active)
|
|
8407
|
+
delivery.setPaused(clientPaused);
|
|
8408
|
+
if (!clientPaused && delivery.needsReplay && delivery.active)
|
|
8400
8409
|
void sendReplay(0);
|
|
8401
8410
|
}
|
|
8402
8411
|
else
|
|
@@ -8437,10 +8446,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
|
|
|
8437
8446
|
initialScreen?.cancel();
|
|
8438
8447
|
initialScreen = null;
|
|
8439
8448
|
attachment?.close();
|
|
8440
|
-
|
|
8441
|
-
clearInterval(tmuxInterval);
|
|
8442
|
-
if (activeProgramInterval)
|
|
8443
|
-
clearInterval(activeProgramInterval);
|
|
8449
|
+
stopSampling();
|
|
8444
8450
|
const clients = wsClients.get(sessionId);
|
|
8445
8451
|
if (clients) {
|
|
8446
8452
|
clients.delete(clientId);
|
|
@@ -8548,7 +8554,9 @@ export function handleControlWebSocket(ws, clientId) {
|
|
|
8548
8554
|
// session/tmux server is no longer alive; broadcast the cleaned list.
|
|
8549
8555
|
const CLIENT_STATE_RECONCILE_INTERVAL_MS = 30_000;
|
|
8550
8556
|
const reconcileTimer = setInterval(() => {
|
|
8551
|
-
void reconcileClientState()
|
|
8557
|
+
void reconcileClientState().catch(error => {
|
|
8558
|
+
console.warn('[reconcile] failed:', getErrorMessage(error));
|
|
8559
|
+
});
|
|
8552
8560
|
}, CLIENT_STATE_RECONCILE_INTERVAL_MS);
|
|
8553
8561
|
// Don't keep the event loop alive for housekeeping.
|
|
8554
8562
|
reconcileTimer.unref?.();
|
|
@@ -8560,17 +8568,30 @@ const automationTimer = setInterval(() => {
|
|
|
8560
8568
|
}
|
|
8561
8569
|
}, 30_000);
|
|
8562
8570
|
automationTimer.unref?.();
|
|
8563
|
-
|
|
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() {
|
|
8564
8579
|
if (globalSessionState.sessions.length === 0)
|
|
8565
8580
|
return;
|
|
8581
|
+
const observedEntries = new Map(globalSessionState.sessions.map(entry => [entry.sessionId, entry]));
|
|
8566
8582
|
// Refresh first so a shared tmux crash is recorded before orphan cleanup
|
|
8567
8583
|
// can discard the metadata needed to recover Agent conversations.
|
|
8568
|
-
await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
|
|
8584
|
+
const inventory = await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
|
|
8569
8585
|
console.warn('[tmux-recovery] reconcile observation failed:', getErrorMessage(error));
|
|
8586
|
+
return null;
|
|
8570
8587
|
});
|
|
8588
|
+
const liveTmuxNames = inventory ? inventoryTmuxLiveness.get(inventory) : undefined;
|
|
8571
8589
|
const toRemove = [];
|
|
8572
8590
|
const toDetach = [];
|
|
8573
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;
|
|
8574
8595
|
if (entry.mode === 'shell') {
|
|
8575
8596
|
// Shell wrapper: backendSessionId must map to a live terminal session.
|
|
8576
8597
|
// A resumable Agent record survives with a detached backend so the tab
|
|
@@ -8590,21 +8611,15 @@ async function reconcileClientState() {
|
|
|
8590
8611
|
// own this session name. We deliberately do NOT require a live
|
|
8591
8612
|
// terminal wrapper here — detaching from the wrapper doesn't kill
|
|
8592
8613
|
// the tmux session.
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
if (
|
|
8596
|
-
if (entry.
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
toRemove.push(entry.sessionId);
|
|
8602
|
-
}
|
|
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);
|
|
8603
8622
|
}
|
|
8604
|
-
}
|
|
8605
|
-
catch {
|
|
8606
|
-
// tmux itself is down — leave the entry alone; the next tick (or
|
|
8607
|
-
// boot-time prune) will clean it up once tmux is back.
|
|
8608
8623
|
}
|
|
8609
8624
|
}
|
|
8610
8625
|
}
|