termdock 1.4.322 → 1.4.324

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/dist/client/assets/App-7IUcmCLC.js +186 -0
  2. package/dist/client/assets/App-7IUcmCLC.js.br +0 -0
  3. package/dist/client/assets/App-7IUcmCLC.js.gz +0 -0
  4. package/dist/client/assets/{ArchitecturePanel-VDM6hrwp.js → ArchitecturePanel-CUAB0wFe.js} +6 -6
  5. package/dist/client/assets/ArchitecturePanel-CUAB0wFe.js.br +0 -0
  6. package/dist/client/assets/ArchitecturePanel-CUAB0wFe.js.gz +0 -0
  7. package/dist/client/assets/{ChangeWalkthroughPanel-B64j3i8o.js → ChangeWalkthroughPanel-CKIG8diY.js} +2 -2
  8. package/dist/client/assets/ChangeWalkthroughPanel-CKIG8diY.js.br +0 -0
  9. package/dist/client/assets/ChangeWalkthroughPanel-CKIG8diY.js.gz +0 -0
  10. package/dist/client/assets/CollaborationMainWorkspace-CIq-JAbF.js +1 -0
  11. package/dist/client/assets/CollaborationMainWorkspace-CIq-JAbF.js.br +0 -0
  12. package/dist/client/assets/CollaborationMainWorkspace-CIq-JAbF.js.gz +0 -0
  13. package/dist/client/assets/CollaborationTaskContent-7vWDzH-d.js +2 -0
  14. package/dist/client/assets/{ComputerControlView-Co-TTYas.js → ComputerControlView-CPSI-M86.js} +2 -2
  15. package/dist/client/assets/ComputerControlView-CPSI-M86.js.br +0 -0
  16. package/dist/client/assets/ComputerControlView-CPSI-M86.js.gz +0 -0
  17. package/dist/client/assets/{DagPlayground-kJczdJ6J.js → DagPlayground-BxocNRPm.js} +1 -1
  18. package/dist/client/assets/DagPlayground-BxocNRPm.js.br +0 -0
  19. package/dist/client/assets/DagPlayground-BxocNRPm.js.gz +0 -0
  20. package/dist/client/assets/{DiffLab-BaaioC-q.js → DiffLab-COGOFO0s.js} +2 -2
  21. package/dist/client/assets/DiffLab-COGOFO0s.js.br +0 -0
  22. package/dist/client/assets/DiffLab-COGOFO0s.js.gz +0 -0
  23. package/dist/client/assets/{DiffReview-Wfck3RIS.js → DiffReview-5ERdwz1C.js} +1 -1
  24. package/dist/client/assets/DiffReview-5ERdwz1C.js.br +0 -0
  25. package/dist/client/assets/DiffReview-5ERdwz1C.js.gz +0 -0
  26. package/dist/client/assets/{DiffReviewLab-Dblr3wgw.js → DiffReviewLab-BSz9IND6.js} +1 -1
  27. package/dist/client/assets/DiffReviewLab-BSz9IND6.js.br +0 -0
  28. package/dist/client/assets/DiffReviewLab-BSz9IND6.js.gz +0 -0
  29. package/dist/client/assets/{DiffViewer-C-utC5Ga.js → DiffViewer-RT-p-ZBo.js} +8 -8
  30. package/dist/client/assets/DiffViewer-RT-p-ZBo.js.br +0 -0
  31. package/dist/client/assets/DiffViewer-RT-p-ZBo.js.gz +0 -0
  32. package/dist/client/assets/{ModelPreview-BBLnHWVQ.js → ModelPreview-DI9anVWA.js} +1 -1
  33. package/dist/client/assets/ModelPreview-DI9anVWA.js.br +0 -0
  34. package/dist/client/assets/ModelPreview-DI9anVWA.js.gz +0 -0
  35. package/dist/client/assets/{RightSidebar-CUHSFkmZ.js → RightSidebar-DOLn8R7c.js} +36 -36
  36. package/dist/client/assets/RightSidebar-DOLn8R7c.js.br +0 -0
  37. package/dist/client/assets/RightSidebar-DOLn8R7c.js.gz +0 -0
  38. package/dist/client/assets/{abnfDiagram-VCTEODGH-DDd99QpA.js → abnfDiagram-VCTEODGH-CXd0unH5.js} +1 -1
  39. package/dist/client/assets/abnfDiagram-VCTEODGH-CXd0unH5.js.br +0 -0
  40. package/dist/client/assets/abnfDiagram-VCTEODGH-CXd0unH5.js.gz +0 -0
  41. package/dist/client/assets/app-DCFOZi_L.js +35 -0
  42. package/dist/client/assets/app-DCFOZi_L.js.br +0 -0
  43. package/dist/client/assets/app-DCFOZi_L.js.gz +0 -0
  44. package/dist/client/assets/{arc-C3dck_E0.js → arc-DCqJmX8M.js} +1 -1
  45. package/dist/client/assets/arc-DCqJmX8M.js.br +0 -0
  46. package/dist/client/assets/arc-DCqJmX8M.js.gz +0 -0
  47. package/dist/client/assets/{architectureDiagram-5GKGNRK7-C3bCQc2w.js → architectureDiagram-5GKGNRK7-DGu_m54H.js} +1 -1
  48. package/dist/client/assets/architectureDiagram-5GKGNRK7-DGu_m54H.js.br +0 -0
  49. package/dist/client/assets/architectureDiagram-5GKGNRK7-DGu_m54H.js.gz +0 -0
  50. package/dist/client/assets/{blockDiagram-I7D4REHJ-znMn6iCQ.js → blockDiagram-I7D4REHJ-BI0gsxDN.js} +1 -1
  51. package/dist/client/assets/blockDiagram-I7D4REHJ-BI0gsxDN.js.br +0 -0
  52. package/dist/client/assets/blockDiagram-I7D4REHJ-BI0gsxDN.js.gz +0 -0
  53. package/dist/client/assets/{c4Diagram-7LVT6UL2-C0sopIsj.js → c4Diagram-7LVT6UL2-Ct3xzGtD.js} +1 -1
  54. package/dist/client/assets/c4Diagram-7LVT6UL2-Ct3xzGtD.js.br +0 -0
  55. package/dist/client/assets/c4Diagram-7LVT6UL2-Ct3xzGtD.js.gz +0 -0
  56. package/dist/client/assets/channel-BLSCUksA.js +1 -0
  57. package/dist/client/assets/{chunk-2Q5K7J3B-f7USyccJ.js → chunk-2Q5K7J3B-BPd2UShA.js} +1 -1
  58. package/dist/client/assets/{chunk-5VM5RSS4-Ck2xirDs.js → chunk-5VM5RSS4-CoFZ5Fo3.js} +1 -1
  59. package/dist/client/assets/{chunk-F27PBJKO-DaDCIj30.js → chunk-F27PBJKO-C5gZrGp4.js} +1 -1
  60. package/dist/client/assets/chunk-F27PBJKO-C5gZrGp4.js.br +0 -0
  61. package/dist/client/assets/chunk-F27PBJKO-C5gZrGp4.js.gz +0 -0
  62. package/dist/client/assets/{chunk-IMKFNOWR-DEC_On5x.js → chunk-IMKFNOWR-GW8b9ya-.js} +1 -1
  63. package/dist/client/assets/chunk-IMKFNOWR-GW8b9ya-.js.br +0 -0
  64. package/dist/client/assets/chunk-IMKFNOWR-GW8b9ya-.js.gz +0 -0
  65. package/dist/client/assets/{chunk-JWPE2WC7-BrswC3n2.js → chunk-JWPE2WC7-GpMoViQf.js} +1 -1
  66. package/dist/client/assets/{chunk-POPQ4Y6H-ClMbDV47.js → chunk-POPQ4Y6H-C_vgVPaw.js} +1 -1
  67. package/dist/client/assets/{chunk-SVP7TREG-D7_1VwC5.js → chunk-SVP7TREG-bA7RXwWn.js} +1 -1
  68. package/dist/client/assets/chunk-SVP7TREG-bA7RXwWn.js.br +0 -0
  69. package/dist/client/assets/chunk-SVP7TREG-bA7RXwWn.js.gz +0 -0
  70. package/dist/client/assets/{chunk-TICWLB2K-D5jzVTZ8.js → chunk-TICWLB2K-BG8DAtll.js} +1 -1
  71. package/dist/client/assets/chunk-TICWLB2K-BG8DAtll.js.br +0 -0
  72. package/dist/client/assets/chunk-TICWLB2K-BG8DAtll.js.gz +0 -0
  73. package/dist/client/assets/{chunk-XXDRQBXY-DOhaoLcX.js → chunk-XXDRQBXY-Ces56sud.js} +1 -1
  74. package/dist/client/assets/classDiagram-ZZMXUADV-DkVIqOvw.js +1 -0
  75. package/dist/client/assets/classDiagram-v2-VYDZK3BY-DkVIqOvw.js +1 -0
  76. package/dist/client/assets/{cose-bilkent-JH36ORCC-DOsaKgiX.js → cose-bilkent-JH36ORCC-Bwch0qL5.js} +1 -1
  77. package/dist/client/assets/cose-bilkent-JH36ORCC-Bwch0qL5.js.br +0 -0
  78. package/dist/client/assets/cose-bilkent-JH36ORCC-Bwch0qL5.js.gz +0 -0
  79. package/dist/client/assets/{cynefin-OW5HDTMX-CMcLh_wh.js → cynefin-OW5HDTMX-CrAXyA2Z.js} +1 -1
  80. package/dist/client/assets/cynefin-OW5HDTMX-CrAXyA2Z.js.br +0 -0
  81. package/dist/client/assets/cynefin-OW5HDTMX-CrAXyA2Z.js.gz +0 -0
  82. package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-Dd8bfq9V.js → cynefinDiagram-5FMLGOSQ-YlP0Q93l.js} +1 -1
  83. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-YlP0Q93l.js.br +0 -0
  84. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-YlP0Q93l.js.gz +0 -0
  85. package/dist/client/assets/{dagre-GXQ25YYZ-BlW7fAi0.js → dagre-GXQ25YYZ-CZ4XcPK9.js} +1 -1
  86. package/dist/client/assets/dagre-GXQ25YYZ-CZ4XcPK9.js.br +0 -0
  87. package/dist/client/assets/dagre-GXQ25YYZ-CZ4XcPK9.js.gz +0 -0
  88. package/dist/client/assets/{diagram-S7CK7UJ4-B0-tKahG.js → diagram-S7CK7UJ4-c_8HfHGA.js} +1 -1
  89. package/dist/client/assets/{diagram-S7CK7UJ4-B0-tKahG.js.br → diagram-S7CK7UJ4-c_8HfHGA.js.br} +0 -0
  90. package/dist/client/assets/diagram-S7CK7UJ4-c_8HfHGA.js.gz +0 -0
  91. package/dist/client/assets/{diagram-UQ7AKVKN-BQumj4Dw.js → diagram-UQ7AKVKN-BGtRfxMR.js} +1 -1
  92. package/dist/client/assets/diagram-UQ7AKVKN-BGtRfxMR.js.br +0 -0
  93. package/dist/client/assets/diagram-UQ7AKVKN-BGtRfxMR.js.gz +0 -0
  94. package/dist/client/assets/{diagram-VSXAHHWV-Zx_FLmPP.js → diagram-VSXAHHWV-_9yZnhuv.js} +1 -1
  95. package/dist/client/assets/diagram-VSXAHHWV-_9yZnhuv.js.br +0 -0
  96. package/dist/client/assets/diagram-VSXAHHWV-_9yZnhuv.js.gz +0 -0
  97. package/dist/client/assets/{diagram-VX7I27RA-CEZvatNH.js → diagram-VX7I27RA-C_mnc4_u.js} +1 -1
  98. package/dist/client/assets/diagram-VX7I27RA-C_mnc4_u.js.br +0 -0
  99. package/dist/client/assets/diagram-VX7I27RA-C_mnc4_u.js.gz +0 -0
  100. package/dist/client/assets/{diagram-Z3DM3KII-CJ9IEQ8j.js → diagram-Z3DM3KII-sZ_AiSpj.js} +1 -1
  101. package/dist/client/assets/diagram-Z3DM3KII-sZ_AiSpj.js.br +0 -0
  102. package/dist/client/assets/diagram-Z3DM3KII-sZ_AiSpj.js.gz +0 -0
  103. package/dist/client/assets/{ebnfDiagram-PWID7BFC-sQQMv3ea.js → ebnfDiagram-PWID7BFC-Dm-BXeqS.js} +1 -1
  104. package/dist/client/assets/ebnfDiagram-PWID7BFC-Dm-BXeqS.js.br +0 -0
  105. package/dist/client/assets/ebnfDiagram-PWID7BFC-Dm-BXeqS.js.gz +0 -0
  106. package/dist/client/assets/{erDiagram-RLTQ6QDP-D3rMpj7M.js → erDiagram-RLTQ6QDP-Dz4cBepd.js} +1 -1
  107. package/dist/client/assets/erDiagram-RLTQ6QDP-Dz4cBepd.js.br +0 -0
  108. package/dist/client/assets/erDiagram-RLTQ6QDP-Dz4cBepd.js.gz +0 -0
  109. package/dist/client/assets/{flowDiagram-HODETNUW-B6QZiE0-.js → flowDiagram-HODETNUW-CbRG8-WW.js} +1 -1
  110. package/dist/client/assets/flowDiagram-HODETNUW-CbRG8-WW.js.br +0 -0
  111. package/dist/client/assets/flowDiagram-HODETNUW-CbRG8-WW.js.gz +0 -0
  112. package/dist/client/assets/{ganttDiagram-EL5Y4UJY-Bw9bltcu.js → ganttDiagram-EL5Y4UJY-CmBXWlr0.js} +1 -1
  113. package/dist/client/assets/ganttDiagram-EL5Y4UJY-CmBXWlr0.js.br +0 -0
  114. package/dist/client/assets/ganttDiagram-EL5Y4UJY-CmBXWlr0.js.gz +0 -0
  115. package/dist/client/assets/{gestureArbiter-Cczz34oI.js → gestureArbiter-B26W6rS7.js} +1 -1
  116. package/dist/client/assets/gestureArbiter-B26W6rS7.js.br +0 -0
  117. package/dist/client/assets/gestureArbiter-B26W6rS7.js.gz +0 -0
  118. package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-mdnkxjeD.js → gitGraphDiagram-WWUBYQGX-JW5QMYEk.js} +1 -1
  119. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-JW5QMYEk.js.br +0 -0
  120. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-JW5QMYEk.js.gz +0 -0
  121. package/dist/client/assets/index-Ctz-IDjR.css +1 -0
  122. package/dist/client/assets/index-Ctz-IDjR.css.br +0 -0
  123. package/dist/client/assets/{index-B4bEMUyn.css.gz → index-Ctz-IDjR.css.gz} +0 -0
  124. package/dist/client/assets/{infoDiagram-27XIBGKW-DyrPP3Yf.js → infoDiagram-27XIBGKW-CsA6WdPn.js} +1 -1
  125. package/dist/client/assets/{ishikawaDiagram-5VMMS53U-CEPH_ePM.js → ishikawaDiagram-5VMMS53U-CsKPUwTw.js} +1 -1
  126. package/dist/client/assets/ishikawaDiagram-5VMMS53U-CsKPUwTw.js.br +0 -0
  127. package/dist/client/assets/ishikawaDiagram-5VMMS53U-CsKPUwTw.js.gz +0 -0
  128. package/dist/client/assets/{journeyDiagram-3NMN7TZE-CQ0PjBy7.js → journeyDiagram-3NMN7TZE-DpN98gN0.js} +1 -1
  129. package/dist/client/assets/journeyDiagram-3NMN7TZE-DpN98gN0.js.br +0 -0
  130. package/dist/client/assets/journeyDiagram-3NMN7TZE-DpN98gN0.js.gz +0 -0
  131. package/dist/client/assets/{kanban-definition-UXKFOSKX-BpQUXA60.js → kanban-definition-UXKFOSKX-RHO--43O.js} +1 -1
  132. package/dist/client/assets/kanban-definition-UXKFOSKX-RHO--43O.js.br +0 -0
  133. package/dist/client/assets/kanban-definition-UXKFOSKX-RHO--43O.js.gz +0 -0
  134. package/dist/client/assets/{layout-BdptC7rq.js → layout-Cm_prknW.js} +1 -1
  135. package/dist/client/assets/layout-Cm_prknW.js.br +0 -0
  136. package/dist/client/assets/layout-Cm_prknW.js.gz +0 -0
  137. package/dist/client/assets/{linear-C2cSRCxu.js → linear-BVAsPkvS.js} +1 -1
  138. package/dist/client/assets/linear-BVAsPkvS.js.br +0 -0
  139. package/dist/client/assets/linear-BVAsPkvS.js.gz +0 -0
  140. package/dist/client/assets/{mermaid.core-ClVMFyAU.js → mermaid.core-ufe77Xaz.js} +5 -5
  141. package/dist/client/assets/mermaid.core-ufe77Xaz.js.br +0 -0
  142. package/dist/client/assets/mermaid.core-ufe77Xaz.js.gz +0 -0
  143. package/dist/client/assets/{mindmap-definition-YA3MSWOX-D0AFCmsY.js → mindmap-definition-YA3MSWOX-CRyy1U2d.js} +1 -1
  144. package/dist/client/assets/mindmap-definition-YA3MSWOX-CRyy1U2d.js.br +0 -0
  145. package/dist/client/assets/mindmap-definition-YA3MSWOX-CRyy1U2d.js.gz +0 -0
  146. package/dist/client/assets/passwordBootstrap-3_SIzSH7.js +2 -0
  147. package/dist/client/assets/passwordBootstrap-3_SIzSH7.js.br +0 -0
  148. package/dist/client/assets/passwordBootstrap-3_SIzSH7.js.gz +0 -0
  149. package/dist/client/assets/{pegDiagram-XKGWAZYB-BGAh0bwg.js → pegDiagram-XKGWAZYB-DSHgdU9q.js} +1 -1
  150. package/dist/client/assets/pegDiagram-XKGWAZYB-DSHgdU9q.js.br +0 -0
  151. package/dist/client/assets/pegDiagram-XKGWAZYB-DSHgdU9q.js.gz +0 -0
  152. package/dist/client/assets/{pieDiagram-E7YTZNPT-CDOZUF44.js → pieDiagram-E7YTZNPT-lIUeAuhN.js} +1 -1
  153. package/dist/client/assets/pieDiagram-E7YTZNPT-lIUeAuhN.js.br +0 -0
  154. package/dist/client/assets/pieDiagram-E7YTZNPT-lIUeAuhN.js.gz +0 -0
  155. package/dist/client/assets/{quadrantDiagram-AXDQQJYC-D_Qn6NTU.js → quadrantDiagram-AXDQQJYC-CIgG9vBX.js} +1 -1
  156. package/dist/client/assets/quadrantDiagram-AXDQQJYC-CIgG9vBX.js.br +0 -0
  157. package/dist/client/assets/quadrantDiagram-AXDQQJYC-CIgG9vBX.js.gz +0 -0
  158. package/dist/client/assets/{railroadDiagram-O6MQD6OU-qUVGWhJN.js → railroadDiagram-O6MQD6OU-DjP8oesA.js} +1 -1
  159. package/dist/client/assets/railroadDiagram-O6MQD6OU-DjP8oesA.js.br +0 -0
  160. package/dist/client/assets/railroadDiagram-O6MQD6OU-DjP8oesA.js.gz +0 -0
  161. package/dist/client/assets/{rdpSession-hgCM4i-j.js → rdpSession-BshIobdj.js} +1 -1
  162. package/dist/client/assets/rdpSession-BshIobdj.js.br +0 -0
  163. package/dist/client/assets/rdpSession-BshIobdj.js.gz +0 -0
  164. package/dist/client/assets/{referenceLongPress-BEMsP11a.js → referenceLongPress-DBJnfFHa.js} +1 -1
  165. package/dist/client/assets/referenceLongPress-DBJnfFHa.js.br +0 -0
  166. package/dist/client/assets/referenceLongPress-DBJnfFHa.js.gz +0 -0
  167. package/dist/client/assets/{requirementDiagram-BXWQKSXE-BBQMQuVZ.js → requirementDiagram-BXWQKSXE-Dbvu6IC_.js} +1 -1
  168. package/dist/client/assets/requirementDiagram-BXWQKSXE-Dbvu6IC_.js.br +0 -0
  169. package/dist/client/assets/requirementDiagram-BXWQKSXE-Dbvu6IC_.js.gz +0 -0
  170. package/dist/client/assets/{sankeyDiagram-P5KCCOFB-CeEExsqW.js → sankeyDiagram-P5KCCOFB-1mZ4s-lv.js} +1 -1
  171. package/dist/client/assets/{sankeyDiagram-P5KCCOFB-CeEExsqW.js.br → sankeyDiagram-P5KCCOFB-1mZ4s-lv.js.br} +0 -0
  172. package/dist/client/assets/sankeyDiagram-P5KCCOFB-1mZ4s-lv.js.gz +0 -0
  173. package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-7ZPO-X0a.js → sequenceDiagram-WJ2MYXX4-Cu3hhzlV.js} +1 -1
  174. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Cu3hhzlV.js.br +0 -0
  175. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Cu3hhzlV.js.gz +0 -0
  176. package/dist/client/assets/{sizeCapture-INFHLROL-MBjz7Snm.js → sizeCapture-INFHLROL-xfm69HlR.js} +1 -1
  177. package/dist/client/assets/sizeCapture-INFHLROL-xfm69HlR.js.br +0 -0
  178. package/dist/client/assets/sizeCapture-INFHLROL-xfm69HlR.js.gz +0 -0
  179. package/dist/client/assets/{sparkles-C9ShmGPf.js → sparkles-CDonlwGu.js} +1 -1
  180. package/dist/client/assets/{stateDiagram-D77RDMKH-DtLp89uP.js → stateDiagram-D77RDMKH-u-bh6mDK.js} +1 -1
  181. package/dist/client/assets/stateDiagram-D77RDMKH-u-bh6mDK.js.br +0 -0
  182. package/dist/client/assets/stateDiagram-D77RDMKH-u-bh6mDK.js.gz +0 -0
  183. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BQ_m8Gzd.js +1 -0
  184. package/dist/client/assets/{swimlanes-42K2YHIH-Bc02S1q-.js → swimlanes-42K2YHIH-U8WN__SO.js} +1 -1
  185. package/dist/client/assets/swimlanes-42K2YHIH-U8WN__SO.js.br +0 -0
  186. package/dist/client/assets/swimlanes-42K2YHIH-U8WN__SO.js.gz +0 -0
  187. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-FDEo3QWw.js +8 -0
  188. package/dist/client/assets/{timeline-definition-24CTP7MA-BLLwbOm5.js → timeline-definition-24CTP7MA-CMsoNo4F.js} +1 -1
  189. package/dist/client/assets/timeline-definition-24CTP7MA-CMsoNo4F.js.br +0 -0
  190. package/dist/client/assets/timeline-definition-24CTP7MA-CMsoNo4F.js.gz +0 -0
  191. package/dist/client/assets/{triangle-alert-i_L8ws6U.js → triangle-alert-BgErMO2V.js} +1 -1
  192. package/dist/client/assets/{vennDiagram-4TSXK5OY-8FcrAdbk.js → vennDiagram-4TSXK5OY-CPE6pVwh.js} +1 -1
  193. package/dist/client/assets/{vennDiagram-4TSXK5OY-8FcrAdbk.js.br → vennDiagram-4TSXK5OY-CPE6pVwh.js.br} +0 -0
  194. package/dist/client/assets/vennDiagram-4TSXK5OY-CPE6pVwh.js.gz +0 -0
  195. package/dist/client/assets/{videoCompression-RIaDabW4.js → videoCompression-BWkZvEjM.js} +1 -1
  196. package/dist/client/assets/videoCompression-BWkZvEjM.js.br +0 -0
  197. package/dist/client/assets/videoCompression-BWkZvEjM.js.gz +0 -0
  198. package/dist/client/assets/{wardleyDiagram-VM6X3IG4-paL2wRrJ.js → wardleyDiagram-VM6X3IG4-4lWd-R4N.js} +1 -1
  199. package/dist/client/assets/wardleyDiagram-VM6X3IG4-4lWd-R4N.js.br +0 -0
  200. package/dist/client/assets/wardleyDiagram-VM6X3IG4-4lWd-R4N.js.gz +0 -0
  201. package/dist/client/assets/{xychartDiagram-S5SC5T6Z-BM5qJ4E7.js → xychartDiagram-S5SC5T6Z-CpKJMCWV.js} +1 -1
  202. package/dist/client/assets/xychartDiagram-S5SC5T6Z-CpKJMCWV.js.br +0 -0
  203. package/dist/client/assets/xychartDiagram-S5SC5T6Z-CpKJMCWV.js.gz +0 -0
  204. package/dist/client/index.html +2 -2
  205. package/dist/client/index.html.br +0 -0
  206. package/dist/client/index.html.gz +0 -0
  207. package/dist/client/sw-encrypted-background.js +1 -1
  208. package/dist/client/sw-encrypted-background.js.br +0 -0
  209. package/dist/client/sw-encrypted-background.js.gz +0 -0
  210. package/dist/client/sw.js +1 -1
  211. package/dist/client/sw.js.br +0 -0
  212. package/dist/client/sw.js.gz +0 -0
  213. package/dist/server/agent/collaborationGroupRoutes.js +15 -0
  214. package/dist/server/agent/collaborationStore.js +63 -2
  215. package/dist/server/agent/collaborationTaskRoutes.js +5 -1
  216. package/dist/server/agent/integrationCli.js +38 -20
  217. package/dist/server/agent/integrationServer.js +18 -1
  218. package/dist/server/federation/packets.js +179 -28
  219. package/dist/server/federation/requestContext.js +6 -1
  220. package/dist/server/federation/runtime.js +19 -8
  221. package/dist/server/routes/terminal.js +60 -20
  222. package/dist/server/utils/autoUpdate.js +55 -8
  223. package/dist/server/utils/npmUpdate.js +84 -8
  224. package/dist/server/utils/terminalPasteBuffer.js +66 -0
  225. package/dist/server/utils/tmuxClientCapabilities.js +4 -0
  226. package/dist/server/utils/tmuxCopySelection.js +94 -0
  227. package/dist/server/utils/tmuxEmptyServerProbe.js +15 -0
  228. package/docs/integrations.md +36 -0
  229. package/package.json +5 -2
  230. package/runtime-manifest.json +3 -3
  231. package/.desktop-dist/main.js +0 -1989
  232. package/dist/client/assets/App-DEKo4SNL.js +0 -186
  233. package/dist/client/assets/App-DEKo4SNL.js.br +0 -0
  234. package/dist/client/assets/App-DEKo4SNL.js.gz +0 -0
  235. package/dist/client/assets/ArchitecturePanel-VDM6hrwp.js.br +0 -0
  236. package/dist/client/assets/ArchitecturePanel-VDM6hrwp.js.gz +0 -0
  237. package/dist/client/assets/ChangeWalkthroughPanel-B64j3i8o.js.br +0 -0
  238. package/dist/client/assets/ChangeWalkthroughPanel-B64j3i8o.js.gz +0 -0
  239. package/dist/client/assets/CollaborationMainWorkspace-DG8TQNqo.js +0 -1
  240. package/dist/client/assets/CollaborationMainWorkspace-DG8TQNqo.js.br +0 -0
  241. package/dist/client/assets/CollaborationMainWorkspace-DG8TQNqo.js.gz +0 -0
  242. package/dist/client/assets/CollaborationTaskContent-msQsgVaI.js +0 -2
  243. package/dist/client/assets/ComputerControlView-Co-TTYas.js.br +0 -0
  244. package/dist/client/assets/ComputerControlView-Co-TTYas.js.gz +0 -0
  245. package/dist/client/assets/DagPlayground-kJczdJ6J.js.br +0 -0
  246. package/dist/client/assets/DagPlayground-kJczdJ6J.js.gz +0 -0
  247. package/dist/client/assets/DiffLab-BaaioC-q.js.br +0 -0
  248. package/dist/client/assets/DiffLab-BaaioC-q.js.gz +0 -0
  249. package/dist/client/assets/DiffReview-Wfck3RIS.js.br +0 -0
  250. package/dist/client/assets/DiffReview-Wfck3RIS.js.gz +0 -0
  251. package/dist/client/assets/DiffReviewLab-Dblr3wgw.js.br +0 -0
  252. package/dist/client/assets/DiffReviewLab-Dblr3wgw.js.gz +0 -0
  253. package/dist/client/assets/DiffViewer-C-utC5Ga.js.br +0 -0
  254. package/dist/client/assets/DiffViewer-C-utC5Ga.js.gz +0 -0
  255. package/dist/client/assets/ModelPreview-BBLnHWVQ.js.br +0 -0
  256. package/dist/client/assets/ModelPreview-BBLnHWVQ.js.gz +0 -0
  257. package/dist/client/assets/RightSidebar-CUHSFkmZ.js.br +0 -0
  258. package/dist/client/assets/RightSidebar-CUHSFkmZ.js.gz +0 -0
  259. package/dist/client/assets/abnfDiagram-VCTEODGH-DDd99QpA.js.br +0 -0
  260. package/dist/client/assets/abnfDiagram-VCTEODGH-DDd99QpA.js.gz +0 -0
  261. package/dist/client/assets/app-CfqF7_nh.js +0 -35
  262. package/dist/client/assets/app-CfqF7_nh.js.br +0 -0
  263. package/dist/client/assets/app-CfqF7_nh.js.gz +0 -0
  264. package/dist/client/assets/arc-C3dck_E0.js.br +0 -0
  265. package/dist/client/assets/arc-C3dck_E0.js.gz +0 -0
  266. package/dist/client/assets/architectureDiagram-5GKGNRK7-C3bCQc2w.js.br +0 -0
  267. package/dist/client/assets/architectureDiagram-5GKGNRK7-C3bCQc2w.js.gz +0 -0
  268. package/dist/client/assets/blockDiagram-I7D4REHJ-znMn6iCQ.js.br +0 -0
  269. package/dist/client/assets/blockDiagram-I7D4REHJ-znMn6iCQ.js.gz +0 -0
  270. package/dist/client/assets/c4Diagram-7LVT6UL2-C0sopIsj.js.br +0 -0
  271. package/dist/client/assets/c4Diagram-7LVT6UL2-C0sopIsj.js.gz +0 -0
  272. package/dist/client/assets/channel-CMC8t83j.js +0 -1
  273. package/dist/client/assets/chunk-F27PBJKO-DaDCIj30.js.br +0 -0
  274. package/dist/client/assets/chunk-F27PBJKO-DaDCIj30.js.gz +0 -0
  275. package/dist/client/assets/chunk-IMKFNOWR-DEC_On5x.js.br +0 -0
  276. package/dist/client/assets/chunk-IMKFNOWR-DEC_On5x.js.gz +0 -0
  277. package/dist/client/assets/chunk-SVP7TREG-D7_1VwC5.js.br +0 -0
  278. package/dist/client/assets/chunk-SVP7TREG-D7_1VwC5.js.gz +0 -0
  279. package/dist/client/assets/chunk-TICWLB2K-D5jzVTZ8.js.br +0 -0
  280. package/dist/client/assets/chunk-TICWLB2K-D5jzVTZ8.js.gz +0 -0
  281. package/dist/client/assets/classDiagram-ZZMXUADV-ElMHzA-E.js +0 -1
  282. package/dist/client/assets/classDiagram-v2-VYDZK3BY-ElMHzA-E.js +0 -1
  283. package/dist/client/assets/cose-bilkent-JH36ORCC-DOsaKgiX.js.br +0 -0
  284. package/dist/client/assets/cose-bilkent-JH36ORCC-DOsaKgiX.js.gz +0 -0
  285. package/dist/client/assets/cynefin-OW5HDTMX-CMcLh_wh.js.br +0 -0
  286. package/dist/client/assets/cynefin-OW5HDTMX-CMcLh_wh.js.gz +0 -0
  287. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Dd8bfq9V.js.br +0 -0
  288. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Dd8bfq9V.js.gz +0 -0
  289. package/dist/client/assets/dagre-GXQ25YYZ-BlW7fAi0.js.br +0 -0
  290. package/dist/client/assets/dagre-GXQ25YYZ-BlW7fAi0.js.gz +0 -0
  291. package/dist/client/assets/diagram-S7CK7UJ4-B0-tKahG.js.gz +0 -0
  292. package/dist/client/assets/diagram-UQ7AKVKN-BQumj4Dw.js.br +0 -0
  293. package/dist/client/assets/diagram-UQ7AKVKN-BQumj4Dw.js.gz +0 -0
  294. package/dist/client/assets/diagram-VSXAHHWV-Zx_FLmPP.js.br +0 -0
  295. package/dist/client/assets/diagram-VSXAHHWV-Zx_FLmPP.js.gz +0 -0
  296. package/dist/client/assets/diagram-VX7I27RA-CEZvatNH.js.br +0 -0
  297. package/dist/client/assets/diagram-VX7I27RA-CEZvatNH.js.gz +0 -0
  298. package/dist/client/assets/diagram-Z3DM3KII-CJ9IEQ8j.js.br +0 -0
  299. package/dist/client/assets/diagram-Z3DM3KII-CJ9IEQ8j.js.gz +0 -0
  300. package/dist/client/assets/ebnfDiagram-PWID7BFC-sQQMv3ea.js.br +0 -0
  301. package/dist/client/assets/ebnfDiagram-PWID7BFC-sQQMv3ea.js.gz +0 -0
  302. package/dist/client/assets/erDiagram-RLTQ6QDP-D3rMpj7M.js.br +0 -0
  303. package/dist/client/assets/erDiagram-RLTQ6QDP-D3rMpj7M.js.gz +0 -0
  304. package/dist/client/assets/flowDiagram-HODETNUW-B6QZiE0-.js.br +0 -0
  305. package/dist/client/assets/flowDiagram-HODETNUW-B6QZiE0-.js.gz +0 -0
  306. package/dist/client/assets/ganttDiagram-EL5Y4UJY-Bw9bltcu.js.br +0 -0
  307. package/dist/client/assets/ganttDiagram-EL5Y4UJY-Bw9bltcu.js.gz +0 -0
  308. package/dist/client/assets/gestureArbiter-Cczz34oI.js.br +0 -0
  309. package/dist/client/assets/gestureArbiter-Cczz34oI.js.gz +0 -0
  310. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-mdnkxjeD.js.br +0 -0
  311. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-mdnkxjeD.js.gz +0 -0
  312. package/dist/client/assets/index-B4bEMUyn.css +0 -1
  313. package/dist/client/assets/index-B4bEMUyn.css.br +0 -0
  314. package/dist/client/assets/ishikawaDiagram-5VMMS53U-CEPH_ePM.js.br +0 -0
  315. package/dist/client/assets/ishikawaDiagram-5VMMS53U-CEPH_ePM.js.gz +0 -0
  316. package/dist/client/assets/journeyDiagram-3NMN7TZE-CQ0PjBy7.js.br +0 -0
  317. package/dist/client/assets/journeyDiagram-3NMN7TZE-CQ0PjBy7.js.gz +0 -0
  318. package/dist/client/assets/kanban-definition-UXKFOSKX-BpQUXA60.js.br +0 -0
  319. package/dist/client/assets/kanban-definition-UXKFOSKX-BpQUXA60.js.gz +0 -0
  320. package/dist/client/assets/layout-BdptC7rq.js.br +0 -0
  321. package/dist/client/assets/layout-BdptC7rq.js.gz +0 -0
  322. package/dist/client/assets/linear-C2cSRCxu.js.br +0 -0
  323. package/dist/client/assets/linear-C2cSRCxu.js.gz +0 -0
  324. package/dist/client/assets/mermaid.core-ClVMFyAU.js.br +0 -0
  325. package/dist/client/assets/mermaid.core-ClVMFyAU.js.gz +0 -0
  326. package/dist/client/assets/mindmap-definition-YA3MSWOX-D0AFCmsY.js.br +0 -0
  327. package/dist/client/assets/mindmap-definition-YA3MSWOX-D0AFCmsY.js.gz +0 -0
  328. package/dist/client/assets/passwordBootstrap-R2qlU2Pq.js +0 -2
  329. package/dist/client/assets/passwordBootstrap-R2qlU2Pq.js.br +0 -0
  330. package/dist/client/assets/passwordBootstrap-R2qlU2Pq.js.gz +0 -0
  331. package/dist/client/assets/pegDiagram-XKGWAZYB-BGAh0bwg.js.br +0 -0
  332. package/dist/client/assets/pegDiagram-XKGWAZYB-BGAh0bwg.js.gz +0 -0
  333. package/dist/client/assets/pieDiagram-E7YTZNPT-CDOZUF44.js.br +0 -0
  334. package/dist/client/assets/pieDiagram-E7YTZNPT-CDOZUF44.js.gz +0 -0
  335. package/dist/client/assets/quadrantDiagram-AXDQQJYC-D_Qn6NTU.js.br +0 -0
  336. package/dist/client/assets/quadrantDiagram-AXDQQJYC-D_Qn6NTU.js.gz +0 -0
  337. package/dist/client/assets/railroadDiagram-O6MQD6OU-qUVGWhJN.js.br +0 -0
  338. package/dist/client/assets/railroadDiagram-O6MQD6OU-qUVGWhJN.js.gz +0 -0
  339. package/dist/client/assets/rdpSession-hgCM4i-j.js.br +0 -0
  340. package/dist/client/assets/rdpSession-hgCM4i-j.js.gz +0 -0
  341. package/dist/client/assets/referenceLongPress-BEMsP11a.js.br +0 -0
  342. package/dist/client/assets/referenceLongPress-BEMsP11a.js.gz +0 -0
  343. package/dist/client/assets/requirementDiagram-BXWQKSXE-BBQMQuVZ.js.br +0 -0
  344. package/dist/client/assets/requirementDiagram-BXWQKSXE-BBQMQuVZ.js.gz +0 -0
  345. package/dist/client/assets/sankeyDiagram-P5KCCOFB-CeEExsqW.js.gz +0 -0
  346. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-7ZPO-X0a.js.br +0 -0
  347. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-7ZPO-X0a.js.gz +0 -0
  348. package/dist/client/assets/sizeCapture-INFHLROL-MBjz7Snm.js.br +0 -0
  349. package/dist/client/assets/sizeCapture-INFHLROL-MBjz7Snm.js.gz +0 -0
  350. package/dist/client/assets/stateDiagram-D77RDMKH-DtLp89uP.js.br +0 -0
  351. package/dist/client/assets/stateDiagram-D77RDMKH-DtLp89uP.js.gz +0 -0
  352. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CIQb4Dp7.js +0 -1
  353. package/dist/client/assets/swimlanes-42K2YHIH-Bc02S1q-.js.br +0 -0
  354. package/dist/client/assets/swimlanes-42K2YHIH-Bc02S1q-.js.gz +0 -0
  355. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-RiKL7eXI.js +0 -8
  356. package/dist/client/assets/timeline-definition-24CTP7MA-BLLwbOm5.js.br +0 -0
  357. package/dist/client/assets/timeline-definition-24CTP7MA-BLLwbOm5.js.gz +0 -0
  358. package/dist/client/assets/vennDiagram-4TSXK5OY-8FcrAdbk.js.gz +0 -0
  359. package/dist/client/assets/videoCompression-RIaDabW4.js.br +0 -0
  360. package/dist/client/assets/videoCompression-RIaDabW4.js.gz +0 -0
  361. package/dist/client/assets/wardleyDiagram-VM6X3IG4-paL2wRrJ.js.br +0 -0
  362. package/dist/client/assets/wardleyDiagram-VM6X3IG4-paL2wRrJ.js.gz +0 -0
  363. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BM5qJ4E7.js.br +0 -0
  364. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BM5qJ4E7.js.gz +0 -0
