@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +23 -1
- package/corpus/core/src/a2a/handlers.ts +65 -1
- package/corpus/core/src/a2a/server.ts +124 -0
- package/corpus/core/src/a2a/task-store.ts +253 -0
- package/corpus/core/src/a2a/types.ts +26 -0
- package/corpus/core/src/agent/production-agent.ts +5 -2
- package/corpus/core/src/coding-tools/run-code.ts +65 -12
- package/corpus/core/src/extensions/actions.ts +141 -7
- package/corpus/core/src/mcp/builtin-tools.ts +25 -1
- package/corpus/core/src/scripts/call-agent.ts +22 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/global.css +53 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/content/app/i18n-data.ts +163 -0
- package/corpus/templates/content/app/root.tsx +2 -0
- package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/shared/document-export.ts +296 -25
- package/corpus/templates/content/shared/inline-math.ts +128 -0
- package/corpus/templates/content/shared/math-rendering.ts +50 -0
- package/corpus/templates/content/shared/nfm.ts +17 -14
- package/corpus/templates/content/vite.config.ts +0 -1
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
- package/corpus/templates/mail/AGENTS.md +21 -19
- package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
- package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
- package/corpus/templates/mail/server/handlers/media.ts +84 -61
- package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
- package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
- package/corpus/templates/mail/server/plugins/auth.ts +9 -1
- package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
- package/dist/a2a/client.d.ts +5 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +18 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +57 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +87 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +27 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +216 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +24 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +5 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/coding-tools/run-code.js +64 -11
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +129 -7
- package/dist/extensions/actions.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +22 -3
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/call-agent.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +19 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
256
|
+
} /> <span>${inlineMarkdownToHtml(task[2])}</span>${
|
|
257
|
+
lines.length > 1 ? markdownToHtml(lines.slice(1).join("\n")) : ""
|
|
258
|
+
}</li>`;
|
|
134
259
|
}
|
|
135
|
-
return
|
|
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
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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 "
|
|
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
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
@@ -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
|
{
|