@drscrewdriver/dsh-better-display 0.3.3-fork.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 (390) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +55 -0
  5. package/README.md +55 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/compat/harness-rc2.json +99 -0
  8. package/cordis.patch.yml +5 -0
  9. package/cordis.yml +4 -0
  10. package/docs/official-rendering-bridge.md +65 -0
  11. package/dshx.yml +5 -0
  12. package/lib/client.js +49292 -0
  13. package/lib/client.js.map +1 -0
  14. package/lib/dsh-better-display.js +101 -0
  15. package/lib/types/client/Blocks.d.ts +49 -0
  16. package/lib/types/client/Blocks.d.ts.map +1 -0
  17. package/lib/types/client/Blocks.js +143 -0
  18. package/lib/types/client/Blocks.js.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  20. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  21. package/lib/types/client/ChoreographedFlow.js +218 -0
  22. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  24. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  25. package/lib/types/client/ClosedProcessSummary.js +16 -0
  26. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  27. package/lib/types/client/DiffPanel.d.ts +13 -0
  28. package/lib/types/client/DiffPanel.d.ts.map +1 -0
  29. package/lib/types/client/DiffPanel.js +115 -0
  30. package/lib/types/client/DiffPanel.js.map +1 -0
  31. package/lib/types/client/LiveFold.d.ts +10 -0
  32. package/lib/types/client/LiveFold.d.ts.map +1 -0
  33. package/lib/types/client/LiveFold.js +39 -0
  34. package/lib/types/client/LiveFold.js.map +1 -0
  35. package/lib/types/client/McpAppFrame.d.ts +32 -0
  36. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  37. package/lib/types/client/McpAppFrame.js +193 -0
  38. package/lib/types/client/McpAppFrame.js.map +1 -0
  39. package/lib/types/client/OfficialContent.d.ts +25 -0
  40. package/lib/types/client/OfficialContent.d.ts.map +1 -0
  41. package/lib/types/client/OfficialContent.js +53 -0
  42. package/lib/types/client/OfficialContent.js.map +1 -0
  43. package/lib/types/client/Reader.d.ts +3 -0
  44. package/lib/types/client/Reader.d.ts.map +1 -0
  45. package/lib/types/client/Reader.js +603 -0
  46. package/lib/types/client/Reader.js.map +1 -0
  47. package/lib/types/client/ReasoningCard.d.ts +11 -0
  48. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  49. package/lib/types/client/ReasoningCard.js +322 -0
  50. package/lib/types/client/ReasoningCard.js.map +1 -0
  51. package/lib/types/client/SettingsSection.d.ts +37 -0
  52. package/lib/types/client/SettingsSection.d.ts.map +1 -0
  53. package/lib/types/client/SettingsSection.js +63 -0
  54. package/lib/types/client/SettingsSection.js.map +1 -0
  55. package/lib/types/client/StickyLane.d.ts +8 -0
  56. package/lib/types/client/StickyLane.d.ts.map +1 -0
  57. package/lib/types/client/StickyLane.js +39 -0
  58. package/lib/types/client/StickyLane.js.map +1 -0
  59. package/lib/types/client/TimelineRail.d.ts +10 -0
  60. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  61. package/lib/types/client/TimelineRail.js +168 -0
  62. package/lib/types/client/TimelineRail.js.map +1 -0
  63. package/lib/types/client/ToolActivity.d.ts +17 -0
  64. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  65. package/lib/types/client/ToolActivity.js +244 -0
  66. package/lib/types/client/ToolActivity.js.map +1 -0
  67. package/lib/types/client/TurnMetrics.d.ts +11 -0
  68. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  69. package/lib/types/client/TurnMetrics.js +44 -0
  70. package/lib/types/client/TurnMetrics.js.map +1 -0
  71. package/lib/types/client/WaitClock.d.ts +16 -0
  72. package/lib/types/client/WaitClock.d.ts.map +1 -0
  73. package/lib/types/client/WaitClock.js +29 -0
  74. package/lib/types/client/WaitClock.js.map +1 -0
  75. package/lib/types/client/WaitingClock.d.ts +15 -0
  76. package/lib/types/client/WaitingClock.d.ts.map +1 -0
  77. package/lib/types/client/WaitingClock.js +30 -0
  78. package/lib/types/client/WaitingClock.js.map +1 -0
  79. package/lib/types/client/WaitingStatus.d.ts +6 -0
  80. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  81. package/lib/types/client/WaitingStatus.js +9 -0
  82. package/lib/types/client/WaitingStatus.js.map +1 -0
  83. package/lib/types/client/conversation-scroll.d.ts +19 -0
  84. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  85. package/lib/types/client/conversation-scroll.js +25 -0
  86. package/lib/types/client/conversation-scroll.js.map +1 -0
  87. package/lib/types/client/deliverables.d.ts +24 -0
  88. package/lib/types/client/deliverables.d.ts.map +1 -0
  89. package/lib/types/client/deliverables.js +104 -0
  90. package/lib/types/client/deliverables.js.map +1 -0
  91. package/lib/types/client/entry-policy.d.ts +10 -0
  92. package/lib/types/client/entry-policy.d.ts.map +1 -0
  93. package/lib/types/client/entry-policy.js +24 -0
  94. package/lib/types/client/entry-policy.js.map +1 -0
  95. package/lib/types/client/entry.d.ts +4 -0
  96. package/lib/types/client/entry.d.ts.map +1 -0
  97. package/lib/types/client/entry.js +34 -0
  98. package/lib/types/client/entry.js.map +1 -0
  99. package/lib/types/client/fold-choreography.d.ts +40 -0
  100. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  101. package/lib/types/client/fold-choreography.js +48 -0
  102. package/lib/types/client/fold-choreography.js.map +1 -0
  103. package/lib/types/client/fold-intensity.d.ts +11 -0
  104. package/lib/types/client/fold-intensity.d.ts.map +1 -0
  105. package/lib/types/client/fold-intensity.js +32 -0
  106. package/lib/types/client/fold-intensity.js.map +1 -0
  107. package/lib/types/client/index.d.ts +7 -0
  108. package/lib/types/client/index.d.ts.map +1 -0
  109. package/lib/types/client/index.js +195 -0
  110. package/lib/types/client/index.js.map +1 -0
  111. package/lib/types/client/live-turn.d.ts +58 -0
  112. package/lib/types/client/live-turn.d.ts.map +1 -0
  113. package/lib/types/client/live-turn.js +190 -0
  114. package/lib/types/client/live-turn.js.map +1 -0
  115. package/lib/types/client/markdown/MarkdownText.d.ts +41 -0
  116. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  117. package/lib/types/client/markdown/MarkdownText.js +178 -0
  118. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  119. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  120. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  121. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  122. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  123. package/lib/types/client/markdown/incremental.d.ts +69 -0
  124. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  125. package/lib/types/client/markdown/incremental.js +108 -0
  126. package/lib/types/client/markdown/incremental.js.map +1 -0
  127. package/lib/types/client/markdown/katex.d.ts +27 -0
  128. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  129. package/lib/types/client/markdown/katex.js +88 -0
  130. package/lib/types/client/markdown/katex.js.map +1 -0
  131. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  132. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  133. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  134. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  135. package/lib/types/client/markdown/parse.d.ts +25 -0
  136. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  137. package/lib/types/client/markdown/parse.js +42 -0
  138. package/lib/types/client/markdown/parse.js.map +1 -0
  139. package/lib/types/client/markdown/render.d.ts +121 -0
  140. package/lib/types/client/markdown/render.d.ts.map +1 -0
  141. package/lib/types/client/markdown/render.js +452 -0
  142. package/lib/types/client/markdown/render.js.map +1 -0
  143. package/lib/types/client/mcp-app.d.ts +50 -0
  144. package/lib/types/client/mcp-app.d.ts.map +1 -0
  145. package/lib/types/client/mcp-app.js +328 -0
  146. package/lib/types/client/mcp-app.js.map +1 -0
  147. package/lib/types/client/message-chrome.d.ts +19 -0
  148. package/lib/types/client/message-chrome.d.ts.map +1 -0
  149. package/lib/types/client/message-chrome.js +42 -0
  150. package/lib/types/client/message-chrome.js.map +1 -0
  151. package/lib/types/client/motion.d.ts +38 -0
  152. package/lib/types/client/motion.d.ts.map +1 -0
  153. package/lib/types/client/motion.js +453 -0
  154. package/lib/types/client/motion.js.map +1 -0
  155. package/lib/types/client/native/ContextBody.d.ts +130 -0
  156. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  157. package/lib/types/client/native/ContextBody.js +398 -0
  158. package/lib/types/client/native/ContextBody.js.map +1 -0
  159. package/lib/types/client/native/ContextInjectionRow.d.ts +35 -0
  160. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  161. package/lib/types/client/native/ContextInjectionRow.js +42 -0
  162. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  163. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  164. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  165. package/lib/types/client/native/ReferenceIcon.js +16 -0
  166. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  167. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  168. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  169. package/lib/types/client/native/tool-call-model.js +219 -0
  170. package/lib/types/client/native/tool-call-model.js.map +1 -0
  171. package/lib/types/client/native/tool-update.d.ts +23 -0
  172. package/lib/types/client/native/tool-update.d.ts.map +1 -0
  173. package/lib/types/client/native/tool-update.js +22 -0
  174. package/lib/types/client/native/tool-update.js.map +1 -0
  175. package/lib/types/client/official-slots.d.ts +71 -0
  176. package/lib/types/client/official-slots.d.ts.map +1 -0
  177. package/lib/types/client/official-slots.js +183 -0
  178. package/lib/types/client/official-slots.js.map +1 -0
  179. package/lib/types/client/open-file.d.ts +29 -0
  180. package/lib/types/client/open-file.d.ts.map +1 -0
  181. package/lib/types/client/open-file.js +71 -0
  182. package/lib/types/client/open-file.js.map +1 -0
  183. package/lib/types/client/pending-submission.d.ts +28 -0
  184. package/lib/types/client/pending-submission.d.ts.map +1 -0
  185. package/lib/types/client/pending-submission.js +28 -0
  186. package/lib/types/client/pending-submission.js.map +1 -0
  187. package/lib/types/client/platform-media.d.ts +6 -0
  188. package/lib/types/client/platform-media.d.ts.map +1 -0
  189. package/lib/types/client/platform-media.js +18 -0
  190. package/lib/types/client/platform-media.js.map +1 -0
  191. package/lib/types/client/primitive-labels.d.ts +10 -0
  192. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  193. package/lib/types/client/primitive-labels.js +38 -0
  194. package/lib/types/client/primitive-labels.js.map +1 -0
  195. package/lib/types/client/projection.d.ts +41 -0
  196. package/lib/types/client/projection.d.ts.map +1 -0
  197. package/lib/types/client/projection.js +112 -0
  198. package/lib/types/client/projection.js.map +1 -0
  199. package/lib/types/client/reasoning-follow.d.ts +10 -0
  200. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  201. package/lib/types/client/reasoning-follow.js +21 -0
  202. package/lib/types/client/reasoning-follow.js.map +1 -0
  203. package/lib/types/client/resource-address.d.ts +3 -0
  204. package/lib/types/client/resource-address.d.ts.map +1 -0
  205. package/lib/types/client/resource-address.js +33 -0
  206. package/lib/types/client/resource-address.js.map +1 -0
  207. package/lib/types/client/settings-copy.d.ts +7 -0
  208. package/lib/types/client/settings-copy.d.ts.map +1 -0
  209. package/lib/types/client/settings-copy.js +53 -0
  210. package/lib/types/client/settings-copy.js.map +1 -0
  211. package/lib/types/client/settings-slots.d.ts +23 -0
  212. package/lib/types/client/settings-slots.d.ts.map +1 -0
  213. package/lib/types/client/settings-slots.js +2 -0
  214. package/lib/types/client/settings-slots.js.map +1 -0
  215. package/lib/types/client/settings.d.ts +4 -0
  216. package/lib/types/client/settings.d.ts.map +1 -0
  217. package/lib/types/client/settings.js +60 -0
  218. package/lib/types/client/settings.js.map +1 -0
  219. package/lib/types/client/skill-status.d.ts +25 -0
  220. package/lib/types/client/skill-status.d.ts.map +1 -0
  221. package/lib/types/client/skill-status.js +44 -0
  222. package/lib/types/client/skill-status.js.map +1 -0
  223. package/lib/types/client/store.d.ts +35 -0
  224. package/lib/types/client/store.d.ts.map +1 -0
  225. package/lib/types/client/store.js +37 -0
  226. package/lib/types/client/store.js.map +1 -0
  227. package/lib/types/client/stream-buffer.d.ts +37 -0
  228. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  229. package/lib/types/client/stream-buffer.js +150 -0
  230. package/lib/types/client/stream-buffer.js.map +1 -0
  231. package/lib/types/client/streaming.d.ts +18 -0
  232. package/lib/types/client/streaming.d.ts.map +1 -0
  233. package/lib/types/client/streaming.js +84 -0
  234. package/lib/types/client/streaming.js.map +1 -0
  235. package/lib/types/client/timeline.d.ts +26 -0
  236. package/lib/types/client/timeline.d.ts.map +1 -0
  237. package/lib/types/client/timeline.js +48 -0
  238. package/lib/types/client/timeline.js.map +1 -0
  239. package/lib/types/client/tool-activity.d.ts +98 -0
  240. package/lib/types/client/tool-activity.d.ts.map +1 -0
  241. package/lib/types/client/tool-activity.js +301 -0
  242. package/lib/types/client/tool-activity.js.map +1 -0
  243. package/lib/types/client/types.d.ts +91 -0
  244. package/lib/types/client/types.d.ts.map +1 -0
  245. package/lib/types/client/types.js +2 -0
  246. package/lib/types/client/types.js.map +1 -0
  247. package/lib/types/client/waiting-clock.d.ts +49 -0
  248. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  249. package/lib/types/client/waiting-clock.js +80 -0
  250. package/lib/types/client/waiting-clock.js.map +1 -0
  251. package/lib/types/client/word-motion.d.ts +16 -0
  252. package/lib/types/client/word-motion.d.ts.map +1 -0
  253. package/lib/types/client/word-motion.js +90 -0
  254. package/lib/types/client/word-motion.js.map +1 -0
  255. package/lib/types/client/word-timeline.d.ts +32 -0
  256. package/lib/types/client/word-timeline.d.ts.map +1 -0
  257. package/lib/types/client/word-timeline.js +119 -0
  258. package/lib/types/client/word-timeline.js.map +1 -0
  259. package/lib/types/dsh-better-display.d.ts +17 -0
  260. package/lib/types/dsh-better-display.d.ts.map +1 -0
  261. package/lib/types/dsh-better-display.js +93 -0
  262. package/lib/types/dsh-better-display.js.map +1 -0
  263. package/lib/types/skill-roots.d.ts +22 -0
  264. package/lib/types/skill-roots.d.ts.map +1 -0
  265. package/lib/types/skill-roots.js +126 -0
  266. package/lib/types/skill-roots.js.map +1 -0
  267. package/lib/types/skill-status.d.ts +36 -0
  268. package/lib/types/skill-status.d.ts.map +1 -0
  269. package/lib/types/skill-status.js +51 -0
  270. package/lib/types/skill-status.js.map +1 -0
  271. package/package.json +133 -0
  272. package/scripts/build-motion-fixture.mjs +26 -0
  273. package/scripts/check-harness-compat.mjs +71 -0
  274. package/scripts/link-harness-dependencies.mjs +54 -0
  275. package/scripts/verify-auto-fold.mjs +114 -0
  276. package/scripts/verify-official-bridge.mjs +121 -0
  277. package/scripts/verify-tool-shimmer.mjs +62 -0
  278. package/skills/generative-mcpapps/SKILL.md +47 -0
  279. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  280. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  281. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  282. package/src/client/Blocks.tsx +213 -0
  283. package/src/client/ChoreographedFlow.tsx +232 -0
  284. package/src/client/ClosedProcessSummary.tsx +23 -0
  285. package/src/client/DiffPanel.tsx +124 -0
  286. package/src/client/LiveFold.tsx +43 -0
  287. package/src/client/McpAppFrame.module.css +210 -0
  288. package/src/client/McpAppFrame.tsx +317 -0
  289. package/src/client/OfficialContent.tsx +69 -0
  290. package/src/client/Reader.module.css +692 -0
  291. package/src/client/Reader.tsx +878 -0
  292. package/src/client/ReasoningCard.tsx +306 -0
  293. package/src/client/SettingsSection.module.css +184 -0
  294. package/src/client/SettingsSection.tsx +194 -0
  295. package/src/client/StickyLane.tsx +34 -0
  296. package/src/client/TimelineRail.module.css +170 -0
  297. package/src/client/TimelineRail.tsx +262 -0
  298. package/src/client/ToolActivity.tsx +274 -0
  299. package/src/client/TurnMetrics.module.css +139 -0
  300. package/src/client/TurnMetrics.tsx +160 -0
  301. package/src/client/WaitClock.tsx +33 -0
  302. package/src/client/WaitingStatus.tsx +13 -0
  303. package/src/client/conversation-scroll.ts +28 -0
  304. package/src/client/deliverables.ts +122 -0
  305. package/src/client/entry-policy.ts +20 -0
  306. package/src/client/entry.tsx +40 -0
  307. package/src/client/fold-choreography.ts +55 -0
  308. package/src/client/fold-intensity.ts +36 -0
  309. package/src/client/index.tsx +222 -0
  310. package/src/client/live-turn.ts +198 -0
  311. package/src/client/markdown/MarkdownText.module.css +304 -0
  312. package/src/client/markdown/MarkdownText.tsx +195 -0
  313. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  314. package/src/client/markdown/incremental.ts +131 -0
  315. package/src/client/markdown/katex.tsx +91 -0
  316. package/src/client/markdown/mathCompatibility.ts +350 -0
  317. package/src/client/markdown/parse.ts +45 -0
  318. package/src/client/markdown/render.tsx +647 -0
  319. package/src/client/markdown/upstream.json +14 -0
  320. package/src/client/mcp-app.ts +323 -0
  321. package/src/client/message-chrome.ts +47 -0
  322. package/src/client/motion.tsx +410 -0
  323. package/src/client/native/ContextBody.module.css +161 -0
  324. package/src/client/native/ContextBody.tsx +591 -0
  325. package/src/client/native/ContextInjectionRow.module.css +66 -0
  326. package/src/client/native/ContextInjectionRow.tsx +106 -0
  327. package/src/client/native/ReferenceIcon.tsx +35 -0
  328. package/src/client/native/tool-call-model.ts +248 -0
  329. package/src/client/native/tool-update.ts +32 -0
  330. package/src/client/native/upstream.json +41 -0
  331. package/src/client/official-slots.tsx +211 -0
  332. package/src/client/open-file.ts +94 -0
  333. package/src/client/pending-submission.ts +49 -0
  334. package/src/client/platform-media.ts +17 -0
  335. package/src/client/primitive-labels.ts +54 -0
  336. package/src/client/projection.ts +118 -0
  337. package/src/client/reasoning-follow.ts +28 -0
  338. package/src/client/settings-copy.ts +80 -0
  339. package/src/client/settings-slots.ts +20 -0
  340. package/src/client/settings.tsx +73 -0
  341. package/src/client/skill-status.ts +78 -0
  342. package/src/client/store.ts +75 -0
  343. package/src/client/stream-buffer.ts +136 -0
  344. package/src/client/streaming.tsx +70 -0
  345. package/src/client/style.d.ts +4 -0
  346. package/src/client/timeline.ts +77 -0
  347. package/src/client/tool-activity.ts +298 -0
  348. package/src/client/types.ts +89 -0
  349. package/src/client/waiting-clock.ts +83 -0
  350. package/src/client/word-motion.tsx +92 -0
  351. package/src/client/word-timeline.ts +113 -0
  352. package/src/dsh-better-display.ts +108 -0
  353. package/src/skill-roots.ts +138 -0
  354. package/src/skill-status.ts +81 -0
  355. package/tests/browser-auto-fold.tsx +64 -0
  356. package/tests/browser-closed-gap.tsx +20 -0
  357. package/tests/browser-motion.tsx +65 -0
  358. package/tests/browser-official-bridge.tsx +152 -0
  359. package/tests/browser-sticky-lanes.tsx +22 -0
  360. package/tests/browser-waiting-clock.tsx +14 -0
  361. package/tests/chat-flow-hook.test.ts +66 -0
  362. package/tests/cloud-gate.test.ts +34 -0
  363. package/tests/context-tool-update.test.ts +33 -0
  364. package/tests/conversation-scroll.test.ts +47 -0
  365. package/tests/deliverables.test.ts +128 -0
  366. package/tests/entry.test.ts +42 -0
  367. package/tests/fixtures/maid-composer-hide.html +31 -0
  368. package/tests/fold-choreography.test.ts +54 -0
  369. package/tests/fold-intensity.test.ts +94 -0
  370. package/tests/footer-precedence.test.ts +129 -0
  371. package/tests/history-word-regression.test.ts +16 -0
  372. package/tests/live-turn.test.ts +218 -0
  373. package/tests/mcp-apps.test.ts +157 -0
  374. package/tests/message-chrome.test.ts +29 -0
  375. package/tests/official-slots.test.ts +112 -0
  376. package/tests/open-file.test.ts +114 -0
  377. package/tests/pending-submission.test.ts +45 -0
  378. package/tests/platform-media.test.ts +23 -0
  379. package/tests/projection.test.ts +184 -0
  380. package/tests/reasoning-follow.test.ts +34 -0
  381. package/tests/settings-copy.test.ts +63 -0
  382. package/tests/skill-status.test.ts +137 -0
  383. package/tests/stock-install.test.ts +58 -0
  384. package/tests/stream-buffer.test.ts +102 -0
  385. package/tests/timeline.test.ts +59 -0
  386. package/tests/tool-diff.test.ts +57 -0
  387. package/tests/waiting-clock.test.ts +83 -0
  388. package/tests/word-timeline.test.ts +146 -0
  389. package/tsconfig.json +11 -0
  390. package/tsdown.config.ts +51 -0
