cli-jaw 1.5.0 → 1.6.0

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 (225) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +21 -2
  3. package/README.zh-CN.md +2 -2
  4. package/dist/bin/cli-jaw.js +5 -1
  5. package/dist/bin/cli-jaw.js.map +1 -1
  6. package/dist/bin/commands/dispatch.js +50 -0
  7. package/dist/bin/commands/dispatch.js.map +1 -0
  8. package/dist/lib/mcp-sync.js +1 -1
  9. package/dist/lib/mcp-sync.js.map +1 -1
  10. package/dist/lib/upload.js +14 -1
  11. package/dist/lib/upload.js.map +1 -1
  12. package/dist/server.js +53 -40
  13. package/dist/server.js.map +1 -1
  14. package/dist/src/agent/events.js +72 -13
  15. package/dist/src/agent/events.js.map +1 -1
  16. package/dist/src/agent/spawn.js +65 -24
  17. package/dist/src/agent/spawn.js.map +1 -1
  18. package/dist/src/cli/commands.js +6 -6
  19. package/dist/src/cli/commands.js.map +1 -1
  20. package/dist/src/cli/handlers.js +11 -9
  21. package/dist/src/cli/handlers.js.map +1 -1
  22. package/dist/src/cli/registry.js +7 -1
  23. package/dist/src/cli/registry.js.map +1 -1
  24. package/dist/src/core/config.js +15 -8
  25. package/dist/src/core/config.js.map +1 -1
  26. package/dist/src/core/db.js +18 -3
  27. package/dist/src/core/db.js.map +1 -1
  28. package/dist/src/core/employees.js +34 -0
  29. package/dist/src/core/employees.js.map +1 -0
  30. package/dist/src/discord/bot.js +72 -9
  31. package/dist/src/discord/bot.js.map +1 -1
  32. package/dist/src/discord/commands.js +12 -8
  33. package/dist/src/discord/commands.js.map +1 -1
  34. package/dist/src/orchestrator/distribute.js +41 -24
  35. package/dist/src/orchestrator/distribute.js.map +1 -1
  36. package/dist/src/orchestrator/gateway.js +3 -1
  37. package/dist/src/orchestrator/gateway.js.map +1 -1
  38. package/dist/src/orchestrator/pipeline.js +93 -27
  39. package/dist/src/orchestrator/pipeline.js.map +1 -1
  40. package/dist/src/orchestrator/research.js +5 -5
  41. package/dist/src/orchestrator/research.js.map +1 -1
  42. package/dist/src/orchestrator/scope.js +55 -0
  43. package/dist/src/orchestrator/scope.js.map +1 -0
  44. package/dist/src/orchestrator/state-machine.js +23 -21
  45. package/dist/src/orchestrator/state-machine.js.map +1 -1
  46. package/dist/src/orchestrator/worker-registry.js +9 -2
  47. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  48. package/dist/src/prompt/builder.js +78 -37
  49. package/dist/src/prompt/builder.js.map +1 -1
  50. package/dist/src/prompt/templates/a1-system.md +40 -0
  51. package/dist/src/prompt/templates/employee.md +10 -1
  52. package/dist/src/prompt/templates/orchestration.md +17 -1
  53. package/dist/src/telegram/bot.js +7 -1
  54. package/dist/src/telegram/bot.js.map +1 -1
  55. package/package.json +4 -2
  56. package/public/assets/fonts/GeistVF.woff2 +0 -0
  57. package/public/assets/fonts/JetBrainsMono-Variable.woff2 +0 -0
  58. package/public/assets/providers/claude-color.svg +1 -0
  59. package/public/assets/providers/claude.svg +1 -0
  60. package/public/assets/providers/copilot-color.svg +1 -0
  61. package/public/assets/providers/copilot.svg +1 -0
  62. package/public/assets/providers/discord.svg +1 -0
  63. package/public/assets/providers/gemini-color.svg +1 -0
  64. package/public/assets/providers/gemini.svg +1 -0
  65. package/public/assets/providers/openai.svg +1 -0
  66. package/public/assets/providers/opencode.svg +1 -0
  67. package/public/assets/providers/telegram.svg +1 -0
  68. package/public/css/chat.css +79 -51
  69. package/public/css/diagram.css +348 -0
  70. package/public/css/layout.css +33 -10
  71. package/public/css/markdown.css +64 -18
  72. package/public/css/modals.css +3 -3
  73. package/public/css/orc-state.css +9 -9
  74. package/public/css/sidebar.css +37 -3
  75. package/public/css/tool-ui.css +46 -11
  76. package/public/css/variables.css +63 -63
  77. package/public/dist/assets/api-Bbmmo0o1.js +1 -0
  78. package/public/dist/assets/architecture-YZFGNWBL-BxiHqtOH.js +1 -0
  79. package/public/dist/assets/architectureDiagram-Q4EWVU46-CJTGDw5p.js +1 -0
  80. package/public/dist/assets/blockDiagram-DXYQGD6D-Btl5Y-Er.js +1 -0
  81. package/public/dist/assets/c4Diagram-AHTNJAMY-sDfjW4ZF.js +1 -0
  82. package/public/dist/assets/classDiagram-6PBFFD2Q-ByCgggL2.js +1 -0
  83. package/public/dist/assets/classDiagram-v2-HSJHXN6E-t1amaXkU.js +1 -0
  84. package/public/dist/assets/constants-C8_0OtK2.js +1 -0
  85. package/public/dist/assets/cose-bilkent-S5V4N54A-eOSFGdaE.js +1 -0
  86. package/public/dist/assets/dagre-KV5264BT-OCB5j8Q6.js +1 -0
  87. package/public/dist/assets/diagram-5BDNPKRD-C-4fgK5P.js +1 -0
  88. package/public/dist/assets/diagram-G4DWMVQ6-C6vmHKDV.js +1 -0
  89. package/public/dist/assets/diagram-MMDJMWI5-BMCo_wmt.js +1 -0
  90. package/public/dist/assets/diagram-TYMM5635-DaCLdttJ.js +1 -0
  91. package/public/dist/assets/employees-D5n7mX5v.js +39 -0
  92. package/public/dist/assets/erDiagram-SMLLAGMA-BiTRdm5s.js +1 -0
  93. package/public/dist/assets/flowDiagram-DWJPFMVM-sv6jVi0d.js +1 -0
  94. package/public/dist/assets/ganttDiagram-T4ZO3ILL-jCP3OW23.js +1 -0
  95. package/public/dist/assets/gitGraph-7Q5UKJZL-BIeKLxpm.js +1 -0
  96. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CokylnbW.js +1 -0
  97. package/public/dist/assets/index-Br2UdKlR.css +1 -0
  98. package/public/dist/assets/index-CmAU0d96.js +49 -0
  99. package/public/dist/assets/info-OMHHGYJF-CIEl5dWF.js +1 -0
  100. package/public/dist/assets/infoDiagram-42DDH7IO-BBnK1Bh2.js +1 -0
  101. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-qXIz0VhS.js +1 -0
  102. package/public/dist/assets/journeyDiagram-VCZTEJTY-BdrOLof3.js +1 -0
  103. package/public/dist/assets/kanban-definition-6JOO6SKY-CcX7CE04.js +1 -0
  104. package/public/dist/assets/mermaid.core-BoxIvw7E.js +1 -0
  105. package/public/dist/assets/mindmap-definition-QFDTVHPH-CdXARskX.js +1 -0
  106. package/public/dist/assets/packet-4T2RLAQJ-Bz4ZwYZ3.js +1 -0
  107. package/public/dist/assets/pie-ZZUOXDRM-AtGL3wZ2.js +1 -0
  108. package/public/dist/assets/pieDiagram-DEJITSTG-B5SOq9Yr.js +1 -0
  109. package/public/dist/assets/quadrantDiagram-34T5L4WZ-D0uNWgpI.js +1 -0
  110. package/public/dist/assets/radar-PYXPWWZC-AbJSfeqB.js +1 -0
  111. package/public/dist/assets/render-C8N0rp4L.js +25 -0
  112. package/public/dist/assets/requirementDiagram-MS252O5E-62td6IQ-.js +1 -0
  113. package/public/dist/assets/sankeyDiagram-XADWPNL6-Crg_02iC.js +1 -0
  114. package/public/dist/assets/sequenceDiagram-FGHM5R23-fbCocZHj.js +1 -0
  115. package/public/dist/assets/settings-BJR-IGM5.js +41 -0
  116. package/public/dist/assets/settings-Dm6OnPmY.js +1 -0
  117. package/public/dist/assets/skills-D6jv9AIs.js +1 -0
  118. package/public/dist/assets/skills-uywskdFh.js +12 -0
  119. package/public/dist/assets/slash-commands-CCDonT40.js +1 -0
  120. package/public/dist/assets/{slash-commands-DMsE88bu.js → slash-commands-DWvL-VDU.js} +1 -1
  121. package/public/dist/assets/stateDiagram-FHFEXIEX-Gy3DhXxQ.js +1 -0
  122. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DJc-FW9O.js +1 -0
  123. package/public/dist/assets/timeline-definition-GMOUNBTQ-B3cA9DgY.js +1 -0
  124. package/public/dist/assets/treeView-SZITEDCU-Cn58DIbB.js +1 -0
  125. package/public/dist/assets/treemap-W4RFUUIX-DPcYjDzH.js +1 -0
  126. package/public/dist/assets/ui-C1daR00l.js +1 -0
  127. package/public/dist/assets/ui-D-oFkXed.js +131 -0
  128. package/public/dist/assets/vendor-icons-1Ec7ZWcT.js +1 -0
  129. package/public/dist/assets/{vendor-mermaid-COidH9HB.js → vendor-mermaid-lvHqQdfg.js} +4 -4
  130. package/public/dist/assets/vennDiagram-DHZGUBPP-DkBfxilo.js +1 -0
  131. package/public/dist/assets/wardley-RL74JXVD-6Dg0PJ4H.js +1 -0
  132. package/public/dist/assets/wardleyDiagram-NUSXRM2D-Dsntl_vy.js +1 -0
  133. package/public/dist/assets/ws-Dnn8HG8B.js +2 -0
  134. package/public/dist/assets/xychartDiagram-5P7HB3ND-B_McB5GE.js +1 -0
  135. package/public/dist/index.html +63 -55
  136. package/public/index.html +62 -53
  137. package/public/js/constants.ts +8 -2
  138. package/public/js/diagram/iframe-renderer.ts +559 -0
  139. package/public/js/diagram/types.ts +129 -0
  140. package/public/js/diagram/widget-validator.ts +82 -0
  141. package/public/js/features/chat.ts +24 -12
  142. package/public/js/features/employees.ts +3 -2
  143. package/public/js/features/heartbeat.ts +4 -3
  144. package/public/js/features/memory.ts +4 -3
  145. package/public/js/features/process-block.ts +12 -11
  146. package/public/js/features/settings-cli-status.ts +10 -7
  147. package/public/js/features/settings-core.ts +13 -3
  148. package/public/js/features/settings-discord.ts +9 -0
  149. package/public/js/features/settings-mcp.ts +8 -7
  150. package/public/js/features/settings-stt.ts +4 -4
  151. package/public/js/features/settings-templates.ts +10 -8
  152. package/public/js/features/settings-types.ts +1 -1
  153. package/public/js/features/settings.ts +1 -1
  154. package/public/js/features/sidebar.ts +4 -2
  155. package/public/js/features/skills.ts +4 -3
  156. package/public/js/features/theme.ts +4 -0
  157. package/public/js/features/tool-ui.ts +6 -5
  158. package/public/js/features/voice-recorder.ts +2 -1
  159. package/public/js/icons.ts +257 -0
  160. package/public/js/main.ts +9 -3
  161. package/public/js/provider-icons.ts +88 -0
  162. package/public/js/render.ts +493 -30
  163. package/public/js/streaming-render.ts +3 -3
  164. package/public/js/ui.ts +38 -18
  165. package/public/js/ws.ts +17 -10
  166. package/public/locales/en.json +89 -88
  167. package/public/locales/ko.json +89 -88
  168. package/scripts/release-preview.sh +1 -1
  169. package/public/dist/assets/api-BlPw3bUI.js +0 -1
  170. package/public/dist/assets/architecture-YZFGNWBL-BkS7SZQi.js +0 -1
  171. package/public/dist/assets/architectureDiagram-Q4EWVU46-Dl3iBKeR.js +0 -1
  172. package/public/dist/assets/blockDiagram-DXYQGD6D-DPr4D17p.js +0 -1
  173. package/public/dist/assets/c4Diagram-AHTNJAMY-CfoxJtDk.js +0 -1
  174. package/public/dist/assets/classDiagram-6PBFFD2Q-BOAdHnnB.js +0 -1
  175. package/public/dist/assets/classDiagram-v2-HSJHXN6E-B2QCJXWC.js +0 -1
  176. package/public/dist/assets/constants-DshMUJbo.js +0 -1
  177. package/public/dist/assets/cose-bilkent-S5V4N54A-CA14jk7w.js +0 -1
  178. package/public/dist/assets/dagre-KV5264BT-BVwNaSwC.js +0 -1
  179. package/public/dist/assets/diagram-5BDNPKRD-CHqIAvdc.js +0 -1
  180. package/public/dist/assets/diagram-G4DWMVQ6-DxvsCvTP.js +0 -1
  181. package/public/dist/assets/diagram-MMDJMWI5-DqOPO7dl.js +0 -1
  182. package/public/dist/assets/diagram-TYMM5635-C9xMWPQn.js +0 -1
  183. package/public/dist/assets/employees-CFRlsbHm.js +0 -39
  184. package/public/dist/assets/erDiagram-SMLLAGMA-BTmpfRkm.js +0 -1
  185. package/public/dist/assets/flowDiagram-DWJPFMVM-DZBSQAKA.js +0 -1
  186. package/public/dist/assets/ganttDiagram-T4ZO3ILL-TAEMCRbT.js +0 -1
  187. package/public/dist/assets/gitGraph-7Q5UKJZL-bbxndRNA.js +0 -1
  188. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CBxtx0MB.js +0 -1
  189. package/public/dist/assets/index-1Gg-6jeC.css +0 -1
  190. package/public/dist/assets/index-CQHqXjrK.js +0 -49
  191. package/public/dist/assets/info-OMHHGYJF-DRXq_Ywr.js +0 -1
  192. package/public/dist/assets/infoDiagram-42DDH7IO-C7FFwX4m.js +0 -1
  193. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-DEZJE79j.js +0 -1
  194. package/public/dist/assets/journeyDiagram-VCZTEJTY-ZHLiY6GV.js +0 -1
  195. package/public/dist/assets/kanban-definition-6JOO6SKY-NP7pY7ML.js +0 -1
  196. package/public/dist/assets/mermaid.core-CHuluSlD.js +0 -1
  197. package/public/dist/assets/mindmap-definition-QFDTVHPH-Bd1OgfN_.js +0 -1
  198. package/public/dist/assets/packet-4T2RLAQJ-CGu8ST97.js +0 -1
  199. package/public/dist/assets/pie-ZZUOXDRM-CDG65mhS.js +0 -1
  200. package/public/dist/assets/pieDiagram-DEJITSTG-BbMBHLDM.js +0 -1
  201. package/public/dist/assets/quadrantDiagram-34T5L4WZ-CDCDgI1e.js +0 -1
  202. package/public/dist/assets/radar-PYXPWWZC-1gS-6ylu.js +0 -1
  203. package/public/dist/assets/render-YHvL5VM_.js +0 -6
  204. package/public/dist/assets/requirementDiagram-MS252O5E-DkmvOtYz.js +0 -1
  205. package/public/dist/assets/sankeyDiagram-XADWPNL6-GNcXIYsL.js +0 -1
  206. package/public/dist/assets/sequenceDiagram-FGHM5R23-BFrw2n6x.js +0 -1
  207. package/public/dist/assets/settings-BQF_u5px.js +0 -37
  208. package/public/dist/assets/settings-C5Q9IPjJ.js +0 -1
  209. package/public/dist/assets/skills-9Pd2rOw-.js +0 -1
  210. package/public/dist/assets/skills-BI7sQAdk.js +0 -12
  211. package/public/dist/assets/slash-commands-FMpJzlDB.js +0 -1
  212. package/public/dist/assets/stateDiagram-FHFEXIEX-BLgKnllT.js +0 -1
  213. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CbzuWsSa.js +0 -1
  214. package/public/dist/assets/timeline-definition-GMOUNBTQ-CcZTJ3gL.js +0 -1
  215. package/public/dist/assets/treeView-SZITEDCU-BDqBsaH1.js +0 -1
  216. package/public/dist/assets/treemap-W4RFUUIX-BaWHA3Di.js +0 -1
  217. package/public/dist/assets/ui-BukgLHuh.js +0 -29
  218. package/public/dist/assets/ui-Bvz1JfTE.js +0 -1
  219. package/public/dist/assets/vennDiagram-DHZGUBPP-BWV_j1bh.js +0 -1
  220. package/public/dist/assets/wardley-RL74JXVD-C7gKA3d7.js +0 -1
  221. package/public/dist/assets/wardleyDiagram-NUSXRM2D-BUoq_wug.js +0 -1
  222. package/public/dist/assets/ws-S_AZgx7L.js +0 -2
  223. package/public/dist/assets/xychartDiagram-5P7HB3ND-PvT5Ec82.js +0 -1
  224. /package/public/dist/assets/{api-B8XKQ4OT.js → api-Ci-lgwRp.js} +0 -0
  225. /package/public/dist/assets/{locale-BjoAcbis.js → locale-DIXc-_34.js} +0 -0
