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,601 +1,601 @@
1
- <script setup lang="ts">
2
- /**
3
- * Transcript 时间线:过滤条(全部/消息/任务/错误)+ cluster 块流 + 自动吸底。
4
- * 事件经 useClusteredBlocks 增量聚类(turn block,内容智能去重),
5
- * 流式更新只命中变化的块组件;新帧到达保持吸底。
6
- * 历史窗口:默认最近 200 帧(loadHistory);顶部"加载更早"按 beforeSeq 向上翻页。
7
- */
8
- import { useEventsStore, type EventFilter } from '@/app/stores/workshop/events'
9
- import { useClusteredBlocks } from '@/app/composables/workshop/useClusteredBlocks'
10
- import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
11
- import { useCodeCopy } from '@/app/composables/useCodeCopy'
12
-
13
- const { t } = useI18n()
14
-
15
- const props = defineProps<{ channelId: string }>()
16
- const events = useEventsStore()
17
- const { conn } = useWorkshopWs()
18
- // 代码块复制事件委托(文档级单例;时间线内所有 .code-copy 通用)
19
- useCodeCopy()
20
- const scroller = ref<HTMLElement | null>(null)
21
- const stickBottom = ref(true)
22
-
23
- /** 连接中骨架:WS 连接中且尚无块 → 按 block 行形态的 shimmer(open-tag skel 声部) */
24
- const connecting = computed(() =>
25
- conn.state === 'connecting' && events.ring(props.channelId).items.length === 0)
26
-
27
- const filter = computed({
28
- get: () => events.filters[props.channelId] ?? 'all',
29
- set: (v: EventFilter) => events.setFilter(props.channelId, v),
30
- })
31
-
32
- const { blocks, totalEvents } = useClusteredBlocks(() => props.channelId)
33
-
34
- // ===== 向上翻页历史 =====
35
- const loadingEarlier = ref(false)
36
- const earlierExhausted = ref(false)
37
- /** 已加载事件数 vs 持久化总量 → 是否可能还有更早(粗判,点击时由 loadEarlier 探底) */
38
- const loadedCount = computed(() => events.ring(props.channelId).items.length)
39
- const maybeMore = computed(() => !earlierExhausted.value && loadedCount.value > 0)
40
- const loadEarlier = async (): Promise<void> => {
41
- if (loadingEarlier.value || earlierExhausted.value) return
42
- loadingEarlier.value = true
43
- // 记住滚动位置:头部插入后保持视口锚定(不吸底跳变)
44
- const el = scroller.value
45
- const anchor = el ? { top: el.scrollTop, height: el.scrollHeight } : null
46
- try {
47
- const hasMore = await events.loadEarlier(props.channelId)
48
- if (!hasMore) earlierExhausted.value = true
49
- if (el && anchor) {
50
- await nextTick()
51
- el.scrollTop = el.scrollHeight - anchor.height + anchor.top
52
- }
53
- }
54
- catch {
55
- /* 历史拉取失败:保持按钮可重试,不打断时间线 */
56
- }
57
- finally {
58
- loadingEarlier.value = false
59
- }
60
- }
61
-
62
- const onScroll = (): void => {
63
- const el = scroller.value
64
- if (!el) return
65
- stickBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < 120
66
- }
67
-
68
- // ===== 回底动画(open-tag jump-bottom:800ms ease-out,动画期抑制按钮闪烁) =====
69
- const scrollingDown = ref(false)
70
- /** 结构化滚动容器(绕开 vue-tsc 双 lib.dom 的 Element 类型不兼容) */
71
- type ScrollBox = { scrollTop: number, scrollHeight: number, clientHeight: number }
72
- const animateScroll = (el: ScrollBox, ms: number): Promise<void> =>
73
- new Promise((resolve) => {
74
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
75
- el.scrollTop = el.scrollHeight
76
- resolve()
77
- return
78
- }
79
- const from = el.scrollTop
80
- const delta = el.scrollHeight - el.clientHeight - from
81
- const t0 = performance.now()
82
- const step = (t: number): void => {
83
- const k = Math.min(1, (t - t0) / ms)
84
- const eased = 1 - (1 - k) ** 3 // ease-out cubic
85
- el.scrollTop = from + delta * eased
86
- if (k < 1) requestAnimationFrame(step)
87
- else resolve()
88
- }
89
- requestAnimationFrame(step)
90
- })
91
-
92
- /** 跳回最新:滚底并恢复吸底(用户离开底部后新内容不再自动跟随) */
93
- const jumpToLatest = async (): Promise<void> => {
94
- stickBottom.value = true
95
- const el = scroller.value
96
- if (!el) return
97
- scrollingDown.value = true
98
- try {
99
- await animateScroll(el, 800)
100
- }
101
- finally {
102
- scrollingDown.value = false
103
- }
104
- }
105
-
106
- // ===== 新块进场编排(open-tag motion charter 移植) =====
107
- /**
108
- * 只有实时新到的尾部块做进场动画(60ms stagger,600ms burst 窗口,上限 8 条);
109
- * 整体重建(过滤切换/聚焦/历史回填——一次出现大量新 id)与组件首载直接显示,
110
- * 不动画不延迟。映射:blockId → 进场延迟 ms;-1 = 不动画。
111
- */
112
- type Stage = { enter: boolean, delay: number }
113
- const staged = ref(new Map<string, Stage>())
114
- let burstAt = 0
115
- let burstN = 0
116
- const mountedAt = Date.now()
117
- watch(blocks, (list, prev) => {
118
- const prevIds = new Set((prev ?? []).map(b => b.id))
119
- const fresh = list.filter(b => !prevIds.has(b.id))
120
- const map = new Map<string, Stage>()
121
- const wholesale = fresh.length === 0
122
- || fresh.length > Math.max(8, Math.ceil(list.length * 0.4))
123
- || Date.now() - mountedAt < 1500
124
- if (!wholesale) {
125
- const now = Date.now()
126
- if (now - burstAt > 600) {
127
- burstAt = now
128
- burstN = 0
129
- }
130
- }
131
- for (const b of list) {
132
- const isFresh = !prevIds.has(b.id)
133
- const delay = isFresh && !wholesale ? Math.min(burstN++, 7) * 60 : -1
134
- map.set(b.id, { enter: delay >= 0, delay: Math.max(0, delay) })
135
- }
136
- staged.value = map
137
- }, { immediate: true })
138
-
139
- /** 吸底:新块出现 / seq 增长 / 块内容尺寸变化(可能高增)后滚到底 */
140
- const scrollToBottom = async (): Promise<void> => {
141
- if (!stickBottom.value) return
142
- await nextTick()
143
- const el = scroller.value
144
- if (el) el.scrollTop = el.scrollHeight
145
- }
146
- watch(() => blocks.value.length, () => {
147
- void scrollToBottom()
148
- })
149
- watch(() => events.lastSeq(props.channelId), () => {
150
- void scrollToBottom()
151
- })
152
-
153
- // 内容高度增长吸底:同一流块 delta 打字机让块变高(块数与 seq 不变或合并帧不推 seq),
154
- // ResizeObserver 观测内容列高度变化补一次滚底 —— 长输出不再"卡"在中途。
155
- // (观察器以结构化类型声明:绕开 vue-tsc 双 lib.dom 下 Element 类型不兼容问题)
156
- const columnEl = ref<HTMLElement | null>(null)
157
- let contentObserver: { disconnect(): void } | null = null
158
- onMounted(() => {
159
- const el = scroller.value
160
- const column = columnEl.value
161
- if (!el || !column || typeof ResizeObserver === 'undefined') return
162
- const Observer = ResizeObserver as unknown as
163
- new (cb: () => void) => { observe(target: unknown): void, disconnect(): void }
164
- const observer = new Observer(() => {
165
- if (stickBottom.value) el.scrollTop = el.scrollHeight
166
- })
167
- observer.observe(column)
168
- contentObserver = observer
169
- })
170
- onBeforeUnmount(() => {
171
- contentObserver?.disconnect()
172
- contentObserver = null
173
- })
174
-
175
- /** 断线待对齐(诚实连接态:open-tag 规范——不假装在线,提示同步中) */
176
- const syncing = computed(() => conn.pendingReplay || conn.state === 'connecting')
177
- /** 最后数据时间(x 秒前;无数据返回空) */
178
- const lastDataAgo = computed(() => {
179
- if (!conn.lastDataAt) return ''
180
- const s = Math.max(0, Math.round((Date.now() - conn.lastDataAt) / 1000))
181
- return s < 5 ? t('transcriptTimeline.k3wwxl026') : t('transcriptTimeline.k2gyl6l027', { p0: s })
182
- })
183
-
184
- const filterOptions: Array<{ value: EventFilter, label: string }> = [
185
- { value: 'all', label: t('transcriptTimeline.k3x4t1012') },
186
- { value: 'key', label: t('transcriptTimeline.k3x5xi013') },
187
- { value: 'messages', label: t('transcriptTimeline.k41ykc014') },
188
- { value: 'tasks', label: t('transcriptTimeline.k3wcox015') },
189
- { value: 'team', label: t('transcriptTimeline.k3y5ja016') },
190
- { value: 'errors', label: t('transcriptTimeline.k49d2l017') },
191
- ]
192
-
193
- // ===== 日期分隔线(open-tag date-divider 移植):块 firstAt 跨日 → 插入 hairline 分隔 =====
194
- const startOfDay = (iso: string): number => {
195
- const d = new Date(iso)
196
- return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
197
- }
198
- const dayLabel = (iso: string): string => {
199
- const d = new Date(iso)
200
- const today = new Date()
201
- const yesterday = new Date()
202
- yesterday.setDate(today.getDate() - 1)
203
- const sameDay = (a: Date, b: Date) =>
204
- a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
205
- if (sameDay(d, today)) return t('transcriptTimeline.k3wcqg018')
206
- if (sameDay(d, yesterday)) return t('transcriptTimeline.k40l1y019')
207
- const y = d.getFullYear() !== today.getFullYear() ? t('transcriptTimeline.k2hda35028', { p0: d.getFullYear() }) : ''
208
- return t('transcriptTimeline.kbs29zx029', { p0: y, p1: d.getMonth() + 1, p2: d.getDate() })
209
- }
210
- /** 每个块是否需要前置日界分隔(与上一块不同日,或首块) */
211
- const blockDayFlags = computed(() => {
212
- const flags: Array<{ divider: string | null }> = []
213
- let prevDay: number | null = null
214
- for (const b of blocks.value) {
215
- const day = startOfDay(b.firstAt)
216
- flags.push({ divider: prevDay === null || day !== prevDay ? dayLabel(b.firstAt) : null })
217
- prevDay = day
218
- }
219
- return flags
220
- })
221
- </script>
222
-
223
- <template>
224
- <div class="transcript">
225
- <div class="filter-bar">
226
- <a-segmented
227
- v-model:value="filter"
228
- size="small"
229
- :options="filterOptions"
230
- />
231
- <span class="count">{{ totalEvents }} {{ $t('transcriptTimeline.k1atjpx1005') }} {{ blocks.length }} {{ $t('transcriptTimeline.k4a9o007') }}</span>
232
- <!-- 连接诚实态:断线待对齐 → 同步中脉搏;在线 → 最后数据时间(open-tag 规范) -->
233
- <span
234
- class="sync-chip"
235
- :data-state="syncing ? 'syncing' : conn.state"
236
- :title="syncing ? '连接中断,重连后将自动对齐缺失事件' : $t('transcriptTimeline.kglov3b025', { p0: lastDataAgo || '无' })"
237
- >
238
- <span
239
- v-if="syncing"
240
- class="sync-pulse"
241
- />
242
- <span
243
- v-else-if="conn.state === 'open'"
244
- class="i-tabler-point-filled sync-dot"
245
- />
246
- {{ syncing ? $t('transcriptTimeline.k3lmtk3008') : conn.state === 'open' ? lastDataAgo || $t('transcriptTimeline.k3y2p8020') : $t('transcriptTimeline.k44c2n024') }}
247
- </span>
248
- </div>
249
- <div
250
- ref="scroller"
251
- class="scroller"
252
- @scroll="onScroll"
253
- >
254
- <!-- 离底时的跳转最新悬浮按钮(流式新内容到达不强制跟随,点击回底;动画期抑制闪烁) -->
255
- <button
256
- v-if="!stickBottom && !scrollingDown"
257
- class="jump-latest"
258
- :title="$t('transcriptTimeline.kqu93z8001')"
259
- @click="jumpToLatest"
260
- >
261
- <span class="i-tabler-arrow-down" />
262
- {{ $t('transcriptTimeline.k40t11002') }}
263
- </button>
264
- <div
265
- ref="columnEl"
266
- class="column"
267
- >
268
- <button
269
- v-if="maybeMore"
270
- class="earlier-btn"
271
- :disabled="loadingEarlier"
272
- @click="loadEarlier"
273
- >
274
- {{ loadingEarlier ? $t('transcriptTimeline.k1br0ij9009') : $t('transcriptTimeline.kywgd38021') }}
275
- </button>
276
- <div
277
- v-if="earlierExhausted && loadedCount > 0"
278
- class="earlier-done"
279
- >
280
- {{ $t('transcriptTimeline.k1kbaden003') }}
281
- </div>
282
- <!-- 连接中骨架:头像圆 + 双行占位,布局对齐最终块形态(免跳变) -->
283
- <div
284
- v-if="connecting"
285
- class="skel-stack"
286
- >
287
- <div
288
- v-for="n in 3"
289
- :key="n"
290
- class="skel-row"
291
- :style="{ '--d': `${(n - 1) * 0.12}s` }"
292
- >
293
- <span class="aw-skel skel-ava" />
294
- <div class="skel-lines">
295
- <span class="aw-skel skel-line name" />
296
- <span class="aw-skel skel-line w70" />
297
- <span class="aw-skel skel-line w45" />
298
- </div>
299
- </div>
300
- </div>
301
- <div
302
- v-else-if="blocks.length === 0 && totalEvents === 0"
303
- class="empty"
304
- >
305
- <!-- 同步中 ≠ 空:诚实区分(快照未到时不断言"无事件") -->
306
- <span
307
- v-if="syncing || conn.state !== 'open'"
308
- class="i-tabler-refresh empty-icon"
309
- />
310
- <span
311
- v-else
312
- class="i-tabler-message-dots empty-icon"
313
- />
314
- <p class="empty-title">
315
- {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.klnne6o010') : $t('transcriptTimeline.k126izdm022') }}
316
- </p>
317
- <p class="empty-hint">
318
- {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.k1iuqpxj011') : $t('transcriptTimeline.kuyjpmi023') }}
319
- </p>
320
- </div>
321
- <!-- 过滤空态:有事件但当前过滤无匹配(区别于真空) -->
322
- <div
323
- v-else-if="blocks.length === 0"
324
- class="empty filtered"
325
- >
326
- <span class="i-tabler-filter-off empty-icon" />
327
- <p class="empty-title">
328
- {{ $t('transcriptTimeline.ktuo1zg004') }}
329
- </p>
330
- <p class="empty-hint">
331
- 「{{ filterOptions.find(o => o.value === filter)?.label ?? filter }}」{{ $t('transcriptTimeline.kapxgq4006') }}
332
- </p>
333
- </div>
334
- <template
335
- v-for="(b, i) in blocks"
336
- :key="b.id"
337
- >
338
- <div
339
- v-if="blockDayFlags[i]?.divider"
340
- class="date-divider"
341
- >
342
- <span class="date-divider-label">{{ blockDayFlags[i]!.divider }}</span>
343
- </div>
344
- <workshop-event-block
345
- :block="b"
346
- :turn-start="i > 0 && blocks[i - 1]!.agentId !== b.agentId"
347
- :compact="i > 0 && blocks[i - 1]!.agentId === b.agentId"
348
- :enter-stage="staged.get(b.id)"
349
- />
350
- </template>
351
- </div>
352
- </div>
353
- </div>
354
- </template>
355
-
356
- <style scoped>
357
- .transcript {
358
- display: flex;
359
- flex-direction: column;
360
- height: 100%;
361
- min-height: 0;
362
- background: var(--paper); /* 灰画布:消息气泡白卡在此浮出(Slack 声部) */
363
- }
364
- .filter-bar {
365
- display: flex;
366
- gap: 10px;
367
- align-items: center;
368
- padding: 6px 16px;
369
- background: var(--paper-raised);
370
- border-bottom: 1px solid var(--line);
371
- }
372
- .count {
373
- font-size: 11px;
374
- font-family: var(--font-mono);
375
- font-variant-numeric: tabular-nums;
376
- color: var(--ink-faint);
377
- }
378
- /* 连接诚实态 chip:同步中琥珀脉搏 / 在线绿点+最后数据时间 / 离线灰 */
379
- .sync-chip {
380
- display: inline-flex;
381
- gap: 5px;
382
- align-items: center;
383
- margin-left: auto;
384
- padding: 1px 8px;
385
- font-family: var(--font-mono);
386
- font-size: 9.5px;
387
- font-variant-numeric: tabular-nums;
388
- color: var(--ink-faint);
389
- background: var(--paper-deep);
390
- border-radius: var(--radius-pill);
391
- }
392
- .sync-chip[data-state='syncing'] {
393
- color: var(--tone-warning-dot);
394
- }
395
- .sync-chip[data-state='open'] .sync-dot {
396
- font-size: 7px;
397
- color: var(--tone-success-dot);
398
- }
399
- .sync-pulse {
400
- width: 6px;
401
- height: 6px;
402
- background: var(--tone-warning-dot);
403
- border-radius: 50%;
404
- animation: sync-breath 1.2s ease-in-out infinite;
405
- }
406
- @keyframes sync-breath {
407
- 0%, 100% { opacity: 0.35; transform: scale(0.85); }
408
- 50% { opacity: 1; transform: scale(1); }
409
- }
410
- @media (prefers-reduced-motion: reduce) {
411
- .sync-pulse { animation: none; opacity: 0.8; }
412
- }
413
- .jump-latest {
414
- position: sticky;
415
- bottom: 12px;
416
- z-index: 5;
417
- display: inline-flex;
418
- gap: 5px;
419
- align-items: center;
420
- float: right;
421
- margin-right: 14px;
422
- padding: 5px 12px;
423
- font-family: var(--font-body);
424
- font-size: 11px;
425
- color: var(--on-accent);
426
- cursor: pointer;
427
- background: var(--accent);
428
- border: none;
429
- border-radius: var(--radius-pill);
430
- box-shadow: var(--shadow-float);
431
- transition: opacity var(--transition-fast), transform var(--transition-fast);
432
- }
433
-
434
- .jump-latest:hover { opacity: 0.92; }
435
- .jump-latest:active { transform: translateY(1px); }
436
-
437
- .scroller {
438
- overscroll-behavior: contain;
439
- flex: 1 1 auto;
440
- min-height: 0;
441
- overflow-y: auto;
442
- }
443
- /* Codex 式阅读列:宽屏下内容居中,阅读行宽 ~900px;首载轻淡入 */
444
- .column {
445
- max-width: 900px;
446
- padding: 10px 4px 24px;
447
- margin: 0 auto;
448
- animation: column-in 0.28s ease-out;
449
- }
450
- @keyframes column-in {
451
- from { opacity: 0; }
452
- to { opacity: 1; }
453
- }
454
- .earlier-btn {
455
- display: block;
456
- margin: 0 auto 10px;
457
- padding: 4px 14px;
458
- font-family: var(--font-body);
459
- font-size: 11px;
460
- color: var(--ink-faint);
461
- cursor: pointer;
462
- background: var(--paper-raised);
463
- border: 1px solid var(--line-strong);
464
- border-radius: var(--radius-pill);
465
- transition: background var(--transition-fast), color var(--transition-fast);
466
- }
467
- .earlier-btn:hover:not(:disabled) {
468
- color: var(--ink);
469
- background: var(--paper-deep);
470
- }
471
- .earlier-btn:disabled { opacity: 0.5; cursor: default; }
472
- .earlier-done {
473
- margin-bottom: 8px;
474
- font-family: var(--font-mono);
475
- font-size: 9.5px;
476
- text-align: center;
477
- color: var(--ink-faint);
478
- }
479
- /* 空态:编辑部式 serif 标题(pane-empty 声部) */
480
- .empty {
481
- display: flex;
482
- flex-direction: column;
483
- gap: 10px;
484
- align-items: center;
485
- padding: 72px 16px;
486
- text-align: center;
487
- }
488
- .empty-icon {
489
- font-size: 28px;
490
- color: var(--ink-faint);
491
- }
492
- .empty-title {
493
- margin: 0;
494
- font-family: var(--font-display);
495
- font-size: 22px;
496
- font-weight: 400;
497
- letter-spacing: -0.01em;
498
- color: var(--ink-soft);
499
- }
500
- .empty-hint {
501
- margin: 0;
502
- font-size: 12px;
503
- color: var(--ink-faint);
504
- }
505
-
506
- /* 连接中骨架:对齐块行网格(26px 头像列 + 内容列),行间延迟入场 */
507
- .skel-stack {
508
- display: flex;
509
- flex-direction: column;
510
- gap: 18px;
511
- padding: 26px 6px;
512
- }
513
-
514
- .skel-row {
515
- display: grid;
516
- grid-template-columns: 26px minmax(0, 1fr);
517
- gap: 10px;
518
- animation: aw-rise 0.4s cubic-bezier(0.22, 0.68, 0.36, 1) backwards;
519
- animation-delay: var(--d, 0s);
520
- }
521
-
522
- .skel-ava {
523
- width: 26px;
524
- height: 26px;
525
- border-radius: 50%;
526
- }
527
-
528
- .skel-lines {
529
- display: flex;
530
- flex-direction: column;
531
- gap: 6px;
532
- }
533
-
534
- .skel-line {
535
- height: 11px;
536
- }
537
-
538
- .skel-line.name { width: 120px; height: 13px; }
539
- .skel-line.w70 { width: 70%; }
540
- .skel-line.w45 { width: 45%; }
541
-
542
- @media (prefers-reduced-motion: reduce) {
543
- .skel-row { animation: none; }
544
- }
545
-
546
- /* ── 窄屏(≤1023):阅读列收边 + 富文本/代码/终端块横向滚动而非顶破视口 ──
547
- 消息体由 workshop-event-block(及其内部的 prose/code-block)渲染,
548
- 这里用 :deep 给它们一条自己的卷轴:代码不折行(折行会毁掉缩进语义),
549
- 改为在自己块内横扫;图片/画布一律不超列宽。 */
550
- @media (max-width: 1023.98px) {
551
- .filter-bar {
552
- flex-wrap: wrap;
553
- gap: 8px;
554
- padding: 6px 10px;
555
- }
556
-
557
- .filter-bar :deep(.ant-segmented) {
558
- flex: 1 1 100%;
559
- }
560
-
561
- .count,
562
- .sync-chip,
563
- .earlier-done {
564
- font-size: 11.5px;
565
- }
566
-
567
- .column {
568
- padding: 8px 8px 22px;
569
- }
570
-
571
- .column :deep(pre),
572
- .column :deep(table),
573
- .column :deep(.code-block),
574
- .column :deep(.st-text) {
575
- max-width: 100%;
576
- overflow-x: auto;
577
- -webkit-overflow-scrolling: touch;
578
- }
579
-
580
- .column :deep(img),
581
- .column :deep(video),
582
- .column :deep(canvas) {
583
- max-width: 100%;
584
- height: auto;
585
- }
586
-
587
- .empty-hint {
588
- font-size: 13px;
589
- }
590
-
591
- .earlier-btn,
592
- .jump-latest {
593
- min-height: 40px;
594
- }
595
-
596
- .jump-latest {
597
- padding: 8px 14px;
598
- font-size: 12px;
599
- }
600
- }
601
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Transcript 时间线:过滤条(全部/消息/任务/错误)+ cluster 块流 + 自动吸底。
4
+ * 事件经 useClusteredBlocks 增量聚类(turn block,内容智能去重),
5
+ * 流式更新只命中变化的块组件;新帧到达保持吸底。
6
+ * 历史窗口:默认最近 200 帧(loadHistory);顶部"加载更早"按 beforeSeq 向上翻页。
7
+ */
8
+ import { useEventsStore, type EventFilter } from '@/app/stores/workshop/events'
9
+ import { useClusteredBlocks } from '@/app/composables/workshop/useClusteredBlocks'
10
+ import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
11
+ import { useCodeCopy } from '@/app/composables/useCodeCopy'
12
+
13
+ const { t } = useI18n()
14
+
15
+ const props = defineProps<{ channelId: string }>()
16
+ const events = useEventsStore()
17
+ const { conn } = useWorkshopWs()
18
+ // 代码块复制事件委托(文档级单例;时间线内所有 .code-copy 通用)
19
+ useCodeCopy()
20
+ const scroller = ref<HTMLElement | null>(null)
21
+ const stickBottom = ref(true)
22
+
23
+ /** 连接中骨架:WS 连接中且尚无块 → 按 block 行形态的 shimmer(open-tag skel 声部) */
24
+ const connecting = computed(() =>
25
+ conn.state === 'connecting' && events.ring(props.channelId).items.length === 0)
26
+
27
+ const filter = computed({
28
+ get: () => events.filters[props.channelId] ?? 'all',
29
+ set: (v: EventFilter) => events.setFilter(props.channelId, v),
30
+ })
31
+
32
+ const { blocks, totalEvents } = useClusteredBlocks(() => props.channelId)
33
+
34
+ // ===== 向上翻页历史 =====
35
+ const loadingEarlier = ref(false)
36
+ const earlierExhausted = ref(false)
37
+ /** 已加载事件数 vs 持久化总量 → 是否可能还有更早(粗判,点击时由 loadEarlier 探底) */
38
+ const loadedCount = computed(() => events.ring(props.channelId).items.length)
39
+ const maybeMore = computed(() => !earlierExhausted.value && loadedCount.value > 0)
40
+ const loadEarlier = async (): Promise<void> => {
41
+ if (loadingEarlier.value || earlierExhausted.value) return
42
+ loadingEarlier.value = true
43
+ // 记住滚动位置:头部插入后保持视口锚定(不吸底跳变)
44
+ const el = scroller.value
45
+ const anchor = el ? { top: el.scrollTop, height: el.scrollHeight } : null
46
+ try {
47
+ const hasMore = await events.loadEarlier(props.channelId)
48
+ if (!hasMore) earlierExhausted.value = true
49
+ if (el && anchor) {
50
+ await nextTick()
51
+ el.scrollTop = el.scrollHeight - anchor.height + anchor.top
52
+ }
53
+ }
54
+ catch {
55
+ /* 历史拉取失败:保持按钮可重试,不打断时间线 */
56
+ }
57
+ finally {
58
+ loadingEarlier.value = false
59
+ }
60
+ }
61
+
62
+ const onScroll = (): void => {
63
+ const el = scroller.value
64
+ if (!el) return
65
+ stickBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < 120
66
+ }
67
+
68
+ // ===== 回底动画(open-tag jump-bottom:800ms ease-out,动画期抑制按钮闪烁) =====
69
+ const scrollingDown = ref(false)
70
+ /** 结构化滚动容器(绕开 vue-tsc 双 lib.dom 的 Element 类型不兼容) */
71
+ type ScrollBox = { scrollTop: number, scrollHeight: number, clientHeight: number }
72
+ const animateScroll = (el: ScrollBox, ms: number): Promise<void> =>
73
+ new Promise((resolve) => {
74
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
75
+ el.scrollTop = el.scrollHeight
76
+ resolve()
77
+ return
78
+ }
79
+ const from = el.scrollTop
80
+ const delta = el.scrollHeight - el.clientHeight - from
81
+ const t0 = performance.now()
82
+ const step = (t: number): void => {
83
+ const k = Math.min(1, (t - t0) / ms)
84
+ const eased = 1 - (1 - k) ** 3 // ease-out cubic
85
+ el.scrollTop = from + delta * eased
86
+ if (k < 1) requestAnimationFrame(step)
87
+ else resolve()
88
+ }
89
+ requestAnimationFrame(step)
90
+ })
91
+
92
+ /** 跳回最新:滚底并恢复吸底(用户离开底部后新内容不再自动跟随) */
93
+ const jumpToLatest = async (): Promise<void> => {
94
+ stickBottom.value = true
95
+ const el = scroller.value
96
+ if (!el) return
97
+ scrollingDown.value = true
98
+ try {
99
+ await animateScroll(el, 800)
100
+ }
101
+ finally {
102
+ scrollingDown.value = false
103
+ }
104
+ }
105
+
106
+ // ===== 新块进场编排(open-tag motion charter 移植) =====
107
+ /**
108
+ * 只有实时新到的尾部块做进场动画(60ms stagger,600ms burst 窗口,上限 8 条);
109
+ * 整体重建(过滤切换/聚焦/历史回填——一次出现大量新 id)与组件首载直接显示,
110
+ * 不动画不延迟。映射:blockId → 进场延迟 ms;-1 = 不动画。
111
+ */
112
+ type Stage = { enter: boolean, delay: number }
113
+ const staged = ref(new Map<string, Stage>())
114
+ let burstAt = 0
115
+ let burstN = 0
116
+ const mountedAt = Date.now()
117
+ watch(blocks, (list, prev) => {
118
+ const prevIds = new Set((prev ?? []).map(b => b.id))
119
+ const fresh = list.filter(b => !prevIds.has(b.id))
120
+ const map = new Map<string, Stage>()
121
+ const wholesale = fresh.length === 0
122
+ || fresh.length > Math.max(8, Math.ceil(list.length * 0.4))
123
+ || Date.now() - mountedAt < 1500
124
+ if (!wholesale) {
125
+ const now = Date.now()
126
+ if (now - burstAt > 600) {
127
+ burstAt = now
128
+ burstN = 0
129
+ }
130
+ }
131
+ for (const b of list) {
132
+ const isFresh = !prevIds.has(b.id)
133
+ const delay = isFresh && !wholesale ? Math.min(burstN++, 7) * 60 : -1
134
+ map.set(b.id, { enter: delay >= 0, delay: Math.max(0, delay) })
135
+ }
136
+ staged.value = map
137
+ }, { immediate: true })
138
+
139
+ /** 吸底:新块出现 / seq 增长 / 块内容尺寸变化(可能高增)后滚到底 */
140
+ const scrollToBottom = async (): Promise<void> => {
141
+ if (!stickBottom.value) return
142
+ await nextTick()
143
+ const el = scroller.value
144
+ if (el) el.scrollTop = el.scrollHeight
145
+ }
146
+ watch(() => blocks.value.length, () => {
147
+ void scrollToBottom()
148
+ })
149
+ watch(() => events.lastSeq(props.channelId), () => {
150
+ void scrollToBottom()
151
+ })
152
+
153
+ // 内容高度增长吸底:同一流块 delta 打字机让块变高(块数与 seq 不变或合并帧不推 seq),
154
+ // ResizeObserver 观测内容列高度变化补一次滚底 —— 长输出不再"卡"在中途。
155
+ // (观察器以结构化类型声明:绕开 vue-tsc 双 lib.dom 下 Element 类型不兼容问题)
156
+ const columnEl = ref<HTMLElement | null>(null)
157
+ let contentObserver: { disconnect(): void } | null = null
158
+ onMounted(() => {
159
+ const el = scroller.value
160
+ const column = columnEl.value
161
+ if (!el || !column || typeof ResizeObserver === 'undefined') return
162
+ const Observer = ResizeObserver as unknown as
163
+ new (cb: () => void) => { observe(target: unknown): void, disconnect(): void }
164
+ const observer = new Observer(() => {
165
+ if (stickBottom.value) el.scrollTop = el.scrollHeight
166
+ })
167
+ observer.observe(column)
168
+ contentObserver = observer
169
+ })
170
+ onBeforeUnmount(() => {
171
+ contentObserver?.disconnect()
172
+ contentObserver = null
173
+ })
174
+
175
+ /** 断线待对齐(诚实连接态:open-tag 规范——不假装在线,提示同步中) */
176
+ const syncing = computed(() => conn.pendingReplay || conn.state === 'connecting')
177
+ /** 最后数据时间(x 秒前;无数据返回空) */
178
+ const lastDataAgo = computed(() => {
179
+ if (!conn.lastDataAt) return ''
180
+ const s = Math.max(0, Math.round((Date.now() - conn.lastDataAt) / 1000))
181
+ return s < 5 ? t('transcriptTimeline.k3wwxl026') : t('transcriptTimeline.k2gyl6l027', { p0: s })
182
+ })
183
+
184
+ const filterOptions: Array<{ value: EventFilter, label: string }> = [
185
+ { value: 'all', label: t('transcriptTimeline.k3x4t1012') },
186
+ { value: 'key', label: t('transcriptTimeline.k3x5xi013') },
187
+ { value: 'messages', label: t('transcriptTimeline.k41ykc014') },
188
+ { value: 'tasks', label: t('transcriptTimeline.k3wcox015') },
189
+ { value: 'team', label: t('transcriptTimeline.k3y5ja016') },
190
+ { value: 'errors', label: t('transcriptTimeline.k49d2l017') },
191
+ ]
192
+
193
+ // ===== 日期分隔线(open-tag date-divider 移植):块 firstAt 跨日 → 插入 hairline 分隔 =====
194
+ const startOfDay = (iso: string): number => {
195
+ const d = new Date(iso)
196
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
197
+ }
198
+ const dayLabel = (iso: string): string => {
199
+ const d = new Date(iso)
200
+ const today = new Date()
201
+ const yesterday = new Date()
202
+ yesterday.setDate(today.getDate() - 1)
203
+ const sameDay = (a: Date, b: Date) =>
204
+ a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
205
+ if (sameDay(d, today)) return t('transcriptTimeline.k3wcqg018')
206
+ if (sameDay(d, yesterday)) return t('transcriptTimeline.k40l1y019')
207
+ const y = d.getFullYear() !== today.getFullYear() ? t('transcriptTimeline.k2hda35028', { p0: d.getFullYear() }) : ''
208
+ return t('transcriptTimeline.kbs29zx029', { p0: y, p1: d.getMonth() + 1, p2: d.getDate() })
209
+ }
210
+ /** 每个块是否需要前置日界分隔(与上一块不同日,或首块) */
211
+ const blockDayFlags = computed(() => {
212
+ const flags: Array<{ divider: string | null }> = []
213
+ let prevDay: number | null = null
214
+ for (const b of blocks.value) {
215
+ const day = startOfDay(b.firstAt)
216
+ flags.push({ divider: prevDay === null || day !== prevDay ? dayLabel(b.firstAt) : null })
217
+ prevDay = day
218
+ }
219
+ return flags
220
+ })
221
+ </script>
222
+
223
+ <template>
224
+ <div class="transcript">
225
+ <div class="filter-bar">
226
+ <a-segmented
227
+ v-model:value="filter"
228
+ size="small"
229
+ :options="filterOptions"
230
+ />
231
+ <span class="count">{{ totalEvents }} {{ $t('transcriptTimeline.k1atjpx1005') }} {{ blocks.length }} {{ $t('transcriptTimeline.k4a9o007') }}</span>
232
+ <!-- 连接诚实态:断线待对齐 → 同步中脉搏;在线 → 最后数据时间(open-tag 规范) -->
233
+ <span
234
+ class="sync-chip"
235
+ :data-state="syncing ? 'syncing' : conn.state"
236
+ :title="syncing ? $t('transcriptTimeline.syncLag') : $t('transcriptTimeline.kglov3b025', { p0: lastDataAgo || $t('transcriptTimeline.none') })"
237
+ >
238
+ <span
239
+ v-if="syncing"
240
+ class="sync-pulse"
241
+ />
242
+ <span
243
+ v-else-if="conn.state === 'open'"
244
+ class="i-tabler-point-filled sync-dot"
245
+ />
246
+ {{ syncing ? $t('transcriptTimeline.k3lmtk3008') : conn.state === 'open' ? lastDataAgo || $t('transcriptTimeline.k3y2p8020') : $t('transcriptTimeline.k44c2n024') }}
247
+ </span>
248
+ </div>
249
+ <div
250
+ ref="scroller"
251
+ class="scroller"
252
+ @scroll="onScroll"
253
+ >
254
+ <!-- 离底时的跳转最新悬浮按钮(流式新内容到达不强制跟随,点击回底;动画期抑制闪烁) -->
255
+ <button
256
+ v-if="!stickBottom && !scrollingDown"
257
+ class="jump-latest"
258
+ :title="$t('transcriptTimeline.kqu93z8001')"
259
+ @click="jumpToLatest"
260
+ >
261
+ <span class="i-tabler-arrow-down" />
262
+ {{ $t('transcriptTimeline.k40t11002') }}
263
+ </button>
264
+ <div
265
+ ref="columnEl"
266
+ class="column"
267
+ >
268
+ <button
269
+ v-if="maybeMore"
270
+ class="earlier-btn"
271
+ :disabled="loadingEarlier"
272
+ @click="loadEarlier"
273
+ >
274
+ {{ loadingEarlier ? $t('transcriptTimeline.k1br0ij9009') : $t('transcriptTimeline.kywgd38021') }}
275
+ </button>
276
+ <div
277
+ v-if="earlierExhausted && loadedCount > 0"
278
+ class="earlier-done"
279
+ >
280
+ {{ $t('transcriptTimeline.k1kbaden003') }}
281
+ </div>
282
+ <!-- 连接中骨架:头像圆 + 双行占位,布局对齐最终块形态(免跳变) -->
283
+ <div
284
+ v-if="connecting"
285
+ class="skel-stack"
286
+ >
287
+ <div
288
+ v-for="n in 3"
289
+ :key="n"
290
+ class="skel-row"
291
+ :style="{ '--d': `${(n - 1) * 0.12}s` }"
292
+ >
293
+ <span class="aw-skel skel-ava" />
294
+ <div class="skel-lines">
295
+ <span class="aw-skel skel-line name" />
296
+ <span class="aw-skel skel-line w70" />
297
+ <span class="aw-skel skel-line w45" />
298
+ </div>
299
+ </div>
300
+ </div>
301
+ <div
302
+ v-else-if="blocks.length === 0 && totalEvents === 0"
303
+ class="empty"
304
+ >
305
+ <!-- 同步中 ≠ 空:诚实区分(快照未到时不断言"无事件") -->
306
+ <span
307
+ v-if="syncing || conn.state !== 'open'"
308
+ class="i-tabler-refresh empty-icon"
309
+ />
310
+ <span
311
+ v-else
312
+ class="i-tabler-message-dots empty-icon"
313
+ />
314
+ <p class="empty-title">
315
+ {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.klnne6o010') : $t('transcriptTimeline.k126izdm022') }}
316
+ </p>
317
+ <p class="empty-hint">
318
+ {{ syncing || conn.state !== 'open' ? $t('transcriptTimeline.k1iuqpxj011') : $t('transcriptTimeline.kuyjpmi023') }}
319
+ </p>
320
+ </div>
321
+ <!-- 过滤空态:有事件但当前过滤无匹配(区别于真空) -->
322
+ <div
323
+ v-else-if="blocks.length === 0"
324
+ class="empty filtered"
325
+ >
326
+ <span class="i-tabler-filter-off empty-icon" />
327
+ <p class="empty-title">
328
+ {{ $t('transcriptTimeline.ktuo1zg004') }}
329
+ </p>
330
+ <p class="empty-hint">
331
+ 「{{ filterOptions.find(o => o.value === filter)?.label ?? filter }}」{{ $t('transcriptTimeline.kapxgq4006') }}
332
+ </p>
333
+ </div>
334
+ <template
335
+ v-for="(b, i) in blocks"
336
+ :key="b.id"
337
+ >
338
+ <div
339
+ v-if="blockDayFlags[i]?.divider"
340
+ class="date-divider"
341
+ >
342
+ <span class="date-divider-label">{{ blockDayFlags[i]!.divider }}</span>
343
+ </div>
344
+ <workshop-event-block
345
+ :block="b"
346
+ :turn-start="i > 0 && blocks[i - 1]!.agentId !== b.agentId"
347
+ :compact="i > 0 && blocks[i - 1]!.agentId === b.agentId"
348
+ :enter-stage="staged.get(b.id)"
349
+ />
350
+ </template>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </template>
355
+
356
+ <style scoped>
357
+ .transcript {
358
+ display: flex;
359
+ flex-direction: column;
360
+ height: 100%;
361
+ min-height: 0;
362
+ background: var(--paper); /* 灰画布:消息气泡白卡在此浮出(Slack 声部) */
363
+ }
364
+ .filter-bar {
365
+ display: flex;
366
+ gap: 10px;
367
+ align-items: center;
368
+ padding: 6px 16px;
369
+ background: var(--paper-raised);
370
+ border-bottom: 1px solid var(--line);
371
+ }
372
+ .count {
373
+ font-size: 11px;
374
+ font-family: var(--font-mono);
375
+ font-variant-numeric: tabular-nums;
376
+ color: var(--ink-faint);
377
+ }
378
+ /* 连接诚实态 chip:同步中琥珀脉搏 / 在线绿点+最后数据时间 / 离线灰 */
379
+ .sync-chip {
380
+ display: inline-flex;
381
+ gap: 5px;
382
+ align-items: center;
383
+ margin-left: auto;
384
+ padding: 1px 8px;
385
+ font-family: var(--font-mono);
386
+ font-size: 9.5px;
387
+ font-variant-numeric: tabular-nums;
388
+ color: var(--ink-faint);
389
+ background: var(--paper-deep);
390
+ border-radius: var(--radius-pill);
391
+ }
392
+ .sync-chip[data-state='syncing'] {
393
+ color: var(--tone-warning-dot);
394
+ }
395
+ .sync-chip[data-state='open'] .sync-dot {
396
+ font-size: 7px;
397
+ color: var(--tone-success-dot);
398
+ }
399
+ .sync-pulse {
400
+ width: 6px;
401
+ height: 6px;
402
+ background: var(--tone-warning-dot);
403
+ border-radius: 50%;
404
+ animation: sync-breath 1.2s ease-in-out infinite;
405
+ }
406
+ @keyframes sync-breath {
407
+ 0%, 100% { opacity: 0.35; transform: scale(0.85); }
408
+ 50% { opacity: 1; transform: scale(1); }
409
+ }
410
+ @media (prefers-reduced-motion: reduce) {
411
+ .sync-pulse { animation: none; opacity: 0.8; }
412
+ }
413
+ .jump-latest {
414
+ position: sticky;
415
+ bottom: 12px;
416
+ z-index: 5;
417
+ display: inline-flex;
418
+ gap: 5px;
419
+ align-items: center;
420
+ float: right;
421
+ margin-right: 14px;
422
+ padding: 5px 12px;
423
+ font-family: var(--font-body);
424
+ font-size: 11px;
425
+ color: var(--on-accent);
426
+ cursor: pointer;
427
+ background: var(--accent);
428
+ border: none;
429
+ border-radius: var(--radius-pill);
430
+ box-shadow: var(--shadow-float);
431
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
432
+ }
433
+
434
+ .jump-latest:hover { opacity: 0.92; }
435
+ .jump-latest:active { transform: translateY(1px); }
436
+
437
+ .scroller {
438
+ overscroll-behavior: contain;
439
+ flex: 1 1 auto;
440
+ min-height: 0;
441
+ overflow-y: auto;
442
+ }
443
+ /* Codex 式阅读列:宽屏下内容居中,阅读行宽 ~900px;首载轻淡入 */
444
+ .column {
445
+ max-width: 900px;
446
+ padding: 10px 4px 24px;
447
+ margin: 0 auto;
448
+ animation: column-in 0.28s ease-out;
449
+ }
450
+ @keyframes column-in {
451
+ from { opacity: 0; }
452
+ to { opacity: 1; }
453
+ }
454
+ .earlier-btn {
455
+ display: block;
456
+ margin: 0 auto 10px;
457
+ padding: 4px 14px;
458
+ font-family: var(--font-body);
459
+ font-size: 11px;
460
+ color: var(--ink-faint);
461
+ cursor: pointer;
462
+ background: var(--paper-raised);
463
+ border: 1px solid var(--line-strong);
464
+ border-radius: var(--radius-pill);
465
+ transition: background var(--transition-fast), color var(--transition-fast);
466
+ }
467
+ .earlier-btn:hover:not(:disabled) {
468
+ color: var(--ink);
469
+ background: var(--paper-deep);
470
+ }
471
+ .earlier-btn:disabled { opacity: 0.5; cursor: default; }
472
+ .earlier-done {
473
+ margin-bottom: 8px;
474
+ font-family: var(--font-mono);
475
+ font-size: 9.5px;
476
+ text-align: center;
477
+ color: var(--ink-faint);
478
+ }
479
+ /* 空态:编辑部式 serif 标题(pane-empty 声部) */
480
+ .empty {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 10px;
484
+ align-items: center;
485
+ padding: 72px 16px;
486
+ text-align: center;
487
+ }
488
+ .empty-icon {
489
+ font-size: 28px;
490
+ color: var(--ink-faint);
491
+ }
492
+ .empty-title {
493
+ margin: 0;
494
+ font-family: var(--font-display);
495
+ font-size: 22px;
496
+ font-weight: 400;
497
+ letter-spacing: -0.01em;
498
+ color: var(--ink-soft);
499
+ }
500
+ .empty-hint {
501
+ margin: 0;
502
+ font-size: 12px;
503
+ color: var(--ink-faint);
504
+ }
505
+
506
+ /* 连接中骨架:对齐块行网格(26px 头像列 + 内容列),行间延迟入场 */
507
+ .skel-stack {
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 18px;
511
+ padding: 26px 6px;
512
+ }
513
+
514
+ .skel-row {
515
+ display: grid;
516
+ grid-template-columns: 26px minmax(0, 1fr);
517
+ gap: 10px;
518
+ animation: aw-rise 0.4s cubic-bezier(0.22, 0.68, 0.36, 1) backwards;
519
+ animation-delay: var(--d, 0s);
520
+ }
521
+
522
+ .skel-ava {
523
+ width: 26px;
524
+ height: 26px;
525
+ border-radius: 50%;
526
+ }
527
+
528
+ .skel-lines {
529
+ display: flex;
530
+ flex-direction: column;
531
+ gap: 6px;
532
+ }
533
+
534
+ .skel-line {
535
+ height: 11px;
536
+ }
537
+
538
+ .skel-line.name { width: 120px; height: 13px; }
539
+ .skel-line.w70 { width: 70%; }
540
+ .skel-line.w45 { width: 45%; }
541
+
542
+ @media (prefers-reduced-motion: reduce) {
543
+ .skel-row { animation: none; }
544
+ }
545
+
546
+ /* ── 窄屏(≤1023):阅读列收边 + 富文本/代码/终端块横向滚动而非顶破视口 ──
547
+ 消息体由 workshop-event-block(及其内部的 prose/code-block)渲染,
548
+ 这里用 :deep 给它们一条自己的卷轴:代码不折行(折行会毁掉缩进语义),
549
+ 改为在自己块内横扫;图片/画布一律不超列宽。 */
550
+ @media (max-width: 1023.98px) {
551
+ .filter-bar {
552
+ flex-wrap: wrap;
553
+ gap: 8px;
554
+ padding: 6px 10px;
555
+ }
556
+
557
+ .filter-bar :deep(.ant-segmented) {
558
+ flex: 1 1 100%;
559
+ }
560
+
561
+ .count,
562
+ .sync-chip,
563
+ .earlier-done {
564
+ font-size: 11.5px;
565
+ }
566
+
567
+ .column {
568
+ padding: 8px 8px 22px;
569
+ }
570
+
571
+ .column :deep(pre),
572
+ .column :deep(table),
573
+ .column :deep(.code-block),
574
+ .column :deep(.st-text) {
575
+ max-width: 100%;
576
+ overflow-x: auto;
577
+ -webkit-overflow-scrolling: touch;
578
+ }
579
+
580
+ .column :deep(img),
581
+ .column :deep(video),
582
+ .column :deep(canvas) {
583
+ max-width: 100%;
584
+ height: auto;
585
+ }
586
+
587
+ .empty-hint {
588
+ font-size: 13px;
589
+ }
590
+
591
+ .earlier-btn,
592
+ .jump-latest {
593
+ min-height: 40px;
594
+ }
595
+
596
+ .jump-latest {
597
+ padding: 8px 14px;
598
+ font-size: 12px;
599
+ }
600
+ }
601
+ </style>