@baique/pi-web-sky 0.1.35 → 0.1.36

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 (294) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +16 -16
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/react-loadable-manifest.json +1 -1
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +3 -3
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
  20. package/.next/server/app/api/agent/[id]/events/route.js +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js +1 -1
  22. package/.next/server/app/api/app-update/route.js +1 -1
  23. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  24. package/.next/server/app/api/cwd/validate/route.js +1 -1
  25. package/.next/server/app/api/default-cwd/route.js +1 -1
  26. package/.next/server/app/api/file-index/route.js +1 -1
  27. package/.next/server/app/api/files/[...path]/route.js +1 -1
  28. package/.next/server/app/api/mcp/route.js +1 -1
  29. package/.next/server/app/api/models/route.js +1 -1
  30. package/.next/server/app/api/plugins/route.js +1 -1
  31. package/.next/server/app/api/project-trust/route.js +1 -1
  32. package/.next/server/app/api/search/route.js +1 -1
  33. package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
  34. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  35. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
  36. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  37. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  38. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  39. package/.next/server/app/api/sessions/route.js +1 -1
  40. package/.next/server/app/api/skills/check/route.js +1 -1
  41. package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
  42. package/.next/server/app/api/skills/install/route.js +1 -1
  43. package/.next/server/app/api/skills/route.js +2 -31
  44. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/update/route.js +1 -1
  46. package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/route.js +1 -1
  48. package/.next/server/app/index.html +1 -1
  49. package/.next/server/app/index.rsc +5 -5
  50. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  51. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  52. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/page.js +2 -2
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app-paths-manifest.json +16 -16
  56. package/.next/server/chunks/2404.js +1 -1
  57. package/.next/server/chunks/3500.js +30 -0
  58. package/.next/server/chunks/4226.js +2 -2
  59. package/.next/server/chunks/542.js +10 -5
  60. package/.next/server/chunks/5794.js +1 -1
  61. package/.next/server/chunks/6429.js +1 -1
  62. package/.next/server/chunks/898.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
  68. package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
  69. package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
  70. package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
  71. package/.next/static/css/422c7dc0faa04d89.css +1 -0
  72. package/.next/trace +8 -9
  73. package/.next/trace-build +1 -1
  74. package/bin/pi-web-sky.js +15 -20
  75. package/lib/agent-client.test.mjs +64 -0
  76. package/lib/agent-client.ts +53 -0
  77. package/lib/agent-event-connection.test.mjs +193 -0
  78. package/lib/agent-event-connection.ts +202 -0
  79. package/lib/agent-event-stream.test.mjs +155 -0
  80. package/lib/agent-event-stream.ts +132 -0
  81. package/lib/agent-event-wire.test.mjs +278 -0
  82. package/lib/agent-event-wire.ts +107 -0
  83. package/lib/agent-phase.ts +24 -0
  84. package/lib/allowed-roots.ts +31 -0
  85. package/lib/ansi.test.mjs +52 -0
  86. package/lib/ansi.ts +211 -0
  87. package/lib/api-types.ts +139 -0
  88. package/lib/app-update.test.mjs +27 -0
  89. package/lib/app-update.ts +28 -0
  90. package/lib/atomic-file.test.mjs +37 -0
  91. package/lib/atomic-file.ts +34 -0
  92. package/lib/audit-session.ts +235 -0
  93. package/lib/bash-output.test.mjs +63 -0
  94. package/lib/bash-output.ts +53 -0
  95. package/lib/bg-image.ts +129 -0
  96. package/lib/board-events.ts +83 -0
  97. package/lib/board-purge.test.mjs +66 -0
  98. package/lib/board-purge.ts +65 -0
  99. package/lib/board-search.test.mjs +110 -0
  100. package/lib/board-search.ts +63 -0
  101. package/lib/board-store.test.mjs +542 -0
  102. package/lib/board-store.ts +794 -0
  103. package/lib/board-types.ts +96 -0
  104. package/lib/board-utils.ts +17 -0
  105. package/lib/bounded-form-data.test.mjs +54 -0
  106. package/lib/bounded-form-data.ts +49 -0
  107. package/lib/browser-notifications.test.mjs +167 -0
  108. package/lib/browser-notifications.ts +104 -0
  109. package/lib/chat-lazy-load.test.mjs +140 -0
  110. package/lib/chat-lazy-load.ts +64 -0
  111. package/lib/clipboard-paths.test.mjs +140 -0
  112. package/lib/clipboard-paths.ts +192 -0
  113. package/lib/clipboard.ts +18 -0
  114. package/lib/compaction-summary.test.mjs +44 -0
  115. package/lib/compaction-summary.ts +34 -0
  116. package/lib/custom-ui-terminal.test.mjs +32 -0
  117. package/lib/custom-ui-terminal.ts +27 -0
  118. package/lib/deepseek-pricing.test.mjs +120 -0
  119. package/lib/deepseek-pricing.ts +71 -0
  120. package/lib/directory-browser.test.mjs +60 -0
  121. package/lib/directory-browser.ts +83 -0
  122. package/lib/draft-stash.test.mjs +91 -0
  123. package/lib/draft-stash.ts +102 -0
  124. package/lib/draft-store.ts +105 -0
  125. package/lib/dropdown-direction.ts +27 -0
  126. package/lib/extension-command.test.mjs +63 -0
  127. package/lib/extension-command.ts +81 -0
  128. package/lib/file-access.test.mjs +31 -0
  129. package/lib/file-access.ts +59 -0
  130. package/lib/file-dirent.test.mjs +56 -0
  131. package/lib/file-dirent.ts +17 -0
  132. package/lib/file-explorer-state.test.mjs +63 -0
  133. package/lib/file-explorer-state.ts +36 -0
  134. package/lib/file-fuzzy.test.mjs +29 -0
  135. package/lib/file-fuzzy.ts +189 -0
  136. package/lib/file-links.test.mjs +92 -0
  137. package/lib/file-links.ts +148 -0
  138. package/lib/file-paths.ts +43 -0
  139. package/lib/file-types.test.mjs +34 -0
  140. package/lib/file-types.ts +73 -0
  141. package/lib/file-upload.test.mjs +70 -0
  142. package/lib/file-upload.ts +59 -0
  143. package/lib/file-viewer-state.test.mjs +20 -0
  144. package/lib/file-viewer-state.ts +15 -0
  145. package/lib/frontmatter.test.mjs +55 -0
  146. package/lib/frontmatter.ts +87 -0
  147. package/lib/git-changes.test.mjs +52 -0
  148. package/lib/git-changes.ts +232 -0
  149. package/lib/git-status.ts +47 -0
  150. package/lib/git-types.ts +29 -0
  151. package/lib/http-dispatcher.test.mjs +83 -0
  152. package/lib/http-dispatcher.ts +86 -0
  153. package/lib/i18n/format.test.mjs +19 -0
  154. package/lib/i18n/format.ts +60 -0
  155. package/lib/i18n/messages/en.ts +662 -0
  156. package/lib/i18n/messages/zh-CN.ts +661 -0
  157. package/lib/i18n/registry.test.mjs +32 -0
  158. package/lib/i18n/registry.ts +43 -0
  159. package/lib/i18n/types.ts +15 -0
  160. package/lib/image-attachments.test.mjs +27 -0
  161. package/lib/image-attachments.ts +56 -0
  162. package/lib/initial-navigation.test.mjs +65 -0
  163. package/lib/initial-navigation.ts +15 -0
  164. package/lib/markdown.test.mjs +104 -0
  165. package/lib/markdown.ts +347 -0
  166. package/lib/message-display.test.mjs +130 -0
  167. package/lib/message-display.ts +47 -0
  168. package/lib/model-catalog.test.mjs +327 -0
  169. package/lib/model-catalog.ts +414 -0
  170. package/lib/model-discovery-auth.ts +58 -0
  171. package/lib/model-discovery.test.mjs +50 -0
  172. package/lib/model-discovery.ts +75 -0
  173. package/lib/model-id-preference.ts +46 -0
  174. package/lib/model-scope.test.mjs +193 -0
  175. package/lib/model-scope.ts +198 -0
  176. package/lib/models-cache.test.mjs +122 -0
  177. package/lib/models-cache.ts +90 -0
  178. package/lib/models-config-store.test.mjs +200 -0
  179. package/lib/models-config-store.ts +84 -0
  180. package/lib/next-config-esm.test.mjs +14 -0
  181. package/lib/next-config.test.mjs +13 -0
  182. package/lib/node-version.test.mjs +33 -0
  183. package/lib/normalize.test.mjs +54 -0
  184. package/lib/normalize.ts +63 -0
  185. package/lib/npx.ts +62 -0
  186. package/lib/panel-layout.test.mjs +56 -0
  187. package/lib/panel-layout.ts +52 -0
  188. package/lib/patch.test.mjs +110 -0
  189. package/lib/patch.ts +134 -0
  190. package/lib/path-security.ts +42 -0
  191. package/lib/paths.test.mjs +58 -0
  192. package/lib/paths.ts +71 -0
  193. package/lib/pi-types.ts +183 -0
  194. package/lib/pi-web-options.test.mjs +63 -0
  195. package/lib/process-lifecycle.test.mjs +83 -0
  196. package/lib/project-command-env.test.mjs +298 -0
  197. package/lib/project-command-env.ts +129 -0
  198. package/lib/project-groups.test.mjs +60 -0
  199. package/lib/project-groups.ts +53 -0
  200. package/lib/project-identity.test.mjs +37 -0
  201. package/lib/project-identity.ts +26 -0
  202. package/lib/project-tree.test.mjs +115 -0
  203. package/lib/project-tree.ts +176 -0
  204. package/lib/project-trust.test.mjs +117 -0
  205. package/lib/project-trust.ts +48 -0
  206. package/lib/prompt-recovery.test.mjs +33 -0
  207. package/lib/prompt-recovery.ts +47 -0
  208. package/lib/provider-api-key-route.test.mjs +12 -0
  209. package/lib/provider-credential-store.test.mjs +126 -0
  210. package/lib/provider-credential-store.ts +114 -0
  211. package/lib/provider-listing-runtime.ts +37 -0
  212. package/lib/provider-listing.test.mjs +114 -0
  213. package/lib/provider-listing.ts +118 -0
  214. package/lib/provider-services.ts +64 -0
  215. package/lib/request-security.test.mjs +256 -0
  216. package/lib/request-security.ts +136 -0
  217. package/lib/rpc-manager-shutdown.test.mjs +372 -0
  218. package/lib/rpc-manager-widgets.test.mjs +342 -0
  219. package/lib/rpc-manager.test.mjs +139 -0
  220. package/lib/rpc-manager.ts +1703 -0
  221. package/lib/rpc-session-info.test.mjs +93 -0
  222. package/lib/session-delete.test.mjs +53 -0
  223. package/lib/session-delete.ts +86 -0
  224. package/lib/session-file-references-core.ts +82 -0
  225. package/lib/session-file-references.test.mjs +91 -0
  226. package/lib/session-file-references.ts +29 -0
  227. package/lib/session-path.test.mjs +24 -0
  228. package/lib/session-path.ts +11 -0
  229. package/lib/session-reader.pagination.test.mjs +115 -0
  230. package/lib/session-reader.test.mjs +446 -0
  231. package/lib/session-reader.ts +548 -0
  232. package/lib/session-resolve.test.mjs +65 -0
  233. package/lib/session-row-context-menu.test.mjs +46 -0
  234. package/lib/session-row-context-menu.ts +29 -0
  235. package/lib/session-scanner.test.mjs +151 -0
  236. package/lib/session-scanner.ts +238 -0
  237. package/lib/session-search.test.mjs +173 -0
  238. package/lib/session-search.ts +284 -0
  239. package/lib/session-stats.ts +26 -0
  240. package/lib/session-timing.test.mjs +107 -0
  241. package/lib/session-timing.ts +45 -0
  242. package/lib/session-title.test.mjs +284 -0
  243. package/lib/session-title.ts +255 -0
  244. package/lib/skill-frontmatter.test.mjs +75 -0
  245. package/lib/skill-frontmatter.ts +40 -0
  246. package/lib/skill-lock.test.mjs +182 -0
  247. package/lib/skill-lock.ts +146 -0
  248. package/lib/skill-updates.test.mjs +182 -0
  249. package/lib/skill-updates.ts +263 -0
  250. package/lib/skills-service.ts +16 -0
  251. package/lib/slash-display.test.mjs +122 -0
  252. package/lib/slash-display.ts +30 -0
  253. package/lib/sqlite-db.test.mjs +140 -0
  254. package/lib/sqlite-db.ts +205 -0
  255. package/lib/startup-preferences.test.mjs +130 -0
  256. package/lib/startup-preferences.ts +65 -0
  257. package/lib/streaming-message.test.mjs +212 -0
  258. package/lib/streaming-message.ts +151 -0
  259. package/lib/subagent-widget.test.mjs +87 -0
  260. package/lib/subagent-widget.ts +109 -0
  261. package/lib/sync-room-server.mjs +137 -0
  262. package/lib/task-card-store.test.mjs +309 -0
  263. package/lib/task-card-store.ts +559 -0
  264. package/lib/task-scheduler.ts +498 -0
  265. package/lib/task-store.test.mjs +216 -0
  266. package/lib/task-store.ts +271 -0
  267. package/lib/terminal-input.test.mjs +46 -0
  268. package/lib/terminal-input.ts +63 -0
  269. package/lib/terminal-manager.test.mjs +71 -0
  270. package/lib/terminal-manager.ts +245 -0
  271. package/lib/tool-execution-progress.test.mjs +32 -0
  272. package/lib/tool-execution-progress.ts +29 -0
  273. package/lib/tool-names.ts +25 -0
  274. package/lib/tool-preset-preference.test.mjs +72 -0
  275. package/lib/tool-preset-preference.ts +41 -0
  276. package/lib/tool-presets.test.mjs +56 -0
  277. package/lib/tool-presets.ts +42 -0
  278. package/lib/turn-written-files.test.mjs +137 -0
  279. package/lib/turn-written-files.ts +60 -0
  280. package/lib/types.ts +349 -0
  281. package/lib/wallpaper-settings.ts +191 -0
  282. package/lib/web-auth.test.mjs +50 -0
  283. package/lib/web-auth.ts +45 -0
  284. package/lib/workspace-memory.test.mjs +129 -0
  285. package/lib/workspace-memory.ts +98 -0
  286. package/lib/worktree.test.mjs +50 -0
  287. package/lib/worktree.ts +250 -0
  288. package/package.json +3 -1
  289. package/server.mjs +34 -0
  290. package/.next/server/chunks/7513.js +0 -30
  291. package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
  292. package/.next/static/css/cf25164cb7c04d95.css +0 -1
  293. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
  294. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
