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
@@ -0,0 +1,559 @@
1
+ // ── Sandboxed iframe renderer for diagram-html widgets ──
2
+ // Phase 2: Inflates diagram-html placeholders into sandbox="allow-scripts" iframes
3
+ // with validated postMessage bridge for theme sync, resize, and sendPrompt.
4
+
5
+ import { ICONS } from '../icons.js';
6
+ import { validateWidgetHtml } from './widget-validator.js';
7
+
8
+ // ── Action Button Helpers ──
9
+ function createDiagramCopyBtn(): HTMLButtonElement {
10
+ const btn = document.createElement('button');
11
+ btn.className = 'diagram-copy-btn';
12
+ btn.type = 'button';
13
+ btn.ariaLabel = 'Copy source';
14
+ btn.title = 'Copy';
15
+ btn.innerHTML = ICONS.copy;
16
+ return btn;
17
+ }
18
+
19
+ function createDiagramSaveBtn(): HTMLButtonElement {
20
+ const btn = document.createElement('button');
21
+ btn.className = 'diagram-save-btn';
22
+ btn.type = 'button';
23
+ btn.ariaLabel = 'Save as image';
24
+ btn.title = 'Save';
25
+ btn.innerHTML = ICONS.download;
26
+ return btn;
27
+ }
28
+
29
+ // ── CDN Allowlist (Phase 5 libraries) ──
30
+ const CDN_ALLOWLIST = [
31
+ 'cdnjs.cloudflare.com',
32
+ 'cdn.jsdelivr.net',
33
+ 'unpkg.com',
34
+ 'esm.sh',
35
+ 'fonts.googleapis.com',
36
+ 'fonts.gstatic.com',
37
+ ];
38
+
39
+ // ── Registered iframes for postMessage validation ──
40
+ const registeredIframes = new Set<Window>();
41
+
42
+ // ── Per-iframe nonces for navigation defense ──
43
+ const iframeNonces = new Map<Window, string>();
44
+
45
+ // ── Cleanup: MutationObserver removes stale iframe refs ──
46
+ // Scoped to #chatMessages (not document.body) to avoid firing on every DOM mutation.
47
+ let cleanupObserver: MutationObserver | null = null;
48
+ function ensureCleanupObserver(): void {
49
+ if (cleanupObserver) return;
50
+ const chatEl = document.getElementById('chatMessages');
51
+ if (!chatEl) return;
52
+ cleanupObserver = new MutationObserver((mutations) => {
53
+ // Early exit: skip if no iframes are registered
54
+ if (!registeredIframes.size) return;
55
+ for (const m of mutations) {
56
+ for (const node of m.removedNodes) {
57
+ if (node instanceof HTMLIFrameElement && node.contentWindow) {
58
+ registeredIframes.delete(node.contentWindow);
59
+ iframeNonces.delete(node.contentWindow);
60
+ }
61
+ if (node instanceof HTMLElement) {
62
+ node.querySelectorAll('iframe').forEach(iframe => {
63
+ if (iframe.contentWindow) {
64
+ registeredIframes.delete(iframe.contentWindow);
65
+ iframeNonces.delete(iframe.contentWindow);
66
+ }
67
+ });
68
+ }
69
+ }
70
+ }
71
+ });
72
+ cleanupObserver.observe(chatEl, { childList: true, subtree: true });
73
+ }
74
+
75
+ // ── Import Map Builder (ES Module bare specifier resolution) ──
76
+ function buildImportMap(htmlCode: string): string {
77
+ // Skip if widget already defines its own importmap
78
+ if (htmlCode.includes('"importmap"') || htmlCode.includes("'importmap'")) return '';
79
+ const imports: Record<string, string> = {};
80
+ // Three.js: map bare 'three' to full CDN URL so addons (OrbitControls etc.) resolve
81
+ const threeMatch = htmlCode.match(/(?:cdn\.jsdelivr\.net\/npm|unpkg\.com)\/three@([\d.]+)/);
82
+ if (threeMatch) {
83
+ const ver = threeMatch[1];
84
+ const cdn = htmlCode.includes('unpkg.com/three@') ? 'unpkg.com' : 'cdn.jsdelivr.net/npm';
85
+ const buildPath = cdn === 'unpkg.com' ? 'build/three.module.js' : 'build/three.module.min.js';
86
+ imports['three'] = `https://${cdn}/three@${ver}/${buildPath}`;
87
+ imports['three/addons/'] = `https://${cdn}/three@${ver}/examples/jsm/`;
88
+ }
89
+ if (Object.keys(imports).length === 0) return '';
90
+ return `<script type="importmap">${JSON.stringify({ imports })}<\/script>`;
91
+ }
92
+
93
+ // ── CSP Meta Builder ──
94
+ function buildCspMeta(htmlCode: string): string {
95
+ let connectSrc = "'none'";
96
+ // D3 topology needs fetch access to jsdelivr for map data
97
+ if (htmlCode.includes('cdn.jsdelivr.net/npm/us-atlas') ||
98
+ htmlCode.includes('cdn.jsdelivr.net/npm/world-atlas') ||
99
+ htmlCode.includes('cdn.jsdelivr.net/npm/datamaps')) {
100
+ connectSrc = 'https://cdn.jsdelivr.net';
101
+ }
102
+
103
+ // Tone.js creates a blob: Worker for its internal clock
104
+ const workerSrc = /Tone\.min\.js|tone@/.test(htmlCode) ? "worker-src blob:;" : '';
105
+
106
+ // Base src lists — start from CDN_ALLOWLIST to prevent drift when allowlist changes
107
+ const allowlistUrls = CDN_ALLOWLIST.map(h => `https://${h}`);
108
+ const scriptSrc = allowlistUrls.join(' ');
109
+ const imgSrcs: string[] = ['data:', 'blob:', ...allowlistUrls];
110
+ const styleSrcs: string[] = ["'unsafe-inline'", 'https://fonts.googleapis.com'];
111
+
112
+ // Leaflet — narrow signal (real API usage, not bare "leaflet" mentions in prose)
113
+ // Adds OSM tile subdomains (a/b/c, no wildcard) to img-src and Leaflet CSS host to style-src.
114
+ // Marker icons are already covered by CDN_ALLOWLIST baseline in imgSrcs.
115
+ // Regex covers: L.map(), L.tileLayer, L.marker(), L.geoJSON, L.polyline, L.polygon, L.circle,
116
+ // any leaflet asset file (leaflet.js / leaflet.min.js / leaflet-src.esm.js / leaflet@1.9.4/dist/leaflet.js),
117
+ // and direct OSM tile URLs.
118
+ if (/L\.(map|tileLayer|marker|geoJSON|polyline|polygon|circle)\(|leaflet[\w.@/-]*\.(js|css)|tile\.openstreetmap\.org/.test(htmlCode)) {
119
+ imgSrcs.push(
120
+ 'https://a.tile.openstreetmap.org',
121
+ 'https://b.tile.openstreetmap.org',
122
+ 'https://c.tile.openstreetmap.org',
123
+ );
124
+ styleSrcs.push('https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net');
125
+ }
126
+
127
+ const imgSrc = imgSrcs.join(' ');
128
+ const styleSrc = styleSrcs.join(' ');
129
+ return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' ${scriptSrc}; style-src ${styleSrc}; img-src ${imgSrc}; font-src https://fonts.gstatic.com; connect-src ${connectSrc}; ${workerSrc} base-uri 'none';">`;
130
+ }
131
+
132
+ // ── Theme Token Injection ──
133
+ export function getThemeTokens(): { isDark: boolean; tokens: Record<string, string> } {
134
+ const isDark = !document.documentElement.hasAttribute('data-theme') ||
135
+ document.documentElement.getAttribute('data-theme') === 'dark';
136
+ const cs = getComputedStyle(document.documentElement);
137
+ return {
138
+ isDark,
139
+ tokens: {
140
+ '--bg': cs.getPropertyValue('--bg').trim(),
141
+ '--surface': cs.getPropertyValue('--surface').trim(),
142
+ '--border': cs.getPropertyValue('--border').trim(),
143
+ '--text': cs.getPropertyValue('--text').trim(),
144
+ '--text-dim': cs.getPropertyValue('--text-dim').trim(),
145
+ '--accent': cs.getPropertyValue('--accent').trim(),
146
+ '--font-ui': cs.getPropertyValue('--font-ui').trim(),
147
+ '--font-mono': cs.getPropertyValue('--font-mono').trim(),
148
+ '--radius-sm': cs.getPropertyValue('--radius-sm').trim(),
149
+ '--radius-md': cs.getPropertyValue('--radius-md').trim(),
150
+ },
151
+ };
152
+ }
153
+
154
+ // ── Bridge Script (injected into every iframe) ──
155
+ function getBridgeScript(nonce: string): string {
156
+ return `
157
+ <script>
158
+ (function() {
159
+ var __nonce = '${nonce}';
160
+
161
+ window.addEventListener('message', function(e) {
162
+ if (e.source !== window.parent) return;
163
+ if (!e.data || typeof e.data !== 'object') return;
164
+
165
+ if (e.data.type === 'jaw-theme-update') {
166
+ window.__jawTheme = { isDark: !!e.data.isDark };
167
+ window.__jawTokens = e.data.tokens || {};
168
+ window.dispatchEvent(new CustomEvent('jaw-theme-change', { detail: window.__jawTheme }));
169
+ }
170
+ if (e.data.type === 'jaw-request-resize') {
171
+ postHeight();
172
+ }
173
+ if (e.data.type === 'jaw-request-screenshot') {
174
+ var canvas = document.querySelector('canvas');
175
+ if (canvas) {
176
+ try {
177
+ var dataUrl = canvas.toDataURL('image/png');
178
+ window.parent.postMessage({ type: 'jaw-screenshot', dataUrl: dataUrl, nonce: __nonce }, '*');
179
+ } catch(ex) { /* tainted canvas or other error */ }
180
+ }
181
+ }
182
+ });
183
+
184
+ function postHeight() {
185
+ var h = Math.max(
186
+ document.body.scrollHeight,
187
+ document.body.offsetHeight,
188
+ document.documentElement.scrollHeight
189
+ );
190
+ window.parent.postMessage({ type: 'jaw-diagram-resize', height: h, nonce: __nonce }, '*');
191
+ }
192
+
193
+ if (typeof ResizeObserver !== 'undefined') {
194
+ var ro = new ResizeObserver(function() {
195
+ clearTimeout(ro._t);
196
+ ro._t = setTimeout(postHeight, 50);
197
+ });
198
+ ro.observe(document.body);
199
+ }
200
+
201
+ window.addEventListener('load', function() {
202
+ postHeight();
203
+ // Deferred re-measure for async chart renders (Chart.js animation, CDN loading)
204
+ setTimeout(postHeight, 200);
205
+ setTimeout(postHeight, 800);
206
+ window.parent.postMessage({ type: 'jaw-widget-ready', nonce: __nonce }, '*');
207
+ });
208
+
209
+ var lastSend = 0;
210
+ window.sendPrompt = function(text) {
211
+ var now = Date.now();
212
+ if (now - lastSend < 3000) return;
213
+ lastSend = now;
214
+ window.parent.postMessage({ type: 'jaw-send-prompt', text: String(text).slice(0, 500), nonce: __nonce }, '*');
215
+ };
216
+
217
+ // Ctrl+C / Cmd+C: forward selected text to host for clipboard access
218
+ document.addEventListener('copy', function() {
219
+ var sel = window.getSelection();
220
+ if (sel && sel.toString().trim()) {
221
+ window.parent.postMessage({
222
+ type: 'jaw-copy-text',
223
+ text: sel.toString().slice(0, 512),
224
+ nonce: __nonce
225
+ }, '*');
226
+ }
227
+ });
228
+ })();
229
+ <\/script>`;
230
+ }
231
+
232
+ // ── CDN Version Corrections (fix known bad versions in existing messages) ──
233
+ const CDN_VERSION_FIXES: [RegExp, string][] = [
234
+ [/\/p5\.js\/1\.11\.1[1-9]\//g, '/p5.js/1.11.10/'],
235
+ ];
236
+ function fixCdnVersions(html: string): string {
237
+ for (const [pattern, replacement] of CDN_VERSION_FIXES) html = html.replace(pattern, replacement);
238
+ return html;
239
+ }
240
+
241
+ // ── iframe Creator ──
242
+ export function createWidgetIframe(htmlCode: string): { iframe: HTMLIFrameElement; nonce: string } {
243
+ ensureCleanupObserver();
244
+ ensureWidgetObserver();
245
+
246
+ htmlCode = fixCdnVersions(htmlCode);
247
+
248
+ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)),
249
+ b => b.toString(16).padStart(2, '0')).join('');
250
+
251
+ const theme = getThemeTokens();
252
+ const cspMeta = buildCspMeta(htmlCode);
253
+ const importMap = buildImportMap(htmlCode);
254
+ const bridge = getBridgeScript(nonce);
255
+
256
+ const cssVars = Object.entries(theme.tokens)
257
+ .map(([k, v]) => `${k}: ${v};`)
258
+ .join('\n ');
259
+
260
+ const srcdoc = `<!DOCTYPE html>
261
+ <html>
262
+ <head>
263
+ <meta charset="utf-8">
264
+ ${cspMeta}
265
+ ${importMap}
266
+ <style>
267
+ :root { ${cssVars} }
268
+ * { margin: 0; box-sizing: border-box; }
269
+ body {
270
+ font-family: var(--font-ui), system-ui, sans-serif;
271
+ color: var(--text);
272
+ background: transparent;
273
+ padding: 16px;
274
+ overflow: hidden;
275
+ }
276
+ </style>
277
+ </head>
278
+ <body>
279
+ <script>
280
+ window.__jawTheme = ${JSON.stringify({ isDark: theme.isDark })};
281
+ window.__jawTokens = ${JSON.stringify(theme.tokens).replace(/<\//g, '<\\/')};
282
+ <\/script>
283
+ ${bridge}
284
+ ${htmlCode}
285
+ </body>
286
+ </html>`;
287
+
288
+ const iframe = document.createElement('iframe');
289
+ iframe.sandbox.add('allow-scripts');
290
+ iframe.srcdoc = srcdoc;
291
+ iframe.style.cssText = 'width: 100%; border: none; overflow: hidden; display: block;';
292
+ iframe.setAttribute('aria-label', 'Interactive diagram widget');
293
+
294
+ return { iframe, nonce };
295
+ }
296
+
297
+ // ── Activate All Pending Widgets ──
298
+ export function activateWidgets(container?: HTMLElement): void {
299
+ const root = container || document;
300
+ root.querySelectorAll('.diagram-widget-pending').forEach(el => {
301
+ const encoded = (el as HTMLElement).dataset.diagramHtml;
302
+ if (!encoded) return;
303
+ let htmlCode: string;
304
+ try {
305
+ // Cap widget payload at 512 KB to prevent memory/CPU abuse
306
+ if (encoded.length > 524_288) {
307
+ throw new Error('Widget payload too large');
308
+ }
309
+ htmlCode = decodeURIComponent(escape(atob(encoded)));
310
+ } catch {
311
+ el.replaceWith(Object.assign(document.createElement('div'), {
312
+ className: 'diagram-error',
313
+ textContent: 'Failed to decode widget content',
314
+ role: 'alert',
315
+ }));
316
+ return;
317
+ }
318
+
319
+ // Validate widget HTML before iframe injection
320
+ const validation = validateWidgetHtml(htmlCode);
321
+ if (!validation.valid) {
322
+ el.replaceWith(Object.assign(document.createElement('div'), {
323
+ className: 'diagram-error',
324
+ textContent: `Widget blocked: ${validation.reason}`,
325
+ role: 'alert',
326
+ }));
327
+ return;
328
+ }
329
+ if (validation.warnings.length) {
330
+ console.warn('[jaw-diagram] Widget warnings:', validation.warnings);
331
+ }
332
+
333
+ const wrapper = document.createElement('div');
334
+ wrapper.className = 'diagram-container diagram-widget';
335
+ // Preserve source for theme-change reload
336
+ wrapper.dataset.widgetHtml = encoded;
337
+
338
+ wrapper.appendChild(createDiagramSaveBtn());
339
+ wrapper.appendChild(createDiagramCopyBtn());
340
+ const { iframe, nonce } = createWidgetIframe(htmlCode);
341
+ wrapper.appendChild(iframe);
342
+
343
+ el.replaceWith(wrapper);
344
+
345
+ // Navigation defense: register ONLY on the first load event.
346
+ // No pre-load registration — prevents race where widget JS reads
347
+ // the nonce and self-navigates before the first load fires.
348
+ let initialLoadFired = false;
349
+ iframe.addEventListener('load', () => {
350
+ if (!initialLoadFired) {
351
+ initialLoadFired = true;
352
+ if (iframe.contentWindow) {
353
+ registeredIframes.add(iframe.contentWindow);
354
+ iframeNonces.set(iframe.contentWindow, nonce);
355
+ // Request initial resize now that channel is established
356
+ iframe.contentWindow.postMessage({ type: 'jaw-request-resize' }, '*');
357
+ // Deferred resize for slow CDN loads / async chart renders
358
+ setTimeout(() => iframe.contentWindow?.postMessage({ type: 'jaw-request-resize' }, '*'), 300);
359
+ setTimeout(() => iframe.contentWindow?.postMessage({ type: 'jaw-request-resize' }, '*'), 1000);
360
+ }
361
+ } else {
362
+ // Navigation detected — revoke postMessage trust permanently
363
+ if (iframe.contentWindow) {
364
+ registeredIframes.delete(iframe.contentWindow);
365
+ iframeNonces.delete(iframe.contentWindow);
366
+ }
367
+ console.warn('[jaw-diagram] iframe navigated — postMessage channel revoked');
368
+ }
369
+ });
370
+
371
+ // Timeout: if no jaw-widget-ready within 10s, show error.
372
+ // Uses a generation counter to detect iframe recreation (theme toggle).
373
+ const gen = Number(wrapper.dataset.gen || '0');
374
+ wrapper.dataset.gen = String(gen);
375
+ let readyReceived = false;
376
+ const readyHandler = (e: MessageEvent) => {
377
+ if (e.source === iframe.contentWindow && e.data?.type === 'jaw-widget-ready'
378
+ && e.data.nonce === nonce) {
379
+ readyReceived = true;
380
+ window.removeEventListener('message', readyHandler);
381
+ }
382
+ };
383
+ window.addEventListener('message', readyHandler);
384
+
385
+ setTimeout(() => {
386
+ window.removeEventListener('message', readyHandler);
387
+ // Skip if iframe was recreated (e.g. theme toggle)
388
+ if (Number(wrapper.dataset.gen || '0') !== gen) return;
389
+ if (!readyReceived && wrapper.isConnected) {
390
+ const failedWin = iframe.contentWindow;
391
+ if (failedWin) {
392
+ registeredIframes.delete(failedWin);
393
+ iframeNonces.delete(failedWin);
394
+ }
395
+ wrapper.innerHTML = `<div class="diagram-error" role="alert">
396
+ Widget failed to load within 10 seconds.
397
+ </div>`;
398
+ console.warn('[jaw-diagram] Widget timeout — iframe deregistered');
399
+ }
400
+ }, 10_000);
401
+ });
402
+ }
403
+
404
+ // ── Widget Reactivation Observer ──
405
+ // Only watches direct children of #chatMessages (not subtree) to avoid
406
+ // firing on every streaming innerHTML update inside message bubbles.
407
+ let widgetObserver: MutationObserver | null = null;
408
+ function ensureWidgetObserver(): void {
409
+ if (widgetObserver) return;
410
+ const chatEl = document.getElementById('chatMessages');
411
+ if (!chatEl) return;
412
+ widgetObserver = new MutationObserver((mutations) => {
413
+ for (const m of mutations) {
414
+ for (const node of m.addedNodes) {
415
+ if (!(node instanceof HTMLElement)) continue;
416
+ const hasPending = node.classList?.contains('diagram-widget-pending') ||
417
+ node.querySelector?.('.diagram-widget-pending');
418
+ if (hasPending) {
419
+ requestAnimationFrame(() => activateWidgets(node.parentElement || chatEl));
420
+ return;
421
+ }
422
+ }
423
+ }
424
+ });
425
+ // childList only (no subtree) — new messages are appended as direct children.
426
+ // activateWidgets is also called explicitly in ui.ts after rendering.
427
+ widgetObserver.observe(chatEl, { childList: true });
428
+ }
429
+
430
+ // ── Resize Throttle (max 1 per 100ms per iframe) ──
431
+ const resizeTimers = new WeakMap<Window, number>();
432
+
433
+ function throttledResize(source: Window, height: number): void {
434
+ if (resizeTimers.has(source)) return;
435
+ resizeTimers.set(source, window.setTimeout(() => resizeTimers.delete(source), 100));
436
+
437
+ document.querySelectorAll('iframe').forEach(iframe => {
438
+ if (iframe.contentWindow === source) {
439
+ iframe.style.height = `${Math.min(Math.max(height, 60), 2000)}px`;
440
+ }
441
+ });
442
+ }
443
+
444
+ // ── Theme Broadcast to All Widget iframes ──
445
+ // Also recreates iframes so baked-in chart colors update with new theme.
446
+ export function broadcastThemeToIframes(): void {
447
+ document.querySelectorAll('.diagram-widget').forEach(container => {
448
+ const encoded = (container as HTMLElement).dataset.widgetHtml;
449
+ if (!encoded) return;
450
+ let htmlCode: string;
451
+ try {
452
+ htmlCode = decodeURIComponent(escape(atob(encoded)));
453
+ } catch { return; }
454
+
455
+ // Deregister old iframe
456
+ const oldIframe = container.querySelector('iframe') as HTMLIFrameElement | null;
457
+ if (oldIframe?.contentWindow) {
458
+ registeredIframes.delete(oldIframe.contentWindow);
459
+ iframeNonces.delete(oldIframe.contentWindow);
460
+ }
461
+
462
+ // Bump generation to invalidate pending timeouts from activateWidgets
463
+ const cEl = container as HTMLElement;
464
+ cEl.dataset.gen = String((Number(cEl.dataset.gen || '0') || 0) + 1);
465
+
466
+ // Recreate with fresh theme tokens
467
+ const { iframe, nonce } = createWidgetIframe(htmlCode);
468
+ container.innerHTML = '';
469
+ container.appendChild(createDiagramSaveBtn());
470
+ container.appendChild(createDiagramCopyBtn());
471
+ container.appendChild(iframe);
472
+
473
+ let initialLoadFired = false;
474
+ iframe.addEventListener('load', () => {
475
+ if (!initialLoadFired) {
476
+ initialLoadFired = true;
477
+ if (iframe.contentWindow) {
478
+ registeredIframes.add(iframe.contentWindow);
479
+ iframeNonces.set(iframe.contentWindow, nonce);
480
+ iframe.contentWindow.postMessage({ type: 'jaw-request-resize' }, '*');
481
+ setTimeout(() => iframe.contentWindow?.postMessage({ type: 'jaw-request-resize' }, '*'), 300);
482
+ setTimeout(() => iframe.contentWindow?.postMessage({ type: 'jaw-request-resize' }, '*'), 1000);
483
+ }
484
+ } else {
485
+ if (iframe.contentWindow) {
486
+ registeredIframes.delete(iframe.contentWindow);
487
+ iframeNonces.delete(iframe.contentWindow);
488
+ }
489
+ }
490
+ });
491
+ });
492
+ }
493
+
494
+ // ── Host postMessage Listener ──
495
+ let lastHostSendPrompt = 0;
496
+ window.addEventListener('message', (e: MessageEvent) => {
497
+ if (!e.data || typeof e.data !== 'object') return;
498
+ if (!e.source) return;
499
+ // Defense-in-depth: sandbox="allow-scripts" without allow-same-origin → opaque origin ("null")
500
+ if (e.origin !== 'null') return;
501
+ if (!registeredIframes.has(e.source as Window)) return;
502
+ // Reject messages from iframes removed from DOM
503
+ const sourceIframe = [...document.querySelectorAll('iframe')].find(f => f.contentWindow === e.source);
504
+ if (!sourceIframe?.isConnected) return;
505
+ // Validate per-iframe nonce
506
+ const expectedNonce = iframeNonces.get(e.source as Window);
507
+ if (!expectedNonce || e.data.nonce !== expectedNonce) return;
508
+
509
+ switch (e.data.type) {
510
+ case 'jaw-diagram-resize': {
511
+ const h = Number(e.data.height);
512
+ if (!Number.isFinite(h) || h < 0) return;
513
+ throttledResize(e.source as Window, h);
514
+ break;
515
+ }
516
+
517
+ case 'jaw-send-prompt': {
518
+ const now = Date.now();
519
+ if (now - lastHostSendPrompt < 3000) return;
520
+ lastHostSendPrompt = now;
521
+
522
+ const text = String(e.data.text || '').trim().slice(0, 500);
523
+ if (!text) return;
524
+ const input = document.getElementById('chatInput') as HTMLTextAreaElement | null;
525
+ if (input) {
526
+ input.value = text;
527
+ input.dispatchEvent(new Event('input', { bubbles: true }));
528
+ input.focus();
529
+ }
530
+ break;
531
+ }
532
+
533
+ case 'jaw-copy-text': {
534
+ const text = String(e.data.text || '').trim().slice(0, 512);
535
+ if (!text) return;
536
+ navigator.clipboard.writeText(text).catch(() => {});
537
+ break;
538
+ }
539
+
540
+ case 'jaw-screenshot': {
541
+ const dataUrl = String(e.data.dataUrl || '');
542
+ if (!dataUrl.startsWith('data:image/')) return;
543
+ // Cap at 5 MB to prevent abuse
544
+ if (dataUrl.length > 5_242_880) return;
545
+ fetch(dataUrl).then(r => r.blob()).then(blob => {
546
+ const url = URL.createObjectURL(blob);
547
+ const a = document.createElement('a');
548
+ a.href = url;
549
+ a.download = `widget-${Date.now()}.png`;
550
+ a.click();
551
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
552
+ }).catch(() => {});
553
+ break;
554
+ }
555
+
556
+ case 'jaw-widget-ready':
557
+ break;
558
+ }
559
+ });
@@ -0,0 +1,129 @@
1
+ // ── Diagram Types & SVG Extraction ──
2
+ // Stack-based SVG parser + code-fence shielding for diagram rendering pipeline
3
+
4
+ export interface SvgBlock {
5
+ id: number;
6
+ index: number;
7
+ length: number;
8
+ svg: string;
9
+ kind: 'complete' | 'partial' | 'error';
10
+ placeholder: string;
11
+ }
12
+
13
+ let svgCounter = 0;
14
+
15
+ /** Reset counter (for testing) */
16
+ export function resetSvgCounter(): void { svgCounter = 0; }
17
+
18
+ /**
19
+ * Pre-shield: wrap SVG inside fenced code blocks with NUL markers
20
+ * so extractTopLevelSvg() skips them.
21
+ * NUL chars are safe — un-shielded BEFORE marked/DOMPurify runs.
22
+ *
23
+ * Shields BOTH backtick (```) and tilde (~~~) fences.
24
+ */
25
+ export function shieldCodeFenceSvg(text: string): { text: string; fences: Map<string, string> } {
26
+ const fences = new Map<string, string>();
27
+ // CommonMark: 0-3 leading spaces allowed before opening fence
28
+ const fenced = text.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n {0,3}\1[ \t]*$/gm, (match) => {
29
+ const key = `\x00FENCE-${svgCounter++}\x00`;
30
+ fences.set(key, match);
31
+ return key;
32
+ });
33
+ return { text: fenced, fences };
34
+ }
35
+
36
+ export function unshieldCodeFenceSvg(text: string, fences: Map<string, string>): string {
37
+ for (const [key, val] of fences) {
38
+ // Use function replacement to avoid $& $' $` special patterns in val
39
+ text = text.replace(key, () => val);
40
+ }
41
+ return text;
42
+ }
43
+
44
+ /**
45
+ * Stack-based top-level SVG extractor.
46
+ * Returns extracted blocks + text with placeholders.
47
+ *
48
+ * Placeholders use `<div data-jaw-svg="N">` — standard HTML block elements that:
49
+ * - Survive marked.parse() (block-level HTML = type 6, never wrapped in <p>)
50
+ * - Survive DOMPurify natively (<div> is allowed; data-* attrs via ADD_ATTR)
51
+ * - Use \n\n wrapping to ensure paragraph boundary separation
52
+ */
53
+ export function extractTopLevelSvg(text: string, isStreaming = false): { text: string; blocks: SvgBlock[] } {
54
+ const blocks: SvgBlock[] = [];
55
+ let result = '';
56
+ let i = 0;
57
+
58
+ while (i < text.length) {
59
+ const openIdx = text.indexOf('<svg', i);
60
+ if (openIdx === -1) {
61
+ result += text.slice(i);
62
+ break;
63
+ }
64
+
65
+ const charAfter = text[openIdx + 4];
66
+ if (charAfter && !/[\s\/>]/.test(charAfter)) {
67
+ result += text.slice(i, openIdx + 4);
68
+ i = openIdx + 4;
69
+ continue;
70
+ }
71
+
72
+ result += text.slice(i, openIdx);
73
+
74
+ let depth = 0;
75
+ let j = openIdx;
76
+ let matched = false;
77
+
78
+ while (j < text.length) {
79
+ const nextOpen = text.indexOf('<svg', j + 1);
80
+ const nextClose = text.indexOf('</svg>', j + (j === openIdx ? 0 : 1));
81
+
82
+ if (nextClose === -1) break;
83
+
84
+ if (nextOpen !== -1 && nextOpen < nextClose) {
85
+ const afterOpen = text[nextOpen + 4];
86
+ if (afterOpen && /[\s\/>]/.test(afterOpen)) {
87
+ depth++;
88
+ }
89
+ j = nextOpen + 4;
90
+ } else {
91
+ if (depth === 0) {
92
+ const endIdx = nextClose + '</svg>'.length;
93
+ const svgRaw = text.slice(openIdx, endIdx);
94
+ const id = svgCounter++;
95
+ const placeholder = `\n\n<div data-jaw-svg="${id}"></div>\n\n`;
96
+ blocks.push({ id, index: openIdx, length: svgRaw.length, svg: svgRaw, kind: 'complete', placeholder });
97
+ result += placeholder;
98
+ i = endIdx;
99
+ matched = true;
100
+ break;
101
+ } else {
102
+ depth--;
103
+ j = nextClose + '</svg>'.length;
104
+ }
105
+ }
106
+ }
107
+
108
+ if (!matched) {
109
+ const id = svgCounter++;
110
+ if (isStreaming) {
111
+ const svgPartial = text.slice(openIdx);
112
+ const placeholder = `\n\n<div data-jaw-svg="${id}" data-jaw-kind="partial"></div>\n\n`;
113
+ blocks.push({ id, index: openIdx, length: svgPartial.length, svg: '', kind: 'partial', placeholder });
114
+ result += placeholder;
115
+ i = text.length;
116
+ } else {
117
+ const placeholder = `\n\n<div data-jaw-svg="${id}" data-jaw-kind="error"></div>\n\n`;
118
+ const restFromOpen = text.slice(openIdx);
119
+ const blankLineIdx = restFromOpen.search(/\n\s*\n/);
120
+ const consumeLen = blankLineIdx !== -1 ? blankLineIdx : restFromOpen.length;
121
+ blocks.push({ id, index: openIdx, length: consumeLen, svg: '', kind: 'error', placeholder });
122
+ result += placeholder;
123
+ i = openIdx + consumeLen;
124
+ }
125
+ }
126
+ }
127
+
128
+ return { text: result, blocks };
129
+ }