@@ -24,7 +24,12 @@ import plaintext from 'highlight.js/lib/languages/plaintext';
24
24
  import katex from 'katex';
25
25
  import DOMPurify from 'dompurify';
26
26
  import { t } from './features/i18n.js';
27
+ import { ICONS } from './icons.js';
27
28
  import { fixCjkPunctuationBoundary } from './cjk-fix.js';
29
+ import {
30
+ SvgBlock, shieldCodeFenceSvg, unshieldCodeFenceSvg,
31
+ extractTopLevelSvg,
32
+ } from './diagram/types.js';
28
33
 
29
34
  // Register hljs languages (core-only import: ~25KB vs ~1MB full)
30
35
  hljs.registerLanguage('javascript', javascript);
@@ -57,35 +62,167 @@ hljs.registerLanguage('text', plaintext);
57
62
 
58
63
  // Lazy mermaid: loaded on first diagram encounter
59
64
  let mermaidModule: typeof import('mermaid') | null = null;
65
+ let mermaidTheme: string | null = null;
66
+
67
+ function getMermaidThemeVars() {
68
+ const isLight = document.documentElement.getAttribute('data-theme') === 'light';
69
+ return isLight ? {
70
+ primaryColor: '#e2e8f0',
71
+ primaryTextColor: '#1a202c',
72
+ primaryBorderColor: '#a0aec0',
73
+ lineColor: '#718096',
74
+ secondaryColor: '#ebf8ff',
75
+ tertiaryColor: '#f7fafc',
76
+ background: 'transparent',
77
+ mainBkg: '#e2e8f0',
78
+ nodeBorder: '#a0aec0',
79
+ clusterBkg: '#f7fafc',
80
+ clusterBorder: '#cbd5e0',
81
+ titleColor: '#1a202c',
82
+ edgeLabelBackground: '#f7fafc',
83
+ } : {
84
+ primaryColor: '#2d3748',
85
+ primaryTextColor: '#e2e8f0',
86
+ primaryBorderColor: '#4a5568',
87
+ lineColor: '#718096',
88
+ secondaryColor: '#1a365d',
89
+ tertiaryColor: '#1a202c',
90
+ background: 'transparent',
91
+ mainBkg: '#2d3748',
92
+ nodeBorder: '#4a5568',
93
+ clusterBkg: '#1a202c',
94
+ clusterBorder: '#2d3748',
95
+ titleColor: '#e2e8f0',
96
+ edgeLabelBackground: '#1a202c',
97
+ };
98
+ }
60
99
 