@@ -0,0 +1,347 @@
1
+ import type { Options as ReactMarkdownOptions } from "react-markdown";
2
+ import rehypeKatex from "rehype-katex";
3
+ import rehypeRaw from "rehype-raw";
4
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
5
+ import remarkFrontmatter from "remark-frontmatter";
6
+ import remarkGfm from "remark-gfm";
7
+ import remarkMath from "remark-math";
8
+
9
+ const markdownSanitizeSchema = {
10
+ ...defaultSchema,
11
+ attributes: {
12
+ ...defaultSchema.attributes,
13
+ code: [["className", /^language-./, "math-inline", "math-display"]],
14
+ },
15
+ strip: [...(defaultSchema.strip || []), "iframe", "object", "style", "form"],
16
+ };
17
+
18
+ export function normalizeDisplayMath(markdown: string): string {
19
+ const lineBreak = markdown.includes("\r\n") ? "\r\n" : "\n";
20
+ const lines = markdown.split(/\r?\n/);
21
+ const normalized: string[] = [];
22
+ let fence: { marker: string; size: number } | null = null;
23
+ let inlineCodeMarkerSize = 0;
24
+ let rawCodeTag: string | null = null;
25
+ const unmatchedDisplayMathUntil = new Map<string, number>();
26
+
27
+ for (let index = 0; index < lines.length; index++) {
28
+ const line = lines[index];
29
+
30
+ if (rawCodeTag) {
31
+ normalized.push(line);
32
+ if (new RegExp(`</${rawCodeTag}\\s*>`, "i").test(line)) rawCodeTag = null;
33
+ continue;
34
+ }
35
+
36
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
37
+ if (fenceMatch) {
38
+ const marker = fenceMatch[1][0];
39
+ const size = fenceMatch[1].length;
40
+ if (!fence) fence = { marker, size };
41
+ else if (marker === fence.marker && size >= fence.size) fence = null;
42
+ inlineCodeMarkerSize = 0;
43
+ normalized.push(line);
44
+ continue;
45
+ }
46
+
47
+ if (fence) {
48
+ normalized.push(line);
49
+ continue;
50
+ }
51
+
52
+ const rawCodeOpen = line.match(/<(code|pre|script|style)\b/i);
53
+ if (rawCodeOpen) {
54
+ const tag = rawCodeOpen[1].toLowerCase();
55
+ const remainder = line.slice((rawCodeOpen.index ?? 0) + rawCodeOpen[0].length);
56
+ if (!new RegExp(`</${tag}\\s*>`, "i").test(remainder)) rawCodeTag = tag;
57
+ inlineCodeMarkerSize = 0;
58
+ normalized.push(line);
59
+ continue;
60
+ }
61
+
62
+ if (/^(?: {4}|\t)/.test(line) || line.trim() === "") {
63
+ inlineCodeMarkerSize = 0;
64
+ normalized.push(line);
65
+ continue;
66
+ }
67
+
68
+ if (inlineCodeMarkerSize || line.includes("`")) {
69
+ inlineCodeMarkerSize = updateInlineCodeMarker(line, inlineCodeMarkerSize);
70
+ normalized.push(line);
71
+ continue;
72
+ }
73
+
74
+ const bracketDisplayOneLine = line.match(/^([ ]{0,3})\\\[[ \t]*(.+?)[ \t]*\\\][ \t]*$/);
75
+ if (bracketDisplayOneLine) {
76
+ const math = bracketDisplayOneLine[2].trim();
77
+ if (math) {
78
+ // Keep the content line indented together with the `$$` fence. When the
79
+ // formula is nested inside a GFM list item (indented `$$`), a content line
80
+ // at column 0 becomes a "lazy continuation" line, which makes remark-math
81
+ // mis-parse the fence pair: the opening `$$` turns into an empty math node
82
+ // and the closing one swallows the rest of the document as math content.
83
+ normalized.push(
84
+ `${bracketDisplayOneLine[1]}$$`,
85
+ `${bracketDisplayOneLine[1]}${math}`,
86
+ `${bracketDisplayOneLine[1]}$$`,
87
+ );
88
+ continue;
89
+ }
90
+ }
91
+
92
+ const bracketDisplayStart = line.match(/^([ ]{0,3})\\\[[ \t]*$/);
93
+ if (bracketDisplayStart) {
94
+ const closingIndex = findBracketDisplayClose(lines, index + 1);
95
+ if (closingIndex !== -1) {
96
+ // Same lazy-continuation guard as above: indent content lines that sit at
97
+ // column 0 so the block stays parseable when nested inside a list item.
98
+ normalized.push(
99
+ `${bracketDisplayStart[1]}$$`,
100
+ ...lines.slice(index + 1, closingIndex).map((mathLine) =>
101
+ indentDisplayMathContent(mathLine, bracketDisplayStart[1]),
102
+ ),
103
+ `${bracketDisplayStart[1]}$$`,
104
+ );
105
+ index = closingIndex;
106
+ continue;
107
+ }
108
+ }
109
+
110
+ const displayMathMatch = line.match(/^([ \t]{0,3})\$\$(.+)\$\$[ \t]*$/);
111
+ if (displayMathMatch) {
112
+ const math = displayMathMatch[2].trim();
113
+ if (math) {
114
+ // See the comment on bracketDisplayOneLine: without matching indentation,
115
+ // a formula nested in a GFM list item is mis-parsed by remark-math and the
116
+ // text after the formula renders as a garbled KaTeX error block.
117
+ normalized.push(
118
+ `${displayMathMatch[1]}$$`,
119
+ `${displayMathMatch[1]}${math}`,
120
+ `${displayMathMatch[1]}$$`,
121
+ );
122
+ continue;
123
+ }
124
+ }
125
+
126
+ // remark-math requires both `$$` delimiters to sit on their own lines, but
127
+ // models also emit display math as a multi-line block where the opening `$$`
128
+ // is glued to the first formula line and/or the closing `$$` is glued to the
129
+ // end of the last one (`$$x = 1` + `y = 2$$`). Without normalization such a
130
+ // block swallows the following text as math content and renders as garbage.
131
+ const displayMathMultiLine = line.match(/^([ \t]{0,3})\$\$(.+)$/);
132
+ if (displayMathMultiLine) {
133
+ const indent = displayMathMultiLine[1];
134
+ const firstLine = displayMathMultiLine[2].trimEnd();
135
+ // Only treat this as a block opener if no other `$$` is embedded mid-line
136
+ // (e.g. `$$x$$ and text` stays untouched and is rendered as inline math).
137
+ if (firstLine && !firstLine.includes("$$")) {
138
+ const closing = findDisplayMathClose(
139
+ lines,
140
+ index + 1,
141
+ indent,
142
+ unmatchedDisplayMathUntil,
143
+ );
144
+ if (closing) {
145
+ normalized.push(`${indent}$$`, `${indent}${firstLine}`);
146
+ for (let j = index + 1; j < closing.index; j++) {
147
+ normalized.push(indentDisplayMathContent(lines[j], indent));
148
+ }
149
+ if (closing.content) normalized.push(`${indent}${closing.content}`);
150
+ normalized.push(`${indent}$$`);
151
+ index = closing.index;
152
+ continue;
153
+ }
154
+ }
155
+ }
156
+
157
+ // Bare `$$` opener (possibly indented inside a GFM list item). Two problems
158
+ // need fixing: (1) when the closing `$$` is glued to the last content line
159
+ // (e.g. `z = w$$`) remark-math never finds a valid closing fence and swallows
160
+ // the rest of the document; (2) inside a list item, content lines at column 0
161
+ // are lazy continuations that break the math flow. Both are fixed by moving
162
+ // the closing `$$` to its own line and re-indenting lazy content lines.
163
+ // A column-0 block with a properly detached closing `$$` is left untouched
164
+ // (remark-math already parses it correctly).
165
+ const displayMathBareOpen = line.match(/^([ \t]{0,3})\$\$\s*$/);
166
+ if (displayMathBareOpen) {
167
+ const indent = displayMathBareOpen[1];
168
+ const closing = findDisplayMathClose(
169
+ lines,
170
+ index + 1,
171
+ indent,
172
+ unmatchedDisplayMathUntil,
173
+ );
174
+ if (closing && (closing.glued || indent !== "")) {
175
+ normalized.push(`${indent}$$`);
176
+ for (let j = index + 1; j < closing.index; j++) {
177
+ normalized.push(indentDisplayMathContent(lines[j], indent));
178
+ }
179
+ if (closing.content) normalized.push(`${indent}${closing.content}`);
180
+ normalized.push(`${indent}$$`);
181
+ index = closing.index;
182
+ continue;
183
+ }
184
+ }
185
+
186
+ normalized.push(normalizeInlineLatexMath(line));
187
+ }
188
+
189
+ return normalized.join(lineBreak);
190
+ }
191
+
192
+ interface DisplayMathClose {
193
+ index: number;
194
+ content: string;
195
+ glued: boolean;
196
+ }
197
+
198
+ function findDisplayMathClose(
199
+ lines: string[],
200
+ startIndex: number,
201
+ indent: string,
202
+ unmatchedUntil: Map<string, number>,
203
+ ): DisplayMathClose | null {
204
+ const knownUnmatchedUntil = unmatchedUntil.get(indent);
205
+ if (knownUnmatchedUntil !== undefined && startIndex < knownUnmatchedUntil) return null;
206
+
207
+ for (let index = startIndex; index < lines.length; index++) {
208
+ const line = lines[index];
209
+ if (isDisplayMathFence(line, indent)) return { index, content: "", glued: false };
210
+
211
+ // A new Markdown block cannot belong to the preceding formula. In particular,
212
+ // do not let a later sibling list item provide a closing `$$` for this block.
213
+ if (isDisplayMathBlockBoundary(line) || isDisplayMathOpeningLine(line)) {
214
+ unmatchedUntil.set(indent, index);
215
+ return null;
216
+ }
217
+
218
+ const content = getDisplayMathGluedCloseContent(line, indent);
219
+ if (content !== null) return { index, content, glued: true };
220
+ }
221
+
222
+ // Multiple unmatched glued openers with the same indentation previously each
223
+ // scanned to EOF. Cache this range so the overall search remains linear.
224
+ unmatchedUntil.set(indent, lines.length);
225
+ return null;
226
+ }
227
+
228
+ function isDisplayMathFence(line: string, indent: string): boolean {
229
+ if (indent === "") return /^ {0,3}\$\$\s*$/.test(line);
230
+ return line.startsWith(indent) && /^\$\$\s*$/.test(line.slice(indent.length));
231
+ }
232
+
233
+ function getDisplayMathGluedCloseContent(line: string, indent: string): string | null {
234
+ if (!line.startsWith(indent)) return null;
235
+
236
+ const match = line.slice(indent.length).match(/^(.+?)\$\$\s*$/);
237
+ if (!match) return null;
238
+
239
+ const content = match[1].trimEnd();
240
+ return content && !content.includes("$$") ? content : null;
241
+ }
242
+
243
+ function isDisplayMathOpeningLine(line: string): boolean {
244
+ return /^ {0,3}\$\$(?:\S|[ \t]+\S)/.test(line);
245
+ }
246
+
247
+ function isDisplayMathBlockBoundary(line: string): boolean {
248
+ return (
249
+ /^ {0,3}(`{3,}|~{3,})/.test(line) ||
250
+ /^[ \t]*(?:[-+*]|\d{1,9}[.)])(?:[ \t]+|$)/.test(line) ||
251
+ /^ {0,3}#{1,6}(?:[ \t]+|$)/.test(line) ||
252
+ /^ {0,3}>/.test(line) ||
253
+ /<(code|pre|script|style)\b/i.test(line)
254
+ );
255
+ }
256
+
257
+ function indentDisplayMathContent(line: string, indent: string): string {
258
+ if (!indent || !line || line.startsWith("\t")) return line;
259
+
260
+ const leadingSpaces = line.match(/^ */)?.[0].length ?? 0;
261
+ if (leadingSpaces >= indent.length) return line;
262
+ return `${indent.slice(leadingSpaces)}${line}`;
263
+ }
264
+
265
+ function findBracketDisplayClose(lines: string[], startIndex: number): number {
266
+ for (let index = startIndex; index < lines.length; index++) {
267
+ const line = lines[index];
268
+ if (/^ {0,3}\\\][ \t]*$/.test(line)) return index;
269
+
270
+ // Do not pair delimiters across another Markdown block boundary.
271
+ if (
272
+ /^ {0,3}(`{3,}|~{3,})/.test(line) ||
273
+ /^ {0,3}\\\[[ \t]*$/.test(line) ||
274
+ /<(code|pre|script|style)\b/i.test(line)
275
+ ) {
276
+ return -1;
277
+ }
278
+ }
279
+
280
+ return -1;
281
+ }
282
+
283
+ function updateInlineCodeMarker(line: string, initialMarkerSize: number): number {
284
+ let markerSize = initialMarkerSize;
285
+ for (let cursor = 0; cursor < line.length;) {
286
+ if (line[cursor] !== "`") {
287
+ cursor++;
288
+ continue;
289
+ }
290
+
291
+ let end = cursor + 1;
292
+ while (line[end] === "`") end++;
293
+ const runSize = end - cursor;
294
+ if (markerSize === 0) markerSize = runSize;
295
+ else if (runSize === markerSize) markerSize = 0;
296
+ cursor = end;
297
+ }
298
+ return markerSize;
299
+ }
300
+
301
+ function normalizeInlineLatexMath(line: string): string {
302
+ if (
303
+ /^\s{0,3}\[[^\]]+\]:/.test(line) ||
304
+ /]\s*\(/.test(line) ||
305
+ /<(?:!--|\/?[A-Za-z][^>]*>)/.test(line) ||
306
+ /\b(?:https?|file|mailto):/i.test(line) ||
307
+ /\b[A-Za-z]:\\/.test(line)
308
+ ) {
309
+ return line;
310
+ }
311
+
312
+ return line.replace(
313
+ /(?<!\\)\\\(([^`\r\n$]+?)(?<!\\)\\\)/g,
314
+ (match, math: string) => (math.trim() ? `$${math}$` : match),
315
+ );
316
+ }
317
+
318
+ // Parse YAML frontmatter into a `yaml` node before the math/GFM plugins run, so
319
+ // the raw metadata never leaks into the rendered output (without it, the opening
320
+ // `---` becomes an <hr> and the closing `---` turns the YAML into a setext heading).
321
+ // singleTilde:false requires ~~double~~ tildes for strikethrough. A single `~`
322
+ // is the standard CJK numeric-range separator (e.g. "5~7U", "100~200倍"), and
323
+ // GFM's default single-tilde strikethrough silently mangled such ranges (#385).
324
+ const remarkGfmOptions = { singleTilde: false } as const;
325
+
326
+ export const markdownRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [
327
+ [remarkFrontmatter, ["yaml"]],
328
+ [remarkGfm, remarkGfmOptions],
329
+ remarkMath,
330
+ ];
331
+ export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [
332
+ [remarkFrontmatter, ["yaml"]],
333
+ [remarkGfm, remarkGfmOptions],
334
+ remarkMath,
335
+ ];
336
+
337
+ export const markdownRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [
338
+ rehypeRaw,
339
+ [rehypeSanitize, markdownSanitizeSchema],
340
+ [rehypeKatex, { throwOnError: false, strict: false }],
341
+ ];
342
+
343
+ export const markdownPreviewRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [
344
+ rehypeRaw,
345
+ [rehypeSanitize, markdownSanitizeSchema],
346
+ [rehypeKatex, { throwOnError: false, strict: false }],
347
+ ];
@@ -0,0 +1,130 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ async function loadSubject() {
5
+ return import("./message-display.ts");
6
+ }
7
+
8
+ function assistant(content) {
9
+ return {
10
+ role: "assistant",
11
+ provider: "test",
12
+ model: "test-model",
13
+ content,
14
+ };
15
+ }
16
+
17
+ test("splits trailing final answer blocks from process blocks", async () => {
18
+ const { splitFinalAssistantBlocks } = await loadSubject();
19
+ const message = assistant([
20
+ { type: "thinking", thinking: "work through it" },
21
+ { type: "toolCall", toolCallId: "call-1", toolName: "bash", input: {} },
22
+ { type: "text", text: "Final answer" },
23
+ { type: "image", source: { type: "url", url: "https://example.com/final.png" } },
24
+ ]);
25
+
26
+ const result = splitFinalAssistantBlocks(message, { isStreaming: false });
27
+
28
+ assert.deepEqual(result.answerBlocks.map((block) => block.type), ["text", "image"]);
29
+ assert.deepEqual(result.processBlocks.map((block) => block.type), ["thinking", "toolCall"]);
30
+ });
31
+
32
+ test("keeps pre-tool text in process blocks", async () => {
33
+ const { splitFinalAssistantBlocks } = await loadSubject();
34
+ const message = assistant([
35
+ { type: "text", text: "I will inspect the repo first." },
36
+ { type: "toolCall", toolCallId: "call-1", toolName: "bash", input: {} },
37
+ { type: "text", text: "Final answer" },
38
+ ]);
39
+
40
+ const result = splitFinalAssistantBlocks(message, { isStreaming: false });
41
+
42
+ assert.deepEqual(result.answerBlocks.map((block) => block.type), ["text"]);
43
+ assert.equal(result.answerBlocks[0].text, "Final answer");
44
+ assert.deepEqual(result.processBlocks.map((block) => block.type), ["text", "toolCall"]);
45
+ });
46
+
47
+ test("does not expose text before a trailing tool call as final answer", async () => {
48
+ const { splitFinalAssistantBlocks } = await loadSubject();
49
+ const message = assistant([
50
+ { type: "thinking", thinking: "work through it" },
51
+ { type: "text", text: "I need to call a tool." },
52
+ { type: "toolCall", toolCallId: "call-1", toolName: "bash", input: {} },
53
+ ]);
54
+
55
+ const result = splitFinalAssistantBlocks(message, { isStreaming: false });
56
+
57
+ assert.deepEqual(result.answerBlocks, []);
58
+ assert.deepEqual(result.processBlocks.map((block) => block.type), ["thinking", "text", "toolCall"]);
59
+ });
60
+
61
+ test("drops empty thinking blocks after completion", async () => {
62
+ const { getDisplayableAssistantBlocks, splitFinalAssistantBlocks } = await loadSubject();
63
+ const message = assistant([
64
+ { type: "thinking", thinking: "" },
65
+ { type: "text", text: "Final answer" },
66
+ ]);
67
+
68
+ assert.deepEqual(
69
+ getDisplayableAssistantBlocks(message, { isStreaming: false }).map((block) => block.type),
70
+ ["text"],
71
+ );
72
+
73
+ const result = splitFinalAssistantBlocks(message, { isStreaming: false });
74
+ assert.deepEqual(result.answerBlocks.map((block) => block.type), ["text"]);
75
+ assert.deepEqual(result.processBlocks, []);
76
+ });
77
+
78
+ test("keeps empty thinking while streaming", async () => {
79
+ const { splitFinalAssistantBlocks } = await loadSubject();
80
+ const message = assistant([
81
+ { type: "thinking", thinking: "" },
82
+ { type: "text", text: "Partial answer" },
83
+ ]);
84
+
85
+ const result = splitFinalAssistantBlocks(message, { isStreaming: true });
86
+
87
+ assert.deepEqual(result.answerBlocks.map((block) => block.type), ["text"]);
88
+ assert.deepEqual(result.processBlocks.map((block) => block.type), ["thinking"]);
89
+ });
90
+
91
+ test("keeps deferred historical thinking placeholders", async () => {
92
+ const { getDisplayableAssistantBlocks } = await loadSubject();
93
+ const message = assistant([
94
+ { type: "thinking", thinking: "", deferred: true },
95
+ { type: "text", text: "Final answer" },
96
+ ]);
97
+
98
+ assert.deepEqual(
99
+ getDisplayableAssistantBlocks(message, { isStreaming: false }).map((block) => block.type),
100
+ ["thinking", "text"],
101
+ );
102
+ });
103
+
104
+ test("returns completed provider errors even when the message has no content", async () => {
105
+ const { getAssistantErrorMessage } = await loadSubject();
106
+ const message = {
107
+ ...assistant([]),
108
+ stopReason: "error",
109
+ errorMessage: "OpenAI API error (403): request forbidden",
110
+ };
111
+
112
+ assert.equal(
113
+ getAssistantErrorMessage(message),
114
+ "OpenAI API error (403): request forbidden",
115
+ );
116
+ assert.equal(getAssistantErrorMessage(message, { isStreaming: true }), null);
117
+ });
118
+
119
+ test("falls back when a provider error has no message", async () => {
120
+ const { getAssistantErrorMessage } = await loadSubject();
121
+
122
+ assert.equal(
123
+ getAssistantErrorMessage({ ...assistant([]), stopReason: "error" }),
124
+ "Unknown provider error",
125
+ );
126
+ assert.equal(
127
+ getAssistantErrorMessage({ ...assistant([]), stopReason: "stop" }),
128
+ null,
129
+ );
130
+ });
@@ -0,0 +1,47 @@
1
+ import type { AssistantContentBlock, AssistantMessage, ThinkingContent, ToolCallContent } from "./types";
2
+
3
+ interface DisplayOptions {
4
+ isStreaming?: boolean;
5
+ }
6
+
7
+ export function isEmptyThinkingBlock(block: AssistantContentBlock, options: DisplayOptions = {}): block is ThinkingContent {
8
+ return block.type === "thinking" && !block.deferred && !options.isStreaming && block.thinking.trim() === "";
9
+ }
10
+
11
+ export function getDisplayableAssistantBlocks(
12
+ message: AssistantMessage,
13
+ options: DisplayOptions = {},
14
+ ): AssistantContentBlock[] {
15
+ return (message.content ?? []).filter((block) => !isEmptyThinkingBlock(block, options));
16
+ }
17
+
18
+ export function getAssistantErrorMessage(
19
+ message: AssistantMessage,
20
+ options: DisplayOptions = {},
21
+ ): string | null {
22
+ if (options.isStreaming || message.stopReason !== "error") return null;
23
+ return message.errorMessage?.trim() || "Unknown provider error";
24
+ }
25
+
26
+ function isFinalAnswerBlock(block: AssistantContentBlock): boolean {
27
+ return block.type === "text" || block.type === "image";
28
+ }
29
+
30
+ export function splitFinalAssistantBlocks(
31
+ message: AssistantMessage,
32
+ options: DisplayOptions = {},
33
+ ): { answerBlocks: AssistantContentBlock[]; processBlocks: AssistantContentBlock[] } {
34
+ const blocks = getDisplayableAssistantBlocks(message, options);
35
+ const lastProcessIndex = blocks.findLastIndex((block) => !isFinalAnswerBlock(block));
36
+ if (lastProcessIndex === -1) {
37
+ return { answerBlocks: blocks, processBlocks: [] };
38
+ }
39
+ return {
40
+ answerBlocks: blocks.slice(lastProcessIndex + 1),
41
+ processBlocks: blocks.slice(0, lastProcessIndex + 1),
42
+ };
43
+ }
44
+
45
+ export function countToolCallBlocks(blocks: AssistantContentBlock[]): number {
46
+ return blocks.filter((block): block is ToolCallContent => block.type === "toolCall").length;
47
+ }