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