@@ -1,1989 +0,0 @@
1
- import { readClipboardFiles } from './clipboardFiles.js';
2
- import { sessionAddress } from './collaborationFederation.js';
3
- import { prepareServiceFrontend, selectServiceFrontend } from './bundledFrontend.js';
4
- import { serviceConnection, serviceConnectionKeys, importServiceConnection, saveServiceConnection, invitationForService } from './serviceConnections.js';
5
- import { serviceMenuEntries } from './serviceMenu.js';
6
- import { LocalServiceSetupController } from './localServiceSetup.js';
7
- const pendingServiceInvitations = new WeakMap();
8
- import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, net, nativeImage, Notification, powerMonitor, screen, session, shell, Tray, } from 'electron';
9
- import { execFile } from 'node:child_process';
10
- import crypto from 'node:crypto';
11
- import dgram from 'node:dgram';
12
- import fs from 'node:fs';
13
- import https from 'node:https';
14
- import os from 'node:os';
15
- import path from 'node:path';
16
- import { promisify } from 'node:util';
17
- import { fileURLToPath, pathToFileURL } from 'node:url';
18
- import { desktopStatusTooltip, formatCompactDesktopStatus, menuBarStatusRows, mergeServiceActivity, nextServiceOrigin, normalizeServiceActivity, summarizeServiceActivity, } from './activityStatus.js';
19
- import { checkForDesktopUpdates, configureDesktopUpdater, getDesktopUpdateState, installDownloadedDesktopUpdate, isDesktopUpdateInstalling, subscribeDesktopUpdateState, } from './updater.js';
20
- import { isExternalLinkStagingUrl, isSafeExternalUrl } from './externalLinks.js';
21
- import { shouldThrottleDesktopRenderer } from './windowPolicy.js';
22
- import { serviceDocumentNeedsReload } from './serviceWindowRecovery.js';
23
- import { CertificateTrustRequests, resolveServiceCertificateTrust, canOfferCertificateTrust, downloadCertificateAuthority, isCertificateTrustError, isLocalNetworkHostname, matchManagedLocalCertificate, } from './certificateTrust.js';
24
- const execFileAsync = promisify(execFile);
25
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
26
- const projectRoot = path.resolve(currentDir, '..');
27
- const termdockDir = path.join(os.homedir(), '.termdock');
28
- const desktopConfigPath = path.join(termdockDir, 'desktop.json');
29
- const serverStatePath = path.join(termdockDir, 'server.json');
30
- const DEFAULT_LOCAL_URL = 'http://localhost:9834';
31
- const PROTOCOL_VERSION = 1;
32
- const HEALTH_TIMEOUT_MS = 3_500;
33
- const SERVICE_RECOVERY_PROBE_TIMEOUT_MS = 5_000;
34
- const RESTORE_LOAD_TIMEOUT_MS = 15_000;
35
- const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
36
- const sessionTrustedCertificateTargets = new Set();
37
- const certificateTrustRequests = new CertificateTrustRequests();
38
- const sessionTrustedLeafByOrigin = new Map();
39
- const sessionTrustedCertificateAuthorities = new Map();
40
- let managedLocalCertificateFingerprint = null;
41
- const serviceWindowRecoveryTimers = new WeakMap();
42
- const serviceWindowRecoveryInFlight = new WeakSet();
43
- /** Delivered-but-unseen desktop notifications, mirrored into the Dock badge. */
44
- const activeNotifications = new Map();
45
- let unreadNotificationCount = 0;
46
- function clearUnreadNotifications() {
47
- unreadNotificationCount = 0;
48
- app.setBadgeCount(0);
49
- }
50
- function triggerLocalNetworkPermission() {
51
- if (process.platform !== 'darwin')
52
- return;
53
- const socket = dgram.createSocket('udp4');
54
- const close = () => {
55
- try {
56
- socket.close();
57
- }
58
- catch {
59
- // The socket may already be closed after an immediate send failure.
60
- }
61
- };
62
- socket.once('error', close);
63
- socket.send(Buffer.from('Termdock'), 5353, '224.0.0.251', close);
64
- }
65
- let mainWindow = null;
66
- const localServiceSetup = new LocalServiceSetupController(getLocalServiceStatus, status => {
67
- if (startupRestoreActive && ['checking', 'starting'].includes(status.phase))
68
- updateStartupProgress(status.message);
69
- if (mainWindow && !mainWindow.isDestroyed()) {
70
- mainWindow.webContents.send('desktop:local-service-setup', status);
71
- }
72
- });
73
- let startupRestoreActive = false;
74
- let startupProgressMessage = '正在启动 Termdock Desktop…';
75
- const serviceWindows = new Map();
76
- const serviceWindowPeers = new WeakMap();
77
- const windowServiceOrigins = new WeakMap();
78
- const restorableServiceWindows = new WeakSet();
79
- const reportedServiceActivity = new Map();
80
- const observedServiceActivity = new Map();
81
- let lastFocusedServiceWindow = null;
82
- let menuBarStatus = null;
83
- let menuBarStatusWidth = 0;
84
- let floatingWidgetWindow = null;
85
- let floatingPositionTimer = null;
86
- let isQuitting = false;
87
- const FLOATING_WIDGET_WIDTHS = [64, 108, 152];
88
- const FLOATING_WIDGET_HEIGHT = 40;
89
- function focusedWorkspaceWindow() {
90
- const focused = BrowserWindow.getFocusedWindow();
91
- if (focused && windowServiceOrigins.has(focused))
92
- return focused;
93
- if (lastFocusedServiceWindow && !lastFocusedServiceWindow.isDestroyed()) {
94
- return lastFocusedServiceWindow;
95
- }
96
- return [...serviceWindows.values()].find((window) => !window.isDestroyed()) ?? null;
97
- }
98
- function showAndFocusWindow(window) {
99
- if (windowServiceOrigins.has(window))
100
- lastFocusedServiceWindow = window;
101
- if (window.isMinimized())
102
- window.restore();
103
- window.show();
104
- window.focus();
105
- }
106
- function serviceActivitySnapshot(targetWindow) {
107
- const currentOrigin = windowServiceOrigins.get(targetWindow);
108
- return [...serviceWindows.entries()].flatMap(([origin, window]) => {
109
- if (window.isDestroyed())
110
- return [];
111
- const activity = mergeServiceActivity(reportedServiceActivity.get(origin), observedServiceActivity.get(origin));
112
- return [{
113
- origin,
114
- targetPeerId: serviceWindowPeers.get(window),
115
- label: serviceLabel(origin),
116
- current: origin === currentOrigin,
117
- focused: window.isFocused(),
118
- ...activity,
119
- }];
120
- });
121
- }
122
- function broadcastServiceActivity() {
123
- for (const window of serviceWindows.values()) {
124
- if (window.isDestroyed())
125
- continue;
126
- window.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(window));
127
- }
128
- refreshDesktopStatusSurfaces();
129
- }
130
- function serviceLabel(url) {
131
- const normalized = normalizeServiceUrl(url);
132
- const saved = readDesktopConfig().connections.find((entry) => entry.url === normalized);
133
- if (saved?.label.trim())
134
- return saved.label.trim();
135
- const parsed = new URL(normalized);
136
- return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
137
- ? '本机'
138
- : parsed.host;
139
- }
140
- function allServiceActivity() {
141
- const focused = BrowserWindow.getFocusedWindow();
142
- return [...serviceWindows.entries()].flatMap(([origin, window]) => {
143
- if (window.isDestroyed())
144
- return [];
145
- return [{
146
- origin,
147
- targetPeerId: serviceWindowPeers.get(window),
148
- label: serviceLabel(origin),
149
- current: window === lastFocusedServiceWindow,
150
- focused: window === focused,
151
- ...mergeServiceActivity(reportedServiceActivity.get(origin), observedServiceActivity.get(origin)),
152
- }];
153
- });
154
- }
155
- function desktopStatusSnapshot() {
156
- const services = allServiceActivity();
157
- const summary = summarizeServiceActivity(services);
158
- return {
159
- ...summary,
160
- text: formatCompactDesktopStatus(summary),
161
- tooltip: desktopStatusTooltip(summary),
162
- services,
163
- preferences: readDesktopConfig().desktopPreferences,
164
- };
165
- }
166
- function nextServiceWindow(scope) {
167
- const currentOrigin = lastFocusedServiceWindow && !lastFocusedServiceWindow.isDestroyed()
168
- ? windowServiceOrigins.get(lastFocusedServiceWindow) ?? null
169
- : null;
170
- const origin = nextServiceOrigin(allServiceActivity(), currentOrigin, scope);
171
- return origin ? serviceWindows.get(origin) ?? null : focusedWorkspaceWindow();
172
- }
173
- function focusNextService(scope = 'attention') {
174
- const target = nextServiceWindow(scope);
175
- if (target && !target.isDestroyed()) {
176
- showAndFocusWindow(target);
177
- return;
178
- }
179
- void showConnectionCenter();
180
- }
181
- function menuBarContextMenu() {
182
- const status = desktopStatusSnapshot();
183
- const serviceItems = status.services.length > 0
184
- ? status.services
185
- .sort((left, right) => right.reviewCount - left.reviewCount
186
- || right.runningCount - left.runningCount
187
- || left.label.localeCompare(right.label))
188
- .map((service) => ({
189
- label: `${service.label}${service.runningCount > 0 ? ` 运行 ${service.runningCount}` : ''}${service.reviewCount > 0 ? ` 待办 ${service.reviewCount}` : ''}`,
190
- click: () => {
191
- const target = serviceWindows.get(service.origin);
192
- if (target && !target.isDestroyed())
193
- showAndFocusWindow(target);
194
- },
195
- }))
196
- : [{ label: '暂无已打开服务', enabled: false }];
197
- return Menu.buildFromTemplate([
198
- { label: status.tooltip, enabled: false },
199
- { type: 'separator' },
200
- ...serviceItems,
201
- { type: 'separator' },
202
- { label: '连接中心…', click: () => void showConnectionCenter() },
203
- {
204
- label: '显示悬浮状态',
205
- type: 'checkbox',
206
- checked: status.preferences.floatingWidgetEnabled,
207
- click: (item) => updateDesktopPreferences({ floatingWidgetEnabled: item.checked }),
208
- },
209
- { type: 'separator' },
210
- { role: 'quit' },
211
- ]);
212
- }
213
- function menuBarStatusImage(status) {
214
- const size = 18;
215
- const scale = 2;
216
- const bitmap = Buffer.alloc(size * scale * size * scale * 4);
217
- const glyphs = {
218
- '0': ['111', '101', '101', '101', '111'],
219
- '1': ['010', '110', '010', '010', '111'],
220
- '2': ['111', '001', '111', '100', '111'],
221
- '3': ['111', '001', '111', '001', '111'],
222
- '4': ['101', '101', '111', '001', '001'],
223
- '5': ['111', '100', '111', '001', '111'],
224
- '6': ['111', '100', '111', '101', '111'],
225
- '7': ['111', '001', '010', '010', '010'],
226
- '8': ['111', '101', '111', '101', '111'],
227
- '9': ['111', '101', '111', '001', '111'],
228
- '+': ['000', '010', '111', '010', '000'],
229
- };
230
- const drawPixel = (x, y, alpha = 255) => {
231
- for (let offsetY = 0; offsetY < scale; offsetY += 1) {
232
- for (let offsetX = 0; offsetX < scale; offsetX += 1) {
233
- const pixelX = x * scale + offsetX;
234
- const pixelY = y * scale + offsetY;
235
- if (pixelX < 0 || pixelX >= size * scale || pixelY < 0 || pixelY >= size * scale)
236
- continue;
237
- bitmap[(pixelY * size * scale + pixelX) * 4 + 3] = alpha;
238
- }
239
- }
240
- };
241
- const drawGlyph = (value, x, y, alpha) => {
242
- const glyph = glyphs[value > 9 ? '+' : String(value)] ?? glyphs['0'];
243
- glyph.forEach((row, rowIndex) => {
244
- [...row].forEach((cell, columnIndex) => {
245
- if (cell === '1')
246
- drawPixel(x + columnIndex, y + rowIndex, alpha);
247
- });
248
- });
249
- };
250
- const icons = {
251
- running: [[0, 0], [0, 1], [1, 1], [0, 2], [1, 2], [2, 2], [3, 2], [0, 3], [1, 3], [0, 4]],
252
- review: [[2, 0], [1, 1], [3, 1], [0, 2], [4, 2], [1, 3], [3, 3], [2, 4]],
253
- services: [[0, 0], [1, 0], [3, 0], [4, 0], [1, 1], [3, 1], [2, 2], [2, 3], [1, 4], [2, 4], [3, 4]],
254
- };
255
- // A single connection is the normal case, so its count is redundant. Give
256
- // the two actionable activity rows more breathing room instead. Zero and
257
- // multiple connections keep the service row because those states matter.
258
- menuBarStatusRows(status).forEach(({ metric, value, y }) => {
259
- const alpha = value > 0 ? 255 : 96;
260
- icons[metric].forEach(([offsetX, offsetY]) => drawPixel(1 + offsetX, y + offsetY, alpha));
261
- drawGlyph(value, 11, y, alpha);
262
- });
263
- // Template images are intentionally monochrome; macOS supplies the correct
264
- // menu-bar color for the active appearance and accessibility state.
265
- const image = nativeImage.createFromBitmap(bitmap, {
266
- width: size * scale,
267
- height: size * scale,
268
- scaleFactor: scale,
269
- });
270
- image.setTemplateImage(true);
271
- return image;
272
- }
273
- function refreshMenuBarStatus(status) {
274
- if (process.platform !== 'darwin' || !status.preferences.menuBarStatusEnabled) {
275
- menuBarStatus?.destroy();
276
- menuBarStatus = null;
277
- menuBarStatusWidth = 0;
278
- return;
279
- }
280
- const image = menuBarStatusImage(status);
281
- const imageWidth = image.getSize().width;
282
- if (!menuBarStatus || menuBarStatus.isDestroyed() || menuBarStatusWidth !== imageWidth) {
283
- menuBarStatus?.destroy();
284
- menuBarStatus = new Tray(image);
285
- menuBarStatusWidth = imageWidth;
286
- menuBarStatus.on('click', () => focusNextService('attention'));
287
- menuBarStatus.on('right-click', () => menuBarStatus?.popUpContextMenu(menuBarContextMenu()));
288
- }
289
- else {
290
- menuBarStatus.setImage(image);
291
- }
292
- menuBarStatus.setTitle('');
293
- menuBarStatus.setToolTip(`${status.tooltip}\n点击循环切换需关注的服务`);
294
- }
295
- function constrainFloatingPosition(position, width = FLOATING_WIDGET_WIDTHS[2]) {
296
- const height = FLOATING_WIDGET_HEIGHT;
297
- const display = position
298
- ? screen.getDisplayMatching({ x: position.x, y: position.y, width, height })
299
- : screen.getPrimaryDisplay();
300
- const area = display.workArea;
301
- const preferred = position ?? {
302
- x: area.x + area.width - width - 18,
303
- y: area.y + 18,
304
- };
305
- return {
306
- x: Math.min(area.x + area.width - width, Math.max(area.x, Math.round(preferred.x))),
307
- y: Math.min(area.y + area.height - height, Math.max(area.y, Math.round(preferred.y))),
308
- };
309
- }
310
- function createFloatingWidget(position) {
311
- const bounds = constrainFloatingPosition(position);
312
- const window = new BrowserWindow({
313
- title: 'Termdock Status',
314
- type: 'panel',
315
- width: FLOATING_WIDGET_WIDTHS[2],
316
- height: FLOATING_WIDGET_HEIGHT,
317
- x: bounds.x,
318
- y: bounds.y,
319
- show: false,
320
- frame: false,
321
- transparent: true,
322
- backgroundColor: 'rgba(0, 0, 0, 0)',
323
- vibrancy: 'popover',
324
- visualEffectState: 'active',
325
- resizable: false,
326
- maximizable: false,
327
- minimizable: false,
328
- fullscreenable: false,
329
- focusable: false,
330
- skipTaskbar: true,
331
- alwaysOnTop: true,
332
- acceptFirstMouse: true,
333
- hasShadow: true,
334
- webPreferences: {
335
- preload: path.join(currentDir, 'preload.cjs'),
336
- nodeIntegration: false,
337
- contextIsolation: true,
338
- sandbox: true,
339
- backgroundThrottling: false,
340
- },
341
- });
342
- window.setAlwaysOnTop(true, 'floating');
343
- window.setVisibleOnAllWorkspaces(true, {
344
- visibleOnFullScreen: true,
345
- // Electron otherwise temporarily transforms the whole process into a UI
346
- // element, which can hide the app menu and remove normal windows from App
347
- // Expose while this panel exists.
348
- skipTransformProcessType: true,
349
- });
350
- // Keep the auxiliary status panel out of App Expose / Mission Control so it
351
- // cannot disrupt macOS's three-finger application-window gesture.
352
- window.setHiddenInMissionControl(true);
353
- let constrainingMove = false;
354
- window.on('move', () => {
355
- if (constrainingMove || window.isDestroyed())
356
- return;
357
- const [x, y] = window.getPosition();
358
- const [width] = window.getSize();
359
- const constrained = constrainFloatingPosition({ x, y }, width);
360
- if (constrained.x === x && constrained.y === y)
361
- return;
362
- constrainingMove = true;
363
- window.setPosition(constrained.x, constrained.y, false);
364
- constrainingMove = false;
365
- });
366
- window.on('moved', () => {
367
- if (floatingPositionTimer)
368
- clearTimeout(floatingPositionTimer);
369
- floatingPositionTimer = setTimeout(() => {
370
- floatingPositionTimer = null;
371
- if (window.isDestroyed())
372
- return;
373
- const [x, y] = window.getPosition();
374
- const config = readDesktopConfig();
375
- config.desktopPreferences.floatingWidgetPosition = { x, y };
376
- writeDesktopConfig(config);
377
- }, 250);
378
- });
379
- window.on('closed', () => {
380
- if (floatingWidgetWindow === window)
381
- floatingWidgetWindow = null;
382
- });
383
- const rendererPath = app.isPackaged
384
- ? path.join(process.resourcesPath, 'renderer', 'floating.html')
385
- : path.join(projectRoot, 'desktop', 'renderer', 'floating.html');
386
- void window.loadFile(rendererPath).then(() => window.showInactive());
387
- return window;
388
- }
389
- function resizeFloatingWidget(metricCount) {
390
- const window = floatingWidgetWindow;
391
- if (!window || window.isDestroyed())
392
- return;
393
- const safeCount = Math.min(3, Math.max(1, Math.round(metricCount)));
394
- const width = FLOATING_WIDGET_WIDTHS[safeCount - 1];
395
- const [currentWidth] = window.getSize();
396
- if (currentWidth === width)
397
- return;
398
- const [x, y] = window.getPosition();
399
- window.setSize(width, FLOATING_WIDGET_HEIGHT, false);
400
- const constrained = constrainFloatingPosition({ x: x + currentWidth - width, y }, width);
401
- window.setPosition(constrained.x, constrained.y, false);
402
- }
403
- function refreshFloatingWidget(status) {
404
- if (process.platform !== 'darwin' || !status.preferences.floatingWidgetEnabled) {
405
- if (floatingWidgetWindow && !floatingWidgetWindow.isDestroyed())
406
- floatingWidgetWindow.destroy();
407
- floatingWidgetWindow = null;
408
- return;
409
- }
410
- if (!floatingWidgetWindow || floatingWidgetWindow.isDestroyed()) {
411
- floatingWidgetWindow = createFloatingWidget(status.preferences.floatingWidgetPosition);
412
- }
413
- }
414
- function keepFloatingWidgetOnScreen() {
415
- if (!floatingWidgetWindow || floatingWidgetWindow.isDestroyed())
416
- return;
417
- const [x, y] = floatingWidgetWindow.getPosition();
418
- const [width] = floatingWidgetWindow.getSize();
419
- const constrained = constrainFloatingPosition({ x, y }, width);
420
- if (constrained.x !== x || constrained.y !== y) {
421
- floatingWidgetWindow.setPosition(constrained.x, constrained.y, false);
422
- }
423
- }
424
- function refreshDesktopStatusSurfaces() {
425
- if (!app.isReady())
426
- return;
427
- const status = desktopStatusSnapshot();
428
- refreshMenuBarStatus(status);
429
- refreshFloatingWidget(status);
430
- for (const window of BrowserWindow.getAllWindows()) {
431
- if (!window.isDestroyed())
432
- window.webContents.send('desktop:status-changed', status);
433
- }
434
- }
435
- function updateDesktopPreferences(patch) {
436
- const config = readDesktopConfig();
437
- if (typeof patch.menuBarStatusEnabled === 'boolean') {
438
- config.desktopPreferences.menuBarStatusEnabled = patch.menuBarStatusEnabled;
439
- }
440
- if (typeof patch.floatingWidgetEnabled === 'boolean') {
441
- config.desktopPreferences.floatingWidgetEnabled = patch.floatingWidgetEnabled;
442
- }
443
- writeDesktopConfig(config);
444
- refreshDesktopStatusSurfaces();
445
- }
446
- function showDesktopMessageBox(options) {
447
- const parent = BrowserWindow.getFocusedWindow() ?? mainWindow;
448
- return parent
449
- ? dialog.showMessageBox(parent, options)
450
- : dialog.showMessageBox(options);
451
- }
452
- function openExternalLink(url) {
453
- if (!isSafeExternalUrl(url))
454
- return;
455
- void shell.openExternal(url);
456
- }
457
- function certificateTrustKey(hostname, fingerprint) {
458
- return `${hostname.toLowerCase().replace(/^\[|\]$/g, '')}\0${fingerprint}`;
459
- }
460
- function readManagedLocalCertificateFingerprint() {
461
- try {
462
- return new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
463
- }
464
- catch {
465
- return null;
466
- }
467
- }
468
- function installCertificateVerifyProcedure(targetSession = session.defaultSession, serviceOrigin) {
469
- targetSession.setCertificateVerifyProc((request, callback) => {
470
- const routeOrigins = serviceOrigin ? readDesktopConfig().connections.flatMap(item => item.routes || []).map(route => route.url) : [];
471
- const requestedOrigin = [serviceOrigin, ...routeOrigins].find(origin => origin && new URL(origin).hostname.replace(/^\[|\]$/g, '').toLowerCase() === request.hostname.replace(/^\[|\]$/g, '').toLowerCase());
472
- let isLocalTarget = false;
473
- try {
474
- isLocalTarget = isLocalNetworkTarget(new URL(`https://${request.hostname}`));
475
- }
476
- catch {
477
- // Keep Chromium's default verification for malformed hostnames.
478
- }
479
- let presentedFingerprint = null;
480
- try {
481
- presentedFingerprint = new crypto.X509Certificate(request.certificate.data).fingerprint256;
482
- }
483
- catch {
484
- // Keep Chromium's default verification if the certificate cannot be parsed.
485
- }
486
- const explicitlyTrustedTarget = presentedFingerprint
487
- ? serviceOrigin
488
- ? !!requestedOrigin && sessionTrustedLeafByOrigin.get(requestedOrigin) === presentedFingerprint
489
- : sessionTrustedCertificateTargets.has(certificateTrustKey(request.hostname, presentedFingerprint))
490
- : false;
491
- const managedMatch = isLocalTarget
492
- ? matchManagedLocalCertificate(presentedFingerprint, managedLocalCertificateFingerprint, readManagedLocalCertificateFingerprint)
493
- : { matches: false, currentFingerprint: managedLocalCertificateFingerprint };
494
- managedLocalCertificateFingerprint = managedMatch.currentFingerprint;
495
- const managedLocalCertificate = managedMatch.matches;
496
- if (explicitlyTrustedTarget || managedLocalCertificate) {
497
- callback(0);
498
- return;
499
- }
500
- if (requestedOrigin && canOfferCertificateTrust(requestedOrigin)
501
- && isCertificateTrustError(request.verificationResult)) {
502
- // Resolve trust before Chromium caches a rejection. This covers fetch,
503
- // WebSocket and navigation requests, not only the initial Node probe.
504
- void resolveServiceCertificateTrust(requestedOrigin, request.hostname, request.certificate.data, () => requestCertificateTrust(requestedOrigin)).then((trusted) => callback(trusted ? 0 : -3), (error) => {
505
- console.warn(`[desktop-certificate] ${serviceOrigin}: ${networkErrorDetails(error)}`);
506
- callback(-3);
507
- });
508
- return;
509
- }
510
- callback(-3);
511
- });
512
- }
513
- function configureLocalServiceCertificateTrust() {
514
- managedLocalCertificateFingerprint = readManagedLocalCertificateFingerprint();
515
- installCertificateVerifyProcedure();
516
- }
517
- function defaultConfig() {
518
- return {
519
- version: 1,
520
- connections: [],
521
- lastConnectionUrl: null,
522
- openConnectionUrls: [],
523
- trustedCertificateAuthorities: [],
524
- desktopPreferences: {
525
- menuBarStatusEnabled: true,
526
- floatingWidgetEnabled: false,
527
- floatingWidgetPosition: null,
528
- },
529
- };
530
- }
531
- function normalizeDesktopPreferences(value) {
532
- const input = value && typeof value === 'object'
533
- ? value
534
- : {};
535
- const position = input.floatingWidgetPosition;
536
- return {
537
- // The compact, text-only status is the useful default on macOS. Existing
538
- // desktop.json files therefore gain the feature without a migration step.
539
- menuBarStatusEnabled: input.menuBarStatusEnabled !== false,
540
- floatingWidgetEnabled: input.floatingWidgetEnabled === true,
541
- floatingWidgetPosition: position
542
- && Number.isFinite(position.x)
543
- && Number.isFinite(position.y)
544
- ? { x: Math.round(position.x), y: Math.round(position.y) }
545
- : null,
546
- };
547
- }
548
- function normalizeServiceUrl(raw) {
549
- const value = raw.trim();
550
- if (!value)
551
- throw new Error('请输入 Termdock 服务地址');
552
- const parsed = new URL(value.includes('://') ? value : `http://${value}`);
553
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
554
- throw new Error('服务地址只支持 http:// 或 https://');
555
- }
556
- parsed.username = '';
557
- parsed.password = '';
558
- parsed.hash = '';
559
- parsed.search = '';
560
- parsed.pathname = parsed.pathname.replace(/\/+$/, '') || '/';
561
- return parsed.toString().replace(/\/$/, '');
562
- }
563
- function isLocalNetworkTarget(url) {
564
- return isLocalNetworkHostname(url.hostname);
565
- }
566
- function networkErrorDetails(error) {
567
- if (!(error instanceof Error))
568
- return String(error);
569
- const cause = error.cause;
570
- return cause ? `${error.message} ${networkErrorDetails(cause)}` : error.message;
571
- }
572
- function looksLikeLocalNetworkPermissionError(error) {
573
- return /ERR_ADDRESS_UNREACHABLE|ERR_NETWORK_ACCESS_DENIED/i.test(networkErrorDetails(error));
574
- }
575
- function certificateDetail(target, certificate) {
576
- return [
577
- `目标:${target}`,
578
- `CA:${certificate.subject.replace(/\n/g, ', ')}`,
579
- `CA SHA-256:${certificate.fingerprint256}`,
580
- `服务证书 SHA-256:${certificate.leafFingerprint256}`,
581
- `有效期:${certificate.validFrom} — ${certificate.validTo}`,
582
- '',
583
- '继续后仅 Termdock 会信任该目标使用的这张 CA,不会修改 macOS 系统钥匙串。请只信任你确认属于该服务的证书。',
584
- ].join('\n');
585
- }
586
- function trustedCertificateAuthorityFor(target) {
587
- const origin = new URL(target).origin;
588
- return readDesktopConfig().trustedCertificateAuthorities.find((entry) => entry.origin === origin);
589
- }
590
- function requestCertificateTrust(target) {
591
- return certificateTrustRequests.request(new URL(target).origin, () => confirmCertificateTrust(target));
592
- }
593
- function rememberSessionCertificate(target, certificate) {
594
- const url = new URL(target);
595
- const previous = sessionTrustedLeafByOrigin.get(url.origin);
596
- if (previous)
597
- sessionTrustedCertificateTargets.delete(certificateTrustKey(url.hostname, previous));
598
- sessionTrustedLeafByOrigin.set(url.origin, certificate.leafFingerprint256);
599
- sessionTrustedCertificateAuthorities.set(url.origin, certificate.certificatePem);
600
- sessionTrustedCertificateTargets.add(certificateTrustKey(url.hostname, certificate.leafFingerprint256));
601
- }
602
- async function confirmCertificateTrust(target) {
603
- let certificate;
604
- try {
605
- certificate = await downloadCertificateAuthority(target);
606
- }
607
- catch (error) {
608
- await showDesktopMessageBox({
609
- type: 'error',
610
- title: '无法获取可信的 CA 证书',
611
- message: `无法为 ${target} 准备证书信任`,
612
- detail: networkErrorDetails(error),
613
- });
614
- return null;
615
- }
616
- const existingTrust = trustedCertificateAuthorityFor(target);
617
- if (existingTrust?.fingerprint256 === certificate.fingerprint256) {
618
- rememberSessionCertificate(target, certificate);
619
- return certificate;
620
- }
621
- const confirmation = await showDesktopMessageBox({
622
- type: 'warning',
623
- title: '信任 HTTPS 服务证书',
624
- message: existingTrust
625
- ? '此服务的 CA 证书已发生变化'
626
- : '此服务使用了 Termdock 尚未信任的 HTTPS 证书',
627
- detail: `${certificateDetail(target, certificate)}${existingTrust
628
- ? `\n\n此前信任的 CA SHA-256:${existingTrust.fingerprint256}`
629
- : ''}`,
630
- buttons: ['信任并连接', '取消'],
631
- defaultId: 1,
632
- cancelId: 1,
633
- noLink: true,
634
- });
635
- if (confirmation.response !== 0)
636
- return null;
637
- const config = readDesktopConfig();
638
- const origin = new URL(target).origin;
639
- config.trustedCertificateAuthorities = config.trustedCertificateAuthorities
640
- .filter((entry) => entry.origin !== origin);
641
- config.trustedCertificateAuthorities.push({
642
- origin,
643
- fingerprint256: certificate.fingerprint256,
644
- subject: certificate.subject,
645
- trustedAt: Date.now(),
646
- });
647
- writeDesktopConfig(config);
648
- rememberSessionCertificate(target, certificate);
649
- return certificate;
650
- }
651
- async function requestLocalNetworkPermissionRetry(target) {
652
- const result = await showDesktopMessageBox({
653
- type: 'warning',
654
- title: '需要本地网络权限',
655
- message: 'macOS 阻止了 Termdock 访问局域网服务',
656
- detail: `目标:${target}\n\n请在系统提示中选择“允许”。如果之前选择过“不允许”,请前往“系统设置 → 隐私与安全性 → 本地网络”开启 Termdock。`,
657
- buttons: ['已允许,重试', '打开系统设置', '取消'],
658
- defaultId: 0,
659
- cancelId: 2,
660
- });
661
- if (result.response === 1) {
662
- await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork');
663
- }
664
- return result.response === 0;
665
- }
666
- function readDesktopConfig() {
667
- try {
668
- const parsed = JSON.parse(fs.readFileSync(desktopConfigPath, 'utf8'));
669
- if (parsed.version !== 1 || !Array.isArray(parsed.connections))
670
- return defaultConfig();
671
- const lastConnectionUrl = typeof parsed.lastConnectionUrl === 'string' ? parsed.lastConnectionUrl : null;
672
- return {
673
- version: 1,
674
- removedServiceKeys: Array.isArray(parsed.removedServiceKeys) ? parsed.removedServiceKeys.filter((key) => typeof key === 'string').slice(-768) : [],
675
- connections: parsed.connections.filter((entry) => Boolean(entry)
676
- && typeof entry.id === 'string'
677
- && typeof entry.label === 'string'
678
- && typeof entry.url === 'string'),
679
- lastConnectionUrl,
680
- // Older desktop.json files only remembered the most recent window. Use
681
- // that value once as their restore list, while preserving an explicit
682
- // empty list when the user intentionally closed every service window.
683
- openConnectionUrls: Array.isArray(parsed.openConnectionUrls)
684
- ? parsed.openConnectionUrls.filter((url) => typeof url === 'string')
685
- : lastConnectionUrl ? [lastConnectionUrl] : [],
686
- trustedCertificateAuthorities: Array.isArray(parsed.trustedCertificateAuthorities)
687
- ? parsed.trustedCertificateAuthorities.filter((entry) => Boolean(entry)
688
- && typeof entry.origin === 'string'
689
- && typeof entry.fingerprint256 === 'string'
690
- && typeof entry.subject === 'string'
691
- && typeof entry.trustedAt === 'number')
692
- : [],
693
- desktopPreferences: normalizeDesktopPreferences(parsed.desktopPreferences),
694
- };
695
- }
696
- catch {
697
- return defaultConfig();
698
- }
699
- }
700
- function writeDesktopConfig(config) {
701
- fs.mkdirSync(termdockDir, { recursive: true, mode: 0o700 });
702
- const temporaryPath = `${desktopConfigPath}.${process.pid}.tmp`;
703
- fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
704
- fs.renameSync(temporaryPath, desktopConfigPath);
705
- }
706
- function readServerState() {
707
- try {
708
- const parsed = JSON.parse(fs.readFileSync(serverStatePath, 'utf8'));
709
- if (typeof parsed.pid !== 'number'
710
- || !Number.isInteger(parsed.pid)
711
- || typeof parsed.host !== 'string'
712
- || typeof parsed.port !== 'number') {
713
- return null;
714
- }
715
- return parsed;
716
- }
717
- catch {
718
- return null;
719
- }
720
- }
721
- function isProcessRunning(pid) {
722
- try {
723
- process.kill(pid, 0);
724
- return true;
725
- }
726
- catch {
727
- return false;
728
- }
729
- }
730
- function stateUrl(state) {
731
- if (state.localUrl)
732
- return normalizeServiceUrl(state.localUrl);
733
- const scheme = state.scheme ?? 'http';
734
- const host = state.host === '0.0.0.0' ? 'localhost' : state.host;
735
- return `${scheme}://${host}:${state.port}`;
736
- }
737
- async function fetchWithTimeout(url) {
738
- const controller = new AbortController();
739
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
740
- try {
741
- // Electron's network stack follows the macOS Keychain trust store. Node's
742
- // global fetch does not, so a healthy local service using Termdock's
743
- // mkcert certificate could otherwise be reported as a failed start.
744
- return await net.fetch(url, {
745
- signal: controller.signal,
746
- redirect: 'error',
747
- headers: { Accept: 'application/json' },
748
- });
749
- }
750
- finally {
751
- clearTimeout(timer);
752
- }
753
- }
754
- async function fetchJsonWithCertificateAuthority(rawUrl, certificateAuthority) {
755
- return await new Promise((resolve, reject) => {
756
- const request = https.get(rawUrl, {
757
- agent: false,
758
- ca: certificateAuthority,
759
- rejectUnauthorized: true,
760
- headers: { Accept: 'application/json' },
761
- }, (response) => {
762
- const chunks = [];
763
- let size = 0;
764
- response.on('data', (chunk) => {
765
- size += chunk.length;
766
- if (size > 256 * 1024) {
767
- request.destroy(new Error('服务响应过大'));
768
- return;
769
- }
770
- chunks.push(chunk);
771
- });
772
- response.on('end', () => {
773
- try {
774
- const body = Buffer.concat(chunks).toString('utf8');
775
- resolve({
776
- status: response.statusCode ?? 0,
777
- json: body ? JSON.parse(body) : null,
778
- });
779
- }
780
- catch {
781
- reject(new Error('服务返回了无效的 JSON'));
782
- }
783
- });
784
- });
785
- request.setTimeout(HEALTH_TIMEOUT_MS, () => request.destroy(new Error('连接超时')));
786
- request.on('error', reject);
787
- });
788
- }
789
- async function probeServiceWithCertificateAuthority(rawUrl, certificateAuthority) {
790
- const url = normalizeServiceUrl(rawUrl);
791
- try {
792
- const healthResponse = await fetchJsonWithCertificateAuthority(`${url}/health`, certificateAuthority);
793
- if (healthResponse.status < 200 || healthResponse.status >= 300) {
794
- return { ok: false, url, error: `健康检查返回 HTTP ${healthResponse.status}` };
795
- }
796
- const health = healthResponse.json;
797
- if (health?.status !== 'ok') {
798
- return { ok: false, url, error: '目标地址不是可识别的 Termdock 服务' };
799
- }
800
- try {
801
- const metadataResponse = await fetchJsonWithCertificateAuthority(`${url}/api/meta`, certificateAuthority);
802
- if (metadataResponse.status >= 200 && metadataResponse.status < 300) {
803
- const metadata = metadataResponse.json;
804
- if (metadata?.product && metadata.product !== 'termdock') {
805
- return { ok: false, url, error: '目标服务的产品标识不是 Termdock' };
806
- }
807
- return {
808
- ok: true,
809
- url,
810
- version: typeof metadata?.version === 'string' ? metadata.version : undefined,
811
- protocolVersion: typeof metadata?.protocolVersion === 'number'
812
- ? metadata.protocolVersion
813
- : undefined,
814
- };
815
- }
816
- }
817
- catch {
818
- // Older Termdock releases do not expose /api/meta.
819
- }
820
- return { ok: true, url };
821
- }
822
- catch (error) {
823
- return { ok: false, url, error: networkErrorDetails(error) };
824
- }
825
- }
826
- async function probeService(rawUrl) {
827
- let url;
828
- try {
829
- url = normalizeServiceUrl(rawUrl);
830
- }
831
- catch (error) {
832
- return { ok: false, url: rawUrl, error: error instanceof Error ? error.message : String(error) };
833
- }
834
- try {
835
- const healthResponse = await fetchWithTimeout(`${url}/health`);
836
- if (!healthResponse.ok) {
837
- return { ok: false, url, error: `健康检查返回 HTTP ${healthResponse.status}` };
838
- }
839
- const health = await healthResponse.json();
840
- if (health.status !== 'ok') {
841
- return { ok: false, url, error: '目标地址不是可识别的 Termdock 服务' };
842
- }
843
- try {
844
- const metadataResponse = await fetchWithTimeout(`${url}/api/meta`);
845
- if (metadataResponse.ok) {
846
- const metadata = await metadataResponse.json();
847
- if (metadata.product && metadata.product !== 'termdock') {
848
- return { ok: false, url, error: '目标服务的产品标识不是 Termdock' };
849
- }
850
- return {
851
- ok: true,
852
- url,
853
- version: typeof metadata.version === 'string' ? metadata.version : undefined,
854
- protocolVersion: typeof metadata.protocolVersion === 'number'
855
- ? metadata.protocolVersion
856
- : undefined,
857
- };
858
- }
859
- }
860
- catch {
861
- // Older Termdock releases do not expose /api/meta. A valid health
862
- // response is enough to allow a backwards-compatible connection.
863
- }
864
- return { ok: true, url };
865
- }
866
- catch (error) {
867
- const message = error instanceof Error && error.name === 'AbortError'
868
- ? '连接超时'
869
- : networkErrorDetails(error);
870
- return { ok: false, url, error: message };
871
- }
872
- }
873
- async function probeServiceWithLocalNetworkPermission(rawUrl, options = {}) {
874
- let normalizedUrl;
875
- try {
876
- normalizedUrl = normalizeServiceUrl(rawUrl);
877
- }
878
- catch {
879
- return await probeService(rawUrl);
880
- }
881
- // Do the first macOS HTTPS probe with Node TLS. Sending an untrusted
882
- // certificate through Chromium first permanently caches that rejection for
883
- // the process and prevents an immediate retry after the user approves it.
884
- if (options.interactive !== false && canOfferCertificateTrust(normalizedUrl)
885
- && trustedCertificateAuthorityFor(normalizedUrl)) {
886
- const certificate = await requestCertificateTrust(normalizedUrl);
887
- if (!certificate)
888
- return { ok: false, url: normalizedUrl, error: 'HTTPS 证书尚未受信任' };
889
- }
890
- const approvedAuthority = sessionTrustedCertificateAuthorities.get(new URL(normalizedUrl).origin);
891
- let probe = approvedAuthority
892
- ? await probeServiceWithCertificateAuthority(normalizedUrl, approvedAuthority)
893
- : canOfferCertificateTrust(normalizedUrl)
894
- ? await probeServiceWithCertificateAuthority(normalizedUrl)
895
- : await probeService(normalizedUrl);
896
- if (probe.ok)
897
- return probe;
898
- let parsedProbeUrl;
899
- try {
900
- parsedProbeUrl = new URL(probe.url);
901
- }
902
- catch {
903
- return probe;
904
- }
905
- if (canOfferCertificateTrust(probe.url) && isCertificateTrustError(probe.error)) {
906
- if (options.interactive === false)
907
- return probe;
908
- const certificate = await requestCertificateTrust(probe.url);
909
- if (!certificate) {
910
- return { ...probe, error: 'HTTPS 证书尚未受信任' };
911
- }
912
- return await probeServiceWithCertificateAuthority(rawUrl, certificate.certificatePem);
913
- }
914
- if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
915
- return probe;
916
- }
917
- if (options.interactive === false)
918
- return probe;
919
- if (!(await requestLocalNetworkPermissionRetry(probe.url))) {
920
- return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
921
- }
922
- probe = await probeService(rawUrl);
923
- return probe;
924
- }
925
- async function getLocalServiceStatus() {
926
- const state = readServerState();
927
- if (!state || !isProcessRunning(state.pid)) {
928
- return { running: false, state, probe: null };
929
- }
930
- const probe = await probeService(stateUrl(state));
931
- return { running: probe.ok, state, probe };
932
- }
933
- async function snapshot() {
934
- const config = readDesktopConfig();
935
- return {
936
- appVersion: app.getVersion(),
937
- localService: { ...await getLocalServiceStatus(), setup: localServiceSetup.status },
938
- connections: config.connections,
939
- lastConnectionUrl: config.lastConnectionUrl,
940
- desktopPreferences: config.desktopPreferences,
941
- };
942
- }
943
- function bundledClientDist() {
944
- return app.isPackaged
945
- ? path.join(process.resourcesPath, 'client')
946
- : path.join(projectRoot, 'dist', 'client');
947
- }
948
- async function connectWindow(rawUrl, options = {}) {
949
- if (options.focus !== false) {
950
- try {
951
- certificateTrustRequests.retry(new URL(normalizeServiceUrl(rawUrl)).origin);
952
- }
953
- catch {
954
- return await probeService(rawUrl);
955
- }
956
- }
957
- const known = readDesktopConfig().connections.find(item => (item.serviceOrigin || item.url) === normalizeServiceUrl(rawUrl));
958
- let probe = known?.targetPeerId ? { ok: true, url: normalizeServiceUrl(rawUrl) } : await probeServiceWithLocalNetworkPermission(rawUrl, {
959
- interactive: options.focus !== false,
960
- });
961
- if (!probe.ok)
962
- return probe;
963
- const parsed = new URL(probe.url);
964
- const key = parsed.origin;
965
- const existingWindow = serviceWindows.get(key) ?? (known?.targetPeerId && !options.invitation
966
- ? [...serviceWindows.values()].find(window => !window.isDestroyed() && serviceWindowPeers.get(window) === known.targetPeerId)
967
- : undefined);
968
- if (existingWindow && !existingWindow.isDestroyed()) {
969
- const existingOrigin = windowServiceOrigins.get(existingWindow);
970
- // An alternate address is still the same service. Keep the existing
971
- // renderer and its storage partition instead of navigating it across origins.
972
- if (existingOrigin && existingOrigin !== key) {
973
- if (options.focus !== false) {
974
- mainWindow?.hide();
975
- showAndFocusWindow(existingWindow);
976
- }
977
- return { ok: true, url: existingOrigin };
978
- }
979
- if (options.persist !== false) {
980
- const config = readDesktopConfig();
981
- if (options.updateLastConnection !== false)
982
- config.lastConnectionUrl = probe.url;
983
- config.openConnectionUrls = [
984
- ...config.openConnectionUrls.filter((url) => {
985
- try {
986
- return new URL(url).origin !== key;
987
- }
988
- catch {
989
- return false;
990
- }
991
- }),
992
- probe.url,
993
- ];
994
- const existing = config.connections.find((entry) => entry.url === probe.url);
995
- if (existing)
996
- existing.lastConnectedAt = Date.now();
997
- writeDesktopConfig(config);
998
- }
999
- if (options.invitation || await serviceDocumentNeedsReload(existingWindow.webContents, key)) {
1000
- if (options.invitation)
1001
- pendingServiceInvitations.set(existingWindow, options.invitation);
1002
- await existingWindow.loadURL(probe.url);
1003
- }
1004
- if (options.focus !== false) {
1005
- mainWindow?.hide();
1006
- showAndFocusWindow(existingWindow);
1007
- }
1008
- else {
1009
- existingWindow.showInactive();
1010
- }
1011
- return probe;
1012
- }
1013
- // Existing windows keep their document when transport connectivity changes.
1014
- // New direct windows use the target's own frontend; a saved relay is the only
1015
- // reason to bootstrap a local frontend under an unreachable target origin.
1016
- if (known?.targetPeerId) {
1017
- probe = await probeServiceWithLocalNetworkPermission(rawUrl, { interactive: options.focus !== false });
1018
- }
1019
- const frontendSource = selectServiceFrontend(probe.ok, known);
1020
- if (!frontendSource)
1021
- return probe;
1022
- if (frontendSource === 'bundled')
1023
- probe = { ok: true, url: normalizeServiceUrl(rawUrl) };
1024
- const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
1025
- if (known?.targetPeerId)
1026
- serviceWindowPeers.set(workspaceWindow, known.targetPeerId);
1027
- try {
1028
- await prepareServiceFrontend(workspaceWindow.webContents.session, key, frontendSource, bundledClientDist);
1029
- }
1030
- catch (error) {
1031
- workspaceWindow.destroy();
1032
- return { ...probe, ok: false, error: networkErrorDetails(error) };
1033
- }
1034
- if (options.invitation)
1035
- pendingServiceInvitations.set(workspaceWindow, options.invitation);
1036
- serviceWindows.set(key, workspaceWindow);
1037
- broadcastServiceActivity();
1038
- while (!workspaceWindow.isDestroyed()) {
1039
- try {
1040
- const load = workspaceWindow.loadURL(probe.url);
1041
- if (options.focus === false) {
1042
- await Promise.race([
1043
- load,
1044
- new Promise((_resolve, reject) => {
1045
- const timer = setTimeout(() => reject(new Error('恢复连接超时')), RESTORE_LOAD_TIMEOUT_MS);
1046
- timer.unref();
1047
- }),
1048
- ]);
1049
- }
1050
- else {
1051
- await load;
1052
- }
1053
- if (options.persist !== false) {
1054
- const config = readDesktopConfig();
1055
- if (options.updateLastConnection !== false)
1056
- config.lastConnectionUrl = probe.url;
1057
- config.openConnectionUrls = [
1058
- ...config.openConnectionUrls.filter((url) => {
1059
- try {
1060
- return new URL(url).origin !== key;
1061
- }
1062
- catch {
1063
- return false;
1064
- }
1065
- }),
1066
- probe.url,
1067
- ];
1068
- const existing = config.connections.find((entry) => entry.url === probe.url);
1069
- if (existing)
1070
- existing.lastConnectedAt = Date.now();
1071
- writeDesktopConfig(config);
1072
- }
1073
- restorableServiceWindows.add(workspaceWindow);
1074
- if (options.focus !== false) {
1075
- mainWindow?.hide();
1076
- showAndFocusWindow(workspaceWindow);
1077
- }
1078
- else {
1079
- // Restored workspaces must be real visible macOS windows so App Expose
1080
- // can select them, but opening several at launch must not steal focus
1081
- // from each other.
1082
- workspaceWindow.showInactive();
1083
- }
1084
- return probe;
1085
- }
1086
- catch (error) {
1087
- const message = networkErrorDetails(error);
1088
- if (options.focus === false) {
1089
- workspaceWindow.destroy();
1090
- return { ...probe, ok: false, error: message };
1091
- }
1092
- if (isLocalNetworkTarget(parsed) && looksLikeLocalNetworkPermissionError(error)) {
1093
- if (await requestLocalNetworkPermissionRetry(probe.url))
1094
- continue;
1095
- workspaceWindow.destroy();
1096
- return { ...probe, ok: false, error: 'Termdock 尚未获得 macOS 本地网络权限' };
1097
- }
1098
- workspaceWindow.destroy();
1099
- await showDesktopMessageBox({
1100
- type: 'error',
1101
- title: '连接 Termdock 服务失败',
1102
- message: `无法打开 ${probe.url}`,
1103
- detail: message,
1104
- });
1105
- return { ...probe, ok: false, error: message };
1106
- }
1107
- }
1108
- return { ...probe, ok: false, error: 'Termdock 窗口已关闭' };
1109
- }
1110
- async function showConnectionCenter() {
1111
- if (!mainWindow || mainWindow.isDestroyed()) {
1112
- mainWindow = createDesktopWindow();
1113
- }
1114
- const rendererPath = app.isPackaged
1115
- ? path.join(process.resourcesPath, 'renderer', 'index.html')
1116
- : path.join(projectRoot, 'desktop', 'renderer', 'index.html');
1117
- await mainWindow?.loadFile(rendererPath);
1118
- if (mainWindow)
1119
- showAndFocusWindow(mainWindow);
1120
- }
1121
- async function showStartupProgress(message) {
1122
- if (!startupRestoreActive)
1123
- return;
1124
- startupProgressMessage = message;
1125
- await showConnectionCenter();
1126
- if (!startupRestoreActive || !mainWindow || mainWindow.isDestroyed())
1127
- return;
1128
- mainWindow.webContents.send('desktop:startup-progress', startupProgressMessage);
1129
- }
1130
- function updateStartupProgress(message) {
1131
- if (!startupRestoreActive)
1132
- return;
1133
- startupProgressMessage = message;
1134
- if (mainWindow && !mainWindow.isDestroyed()) {
1135
- mainWindow.webContents.send('desktop:startup-progress', startupProgressMessage);
1136
- }
1137
- }
1138
- function finishStartupProgress() {
1139
- startupRestoreActive = false;
1140
- if (!mainWindow || mainWindow.isDestroyed())
1141
- return;
1142
- mainWindow.webContents.send('desktop:startup-progress', null);
1143
- }
1144
- function scheduleServiceWindowRecovery(window, reason, delayMs = 750) {
1145
- if (window.isDestroyed() || !windowServiceOrigins.has(window))
1146
- return;
1147
- const previous = serviceWindowRecoveryTimers.get(window);
1148
- if (previous)
1149
- clearTimeout(previous);
1150
- const timer = setTimeout(() => {
1151
- serviceWindowRecoveryTimers.delete(window);
1152
- void recoverServiceWindow(window, reason);
1153
- }, delayMs);
1154
- timer.unref();
1155
- serviceWindowRecoveryTimers.set(window, timer);
1156
- }
1157
- async function recoverServiceWindow(window, reason) {
1158
- if (window.isDestroyed() || serviceWindowRecoveryInFlight.has(window))
1159
- return;
1160
- const serviceOrigin = windowServiceOrigins.get(window);
1161
- if (!serviceOrigin)
1162
- return;
1163
- serviceWindowRecoveryInFlight.add(window);
1164
- try {
1165
- let probeTimer = null;
1166
- const probe = await Promise.race([
1167
- probeServiceWithLocalNetworkPermission(serviceOrigin, { interactive: false }),
1168
- new Promise((resolve) => {
1169
- probeTimer = setTimeout(() => resolve({
1170
- ok: false,
1171
- url: serviceOrigin,
1172
- error: '桌面恢复健康检查超时',
1173
- }), SERVICE_RECOVERY_PROBE_TIMEOUT_MS);
1174
- probeTimer.unref();
1175
- }),
1176
- ]);
1177
- if (probeTimer)
1178
- clearTimeout(probeTimer);
1179
- if (!probe.ok || window.isDestroyed())
1180
- return;
1181
- if (!(await serviceDocumentNeedsReload(window.webContents, serviceOrigin)) || window.isDestroyed())
1182
- return;
1183
- console.warn(`[desktop-recovery] reloading ${serviceOrigin} after ${reason}`);
1184
- await window.loadURL(probe.url);
1185
- }
1186
- catch (error) {
1187
- console.warn(`[desktop-recovery] ${serviceOrigin} recovery after ${reason} failed: ${networkErrorDetails(error)}`);
1188
- }
1189
- finally {
1190
- serviceWindowRecoveryInFlight.delete(window);
1191
- }
1192
- }
1193
- function createDesktopWindow(options) {
1194
- const serviceSession = options
1195
- ? session.fromPartition(`persist:termdock-service-${crypto
1196
- .createHash('sha256')
1197
- .update(options.serviceOrigin)
1198
- .digest('hex')
1199
- .slice(0, 24)}`)
1200
- : null;
1201
- if (serviceSession)
1202
- installCertificateVerifyProcedure(serviceSession, options?.serviceOrigin);
1203
- // macOS clips the old fixed width on smaller displays but leaves the fixed
1204
- // height untouched. Size workspace windows to the active display's usable
1205
- // area before loading, including restored windows shown without focus.
1206
- const workspaceBounds = options && process.platform === 'darwin'
1207
- ? screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).workArea
1208
- : { width: options ? 1500 : 1280, height: 820 };
1209
- const window = new BrowserWindow({
1210
- title: options ? `Termdock — ${options.label}` : 'Termdock — 连接中心',
1211
- show: false,
1212
- ...workspaceBounds,
1213
- minWidth: 760,
1214
- minHeight: 520,
1215
- backgroundColor: 'rgb(28, 27, 26)',
1216
- titleBarStyle: 'hiddenInset',
1217
- trafficLightPosition: { x: 16, y: 16 },
1218
- webPreferences: {
1219
- ...(serviceSession ? { session: serviceSession } : {}),
1220
- preload: path.join(currentDir, 'preload.cjs'),
1221
- nodeIntegration: false,
1222
- contextIsolation: true,
1223
- sandbox: true,
1224
- // A service window owns live terminal streams. If Chromium suspends it
1225
- // while macOS places the app in the background, tens of seconds of WS
1226
- // messages can hit the renderer in one burst when the user returns.
1227
- backgroundThrottling: shouldThrottleDesktopRenderer(Boolean(options)),
1228
- },
1229
- });
1230
- if (options)
1231
- windowServiceOrigins.set(window, options.serviceOrigin);
1232
- if (options) {
1233
- window.webContents.on('certificate-error', (event, url, error, certificate, callback) => {
1234
- if (new URL(url).origin !== options.serviceOrigin
1235
- || !canOfferCertificateTrust(url) || !isCertificateTrustError(error))
1236
- return;
1237
- event.preventDefault();
1238
- void resolveServiceCertificateTrust(options.serviceOrigin, new URL(url).hostname, certificate.data, () => requestCertificateTrust(options.serviceOrigin)).then(callback, () => callback(false));
1239
- });
1240
- }
1241
- if (options) {
1242
- window.webContents.on('page-title-updated', (event) => {
1243
- event.preventDefault();
1244
- window.setTitle(`Termdock — ${options.label}`);
1245
- });
1246
- }
1247
- window.webContents.setWindowOpenHandler(({ url }) => {
1248
- if (isExternalLinkStagingUrl(url)) {
1249
- return {
1250
- action: 'allow',
1251
- overrideBrowserWindowOptions: {
1252
- show: false,
1253
- webPreferences: {
1254
- nodeIntegration: false,
1255
- contextIsolation: true,
1256
- sandbox: true,
1257
- },
1258
- },
1259
- };
1260
- }
1261
- openExternalLink(url);
1262
- return { action: 'deny' };
1263
- });
1264
- window.webContents.on('did-create-window', (childWindow, details) => {
1265
- if (!isExternalLinkStagingUrl(details.url))
1266
- return;
1267
- childWindow.hide();
1268
- childWindow.webContents.on('will-navigate', (event, url) => {
1269
- event.preventDefault();
1270
- openExternalLink(url);
1271
- childWindow.close();
1272
- });
1273
- });
1274
- window.webContents.on('will-navigate', (event, url) => {
1275
- const serviceOrigin = windowServiceOrigins.get(window);
1276
- if (!serviceOrigin)
1277
- return;
1278
- try {
1279
- if (new URL(url).origin === serviceOrigin)
1280
- return;
1281
- }
1282
- catch {
1283
- // Block malformed navigations.
1284
- }
1285
- event.preventDefault();
1286
- openExternalLink(url);
1287
- });
1288
- window.webContents.on('did-finish-load', () => {
1289
- if (!windowServiceOrigins.has(window))
1290
- return;
1291
- void window.webContents.insertCSS(`
1292
- html[data-termdock-desktop='true'] {
1293
- --safe-top-inset: max(env(safe-area-inset-top, 0px), 38px) !important;
1294
- }
1295
- html[data-termdock-desktop='true'] #root {
1296
- padding-top: 0 !important;
1297
- }
1298
- html[data-termdock-desktop='true'] body::after {
1299
- display: none !important;
1300
- }
1301
- html[data-termdock-desktop='true'] #root main > div > div:first-child {
1302
- padding-left: 80px !important;
1303
- position: relative;
1304
- -webkit-app-region: no-drag;
1305
- }
1306
- html[data-termdock-desktop='true'] #root main > div > div:first-child::before {
1307
- content: '';
1308
- position: absolute;
1309
- top: 0;
1310
- bottom: 0;
1311
- left: 50%;
1312
- width: min(240px, 25vw);
1313
- transform: translateX(-50%);
1314
- pointer-events: none;
1315
- -webkit-app-region: drag;
1316
- }
1317
- html[data-termdock-desktop='true'] #root .h-full.flex.flex-col.app-chrome-bg.border-r > .shrink-0.border-b {
1318
- padding-left: 80px !important;
1319
- -webkit-app-region: drag;
1320
- }
1321
- html[data-termdock-desktop='true'] #root button,
1322
- html[data-termdock-desktop='true'] #root a,
1323
- html[data-termdock-desktop='true'] #root input,
1324
- html[data-termdock-desktop='true'] #root textarea,
1325
- html[data-termdock-desktop='true'] #root select,
1326
- html[data-termdock-desktop='true'] #root [data-rbd-draggable-id] {
1327
- -webkit-app-region: no-drag;
1328
- }
1329
- html[data-termdock-desktop='true'] [data-sidebar='right'] {
1330
- padding-top: 0 !important;
1331
- }
1332
- html[data-termdock-desktop='true'] [data-sidebar='right'][style*='pointer-events: auto'] {
1333
- transform: none !important;
1334
- }
1335
- html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child {
1336
- -webkit-app-region: no-drag;
1337
- }
1338
- html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child > div:first-child > .min-w-0.flex-1 {
1339
- -webkit-app-region: drag;
1340
- }
1341
- html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) {
1342
- width: fit-content !important;
1343
- margin-left: auto !important;
1344
- }
1345
- html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) > button {
1346
- flex: none !important;
1347
- padding: 0.125rem 0.5rem !important;
1348
- font-size: 10px !important;
1349
- }
1350
- `);
1351
- });
1352
- window.webContents.on('did-fail-load', (_event, _errorCode, _errorDescription, _validatedURL, isMainFrame) => {
1353
- if (isMainFrame)
1354
- scheduleServiceWindowRecovery(window, 'main-frame-load-failure');
1355
- });
1356
- window.webContents.on('render-process-gone', (_event, details) => {
1357
- scheduleServiceWindowRecovery(window, `renderer-${details.reason}`);
1358
- });
1359
- window.on('unresponsive', () => scheduleServiceWindowRecovery(window, 'unresponsive'));
1360
- window.on('closed', () => {
1361
- if (mainWindow === window)
1362
- mainWindow = null;
1363
- if (lastFocusedServiceWindow === window)
1364
- lastFocusedServiceWindow = null;
1365
- const serviceOrigin = windowServiceOrigins.get(window);
1366
- if (serviceOrigin && serviceWindows.get(serviceOrigin) === window) {
1367
- serviceWindows.delete(serviceOrigin);
1368
- reportedServiceActivity.delete(serviceOrigin);
1369
- observedServiceActivity.delete(serviceOrigin);
1370
- broadcastServiceActivity();
1371
- }
1372
- });
1373
- window.on('focus', () => {
1374
- if (windowServiceOrigins.has(window))
1375
- lastFocusedServiceWindow = window;
1376
- broadcastServiceActivity();
1377
- // User is looking at the app — the Dock badge has served its purpose.
1378
- clearUnreadNotifications();
1379
- scheduleServiceWindowRecovery(window, 'window-focus');
1380
- });
1381
- window.on('close', (event) => {
1382
- const closingForQuit = isQuitting || isDesktopUpdateInstalling();
1383
- const serviceOrigin = windowServiceOrigins.get(window);
1384
- if (serviceOrigin && !closingForQuit && restorableServiceWindows.has(window)) {
1385
- const config = readDesktopConfig();
1386
- config.openConnectionUrls = config.openConnectionUrls.filter((url) => {
1387
- try {
1388
- return new URL(url).origin !== serviceOrigin;
1389
- }
1390
- catch {
1391
- return false;
1392
- }
1393
- });
1394
- writeDesktopConfig(config);
1395
- }
1396
- if (window === mainWindow && process.platform === 'darwin' && !closingForQuit) {
1397
- event.preventDefault();
1398
- window.hide();
1399
- }
1400
- });
1401
- return window;
1402
- }
1403
- function installIpcHandlers() {
1404
- ipcMain.handle('desktop:read-clipboard-files', (event, options) => {
1405
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1406
- const origin = sourceWindow ? windowServiceOrigins.get(sourceWindow) : undefined;
1407
- if (!origin || event.senderFrame !== event.sender.mainFrame
1408
- || new URL(event.sender.getURL()).origin !== origin)
1409
- throw new Error('未授权的服务窗口');
1410
- return readClipboardFiles(options);
1411
- });
1412
- ipcMain.handle('desktop:read-clipboard-image', (event) => {
1413
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1414
- if (!sourceWindow || !windowServiceOrigins.has(sourceWindow))
1415
- return null;
1416
- const image = clipboard.readImage();
1417
- if (image.isEmpty())
1418
- return null;
1419
- const png = image.toPNG();
1420
- return png.buffer.slice(png.byteOffset, png.byteOffset + png.byteLength);
1421
- });
1422
- const collaborationOrigin = (event) => {
1423
- const window = BrowserWindow.fromWebContents(event.sender);
1424
- const origin = window ? windowServiceOrigins.get(window) : undefined;
1425
- if (!origin || event.senderFrame !== event.sender.mainFrame
1426
- || new URL(event.sender.getURL()).origin !== origin)
1427
- throw new Error('未授权的服务窗口');
1428
- return origin;
1429
- };
1430
- ipcMain.handle('desktop:collaboration-focus', (event, id) => {
1431
- collaborationOrigin(event);
1432
- const address = sessionAddress(id);
1433
- const target = address ? serviceWindows.get(address.origin) : null;
1434
- if (!target || target.isDestroyed())
1435
- return false;
1436
- showAndFocusWindow(target);
1437
- target.webContents.send('desktop:focus-session', address.id);
1438
- return true;
1439
- });
1440
- let deviceInfo;
1441
- ipcMain.handle('desktop:device-info', event => {
1442
- collaborationOrigin(event);
1443
- return deviceInfo ??= (async () => {
1444
- let model = '';
1445
- if (process.platform === 'darwin') {
1446
- try {
1447
- model = (await execFileAsync('/usr/sbin/sysctl', ['-n', 'hw.model'], { timeout: 3000 })).stdout.trim();
1448
- }
1449
- catch { /* Optional hardware information. */ }
1450
- }
1451
- return { system: process.platform === 'darwin' ? `macOS ${process.getSystemVersion()}` : `${os.type()} ${os.release()}`,
1452
- client: `Termdock ${app.getVersion()}`, model, arch: process.arch, cpu: os.cpus()[0]?.model || '', hostname: os.hostname(), mode: '桌面客户端' };
1453
- })();
1454
- });
1455
- ipcMain.handle('desktop:snapshot', () => snapshot());
1456
- const connectionCenterSender = (event) => {
1457
- const rendererPath = app.isPackaged
1458
- ? path.join(process.resourcesPath, 'renderer', 'index.html')
1459
- : path.join(projectRoot, 'desktop', 'renderer', 'index.html');
1460
- if (!mainWindow || event.sender !== mainWindow.webContents
1461
- || event.senderFrame !== event.sender.mainFrame
1462
- || event.senderFrame.url !== pathToFileURL(rendererPath).href) {
1463
- throw new Error('请在连接中心准备本机服务。');
1464
- }
1465
- };
1466
- ipcMain.handle('desktop:prepare-local-service', async (event, install) => {
1467
- connectionCenterSender(event);
1468
- await localServiceSetup.prepare(install === true);
1469
- return snapshot();
1470
- });
1471
- ipcMain.handle('desktop:download-node', async (event) => {
1472
- connectionCenterSender(event);
1473
- await shell.openExternal('https://nodejs.org/en/download');
1474
- });
1475
- const directorySender = (event) => {
1476
- const window = BrowserWindow.fromWebContents(event.sender);
1477
- if (!window || event.senderFrame !== event.sender.mainFrame)
1478
- throw new Error('无法访问服务列表。');
1479
- const origin = windowServiceOrigins.get(window);
1480
- const url = new URL(event.senderFrame.url);
1481
- if (origin ? url.origin !== origin || url.pathname !== '/' : window !== mainWindow || url.protocol !== 'file:')
1482
- throw new Error('无法访问服务列表。');
1483
- return window;
1484
- };
1485
- const directoryChanged = () => {
1486
- const connections = readDesktopConfig().connections;
1487
- for (const [origin, window] of serviceWindows) {
1488
- const connection = connections.find(item => (item.serviceOrigin || item.url) === origin);
1489
- if (connection?.targetPeerId)
1490
- serviceWindowPeers.set(window, connection.targetPeerId);
1491
- }
1492
- installMenu();
1493
- for (const window of BrowserWindow.getAllWindows())
1494
- window.webContents.send('desktop:service-connections-changed');
1495
- broadcastServiceActivity();
1496
- };
1497
- ipcMain.handle('desktop:service-connections', event => { directorySender(event); return readDesktopConfig().connections; });
1498
- ipcMain.handle('desktop:save-service-connection', (event, input) => {
1499
- directorySender(event);
1500
- const config = readDesktopConfig();
1501
- Object.assign(config, saveServiceConnection(config.connections, config.removedServiceKeys || [], input));
1502
- writeDesktopConfig(config);
1503
- directoryChanged();
1504
- return config.connections;
1505
- });
1506
- ipcMain.handle('desktop:import-service-connection', (event, input) => {
1507
- directorySender(event);
1508
- const config = readDesktopConfig();
1509
- config.connections = importServiceConnection(config.connections, config.removedServiceKeys || [], input);
1510
- writeDesktopConfig(config);
1511
- directoryChanged();
1512
- return config.connections;
1513
- });
1514
- ipcMain.handle('desktop:remove-service-connection', (event, id) => {
1515
- directorySender(event);
1516
- const config = readDesktopConfig();
1517
- const removed = config.connections.find(item => item.id === id);
1518
- if (removed)
1519
- config.removedServiceKeys = [...new Set([...(config.removedServiceKeys || []), ...serviceConnectionKeys(removed)])].slice(-768);
1520
- config.connections = config.connections.filter(item => item.id !== id);
1521
- writeDesktopConfig(config);
1522
- directoryChanged();
1523
- return config.connections;
1524
- });
1525
- ipcMain.handle('desktop:current-service-connection', event => {
1526
- const window = directorySender(event), origin = windowServiceOrigins.get(window);
1527
- const connection = readDesktopConfig().connections.find(item => (item.serviceOrigin || item.url) === origin);
1528
- const invitation = pendingServiceInvitations.get(window);
1529
- pendingServiceInvitations.delete(window);
1530
- return connection ? { ...connection, ...(invitation ? { invitation } : {}) } : null;
1531
- });
1532
- ipcMain.handle('desktop:open-service-connection', async (event, input, rawInvitation) => {
1533
- directorySender(event);
1534
- const connection = serviceConnection(input), invitation = invitationForService(rawInvitation, connection);
1535
- // A bookmark pin is connection metadata, never authorization to the service.
1536
- const config = readDesktopConfig();
1537
- Object.assign(config, saveServiceConnection(config.connections, config.removedServiceKeys || [], connection));
1538
- writeDesktopConfig(config);
1539
- directoryChanged();
1540
- return connectWindow(connection.serviceOrigin || connection.url, { invitation });
1541
- });
1542
- ipcMain.handle('desktop:probe', (_event, url) => {
1543
- try {
1544
- certificateTrustRequests.retry(new URL(normalizeServiceUrl(url)).origin);
1545
- }
1546
- catch { /* probe reports invalid URLs */ }
1547
- return probeServiceWithLocalNetworkPermission(url);
1548
- });
1549
- ipcMain.handle('desktop:save-connection', async (_event, input) => {
1550
- const url = normalizeServiceUrl(input.url);
1551
- const config = readDesktopConfig();
1552
- const existing = config.connections.find((entry) => entry.url === url);
1553
- Object.assign(config, saveServiceConnection(config.connections, config.removedServiceKeys || [], {
1554
- id: existing?.id || crypto.randomUUID(),
1555
- label: input.label.trim() || new URL(url).host,
1556
- url,
1557
- }));
1558
- writeDesktopConfig(config);
1559
- installMenu();
1560
- return snapshot();
1561
- });
1562
- ipcMain.handle('desktop:remove-connection', async (_event, id) => {
1563
- const config = readDesktopConfig();
1564
- config.connections = config.connections.filter((entry) => entry.id !== id);
1565
- writeDesktopConfig(config);
1566
- installMenu();
1567
- return snapshot();
1568
- });
1569
- ipcMain.handle('desktop:connect', (_event, url) => connectWindow(url));
1570
- ipcMain.on('desktop:report-service-activity', (event, activity) => {
1571
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1572
- if (!sourceWindow)
1573
- return;
1574
- const origin = windowServiceOrigins.get(sourceWindow);
1575
- if (!origin)
1576
- return;
1577
- const next = normalizeServiceActivity(activity);
1578
- const current = reportedServiceActivity.get(origin);
1579
- if (current?.runningCount === next.runningCount && current.reviewCount === next.reviewCount) {
1580
- // The sender may have just reloaded and still needs the complete roster.
1581
- sourceWindow.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(sourceWindow));
1582
- return;
1583
- }
1584
- reportedServiceActivity.set(origin, next);
1585
- broadcastServiceActivity();
1586
- });
1587
- ipcMain.on('desktop:observe-service-activity', (event, activity) => {
1588
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1589
- if (!sourceWindow)
1590
- return;
1591
- const origin = windowServiceOrigins.get(sourceWindow);
1592
- if (!origin)
1593
- return;
1594
- const next = normalizeServiceActivity(activity);
1595
- const current = observedServiceActivity.get(origin);
1596
- if (current?.runningCount === next.runningCount && current.reviewCount === next.reviewCount)
1597
- return;
1598
- observedServiceActivity.set(origin, next);
1599
- broadcastServiceActivity();
1600
- });
1601
- ipcMain.handle('desktop:focus-service', (event, origin) => {
1602
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1603
- if (!sourceWindow || !windowServiceOrigins.has(sourceWindow))
1604
- return false;
1605
- const targetWindow = serviceWindows.get(origin);
1606
- if (!targetWindow || targetWindow.isDestroyed())
1607
- return false;
1608
- showAndFocusWindow(targetWindow);
1609
- broadcastServiceActivity();
1610
- return true;
1611
- });
1612
- ipcMain.handle('desktop:update-preferences', async (_event, preferences) => {
1613
- updateDesktopPreferences({
1614
- ...(typeof preferences?.menuBarStatusEnabled === 'boolean'
1615
- ? { menuBarStatusEnabled: preferences.menuBarStatusEnabled }
1616
- : {}),
1617
- ...(typeof preferences?.floatingWidgetEnabled === 'boolean'
1618
- ? { floatingWidgetEnabled: preferences.floatingWidgetEnabled }
1619
- : {}),
1620
- });
1621
- return snapshot();
1622
- });
1623
- ipcMain.handle('desktop:status-snapshot', () => desktopStatusSnapshot());
1624
- ipcMain.handle('desktop:focus-next-service', (_event, scope) => {
1625
- const normalized = scope === 'running'
1626
- || scope === 'review'
1627
- || scope === 'all'
1628
- ? scope
1629
- : 'attention';
1630
- focusNextService(normalized);
1631
- });
1632
- ipcMain.handle('desktop:set-floating-metric-count', (event, count) => {
1633
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1634
- if (sourceWindow !== floatingWidgetWindow || typeof count !== 'number' || !Number.isFinite(count))
1635
- return;
1636
- resizeFloatingWidget(count);
1637
- });
1638
- ipcMain.handle('desktop:disable-floating-widget', async () => {
1639
- updateDesktopPreferences({ floatingWidgetEnabled: false });
1640
- return snapshot();
1641
- });
1642
- ipcMain.handle('desktop:update-state', () => getDesktopUpdateState());
1643
- ipcMain.handle('desktop:check-update', () => checkForDesktopUpdates());
1644
- ipcMain.handle('desktop:install-update', () => installDownloadedDesktopUpdate());
1645
- ipcMain.handle('desktop:show-connection-center', () => showConnectionCenter());
1646
- ipcMain.handle('desktop:reveal-data-directory', async () => {
1647
- fs.mkdirSync(termdockDir, { recursive: true, mode: 0o700 });
1648
- const error = await shell.openPath(termdockDir);
1649
- if (error)
1650
- throw new Error(error);
1651
- });
1652
- ipcMain.handle('desktop:open-notification-settings', async () => {
1653
- await shell.openExternal('x-apple.systempreferences:com.apple.Notifications-Settings.extension?bundleId=com.jovines.termdock');
1654
- });
1655
- ipcMain.handle('desktop:prepare-notification-test', (event) => {
1656
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1657
- if (sourceWindow && !sourceWindow.isDestroyed())
1658
- sourceWindow.minimize();
1659
- });
1660
- ipcMain.handle('desktop:show-notification', (event, payload) => {
1661
- if (!Notification.isSupported() || typeof payload?.title !== 'string')
1662
- return false;
1663
- const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1664
- // The renderer also suppresses automatic notifications while focused, but
1665
- // enforce the rule in the native process as well so a focus race can never
1666
- // produce a foreground sound, Notification Center entry, or Dock badge.
1667
- if (sourceWindow?.isFocused())
1668
- return true;
1669
- // Web Notification tag semantics: a new notification with the same tag
1670
- // replaces the previous one instead of stacking.
1671
- const tag = typeof payload.tag === 'string' && payload.tag ? payload.tag : null;
1672
- if (tag) {
1673
- const previous = activeNotifications.get(tag);
1674
- if (previous) {
1675
- previous.close();
1676
- activeNotifications.delete(tag);
1677
- }
1678
- }
1679
- const notification = new Notification({
1680
- title: payload.title.slice(0, 160),
1681
- body: typeof payload.body === 'string' ? payload.body.slice(0, 1000) : undefined,
1682
- silent: payload.silent === true,
1683
- });
1684
- if (tag) {
1685
- activeNotifications.set(tag, notification);
1686
- notification.on('close', () => {
1687
- if (activeNotifications.get(tag) === notification)
1688
- activeNotifications.delete(tag);
1689
- });
1690
- }
1691
- notification.on('click', () => {
1692
- const targetWindow = sourceWindow && !sourceWindow.isDestroyed()
1693
- ? sourceWindow
1694
- : focusedWorkspaceWindow();
1695
- targetWindow?.show();
1696
- targetWindow?.focus();
1697
- if (typeof payload.sessionId === 'string') {
1698
- targetWindow?.webContents.send('desktop:focus-session', payload.sessionId);
1699
- }
1700
- });
1701
- return new Promise((resolve) => {
1702
- let settled = false;
1703
- const finish = (delivered, error) => {
1704
- if (settled)
1705
- return;
1706
- settled = true;
1707
- clearTimeout(timeout);
1708
- if (!delivered) {
1709
- if (tag && activeNotifications.get(tag) === notification) {
1710
- activeNotifications.delete(tag);
1711
- }
1712
- if (error)
1713
- console.error(`[desktop notification] ${error}`);
1714
- resolve(false);
1715
- return;
1716
- }
1717
- unreadNotificationCount += 1;
1718
- app.setBadgeCount(unreadNotificationCount);
1719
- // macOS banners auto-dismiss even for "persistent" alerts; bounce the Dock
1720
- // icon so the signal survives until the user looks at the app.
1721
- if (payload.persistent === true && !sourceWindow?.isFocused()) {
1722
- app.dock?.bounce('informational');
1723
- }
1724
- resolve(true);
1725
- };
1726
- notification.once('show', () => finish(true));
1727
- notification.once('failed', (_notificationEvent, error) => finish(false, error));
1728
- // macOS can silently discard notifications when this user disabled the
1729
- // app in System Settings. Do not report success or increment the Dock
1730
- // badge unless Electron confirms delivery.
1731
- const timeout = setTimeout(() => {
1732
- finish(false, 'macOS did not confirm notification delivery');
1733
- }, 2_000);
1734
- notification.show();
1735
- });
1736
- });
1737
- }
1738
- function installMenu() {
1739
- const sendCommand = (command) => {
1740
- focusedWorkspaceWindow()?.webContents.send('desktop:command', command);
1741
- };
1742
- const openService = (url) => {
1743
- void connectWindow(url).then((probe) => {
1744
- if (!probe.ok) {
1745
- void showDesktopMessageBox({
1746
- type: 'error',
1747
- title: '连接 Termdock 服务失败',
1748
- message: `无法打开 ${probe.url}`,
1749
- detail: probe.error,
1750
- });
1751
- }
1752
- });
1753
- };
1754
- const openLocalService = () => {
1755
- void getLocalServiceStatus().then((status) => {
1756
- if (status.running && status.state) {
1757
- openService(stateUrl(status.state));
1758
- return;
1759
- }
1760
- void showConnectionCenter();
1761
- });
1762
- };
1763
- const config = readDesktopConfig();
1764
- const localState = readServerState();
1765
- const localUrl = localState ? stateUrl(localState) : DEFAULT_LOCAL_URL;
1766
- const serviceItems = serviceMenuEntries(config.connections, localUrl)
1767
- .map(({ label, connection }) => ({
1768
- label,
1769
- click: connection
1770
- ? () => openService(connection.serviceOrigin || connection.url)
1771
- : openLocalService,
1772
- }));
1773
- const menu = Menu.buildFromTemplate([
1774
- {
1775
- label: 'Termdock',
1776
- submenu: [
1777
- { role: 'about' },
1778
- { type: 'separator' },
1779
- {
1780
- label: '设置…',
1781
- accelerator: 'CmdOrCtrl+,',
1782
- click: () => focusedWorkspaceWindow()?.webContents.send('desktop:open-settings'),
1783
- },
1784
- {
1785
- label: '新建连接或切换服务…',
1786
- accelerator: 'CmdOrCtrl+N',
1787
- click: () => void showConnectionCenter(),
1788
- },
1789
- { label: '打开服务', submenu: serviceItems },
1790
- {
1791
- label: '检查更新…',
1792
- click: () => void checkForDesktopUpdates({ presentNativeDialogs: true }),
1793
- },
1794
- {
1795
- label: '打开 Termdock 数据目录',
1796
- click: () => {
1797
- fs.mkdirSync(termdockDir, { recursive: true, mode: 0o700 });
1798
- void shell.openPath(termdockDir);
1799
- },
1800
- },
1801
- { type: 'separator' },
1802
- { role: 'hide' },
1803
- { role: 'hideOthers' },
1804
- { role: 'unhide' },
1805
- { type: 'separator' },
1806
- { role: 'quit' },
1807
- ],
1808
- },
1809
- {
1810
- label: '文件',
1811
- submenu: [
1812
- {
1813
- label: '新建会话',
1814
- accelerator: 'CmdOrCtrl+T',
1815
- click: () => sendCommand('new-session'),
1816
- },
1817
- {
1818
- label: '关闭当前会话',
1819
- accelerator: 'CmdOrCtrl+W',
1820
- click: () => sendCommand('close-session'),
1821
- },
1822
- { type: 'separator' },
1823
- {
1824
- label: '上一个会话',
1825
- accelerator: 'CmdOrCtrl+Shift+[',
1826
- click: () => sendCommand('previous-session'),
1827
- },
1828
- {
1829
- label: '下一个会话',
1830
- accelerator: 'CmdOrCtrl+Shift+]',
1831
- click: () => sendCommand('next-session'),
1832
- },
1833
- { type: 'separator' },
1834
- {
1835
- label: '上一个待处理',
1836
- accelerator: 'CmdOrCtrl+Alt+[',
1837
- click: () => sendCommand('previous-attention'),
1838
- },
1839
- {
1840
- label: '下一个待处理',
1841
- accelerator: 'CmdOrCtrl+Alt+]',
1842
- click: () => sendCommand('next-attention'),
1843
- },
1844
- ],
1845
- },
1846
- {
1847
- label: '编辑',
1848
- submenu: [
1849
- { role: 'undo' },
1850
- { role: 'redo' },
1851
- { type: 'separator' },
1852
- { role: 'cut' },
1853
- { role: 'copy' },
1854
- { role: 'paste' },
1855
- { role: 'selectAll' },
1856
- ],
1857
- },
1858
- {
1859
- label: '显示',
1860
- submenu: [
1861
- {
1862
- label: '切换会话侧栏',
1863
- accelerator: 'CmdOrCtrl+B',
1864
- click: () => sendCommand('toggle-left-sidebar'),
1865
- },
1866
- {
1867
- label: '切换文件侧栏',
1868
- accelerator: 'CmdOrCtrl+Shift+B',
1869
- click: () => sendCommand('toggle-right-sidebar'),
1870
- },
1871
- { type: 'separator' },
1872
- { role: 'reload' },
1873
- { role: 'forceReload' },
1874
- { type: 'separator' },
1875
- { role: 'resetZoom' },
1876
- { role: 'zoomIn' },
1877
- { role: 'zoomOut' },
1878
- { type: 'separator' },
1879
- { role: 'togglefullscreen' },
1880
- ],
1881
- },
1882
- {
1883
- label: '窗口',
1884
- submenu: [
1885
- {
1886
- label: '连接中心',
1887
- accelerator: 'CmdOrCtrl+Shift+O',
1888
- click: () => void showConnectionCenter(),
1889
- },
1890
- { label: '打开服务', submenu: serviceItems },
1891
- { type: 'separator' },
1892
- { role: 'minimize' },
1893
- { role: 'zoom' },
1894
- { role: 'front' },
1895
- ],
1896
- },
1897
- ]);
1898
- Menu.setApplicationMenu(menu);
1899
- app.dock?.setMenu(Menu.buildFromTemplate([
1900
- { label: '连接中心', click: () => void showConnectionCenter() },
1901
- { type: 'separator' },
1902
- ...serviceItems,
1903
- ]));
1904
- }
1905
- app.whenReady().then(async () => {
1906
- triggerLocalNetworkPermission();
1907
- configureLocalServiceCertificateTrust();
1908
- installIpcHandlers();
1909
- const localPreparation = localServiceSetup.prepare();
1910
- installMenu();
1911
- refreshDesktopStatusSurfaces();
1912
- screen.on('display-removed', keepFloatingWidgetOnScreen);
1913
- screen.on('display-metrics-changed', keepFloatingWidgetOnScreen);
1914
- powerMonitor.on('resume', () => {
1915
- for (const window of serviceWindows.values()) {
1916
- scheduleServiceWindowRecovery(window, 'system-resume', 1_200);
1917
- }
1918
- });
1919
- configureDesktopUpdater(showDesktopMessageBox);
1920
- subscribeDesktopUpdateState((state) => {
1921
- for (const window of BrowserWindow.getAllWindows()) {
1922
- if (!window.isDestroyed())
1923
- window.webContents.send('desktop:update-state-changed', state);
1924
- }
1925
- });
1926
- const startupConfig = readDesktopConfig();
1927
- const restoreUrls = startupConfig.openConnectionUrls;
1928
- startupRestoreActive = true;
1929
- const startupProgressTimer = setTimeout(() => {
1930
- void showStartupProgress('正在检查已保存连接…');
1931
- }, 350);
1932
- startupProgressTimer.unref();
1933
- let completedRestores = 0;
1934
- updateStartupProgress(`正在恢复连接 0/${restoreUrls.length}…`);
1935
- const restoreResults = await Promise.all(restoreUrls.map(async (url) => {
1936
- try {
1937
- if (['localhost', '127.0.0.1', '[::1]'].includes(new URL(url).hostname))
1938
- await localPreparation;
1939
- }
1940
- catch { /* The connection probe reports malformed saved URLs. */ }
1941
- const probe = await connectWindow(url, {
1942
- focus: false,
1943
- updateLastConnection: false,
1944
- persist: false,
1945
- });
1946
- completedRestores += 1;
1947
- updateStartupProgress(`正在恢复连接 ${completedRestores}/${restoreUrls.length}…`);
1948
- return probe;
1949
- }));
1950
- const restoredWindowCount = restoreResults.filter((probe) => probe.ok).length;
1951
- clearTimeout(startupProgressTimer);
1952
- finishStartupProgress();
1953
- if (restoredWindowCount > 0) {
1954
- let preferredOrigin = null;
1955
- try {
1956
- preferredOrigin = startupConfig.lastConnectionUrl
1957
- ? new URL(startupConfig.lastConnectionUrl).origin
1958
- : null;
1959
- }
1960
- catch {
1961
- // A malformed legacy value must not prevent the other saved windows
1962
- // from being restored.
1963
- }
1964
- const preferredWindow = preferredOrigin ? serviceWindows.get(preferredOrigin) : null;
1965
- showAndFocusWindow(preferredWindow ?? [...serviceWindows.values()][0]);
1966
- }
1967
- else {
1968
- await showConnectionCenter();
1969
- }
1970
- app.on('activate', () => {
1971
- const workspaceWindow = focusedWorkspaceWindow();
1972
- if (workspaceWindow) {
1973
- showAndFocusWindow(workspaceWindow);
1974
- return;
1975
- }
1976
- void showConnectionCenter();
1977
- });
1978
- }).catch((error) => {
1979
- void dialog.showErrorBox('Termdock 启动失败', error instanceof Error ? error.message : String(error));
1980
- app.quit();
1981
- });
1982
- app.on('window-all-closed', () => {
1983
- // Keep the macOS application lifecycle conventional: closing the last
1984
- // window keeps the app available in the Dock.
1985
- });
1986
- app.on('before-quit', () => {
1987
- isQuitting = true;
1988
- });
1989
- export { PROTOCOL_VERSION };