agentworkshop 0.7.41 → 0.7.44

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 (265) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B8uy9iRW.js +1 -0
  3. package/.output/public/_nuxt/BGTYYQH-.js +1 -0
  4. package/.output/public/_nuxt/BUmHEJ77.js +2 -0
  5. package/.output/public/_nuxt/BY7YrV_D.js +1 -0
  6. package/.output/public/_nuxt/Bqcgz95s.js +18 -0
  7. package/.output/public/_nuxt/C7UlqPeD.js +1 -0
  8. package/.output/public/_nuxt/CTmDn-YP.js +1 -0
  9. package/.output/public/_nuxt/Cs8pVeVG.js +1 -0
  10. package/.output/public/_nuxt/DKySmNPI.js +14 -0
  11. package/.output/public/_nuxt/Di0NY_ht.js +1 -0
  12. package/.output/public/_nuxt/Du9v6qGB.js +1 -0
  13. package/.output/public/_nuxt/{DGbd-UiG.js → EHlEMRii.js} +1 -1
  14. package/.output/public/_nuxt/Eke1fs-U.js +1 -0
  15. package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
  16. package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
  17. package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
  18. package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
  19. package/.output/public/_nuxt/SSyAPaHc.js +1 -0
  20. package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
  21. package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
  22. package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
  23. package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
  24. package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
  25. package/.output/public/_nuxt/aShsv_rG.js +19 -0
  26. package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
  27. package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
  28. package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
  29. package/.output/public/_nuxt/builds/latest.json +1 -1
  30. package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
  31. package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
  32. package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
  33. package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
  34. package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
  35. package/.output/public/_nuxt/lHAxdSsj.js +1 -0
  36. package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
  37. package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
  38. package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
  39. package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
  40. package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
  41. package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
  42. package/.output/server/chunks/_/nitro.mjs +748 -733
  43. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
  44. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
  45. package/.output/server/chunks/build/{TownView--otsnr_7.mjs → TownView-CJ6IsDCc.mjs} +226 -225
  46. package/.output/server/chunks/build/{TownView--otsnr_7.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
  47. package/.output/server/chunks/build/{_id_-Brv-JSvr.mjs → _id_-CVD_H0ir.mjs} +143 -142
  48. package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
  49. package/.output/server/chunks/build/{_id_-D9pl1OZy.mjs → _id_-CtxMb3qp.mjs} +41 -41
  50. package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
  51. package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs → _wsId_-DpY3T-md.mjs} +514 -444
  52. package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
  53. package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
  54. package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
  55. package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
  56. package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
  57. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
  58. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
  59. package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
  60. package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
  61. package/.output/server/chunks/build/{daq-DkMvD8sg.mjs → daq-DWWSKGAV.mjs} +89 -88
  62. package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
  63. package/.output/server/chunks/build/{dcw-C1jgVJkO.mjs → dcw-CT8k17_E.mjs} +44 -43
  64. package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
  65. package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
  66. package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
  67. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
  68. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
  69. package/.output/server/chunks/build/{pages-B0bUWnC6.mjs → pages-BHcSBM2D.mjs} +24 -18
  70. package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
  71. package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
  72. package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
  73. package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
  74. package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
  75. package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
  76. package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
  77. package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
  78. package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
  79. package/.output/server/chunks/build/{town-CoQdblbZ.mjs → town-QJWB5wng.mjs} +3 -2
  80. package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
  81. package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
  82. package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
  83. package/.output/server/chunks/routes/renderer.mjs +1 -1
  84. package/.output/server/chunks/virtual/entry.mjs +637 -86
  85. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  86. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  87. package/.output/server/package.json +1 -1
  88. package/README-zh.md +13 -3
  89. package/README.md +13 -3
  90. package/app/components/AppHeader.vue +961 -959
  91. package/app/components/workshop/A2aRpcDebugger.vue +237 -237
  92. package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
  93. package/app/components/workshop/AgentLanesView.vue +998 -998
  94. package/app/components/workshop/ArtifactCard.vue +120 -120
  95. package/app/components/workshop/AssetLibrary.vue +452 -452
  96. package/app/components/workshop/ChannelSessionList.vue +812 -804
  97. package/app/components/workshop/CommandPalette.vue +269 -269
  98. package/app/components/workshop/Composer.vue +827 -827
  99. package/app/components/workshop/DeviceTwinPanel.vue +643 -643
  100. package/app/components/workshop/EventBlock.vue +602 -602
  101. package/app/components/workshop/FileSelectorModal.vue +188 -188
  102. package/app/components/workshop/InspectorPanel.vue +301 -301
  103. package/app/components/workshop/MemoryPanel.vue +364 -364
  104. package/app/components/workshop/MultiChannelView.vue +189 -189
  105. package/app/components/workshop/TaskBoardView.vue +686 -686
  106. package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
  107. package/app/components/workshop/TranscriptTimeline.vue +601 -601
  108. package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
  109. package/app/components/workshop/blocks/ClusterError.vue +85 -85
  110. package/app/components/workshop/blocks/ClusterLife.vue +87 -87
  111. package/app/components/workshop/blocks/ClusterMember.vue +95 -93
  112. package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
  113. package/app/components/workshop/blocks/ClusterOther.vue +67 -67
  114. package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
  115. package/app/components/workshop/blocks/ClusterTool.vue +158 -158
  116. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
  117. package/app/components/workshop/town/TownView.vue +56 -56
  118. package/app/middleware/locale-cookie.global.ts +22 -0
  119. package/app/pages/aml/index.vue +24 -2
  120. package/app/pages/daq/[id].vue +11 -11
  121. package/app/pages/daq/index.vue +2316 -2316
  122. package/app/pages/dcw/[id].vue +18 -19
  123. package/app/pages/dcw/index.vue +3 -3
  124. package/app/pages/index.vue +987 -981
  125. package/app/pages/plugins/index.vue +317 -311
  126. package/app/pages/settings.vue +1564 -1562
  127. package/app/pages/tokens.vue +15 -15
  128. package/app/pages/workshop/agents.vue +612 -609
  129. package/app/pages/workshop/channel-templates.vue +18 -11
  130. package/app/pages/workshop/index.vue +668 -668
  131. package/app/pages/workshop/teams.vue +743 -740
  132. package/app/pages/workshop/w/[wsId].vue +704 -704
  133. package/app/plugins/locale-restore.client.ts +16 -1
  134. package/app/utils/builtin-catalog.ts +104 -0
  135. package/config.yml +170 -170
  136. package/docs/full-test-plan.md +2 -3
  137. package/i18n/locales/en.ts +293 -35
  138. package/i18n/locales/zh-CN.ts +265 -7
  139. package/package.json +2 -2
  140. package/scripts/_dbg-perms-e2e.mjs +132 -109
  141. package/scripts/_demo-agentteam-capture.mjs +278 -0
  142. package/scripts/_e2e-ui-functional.mjs +371 -0
  143. package/scripts/_i18n-en-cjk-scan.mjs +28 -0
  144. package/scripts/_i18n-en-verify.mjs +107 -0
  145. package/scripts/_i18n-extra-checks.mjs +53 -0
  146. package/scripts/_i18n-scan-hardcoded.mjs +180 -0
  147. package/scripts/_i18n-verify-keys.mjs +50 -0
  148. package/scripts/_loop-biax-campaign.mjs +15 -9
  149. package/scripts/_sweep-button-contrast.mjs +87 -0
  150. package/scripts/ui/record-demo.mjs +557 -0
  151. package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
  152. package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
  153. package/server/services/workshop/daq/drivers.ts +11 -0
  154. package/.output/public/_nuxt/88GOowgO2.js +0 -1
  155. package/.output/public/_nuxt/BN16aRdQ.js +0 -1
  156. package/.output/public/_nuxt/BWVhZntO.js +0 -1
  157. package/.output/public/_nuxt/BXxH0UQE.js +0 -1
  158. package/.output/public/_nuxt/BsROmY89.js +0 -1
  159. package/.output/public/_nuxt/C0ZrS1KL.js +0 -19
  160. package/.output/public/_nuxt/C4s_niy-.js +0 -14
  161. package/.output/public/_nuxt/D6mbFA-a.js +0 -1
  162. package/.output/public/_nuxt/DS8qj8EL.js +0 -1
  163. package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
  164. package/.output/public/_nuxt/Dap5PmTL.js +0 -2
  165. package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
  166. package/.output/public/_nuxt/DvLxILQP.js +0 -1
  167. package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
  168. package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
  169. package/.output/public/_nuxt/_id_.D9ymtHB5.css +0 -1
  170. package/.output/public/_nuxt/_id_.jf-_8E_c.css +0 -1
  171. package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
  172. package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
  173. package/.output/public/_nuxt/builds/meta/78a295bb-2072-48a4-8b46-6d545fdae743.json +0 -1
  174. package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
  175. package/.output/public/_nuxt/daq.DjSgY94l.css +0 -1
  176. package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
  177. package/.output/public/_nuxt/eqq3wWQF.js +0 -18
  178. package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
  179. package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
  180. package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
  181. package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
  182. package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
  183. package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
  184. package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
  185. package/.output/server/chunks/build/_id_-Brv-JSvr.mjs.map +0 -1
  186. package/.output/server/chunks/build/_id_-D9pl1OZy.mjs.map +0 -1
  187. package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
  188. package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
  189. package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
  190. package/.output/server/chunks/build/daq-DkMvD8sg.mjs.map +0 -1
  191. package/.output/server/chunks/build/dcw-C1jgVJkO.mjs.map +0 -1
  192. package/.output/server/chunks/build/pages-B0bUWnC6.mjs.map +0 -1
  193. package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
  194. package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
  195. package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
  196. package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
  197. package/.output/server/chunks/build/town-CoQdblbZ.mjs.map +0 -1
  198. package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
  199. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
  200. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
  201. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
  202. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
  203. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
  204. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
  205. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
  206. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
  207. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
  208. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
  209. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
  210. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
  211. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
  212. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
  213. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
  214. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
  215. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
  216. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
  217. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
  218. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
  219. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
  220. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
  221. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
  222. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
  223. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
  224. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
  225. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
  226. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
  227. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
  228. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
  229. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
  230. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
  231. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
  232. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
  233. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
  234. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
  235. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
  236. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
  237. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
  238. package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
  239. package/scripts/_dbg-audit-all-pages.mjs +0 -43
  240. package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
  241. package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
  242. package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
  243. package/scripts/_dbg-live-line-e2e.mjs +0 -279
  244. package/scripts/_dbg-minimal-omp.mjs +0 -77
  245. package/scripts/_dbg-mode-nav.mjs +0 -173
  246. package/scripts/_dbg-real-scenario-timeline.log +0 -55
  247. package/scripts/_dbg-team-repro.mjs +0 -76
  248. package/scripts/_dbg-town-agents-probe.mjs +0 -60
  249. package/scripts/_dbg-visual-acceptance.mjs +0 -63
  250. package/scripts/_townview_style.css +0 -1185
  251. package/scripts/_townview_template.html +0 -1215
  252. package/server/data/agent-node-bindings.json +0 -554
  253. package/server/data/daq-templates.json +0 -16
  254. package/server/data/daqs.json +0 -7464
  255. package/server/data/dcw-lines.json +0 -1787
  256. package/server/data/dcw-products.json +0 -1626
  257. package/server/data/dcw-recipes.json +0 -4672
  258. package/server/data/dcw-rollback.json +0 -20754
  259. package/server/data/dcw-runs.json +0 -6354
  260. package/server/data/dcw-templates.json +0 -146
  261. package/server/data/dcw-writes.json +0 -15650
  262. package/server/data/dcws.json +0 -9033
  263. package/server/data/device-twins.json +0 -133
  264. package/server/data/line-runs.json +0 -322
  265. package/server/data/scene-layouts.json +0 -277
