@ai-gui/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -126,7 +126,10 @@ function repair(str) {
126
126
  break;
127
127
  }
128
128
  }
129
- if (inString && !stringIsKey) return [closeContainers(str + "\"", stack)];
129
+ if (inString && !stringIsKey) {
130
+ const body = escaped ? str.slice(0, -1) : str;
131
+ return [closeContainers(body + "\"", stack)];
132
+ }
130
133
  const fallback = lastGoodEnd === -1 ? [] : [closeContainers(str.slice(0, lastGoodEnd), stack)];
131
134
  if (inLiteral) return [closeContainers(str, stack), ...fallback];
132
135
  return fallback;
@@ -150,20 +153,68 @@ function closeContainers(body, stack) {
150
153
  */
151
154
  function repairMarkdown(buffer) {
152
155
  let out = buffer;
153
- const fenceCount = (out.match(/^```/gm) ?? []).length;
154
- if (fenceCount % 2 === 1) {
156
+ const openFence = findOpenFence(out);
157
+ if (openFence) {
155
158
  if (!out.endsWith("\n")) out += "\n";
156
- out += "```";
159
+ out += openFence;
157
160
  return out;
158
161
  }
159
- const tick = (out.match(/`/g) ?? []).length;
160
- if (tick % 2 === 1) out += "`";
161
- const bold = (out.match(/\*\*/g) ?? []).length;
162
- if (bold % 2 === 1) out += "**";
163
- const strike = (out.match(/~~/g) ?? []).length;
162
+ const inline = findOpenInlineCode(out);
163
+ const visible = inline ? out.slice(0, inline.index) : out;
164
+ const bold = countUnescaped(visible, "**");
165
+ const strike = countUnescaped(visible, "~~");
166
+ if (inline) out += inline.marker;
164
167
  if (strike % 2 === 1) out += "~~";
168
+ if (bold % 2 === 1) out += "**";
165
169
  return out;
166
170
  }
171
+ function findOpenFence(buffer) {
172
+ let open;
173
+ for (const line of buffer.split(/\r\n|\r|\n/)) {
174
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
175
+ if (!match) continue;
176
+ const marker = match[1];
177
+ if (!open) open = {
178
+ char: marker[0],
179
+ length: marker.length,
180
+ marker
181
+ };
182
+ else if (marker[0] === open.char && marker.length >= open.length && match[2].trim() === "") open = void 0;
183
+ }
184
+ return open?.marker;
185
+ }
186
+ function findOpenInlineCode(buffer) {
187
+ let open;
188
+ for (let i = 0; i < buffer.length;) {
189
+ if (buffer[i] !== "`" || isEscaped(buffer, i)) {
190
+ i++;
191
+ continue;
192
+ }
193
+ let end = i + 1;
194
+ while (buffer[end] === "`") end++;
195
+ const marker = buffer.slice(i, end);
196
+ if (!open) open = {
197
+ index: i,
198
+ marker
199
+ };
200
+ else if (open.marker === marker) open = void 0;
201
+ i = end;
202
+ }
203
+ return open;
204
+ }
205
+ function countUnescaped(buffer, marker) {
206
+ let count = 0;
207
+ for (let i = 0; i <= buffer.length - marker.length; i++) if (buffer.startsWith(marker, i) && !isEscaped(buffer, i)) {
208
+ count++;
209
+ i += marker.length - 1;
210
+ }
211
+ return count;
212
+ }
213
+ function isEscaped(buffer, index) {
214
+ let slashes = 0;
215
+ for (let i = index - 1; i >= 0 && buffer[i] === "\\"; i--) slashes++;
216
+ return slashes % 2 === 1;
217
+ }
167
218
 
168
219
  //#endregion
169
220
  //#region src/sanitizer.ts
@@ -179,8 +230,12 @@ function escapeHtml(html) {
179
230
  * fall back to escaping the HTML-significant characters. This never emits raw
180
231
  * markup and never throws.
181
232
  */
182
- function sanitizeHtml(html) {
183
- if (typeof window === "undefined") return escapeHtml(html);
233
+ function sanitizeHtml(html, options = {}) {
234
+ if (options.sanitizer) return options.sanitizer(html);
235
+ if (typeof window === "undefined") {
236
+ if (options.ssr === "throw") throw new Error("sanitizeHtml requires a DOM or an injected sanitizer");
237
+ return escapeHtml(html);
238
+ }
184
239
  return dompurify.default.sanitize(html);
185
240
  }
186
241
 
@@ -213,11 +268,16 @@ var CardRegistry = class {
213
268
  };
214
269
  }
215
270
  validate(def, data) {
216
- if (def.validate) return def.validate(data);
217
- if (def.schema) return validateSchema(def.schema, data);
218
- return true;
271
+ try {
272
+ if (def.validate) return def.validate(data);
273
+ if (def.schema) return validateSchema(def.schema, data);
274
+ return true;
275
+ } catch {
276
+ return false;
277
+ }
219
278
  }
220
279
  toPromptSpec() {
280
+ if (this.cards.size === 0) return "";
221
281
  const lines = ["You can output cards. Format: a ```card:<type> fenced block with JSON inside. Available cards:"];
222
282
  for (const def of this.cards.values()) {
223
283
  lines.push(`- \`card:${def.type}\`: ${def.description}`);
@@ -229,6 +289,9 @@ var CardRegistry = class {
229
289
  }
230
290
  return lines.join("\n");
231
291
  }
292
+ get size() {
293
+ return this.cards.size;
294
+ }
232
295
  toJSONSchema() {
233
296
  const properties = {};
234
297
  for (const def of this.cards.values()) if (def.schema) properties[def.type] = def.schema;
@@ -262,29 +325,63 @@ function validateSchema(schema, data) {
262
325
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
263
326
  function collectNodeRenderers(plugins = []) {
264
327
  const map = {};
265
- for (const p of plugins) for (const [k, v] of Object.entries(p.nodeRenderers ?? {})) map[k] = v;
328
+ for (const p of plugins) for (const [k, render] of Object.entries(p.nodeRenderers ?? {})) map[k] = (node) => node.complete === false ? {
329
+ kind: "html",
330
+ html: `<div data-aigui-block-loading="" data-block-type="${escapeAttr(node.type)}"></div>`
331
+ } : render(node);
266
332
  return map;
267
333
  }
268
334
  /** The set of node types claimed by the given plugins. */
269
335
  function pluginNodeTypes(plugins = []) {
270
336
  return new Set(Object.keys(collectNodeRenderers(plugins)));
271
337
  }
338
+ function escapeAttr(value) {
339
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
340
+ }
272
341
 
273
342
  //#endregion
274
343
  //#region src/parser.ts
275
344
  /** Build a parser that turns markdown source into a flat list of ASTNodes. */
276
345
  function createParser(options = {}) {
346
+ const parse = createParserWithMetadata(options);
347
+ return (src, rawSrc = src) => parse(src, rawSrc).nodes;
348
+ }
349
+ /** Build a parser that also reports the source range for each top-level block. */
350
+ function createParserWithMetadata(options = {}) {
277
351
  const md = new markdown_it.default({
278
352
  html: true,
279
353
  linkify: true
280
354
  });
281
355
  options.configureMd?.(md);
282
356
  for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
357
+ const hasParserExtensions = Boolean(options.configureMd || options.plugins?.some((plugin) => plugin.extendParser));
283
358
  const pluginTypes = pluginNodeTypes(options.plugins);
284
- return (src) => {
285
- const tokens = md.parse(src, {});
359
+ const completeness = new Map();
360
+ for (const plugin of options.plugins ?? []) for (const type of Object.keys(plugin.nodeRenderers ?? {})) if (plugin.isBlockComplete) completeness.set(type, plugin.isBlockComplete);
361
+ return (src, rawSrc = src, sourceOffset = 0) => {
362
+ const env = {};
363
+ const tokens = md.parse(normalizeLineEndings(src), env);
286
364
  const nodes = [];
287
- let index = 0;
365
+ const blocks = [];
366
+ const lineOffsets = getLineOffsets(rawSrc);
367
+ const startSlots = new Map();
368
+ const addNode = (node, map) => {
369
+ const range = sourceRange(map, rawSrc.length, lineOffsets, sourceOffset);
370
+ const previous = blocks.at(-1);
371
+ const block = previous && previous.start === range.start && previous.end === range.end ? previous : void 0;
372
+ const slot = startSlots.get(range.start) ?? 0;
373
+ startSlots.set(range.start, slot + 1);
374
+ nodes.push({
375
+ key: `${range.start}:${slot}`,
376
+ ...node
377
+ });
378
+ if (block) block.nodeEnd = nodes.length;
379
+ else blocks.push({
380
+ ...range,
381
+ nodeStart: nodes.length - 1,
382
+ nodeEnd: nodes.length
383
+ });
384
+ };
288
385
  for (let i = 0; i < tokens.length; i++) {
289
386
  const t = tokens[i];
290
387
  if (t.type === "fence") {
@@ -292,79 +389,82 @@ function createParser(options = {}) {
292
389
  if (info.startsWith("card:") && options.registry) {
293
390
  const cardType = info.slice(5);
294
391
  const res = options.registry.parse(cardType, t.content);
295
- nodes.push({
296
- key: `${index++}:card`,
392
+ const complete = res.complete && isClosedFence(rawSrc, t.map, t.markup);
393
+ addNode({
297
394
  type: "card",
298
395
  card: {
299
396
  type: cardType,
300
397
  data: res.data,
301
- complete: res.complete,
302
- valid: res.valid
398
+ complete,
399
+ valid: complete && res.valid
303
400
  }
304
- });
305
- } else if (pluginTypes.has(info)) nodes.push({
306
- key: `${index++}:${info}`,
307
- type: info,
308
- content: t.content,
309
- attrs: { info }
310
- });
311
- else nodes.push({
312
- key: `${index++}:code`,
401
+ }, t.map);
402
+ } else if (pluginTypes.has(info)) {
403
+ const predicate = completeness.get(info);
404
+ const fenceComplete = isClosedFence(rawSrc, t.map, t.markup);
405
+ let complete = fenceComplete;
406
+ if (complete && predicate) try {
407
+ complete = predicate(info, t.content);
408
+ } catch {
409
+ complete = false;
410
+ }
411
+ addNode({
412
+ type: info,
413
+ content: t.content,
414
+ attrs: { info },
415
+ complete
416
+ }, t.map);
417
+ } else addNode({
313
418
  type: "code",
314
419
  tag: "code",
315
420
  attrs: info ? { lang: info } : void 0,
316
421
  content: t.content
317
- });
422
+ }, t.map);
318
423
  continue;
319
424
  }
320
425
  if (t.type === "hr") {
321
- nodes.push({
322
- key: `${index++}:hr`,
426
+ addNode({
323
427
  type: "hr",
324
428
  tag: "hr"
325
- });
429
+ }, t.map);
326
430
  continue;
327
431
  }
328
432
  if (t.type === "code_block") {
329
- nodes.push({
330
- key: `${index++}:code`,
433
+ addNode({
331
434
  type: "code",
332
435
  tag: "code",
333
436
  content: t.content
334
- });
437
+ }, t.map);
335
438
  continue;
336
439
  }
337
440
  if (t.type === "html_block") {
338
- nodes.push({
339
- key: `${index++}:html`,
441
+ addNode({
340
442
  type: "html",
341
443
  content: t.content
342
- });
444
+ }, t.map);
343
445
  continue;
344
446
  }
345
447
  if (t.type === "heading_open") {
346
448
  const inline = tokens[i + 1];
347
449
  const raw = inline?.content ?? "";
348
- nodes.push({
349
- key: `${index++}:heading`,
450
+ addNode({
350
451
  type: "heading",
351
452
  tag: t.tag,
352
453
  content: raw,
353
- html: md.renderInline(raw)
354
- });
454
+ html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
455
+ }, t.map);
355
456
  i += 2;
356
457
  continue;
357
458
  }
358
459
  if (t.type === "paragraph_open") {
359
460
  const inline = tokens[i + 1];
360
461
  const raw = inline?.content ?? "";
361
- nodes.push({
362
- key: `${index++}:paragraph`,
462
+ addNode({
363
463
  type: "paragraph",
364
464
  tag: "p",
365
465
  content: raw,
366
- html: md.renderInline(raw)
367
- });
466
+ html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
467
+ }, t.map);
368
468
  i += 2;
369
469
  continue;
370
470
  }
@@ -380,54 +480,127 @@ function createParser(options = {}) {
380
480
  }
381
481
  }
382
482
  const slice = tokens.slice(i, j + 1);
383
- nodes.push({
384
- key: `${index++}:${t.type}`,
483
+ addNode({
385
484
  type: "html",
386
- content: md.renderer.render(slice, md.options, {})
387
- });
485
+ content: md.renderer.render(slice, md.options, env)
486
+ }, t.map);
388
487
  i = j;
389
488
  continue;
390
489
  }
391
490
  if (t.block && t.level === 0) {
392
- nodes.push({
393
- key: `${index++}:${t.type}`,
491
+ addNode({
394
492
  type: "html",
395
- content: md.renderer.render([t], md.options, {})
396
- });
493
+ content: md.renderer.render([t], md.options, env)
494
+ }, t.map);
397
495
  continue;
398
496
  }
399
497
  }
400
- return nodes;
498
+ return {
499
+ nodes,
500
+ blocks,
501
+ incrementalSafe: !hasParserExtensions && !hasReferenceSyntax(rawSrc)
502
+ };
401
503
  };
402
504
  }
505
+ function getLineOffsets(src) {
506
+ const offsets = [0];
507
+ for (let i = 0; i < src.length; i++) if (src[i] === "\r") {
508
+ if (src[i + 1] === "\n") i++;
509
+ offsets.push(i + 1);
510
+ } else if (src[i] === "\n") offsets.push(i + 1);
511
+ offsets.push(src.length);
512
+ return offsets;
513
+ }
514
+ function sourceRange(map, sourceLength, lineOffsets, sourceOffset) {
515
+ if (!map) return {
516
+ start: sourceOffset,
517
+ end: sourceOffset + sourceLength
518
+ };
519
+ return {
520
+ start: sourceOffset + Math.min(lineOffsets[map[0]] ?? sourceLength, sourceLength),
521
+ end: sourceOffset + Math.min(lineOffsets[map[1]] ?? sourceLength, sourceLength)
522
+ };
523
+ }
524
+ function hasReferenceSyntax(src) {
525
+ return /^ {0,3}\[[^\]\n]+\]:/m.test(src) || /\[[^\]\n]+\]\s*\[[^\]\n]*\]/.test(src) || /(^|[^!])\[[^\]\n]+\](?![([])/m.test(src);
526
+ }
527
+ function isClosedFence(src, map, markup) {
528
+ if (!map) return false;
529
+ const line = src.split(/\r\n|\r|\n/)[map[1] - 1]?.trim() ?? "";
530
+ if (line.length < markup.length || line[0] !== markup[0]) return false;
531
+ return line.split("").every((char) => char === markup[0]);
532
+ }
533
+ function normalizeLineEndings(src) {
534
+ return src.replace(/\r\n|\r/g, "\n");
535
+ }
403
536
 
404
537
  //#endregion
405
538
  //#region src/diff.ts
406
539
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
407
540
  function diffAst(prev, next) {
408
541
  const patches = [];
409
- const prevByKey = new Map(prev.map((n) => [n.key, n]));
410
542
  const nextKeys = new Set(next.map((n) => n.key));
411
- next.forEach((node, index) => {
412
- const old = prevByKey.get(node.key);
413
- if (!old) patches.push({
414
- op: "insert",
415
- index,
416
- node
417
- });
418
- else if (!nodeEqual(old, node)) patches.push({
419
- op: "update",
420
- key: node.key,
421
- node
543
+ const working = [...prev];
544
+ for (let index = working.length - 1; index >= 0; index--) if (!nextKeys.has(working[index].key)) {
545
+ patches.push({
546
+ op: "remove",
547
+ key: working[index].key
422
548
  });
423
- });
424
- for (const node of prev) if (!nextKeys.has(node.key)) patches.push({
425
- op: "remove",
426
- key: node.key
427
- });
549
+ working.splice(index, 1);
550
+ }
551
+ for (let index = 0; index < next.length; index++) {
552
+ const node = next[index];
553
+ const currentIndex = working.findIndex((candidate) => candidate.key === node.key);
554
+ if (currentIndex === -1) {
555
+ patches.push({
556
+ op: "insert",
557
+ index,
558
+ node
559
+ });
560
+ working.splice(index, 0, node);
561
+ continue;
562
+ }
563
+ if (currentIndex !== index) {
564
+ patches.push({
565
+ op: "move",
566
+ key: node.key,
567
+ index
568
+ });
569
+ const [moved] = working.splice(currentIndex, 1);
570
+ working.splice(index, 0, moved);
571
+ }
572
+ if (!nodeEqual(working[index], node)) {
573
+ patches.push({
574
+ op: "update",
575
+ key: node.key,
576
+ node
577
+ });
578
+ working[index] = node;
579
+ }
580
+ }
428
581
  return patches;
429
582
  }
583
+ /** Apply patches in order, primarily for framework adapters and verification. */
584
+ function applyPatches(nodes, patches) {
585
+ const next = [...nodes];
586
+ for (const patch of patches) if (patch.op === "insert") next.splice(patch.index, 0, patch.node);
587
+ else if (patch.op === "remove") {
588
+ const index = next.findIndex((node) => node.key === patch.key);
589
+ if (index !== -1) next.splice(index, 1);
590
+ } else if (patch.op === "move") {
591
+ const index = next.findIndex((node) => node.key === patch.key);
592
+ if (index !== -1) {
593
+ const [node] = next.splice(index, 1);
594
+ next.splice(patch.index, 0, node);
595
+ }
596
+ } else {
597
+ const index = next.findIndex((node) => node.key === patch.key);
598
+ if (index !== -1) next[index] = patch.node;
599
+ }
600
+ return next;
601
+ }
430
602
  function nodeEqual(a, b) {
603
+ if (a === b) return true;
431
604
  return JSON.stringify(a) === JSON.stringify(b);
432
605
  }
433
606
 
@@ -442,42 +615,170 @@ var Renderer = class {
442
615
  buffer = "";
443
616
  prevAst = [];
444
617
  parse;
618
+ parsed;
445
619
  options;
446
620
  sanitize;
621
+ generation = 0;
622
+ activeFeed;
623
+ renderScheduled = false;
624
+ scheduleGeneration = 0;
447
625
  constructor(options = {}) {
448
626
  this.options = options;
449
- this.sanitize = options.sanitize !== false;
627
+ this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
450
628
  if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
451
- this.parse = createParser({
629
+ this.parse = createParserWithMetadata({
452
630
  registry: options.registry,
453
631
  plugins: options.plugins
454
632
  });
455
633
  }
456
634
  push(chunk) {
457
635
  this.buffer += chunk;
458
- this.render();
636
+ this.scheduleRender();
459
637
  }
460
- async feed(source) {
461
- if (Symbol.asyncIterator in source) {
462
- for await (const chunk of source) this.push(chunk);
463
- return;
464
- }
465
- const reader = source.getReader();
466
- for (;;) {
467
- const { done, value } = await reader.read();
468
- if (done) break;
469
- if (value != null) this.push(value);
638
+ async feed(source, options = {}) {
639
+ const generation = ++this.generation;
640
+ this.cancelActiveFeed(createAbortError("Superseded by a newer feed"));
641
+ const decoder = new TextDecoder();
642
+ const signal = options.signal;
643
+ if (signal?.aborted) throw abortReason(signal);
644
+ let aborted = false;
645
+ let abortError;
646
+ const abort = () => {
647
+ aborted = true;
648
+ abortError = abortReason(signal);
649
+ if (this.activeFeed?.generation === generation) this.cancelActiveFeed(abortError);
650
+ };
651
+ signal?.addEventListener("abort", abort, { once: true });
652
+ const consume = (chunk) => {
653
+ if (generation !== this.generation || aborted) return;
654
+ const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
655
+ if (text) this.push(text);
656
+ };
657
+ try {
658
+ if (isReadableStream$1(source)) {
659
+ const reader = source.getReader();
660
+ let cancelled = false;
661
+ this.activeFeed = {
662
+ generation,
663
+ cancel: async (reason) => {
664
+ if (cancelled) return;
665
+ cancelled = true;
666
+ await reader.cancel(reason);
667
+ }
668
+ };
669
+ try {
670
+ for (;;) {
671
+ const { done, value } = await reader.read();
672
+ if (done || generation !== this.generation || aborted) break;
673
+ if (value != null) consume(value);
674
+ }
675
+ } finally {
676
+ reader.releaseLock();
677
+ }
678
+ } else {
679
+ const iterator = source[Symbol.asyncIterator]();
680
+ let returned = false;
681
+ this.activeFeed = {
682
+ generation,
683
+ cancel: async () => {
684
+ if (returned) return;
685
+ returned = true;
686
+ await iterator.return?.();
687
+ }
688
+ };
689
+ try {
690
+ for (;;) {
691
+ const { done, value } = await iterator.next();
692
+ if (done || generation !== this.generation || aborted) break;
693
+ consume(value);
694
+ }
695
+ } finally {
696
+ if ((generation !== this.generation || aborted) && !returned) {
697
+ returned = true;
698
+ await iterator.return?.();
699
+ }
700
+ }
701
+ }
702
+ if (generation === this.generation && !aborted) {
703
+ const tail = decoder.decode();
704
+ if (tail) this.push(tail);
705
+ this.flush();
706
+ }
707
+ if (aborted) throw abortError;
708
+ } finally {
709
+ signal?.removeEventListener("abort", abort);
710
+ if (this.activeFeed?.generation === generation) this.activeFeed = void 0;
470
711
  }
471
712
  }
472
713
  reset() {
714
+ this.generation++;
715
+ this.cancelActiveFeed(createAbortError("Renderer reset"));
716
+ this.activeFeed = void 0;
473
717
  this.buffer = "";
718
+ this.renderScheduled = false;
719
+ this.scheduleGeneration++;
720
+ const patches = diffAst(this.prevAst, []);
474
721
  this.prevAst = [];
722
+ this.parsed = void 0;
723
+ this.options.onPatch?.(patches, []);
724
+ }
725
+ /** Immediately render pending buffered input, bypassing the scheduler. */
726
+ flush() {
727
+ if (!this.renderScheduled) return;
728
+ this.renderScheduled = false;
729
+ this.scheduleGeneration++;
730
+ this.render();
731
+ }
732
+ scheduleRender() {
733
+ if (!this.options.scheduler) {
734
+ this.render();
735
+ return;
736
+ }
737
+ if (this.renderScheduled) return;
738
+ this.renderScheduled = true;
739
+ const scheduledGeneration = ++this.scheduleGeneration;
740
+ this.options.scheduler(() => {
741
+ if (!this.renderScheduled || scheduledGeneration !== this.scheduleGeneration) return;
742
+ this.renderScheduled = false;
743
+ this.render();
744
+ });
745
+ }
746
+ cancelActiveFeed(reason) {
747
+ const active = this.activeFeed;
748
+ if (!active) return;
749
+ Promise.resolve(active.cancel(reason)).catch(() => {});
475
750
  }
476
751
  render() {
477
- const repaired = repairMarkdown(this.buffer);
478
- const nextAst = this.parse(repaired);
479
- if (this.sanitize) sanitizeNodes(nextAst);
752
+ const previous = this.parsed;
753
+ let next;
754
+ if (previous?.incrementalSafe && previous.blocks.length > 1) {
755
+ const mutable = previous.blocks.at(-1);
756
+ const stableBlocks = previous.blocks.slice(0, -1);
757
+ const stableNodeEnd = mutable.nodeStart;
758
+ const rawTail = this.buffer.slice(mutable.start);
759
+ const tail = this.parse(repairMarkdown(rawTail), rawTail, mutable.start);
760
+ if (tail.incrementalSafe) {
761
+ if (this.sanitize !== false) sanitizeNodes(tail.nodes, this.sanitize);
762
+ next = {
763
+ nodes: [...previous.nodes.slice(0, stableNodeEnd), ...tail.nodes],
764
+ blocks: [...stableBlocks, ...tail.blocks.map((block) => ({
765
+ ...block,
766
+ nodeStart: block.nodeStart + stableNodeEnd,
767
+ nodeEnd: block.nodeEnd + stableNodeEnd
768
+ }))],
769
+ incrementalSafe: true
770
+ };
771
+ } else {
772
+ next = this.parse(repairMarkdown(this.buffer), this.buffer);
773
+ if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
774
+ }
775
+ } else {
776
+ next = this.parse(repairMarkdown(this.buffer), this.buffer);
777
+ if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
778
+ }
779
+ const nextAst = next.nodes;
480
780
  const patches = diffAst(this.prevAst, nextAst);
781
+ this.parsed = next;
481
782
  this.prevAst = nextAst;
482
783
  if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
483
784
  }
@@ -486,119 +787,132 @@ var Renderer = class {
486
787
  * Recursively sanitize node markup in place: the content of `html` nodes and
487
788
  * the rendered inline `html` field carried by any node.
488
789
  */
489
- function sanitizeNodes(nodes) {
790
+ function sanitizeNodes(nodes, options) {
490
791
  for (const node of nodes) {
491
- if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content);
492
- if (node.html) node.html = sanitizeHtml(node.html);
493
- if (node.children) sanitizeNodes(node.children);
792
+ if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content, options);
793
+ if (node.html) node.html = sanitizeHtml(node.html, options);
794
+ if (node.children) sanitizeNodes(node.children, options);
494
795
  }
495
796
  }
797
+ function isReadableStream$1(source) {
798
+ return "getReader" in source && typeof source.getReader === "function";
799
+ }
800
+ function abortReason(signal) {
801
+ return signal?.reason ?? createAbortError("The operation was aborted");
802
+ }
803
+ function createAbortError(message) {
804
+ const error = new Error(message);
805
+ error.name = "AbortError";
806
+ return error;
807
+ }
496
808
 
497
809
  //#endregion
498
810
  //#region src/stream-router.ts
499
811
  const DEFAULT_CHANNEL = "content";
500
- /**
501
- * Demultiplexes one incoming stream into multiple named channels so different
502
- * UI regions can update independently (e.g. `progress` + `content` from a single
503
- * SSE). Framing is auto-detected per line and supports:
504
- *
505
- * 1. JSON envelope: `{"ch":"<channel>","delta":"text"}` (a text delta) or
506
- * `{"ch":"<channel>","data":<any>}` (a structured value). May appear bare or
507
- * after a `data: ` prefix.
508
- * 2. SSE `event: <name>` sets the channel for the following `data:` line(s).
509
- * 3. A plain `data: <text>` line with no `ch` and no preceding `event:` is a
510
- * text delta on the default `content` channel.
511
- */
812
+ /** Demultiplex JSON-lines and standards-compliant SSE into named channels. */
512
813
  var StreamRouter = class {
513
814
  sinks = new Map();
514
815
  handlers = new Map();
515
- /** Bind a text-stream sink to a channel: text deltas call `sink.push(delta)`. */
816
+ /** Most recently received SSE `id` field. */
817
+ lastEventId = "";
818
+ /** Most recently received valid non-negative SSE reconnection delay. */
819
+ retry;
516
820
  channel(name, sink) {
517
821
  this.sinks.set(name, sink);
518
822
  return this;
519
823
  }
520
- /** Bind a handler to a channel: data values (and deltas as strings) call it. */
521
824
  on(name, handler) {
522
825
  this.handlers.set(name, handler);
523
826
  return this;
524
827
  }
525
- /**
526
- * Consume a stream to completion, dispatching each parsed line to its channel.
527
- * Accepts an async iterable of strings, or a `ReadableStream` of either strings
528
- * or `Uint8Array` bytes (decoded as UTF-8).
529
- */
530
828
  async feed(source) {
531
829
  let buffer = "";
532
- let currentEvent;
830
+ let eventName = "";
831
+ let dataLines = [];
832
+ const decoder = new TextDecoder();
833
+ const dispatchEvent = () => {
834
+ if (dataLines.length === 0) {
835
+ eventName = "";
836
+ return;
837
+ }
838
+ const payload = dataLines.join("\n");
839
+ const channel = eventName || DEFAULT_CHANNEL;
840
+ dataLines = [];
841
+ eventName = "";
842
+ this.dispatchPayload(channel, payload, channel === DEFAULT_CHANNEL);
843
+ };
844
+ const processLine = (rawLine) => {
845
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
846
+ if (line === "") {
847
+ dispatchEvent();
848
+ return;
849
+ }
850
+ if (line.startsWith(":")) return;
851
+ const bareJson = tryParseJson(line);
852
+ if (isEnvelope(bareJson)) {
853
+ this.dispatchEnvelope(bareJson);
854
+ return;
855
+ }
856
+ const colon = line.indexOf(":");
857
+ const field = colon === -1 ? line : line.slice(0, colon);
858
+ let value = colon === -1 ? "" : line.slice(colon + 1);
859
+ if (value.startsWith(" ")) value = value.slice(1);
860
+ if (field === "event") eventName = value;
861
+ else if (field === "data") {
862
+ const envelope = tryParseJson(value);
863
+ if (!eventName && isEnvelope(envelope)) this.dispatchEnvelope(envelope);
864
+ else dataLines.push(value);
865
+ } else if (field === "id") {
866
+ if (!value.includes("\0")) this.lastEventId = value;
867
+ } else if (field === "retry") {
868
+ if (/^\d+$/.test(value)) this.retry = Number(value);
869
+ } else if (colon === -1) dataLines.push(line);
870
+ };
533
871
  const consume = (chunk) => {
534
- buffer += chunk;
535
- let index;
536
- while ((index = buffer.indexOf("\n")) !== -1) {
537
- const line = buffer.slice(0, index);
538
- buffer = buffer.slice(index + 1);
539
- currentEvent = this.processLine(line, currentEvent);
872
+ const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
873
+ buffer += text;
874
+ let newline;
875
+ while ((newline = buffer.indexOf("\n")) !== -1) {
876
+ processLine(buffer.slice(0, newline));
877
+ buffer = buffer.slice(newline + 1);
540
878
  }
541
879
  };
542
- if ("getReader" in source && typeof source.getReader === "function") {
880
+ if (isReadableStream(source)) {
543
881
  const reader = source.getReader();
544
- const decoder = new TextDecoder();
545
- for (;;) {
546
- const { done, value } = await reader.read();
547
- if (done) break;
548
- if (value == null) continue;
549
- consume(typeof value === "string" ? value : decoder.decode(value, { stream: true }));
882
+ try {
883
+ for (;;) {
884
+ const { done, value } = await reader.read();
885
+ if (done) break;
886
+ if (value != null) consume(value);
887
+ }
888
+ } finally {
889
+ reader.releaseLock();
550
890
  }
551
- const tail = decoder.decode();
552
- if (tail) consume(tail);
553
891
  } else for await (const chunk of source) consume(chunk);
554
- if (buffer.length > 0) this.processLine(buffer, currentEvent);
555
- }
556
- /** Process a single raw line; returns the (possibly updated) current event. */
557
- processLine(rawLine, currentEvent) {
558
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
559
- if (line.trim() === "") return void 0;
560
- if (line.startsWith("event:")) return line.slice(6).trim();
561
- let payload = line;
562
- if (payload.startsWith("data:")) {
563
- payload = payload.slice(5);
564
- if (payload.startsWith(" ")) payload = payload.slice(1);
565
- }
566
- if (payload.startsWith("{")) {
567
- const parsed = tryParseJson(payload);
568
- if (parsed !== void 0 && isRecord(parsed) && typeof parsed.ch === "string") {
569
- if ("delta" in parsed) this.routeDelta(parsed.ch, String(parsed.delta));
570
- else this.routeData(parsed.ch, parsed.data);
571
- return currentEvent;
572
- }
573
- if (parsed !== void 0) {
574
- this.routeData(currentEvent ?? DEFAULT_CHANNEL, parsed);
575
- return currentEvent;
576
- }
577
- }
578
- if (currentEvent !== void 0) {
579
- const parsed = tryParseJson(payload);
580
- this.routeData(currentEvent, parsed !== void 0 ? parsed : payload);
581
- return currentEvent;
582
- }
583
- this.routeDelta(DEFAULT_CHANNEL, payload);
584
- return currentEvent;
892
+ const tail = decoder.decode();
893
+ if (tail) consume(tail);
894
+ if (buffer.length > 0) processLine(buffer);
895
+ dispatchEvent();
896
+ }
897
+ dispatchPayload(channel, payload, defaultIsDelta) {
898
+ const parsed = tryParseJson(payload);
899
+ if (isEnvelope(parsed)) this.dispatchEnvelope(parsed);
900
+ else if (defaultIsDelta) this.routeDelta(channel, payload);
901
+ else this.routeData(channel, parsed !== void 0 ? parsed : payload);
902
+ }
903
+ dispatchEnvelope(envelope) {
904
+ const channel = envelope.ch;
905
+ if ("delta" in envelope) this.routeDelta(channel, String(envelope.delta));
906
+ else this.routeData(channel, envelope.data);
585
907
  }
586
- /** Route a text delta: to a bound sink and/or an on() handler (either/both). */
587
908
  routeDelta(channel, text) {
588
- const sink = this.sinks.get(channel);
589
- const handler = this.handlers.get(channel);
590
- if (sink) sink.push(text);
591
- if (handler) handler(text);
909
+ this.sinks.get(channel)?.push(text);
910
+ this.handlers.get(channel)?.(text);
592
911
  }
593
- /** Route a structured value: to the handler, or a string value to a lone sink. */
594
912
  routeData(channel, value) {
595
913
  const handler = this.handlers.get(channel);
596
- if (handler) {
597
- handler(value);
598
- return;
599
- }
600
- const sink = this.sinks.get(channel);
601
- if (sink && typeof value === "string") sink.push(value);
914
+ if (handler) handler(value);
915
+ else if (typeof value === "string") this.sinks.get(channel)?.push(value);
602
916
  }
603
917
  };
604
918
  function tryParseJson(text) {
@@ -608,8 +922,11 @@ function tryParseJson(text) {
608
922
  return void 0;
609
923
  }
610
924
  }
611
- function isRecord(value) {
612
- return typeof value === "object" && value !== null;
925
+ function isEnvelope(value) {
926
+ return typeof value === "object" && value !== null && typeof value.ch === "string";
927
+ }
928
+ function isReadableStream(source) {
929
+ return "getReader" in source && typeof source.getReader === "function";
613
930
  }
614
931
 
615
932
  //#endregion
@@ -623,21 +940,20 @@ function buildSystemPrompt(options = {}) {
623
940
  const parts = [];
624
941
  if (options.base) parts.push(options.base);
625
942
  const cardSpec = options.registry?.toPromptSpec();
626
- if (options.registry && hasCards(options.registry) && cardSpec) parts.push(cardSpec);
943
+ if (cardSpec) parts.push(cardSpec);
627
944
  for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
628
945
  return parts.join("\n\n");
629
946
  }
630
- function hasCards(registry) {
631
- return registry.toPromptSpec().includes("card:");
632
- }
633
947
 
634
948
  //#endregion
635
949
  exports.CardRegistry = CardRegistry
636
950
  exports.Renderer = Renderer
637
951
  exports.StreamRouter = StreamRouter
952
+ exports.applyPatches = applyPatches
638
953
  exports.buildSystemPrompt = buildSystemPrompt
639
954
  exports.collectNodeRenderers = collectNodeRenderers
640
955
  exports.createParser = createParser
956
+ exports.createParserWithMetadata = createParserWithMetadata
641
957
  exports.diffAst = diffAst
642
958
  exports.parsePartialJSON = parsePartialJSON
643
959
  exports.pluginNodeTypes = pluginNodeTypes