@@ -0,0 +1,198 @@
1
+ import type { AssistantChatData, ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
2
+ import type { AssistantBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ import { assistantSegments, hasVisibleBody } from './projection.js';
4
+ import type { TurnBoundary } from './projection.js';
5
+ import { activitySummary, stringValue } from './tool-activity.js';
6
+ import type { ReaderFlowEntry, ToolActivityEntry } from './tool-activity.js';
7
+
8
+ export type LiveStep =
9
+ | { kind: 'reasoning'; key: string; nodeKey: string; start: number; blocks: AssistantBlock[]; step: number }
10
+ | { kind: 'body'; key: string; nodeKey: string; start: number; blocks: AssistantBlock[]; step: number }
11
+ | { kind: 'tool'; key: string; entry: ToolActivityEntry }
12
+ | { kind: 'user'; key: string; nodeKey: string }
13
+ | { kind: 'other'; key: string; nodeKey: string };
14
+
15
+ export type LiveTurnItem =
16
+ | { kind: 'user'; key: string; step: Extract<LiveStep, { kind: 'user' }> }
17
+ | { kind: 'fold'; key: string; steps: readonly LiveStep[]; summary: string }
18
+ | { kind: 'open'; key: string; step: LiveStep };
19
+
20
+ export function liveFoldEnabled(boundary: TurnBoundary): boolean {
21
+ return boundary.status === 'open';
22
+ }
23
+
24
+ export function foldSummary(steps: readonly LiveStep[]): string {
25
+ let reasoning = 0;
26
+ let body = 0;
27
+ let tool = 0;
28
+ let extra = 0;
29
+ for (const step of steps) {
30
+ if (step.kind === 'reasoning') reasoning += 1;
31
+ else if (step.kind === 'body') body += 1;
32
+ else if (step.kind === 'tool') tool += 1;
33
+ else if (step.kind !== 'user') extra += 1;
34
+ }
35
+ const parts: string[] = [];
36
+ if (reasoning) parts.push(`思考×${reasoning}`);
37
+ if (body) parts.push(`输出×${body}`);
38
+ if (tool) parts.push(`工具×${tool}`);
39
+ if (extra) parts.push(`记录×${extra}`);
40
+ return parts.join(' · ') || '此前步骤';
41
+ }
42
+
43
+ /** One chain: fold only when a new reasoning step has prior body/tool/reasoning. */
44
+ export function splitChain(chain: readonly LiveStep[]): { fold: readonly LiveStep[] | null; open: readonly LiveStep[] } {
45
+ const lastReasoning = chain.findLastIndex(step => step.kind === 'reasoning');
46
+ if (lastReasoning < 0) return { fold: null, open: chain };
47
+ const prior = chain.slice(0, lastReasoning);
48
+ if (!prior.length) return { fold: null, open: chain };
49
+ const trigger = prior.some(step => step.kind === 'reasoning' || step.kind === 'body' || step.kind === 'tool');
50
+ if (!trigger) return { fold: null, open: chain };
51
+ return { fold: prior, open: chain.slice(lastReasoning) };
52
+ }
53
+
54
+ function skipReasoning(part: { kind: string; blocks: AssistantBlock[] }): boolean {
55
+ return part.kind === 'reasoning' && !part.blocks.some(block => block.kind === 'reasoning' && block.text.trim() !== '');
56
+ }
57
+
58
+ function skipBody(part: { kind: string; blocks: AssistantBlock[] }): boolean {
59
+ return part.kind === 'body' && !hasVisibleBody(part.blocks);
60
+ }
61
+
62
+ function stepsFromAssistant(
63
+ nodeKey: string,
64
+ data: AssistantChatData,
65
+ toolsByCallId: Map<string, ToolActivityEntry>,
66
+ consumed: Set<string>,
67
+ ): LiveStep[] {
68
+ const marks: { at: number; step: LiveStep }[] = [];
69
+ for (const part of assistantSegments(data.blocks)) {
70
+ if (skipReasoning(part) || skipBody(part)) continue;
71
+ marks.push({
72
+ at: part.start,
73
+ step: {
74
+ kind: part.kind,
75
+ key: `${nodeKey}:${part.kind}:${part.start}`,
76
+ nodeKey,
77
+ start: part.start,
78
+ blocks: part.blocks,
79
+ step: data.step,
80
+ },
81
+ });
82
+ }
83
+ data.blocks.forEach((block, index) => {
84
+ if (block.kind !== 'tool-call' || !block.callId) return;
85
+ const tool = toolsByCallId.get(block.callId);
86
+ if (!tool || consumed.has(tool.callId)) return;
87
+ consumed.add(tool.callId);
88
+ marks.push({ at: index, step: { kind: 'tool', key: tool.key, entry: tool } });
89
+ });
90
+ marks.sort((left, right) => left.at - right.at || left.step.key.localeCompare(right.step.key));
91
+ return marks.map(mark => mark.step);
92
+ }
93
+
94
+ /** Expand readerFlow into source-ordered live steps using existing block boundaries. */
95
+ export function segmentLiveTurn(
96
+ flow: readonly ReaderFlowEntry[],
97
+ get: (key: string) => ChatConversationViewNode | undefined,
98
+ ): LiveStep[] {
99
+ const steps: LiveStep[] = [];
100
+ const consumed = new Set<string>();
101
+ const toolsByCallId = new Map<string, ToolActivityEntry>();
102
+ for (const entry of flow) {
103
+ if (entry.kind === 'tool') toolsByCallId.set(entry.callId, entry);
104
+ }
105
+ for (const entry of flow) {
106
+ if (entry.kind === 'tool') {
107
+ if (!consumed.has(entry.callId)) {
108
+ steps.push({ kind: 'tool', key: entry.key, entry });
109
+ consumed.add(entry.callId);
110
+ }
111
+ continue;
112
+ }
113
+ const node = get(entry.nodeKey);
114
+ if (!node || node.visibility === 'hidden' || node.kind === 'turn-tail') continue;
115
+ if (node.kind === 'user' || node.kind === 'steering') {
116
+ steps.push({ kind: 'user', key: entry.key, nodeKey: entry.nodeKey });
117
+ continue;
118
+ }
119
+ if (node.kind === 'assistant-step') {
120
+ steps.push(...stepsFromAssistant(entry.nodeKey, node.data as AssistantChatData, toolsByCallId, consumed));
121
+ continue;
122
+ }
123
+ steps.push({ kind: 'other', key: entry.key, nodeKey: entry.nodeKey });
124
+ }
125
+ return steps;
126
+ }
127
+
128
+ /** One finished tool call, described by what it actually did. */
129
+ function toolSummary(entry: ToolActivityEntry): string {
130
+ const info = activitySummary(entry);
131
+ const clip = (value: string | undefined, max = 38): string | undefined => {
132
+ if (!value) return undefined;
133
+ const flat = value.replace(/\s+/gu, ' ').trim();
134
+ if (!flat) return undefined;
135
+ return flat.length > max ? flat.slice(0, max - 1) + '\u2026' : flat;
136
+ };
137
+ const base = info.target ? clip(info.target.split(/[/\\]/u).at(-1), 28) : undefined;
138
+ const said = clip(stringValue(info.args, 'description'), 40);
139
+ switch (info.category) {
140
+ case 'read': return '读取 ' + (base ?? '文件');
141
+ case 'write': return clip(info.title, 40) ?? '写入文件';
142
+ case 'terminal': {
143
+ const cmd = clip(info.command, 32);
144
+ return said ?? (cmd ? '运行 ' + cmd : '运行命令');
145
+ }
146
+ case 'search': {
147
+ const needle = clip(stringValue(info.args, 'pattern', 'query'), 26);
148
+ return info.name === 'glob' ? '查找 ' + (needle ?? '文件') : '搜索 ' + (needle ?? '内容');
149
+ }
150
+ case 'web': {
151
+ const query = clip(info.target, 30);
152
+ return (info.name === 'web_search' ? '搜索网页' : '读取网页') + (query ? ' ' + query : '');
153
+ }
154
+ default: {
155
+ // Schema-less tools (run_code and friends) describe themselves in an
156
+ // argument rather than a known field: prefer the model's own
157
+ // description, then the first meaningful line of the code it ran.
158
+ if (said) return said;
159
+ const code = stringValue(info.args, 'code', 'source', 'script');
160
+ if (code) {
161
+ const line = code.split('\n').map(part => part.trim())
162
+ .find(part => part && !/^[)\]}]/.test(part) && !/^(\/\/|\*|\/\*|#)/.test(part));
163
+ if (line) return clip(line.replace(/\s+/gu, ' '), 44) ?? '运行代码';
164
+ }
165
+ return clip(info.title ?? info.name, 32) ?? '工具调用';
166
+ }
167
+ }
168
+ }
169
+
170
+ export function presentLiveTurn(steps: readonly LiveStep[], boundary: TurnBoundary, autoFold = true): LiveTurnItem[] {
171
+ const live = autoFold && liveFoldEnabled(boundary);
172
+ const items: LiveTurnItem[] = [];
173
+ let chain: LiveStep[] = [];
174
+ const flush = () => {
175
+ if (!chain.length) return;
176
+ if (!live) {
177
+ for (const step of chain) items.push({ kind: 'open', key: step.key, step });
178
+ chain = [];
179
+ return;
180
+ }
181
+ const { fold, open } = splitChain(chain);
182
+ if (fold?.length) {
183
+ items.push({ kind: 'fold', key: `live-fold:${chain[0]!.key}`, steps: fold, summary: foldSummary(fold) });
184
+ }
185
+ for (const step of open) items.push({ kind: 'open', key: step.key, step });
186
+ chain = [];
187
+ };
188
+ for (const step of steps) {
189
+ if (step.kind === 'user') {
190
+ flush();
191
+ items.push({ kind: 'user', key: step.key, step });
192
+ } else {
193
+ chain.push(step);
194
+ }
195
+ }
196
+ flush();
197
+ return items;
198
+ }
@@ -0,0 +1,304 @@
1
+ /* Visual baseline: deepsuite `@deepseek/md` markdown.css, adapted to CSS
2
+ Modules. Cite pills, header anchors, and thinking-small variants are
3
+ intentionally absent (no matching DOM). Token names match that sheet. */
4
+
5
+ .markdown {
6
+ min-width: 0;
7
+ overflow-wrap: anywhere;
8
+ font: var(--dsw-font-markdown-base);
9
+ color: var(--dsw-alias-label-primary);
10
+ }
11
+
12
+ .markdown strong {
13
+ font-weight: 600;
14
+ }
15
+
16
+ .markdown h1 {
17
+ font: var(--dsw-font-markdown-h1);
18
+ margin: 32px 0 16px;
19
+ }
20
+
21
+ .markdown h2 {
22
+ font: var(--dsw-font-markdown-h2);
23
+ margin: 32px 0 16px;
24
+ }
25
+
26
+ .markdown h3 {
27
+ font: var(--dsw-font-markdown-h3);
28
+ margin: 32px 0 16px;
29
+ }
30
+
31
+ .markdown h4 {
32
+ font: var(--dsw-font-markdown-h4);
33
+ margin: 16px 0;
34
+ }
35
+
36
+ .markdown :where(h5, h6) {
37
+ font: var(--dsw-font-markdown-base-strong);
38
+ margin: 16px 0;
39
+ }
40
+
41
+ .markdown :where(h1, h2, h3, h4, h5, h6) strong {
42
+ font-weight: inherit;
43
+ }
44
+
45
+ .markdown p {
46
+ margin: 16px 0;
47
+ }
48
+
49
+ /* Tighten h4–h6 against a following list (design: 8px gap). */
50
+ .markdown :where(h4, h5, h6) + :where(ul, ol) {
51
+ margin-top: 8px;
52
+ }
53
+
54
+ .markdown :where(h4, h5, h6):has(+ :where(ul, ol)) {
55
+ margin-bottom: 8px;
56
+ }
57
+
58
+ .markdown a {
59
+ /* deepsuite markdown.css uses brand-text (blue in newDesign); this sheet
60
+ keeps design-platform brand-text as near-black, so links use the blue
61
+ business-primary alias instead. */
62
+ color: var(--dsw-alias-state-business-primary);
63
+ transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
64
+ position: relative;
65
+ text-decoration: none;
66
+ /* Transparent hit-area padding; literal zero-alpha only (no painted color). */
67
+ border-left: 3px solid rgb(255 255 255 / 0);
68
+ border-right: 3px solid rgb(255 255 255 / 0);
69
+ border-top: 2px solid rgb(255 255 255 / 0);
70
+ border-bottom: 2px solid rgb(255 255 255 / 0);
71
+ margin-left: -3px;
72
+ margin-right: -3px;
73
+ }
74
+
75
+ .markdown a:hover,
76
+ .markdown a:focus {
77
+ outline: none;
78
+ text-decoration: underline var(--dsw-alias-state-business-primary);
79
+ }
80
+
81
+ .markdown a:focus-visible {
82
+ box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
83
+ }
84
+
85
+ .markdown :where(ul, ol) {
86
+ margin: 16px 0;
87
+ padding-left: 18px;
88
+ }
89
+
90
+ .markdown li:not(:first-child) {
91
+ margin-top: 6px;
92
+ }
93
+
94
+ .markdown li > :where(ul, ol) {
95
+ margin-top: 4px;
96
+ }
97
+
98
+ .markdown li::marker {
99
+ line-height: 28px;
100
+ color: var(--dsw-alias-label-secondary);
101
+ }
102
+
103
+ /* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */
104
+ .markdown :where(ul, ol) ol {
105
+ list-style-position: inside;
106
+ padding-left: 0;
107
+ }
108
+
109
+ .markdown :where(ul, ol) ol li p {
110
+ display: inline;
111
+ }
112
+
113
+ .markdown li > p {
114
+ margin: 8px 0;
115
+ }
116
+
117
+ .markdown li > *:first-child {
118
+ margin-top: 0;
119
+ }
120
+
121
+ /* Keep list-nested code-block vertical margins (design: +4px vs other last children). */
122
+ .markdown li > *:last-child:not(:global(.md-code-block)) {
123
+ margin-bottom: 0;
124
+ }
125
+
126
+ .markdown hr {
127
+ display: block;
128
+ border: none;
129
+ height: 1px;
130
+ margin: 32px 0;
131
+ background: var(--dsw-alias-border-l2);
132
+ }
133
+
134
+ .markdown blockquote {
135
+ border-left: 2px solid var(--dsw-alias-label-caption);
136
+ margin: 16px 0 0;
137
+ padding-left: 14px;
138
+ }
139
+
140
+ .markdown pre {
141
+ margin: 16px 0;
142
+ font-family: var(--ds-font-family-code);
143
+ overflow: auto;
144
+ }
145
+
146
+ .markdown :not(pre) > code {
147
+ display: inline-flex;
148
+ align-items: center;
149
+ box-sizing: border-box;
150
+ font: var(--dsw-font-markdown-code);
151
+ font-family: var(--ds-font-family-code);
152
+ font-size: 0.875em !important;
153
+ background-color: var(--dsw-alias-markdown-inline-code);
154
+ border-radius: 6px;
155
+ padding: 0 5px;
156
+ }
157
+
158
+ .markdown :where(h1, h2, h3, h4, h5, h6) code {
159
+ font: inherit;
160
+ font-family: var(--ds-font-family-code);
161
+ }
162
+
163
+ .markdown :global(.katex-display) {
164
+ max-width: 100%;
165
+ overflow-x: auto;
166
+ overflow-y: hidden;
167
+ }
168
+
169
+ .markdown input[type='checkbox'] {
170
+ margin: 0 8px 0 0;
171
+ accent-color: var(--dsw-alias-label-secondary);
172
+ }
173
+
174
+ .tableScroll {
175
+ max-width: 100%;
176
+ overflow-x: auto;
177
+ overscroll-behavior-x: contain;
178
+ }
179
+
180
+ /* Wide tables reveal their horizontal bar on hover (or keyboard focus)
181
+ instead of keeping it painted. Chromium never repaints state-conditioned
182
+ scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules
183
+ nor a :hover `scrollbar-color` change reaches the painted bar), so the
184
+ toggle is `overflow-x` itself — a layout change repaints reliably. The
185
+ resting padding matches the themed bar's height, so on an overflowing
186
+ table the appearing bar exactly replaces it and nothing below shifts.
187
+ Wheel and trackpad scrolling need the pointer over the table, which is
188
+ already the hover that re-enables `auto`. */
189
+ .tableScroll:global(.md-table-wide) {
190
+ overflow-x: hidden;
191
+ padding-bottom: var(--dsh-scrollbar-width, 8px);
192
+ }
193
+
194
+ .tableScroll:global(.md-table-wide):hover,
195
+ .tableScroll:global(.md-table-wide):focus-visible {
196
+ overflow-x: auto;
197
+ padding-bottom: 0;
198
+ }
199
+
200
+ /* Chromium keyboard-focuses scrollable containers by default; the ring uses
201
+ the sheet's link focus color. */
202
+ .tableScroll:focus-visible {
203
+ outline: none;
204
+ box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
205
+ }
206
+
207
+ /* Many-column tables (the renderer's `md-table-wide` hook) keep their
208
+ natural width and scroll inside the wrapper; a hosting layout may widen
209
+ the wrapper through the hook (deepsuite chat TableWrapper parity). */
210
+ .tableScroll table {
211
+ border-collapse: collapse;
212
+ width: max-content;
213
+ max-width: max-content;
214
+ }
215
+
216
+ /* Tables under four columns, and any table inside a blockquote, fill the
217
+ column instead: cells wrap down to their minimum readable width, and the
218
+ wrapper's scroll only remains for the below-floor case (deepsuite chat
219
+ parity: `.wrapper:not(:has(th:nth-child(4), td:nth-child(4)))`). */
220
+ .tableFill table {
221
+ width: 100%;
222
+ max-width: none;
223
+ }
224
+
225
+ .tableScroll th {
226
+ text-align: start;
227
+ padding: 10px 16px;
228
+ border-bottom: 1px solid var(--dsw-alias-border-l3);
229
+ border-top: none;
230
+ font: var(--dsw-font-markdown-table-head);
231
+ max-width: 320px;
232
+ max-width: min(30vw, 320px);
233
+ min-width: 100px;
234
+ }
235
+
236
+ .tableScroll td {
237
+ padding: 10px 16px;
238
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
239
+ font: var(--dsw-font-markdown-table);
240
+ max-width: 320px;
241
+ max-width: min(30vw, 320px);
242
+ min-width: 100px;
243
+ }
244
+
245
+ .tableScroll th:first-child,
246
+ .tableScroll td:first-child {
247
+ padding-left: 0;
248
+ }
249
+
250
+ .tableScroll td:last-child {
251
+ padding-right: 0;
252
+ }
253
+
254
+ .tableScroll table code {
255
+ font-size: 13px;
256
+ }
257
+
258
+ .markdown > *:first-child,
259
+ .markdown p:first-child {
260
+ margin-top: 0 !important;
261
+ }
262
+
263
+ .markdown > *:last-child,
264
+ .markdown p:last-child {
265
+ margin-bottom: 0 !important;
266
+ }
267
+
268
+ .imageAlt {
269
+ color: var(--dsw-alias-label-tertiary);
270
+ font-style: italic;
271
+ }
272
+
273
+ .image {
274
+ display: block;
275
+ width: auto;
276
+ max-width: 100%;
277
+ height: auto;
278
+ margin: 0;
279
+ border-radius: 8px;
280
+ background: var(--dsw-alias-bg-base);
281
+ object-fit: contain;
282
+ }
283
+
284
+ /* Inline file mention: the same link language this sheet gives anchors (and
285
+ thereby URL-promoted inline code) — link-blue at rest, underline only on
286
+ hover/focus. An underline at rest reads badly inside the code chip, where
287
+ it collides with monospace descenders and the pill background. */
288
+ .fileMention {
289
+ margin: 0;
290
+ padding: 0;
291
+ border: none;
292
+ background: none;
293
+ font: inherit;
294
+ color: var(--dsw-alias-state-business-primary);
295
+ text-decoration: none;
296
+ cursor: pointer;
297
+ }
298
+
299
+ .fileMention:hover,
300
+ .fileMention:focus {
301
+ outline: none;
302
+ text-decoration: underline var(--dsw-alias-state-business-primary);
303
+ text-underline-offset: 3px;
304
+ }
@@ -0,0 +1,195 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /**
3
+ * Untrusted assistant-Markdown renderer over the direct mdast pipeline:
4
+ * `parse.ts` grammars, the incremental streaming parser, and `render.tsx`.
5
+ * While a message streams, all but the trailing two blocks freeze as cached
6
+ * React elements and only the source tail behind them re-parses per chunk,
7
+ * so per-chunk work tracks the tail size instead of the whole reply. Frozen
8
+ * blocks keep their source-offset keys when they cross the freeze boundary,
9
+ * so React reconciles instead of remounting. Known deviation while
10
+ * streaming: a reference-style link or footnote whose definition sits on the
11
+ * other side of the freeze boundary renders literally until the settled
12
+ * full parse self-heals it.
13
+ */
14
+
15
+ import { memo, useMemo, useRef } from 'react'
16
+ import type { ReactNode } from 'react'
17
+ import type { MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives'
18
+ import { IncrementalMarkdownParser } from './incremental.js'
19
+ import { parseGfm, parseGfmWithMath } from './parse.js'
20
+ import {
21
+ collectReferenceTargets, createReferenceTargets, renderBlocks, renderFootnoteSection,
22
+ wrapBlockChildren,
23
+ } from './render.js'
24
+ import type { MarkdownCodeLabels, MarkdownFileMentions, MarkdownRenderContext, ReferenceTargets } from './render.js'
25
+ // KaTeX CSS is supplied once by the shared DSH UI-primitives client bundle.
26
+ import css from './MarkdownText.module.css'
27
+
28
+ export type { MarkdownCodeLabels, MarkdownFileMentions } from './render.js'
29
+
30
+ /** One settled full render: parse with math, resolve references, append the footnote section. */
31
+ function renderSettled(
32
+ text: string,
33
+ codeLabels: MarkdownCodeLabels | undefined,
34
+ fileMentions: MarkdownFileMentions | undefined,
35
+ pathImages: MarkdownPathImages | undefined,
36
+ renderText: MarkdownRenderContext['renderText'],
37
+ renderAtom: MarkdownRenderContext['renderAtom'],
38
+ ): ReactNode[] {
39
+ const root = parseGfmWithMath(text)
40
+ const targets = createReferenceTargets()
41
+ collectReferenceTargets(root.children, targets)
42
+ const context: MarkdownRenderContext = {
43
+ renderText,
44
+ renderAtom,
45
+ streaming: false,
46
+ codeLabels,
47
+ fileMentions,
48
+ pathImages,
49
+ targets,
50
+ footnoteOrder: [],
51
+ footnoteCounts: new Map(),
52
+ }
53
+ const blocks = wrapBlockChildren(
54
+ renderBlocks(root.children.map((node, index) => ({ node, key: node.position?.start.offset ?? -(index + 1) })), context),
55
+ false,
56
+ )
57
+ const section = renderFootnoteSection(context)
58
+ return section === null ? blocks : [...blocks, '\n', section]
59
+ }
60
+
61
+ /**
62
+ * Streaming render state for one growing message: the incremental parser,
63
+ * the frozen blocks' cached elements, and the reference/footnote state their
64
+ * rendering consumed (footnote numbering assigned to frozen references is
65
+ * final, so the tail continues from a copy of it each frame).
66
+ */
67
+ class StreamingRenderer {
68
+ private readonly parser = new IncrementalMarkdownParser(parseGfm)
69
+ private generation = -1
70
+ private frozenCount = 0
71
+ private frozenElements: ReactNode[] = []
72
+ private frozenTargets: ReferenceTargets = createReferenceTargets()
73
+ private frozenFootnoteOrder: string[] = []
74
+ private frozenFootnoteCounts = new Map<string, number>()
75
+ private lastText: string | null = null
76
+ private lastRendered: ReactNode[] = []
77
+
78
+ /** @param codeLabels - Fence copy labels baked into cached elements; the owner replaces the renderer when they change. */
79
+ constructor(private readonly codeLabels: MarkdownCodeLabels | undefined, private readonly renderText: MarkdownRenderContext['renderText'], private readonly renderAtom: MarkdownRenderContext['renderAtom'], private readonly pathImages: MarkdownPathImages | undefined) {}
80
+
81
+ /**
82
+ * Render the current accumulated text. Idempotent per text value, so React
83
+ * may re-execute the calling render freely.
84
+ * @param text - The full accumulated markdown source.
85
+ * @returns Frozen elements, re-rendered tail, and the footnote section.
86
+ */
87
+ render(text: string): ReactNode[] {
88
+ if (text === this.lastText) return this.lastRendered
89
+ const { frozen, tail, generation } = this.parser.update(text)
90
+ if (generation !== this.generation) {
91
+ this.generation = generation
92
+ this.frozenCount = 0
93
+ this.frozenElements = []
94
+ this.frozenTargets = createReferenceTargets()
95
+ this.frozenFootnoteOrder = []
96
+ this.frozenFootnoteCounts = new Map()
97
+ }
98
+ const newlyFrozen = frozen.slice(this.frozenCount)
99
+ collectReferenceTargets(newlyFrozen.map(block => block.node), this.frozenTargets)
100
+ // Targets visible this frame: everything frozen so far plus the current
101
+ // tail parse — a newly frozen block's references resolved against the
102
+ // same parse tree its definitions came from.
103
+ const frameTargets: ReferenceTargets = {
104
+ definitions: new Map(this.frozenTargets.definitions),
105
+ footnotes: new Map(this.frozenTargets.footnotes),
106
+ }
107
+ collectReferenceTargets(tail.map(block => block.node), frameTargets)
108
+ if (newlyFrozen.length > 0) {
109
+ const frozenContext: MarkdownRenderContext = {
110
+ renderText: this.renderText,
111
+ renderAtom: this.renderAtom,
112
+ streaming: true,
113
+ codeLabels: this.codeLabels,
114
+ fileMentions: undefined,
115
+ pathImages: this.pathImages,
116
+ targets: frameTargets,
117
+ footnoteOrder: this.frozenFootnoteOrder,
118
+ footnoteCounts: this.frozenFootnoteCounts,
119
+ }
120
+ // Separator newlines are cached alongside the elements so the
121
+ // assembled children match the settled pipeline's block wrapping.
122
+ const batch = [...this.frozenElements]
123
+ for (const element of renderBlocks(newlyFrozen, frozenContext)) {
124
+ if (batch.length > 0) batch.push('\n')
125
+ batch.push(element)
126
+ }
127
+ this.frozenElements = batch
128
+ this.frozenCount = frozen.length
129
+ }
130
+ const tailContext: MarkdownRenderContext = {
131
+ renderText: this.renderText,
132
+ renderAtom: this.renderAtom,
133
+ streaming: true,
134
+ codeLabels: this.codeLabels,
135
+ fileMentions: undefined,
136
+ pathImages: this.pathImages,
137
+ targets: frameTargets,
138
+ footnoteOrder: [...this.frozenFootnoteOrder],
139
+ footnoteCounts: new Map(this.frozenFootnoteCounts),
140
+ }
141
+ const children = [...this.frozenElements]
142
+ for (const element of renderBlocks(tail, tailContext)) {
143
+ if (children.length > 0) children.push('\n')
144
+ children.push(element)
145
+ }
146
+ const section = renderFootnoteSection(tailContext)
147
+ if (section !== null) children.push('\n', section)
148
+ this.lastText = text
149
+ this.lastRendered = children
150
+ return this.lastRendered
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Render untrusted assistant-authored Markdown as semantic React elements.
156
+ * @param props - Markdown source text preserved by the session projection;
157
+ * `streaming` renders fences and TeX plain (highlighting and KaTeX land on
158
+ * the finalize swap) and parses incrementally across chunks; `codeLabels`
159
+ * forwards localized copy-button labels to fence CodeBlocks — pass a
160
+ * reference-stable object (memoized per locale revision), because a new
161
+ * identity discards the streaming render cache mid-message. `fileMentions`
162
+ * links inline-code tokens its resolver recognizes as real files; this is
163
+ * the single streaming gate — it applies to settled renders only, because a
164
+ * streaming message's vocabulary is not final and frozen cached elements
165
+ * must not bake in handlers that could go stale.
166
+ * @returns A GFM document with TeX math rendered through KaTeX; raw HTML,
167
+ * relative links, and unsafe protocols are disabled, while absolute HTTP(S)
168
+ * images render directly.
169
+ */
170
+ export const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels, fileMentions, pathImages, renderText, renderAtom }: {
171
+ text: string
172
+ streaming?: boolean
173
+ codeLabels?: MarkdownCodeLabels | undefined
174
+ fileMentions?: MarkdownFileMentions | undefined
175
+ pathImages?: MarkdownPathImages | undefined
176
+ renderText?: MarkdownRenderContext['renderText']
177
+ renderAtom?: MarkdownRenderContext['renderAtom']
178
+ }) {
179
+ const streamRef = useRef<StreamingRenderer | null>(null)
180
+ const streamImagesRef = useRef(pathImages)
181
+ const streamLabelsRef = useRef<MarkdownCodeLabels | undefined>(codeLabels)
182
+ const children = useMemo(() => {
183
+ if (!streaming) {
184
+ streamRef.current = null
185
+ return renderSettled(text, codeLabels, fileMentions, pathImages, renderText, renderAtom)
186
+ }
187
+ if (streamRef.current === null || streamLabelsRef.current !== codeLabels || streamImagesRef.current !== pathImages) {
188
+ streamRef.current = new StreamingRenderer(codeLabels, renderText, renderAtom, pathImages)
189
+ streamLabelsRef.current = codeLabels
190
+ streamImagesRef.current = pathImages
191
+ }
192
+ return streamRef.current.render(text)
193
+ }, [text, streaming, codeLabels, fileMentions, pathImages, renderText, renderAtom])
194
+ return <div className={css.markdown}>{children}</div>
195
+ })