@agent-native/core 0.101.10 → 0.101.12

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 (91) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +23 -1
  5. package/corpus/core/src/a2a/handlers.ts +65 -1
  6. package/corpus/core/src/a2a/server.ts +124 -0
  7. package/corpus/core/src/a2a/task-store.ts +253 -0
  8. package/corpus/core/src/a2a/types.ts +26 -0
  9. package/corpus/core/src/agent/production-agent.ts +5 -2
  10. package/corpus/core/src/coding-tools/run-code.ts +65 -12
  11. package/corpus/core/src/extensions/actions.ts +141 -7
  12. package/corpus/core/src/mcp/builtin-tools.ts +25 -1
  13. package/corpus/core/src/scripts/call-agent.ts +22 -3
  14. package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
  15. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  16. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
  17. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
  18. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
  19. package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
  20. package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
  21. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
  22. package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
  23. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
  24. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
  25. package/corpus/templates/content/app/global.css +53 -0
  26. package/corpus/templates/content/app/i18n/zh-TW.ts +13 -0
  27. package/corpus/templates/content/app/i18n-data.ts +163 -0
  28. package/corpus/templates/content/app/root.tsx +2 -0
  29. package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
  30. package/corpus/templates/content/package.json +1 -0
  31. package/corpus/templates/content/shared/document-export.ts +296 -25
  32. package/corpus/templates/content/shared/inline-math.ts +128 -0
  33. package/corpus/templates/content/shared/math-rendering.ts +50 -0
  34. package/corpus/templates/content/shared/nfm.ts +17 -14
  35. package/corpus/templates/content/vite.config.ts +0 -1
  36. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
  37. package/corpus/templates/mail/AGENTS.md +21 -19
  38. package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
  39. package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
  40. package/corpus/templates/mail/server/handlers/media.ts +84 -61
  41. package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
  42. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
  43. package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
  44. package/corpus/templates/mail/server/plugins/auth.ts +9 -1
  45. package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
  46. package/dist/a2a/client.d.ts +5 -1
  47. package/dist/a2a/client.d.ts.map +1 -1
  48. package/dist/a2a/client.js +18 -1
  49. package/dist/a2a/client.js.map +1 -1
  50. package/dist/a2a/handlers.d.ts.map +1 -1
  51. package/dist/a2a/handlers.js +57 -8
  52. package/dist/a2a/handlers.js.map +1 -1
  53. package/dist/a2a/server.d.ts.map +1 -1
  54. package/dist/a2a/server.js +87 -1
  55. package/dist/a2a/server.js.map +1 -1
  56. package/dist/a2a/task-store.d.ts +27 -0
  57. package/dist/a2a/task-store.d.ts.map +1 -1
  58. package/dist/a2a/task-store.js +216 -0
  59. package/dist/a2a/task-store.js.map +1 -1
  60. package/dist/a2a/types.d.ts +24 -0
  61. package/dist/a2a/types.d.ts.map +1 -1
  62. package/dist/a2a/types.js.map +1 -1
  63. package/dist/agent/production-agent.d.ts +1 -0
  64. package/dist/agent/production-agent.d.ts.map +1 -1
  65. package/dist/agent/production-agent.js +5 -2
  66. package/dist/agent/production-agent.js.map +1 -1
  67. package/dist/coding-tools/run-code.js +64 -11
  68. package/dist/coding-tools/run-code.js.map +1 -1
  69. package/dist/collab/struct-routes.d.ts +1 -1
  70. package/dist/extensions/actions.d.ts.map +1 -1
  71. package/dist/extensions/actions.js +129 -7
  72. package/dist/extensions/actions.js.map +1 -1
  73. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  74. package/dist/mcp/builtin-tools.js +22 -3
  75. package/dist/mcp/builtin-tools.js.map +1 -1
  76. package/dist/notifications/routes.d.ts +2 -2
  77. package/dist/observability/routes.d.ts +5 -5
  78. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  79. package/dist/resources/handlers.d.ts +3 -3
  80. package/dist/scripts/call-agent.d.ts +1 -1
  81. package/dist/scripts/call-agent.d.ts.map +1 -1
  82. package/dist/scripts/call-agent.js +19 -1
  83. package/dist/scripts/call-agent.js.map +1 -1
  84. package/dist/secrets/routes.d.ts +9 -9
  85. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  86. package/dist/server/agent-chat-plugin.js +71 -2
  87. package/dist/server/agent-chat-plugin.js.map +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  90. package/package.json +1 -1
  91. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