61
100
  async function getMermaid() {
101
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
62
102
  if (!mermaidModule) {
63
103
  mermaidModule = await import('mermaid');
104
+ }
105
+ if (mermaidTheme !== currentTheme) {
106
+ mermaidTheme = currentTheme;
64
107
  mermaidModule.default.initialize({
65
108
  startOnLoad: false,
66
- theme: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
109
+ theme: 'base',
110
+ themeVariables: getMermaidThemeVars(),
67
111
  securityLevel: 'strict',
68
112
  });
69
113
  }
70
114
  return mermaidModule.default;
71
115
  }
72
116
 
117
+ // Mermaid SVG sanitizer — allows <style> (required for Mermaid theming)
118
+ // Separate from sanitizeHtml() which blocks <style> for user-supplied SVGs.
119
+ // Mermaid is configured with htmlLabels:false so labels use SVG <text>,
120
+ // not <foreignObject> + HTML. This avoids DOMPurify namespace issues.
121
+ function sanitizeMermaidSvg(svg: string): string {
122
+ const clean = DOMPurify.sanitize(svg, {
123
+ USE_PROFILES: { svg: true, svgFilters: true },
124
+ FORBID_TAGS: [
125
+ 'script', 'iframe', 'object', 'embed', 'form', 'input',
126
+ 'foreignObject', 'animate', 'set', 'animateTransform', 'animateMotion',
127
+ ],
128
+ FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur',
129
+ 'background'],
130
+ });
131
+ // Sanitize CSS inside <style> blocks: strip @import, @font-face, external url()
132
+ const div = document.createElement('div');
133
+ div.innerHTML = clean;
134
+ for (const style of div.querySelectorAll('style')) {
135
+ let css = style.textContent || '';
136
+ css = css.replace(/@import\b[^;]*;?/gi, '/* stripped */');
137
+ css = css.replace(/@font-face\s*\{[^}]*\}/gi, '/* stripped */');
138
+ css = css.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
139
+ style.textContent = css;
140
+ }
141
+ return div.innerHTML;
142
+ }
143
+
73
144
  export function escapeHtml(str: string): string {
74
145
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
75
146
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
76
147
  }
