@ai-gui/core 0.21.1 → 0.22.1

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/README.md CHANGED
@@ -110,12 +110,16 @@ const runtime = createActionRuntime({ registry: actions, cardStore })
110
110
 
111
111
  ## Exports
112
112
 
113
- - `Renderer` — `push(chunk)`, `feed(AsyncIterable | ReadableStream)`, `reset()`; constructor `{ registry?, plugins?, sanitize?, onPatch?(patches, nodes) }`.
113
+ - `Renderer` — `push(chunk)`, `feed(AsyncIterable | ReadableStream)`, `reset()`, `setPlugins(plugins)`; constructor `{ registry?, plugins?, sanitize?, rawHtml?, onPatch?(patches, nodes) }`.
114
+ - `setPlugins` swaps the grammar and reparses the buffered source, so plugins deferred behind a dynamic import can arrive mid-answer without the host replaying what it pushed.
115
+ - `rawHtml: false` escapes raw HTML the model wrote instead of interpreting it — a stray `<code>` in prose otherwise swallows the rest of the line.
116
+ - Emphasis is parsed CJK-friendly, one deliberate deviation from CommonMark. CommonMark will not let `**` close when it follows punctuation and precedes a character that is neither whitespace nor punctuation, so `**严格单调(单射)**的函数` renders its asterisks literally. ASCII is unaffected: `a * b * c` and `snake_case_word` parse exactly as before.
114
117
  - `StreamRouter` — demultiplex one stream into named channels: `.channel(name, sink)`, `.on(name, cb)`, `.feed(source)`.
115
118
  - `CardRegistry` — `register(def)`, `parse(type, rawJson)`, `getRender(type)`, `toPromptSpec()`, `toJSONSchema()`.
116
119
  - `CardStore` — `register`, `get`, `list`, `subscribe`, `apply`, `applyAll`, `delete`, `clear`, `snapshot`, and `restore` for Cards with stable IDs.
117
120
  - `ActionRegistry`, `ActionRuntime`, `createActionRuntime`, `getActionKey`, `getIdleActionState` — validated application-owned action execution and observable lifecycle state.
118
- - `buildSystemPrompt({ base?, registry?, plugins? })`.
121
+ - `buildSystemPrompt({ base?, registry?, plugins?, locale? })` — collects the card specs and every plugin's `promptSpec`. Pass `plugins`: installing a plugin teaches the renderer to draw a block, not the model to ask for one.
122
+ - `loadPlugins(source)` / `samePlugins(a, b)` — normalize `AIGuiPlugin[] | (() => Promise<AIGuiPlugin[]>)` and compare two lists by members.
119
123
  - Utilities: `parsePartialJSON`, `repairMarkdown`, `sanitizeHtml`, `createParser`, `diffAst`, `collectNodeRenderers`.
120
124
  - Types: `ASTNode`, `Patch`, `RenderOutput` (`html | element | card | mount`), `CardDef`, `AIGuiPlugin`, `NodeRenderer`, `RendererOptions`, `JSONSchema`.
121
125
 
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  //#endregion
25
25
  const dompurify = __toESM(require("dompurify"));
26
26
  const markdown_it = __toESM(require("markdown-it"));
27
+ const markdown_it_cjk_friendly = __toESM(require("markdown-it-cjk-friendly"));
27
28
 
28
29
  //#region src/partial-json.ts
