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,187 @@
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 { IncrementalMarkdownParser } from './incremental.js'
18
+ import { parseGfm, parseGfmWithMath } from './parse.js'
19
+ import {
20
+ collectReferenceTargets, createReferenceTargets, renderBlocks, renderFootnoteSection,
21
+ wrapBlockChildren,
22
+ } from './render.js'
23
+ import type { MarkdownCodeLabels, MarkdownFileMentions, MarkdownRenderContext, ReferenceTargets } from './render.js'
24
+ // KaTeX CSS is supplied once by the shared DSH UI-primitives client bundle.
25
+ import css from './MarkdownText.module.css'
26
+
27
+ export type { MarkdownCodeLabels, MarkdownFileMentions } from './render.js'
28
+
29
+ /** One settled full render: parse with math, resolve references, append the footnote section. */
30
+ function renderSettled(
31
+ text: string,
32
+ codeLabels: MarkdownCodeLabels | undefined,
33
+ fileMentions: MarkdownFileMentions | undefined,
34
+ renderText: MarkdownRenderContext['renderText'],
35
+ renderAtom: MarkdownRenderContext['renderAtom'],
36
+ ): ReactNode[] {
37
+ const root = parseGfmWithMath(text)
38
+ const targets = createReferenceTargets()
39
+ collectReferenceTargets(root.children, targets)
40
+ const context: MarkdownRenderContext = {
41
+ renderText,
42
+ renderAtom,
43
+ streaming: false,
44
+ codeLabels,
45
+ fileMentions,
46
+ targets,
47
+ footnoteOrder: [],
48
+ footnoteCounts: new Map(),
49
+ }
50
+ const blocks = wrapBlockChildren(
51
+ renderBlocks(root.children.map((node, index) => ({ node, key: node.position?.start.offset ?? -(index + 1) })), context),
52
+ false,
53
+ )
54
+ const section = renderFootnoteSection(context)
55
+ return section === null ? blocks : [...blocks, '\n', section]
56
+ }
57
+
58
+ /**
59
+ * Streaming render state for one growing message: the incremental parser,
60
+ * the frozen blocks' cached elements, and the reference/footnote state their
61
+ * rendering consumed (footnote numbering assigned to frozen references is
62
+ * final, so the tail continues from a copy of it each frame).
63
+ */
64
+ class StreamingRenderer {
65
+ private readonly parser = new IncrementalMarkdownParser(parseGfm)
66
+ private generation = -1
67
+ private frozenCount = 0
68
+ private frozenElements: ReactNode[] = []
69
+ private frozenTargets: ReferenceTargets = createReferenceTargets()
70
+ private frozenFootnoteOrder: string[] = []
71
+ private frozenFootnoteCounts = new Map<string, number>()
72
+ private lastText: string | null = null
73
+ private lastRendered: ReactNode[] = []
74
+
75
+ /** @param codeLabels - Fence copy labels baked into cached elements; the owner replaces the renderer when they change. */
76
+ constructor(private readonly codeLabels: MarkdownCodeLabels | undefined, private readonly renderText: MarkdownRenderContext['renderText'], private readonly renderAtom: MarkdownRenderContext['renderAtom']) {}
77
+
78
+ /**
79
+ * Render the current accumulated text. Idempotent per text value, so React
80
+ * may re-execute the calling render freely.
81
+ * @param text - The full accumulated markdown source.
82
+ * @returns Frozen elements, re-rendered tail, and the footnote section.
83
+ */
84
+ render(text: string): ReactNode[] {
85
+ if (text === this.lastText) return this.lastRendered
86
+ const { frozen, tail, generation } = this.parser.update(text)
87
+ if (generation !== this.generation) {
88
+ this.generation = generation
89
+ this.frozenCount = 0
90
+ this.frozenElements = []
91
+ this.frozenTargets = createReferenceTargets()
92
+ this.frozenFootnoteOrder = []
93
+ this.frozenFootnoteCounts = new Map()
94
+ }
95
+ const newlyFrozen = frozen.slice(this.frozenCount)
96
+ collectReferenceTargets(newlyFrozen.map(block => block.node), this.frozenTargets)
97
+ // Targets visible this frame: everything frozen so far plus the current
98
+ // tail parse — a newly frozen block's references resolved against the
99
+ // same parse tree its definitions came from.
100
+ const frameTargets: ReferenceTargets = {
101
+ definitions: new Map(this.frozenTargets.definitions),
102
+ footnotes: new Map(this.frozenTargets.footnotes),
103
+ }
104
+ collectReferenceTargets(tail.map(block => block.node), frameTargets)
105
+ if (newlyFrozen.length > 0) {
106
+ const frozenContext: MarkdownRenderContext = {
107
+ renderText: this.renderText,
108
+ renderAtom: this.renderAtom,
109
+ streaming: true,
110
+ codeLabels: this.codeLabels,
111
+ fileMentions: undefined,
112
+ targets: frameTargets,
113
+ footnoteOrder: this.frozenFootnoteOrder,
114
+ footnoteCounts: this.frozenFootnoteCounts,
115
+ }
116
+ // Separator newlines are cached alongside the elements so the
117
+ // assembled children match the settled pipeline's block wrapping.
118
+ const batch = [...this.frozenElements]
119
+ for (const element of renderBlocks(newlyFrozen, frozenContext)) {
120
+ if (batch.length > 0) batch.push('\n')
121
+ batch.push(element)
122
+ }
123
+ this.frozenElements = batch
124
+ this.frozenCount = frozen.length
125
+ }
126
+ const tailContext: MarkdownRenderContext = {
127
+ renderText: this.renderText,
128
+ renderAtom: this.renderAtom,
129
+ streaming: true,
130
+ codeLabels: this.codeLabels,
131
+ fileMentions: undefined,
132
+ targets: frameTargets,
133
+ footnoteOrder: [...this.frozenFootnoteOrder],
134
+ footnoteCounts: new Map(this.frozenFootnoteCounts),
135
+ }
136
+ const children = [...this.frozenElements]
137
+ for (const element of renderBlocks(tail, tailContext)) {
138
+ if (children.length > 0) children.push('\n')
139
+ children.push(element)
140
+ }
141
+ const section = renderFootnoteSection(tailContext)
142
+ if (section !== null) children.push('\n', section)
143
+ this.lastText = text
144
+ this.lastRendered = children
145
+ return this.lastRendered
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Render untrusted assistant-authored Markdown as semantic React elements.
151
+ * @param props - Markdown source text preserved by the session projection;
152
+ * `streaming` renders fences and TeX plain (highlighting and KaTeX land on
153
+ * the finalize swap) and parses incrementally across chunks; `codeLabels`
154
+ * forwards localized copy-button labels to fence CodeBlocks — pass a
155
+ * reference-stable object (memoized per locale revision), because a new
156
+ * identity discards the streaming render cache mid-message. `fileMentions`
157
+ * links inline-code tokens its resolver recognizes as real files; this is
158
+ * the single streaming gate — it applies to settled renders only, because a
159
+ * streaming message's vocabulary is not final and frozen cached elements
160
+ * must not bake in handlers that could go stale.
161
+ * @returns A GFM document with TeX math rendered through KaTeX; raw HTML,
162
+ * relative links, and unsafe protocols are disabled, while absolute HTTP(S)
163
+ * images render directly.
164
+ */
165
+ export const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels, fileMentions, renderText, renderAtom }: {
166
+ text: string
167
+ streaming?: boolean
168
+ codeLabels?: MarkdownCodeLabels | undefined
169
+ fileMentions?: MarkdownFileMentions | undefined
170
+ renderText?: MarkdownRenderContext['renderText']
171
+ renderAtom?: MarkdownRenderContext['renderAtom']
172
+ }) {
173
+ const streamRef = useRef<StreamingRenderer | null>(null)
174
+ const streamLabelsRef = useRef<MarkdownCodeLabels | undefined>(codeLabels)
175
+ const children = useMemo(() => {
176
+ if (!streaming) {
177
+ streamRef.current = null
178
+ return renderSettled(text, codeLabels, fileMentions, renderText, renderAtom)
179
+ }
180
+ if (streamRef.current === null || streamLabelsRef.current !== codeLabels) {
181
+ streamRef.current = new StreamingRenderer(codeLabels, renderText, renderAtom)
182
+ streamLabelsRef.current = codeLabels
183
+ }
184
+ return streamRef.current.render(text)
185
+ }, [text, streaming, codeLabels, fileMentions, renderText, renderAtom])
186
+ return <div className={css.markdown}>{children}</div>
187
+ })
@@ -0,0 +1,84 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /** Let asterisk strong emphasis close after punctuation when CJK prose continues without whitespace. */
3
+
4
+ import { attention } from 'micromark-core-commonmark'
5
+ import { unicodePunctuation } from 'micromark-util-character'
6
+ import { classifyCharacter } from 'micromark-util-classify-character'
7
+ import { codes, constants } from 'micromark-util-symbol'
8
+ import type { Construct, Extension, State, Tokenizer } from 'micromark-util-types'
9
+
10
+ const cjkCharacter = new RegExp([
11
+ '\\p{Script_Extensions=Han}',
12
+ '\\p{Script_Extensions=Hiragana}',
13
+ '\\p{Script_Extensions=Katakana}',
14
+ '\\p{Script_Extensions=Hangul}',
15
+ '\\p{Script_Extensions=Bopomofo}',
16
+ ].join('|'), 'u')
17
+
18
+ function isCjkCharacter(code: number | null): boolean {
19
+ return code !== null && code >= 0 && cjkCharacter.test(String.fromCodePoint(code))
20
+ }
21
+
22
+ const tokenizeCjkFriendlyAttention: Tokenizer = function (effects, ok, nok) {
23
+ const configuredAttentionMarkers = this.parser.constructs.attentionMarkers.null
24
+ if (configuredAttentionMarkers === undefined) {
25
+ throw new Error('micromark CommonMark attention markers are unavailable')
26
+ }
27
+ const attentionMarkers = configuredAttentionMarkers
28
+ const previous = this.previous
29
+ const before = classifyCharacter(previous)
30
+ let marker: number | null = codes.eof
31
+
32
+ return start
33
+
34
+ function start(code: number | null): State | undefined {
35
+ /* v8 ignore next -- this text construct is dispatched only for an asterisk. */
36
+ if (code !== codes.asterisk) return nok(code)
37
+ marker = code
38
+ effects.enter('attentionSequence')
39
+ return inside(code)
40
+ }
41
+
42
+ function inside(code: number | null): State | undefined {
43
+ if (code === marker) {
44
+ effects.consume(code)
45
+ return inside
46
+ }
47
+
48
+ const token = effects.exit('attentionSequence')
49
+ const after = classifyCharacter(code)
50
+ const open = !after || (after === constants.characterGroupPunctuation && Boolean(before))
51
+ || attentionMarkers.includes(code)
52
+ const commonMarkClose = !before
53
+ || (before === constants.characterGroupPunctuation && Boolean(after))
54
+ || attentionMarkers.includes(previous)
55
+ const markerCount = token.end.offset - token.start.offset
56
+ const cjkStrongClose = markerCount >= 2
57
+ && unicodePunctuation(previous)
58
+ && isCjkCharacter(code)
59
+ const close = commonMarkClose || cjkStrongClose
60
+
61
+ token._open = open
62
+ token._close = close
63
+ return ok(code)
64
+ }
65
+ }
66
+
67
+ const cjkFriendlyAttention: Construct = {
68
+ name: 'cjkFriendlyAttention',
69
+ resolveAll: attention.resolveAll,
70
+ tokenize: tokenizeCjkFriendlyAttention,
71
+ }
72
+
73
+ const cjkFriendlyStrongExtension: Extension = {
74
+ text: { [codes.asterisk]: cjkFriendlyAttention },
75
+ }
76
+
77
+ /**
78
+ * Extend CommonMark asterisk strong emphasis for punctuation-delimited CJK
79
+ * prose, as a micromark syntax extension for `fromMarkdown`.
80
+ * @returns The micromark syntax extension.
81
+ */
82
+ export function cjkFriendlyStrong(): Extension {
83
+ return cjkFriendlyStrongExtension
84
+ }
@@ -0,0 +1,131 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /**
3
+ * Incremental block-level markdown parsing for an append-only text stream.
4
+ *
5
+ * Re-parsing the whole accumulated document on every streaming chunk is
6
+ * quadratic in the final reply length. CommonMark block parsing is line-based
7
+ * and appended text can only reshape the parse frontier — the last top-level
8
+ * block (a paragraph becoming a setext heading or a table, a list continuing
9
+ * after a blank line, an unclosed fence swallowing lines) — so earlier blocks
10
+ * are final. This parser therefore freezes all but the trailing
11
+ * {@link UNSTABLE_TAIL_BLOCKS} blocks and re-parses only the source tail
12
+ * behind them: each source region is parsed O(1) times over the stream
13
+ * instead of once per chunk.
14
+ *
15
+ * The freeze boundary comes from the parser's own `position` offsets, never
16
+ * from custom source scanning. The cut sits at the *end offset* of the last
17
+ * frozen block (not the next block's start): a following block's start offset
18
+ * excludes up to three spaces of insignificant leading indentation, which is
19
+ * harmless to drop, but cutting at the previous end also keeps the
20
+ * inter-block blank lines in the tail so the sliced source stays verbatim.
21
+ *
22
+ * Known deviation, shared with any prefix-freeze scheme: micromark resolves
23
+ * reference-style links and footnotes document-wide at parse time, so a
24
+ * reference whose definition lands on the other side of the freeze boundary
25
+ * renders literally until the settled full parse self-heals it.
26
+ */
27
+
28
+ import type { Root, RootContent } from 'mdast'
29
+
30
+ /**
31
+ * Trailing blocks kept unstable. Appended text reshapes at most the last
32
+ * block; the second-to-last is retained as safety margin so a freeze decision
33
+ * never has to reason about the parse frontier.
34
+ */
35
+ const UNSTABLE_TAIL_BLOCKS = 2
36
+
37
+ /** A top-level mdast block plus a render key that is stable across chunks. */
38
+ export interface PositionedBlock {
39
+ /** The parsed block. Positions inside it are relative to its parse slice. */
40
+ readonly node: RootContent
41
+ /**
42
+ * The block's start offset in the full source text. Stable from the frame
43
+ * a block first appears through freezing, so React reconciles rather than
44
+ * remounts when a block crosses the freeze boundary.
45
+ */
46
+ readonly key: number
47
+ }
48
+
49
+ /** One {@link IncrementalMarkdownParser.update} result. */
50
+ export interface IncrementalBlocks {
51
+ /** Blocks that can no longer change; grows monotonically per generation. */
52
+ readonly frozen: readonly PositionedBlock[]
53
+ /** The re-parsed unstable tail (at most {@link UNSTABLE_TAIL_BLOCKS} blocks plus growth). */
54
+ readonly tail: readonly PositionedBlock[]
55
+ /** Bumped whenever non-append input discards the frozen prefix; callers drop caches keyed on it. */
56
+ readonly generation: number
57
+ }
58
+
59
+ /**
60
+ * A block's render key: its absolute source start offset. A position-less
61
+ * node (a grammar is free to omit positions) falls back to a negative
62
+ * list-index key — unique within one update's tail, which is the only place
63
+ * the fallback can occur: freezing requires the cut block's position, so a
64
+ * position-less parse keeps every block in the tail (real grammars always
65
+ * stamp positions and never take this path).
66
+ */
67
+ function blockKey(node: RootContent, base: number, index: number): number {
68
+ const offset = node.position?.start.offset
69
+ return offset === undefined ? -(index + 1) : base + offset
70
+ }
71
+
72
+ /**
73
+ * Append-only incremental parser over a caller-supplied grammar. One instance
74
+ * accumulates one streaming document; non-append input resets it.
75
+ */
76
+ export class IncrementalMarkdownParser {
77
+ private prevText = ''
78
+ private tailStart = 0
79
+ private frozen: PositionedBlock[] = []
80
+ private generation = 0
81
+ private cached: IncrementalBlocks | null = null
82
+
83
+ /** @param parse - Grammar shared with whatever renders the blocks, so boundaries agree. */
84
+ constructor(private readonly parse: (text: string) => Root) {}
85
+
86
+ /**
87
+ * Fold the current accumulated text and return the frozen/tail split.
88
+ * Idempotent for identical input (the previous result is returned as-is),
89
+ * so callers may invoke it from render paths that re-execute.
90
+ * @param text - The full accumulated markdown source.
91
+ * @returns Frozen and tail blocks with stream-stable render keys.
92
+ */
93
+ update(text: string): IncrementalBlocks {
94
+ if (this.cached !== null && text === this.prevText) return this.cached
95
+ // Deliberate O(prefix) memcmp per update: sound divergence detection has
96
+ // to verify the whole retained prefix, and startsWith compares bytes two
97
+ // orders of magnitude faster than parsing them — the cost this class
98
+ // exists to remove. Passing append/reset deltas instead would push
99
+ // append bookkeeping across the session-projection update boundary for a check
100
+ // that stays sub-millisecond at realistic reply sizes.
101
+ if (!text.startsWith(this.prevText)) {
102
+ this.prevText = ''
103
+ this.tailStart = 0
104
+ this.frozen = []
105
+ this.generation += 1
106
+ }
107
+ this.prevText = text
108
+ const base = this.tailStart
109
+ const blocks = this.parse(text.slice(base)).children
110
+ let firstUnstable = Math.max(0, blocks.length - UNSTABLE_TAIL_BLOCKS)
111
+ if (firstUnstable > 0) {
112
+ const cutEnd = blocks[firstUnstable - 1]?.position?.end.offset
113
+ if (cutEnd === undefined) {
114
+ // A grammar that omits positions leaves nothing to cut at; keep the
115
+ // whole parse in the tail rather than guessing a boundary.
116
+ firstUnstable = 0
117
+ } else {
118
+ for (const node of blocks.slice(0, firstUnstable)) {
119
+ this.frozen.push({ node, key: blockKey(node, base, this.frozen.length) })
120
+ }
121
+ this.tailStart = base + cutEnd
122
+ }
123
+ }
124
+ const tail = blocks.slice(firstUnstable).map((node, index) => ({
125
+ node,
126
+ key: blockKey(node, base, index),
127
+ }))
128
+ this.cached = { frozen: [...this.frozen], tail, generation: this.generation }
129
+ return this.cached
130
+ }
131
+ }
@@ -0,0 +1,91 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /**
3
+ * TeX-to-React via KaTeX, replicating the rehype-katex pipeline this renderer
4
+ * replaced: the same three-arm error chain (strict render, `strict: 'ignore'`
5
+ * retry, error span) and a DOM-identical element tree, so settled math keeps
6
+ * its exact markup. KaTeX emits an HTML string; the browser's own HTML parser
7
+ * (`DOMParser`, applying the spec's SVG/MathML foreign-content attribute
8
+ * adjustments KaTeX output relies on) turns it into a tree this module maps
9
+ * onto React elements — KaTeX output is a static span/MathML/SVG vocabulary
10
+ * with no raw user HTML, the same trust shiki's tree gets in CodeBlock.
11
+ *
12
+ * React 18 has no MathML support, so the `.katex-mathml` subtree's elements
13
+ * land in the HTML namespace — exactly as they did under the replaced
14
+ * hast-util-to-jsx-runtime pipeline. The visual arm is the `.katex-html`
15
+ * span tree; the MathML arm serves assistive technology, which reads it by
16
+ * tag name regardless of namespace.
17
+ */
18
+
19
+ import { createElement } from 'react'
20
+ import type { CSSProperties, ReactNode } from 'react'
21
+ import katex from 'katex'
22
+
23
+ /**
24
+ * Convert one inline `style` attribute string into React's style object.
25
+ * KaTeX emits only plain kebab-case declarations (no custom properties and no
26
+ * nameless declarations), so camel-casing the property is the whole mapping.
27
+ */
28
+ function styleObject(css: string): CSSProperties {
29
+ const style: Record<string, string> = {}
30
+ for (const declaration of css.split(';')) {
31
+ const colon = declaration.indexOf(':')
32
+ if (colon === -1) continue
33
+ const name = declaration.slice(0, colon).trim()
34
+ const key = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
35
+ style[key] = declaration.slice(colon + 1).trim()
36
+ }
37
+ return style
38
+ }
39
+
40
+ /** Map one parsed DOM node onto a React element (text nodes pass through). */
41
+ function domToReact(node: ChildNode, key: number): ReactNode {
42
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent
43
+ /* v8 ignore next 2 -- KaTeX output holds only elements and text; other
44
+ node kinds cannot appear in its serialized vocabulary. */
45
+ if (node.nodeType !== Node.ELEMENT_NODE) return null
46
+ const element = node as Element
47
+ const props: Record<string, unknown> = { key }
48
+ for (const attribute of element.attributes) {
49
+ if (attribute.name === 'class') props['className'] = attribute.value
50
+ else if (attribute.name === 'style') props['style'] = styleObject(attribute.value)
51
+ else props[attribute.name] = attribute.value
52
+ }
53
+ const children = [...element.childNodes].map(domToReact)
54
+ return children.length === 0
55
+ ? createElement(element.localName, props)
56
+ : createElement(element.localName, props, ...children)
57
+ }
58
+
59
+ /**
60
+ * Render TeX source to React elements through KaTeX.
61
+ * @param value - The TeX source (math node value; fenced `math` blocks append
62
+ * their trailing newline to match the replaced pipeline's text extraction).
63
+ * @param displayMode - Display (block) versus inline rendering.
64
+ * @returns KaTeX's element tree, or the error span when the source does not
65
+ * parse (colored with KaTeX's stock `errorColor`, matching rehype-katex).
66
+ */
67
+ export function renderTexToReact(value: string, displayMode: boolean): ReactNode {
68
+ let html: string
69
+ try {
70
+ html = katex.renderToString(value, { displayMode, throwOnError: true })
71
+ } catch (error) {
72
+ try {
73
+ html = katex.renderToString(value, { displayMode, strict: 'ignore', throwOnError: false })
74
+ } catch {
75
+ // KaTeX renders ParseErrors itself under throwOnError: false; only its
76
+ // internal errors reach here, so mirror rehype-katex's manual span.
77
+ /* v8 ignore next 8 */
78
+ return (
79
+ <span
80
+ className="katex-error"
81
+ style={{ color: '#cc0000' }}
82
+ title={String(error)}
83
+ >
84
+ {value}
85
+ </span>
86
+ )
87
+ }
88
+ }
89
+ const parsed = new DOMParser().parseFromString(html, 'text/html')
90
+ return [...parsed.body.childNodes].map(domToReact)
91
+ }