77
148
 
78
- // ── XSS sanitization ──
149
+ // ── XSS sanitization (hardened for inline SVG — Phase 1) ──
79
150
  export function sanitizeHtml(html: string): string {
80
151
  return DOMPurify.sanitize(html, {
81
152
  USE_PROFILES: { html: true, svg: true, svgFilters: true },
82
- FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
83
- FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur'],
153
+ FORBID_TAGS: [
154
+ 'script', 'style', 'iframe', 'object', 'embed', 'form', 'input',
155
+ // SVG security: block animation + foreignObject (script injection vectors)
156
+ 'foreignObject', 'animate', 'set', 'animateTransform', 'animateMotion',
157
+ ],
158
+ FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur',
159
+ 'background'], // legacy HTML attr that triggers remote fetch
84
160
  ADD_TAGS: ['use'],
85
- ADD_ATTR: ['aria-hidden', 'xmlns', 'viewBox'],
161
+ ADD_ATTR: ['aria-hidden', 'xmlns', 'viewBox', 'role', 'aria-label',
162
+ 'data-jaw-svg', 'data-jaw-kind'],
86
163
  });
87
164
  }
88
165
 
166
+ // Hook: strip external href/xlink:href on <use> and <image>
167
+ // Only fragment references (#id) allowed — blocks external resource loading.
168
+ const SVG_NS = 'http://www.w3.org/2000/svg';
169
+ const HTML_HREF_ALLOWED = new Set(['a', 'area', 'link']);
170
+
171
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
172
+ const tag = node.tagName.toLowerCase();
173
+
174
+ // ── href / xlink:href: deny-by-default ──
175
+ // Only standard HTML (non-SVG) elements in the allow-set may carry external href.
176
+ // SVG <a> shares tagName 'a' with HTML <a>, so we also check namespaceURI
177
+ // to distinguish them — SVG elements always get fragment-only.
178
+ const isSvgElement = node.namespaceURI === SVG_NS;
179
+ if (isSvgElement || !HTML_HREF_ALLOWED.has(tag)) {
180
+ const href = node.getAttribute('href') || '';
181
+ if (href && !href.startsWith('#')) {
182
+ node.removeAttribute('href');
183
+ }
184
+ }
185
+ // xlink:href is SVG-only — always enforce fragment-only on ALL elements
186
+ const xlinkHref = node.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
187
+ || node.getAttribute('xlink:href') || '';
188
+ if (xlinkHref && !xlinkHref.startsWith('#')) {
189
+ node.removeAttributeNS('http://www.w3.org/1999/xlink', 'href');
190
+ node.removeAttribute('xlink:href');
191
+ }
192
+ // SVG <image>/<feimage> may carry src in HTML parser context (belt-and-suspenders)
193
+ if (tag === 'image' || tag === 'feimage') {
194
+ const src = node.getAttribute('src') || '';
195
+ if (src && !src.startsWith('#')) {
196
+ node.removeAttribute('src');
197
+ }
198
+ }
199
+ // Strip external url() from style and SVG presentation attributes
200
+ // Prevents outbound requests / beaconing via CSS or SVG attrs like
201
+ // filter="url(https://evil)", fill="url(https://evil)", mask, clip-path, marker-*
202
+ const URL_CAPABLE_ATTRS = [
203
+ 'fill', 'stroke', 'filter', 'mask', 'clip-path',
204
+ 'marker-start', 'marker-mid', 'marker-end', 'cursor',
205
+ ];
206
+ // For style: use cssText (browser-parsed) to defeat CSS hex-escape bypass (\75\72\6c = url)
207
+ if (node.hasAttribute('style')) {
208
+ const cssText = (node as HTMLElement).style?.cssText || '';
209
+ if (/url\s*\(/i.test(cssText)) {
210
+ const cleaned = cssText.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
211
+ (node as HTMLElement).style.cssText = cleaned;
212
+ }
213
+ }
214
+ for (const attr of URL_CAPABLE_ATTRS) {
215
+ if (node.hasAttribute(attr)) {
216
+ const val = node.getAttribute(attr) || '';
217
+ if (/url\s*\(/i.test(val)) {
218
+ // Keep fragment-only url(#id), strip external url()
219
+ const cleaned = val.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
220
+ node.setAttribute(attr, cleaned);
221
+ }
222
+ }
223
+ }
224
+ });
225
+
89
226
  // ── Orchestration JSON stripping ──