29
30
  /**
@@ -1443,6 +1444,28 @@ function settleCardAction(settle) {
1443
1444
 
1444
1445
  //#endregion
1445
1446
  //#region src/plugins.ts
1447
+ /**
1448
+ * Resolve a plugin source to what the caller can act on now: the array itself, or a promise of it.
1449
+ *
1450
+ * The array form must stay synchronous. Deferring it by a microtask would render the first chunk
1451
+ * of every answer under the plain-markdown grammar and then redraw it, which is a visible flash
1452
+ * for a host that had its plugins all along.
1453
+ */
1454
+ function loadPlugins(source) {
1455
+ if (!source) return [];
1456
+ return Array.isArray(source) ? source : source();
1457
+ }
1458
+ /**
1459
+ * Whether two lists hold the same plugins in the same order.
1460
+ *
1461
+ * `plugins={[chart, katex]}` is a new array on every render and the same two plugins every time.
1462
+ * What matters is the members, not the array.
1463
+ */
1464
+ function samePlugins(a, b) {
1465
+ if (a === b) return true;
1466
+ if (!a || !b || a.length !== b.length) return false;
1467
+ return a.every((plugin, index) => plugin === b[index]);
1468
+ }
1446
1469
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
1447
1470
  function collectNodeRenderers(plugins = [], debugOptions = {}) {
1448
1471
  const map = {};
@@ -1529,9 +1552,10 @@ function createParser(options = {}) {
1529
1552
  /** Build a parser that also reports the source range for each top-level block. */
1530
1553
  function createParserWithMetadata(options = {}) {
1531
1554
  const md = new markdown_it.default({
1532
- html: true,
1555
+ html: options.rawHtml !== false,
1533
1556
  linkify: true
1534
1557
  });
1558
+ md.use(markdown_it_cjk_friendly.default);
1535
1559
  options.configureMd?.(md);
1536
1560
  for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
1537
1561
  const hasParserExtensions = Boolean(options.configureMd || options.plugins?.some((plugin) => plugin.extendParser));
@@ -2047,12 +2071,51 @@ var Renderer = class {
2047
2071
  this.options = options;
2048
2072
  this.debug = new DebugEmitter(this.debugSource, options);
2049
2073
  this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
2050
- if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
2074
+ this.registerPluginCards(options.plugins);
2051
2075
  this.parse = createParserWithMetadata({
2052
2076
  registry: options.registry,
2053
- plugins: options.plugins
2077
+ plugins: options.plugins,
2078
+ rawHtml: options.rawHtml
2054
2079
  });
2055
2080
  }
2081
+ /** The plugins currently parsing and rendering this renderer's output. */
2082
+ get plugins() {
2083
+ return this.options.plugins ?? [];
2084
+ }
2085
+ /**
2086
+ * Swap the plugins in and redraw the answer already buffered.
2087
+ *
2088
+ * A plugin bundle is worth deferring — diagrams, maths and charts together outweigh everything
2089
+ * else a page loads — but the stream does not wait for the import, so whatever arrived meanwhile
2090
+ * was parsed under the plain-markdown grammar. Reparsing the buffer this renderer still holds is
2091
+ * what turns that text into diagrams, which is why a host does not have to remember what it
2092
+ * pushed and replay it once the chunk lands.
2093
+ *
2094
+ * Passing the same plugins again is a no-op, so a host may call this on every render.
2095
+ */
2096
+ setPlugins(plugins) {
2097
+ if (samePlugins(this.options.plugins, plugins)) return;
2098
+ this.options = {
2099
+ ...this.options,
2100
+ plugins
2101
+ };
2102
+ this.registerPluginCards(plugins);
2103
+ this.parse = createParserWithMetadata({
2104
+ registry: this.options.registry,
2105
+ plugins,
2106
+ rawHtml: this.options.rawHtml
2107
+ });
2108
+ this.parsed = void 0;
2109
+ this.renderScheduled = false;
2110
+ this.scheduleGeneration++;
2111
+ if (this.debug.active) this.debug.emit("plugins-changed", { plugins: (plugins ?? []).map((plugin) => plugin.name) });
2112
+ this.render();
2113
+ }
2114
+ registerPluginCards(plugins) {
2115
+ const registry = this.options.registry;
2116
+ if (!registry) return;
2117
+ for (const plugin of plugins ?? []) for (const card of plugin.cards ?? []) registry.register(card);
2118
+ }
2056
2119
  get debugEnabled() {
2057
2120
  return this.debug.available;
2058
2121
  }
@@ -2844,6 +2907,7 @@ exports.getIdleActionState = getIdleActionState
2844
2907
  exports.injectPluginStyles = injectPluginStyles
2845
2908
  exports.isCardPatchResult = isCardPatchResult
2846
2909
  exports.jsonLines = jsonLines
2910
+ exports.loadPlugins = loadPlugins
2847
2911
  exports.mockModelStream = mockModelStream
2848
2912
  exports.ndjson = ndjson
2849
2913
  exports.parsePartialJSON = parsePartialJSON
@@ -2853,6 +2917,7 @@ exports.readableBytes = readableBytes
2853
2917
  exports.repairMarkdown = repairMarkdown
2854
2918
  exports.resolveMessages = resolveMessages
2855
2919
  exports.safeDebugValue = safeDebugValue
2920
+ exports.samePlugins = samePlugins
2856
2921
  exports.sanitizeHtml = sanitizeHtml
2857
2922
  exports.sanitizeRenderedHtml = sanitizeRenderedHtml
2858
2923
  exports.textLines = textLines
package/dist/index.d.cts CHANGED
@@ -265,6 +265,17 @@ interface RendererOptions extends DebugOptions {
265
265
  registry?: CardRegistry;
266
266
  plugins?: AIGuiPlugin[];
267
267
  sanitize?: boolean | SanitizeHtmlOptions;
268
+ /**
269
+ * Whether raw HTML in the model's output is interpreted as markup. On by default.
270
+ *
271
+ * A tag a model wrote inside prose is usually text it is describing, not markup it means: one
272
+ * stray `<code>` in a sentence about code swallows the rest of the line into an element. Turning
273
+ * this off escapes every tag the model writes and shows the characters instead, which is what a
274
+ * product wants when the model is meant to produce markdown and nothing else. It is not a
275
+ * substitute for `sanitize` — a plugin's own markup and the host's cards are unaffected either
276
+ * way.
277
+ */
278
+ rawHtml?: boolean;
268
279
  /** Coalesce multiple pushes by scheduling one render callback. */
269
280
  scheduler?: (render: () => void) => void;
270
281
  onPatch?: (patches: Patch[], nodes: ASTNode[]) => void;
@@ -614,6 +625,17 @@ interface ParserOptions {
614
625
  registry?: CardRegistry;
615
626
  plugins?: AIGuiPlugin[];
616
627
  configureMd?: (md: MarkdownIt) => void;
628
+ /**
629
+ * Whether raw HTML in the model's output is interpreted as markup. On by default.
630
+ *
631
+ * A model writing *about* code emits tags in prose it never meant as markup — a line like
632
+ * `return "done\n<code>"` is text, but interpreting it swallows everything after the tag into an
633
+ * element and the rest of the sentence lands outside it. Sanitizing does not help: `<code>` is a
634
+ * tag any allowlist keeps. Turn this off and every tag the model writes is escaped and shown as
635
+ * the characters it wrote, so a model that gets HTML wrong produces an ugly line rather than a
636
+ * mangled answer.
637
+ */
638
+ rawHtml?: boolean;
617
639
  }
618
640
  interface SourceBlock {
619
641
  start: number;
@@ -636,6 +658,32 @@ declare function createParserWithMetadata(options?: ParserOptions): (src: string
636
658
  interface CollectNodeRendererOptions extends DebugOptions {
637
659
  debugTarget?: DebugInstrumentationTarget;
638
660
  }
661
+ /** A function that produces the plugins, loading them first if they are not in the bundle yet. */
662
+ type PluginsLoader = () => AIGuiPlugin[] | Promise<AIGuiPlugin[]>;
663
+ /**
664
+ * Either the plugins themselves or a function that loads them.
665
+ *
666
+ * Diagrams, maths and charts are the heaviest thing a page carrying them loads, and an answer that
667
+ * never draws one should not pay for them. A loader lets the host defer the import: the renderer
668
+ * shows plain markdown until it resolves and reparses what has arrived by then, so the host does
669
+ * not have to hold the stream or replay it.
670
+ */
671
+ type PluginSource = AIGuiPlugin[] | PluginsLoader;
672
+ /**
673
+ * Resolve a plugin source to what the caller can act on now: the array itself, or a promise of it.
674
+ *
675
+ * The array form must stay synchronous. Deferring it by a microtask would render the first chunk
676
+ * of every answer under the plain-markdown grammar and then redraw it, which is a visible flash
677
+ * for a host that had its plugins all along.
678
+ */
679
+ declare function loadPlugins(source?: PluginSource): AIGuiPlugin[] | Promise<AIGuiPlugin[]>;
680
+ /**
681
+ * Whether two lists hold the same plugins in the same order.
682
+ *
683
+ * `plugins={[chart, katex]}` is a new array on every render and the same two plugins every time.
684
+ * What matters is the members, not the array.
685
+ */
686
+ declare function samePlugins(a?: AIGuiPlugin[], b?: AIGuiPlugin[]): boolean;
639
687
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
640
688
  declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: CollectNodeRendererOptions): Record<string, NodeRenderer>;
641
689
  /** The set of node types claimed by the given plugins. */
@@ -798,6 +846,21 @@ declare class Renderer {
798
846
  private scheduleGeneration;
799
847
  private readonly debug;
800
848
  constructor(options?: RendererOptions);
849
+ /** The plugins currently parsing and rendering this renderer's output. */
850
+ get plugins(): readonly AIGuiPlugin[];
851
+ /**
852
+ * Swap the plugins in and redraw the answer already buffered.
853
+ *
854
+ * A plugin bundle is worth deferring — diagrams, maths and charts together outweigh everything
855
+ * else a page loads — but the stream does not wait for the import, so whatever arrived meanwhile
856
+ * was parsed under the plain-markdown grammar. Reparsing the buffer this renderer still holds is
857
+ * what turns that text into diagrams, which is why a host does not have to remember what it
858
+ * pushed and replay it once the chunk lands.
859
+ *
860
+ * Passing the same plugins again is a no-op, so a host may call this on every render.
861
+ */
862
+ setPlugins(plugins: AIGuiPlugin[] | undefined): void;
863
+ private registerPluginCards;
801
864
  get debugEnabled(): boolean;
802
865
  emitDebug(type: string, data?: Record<string, unknown>): void;
803
866
  push(chunk: string): void;
@@ -922,4 +985,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
922
985
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
923
986
 
924
987
  //#endregion
925
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionOutcome, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DEFAULT_LOCALE, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, Locale, MessageBundle, Messages, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, PluginStyle, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
988
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionOutcome, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DEFAULT_LOCALE, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, Locale, MessageBundle, Messages, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, PluginSource, PluginStyle, PluginsLoader, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, loadPlugins, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, samePlugins, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
package/dist/index.d.ts CHANGED
@@ -265,6 +265,17 @@ interface RendererOptions extends DebugOptions {
265
265
  registry?: CardRegistry;
266
266
  plugins?: AIGuiPlugin[];
267
267
  sanitize?: boolean | SanitizeHtmlOptions;
268
+ /**
269
+ * Whether raw HTML in the model's output is interpreted as markup. On by default.
270
+ *
271
+ * A tag a model wrote inside prose is usually text it is describing, not markup it means: one
272
+ * stray `<code>` in a sentence about code swallows the rest of the line into an element. Turning
273
+ * this off escapes every tag the model writes and shows the characters instead, which is what a
274
+ * product wants when the model is meant to produce markdown and nothing else. It is not a
275
+ * substitute for `sanitize` — a plugin's own markup and the host's cards are unaffected either
276
+ * way.
277
+ */
278
+ rawHtml?: boolean;
268
279
  /** Coalesce multiple pushes by scheduling one render callback. */
269
280
  scheduler?: (render: () => void) => void;
270
281
  onPatch?: (patches: Patch[], nodes: ASTNode[]) => void;
@@ -614,6 +625,17 @@ interface ParserOptions {
614
625
  registry?: CardRegistry;
615
626
  plugins?: AIGuiPlugin[];
616
627
  configureMd?: (md: MarkdownIt) => void;
628
+ /**
629
+ * Whether raw HTML in the model's output is interpreted as markup. On by default.
630
+ *
631
+ * A model writing *about* code emits tags in prose it never meant as markup — a line like
632
+ * `return "done\n<code>"` is text, but interpreting it swallows everything after the tag into an
633
+ * element and the rest of the sentence lands outside it. Sanitizing does not help: `<code>` is a
634
+ * tag any allowlist keeps. Turn this off and every tag the model writes is escaped and shown as
635
+ * the characters it wrote, so a model that gets HTML wrong produces an ugly line rather than a
636
+ * mangled answer.
637
+ */
638
+ rawHtml?: boolean;
617
639
  }
618
640
  interface SourceBlock {
619
641
  start: number;
@@ -636,6 +658,32 @@ declare function createParserWithMetadata(options?: ParserOptions): (src: string
636
658
  interface CollectNodeRendererOptions extends DebugOptions {
637
659
  debugTarget?: DebugInstrumentationTarget;
638
660
  }
661
+ /** A function that produces the plugins, loading them first if they are not in the bundle yet. */
662
+ type PluginsLoader = () => AIGuiPlugin[] | Promise<AIGuiPlugin[]>;
663
+ /**
664
+ * Either the plugins themselves or a function that loads them.
665
+ *
666
+ * Diagrams, maths and charts are the heaviest thing a page carrying them loads, and an answer that
667
+ * never draws one should not pay for them. A loader lets the host defer the import: the renderer
668
+ * shows plain markdown until it resolves and reparses what has arrived by then, so the host does
669
+ * not have to hold the stream or replay it.
670
+ */
671
+ type PluginSource = AIGuiPlugin[] | PluginsLoader;
672
+ /**
673
+ * Resolve a plugin source to what the caller can act on now: the array itself, or a promise of it.
674
+ *
675
+ * The array form must stay synchronous. Deferring it by a microtask would render the first chunk
676
+ * of every answer under the plain-markdown grammar and then redraw it, which is a visible flash
677
+ * for a host that had its plugins all along.
678
+ */
679
+ declare function loadPlugins(source?: PluginSource): AIGuiPlugin[] | Promise<AIGuiPlugin[]>;
680
+ /**
681
+ * Whether two lists hold the same plugins in the same order.
682
+ *
683
+ * `plugins={[chart, katex]}` is a new array on every render and the same two plugins every time.
684
+ * What matters is the members, not the array.
685
+ */
686
+ declare function samePlugins(a?: AIGuiPlugin[], b?: AIGuiPlugin[]): boolean;
639
687
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
640
688
  declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: CollectNodeRendererOptions): Record<string, NodeRenderer>;
641
689
  /** The set of node types claimed by the given plugins. */
@@ -798,6 +846,21 @@ declare class Renderer {
798
846
  private scheduleGeneration;
799
847
  private readonly debug;
800
848
  constructor(options?: RendererOptions);
849
+ /** The plugins currently parsing and rendering this renderer's output. */
850
+ get plugins(): readonly AIGuiPlugin[];
851
+ /**
852
+ * Swap the plugins in and redraw the answer already buffered.
853
+ *
854
+ * A plugin bundle is worth deferring — diagrams, maths and charts together outweigh everything
855
+ * else a page loads — but the stream does not wait for the import, so whatever arrived meanwhile
856
+ * was parsed under the plain-markdown grammar. Reparsing the buffer this renderer still holds is
857
+ * what turns that text into diagrams, which is why a host does not have to remember what it
858
+ * pushed and replay it once the chunk lands.
859
+ *
860
+ * Passing the same plugins again is a no-op, so a host may call this on every render.
861
+ */
862
+ setPlugins(plugins: AIGuiPlugin[] | undefined): void;
863
+ private registerPluginCards;
801
864
  get debugEnabled(): boolean;
802
865
  emitDebug(type: string, data?: Record<string, unknown>): void;
803
866
  push(chunk: string): void;
@@ -922,4 +985,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
922
985
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
923
986
 
924
987
  //#endregion
925
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionOutcome, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DEFAULT_LOCALE, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, Locale, MessageBundle, Messages, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, PluginStyle, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
988
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionOutcome, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DEFAULT_LOCALE, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, Locale, MessageBundle, Messages, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, PluginSource, PluginStyle, PluginsLoader, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, loadPlugins, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, samePlugins, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import DOMPurify from "dompurify";
2
2
  import MarkdownIt from "markdown-it";
3
+ import cjkFriendly from "markdown-it-cjk-friendly";
3
4
 
4
5
  //#region src/partial-json.ts
5
6
  /**
@@ -1419,6 +1420,28 @@ function settleCardAction(settle) {
1419
1420
 
1420
1421
  //#endregion
1421
1422
  //#region src/plugins.ts
1423
+ /**
1424
+ * Resolve a plugin source to what the caller can act on now: the array itself, or a promise of it.
1425
+ *
1426
+ * The array form must stay synchronous. Deferring it by a microtask would render the first chunk
1427
+ * of every answer under the plain-markdown grammar and then redraw it, which is a visible flash
1428
+ * for a host that had its plugins all along.
1429
+ */
1430
+ function loadPlugins(source) {
1431
+ if (!source) return [];
1432
+ return Array.isArray(source) ? source : source();
1433
+ }
1434
+ /**
1435
+ * Whether two lists hold the same plugins in the same order.
1436
+ *
1437
+ * `plugins={[chart, katex]}` is a new array on every render and the same two plugins every time.
1438
+ * What matters is the members, not the array.
1439
+ */
1440
+ function samePlugins(a, b) {
1441
+ if (a === b) return true;
1442
+ if (!a || !b || a.length !== b.length) return false;
1443
+ return a.every((plugin, index) => plugin === b[index]);
1444
+ }
1422
1445
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
1423
1446
  function collectNodeRenderers(plugins = [], debugOptions = {}) {
1424
1447
  const map = {};
@@ -1505,9 +1528,10 @@ function createParser(options = {}) {
1505
1528
  /** Build a parser that also reports the source range for each top-level block. */
1506
1529
  function createParserWithMetadata(options = {}) {
1507
1530
  const md = new MarkdownIt({
1508
- html: true,
1531
+ html: options.rawHtml !== false,
1509
1532
  linkify: true
1510
1533
  });
1534
+ md.use(cjkFriendly);
1511
1535
  options.configureMd?.(md);
1512
1536
  for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
1513
1537
  const hasParserExtensions = Boolean(options.configureMd || options.plugins?.some((plugin) => plugin.extendParser));
@@ -2023,12 +2047,51 @@ var Renderer = class {
2023
2047
  this.options = options;
2024
2048
  this.debug = new DebugEmitter(this.debugSource, options);
2025
2049
  this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
2026
- if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
2050
+ this.registerPluginCards(options.plugins);
2027
2051
  this.parse = createParserWithMetadata({
2028
2052
  registry: options.registry,
2029
- plugins: options.plugins
2053
+ plugins: options.plugins,
2054
+ rawHtml: options.rawHtml
2030
2055
  });
2031
2056
  }
2057
+ /** The plugins currently parsing and rendering this renderer's output. */
2058
+ get plugins() {
2059
+ return this.options.plugins ?? [];
2060
+ }
2061
+ /**
2062
+ * Swap the plugins in and redraw the answer already buffered.
2063
+ *
2064
+ * A plugin bundle is worth deferring — diagrams, maths and charts together outweigh everything
2065
+ * else a page loads — but the stream does not wait for the import, so whatever arrived meanwhile
2066
+ * was parsed under the plain-markdown grammar. Reparsing the buffer this renderer still holds is
2067
+ * what turns that text into diagrams, which is why a host does not have to remember what it
2068
+ * pushed and replay it once the chunk lands.
2069
+ *
2070
+ * Passing the same plugins again is a no-op, so a host may call this on every render.
2071
+ */
2072
+ setPlugins(plugins) {
2073
+ if (samePlugins(this.options.plugins, plugins)) return;
2074
+ this.options = {
2075
+ ...this.options,
2076
+ plugins
2077
+ };
2078
+ this.registerPluginCards(plugins);
2079
+ this.parse = createParserWithMetadata({
2080
+ registry: this.options.registry,
2081
+ plugins,
2082
+ rawHtml: this.options.rawHtml
2083
+ });
2084
+ this.parsed = void 0;
2085
+ this.renderScheduled = false;
2086
+ this.scheduleGeneration++;
2087
+ if (this.debug.active) this.debug.emit("plugins-changed", { plugins: (plugins ?? []).map((plugin) => plugin.name) });
2088
+ this.render();
2089
+ }
2090
+ registerPluginCards(plugins) {
2091
+ const registry = this.options.registry;
2092
+ if (!registry) return;
2093
+ for (const plugin of plugins ?? []) for (const card of plugin.cards ?? []) registry.register(card);
2094
+ }
2032
2095
  get debugEnabled() {
2033
2096
  return this.debug.available;
2034
2097
  }
@@ -2772,4 +2835,4 @@ function delay(ms, signal) {
2772
2835
  }
2773
2836
 
2774
2837
  //#endregion
2775
- export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DEFAULT_LOCALE, DebugEmitter, Renderer, StreamRouter, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
2838
+ export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DEFAULT_LOCALE, DebugEmitter, Renderer, StreamRouter, actionOutcome, applyPatches, availableLocales, baseCss, buildSystemPrompt, collectNodeRenderers, collectPluginStyles, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, injectPluginStyles, isCardPatchResult, jsonLines, loadPlugins, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, resolveMessages, safeDebugValue, samePlugins, sanitizeHtml, sanitizeRenderedHtml, textLines, translate, translator, validateJSONSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/core",
3
- "version": "0.21.1",
3
+ "version": "0.22.1",
4
4
  "description": "Headless streaming renderer core for LLM-generated UI — markdown/JSON repair, card registry, AST diff, plugin engine, sanitizer.",
5
5
  "keywords": [
6
6
  "llm",
@@ -52,7 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "dompurify": "^3.4.12",
55
- "markdown-it": "^14.1.0"
55
+ "markdown-it": "^14.1.0",
56
+ "markdown-it-cjk-friendly": "^2.0.2"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/markdown-it": "^14.1.2",