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,959 +1,961 @@
1
- <script setup lang="ts">
2
- import { message } from 'ant-design-vue'
3
- import type { MenuProps, SelectProps } from 'ant-design-vue'
4
- import type { AepHitlItem } from '#shared/workshop-protocol'
5
- import { useUserStore } from '@/app/stores/workshop/user'
6
- import { useWsConnectionStore } from '@/app/stores/workshop/connection'
7
- import { useHitlStore } from '@/app/stores/workshop/hitl'
8
- import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
9
- import { useWorkshopApi } from '@/app/composables/workshop/useWorkshopApi'
10
-
11
- const { t, locale, locales, setLocale } = useI18n()
12
- const store = useAppStore()
13
- const route = useRoute()
14
- const trail = useRouteTrailStore()
15
- const userStore = useUserStore()
16
- const { metaFor } = useRouteMeta()
17
-
18
- // ── HITL 全局待办(omp ask 对话框 + dcw 审批统一徽标;页头保底建连 ——
19
- // hitl 全员直推帧只达已连 peer,不建连的页面收不到提醒;快照兜底刷新前待办) ──
20
- const hitl = useHitlStore()
21
- const wsSession = useWorkshopWs()
22
- const ensureHitlLive = () => {
23
- if (userStore.isLoggedIn) wsSession.ensureConnected()
24
- }
25
- watch(() => userStore.token, (t2) => {
26
- if (t2) {
27
- ensureHitlLive()
28
- void hitl.loadSnapshot()
29
- }
30
- else hitl.clear()
31
- }, { immediate: true })
32
- const HITL_ANSWERABLE = new Set(['codex-approval', 'opencode-permission', 'dsh-permission'])
33
- const hitlKindLabel = (kind: AepHitlItem['kind']) => {
34
- switch (kind) {
35
- case 'omp-dialog': return t('appHeader.hitlKindOmp')
36
- case 'dcw-approval': return t('appHeader.hitlKindDcw')
37
- case 'codex-approval': return t('appHeader.hitlKindCodex')
38
- case 'opencode-permission': return t('appHeader.hitlKindOpencode')
39
- case 'dsh-permission': return t('appHeader.hitlKindDsh')
40
- default: return kind
41
- }
42
- }
43
- const hitlGo = (item: AepHitlItem) => {
44
- navigateTo({ path: '/monitor', query: { agentId: item.agentId, channelId: item.channelId } })
45
- }
46
- // 引擎审批(codex/opencode/dsh)页头内联应答:批准/拒绝直接回传引擎,无需进终端
47
- const hitlApi = useWorkshopApi()
48
- const hitlAnswering = ref<string | null>(null)
49
- const hitlAnswer = async (item: AepHitlItem, confirmed: boolean): Promise<void> => {
50
- hitlAnswering.value = `${item.kind}:${item.id}`
51
- try {
52
- await hitlApi.respondHitl({ kind: item.kind, id: item.id, confirmed, cancelled: !confirmed })
53
- await hitl.loadSnapshot()
54
- }
55
- catch (e) {
56
- message.error(apiErrorMessage(e))
57
- }
58
- finally {
59
- hitlAnswering.value = null
60
- }
61
- }
62
-
63
- /* 语言选择器在窄屏显示"短名":完整语言名(简体中文=56px @14px)配上
64
- * antd 给箭头预留的 24px 内距,92px 的选择器只剩 46px 文本位 ——
65
- * 实测被截成「简 体…」,这是每页都出现的破相。
66
- * 窄屏用 2 字标签,桌面保留完整语言名。 */
67
- const localeOptions = computed(() =>
68
- (locales.value as Array<{ code: string, name: string }>).map(l => ({
69
- label: isMobile.value ? l.code.split('-')[0]!.toUpperCase() : l.name,
70
- value: l.code,
71
- })),
72
- )
73
-
74
- const switchLocale: SelectProps['onChange'] = (value) => {
75
- if (value != null) {
76
- // 持久化 + 强刷:setup 期求值的词条(脚本常量)只有重载才能整体切换
77
- localStorage.setItem('aw.locale', String(value))
78
- setLocale(String(value) as 'zh-CN' | 'en')
79
- window.location.reload()
80
- }
81
- }
82
-
83
- /* ── 响应式:窄屏顶栏不是"缩小版顶栏",而是换一套信息优先级 ─────────────
84
- * 桌面:航迹页签是主角(多页并行来回切)。
85
- * 窄屏:页签轨与右侧功能簇争抢横向空间 —— 实测 390px 下页签被右侧图标
86
- * 逐个压过去,最后连用户铭牌都被推出视口(顶栏 overflow:hidden 直接裁掉)。
87
- * 所以窄屏撤掉页签轨,用**当前页标题**替代:窄屏用户要的是"我在哪",
88
- * 不是"我刚才还开过哪 5 个页面"。 */
89
- const { isDrawer, isMobile } = useResponsive()
90
-
91
- const navToggleLabel = computed(() =>
92
- isDrawer.value
93
- ? (store.mobileNavOpen ? t('header.closeNav') : t('header.openNav'))
94
- : (store.sidebarCollapsed ? t('header.expand') : t('header.collapse')))
95
-
96
- const toggleNav = () => {
97
- if (isDrawer.value) store.toggleMobileNav()
98
- else store.toggleSidebar()
99
- }
100
-
101
- const currentTitle = computed(() => metaFor(route.path).title)
102
-
103
- // ── 航迹导航:路由变化 → 记录航点;切换时进度线扫过 ──
104
- const hydrated = ref(false)
105
- const plotting = ref(false)
106
- let plotTimer: ReturnType<typeof setTimeout> | null = null
107
-
108
- watch(() => route.path, () => {
109
- trail.visit(route.path)
110
- plotting.value = false
111
- requestAnimationFrame(() => {
112
- plotting.value = true
113
- if (plotTimer) clearTimeout(plotTimer)
114
- plotTimer = setTimeout(() => {
115
- plotting.value = false
116
- }, 620)
117
- })
118
- }, { immediate: false })
119
-
120
- onMounted(() => {
121
- hydrated.value = true
122
- trail.visit(route.path)
123
- })
124
-
125
- onBeforeUnmount(() => {
126
- if (plotTimer) clearTimeout(plotTimer)
127
- })
128
-
129
- const go = (path: string) => {
130
- if (path !== route.path) {
131
- navigateTo(path)
132
- }
133
- }
134
-
135
- /**
136
- * 关闭航点标签页:
137
- * - 非当前页 → 仅从航迹移除;
138
- * - 当前页 → 先选好跳转目标(优先左侧相邻航点,其次右侧,兜底仪表盘)再移除并跳转,
139
- * 避免关闭后停留在一个已无标签的路由上(watcher 会在跳转后 visit 目标页,不会回补被关页)。
140
- */
141
- const closeWaypoint = (path: string) => {
142
- const idx = trail.remove(path)
143
- if (path !== route.path) return
144
- const rest = trail.waypoints
145
- const target = rest[Math.min(Math.max(idx - 1, 0), Math.max(rest.length - 1, 0))]?.path ?? '/'
146
- navigateTo(target)
147
- }
148
-
149
- const isFullscreen = ref(false)
150
-
151
- function toggleFullscreen() {
152
- if (!document.fullscreenElement) {
153
- document.documentElement.requestFullscreen()
154
- isFullscreen.value = true
155
- }
156
- else {
157
- document.exitFullscreen()
158
- isFullscreen.value = false
159
- }
160
- }
161
-
162
- // 用户铭牌:真实身份(workshop 用户系统;未登录 = 访客)
163
- const userInitial = computed(() => (userStore.user?.name ?? '?').trim().charAt(0).toUpperCase())
164
- const userName = computed(() => userStore.user?.name ?? t('header.guest'))
165
- const userRole = computed(() => (userStore.isLoggedIn ? userStore.user?.role ?? 'user' : 'anonymous'))
166
-
167
- // ── 实时连接状态点(全局 WS 单例的诚实在线指示;断连不假装在线) ──
168
- const conn = useWsConnectionStore()
169
- const wsVisible = computed(() => conn.state !== 'closed' || conn.lastDataAt > 0)
170
- const wsClass = computed(() => {
171
- if (conn.state === 'open') return conn.pendingReplay ? 'syncing' : 'live'
172
- if (conn.state === 'connecting') return 'syncing'
173
- return 'down'
174
- })
175
- const wsLabel = computed(() => {
176
- if (conn.state === 'open') return conn.pendingReplay ? t('appHeader.kwsdot0002') : t('appHeader.kwsdot0001')
177
- if (conn.state === 'connecting') return t('appHeader.kwsdot0003')
178
- return t('appHeader.kwsdot0004')
179
- })
180
-
181
- interface AvatarMenuEntry {
182
- key: string
183
- label: string
184
- icon?: string
185
- danger?: boolean
186
- divider?: boolean
187
- }
188
-
189
- const avatarItems = computed<AvatarMenuEntry[]>(() => {
190
- const items: AvatarMenuEntry[] = [
191
- { key: 'tokens', icon: 'i-tabler-key', label: t('menu.tokens') },
192
- { key: 'settings', icon: 'i-tabler-adjustments', label: t('menu.settings') },
193
- ]
194
- if (userStore.isLoggedIn) {
195
- items.push({ key: 'd-logout', label: '', divider: true })
196
- items.push({ key: 'logout', icon: 'i-tabler-logout', label: t('header.logout'), danger: true })
197
- }
198
- return items
199
- })
200
-
201
- const onAvatarMenu: MenuProps['onClick'] = async ({ key }) => {
202
- if (key === 'settings') {
203
- navigateTo('/settings')
204
- }
205
- else if (key === 'tokens') {
206
- navigateTo('/tokens')
207
- }
208
- else if (key === 'logout') {
209
- await userStore.logout()
210
- navigateTo('/workshop')
211
- }
212
- }
213
- </script>
214
-
215
- <template>
216
- <a-layout-header
217
- class="app-header app-header-glass"
218
- >
219
- <!-- 左侧:折叠 + 航迹标绘轨 -->
220
- <div class="header-left">
221
- <button
222
- class="collapse-btn"
223
- :aria-label="navToggleLabel"
224
- :aria-expanded="isDrawer ? store.mobileNavOpen : !store.sidebarCollapsed"
225
- @click="toggleNav()"
226
- >
227
- <span class="i-tabler-menu-2" />
228
- </button>
229
-
230
- <!-- 窄屏:当前页标题(替代被撤掉的页签轨) -->
231
- <span class="mobile-title">{{ currentTitle }}</span>
232
-
233
- <span class="rail-mark i-tabler-route" />
234
-
235
- <nav
236
- class="trail"
237
- :aria-label="t('header.trail')"
238
- >
239
- <transition-group
240
- name="stamp"
241
- tag="div"
242
- class="trail-row"
243
- >
244
- <template v-if="hydrated">
245
- <template
246
- v-for="(w, i) in trail.waypoints"
247
- :key="w.path"
248
- >
249
- <span
250
- v-if="i > 0"
251
- :key="`link-${w.path}`"
252
- class="trail-link"
253
- aria-hidden="true"
254
- />
255
- <div
256
- class="trail-node"
257
- :class="{ active: w.path === route.path }"
258
- role="button"
259
- tabindex="0"
260
- :title="metaFor(w.path).title"
261
- @click="go(w.path)"
262
- @keydown.enter.prevent="go(w.path)"
263
- >
264
- <span
265
- class="node-icon"
266
- :class="metaFor(w.path).icon"
267
- />
268
- <span class="node-title">{{ metaFor(w.path).title }}</span>
269
- <button
270
- class="node-close"
271
- :aria-label="$t('appHeader.kzwjv99001', { p0: metaFor(w.path).title })"
272
- :title="$t('appHeader.kzwjv99001', { p0: metaFor(w.path).title })"
273
- @click.stop="closeWaypoint(w.path)"
274
- >
275
- <span class="i-tabler-x" />
276
- </button>
277
- </div>
278
- </template>
279
- </template>
280
- </transition-group>
281
- </nav>
282
- </div>
283
-
284
- <!-- 右侧:功能集群 -->
285
- <div class="header-right">
286
- <!-- HITL 待办铃标(有待办才出现;点击下拉 → 进入对应 agent 终端处理) -->
287
- <ClientOnly>
288
- <a-dropdown
289
- v-if="hitl.count > 0"
290
- placement="bottomRight"
291
- >
292
- <button
293
- class="icon-btn hitl-bell"
294
- :title="t('appHeader.hitlBadge')"
295
- >
296
- <span class="i-tabler-bell-ringing" />
297
- <span class="hitl-count">{{ hitl.count }}</span>
298
- </button>
299
- <template #overlay>
300
- <div class="hitl-menu">
301
- <div class="hitl-menu-title">
302
- {{ t('appHeader.hitlBadge') }} · {{ hitl.count }}
303
- </div>
304
- <button
305
- v-for="item in hitl.items"
306
- :key="`${item.kind}:${item.id}`"
307
- class="hitl-item"
308
- @click="hitlGo(item)"
309
- >
310
- <span class="hitl-item-top">
311
- <span class="hitl-item-agent">{{ item.agentName }}</span>
312
- <span class="hitl-item-kind">{{ hitlKindLabel(item.kind) }}</span>
313
- </span>
314
- <span class="hitl-item-title">{{ item.title }}</span>
315
- <span
316
- v-if="HITL_ANSWERABLE.has(item.kind)"
317
- class="hitl-item-actions"
318
- @click.stop
319
- >
320
- <a-button
321
- size="small"
322
- type="primary"
323
- :loading="hitlAnswering === `${item.kind}:${item.id}`"
324
- @click="hitlAnswer(item, true)"
325
- >
326
- {{ t('appHeader.hitlApprove') }}
327
- </a-button>
328
- <a-button
329
- size="small"
330
- danger
331
- :disabled="hitlAnswering === `${item.kind}:${item.id}`"
332
- @click="hitlAnswer(item, false)"
333
- >
334
- {{ t('appHeader.hitlReject') }}
335
- </a-button>
336
- </span>
337
- </button>
338
- </div>
339
- </template>
340
- </a-dropdown>
341
- </ClientOnly>
342
- <!-- 实时连接状态点(WS 会话全局单例;未用过 WS 的会话不显示) -->
343
- <span
344
- v-if="hydrated && wsVisible"
345
- class="ws-dot"
346
- :class="wsClass"
347
- :title="wsLabel"
348
- />
349
- <a-tooltip :title="t('header.fullscreen')">
350
- <button
351
- class="icon-btn hdr-fullscreen"
352
- @click="toggleFullscreen"
353
- >
354
- <span
355
- class="i-tabler-arrows-maximize"
356
- :class="{ hidden: isFullscreen }"
357
- />
358
- <span
359
- class="i-tabler-arrows-minimize"
360
- :class="{ hidden: !isFullscreen }"
361
- />
362
- </button>
363
- </a-tooltip>
364
-
365
- <a-select
366
- id="hdr-locale"
367
- :value="locale"
368
- size="middle"
369
- :options="localeOptions"
370
- class="lang-select"
371
- @change="switchLocale"
372
- />
373
-
374
- <a-tooltip :title="store.isDark ? t('common.light') : t('common.dark')">
375
- <button
376
- class="icon-btn"
377
- @click="store.toggleDark()"
378
- >
379
- <span
380
- class="i-tabler-sun-high"
381
- :class="{ hidden: store.isDark }"
382
- />
383
- <span
384
- class="i-tabler-moon-stars"
385
- :class="{ hidden: !store.isDark }"
386
- />
387
- </button>
388
- </a-tooltip>
389
-
390
- <a-dropdown>
391
- <div class="user-chip">
392
- <!-- 用户身份(session 异步解析)仅客户端可知:SSR 渲染中性占位,挂载后填充
393
- —— 消除 hydration mismatch(unhead dispose 噪音的根因) -->
394
- <ClientOnly>
395
- <span class="user-initial aw-avatar">{{ userInitial }}</span>
396
- <span class="user-meta">
397
- <span class="user-name">{{ userName }}</span>
398
- <span class="user-role">{{ userRole }}</span>
399
- </span>
400
- <template #fallback>
401
- <span class="user-initial aw-avatar">·</span>
402
- <span class="user-meta">
403
- <span class="user-name">·</span>
404
- <span class="user-role">·</span>
405
- </span>
406
- </template>
407
- </ClientOnly>
408
- </div>
409
- <template #overlay>
410
- <a-menu @click="onAvatarMenu">
411
- <template
412
- v-for="item in avatarItems"
413
- :key="item.key"
414
- >
415
- <a-menu-divider v-if="item.divider" />
416
- <a-menu-item
417
- v-else
418
- :key="item.key"
419
- :danger="item.danger"
420
- >
421
- <span
422
- :class="item.icon"
423
- class="mr-2"
424
- />
425
- {{ item.label }}
426
- </a-menu-item>
427
- </template>
428
- </a-menu>
429
- </template>
430
- </a-dropdown>
431
- </div>
432
-
433
- <!-- 绘图仪进度线:路由切换时自左向右扫过 -->
434
- <span
435
- class="plotter-line"
436
- :class="{ run: plotting }"
437
- aria-hidden="true"
438
- />
439
- </a-layout-header>
440
- </template>
441
-
442
- <style scoped>
443
- .app-header {
444
- position: relative;
445
- display: flex;
446
- align-items: center;
447
- justify-content: space-between;
448
- height: var(--app-header-h, 56px);
449
- padding: 0 20px 0 10px;
450
- overflow: hidden;
451
- border-bottom: 1px solid var(--divider-hair);
452
- transition: background 0.3s ease, border-color 0.3s ease;
453
- }
454
-
455
- /* 顶栏玻璃:透出极光画布;下缘发丝线 + 微暗渐变保对比
456
- * v4:与侧栏同属壳层材质(同一观察窗),底部内阴影替代原先的顶部渐变 —— 玻璃的"厚度"来自遮蔽而非压暗 */
457
- .app-header-glass {
458
- position: sticky;
459
- top: 0;
460
- z-index: 30;
461
- background: var(--mat-chrome-bg);
462
- backdrop-filter: var(--vibrancy-chrome);
463
- border-bottom: 1px solid var(--glass-line);
464
- box-shadow: var(--glass-specular);
465
- }
466
-
467
- .header-left,
468
- .header-right {
469
- display: flex;
470
- align-items: center;
471
- gap: 10px;
472
- min-width: 0;
473
- }
474
-
475
- /* ── 航迹标绘轨 ── */
476
- .rail-mark {
477
- flex: 0 0 auto;
478
- font-size: 15px;
479
- color: var(--ink-faint);
480
- }
481
-
482
- .trail {
483
- flex: 0 1 auto;
484
- min-width: 0;
485
- overflow-x: auto;
486
- overflow-y: hidden;
487
- scrollbar-width: none;
488
- -webkit-mask-image: linear-gradient(90deg, transparent 0, black 8px, black calc(100% - 18px), transparent 100%);
489
- mask-image: linear-gradient(90deg, transparent 0, black 8px, black calc(100% - 18px), transparent 100%);
490
- }
491
-
492
- .trail::-webkit-scrollbar {
493
- display: none;
494
- }
495
-
496
- .trail-row {
497
- display: flex;
498
- align-items: center;
499
- gap: 0;
500
- padding: 3px 4px;
501
- white-space: nowrap;
502
- }
503
-
504
- /* 航点间细线:同色系 hairline */
505
- .trail-link {
506
- flex: 0 0 auto;
507
- width: 14px;
508
- height: 1px;
509
- background: var(--line-strong);
510
- }
511
-
512
- /* 航点:chip 圆角描边,悬停抬亮,当前页墨色填充(ink pill) */
513
- .trail-node {
514
- position: relative;
515
- display: inline-flex;
516
- flex: 0 0 auto;
517
- gap: 6px;
518
- align-items: center;
519
- height: 32px;
520
- padding: 0 12px;
521
- font-family: var(--font-body);
522
- font-size: 12px;
523
- font-weight: 500;
524
- letter-spacing: 0.01em;
525
- color: var(--app-text, var(--ink-soft));
526
- cursor: pointer;
527
- background: var(--paper-deep);
528
- border: 1px solid var(--line);
529
- border-radius: var(--radius-pill);
530
- transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
531
- }
532
-
533
- .trail-node:hover {
534
- color: var(--ink);
535
- background: var(--paper-tint);
536
- border-color: var(--line-strong);
537
- }
538
-
539
- .trail-node:active {
540
- transform: scale(0.98);
541
- }
542
-
543
- .trail-node:focus-visible {
544
- outline: 2px solid var(--accent);
545
- outline-offset: 2px;
546
- }
547
-
548
- .trail-node.active {
549
- color: var(--on-accent);
550
- background: var(--accent);
551
- border-color: var(--accent);
552
- }
553
-
554
- .node-icon {
555
- font-size: 13px;
556
- }
557
-
558
- .node-title {
559
- max-width: 132px;
560
- overflow: hidden;
561
- letter-spacing: 0.02em;
562
- text-overflow: ellipsis;
563
- white-space: nowrap;
564
- transition: max-width 0.18s var(--ease-out-quart), margin 0.18s var(--ease-out-quart), opacity 0.18s var(--ease-out-quart);
565
- }
566
-
567
- /* 标签关闭钮:槽位常驻(零布局抖动),悬停标签时图标淡入展开(浏览器页签交互);
568
- * 当前页签(朱砂底)用白色保证对比 */
569
- .node-close {
570
- display: inline-flex;
571
- flex: 0 0 auto;
572
- align-items: center;
573
- justify-content: center;
574
- width: 16px;
575
- height: 16px;
576
- margin-left: 4px;
577
- font-size: 12px;
578
- color: var(--ink-faint);
579
- cursor: pointer;
580
- background: transparent;
581
- border: none;
582
- border-radius: var(--radius-chip);
583
- opacity: 0;
584
- transform: scale(0.6);
585
- transition:
586
- opacity 0.18s var(--ease-out-quart),
587
- transform 0.18s var(--ease-out-quart),
588
- color var(--transition-fast),
589
- background var(--transition-fast);
590
- }
591
-
592
- .trail-node:hover .node-close,
593
- .trail-node:focus-within .node-close {
594
- opacity: 1;
595
- transform: none;
596
- }
597
-
598
- .node-close:hover {
599
- color: var(--ink);
600
- background: var(--hover-tint);
601
- }
602
-
603
- .node-close:active {
604
- transform: scale(0.9);
605
- }
606
-
607
- .trail-node.active .node-close {
608
- color: color-mix(in srgb, var(--on-accent) 85%, transparent);
609
- }
610
-
611
- .trail-node.active .node-close:hover {
612
- color: var(--on-accent);
613
- background: color-mix(in srgb, var(--on-accent) 18%, transparent);
614
- }
615
-
616
- .node-close:focus-visible {
617
- outline: 2px solid var(--accent);
618
- outline-offset: 1px;
619
- }
620
-
621
- /* 航点进场:轻抬升淡入 */
622
- .stamp-enter-active {
623
- transition: opacity 0.22s var(--ease-out-quart), transform 0.22s var(--ease-out-quart);
624
- }
625
-
626
- .stamp-enter-from {
627
- opacity: 0;
628
- transform: translateY(4px);
629
- }
630
-
631
- .stamp-leave-active {
632
- position: absolute;
633
- transition: opacity 0.14s ease;
634
- }
635
-
636
- .stamp-leave-to {
637
- opacity: 0;
638
- }
639
-
640
- .stamp-move {
641
- transition: transform 0.22s ease;
642
- }
643
-
644
- /* 路由切换进度线:墨色细扫过(替换原天青→蜜桃粉彩渐变,色锁:温灰下仅墨色动线) */
645
- .plotter-line {
646
- position: absolute;
647
- bottom: -1px;
648
- left: 0;
649
- width: 100%;
650
- height: 2px;
651
- background: linear-gradient(90deg, transparent, var(--ink-fainter) 30%, var(--accent));
652
- opacity: 0;
653
- pointer-events: none;
654
- transform: scaleX(0);
655
- transform-origin: left center;
656
- }
657
-
658
- .plotter-line.run {
659
- animation: plot-sweep 0.6s cubic-bezier(0.3, 0.8, 0.4, 1) forwards;
660
- }
661
-
662
- @keyframes plot-sweep {
663
- 0% {
664
- transform: scaleX(0);
665
- opacity: 0.9;
666
- }
667
-
668
- 70% {
669
- opacity: 0.9;
670
- }
671
-
672
- 100% {
673
- transform: scaleX(1);
674
- opacity: 0;
675
- }
676
- }
677
-
678
- /* 幽灵图标钮:无描边,悬停浮 surface(open-tag tp-close 声部) */
679
- .collapse-btn,
680
- .icon-btn {
681
- display: inline-flex;
682
- flex: 0 0 auto;
683
- align-items: center;
684
- justify-content: center;
685
- width: 34px;
686
- height: 34px;
687
- font-size: 16px;
688
- color: var(--app-text, var(--ink-soft));
689
- cursor: pointer;
690
- background: transparent;
691
- border: 0;
692
- border-radius: var(--radius-panel-sm);
693
- transition:
694
- background var(--transition-fast),
695
- color var(--transition-fast),
696
- transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
697
- }
698
-
699
- .collapse-btn:hover,
700
- .icon-btn:hover {
701
- color: var(--ink);
702
- background: var(--paper-deep);
703
- }
704
-
705
- .collapse-btn:active,
706
- .icon-btn:active {
707
- transform: scale(0.94);
708
- }
709
-
710
- .lang-select {
711
- width: 118px;
712
- }
713
-
714
- /* 语言选择器:令牌驱动(主题失配时 antd 兜底色会露白,这里显式钉死) */
715
- .lang-select :deep(.ant-select-selector) {
716
- color: var(--app-text, var(--ink));
717
- background: transparent !important;
718
- border-color: var(--app-border, var(--line)) !important;
719
- }
720
-
721
- .lang-select :deep(.ant-select-arrow) {
722
- color: var(--app-text-secondary, var(--ink-faint));
723
- }
724
-
725
- /* 操作者铭牌:头像 + 双行 */
726
- .user-chip {
727
- display: flex;
728
- align-items: center;
729
- gap: 10px;
730
- height: 42px;
731
- margin-left: 4px;
732
- padding: 0 12px 0 8px;
733
- cursor: pointer;
734
- border: 1px solid transparent;
735
- border-radius: var(--radius-pill);
736
- transition: border-color 0.16s ease, background 0.16s ease;
737
- }
738
-
739
- .user-chip:hover {
740
- border-color: var(--line);
741
- background: var(--hover-tint);
742
- }
743
-
744
- .user-initial {
745
- width: 28px;
746
- height: 28px;
747
- font-size: 12px;
748
- }
749
-
750
- .user-meta {
751
- display: flex;
752
- flex-direction: column;
753
- line-height: 1.15;
754
- }
755
-
756
- .user-name {
757
- font-size: 12.5px;
758
- font-weight: 600;
759
- color: var(--app-text, var(--ink));
760
- letter-spacing: 0.01em;
761
- }
762
-
763
- .user-role {
764
- font-family: var(--font-mono);
765
- /* 8.5px 是"看得见读不了"的下限之外(实测常驻被审计标红);
766
- 角色是身份信息,不是装饰刻度,抬到 9.5px 仍保持铭牌声部 */
767
- font-size: 9.5px;
768
- letter-spacing: 0.14em;
769
- text-transform: uppercase;
770
- color: var(--app-text-secondary, var(--ink-faint));
771
- }
772
-
773
- /* 移动端当前页标题:桌面不存在(那里有页签轨) */
774
- .mobile-title {
775
- display: none;
776
- min-width: 0;
777
- overflow: hidden;
778
- font-size: 14px;
779
- font-weight: 600;
780
- color: var(--app-text, var(--ink));
781
- text-overflow: ellipsis;
782
- white-space: nowrap;
783
- }
784
-
785
- /* ══ 顶栏响应式:按信息优先级逐级卸载,而不是让它们互相挤压 ═══════════════
786
- * 卸载顺序(从最可省到最不可省):页签轨 → 全屏钮 → 用户双行铭牌 → 语言文字。
787
- * 留下的核心是:HITL 铃标(有待办必须看得见)、连接状态点、主题、语言、身份。 */
788
- @media (max-width: 899px) {
789
- .rail-mark,
790
- .trail {
791
- display: none;
792
- }
793
-
794
- .mobile-title {
795
- display: block;
796
- margin-left: 2px;
797
- }
798
-
799
- .app-header {
800
- padding: 0 12px 0 8px;
801
- }
802
- }
803
-
804
- @media (max-width: 639px) {
805
- .hdr-fullscreen {
806
- display: none;
807
- }
808
-
809
- .user-chip {
810
- height: 38px;
811
- padding: 0 6px;
812
- }
813
-
814
- .user-meta {
815
- display: none;
816
- }
817
-
818
- /* 窄屏标签已换成 2 字符(ZH / EN),选择器只需容纳标签 + 箭头 + 内距 */
819
- .lang-select {
820
- width: 76px;
821
- }
822
-
823
- .lang-select :deep(.ant-select-selection-item) {
824
- padding-inline-end: 16px;
825
- font-size: 12.5px;
826
- }
827
-
828
- .header-left,
829
- .header-right {
830
- gap: 4px;
831
- }
832
- }
833
-
834
- .hidden {
835
- display: none;
836
- }
837
-
838
- /* 实时连接状态点(诚实在线:open=绿/syncing=琥珀呼吸/closed=红;reduced-motion 全局收敛) */
839
- .ws-dot {
840
- flex: none;
841
- width: 8px;
842
- height: 8px;
843
- margin: 0 2px;
844
- border-radius: 50%;
845
- background: var(--tone-success-dot);
846
- box-shadow: 0 0 6px color-mix(in srgb, var(--tone-success-dot) 55%, transparent);
847
- }
848
- .ws-dot.syncing {
849
- background: var(--tone-warning-dot);
850
- box-shadow: 0 0 6px color-mix(in srgb, var(--tone-warning-dot) 55%, transparent);
851
- animation: ws-pulse 1.2s ease-in-out infinite;
852
- }
853
- .ws-dot.down {
854
- background: var(--tone-danger-dot);
855
- box-shadow: 0 0 6px color-mix(in srgb, var(--tone-danger-dot) 55%, transparent);
856
- }
857
- @keyframes ws-pulse {
858
- 50% { opacity: 0.45; }
859
- }
860
-
861
- /* ── HITL 待办铃标(琥珀警示;角标数字极简,无装饰堆砌) ── */
862
- .hitl-bell {
863
- position: relative;
864
- color: var(--tone-warning-dot, #d4a017);
865
- }
866
- .hitl-count {
867
- position: absolute;
868
- top: 2px;
869
- right: 2px;
870
- min-width: 15px;
871
- height: 15px;
872
- padding: 0 4px;
873
- font-family: var(--font-mono);
874
- font-size: 9.5px;
875
- font-weight: 700;
876
- line-height: 15px;
877
- color: var(--on-accent, #fff);
878
- text-align: center;
879
- background: var(--tone-danger-dot, #e05252);
880
- border-radius: 8px;
881
- }
882
- .hitl-menu {
883
- min-width: 260px;
884
- max-width: 340px;
885
- padding: 6px;
886
- background: var(--paper, #fff);
887
- border: 1px solid var(--line);
888
- border-radius: var(--radius-panel-sm, 10px);
889
- box-shadow: 0 10px 32px rgba(0, 0, 0, 0.14);
890
- }
891
- .hitl-menu-title {
892
- padding: 6px 10px;
893
- font-size: 11px;
894
- font-weight: 600;
895
- letter-spacing: 0.08em;
896
- color: var(--ink-faint);
897
- border-bottom: 1px solid var(--line);
898
- }
899
- .hitl-item {
900
- display: flex;
901
- flex-direction: column;
902
- gap: 3px;
903
- width: 100%;
904
- margin-top: 4px;
905
- padding: 8px 10px;
906
- text-align: left;
907
- cursor: pointer;
908
- background: transparent;
909
- border: 1px solid transparent;
910
- border-radius: var(--radius-panel-sm, 8px);
911
- transition: background var(--transition-fast), border-color var(--transition-fast);
912
- }
913
- .hitl-item:hover {
914
- background: var(--paper-deep);
915
- border-color: var(--line);
916
- }
917
- .hitl-item-top {
918
- display: flex;
919
- gap: 8px;
920
- align-items: baseline;
921
- justify-content: space-between;
922
- }
923
- .hitl-item-agent {
924
- font-size: 12.5px;
925
- font-weight: 600;
926
- color: var(--ink);
927
- }
928
- .hitl-item-kind {
929
- font-family: var(--font-mono);
930
- font-size: 9.5px;
931
- letter-spacing: 0.1em;
932
- color: var(--tone-warning-dot, #d4a017);
933
- }
934
- .hitl-item-title {
935
- overflow: hidden;
936
- font-size: 12px;
937
- color: var(--ink-soft);
938
- text-overflow: ellipsis;
939
- white-space: nowrap;
940
- }
941
- .hitl-item-actions {
942
- display: flex;
943
- gap: 6px;
944
- margin-top: 6px;
945
- }
946
-
947
- @media (prefers-reduced-motion: reduce) {
948
- .plotter-line.run {
949
- animation: none;
950
- opacity: 0;
951
- }
952
-
953
- .stamp-enter-active,
954
- .stamp-leave-active,
955
- .stamp-move {
956
- transition: none;
957
- }
958
- }
959
- </style>
1
+ <script setup lang="ts">
2
+ import { message } from 'ant-design-vue'
3
+ import type { MenuProps, SelectProps } from 'ant-design-vue'
4
+ import type { AepHitlItem } from '#shared/workshop-protocol'
5
+ import { useUserStore } from '@/app/stores/workshop/user'
6
+ import { useWsConnectionStore } from '@/app/stores/workshop/connection'
7
+ import { useHitlStore } from '@/app/stores/workshop/hitl'
8
+ import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
9
+ import { useWorkshopApi } from '@/app/composables/workshop/useWorkshopApi'
10
+
11
+ const { t, locale, locales, setLocale } = useI18n()
12
+ const store = useAppStore()
13
+ const route = useRoute()
14
+ const trail = useRouteTrailStore()
15
+ const userStore = useUserStore()
16
+ const { metaFor } = useRouteMeta()
17
+
18
+ // ── HITL 全局待办(omp ask 对话框 + dcw 审批统一徽标;页头保底建连 ——
19
+ // hitl 全员直推帧只达已连 peer,不建连的页面收不到提醒;快照兜底刷新前待办) ──
20
+ const hitl = useHitlStore()
21
+ const wsSession = useWorkshopWs()
22
+ const ensureHitlLive = () => {
23
+ if (userStore.isLoggedIn) wsSession.ensureConnected()
24
+ }
25
+ watch(() => userStore.token, (t2) => {
26
+ if (t2) {
27
+ ensureHitlLive()
28
+ void hitl.loadSnapshot()
29
+ }
30
+ else hitl.clear()
31
+ }, { immediate: true })
32
+ const HITL_ANSWERABLE = new Set(['codex-approval', 'opencode-permission', 'dsh-permission'])
33
+ const hitlKindLabel = (kind: AepHitlItem['kind']) => {
34
+ switch (kind) {
35
+ case 'omp-dialog': return t('appHeader.hitlKindOmp')
36
+ case 'dcw-approval': return t('appHeader.hitlKindDcw')
37
+ case 'codex-approval': return t('appHeader.hitlKindCodex')
38
+ case 'opencode-permission': return t('appHeader.hitlKindOpencode')
39
+ case 'dsh-permission': return t('appHeader.hitlKindDsh')
40
+ default: return kind
41
+ }
42
+ }
43
+ const hitlGo = (item: AepHitlItem) => {
44
+ navigateTo({ path: '/monitor', query: { agentId: item.agentId, channelId: item.channelId } })
45
+ }
46
+ // 引擎审批(codex/opencode/dsh)页头内联应答:批准/拒绝直接回传引擎,无需进终端
47
+ const hitlApi = useWorkshopApi()
48
+ const hitlAnswering = ref<string | null>(null)
49
+ const hitlAnswer = async (item: AepHitlItem, confirmed: boolean): Promise<void> => {
50
+ hitlAnswering.value = `${item.kind}:${item.id}`
51
+ try {
52
+ await hitlApi.respondHitl({ kind: item.kind, id: item.id, confirmed, cancelled: !confirmed })
53
+ await hitl.loadSnapshot()
54
+ }
55
+ catch (e) {
56
+ message.error(apiErrorMessage(e))
57
+ }
58
+ finally {
59
+ hitlAnswering.value = null
60
+ }
61
+ }
62
+
63
+ /* 语言选择器在窄屏显示"短名":完整语言名(简体中文=56px @14px)配上
64
+ * antd 给箭头预留的 24px 内距,92px 的选择器只剩 46px 文本位 ——
65
+ * 实测被截成「简 体…」,这是每页都出现的破相。
66
+ * 窄屏用 2 字标签,桌面保留完整语言名。 */
67
+ const localeOptions = computed(() =>
68
+ (locales.value as Array<{ code: string, name: string }>).map(l => ({
69
+ label: isMobile.value ? l.code.split('-')[0]!.toUpperCase() : l.name,
70
+ value: l.code,
71
+ })),
72
+ )
73
+
74
+ const switchLocale: SelectProps['onChange'] = (value) => {
75
+ if (value != null) {
76
+ // 持久化 + 强刷:setup 期求值的词条(脚本常量)只有重载才能整体切换
77
+ // cookie 参与 SSR 首帧(locale-cookie.global.ts);localStorage 保留为插件回退
78
+ useCookie('aw.locale', { maxAge: 60 * 60 * 24 * 365 }).value = String(value)
79
+ localStorage.setItem('aw.locale', String(value))
80
+ setLocale(String(value) as 'zh-CN' | 'en')
81
+ window.location.reload()
82
+ }
83
+ }
84
+
85
+ /* ── 响应式:窄屏顶栏不是"缩小版顶栏",而是换一套信息优先级 ─────────────
86
+ * 桌面:航迹页签是主角(多页并行来回切)。
87
+ * 窄屏:页签轨与右侧功能簇争抢横向空间 —— 实测 390px 下页签被右侧图标
88
+ * 逐个压过去,最后连用户铭牌都被推出视口(顶栏 overflow:hidden 直接裁掉)。
89
+ * 所以窄屏撤掉页签轨,用**当前页标题**替代:窄屏用户要的是"我在哪",
90
+ * 不是"我刚才还开过哪 5 个页面"。 */
91
+ const { isDrawer, isMobile } = useResponsive()
92
+
93
+ const navToggleLabel = computed(() =>
94
+ isDrawer.value
95
+ ? (store.mobileNavOpen ? t('header.closeNav') : t('header.openNav'))
96
+ : (store.sidebarCollapsed ? t('header.expand') : t('header.collapse')))
97
+
98
+ const toggleNav = () => {
99
+ if (isDrawer.value) store.toggleMobileNav()
100
+ else store.toggleSidebar()
101
+ }
102
+
103
+ const currentTitle = computed(() => metaFor(route.path).title)
104
+
105
+ // ── 航迹导航:路由变化 → 记录航点;切换时进度线扫过 ──
106
+ const hydrated = ref(false)
107
+ const plotting = ref(false)
108
+ let plotTimer: ReturnType<typeof setTimeout> | null = null
109
+
110
+ watch(() => route.path, () => {
111
+ trail.visit(route.path)
112
+ plotting.value = false
113
+ requestAnimationFrame(() => {
114
+ plotting.value = true
115
+ if (plotTimer) clearTimeout(plotTimer)
116
+ plotTimer = setTimeout(() => {
117
+ plotting.value = false
118
+ }, 620)
119
+ })
120
+ }, { immediate: false })
121
+
122
+ onMounted(() => {
123
+ hydrated.value = true
124
+ trail.visit(route.path)
125
+ })
126
+
127
+ onBeforeUnmount(() => {
128
+ if (plotTimer) clearTimeout(plotTimer)
129
+ })
130
+
131
+ const go = (path: string) => {
132
+ if (path !== route.path) {
133
+ navigateTo(path)
134
+ }
135
+ }
136
+
137
+ /**
138
+ * 关闭航点标签页:
139
+ * - 非当前页 → 仅从航迹移除;
140
+ * - 当前页 → 先选好跳转目标(优先左侧相邻航点,其次右侧,兜底仪表盘)再移除并跳转,
141
+ * 避免关闭后停留在一个已无标签的路由上(watcher 会在跳转后 visit 目标页,不会回补被关页)。
142
+ */
143
+ const closeWaypoint = (path: string) => {
144
+ const idx = trail.remove(path)
145
+ if (path !== route.path) return
146
+ const rest = trail.waypoints
147
+ const target = rest[Math.min(Math.max(idx - 1, 0), Math.max(rest.length - 1, 0))]?.path ?? '/'
148
+ navigateTo(target)
149
+ }
150
+
151
+ const isFullscreen = ref(false)
152
+
153
+ function toggleFullscreen() {
154
+ if (!document.fullscreenElement) {
155
+ document.documentElement.requestFullscreen()
156
+ isFullscreen.value = true
157
+ }
158
+ else {
159
+ document.exitFullscreen()
160
+ isFullscreen.value = false
161
+ }
162
+ }
163
+
164
+ // 用户铭牌:真实身份(workshop 用户系统;未登录 = 访客)
165
+ const userInitial = computed(() => (userStore.user?.name ?? '?').trim().charAt(0).toUpperCase())
166
+ const userName = computed(() => userStore.user?.name ?? t('header.guest'))
167
+ const userRole = computed(() => (userStore.isLoggedIn ? userStore.user?.role ?? 'user' : 'anonymous'))
168
+
169
+ // ── 实时连接状态点(全局 WS 单例的诚实在线指示;断连不假装在线) ──
170
+ const conn = useWsConnectionStore()
171
+ const wsVisible = computed(() => conn.state !== 'closed' || conn.lastDataAt > 0)
172
+ const wsClass = computed(() => {
173
+ if (conn.state === 'open') return conn.pendingReplay ? 'syncing' : 'live'
174
+ if (conn.state === 'connecting') return 'syncing'
175
+ return 'down'
176
+ })
177
+ const wsLabel = computed(() => {
178
+ if (conn.state === 'open') return conn.pendingReplay ? t('appHeader.kwsdot0002') : t('appHeader.kwsdot0001')
179
+ if (conn.state === 'connecting') return t('appHeader.kwsdot0003')
180
+ return t('appHeader.kwsdot0004')
181
+ })
182
+
183
+ interface AvatarMenuEntry {
184
+ key: string
185
+ label: string
186
+ icon?: string
187
+ danger?: boolean
188
+ divider?: boolean
189
+ }
190
+
191
+ const avatarItems = computed<AvatarMenuEntry[]>(() => {
192
+ const items: AvatarMenuEntry[] = [
193
+ { key: 'tokens', icon: 'i-tabler-key', label: t('menu.tokens') },
194
+ { key: 'settings', icon: 'i-tabler-adjustments', label: t('menu.settings') },
195
+ ]
196
+ if (userStore.isLoggedIn) {
197
+ items.push({ key: 'd-logout', label: '', divider: true })
198
+ items.push({ key: 'logout', icon: 'i-tabler-logout', label: t('header.logout'), danger: true })
199
+ }
200
+ return items
201
+ })
202
+
203
+ const onAvatarMenu: MenuProps['onClick'] = async ({ key }) => {
204
+ if (key === 'settings') {
205
+ navigateTo('/settings')
206
+ }
207
+ else if (key === 'tokens') {
208
+ navigateTo('/tokens')
209
+ }
210
+ else if (key === 'logout') {
211
+ await userStore.logout()
212
+ navigateTo('/workshop')
213
+ }
214
+ }
215
+ </script>
216
+
217
+ <template>
218
+ <a-layout-header
219
+ class="app-header app-header-glass"
220
+ >
221
+ <!-- 左侧:折叠 + 航迹标绘轨 -->
222
+ <div class="header-left">
223
+ <button
224
+ class="collapse-btn"
225
+ :aria-label="navToggleLabel"
226
+ :aria-expanded="isDrawer ? store.mobileNavOpen : !store.sidebarCollapsed"
227
+ @click="toggleNav()"
228
+ >
229
+ <span class="i-tabler-menu-2" />
230
+ </button>
231
+
232
+ <!-- 窄屏:当前页标题(替代被撤掉的页签轨) -->
233
+ <span class="mobile-title">{{ currentTitle }}</span>
234
+
235
+ <span class="rail-mark i-tabler-route" />
236
+
237
+ <nav
238
+ class="trail"
239
+ :aria-label="t('header.trail')"
240
+ >
241
+ <transition-group
242
+ name="stamp"
243
+ tag="div"
244
+ class="trail-row"
245
+ >
246
+ <template v-if="hydrated">
247
+ <template
248
+ v-for="(w, i) in trail.waypoints"
249
+ :key="w.path"
250
+ >
251
+ <span
252
+ v-if="i > 0"
253
+ :key="`link-${w.path}`"
254
+ class="trail-link"
255
+ aria-hidden="true"
256
+ />
257
+ <div
258
+ class="trail-node"
259
+ :class="{ active: w.path === route.path }"
260
+ role="button"
261
+ tabindex="0"
262
+ :title="metaFor(w.path).title"
263
+ @click="go(w.path)"
264
+ @keydown.enter.prevent="go(w.path)"
265
+ >
266
+ <span
267
+ class="node-icon"
268
+ :class="metaFor(w.path).icon"
269
+ />
270
+ <span class="node-title">{{ metaFor(w.path).title }}</span>
271
+ <button
272
+ class="node-close"
273
+ :aria-label="$t('appHeader.kzwjv99001', { p0: metaFor(w.path).title })"
274
+ :title="$t('appHeader.kzwjv99001', { p0: metaFor(w.path).title })"
275
+ @click.stop="closeWaypoint(w.path)"
276
+ >
277
+ <span class="i-tabler-x" />
278
+ </button>
279
+ </div>
280
+ </template>
281
+ </template>
282
+ </transition-group>
283
+ </nav>
284
+ </div>
285
+
286
+ <!-- 右侧:功能集群 -->
287
+ <div class="header-right">
288
+ <!-- HITL 待办铃标(有待办才出现;点击下拉 → 进入对应 agent 终端处理) -->
289
+ <ClientOnly>
290
+ <a-dropdown
291
+ v-if="hitl.count > 0"
292
+ placement="bottomRight"
293
+ >
294
+ <button
295
+ class="icon-btn hitl-bell"
296
+ :title="t('appHeader.hitlBadge')"
297
+ >
298
+ <span class="i-tabler-bell-ringing" />
299
+ <span class="hitl-count">{{ hitl.count }}</span>
300
+ </button>
301
+ <template #overlay>
302
+ <div class="hitl-menu">
303
+ <div class="hitl-menu-title">
304
+ {{ t('appHeader.hitlBadge') }} · {{ hitl.count }}
305
+ </div>
306
+ <button
307
+ v-for="item in hitl.items"
308
+ :key="`${item.kind}:${item.id}`"
309
+ class="hitl-item"
310
+ @click="hitlGo(item)"
311
+ >
312
+ <span class="hitl-item-top">
313
+ <span class="hitl-item-agent">{{ item.agentName }}</span>
314
+ <span class="hitl-item-kind">{{ hitlKindLabel(item.kind) }}</span>
315
+ </span>
316
+ <span class="hitl-item-title">{{ item.title }}</span>
317
+ <span
318
+ v-if="HITL_ANSWERABLE.has(item.kind)"
319
+ class="hitl-item-actions"
320
+ @click.stop
321
+ >
322
+ <a-button
323
+ size="small"
324
+ type="primary"
325
+ :loading="hitlAnswering === `${item.kind}:${item.id}`"
326
+ @click="hitlAnswer(item, true)"
327
+ >
328
+ {{ t('appHeader.hitlApprove') }}
329
+ </a-button>
330
+ <a-button
331
+ size="small"
332
+ danger
333
+ :disabled="hitlAnswering === `${item.kind}:${item.id}`"
334
+ @click="hitlAnswer(item, false)"
335
+ >
336
+ {{ t('appHeader.hitlReject') }}
337
+ </a-button>
338
+ </span>
339
+ </button>
340
+ </div>
341
+ </template>
342
+ </a-dropdown>
343
+ </ClientOnly>
344
+ <!-- 实时连接状态点(WS 会话全局单例;未用过 WS 的会话不显示) -->
345
+ <span
346
+ v-if="hydrated && wsVisible"
347
+ class="ws-dot"
348
+ :class="wsClass"
349
+ :title="wsLabel"
350
+ />
351
+ <a-tooltip :title="t('header.fullscreen')">
352
+ <button
353
+ class="icon-btn hdr-fullscreen"
354
+ @click="toggleFullscreen"
355
+ >
356
+ <span
357
+ class="i-tabler-arrows-maximize"
358
+ :class="{ hidden: isFullscreen }"
359
+ />
360
+ <span
361
+ class="i-tabler-arrows-minimize"
362
+ :class="{ hidden: !isFullscreen }"
363
+ />
364
+ </button>
365
+ </a-tooltip>
366
+
367
+ <a-select
368
+ id="hdr-locale"
369
+ :value="locale"
370
+ size="middle"
371
+ :options="localeOptions"
372
+ class="lang-select"
373
+ @change="switchLocale"
374
+ />
375
+
376
+ <a-tooltip :title="store.isDark ? t('common.light') : t('common.dark')">
377
+ <button
378
+ class="icon-btn"
379
+ @click="store.toggleDark()"
380
+ >
381
+ <span
382
+ class="i-tabler-sun-high"
383
+ :class="{ hidden: store.isDark }"
384
+ />
385
+ <span
386
+ class="i-tabler-moon-stars"
387
+ :class="{ hidden: !store.isDark }"
388
+ />
389
+ </button>
390
+ </a-tooltip>
391
+
392
+ <a-dropdown>
393
+ <div class="user-chip">
394
+ <!-- 用户身份(session 异步解析)仅客户端可知:SSR 渲染中性占位,挂载后填充
395
+ —— 消除 hydration mismatch(unhead dispose 噪音的根因) -->
396
+ <ClientOnly>
397
+ <span class="user-initial aw-avatar">{{ userInitial }}</span>
398
+ <span class="user-meta">
399
+ <span class="user-name">{{ userName }}</span>
400
+ <span class="user-role">{{ userRole }}</span>
401
+ </span>
402
+ <template #fallback>
403
+ <span class="user-initial aw-avatar">·</span>
404
+ <span class="user-meta">
405
+ <span class="user-name">·</span>
406
+ <span class="user-role">·</span>
407
+ </span>
408
+ </template>
409
+ </ClientOnly>
410
+ </div>
411
+ <template #overlay>
412
+ <a-menu @click="onAvatarMenu">
413
+ <template
414
+ v-for="item in avatarItems"
415
+ :key="item.key"
416
+ >
417
+ <a-menu-divider v-if="item.divider" />
418
+ <a-menu-item
419
+ v-else
420
+ :key="item.key"
421
+ :danger="item.danger"
422
+ >
423
+ <span
424
+ :class="item.icon"
425
+ class="mr-2"
426
+ />
427
+ {{ item.label }}
428
+ </a-menu-item>
429
+ </template>
430
+ </a-menu>
431
+ </template>
432
+ </a-dropdown>
433
+ </div>
434
+
435
+ <!-- 绘图仪进度线:路由切换时自左向右扫过 -->
436
+ <span
437
+ class="plotter-line"
438
+ :class="{ run: plotting }"
439
+ aria-hidden="true"
440
+ />
441
+ </a-layout-header>
442
+ </template>
443
+
444
+ <style scoped>
445
+ .app-header {
446
+ position: relative;
447
+ display: flex;
448
+ align-items: center;
449
+ justify-content: space-between;
450
+ height: var(--app-header-h, 56px);
451
+ padding: 0 20px 0 10px;
452
+ overflow: hidden;
453
+ border-bottom: 1px solid var(--divider-hair);
454
+ transition: background 0.3s ease, border-color 0.3s ease;
455
+ }
456
+
457
+ /* 顶栏玻璃:透出极光画布;下缘发丝线 + 微暗渐变保对比
458
+ * v4:与侧栏同属壳层材质(同一观察窗),底部内阴影替代原先的顶部渐变 —— 玻璃的"厚度"来自遮蔽而非压暗 */
459
+ .app-header-glass {
460
+ position: sticky;
461
+ top: 0;
462
+ z-index: 30;
463
+ background: var(--mat-chrome-bg);
464
+ backdrop-filter: var(--vibrancy-chrome);
465
+ border-bottom: 1px solid var(--glass-line);
466
+ box-shadow: var(--glass-specular);
467
+ }
468
+
469
+ .header-left,
470
+ .header-right {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 10px;
474
+ min-width: 0;
475
+ }
476
+
477
+ /* ── 航迹标绘轨 ── */
478
+ .rail-mark {
479
+ flex: 0 0 auto;
480
+ font-size: 15px;
481
+ color: var(--ink-faint);
482
+ }
483
+
484
+ .trail {
485
+ flex: 0 1 auto;
486
+ min-width: 0;
487
+ overflow-x: auto;
488
+ overflow-y: hidden;
489
+ scrollbar-width: none;
490
+ -webkit-mask-image: linear-gradient(90deg, transparent 0, black 8px, black calc(100% - 18px), transparent 100%);
491
+ mask-image: linear-gradient(90deg, transparent 0, black 8px, black calc(100% - 18px), transparent 100%);
492
+ }
493
+
494
+ .trail::-webkit-scrollbar {
495
+ display: none;
496
+ }
497
+
498
+ .trail-row {
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 0;
502
+ padding: 3px 4px;
503
+ white-space: nowrap;
504
+ }
505
+
506
+ /* 航点间细线:同色系 hairline */
507
+ .trail-link {
508
+ flex: 0 0 auto;
509
+ width: 14px;
510
+ height: 1px;
511
+ background: var(--line-strong);
512
+ }
513
+
514
+ /* 航点:chip 圆角描边,悬停抬亮,当前页墨色填充(ink pill) */
515
+ .trail-node {
516
+ position: relative;
517
+ display: inline-flex;
518
+ flex: 0 0 auto;
519
+ gap: 6px;
520
+ align-items: center;
521
+ height: 32px;
522
+ padding: 0 12px;
523
+ font-family: var(--font-body);
524
+ font-size: 12px;
525
+ font-weight: 500;
526
+ letter-spacing: 0.01em;
527
+ color: var(--app-text, var(--ink-soft));
528
+ cursor: pointer;
529
+ background: var(--paper-deep);
530
+ border: 1px solid var(--line);
531
+ border-radius: var(--radius-pill);
532
+ transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
533
+ }
534
+
535
+ .trail-node:hover {
536
+ color: var(--ink);
537
+ background: var(--paper-tint);
538
+ border-color: var(--line-strong);
539
+ }
540
+
541
+ .trail-node:active {
542
+ transform: scale(0.98);
543
+ }
544
+
545
+ .trail-node:focus-visible {
546
+ outline: 2px solid var(--accent);
547
+ outline-offset: 2px;
548
+ }
549
+
550
+ .trail-node.active {
551
+ color: var(--on-accent);
552
+ background: var(--accent);
553
+ border-color: var(--accent);
554
+ }
555
+
556
+ .node-icon {
557
+ font-size: 13px;
558
+ }
559
+
560
+ .node-title {
561
+ max-width: 132px;
562
+ overflow: hidden;
563
+ letter-spacing: 0.02em;
564
+ text-overflow: ellipsis;
565
+ white-space: nowrap;
566
+ transition: max-width 0.18s var(--ease-out-quart), margin 0.18s var(--ease-out-quart), opacity 0.18s var(--ease-out-quart);
567
+ }
568
+
569
+ /* 标签关闭钮:槽位常驻(零布局抖动),悬停标签时图标淡入展开(浏览器页签交互);
570
+ * 当前页签(朱砂底)用白色保证对比 */
571
+ .node-close {
572
+ display: inline-flex;
573
+ flex: 0 0 auto;
574
+ align-items: center;
575
+ justify-content: center;
576
+ width: 16px;
577
+ height: 16px;
578
+ margin-left: 4px;
579
+ font-size: 12px;
580
+ color: var(--ink-faint);
581
+ cursor: pointer;
582
+ background: transparent;
583
+ border: none;
584
+ border-radius: var(--radius-chip);
585
+ opacity: 0;
586
+ transform: scale(0.6);
587
+ transition:
588
+ opacity 0.18s var(--ease-out-quart),
589
+ transform 0.18s var(--ease-out-quart),
590
+ color var(--transition-fast),
591
+ background var(--transition-fast);
592
+ }
593
+
594
+ .trail-node:hover .node-close,
595
+ .trail-node:focus-within .node-close {
596
+ opacity: 1;
597
+ transform: none;
598
+ }
599
+
600
+ .node-close:hover {
601
+ color: var(--ink);
602
+ background: var(--hover-tint);
603
+ }
604
+
605
+ .node-close:active {
606
+ transform: scale(0.9);
607
+ }
608
+
609
+ .trail-node.active .node-close {
610
+ color: color-mix(in srgb, var(--on-accent) 85%, transparent);
611
+ }
612
+
613
+ .trail-node.active .node-close:hover {
614
+ color: var(--on-accent);
615
+ background: color-mix(in srgb, var(--on-accent) 18%, transparent);
616
+ }
617
+
618
+ .node-close:focus-visible {
619
+ outline: 2px solid var(--accent);
620
+ outline-offset: 1px;
621
+ }
622
+
623
+ /* 航点进场:轻抬升淡入 */
624
+ .stamp-enter-active {
625
+ transition: opacity 0.22s var(--ease-out-quart), transform 0.22s var(--ease-out-quart);
626
+ }
627
+
628
+ .stamp-enter-from {
629
+ opacity: 0;
630
+ transform: translateY(4px);
631
+ }
632
+
633
+ .stamp-leave-active {
634
+ position: absolute;
635
+ transition: opacity 0.14s ease;
636
+ }
637
+
638
+ .stamp-leave-to {
639
+ opacity: 0;
640
+ }
641
+
642
+ .stamp-move {
643
+ transition: transform 0.22s ease;
644
+ }
645
+
646
+ /* 路由切换进度线:墨色细扫过(替换原天青→蜜桃粉彩渐变,色锁:温灰下仅墨色动线) */
647
+ .plotter-line {
648
+ position: absolute;
649
+ bottom: -1px;
650
+ left: 0;
651
+ width: 100%;
652
+ height: 2px;
653
+ background: linear-gradient(90deg, transparent, var(--ink-fainter) 30%, var(--accent));
654
+ opacity: 0;
655
+ pointer-events: none;
656
+ transform: scaleX(0);
657
+ transform-origin: left center;
658
+ }
659
+
660
+ .plotter-line.run {
661
+ animation: plot-sweep 0.6s cubic-bezier(0.3, 0.8, 0.4, 1) forwards;
662
+ }
663
+
664
+ @keyframes plot-sweep {
665
+ 0% {
666
+ transform: scaleX(0);
667
+ opacity: 0.9;
668
+ }
669
+
670
+ 70% {
671
+ opacity: 0.9;
672
+ }
673
+
674
+ 100% {
675
+ transform: scaleX(1);
676
+ opacity: 0;
677
+ }
678
+ }
679
+
680
+ /* 幽灵图标钮:无描边,悬停浮 surface(open-tag tp-close 声部) */
681
+ .collapse-btn,
682
+ .icon-btn {
683
+ display: inline-flex;
684
+ flex: 0 0 auto;
685
+ align-items: center;
686
+ justify-content: center;
687
+ width: 34px;
688
+ height: 34px;
689
+ font-size: 16px;
690
+ color: var(--app-text, var(--ink-soft));
691
+ cursor: pointer;
692
+ background: transparent;
693
+ border: 0;
694
+ border-radius: var(--radius-panel-sm);
695
+ transition:
696
+ background var(--transition-fast),
697
+ color var(--transition-fast),
698
+ transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
699
+ }
700
+
701
+ .collapse-btn:hover,
702
+ .icon-btn:hover {
703
+ color: var(--ink);
704
+ background: var(--paper-deep);
705
+ }
706
+
707
+ .collapse-btn:active,
708
+ .icon-btn:active {
709
+ transform: scale(0.94);
710
+ }
711
+
712
+ .lang-select {
713
+ width: 118px;
714
+ }
715
+
716
+ /* 语言选择器:令牌驱动(主题失配时 antd 兜底色会露白,这里显式钉死) */
717
+ .lang-select :deep(.ant-select-selector) {
718
+ color: var(--app-text, var(--ink));
719
+ background: transparent !important;
720
+ border-color: var(--app-border, var(--line)) !important;
721
+ }
722
+
723
+ .lang-select :deep(.ant-select-arrow) {
724
+ color: var(--app-text-secondary, var(--ink-faint));
725
+ }
726
+
727
+ /* 操作者铭牌:头像 + 双行 */
728
+ .user-chip {
729
+ display: flex;
730
+ align-items: center;
731
+ gap: 10px;
732
+ height: 42px;
733
+ margin-left: 4px;
734
+ padding: 0 12px 0 8px;
735
+ cursor: pointer;
736
+ border: 1px solid transparent;
737
+ border-radius: var(--radius-pill);
738
+ transition: border-color 0.16s ease, background 0.16s ease;
739
+ }
740
+
741
+ .user-chip:hover {
742
+ border-color: var(--line);
743
+ background: var(--hover-tint);
744
+ }
745
+
746
+ .user-initial {
747
+ width: 28px;
748
+ height: 28px;
749
+ font-size: 12px;
750
+ }
751
+
752
+ .user-meta {
753
+ display: flex;
754
+ flex-direction: column;
755
+ line-height: 1.15;
756
+ }
757
+
758
+ .user-name {
759
+ font-size: 12.5px;
760
+ font-weight: 600;
761
+ color: var(--app-text, var(--ink));
762
+ letter-spacing: 0.01em;
763
+ }
764
+
765
+ .user-role {
766
+ font-family: var(--font-mono);
767
+ /* 8.5px 是"看得见读不了"的下限之外(实测常驻被审计标红);
768
+ 角色是身份信息,不是装饰刻度,抬到 9.5px 仍保持铭牌声部 */
769
+ font-size: 9.5px;
770
+ letter-spacing: 0.14em;
771
+ text-transform: uppercase;
772
+ color: var(--app-text-secondary, var(--ink-faint));
773
+ }
774
+
775
+ /* 移动端当前页标题:桌面不存在(那里有页签轨) */
776
+ .mobile-title {
777
+ display: none;
778
+ min-width: 0;
779
+ overflow: hidden;
780
+ font-size: 14px;
781
+ font-weight: 600;
782
+ color: var(--app-text, var(--ink));
783
+ text-overflow: ellipsis;
784
+ white-space: nowrap;
785
+ }
786
+
787
+ /* ══ 顶栏响应式:按信息优先级逐级卸载,而不是让它们互相挤压 ═══════════════
788
+ * 卸载顺序(从最可省到最不可省):页签轨 → 全屏钮 → 用户双行铭牌 → 语言文字。
789
+ * 留下的核心是:HITL 铃标(有待办必须看得见)、连接状态点、主题、语言、身份。 */
790
+ @media (max-width: 899px) {
791
+ .rail-mark,
792
+ .trail {
793
+ display: none;
794
+ }
795
+
796
+ .mobile-title {
797
+ display: block;
798
+ margin-left: 2px;
799
+ }
800
+
801
+ .app-header {
802
+ padding: 0 12px 0 8px;
803
+ }
804
+ }
805
+
806
+ @media (max-width: 639px) {
807
+ .hdr-fullscreen {
808
+ display: none;
809
+ }
810
+
811
+ .user-chip {
812
+ height: 38px;
813
+ padding: 0 6px;
814
+ }
815
+
816
+ .user-meta {
817
+ display: none;
818
+ }
819
+
820
+ /* 窄屏标签已换成 2 字符(ZH / EN),选择器只需容纳标签 + 箭头 + 内距 */
821
+ .lang-select {
822
+ width: 76px;
823
+ }
824
+
825
+ .lang-select :deep(.ant-select-selection-item) {
826
+ padding-inline-end: 16px;
827
+ font-size: 12.5px;
828
+ }
829
+
830
+ .header-left,
831
+ .header-right {
832
+ gap: 4px;
833
+ }
834
+ }
835
+
836
+ .hidden {
837
+ display: none;
838
+ }
839
+
840
+ /* 实时连接状态点(诚实在线:open=绿/syncing=琥珀呼吸/closed=红;reduced-motion 全局收敛) */
841
+ .ws-dot {
842
+ flex: none;
843
+ width: 8px;
844
+ height: 8px;
845
+ margin: 0 2px;
846
+ border-radius: 50%;
847
+ background: var(--tone-success-dot);
848
+ box-shadow: 0 0 6px color-mix(in srgb, var(--tone-success-dot) 55%, transparent);
849
+ }
850
+ .ws-dot.syncing {
851
+ background: var(--tone-warning-dot);
852
+ box-shadow: 0 0 6px color-mix(in srgb, var(--tone-warning-dot) 55%, transparent);
853
+ animation: ws-pulse 1.2s ease-in-out infinite;
854
+ }
855
+ .ws-dot.down {
856
+ background: var(--tone-danger-dot);
857
+ box-shadow: 0 0 6px color-mix(in srgb, var(--tone-danger-dot) 55%, transparent);
858
+ }
859
+ @keyframes ws-pulse {
860
+ 50% { opacity: 0.45; }
861
+ }
862
+
863
+ /* ── HITL 待办铃标(琥珀警示;角标数字极简,无装饰堆砌) ── */
864
+ .hitl-bell {
865
+ position: relative;
866
+ color: var(--tone-warning-dot, #d4a017);
867
+ }
868
+ .hitl-count {
869
+ position: absolute;
870
+ top: 2px;
871
+ right: 2px;
872
+ min-width: 15px;
873
+ height: 15px;
874
+ padding: 0 4px;
875
+ font-family: var(--font-mono);
876
+ font-size: 9.5px;
877
+ font-weight: 700;
878
+ line-height: 15px;
879
+ color: var(--on-accent, #fff);
880
+ text-align: center;
881
+ background: var(--tone-danger-dot, #e05252);
882
+ border-radius: 8px;
883
+ }
884
+ .hitl-menu {
885
+ min-width: 260px;
886
+ max-width: 340px;
887
+ padding: 6px;
888
+ background: var(--paper, #fff);
889
+ border: 1px solid var(--line);
890
+ border-radius: var(--radius-panel-sm, 10px);
891
+ box-shadow: 0 10px 32px rgba(0, 0, 0, 0.14);
892
+ }
893
+ .hitl-menu-title {
894
+ padding: 6px 10px;
895
+ font-size: 11px;
896
+ font-weight: 600;
897
+ letter-spacing: 0.08em;
898
+ color: var(--ink-faint);
899
+ border-bottom: 1px solid var(--line);
900
+ }
901
+ .hitl-item {
902
+ display: flex;
903
+ flex-direction: column;
904
+ gap: 3px;
905
+ width: 100%;
906
+ margin-top: 4px;
907
+ padding: 8px 10px;
908
+ text-align: left;
909
+ cursor: pointer;
910
+ background: transparent;
911
+ border: 1px solid transparent;
912
+ border-radius: var(--radius-panel-sm, 8px);
913
+ transition: background var(--transition-fast), border-color var(--transition-fast);
914
+ }
915
+ .hitl-item:hover {
916
+ background: var(--paper-deep);
917
+ border-color: var(--line);
918
+ }
919
+ .hitl-item-top {
920
+ display: flex;
921
+ gap: 8px;
922
+ align-items: baseline;
923
+ justify-content: space-between;
924
+ }
925
+ .hitl-item-agent {
926
+ font-size: 12.5px;
927
+ font-weight: 600;
928
+ color: var(--ink);
929
+ }
930
+ .hitl-item-kind {
931
+ font-family: var(--font-mono);
932
+ font-size: 9.5px;
933
+ letter-spacing: 0.1em;
934
+ color: var(--tone-warning-dot, #d4a017);
935
+ }
936
+ .hitl-item-title {
937
+ overflow: hidden;
938
+ font-size: 12px;
939
+ color: var(--ink-soft);
940
+ text-overflow: ellipsis;
941
+ white-space: nowrap;
942
+ }
943
+ .hitl-item-actions {
944
+ display: flex;
945
+ gap: 6px;
946
+ margin-top: 6px;
947
+ }
948
+
949
+ @media (prefers-reduced-motion: reduce) {
950
+ .plotter-line.run {
951
+ animation: none;
952
+ opacity: 0;
953
+ }
954
+
955
+ .stamp-enter-active,
956
+ .stamp-leave-active,
957
+ .stamp-move {
958
+ transition: none;
959
+ }
960
+ }
961
+ </style>