90
227
  // Only strip JSON blocks that contain orchestration-specific keys, not all JSON blocks
91
228
  // Require keys unique to orchestration payloads (avoid generic words like "phase")
@@ -167,29 +304,92 @@ export function unshieldMath(html: string, blocks: MathBlock[]): string {
167
304
  // ── Mermaid deferred rendering (lazy-loaded) ──
168
305
  let mermaidId = 0;
169
306
 
170
- function renderMermaidBlocks(): void {
171
- const pending = document.querySelectorAll('.mermaid-pending');
172
- if (!pending.length) return;
173
- pending.forEach(async (el) => {
174
- el.classList.remove('mermaid-pending');
175
- const code = el.textContent || '';
307
+ /** Re-render all existing Mermaid diagrams (call on theme toggle). */
308
+ export async function rerenderMermaidDiagrams(): Promise<void> {
309
+ mermaidTheme = null; // force re-init with new theme vars
310
+ const rendered = document.querySelectorAll('.mermaid-rendered');
311
+ if (!rendered.length) return;
312
+ const mm = await getMermaid();
313
+ for (const el of rendered) {
314
+ const code = (el as HTMLElement).dataset.mermaidCode;
315
+ if (!code) continue;
176
316
  const id = `mermaid-${++mermaidId}`;
177
317
  try {
178
- const mm = await getMermaid();
179
318
  const { svg } = await mm.render(id, code);
180
- el.innerHTML = sanitizeHtml(svg);
181
- el.classList.add('mermaid-rendered');
182
- } catch (err: unknown) {
183
- const errMsg = (err as { message?: string; str?: string })?.message
184
- || (err as { str?: string })?.str || 'Unknown error';
185
- el.innerHTML = `
186
- <div class="mermaid-error">
187
- <div class="mermaid-error-title">⚠️ ${escapeHtml(t('mermaid.renderFail') || 'Mermaid render failed')}</div>
188
- <div class="mermaid-error-msg">${escapeHtml(errMsg.slice(0, 200))}</div>
189
- <pre class="mermaid-error-code"><code>${escapeHtml(code)}</code></pre>
190
- </div>`;
319
+ el.innerHTML = svg;
320
+ appendMermaidActionBtns(el as HTMLElement);
321
+ } catch { /* keep existing render on failure */ }
322
+ }
323
+ }
324
+
325
+ // Lazy Mermaid rendering — only render blocks near the viewport
326
+ let mermaidObserver: IntersectionObserver | null = null;
327
+
328
+ function ensureMermaidObserver(): void {
329
+ if (mermaidObserver) return;
330
+ mermaidObserver = new IntersectionObserver((entries) => {
331
+ for (const entry of entries) {
332
+ if (!entry.isIntersecting) continue;
333
+ const el = entry.target as HTMLElement;
334
+ if (!el.classList.contains('mermaid-pending')) continue;
335
+ mermaidObserver!.unobserve(el);
336
+ renderSingleMermaid(el);
191
337
  }
192
- });
338
+ }, { rootMargin: '200px' }); // pre-render 200px before visible
339
+ }
340
+
341
+ function appendMermaidActionBtns(el: HTMLElement): void {
342
+ // Remove existing buttons if present (e.g. re-render)
343
+ el.querySelector('.mermaid-copy-btn')?.remove();
344
+ el.querySelector('.mermaid-save-btn')?.remove();
345
+
346
+ const saveBtn = document.createElement('button');
347
+ saveBtn.className = 'mermaid-save-btn';
348
+ saveBtn.type = 'button';
349
+ saveBtn.ariaLabel = 'Save as image';
350
+ saveBtn.title = 'Save';
351
+ saveBtn.innerHTML = ICONS.download;
352
+ el.appendChild(saveBtn);
353
+
354
+ const copyBtn = document.createElement('button');
355
+ copyBtn.className = 'mermaid-copy-btn';
356
+ copyBtn.type = 'button';
357
+ copyBtn.ariaLabel = 'Copy source';
358
+ copyBtn.title = 'Copy';
359
+ copyBtn.innerHTML = ICONS.copy;
360
+ el.appendChild(copyBtn);
361
+ }
362
+
363
+ async function renderSingleMermaid(el: HTMLElement): Promise<void> {
364
+ el.classList.remove('mermaid-pending');
365
+ const code = el.textContent || '';
366
+ el.dataset.mermaidCode = code;
367
+ const id = `mermaid-${++mermaidId}`;
368
+ try {
369
+ const mm = await getMermaid();
370
+ const { svg } = await mm.render(id, code);
371
+ el.innerHTML = svg;
372
+ el.classList.add('mermaid-rendered');
373
+ appendMermaidActionBtns(el);
374
+ } catch (err: unknown) {
375
+ const errMsg = (err as { message?: string; str?: string })?.message
376
+ || (err as { str?: string })?.str || 'Unknown error';
377
+ el.innerHTML = `
378
+ <div class="mermaid-error">
379
+ <div class="mermaid-error-title">${ICONS.warning} ${escapeHtml(t('mermaid.renderFail') || 'Mermaid render failed')}</div>
380
+ <div class="mermaid-error-msg">${escapeHtml(errMsg.slice(0, 200))}</div>
381
+ <pre class="mermaid-error-code"><code>${escapeHtml(code)}</code></pre>
382
+ </div>`;
383
+ }
384
+ }
385
+
386
+ async function renderMermaidBlocks(): Promise<void> {
387
+ const pending = document.querySelectorAll('.mermaid-pending');
388
+ if (!pending.length) return;
389
+ ensureMermaidObserver();
390
+ for (const el of pending) {
391
+ mermaidObserver!.observe(el);
392
+ }
193
393
  }
194
394
 
195
395
  // ── marked.js configuration (ES module — always available) ──
@@ -200,11 +400,19 @@ function ensureMarked(): boolean {
200
400
 
201
401
  const renderer = new Renderer();
202
402
 
203
- // Code blocks: highlight.js + mermaid detection
403
+ // Code blocks: highlight.js + mermaid + diagram-html detection
204
404
  renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
205
405
  if (lang === 'mermaid') {
206
406
  return `<div class="mermaid-container mermaid-pending">${escapeHtml(text)}</div>`;
207
407
  }
408
+ // diagram-html: encode as base64, Phase 2 activateWidgets() inflates to sandboxed iframe
409
+ if (lang?.trim().toLowerCase() === 'diagram-html') {
410
+ const encoded = btoa(unescape(encodeURIComponent(text)));
411
+ return `<div class="diagram-widget-pending" data-diagram-html="${encoded}"
412
+ role="status" aria-label="Interactive widget loading">
413
+ <div class="diagram-spinner"></div>
414
+ </div>`;
415
+ }
208
416
  let highlighted = escapeHtml(text);
209
417
  if (lang && hljs.getLanguage(lang)) {
210
418
  try {
@@ -223,7 +431,7 @@ function ensureMarked(): boolean {
223
431
  marked.setOptions({
224
432
  renderer,
225
433
  gfm: true,
226
- breaks: true,
434
+ breaks: false,
227
435
  });
228
436
 
229
437
  markedReady = true;
@@ -292,27 +500,282 @@ function ensureCopyDelegation(): void {
292
500
  });
293
501
  }
294
502
 
503
+ // ── Diagram action button event delegation (copy + save) ──
504
+ let diagramActionsReady = false;
505
+
506
+ function ensureDiagramActionDelegation(): void {
507
+ if (diagramActionsReady) return;
508
+ diagramActionsReady = true;
509
+
510
+ document.addEventListener('click', (e: MouseEvent) => {
511
+ const target = e.target as HTMLElement;
512
+
513
+ // ── Copy buttons ──
514
+ const diagCopyBtn = target?.closest('.diagram-copy-btn') as HTMLElement | null;
515
+ if (diagCopyBtn) {
516
+ const container = diagCopyBtn.closest('.diagram-container') as HTMLElement | null;
517
+ if (!container) return;
518
+ let text = '';
519
+ if (container.dataset.widgetHtml) {
520
+ try { text = decodeURIComponent(escape(atob(container.dataset.widgetHtml))); }
521
+ catch { return; }
522
+ } else {
523
+ const svgEl = container.querySelector('svg');
524
+ if (svgEl) text = svgEl.outerHTML;
525
+ }
526
+ if (text) btnFeedback(diagCopyBtn, text, 'copy');
527
+ return;
528
+ }
529
+
530
+ const mermaidCopyBtn = target?.closest('.mermaid-copy-btn') as HTMLElement | null;
531
+ if (mermaidCopyBtn) {
532
+ const container = mermaidCopyBtn.closest('.mermaid-container') as HTMLElement | null;
533
+ if (!container) return;
534
+ const code = container.dataset.mermaidCode || '';
535
+ if (code) btnFeedback(mermaidCopyBtn, code, 'copy');
536
+ return;
537
+ }
538
+
539
+ // ── Save buttons ──
540
+ const diagSaveBtn = target?.closest('.diagram-save-btn') as HTMLElement | null;
541
+ if (diagSaveBtn) {
542
+ const container = diagSaveBtn.closest('.diagram-container') as HTMLElement | null;
543
+ if (!container) return;
544
+ // Widget: request screenshot via bridge
545
+ if (container.dataset.widgetHtml) {
546
+ const iframe = container.querySelector('iframe') as HTMLIFrameElement | null;
547
+ if (iframe?.contentWindow) {
548
+ iframe.contentWindow.postMessage({ type: 'jaw-request-screenshot' }, '*');
549
+ btnFeedback(diagSaveBtn, '', 'save');
550
+ }
551
+ return;
552
+ }
553
+ // SVG: convert to PNG
554
+ const svgEl = container.querySelector('svg');
555
+ if (svgEl) saveSvgAsPng(svgEl, diagSaveBtn);
556
+ return;
557
+ }
558
+
559
+ const mermaidSaveBtn = target?.closest('.mermaid-save-btn') as HTMLElement | null;
560
+ if (mermaidSaveBtn) {
561
+ const container = mermaidSaveBtn.closest('.mermaid-container') as HTMLElement | null;
562
+ if (!container) return;
563
+ const svgEl = container.querySelector('svg');
564
+ if (svgEl) saveSvgAsPng(svgEl, mermaidSaveBtn);
565
+ return;
566
+ }
567
+ });
568
+ }
569
+
570
+ function btnFeedback(btn: HTMLElement, text: string, action: 'copy' | 'save'): void {
571
+ const doFeedback = () => {
572
+ const orig = btn.innerHTML;
573
+ btn.innerHTML = ICONS.checkSimple;
574
+ btn.classList.add('copied');
575
+ setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('copied'); }, 1500);
576
+ };
577
+ if (action === 'copy') {
578
+ navigator.clipboard.writeText(text).then(doFeedback).catch(() => {});
579
+ } else {
580
+ doFeedback();
581
+ }
582
+ }
583
+
584
+ function saveSvgAsPng(svgEl: SVGElement, btn: HTMLElement): void {
585
+ const clone = svgEl.cloneNode(true) as SVGElement;
586
+ // Ensure width/height attributes for canvas rendering
587
+ const bbox = svgEl.getBoundingClientRect();
588
+ if (!clone.getAttribute('width')) clone.setAttribute('width', String(bbox.width));
589
+ if (!clone.getAttribute('height')) clone.setAttribute('height', String(bbox.height));
590
+
591
+ const svgData = new XMLSerializer().serializeToString(clone);
592
+ // Data URL avoids tainted canvas (blob URL treated as cross-origin)
593
+ const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
594
+ const img = new Image();
595
+ img.onload = () => {
596
+ const scale = 2; // retina
597
+ const canvas = document.createElement('canvas');
598
+ canvas.width = img.naturalWidth * scale;
599
+ canvas.height = img.naturalHeight * scale;
600
+ const ctx = canvas.getContext('2d')!;
601
+ ctx.scale(scale, scale);
602
+ ctx.drawImage(img, 0, 0);
603
+ canvas.toBlob(blob => {
604
+ if (!blob) return;
605
+ downloadBlob(blob, `diagram-${Date.now()}.png`);
606
+ btnFeedback(btn, '', 'save');
607
+ }, 'image/png');
608
+ };
609
+ img.onerror = () => {
610
+ // Fallback: download as SVG
611
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
612
+ downloadBlob(svgBlob, `diagram-${Date.now()}.svg`);
613
+ btnFeedback(btn, '', 'save');
614
+ };
615
+ img.src = dataUrl;
616
+ }
617
+
618
+ function downloadBlob(blob: Blob, filename: string): void {
619
+ const url = URL.createObjectURL(blob);
620
+ const a = document.createElement('a');
621
+ a.href = url;
622
+ a.download = filename;
623
+ a.click();
624
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
625
+ }
626
+
627
+ // ── SVG Block Rendering (Phase 1) ──
628
+
629
+ function renderSvgBlock(block: SvgBlock): string {
630
+ if (block.kind === 'partial') {
631
+ return `<div class="diagram-container diagram-loading" role="status"
632
+ aria-label="Diagram loading"><div class="diagram-spinner"></div></div>`;
633
+ }
634
+ if (block.kind === 'error') {
635
+ return `<div class="diagram-container diagram-error" role="alert">
636
+ Malformed SVG: unclosed element</div>`;
637
+ }
638
+ // Complete SVG — sanitize individually (extracted SVGs bypass main pipeline)
639
+ const sanitized = sanitizeHtml(block.svg);
640
+ return `<div class="diagram-container diagram-svg" tabindex="0"
641
+ role="figure" aria-label="SVG diagram">
642
+ ${sanitized}
643
+ <button class="diagram-save-btn" type="button"
644
+ aria-label="Save as image" title="Save">${ICONS.download}</button>
645
+ <button class="diagram-copy-btn" type="button"
646
+ aria-label="Copy source" title="Copy">${ICONS.copy}</button>
647
+ <button class="diagram-zoom-btn" type="button"
648
+ aria-label="Expand diagram" title="Expand">⤢</button>
649
+ </div>`;
650
+ }
651
+
652
+ function unshieldSvgBlocks(html: string, blocks: SvgBlock[]): string {
653
+ for (const block of blocks) {
654
+ const pattern = `<div\\b[^>]*?\\bdata-jaw-svg="${block.id}"[^>]*></div>`;
655
+ const re = new RegExp(pattern, 'g');
656
+ const rendered = renderSvgBlock(block);
657
+ // Use function replacement to avoid $& $' $` special patterns in SVG content
658
+ html = html.replace(re, () => rendered);
659
+ }
660
+ return html;
661
+ }
662
+
663
+ // ── Diagram Zoom Overlay ──
664
+
665
+ export function bindDiagramZoom(): void {
666
+ document.querySelectorAll('.diagram-zoom-btn').forEach(btn => {
667
+ if ((btn as HTMLElement).dataset.bound) return;
668
+ (btn as HTMLElement).dataset.bound = '1';
669
+ btn.addEventListener('click', () => {
670
+ const container = btn.closest('.diagram-container');
671
+ if (!container) return;
672
+ // Clone without zoom button to prevent nesting
673
+ const clone = container.cloneNode(true) as HTMLElement;
674
+ clone.querySelectorAll('.diagram-zoom-btn, .diagram-copy-btn, .diagram-save-btn').forEach(b => b.remove());
675
+ openDiagramOverlay(clone.innerHTML);
676
+ });
677
+ });
678
+ }
679
+
680
+ export function openDiagramOverlay(innerHtml: string): void {
681
+ const previousFocus = document.activeElement as HTMLElement | null;
682
+ const overlay = document.createElement('div');
683
+ overlay.className = 'diagram-overlay';
684
+ overlay.setAttribute('role', 'dialog');
685
+ overlay.setAttribute('aria-modal', 'true');
686
+ overlay.setAttribute('aria-label', 'Expanded diagram');
687
+ // Re-sanitize to prevent mXSS from double HTML parsing
688
+ const safeHtml = sanitizeHtml(innerHtml);
689
+ overlay.innerHTML = `
690
+ <div class="diagram-overlay-content">${safeHtml}</div>
691
+ <button class="diagram-overlay-close" type="button" aria-label="Close">✕</button>
692
+ `;
693
+
694
+ // Ensure SVGs scale inside overlay: add viewBox if missing, remove fixed dimensions
695
+ overlay.querySelectorAll<SVGSVGElement>('.diagram-overlay-content svg').forEach(svg => {
696
+ if (!svg.getAttribute('viewBox')) {
697
+ const w = svg.getAttribute('width') || svg.getBBox?.()?.width;
698
+ const h = svg.getAttribute('height') || svg.getBBox?.()?.height;
699
+ if (w && h) svg.setAttribute('viewBox', `0 0 ${parseFloat(String(w))} ${parseFloat(String(h))}`);
700
+ }
701
+ svg.removeAttribute('width');
702
+ svg.removeAttribute('height');
703
+ });
704
+
705
+ const closeBtn = overlay.querySelector('.diagram-overlay-close') as HTMLElement;
706
+
707
+ const close = () => {
708
+ overlay.remove();
709
+ document.removeEventListener('keydown', onKey);
710
+ // Restore focus to the element that opened the overlay
711
+ if (previousFocus && previousFocus.isConnected) previousFocus.focus();
712
+ };
713
+ const onKey = (e: KeyboardEvent) => {
714
+ if (e.key === 'Escape') { close(); return; }
715
+ // Focus trap: Tab cycles within overlay
716
+ if (e.key === 'Tab') {
717
+ const focusable = overlay.querySelectorAll<HTMLElement>(
718
+ 'button, [href], [tabindex]:not([tabindex="-1"])');
719
+ if (focusable.length === 0) return;
720
+ const first = focusable[0];
721
+ const last = focusable[focusable.length - 1];
722
+ if (e.shiftKey && document.activeElement === first) {
723
+ e.preventDefault(); last.focus();
724
+ } else if (!e.shiftKey && document.activeElement === last) {
725
+ e.preventDefault(); first.focus();
726
+ }
727
+ }
728
+ };
729
+
730
+ closeBtn.addEventListener('click', close);
731
+ document.addEventListener('keydown', onKey);
732
+ document.body.appendChild(overlay);
733
+ closeBtn.focus();
734
+ }
735
+
295
736
  // ── Main export ──
296
- export function renderMarkdown(text: string): string {
297
- const cleaned = stripOrchestration(text);
298
- if (!cleaned) return `<em class="text-dim orchestrate-placeholder">${escapeHtml(t('orchestrator.dispatching'))}</em>`;
737
+ export function renderMarkdown(text: string, isStreaming = false): string {
738
+ const rawCleaned = stripOrchestration(text);
739
+ if (!rawCleaned) return `<em class="text-dim orchestrate-placeholder">${escapeHtml(t('orchestrator.dispatching'))}</em>`;
740
+ // Collapse 3+ consecutive newlines → double newline (prevents excessive paragraph breaks)
741
+ const cleaned = rawCleaned.replace(/\n{3,}/g, '\n\n');
742
+
743
+ // 1. Shield code fences (protect SVG in code blocks)
744
+ const { text: fenceShielded, fences } = shieldCodeFenceSvg(cleaned);
745
+
746
+ // 2. Extract top-level SVGs
747
+ const { text: svgShielded, blocks: svgBlocks } = extractTopLevelSvg(fenceShielded, isStreaming);
299
748
 
300
- const { text: shielded, blocks: mathBlocks } = shieldMath(cleaned);
749
+ // 3. Unshield code fences (restore for marked processing)
750
+ const restored = unshieldCodeFenceSvg(svgShielded, fences);
301
751
 
752
+ // 4. Shield math
753
+ const { text: shielded, blocks: mathBlocks } = shieldMath(restored);
754
+
755
+ // 5. Marked parse
302
756
  ensureMarked();
303
757
  const fixed = fixCjkPunctuationBoundary(shielded);
304
758
  let html = marked.parse(fixed) as string;
305
759
  html = html.replace(/<table/g, '<div class="table-wrapper"><table').replace(/<\/table>/g, '</table></div>');
306
760
 
761
+ // 6. Unshield math
307
762
  html = unshieldMath(html, mathBlocks);
763
+
764
+ // 7. Sanitize
308
765
  html = sanitizeHtml(html);
309
766
 
767
+ // 8. Unshield SVGs (after sanitize — SVGs sanitized individually in renderSvgBlock)
768
+ html = unshieldSvgBlocks(html, svgBlocks);
769
+
770
+ // 9. Post-render async tasks
310
771
  requestAnimationFrame(() => {
311
772
  renderMermaidBlocks();
312
773
  rehighlightAll();
774
+ bindDiagramZoom();
313
775
  });
314
776
 
315
777
  ensureCopyDelegation();
778
+ ensureDiagramActionDelegation();
316
779
 
317
780
  return html;
318
781
  }
@@ -13,7 +13,7 @@ export interface StreamState {
13
13
  }
14
14
 
15
15
  const FULL_RENDER_THRESHOLD = 2000;
16
- const THROTTLE_MS = 32;
16
+ const THROTTLE_MS = 80; // ~12fps — was 32ms (30fps), reduced to avoid blocking input
17
17
 
18
18
  export function createStreamRenderer(el: HTMLElement): StreamState {
19
19
  return { fullText: '', element: el, pendingRAF: null, isFinalized: false, lastRenderTime: 0 };
@@ -27,7 +27,7 @@ export function appendChunk(ss: StreamState, chunk: string): void {
27
27
  if (ss.isFinalized) return;
28
28
  const now = performance.now();
29
29
  if (ss.fullText.length < FULL_RENDER_THRESHOLD || now - ss.lastRenderTime > THROTTLE_MS) {
30
- ss.element.innerHTML = renderMarkdown(ss.fullText) +
30
+ ss.element.innerHTML = renderMarkdown(ss.fullText, true) +
31
31
  '<span class="stream-cursor" aria-hidden="true"></span>';
32
32
  ss.lastRenderTime = now;
33
33
  } else {
@@ -35,7 +35,7 @@ export function appendChunk(ss: StreamState, chunk: string): void {
35
35
  ss.pendingRAF = requestAnimationFrame(() => {
36
36
  ss.pendingRAF = null;
37
37
  if (ss.isFinalized) return;
38
- ss.element.innerHTML = renderMarkdown(ss.fullText) +
38
+ ss.element.innerHTML = renderMarkdown(ss.fullText, true) +
39
39
  '<span class="stream-cursor" aria-hidden="true"></span>';
40
40
  ss.lastRenderTime = performance.now();
41
41
  });