ompchamber 0.3.0 → 0.4.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 (240) hide show
  1. package/README.md +9 -6
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/static/css/index.5291f58223.css +2 -0
  4. package/dist/client/static/js/async/{1k.544adbf77d.js → 1k.5974df3b87.js} +2 -2
  5. package/dist/client/static/js/async/3j.357c80ff38.js +2 -0
  6. package/dist/client/static/js/async/{3y.22075aa079.js → 3y.ededeebde8.js} +5 -5
  7. package/dist/client/static/js/async/51.95bcae98af.js +2 -0
  8. package/dist/client/static/js/async/6.f034c9b528.js +1 -0
  9. package/dist/client/static/js/async/a.e05da304c8.js +10 -0
  10. package/dist/client/static/js/async/ar.1e14da5588.js +1 -0
  11. package/dist/client/static/js/async/art.62139ed479.js +2 -0
  12. package/dist/client/static/js/async/ct.b74928f7a3.js +1 -0
  13. package/dist/client/static/js/async/fw.6a51998a8e.js +6 -0
  14. package/dist/client/static/js/async/g.7a4bbd4798.js +2 -0
  15. package/dist/client/static/js/async/g.7a4bbd4798.js.LICENSE.txt +11 -0
  16. package/dist/client/static/js/async/{k.e878dd350f.js → k.7466330d1a.js} +2 -2
  17. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +1 -0
  18. package/dist/client/static/js/async/kc.bbdb35b771.js +1 -0
  19. package/dist/client/static/js/async/l6.c56640c205.js +1 -0
  20. package/dist/client/static/js/async/{m.3fc017a055.js → m.4b29d34f93.js} +3 -3
  21. package/dist/client/static/js/async/{og.ae7acf03ac.js → og.634fba99ac.js} +2 -2
  22. package/dist/client/static/js/async/q.7fb4d2f374.js +21 -0
  23. package/dist/client/static/js/async/rd.cb6bf495bb.js +2 -0
  24. package/dist/client/static/js/async/rf.d626afc367.js +2 -0
  25. package/dist/client/static/js/async/s.413c2a4683.js +2 -0
  26. package/dist/client/static/js/async/s.413c2a4683.js.LICENSE.txt +6 -0
  27. package/dist/client/static/js/async/sy.d16be843a7.js +94 -0
  28. package/dist/client/static/js/async/t.cb3fcf8615.js +1 -0
  29. package/dist/client/static/js/async/ud.3065bfee31.js +2 -0
  30. package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
  31. package/dist/client/static/js/async/uf.46ad6209fa.js.LICENSE.txt +6 -0
  32. package/dist/client/static/js/async/v.7d28d3c241.js +34 -0
  33. package/dist/client/static/js/async/v0.394ed37c6b.js +2 -0
  34. package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +6 -0
  35. package/dist/client/static/js/async/vt.42416b707d.js +1 -0
  36. package/dist/client/static/js/async/vy.bbe114c4e0.js +2 -0
  37. package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +6 -0
  38. package/dist/client/static/js/async/xw.ae67ba06fd.js +31 -0
  39. package/dist/client/static/js/async/xw.ae67ba06fd.js.LICENSE.txt +4 -0
  40. package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
  41. package/dist/client/static/js/async/yv.2fa22f009a.js +1 -0
  42. package/dist/client/static/js/index.bf1b1fe988.js +33 -0
  43. package/dist/client/static/js/index.bf1b1fe988.js.LICENSE.txt +4 -0
  44. package/dist/client/static/js/{markdown-vendor.9da142c67b.js → markdown-vendor.71e43278b3.js} +1 -1
  45. package/dist/client/static/js/uy.5f791144ce.js +139 -0
  46. package/package.json +2 -1
  47. package/src/client/App.tsx +22 -5
  48. package/src/client/components/common/MarkdownRenderer.tsx +36 -0
  49. package/src/client/components/layout/desktop-layout/index.tsx +1 -1
  50. package/src/client/components/layout/session-sidebar/Modals.tsx +1 -1
  51. package/src/client/components/mobile/LayoutWrapper.tsx +1 -1
  52. package/src/client/components/settings/LazyModal.tsx +46 -0
  53. package/src/client/components/settings/Modal.tsx +1 -1
  54. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +43 -29
  55. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +15 -69
  56. package/src/client/components/workspace/chat-timeline/MessageList.tsx +47 -66
  57. package/src/client/components/workspace/chat-timeline/RunFooter.tsx +60 -0
  58. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +37 -21
  59. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +22 -4
  60. package/src/client/components/workspace/chat-timeline/index.tsx +3 -6
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +15 -19
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader.tsx +46 -0
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput.tsx +18 -68
  64. package/src/client/components/workspace/file-explorer/TreeItem.tsx +8 -2
  65. package/src/client/hooks/chat/omp/index.ts +9 -10
  66. package/src/client/hooks/chat/omp/revalidation-throttle.ts +6 -5
  67. package/src/client/hooks/chat/timeline/actions.ts +1 -2
  68. package/src/client/hooks/chat/timeline/index.ts +5 -0
  69. package/src/client/hooks/chat/timeline/send.ts +1 -2
  70. package/src/client/hooks/chat/timeline/session-load.ts +3 -4
  71. package/src/client/hooks/ui/syntax-ready.ts +16 -3
  72. package/src/client/main.tsx +2 -5
  73. package/src/client/stubs/elkjs.ts +37 -0
  74. package/src/client/tailwind.css +10 -0
  75. package/src/server/lib/fs/git-ignore.ts +49 -0
  76. package/src/server/lib/omp/rpc/constants.ts +10 -0
  77. package/src/server/lib/omp/rpc/lifecycle.test.ts +138 -0
  78. package/src/server/lib/omp/rpc/manager.ts +32 -4
  79. package/src/server/lib/omp/rpc/session-commands.ts +32 -9
  80. package/src/server/lib/omp/rpc/session-registry.ts +3 -2
  81. package/src/server/lib/omp/rpc/subagent-liveness.test.ts +62 -0
  82. package/src/server/lib/omp/rpc/subagent-liveness.ts +100 -0
  83. package/src/server/lib/omp/session/messages.test.ts +112 -0
  84. package/src/server/lib/omp/session/messages.ts +6 -5
  85. package/src/server/routes/agent/command.ts +34 -16
  86. package/src/server/routes/fs/read.ts +15 -7
  87. package/src/shared/lib/chat/notice-row.test.ts +89 -0
  88. package/src/shared/lib/chat/notice-row.ts +78 -0
  89. package/src/shared/lib/chat/omp/abort.ts +41 -0
  90. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +154 -0
  91. package/src/shared/lib/chat/timeline/omp-callbacks.ts +36 -7
  92. package/src/shared/lib/chat/timeline/run-footer.test.ts +84 -0
  93. package/src/shared/lib/chat/timeline/run-footer.ts +96 -0
  94. package/src/shared/lib/chat/tool-output.test.ts +91 -0
  95. package/src/shared/lib/chat/tool-output.ts +54 -0
  96. package/src/shared/lib/chat/xml-envelope.ts +158 -0
  97. package/src/shared/lib/code/highlighter-lazy.ts +106 -0
  98. package/src/shared/lib/code/highlighter.ts +31 -52
  99. package/src/shared/lib/code/syntax-highlight.test.ts +36 -1
  100. package/src/shared/lib/code/syntax-highlight.ts +17 -3
  101. package/src/shared/lib/format/time.ts +19 -0
  102. package/src/shared/lib/markdown/fira-code-fonts.css +51 -0
  103. package/src/shared/lib/markdown/katex-fonts.css +20 -0
  104. package/src/shared/lib/markdown/katex.ts +101 -0
  105. package/src/shared/lib/markdown/marked.ts +23 -14
  106. package/src/shared/lib/markdown/math.test.ts +109 -0
  107. package/src/shared/lib/markdown/mermaid.ts +6 -0
  108. package/src/shared/lib/markdown/sanitize.ts +48 -2
  109. package/src/shared/lib/omp/session/mapper.ts +11 -3
  110. package/src/shared/lib/omp/session/messages-map.ts +12 -12
  111. package/src/shared/types/chat.ts +3 -1
  112. package/src/shared/types/fs.ts +3 -0
  113. package/dist/client/static/css/g.f36aa429fa.css +0 -1
  114. package/dist/client/static/css/index.a0f07455bd.css +0 -2
  115. package/dist/client/static/css/katex-vendor.ca3a0600e8.css +0 -1
  116. package/dist/client/static/font/KaTeX_AMS-Regular.1608a09b4a.woff +0 -0
  117. package/dist/client/static/font/KaTeX_AMS-Regular.4aafdb685c.ttf +0 -0
  118. package/dist/client/static/font/KaTeX_Caligraphic-Bold.b67709187b.woff +0 -0
  119. package/dist/client/static/font/KaTeX_Caligraphic-Bold.cce5b8ecea.ttf +0 -0
  120. package/dist/client/static/font/KaTeX_Caligraphic-Regular.07ef19e7b0.ttf +0 -0
  121. package/dist/client/static/font/KaTeX_Caligraphic-Regular.dad44a7fc6.woff +0 -0
  122. package/dist/client/static/font/KaTeX_Fraktur-Bold.9f256b8593.woff +0 -0
  123. package/dist/client/static/font/KaTeX_Fraktur-Bold.b18f59e1d1.ttf +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Regular.7c1871215e.woff +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.ed38e79f57.ttf +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.b74a1a8b2c.ttf +0 -0
  127. package/dist/client/static/font/KaTeX_Main-Bold.d181c4650d.woff +0 -0
  128. package/dist/client/static/font/KaTeX_Main-BoldItalic.70d8b0a530.ttf +0 -0
  129. package/dist/client/static/font/KaTeX_Main-BoldItalic.e3f82f9d27.woff +0 -0
  130. package/dist/client/static/font/KaTeX_Main-Italic.47373d1e51.ttf +0 -0
  131. package/dist/client/static/font/KaTeX_Main-Italic.9024d815ba.woff +0 -0
  132. package/dist/client/static/font/KaTeX_Main-Regular.7f51fe0340.woff +0 -0
  133. package/dist/client/static/font/KaTeX_Main-Regular.b7f8fe9b5f.ttf +0 -0
  134. package/dist/client/static/font/KaTeX_Math-BoldItalic.a879cf8383.ttf +0 -0
  135. package/dist/client/static/font/KaTeX_Math-BoldItalic.f1035d8d5d.woff +0 -0
  136. package/dist/client/static/font/KaTeX_Math-Italic.5295ba483a.woff +0 -0
  137. package/dist/client/static/font/KaTeX_Math-Italic.939bc64440.ttf +0 -0
  138. package/dist/client/static/font/KaTeX_SansSerif-Bold.94e1e8dc5f.ttf +0 -0
  139. package/dist/client/static/font/KaTeX_SansSerif-Bold.bf59d231c3.woff +0 -0
  140. package/dist/client/static/font/KaTeX_SansSerif-Italic.7c9bc82b17.woff +0 -0
  141. package/dist/client/static/font/KaTeX_SansSerif-Italic.b4c20c84d8.ttf +0 -0
  142. package/dist/client/static/font/KaTeX_SansSerif-Regular.740484788f.woff +0 -0
  143. package/dist/client/static/font/KaTeX_SansSerif-Regular.d4d7ba4804.ttf +0 -0
  144. package/dist/client/static/font/KaTeX_Script-Regular.0750571032.woff +0 -0
  145. package/dist/client/static/font/KaTeX_Script-Regular.fe9cbbe1a0.ttf +0 -0
  146. package/dist/client/static/font/KaTeX_Size1-Regular.e1e279cbdd.woff +0 -0
  147. package/dist/client/static/font/KaTeX_Size1-Regular.fabc004aab.ttf +0 -0
  148. package/dist/client/static/font/KaTeX_Size2-Regular.577270225e.woff +0 -0
  149. package/dist/client/static/font/KaTeX_Size2-Regular.d6b476ecd3.ttf +0 -0
  150. package/dist/client/static/font/KaTeX_Size3-Regular.9acaf01c64.woff +0 -0
  151. package/dist/client/static/font/KaTeX_Size3-Regular.a144ef5840.ttf +0 -0
  152. package/dist/client/static/font/KaTeX_Size4-Regular.7a996c9da2.woff +0 -0
  153. package/dist/client/static/font/KaTeX_Size4-Regular.fbccdabe0a.ttf +0 -0
  154. package/dist/client/static/font/KaTeX_Typewriter-Regular.6258592bdc.woff +0 -0
  155. package/dist/client/static/font/KaTeX_Typewriter-Regular.d97aaf4a1e.ttf +0 -0
  156. package/dist/client/static/font/fira-code-cyrillic-400-normal.5971438a4a.woff2 +0 -0
  157. package/dist/client/static/font/fira-code-cyrillic-400-normal.9bb7bfea9c.woff +0 -0
  158. package/dist/client/static/font/fira-code-cyrillic-500-normal.5486265b18.woff2 +0 -0
  159. package/dist/client/static/font/fira-code-cyrillic-500-normal.89f92bfb31.woff +0 -0
  160. package/dist/client/static/font/fira-code-cyrillic-600-normal.2805b497e5.woff +0 -0
  161. package/dist/client/static/font/fira-code-cyrillic-600-normal.86e0df22a3.woff2 +0 -0
  162. package/dist/client/static/font/fira-code-cyrillic-700-normal.505a88f22a.woff +0 -0
  163. package/dist/client/static/font/fira-code-cyrillic-700-normal.76f5e8d2e2.woff2 +0 -0
  164. package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.3c4b15e57d.woff2 +0 -0
  165. package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.61323debaa.woff +0 -0
  166. package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.0f34210f2a.woff2 +0 -0
  167. package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.2497cfa1aa.woff +0 -0
  168. package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.40fce94af9.woff2 +0 -0
  169. package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.6c8774b829.woff +0 -0
  170. package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.4b742eb865.woff +0 -0
  171. package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.e75eb4fccc.woff2 +0 -0
  172. package/dist/client/static/font/fira-code-greek-400-normal.bb900468b6.woff2 +0 -0
  173. package/dist/client/static/font/fira-code-greek-400-normal.be76e699ba.woff +0 -0
  174. package/dist/client/static/font/fira-code-greek-500-normal.176c9fb001.woff2 +0 -0
  175. package/dist/client/static/font/fira-code-greek-500-normal.d2c596136b.woff +0 -0
  176. package/dist/client/static/font/fira-code-greek-600-normal.490c3deed7.woff +0 -0
  177. package/dist/client/static/font/fira-code-greek-600-normal.7df14376d9.woff2 +0 -0
  178. package/dist/client/static/font/fira-code-greek-700-normal.c82b6072fc.woff2 +0 -0
  179. package/dist/client/static/font/fira-code-greek-700-normal.d5f2b2917b.woff +0 -0
  180. package/dist/client/static/font/fira-code-greek-ext-400-normal.4849a79781.woff +0 -0
  181. package/dist/client/static/font/fira-code-greek-ext-400-normal.7f5bfc4538.woff2 +0 -0
  182. package/dist/client/static/font/fira-code-greek-ext-500-normal.aaccca4075.woff2 +0 -0
  183. package/dist/client/static/font/fira-code-greek-ext-500-normal.e383181256.woff +0 -0
  184. package/dist/client/static/font/fira-code-greek-ext-600-normal.78cb4bff8e.woff +0 -0
  185. package/dist/client/static/font/fira-code-greek-ext-600-normal.d6c93d54f9.woff2 +0 -0
  186. package/dist/client/static/font/fira-code-greek-ext-700-normal.57ec7c92d9.woff +0 -0
  187. package/dist/client/static/font/fira-code-greek-ext-700-normal.8fafdae5e8.woff2 +0 -0
  188. package/dist/client/static/font/fira-code-latin-400-normal.973295532b.woff +0 -0
  189. package/dist/client/static/font/fira-code-latin-500-normal.7ae2be8fb8.woff +0 -0
  190. package/dist/client/static/font/fira-code-latin-600-normal.1dbbe5bc27.woff +0 -0
  191. package/dist/client/static/font/fira-code-latin-700-normal.497a70148b.woff +0 -0
  192. package/dist/client/static/font/fira-code-latin-ext-400-normal.9dfae29aaf.woff +0 -0
  193. package/dist/client/static/font/fira-code-latin-ext-400-normal.e534ae46e2.woff2 +0 -0
  194. package/dist/client/static/font/fira-code-latin-ext-500-normal.24c7b781b0.woff +0 -0
  195. package/dist/client/static/font/fira-code-latin-ext-500-normal.aad44ab072.woff2 +0 -0
  196. package/dist/client/static/font/fira-code-latin-ext-600-normal.7bed0f2f7e.woff +0 -0
  197. package/dist/client/static/font/fira-code-latin-ext-600-normal.c509a7c9f1.woff2 +0 -0
  198. package/dist/client/static/font/fira-code-latin-ext-700-normal.6c66c8b0a9.woff2 +0 -0
  199. package/dist/client/static/font/fira-code-latin-ext-700-normal.88f64e2822.woff +0 -0
  200. package/dist/client/static/font/fira-code-symbols2-400-normal.97d3f4b17c.woff +0 -0
  201. package/dist/client/static/font/fira-code-symbols2-500-normal.b06f1a06e4.woff +0 -0
  202. package/dist/client/static/font/fira-code-symbols2-600-normal.304b653b93.woff +0 -0
  203. package/dist/client/static/font/fira-code-symbols2-700-normal.d8ce0ad586.woff +0 -0
  204. package/dist/client/static/js/async/3j.a2fcea99be.js +0 -2
  205. package/dist/client/static/js/async/51.1938cec320.js +0 -2
  206. package/dist/client/static/js/async/6.7929428505.js +0 -1
  207. package/dist/client/static/js/async/69.a18a10882f.js +0 -1
  208. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  209. package/dist/client/static/js/async/ar.9dc8b1992a.js +0 -2
  210. package/dist/client/static/js/async/bi.cbfe8eaa8e.js +0 -1
  211. package/dist/client/static/js/async/ct.38d9da8a5a.js +0 -1
  212. package/dist/client/static/js/async/fw.fb1a08b92d.js +0 -6
  213. package/dist/client/static/js/async/l6.45f3b62e15.js +0 -1
  214. package/dist/client/static/js/async/q.0b6fb9bf8d.js +0 -21
  215. package/dist/client/static/js/async/rd.b77011ab7f.js +0 -2
  216. package/dist/client/static/js/async/rf.cfbde7f744.js +0 -2
  217. package/dist/client/static/js/async/s.c7d521a8ae.js +0 -2
  218. package/dist/client/static/js/async/sy.7345abcd67.js +0 -94
  219. package/dist/client/static/js/async/t.0dcb01dfde.js +0 -1
  220. package/dist/client/static/js/async/v.5c7a4b087b.js +0 -1
  221. package/dist/client/static/js/async/vu.311f1d202c.js +0 -34
  222. package/dist/client/static/js/async/yg.9bd4e09ecb.js +0 -149
  223. package/dist/client/static/js/async/yv.593a3f7d54.js +0 -1
  224. package/dist/client/static/js/g.124fba0826.js +0 -139
  225. package/dist/client/static/js/index.907fd4670e.js +0 -68
  226. package/dist/client/static/js/katex-vendor.95e48cf65a.js +0 -1
  227. package/src/shared/lib/chat/order.ts +0 -40
  228. /package/dist/client/static/js/async/{1k.544adbf77d.js.LICENSE.txt → 1k.5974df3b87.js.LICENSE.txt} +0 -0
  229. /package/dist/client/static/js/async/{3j.a2fcea99be.js.LICENSE.txt → 3j.357c80ff38.js.LICENSE.txt} +0 -0
  230. /package/dist/client/static/js/async/{3y.22075aa079.js.LICENSE.txt → 3y.ededeebde8.js.LICENSE.txt} +0 -0
  231. /package/dist/client/static/js/async/{51.1938cec320.js.LICENSE.txt → 51.95bcae98af.js.LICENSE.txt} +0 -0
  232. /package/dist/client/static/js/async/{ar.9dc8b1992a.js.LICENSE.txt → a.e05da304c8.js.LICENSE.txt} +0 -0
  233. /package/dist/client/static/js/async/{fw.fb1a08b92d.js.LICENSE.txt → art.62139ed479.js.LICENSE.txt} +0 -0
  234. /package/dist/client/static/js/async/{m.3fc017a055.js.LICENSE.txt → fw.6a51998a8e.js.LICENSE.txt} +0 -0
  235. /package/dist/client/static/js/async/{q.0b6fb9bf8d.js.LICENSE.txt → m.4b29d34f93.js.LICENSE.txt} +0 -0
  236. /package/dist/client/static/js/async/{rf.cfbde7f744.js.LICENSE.txt → q.7fb4d2f374.js.LICENSE.txt} +0 -0
  237. /package/dist/client/static/js/async/{rd.b77011ab7f.js.LICENSE.txt → rd.cb6bf495bb.js.LICENSE.txt} +0 -0
  238. /package/dist/client/static/js/async/{s.c7d521a8ae.js.LICENSE.txt → rf.d626afc367.js.LICENSE.txt} +0 -0
  239. /package/dist/client/static/js/{index.907fd4670e.js.LICENSE.txt → async/ud.3065bfee31.js.LICENSE.txt} +0 -0
  240. /package/dist/client/static/js/{g.124fba0826.js.LICENSE.txt → uy.5f791144ce.js.LICENSE.txt} +0 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Demand-driven KaTeX rendering.
