@lazyingart/agent-web 0.1.40

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 (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/docs/architecture.md +503 -0
  4. package/package.json +43 -0
  5. package/src/aginti-adapter.js +602 -0
  6. package/src/chat-context.js +1020 -0
  7. package/src/chat-migrations.js +947 -0
  8. package/src/chat-store.js +3308 -0
  9. package/src/cli.js +134 -0
  10. package/src/cloud-server.js +2043 -0
  11. package/src/contracts.js +103 -0
  12. package/src/deterministic-context-summarizer.js +254 -0
  13. package/src/direct-chat-capability-limits.js +66 -0
  14. package/src/direct-chat-contract.js +3 -0
  15. package/src/errors.js +50 -0
  16. package/src/http-contract.js +592 -0
  17. package/src/index.js +88 -0
  18. package/src/localllm-connector.js +667 -0
  19. package/src/migrations.js +231 -0
  20. package/src/operator-health.js +184 -0
  21. package/src/password-verifier.js +131 -0
  22. package/src/service-config.js +547 -0
  23. package/src/service.js +408 -0
  24. package/src/sqlite-health.js +83 -0
  25. package/src/storage-path.js +130 -0
  26. package/src/store.js +914 -0
  27. package/src/validation.js +181 -0
  28. package/src/vision-attachment.js +404 -0
  29. package/src/web/aginti-client.js +552 -0
  30. package/src/web/aginti-protocol.js +1146 -0
  31. package/src/web/asset-map.js +462 -0
  32. package/src/web/browser-app.js +6491 -0
  33. package/src/web/cloud-session-client.js +427 -0
  34. package/src/web/direct-chat-client.js +1482 -0
  35. package/src/web/index.js +10 -0
  36. package/src/web/presentation-state.js +107 -0
  37. package/src/web/pwa-assets.js +854 -0
  38. package/src/web/pwa-update-handoff-store.js +179 -0
  39. package/src/web/safe-rendering.js +836 -0
  40. package/src/web/vision-image-client.js +546 -0
  41. package/src/web/vision-image-sanitizer.js +168 -0
  42. package/src/web/web-release.js +28 -0
@@ -0,0 +1,836 @@
1
+ import { validateArtifact } from "./aginti-protocol.js";
2
+ import { optionalWebRelease } from "./web-release.js";
3
+
4
+ const MAX_MARKDOWN = 32_000;
5
+ const MAX_TEX_TOTAL = 8_192;
6
+ const MAX_TEX_EXPRESSION = 4_096;
7
+ const MAX_MATH_EXPRESSIONS = 32;
8
+ const MAX_INLINE_DEPTH = 12;
9
+ const MAX_BLOCK_DEPTH = 8;
10
+ const SVG_NS = "http://www.w3.org/2000/svg";
11
+ const PLOT_COLORS = Object.freeze(["#147d75", "#4472ca", "#c55c37", "#8c5bbd", "#73802d", "#bb4f7b", "#427f9e", "#9b6b2f"]);
12
+
13
+ function browserDocument(value) {
14
+ if (!value || typeof value.createElement !== "function" || typeof value.createTextNode !== "function"
15
+ || typeof value.createDocumentFragment !== "function" || typeof value.createElementNS !== "function") {
16
+ throw new TypeError("a DOM document implementation is required");
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function targetNode(value) {
22
+ if (!value || typeof value.replaceChildren !== "function" || typeof value.appendChild !== "function") {
23
+ throw new TypeError("render target must be a DOM node");
24
+ }
25
+ return value;
26
+ }
27
+
28
+ function createNode(document, name, className) {
29
+ const node = document.createElement(name);
30
+ if (className) node.className = className;
31
+ return node;
32
+ }
33
+
34
+ function createSvg(document, name, attributes = {}) {
35
+ const node = document.createElementNS(SVG_NS, name);
36
+ for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));
37
+ return node;
38
+ }
39
+
40
+ function appendText(document, parent, value) {
41
+ parent.appendChild(document.createTextNode(value));
42
+ }
43
+
44
+ function escapedAt(value, index) {
45
+ let slashes = 0;
46
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
47
+ return slashes % 2 === 1;
48
+ }
49
+
50
+ function closingDelimiter(value, delimiter, start) {
51
+ let index = start;
52
+ while ((index = value.indexOf(delimiter, index)) !== -1) {
53
+ if (!escapedAt(value, index)) return index;
54
+ index += delimiter.length;
55
+ }
56
+ return -1;
57
+ }
58
+
59
+ function safeHref(value, locationHref) {
60
+ if (typeof value !== "string" || value.length < 1 || value.length > 2_048 || /[\u0000-\u001f\u007f]/u.test(value)) return null;
61
+ if (value.startsWith("#") && /^#[A-Za-z0-9_.:-]{1,200}$/u.test(value)) return value;
62
+ let parsed;
63
+ try {
64
+ parsed = new URL(value, locationHref);
65
+ } catch {
66
+ return null;
67
+ }
68
+ if (!["https:", "http:", "mailto:"].includes(parsed.protocol) || parsed.username || parsed.password) return null;
69
+ return parsed.href;
70
+ }
71
+
72
+ function mathNode({ document, katex }, source, displayMode, budget) {
73
+ const container = createNode(document, displayMode ? "div" : "span", displayMode ? "math-display" : "math-inline");
74
+ const bounded = source.slice(0, MAX_TEX_EXPRESSION);
75
+ if (source.length > MAX_TEX_EXPRESSION || budget.expressions >= MAX_MATH_EXPRESSIONS
76
+ || budget.characters + bounded.length > MAX_TEX_TOTAL || !katex || typeof katex.render !== "function") {
77
+ const fallback = createNode(document, "code", "math-fallback");
78
+ fallback.textContent = `${displayMode ? "$$" : "$"}${source}${displayMode ? "$$" : "$"}`;
79
+ container.appendChild(fallback);
80
+ return container;
81
+ }
82
+ budget.expressions += 1;
83
+ budget.characters += bounded.length;
84
+ try {
85
+ katex.render(bounded, container, {
86
+ displayMode,
87
+ output: "mathml",
88
+ trust: false,
89
+ throwOnError: false,
90
+ strict: "error",
91
+ maxExpand: 500,
92
+ maxSize: 10,
93
+ macros: {},
94
+ });
95
+ } catch {
96
+ const fallback = createNode(document, "code", "math-fallback");
97
+ fallback.textContent = `${displayMode ? "$$" : "$"}${bounded}${displayMode ? "$$" : "$"}`;
98
+ container.replaceChildren(fallback);
99
+ }
100
+ return container;
101
+ }
102
+
103
+ function appendInline(runtime, parent, source, budget, depth = 0) {
104
+ const { document, locationHref } = runtime;
105
+ if (depth > MAX_INLINE_DEPTH) {
106
+ appendText(document, parent, source);
107
+ return;
108
+ }
109
+ let cursor = 0;
110
+ while (cursor < source.length) {
111
+ const remaining = source.slice(cursor);
112
+ if (remaining.startsWith("\\(") && !escapedAt(source, cursor)) {
113
+ const end = closingDelimiter(source, "\\)", cursor + 2);
114
+ if (end > cursor + 2) {
115
+ parent.appendChild(mathNode(runtime, source.slice(cursor + 2, end), false, budget));
116
+ cursor = end + 2;
117
+ continue;
118
+ }
119
+ }
120
+ if (source[cursor] === "$" && source[cursor + 1] !== "$" && !escapedAt(source, cursor)) {
121
+ const end = closingDelimiter(source, "$", cursor + 1);
122
+ if (end > cursor + 1 && !/\s/u.test(source[cursor + 1]) && !/\s/u.test(source[end - 1])) {
123
+ parent.appendChild(mathNode(runtime, source.slice(cursor + 1, end), false, budget));
124
+ cursor = end + 1;
125
+ continue;
126
+ }
127
+ }
128
+ if (source[cursor] === "`") {
129
+ let length = 1;
130
+ while (source[cursor + length] === "`") length += 1;
131
+ const delimiter = "`".repeat(length);
132
+ const end = source.indexOf(delimiter, cursor + length);
133
+ if (end !== -1) {
134
+ const code = createNode(document, "code", "inline-code");
135
+ code.textContent = source.slice(cursor + length, end);
136
+ parent.appendChild(code);
137
+ cursor = end + length;
138
+ continue;
139
+ }
140
+ }
141
+ const link = /^\[([^\]\n]{1,500})\]\(([^\s()]{1,2048})\)/u.exec(remaining);
142
+ if (link) {
143
+ const href = safeHref(link[2], locationHref);
144
+ if (href === null) appendText(document, parent, link[0]);
145
+ else {
146
+ const anchor = createNode(document, "a");
147
+ anchor.href = href;
148
+ anchor.rel = "noopener noreferrer";
149
+ if (/^https?:/u.test(href)) anchor.target = "_blank";
150
+ appendInline(runtime, anchor, link[1], budget, depth + 1);
151
+ parent.appendChild(anchor);
152
+ }
153
+ cursor += link[0].length;
154
+ continue;
155
+ }
156
+ const autoLink = /^<(https?:\/\/[^<>\s]{1,2048}|mailto:[^<>\s]{1,2048})>/iu.exec(remaining);
157
+ if (autoLink) {
158
+ const href = safeHref(autoLink[1], locationHref);
159
+ if (href === null) appendText(document, parent, autoLink[0]);
160
+ else {
161
+ const anchor = createNode(document, "a");
162
+ anchor.href = href;
163
+ anchor.rel = "noopener noreferrer";
164
+ if (/^https?:/u.test(href)) anchor.target = "_blank";
165
+ anchor.textContent = autoLink[1];
166
+ parent.appendChild(anchor);
167
+ }
168
+ cursor += autoLink[0].length;
169
+ continue;
170
+ }
171
+ const strong = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/u.exec(remaining);
172
+ if (strong) {
173
+ const node = createNode(document, "strong");
174
+ appendInline(runtime, node, strong[2], budget, depth + 1);
175
+ parent.appendChild(node);
176
+ cursor += strong[0].length;
177
+ continue;
178
+ }
179
+ const strike = /^~~(?=\S)([\s\S]*?\S)~~/u.exec(remaining);
180
+ if (strike) {
181
+ const node = createNode(document, "del");
182
+ appendInline(runtime, node, strike[1], budget, depth + 1);
183
+ parent.appendChild(node);
184
+ cursor += strike[0].length;
185
+ continue;
186
+ }
187
+ const emphasis = /^(\*|_)(?=\S)([^\n]*?\S)\1/u.exec(remaining);
188
+ if (emphasis) {
189
+ const node = createNode(document, "em");
190
+ appendInline(runtime, node, emphasis[2], budget, depth + 1);
191
+ parent.appendChild(node);
192
+ cursor += emphasis[0].length;
193
+ continue;
194
+ }
195
+ if (source[cursor] === "\\" && /[\\`*_[\]{}()#+.!$~-]/u.test(source[cursor + 1] ?? "")) {
196
+ appendText(document, parent, source[cursor + 1]);
197
+ cursor += 2;
198
+ continue;
199
+ }
200
+ if (source[cursor] === "\n") {
201
+ parent.appendChild(source.slice(Math.max(0, cursor - 2), cursor) === " " ? createNode(document, "br") : document.createTextNode(" "));
202
+ cursor += 1;
203
+ continue;
204
+ }
205
+ let next = cursor + 1;
206
+ while (next < source.length && !/[\\`*$\[<_~\n]/u.test(source[next])) next += 1;
207
+ appendText(document, parent, source.slice(cursor, next));
208
+ cursor = next;
209
+ }
210
+ }
211
+
212
+ function splitTableRow(line) {
213
+ let value = line.trim();
214
+ if (value.startsWith("|")) value = value.slice(1);
215
+ if (value.endsWith("|") && !escapedAt(value, value.length - 1)) value = value.slice(0, -1);
216
+ const cells = [];
217
+ let cell = "";
218
+ for (let index = 0; index < value.length; index += 1) {
219
+ if (value[index] === "|" && !escapedAt(value, index)) {
220
+ cells.push(cell.trim());
221
+ cell = "";
222
+ } else cell += value[index];
223
+ }
224
+ cells.push(cell.trim());
225
+ return cells.slice(0, 20);
226
+ }
227
+
228
+ function tableSeparator(line) {
229
+ const cells = splitTableRow(line);
230
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
231
+ }
232
+
233
+ function blockStart(lines, index) {
234
+ const line = lines[index] ?? "";
235
+ if (!line.trim()) return true;
236
+ if (/^ {0,3}(?:#{1,6})\s+|^ {0,3}(?:[-+*]|\d+[.)])\s+|^ {0,3}>|^ {0,3}(?:`{3,}|~{3,})/u.test(line)) return true;
237
+ if (/^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/u.test(line) || line.trim().startsWith("$$") || line.trim().startsWith("\\[")) return true;
238
+ return index + 1 < lines.length && line.includes("|") && tableSeparator(lines[index + 1]);
239
+ }
240
+
241
+ function renderBlocks(runtime, target, lines, budget, depth = 0) {
242
+ const { document } = runtime;
243
+ if (depth > MAX_BLOCK_DEPTH) {
244
+ const fallback = createNode(document, "pre", "markdown-fallback");
245
+ fallback.textContent = lines.join("\n");
246
+ target.appendChild(fallback);
247
+ return;
248
+ }
249
+ let index = 0;
250
+ while (index < lines.length) {
251
+ const line = lines[index];
252
+ if (!line.trim()) { index += 1; continue; }
253
+ const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/u.exec(line);
254
+ if (fence) {
255
+ const marker = fence[1][0];
256
+ const body = [];
257
+ index += 1;
258
+ while (index < lines.length && !new RegExp(`^ {0,3}${marker === "`" ? "`" : "~"}{${fence[1].length},}\\s*$`, "u").test(lines[index])) {
259
+ body.push(lines[index]);
260
+ index += 1;
261
+ }
262
+ if (index < lines.length) index += 1;
263
+ const pre = createNode(document, "pre", "code-block");
264
+ const code = createNode(document, "code");
265
+ const language = fence[2].trim().split(/\s+/u, 1)[0].toLowerCase();
266
+ if (/^[a-z0-9_+-]{1,32}$/u.test(language)) code.className = `language-${language}`;
267
+ code.textContent = body.join("\n");
268
+ pre.appendChild(code);
269
+ target.appendChild(pre);
270
+ continue;
271
+ }
272
+ const trimmed = line.trim();
273
+ if (trimmed.startsWith("$$") || trimmed.startsWith("\\[")) {
274
+ const open = trimmed.startsWith("$$") ? "$$" : "\\[";
275
+ const close = open === "$$" ? "$$" : "\\]";
276
+ let expression = trimmed.slice(open.length);
277
+ let closed = expression.endsWith(close) && expression.length > close.length;
278
+ if (closed) expression = expression.slice(0, -close.length);
279
+ index += 1;
280
+ while (!closed && index < lines.length) {
281
+ const candidate = lines[index];
282
+ if (candidate.trim().endsWith(close)) {
283
+ expression += `${expression ? "\n" : ""}${candidate.slice(0, candidate.lastIndexOf(close))}`;
284
+ closed = true;
285
+ index += 1;
286
+ } else {
287
+ expression += `${expression ? "\n" : ""}${candidate}`;
288
+ index += 1;
289
+ }
290
+ }
291
+ if (closed && expression.trim()) target.appendChild(mathNode(runtime, expression.trim(), true, budget));
292
+ else {
293
+ const fallback = createNode(document, "pre", "math-fallback");
294
+ fallback.textContent = `${open}${expression}`;
295
+ target.appendChild(fallback);
296
+ }
297
+ continue;
298
+ }
299
+ const heading = /^ {0,3}(#{1,6})\s+(.+?)\s*#*$/u.exec(line);
300
+ if (heading) {
301
+ const node = createNode(document, `h${heading[1].length}`);
302
+ appendInline(runtime, node, heading[2], budget);
303
+ target.appendChild(node);
304
+ index += 1;
305
+ continue;
306
+ }
307
+ if (/^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/u.test(line)) {
308
+ target.appendChild(createNode(document, "hr"));
309
+ index += 1;
310
+ continue;
311
+ }
312
+ if (/^ {0,3}>/u.test(line)) {
313
+ const quoteLines = [];
314
+ while (index < lines.length && /^ {0,3}>/u.test(lines[index])) {
315
+ quoteLines.push(lines[index].replace(/^ {0,3}> ?/u, ""));
316
+ index += 1;
317
+ }
318
+ const quote = createNode(document, "blockquote");
319
+ renderBlocks(runtime, quote, quoteLines, budget, depth + 1);
320
+ target.appendChild(quote);
321
+ continue;
322
+ }
323
+ const listStart = /^ {0,3}([-+*]|\d+[.)])\s+(.+)$/u.exec(line);
324
+ if (listStart) {
325
+ const ordered = /^\d/u.test(listStart[1]);
326
+ const list = createNode(document, ordered ? "ol" : "ul");
327
+ let rows = 0;
328
+ while (index < lines.length && rows < 500) {
329
+ const item = /^ {0,3}([-+*]|\d+[.)])\s+(.+)$/u.exec(lines[index]);
330
+ if (!item || /^\d/u.test(item[1]) !== ordered) break;
331
+ const entry = createNode(document, "li");
332
+ appendInline(runtime, entry, item[2], budget);
333
+ list.appendChild(entry);
334
+ index += 1;
335
+ rows += 1;
336
+ }
337
+ target.appendChild(list);
338
+ continue;
339
+ }
340
+ if (index + 1 < lines.length && line.includes("|") && tableSeparator(lines[index + 1])) {
341
+ const headings = splitTableRow(line);
342
+ const alignments = splitTableRow(lines[index + 1]);
343
+ const wrapper = createNode(document, "div", "table-scroll");
344
+ const table = createNode(document, "table");
345
+ const head = createNode(document, "thead");
346
+ const headingRow = createNode(document, "tr");
347
+ headings.forEach((heading, column) => {
348
+ const cell = createNode(document, "th");
349
+ cell.scope = "col";
350
+ cell.dataset.align = alignments[column]?.startsWith(":") && alignments[column]?.endsWith(":")
351
+ ? "center" : (alignments[column]?.endsWith(":") ? "right" : "left");
352
+ appendInline(runtime, cell, heading, budget);
353
+ headingRow.appendChild(cell);
354
+ });
355
+ head.appendChild(headingRow);
356
+ table.appendChild(head);
357
+ const body = createNode(document, "tbody");
358
+ index += 2;
359
+ let rows = 0;
360
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim() && rows < 200) {
361
+ const row = createNode(document, "tr");
362
+ const cells = splitTableRow(lines[index]);
363
+ headings.forEach((unused, column) => {
364
+ const cell = createNode(document, "td");
365
+ cell.dataset.align = alignments[column]?.startsWith(":") && alignments[column]?.endsWith(":")
366
+ ? "center" : (alignments[column]?.endsWith(":") ? "right" : "left");
367
+ appendInline(runtime, cell, cells[column] ?? "", budget);
368
+ row.appendChild(cell);
369
+ });
370
+ body.appendChild(row);
371
+ index += 1;
372
+ rows += 1;
373
+ }
374
+ table.appendChild(body);
375
+ wrapper.appendChild(table);
376
+ target.appendChild(wrapper);
377
+ continue;
378
+ }
379
+ const paragraphLines = [line];
380
+ index += 1;
381
+ while (index < lines.length && !blockStart(lines, index)) {
382
+ paragraphLines.push(lines[index]);
383
+ index += 1;
384
+ }
385
+ const paragraph = createNode(document, "p");
386
+ appendInline(runtime, paragraph, paragraphLines.join("\n"), budget);
387
+ target.appendChild(paragraph);
388
+ }
389
+ }
390
+
391
+ function normalizedPlot(spec) {
392
+ const categorical = spec.type !== "scatter";
393
+ return {
394
+ ...spec,
395
+ series: spec.series.map((series, index) => ({
396
+ name: series.name,
397
+ color: PLOT_COLORS[index % PLOT_COLORS.length],
398
+ points: categorical
399
+ ? series.data.map((y, point) => ({ x: point, y, label: spec.labels[point] }))
400
+ : series.points.map(({ x, y }) => ({ x, y, label: String(x) })),
401
+ })),
402
+ };
403
+ }
404
+
405
+ function plotBounds(plot) {
406
+ const points = plot.series.flatMap((series) => series.points);
407
+ let minX = Math.min(...points.map((point) => point.x));
408
+ let maxX = Math.max(...points.map((point) => point.x));
409
+ let minY = Math.min(0, ...points.map((point) => point.y));
410
+ let maxY = Math.max(0, ...points.map((point) => point.y));
411
+ if (minX === maxX) { minX -= 1; maxX += 1; }
412
+ if (minY === maxY) { minY -= 1; maxY += 1; }
413
+ const spanX = maxX - minX;
414
+ const spanY = maxY - minY;
415
+ if (![minX, maxX, minY, maxY, spanX, spanY].every(Number.isFinite)
416
+ || spanX <= 0 || spanY <= 0) {
417
+ throw new TypeError("plot bounds are not safely renderable");
418
+ }
419
+ return { minX, maxX, minY, maxY };
420
+ }
421
+
422
+ function formatPlotTick(value, precision = 2) {
423
+ if (Object.is(value, -0) || value === 0) return "0";
424
+ const magnitude = Math.abs(value);
425
+ if (magnitude >= 1_000 || magnitude < 0.001) {
426
+ const [coefficient, exponent] = value.toExponential(Math.max(0, precision - 1)).split("e");
427
+ const compactCoefficient = coefficient.includes(".")
428
+ ? coefficient.replace(/0+$/u, "").replace(/\.$/u, "")
429
+ : coefficient;
430
+ return `${compactCoefficient}e${Number(exponent)}`;
431
+ }
432
+ return Number(value.toPrecision(precision)).toString();
433
+ }
434
+
435
+ function labelsDistinguishValues(values, labels) {
436
+ for (let left = 0; left < values.length; left += 1) {
437
+ for (let right = left + 1; right < values.length; right += 1) {
438
+ if (values[left] !== values[right] && labels[left] === labels[right]) return false;
439
+ }
440
+ }
441
+ return true;
442
+ }
443
+
444
+ function formatPlotTicks(values, { allowOffset = false, preferCompact = false } = {}) {
445
+ for (let precision = preferCompact ? 2 : 3; precision <= 3; precision += 1) {
446
+ const readableLabels = values.map((value) => formatPlotTick(value, precision));
447
+ if (labelsDistinguishValues(values, readableLabels)) return { labels: readableLabels, offset: null };
448
+ }
449
+ if (allowOffset) {
450
+ const offset = values[0];
451
+ const relative = values.map((value) => value - offset);
452
+ for (let precision = 2; precision <= 6; precision += 1) {
453
+ const labels = relative.map((value) => {
454
+ const label = formatPlotTick(value, precision);
455
+ return value > 0 ? `+${label}` : label;
456
+ });
457
+ if (labelsDistinguishValues(relative, labels)) {
458
+ return { labels, offset };
459
+ }
460
+ }
461
+ }
462
+ for (let precision = 4; precision <= 15; precision += 1) {
463
+ const labels = values.map((value) => formatPlotTick(value, precision));
464
+ if (labelsDistinguishValues(values, labels)) return { labels, offset: null };
465
+ }
466
+ return { labels: values.map(String), offset: null };
467
+ }
468
+
469
+ function formatPlotOffset(value) {
470
+ return value.toString().replace("e+", "e");
471
+ }
472
+
473
+ function compactPlotLabel(value, maximum) {
474
+ const characters = [...value];
475
+ return characters.length > maximum
476
+ ? `${characters.slice(0, maximum - 1).join("")}…`
477
+ : value;
478
+ }
479
+
480
+ function categoricalTickIndices(count, maximum) {
481
+ if (count <= maximum) return Array.from({ length: count }, (unused, index) => index);
482
+ const step = Math.max(2, Math.ceil((count - 1) / (maximum - 1)));
483
+ const indices = [];
484
+ for (let index = 0; index < count; index += step) indices.push(index);
485
+ if (indices.at(-1) !== count - 1) {
486
+ if (count - 1 - indices.at(-1) < 2) indices[indices.length - 1] = count - 1;
487
+ else indices.push(count - 1);
488
+ }
489
+ return indices;
490
+ }
491
+
492
+ function renderPlot(document, target, artifact) {
493
+ const plot = normalizedPlot(artifact.spec);
494
+ const bounds = plotBounds(plot);
495
+ const dimensions = { width: 720, height: 390, left: 116, right: 32, top: 40, bottom: 58 };
496
+ const innerWidth = dimensions.width - dimensions.left - dimensions.right;
497
+ const innerHeight = dimensions.height - dimensions.top - dimensions.bottom;
498
+ const xAt = (value) => dimensions.left + (value - bounds.minX) / (bounds.maxX - bounds.minX) * innerWidth;
499
+ const yAt = (value) => dimensions.top + (bounds.maxY - value) / (bounds.maxY - bounds.minY) * innerHeight;
500
+ const yTickValues = Array.from({ length: 5 }, (unused, tick) => (
501
+ bounds.maxY - (bounds.maxY - bounds.minY) * tick / 4
502
+ ));
503
+ const yTickLabels = formatPlotTicks(yTickValues).labels;
504
+ const categoricalIndices = plot.labels ? categoricalTickIndices(plot.labels.length, 4) : null;
505
+ const xTickValues = plot.labels ? null : Array.from({ length: 5 }, (unused, index) => (
506
+ bounds.minX + (bounds.maxX - bounds.minX) * index / 4
507
+ ));
508
+ const xTickPlan = xTickValues === null ? null : formatPlotTicks(xTickValues, {
509
+ allowOffset: true,
510
+ preferCompact: true,
511
+ });
512
+ const descriptionId = `plot-description-${artifact.id.slice(4)}`;
513
+ const svg = createSvg(document, "svg", {
514
+ viewBox: `0 0 ${dimensions.width} ${dimensions.height}`,
515
+ width: dimensions.width,
516
+ height: dimensions.height,
517
+ role: "img",
518
+ "aria-label": artifact.title,
519
+ "aria-describedby": descriptionId,
520
+ preserveAspectRatio: "xMidYMid meet",
521
+ });
522
+ svg.classList.add("artifact-plot");
523
+ const title = createSvg(document, "title");
524
+ title.textContent = artifact.title;
525
+ svg.appendChild(title);
526
+ const description = createSvg(document, "desc", { id: descriptionId });
527
+ description.textContent = [
528
+ plot.xLabel ? `X axis: ${plot.xLabel}` : "",
529
+ plot.yLabel ? `Y axis: ${plot.yLabel}` : "",
530
+ categoricalIndices === null
531
+ ? `X-axis absolute ticks: ${xTickValues.map(String).join(", ")}${xTickPlan.offset === null
532
+ ? ""
533
+ : `; visual labels use offset ${xTickPlan.offset > 0 ? "+" : ""}${formatPlotOffset(xTickPlan.offset)}`}`
534
+ : `Displayed category ticks: ${categoricalIndices.map((index) => plot.labels[index]).join("; ")}`,
535
+ `Y-axis ticks: ${yTickValues.map(String).join(", ")}`,
536
+ `Series: ${plot.series.map((series) => series.name).join("; ")}`,
537
+ ].filter(Boolean).join(". ");
538
+ svg.appendChild(description);
539
+ for (let tick = 0; tick <= 4; tick += 1) {
540
+ const y = dimensions.top + innerHeight * tick / 4;
541
+ svg.appendChild(createSvg(document, "line", { class: "plot-grid", x1: dimensions.left, y1: y, x2: dimensions.width - dimensions.right, y2: y }));
542
+ const text = createSvg(document, "text", {
543
+ class: "plot-tick plot-y-tick",
544
+ x: dimensions.left - 10,
545
+ y: y + 4,
546
+ "text-anchor": "end",
547
+ "aria-label": yTickValues[tick].toString(),
548
+ });
549
+ text.textContent = yTickLabels[tick];
550
+ svg.appendChild(text);
551
+ }
552
+ const zeroY = yAt(0);
553
+ plot.series.forEach((series, seriesIndex) => {
554
+ const group = createSvg(document, "g", { class: "plot-series", "data-series": seriesIndex });
555
+ if (plot.type === "bar") {
556
+ const groupWidth = innerWidth / series.points.length;
557
+ const barWidth = Math.max(2, Math.min(36, groupWidth * 0.72 / plot.series.length));
558
+ series.points.forEach((point) => {
559
+ const center = dimensions.left + (point.x + 0.5) / series.points.length * innerWidth;
560
+ const x = center - (barWidth * plot.series.length / 2) + seriesIndex * barWidth;
561
+ const y = yAt(Math.max(0, point.y));
562
+ const bottom = yAt(Math.min(0, point.y));
563
+ group.appendChild(createSvg(document, "rect", { x, y, width: Math.max(1, barWidth - 1), height: Math.max(1, bottom - y), rx: 2, fill: series.color }));
564
+ });
565
+ } else {
566
+ const commands = series.points.map((point, index) => `${index ? "L" : "M"}${xAt(point.x).toFixed(2)} ${yAt(point.y).toFixed(2)}`).join(" ");
567
+ if (plot.type === "area") {
568
+ group.appendChild(createSvg(document, "path", {
569
+ d: `${commands} L${xAt(series.points.at(-1).x).toFixed(2)} ${zeroY.toFixed(2)} L${xAt(series.points[0].x).toFixed(2)} ${zeroY.toFixed(2)} Z`,
570
+ fill: series.color,
571
+ opacity: 0.18,
572
+ }));
573
+ }
574
+ if (plot.type !== "scatter") group.appendChild(createSvg(document, "path", { d: commands, fill: "none", stroke: series.color, "stroke-width": 2.5 }));
575
+ series.points.forEach((point) => group.appendChild(createSvg(document, "circle", {
576
+ cx: xAt(point.x), cy: yAt(point.y), r: plot.type === "scatter" ? 4.5 : 3, fill: series.color,
577
+ })));
578
+ }
579
+ svg.appendChild(group);
580
+ });
581
+ svg.appendChild(createSvg(document, "line", { class: "plot-axis", x1: dimensions.left, y1: zeroY, x2: dimensions.width - dimensions.right, y2: zeroY }));
582
+ svg.appendChild(createSvg(document, "line", { class: "plot-axis", x1: dimensions.left, y1: dimensions.top, x2: dimensions.left, y2: dimensions.height - dimensions.bottom }));
583
+ if (plot.labels) {
584
+ categoricalIndices.forEach((index, position) => {
585
+ const value = plot.labels[index];
586
+ const tick = createSvg(document, "text", {
587
+ class: "plot-tick plot-x-tick",
588
+ "aria-label": value,
589
+ "data-label-index": index,
590
+ x: plot.type === "bar"
591
+ ? dimensions.left + (index + 0.5) / plot.labels.length * innerWidth
592
+ : xAt(index),
593
+ y: dimensions.height - dimensions.bottom + 19,
594
+ "text-anchor": categoricalIndices.length === 1
595
+ ? "middle"
596
+ : (position === 0 ? "start" : (position === categoricalIndices.length - 1 ? "end" : "middle")),
597
+ });
598
+ const wide = createSvg(document, "tspan", { class: "plot-label-wide" });
599
+ wide.textContent = compactPlotLabel(value, 7);
600
+ tick.appendChild(wide);
601
+ const compact = createSvg(document, "tspan", { class: "plot-label-compact" });
602
+ compact.textContent = compactPlotLabel(value, 5);
603
+ tick.appendChild(compact);
604
+ svg.appendChild(tick);
605
+ });
606
+ } else {
607
+ for (let index = 0; index <= 4; index += 1) {
608
+ const tick = createSvg(document, "text", {
609
+ class: "plot-tick plot-x-tick",
610
+ x: dimensions.left + innerWidth * index / 4,
611
+ y: dimensions.height - dimensions.bottom + 19,
612
+ "text-anchor": index === 0 ? "start" : (index === 4 ? "end" : "middle"),
613
+ "aria-label": xTickValues[index].toString(),
614
+ });
615
+ tick.textContent = xTickPlan.labels[index];
616
+ svg.appendChild(tick);
617
+ }
618
+ if (xTickPlan.offset !== null) {
619
+ const offset = createSvg(document, "text", {
620
+ class: "plot-tick plot-axis-offset plot-x-offset",
621
+ x: dimensions.width - dimensions.right,
622
+ y: 26,
623
+ "text-anchor": "end",
624
+ });
625
+ offset.textContent = `offset ${xTickPlan.offset > 0 ? "+" : ""}${formatPlotOffset(xTickPlan.offset)}`;
626
+ svg.appendChild(offset);
627
+ }
628
+ }
629
+ if (plot.xLabel) {
630
+ const text = createSvg(document, "text", {
631
+ class: "plot-tick plot-axis-label plot-x-label",
632
+ x: dimensions.left + innerWidth / 2,
633
+ y: dimensions.height - 14,
634
+ "text-anchor": "middle",
635
+ });
636
+ text.textContent = plot.xLabel;
637
+ svg.appendChild(text);
638
+ }
639
+ if (plot.yLabel) {
640
+ const middle = dimensions.top + innerHeight / 2;
641
+ const text = createSvg(document, "text", {
642
+ class: "plot-tick plot-axis-label plot-y-label",
643
+ x: 18,
644
+ y: middle,
645
+ transform: `rotate(-90 18 ${middle})`,
646
+ "text-anchor": "middle",
647
+ "dominant-baseline": "middle",
648
+ });
649
+ text.textContent = plot.yLabel;
650
+ svg.appendChild(text);
651
+ }
652
+ target.appendChild(svg);
653
+ const legend = createNode(document, "ul", "artifact-legend");
654
+ plot.series.forEach((series, seriesIndex) => {
655
+ const item = createNode(document, "li");
656
+ const swatch = createNode(document, "span", `artifact-swatch artifact-swatch-${seriesIndex % PLOT_COLORS.length}`);
657
+ item.appendChild(swatch);
658
+ appendText(document, item, series.name);
659
+ legend.appendChild(item);
660
+ });
661
+ target.appendChild(legend);
662
+ }
663
+
664
+ function renderTable(document, target, artifact) {
665
+ const wrapper = createNode(document, "div", "artifact-table-scroll");
666
+ const table = createNode(document, "table", "artifact-table");
667
+ const head = createNode(document, "thead");
668
+ const heading = createNode(document, "tr");
669
+ artifact.spec.columns.forEach((column) => {
670
+ const cell = createNode(document, "th");
671
+ cell.scope = "col";
672
+ cell.textContent = column.label;
673
+ heading.appendChild(cell);
674
+ });
675
+ head.appendChild(heading);
676
+ table.appendChild(head);
677
+ const body = createNode(document, "tbody");
678
+ artifact.spec.rows.forEach((row) => {
679
+ const tableRow = createNode(document, "tr");
680
+ artifact.spec.columns.forEach(({ key }) => {
681
+ const cell = createNode(document, "td");
682
+ cell.textContent = row[key] === null ? "" : String(row[key]);
683
+ tableRow.appendChild(cell);
684
+ });
685
+ body.appendChild(tableRow);
686
+ });
687
+ table.appendChild(body);
688
+ wrapper.appendChild(table);
689
+ target.appendChild(wrapper);
690
+ }
691
+
692
+ function renderSources(document, target, artifact) {
693
+ const list = createNode(document, "ol", "artifact-sources");
694
+ artifact.spec.sources.forEach((source) => {
695
+ const item = createNode(document, "li", "artifact-source-card");
696
+ const heading = createNode(document, "h4", "artifact-source-title");
697
+ const anchor = createNode(document, "a");
698
+ anchor.setAttribute("href", source.url);
699
+ anchor.setAttribute("target", "_blank");
700
+ anchor.setAttribute("rel", "noopener noreferrer");
701
+ appendText(document, anchor, source.title);
702
+ heading.appendChild(anchor);
703
+ item.appendChild(heading);
704
+ if (source.snippet) {
705
+ const snippet = createNode(document, "p", "artifact-source-snippet");
706
+ appendText(document, snippet, source.snippet);
707
+ item.appendChild(snippet);
708
+ }
709
+ const metadata = createNode(document, "p", "artifact-source-metadata");
710
+ const values = [
711
+ source.kind === "paper" ? "Paper" : "Web",
712
+ source.providers.join(", "),
713
+ source.publishedDate,
714
+ source.doi === null ? null : `DOI ${source.doi}`,
715
+ ].filter((value) => value !== null);
716
+ appendText(document, metadata, values.join(" · "));
717
+ item.appendChild(metadata);
718
+ list.appendChild(item);
719
+ });
720
+ target.appendChild(list);
721
+ }
722
+
723
+ function formatFileBytes(value) {
724
+ if (value < 1_024) return `${value} B`;
725
+ if (value < 1_024 * 1_024) return `${(value / 1_024).toFixed(value < 10 * 1_024 ? 1 : 0)} KB`;
726
+ return `${(value / (1_024 * 1_024)).toFixed(value < 10 * 1_024 * 1_024 ? 1 : 0)} MB`;
727
+ }
728
+
729
+ function renderFile(runtime, target, artifact) {
730
+ const { document, locationHref, releaseId } = runtime;
731
+ if (releaseId === null) throw new TypeError('file artifact URL requires an immutable web release');
732
+ const base = new URL(locationHref);
733
+ const openHref = new URL(`/api/agent/artifacts/${artifact.id}/content`, base);
734
+ openHref.search = `?v=${encodeURIComponent(releaseId)}`;
735
+ const downloadHref = new URL(openHref.href);
736
+ downloadHref.search = `?v=${encodeURIComponent(releaseId)}&download=1`;
737
+ if (!['http:', 'https:'].includes(base.protocol) || openHref.origin !== base.origin
738
+ || downloadHref.origin !== base.origin || openHref.username || openHref.password
739
+ || downloadHref.username || downloadHref.password || openHref.search !== `?v=${releaseId}`
740
+ || downloadHref.search !== `?v=${releaseId}&download=1` || openHref.hash || downloadHref.hash) {
741
+ throw new TypeError('file artifact URL is not same-origin');
742
+ }
743
+ const metadata = createNode(document, 'p', 'artifact-file-metadata');
744
+ appendText(document, metadata, `${artifact.spec.filename} · ${formatFileBytes(artifact.spec.bytes)}`);
745
+ target.appendChild(metadata);
746
+ const controls = createNode(document, 'div', 'artifact-file-controls');
747
+ const open = createNode(document, 'a', 'artifact-file-action artifact-file-open');
748
+ open.setAttribute('href', openHref.href);
749
+ // Keep protected artifacts in the current PWA browsing context. On iOS a
750
+ // new top-level window can leave the installed app's authenticated cookie
751
+ // store and turn a valid local artifact into a misleading sign-in failure.
752
+ open.setAttribute('aria-label', `Open ${artifact.spec.filename}`);
753
+ appendText(document, open, 'Open');
754
+ controls.appendChild(open);
755
+ const download = createNode(document, 'a', 'artifact-file-action artifact-file-download');
756
+ download.setAttribute('href', downloadHref.href);
757
+ download.setAttribute('download', artifact.spec.filename);
758
+ download.setAttribute('rel', 'noopener');
759
+ download.setAttribute('aria-label', `Download ${artifact.spec.filename}`);
760
+ appendText(document, download, 'Download');
761
+ controls.appendChild(download);
762
+ target.appendChild(controls);
763
+ const privacy = createNode(document, 'p', 'artifact-file-privacy');
764
+ appendText(document, privacy, 'Served from your local Agent session. Not stored or cached by the web edge.');
765
+ target.appendChild(privacy);
766
+ }
767
+
768
+ export function createSafeRenderer({
769
+ document = globalThis.document,
770
+ katex,
771
+ locationHref = globalThis.location?.href ?? "https://invalid.local/",
772
+ releaseId,
773
+ } = {}) {
774
+ const declaredRelease = releaseId === undefined
775
+ ? document?.querySelector?.('meta[name="lazying-agent-release"]')?.getAttribute?.('content')
776
+ : releaseId;
777
+ const runtime = Object.freeze({
778
+ document: browserDocument(document),
779
+ katex,
780
+ locationHref: String(locationHref),
781
+ releaseId: optionalWebRelease(declaredRelease),
782
+ });
783
+ const renderMarkdown = (target, source) => {
784
+ targetNode(target);
785
+ const normalized = typeof source === "string"
786
+ ? source.slice(0, MAX_MARKDOWN).replace(/\r\n?|\u2028|\u2029/gu, "\n")
787
+ : "";
788
+ const fragment = document.createDocumentFragment();
789
+ try {
790
+ renderBlocks(runtime, fragment, normalized.split("\n"), { expressions: 0, characters: 0 });
791
+ } catch {
792
+ const fallback = createNode(document, "pre", "markdown-fallback");
793
+ fallback.textContent = normalized;
794
+ fragment.replaceChildren(fallback);
795
+ }
796
+ target.replaceChildren(fragment);
797
+ };
798
+ return Object.freeze({
799
+ renderMarkdown,
800
+ renderArtifact(target, value) {
801
+ targetNode(target);
802
+ target.replaceChildren();
803
+ let artifact;
804
+ try {
805
+ artifact = validateArtifact(value);
806
+ } catch {
807
+ target.dataset.status = "rejected";
808
+ const rejected = createNode(document, "p", "artifact-rejected");
809
+ rejected.textContent = "This artifact could not be displayed safely.";
810
+ target.appendChild(rejected);
811
+ return false;
812
+ }
813
+ target.dataset.status = "ready";
814
+ target.dataset.artifactKind = artifact.kind;
815
+ try {
816
+ if (artifact.kind === "plot") renderPlot(document, target, artifact);
817
+ else if (artifact.kind === "table") renderTable(document, target, artifact);
818
+ else if (artifact.kind === "markdown") {
819
+ const markdown = createNode(document, "div", "artifact-markdown");
820
+ renderMarkdown(markdown, artifact.spec.markdown);
821
+ target.appendChild(markdown);
822
+ } else if (artifact.kind === "sources") renderSources(document, target, artifact);
823
+ else renderFile(runtime, target, artifact);
824
+ } catch {
825
+ target.replaceChildren();
826
+ target.dataset.status = "rejected";
827
+ delete target.dataset.artifactKind;
828
+ const rejected = createNode(document, "p", "artifact-rejected");
829
+ rejected.textContent = "This artifact could not be displayed safely.";
830
+ target.appendChild(rejected);
831
+ return false;
832
+ }
833
+ return true;
834
+ },
835
+ });
836
+ }