dsh-better-display 0.1.1

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 (300) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +41 -0
  5. package/README.md +41 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/cordis.patch.yml +5 -0
  8. package/cordis.yml +4 -0
  9. package/dshx.yml +5 -0
  10. package/lib/client.js +49923 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/dsh-better-display.js +256 -0
  13. package/lib/types/client/Blocks.d.ts +45 -0
  14. package/lib/types/client/Blocks.d.ts.map +1 -0
  15. package/lib/types/client/Blocks.js +136 -0
  16. package/lib/types/client/Blocks.js.map +1 -0
  17. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  18. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.js +170 -0
  20. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  21. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  22. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.js +15 -0
  24. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  25. package/lib/types/client/LiveFold.d.ts +10 -0
  26. package/lib/types/client/LiveFold.d.ts.map +1 -0
  27. package/lib/types/client/LiveFold.js +39 -0
  28. package/lib/types/client/LiveFold.js.map +1 -0
  29. package/lib/types/client/McpAppFrame.d.ts +32 -0
  30. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  31. package/lib/types/client/McpAppFrame.js +193 -0
  32. package/lib/types/client/McpAppFrame.js.map +1 -0
  33. package/lib/types/client/Reader.d.ts +3 -0
  34. package/lib/types/client/Reader.d.ts.map +1 -0
  35. package/lib/types/client/Reader.js +537 -0
  36. package/lib/types/client/Reader.js.map +1 -0
  37. package/lib/types/client/ReasoningCard.d.ts +11 -0
  38. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  39. package/lib/types/client/ReasoningCard.js +301 -0
  40. package/lib/types/client/ReasoningCard.js.map +1 -0
  41. package/lib/types/client/StickyLane.d.ts +8 -0
  42. package/lib/types/client/StickyLane.d.ts.map +1 -0
  43. package/lib/types/client/StickyLane.js +28 -0
  44. package/lib/types/client/StickyLane.js.map +1 -0
  45. package/lib/types/client/TimelineRail.d.ts +10 -0
  46. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  47. package/lib/types/client/TimelineRail.js +168 -0
  48. package/lib/types/client/TimelineRail.js.map +1 -0
  49. package/lib/types/client/ToolActivity.d.ts +17 -0
  50. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  51. package/lib/types/client/ToolActivity.js +192 -0
  52. package/lib/types/client/ToolActivity.js.map +1 -0
  53. package/lib/types/client/TurnMetrics.d.ts +11 -0
  54. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  55. package/lib/types/client/TurnMetrics.js +44 -0
  56. package/lib/types/client/TurnMetrics.js.map +1 -0
  57. package/lib/types/client/WaitingStatus.d.ts +6 -0
  58. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  59. package/lib/types/client/WaitingStatus.js +19 -0
  60. package/lib/types/client/WaitingStatus.js.map +1 -0
  61. package/lib/types/client/conversation-scroll.d.ts +19 -0
  62. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  63. package/lib/types/client/conversation-scroll.js +25 -0
  64. package/lib/types/client/conversation-scroll.js.map +1 -0
  65. package/lib/types/client/deliverables.d.ts +24 -0
  66. package/lib/types/client/deliverables.d.ts.map +1 -0
  67. package/lib/types/client/deliverables.js +104 -0
  68. package/lib/types/client/deliverables.js.map +1 -0
  69. package/lib/types/client/entry-policy.d.ts +10 -0
  70. package/lib/types/client/entry-policy.d.ts.map +1 -0
  71. package/lib/types/client/entry-policy.js +24 -0
  72. package/lib/types/client/entry-policy.js.map +1 -0
  73. package/lib/types/client/entry.d.ts +4 -0
  74. package/lib/types/client/entry.d.ts.map +1 -0
  75. package/lib/types/client/entry.js +34 -0
  76. package/lib/types/client/entry.js.map +1 -0
  77. package/lib/types/client/fold-choreography.d.ts +28 -0
  78. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  79. package/lib/types/client/fold-choreography.js +31 -0
  80. package/lib/types/client/fold-choreography.js.map +1 -0
  81. package/lib/types/client/index.d.ts +7 -0
  82. package/lib/types/client/index.d.ts.map +1 -0
  83. package/lib/types/client/index.js +153 -0
  84. package/lib/types/client/index.js.map +1 -0
  85. package/lib/types/client/live-turn.d.ts +58 -0
  86. package/lib/types/client/live-turn.d.ts.map +1 -0
  87. package/lib/types/client/live-turn.js +144 -0
  88. package/lib/types/client/live-turn.js.map +1 -0
  89. package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
  90. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  91. package/lib/types/client/markdown/MarkdownText.js +171 -0
  92. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  93. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  94. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  95. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  96. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  97. package/lib/types/client/markdown/incremental.d.ts +69 -0
  98. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  99. package/lib/types/client/markdown/incremental.js +108 -0
  100. package/lib/types/client/markdown/incremental.js.map +1 -0
  101. package/lib/types/client/markdown/katex.d.ts +27 -0
  102. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  103. package/lib/types/client/markdown/katex.js +88 -0
  104. package/lib/types/client/markdown/katex.js.map +1 -0
  105. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  106. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  107. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  108. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  109. package/lib/types/client/markdown/parse.d.ts +25 -0
  110. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  111. package/lib/types/client/markdown/parse.js +42 -0
  112. package/lib/types/client/markdown/parse.js.map +1 -0
  113. package/lib/types/client/markdown/render.d.ts +119 -0
  114. package/lib/types/client/markdown/render.d.ts.map +1 -0
  115. package/lib/types/client/markdown/render.js +435 -0
  116. package/lib/types/client/markdown/render.js.map +1 -0
  117. package/lib/types/client/mcp-app.d.ts +50 -0
  118. package/lib/types/client/mcp-app.d.ts.map +1 -0
  119. package/lib/types/client/mcp-app.js +328 -0
  120. package/lib/types/client/mcp-app.js.map +1 -0
  121. package/lib/types/client/message-chrome.d.ts +19 -0
  122. package/lib/types/client/message-chrome.d.ts.map +1 -0
  123. package/lib/types/client/message-chrome.js +42 -0
  124. package/lib/types/client/message-chrome.js.map +1 -0
  125. package/lib/types/client/motion.d.ts +38 -0
  126. package/lib/types/client/motion.d.ts.map +1 -0
  127. package/lib/types/client/motion.js +367 -0
  128. package/lib/types/client/motion.js.map +1 -0
  129. package/lib/types/client/native/ContextBody.d.ts +130 -0
  130. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  131. package/lib/types/client/native/ContextBody.js +398 -0
  132. package/lib/types/client/native/ContextBody.js.map +1 -0
  133. package/lib/types/client/native/ContextInjectionRow.d.ts +26 -0
  134. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  135. package/lib/types/client/native/ContextInjectionRow.js +27 -0
  136. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  137. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  138. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  139. package/lib/types/client/native/ReferenceIcon.js +16 -0
  140. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  141. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  142. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  143. package/lib/types/client/native/tool-call-model.js +219 -0
  144. package/lib/types/client/native/tool-call-model.js.map +1 -0
  145. package/lib/types/client/open-file.d.ts +24 -0
  146. package/lib/types/client/primitive-labels.d.ts +10 -0
  147. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  148. package/lib/types/client/primitive-labels.js +37 -0
  149. package/lib/types/client/primitive-labels.js.map +1 -0
  150. package/lib/types/client/projection.d.ts +41 -0
  151. package/lib/types/client/projection.d.ts.map +1 -0
  152. package/lib/types/client/projection.js +112 -0
  153. package/lib/types/client/projection.js.map +1 -0
  154. package/lib/types/client/reasoning-follow.d.ts +6 -0
  155. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  156. package/lib/types/client/reasoning-follow.js +12 -0
  157. package/lib/types/client/reasoning-follow.js.map +1 -0
  158. package/lib/types/client/settings-copy.d.ts +6 -0
  159. package/lib/types/client/settings-slots.d.ts +16 -0
  160. package/lib/types/client/store.d.ts +17 -0
  161. package/lib/types/client/store.d.ts.map +1 -0
  162. package/lib/types/client/store.js +13 -0
  163. package/lib/types/client/store.js.map +1 -0
  164. package/lib/types/client/stream-buffer.d.ts +29 -0
  165. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  166. package/lib/types/client/stream-buffer.js +113 -0
  167. package/lib/types/client/stream-buffer.js.map +1 -0
  168. package/lib/types/client/streaming.d.ts +18 -0
  169. package/lib/types/client/streaming.d.ts.map +1 -0
  170. package/lib/types/client/streaming.js +77 -0
  171. package/lib/types/client/streaming.js.map +1 -0
  172. package/lib/types/client/timeline.d.ts +26 -0
  173. package/lib/types/client/timeline.d.ts.map +1 -0
  174. package/lib/types/client/timeline.js +48 -0
  175. package/lib/types/client/timeline.js.map +1 -0
  176. package/lib/types/client/tool-activity.d.ts +52 -0
  177. package/lib/types/client/tool-activity.d.ts.map +1 -0
  178. package/lib/types/client/tool-activity.js +178 -0
  179. package/lib/types/client/tool-activity.js.map +1 -0
  180. package/lib/types/client/types.d.ts +66 -0
  181. package/lib/types/client/types.d.ts.map +1 -0
  182. package/lib/types/client/types.js +2 -0
  183. package/lib/types/client/types.js.map +1 -0
  184. package/lib/types/client/waiting-clock.d.ts +17 -0
  185. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  186. package/lib/types/client/waiting-clock.js +24 -0
  187. package/lib/types/client/waiting-clock.js.map +1 -0
  188. package/lib/types/client/word-motion.d.ts +16 -0
  189. package/lib/types/client/word-motion.d.ts.map +1 -0
  190. package/lib/types/client/word-motion.js +89 -0
  191. package/lib/types/client/word-motion.js.map +1 -0
  192. package/lib/types/client/word-timeline.d.ts +28 -0
  193. package/lib/types/client/word-timeline.d.ts.map +1 -0
  194. package/lib/types/client/word-timeline.js +100 -0
  195. package/lib/types/client/word-timeline.js.map +1 -0
  196. package/lib/types/dsh-better-display.d.ts +17 -0
  197. package/lib/types/dsh-better-display.d.ts.map +1 -0
  198. package/lib/types/dsh-better-display.js +53 -0
  199. package/lib/types/dsh-better-display.js.map +1 -0
  200. package/package.json +121 -0
  201. package/scripts/build-motion-fixture.mjs +26 -0
  202. package/scripts/link-harness-dependencies.mjs +51 -0
  203. package/skills/generative-mcpapps/SKILL.md +47 -0
  204. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  205. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  206. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  207. package/src/client/Blocks.tsx +205 -0
  208. package/src/client/ChoreographedFlow.tsx +182 -0
  209. package/src/client/ClosedProcessSummary.tsx +19 -0
  210. package/src/client/LiveFold.tsx +43 -0
  211. package/src/client/McpAppFrame.module.css +210 -0
  212. package/src/client/McpAppFrame.tsx +317 -0
  213. package/src/client/Reader.module.css +329 -0
  214. package/src/client/Reader.tsx +814 -0
  215. package/src/client/ReasoningCard.tsx +285 -0
  216. package/src/client/SettingsSection.module.css +147 -0
  217. package/src/client/SettingsSection.tsx +117 -0
  218. package/src/client/StickyLane.tsx +24 -0
  219. package/src/client/TimelineRail.module.css +170 -0
  220. package/src/client/TimelineRail.tsx +262 -0
  221. package/src/client/ToolActivity.tsx +214 -0
  222. package/src/client/TurnMetrics.module.css +139 -0
  223. package/src/client/TurnMetrics.tsx +160 -0
  224. package/src/client/WaitingStatus.tsx +22 -0
  225. package/src/client/conversation-scroll.ts +28 -0
  226. package/src/client/deliverables.ts +122 -0
  227. package/src/client/entry-policy.ts +20 -0
  228. package/src/client/entry.tsx +40 -0
  229. package/src/client/fold-choreography.ts +38 -0
  230. package/src/client/index.tsx +198 -0
  231. package/src/client/live-turn.ts +155 -0
  232. package/src/client/markdown/MarkdownText.module.css +304 -0
  233. package/src/client/markdown/MarkdownText.tsx +187 -0
  234. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  235. package/src/client/markdown/incremental.ts +131 -0
  236. package/src/client/markdown/katex.tsx +91 -0
  237. package/src/client/markdown/mathCompatibility.ts +350 -0
  238. package/src/client/markdown/parse.ts +45 -0
  239. package/src/client/markdown/render.tsx +630 -0
  240. package/src/client/markdown/upstream.json +14 -0
  241. package/src/client/mcp-app.ts +323 -0
  242. package/src/client/message-chrome.ts +47 -0
  243. package/src/client/motion.tsx +338 -0
  244. package/src/client/native/ContextBody.module.css +161 -0
  245. package/src/client/native/ContextBody.tsx +591 -0
  246. package/src/client/native/ContextInjectionRow.module.css +62 -0
  247. package/src/client/native/ContextInjectionRow.tsx +72 -0
  248. package/src/client/native/ReferenceIcon.tsx +35 -0
  249. package/src/client/native/tool-call-model.ts +248 -0
  250. package/src/client/native/upstream.json +41 -0
  251. package/src/client/open-file.ts +94 -0
  252. package/src/client/pending-submission.ts +49 -0
  253. package/src/client/primitive-labels.ts +53 -0
  254. package/src/client/projection.ts +118 -0
  255. package/src/client/reasoning-follow.ts +12 -0
  256. package/src/client/settings-copy.ts +53 -0
  257. package/src/client/settings-slots.ts +20 -0
  258. package/src/client/settings.tsx +73 -0
  259. package/src/client/skill-status.ts +78 -0
  260. package/src/client/store.ts +30 -0
  261. package/src/client/stream-buffer.ts +100 -0
  262. package/src/client/streaming.tsx +62 -0
  263. package/src/client/style.d.ts +4 -0
  264. package/src/client/timeline.ts +77 -0
  265. package/src/client/tool-activity.ts +152 -0
  266. package/src/client/types.ts +66 -0
  267. package/src/client/waiting-clock.ts +25 -0
  268. package/src/client/word-motion.tsx +91 -0
  269. package/src/client/word-timeline.ts +96 -0
  270. package/src/dsh-better-display.ts +108 -0
  271. package/src/skill-roots.ts +138 -0
  272. package/src/skill-status.ts +81 -0
  273. package/tests/browser-closed-gap.tsx +20 -0
  274. package/tests/browser-motion.tsx +65 -0
  275. package/tests/browser-sticky-lanes.tsx +22 -0
  276. package/tests/browser-waiting-clock.tsx +14 -0
  277. package/tests/chat-flow-hook.test.ts +66 -0
  278. package/tests/cloud-gate.test.ts +34 -0
  279. package/tests/conversation-scroll.test.ts +47 -0
  280. package/tests/deliverables.test.ts +128 -0
  281. package/tests/entry.test.ts +42 -0
  282. package/tests/fixtures/maid-composer-hide.html +31 -0
  283. package/tests/fold-choreography.test.ts +38 -0
  284. package/tests/history-word-regression.test.ts +16 -0
  285. package/tests/live-turn.test.ts +218 -0
  286. package/tests/mcp-apps.test.ts +157 -0
  287. package/tests/message-chrome.test.ts +29 -0
  288. package/tests/open-file.test.ts +103 -0
  289. package/tests/pending-submission.test.ts +45 -0
  290. package/tests/projection.test.ts +184 -0
  291. package/tests/reasoning-follow.test.ts +34 -0
  292. package/tests/settings-copy.test.ts +63 -0
  293. package/tests/skill-status.test.ts +137 -0
  294. package/tests/stock-install.test.ts +50 -0
  295. package/tests/stream-buffer.test.ts +97 -0
  296. package/tests/timeline.test.ts +59 -0
  297. package/tests/waiting-clock.test.ts +24 -0
  298. package/tests/word-timeline.test.ts +136 -0
  299. package/tsconfig.json +11 -0
  300. package/tsdown.config.ts +49 -0