8
+ *
9
+ * `katex` is ~522 kB of JS that only matters when a message actually contains
10
+ * math. Importing it statically from `marked.ts` put it in the *synchronous*
11
+ * bundle: every page load paid for it, whether or not a single formula was ever
12
+ * displayed.
13
+ *
14
+ * The pipeline keeps its synchronous shape by emitting a placeholder instead:
15
+ * `marked.ts` renders `<span class="math-pending" data-math="…">` during parse,
16
+ * and this module swaps those placeholders for real KaTeX markup once the
17
+ * library lands — the same split `mermaid.ts` uses for ```mermaid fences, so
18
+ * the two features behave identically from the renderer's point of view.
19
+ */
20
+
21
+ import { sanitizeKatexHtml } from '@/shared/lib/markdown/sanitize';
22
+
23
+ type KatexModule = typeof import('katex');
24
+
25
+ let katexPromise: Promise<KatexModule['default'] | null> | null = null;
26
+
27
+ /** Placeholder class emitted by `marked.ts` in place of rendered math. */
28
+ export const MATH_PENDING_CLASS = 'math-pending';
29
+
30
+ /**
31
+ * Load KaTeX once per page. Never throws: on failure the promise resolves
32
+ * `null` and placeholders stay as visible source text, which is what the old
33
+ * synchronous `renderKatex` fallback did for invalid LaTeX.
34
+ */
35
+ function loadKatex(): Promise<KatexModule['default'] | null> {
36
+ if (!katexPromise) {
37
+ katexPromise = import('katex')
38
+ .then((mod) => mod.default)
39
+ .catch((error) => {
40
+ console.error('[katex] load failed', error);
41
+ return null;
42
+ });
43
+ }
44
+ return katexPromise;
45
+ }
46
+
47
+ /**
48
+ * Start loading KaTeX without awaiting it. Called from the renderer so the
49
+ * library is already in flight while the rest of the message renders.
50
+ */
51
+ export function preloadKatex(): void {
52
+ void loadKatex();
53
+ }
54
+
55
+ /**
56
+ * Render one placeholder element into real KaTeX markup. `displayMode` is
57
+ * carried on the placeholder as `data-math-display` ("1" for block math).
58
+ *
59
+ * The generated markup is passed through DOMPurify's MathML profile before it
60
+ * touches the DOM: `tex` is model output, so although KaTeX escapes its own
61
+ * output, the result still crosses an innerHTML boundary and gets sanitized
62
+ * like every other model-authored fragment in this pipeline.
63
+ */
64
+ function renderPlaceholder(el: HTMLElement, katex: KatexModule['default']): void {
65
+ const tex = el.dataset.math ?? '';
66
+ const displayMode = el.dataset.mathDisplay === '1';
67
+ try {
68
+ el.innerHTML = sanitizeKatexHtml(katex.renderToString(tex, { throwOnError: false, displayMode }));
69
+ el.classList.remove(MATH_PENDING_CLASS);
70
+ el.removeAttribute('data-math');
71
+ el.removeAttribute('data-math-display');
72
+ } catch {
73
+ // Keep the source text visible rather than blanking the formula.
74
+ el.textContent = tex;
75
+ el.classList.remove(MATH_PENDING_CLASS);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Replace every pending math placeholder under `root` with KaTeX output.
81
+ * Loads the library on demand; resolves with how many were rendered.
82
+ */
83
+ export async function hydrateMathBlocks(root: ParentNode): Promise<{ rendered: number; failed: number }> {
84
+ const pending = [...root.querySelectorAll<HTMLElement>(`.${MATH_PENDING_CLASS}`)];
85
+ if (pending.length === 0) return { rendered: 0, failed: 0 };
86
+
87
+ const katex = await loadKatex();
88
+ if (!katex) return { rendered: 0, failed: pending.length };
89
+
90
+ let rendered = 0;
91
+ for (const el of pending) {
92
+ // A streaming re-render may have replaced the node while awaiting.
93
+ if (!el.isConnected) continue;
94
+ renderPlaceholder(el, katex);
95
+ rendered += 1;
96
+ }
97
+ return { rendered, failed: 0 };
98
+ }
99
+
100
+ /** Sanitize KaTeX-generated markup (kept here so callers need one import). */
101
+ export { sanitizeKatexHtml };
@@ -19,9 +19,9 @@
19
19
 
20
20
  import { Marked } from 'marked';
21
21
  import markedLinkifyIt from 'marked-linkify-it';
22
- import katex from 'katex';
23
22
  import remend from 'remend';
24
23
  import { highlightCode } from '@/shared/lib/code/syntax-highlight';
24
+ import { MATH_PENDING_CLASS } from '@/shared/lib/markdown/katex';
25
25
 
26
26
  /** Escape HTML for safe interpolation inside highlighted output. */
27
27
  function escapeHtml(value: string): string {
@@ -33,15 +33,24 @@ function escapeHtml(value: string): string {
33
33
  .replace(/'/g, '&#039;');
34
34
  }
35
35
 
36
- /** Render KaTeX to HTML. Errors are non-fatal: invalid math renders as red
37
- * source (throwOnError:false), so a stale LaTeX half during streaming never
38
- * breaks the whole message. */
39
- function renderKatex(tex: string, displayMode: boolean): string {
40
- try {
41
- return katex.renderToString(tex, { throwOnError: false, displayMode });
42
- } catch {
43
- return displayMode ? `<p>${tex}</p>` : tex;
44
- }
36
+ /**
37
+ * Emit a math placeholder instead of rendering KaTeX inline.
38
+ *
39
+ * `katex` is ~522 kB of JS worth loading only when a message contains math, so
40
+ * it is not imported here. The placeholder carries the TeX source and the
41
+ * display flag; `katex.ts` swaps it for real markup once the library lands
42
+ * (see `hydrateMathBlocks`). Both attributes are HTML-escaped, and escaping
43
+ * `&` first keeps the entity encoding intact through the attribute round-trip.
44
+ */
45
+ function renderMathPlaceholder(tex: string, displayMode: boolean): string {
46
+ const escaped = escapeHtml(tex);
47
+ const display = displayMode ? ' data-math-display="1"' : '';
48
+ const span = `<span class="${MATH_PENDING_CLASS}" data-math="${escaped}"${display}></span>`;
49
+ // Block math must stay inside a block element: DOMPurify drops a bare
50
+ // `<span>` that sits at the top level of the fragment, which silently deleted
51
+ // every `$$…$$` formula. KaTeX's own output is inline-level too, so the old
52
+ // synchronous renderer relied on the same wrapper being present.
53
+ return displayMode ? `<p>${span}</p>` : span;
45
54
  }
46
55
 
47
56
  interface TokenLike {
@@ -130,7 +139,7 @@ marked.use({
130
139
  : undefined;
131
140
  },
132
141
  renderer(token: TokenLike) {
133
- return renderKatex(token.text ?? '', false);
142
+ return renderMathPlaceholder(token.text ?? '', false);
134
143
  },
135
144
  },
136
145
  // \[ ... \] block math
@@ -147,7 +156,7 @@ marked.use({
147
156
  : undefined;
148
157
  },
149
158
  renderer(token: TokenLike) {
150
- return renderKatex(token.text ?? '', true);
159
+ return renderMathPlaceholder(token.text ?? '', true);
151
160
  },
152
161
  },
153
162
  // $$ ... $$ display math (block-level, must be its own line)
@@ -164,7 +173,7 @@ marked.use({
164
173
  : undefined;
165
174
  },
166
175
  renderer(token: TokenLike) {
167
- return renderKatex(token.text ?? '', true);
176
+ return renderMathPlaceholder(token.text ?? '', true);
168
177
  },
169
178
  },
170
179
  // $ ... $ inline math, per remark-math flanking rules: the opening $ must
@@ -186,7 +195,7 @@ marked.use({
186
195
  : undefined;
187
196
  },
188
197
  renderer(token: TokenLike) {
189
- return renderKatex(token.text ?? '', false);
198
+ return renderMathPlaceholder(token.text ?? '', false);
190
199
  },
191
200
  },
192
201
  ],
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * End-to-end coverage for lazy math rendering.
8
+ *
9
+ * KaTeX is no longer rendered during markdown parsing (it is a ~522 kB async
10
+ * chunk), so the pipeline emits a `.math-pending` placeholder that
11
+ * `hydrateMathBlocks` later swaps for real markup. That split has two failure
12
+ * modes a unit test on either half alone would miss:
13
+ *
14
+ * 1. DOMPurify drops a bare root-level `<span>`, which silently deleted every
15
+ * block (`$$…$$`) formula and stripped KaTeX's outer `.katex` wrapper.
16
+ * 2. The placeholder attributes (`data-math`, `data-math-display`) must survive
17
+ * `sanitizeHtml`, or hydration has nothing to read.
18
+ *
19
+ * These run against a real DOM (happy-dom) because both paths are DOM-dependent;
20
+ * they are no-ops everywhere else.
21
+ */
22
+
23
+ import { beforeAll, describe, expect, test } from 'bun:test';
24
+ import { Window } from 'happy-dom';
25
+
26
+ let hydrateMathBlocks: typeof import('@/shared/lib/markdown/katex').hydrateMathBlocks;
27
+ let renderMarkdown: typeof import('@/shared/lib/markdown/marked').renderMarkdown;
28
+ let sanitizeHtml: typeof import('@/shared/lib/markdown/sanitize').sanitizeHtml;
29
+
30
+ beforeAll(async () => {
31
+ const win = new Window({ url: 'http://localhost' });
32
+ Object.assign(globalThis, {
33
+ window: win,
34
+ document: win.document,
35
+ MutationObserver: win.MutationObserver,
36
+ Node: win.Node,
37
+ Element: win.Element,
38
+ HTMLElement: win.HTMLElement,
39
+ NodeFilter: win.NodeFilter,
40
+ });
41
+ // Dynamic import is required here, not stylistic: `sanitize.ts` binds
42
+ // DOMPurify at module scope, which captures the DOM globals present at
43
+ // evaluation time. A static import would run before the happy-dom globals
44
+ // above exist and bind the sanitizer to a window-less environment.
45
+ ({ hydrateMathBlocks } = await import('@/shared/lib/markdown/katex'));
46
+ ({ renderMarkdown } = await import('@/shared/lib/markdown/marked'));
47
+ ({ sanitizeHtml } = await import('@/shared/lib/markdown/sanitize'));
48
+ });
49
+
50
+ /** Render markdown and mount it exactly as `MarkdownRenderer` does. */
51
+ function mount(content: string): HTMLElement {
52
+ const host = document.createElement('div');
53
+ host.innerHTML = sanitizeHtml(renderMarkdown(content));
54
+ document.body.appendChild(host);
55
+ return host;
56
+ }
57
+
58
+ describe('lazy math rendering', () => {
59
+ test('inline math survives sanitization and hydrates to KaTeX', async () => {
60
+ const host = mount('rumus $E = mc^2$ disini');
61
+
62
+ const placeholder = host.querySelector('.math-pending');
63
+ expect(placeholder).not.toBeNull();
64
+ expect(placeholder?.getAttribute('data-math')).toBe('E = mc^2');
65
+
66
+ expect(await hydrateMathBlocks(host)).toEqual({ rendered: 1, failed: 0 });
67
+
68
+ expect(host.querySelectorAll('.math-pending').length).toBe(0);
69
+ // The outer wrapper is what every katex.css layout rule targets.
70
+ expect(host.querySelector('.katex')).not.toBeNull();
71
+ expect(host.querySelectorAll('.katex math').length).toBeGreaterThan(0);
72
+ });
73
+
74
+ test('block math renders in display mode', async () => {
75
+ const host = mount('$$\\int_0^1 x^2 dx$$');
76
+
77
+ const placeholder = host.querySelector('.math-pending');
78
+ expect(placeholder).not.toBeNull();
79
+ expect(placeholder?.getAttribute('data-math-display')).toBe('1');
80
+
81
+ await hydrateMathBlocks(host);
82
+ expect(host.querySelector('.katex-display')).not.toBeNull();
83
+ });
84
+
85
+ test('multiple formulas each hydrate', async () => {
86
+ // `$$…$$` only parses as display math on its own line (see the flanking
87
+ // rules in marked.ts), so the block case is a separate line here.
88
+ const host = mount('$a$ dan $b$\n\n$$c$$');
89
+ expect(host.querySelectorAll('.math-pending').length).toBe(3);
90
+
91
+ expect((await hydrateMathBlocks(host)).rendered).toBe(3);
92
+ expect(host.querySelectorAll('.katex').length).toBe(3);
93
+ });
94
+
95
+ test('no placeholders is a no-op', async () => {
96
+ const host = mount('plain text, no math');
97
+ expect(await hydrateMathBlocks(host)).toEqual({ rendered: 0, failed: 0 });
98
+ });
99
+
100
+ test('invalid LaTeX degrades without throwing', async () => {
101
+ const host = mount('bad $\\frac{1}{$ here');
102
+ await expect(hydrateMathBlocks(host)).resolves.toBeDefined();
103
+ });
104
+
105
+ test('escapes the tex source so it cannot break out of the attribute', () => {
106
+ const html = renderMarkdown('a $x" onmouseover="alert(1)$ b');
107
+ expect(html).not.toContain('onmouseover="alert');
108
+ });
109
+ });
@@ -63,6 +63,12 @@ async function loadMermaid(themeKey: ThemeKey): Promise<MermaidModule['default']
63
63
  startOnLoad: false,
64
64
  securityLevel: 'strict',
65
65
  theme: themeKey,
66
+ // Mermaid 12 defaults the global layout to `elk`, which is what pulled
67
+ // the ~1.4 MB elkjs bundle into the client build for every diagram.
68
+ // `dagre` ships inside mermaid's own core chunk and renders all the
69
+ // diagram types this app sees, so pin it here and alias `elkjs` to a
70
+ // stub in the build (`src/client/stubs/elkjs.ts`).
71
+ layout: 'dagre',
66
72
  flowchart: { useMaxWidth: false, wrappingWidth: 100000 },
67
73
  sequence: { useMaxWidth: false },
68
74
  });
@@ -103,11 +103,23 @@ export function sanitizeHtml(html: string): string {
103
103
  if (typeof window === 'undefined' || !window.document) return html;
104
104
 
105
105
  const hasSvgIcon = /<svg[^>]*>/.test(html);
106
- const config = hasKatex(html) || hasSvgIcon
106
+ const hasMathPlaceholder = html.includes('math-pending');
107
+ const config = hasKatex(html) || hasSvgIcon || hasMathPlaceholder
107
108
  ? {
108
109
  USE_PROFILES: { html: true, mathMl: true } as const,
109
110
  ADD_TAGS: [...KATEX_MATHML_TAGS, ...(hasSvgIcon ? COPY_ICON_TAGS : [])],
110
- ADD_ATTR: ['aria-hidden', 'aria-label', 'encoding', 'nonce', ...(hasSvgIcon ? COPY_ICON_ATTRS : [])],
111
+ ADD_ATTR: [
112
+ 'aria-hidden',
113
+ 'aria-label',
114
+ 'encoding',
115
+ 'nonce',
116
+ // Math placeholders carry the TeX source and display flag for the
117
+ // lazy KaTeX pass; DOMPurify strips unknown data-* attributes unless
118
+ // they are listed here.
119
+ 'data-math',
120
+ 'data-math-display',
121
+ ...(hasSvgIcon ? COPY_ICON_ATTRS : []),
122
+ ],
111
123
  }
112
124
  : { USE_PROFILES: { html: true } as const };
113
125
 
@@ -136,3 +148,37 @@ export function sanitizeMermaidSvg(svg: string): string {
136
148
  return svg;
137
149
  }
138
150
  }
151
+
152
+ /**
153
+ * Sanitize a KaTeX-rendered HTML fragment before it is injected via innerHTML.
154
+ *
155
+ * `sanitizeHtml` already allows the MathML profile when the *parsed markdown*
156
+ * contains KaTeX classes, but lazy math rendering injects after that pass, so
157
+ * the generated fragment is sanitized separately.
158
+ *
159
+ * `class` MUST be allowed: KaTeX's visual layout is entirely class-driven
160
+ * (`.katex`, `.katex-mathml`, `.katex-html`), and DOMPurify drops attributes it
161
+ * has not been told about. Without it the wrapper spans are stripped and only
162
+ * bare MathML survives, which renders unstyled.
163
+ */
164
+ export function sanitizeKatexHtml(html: string): string {
165
+ if (!html) return html;
166
+ if (typeof window === 'undefined' || !window.document) return html;
167
+
168
+ // DOMPurify drops a bare root-level <span>, which would strip KaTeX's outer
169
+ // `.katex` wrapper — the element every layout rule in katex.css hangs off.
170
+ // Wrapping the fragment keeps the parser from discarding it; the wrapper is
171
+ // removed again afterwards so only the KaTeX markup is injected.
172
+ const wrapped = `<div data-katex-wrap>${html}</div>`;
173
+ try {
174
+ const clean = DOMPurify.sanitize(wrapped, {
175
+ USE_PROFILES: { html: true, mathMl: true },
176
+ ADD_ATTR: ['class', 'aria-hidden', 'encoding', 'style', 'data-katex-wrap'],
177
+ } as Config);
178
+ const host = document.createElement('div');
179
+ host.innerHTML = clean;
180
+ return host.querySelector('[data-katex-wrap]')?.innerHTML ?? clean;
181
+ } catch {
182
+ return html;
183
+ }
184
+ }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import type { ChatMessageData, ToolCallData } from '@/shared/types';
16
16
  import { extractText, extractUserImageAttachments, parseMessageBlocks, stripInlinedTextAttachments, toToolCallData } from '@/shared/lib/omp/session/parse-message-blocks';
17
+ import { reminderPartIndex } from '@/shared/lib/chat/notice-row';
17
18
  import { deriveTurnError } from '@/shared/lib/omp/session/turn-error';
18
19
  import { formatClock } from '@/shared/lib/format/time';
19
20
  import { toEpochMs } from '@/shared/lib/omp/session/timestamps';
@@ -72,9 +73,16 @@ export function toChatMessage(raw: Record<string, unknown>, streaming = true): C
72
73
  ?? (startedAt !== undefined && durationMs !== undefined ? startedAt + durationMs : undefined);
73
74
  const provider = typeof raw.provider === 'string' ? raw.provider : undefined;
74
75
 
75
- if (raw.role === 'developer' || raw.role === 'system' || parsed.textParts.some((t) => /<\/?system-reminder[^>]*>/i.test(t))) {
76
- const rawText = parsed.textParts.join('\n').trim();
77
- const notice = rawText.replace(/<\/?system-reminder[^>]*>/g, '').trim();
76
+ const reminderIdx = reminderPartIndex(parsed.textParts);
77
+ if (raw.role === 'developer' || raw.role === 'system' || reminderIdx !== undefined) {
78
+ // A reminder envelope keeps its wrapper — `SystemNotice` reads that tag to
79
+ // label the card and strips it before markdown, and the JSONL path stores
80
+ // it the same way so a live row and its reloaded twin are identical.
81
+ // Developer/system turns are notices by role and have no wrapper to keep.
82
+ const source = reminderIdx !== undefined ? parsed.textParts[reminderIdx] : parsed.textParts.join('\n');
83
+ const notice = reminderIdx !== undefined
84
+ ? source.trim()
85
+ : source.replace(/<\/?system-reminder[^>]*>/g, '').trim();
78
86
  if (!notice) return null;
79
87
  return {
80
88
  id,
@@ -6,6 +6,7 @@
6
6
  import type { ChatMessageData } from '@/shared/types/chat';
7
7
  import { extractText, extractUserImageAttachments, parseAssistantContent, resultOutput, roleFor, stripInlinedTextAttachments, type OmpMessageEntry } from '@/shared/lib/omp/session/messages-parse';
8
8
  import { isRecord } from '@/shared/lib/util/guards';
9
+ import { reminderPartIndex } from '@/shared/lib/chat/notice-row';
9
10
  import { deriveTurnError } from '@/shared/lib/omp/session/turn-error';
10
11
  import { toEpochMs } from '@/shared/lib/omp/session/timestamps';
11
12
 
@@ -74,19 +75,18 @@ export function toChatMessage(entry: OmpMessageEntry): ChatMessageData | null {
74
75
  durationMs,
75
76
  usage,
76
77
  };
77
- // Live-path parity (mapper.toChatMessage): an assistant turn whose text is a
78
- // <system-reminder> block renders as a SystemNotice alert, never as raw
78
+ // Live-path parity (mapper.toChatMessage): a text block that IS a
79
+ // <system-reminder> envelope renders as a SystemNotice alert, never as raw
79
80
  // content — otherwise the reminder leaks into the timeline as markdown.
80
- if (parsed.textParts.some((t) => /<\/?system-reminder[^>]*>/i.test(t))) {
81
- const notice = parsed.textParts.join('\n').replace(/<\/?system-reminder[^>]*>/gi, '').trim();
82
- if (notice) {
83
- message.notice = notice;
84
- message.content = '';
85
- } else if (!parsed.thinking && parsed.toolCalls.length === 0 && !turnError) {
86
- return null;
87
- } else {
88
- message.content = '';
89
- }
81
+ // Only a wrapper counts: prose that merely quotes the tag is content, and
82
+ // moving it into `notice` would hide the answer and delete the quoted tags.
83
+ const reminderIdx = reminderPartIndex(parsed.textParts);
84
+ if (reminderIdx !== undefined) {
85
+ message.notice = parsed.textParts[reminderIdx].trim();
86
+ message.content = parsed.textParts
87
+ .filter((_, index) => index !== reminderIdx)
88
+ .join('\n')
89
+ .trim();
90
90
  }
91
91
  if (turnError) message.error = turnError;
92
92
  if (parsed.thinking) message.thinking = { thought: parsed.thinking, isGenerating: false };
@@ -125,7 +125,9 @@ export interface ChatMessageData {
125
125
  /** Short human intent for tool calls (omp arguments.i) shown before the tools. */
126
126
  intent?: string;
127
127
  /** System notice surfaced by omp (e.g. ultrathink-notice) — rendered as an
128
- * alert row, not as assistant content. */
128
+ * alert row, not as assistant content. A row that ALSO carries the turn's
129
+ * own metadata (model/provider/usage/…) is an assistant answer omp diverted
130
+ * into this field and renders as text — see chat/notice-row.ts. */
129
131
  notice?: string;
130
132
  toolCalls?: ToolCallData[];
131
133
  actions?: (AgentActionData | ToolCallData)[];
@@ -6,6 +6,9 @@ export interface FsNode {
6
6
  modified?: number;
7
7
  children?: FsNode[];
8
8
  forceExpanded?: boolean;
9
+ /** Matches a git ignore rule (repo `.gitignore`, `.git/info/exclude`, or the
10
+ * global `core.excludesFile`) — rendered faded in the Files panel. */
11
+ ignored?: boolean;
9
12
  }
10
13
 
11
14
  export interface OpenedFile {
@@ -1 +0,0 @@
1
- @font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-cyrillic-ext-400-normal.3c4b15e57d.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-ext-400-normal.61323debaa.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-cyrillic-400-normal.5971438a4a.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-400-normal.9bb7bfea9c.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-greek-ext-400-normal.7f5bfc4538.woff2)format("woff2"),url(/static/font/fira-code-greek-ext-400-normal.4849a79781.woff)format("woff");unicode-range:U+1F??}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-greek-400-normal.bb900468b6.woff2)format("woff2"),url(/static/font/fira-code-greek-400-normal.be76e699ba.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,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)format("woff2"),url(/static/font/fira-code-symbols2-400-normal.97d3f4b17c.woff)format("woff");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-latin-ext-400-normal.e534ae46e2.woff2)format("woff2"),url(/static/font/fira-code-latin-ext-400-normal.9dfae29aaf.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:400;src:url(/static/font/fira-code-latin-400-normal.2655b10c55.woff2)format("woff2"),url(/static/font/fira-code-latin-400-normal.973295532b.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-cyrillic-ext-500-normal.0f34210f2a.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-ext-500-normal.2497cfa1aa.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-cyrillic-500-normal.5486265b18.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-500-normal.89f92bfb31.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-greek-ext-500-normal.aaccca4075.woff2)format("woff2"),url(/static/font/fira-code-greek-ext-500-normal.e383181256.woff)format("woff");unicode-range:U+1F??}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-greek-500-normal.176c9fb001.woff2)format("woff2"),url(/static/font/fira-code-greek-500-normal.d2c596136b.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(data:font/woff2;base64,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)format("woff2"),url(/static/font/fira-code-symbols2-500-normal.b06f1a06e4.woff)format("woff");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-latin-ext-500-normal.aad44ab072.woff2)format("woff2"),url(/static/font/fira-code-latin-ext-500-normal.24c7b781b0.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:500;src:url(/static/font/fira-code-latin-500-normal.92a991f204.woff2)format("woff2"),url(/static/font/fira-code-latin-500-normal.7ae2be8fb8.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-cyrillic-ext-600-normal.40fce94af9.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-ext-600-normal.6c8774b829.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-cyrillic-600-normal.86e0df22a3.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-600-normal.2805b497e5.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-greek-ext-600-normal.d6c93d54f9.woff2)format("woff2"),url(/static/font/fira-code-greek-ext-600-normal.78cb4bff8e.woff)format("woff");unicode-range:U+1F??}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-greek-600-normal.7df14376d9.woff2)format("woff2"),url(/static/font/fira-code-greek-600-normal.490c3deed7.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(data:font/woff2;base64,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)format("woff2"),url(/static/font/fira-code-symbols2-600-normal.304b653b93.woff)format("woff");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-latin-ext-600-normal.c509a7c9f1.woff2)format("woff2"),url(/static/font/fira-code-latin-ext-600-normal.7bed0f2f7e.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:600;src:url(/static/font/fira-code-latin-600-normal.43788eb7f1.woff2)format("woff2"),url(/static/font/fira-code-latin-600-normal.1dbbe5bc27.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-cyrillic-ext-700-normal.e75eb4fccc.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-ext-700-normal.4b742eb865.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-cyrillic-700-normal.76f5e8d2e2.woff2)format("woff2"),url(/static/font/fira-code-cyrillic-700-normal.505a88f22a.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-greek-ext-700-normal.8fafdae5e8.woff2)format("woff2"),url(/static/font/fira-code-greek-ext-700-normal.57ec7c92d9.woff)format("woff");unicode-range:U+1F??}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-greek-700-normal.c82b6072fc.woff2)format("woff2"),url(/static/font/fira-code-greek-700-normal.d5f2b2917b.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(data:font/woff2;base64,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)format("woff2"),url(/static/font/fira-code-symbols2-700-normal.d8ce0ad586.woff)format("woff");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-latin-ext-700-normal.6c66c8b0a9.woff2)format("woff2"),url(/static/font/fira-code-latin-ext-700-normal.88f64e2822.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fira Code;font-style:normal;font-display:swap;font-weight:700;src:url(/static/font/fira-code-latin-700-normal.2667215d33.woff2)format("woff2"),url(/static/font/fira-code-latin-700-normal.497a70148b.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}