@@ -1,1011 +1,1011 @@
1
- <script setup lang="ts">
2
- /**
3
- * OmpTerminalPanel —— omp harness 原生终端面板(依赖 DOM/xterm,浏览器端动态加载)。
4
- *
5
- * 数据面:WS /api/system/monitor/terminal/ws?pid&token(#shared/terminal-protocol)
6
- * - 服务端 terminal-hub 镜像 omp `--mode rpc-ui` 子进程的全部 RPC 帧;
7
- * - 本组件把结构化帧流渲染成 xterm TUI(user 回显 / assistant 流式 / thinking /
8
- * 工具执行 / host tool / HITL 对话框),还原"原生终端输出"体验;
9
- * - 控制面(真 Human-in-the-loop):行内输入 → input(steer·follow_up 注入 omp
10
- * 会话);Ctrl+C / 中止按钮 → abort;HITL 面板 → ui_response(select·confirm·
11
- * input·editor 对话框应答,直写 extension_ui_response)。
12
- * - 断线自动重连(指数退避);重连全量重放按 seq 去重,时间线无缝续接。
13
- *
14
- * SSR 安全:xterm(CJS 包)在抽屉打开时动态 import(ensureTerm 内 await),
15
- * 服务端渲染路径零执行;xterm 样式经 nuxt.config css 全局注入。
16
- */
17
- import type { Terminal } from '@xterm/xterm'
18
- import type { FitAddon } from '@xterm/addon-fit'
19
- import { message } from 'ant-design-vue'
20
- import { useUserStore } from '@/app/stores/workshop/user'
21
- import { nowLocalIso } from '~/composables/workshop/useLocalTime'
22
- import type {
23
- TerminalHitlDialog,
24
- TerminalServerMessage,
25
- TermFrame,
26
- } from '#shared/terminal-protocol'
27
-
28
- const props = defineProps<{
29
- /** pid 直连寻址(monitor 进程表) */
30
- pid?: number | null
31
- /** agent 寻址(lanes):解析该成员当前存活的 omp 进程,重启后自动落到新会话 */
32
- agentId?: string | null
33
- channelId?: string | null
34
- /** 抽屉标题附加信息(monitor 行/lane 头快照) */
35
- subtitle?: string
36
- }>()
37
- const open = defineModel<boolean>('open', { default: false })
38
-
39
- const { t } = useI18n()
40
- const userStore = useUserStore()
41
-
42
- // ===== 终端实例 =====
43
- const hostEl = ref<HTMLElement | null>(null)
44
- let term: Terminal | null = null
45
- let fit: FitAddon | null = null
46
- let resizeObs: ResizeObserver | null = null
47
-
48
- // ===== 连接与状态 =====
49
- type ConnState = 'idle' | 'connecting' | 'open' | 'retrying' | 'dead'
50
- const connState = ref<ConnState>('idle')
51
- const alive = ref(true)
52
- const running = ref(false)
53
- const streaming = ref(false)
54
- const hitl = ref<TerminalHitlDialog | null>(null)
55
- let ws: WebSocket | null = null
56
- let retryTimer: ReturnType<typeof setTimeout> | null = null
57
- let retryCount = 0
58
- let closedByUser = false
59
- /** 已渲染 seq(重连全量重放去重) */
60
- let lastRenderedSeq = 0
61
-
62
- // ===== 行内输入(原生终端体验:xterm 内直接打字) =====
63
- let inputLine = ''
64
- let inputHistory: string[] = []
65
- let inputHistoryIdx = -1
66
- /** 流式增量渲染中(避免输入行与输出交错重绘抖动的细粒度锁不需要:批后重绘) */
67
-
68
- // ANSI 快捷片段
69
- const C = {
70
- reset: '\x1b[0m',
71
- dim: '\x1b[2m',
72
- bold: '\x1b[1m',
73
- red: '\x1b[31m',
74
- green: '\x1b[32m',
75
- yellow: '\x1b[33m',
76
- blue: '\x1b[34m',
77
- magenta: '\x1b[35m',
78
- cyan: '\x1b[36m',
79
- dimItalicMagenta: '\x1b[2;3;35m',
80
- }
81
-
82
- /** 流式行未收尾标志:光标停在流式行中间 —— 下一个块/回合收尾前先补换行 */
83
- let streamLineOpen = false
84
-
85
- /**
86
- * 冲刷流式增量:直接续写在光标后。
87
- * 不补换行、不清行、不碰底部输入行 —— 流式批次之间是同一段正文,
88
- * 旧实现按"块语义"逐批强制换行 + 重绘 prompt,导致实时消费出现逐批断行
89
- * (历史重放一次冲刷故正常)。
90
- */
91
- function flushStream(): void {
92
- if (streamBuf.length === 0) return
93
- const text = streamBuf.join('')
94
- streamBuf.length = 0
95
- term?.write(text)
96
- if (!/\r?\n$/.test(text)) streamLineOpen = true
97
- }
98
-
99
- /** 收尾流式行:仅在光标停在流式行中时补一个换行 */
100
- function closeStreamLine(): void {
101
- if (!streamLineOpen) return
102
- streamLineOpen = false
103
- term?.write('\r\n')
104
- }
105
-
106
- /** 写入完整块(独立行):冲刷流式增量并收尾流式行 → 清输入行 → 写块 → 重绘输入行 */
107
- function writeBlock(text: string): void {
108
- if (!term) return
109
- flushStream()
110
- closeStreamLine()
111
- term.write('\r\x1b[K')
112
- term.write(text.endsWith('\r\n') || text.endsWith('\n') ? text : `${text}\r\n`)
113
- renderInputRow()
114
- }
115
-
116
- /** 重绘底部输入行(prompt + 已键入内容);流式行未收尾时跳过(回合收尾后统一补绘) */
117
- function renderInputRow(): void {
118
- if (!term) return
119
- if (streamLineOpen || streamBuf.length > 0) return
120
- term.write(`\r\x1b[K${C.dim}❯${C.reset} ${C.cyan}${inputLine}${C.reset}`)
121
- }
122
-
123
- /**
124
- * 清空视图(切换目标 / 重开抽屉):丢弃上一会话的全部渲染状态与 xterm 缓冲。
125
- * 不清空会把前一个 Agent 的历史输出叠进本 Agent 的终端,重开抽屉还会整环
126
- * 重放造成重复 —— 每个 Agent 的终端必须只渲染自己的信息。
127
- */
128
- function resetView(): void {
129
- streamBuf.length = 0
130
- streamLineOpen = false
131
- assistantStreamActive = false
132
- textSent.clear()
133
- lastHumanInput = ''
134
- inputLine = ''
135
- // 命令历史随视图一并清空:历史属于会话,跨 Agent/重开共享会串上下文
136
- inputHistory = []
137
- inputHistoryIdx = -1
138
- term?.reset()
139
- renderInputRow()
140
- }
141
-
142
- const ts = (): string => {
143
- const d = new Date()
144
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
145
- }
146
-
147
- /** 单行/短文本裁剪(空白折叠) */
148
- function oneLine(text: string, max = 160): string {
149
- const flat = text.replace(/\s+/g, ' ').trim()
150
- return flat.length > max ? `${flat.slice(0, max)}…` : flat
151
- }
152
-
153
- // ===== 帧渲染器:结构化 RPC 帧 → ANSI TUI =====
154
-
155
- /** 最近一次人类输入(follow_up 路径 omp 回显 user 消息 → 去重) */
156
- let lastHumanInput = ''
157
- /** 当前回合 assistant 流式是否已输出过换行收尾标记 */
158
- let assistantStreamActive = false
159
- /** 流式增量缓冲(text/thinking delta 按批 flush,防与输入行交错) */
160
- const streamBuf: string[] = []
161
- /**
162
- * 正文差额账本:contentIndex → 已渲染长度。部分 provider 的 text 块不走
163
- * text_delta(只有换行),全文在 text_end.content 落定 —— 差额补发防止终端缺正文。
164
- */
165
- const textSent = new Map<number, number>()
166
-
167
- function renderFrame(f: TermFrame): void {
168
- if (f.seq <= lastRenderedSeq) return
169
- lastRenderedSeq = f.seq
170
- const frame = f.frame
171
- const type = frame.type as string
172
-
173
- switch (type) {
174
- case '__terminal_notice': {
175
- const level = String(frame.level ?? 'info')
176
- const color = level === 'error' ? C.red : level === 'warning' ? C.yellow : C.dim
177
- writeBlock(`${C.dim}[${ts()}]${C.reset} ${color}⚡ ${String(frame.message ?? '')}${C.reset}`)
178
- return
179
- }
180
- case '__human_input': {
181
- lastHumanInput = String(frame.text ?? '')
182
- writeBlock(`${C.bold}${C.cyan}❯ ${lastHumanInput}${C.reset}`)
183
- return
184
- }
185
- case 'ready':
186
- writeBlock(`${C.green}● omp ready${C.reset} ${C.dim}protocol v${String(frame.protocolVersion ?? '?')} · mode rpc-ui${C.reset}`)
187
- return
188
- case 'agent_start':
189
- assistantStreamActive = false
190
- textSent.clear()
191
- writeBlock(`${C.dim}── agent turn started ──${C.reset}`)
192
- return
193
- case 'agent_end':
194
- writeBlock(`${C.dim}── agent turn ended(${String(frame.messageCount ?? '?')} messages)──${C.reset}`)
195
- return
196
- case 'message_start': {
197
- const role = String(frame.role ?? '')
198
- if (role === 'user') {
199
- const text = String(frame.text ?? '')
200
- // follow_up 的 user 回显与人类输入同文 → 已渲染,跳过;平台注入的任务 prompt 正常回显
201
- if (text === lastHumanInput) return
202
- writeBlock(`${C.bold}${C.cyan}❯ ${text.replace(/\n/g, '\r\n')}${C.reset}`)
203
- }
204
- return
205
- }
206
- case 'message_update': {
207
- const ev = frame.assistantMessageEvent as { type?: string, delta?: string, content?: string, contentIndex?: number } | undefined
208
- if (!ev) return
209
- // 流式增量先入缓冲,按批(flushStream)统一走 writeBlock:
210
- // 直接 term.write 会落在底部输入行上,与输入行互相覆盖
211
- if (ev.type === 'text_delta' && ev.delta) {
212
- assistantStreamActive = true
213
- const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
214
- textSent.set(ci, (textSent.get(ci) ?? 0) + ev.delta.length)
215
- streamBuf.push(ev.delta.replace(/\n/g, '\r\n'))
216
- }
217
- else if (ev.type === 'text_end' && typeof ev.content === 'string' && ev.content.length > 0) {
218
- // 落定差额兜底:provider 未流式 text 块时全文在 text_end,补发未渲染部分
219
- const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
220
- const sent = textSent.get(ci) ?? 0
221
- if (ev.content.length > sent) {
222
- assistantStreamActive = true
223
- textSent.set(ci, ev.content.length)
224
- streamBuf.push(ev.content.slice(sent).replace(/\n/g, '\r\n'))
225
- }
226
- }
227
- else if (ev.type === 'thinking_delta' && ev.delta) {
228
- assistantStreamActive = true
229
- streamBuf.push(`${C.dimItalicMagenta}${ev.delta.replace(/\n/g, '\r\n')}${C.reset}`)
230
- }
231
- return
232
- }
233
- case 'message_end': {
234
- const role = String(frame.role ?? '')
235
- if (role === 'assistant' && assistantStreamActive) {
236
- // 回合收尾:冲刷剩余增量 → 流式行补换行 → 恢复底部输入行
237
- assistantStreamActive = false
238
- flushStream()
239
- closeStreamLine()
240
- renderInputRow()
241
- }
242
- return
243
- }
244
- case 'tool_execution_start':
245
- writeBlock(` ${C.yellow}⚙ ${String(frame.toolName ?? 'tool')}${C.reset}${C.dim}(${oneLine(String(frame.args ?? ''), 120)})${C.reset}`)
246
- return
247
- case 'tool_execution_update':
248
- writeBlock(` ${C.dim}↳ … ${oneLine(String(frame.update ?? ''), 100)}${C.reset}`)
249
- return
250
- case 'tool_execution_end': {
251
- const err = frame.isError === true
252
- const mark = err ? `${C.red}✗${C.reset}` : `${C.green}✓${C.reset}`
253
- const preview = oneLine(String(frame.result ?? ''), 180)
254
- writeBlock(` ${mark} ${C.dim}${preview}${C.reset}`)
255
- return
256
- }
257
- case 'host_tool_call':
258
- writeBlock(` ${C.magenta}⇄ host:${String(frame.toolName ?? '?')}${C.reset}${C.dim}(${oneLine(String(frame.args ?? ''), 100)})${C.reset}`)
259
- return
260
- case 'extension_ui_request': {
261
- const method = String(frame.method ?? '')
262
- if (method === 'cancel') {
263
- writeBlock(t('ompTerminalPanel.k14jk3nu001', { p0: C.dim, p1: String(frame.targetId ?? ''), p2: C.reset }))
264
- // omp 主动撤销(回合中止等)→ 关闭交互面板
265
- if (hitl.value && hitl.value.id === String(frame.targetId ?? '')) hitl.value = null
266
- return
267
- }
268
- if (method === 'notify') {
269
- writeBlock(` ${C.blue}ℹ ${String(frame.message ?? '')}${C.reset}`)
270
- return
271
- }
272
- if (method === 'select' || method === 'confirm' || method === 'input' || method === 'editor') {
273
- const title = String(frame.title ?? '')
274
- writeBlock(`${C.bold}${C.magenta}⚠ HITL · ${method}: ${title}${C.reset}`)
275
- if (Array.isArray(frame.options)) {
276
- for (const opt of frame.options) {
277
- writeBlock(` ${C.magenta}○${C.reset} ${String(opt)}`)
278
- }
279
- }
280
- // 弹出交互面板(实时帧驱动;term.init 的 hitl 仅覆盖重连时已在等待的对话框)
281
- hitl.value = {
282
- id: String(frame.id ?? ''),
283
- method,
284
- title,
285
- options: Array.isArray(frame.options) ? frame.options.map(String) : undefined,
286
- message: typeof frame.message === 'string' ? frame.message : undefined,
287
- placeholder: typeof frame.placeholder === 'string' ? frame.placeholder : undefined,
288
- prefill: typeof frame.prefill === 'string' ? frame.prefill : undefined,
289
- at: nowLocalIso(),
290
- }
291
- }
292
- // 其余 UI 方法(setStatus/setWidget/…)静默
293
- return
294
- }
295
- case 'command_output':
296
- writeBlock(`${C.dim}${String(frame.text ?? '').replace(/\n/g, '\r\n')}${C.reset}`)
297
- return
298
- case 'response': {
299
- const command = String(frame.command ?? '')
300
- // 只回显人工控制命令的受理结果,其余(get_state 等)为噪音
301
- if (command === 'steer' || command === 'follow_up' || command === 'abort' || command === 'prompt') {
302
- const ok = frame.success !== false
303
- writeBlock(` ${C.dim}· ${command} ${ok ? 'accepted' : `failed: ${String(frame.error ?? '')}`}${C.reset}`)
304
- }
305
- return
306
- }
307
- default:
308
- // available_commands_update / session_info_update / config_update / 未知帧:静默
309
- return
310
- }
311
- }
312
-
313
- // ===== WS 生命周期 =====
314
-
315
- /** 连接目标(优先 pid 直连;否则 agentId 解析当前存活进程) */
316
- const hasTarget = computed(() => (props.pid != null && props.pid > 0) || !!props.agentId)
317
- /** 已连接会话的实际 pid(agent 寻址下由 term.init 回填;进程重启后变化) */
318
- const livePid = ref<number | null>(null)
319
-
320
- function wsUrl(): string {
321
- const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
322
- const token = encodeURIComponent(userStore.token ?? '')
323
- const target = props.pid != null && props.pid > 0
324
- ? `pid=${props.pid}`
325
- : `agentId=${encodeURIComponent(props.agentId ?? '')}${props.channelId ? `&channelId=${encodeURIComponent(props.channelId)}` : ''}`
326
- return `${proto}://${window.location.host}/api/system/monitor/terminal/ws?${target}&token=${token}`
327
- }
328
-
329
- /** 上一次错误去重(NO_SESSION 重试风暴防刷屏) */
330
- let lastErrorMessage = ''
331
-
332
- function handleServerMessage(msg: TerminalServerMessage): void {
333
- switch (msg.type) {
334
- case 'term.init': {
335
- // 进程重启(agent 寻址重连)→ 新会话 seq 从 1 重来,去重游标必须重置
336
- if (livePid.value !== msg.meta.pid) {
337
- livePid.value = msg.meta.pid
338
- lastRenderedSeq = 0
339
- lastErrorMessage = ''
340
- writeBlock(t('ompTerminalPanel.k68kkpc002', { p0: C.dim, p1: msg.meta.pid, p2: msg.lastSeq, p3: C.reset }))
341
- }
342
- alive.value = msg.alive
343
- running.value = msg.running
344
- streaming.value = msg.streaming
345
- hitl.value = msg.hitl
346
- return
347
- }
348
- case 'term.frames':
349
- for (const f of msg.frames) renderFrame(f)
350
- flushStream()
351
- return
352
- case 'term.state':
353
- alive.value = msg.alive
354
- running.value = msg.running
355
- streaming.value = msg.streaming
356
- return
357
- case 'term.notice':
358
- message.info(msg.message)
359
- return
360
- case 'term.error': {
361
- // 重试期重复同错误只提示一次(进程未启动时每 2s 一条会刷屏)
362
- const line = `[${msg.code}] ${msg.message}`
363
- if (line === lastErrorMessage) return
364
- lastErrorMessage = line
365
- writeBlock(`${C.red}✗ ${line}${C.reset}`)
366
- return
367
- }
368
- case 'pong':
369
- return
370
- }
371
- }
372
-
373
- function connect(): void {
374
- if (!hasTarget.value || !userStore.token) return
375
- if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
376
- closedByUser = false
377
- connState.value = retryCount === 0 ? 'connecting' : 'retrying'
378
- const sock = new WebSocket(wsUrl())
379
- ws = sock
380
- sock.onopen = () => {
381
- if (ws !== sock) return
382
- retryCount = 0
383
- connState.value = 'open'
384
- }
385
- sock.onmessage = (ev) => {
386
- if (ws !== sock) return
387
- try {
388
- handleServerMessage(JSON.parse(ev.data as string) as TerminalServerMessage)
389
- }
390
- catch { /* 非 JSON 帧忽略 */ }
391
- }
392
- sock.onclose = () => {
393
- if (ws !== sock) return
394
- ws = null
395
- hitl.value = null
396
- if (closedByUser || !hasTarget.value) {
397
- connState.value = 'dead'
398
- return
399
- }
400
- // 断线重连:1s 起指数退避,上限 10s(agent 寻址下进程未启动/重启由
401
- // NO_SESSION 关闭驱动重试,直到首个任务触发 spawn 后接入)
402
- retryCount += 1
403
- const delay = Math.min(1000 * 2 ** Math.min(retryCount - 1, 4), 10_000)
404
- connState.value = 'retrying'
405
- retryTimer = setTimeout(() => {
406
- retryTimer = null
407
- if (!closedByUser && hasTarget.value) connect()
408
- }, delay)
409
- }
410
- sock.onerror = () => { /* onclose 兜底 */ }
411
- }
412
-
413
- function disconnect(): void {
414
- closedByUser = true
415
- if (retryTimer) {
416
- clearTimeout(retryTimer)
417
- retryTimer = null
418
- }
419
- ws?.close()
420
- ws = null
421
- }
422
-
423
- // ===== 控制面动作 =====
424
-
425
- function sendRaw(obj: unknown): void {
426
- if (ws?.readyState === WebSocket.OPEN) {
427
- ws.send(JSON.stringify(obj))
428
- return
429
- }
430
- writeBlock(t('ompTerminalPanel.k59zj91003', { p0: C.red, p1: C.reset }))
431
- }
432
-
433
- function submitInput(text: string): void {
434
- const trimmed = text.trim()
435
- if (!trimmed) return
436
- inputHistory.push(trimmed)
437
- inputHistory = inputHistory.slice(-50)
438
- inputHistoryIdx = -1
439
- sendRaw({ type: 'input', text: trimmed })
440
- }
441
-
442
- function doAbort(): void {
443
- sendRaw({ type: 'abort' })
444
- writeBlock(`${C.yellow}^C${C.reset}`)
445
- }
446
-
447
- function hitlRespond(response: { id: string, value?: string, confirmed?: boolean, cancelled?: boolean }): void {
448
- sendRaw({ type: 'ui_response', ...response })
449
- const label = response.cancelled
450
- ? 'cancelled'
451
- : response.confirmed !== undefined
452
- ? String(response.confirmed)
453
- : `"${oneLine(response.value ?? '', 80)}"`
454
- writeBlock(t('ompTerminalPanel.kvzhi95004', { p0: C.green, p1: label, p2: C.reset }))
455
- hitl.value = null
456
- }
457
-
458
- const hitlText = ref('')
459
- watch(hitl, (h) => {
460
- hitlText.value = h?.prefill ?? h?.placeholder ?? ''
461
- })
462
-
463
- // ===== xterm 装配与输入处理 =====
464
-
465
- let termKeydown: ((e: KeyboardEvent) => void) | null = null
466
-
467
- async function ensureTerm(): Promise<void> {
468
- if (term || !hostEl.value) return
469
- // 模板 ref 的 unibabel 结构类型与 lib.dom 的 HTMLElement 结构不完全兼容,
470
- // 跨库边界(xterm / ResizeObserver)显式收窄
471
- const host: HTMLElement = hostEl.value as unknown as HTMLElement
472
- // 浏览器端动态加载 xterm(SSR 零执行;CJS 包不能静态命名导入)
473
- const [{ Terminal: XTerm }, { FitAddon: XFit }] = await Promise.all([
474
- import('@xterm/xterm'),
475
- import('@xterm/addon-fit'),
476
- ])
477
- term = new XTerm({
478
- convertEol: false,
479
- cursorBlink: true,
480
- cursorStyle: 'bar',
481
- fontFamily: '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
482
- fontSize: 12.5,
483
- lineHeight: 1.25,
484
- scrollback: 8000,
485
- theme: {
486
- background: '#10151c',
487
- foreground: '#d6dee8',
488
- cursor: '#7aa2f7',
489
- selectionBackground: '#33467c',
490
- black: '#10151c',
491
- red: '#f7768e',
492
- green: '#9ece6a',
493
- yellow: '#e0af68',
494
- blue: '#7aa2f7',
495
- magenta: '#bb9af7',
496
- cyan: '#7dcfff',
497
- white: '#d6dee8',
498
- },
499
- })
500
- fit = new XFit()
501
- term.loadAddon(fit)
502
- term.open(host)
503
- renderInputRow()
504
-
505
- // 输入通道 1:宿主容器 keydown(捕获阶段)——真实键盘与合成事件(CDP/自动化)
506
- // 都可靠触发;拦截已处理的键,阻止 xterm 内部重复消费
507
- const onKeyDown = (e: KeyboardEvent): void => {
508
- if (e.ctrlKey || e.metaKey || e.altKey) {
509
- if (e.key === 'c' || e.key === 'C') {
510
- e.preventDefault()
511
- e.stopPropagation()
512
- doAbort()
513
- }
514
- return // 其余组合键(Ctrl+R/F5 等)交给浏览器
515
- }
516
- if (e.key === 'Enter') {
517
- e.preventDefault()
518
- e.stopPropagation()
519
- // 清掉本地键入行,由服务端 __human_input 回显统一渲染(回放观众一致,无重复)
520
- term?.write('\r\x1b[K')
521
- const line = inputLine
522
- inputLine = ''
523
- submitInput(line)
524
- renderInputRow()
525
- return
526
- }
527
- if (e.key === 'Backspace') {
528
- e.preventDefault()
529
- e.stopPropagation()
530
- inputLine = inputLine.slice(0, -1)
531
- renderInputRow()
532
- return
533
- }
534
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
535
- if (inputHistory.length === 0) return
536
- e.preventDefault()
537
- e.stopPropagation()
538
- if (e.key === 'ArrowUp') {
539
- inputHistoryIdx = inputHistoryIdx < 0 ? inputHistory.length - 1 : Math.max(0, inputHistoryIdx - 1)
540
- }
541
- else {
542
- inputHistoryIdx = inputHistoryIdx < 0 ? -1 : Math.min(inputHistory.length - 1, inputHistoryIdx + 1)
543
- if (inputHistoryIdx === inputHistory.length - 1) inputHistoryIdx = -1
544
- }
545
- inputLine = inputHistoryIdx < 0 ? '' : (inputHistory[inputHistoryIdx] ?? '')
546
- renderInputRow()
547
- return
548
- }
549
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
550
- e.preventDefault()
551
- e.stopPropagation()
552
- inputLine += e.key
553
- renderInputRow()
554
- }
555
- }
556
- host.addEventListener('keydown', onKeyDown, true)
557
- termKeydown = onKeyDown
558
-
559
- // 输入通道 2:xterm onData 仅消费多字符输入(粘贴;单字符已由 keydown 处理)
560
- term.onData((data) => {
561
- if (data.length <= 1) return
562
- inputLine += data.replace(/[\r\n]+$/, '')
563
- renderInputRow()
564
- })
565
-
566
- resizeObs = new ResizeObserver(() => {
567
- try {
568
- fit?.fit()
569
- }
570
- catch { /* 尺寸未就绪 */ }
571
- })
572
- resizeObs.observe(host)
573
- }
574
-
575
- function teardownTerm(): void {
576
- if (termKeydown && hostEl.value) {
577
- hostEl.value.removeEventListener('keydown', termKeydown, true)
578
- termKeydown = null
579
- }
580
- resizeObs?.disconnect()
581
- resizeObs = null
582
- term?.dispose()
583
- term = null
584
- fit = null
585
- }
586
-
587
- // ===== 生命周期:抽屉开关驱动连接/实例 =====
588
- watch(open, (v) => {
589
- if (v) {
590
- nextTick(async () => {
591
- await ensureTerm()
592
- resetView()
593
- lastRenderedSeq = 0
594
- retryCount = 0
595
- lastErrorMessage = ''
596
- writeBlock(`${C.dim}── AgentWorkShop harness terminal · ${props.pid ? `PID ${props.pid}` : `agent ${props.agentId?.slice(0, 8)}…`} ──${C.reset}`)
597
- connect()
598
- nextTick(() => {
599
- try {
600
- fit?.fit()
601
- }
602
- catch { /* ignore */ }
603
- })
604
- })
605
- }
606
- else {
607
- disconnect()
608
- hitl.value = null
609
- }
610
- })
611
-
612
- /** 目标切换(pid 直连或 agent 寻址变化)→ 清空视图重连(不同 Agent 的输出不得混渲染) */
613
- watch(() => [props.pid, props.agentId], () => {
614
- if (!open.value) return
615
- disconnect()
616
- nextTick(() => {
617
- resetView()
618
- lastRenderedSeq = 0
619
- retryCount = 0
620
- lastErrorMessage = ''
621
- livePid.value = null
622
- writeBlock(`${C.dim}── 切换目标 · ${props.pid ? `PID ${props.pid}` : `agent ${props.agentId?.slice(0, 8)}…`} ──${C.reset}`)
623
- connect()
624
- })
625
- })
626
-
627
- onBeforeUnmount(() => {
628
- disconnect()
629
- teardownTerm()
630
- })
631
-
632
- const doClear = (): void => {
633
- streamBuf.length = 0
634
- streamLineOpen = false
635
- term?.clear()
636
- renderInputRow()
637
- }
638
-
639
- const stateLabel = computed(() => {
640
- if (!alive.value) return { text: t('terminal.stateExited'), color: 'error' as const }
641
- if (streaming.value) return { text: t('terminal.stateStreaming'), color: 'processing' as const }
642
- if (running.value) return { text: t('terminal.stateRunning'), color: 'warning' as const }
643
- return { text: t('terminal.stateIdle'), color: 'success' as const }
644
- })
645
- const connLabel = computed(() => {
646
- switch (connState.value) {
647
- case 'open': return { text: t('terminal.connOpen'), color: 'success' as const }
648
- case 'connecting': return { text: t('terminal.connConnecting'), color: 'default' as const }
649
- case 'retrying': return { text: `${t('terminal.connRetrying')}(${retryCount})`, color: 'warning' as const }
650
- default: return { text: t('terminal.connClosed'), color: 'default' as const }
651
- }
652
- })
653
- </script>
654
-
655
- <template>
656
- <a-drawer
657
- v-model:open="open"
658
- placement="right"
659
- :width="860"
660
- :body-style="{ padding: '0', display: 'flex', flexDirection: 'column', background: '#10151c' }"
661
- :header-style="{ background: '#10151c', borderBottom: '1px solid #1d2733' }"
662
- class="omp-terminal-drawer"
663
- >
664
- <template #title>
665
- <div class="term-title">
666
- <span class="term-brand">
667
- <span class="i-tabler-terminal-2" />
668
- </span>
669
- <span class="term-name">{{ t('terminal.title') }}</span>
670
- <span class="ts-chip aw-mono ts-pid">PID {{ livePid ?? pid ?? '–' }}</span>
671
- <span
672
- v-if="subtitle"
673
- class="ts-chip ts-sub"
674
- >{{ subtitle }}</span>
675
- </div>
676
- </template>
677
- <template #extra>
678
- <div class="term-toolbar">
679
- <!-- 连接态 chip:呼吸点 + 文本(open=绿 / retrying=琥珀脉冲 / 其他=灰蓝) -->
680
- <span
681
- class="ts-chip aw-mono"
682
- :data-state="connState"
683
- ><span class="ts-dot" />{{ connLabel.text }}</span>
684
- <!-- 会话态 chip:idle=青 / busy=琥珀 / 流式=青脉冲 -->
685
- <span
686
- class="ts-chip aw-mono"
687
- :data-live="running ? (streaming ? 'streaming' : 'running') : 'idle'"
688
- >{{ stateLabel.text }}</span>
689
- <a-button
690
- size="small"
691
- danger
692
- class="ts-btn"
693
- :disabled="!running && !streaming"
694
- @click="doAbort"
695
- >
696
- <template #icon>
697
- <span class="i-tabler-player-stop" />
698
- </template>
699
- {{ t('terminal.abort') }}
700
- </a-button>
701
- <a-button
702
- size="small"
703
- class="ts-btn"
704
- @click="doClear"
705
- >
706
- <template #icon>
707
- <span class="i-tabler-eraser" />
708
- </template>
709
- {{ t('terminal.clear') }}
710
- </a-button>
711
- </div>
712
- </template>
713
-
714
- <div class="term-body">
715
- <!-- HITL 对话框(omp extension_ui_request;answer → ui_response) -->
716
- <div
717
- v-if="hitl"
718
- class="hitl-card"
719
- >
720
- <div class="hitl-head">
721
- <span class="i-tabler-user-question" />
722
- <b>{{ t('terminal.hitlTitle') }}</b>
723
- <a-tag color="magenta">
724
- {{ hitl.method }}
725
- </a-tag>
726
- <a-button
727
- size="small"
728
- type="text"
729
- danger
730
- @click="hitlRespond({ id: hitl.id, cancelled: true })"
731
- >
732
- {{ t('terminal.hitlCancel') }}
733
- </a-button>
734
- </div>
735
- <p class="hitl-question">
736
- {{ hitl.title }}
737
- </p>
738
-
739
- <template v-if="hitl.method === 'select'">
740
- <div class="hitl-options">
741
- <a-button
742
- v-for="opt in hitl.options ?? []"
743
- :key="opt"
744
- block
745
- @click="hitlRespond({ id: hitl.id, value: opt })"
746
- >
747
- {{ opt }}
748
- </a-button>
749
- </div>
750
- </template>
751
- <template v-else-if="hitl.method === 'confirm'">
752
- <p class="hitl-message">
753
- {{ hitl.message }}
754
- </p>
755
- <div class="hitl-row">
756
- <a-button
757
- type="primary"
758
- @click="hitlRespond({ id: hitl.id, confirmed: true })"
759
- >
760
- {{ t('terminal.hitlConfirm') }}
761
- </a-button>
762
- <a-button @click="hitlRespond({ id: hitl.id, confirmed: false })">
763
- {{ t('terminal.hitlDeny') }}
764
- </a-button>
765
- </div>
766
- </template>
767
- <template v-else>
768
- <a-textarea
769
- v-model:value="hitlText"
770
- :rows="3"
771
- :placeholder="hitl.placeholder"
772
- class="hitl-textarea"
773
- @press-enter="hitlRespond({ id: hitl.id, value: hitlText })"
774
- />
775
- <div class="hitl-row">
776
- <a-button
777
- type="primary"
778
- @click="hitlRespond({ id: hitl.id, value: hitlText })"
779
- >
780
- {{ t('terminal.hitlSubmit') }}
781
- </a-button>
782
- <a-button @click="hitlRespond({ id: hitl.id, cancelled: true })">
783
- {{ t('terminal.hitlCancel') }}
784
- </a-button>
785
- </div>
786
- </template>
787
- </div>
788
-
789
- <!-- xterm 终端 -->
790
- <div
791
- ref="hostEl"
792
- class="term-host"
793
- />
794
-
795
- <div class="term-hint aw-kicker">
796
- {{ t('terminal.hint') }}
797
- </div>
798
- </div>
799
- </a-drawer>
800
- </template>
801
-
802
- <style scoped>
803
- /* 状态栏品牌块:终端图标入方形墨章(与侧栏墨方印同族,tokyo-night 冷暖对撞) */
804
- .term-title {
805
- display: inline-flex;
806
- gap: 10px;
807
- align-items: center;
808
- }
809
- .term-brand {
810
- display: flex;
811
- align-items: center;
812
- justify-content: center;
813
- width: 26px;
814
- height: 26px;
815
- font-size: 14px;
816
- color: #9ece6a;
817
- background: rgb(158 206 106 / 10%);
818
- border: 1px solid rgb(158 206 106 / 22%);
819
- border-radius: var(--radius-panel-sm);
820
- }
821
- .term-name {
822
- font-size: 13.5px;
823
- font-weight: 600;
824
- letter-spacing: 0.01em;
825
- color: #e6edf3;
826
- }
827
-
828
- /* 状态 chip:等宽微字 + 语义点;色板对齐 xterm tokyo-night */
829
- .ts-chip {
830
- display: inline-flex;
831
- gap: 6px;
832
- align-items: center;
833
- padding: 1px 8px;
834
- font-size: 10.5px;
835
- letter-spacing: 0.04em;
836
- color: #a9b1d6;
837
- background: rgb(65 72 104 / 26%);
838
- border: 1px solid rgb(146 153 179 / 16%);
839
- border-radius: var(--radius-pill);
840
- }
841
- .ts-chip.ts-pid { color: #7dcfff; }
842
- .ts-chip.ts-sub {
843
- font-family: var(--font-body);
844
- color: #9aa5ce;
845
- }
846
- .ts-dot {
847
- width: 6px;
848
- height: 6px;
849
- background: #565f89;
850
- border-radius: 50%;
851
- }
852
- /* 连接态语义色 */
853
- .ts-chip[data-state='open'] { color: #9ece6a; background: rgb(158 206 106 / 10%); border-color: rgb(158 206 106 / 22%); }
854
- .ts-chip[data-state='open'] .ts-dot { background: #9ece6a; animation: ts-breathe 2.2s ease-in-out infinite; }
855
- .ts-chip[data-state='retrying'] { color: #e0af68; background: rgb(224 175 104 / 10%); border-color: rgb(224 175 104 / 24%); }
856
- .ts-chip[data-state='retrying'] .ts-dot { background: #e0af68; animation: ts-breathe 1.1s ease-in-out infinite; }
857
- .ts-chip[data-state='connecting'] { color: #7aa2f7; }
858
- .ts-chip[data-state='connecting'] .ts-dot { background: #7aa2f7; animation: ts-breathe 1.1s ease-in-out infinite; }
859
- /* 会话态 */
860
- .ts-chip[data-live='streaming'] { color: #7dcfff; background: rgb(125 207 255 / 10%); border-color: rgb(125 207 255 / 24%); }
861
- .ts-chip[data-live='streaming']::before {
862
- content: "";
863
- width: 6px;
864
- height: 6px;
865
- background: #7dcfff;
866
- border-radius: 50%;
867
- animation: ts-breathe 0.9s ease-in-out infinite;
868
- }
869
- .ts-chip[data-live='running'] { color: #e0af68; background: rgb(224 175 104 / 10%); border-color: rgb(224 175 104 / 24%); }
870
- .ts-chip[data-live='running']::before {
871
- content: "";
872
- width: 6px;
873
- height: 6px;
874
- background: #e0af68;
875
- border-radius: 50%;
876
- animation: ts-breathe 1.6s ease-in-out infinite;
877
- }
878
- .ts-chip[data-live='idle'] { color: #565f89; }
879
- .ts-chip[data-live='idle']::before {
880
- content: "";
881
- width: 6px;
882
- height: 6px;
883
- background: #565f89;
884
- border-radius: 50%;
885
- }
886
- @keyframes ts-breathe {
887
- 0%, 100% { opacity: 0.45; }
888
- 50% { opacity: 1; }
889
- }
890
- @media (prefers-reduced-motion: reduce) {
891
- .ts-dot, .ts-chip::before { animation: none !important; opacity: 0.85; }
892
- }
893
-
894
- /* 工具按钮:暗面浮起 + 按压反馈 */
895
- .term-toolbar {
896
- display: flex;
897
- gap: 8px;
898
- align-items: center;
899
- }
900
- :deep(.ts-btn.ant-btn) {
901
- display: inline-flex;
902
- gap: 5px;
903
- align-items: center;
904
- color: #c0caf5;
905
- background: rgb(65 72 104 / 30%);
906
- border: 1px solid rgb(146 153 179 / 18%);
907
- transition: background 0.15s ease, transform 0.12s ease;
908
- }
909
- :deep(.ts-btn.ant-btn:hover:not(:disabled)) { background: rgb(65 72 104 / 50%); }
910
- :deep(.ts-btn.ant-btn:active:not(:disabled)) { transform: translateY(1px); }
911
- :deep(.ts-btn.ant-btn:disabled) { opacity: 0.4; }
912
-
913
- .term-body {
914
- position: relative;
915
- display: flex;
916
- flex: 1;
917
- min-height: 0;
918
- flex-direction: column;
919
- }
920
-
921
- .term-host {
922
- flex: 1;
923
- min-height: 0;
924
- padding: 10px 12px 4px;
925
- }
926
-
927
- .term-hint {
928
- padding: 6px 12px 8px;
929
- color: rgb(214 222 232 / 45%);
930
- background: #10151c;
931
- }
932
-
933
- /* HITL 对话框:悬浮在终端上方(正在等待人类决策) */
934
- .hitl-card {
935
- position: absolute;
936
- top: 14px;
937
- right: 16px;
938
- z-index: 10;
939
- width: min(420px, calc(100% - 32px));
940
- padding: 12px 14px;
941
- background: rgb(23 30 40 / 97%);
942
- border: 1px solid #bb9af7;
943
- border-radius: var(--radius-panel-sm);
944
- box-shadow: 0 8px 28px rgb(0 0 0 / 55%);
945
- }
946
-
947
- .hitl-head {
948
- display: flex;
949
- gap: 8px;
950
- align-items: center;
951
- margin-bottom: 8px;
952
- color: #bb9af7;
953
- }
954
-
955
- .hitl-head > b {
956
- color: #e6edf3;
957
- }
958
-
959
- .hitl-head .ant-btn {
960
- margin-left: auto;
961
- }
962
-
963
- .hitl-question {
964
- margin: 0 0 10px;
965
- font-weight: 600;
966
- color: #e6edf3;
967
- white-space: pre-wrap;
968
- }
969
-
970
- .hitl-message {
971
- margin: 0 0 10px;
972
- color: #a9b4c0;
973
- white-space: pre-wrap;
974
- }
975
-
976
- .hitl-options {
977
- display: flex;
978
- max-height: 300px;
979
- flex-direction: column;
980
- gap: 6px;
981
- overflow: auto;
982
- }
983
-
984
- .hitl-options .ant-btn {
985
- justify-content: flex-start;
986
- text-align: left;
987
- white-space: normal;
988
- }
989
-
990
- .hitl-row {
991
- display: flex;
992
- gap: 8px;
993
- margin-top: 10px;
994
- }
995
-
996
- .hitl-textarea {
997
- margin-top: 4px;
998
- }
999
- </style>
1000
-
1001
- <style>
1002
- /* drawer 挂 body teleport,全局态:暗色标题/内容(xterm 主题一致) */
1003
- .omp-terminal-drawer .ant-drawer-header {
1004
- background: linear-gradient(180deg, #131a24 0%, #10151c 100%) !important;
1005
- border-bottom: 1px solid #1d2733 !important;
1006
- box-shadow: inset 0 1px 0 rgb(146 153 179 / 6%);
1007
- }
1008
- .omp-terminal-drawer .ant-drawer-header-title {
1009
- color: #e6edf3;
1010
- }
1011
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * OmpTerminalPanel —— omp harness 原生终端面板(依赖 DOM/xterm,浏览器端动态加载)。
4
+ *
5
+ * 数据面:WS /api/system/monitor/terminal/ws?pid&token(#shared/terminal-protocol)
6
+ * - 服务端 terminal-hub 镜像 omp `--mode rpc-ui` 子进程的全部 RPC 帧;
7
+ * - 本组件把结构化帧流渲染成 xterm TUI(user 回显 / assistant 流式 / thinking /
8
+ * 工具执行 / host tool / HITL 对话框),还原"原生终端输出"体验;
9
+ * - 控制面(真 Human-in-the-loop):行内输入 → input(steer·follow_up 注入 omp
10
+ * 会话);Ctrl+C / 中止按钮 → abort;HITL 面板 → ui_response(select·confirm·
11
+ * input·editor 对话框应答,直写 extension_ui_response)。
12
+ * - 断线自动重连(指数退避);重连全量重放按 seq 去重,时间线无缝续接。
13
+ *
14
+ * SSR 安全:xterm(CJS 包)在抽屉打开时动态 import(ensureTerm 内 await),
15
+ * 服务端渲染路径零执行;xterm 样式经 nuxt.config css 全局注入。
16
+ */
17
+ import type { Terminal } from '@xterm/xterm'
18
+ import type { FitAddon } from '@xterm/addon-fit'
19
+ import { message } from 'ant-design-vue'
20
+ import { useUserStore } from '@/app/stores/workshop/user'
21
+ import { nowLocalIso } from '~/composables/workshop/useLocalTime'
22
+ import type {
23
+ TerminalHitlDialog,
24
+ TerminalServerMessage,
25
+ TermFrame,
26
+ } from '#shared/terminal-protocol'
27
+
28
+ const props = defineProps<{
29
+ /** pid 直连寻址(monitor 进程表) */
30
+ pid?: number | null
31
+ /** agent 寻址(lanes):解析该成员当前存活的 omp 进程,重启后自动落到新会话 */
32
+ agentId?: string | null
33
+ channelId?: string | null
34
+ /** 抽屉标题附加信息(monitor 行/lane 头快照) */
35
+ subtitle?: string
36
+ }>()
37
+ const open = defineModel<boolean>('open', { default: false })
38
+
39
+ const { t } = useI18n()
40
+ const userStore = useUserStore()
41
+
42
+ // ===== 终端实例 =====
43
+ const hostEl = ref<HTMLElement | null>(null)
44
+ let term: Terminal | null = null
45
+ let fit: FitAddon | null = null
46
+ let resizeObs: ResizeObserver | null = null
47
+
48
+ // ===== 连接与状态 =====
49
+ type ConnState = 'idle' | 'connecting' | 'open' | 'retrying' | 'dead'
50
+ const connState = ref<ConnState>('idle')
51
+ const alive = ref(true)
52
+ const running = ref(false)
53
+ const streaming = ref(false)
54
+ const hitl = ref<TerminalHitlDialog | null>(null)
55
+ let ws: WebSocket | null = null
56
+ let retryTimer: ReturnType<typeof setTimeout> | null = null
57
+ let retryCount = 0
58
+ let closedByUser = false
59
+ /** 已渲染 seq(重连全量重放去重) */
60
+ let lastRenderedSeq = 0
61
+
62
+ // ===== 行内输入(原生终端体验:xterm 内直接打字) =====
63
+ let inputLine = ''
64
+ let inputHistory: string[] = []
65
+ let inputHistoryIdx = -1
66
+ /** 流式增量渲染中(避免输入行与输出交错重绘抖动的细粒度锁不需要:批后重绘) */
67
+
68
+ // ANSI 快捷片段
69
+ const C = {
70
+ reset: '\x1b[0m',
71
+ dim: '\x1b[2m',
72
+ bold: '\x1b[1m',
73
+ red: '\x1b[31m',
74
+ green: '\x1b[32m',
75
+ yellow: '\x1b[33m',
76
+ blue: '\x1b[34m',
77
+ magenta: '\x1b[35m',
78
+ cyan: '\x1b[36m',
79
+ dimItalicMagenta: '\x1b[2;3;35m',
80
+ }
81
+
82
+ /** 流式行未收尾标志:光标停在流式行中间 —— 下一个块/回合收尾前先补换行 */
83
+ let streamLineOpen = false
84
+
85
+ /**
86
+ * 冲刷流式增量:直接续写在光标后。
87
+ * 不补换行、不清行、不碰底部输入行 —— 流式批次之间是同一段正文,
88
+ * 旧实现按"块语义"逐批强制换行 + 重绘 prompt,导致实时消费出现逐批断行
89
+ * (历史重放一次冲刷故正常)。
90
+ */
91
+ function flushStream(): void {
92
+ if (streamBuf.length === 0) return
93
+ const text = streamBuf.join('')
94
+ streamBuf.length = 0
95
+ term?.write(text)
96
+ if (!/\r?\n$/.test(text)) streamLineOpen = true
97
+ }
98
+
99
+ /** 收尾流式行:仅在光标停在流式行中时补一个换行 */
100
+ function closeStreamLine(): void {
101
+ if (!streamLineOpen) return
102
+ streamLineOpen = false
103
+ term?.write('\r\n')
104
+ }
105
+
106
+ /** 写入完整块(独立行):冲刷流式增量并收尾流式行 → 清输入行 → 写块 → 重绘输入行 */
107
+ function writeBlock(text: string): void {
108
+ if (!term) return
109
+ flushStream()
110
+ closeStreamLine()
111
+ term.write('\r\x1b[K')
112
+ term.write(text.endsWith('\r\n') || text.endsWith('\n') ? text : `${text}\r\n`)
113
+ renderInputRow()
114
+ }
115
+
116
+ /** 重绘底部输入行(prompt + 已键入内容);流式行未收尾时跳过(回合收尾后统一补绘) */
117
+ function renderInputRow(): void {
118
+ if (!term) return
119
+ if (streamLineOpen || streamBuf.length > 0) return
120
+ term.write(`\r\x1b[K${C.dim}❯${C.reset} ${C.cyan}${inputLine}${C.reset}`)
121
+ }
122
+
123
+ /**
124
+ * 清空视图(切换目标 / 重开抽屉):丢弃上一会话的全部渲染状态与 xterm 缓冲。
125
+ * 不清空会把前一个 Agent 的历史输出叠进本 Agent 的终端,重开抽屉还会整环
126
+ * 重放造成重复 —— 每个 Agent 的终端必须只渲染自己的信息。
127
+ */
128
+ function resetView(): void {
129
+ streamBuf.length = 0
130
+ streamLineOpen = false
131
+ assistantStreamActive = false
132
+ textSent.clear()
133
+ lastHumanInput = ''
134
+ inputLine = ''
135
+ // 命令历史随视图一并清空:历史属于会话,跨 Agent/重开共享会串上下文
136
+ inputHistory = []
137
+ inputHistoryIdx = -1
138
+ term?.reset()
139
+ renderInputRow()
140
+ }
141
+
142
+ const ts = (): string => {
143
+ const d = new Date()
144
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
145
+ }
146
+
147
+ /** 单行/短文本裁剪(空白折叠) */
148
+ function oneLine(text: string, max = 160): string {
149
+ const flat = text.replace(/\s+/g, ' ').trim()
150
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat
151
+ }
152
+
153
+ // ===== 帧渲染器:结构化 RPC 帧 → ANSI TUI =====
154
+
155
+ /** 最近一次人类输入(follow_up 路径 omp 回显 user 消息 → 去重) */
156
+ let lastHumanInput = ''
157
+ /** 当前回合 assistant 流式是否已输出过换行收尾标记 */
158
+ let assistantStreamActive = false
159
+ /** 流式增量缓冲(text/thinking delta 按批 flush,防与输入行交错) */
160
+ const streamBuf: string[] = []
161
+ /**
162
+ * 正文差额账本:contentIndex → 已渲染长度。部分 provider 的 text 块不走
163
+ * text_delta(只有换行),全文在 text_end.content 落定 —— 差额补发防止终端缺正文。
164
+ */
165
+ const textSent = new Map<number, number>()
166
+
167
+ function renderFrame(f: TermFrame): void {
168
+ if (f.seq <= lastRenderedSeq) return
169
+ lastRenderedSeq = f.seq
170
+ const frame = f.frame
171
+ const type = frame.type as string
172
+
173
+ switch (type) {
174
+ case '__terminal_notice': {
175
+ const level = String(frame.level ?? 'info')
176
+ const color = level === 'error' ? C.red : level === 'warning' ? C.yellow : C.dim
177
+ writeBlock(`${C.dim}[${ts()}]${C.reset} ${color}⚡ ${String(frame.message ?? '')}${C.reset}`)
178
+ return
179
+ }
180
+ case '__human_input': {
181
+ lastHumanInput = String(frame.text ?? '')
182
+ writeBlock(`${C.bold}${C.cyan}❯ ${lastHumanInput}${C.reset}`)
183
+ return
184
+ }
185
+ case 'ready':
186
+ writeBlock(`${C.green}● omp ready${C.reset} ${C.dim}protocol v${String(frame.protocolVersion ?? '?')} · mode rpc-ui${C.reset}`)
187
+ return
188
+ case 'agent_start':
189
+ assistantStreamActive = false
190
+ textSent.clear()
191
+ writeBlock(`${C.dim}── agent turn started ──${C.reset}`)
192
+ return
193
+ case 'agent_end':
194
+ writeBlock(`${C.dim}── agent turn ended(${String(frame.messageCount ?? '?')} messages)──${C.reset}`)
195
+ return
196
+ case 'message_start': {
197
+ const role = String(frame.role ?? '')
198
+ if (role === 'user') {
199
+ const text = String(frame.text ?? '')
200
+ // follow_up 的 user 回显与人类输入同文 → 已渲染,跳过;平台注入的任务 prompt 正常回显
201
+ if (text === lastHumanInput) return
202
+ writeBlock(`${C.bold}${C.cyan}❯ ${text.replace(/\n/g, '\r\n')}${C.reset}`)
203
+ }
204
+ return
205
+ }
206
+ case 'message_update': {
207
+ const ev = frame.assistantMessageEvent as { type?: string, delta?: string, content?: string, contentIndex?: number } | undefined
208
+ if (!ev) return
209
+ // 流式增量先入缓冲,按批(flushStream)统一走 writeBlock:
210
+ // 直接 term.write 会落在底部输入行上,与输入行互相覆盖
211
+ if (ev.type === 'text_delta' && ev.delta) {
212
+ assistantStreamActive = true
213
+ const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
214
+ textSent.set(ci, (textSent.get(ci) ?? 0) + ev.delta.length)
215
+ streamBuf.push(ev.delta.replace(/\n/g, '\r\n'))
216
+ }
217
+ else if (ev.type === 'text_end' && typeof ev.content === 'string' && ev.content.length > 0) {
218
+ // 落定差额兜底:provider 未流式 text 块时全文在 text_end,补发未渲染部分
219
+ const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
220
+ const sent = textSent.get(ci) ?? 0
221
+ if (ev.content.length > sent) {
222
+ assistantStreamActive = true
223
+ textSent.set(ci, ev.content.length)
224
+ streamBuf.push(ev.content.slice(sent).replace(/\n/g, '\r\n'))
225
+ }
226
+ }
227
+ else if (ev.type === 'thinking_delta' && ev.delta) {
228
+ assistantStreamActive = true
229
+ streamBuf.push(`${C.dimItalicMagenta}${ev.delta.replace(/\n/g, '\r\n')}${C.reset}`)
230
+ }
231
+ return
232
+ }
233
+ case 'message_end': {
234
+ const role = String(frame.role ?? '')
235
+ if (role === 'assistant' && assistantStreamActive) {
236
+ // 回合收尾:冲刷剩余增量 → 流式行补换行 → 恢复底部输入行
237
+ assistantStreamActive = false
238
+ flushStream()
239
+ closeStreamLine()
240
+ renderInputRow()
241
+ }
242
+ return
243
+ }
244
+ case 'tool_execution_start':
245
+ writeBlock(` ${C.yellow}⚙ ${String(frame.toolName ?? 'tool')}${C.reset}${C.dim}(${oneLine(String(frame.args ?? ''), 120)})${C.reset}`)
246
+ return
247
+ case 'tool_execution_update':
248
+ writeBlock(` ${C.dim}↳ … ${oneLine(String(frame.update ?? ''), 100)}${C.reset}`)
249
+ return
250
+ case 'tool_execution_end': {
251
+ const err = frame.isError === true
252
+ const mark = err ? `${C.red}✗${C.reset}` : `${C.green}✓${C.reset}`
253
+ const preview = oneLine(String(frame.result ?? ''), 180)
254
+ writeBlock(` ${mark} ${C.dim}${preview}${C.reset}`)
255
+ return
256
+ }
257
+ case 'host_tool_call':
258
+ writeBlock(` ${C.magenta}⇄ host:${String(frame.toolName ?? '?')}${C.reset}${C.dim}(${oneLine(String(frame.args ?? ''), 100)})${C.reset}`)
259
+ return
260
+ case 'extension_ui_request': {
261
+ const method = String(frame.method ?? '')
262
+ if (method === 'cancel') {
263
+ writeBlock(t('ompTerminalPanel.k14jk3nu001', { p0: C.dim, p1: String(frame.targetId ?? ''), p2: C.reset }))
264
+ // omp 主动撤销(回合中止等)→ 关闭交互面板
265
+ if (hitl.value && hitl.value.id === String(frame.targetId ?? '')) hitl.value = null
266
+ return
267
+ }
268
+ if (method === 'notify') {
269
+ writeBlock(` ${C.blue}ℹ ${String(frame.message ?? '')}${C.reset}`)
270
+ return
271
+ }
272
+ if (method === 'select' || method === 'confirm' || method === 'input' || method === 'editor') {
273
+ const title = String(frame.title ?? '')
274
+ writeBlock(`${C.bold}${C.magenta}⚠ HITL · ${method}: ${title}${C.reset}`)
275
+ if (Array.isArray(frame.options)) {
276
+ for (const opt of frame.options) {
277
+ writeBlock(` ${C.magenta}○${C.reset} ${String(opt)}`)
278
+ }
279
+ }
280
+ // 弹出交互面板(实时帧驱动;term.init 的 hitl 仅覆盖重连时已在等待的对话框)
281
+ hitl.value = {
282
+ id: String(frame.id ?? ''),
283
+ method,
284
+ title,
285
+ options: Array.isArray(frame.options) ? frame.options.map(String) : undefined,
286
+ message: typeof frame.message === 'string' ? frame.message : undefined,
287
+ placeholder: typeof frame.placeholder === 'string' ? frame.placeholder : undefined,
288
+ prefill: typeof frame.prefill === 'string' ? frame.prefill : undefined,
289
+ at: nowLocalIso(),
290
+ }
291
+ }
292
+ // 其余 UI 方法(setStatus/setWidget/…)静默
293
+ return
294
+ }
295
+ case 'command_output':
296
+ writeBlock(`${C.dim}${String(frame.text ?? '').replace(/\n/g, '\r\n')}${C.reset}`)
297
+ return
298
+ case 'response': {
299
+ const command = String(frame.command ?? '')
300
+ // 只回显人工控制命令的受理结果,其余(get_state 等)为噪音
301
+ if (command === 'steer' || command === 'follow_up' || command === 'abort' || command === 'prompt') {
302
+ const ok = frame.success !== false
303
+ writeBlock(` ${C.dim}· ${command} ${ok ? 'accepted' : `failed: ${String(frame.error ?? '')}`}${C.reset}`)
304
+ }
305
+ return
306
+ }
307
+ default:
308
+ // available_commands_update / session_info_update / config_update / 未知帧:静默
309
+ return
310
+ }
311
+ }
312
+
313
+ // ===== WS 生命周期 =====
314
+
315
+ /** 连接目标(优先 pid 直连;否则 agentId 解析当前存活进程) */
316
+ const hasTarget = computed(() => (props.pid != null && props.pid > 0) || !!props.agentId)
317
+ /** 已连接会话的实际 pid(agent 寻址下由 term.init 回填;进程重启后变化) */
318
+ const livePid = ref<number | null>(null)
319
+
320
+ function wsUrl(): string {
321
+ const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
322
+ const token = encodeURIComponent(userStore.token ?? '')
323
+ const target = props.pid != null && props.pid > 0
324
+ ? `pid=${props.pid}`
325
+ : `agentId=${encodeURIComponent(props.agentId ?? '')}${props.channelId ? `&channelId=${encodeURIComponent(props.channelId)}` : ''}`
326
+ return `${proto}://${window.location.host}/api/system/monitor/terminal/ws?${target}&token=${token}`
327
+ }
328
+
329
+ /** 上一次错误去重(NO_SESSION 重试风暴防刷屏) */
330
+ let lastErrorMessage = ''
331
+
332
+ function handleServerMessage(msg: TerminalServerMessage): void {
333
+ switch (msg.type) {
334
+ case 'term.init': {
335
+ // 进程重启(agent 寻址重连)→ 新会话 seq 从 1 重来,去重游标必须重置
336
+ if (livePid.value !== msg.meta.pid) {
337
+ livePid.value = msg.meta.pid
338
+ lastRenderedSeq = 0
339
+ lastErrorMessage = ''
340
+ writeBlock(t('ompTerminalPanel.k68kkpc002', { p0: C.dim, p1: msg.meta.pid, p2: msg.lastSeq, p3: C.reset }))
341
+ }
342
+ alive.value = msg.alive
343
+ running.value = msg.running
344
+ streaming.value = msg.streaming
345
+ hitl.value = msg.hitl
346
+ return
347
+ }
348
+ case 'term.frames':
349
+ for (const f of msg.frames) renderFrame(f)
350
+ flushStream()
351
+ return
352
+ case 'term.state':
353
+ alive.value = msg.alive
354
+ running.value = msg.running
355
+ streaming.value = msg.streaming
356
+ return
357
+ case 'term.notice':
358
+ message.info(msg.message)
359
+ return
360
+ case 'term.error': {
361
+ // 重试期重复同错误只提示一次(进程未启动时每 2s 一条会刷屏)
362
+ const line = `[${msg.code}] ${msg.message}`
363
+ if (line === lastErrorMessage) return
364
+ lastErrorMessage = line
365
+ writeBlock(`${C.red}✗ ${line}${C.reset}`)
366
+ return
367
+ }
368
+ case 'pong':
369
+ return
370
+ }
371
+ }
372
+
373
+ function connect(): void {
374
+ if (!hasTarget.value || !userStore.token) return
375
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
376
+ closedByUser = false
377
+ connState.value = retryCount === 0 ? 'connecting' : 'retrying'
378
+ const sock = new WebSocket(wsUrl())
379
+ ws = sock
380
+ sock.onopen = () => {
381
+ if (ws !== sock) return
382
+ retryCount = 0
383
+ connState.value = 'open'
384
+ }
385
+ sock.onmessage = (ev) => {
386
+ if (ws !== sock) return
387
+ try {
388
+ handleServerMessage(JSON.parse(ev.data as string) as TerminalServerMessage)
389
+ }
390
+ catch { /* 非 JSON 帧忽略 */ }
391
+ }
392
+ sock.onclose = () => {
393
+ if (ws !== sock) return
394
+ ws = null
395
+ hitl.value = null
396
+ if (closedByUser || !hasTarget.value) {
397
+ connState.value = 'dead'
398
+ return
399
+ }
400
+ // 断线重连:1s 起指数退避,上限 10s(agent 寻址下进程未启动/重启由
401
+ // NO_SESSION 关闭驱动重试,直到首个任务触发 spawn 后接入)
402
+ retryCount += 1
403
+ const delay = Math.min(1000 * 2 ** Math.min(retryCount - 1, 4), 10_000)
404
+ connState.value = 'retrying'
405
+ retryTimer = setTimeout(() => {
406
+ retryTimer = null
407
+ if (!closedByUser && hasTarget.value) connect()
408
+ }, delay)
409
+ }
410
+ sock.onerror = () => { /* onclose 兜底 */ }
411
+ }
412
+
413
+ function disconnect(): void {
414
+ closedByUser = true
415
+ if (retryTimer) {
416
+ clearTimeout(retryTimer)
417
+ retryTimer = null
418
+ }
419
+ ws?.close()
420
+ ws = null
421
+ }
422
+
423
+ // ===== 控制面动作 =====
424
+
425
+ function sendRaw(obj: unknown): void {
426
+ if (ws?.readyState === WebSocket.OPEN) {
427
+ ws.send(JSON.stringify(obj))
428
+ return
429
+ }
430
+ writeBlock(t('ompTerminalPanel.k59zj91003', { p0: C.red, p1: C.reset }))
431
+ }
432
+
433
+ function submitInput(text: string): void {
434
+ const trimmed = text.trim()
435
+ if (!trimmed) return
436
+ inputHistory.push(trimmed)
437
+ inputHistory = inputHistory.slice(-50)
438
+ inputHistoryIdx = -1
439
+ sendRaw({ type: 'input', text: trimmed })
440
+ }
441
+
442
+ function doAbort(): void {
443
+ sendRaw({ type: 'abort' })
444
+ writeBlock(`${C.yellow}^C${C.reset}`)
445
+ }
446
+
447
+ function hitlRespond(response: { id: string, value?: string, confirmed?: boolean, cancelled?: boolean }): void {
448
+ sendRaw({ type: 'ui_response', ...response })
449
+ const label = response.cancelled
450
+ ? 'cancelled'
451
+ : response.confirmed !== undefined
452
+ ? String(response.confirmed)
453
+ : `"${oneLine(response.value ?? '', 80)}"`
454
+ writeBlock(t('ompTerminalPanel.kvzhi95004', { p0: C.green, p1: label, p2: C.reset }))
455
+ hitl.value = null
456
+ }
457
+
458
+ const hitlText = ref('')
459
+ watch(hitl, (h) => {
460
+ hitlText.value = h?.prefill ?? h?.placeholder ?? ''
461
+ })
462
+
463
+ // ===== xterm 装配与输入处理 =====
464
+
465
+ let termKeydown: ((e: KeyboardEvent) => void) | null = null
466
+
467
+ async function ensureTerm(): Promise<void> {
468
+ if (term || !hostEl.value) return
469
+ // 模板 ref 的 unibabel 结构类型与 lib.dom 的 HTMLElement 结构不完全兼容,
470
+ // 跨库边界(xterm / ResizeObserver)显式收窄
471
+ const host: HTMLElement = hostEl.value as unknown as HTMLElement
472
+ // 浏览器端动态加载 xterm(SSR 零执行;CJS 包不能静态命名导入)
473
+ const [{ Terminal: XTerm }, { FitAddon: XFit }] = await Promise.all([
474
+ import('@xterm/xterm'),
475
+ import('@xterm/addon-fit'),
476
+ ])
477
+ term = new XTerm({
478
+ convertEol: false,
479
+ cursorBlink: true,
480
+ cursorStyle: 'bar',
481
+ fontFamily: '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
482
+ fontSize: 12.5,
483
+ lineHeight: 1.25,
484
+ scrollback: 8000,
485
+ theme: {
486
+ background: '#10151c',
487
+ foreground: '#d6dee8',
488
+ cursor: '#7aa2f7',
489
+ selectionBackground: '#33467c',
490
+ black: '#10151c',
491
+ red: '#f7768e',
492
+ green: '#9ece6a',
493
+ yellow: '#e0af68',
494
+ blue: '#7aa2f7',
495
+ magenta: '#bb9af7',
496
+ cyan: '#7dcfff',
497
+ white: '#d6dee8',
498
+ },
499
+ })
500
+ fit = new XFit()
501
+ term.loadAddon(fit)
502
+ term.open(host)
503
+ renderInputRow()
504
+
505
+ // 输入通道 1:宿主容器 keydown(捕获阶段)——真实键盘与合成事件(CDP/自动化)
506
+ // 都可靠触发;拦截已处理的键,阻止 xterm 内部重复消费
507
+ const onKeyDown = (e: KeyboardEvent): void => {
508
+ if (e.ctrlKey || e.metaKey || e.altKey) {
509
+ if (e.key === 'c' || e.key === 'C') {
510
+ e.preventDefault()
511
+ e.stopPropagation()
512
+ doAbort()
513
+ }
514
+ return // 其余组合键(Ctrl+R/F5 等)交给浏览器
515
+ }
516
+ if (e.key === 'Enter') {
517
+ e.preventDefault()
518
+ e.stopPropagation()
519
+ // 清掉本地键入行,由服务端 __human_input 回显统一渲染(回放观众一致,无重复)
520
+ term?.write('\r\x1b[K')
521
+ const line = inputLine
522
+ inputLine = ''
523
+ submitInput(line)
524
+ renderInputRow()
525
+ return
526
+ }
527
+ if (e.key === 'Backspace') {
528
+ e.preventDefault()
529
+ e.stopPropagation()
530
+ inputLine = inputLine.slice(0, -1)
531
+ renderInputRow()
532
+ return
533
+ }
534
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
535
+ if (inputHistory.length === 0) return
536
+ e.preventDefault()
537
+ e.stopPropagation()
538
+ if (e.key === 'ArrowUp') {
539
+ inputHistoryIdx = inputHistoryIdx < 0 ? inputHistory.length - 1 : Math.max(0, inputHistoryIdx - 1)
540
+ }
541
+ else {
542
+ inputHistoryIdx = inputHistoryIdx < 0 ? -1 : Math.min(inputHistory.length - 1, inputHistoryIdx + 1)
543
+ if (inputHistoryIdx === inputHistory.length - 1) inputHistoryIdx = -1
544
+ }
545
+ inputLine = inputHistoryIdx < 0 ? '' : (inputHistory[inputHistoryIdx] ?? '')
546
+ renderInputRow()
547
+ return
548
+ }
549
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
550
+ e.preventDefault()
551
+ e.stopPropagation()
552
+ inputLine += e.key
553
+ renderInputRow()
554
+ }
555
+ }
556
+ host.addEventListener('keydown', onKeyDown, true)
557
+ termKeydown = onKeyDown
558
+
559
+ // 输入通道 2:xterm onData 仅消费多字符输入(粘贴;单字符已由 keydown 处理)
560
+ term.onData((data) => {
561
+ if (data.length <= 1) return
562
+ inputLine += data.replace(/[\r\n]+$/, '')
563
+ renderInputRow()
564
+ })
565
+
566
+ resizeObs = new ResizeObserver(() => {
567
+ try {
568
+ fit?.fit()
569
+ }
570
+ catch { /* 尺寸未就绪 */ }
571
+ })
572
+ resizeObs.observe(host)
573
+ }
574
+
575
+ function teardownTerm(): void {
576
+ if (termKeydown && hostEl.value) {
577
+ hostEl.value.removeEventListener('keydown', termKeydown, true)
578
+ termKeydown = null
579
+ }
580
+ resizeObs?.disconnect()
581
+ resizeObs = null
582
+ term?.dispose()
583
+ term = null
584
+ fit = null
585
+ }
586
+
587
+ // ===== 生命周期:抽屉开关驱动连接/实例 =====
588
+ watch(open, (v) => {
589
+ if (v) {
590
+ nextTick(async () => {
591
+ await ensureTerm()
592
+ resetView()
593
+ lastRenderedSeq = 0
594
+ retryCount = 0
595
+ lastErrorMessage = ''
596
+ writeBlock(`${C.dim}── AgentWorkShop harness terminal · ${props.pid ? `PID ${props.pid}` : `agent ${props.agentId?.slice(0, 8)}…`} ──${C.reset}`)
597
+ connect()
598
+ nextTick(() => {
599
+ try {
600
+ fit?.fit()
601
+ }
602
+ catch { /* ignore */ }
603
+ })
604
+ })
605
+ }
606
+ else {
607
+ disconnect()
608
+ hitl.value = null
609
+ }
610
+ })
611
+
612
+ /** 目标切换(pid 直连或 agent 寻址变化)→ 清空视图重连(不同 Agent 的输出不得混渲染) */
613
+ watch(() => [props.pid, props.agentId], () => {
614
+ if (!open.value) return
615
+ disconnect()
616
+ nextTick(() => {
617
+ resetView()
618
+ lastRenderedSeq = 0
619
+ retryCount = 0
620
+ lastErrorMessage = ''
621
+ livePid.value = null
622
+ writeBlock(`${C.dim}${t('ompTerminalPanel.switchTarget', { p0: props.pid ? `PID ${props.pid}` : `agent ${props.agentId?.slice(0, 8)}…` })}${C.reset}`)
623
+ connect()
624
+ })
625
+ })
626
+
627
+ onBeforeUnmount(() => {
628
+ disconnect()
629
+ teardownTerm()
630
+ })
631
+
632
+ const doClear = (): void => {
633
+ streamBuf.length = 0
634
+ streamLineOpen = false
635
+ term?.clear()
636
+ renderInputRow()
637
+ }
638
+
639
+ const stateLabel = computed(() => {
640
+ if (!alive.value) return { text: t('terminal.stateExited'), color: 'error' as const }
641
+ if (streaming.value) return { text: t('terminal.stateStreaming'), color: 'processing' as const }
642
+ if (running.value) return { text: t('terminal.stateRunning'), color: 'warning' as const }
643
+ return { text: t('terminal.stateIdle'), color: 'success' as const }
644
+ })
645
+ const connLabel = computed(() => {
646
+ switch (connState.value) {
647
+ case 'open': return { text: t('terminal.connOpen'), color: 'success' as const }
648
+ case 'connecting': return { text: t('terminal.connConnecting'), color: 'default' as const }
649
+ case 'retrying': return { text: `${t('terminal.connRetrying')}(${retryCount})`, color: 'warning' as const }
650
+ default: return { text: t('terminal.connClosed'), color: 'default' as const }
651
+ }
652
+ })
653
+ </script>
654
+
655
+ <template>
656
+ <a-drawer
657
+ v-model:open="open"
658
+ placement="right"
659
+ :width="860"
660
+ :body-style="{ padding: '0', display: 'flex', flexDirection: 'column', background: '#10151c' }"
661
+ :header-style="{ background: '#10151c', borderBottom: '1px solid #1d2733' }"
662
+ class="omp-terminal-drawer"
663
+ >
664
+ <template #title>
665
+ <div class="term-title">
666
+ <span class="term-brand">
667
+ <span class="i-tabler-terminal-2" />
668
+ </span>
669
+ <span class="term-name">{{ t('terminal.title') }}</span>
670
+ <span class="ts-chip aw-mono ts-pid">PID {{ livePid ?? pid ?? '–' }}</span>
671
+ <span
672
+ v-if="subtitle"
673
+ class="ts-chip ts-sub"
674
+ >{{ subtitle }}</span>
675
+ </div>
676
+ </template>
677
+ <template #extra>
678
+ <div class="term-toolbar">
679
+ <!-- 连接态 chip:呼吸点 + 文本(open=绿 / retrying=琥珀脉冲 / 其他=灰蓝) -->
680
+ <span
681
+ class="ts-chip aw-mono"
682
+ :data-state="connState"
683
+ ><span class="ts-dot" />{{ connLabel.text }}</span>
684
+ <!-- 会话态 chip:idle=青 / busy=琥珀 / 流式=青脉冲 -->
685
+ <span
686
+ class="ts-chip aw-mono"
687
+ :data-live="running ? (streaming ? 'streaming' : 'running') : 'idle'"
688
+ >{{ stateLabel.text }}</span>
689
+ <a-button
690
+ size="small"
691
+ danger
692
+ class="ts-btn"
693
+ :disabled="!running && !streaming"
694
+ @click="doAbort"
695
+ >
696
+ <template #icon>
697
+ <span class="i-tabler-player-stop" />
698
+ </template>
699
+ {{ t('terminal.abort') }}
700
+ </a-button>
701
+ <a-button
702
+ size="small"
703
+ class="ts-btn"
704
+ @click="doClear"
705
+ >
706
+ <template #icon>
707
+ <span class="i-tabler-eraser" />
708
+ </template>
709
+ {{ t('terminal.clear') }}
710
+ </a-button>
711
+ </div>
712
+ </template>
713
+
714
+ <div class="term-body">
715
+ <!-- HITL 对话框(omp extension_ui_request;answer → ui_response) -->
716
+ <div
717
+ v-if="hitl"
718
+ class="hitl-card"
719
+ >
720
+ <div class="hitl-head">
721
+ <span class="i-tabler-user-question" />
722
+ <b>{{ t('terminal.hitlTitle') }}</b>
723
+ <a-tag color="magenta">
724
+ {{ hitl.method }}
725
+ </a-tag>
726
+ <a-button
727
+ size="small"
728
+ type="text"
729
+ danger
730
+ @click="hitlRespond({ id: hitl.id, cancelled: true })"
731
+ >
732
+ {{ t('terminal.hitlCancel') }}
733
+ </a-button>
734
+ </div>
735
+ <p class="hitl-question">
736
+ {{ hitl.title }}
737
+ </p>
738
+
739
+ <template v-if="hitl.method === 'select'">
740
+ <div class="hitl-options">
741
+ <a-button
742
+ v-for="opt in hitl.options ?? []"
743
+ :key="opt"
744
+ block
745
+ @click="hitlRespond({ id: hitl.id, value: opt })"
746
+ >
747
+ {{ opt }}
748
+ </a-button>
749
+ </div>
750
+ </template>
751
+ <template v-else-if="hitl.method === 'confirm'">
752
+ <p class="hitl-message">
753
+ {{ hitl.message }}
754
+ </p>
755
+ <div class="hitl-row">
756
+ <a-button
757
+ type="primary"
758
+ @click="hitlRespond({ id: hitl.id, confirmed: true })"
759
+ >
760
+ {{ t('terminal.hitlConfirm') }}
761
+ </a-button>
762
+ <a-button @click="hitlRespond({ id: hitl.id, confirmed: false })">
763
+ {{ t('terminal.hitlDeny') }}
764
+ </a-button>
765
+ </div>
766
+ </template>
767
+ <template v-else>
768
+ <a-textarea
769
+ v-model:value="hitlText"
770
+ :rows="3"
771
+ :placeholder="hitl.placeholder"
772
+ class="hitl-textarea"
773
+ @press-enter="hitlRespond({ id: hitl.id, value: hitlText })"
774
+ />
775
+ <div class="hitl-row">
776
+ <a-button
777
+ type="primary"
778
+ @click="hitlRespond({ id: hitl.id, value: hitlText })"
779
+ >
780
+ {{ t('terminal.hitlSubmit') }}
781
+ </a-button>
782
+ <a-button @click="hitlRespond({ id: hitl.id, cancelled: true })">
783
+ {{ t('terminal.hitlCancel') }}
784
+ </a-button>
785
+ </div>
786
+ </template>
787
+ </div>
788
+
789
+ <!-- xterm 终端 -->
790
+ <div
791
+ ref="hostEl"
792
+ class="term-host"
793
+ />
794
+
795
+ <div class="term-hint aw-kicker">
796
+ {{ t('terminal.hint') }}
797
+ </div>
798
+ </div>
799
+ </a-drawer>
800
+ </template>
801
+
802
+ <style scoped>
803
+ /* 状态栏品牌块:终端图标入方形墨章(与侧栏墨方印同族,tokyo-night 冷暖对撞) */
804
+ .term-title {
805
+ display: inline-flex;
806
+ gap: 10px;
807
+ align-items: center;
808
+ }
809
+ .term-brand {
810
+ display: flex;
811
+ align-items: center;
812
+ justify-content: center;
813
+ width: 26px;
814
+ height: 26px;
815
+ font-size: 14px;
816
+ color: #9ece6a;
817
+ background: rgb(158 206 106 / 10%);
818
+ border: 1px solid rgb(158 206 106 / 22%);
819
+ border-radius: var(--radius-panel-sm);
820
+ }
821
+ .term-name {
822
+ font-size: 13.5px;
823
+ font-weight: 600;
824
+ letter-spacing: 0.01em;
825
+ color: #e6edf3;
826
+ }
827
+
828
+ /* 状态 chip:等宽微字 + 语义点;色板对齐 xterm tokyo-night */
829
+ .ts-chip {
830
+ display: inline-flex;
831
+ gap: 6px;
832
+ align-items: center;
833
+ padding: 1px 8px;
834
+ font-size: 10.5px;
835
+ letter-spacing: 0.04em;
836
+ color: #a9b1d6;
837
+ background: rgb(65 72 104 / 26%);
838
+ border: 1px solid rgb(146 153 179 / 16%);
839
+ border-radius: var(--radius-pill);
840
+ }
841
+ .ts-chip.ts-pid { color: #7dcfff; }
842
+ .ts-chip.ts-sub {
843
+ font-family: var(--font-body);
844
+ color: #9aa5ce;
845
+ }
846
+ .ts-dot {
847
+ width: 6px;
848
+ height: 6px;
849
+ background: #565f89;
850
+ border-radius: 50%;
851
+ }
852
+ /* 连接态语义色 */
853
+ .ts-chip[data-state='open'] { color: #9ece6a; background: rgb(158 206 106 / 10%); border-color: rgb(158 206 106 / 22%); }
854
+ .ts-chip[data-state='open'] .ts-dot { background: #9ece6a; animation: ts-breathe 2.2s ease-in-out infinite; }
855
+ .ts-chip[data-state='retrying'] { color: #e0af68; background: rgb(224 175 104 / 10%); border-color: rgb(224 175 104 / 24%); }
856
+ .ts-chip[data-state='retrying'] .ts-dot { background: #e0af68; animation: ts-breathe 1.1s ease-in-out infinite; }
857
+ .ts-chip[data-state='connecting'] { color: #7aa2f7; }
858
+ .ts-chip[data-state='connecting'] .ts-dot { background: #7aa2f7; animation: ts-breathe 1.1s ease-in-out infinite; }
859
+ /* 会话态 */
860
+ .ts-chip[data-live='streaming'] { color: #7dcfff; background: rgb(125 207 255 / 10%); border-color: rgb(125 207 255 / 24%); }
861
+ .ts-chip[data-live='streaming']::before {
862
+ content: "";
863
+ width: 6px;
864
+ height: 6px;
865
+ background: #7dcfff;
866
+ border-radius: 50%;
867
+ animation: ts-breathe 0.9s ease-in-out infinite;
868
+ }
869
+ .ts-chip[data-live='running'] { color: #e0af68; background: rgb(224 175 104 / 10%); border-color: rgb(224 175 104 / 24%); }
870
+ .ts-chip[data-live='running']::before {
871
+ content: "";
872
+ width: 6px;
873
+ height: 6px;
874
+ background: #e0af68;
875
+ border-radius: 50%;
876
+ animation: ts-breathe 1.6s ease-in-out infinite;
877
+ }
878
+ .ts-chip[data-live='idle'] { color: #565f89; }
879
+ .ts-chip[data-live='idle']::before {
880
+ content: "";
881
+ width: 6px;
882
+ height: 6px;
883
+ background: #565f89;
884
+ border-radius: 50%;
885
+ }
886
+ @keyframes ts-breathe {
887
+ 0%, 100% { opacity: 0.45; }
888
+ 50% { opacity: 1; }
889
+ }
890
+ @media (prefers-reduced-motion: reduce) {
891
+ .ts-dot, .ts-chip::before { animation: none !important; opacity: 0.85; }
892
+ }
893
+
894
+ /* 工具按钮:暗面浮起 + 按压反馈 */
895
+ .term-toolbar {
896
+ display: flex;
897
+ gap: 8px;
898
+ align-items: center;
899
+ }
900
+ :deep(.ts-btn.ant-btn) {
901
+ display: inline-flex;
902
+ gap: 5px;
903
+ align-items: center;
904
+ color: #c0caf5;
905
+ background: rgb(65 72 104 / 30%);
906
+ border: 1px solid rgb(146 153 179 / 18%);
907
+ transition: background 0.15s ease, transform 0.12s ease;
908
+ }
909
+ :deep(.ts-btn.ant-btn:hover:not(:disabled)) { background: rgb(65 72 104 / 50%); }
910
+ :deep(.ts-btn.ant-btn:active:not(:disabled)) { transform: translateY(1px); }
911
+ :deep(.ts-btn.ant-btn:disabled) { opacity: 0.4; }
912
+
913
+ .term-body {
914
+ position: relative;
915
+ display: flex;
916
+ flex: 1;
917
+ min-height: 0;
918
+ flex-direction: column;
919
+ }
920
+
921
+ .term-host {
922
+ flex: 1;
923
+ min-height: 0;
924
+ padding: 10px 12px 4px;
925
+ }
926
+
927
+ .term-hint {
928
+ padding: 6px 12px 8px;
929
+ color: rgb(214 222 232 / 45%);
930
+ background: #10151c;
931
+ }
932
+
933
+ /* HITL 对话框:悬浮在终端上方(正在等待人类决策) */
934
+ .hitl-card {
935
+ position: absolute;
936
+ top: 14px;
937
+ right: 16px;
938
+ z-index: 10;
939
+ width: min(420px, calc(100% - 32px));
940
+ padding: 12px 14px;
941
+ background: rgb(23 30 40 / 97%);
942
+ border: 1px solid #bb9af7;
943
+ border-radius: var(--radius-panel-sm);
944
+ box-shadow: 0 8px 28px rgb(0 0 0 / 55%);
945
+ }
946
+
947
+ .hitl-head {
948
+ display: flex;
949
+ gap: 8px;
950
+ align-items: center;
951
+ margin-bottom: 8px;
952
+ color: #bb9af7;
953
+ }
954
+
955
+ .hitl-head > b {
956
+ color: #e6edf3;
957
+ }
958
+
959
+ .hitl-head .ant-btn {
960
+ margin-left: auto;
961
+ }
962
+
963
+ .hitl-question {
964
+ margin: 0 0 10px;
965
+ font-weight: 600;
966
+ color: #e6edf3;
967
+ white-space: pre-wrap;
968
+ }
969
+
970
+ .hitl-message {
971
+ margin: 0 0 10px;
972
+ color: #a9b4c0;
973
+ white-space: pre-wrap;
974
+ }
975
+
976
+ .hitl-options {
977
+ display: flex;
978
+ max-height: 300px;
979
+ flex-direction: column;
980
+ gap: 6px;
981
+ overflow: auto;
982
+ }
983
+
984
+ .hitl-options .ant-btn {
985
+ justify-content: flex-start;
986
+ text-align: left;
987
+ white-space: normal;
988
+ }
989
+
990
+ .hitl-row {
991
+ display: flex;
992
+ gap: 8px;
993
+ margin-top: 10px;
994
+ }
995
+
996
+ .hitl-textarea {
997
+ margin-top: 4px;
998
+ }
999
+ </style>
1000
+
1001
+ <style>
1002
+ /* drawer 挂 body teleport,全局态:暗色标题/内容(xterm 主题一致) */
1003
+ .omp-terminal-drawer .ant-drawer-header {
1004
+ background: linear-gradient(180deg, #131a24 0%, #10151c 100%) !important;
1005
+ border-bottom: 1px solid #1d2733 !important;
1006
+ box-shadow: inset 0 1px 0 rgb(146 153 179 / 6%);
1007
+ }
1008
+ .omp-terminal-drawer .ant-drawer-header-title {
1009
+ color: #e6edf3;
1010
+ }
1011
+ </style>