@@ -0,0 +1,170 @@
1
+ /* Zero-height sticky slot: floats over the transcript right gutter */
2
+ .slot {
3
+ position: sticky;
4
+ top: 0;
5
+ z-index: 10;
6
+ height: 0;
7
+ pointer-events: none;
8
+ }
9
+
10
+ .frame {
11
+ --turn-rail-band: calc(
12
+ var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px)
13
+ );
14
+ --turn-preview-height: 100px;
15
+
16
+ position: absolute;
17
+ top: calc(var(--turn-rail-band) / 2);
18
+ /* .root is the containing block (container-type creates layout containment)
19
+ and already reserves the right gutter via its own padding, so a small
20
+ positive inset keeps the rail inside the gutter, clear of the text. */
21
+ right: 6px;
22
+ width: 26px;
23
+ height: min(
24
+ var(--turn-natural-height),
25
+ max(0px, calc(var(--turn-rail-band) - 64px)),
26
+ 420px
27
+ );
28
+ cursor: pointer;
29
+ pointer-events: auto;
30
+ transform: translateY(-50%);
31
+ transition: height 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
32
+ }
33
+
34
+ .scroller {
35
+ position: absolute;
36
+ inset: 0;
37
+ overflow-y: auto;
38
+ overscroll-behavior: contain;
39
+ scrollbar-width: none;
40
+ }
41
+
42
+ .scroller::-webkit-scrollbar {
43
+ display: none;
44
+ }
45
+
46
+ .fadeTop {
47
+ mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 100%);
48
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 100%);
49
+ }
50
+
51
+ .fadeBottom {
52
+ mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 24px), transparent 100%);
53
+ -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 24px), transparent 100%);
54
+ }
55
+
56
+ .fadeTop.fadeBottom {
57
+ mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
58
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
59
+ }
60
+
61
+ .marks {
62
+ position: relative;
63
+ height: var(--turn-natural-height);
64
+ }
65
+
66
+ .markPosition {
67
+ position: absolute;
68
+ top: calc(var(--turn-natural-position) + var(--turn-rail-inset));
69
+ right: 0;
70
+ left: 0;
71
+ height: 12px;
72
+ transform: translateY(-50%);
73
+ transition: top 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
74
+ }
75
+
76
+ .mark {
77
+ position: absolute;
78
+ inset: 0 0 0 auto;
79
+ width: 24px;
80
+ padding: 0;
81
+ border: 0;
82
+ background: transparent;
83
+ cursor: pointer;
84
+ pointer-events: none;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: flex-end;
88
+ }
89
+
90
+ .tick {
91
+ border-radius: 2px;
92
+ flex: none;
93
+ transition: width 100ms cubic-bezier(0.16, 1, 0.3, 1),
94
+ height 100ms cubic-bezier(0.16, 1, 0.3, 1),
95
+ opacity 100ms ease,
96
+ background-color 140ms ease;
97
+ }
98
+
99
+ .markBusy .tick {
100
+ animation: markBusyPulse 1s ease-in-out infinite alternate;
101
+ }
102
+
103
+ @keyframes markBusyPulse {
104
+ from { width: 8px; opacity: 0.4; }
105
+ to { width: 18px; opacity: 1; }
106
+ }
107
+
108
+ .preview {
109
+ position: absolute;
110
+ top: clamp(
111
+ 0px,
112
+ calc(
113
+ var(--turn-natural-position) + var(--turn-rail-inset)
114
+ - var(--turn-scroll-top, 0px) - var(--turn-preview-height) / 2
115
+ ),
116
+ calc(100% - var(--turn-preview-height))
117
+ );
118
+ right: calc(100% + 10px);
119
+ box-sizing: border-box;
120
+ width: min(280px, calc(100cqw - 120px));
121
+ max-height: var(--turn-preview-height);
122
+ overflow: hidden;
123
+ padding: 10px 12px;
124
+ border: 1px solid var(--dsw-alias-border-l2);
125
+ border-radius: 10px;
126
+ color: var(--dsw-alias-label-primary);
127
+ background: var(--dsw-alias-bg-base);
128
+ box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
129
+ backdrop-filter: blur(12px);
130
+ pointer-events: none;
131
+ animation: previewFadeIn 120ms ease-out;
132
+ transition: top 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
133
+ z-index: 40;
134
+ }
135
+
136
+ @keyframes previewFadeIn {
137
+ from { opacity: 0; transform: translateX(4px); }
138
+ to { opacity: 1; transform: translateX(0); }
139
+ }
140
+
141
+ .previewPrompt {
142
+ font-size: 12px;
143
+ line-height: 18px;
144
+ font-weight: 500;
145
+ display: -webkit-box;
146
+ -webkit-line-clamp: 2;
147
+ -webkit-box-orient: vertical;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ word-break: break-word;
151
+ }
152
+
153
+ .previewResponse {
154
+ margin-top: 4px;
155
+ color: var(--dsw-alias-label-tertiary);
156
+ font-size: 11px;
157
+ line-height: 16px;
158
+ display: -webkit-box;
159
+ -webkit-line-clamp: 2;
160
+ -webkit-box-orient: vertical;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ word-break: break-word;
164
+ }
165
+
166
+ @media (max-width: 960px) {
167
+ .slot {
168
+ display: none !important;
169
+ }
170
+ }
@@ -0,0 +1,262 @@
1
+ import {
2
+ memo, useEffect, useId, useRef, useState,
3
+ type CSSProperties, type MouseEvent, type PointerEvent,
4
+ } from 'react';
5
+ import type { TimelineItem } from './timeline.js';
6
+ import css from './TimelineRail.module.css';
7
+
8
+ interface TimelineRailProps {
9
+ items: readonly TimelineItem[];
10
+ activeTurn: number | null;
11
+ busyTurn?: number | null;
12
+ onNavigate: (item: TimelineItem) => void;
13
+ }
14
+
15
+ const TURN_SPACING_PX = 12;
16
+ const RAIL_INSET_PX = 6;
17
+ const FADE_PX = 24;
18
+
19
+ type TurnPositionStyle = CSSProperties & {
20
+ readonly '--turn-natural-position': string;
21
+ };
22
+
23
+ type TurnFrameStyle = CSSProperties & {
24
+ readonly '--turn-natural-height': string;
25
+ readonly '--turn-rail-inset': string;
26
+ readonly '--turn-scroll-top': string;
27
+ };
28
+
29
+ function itemPosition(index: number): TurnPositionStyle {
30
+ return { '--turn-natural-position': `${index * TURN_SPACING_PX}px` };
31
+ }
32
+
33
+ function frameStyle(count: number, scrollTop: number): TurnFrameStyle {
34
+ return {
35
+ '--turn-natural-height': `${(count - 1) * TURN_SPACING_PX + 2 * RAIL_INSET_PX}px`,
36
+ '--turn-rail-inset': `${RAIL_INSET_PX}px`,
37
+ '--turn-scroll-top': `${scrollTop}px`,
38
+ };
39
+ }
40
+
41
+ function itemIndexAtPointer(
42
+ items: readonly TimelineItem[],
43
+ frame: HTMLElement,
44
+ scrollTop: number,
45
+ clientY: number,
46
+ ): number {
47
+ const rect = frame.getBoundingClientRect();
48
+ const offset = clientY - rect.top + scrollTop - RAIL_INSET_PX;
49
+ return Math.max(0, Math.min(items.length - 1, Math.round(offset / TURN_SPACING_PX)));
50
+ }
51
+
52
+ interface RailScrollState {
53
+ readonly top: number;
54
+ readonly canScrollUp: boolean;
55
+ readonly canScrollDown: boolean;
56
+ }
57
+
58
+ const RAIL_AT_REST: RailScrollState = { top: 0, canScrollUp: false, canScrollDown: false };
59
+
60
+ function railScrollState(scroller: HTMLElement): RailScrollState {
61
+ const top = scroller.scrollTop;
62
+ return {
63
+ top,
64
+ canScrollUp: top > 1,
65
+ canScrollDown: top < scroller.scrollHeight - scroller.clientHeight - 1,
66
+ };
67
+ }
68
+
69
+ export const TimelineRail = memo(function TimelineRail({
70
+ items,
71
+ activeTurn,
72
+ busyTurn,
73
+ onNavigate,
74
+ }: TimelineRailProps) {
75
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
76
+ const [scrollState, setScrollState] = useState<RailScrollState>(RAIL_AT_REST);
77
+ const scrollerRef = useRef<HTMLDivElement | null>(null);
78
+ const pointerInsideRef = useRef(false);
79
+ const previewId = useId();
80
+
81
+ const syncScrollState = () => {
82
+ const scroller = scrollerRef.current;
83
+ if (!scroller) return;
84
+ const next = railScrollState(scroller);
85
+ setScrollState(cur => (cur.top === next.top && cur.canScrollUp === next.canScrollUp && cur.canScrollDown === next.canScrollDown ? cur : next));
86
+ };
87
+
88
+ useEffect(() => {
89
+ const scroller = scrollerRef.current;
90
+ if (!scroller || typeof ResizeObserver === 'undefined') return;
91
+ const observer = new ResizeObserver(syncScrollState);
92
+ observer.observe(scroller);
93
+ return () => observer.disconnect();
94
+ }, []);
95
+
96
+ useEffect(syncScrollState, [items.length]);
97
+
98
+ // Keep active mark visible
99
+ useEffect(() => {
100
+ const scroller = scrollerRef.current;
101
+ const index = items.findIndex(item => item.turn === activeTurn);
102
+ if (!scroller || index < 0 || pointerInsideRef.current) return;
103
+ const markTop = index * TURN_SPACING_PX + RAIL_INSET_PX;
104
+ const viewTop = scroller.scrollTop;
105
+ const viewHeight = scroller.clientHeight;
106
+ if (viewHeight <= 0 || (markTop >= viewTop + FADE_PX && markTop <= viewTop + viewHeight - FADE_PX)) return;
107
+ const target = Math.max(0, markTop - viewHeight / 2);
108
+ if (typeof scroller.scrollTo === 'function') {
109
+ scroller.scrollTo({ top: target, behavior: 'smooth' });
110
+ } else {
111
+ scroller.scrollTop = target;
112
+ }
113
+ syncScrollState();
114
+ }, [activeTurn, items]);
115
+
116
+ if (items.length < 2) return null;
117
+
118
+ const preview = hoveredIndex !== null && hoveredIndex >= 0 ? items[hoveredIndex] : undefined;
119
+ const previewPosition = hoveredIndex !== null && hoveredIndex >= 0 ? itemPosition(hoveredIndex) : undefined;
120
+
121
+ const onPointerMove = (event: PointerEvent<HTMLElement>) => {
122
+ const scroller = scrollerRef.current;
123
+ const scrollTop = scroller?.scrollTop ?? 0;
124
+ const idx = itemIndexAtPointer(items, event.currentTarget, scrollTop, event.clientY);
125
+ setHoveredIndex(idx);
126
+ };
127
+
128
+ const onPointerLeave = () => {
129
+ pointerInsideRef.current = false;
130
+ setHoveredIndex(null);
131
+ };
132
+
133
+ const navigateAtPointer = (event: MouseEvent<HTMLElement>) => {
134
+ const scroller = scrollerRef.current;
135
+ const scrollTop = scroller?.scrollTop ?? 0;
136
+ const idx = itemIndexAtPointer(items, event.currentTarget, scrollTop, event.clientY);
137
+ const item = items[idx];
138
+ if (item !== undefined) onNavigate(item);
139
+ };
140
+
141
+ const fadeClasses = [css.scroller];
142
+ if (scrollState.canScrollUp) fadeClasses.push(css.fadeTop);
143
+ if (scrollState.canScrollDown) fadeClasses.push(css.fadeBottom);
144
+
145
+ return (
146
+ <div className={css.slot}>
147
+ <nav
148
+ className={css.frame}
149
+ style={frameStyle(items.length, scrollState.top)}
150
+ aria-label="轮次导航时间轴"
151
+ onClick={navigateAtPointer}
152
+ onPointerMove={onPointerMove}
153
+ onPointerEnter={() => { pointerInsideRef.current = true; }}
154
+ onPointerLeave={onPointerLeave}
155
+ onMouseDown={event => { event.preventDefault(); }}
156
+ >
157
+ <div
158
+ ref={scrollerRef}
159
+ className={fadeClasses.join(' ')}
160
+ onScroll={syncScrollState}
161
+ >
162
+ <div className={css.marks}>
163
+ {items.map((item, index) => {
164
+ const isBusy = busyTurn === item.turn;
165
+ const isActive = activeTurn === item.turn;
166
+ const isUnloaded = item.anchor.kind === 'unloaded';
167
+
168
+ // Wave calculation: peak at cursor, smoothly tapering off in width and height
169
+ let width = isUnloaded ? 6 : 8;
170
+ let height = 2;
171
+ let opacity = isUnloaded ? 0.3 : 0.45;
172
+ let background = 'var(--dsw-alias-label-tertiary)';
173
+
174
+ if (hoveredIndex !== null) {
175
+ const diff = Math.abs(index - hoveredIndex);
176
+ if (diff === 0) {
177
+ // Cursor peak (最长最高)
178
+ width = 26;
179
+ height = 2.8;
180
+ opacity = 1.0;
181
+ background = 'var(--dsw-alias-label-primary)';
182
+ } else if (diff === 1) {
183
+ // 旁边第 1 阶 (次长次高)
184
+ width = 18;
185
+ height = 2.2;
186
+ opacity = 0.8;
187
+ background = 'var(--dsw-alias-label-primary)';
188
+ } else if (diff === 2) {
189
+ // 旁边第 2 阶 (继续变矮变短)
190
+ width = 13;
191
+ height = 1.8;
192
+ opacity = 0.6;
193
+ background = 'var(--dsw-alias-label-secondary)';
194
+ } else if (diff === 3) {
195
+ // 旁边第 3 阶 (平缓过渡)
196
+ width = 9;
197
+ height = 1.6;
198
+ opacity = 0.45;
199
+ background = 'var(--dsw-alias-label-tertiary)';
200
+ } else {
201
+ // 远离光标的基线
202
+ width = 8;
203
+ height = 2;
204
+ opacity = 0.35;
205
+ background = 'var(--dsw-alias-label-tertiary)';
206
+ }
207
+ } else if (isActive) {
208
+ width = 20;
209
+ height = 2.4;
210
+ opacity = 0.95;
211
+ background = 'var(--dsw-alias-label-primary)';
212
+ }
213
+
214
+ const tickStyle: CSSProperties = {
215
+ width: `${width}px`,
216
+ height: `${height}px`,
217
+ opacity,
218
+ backgroundColor: background,
219
+ };
220
+
221
+ const markClasses = [css.mark];
222
+ if (isBusy) markClasses.push(css.markBusy);
223
+
224
+ return (
225
+ <div key={item.turn} className={css.markPosition} style={itemPosition(index)}>
226
+ <button
227
+ type="button"
228
+ className={markClasses.join(' ')}
229
+ aria-label={`第 ${item.turn + 1} 轮`}
230
+ aria-current={isActive ? 'true' : undefined}
231
+ aria-busy={isBusy ? 'true' : undefined}
232
+ aria-describedby={hoveredIndex === index ? previewId : undefined}
233
+ onMouseDown={event => { event.preventDefault(); }}
234
+ onClick={(e) => {
235
+ e.stopPropagation();
236
+ onNavigate(item);
237
+ }}
238
+ onFocus={() => setHoveredIndex(index)}
239
+ onBlur={() => setHoveredIndex(null)}
240
+ >
241
+ <span className={css.tick} style={tickStyle} />
242
+ </button>
243
+ </div>
244
+ );
245
+ })}
246
+ </div>
247
+ </div>
248
+
249
+ {preview !== undefined && previewPosition !== undefined && (
250
+ <div id={previewId} role="tooltip" className={css.preview} style={previewPosition}>
251
+ <div className={css.previewPrompt}>
252
+ {preview.prompt || `第 ${preview.turn + 1} 轮`}
253
+ </div>
254
+ {preview.response !== '' && (
255
+ <div className={css.previewResponse}>{preview.response}</div>
256
+ )}
257
+ </div>
258
+ )}
259
+ </nav>
260
+ </div>
261
+ );
262
+ });
@@ -0,0 +1,214 @@
1
+ import { memo, useEffect, useId, useMemo, useRef, useState } from 'react';
2
+ import type { ToolCallBlock, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ import type { DiffHunk, ReadBlockLine, SearchFileGroup } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { DiffBlock, DisclosureRow, JsonTree, ReadBlock, SearchBlock, TerminalBlock, WebBlock,
5
+ IconApiOutline14, IconBrowseOutline16, IconEditOutline16, IconSearchOutline16, IconSkillOutline16, IconSparkle16 } from '@deepseek-ai/dsh-client-ui-primitives';
6
+ import { Blocks, contentBlocks } from './Blocks.js';
7
+ import { ProcessFragment } from './motion.js';
8
+ import { activityPhase, activitySummary, executionFacts, objectValue, toolIdentity } from './tool-activity.js';
9
+ import type { ToolActivityEntry, ToolCategory, ToolPhase } from './tool-activity.js';
10
+ import type { BlockRenderProps } from './types.js';
11
+ import { classifyTool, toolRowModel, VARIANT_TITLES } from './native/tool-call-model.js';
12
+ import { McpAppFrame, StreamingMcpAppPlaceholder } from './McpAppFrame.js';
13
+ import { diffBlockLabels, jsonTreeLabels, readBlockLabels, searchBlockLabels, terminalBlockLabels, webBlockLabels } from './primitive-labels.js';
14
+ import css from './Reader.module.css';
15
+
16
+ const LABEL: Record<ToolPhase, string> = { preparing: '输入生成中', running: '执行中', returned: '已返回', succeeded: '已完成', failed: '失败', interrupted: '已中断' };
17
+ const ICONS = { write: IconEditOutline16, read: IconBrowseOutline16, terminal: IconApiOutline14, search: IconSearchOutline16, web: IconSearchOutline16, other: IconSparkle16 } satisfies Record<ToolCategory, unknown>;
18
+ const number = new Intl.NumberFormat('zh-CN');
19
+ const language = (path: string | undefined) => path?.split('.').at(-1);
20
+ const duration = (ms: number) => ms < 1000 ? `${Math.round(ms)} 毫秒` : `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} 秒`;
21
+
22
+ function generatedInput(content: string, target: string | undefined, preparing: boolean) {
23
+ const lines = content.split('\n').map((text, index) => ({ number: index + 1, text }));
24
+ const visible = preparing ? lines.slice(-12) : lines.slice(0, 1600);
25
+ return <div data-reader-tool-file><p className={css.toolDetailNote}>{preparing ? '正在生成的输入 · 尚未执行 · 末尾 12 行' : '工具输入中的文件内容'}{!preparing && lines.length > visible.length ? ' · 预览前 1,600 行,完整内容在原始数据中' : ''}</p>
26
+ <ReadBlock label={target ?? '文件内容'} lang={language(target)} lines={visible} totalLines={lines.length} maxLines={16} labels={readBlockLabels} />
27
+ </div>;
28
+ }
29
+
30
+ function InputView({ model, preparing, fillComposer }: { model: ReturnType<typeof activitySummary>; preparing: boolean; fillComposer: BlockRenderProps['fillComposer'] }) {
31
+ if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
32
+ return preparing
33
+ ? <StreamingMcpAppPlaceholder title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} />
34
+ : <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={fillComposer} />;
35
+ }
36
+ if (model.content) return generatedInput(model.content, model.target, preparing);
37
+ if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} labels={terminalBlockLabels} /></div>;
38
+ return <JsonTree data={model.args} label={preparing ? '已收到的输入字段' : '工具输入'} labels={jsonTreeLabels} />;
39
+ }
40
+
41
+ function readLines(value: unknown): ReadBlockLine[] | null {
42
+ if (!Array.isArray(value)) return null;
43
+ const lines: ReadBlockLine[] = [];
44
+ for (const item of value) {
45
+ const row = objectValue(item);
46
+ if (typeof row?.number !== 'number' || !Number.isInteger(row.number) || typeof row.text !== 'string') return null;
47
+ lines.push({ number: row.number, text: row.text });
48
+ }
49
+ return lines;
50
+ }
51
+
52
+ function diffHunks(value: unknown): DiffHunk[] | null {
53
+ if (!Array.isArray(value) || value.length === 0) return null;
54
+ const diffs: DiffHunk[] = [];
55
+ for (const item of value) {
56
+ const row = objectValue(item);
57
+ if (typeof row?.path !== 'string' || (row.oldText !== null && typeof row.oldText !== 'string') || typeof row.newText !== 'string') return null;
58
+ diffs.push({ path: row.path, oldText: row.oldText, newText: row.newText });
59
+ }
60
+ return diffs;
61
+ }
62
+
63
+ function searchFiles(value: unknown): SearchFileGroup[] | null {
64
+ if (!Array.isArray(value)) return null;
65
+ const files: SearchFileGroup[] = [];
66
+ for (const item of value) {
67
+ const row = objectValue(item);
68
+ if (typeof row?.path !== 'string' || !Array.isArray(row.matches)) return null;
69
+ const matches: { lineNumber: number; line: string }[] = [];
70
+ for (const itemMatch of row.matches) {
71
+ const match = objectValue(itemMatch);
72
+ if (typeof match?.lineNumber !== 'number' || !Number.isInteger(match.lineNumber) || typeof match.line !== 'string') return null;
73
+ matches.push({ lineNumber: match.lineNumber, line: match.line });
74
+ }
75
+ files.push({ path: row.path, matches });
76
+ }
77
+ return files;
78
+ }
79
+
80
+ function ResultView({ entry, model, phase, ...render }: BlockRenderProps & { entry: ToolActivityEntry; model: ReturnType<typeof activitySummary>; phase: ToolPhase }) {
81
+ if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
82
+ return <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={render.fillComposer} />;
83
+ }
84
+ const block = entry.block;
85
+ if (!block || !('kind' in block)) return <>
86
+ <p className={css.toolDetailNote}>{phase === 'interrupted' ? '已中断,没有工具结果。已生成的输入仍可查看。' : phase === 'preparing' ? '模型正在生成工具输入,工具还未开始执行。' : '工具已开始执行,正在等待结果。'}</p>
87
+ <InputView model={model} preparing={phase === 'preparing'} fillComposer={render.fillComposer} />
88
+ </>;
89
+ const meta = objectValue(block.meta);
90
+ const text = block.content.filter(item => item.type === 'text').map(item => item.text).join('\n');
91
+ if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model} preparing={false} fillComposer={render.fillComposer} /><pre className={css.toolRaw}>{text}</pre></>;
92
+ if (model.category === 'terminal') {
93
+ const facts = executionFacts(block);
94
+ const output = text.replace(/\n\[(?:exit code: \d+|killed by signal: [^\]\n]+)\]$/, '');
95
+ return <div data-reader-tool-terminal><TerminalBlock command={model.command ?? model.name} cwd={model.cwd}
96
+ output={output} exitCode={facts.exitCode} signal={facts.signal} maxLines={18} labels={terminalBlockLabels} /></div>;
97
+ }
98
+ const lines = readLines(meta?.lines);
99
+ if (model.category === 'read' && typeof meta?.path === 'string' && typeof meta.totalLines === 'number' && lines) return <div data-reader-tool-file><ReadBlock label={meta.path} lang={typeof meta.lang === 'string' ? meta.lang : undefined} lines={lines} totalLines={meta.totalLines} maxLines={18} labels={readBlockLabels} /></div>;
100
+ const diffs = diffHunks(meta?.diffs);
101
+ if (model.category === 'write' && diffs) return <div data-reader-tool-diff><DiffBlock diffs={diffs} maxLines={18} labels={diffBlockLabels} /></div>;
102
+ if (model.category === 'search' && typeof meta?.total === 'number' && typeof meta.truncated === 'boolean') {
103
+ if (meta.shape === 'paths' && Array.isArray(meta.paths) && meta.paths.every((path): path is string => typeof path === 'string')) return <div data-reader-tool-search><SearchBlock kind="paths" paths={meta.paths} total={meta.total} truncated={meta.truncated} maxLines={18} labels={searchBlockLabels} /></div>;
104
+ const files = searchFiles(meta.files);
105
+ if (meta.shape === 'matches' && files) return <div data-reader-tool-search><SearchBlock kind="matches" files={files} total={meta.total} truncated={meta.truncated} maxLines={18} labels={searchBlockLabels} /></div>;
106
+ }
107
+ if (model.category === 'web' && typeof meta?.truncated === 'boolean') {
108
+ if (model.name === 'web_fetch' && typeof meta.url === 'string' && typeof meta.statusCode === 'number') return <div data-reader-tool-web><WebBlock kind="fetch" url={meta.url} statusCode={meta.statusCode} truncated={meta.truncated} labels={webBlockLabels} /></div>;
109
+ if (model.name === 'web_search' && Array.isArray(meta.sources)) {
110
+ const sources = meta.sources.flatMap(source => {
111
+ const item = objectValue(source);
112
+ return typeof item?.url === 'string' ? [{ url: item.url, ...(typeof item.title === 'string' ? { title: item.title } : {}), ...(typeof item.snippet === 'string' ? { snippet: item.snippet } : {}), ...(typeof item.publishedAt === 'string' ? { publishedAt: item.publishedAt } : {}) }] : [];
113
+ });
114
+ if (sources.length === meta.sources.length) return <div data-reader-tool-web><WebBlock kind="search" sources={sources} answer={typeof meta.answer === 'string' ? meta.answer : undefined} truncated={meta.truncated} labels={webBlockLabels} /></div>;
115
+ }
116
+ }
117
+ // A trace/export may omit wire presentation. Keep the generated input clearly
118
+ // labelled; it is not proof of an applied diff or a successful file mutation.
119
+ if (model.category === 'write' && model.content && !block.isError) return <>
120
+ <p className={css.toolDetailNote}>文件工具已返回。以下为提交的内容;完整返回记录可在「原始数据」查看。</p>
121
+ {generatedInput(model.content, model.target, false)}
122
+ </>;
123
+ const content: ToolResultNode['content'] = block.content;
124
+ if (content.some(item => item.type === 'text')) return <div className={css.toolDocument}><Blocks {...render} blocks={contentBlocks(content).filter(item => item.kind === 'text')} source="tool" /></div>;
125
+ if (content.length) return <p className={css.toolDetailNote}>图片或扩展内容已在对话中单独展示。</p>;
126
+ return <p className={css.toolDetailNote}>工具没有返回可展示的内容。</p>;
127
+ }
128
+
129
+ /** One occurrence, keyed by call id all the way from generation to result. */
130
+ export const ToolActivity = memo(function ToolActivityView({ entry, motion, turnClosed, onRead, depth = 0, ...render }: BlockRenderProps & {
131
+ entry: ToolActivityEntry; motion: boolean; turnClosed: boolean; onRead: () => void; depth?: number;
132
+ }) {
133
+ const [open, setOpen] = useState(false);
134
+ const [tab, setTab] = useState<'result' | 'input' | 'raw'>('result');
135
+ const control = useRef<HTMLElement | null>(null);
136
+ const panel = useRef<HTMLDivElement>(null);
137
+ const [selected, setSelected] = useState(false);
138
+ const detailId = useId();
139
+ const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
140
+ const model = useMemo(() => activitySummary(entry), [entry.block, entry.draft]);
141
+ const phase = activityPhase(entry, turnClosed);
142
+ const heldPreview = useRef({ entry, model, phase });
143
+ if (!selected) heldPreview.current = { entry, model, phase };
144
+ const preview = heldPreview.current;
145
+ useEffect(() => {
146
+ const track = () => {
147
+ const selection = document.getSelection();
148
+ setSelected(!!selection && !selection.isCollapsed && !!selection.anchorNode && !!panel.current?.contains(selection.anchorNode));
149
+ };
150
+ document.addEventListener('selectionchange', track);
151
+ return () => document.removeEventListener('selectionchange', track);
152
+ }, []);
153
+ const facts = executionFacts(entry.block);
154
+ const Icon = model.name === 'skill' ? IconSkillOutline16 : ICONS[model.category];
155
+ const block = entry.block;
156
+ const native = block ? toolRowModel(model.name, block) : null;
157
+ const skillName = typeof model.args?.name === 'string' ? model.args.name.split('\n')[0] : model.raw.split('\n')[0];
158
+ const rowTitle = model.name === 'skill' ? 'Skill' : native?.title ?? VARIANT_TITLES[classifyTool(model.name)];
159
+ const rowSummary = phase === 'interrupted' ? '已停止 · 调用记录保留' : model.name === 'skill' ? skillName : native?.errorSummary ?? native?.summary
160
+ ?? (classifyTool(model.name) === 'others' ? `${model.name} · ${model.target ?? model.title}` : model.target ?? model.title);
161
+ const showState = phase === 'preparing' || phase === 'running' || phase === 'failed' || phase === 'interrupted';
162
+ const elapsed = block && 'kind' in block && block.callTime != null ? Math.max(0, block.time - block.callTime) : null;
163
+ const rawResult = useMemo(() => {
164
+ const value = preview.entry.block;
165
+ return value && 'kind' in value ? JSON.stringify({ content: value.content, isError: value.isError, meta: value.meta }, null, 2) : '';
166
+ }, [preview.entry.block]);
167
+ const tabs = [['result', phase === 'preparing' ? '生成预览' : '结果'], ['input', '输入'], ['raw', '原始数据']] as const;
168
+ const activate = (index: number) => { const item = tabs[(index + tabs.length) % tabs.length]!; setTab(item[0]); tabRefs.current[(index + tabs.length) % tabs.length]?.focus(); };
169
+ if (depth > 6) return <p className={css.meta}>更深的嵌套调用可在原对话查看。</p>;
170
+ return <div ref={element => { control.current = element?.querySelector<HTMLElement>('[data-disclosure-row]') ?? null; }} className={css.toolActivity} data-reader-tool-call={entry.callId} data-tool-phase={phase} data-tool-args-length={model.raw.length} data-tool-category={model.category} data-expanded={open} data-ud-check="reader-tool-activity">
171
+ <DisclosureRow icon={<Icon size={14} />} title={rowTitle} open={open} expandable expandOnRowClick keepContentWhenOpen
172
+ onToggle={() => { onRead(); setOpen(value => !value); }} rowClassName={css.nativeToolRow}
173
+ collapsedContent={<><span className={css.rowSeparator} aria-hidden /><span className={css.nativeToolSummary} title={rowSummary} data-reader-tool-summary>{rowSummary}</span>
174
+ {showState && <span className={css.toolState} data-phase={phase}>{LABEL[phase]}</span>}</>} />
175
+ <ProcessFragment open={open} motion={motion} onRead={onRead} returnFocusTo={control} nodeKey={`${entry.key}:detail`} framed>
176
+ <div id={detailId} className={css.toolDetails}>
177
+ <div className={css.toolLedger} aria-live="off">
178
+ <span>工具 · <span className={css.toolEngine}>{model.name}</span></span>
179
+ <span data-reader-tool-progress>{phase === 'preparing' ? `已接收 ${number.format(model.raw.length)} 字符输入` : phase === 'running' ? '已提交 · 等待工具返回' : phase === 'interrupted' ? '已停止 · 输入记录保留' : elapsed !== null ? `执行 ${duration(elapsed)}` : '结果已记录'}</span>
180
+ {facts.exitCode !== undefined && <span>退出码 {facts.exitCode}</span>}
181
+ {facts.signal && <span>信号 {facts.signal}</span>}
182
+ </div>
183
+ <div className={css.toolTabs} role="tablist" aria-label={`${model.title}的执行数据`} onKeyDown={event => {
184
+ const index = tabs.findIndex(item => item[0] === tab);
185
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') { event.preventDefault(); activate(index + (event.key === 'ArrowRight' ? 1 : -1)); }
186
+ else if (event.key === 'Home' || event.key === 'End') { event.preventDefault(); activate(event.key === 'Home' ? 0 : tabs.length - 1); }
187
+ }}>{tabs.map(([id, title], index) => <button key={id} ref={element => { tabRefs.current[index] = element; }} type="button" role="tab" id={`${detailId}-${id}`} aria-selected={tab === id} aria-controls={`${detailId}-panel`} tabIndex={tab === id ? 0 : -1} onClick={() => setTab(id)}>{title}</button>)}</div>
188
+ <div ref={panel} id={`${detailId}-panel`} className={css.toolPanel} role="tabpanel" aria-labelledby={`${detailId}-${tab}`} tabIndex={0}>
189
+ {selected && <p className={css.toolDetailNote}>为保留选区,预览暂停更新;当前状态见卡片标题。</p>}
190
+ {tab === 'result' && <ResultView {...render} {...preview} />}
191
+ {tab === 'input' && <><InputView model={preview.model} preparing={preview.phase === 'preparing'} fillComposer={render.fillComposer} /><details className={css.detail}><summary>全部输入字段</summary><JsonTree data={preview.model.args} label="输入字段" labels={jsonTreeLabels} /></details></>}
192
+ {tab === 'raw' && <><p className={css.toolDetailNote}>完整记录 · 只读 · 不执行其中的代码</p><h4 className={css.toolRawLabel}>工具输入</h4><pre className={css.toolRaw}>{preview.model.raw || '输入尚未到达'}</pre>{rawResult && <><h4 className={css.toolRawLabel}>工具结果</h4><pre className={css.toolRaw}>{rawResult}</pre></>}</>}
193
+ </div>
194
+ </div>
195
+ </ProcessFragment>
196
+ {!!block?.subCalls.length && <div className={css.toolChildren} aria-label="子调用">{block.subCalls.map((child, index) => <ToolActivity key={child.callId} {...render}
197
+ entry={{ kind: 'tool', key: `reader-tool:${child.callId}`, callId: child.callId, step: entry.step, order: index, block: child }}
198
+ motion={motion} turnClosed={turnClosed} onRead={onRead} depth={depth + 1} />)}</div>}
199
+ </div>;
200
+ }, (previous, next) => previous.entry.callId === next.entry.callId && previous.entry.block === next.entry.block
201
+ && previous.entry.draft === next.entry.draft && previous.entry.step === next.entry.step
202
+ && previous.motion === next.motion && previous.turnClosed === next.turnClosed && previous.depth === next.depth
203
+ && previous.onRead === next.onRead && previous.renderSlotChain === next.renderSlotChain && previous.loadImage === next.loadImage && previous.fillComposer === next.fillComposer);
204
+
205
+ /** Rich media (images, MCP widgets) rendered outside the folded tool ledger. */
206
+ export function ToolMedia({ block, depth = 0, ...render }: BlockRenderProps & { block: ToolCallBlock; depth?: number }) {
207
+ if (depth > 6) return null;
208
+ const settled = 'kind' in block;
209
+ const visible = settled ? contentBlocks(block.content).filter(item => item.kind === 'image' || item.kind === 'other') : [];
210
+ return <>
211
+ {visible.length > 0 && <Blocks {...render} blocks={visible} source="tool" />}
212
+ {block.subCalls.map(child => <ToolMedia key={child.callId} {...render} block={child} depth={depth + 1} />)}
213
+ </>;
214
+ }