@@ -1,3 +1,6 @@
1
+ import { matchInlineMathAt } from "./inline-math.js";
2
+ import { KATEX_STYLESHEET_URL, renderMathToHtml } from "./math-rendering.js";
3
+
1
4
  export type DocumentExportFormat = "pdf" | "markdown" | "html";
2
5
 
3
6
  export interface DocumentExportInput {
@@ -100,18 +103,37 @@ export function markdownWithTitle(
100
103
  return `${`# ${safeTitle}`}${body ? `\n\n${body}` : ""}\n`;
101
104
  }
102
105
 
103
- function inlineMarkdownToHtml(text: string): string {
106
+ interface InlineExportToken {
107
+ marker: string;
108
+ html: string;
109
+ source: string;
110
+ }
111
+
112
+ function restoreTokenSources(
113
+ text: string,
114
+ tokens: InlineExportToken[],
115
+ ): string {
116
+ return tokens.reduce(
117
+ (restored, token) =>
118
+ restored.split(token.marker).join(escapeHtml(token.source)),
119
+ text,
120
+ );
121
+ }
122
+
123
+ function inlineMarkdownSegmentToHtml(
124
+ text: string,
125
+ tokens: InlineExportToken[],
126
+ ): string {
104
127
  return escapeHtml(text)
105
128
  .replace(
106
129
  /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^&]*")?\)/g,
107
130
  (_m, alt, src) => {
108
- return `<img src="${safeExportUrl(src, "image")}" alt="${alt}" />`;
131
+ return `<img src="${safeExportUrl(src, "image")}" alt="${restoreTokenSources(alt, tokens)}" />`;
109
132
  },
110
133
  )
111
134
  .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, href) => {
112
135
  return `<a href="${safeExportUrl(href, "link")}">${label}</a>`;
113
136
  })
