termdock 1.4.189 → 1.4.191
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/dist/client/assets/App-BbIl0DAa.js +155 -0
- package/dist/client/assets/App-BbIl0DAa.js.br +0 -0
- package/dist/client/assets/App-BbIl0DAa.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-CtZKX2AS.js → ChangeWalkthroughPanel-CT7mLy_e.js} +2 -2
- package/dist/client/assets/ChangeWalkthroughPanel-CT7mLy_e.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-CT7mLy_e.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-B9ly2Wta.js → DagPlayground-Bsf4bv7e.js} +1 -1
- package/dist/client/assets/DagPlayground-Bsf4bv7e.js.br +0 -0
- package/dist/client/assets/DagPlayground-Bsf4bv7e.js.gz +0 -0
- package/dist/client/assets/{DiffLab-Bkzvr6Ys.js → DiffLab-BjiZcUSn.js} +1 -1
- package/dist/client/assets/DiffLab-BjiZcUSn.js.br +0 -0
- package/dist/client/assets/DiffLab-BjiZcUSn.js.gz +0 -0
- package/dist/client/assets/{DiffReview-B5AC89kp.js → DiffReview-DmkAyOh4.js} +1 -1
- package/dist/client/assets/DiffReview-DmkAyOh4.js.br +0 -0
- package/dist/client/assets/DiffReview-DmkAyOh4.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab--K2fE9B_.js → DiffReviewLab-wXYUmdna.js} +2 -2
- package/dist/client/assets/DiffReviewLab-wXYUmdna.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-wXYUmdna.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-YO7X8DnR.js → DiffViewer-BlwwMnsB.js} +1 -1
- package/dist/client/assets/DiffViewer-BlwwMnsB.js.br +0 -0
- package/dist/client/assets/DiffViewer-BlwwMnsB.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-B0942mh0.js → ModelPreview-DGIaQAcq.js} +1 -1
- package/dist/client/assets/ModelPreview-DGIaQAcq.js.br +0 -0
- package/dist/client/assets/ModelPreview-DGIaQAcq.js.gz +0 -0
- package/dist/client/assets/{RightSidebar-Dvo-tMYw.js → RightSidebar-PgJ0XzEh.js} +47 -47
- package/dist/client/assets/RightSidebar-PgJ0XzEh.js.br +0 -0
- package/dist/client/assets/RightSidebar-PgJ0XzEh.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-DR1DwZvG.js → abnfDiagram-VCTEODGH-B_sCIKms.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-B_sCIKms.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-B_sCIKms.js.gz +0 -0
- package/dist/client/assets/app-5zC_J9mq.js +34 -0
- package/dist/client/assets/app-5zC_J9mq.js.br +0 -0
- package/dist/client/assets/app-5zC_J9mq.js.gz +0 -0
- package/dist/client/assets/{arc-BiK-hDRQ.js → arc-Ps2GTfJ3.js} +1 -1
- package/dist/client/assets/arc-Ps2GTfJ3.js.br +0 -0
- package/dist/client/assets/arc-Ps2GTfJ3.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-BRoIiPdr.js → architectureDiagram-5GKGNRK7-DxKfqPAR.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DxKfqPAR.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DxKfqPAR.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ--oR7UeK0.js → blockDiagram-I7D4REHJ-BNL8tkC0.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-BNL8tkC0.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-BNL8tkC0.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DWZqhfmV.js → c4Diagram-7LVT6UL2-hyEavwo6.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-hyEavwo6.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-hyEavwo6.js.gz +0 -0
- package/dist/client/assets/channel-Bkfq4dnz.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-CDds2Uuw.js → chunk-2Q5K7J3B-DN_HrxPz.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-C6tFNdRP.js → chunk-5VM5RSS4-BofAFEcC.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-Byjlqrbp.js → chunk-F27PBJKO-CLNw1JDM.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-CLNw1JDM.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-CLNw1JDM.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR--FcxAC6S.js → chunk-IMKFNOWR-DsVDl8p7.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-DsVDl8p7.js.br +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR--FcxAC6S.js.gz → chunk-IMKFNOWR-DsVDl8p7.js.gz} +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-DXF5NaP8.js → chunk-JWPE2WC7-CdfzOrPC.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-BX-3HMDv.js → chunk-POPQ4Y6H-MXmTC-sG.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-BOiT609f.js → chunk-SVP7TREG-DfwfQAY6.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-DfwfQAY6.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-DfwfQAY6.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-e4h0BXa_.js → chunk-TICWLB2K-N3OPIvmU.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-N3OPIvmU.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-N3OPIvmU.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-CGDPxIY9.js → chunk-XXDRQBXY-IurIojt5.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-Dahb2o7K.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-Dahb2o7K.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-BI_vxPbJ.js → cose-bilkent-JH36ORCC-DHbDHTvp.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-DHbDHTvp.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-DHbDHTvp.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-Do00pWzR.js → cynefin-OW5HDTMX-DyTW2Iyi.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-DyTW2Iyi.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-DyTW2Iyi.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-1WmUYFxF.js → cynefinDiagram-5FMLGOSQ-DEan3pcN.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DEan3pcN.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DEan3pcN.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-DuPIdG-w.js → dagre-GXQ25YYZ-Rnw2v1uV.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-Rnw2v1uV.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Rnw2v1uV.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-umWopOc1.js → diagram-S7CK7UJ4-CmBa29S9.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-CmBa29S9.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CmBa29S9.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-BZGG-JMX.js → diagram-UQ7AKVKN-Cqzmpane.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-Cqzmpane.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cqzmpane.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-3yFnWBx7.js → diagram-VSXAHHWV-CrHTQozH.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-CrHTQozH.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-CrHTQozH.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-zH56mQpP.js → diagram-VX7I27RA-C7nQgan9.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-C7nQgan9.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-C7nQgan9.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-gXKs8FAs.js → diagram-Z3DM3KII-93X66n3v.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-93X66n3v.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-93X66n3v.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-S-Aae0-1.js → ebnfDiagram-PWID7BFC-LfScrrH8.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-LfScrrH8.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-LfScrrH8.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-xdgylvRj.js → erDiagram-RLTQ6QDP-DAWKsrIB.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-DAWKsrIB.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DAWKsrIB.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-B6UrhCh4.js → flowDiagram-HODETNUW-CcdXfpU7.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-CcdXfpU7.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CcdXfpU7.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-C3TlLmkc.js → ganttDiagram-EL5Y4UJY-DPLNbHSY.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DPLNbHSY.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DPLNbHSY.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-BVBarnU9.js → gestureArbiter-B5PxTluh.js} +1 -1
- package/dist/client/assets/gestureArbiter-B5PxTluh.js.br +0 -0
- package/dist/client/assets/gestureArbiter-B5PxTluh.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-wwc8lPj0.js → gitGraphDiagram-WWUBYQGX-P741cwUf.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-P741cwUf.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-P741cwUf.js.gz +0 -0
- package/dist/client/assets/{index-CreRkeNE.css → index-oKp6kSMR.css} +1 -1
- package/dist/client/assets/index-oKp6kSMR.css.br +0 -0
- package/dist/client/assets/{index-CreRkeNE.css.gz → index-oKp6kSMR.css.gz} +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-CuWcAjaO.js → infoDiagram-27XIBGKW-xrl-VW74.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-ixH6dlqa.js → ishikawaDiagram-5VMMS53U-BJmiVWrB.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BJmiVWrB.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BJmiVWrB.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-jsf7IH_2.js → journeyDiagram-3NMN7TZE-DZX8uYK2.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DZX8uYK2.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DZX8uYK2.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-BjqjHq8B.js → kanban-definition-UXKFOSKX-1SmdXf0S.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-1SmdXf0S.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-1SmdXf0S.js.gz +0 -0
- package/dist/client/assets/{layout-DJhyTq7J.js → layout-De4QxmMN.js} +1 -1
- package/dist/client/assets/layout-De4QxmMN.js.br +0 -0
- package/dist/client/assets/layout-De4QxmMN.js.gz +0 -0
- package/dist/client/assets/{linear-Cuf3UwJO.js → linear-Dc93ZnhP.js} +1 -1
- package/dist/client/assets/linear-Dc93ZnhP.js.br +0 -0
- package/dist/client/assets/linear-Dc93ZnhP.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-q_70PIqZ.js → mermaid.core-Ny4ifEul.js} +5 -5
- package/dist/client/assets/mermaid.core-Ny4ifEul.js.br +0 -0
- package/dist/client/assets/mermaid.core-Ny4ifEul.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-Dk3Drgrh.js → mindmap-definition-YA3MSWOX-CE75rUrK.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CE75rUrK.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CE75rUrK.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-CQsYwRFF.js → passwordBootstrap-BuXV9qsA.js} +1 -1
- package/dist/client/assets/passwordBootstrap-BuXV9qsA.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-BuXV9qsA.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-Cx2kpJ53.js → pegDiagram-XKGWAZYB-CR8E9wWF.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-CR8E9wWF.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-CR8E9wWF.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-CAECgOHT.js → pieDiagram-E7YTZNPT-CKZz2-Iz.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-CKZz2-Iz.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CKZz2-Iz.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BYb3yLp6.js → quadrantDiagram-AXDQQJYC-w0wqPGg1.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BYb3yLp6.js.br → quadrantDiagram-AXDQQJYC-w0wqPGg1.js.br} +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-w0wqPGg1.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-9NFnMkmP.js → railroadDiagram-O6MQD6OU-CWYxj4Iu.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-CWYxj4Iu.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-CWYxj4Iu.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-B-A_CKaJ.js → referenceLongPress-xQTJmyUv.js} +1 -1
- package/dist/client/assets/referenceLongPress-xQTJmyUv.js.br +0 -0
- package/dist/client/assets/referenceLongPress-xQTJmyUv.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-ClYJW_kx.js → requirementDiagram-BXWQKSXE-DzcUlIZo.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DzcUlIZo.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DzcUlIZo.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BVKU1OEs.js → sankeyDiagram-P5KCCOFB-B1KFzqqb.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B1KFzqqb.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B1KFzqqb.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-BphaQddV.js → sequenceDiagram-WJ2MYXX4-Bph5Zy95.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Bph5Zy95.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Bph5Zy95.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-B7SoOz1O.js → sizeCapture-INFHLROL-B22qdSe2.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-B22qdSe2.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-B22qdSe2.js.gz +0 -0
- package/dist/client/assets/{sparkles-B3lcHnSJ.js → sparkles-iFGazcNw.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-CrvEfGMj.js → stateDiagram-D77RDMKH-qt4ZIeIU.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-qt4ZIeIU.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-qt4ZIeIU.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-xKezo5Hj.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-BTV61yEQ.js → swimlanes-42K2YHIH-BLCfdR2d.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BLCfdR2d.js.br +0 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-BTV61yEQ.js.gz → swimlanes-42K2YHIH-BLCfdR2d.js.gz} +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CNA032xr.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-CO2JWGKo.js → timeline-definition-24CTP7MA-B1KrZ6Jd.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-B1KrZ6Jd.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-B1KrZ6Jd.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-eoJEloNw.js → triangle-alert-BNMREFNU.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-CH1q34kT.js → vennDiagram-4TSXK5OY-DWNDoCmv.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-DWNDoCmv.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-DWNDoCmv.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-DiU2pGsi.js → wardleyDiagram-VM6X3IG4-kdVZqb5Z.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-kdVZqb5Z.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-kdVZqb5Z.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-CIufUBWj.js → xychartDiagram-S5SC5T6Z-C1U9Cr8F.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-C1U9Cr8F.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-C1U9Cr8F.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 +224 -11
- package/dist/server/agent/collaborationDeliveryWorker.js +67 -1
- package/dist/server/agent/collaborationGroupRoutes.js +25 -0
- package/dist/server/agent/collaborationPrompt.js +105 -70
- package/dist/server/agent/collaborationRoutes.js +119 -9
- package/dist/server/agent/collaborationSpawn.js +16 -0
- package/dist/server/agent/collaborationStore.js +86 -12
- package/dist/server/agent/collaborationTmuxDelivery.js +56 -1
- package/dist/server/cli.js +11 -4
- package/dist/server/routes/terminal.js +270 -13
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-CqPSkz_g.js +0 -150
- package/dist/client/assets/App-CqPSkz_g.js.br +0 -0
- package/dist/client/assets/App-CqPSkz_g.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-CtZKX2AS.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-CtZKX2AS.js.gz +0 -0
- package/dist/client/assets/DagPlayground-B9ly2Wta.js.br +0 -0
- package/dist/client/assets/DagPlayground-B9ly2Wta.js.gz +0 -0
- package/dist/client/assets/DiffLab-Bkzvr6Ys.js.br +0 -0
- package/dist/client/assets/DiffLab-Bkzvr6Ys.js.gz +0 -0
- package/dist/client/assets/DiffReview-B5AC89kp.js.br +0 -0
- package/dist/client/assets/DiffReview-B5AC89kp.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab--K2fE9B_.js.br +0 -0
- package/dist/client/assets/DiffReviewLab--K2fE9B_.js.gz +0 -0
- package/dist/client/assets/DiffViewer-YO7X8DnR.js.br +0 -0
- package/dist/client/assets/DiffViewer-YO7X8DnR.js.gz +0 -0
- package/dist/client/assets/ModelPreview-B0942mh0.js.br +0 -0
- package/dist/client/assets/ModelPreview-B0942mh0.js.gz +0 -0
- package/dist/client/assets/RightSidebar-Dvo-tMYw.js.br +0 -0
- package/dist/client/assets/RightSidebar-Dvo-tMYw.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DR1DwZvG.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DR1DwZvG.js.gz +0 -0
- package/dist/client/assets/app-CSjqfJEX.js +0 -34
- package/dist/client/assets/app-CSjqfJEX.js.br +0 -0
- package/dist/client/assets/app-CSjqfJEX.js.gz +0 -0
- package/dist/client/assets/arc-BiK-hDRQ.js.br +0 -0
- package/dist/client/assets/arc-BiK-hDRQ.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-BRoIiPdr.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-BRoIiPdr.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ--oR7UeK0.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ--oR7UeK0.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DWZqhfmV.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DWZqhfmV.js.gz +0 -0
- package/dist/client/assets/channel-CC0QyaPQ.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-Byjlqrbp.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-Byjlqrbp.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR--FcxAC6S.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-BOiT609f.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-BOiT609f.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-e4h0BXa_.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-e4h0BXa_.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-C5HydkJF.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-C5HydkJF.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-BI_vxPbJ.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-BI_vxPbJ.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-Do00pWzR.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-Do00pWzR.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-1WmUYFxF.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-1WmUYFxF.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DuPIdG-w.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DuPIdG-w.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-umWopOc1.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-umWopOc1.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BZGG-JMX.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BZGG-JMX.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-3yFnWBx7.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-3yFnWBx7.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-zH56mQpP.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-zH56mQpP.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-gXKs8FAs.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-gXKs8FAs.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-S-Aae0-1.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-S-Aae0-1.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-xdgylvRj.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-xdgylvRj.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-B6UrhCh4.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-B6UrhCh4.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-C3TlLmkc.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-C3TlLmkc.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-BVBarnU9.js.br +0 -0
- package/dist/client/assets/gestureArbiter-BVBarnU9.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-wwc8lPj0.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-wwc8lPj0.js.gz +0 -0
- package/dist/client/assets/index-CreRkeNE.css.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-ixH6dlqa.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-ixH6dlqa.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-jsf7IH_2.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-jsf7IH_2.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BjqjHq8B.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BjqjHq8B.js.gz +0 -0
- package/dist/client/assets/layout-DJhyTq7J.js.br +0 -0
- package/dist/client/assets/layout-DJhyTq7J.js.gz +0 -0
- package/dist/client/assets/linear-Cuf3UwJO.js.br +0 -0
- package/dist/client/assets/linear-Cuf3UwJO.js.gz +0 -0
- package/dist/client/assets/mermaid.core-q_70PIqZ.js.br +0 -0
- package/dist/client/assets/mermaid.core-q_70PIqZ.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-Dk3Drgrh.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-Dk3Drgrh.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-CQsYwRFF.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-CQsYwRFF.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Cx2kpJ53.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Cx2kpJ53.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CAECgOHT.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CAECgOHT.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BYb3yLp6.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-9NFnMkmP.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-9NFnMkmP.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-B-A_CKaJ.js.br +0 -0
- package/dist/client/assets/referenceLongPress-B-A_CKaJ.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-ClYJW_kx.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-ClYJW_kx.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BVKU1OEs.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BVKU1OEs.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BphaQddV.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BphaQddV.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-B7SoOz1O.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-B7SoOz1O.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CrvEfGMj.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CrvEfGMj.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-u1T2HR3l.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BTV61yEQ.js.br +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DFZ62Fin.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-CO2JWGKo.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-CO2JWGKo.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CH1q34kT.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CH1q34kT.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DiU2pGsi.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DiU2pGsi.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CIufUBWj.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CIufUBWj.js.gz +0 -0
|
@@ -1,82 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { sanitizeCollaborationRole } from './collaborationStore.js';
|
|
2
|
+
const SHELL_RULE = '─'.repeat(30);
|
|
3
|
+
/** Body larger than this (in UTF-8 bytes) is replaced by a retrieval pointer
|
|
4
|
+
* instead of being injected into the terminal verbatim. */
|
|
5
|
+
const MAX_INLINE_BODY_BYTES = 8_192;
|
|
6
|
+
/**
|
|
7
|
+
* Characters that would corrupt the delivery shell header `「X」群(N 个成员)`
|
|
8
|
+
* and the per-message `来自:X · kind` line: the quote brackets and the middle
|
|
9
|
+
* dot, which the render layer uses as structure, plus control sequences
|
|
10
|
+
* (C0 controls + DEL) that could inject terminal output. Legacy and federated
|
|
11
|
+
* names may carry them, so the render layer neutralizes them defensively;
|
|
12
|
+
* user-entered names are rejected at creation instead.
|
|
13
|
+
*/
|
|
14
|
+
export const COLLAB_NAME_FORBIDDEN = /[【】「」·\x00-\x1f\x7f]/;
|
|
15
|
+
/** Render-safe name for shell header / per-message source lines: reserved
|
|
16
|
+
* bracket and delimiter characters become spaces (they would otherwise split
|
|
17
|
+
* the `「X」群` and `来自:X · kind` lines), control characters are
|
|
18
|
+
* neutralized, whitespace collapses, and the display length is capped. */
|
|
19
|
+
export function sanitizeCollaborationName(name, max = 40) {
|
|
20
|
+
const cleaned = name
|
|
21
|
+
.replace(/[【】「」]/g, ' ')
|
|
22
|
+
.replace(/·/g, ' ')
|
|
23
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim();
|
|
26
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned;
|
|
27
|
+
}
|
|
28
|
+
/** A single delivered message: `来自:X · kind` over a fenced body, with the
|
|
29
|
+
* reply route for agent-sourced messages kept outside the fence. A fan-out
|
|
30
|
+
* dispatch (message.fanOutIds present) is flagged `· 群发` and names the
|
|
31
|
+
* sibling recipients on their own line, so a broadcast is never mistaken for
|
|
32
|
+
* a one-to-one assignment — raw ids fall back to the sanitized id itself. */
|
|
33
|
+
function formatCollaborationMessage(message, source, fannedNames, fence) {
|
|
34
|
+
const lines = [`来自:${source} · ${message.kind}${fannedNames.length ? ' · 群发' : ''}`];
|
|
35
|
+
if (fannedNames.length)
|
|
36
|
+
lines.push(`同时发给了:${fannedNames.join('、')}`);
|
|
37
|
+
lines.push('', fence, message.content, fence);
|
|
38
|
+
if (message.task)
|
|
39
|
+
lines.push('', `任务上报:${JSON.stringify(message.task)}`);
|
|
40
|
+
if (message.fromSessionId)
|
|
41
|
+
lines.push('', `回复:td collab reply ${message.id} "回复内容" --text`);
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
9
44
|
export function formatCollaborationDelivery(input) {
|
|
10
45
|
const groupsById = new Map(input.groups.map((group) => [group.id, group]));
|
|
11
46
|
const sessionsById = new Map(input.sessions.map((session) => [session.sessionId, session]));
|
|
12
|
-
const
|
|
47
|
+
const blocks = [];
|
|
48
|
+
for (const message of input.messages) {
|
|
13
49
|
const sourceSession = message.fromSessionId ? sessionsById.get(message.fromSessionId) : null;
|
|
14
50
|
const source = message.fromSessionId
|
|
15
|
-
?
|
|
16
|
-
sourceSession?.name ?? message.fromSessionId,
|
|
17
|
-
sourceSession?.agentNativeSessionId ? `Agent 原生 Session ID:${sourceSession.agentNativeSessionId}` : null,
|
|
18
|
-
`Termdock 会话 ID:${message.fromSessionId}`,
|
|
19
|
-
].filter(Boolean).join('\n')
|
|
51
|
+
? sanitizeCollaborationName(sourceSession?.name ?? message.fromSessionId)
|
|
20
52
|
: '用户';
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
const bytes = Buffer.byteLength(message.content);
|
|
54
|
+
const body = bytes > MAX_INLINE_BODY_BYTES
|
|
55
|
+
? `大消息已完整保存(${bytes} 字节)。使用 td collab message get ${message.id} --json 获取正文;不要把这条提示当作消息正文。`
|
|
56
|
+
: message.content;
|
|
57
|
+
const fence = '`'.repeat(Math.max(3, ...Array.from(body.matchAll(/`+/g), (match) => match[0].length + 1)));
|
|
58
|
+
const fannedNames = (message.fanOutIds ?? [])
|
|
59
|
+
.map((sessionId) => sanitizeCollaborationName(sessionsById.get(sessionId)?.name ?? sessionId));
|
|
60
|
+
blocks.push(formatCollaborationMessage({ ...message, content: body }, source, fannedNames, fence));
|
|
61
|
+
}
|
|
62
|
+
// The shell header names the group only when every block belongs to one;
|
|
63
|
+
// a mixed batch drops the header entirely (no single-group claim to make).
|
|
64
|
+
// The member count rides along as the at-a-glance size of that group.
|
|
65
|
+
const groupIds = [...new Set(input.messages.map((message) => message.groupId))];
|
|
66
|
+
const singleGroup = groupIds.length === 1 ? groupsById.get(groupIds[0]) : null;
|
|
67
|
+
const shellHeader = singleGroup
|
|
68
|
+
? `「${sanitizeCollaborationName(singleGroup.name ?? '协作组')}」群(${singleGroup.sessionIds.length} 个成员)`
|
|
69
|
+
: '';
|
|
35
70
|
const peerIds = Array.from(new Set(input.groups.flatMap((group) => group.sessionIds)))
|
|
36
71
|
.filter((sessionId) => sessionId !== input.targetSessionId);
|
|
37
|
-
|
|
72
|
+
// Keep routing IDs once, only for peers who are not already directly replyable.
|
|
73
|
+
const sourceIds = new Set(input.messages.map((message) => message.fromSessionId));
|
|
74
|
+
const peers = peerIds.filter((id) => !sourceIds.has(id)).map((sessionId) => {
|
|
38
75
|
const session = sessionsById.get(sessionId);
|
|
39
|
-
return
|
|
40
|
-
`- ${session?.name ?? '离线会话'} · ${session?.status === 'service-unreachable' ? '服务不可达,消息无法送达;仅可排队等待服务重连' : session?.status ?? 'offline'}`,
|
|
41
|
-
...(session?.agentNativeSessionId ? [` Agent 原生 Session ID:${session.agentNativeSessionId}`] : []),
|
|
42
|
-
` Termdock 会话 ID:${sessionId}`,
|
|
43
|
-
].join('\n');
|
|
76
|
+
return `- ${session ? sanitizeCollaborationName(session.name) : '离线会话'}:\`td collab send ${sessionId} "消息内容" --text\``;
|
|
44
77
|
});
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: []),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'- 发消息:`td collab send <会话ID> "消息内容"`',
|
|
58
|
-
'- 交接:`td collab handoff <会话ID> "交接摘要"`',
|
|
59
|
-
'- 加成员:`td collab add <协作组ID> <会话ID>`',
|
|
60
|
-
'- 移成员:`td collab remove <协作组ID> <会话ID>`',
|
|
61
|
-
'- 新建 Agent:`td collab spawn <协作组ID> <agent-slug> --name "名称" --task "初始任务"`',
|
|
62
|
-
'- 查看:`td collab status` / `td collab inbox`;完整选项:`td collab --help`。默认 JSON,pending 只表示已入队。',
|
|
63
|
-
'- 确认投递:`td collab message get <消息ID> --receipt-only`;send/reply 可加 `--wait-until delivered --timeout 30s`。超时不会取消消息,重试使用同一 `--idempotency-key`。',
|
|
64
|
-
'- 只取新回复:`td collab inbox --consumer <名称> --limit 20`;处理完成后用返回的 next_cursor 执行 `td collab cursor commit <游标> --consumer <名称>`。需要明确标记读取时用 `td collab message read <消息ID>`。',
|
|
65
|
-
'- 回复应明确类型:reply 加 `--response-kind ack|progress|result`;可附 `--task-envelope` JSON(task_id、status、progress、evidence、blocker)。不要把 ACK 或 turn done 汇报为任务完成。',
|
|
66
|
-
'- Agent 状态与工具活动只作可选提示;任意接入方都可通过收件箱主动读取、确认和回复,无需专用 hooks。',
|
|
78
|
+
const unreachable = peerIds.filter((id) => sessionsById.get(id)?.status === 'service-unreachable');
|
|
79
|
+
// Education (peer roster examples) decays once the session knows the group;
|
|
80
|
+
// the `--help` pointer stays permanently as the one-line entrance to the
|
|
81
|
+
// full command surface — everything else can be looked up from there.
|
|
82
|
+
const routingHelp = input.showRoutingHelp === false ? [] : (peers.length ? ['联系其他成员:', ...peers] : []);
|
|
83
|
+
const dynamicNotices = [
|
|
84
|
+
...(peerIds.some((id) => id.startsWith('remote:'))
|
|
85
|
+
? ['跨服务通信使用 td collab;转发客户端须保持运行,网页或 PWA 暂停后需返回前台继续转发。'] : []),
|
|
86
|
+
...unreachable.map((id) => {
|
|
87
|
+
const session = sessionsById.get(id);
|
|
88
|
+
return `注意:${session ? sanitizeCollaborationName(session.name) : id} 服务不可达,消息无法送达;仅可排队等待重连。`;
|
|
89
|
+
}),
|
|
67
90
|
];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
''
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
const notes = [...routingHelp, ...dynamicNotices, '更多操作:`td collab --help`。'];
|
|
92
|
+
// Notes (education + dynamic notices) live inside the shell, after the last
|
|
93
|
+
// message, so the closing rule still marks the end of the delivered block.
|
|
94
|
+
if (!blocks.length)
|
|
95
|
+
return notes.join('\n');
|
|
96
|
+
// Self-identity rides the shell under the header so the recipient always
|
|
97
|
+
// knows the name peers see (a rename lands on the next delivery — no
|
|
98
|
+
// separate notification needed) and, in a single-group delivery, its role.
|
|
99
|
+
// The name needs no group claim and survives mixed batches; the role (定位)
|
|
100
|
+
// only rides when the batch maps to exactly one group.
|
|
101
|
+
const ownName = sanitizeCollaborationName(sessionsById.get(input.targetSessionId)?.name ?? '');
|
|
102
|
+
const ownRole = singleGroup
|
|
103
|
+
? sanitizeCollaborationRole(singleGroup.roles?.[input.targetSessionId] ?? '')
|
|
104
|
+
: '';
|
|
105
|
+
const lines = [SHELL_RULE];
|
|
106
|
+
if (shellHeader)
|
|
107
|
+
lines.push(shellHeader);
|
|
108
|
+
if (ownName)
|
|
109
|
+
lines.push(`你的名字:${ownName}`);
|
|
110
|
+
if (ownRole)
|
|
111
|
+
lines.push(`你的定位:${ownRole}`);
|
|
112
|
+
// No blank line between the identity block and the first message, nor
|
|
113
|
+
// between the last message and the notes: the shell reads as one compact
|
|
114
|
+
// block, and only message-to-message boundaries keep a blank line.
|
|
115
|
+
lines.push(blocks.join('\n\n'), ...notes, SHELL_RULE);
|
|
116
|
+
return lines.join('\n');
|
|
82
117
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { COLLAB_LIMITS, CollaborationError, extrasFromBody } from './collaborationProtocol.js';
|
|
3
|
-
|
|
3
|
+
/** Member names arrive agent-authored over a CLI; control characters would
|
|
4
|
+
* corrupt terminal shells, tmux options and persisted records. Fold them to
|
|
5
|
+
* spaces, collapse runs, and cap the length (display sanitization for the
|
|
6
|
+
* delivery shell stays in the presentation layer). */
|
|
7
|
+
export function cleanMemberName(value) {
|
|
8
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
9
|
+
}
|
|
10
|
+
export function collaborationRoutes({ store, resolveSession, deliver, rebind, resolveNames, renameSession }) {
|
|
4
11
|
const router = Router();
|
|
5
12
|
const run = (handler) => async (req, res) => {
|
|
6
13
|
try {
|
|
@@ -34,6 +41,27 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind })
|
|
|
34
41
|
const receipts = ids.map((id) => store.receipt(id));
|
|
35
42
|
res.json({ ok: true, ...receipts[0], messages, receipts });
|
|
36
43
|
};
|
|
44
|
+
/** When the recipient is served by this server's own delivery worker, give
|
|
45
|
+
* the write (and its post-write snapshot capture) a short window to finish
|
|
46
|
+
* before the receipt leaves the send/reply call — the sender then sees the
|
|
47
|
+
* recipient's screen right away. Un-waitable targets (relayed/federated,
|
|
48
|
+
* offline, or a mock deliver) return immediately with the message queued. */
|
|
49
|
+
const awaitDelivery = async (id, outcome) => {
|
|
50
|
+
if (!outcome?.waitable)
|
|
51
|
+
return;
|
|
52
|
+
const deadline = Date.now() + 2_000;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const receipt = store.receipt(id);
|
|
55
|
+
const settled = receipt.status !== 'pending'
|
|
56
|
+
|| receipt.snapshot != null
|
|
57
|
+
|| (receipt.attempt_count > 0
|
|
58
|
+
&& receipt.last_error !== 'DELIVERY_IN_PROGRESS'
|
|
59
|
+
&& (receipt.next_retry_at ?? 0) > Date.now());
|
|
60
|
+
if (settled || Date.now() >= deadline)
|
|
61
|
+
return;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
37
65
|
router.get('/capabilities', run((_req, res) => {
|
|
38
66
|
res.json({ protocol_version: 2, limits: COLLAB_LIMITS,
|
|
39
67
|
routing: { background_recovery: true, explicit_rebind: Boolean(rebind), fixed_tmux_pane: true },
|
|
@@ -44,20 +72,37 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind })
|
|
|
44
72
|
timeout: 'stops waiting, does not cancel delivery',
|
|
45
73
|
retry: 'same message id may be submitted again after a crash or uncertain transport result; consumers deduplicate by message id' } });
|
|
46
74
|
}));
|
|
47
|
-
router.post('/send', run((req, res, sessionId) => {
|
|
75
|
+
router.post('/send', run(async (req, res, sessionId) => {
|
|
48
76
|
const target = typeof req.body.targetSessionId === 'string' ? req.body.targetSessionId.trim() : '';
|
|
49
|
-
|
|
77
|
+
// A fan-out send lists every recipient in toSessionIds; the store persists
|
|
78
|
+
// each edge with its sibling fanOutIds so recipients can tell a broadcast
|
|
79
|
+
// from a one-to-one assignment. The single-recipient shape stays intact.
|
|
80
|
+
const requestedTargets = Array.isArray(req.body.toSessionIds)
|
|
81
|
+
? [...new Set(req.body.toSessionIds.filter((id) => typeof id === 'string' && id.trim().length > 0))]
|
|
82
|
+
: [];
|
|
83
|
+
const targets = requestedTargets.length > 0 ? requestedTargets : (target ? [target] : []);
|
|
84
|
+
if (!targets.length)
|
|
85
|
+
throw new CollaborationError('NO_TARGET', 'send requires a targetSessionId (or toSessionIds for a fan-out)', 400);
|
|
86
|
+
const groups = store.groupsForSession(sessionId)
|
|
87
|
+
.filter((group) => targets.every((candidate) => group.sessionIds.includes(candidate)) && (!req.body.group_id || group.id === req.body.group_id));
|
|
50
88
|
if (!groups.length)
|
|
51
|
-
throw new CollaborationError('GROUP_NOT_FOUND', 'Sender and
|
|
89
|
+
throw new CollaborationError('GROUP_NOT_FOUND', 'Sender and recipients must share the specified group');
|
|
52
90
|
if (groups.length > 1 && !req.body.group_id)
|
|
53
91
|
throw new CollaborationError('AMBIGUOUS_GROUP', 'Multiple shared groups; specify --group');
|
|
54
|
-
|
|
92
|
+
if (req.body.task)
|
|
93
|
+
throw new CollaborationError('DISPATCH_CARRIES_TASK', 'Dispatch carries no task state; recipients report task status through replies', 400);
|
|
94
|
+
const messages = store.send({ ...extrasFromBody(req.body), groupId: groups[0].id, fromSessionId: sessionId, toSessionIds: targets,
|
|
55
95
|
kind: (req.body.kind ?? 'message'), content: typeof req.body.message === 'string' ? req.body.message : '',
|
|
56
96
|
threadId: typeof req.body.thread_id === 'string' ? req.body.thread_id : undefined });
|
|
57
|
-
|
|
97
|
+
// Wake and await every local recipient edge in parallel; relayed or
|
|
98
|
+
// federated ids answer immediately with the message queued, so fan-out
|
|
99
|
+
// never stalls on a peer.
|
|
100
|
+
await Promise.all(messages.map(async (message) => {
|
|
101
|
+
await awaitDelivery(message.id, (await deliver(message.toSessionId)) ?? null);
|
|
102
|
+
}));
|
|
58
103
|
sent(res, messages.map((message) => message.id));
|
|
59
104
|
}));
|
|
60
|
-
router.post('/reply', run((req, res, sessionId) => {
|
|
105
|
+
router.post('/reply', run(async (req, res, sessionId) => {
|
|
61
106
|
const original = ownMessage(String(req.body.messageId ?? ''), sessionId, true);
|
|
62
107
|
if (!original.fromSessionId)
|
|
63
108
|
throw new CollaborationError('NO_REPLY_TARGET', 'User messages have no agent reply target');
|
|
@@ -66,7 +111,7 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind })
|
|
|
66
111
|
replyTo: original.id, threadId: original.threadId });
|
|
67
112
|
// Reply is explicit consumption; only mark after successful validation/persistence.
|
|
68
113
|
store.markRead([original.id]);
|
|
69
|
-
deliver(original.fromSessionId);
|
|
114
|
+
await awaitDelivery(messages[0].id, (await deliver(original.fromSessionId)) ?? null);
|
|
70
115
|
sent(res, messages.map((message) => message.id));
|
|
71
116
|
}));
|
|
72
117
|
router.get('/message/:id', run((req, res, sessionId) => {
|
|
@@ -87,11 +132,76 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind })
|
|
|
87
132
|
const page = store.page(sessionId, { unread: string('unread') === 'true', since, afterId: string('after_id'), cursor: string('cursor'),
|
|
88
133
|
consumer: string('consumer'), limit: string('limit') ? Number(string('limit')) : undefined,
|
|
89
134
|
from: string('from'), group: string('group'), thread: string('thread'), kind: string('kind'), responseKind: string('response_kind'), order: string('order') });
|
|
90
|
-
|
|
135
|
+
// Fan-out edges carry sibling recipients; the names map lets the CLI text
|
|
136
|
+
// renderer show 同时发给了:… as people instead of bare session ids. The
|
|
137
|
+
// fallback to the raw id stays server-side agnostic (federated/offline).
|
|
138
|
+
const nameIds = [...new Set(page.messages.flatMap((message) => [message.fromSessionId, ...(message.fanOutIds ?? [])].filter((id) => typeof id === 'string' && id.length > 0)))];
|
|
139
|
+
res.json({ ok: true, ...page, ...(nameIds.length ? { names: resolveNames?.(nameIds) ?? {} } : {}) });
|
|
91
140
|
}));
|
|
92
141
|
router.post('/cursor/commit', run((req, res, sessionId) => {
|
|
93
142
|
store.commitCursor(sessionId, String(req.body.cursor ?? ''), String(req.body.consumer ?? ''));
|
|
94
143
|
res.json({ ok: true, consumer: req.body.consumer, cursor: req.body.cursor });
|
|
95
144
|
}));
|
|
145
|
+
const groupOf = (id, sessionId) => {
|
|
146
|
+
const group = store.getGroup(id);
|
|
147
|
+
if (!group || group.deleted)
|
|
148
|
+
throw new CollaborationError('GROUP_NOT_FOUND', 'Collaboration group not found', 404);
|
|
149
|
+
if (!group.sessionIds.includes(sessionId))
|
|
150
|
+
throw new CollaborationError('NOT_A_MEMBER', 'Session is not a member of this group', 403);
|
|
151
|
+
return group;
|
|
152
|
+
};
|
|
153
|
+
// Member names ride along so role lists read as people, not bare ids; the
|
|
154
|
+
// caller falls back to the session id when a name is unknown (federated or
|
|
155
|
+
// offline members have no local record).
|
|
156
|
+
const groupView = (group) => {
|
|
157
|
+
const names = resolveNames?.(group.sessionIds) ?? {};
|
|
158
|
+
return {
|
|
159
|
+
id: group.id, name: group.name, sessionIds: group.sessionIds,
|
|
160
|
+
members: group.sessionIds.map((sessionId) => ({ sessionId, name: names[sessionId] ?? null })),
|
|
161
|
+
roles: group.roles ?? {},
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
router.get('/role', run((req, res, sessionId) => {
|
|
165
|
+
const groupId = String(req.query.group ?? '');
|
|
166
|
+
if (groupId) {
|
|
167
|
+
res.json({ ok: true, group: groupView(groupOf(groupId, sessionId)) });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Without a group this lists the caller's own groups with full role
|
|
171
|
+
// snapshots — what `td collab --help` renders so a newcomer sees every
|
|
172
|
+
// member's role in one step instead of hunting the group id first.
|
|
173
|
+
res.json({ ok: true, groups: store.groupsForSession(sessionId)
|
|
174
|
+
.filter((group) => !group.deleted)
|
|
175
|
+
.map(groupView) });
|
|
176
|
+
}));
|
|
177
|
+
router.post('/role', run((req, res, sessionId) => {
|
|
178
|
+
const group = groupOf(String(req.body.group_id ?? ''), sessionId);
|
|
179
|
+
const role = req.body.role;
|
|
180
|
+
if (typeof req.body.session_id !== 'string' || !group.sessionIds.includes(req.body.session_id)) {
|
|
181
|
+
throw new CollaborationError('NOT_A_MEMBER', 'Role target must be a member of this group', 400);
|
|
182
|
+
}
|
|
183
|
+
if (role === undefined || (role !== null && typeof role !== 'string')) {
|
|
184
|
+
throw new CollaborationError('INVALID_ROLE', 'role must be text or null', 400);
|
|
185
|
+
}
|
|
186
|
+
const updated = store.setRole({ groupId: group.id, sessionId: req.body.session_id, role: role });
|
|
187
|
+
res.json({ ok: true, group: { ...updated, roles: updated.roles ?? {} } });
|
|
188
|
+
}));
|
|
189
|
+
router.post('/name', run(async (req, res, sessionId) => {
|
|
190
|
+
if (!renameSession)
|
|
191
|
+
throw new CollaborationError('RENAME_UNAVAILABLE', 'Member renaming is not available', 503);
|
|
192
|
+
const target = typeof req.body.session_id === 'string' ? req.body.session_id.trim() : '';
|
|
193
|
+
if (!store.groupsForSession(sessionId).some((group) => !group.deleted && group.sessionIds.includes(target))) {
|
|
194
|
+
throw new CollaborationError('NOT_A_MEMBER', 'Rename target must share a collaboration group with the caller', 403);
|
|
195
|
+
}
|
|
196
|
+
const name = typeof req.body.name === 'string' ? cleanMemberName(req.body.name) : '';
|
|
197
|
+
if (!name)
|
|
198
|
+
throw new CollaborationError('INVALID_NAME', 'name must be non-empty text up to 80 characters', 400);
|
|
199
|
+
const result = await renameSession(target, name);
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
const missing = result.code === 'SESSION_NOT_FOUND';
|
|
202
|
+
throw new CollaborationError(missing ? 'SESSION_NOT_FOUND' : 'RENAME_FAILED', result.error ?? 'Unable to rename the member', missing ? 404 : 400);
|
|
203
|
+
}
|
|
204
|
+
res.json({ ok: true, sessionId: target, name });
|
|
205
|
+
}));
|
|
96
206
|
return router;
|
|
97
207
|
}
|
|
@@ -7,3 +7,19 @@ export function resolveCollaborationSpawnMode(input) {
|
|
|
7
7
|
?? asSpawnMode(input.fallbackMode)
|
|
8
8
|
?? 'shell';
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Shell command line that boots a collaboration member agent. Spawned agents
|
|
12
|
+
* have no human at the keyboard, yet their delivery shells instruct them to
|
|
13
|
+
* run `td collab …` — without pre-authorization every such call stops at the
|
|
14
|
+
* agent's permission dialog and the collaboration loop stalls. Agents whose
|
|
15
|
+
* CLI flags are known get a scoped allowlist (collab surface only; the server
|
|
16
|
+
* guards every collab action anyway). Unsupported agents launch plain — the
|
|
17
|
+
* delivery confirm gate can dismiss their dialogs instead.
|
|
18
|
+
*/
|
|
19
|
+
export function buildCollaborationSpawnCommand(input) {
|
|
20
|
+
const { slug, command } = input;
|
|
21
|
+
if (slug === 'claude') {
|
|
22
|
+
return `${command} --allowedTools "Bash(td collab *)"`;
|
|
23
|
+
}
|
|
24
|
+
return command;
|
|
25
|
+
}
|
|
@@ -2,6 +2,25 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { COLLAB_LIMITS, CollaborationError, STATUS_RANK, validateExtras } from './collaborationProtocol.js';
|
|
5
|
+
/** Characters that would corrupt a role line injected into the delivery shell
|
|
6
|
+
* (`你的定位:...`) or smuggle terminal output. Unlike names, roles are plain
|
|
7
|
+
* prose — 「」 and · are harmless there — so only control sequences are barred. */
|
|
8
|
+
export const COLLAB_ROLE_FORBIDDEN = /[\x00-\x1f\x7f]/;
|
|
9
|
+
export const COLLAB_ROLE_MAX_CHARS = 200;
|
|
10
|
+
/** Data/render-safe role: control characters collapse to spaces, whitespace
|
|
11
|
+
* folds, and the display length is capped with an ellipsis. Applied on write
|
|
12
|
+
* and again defensively at render time (federated peers may carry junk). */
|
|
13
|
+
export function sanitizeCollaborationRole(role, max = COLLAB_ROLE_MAX_CHARS) {
|
|
14
|
+
const cleaned = role.replace(/[\x00-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
15
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned;
|
|
16
|
+
}
|
|
17
|
+
/** Drop roles whose member has left the group; survivors keep theirs. */
|
|
18
|
+
function pruneRoles(group, sessionIds) {
|
|
19
|
+
if (!group.roles)
|
|
20
|
+
return group;
|
|
21
|
+
const roles = Object.fromEntries(Object.entries(group.roles).filter(([id]) => sessionIds.includes(id)));
|
|
22
|
+
return Object.keys(roles).length ? { ...group, roles } : { ...group, roles: undefined };
|
|
23
|
+
}
|
|
5
24
|
const MAX_MESSAGES = 2_000;
|
|
6
25
|
const MESSAGE_KINDS = new Set(['message', 'ask', 'reply', 'task', 'handoff', 'done']);
|
|
7
26
|
export class CollaborationStore {
|
|
@@ -41,21 +60,44 @@ export class CollaborationStore {
|
|
|
41
60
|
save(input) {
|
|
42
61
|
const now = Date.now();
|
|
43
62
|
const existing = input.id ? this.document.groups.find((group) => group.id === input.id) : null;
|
|
44
|
-
const
|
|
63
|
+
const sessionIds = Array.from(new Set(input.sessionIds.map((id) => id.trim()).filter(Boolean)));
|
|
64
|
+
const group = pruneRoles({
|
|
45
65
|
...existing,
|
|
46
|
-
...(existing?.remoteSessions ? { remoteSessions: existing.remoteSessions.filter((session) =>
|
|
66
|
+
...(existing?.remoteSessions ? { remoteSessions: existing.remoteSessions.filter((session) => sessionIds.includes(session.sessionId)) } : {}),
|
|
47
67
|
id: existing?.id ?? crypto.randomUUID(),
|
|
48
68
|
name: input.name.trim(),
|
|
49
|
-
sessionIds
|
|
69
|
+
sessionIds,
|
|
50
70
|
createdAt: existing?.createdAt ?? now,
|
|
51
71
|
updatedAt: Math.max(now, (existing?.updatedAt ?? 0) + 1),
|
|
52
|
-
};
|
|
72
|
+
}, sessionIds);
|
|
53
73
|
this.document.groups = existing
|
|
54
74
|
? this.document.groups.map((candidate) => candidate.id === group.id ? group : candidate)
|
|
55
75
|
: [...this.document.groups, group];
|
|
56
76
|
this.persist();
|
|
57
77
|
return group;
|
|
58
78
|
}
|
|
79
|
+
/** Set or clear a member's role (定位). The target may be any group member —
|
|
80
|
+
* oneself, a peer, or a federated remote session; peers can set each other's
|
|
81
|
+
* roles. Roles ride the group document into federation and are dropped
|
|
82
|
+
* together with the member on removal. */
|
|
83
|
+
setRole(input) {
|
|
84
|
+
const group = this.getGroup(input.groupId);
|
|
85
|
+
if (!group || group.deleted)
|
|
86
|
+
throw new CollaborationError('GROUP_NOT_FOUND', '协作组已删除,请刷新列表', 404);
|
|
87
|
+
if (!group.sessionIds.includes(input.sessionId))
|
|
88
|
+
throw new CollaborationError('NOT_A_MEMBER', '该会话不在协作组中,无法设置定位', 400);
|
|
89
|
+
const role = input.role === null ? null : sanitizeCollaborationRole(input.role);
|
|
90
|
+
const roles = { ...(group.roles ?? {}) };
|
|
91
|
+
if (role)
|
|
92
|
+
roles[input.sessionId] = role;
|
|
93
|
+
else
|
|
94
|
+
delete roles[input.sessionId];
|
|
95
|
+
const updated = { ...group, roles: Object.keys(roles).length ? roles : undefined,
|
|
96
|
+
updatedAt: Math.max(Date.now(), group.updatedAt + 1) };
|
|
97
|
+
this.document.groups = this.document.groups.map((candidate) => candidate.id === group.id ? updated : candidate);
|
|
98
|
+
this.persist();
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
59
101
|
remove(id) {
|
|
60
102
|
const before = this.document.groups.length;
|
|
61
103
|
const existing = this.getGroup(id);
|
|
@@ -88,9 +130,9 @@ export class CollaborationStore {
|
|
|
88
130
|
if (group.id === source.id) {
|
|
89
131
|
if (dissolved)
|
|
90
132
|
return group.federated ? [{ ...group, deleted: true, updatedAt: Math.max(now, group.updatedAt + 1) }] : [];
|
|
91
|
-
return [{ ...group, sessionIds: sourceIds,
|
|
133
|
+
return [pruneRoles({ ...group, sessionIds: sourceIds,
|
|
92
134
|
...(group.remoteSessions ? { remoteSessions: group.remoteSessions.filter((session) => sourceIds.includes(session.sessionId)) } : {}),
|
|
93
|
-
updatedAt: Math.max(now, group.updatedAt + 1) }];
|
|
135
|
+
updatedAt: Math.max(now, group.updatedAt + 1) }, sourceIds)];
|
|
94
136
|
}
|
|
95
137
|
return group.id === target.id ? [{ ...group, sessionIds: [...new Set([...group.sessionIds, input.sessionId])], updatedAt: Math.max(now, group.updatedAt + 1) }] : [group];
|
|
96
138
|
}),
|
|
@@ -109,7 +151,8 @@ export class CollaborationStore {
|
|
|
109
151
|
return [group];
|
|
110
152
|
if (dissolvedIds.has(group.id))
|
|
111
153
|
return group.federated ? [{ ...group, deleted: true, updatedAt: Math.max(now, group.updatedAt + 1) }] : [];
|
|
112
|
-
|
|
154
|
+
const remaining = group.sessionIds.filter((id) => id !== sessionId);
|
|
155
|
+
return [pruneRoles({ ...group, sessionIds: remaining, updatedAt: Math.max(now, group.updatedAt + 1) }, remaining)];
|
|
113
156
|
});
|
|
114
157
|
this.document.messages = this.document.messages.filter((message) => !dissolvedIds.has(message.groupId)
|
|
115
158
|
&& message.fromSessionId !== sessionId
|
|
@@ -160,6 +203,7 @@ export class CollaborationStore {
|
|
|
160
203
|
sequence: this.nextSequence(),
|
|
161
204
|
id: crypto.randomUUID(), groupId: group.id, fromSessionId: input.fromSessionId,
|
|
162
205
|
toSessionId, kind: input.kind, content, threadId,
|
|
206
|
+
fanOutIds: recipients.length > 1 ? recipients.filter((id) => id !== toSessionId) : undefined,
|
|
163
207
|
replyTo: input.replyTo?.trim() || null, status: 'pending', createdAt: now,
|
|
164
208
|
deliveredAt: null, readAt: null,
|
|
165
209
|
}));
|
|
@@ -195,10 +239,14 @@ export class CollaborationStore {
|
|
|
195
239
|
markDelivered(messageIds) {
|
|
196
240
|
return this.updateStatus(messageIds, 'delivered');
|
|
197
241
|
}
|
|
198
|
-
|
|
199
|
-
|
|
242
|
+
/**
|
|
243
|
+
* @param source 'explicit' — a consumer acknowledged the message (CLI/UI).
|
|
244
|
+
* Idempotent; never implies an application-level ACK.
|
|
245
|
+
*/
|
|
246
|
+
markRead(messageIds, source = 'explicit') {
|
|
247
|
+
return this.updateStatus(messageIds, 'read', source);
|
|
200
248
|
}
|
|
201
|
-
updateStatus(messageIds, status) {
|
|
249
|
+
updateStatus(messageIds, status, readSource = 'explicit') {
|
|
202
250
|
const ids = new Set(messageIds);
|
|
203
251
|
this.expire();
|
|
204
252
|
const now = Date.now();
|
|
@@ -207,7 +255,7 @@ export class CollaborationStore {
|
|
|
207
255
|
if (!ids.has(message.id) || message.status === 'expired' || message.status === 'failed' || message.status === 'read')
|
|
208
256
|
return message;
|
|
209
257
|
const updated = status === 'read'
|
|
210
|
-
? { ...message, status, deliveredAt: message.deliveredAt ?? now, readAt: now, readSource
|
|
258
|
+
? { ...message, status, deliveredAt: message.deliveredAt ?? now, readAt: now, readSource, deliverySource: message.deliverySource ?? (message.deliveredAt === null ? 'consumer_read' : 'legacy_or_unspecified') }
|
|
211
259
|
: message.status === 'pending' ? { ...message, status, deliveredAt: now, deliverySource: 'pty_written' } : message;
|
|
212
260
|
changed.push(updated);
|
|
213
261
|
return updated;
|
|
@@ -321,6 +369,13 @@ export class CollaborationStore {
|
|
|
321
369
|
this.persist();
|
|
322
370
|
}
|
|
323
371
|
}
|
|
372
|
+
setSnapshot(id, snapshot) {
|
|
373
|
+
const message = this.getMessage(id);
|
|
374
|
+
if (!message)
|
|
375
|
+
return;
|
|
376
|
+
message.snapshot = snapshot;
|
|
377
|
+
this.persist();
|
|
378
|
+
}
|
|
324
379
|
diagnostic(id) { return this.document.transport?.[id] ?? null; }
|
|
325
380
|
recordTransport(id, diagnostic) {
|
|
326
381
|
if (!this.getMessage(id))
|
|
@@ -337,6 +392,7 @@ export class CollaborationStore {
|
|
|
337
392
|
return { message_id: id, thread_id: message.threadId, status: message.status, queued_at: message.createdAt,
|
|
338
393
|
delivered_at: message.deliveredAt, read_at: message.readAt, expires_at: message.expiresAt ?? null,
|
|
339
394
|
failure_reason: message.failureReason ?? null, delivery_semantics: message.deliverySource ?? (message.deliveredAt === null ? 'not_delivered' : 'legacy_or_unspecified'), read_semantics: message.readAt ? message.readSource ?? 'legacy_or_unspecified' : 'not_read',
|
|
395
|
+
snapshot: message.snapshot ?? null,
|
|
340
396
|
idempotency_key: message.idempotencyKey ?? null,
|
|
341
397
|
ack_at: replies.find((reply) => reply.responseKind === 'ack')?.createdAt ?? null,
|
|
342
398
|
reply_ids: replies.map((reply) => reply.id), result_ids: replies.filter((reply) => reply.responseKind === 'result').map((reply) => reply.id),
|
|
@@ -354,11 +410,29 @@ export class CollaborationStore {
|
|
|
354
410
|
for (const message of reports)
|
|
355
411
|
if (message.task)
|
|
356
412
|
tasks.set(message.task.task_id, { task_id: message.task.task_id, status: message.task.status, reported_at: message.createdAt, message_id: message.id });
|
|
413
|
+
// Aggregated from the *latest* report of each task, never inferred: an
|
|
414
|
+
// agent handed a task that has not yet replied is simply idle here —
|
|
415
|
+
// dispatch in flight is the sender-side receipt's concern (pending), not a
|
|
416
|
+
// fact about this session. blocked counts as active: the agent holds the
|
|
417
|
+
// task open. A failed terminal state only surfaces once no task is still
|
|
418
|
+
// in flight, since another in-progress task means the agent is busy.
|
|
419
|
+
const latest = [...tasks.values()];
|
|
420
|
+
const taskState = latest.some((task) => task.status === 'ack' || task.status === 'working' || task.status === 'blocked') ? 'active'
|
|
421
|
+
: latest.some((task) => task.status === 'failed') ? 'failed'
|
|
422
|
+
: 'idle';
|
|
357
423
|
return { session_state: online ? 'online' : 'offline', turn_state: turnState === 'done' ? 'ended' : turnState ?? 'unknown',
|
|
358
|
-
task_state:
|
|
424
|
+
task_state: taskState, tasks: [...tasks.values()], last_heartbeat: null, last_tool_activity_at: null,
|
|
359
425
|
last_message_at: reports.length ? Math.max(...reports.map((message) => message.createdAt)) : null,
|
|
360
426
|
state_source: 'optional_adapter', task_source: 'explicit_message' };
|
|
361
427
|
}
|
|
428
|
+
/** Timeline of one task as reported by a session, oldest first. The lineage
|
|
429
|
+
* starts at the recipient's first reply carrying that task_id — a bare
|
|
430
|
+
* dispatch carries no envelope. Messages without a task simply never
|
|
431
|
+
* match. */
|
|
432
|
+
byTask(sessionId, taskId) {
|
|
433
|
+
this.expire();
|
|
434
|
+
return this.document.messages.filter((message) => message.fromSessionId === sessionId && message.task?.task_id === taskId);
|
|
435
|
+
}
|
|
362
436
|
page(sessionId, options = {}) {
|
|
363
437
|
this.expire();
|
|
364
438
|
if (options.responseKind && !['ack', 'progress', 'result'].includes(options.responseKind))
|
|
@@ -1,10 +1,65 @@
|
|
|
1
1
|
import { buildBracketedSubmitBytes } from './promptDelivery.js';
|
|
2
|
-
|
|
2
|
+
const TMUX_KEY_NAMES = {
|
|
3
|
+
enter: 'Enter', escape: 'Escape', space: 'Space', left: 'Left', right: 'Right', up: 'Up', down: 'Down',
|
|
4
|
+
};
|
|
5
|
+
/** Screen text that means an interactive approval/confirm dialog is showing.
|
|
6
|
+
* Driving approve sends Enter, which activates the highlighted option — the
|
|
7
|
+
* number of tools that render such prompts is finite, but the phrasing is
|
|
8
|
+
* not, so match generously and never press keys without this evidence. */
|
|
9
|
+
const APPROVAL_DIALOG_PATTERNS = [
|
|
10
|
+
/requires approval/i,
|
|
11
|
+
/needs? approval/i,
|
|
12
|
+
/do you want to proceed/i,
|
|
13
|
+
/do you want to (continue|run this)/i,
|
|
14
|
+
/is this ok/i,
|
|
15
|
+
/\b1\.\s*yes\b/i,
|
|
16
|
+
/需要批准|需要授权|等待批准/i,
|
|
17
|
+
/是否(允许|继续|执行|批准)/,
|
|
18
|
+
/\b1\.\s*是\b/,
|
|
19
|
+
/允许.*执行|确认.*执行/i,
|
|
20
|
+
];
|
|
21
|
+
export function detectApprovalDialog(content) {
|
|
22
|
+
return APPROVAL_DIALOG_PATTERNS.some((pattern) => pattern.test(content));
|
|
23
|
+
}
|
|
24
|
+
async function assertSamePane(run, pane) {
|
|
3
25
|
const identity = (await run(['display-message', '-p', '-t', pane.paneId,
|
|
4
26
|
'#{pid}:#{session_id}:#{pane_id}:#{pane_pid}'])).trim();
|
|
5
27
|
if (identity !== `${pane.serverPid}:${pane.sessionId}:${pane.paneId}:${pane.panePid}`)
|
|
6
28
|
throw new Error('TMUX_PANE_CHANGED');
|
|
29
|
+
}
|
|
30
|
+
export async function writeCollaborationTmuxPane(run, pane, prompt) {
|
|
31
|
+
await assertSamePane(run, pane);
|
|
7
32
|
// A fixed pane target is independent of the current window, keyboard focus
|
|
8
33
|
// and browser presence. One invocation preserves the bracketed submit bytes.
|
|
9
34
|
await run(['send-keys', '-t', pane.paneId, '-l', '--', buildBracketedSubmitBytes(prompt)]);
|
|
10
35
|
}
|
|
36
|
+
/** Inject a named key into a member pane after proving the pane identity
|
|
37
|
+
* unchanged. Named keys only — no freeform keystroke passthrough. */
|
|
38
|
+
export async function sendTmuxPaneKey(run, pane, key) {
|
|
39
|
+
await assertSamePane(run, pane);
|
|
40
|
+
await run(['send-keys', '-t', pane.paneId, TMUX_KEY_NAMES[key]]);
|
|
41
|
+
}
|
|
42
|
+
/** Current pane screen text (viewport), identity-checked. */
|
|
43
|
+
export async function captureTmuxPaneText(run, pane) {
|
|
44
|
+
await assertSamePane(run, pane);
|
|
45
|
+
const raw = await run(['capture-pane', '-p', '-J', '-t', pane.paneId]);
|
|
46
|
+
return raw.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, '').replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
|
|
47
|
+
}
|
|
48
|
+
/** Dismiss an interactive approval dialog on the member pane by pressing
|
|
49
|
+
* Enter (the highlighted option). Refuses unless the screen actually shows
|
|
50
|
+
* an approval prompt: driving a dialog that isn't there would only inject
|
|
51
|
+
* a stray keystroke into the agent's work. Returns false when no dialog. */
|
|
52
|
+
export async function approveCollaborationDialog(run, pane) {
|
|
53
|
+
const content = await captureTmuxPaneText(run, pane);
|
|
54
|
+
if (!detectApprovalDialog(content))
|
|
55
|
+
return false;
|
|
56
|
+
await sendTmuxPaneKey(run, pane, 'enter');
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/** History-inclusive pane text (scrollback + viewport) used to confirm a
|
|
60
|
+
* delivery was actually rendered by the agent, not wiped by a boot clear. */
|
|
61
|
+
export async function captureTmuxPaneHistory(run, pane, lines = 800) {
|
|
62
|
+
await assertSamePane(run, pane);
|
|
63
|
+
const raw = await run(['capture-pane', '-p', '-J', '-S', `-${lines}`, '-t', pane.paneId]);
|
|
64
|
+
return raw.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, '').replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
|
|
65
|
+
}
|