114
- .replace(/`([^`]+)`/g, "<code>$1</code>")
115
137
  .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
116
138
  .replace(/__([^_]+)__/g, "<strong>$1</strong>")
117
139
  .replace(/~~([^~]+)~~/g, "<s>$1</s>")
@@ -119,24 +141,229 @@ function inlineMarkdownToHtml(text: string): string {
119
141
  .replace(/(^|[^_])_([^_]+)_/g, "$1<em>$2</em>");
120
142
  }
121
143
 
122
- function listItemsToHtml(lines: string[], ordered: boolean): string {
123
- const items = lines
124
- .map((line) => {
125
- const text = ordered
126
- ? line.replace(/^\s*\d+[.)]\s+/, "")
127
- : line.replace(/^\s*[-*+]\s+/, "");
128
- const task = text.match(/^\[( |x|X)\]\s+(.*)$/);
144
+ function isEscapedDelimiter(text: string, index: number): boolean {
145
+ let backslashes = 0;
146
+ for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor--) {
147
+ backslashes++;
148
+ }
149
+ return backslashes % 2 === 1;
150
+ }
151
+
152
+ function mathErrorHtml(
153
+ source: string,
154
+ error: string,
155
+ displayMode: boolean,
156
+ ): string {
157
+ const className = displayMode
158
+ ? "math-error math-error-block"
159
+ : "math-error math-error-inline";
160
+ const tagName = displayMode ? "pre" : "code";
161
+ return `<${tagName} class="${className}" title="${escapeHtml(error)}">${escapeHtml(source)}</${tagName}>`;
162
+ }
163
+
164
+ function inlineMarkdownToHtml(text: string): string {
165
+ const tokens: InlineExportToken[] = [];
166
+ let markerStart = "\uE000agent-native-inline-";
167
+ while (text.includes(markerStart)) markerStart += "-";
168
+
169
+ const protectedText: string[] = [];
170
+ let cursor = 0;
171
+
172
+ while (cursor < text.length) {
173
+ if (text.startsWith("$`", cursor) && isEscapedDelimiter(text, cursor)) {
174
+ const close = text.indexOf("`$", cursor + 2);
175
+ if (close !== -1) {
176
+ protectedText.push(text.slice(cursor, close + 2));
177
+ cursor = close + 2;
178
+ continue;
179
+ }
180
+ }
181
+
182
+ if (text[cursor] === "$") {
183
+ const inlineMath = matchInlineMathAt(text, cursor);
184
+ if (inlineMath) {
185
+ const { latex } = inlineMath;
186
+ const source = text.slice(cursor, inlineMath.to);
187
+ const math = renderMathToHtml(latex, false);
188
+ const marker = `${markerStart}${tokens.length}\uE001`;
189
+ tokens.push({
190
+ marker,
191
+ source,
192
+ html: math.ok
193
+ ? `<span class="math-inline">${math.html}</span>`
194
+ : mathErrorHtml(source, math.error, false),
195
+ });
196
+ protectedText.push(marker);
197
+ cursor = inlineMath.to;
198
+ continue;
199
+ }
200
+ }
201
+
202
+ if (text[cursor] === "`" && !isEscapedDelimiter(text, cursor)) {
203
+ let delimiterLength = 1;
204
+ while (text[cursor + delimiterLength] === "`") delimiterLength++;
205
+ const delimiter = "`".repeat(delimiterLength);
206
+ let close = text.indexOf(delimiter, cursor + delimiterLength);
207
+ while (
208
+ close !== -1 &&
209
+ (text[close - 1] === "`" || text[close + delimiterLength] === "`")
210
+ ) {
211
+ close = text.indexOf(delimiter, close + delimiterLength);
212
+ }
213
+
214
+ if (close !== -1) {
215
+ const source = text.slice(cursor, close + delimiterLength);
216
+ let code = text
217
+ .slice(cursor + delimiterLength, close)
218
+ .replace(/\r?\n/g, " ");
219
+ if (
220
+ code.startsWith(" ") &&
221
+ code.endsWith(" ") &&
222
+ code.trim().length > 0
223
+ ) {
224
+ code = code.slice(1, -1);
225
+ }
226
+ const marker = `${markerStart}${tokens.length}\uE001`;
227
+ tokens.push({
228
+ marker,
229
+ source,
230
+ html: `<code>${escapeHtml(code)}</code>`,
231
+ });
232
+ protectedText.push(marker);
233
+ cursor = close + delimiterLength;
234
+ continue;
235
+ }
236
+ }
237
+
238
+ protectedText.push(text[cursor]);
239
+ cursor++;
240
+ }
241
+
242
+ return tokens.reduce(
243
+ (html, token) => html.split(token.marker).join(token.html),
244
+ inlineMarkdownSegmentToHtml(protectedText.join(""), tokens),
245
+ );
246
+ }
247
+
248
+ function listItemsToHtml(items: string[][], ordered: boolean): string {
249
+ const renderedItems = items
250
+ .map((lines) => {
251
+ const task = lines[0].match(/^\[( |x|X)\]\s+(.*)$/);
129
252
  if (task) {
130
253
  const checked = task[1].toLowerCase() === "x";
131
254
  return `<li class="task"><input type="checkbox" disabled${
132
255
  checked ? " checked" : ""
133
- } /> <span>${inlineMarkdownToHtml(task[2])}</span></li>`;
256
+ } /> <span>${inlineMarkdownToHtml(task[2])}</span>${
257
+ lines.length > 1 ? markdownToHtml(lines.slice(1).join("\n")) : ""
258
+ }</li>`;
134
259
  }
135
- return `<li>${inlineMarkdownToHtml(text)}</li>`;
260
+ return lines.length === 1
261
+ ? `<li>${inlineMarkdownToHtml(lines[0])}</li>`
262
+ : `<li>${markdownToHtml(lines.join("\n"))}</li>`;
136
263
  })
137
264
  .join("\n");
138
265
 
139
- return ordered ? `<ol>\n${items}\n</ol>` : `<ul>\n${items}\n</ul>`;
266
+ return ordered
267
+ ? `<ol>\n${renderedItems}\n</ol>`
268
+ : `<ul>\n${renderedItems}\n</ul>`;
269
+ }
270
+
271
+ interface ListMarkerMatch {
272
+ baseIndent: number;
273
+ contentIndent: number;
274
+ content: string;
275
+ }
276
+
277
+ function indentationWidth(value: string): number {
278
+ let width = 0;
279
+ for (const character of value) {
280
+ width += character === "\t" ? 4 - (width % 4) : 1;
281
+ }
282
+ return width;
283
+ }
284
+
285
+ function matchListMarker(
286
+ line: string,
287
+ ordered: boolean,
288
+ ): ListMarkerMatch | null {
289
+ const match = line.match(
290
+ ordered
291
+ ? /^([ \t]*)(?:\d+[.)])([ \t]+)(.*)$/
292
+ : /^([ \t]*)(?:[-*+])([ \t]+)(.*)$/,
293
+ );
294
+ if (!match) return null;
295
+ const contentStart = match[0].length - match[3].length;
296
+ return {
297
+ baseIndent: indentationWidth(match[1]),
298
+ contentIndent: indentationWidth(line.slice(0, contentStart)),
299
+ content: match[3],
300
+ };
301
+ }
302
+
303
+ function leadingIndentWidth(line: string): number {
304
+ return indentationWidth(line.match(/^[ \t]*/)?.[0] ?? "");
305
+ }
306
+
307
+ function stripLeadingIndent(line: string, targetWidth: number): string {
308
+ let width = 0;
309
+ let index = 0;
310
+ while (index < line.length && width < targetWidth) {
311
+ const character = line[index];
312
+ if (character !== " " && character !== "\t") break;
313
+ width += character === "\t" ? 4 - (width % 4) : 1;
314
+ index++;
315
+ }
316
+ return line.slice(index);
317
+ }
318
+
319
+ function collectListItems(
320
+ lines: string[],
321
+ start: number,
322
+ ordered: boolean,
323
+ ): { items: string[][]; nextIndex: number } {
324
+ const firstMarker = matchListMarker(lines[start], ordered);
325
+ if (!firstMarker) return { items: [], nextIndex: start };
326
+
327
+ const baseIndent = firstMarker.baseIndent;
328
+ const items: string[][] = [];
329
+ let index = start;
330
+
331
+ while (index < lines.length) {
332
+ const itemMarker = matchListMarker(lines[index], ordered);
333
+ if (!itemMarker || itemMarker.baseIndent !== baseIndent) break;
334
+
335
+ const item = [itemMarker.content];
336
+ index++;
337
+
338
+ while (index < lines.length) {
339
+ const nextMarker = matchListMarker(lines[index], ordered);
340
+ if (nextMarker?.baseIndent === baseIndent) break;
341
+ if (leadingIndentWidth(lines[index]) >= itemMarker.contentIndent) {
342
+ item.push(stripLeadingIndent(lines[index], itemMarker.contentIndent));
343
+ index++;
344
+ continue;
345
+ }
346
+ if (!lines[index].trim()) {
347
+ let nextContent = index + 1;
348
+ while (nextContent < lines.length && !lines[nextContent].trim()) {
349
+ nextContent++;
350
+ }
351
+ if (
352
+ nextContent < lines.length &&
353
+ leadingIndentWidth(lines[nextContent]) >= itemMarker.contentIndent
354
+ ) {
355
+ item.push("");
356
+ index++;
357
+ continue;
358
+ }
359
+ }
360
+ break;
361
+ }
362
+
363
+ items.push(item);
364
+ }
365
+
366
+ return { items, nextIndex: index };
140
367
  }
141
368
 
142
369
  function isEmptyBlockLine(trimmed: string): boolean {
@@ -163,6 +390,42 @@ function markdownToHtml(markdown: string): string {
163
390
  continue;
164
391
  }
165
392
 
393
+ if (trimmed === "$$") {
394
+ const source = [line];
395
+ const latex: string[] = [];
396
+ index++;
397
+ while (index < lines.length && lines[index].trim() !== "$$") {
398
+ source.push(lines[index]);
399
+ latex.push(lines[index]);
400
+ index++;
401
+ }
402
+
403
+ const closed = index < lines.length;
404
+ if (closed) {
405
+ source.push(lines[index]);
406
+ index++;
407
+ }
408
+
409
+ if (!closed) {
410
+ blocks.push(
411
+ mathErrorHtml(
412
+ source.join("\n"),
413
+ "This block equation is missing its closing delimiter.",
414
+ true,
415
+ ),
416
+ );
417
+ continue;
418
+ }
419
+
420
+ const math = renderMathToHtml(latex.join("\n"), true);
421
+ blocks.push(
422
+ math.ok
423
+ ? `<div class="math-block">${math.html}</div>`
424
+ : mathErrorHtml(source.join("\n"), math.error, true),
425
+ );
426
+ continue;
427
+ }
428
+
166
429
  const codeFence = trimmed.match(/^```(\w+)?/);
167
430
  if (codeFence) {
168
431
  const code: string[] = [];
@@ -208,22 +471,16 @@ function markdownToHtml(markdown: string): string {
208
471
  }
209
472
 
210
473
  if (/^\s*[-*+]\s+/.test(line)) {
211
- const items: string[] = [];
212
- while (index < lines.length && /^\s*[-*+]\s+/.test(lines[index])) {
213
- items.push(lines[index]);
214
- index++;
215
- }
216
- blocks.push(listItemsToHtml(items, false));
474
+ const list = collectListItems(lines, index, false);
475
+ index = list.nextIndex;
476
+ blocks.push(listItemsToHtml(list.items, false));
217
477
  continue;
218
478
  }
219
479
 
220
480
  if (/^\s*\d+[.)]\s+/.test(line)) {
221
- const items: string[] = [];
222
- while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) {
223
- items.push(lines[index]);
224
- index++;
225
- }
226
- blocks.push(listItemsToHtml(items, true));
481
+ const list = collectListItems(lines, index, true);
482
+ index = list.nextIndex;
483
+ blocks.push(listItemsToHtml(list.items, true));
227
484
  continue;
228
485
  }
229
486
 
@@ -233,6 +490,7 @@ function markdownToHtml(markdown: string): string {
233
490
  index < lines.length &&
234
491
  lines[index].trim() &&
235
492
  !isEmptyBlockLine(lines[index].trim()) &&
493
+ lines[index].trim() !== "$$" &&
236
494
  !/^(#{1,6})\s+/.test(lines[index].trim()) &&
237
495
  !/^```/.test(lines[index].trim()) &&
238
496
  !/^>\s?/.test(lines[index].trim()) &&
@@ -273,6 +531,7 @@ function buildHtmlDocument(input: {
273
531
  <meta charset="utf-8" />
274
532
  <meta name="viewport" content="width=device-width, initial-scale=1" />
275
533
  <title>${escapeHtml(input.title)}</title>
534
+ <link rel="stylesheet" href="${KATEX_STYLESHEET_URL}" crossorigin="anonymous" />
276
535
  <style>
277
536
  :root { color-scheme: light; }
278
537
  body {
@@ -329,6 +588,18 @@ function buildHtmlDocument(input: {
329
588
  border-radius: 4px;
330
589
  padding: 0.12rem 0.3rem;
331
590
  }
591
+ .math-inline { display: inline-block; max-width: 100%; vertical-align: -0.08em; }
592
+ .math-block { max-width: 100%; margin: 18px 0; overflow-x: auto; padding: 4px 0; }
593
+ .math-error {
594
+ border: 1px solid #fecaca;
595
+ border-radius: 6px;
596
+ background: #fef2f2;
597
+ color: #262626;
598
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
599
+ white-space: pre-wrap;
600
+ }
601
+ .math-error-inline { padding: 0.05rem 0.25rem; font-size: 0.9em; }
602
+ .math-error-block { overflow-wrap: anywhere; padding: 12px 14px; }
332
603
  a { color: #2563eb; }
333
604
  img {
334
605
  display: block;
@@ -0,0 +1,128 @@
1
+ export const MAX_INLINE_MATH_LENGTH = 1_000;
2
+
3
+ export type InlineMathSyntax = "plain" | "github";
4
+
5
+ export interface InlineMathMatch {
6
+ from: number;
7
+ to: number;
8
+ latex: string;
9
+ syntax: InlineMathSyntax;
10
+ }
11
+
12
+ export function isEscapedInlineDelimiter(text: string, index: number): boolean {
13
+ let backslashes = 0;
14
+ for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor--) {
15
+ backslashes += 1;
16
+ }
17
+ return backslashes % 2 === 1;
18
+ }
19
+
20
+ function matchGithubInlineMath(
21
+ text: string,
22
+ start: number,
23
+ ): InlineMathMatch | null {
24
+ if (!text.startsWith("$`", start)) return null;
25
+
26
+ const close = text.indexOf("`$", start + 2);
27
+ if (close === -1 || close - (start + 2) > MAX_INLINE_MATH_LENGTH) {
28
+ return null;
29
+ }
30
+
31
+ return {
32
+ from: start,
33
+ to: close + 2,
34
+ latex: text.slice(start + 2, close),
35
+ syntax: "github",
36
+ };
37
+ }
38
+
39
+ function matchPlainInlineMath(
40
+ text: string,
41
+ start: number,
42
+ ): InlineMathMatch | null {
43
+ if (
44
+ text[start] !== "$" ||
45
+ text[start + 1] === "$" ||
46
+ text[start + 1] === "`" ||
47
+ !text[start + 1] ||
48
+ /\s/.test(text[start + 1])
49
+ ) {
50
+ return null;
51
+ }
52
+
53
+ const searchLimit = Math.min(text.length, start + MAX_INLINE_MATH_LENGTH + 2);
54
+ for (let close = start + 1; close < searchLimit; close++) {
55
+ const char = text[close];
56
+ if (char === "\n" || char === "\r") return null;
57
+ if (
58
+ char !== "$" ||
59
+ isEscapedInlineDelimiter(text, close) ||
60
+ text[close - 1] === "$" ||
61
+ text[close + 1] === "$" ||
62
+ /\s/.test(text[close - 1]) ||
63
+ /\d/.test(text[close + 1] || "")
64
+ ) {
65
+ continue;
66
+ }
67
+
68
+ return {
69
+ from: start,
70
+ to: close + 1,
71
+ latex: text.slice(start + 1, close),
72
+ syntax: "plain",
73
+ };
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ export function matchInlineMathAt(
80
+ text: string,
81
+ start: number,
82
+ ): InlineMathMatch | null {
83
+ if (text[start] !== "$" || isEscapedInlineDelimiter(text, start)) {
84
+ return null;
85
+ }
86
+
87
+ return (
88
+ matchGithubInlineMath(text, start) ?? matchPlainInlineMath(text, start)
89
+ );
90
+ }
91
+
92
+ export function findTrailingPlainInlineMath(
93
+ text: string,
94
+ ): InlineMathMatch | null {
95
+ if (!text.endsWith("$") || text.endsWith("$$")) return null;
96
+
97
+ for (let start = text.length - 2; start >= 0; start--) {
98
+ if (text[start] !== "$") continue;
99
+ const match = matchInlineMathAt(text, start);
100
+ if (
101
+ match?.syntax === "plain" &&
102
+ match.to === text.length &&
103
+ !hasUnclosedCodeSpan(text.slice(0, start))
104
+ ) {
105
+ return match;
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ function hasUnclosedCodeSpan(text: string): boolean {
113
+ let openRun = 0;
114
+
115
+ for (let cursor = 0; cursor < text.length; cursor++) {
116
+ if (text[cursor] !== "`" || isEscapedInlineDelimiter(text, cursor)) {
117
+ continue;
118
+ }
119
+
120
+ let run = 1;
121
+ while (text[cursor + run] === "`") run += 1;
122
+ if (openRun === 0) openRun = run;
123
+ else if (openRun === run) openRun = 0;
124
+ cursor += run - 1;
125
+ }
126
+
127
+ return openRun !== 0;
128
+ }
@@ -0,0 +1,50 @@
1
+ import katex, { type KatexOptions } from "katex";
2
+
3
+ export const KATEX_VERSION = "0.17.0";
4
+ export const KATEX_STYLESHEET_URL = `https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist/katex.min.css`;
5
+
6
+ export interface MathRenderSuccess {
7
+ ok: true;
8
+ html: string;
9
+ }
10
+
11
+ export interface MathRenderFailure {
12
+ ok: false;
13
+ error: string;
14
+ }
15
+
16
+ export type MathRenderResult = MathRenderSuccess | MathRenderFailure;
17
+
18
+ export function mathRenderOptions(displayMode: boolean): KatexOptions {
19
+ return {
20
+ displayMode,
21
+ maxExpand: 1_000,
22
+ output: "htmlAndMathml",
23
+ strict: "warn",
24
+ throwOnError: true,
25
+ trust: false,
26
+ };
27
+ }
28
+
29
+ export function mathRenderErrorMessage(error: unknown): string {
30
+ if (error instanceof Error && error.message.trim()) return error.message;
31
+ return "This equation could not be rendered.";
32
+ }
33
+
34
+ export function renderMathToHtml(
35
+ latex: string,
36
+ displayMode: boolean,
37
+ ): MathRenderResult {
38
+ if (!latex.trim()) {
39
+ return { ok: false, error: "This equation is empty." };
40
+ }
41
+
42
+ try {
43
+ return {
44
+ ok: true,
45
+ html: katex.renderToString(latex, mathRenderOptions(displayMode)),
46
+ };
47
+ } catch (error) {
48
+ return { ok: false, error: mathRenderErrorMessage(error) };
49
+ }
50
+ }
@@ -41,6 +41,7 @@
41
41
  * exposing simple string props for the editor preview layer.
42
42
  */
43
43
 
44
+ import { matchInlineMathAt } from "./inline-math.js";
44
45
  import { isRegistryBlockTag, registryBlockSpecByTag } from "./nfm-registry.js";
45
46
 
46
47
  // ── Shared PM JSON types ────────────────────────────────────────────
@@ -279,7 +280,7 @@ function serializeInlineAtom(node: PMNode): string {
279
280
  attrs = {};
280
281
  }
281
282
  if (tagName === "math") {
282
- return "$`" + (label || attrs.latex || "") + "`$";
283
+ return "$" + (label || attrs.latex || "") + "$";
283
284
  }
284
285
  const attrEntries = Object.entries(attrs).filter(([k]) => k !== "latex");
285
286
  const attrStr = serializeAttrs(attrEntries);
@@ -446,19 +447,21 @@ function parseInline(input: string): PMNode[] {
446
447
  continue;
447
448
  }
448
449
 
449
- // Inline math $`...`$
450
- if (ch === "$" && input[i + 1] === "`") {
451
- const close = input.indexOf("`$", i + 2);
452
- if (close !== -1) {
453
- flush();
454
- const latex = input.slice(i + 2, close);
455
- out.push({
456
- type: "notionInlineAtom",
457
- attrs: { tagName: "math", attrsJson: "{}", label: latex },
458
- });
459
- i = close + 2;
460
- continue;
461
- }
450
+ // Canonical inline math is $...$; GitHub's $`...`$ form remains a
451
+ // backwards-compatible input alias and canonicalizes on the next write.
452
+ const inlineMath = ch === "$" ? matchInlineMathAt(input, i) : null;
453
+ if (inlineMath) {
454
+ flush();
455
+ out.push({
456
+ type: "notionInlineAtom",
457
+ attrs: {
458
+ tagName: "math",
459
+ attrsJson: "{}",
460
+ label: inlineMath.latex,
461
+ },
462
+ });
463
+ i = inlineMath.to;
464
+ continue;
462
465
  }
463
466
 
464
467
  // Inline code `...` — CommonMark-style variable-length delimiter: the
@@ -413,7 +413,6 @@ const cloudflareSsrStubs =
413
413
  "@xterm/addon-fit",
414
414
  "@xterm/addon-web-links",
415
415
  "@xterm/xterm",
416
- "katex",
417
416
  "lowlight",
418
417
  "prettier",
419
418
  "react-markdown",
@@ -121,6 +121,23 @@ const drafts = await listAppState("compose-");
121
121
 
122
122
  The `send-email` action accepts an optional `attachments` array. Each entry must reference a file that was previously uploaded via the media-upload endpoint (`/api/media/upload`). Pass the server-side `filename` (the key returned by the upload endpoint, e.g. `abc123.pdf`), and optionally `originalName` (display name for the recipient) and `mimeType`. The attachment plumbing resolves the file from `data/uploads/` or from the configured file-storage URL recorded by the upload endpoint, then includes it as a MIME multipart attachment in the outgoing Gmail message. Do not store or paste attachment bytes, base64, or `data:` URLs in draft state/settings; files are never sent speculatively — only attach what the user has explicitly provided and confirmed.
123
123
 
124
+ ### Local files from an MCP host
125
+
126
+ A hosted Mail app cannot read a path on the MCP host's local filesystem. Use
127
+ `create-attachment-upload` with the file's display name. It returns a
128
+ five-minute, owner-bound `uploadUrl`, a `PUT` method, and the attachment handle
129
+ Mail expects. From the local host, upload the raw bytes directly:
130
+
131
+ ```bash
132
+ curl --fail-with-body --request PUT --header '<Authorization header>' --data-binary @/absolute/path/to/report.pdf '<uploadUrl>'
133
+ ```
134
+
135
+ Use the response's `attachment` object in the draft/send request. The upload
136
+ Authorization header is a short-lived bearer credential: do not log, persist,
137
+ reuse, or share it. Creating or consuming it does not send mail. Keep `send-email` behind its
138
+ normal explicit-intent and human-approval gate; for drafting, pass the handle
139
+ through `ask_app` or open the returned compose deep link.
140
+
124
141
  Example:
125
142
  ```json
126
143
  {