@ai-gui/core 0.20.2 → 0.21.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
@@ -1721,6 +1721,115 @@ function normalizeLineEndings(src) {
1721
1721
  return src.replace(/\r\n|\r/g, "\n");
1722
1722
  }
1723
1723
 
1724
+ //#endregion
1725
+ //#region src/plugin-styles.ts
1726
+ /**
1727
+ * The stylesheet every renderer needs regardless of which plugins are loaded.
1728
+ *
1729
+ * Model output is written without knowing the viewport, so a wide table, a long code line or a
1730
+ * diagram sized for a desktop will otherwise push the page sideways on a phone. Each block is
1731
+ * made to scroll inside its own box instead of widening the column that holds it.
1732
+ */
1733
+ const baseCss = [
1734
+ "[data-aigui-renderer]{max-width:100%}",
1735
+ "[data-aigui-renderer] img,[data-aigui-renderer] svg,[data-aigui-renderer] video,[data-aigui-renderer] canvas{max-width:100%;height:auto}",
1736
+ "[data-aigui-renderer] pre{max-width:100%;overflow-x:auto}",
1737
+ "[data-aigui-renderer] code{overflow-wrap:anywhere}",
1738
+ "[data-aigui-renderer] pre code{overflow-wrap:normal}",
1739
+ "[data-aigui-renderer] table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse}",
1740
+ "[data-aigui-renderer] p,[data-aigui-renderer] li,[data-aigui-renderer] h1,[data-aigui-renderer] h2,[data-aigui-renderer] h3{overflow-wrap:break-word}",
1741
+ "[data-aigui-renderer] a{overflow-wrap:anywhere}",
1742
+ "[data-aigui-renderer] [data-aigui-chart],[data-aigui-renderer] [data-aigui-mermaid],[data-aigui-renderer] [data-aigui-map],[data-aigui-renderer] [data-aigui-molecule]{max-width:100%;overflow-x:auto}"
1743
+ ].join("");
1744
+ /**
1745
+ * Collect the stylesheets of the given plugins, base styles first.
1746
+ *
1747
+ * Plugins declare `css` but cannot inject it themselves — they never see the document. Each
1748
+ * plugin appears once even if it is passed twice, and later plugins of the same name win, which
1749
+ * matches how `collectNodeRenderers` resolves duplicates.
1750
+ */
1751
+ function collectPluginStyles(plugins) {
1752
+ const styles = new Map();
1753
+ styles.set("base", baseCss);
1754
+ for (const plugin of plugins ?? []) {
1755
+ const css = plugin?.css?.trim();
1756
+ if (!css) continue;
1757
+ if (css.startsWith("@import") && !/@import\s+url\(|@import\s+["'](?:https?:)?\/\//.test(css)) continue;
1758
+ styles.set(plugin.name, css);
1759
+ }
1760
+ return [...styles].map(([name, css]) => ({
1761
+ name,
1762
+ css
1763
+ }));
1764
+ }
1765
+ const STYLE_ATTR = "data-aigui-style";
1766
+ /**
1767
+ * Put the plugins' stylesheets in the document, once each.
1768
+ *
1769
+ * Called on every render by every renderer on the page, so it must be idempotent: a stylesheet
1770
+ * already present is left alone rather than duplicated. No-ops without a document, which is what
1771
+ * server-side rendering gets.
1772
+ */
1773
+ function injectPluginStyles(plugins, doc) {
1774
+ const target = doc ?? (typeof document === "undefined" ? void 0 : document);
1775
+ if (!target?.head) return;
1776
+ for (const { name, css } of collectPluginStyles(plugins)) {
1777
+ if (target.querySelector(`style[${STYLE_ATTR}="${CSS_ESCAPE(name)}"]`)) continue;
1778
+ const el = target.createElement("style");
1779
+ el.setAttribute(STYLE_ATTR, name);
1780
+ el.textContent = css;
1781
+ target.head.appendChild(el);
1782
+ }
1783
+ }
1784
+ /** Quote a plugin name for use inside an attribute selector. */
1785
+ function CSS_ESCAPE(name) {
1786
+ return name.replace(/["\\]/g, "\\$&");
1787
+ }
1788
+
1789
+ //#endregion
1790
+ //#region src/i18n.ts
1791
+ /** The default locale, used when a host does not say otherwise. */
1792
+ const DEFAULT_LOCALE = "en";
1793
+ /**
1794
+ * Pick the messages for a locale: exact match, then the base language, then English.
1795
+ *
1796
+ * "zh-CN" therefore finds a "zh-CN" bundle, falls back to "zh", and finally to English — a host
1797
+ * asking for a regional variant nobody translated still gets the language.
1798
+ */
1799
+ function resolveMessages(bundle, locale) {
1800
+ const en = bundle.en ?? {};
1801
+ if (!locale) return en;
1802
+ const exact = bundle[locale];
1803
+ if (exact) return {
1804
+ ...en,
1805
+ ...exact
1806
+ };
1807
+ const base = locale.split("-")[0];
1808
+ const language = base && base !== locale ? bundle[base] : void 0;
1809
+ return language ? {
1810
+ ...en,
1811
+ ...language
1812
+ } : en;
1813
+ }
1814
+ /** Look up one string, falling back to English and finally to the key itself. */
1815
+ function translate(bundle, locale, key) {
1816
+ return resolveMessages(bundle, locale)[key] ?? key;
1817
+ }
1818
+ /**
1819
+ * A lookup function bound to one bundle and locale.
1820
+ *
1821
+ * Plugins render many strings per node, so resolving the bundle once and closing over it keeps
1822
+ * the per-string cost to a map lookup.
1823
+ */
1824
+ function translator(bundle, locale) {
1825
+ const messages = resolveMessages(bundle, locale);
1826
+ return (key) => messages[key] ?? key;
1827
+ }
1828
+ /** The locales a bundle actually carries. */
1829
+ function availableLocales(bundle) {
1830
+ return Object.keys(bundle);
1831
+ }
1832
+
1724
1833
  //#endregion
1725
1834
  //#region src/export-image.ts
1726
1835
  /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
@@ -2396,7 +2505,7 @@ function buildSystemPrompt(options = {}) {
2396
2505
  const cardSpec = options.registry?.toPromptSpec();
2397
2506
  if (cardSpec) parts.push(cardSpec);
2398
2507
  for (const plugin of options.plugins ?? []) {
2399
- const spec = typeof plugin.promptSpec === "function" ? plugin.promptSpec() : plugin.promptSpec;
2508
+ const spec = typeof plugin.promptSpec === "function" ? plugin.promptSpec(options.locale) : plugin.promptSpec;
2400
2509
  if (spec) parts.push(spec);
2401
2510
  }
2402
2511
  return parts.join("\n\n");
@@ -2711,13 +2820,17 @@ exports.CardStore = CardStore
2711
2820
  exports.CardStoreError = CardStoreError
2712
2821
  exports.CardTypeConflictError = CardTypeConflictError
2713
2822
  exports.CardValidationError = CardValidationError
2823
+ exports.DEFAULT_LOCALE = DEFAULT_LOCALE
2714
2824
  exports.DebugEmitter = DebugEmitter
2715
2825
  exports.Renderer = Renderer
2716
2826
  exports.StreamRouter = StreamRouter
2717
2827
  exports.actionOutcome = actionOutcome
2718
2828
  exports.applyPatches = applyPatches
2829
+ exports.availableLocales = availableLocales
2830
+ exports.baseCss = baseCss
2719
2831
  exports.buildSystemPrompt = buildSystemPrompt
2720
2832
  exports.collectNodeRenderers = collectNodeRenderers
2833
+ exports.collectPluginStyles = collectPluginStyles
2721
2834
  exports.contentDeltas = contentDeltas
2722
2835
  exports.createActionRuntime = createActionRuntime
2723
2836
  exports.createParser = createParser
@@ -2728,6 +2841,7 @@ exports.exportRenderedImages = exportRenderedImages
2728
2841
  exports.exportSVGToImage = exportSVGToImage
2729
2842
  exports.getActionKey = getActionKey
2730
2843
  exports.getIdleActionState = getIdleActionState
2844
+ exports.injectPluginStyles = injectPluginStyles
2731
2845
  exports.isCardPatchResult = isCardPatchResult
2732
2846
  exports.jsonLines = jsonLines
2733
2847
  exports.mockModelStream = mockModelStream
@@ -2737,8 +2851,11 @@ exports.parseSSE = parseSSE
2737
2851
  exports.pluginNodeTypes = pluginNodeTypes
2738
2852
  exports.readableBytes = readableBytes
2739
2853
  exports.repairMarkdown = repairMarkdown
2854
+ exports.resolveMessages = resolveMessages
2740
2855
  exports.safeDebugValue = safeDebugValue
2741
2856
  exports.sanitizeHtml = sanitizeHtml
2742
2857
  exports.sanitizeRenderedHtml = sanitizeRenderedHtml
2743
2858
  exports.textLines = textLines
2859
+ exports.translate = translate
2860
+ exports.translator = translator
2744
2861
  exports.validateJSONSchema = validateJSONSchema
package/dist/index.d.cts CHANGED
@@ -203,6 +203,14 @@ interface NodeRenderContext {
203
203
  * with white plot areas.
204
204
  */
205
205
  readonly theme?: string;
206
+ /**
207
+ * The host's locale as a BCP-47 tag, e.g. "zh-CN".
208
+ *
209
+ * A plugin draws its own labels — a Copy button, an error line — and cannot read the page's
210
+ * language, so without this a Chinese product renders English chrome around Chinese content.
211
+ * English is the fallback for anything a plugin has not translated.
212
+ */
213
+ readonly locale?: string;
206
214
  }
207
215
  type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
208
216
  interface PluginCommitContext {
@@ -244,8 +252,14 @@ interface AIGuiPlugin {
244
252
  /** Runs synchronously after the AST is finalized and before patches are dispatched. */
245
253
  onASTCommit?: (nodes: readonly ASTNode[], context: PluginCommitContext) => void;
246
254
  css?: string;
247
- /** LLM-facing guidance describing this plugin's fence syntax. */
248
- promptSpec?: string | (() => string);
255
+ /**
256
+ * LLM-facing guidance describing this plugin's fence syntax.
257
+ *
258
+ * Receives the locale asked of `buildSystemPrompt`, so the rules can be written in the language
259
+ * the product answers in — a Chinese persona followed by English rules reads as a contradiction
260
+ * to the model. Plugins that only ship English simply ignore the argument.
261
+ */
262
+ promptSpec?: string | ((locale?: string) => string);
249
263
  }
250
264
  interface RendererOptions extends DebugOptions {
251
265
  registry?: CardRegistry;
@@ -627,6 +641,76 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
627
641
  /** The set of node types claimed by the given plugins. */
628
642
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
629
643
 
644
+ //#endregion
645
+ //#region src/plugin-styles.d.ts
646
+ /**
647
+ * The stylesheet every renderer needs regardless of which plugins are loaded.
648
+ *
649
+ * Model output is written without knowing the viewport, so a wide table, a long code line or a
650
+ * diagram sized for a desktop will otherwise push the page sideways on a phone. Each block is
651
+ * made to scroll inside its own box instead of widening the column that holds it.
652
+ */
653
+ declare const baseCss: string;
654
+ /** One plugin's stylesheet, keyed by the plugin that owns it. */
655
+ interface PluginStyle {
656
+ name: string;
657
+ css: string;
658
+ }
659
+ /**
660
+ * Collect the stylesheets of the given plugins, base styles first.
661
+ *
662
+ * Plugins declare `css` but cannot inject it themselves — they never see the document. Each
663
+ * plugin appears once even if it is passed twice, and later plugins of the same name win, which
664
+ * matches how `collectNodeRenderers` resolves duplicates.
665
+ */
666
+ declare function collectPluginStyles(plugins?: AIGuiPlugin[]): PluginStyle[];
667
+ /**
668
+ * Put the plugins' stylesheets in the document, once each.
669
+ *
670
+ * Called on every render by every renderer on the page, so it must be idempotent: a stylesheet
671
+ * already present is left alone rather than duplicated. No-ops without a document, which is what
672
+ * server-side rendering gets.
673
+ */
674
+ declare function injectPluginStyles(plugins?: AIGuiPlugin[], doc?: Document): void;
675
+
676
+ //#endregion
677
+ //#region src/i18n.d.ts
678
+ /**
679
+ * Locale handling for both sides of a rendered answer: the strings a plugin draws on screen, and
680
+ * the guidance a plugin gives the model.
681
+ *
682
+ * Locales are BCP-47 tags ("zh-CN", "pt-BR", "en"). English is the fallback and is always
683
+ * complete, so a partial translation degrades to English strings rather than to blank UI.
684
+ */
685
+ /** A locale tag, e.g. "en", "zh-CN". */
686
+ type Locale = string;
687
+ /** The strings of one locale, keyed by a stable id the plugin chooses. */
688
+ type Messages = Record<string, string>;
689
+ /** Every locale a plugin ships, keyed by tag. `en` is required as the fallback. */
690
+ type MessageBundle = Record<Locale, Messages> & {
691
+ en: Messages;
692
+ };
693
+ /** The default locale, used when a host does not say otherwise. */
694
+ declare const DEFAULT_LOCALE = "en";
695
+ /**
696
+ * Pick the messages for a locale: exact match, then the base language, then English.
697
+ *
698
+ * "zh-CN" therefore finds a "zh-CN" bundle, falls back to "zh", and finally to English — a host
699
+ * asking for a regional variant nobody translated still gets the language.
700
+ */
701
+ declare function resolveMessages(bundle: MessageBundle, locale?: Locale): Messages;
702
+ /** Look up one string, falling back to English and finally to the key itself. */
703
+ declare function translate(bundle: MessageBundle, locale: Locale | undefined, key: string): string;
704
+ /**
705
+ * A lookup function bound to one bundle and locale.
706
+ *
707
+ * Plugins render many strings per node, so resolving the bundle once and closing over it keeps
708
+ * the per-string cost to a map lookup.
709
+ */
710
+ declare function translator(bundle: MessageBundle, locale?: Locale): (key: string) => string;
711
+ /** The locales a bundle actually carries. */
712
+ declare function availableLocales(bundle: MessageBundle): Locale[];
713
+
630
714
  //#endregion
631
715
  //#region src/export-image.d.ts
632
716
  /**
@@ -760,6 +844,14 @@ interface BuildSystemPromptOptions {
760
844
  base?: string;
761
845
  registry?: CardRegistry;
762
846
  plugins?: AIGuiPlugin[];
847
+ /**
848
+ * The locale to write the guidance in, as a BCP-47 tag, e.g. "zh-CN".
849
+ *
850
+ * A product whose persona says "always answer in Chinese" ends up appending English rules to
851
+ * it, which reads as a contradiction. Plugins fall back to English for locales they have not
852
+ * been translated into.
853
+ */
854
+ locale?: string;
763
855
  }
764
856
  /**
765
857
  * Assembles the LLM system-prompt guidance: an optional base, the registered
@@ -830,4 +922,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
830
922
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
831
923
 
832
924
  //#endregion
833
- 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, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
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 };
package/dist/index.d.ts CHANGED
@@ -203,6 +203,14 @@ interface NodeRenderContext {
203
203
  * with white plot areas.
204
204
  */
205
205
  readonly theme?: string;
206
+ /**
207
+ * The host's locale as a BCP-47 tag, e.g. "zh-CN".
208
+ *
209
+ * A plugin draws its own labels — a Copy button, an error line — and cannot read the page's
210
+ * language, so without this a Chinese product renders English chrome around Chinese content.
211
+ * English is the fallback for anything a plugin has not translated.
212
+ */
213
+ readonly locale?: string;
206
214
  }
207
215
  type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
208
216
  interface PluginCommitContext {
@@ -244,8 +252,14 @@ interface AIGuiPlugin {
244
252
  /** Runs synchronously after the AST is finalized and before patches are dispatched. */
245
253
  onASTCommit?: (nodes: readonly ASTNode[], context: PluginCommitContext) => void;
246
254
  css?: string;
247
- /** LLM-facing guidance describing this plugin's fence syntax. */
248
- promptSpec?: string | (() => string);
255
+ /**
256
+ * LLM-facing guidance describing this plugin's fence syntax.
257
+ *
258
+ * Receives the locale asked of `buildSystemPrompt`, so the rules can be written in the language
259
+ * the product answers in — a Chinese persona followed by English rules reads as a contradiction
260
+ * to the model. Plugins that only ship English simply ignore the argument.
261
+ */
262
+ promptSpec?: string | ((locale?: string) => string);
249
263
  }
250
264
  interface RendererOptions extends DebugOptions {
251
265
  registry?: CardRegistry;
@@ -627,6 +641,76 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
627
641
  /** The set of node types claimed by the given plugins. */
628
642
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
629
643
 
644
+ //#endregion
645
+ //#region src/plugin-styles.d.ts
646
+ /**
647
+ * The stylesheet every renderer needs regardless of which plugins are loaded.
648
+ *
649
+ * Model output is written without knowing the viewport, so a wide table, a long code line or a
650
+ * diagram sized for a desktop will otherwise push the page sideways on a phone. Each block is
651
+ * made to scroll inside its own box instead of widening the column that holds it.
652
+ */
653
+ declare const baseCss: string;
654
+ /** One plugin's stylesheet, keyed by the plugin that owns it. */
655
+ interface PluginStyle {
656
+ name: string;
657
+ css: string;
658
+ }
659
+ /**
660
+ * Collect the stylesheets of the given plugins, base styles first.
661
+ *
662
+ * Plugins declare `css` but cannot inject it themselves — they never see the document. Each
663
+ * plugin appears once even if it is passed twice, and later plugins of the same name win, which
664
+ * matches how `collectNodeRenderers` resolves duplicates.
665
+ */
666
+ declare function collectPluginStyles(plugins?: AIGuiPlugin[]): PluginStyle[];
667
+ /**
668
+ * Put the plugins' stylesheets in the document, once each.
669
+ *
670
+ * Called on every render by every renderer on the page, so it must be idempotent: a stylesheet
671
+ * already present is left alone rather than duplicated. No-ops without a document, which is what
672
+ * server-side rendering gets.
673
+ */
674
+ declare function injectPluginStyles(plugins?: AIGuiPlugin[], doc?: Document): void;
675
+
676
+ //#endregion
677
+ //#region src/i18n.d.ts
678
+ /**
679
+ * Locale handling for both sides of a rendered answer: the strings a plugin draws on screen, and
680
+ * the guidance a plugin gives the model.
681
+ *
682
+ * Locales are BCP-47 tags ("zh-CN", "pt-BR", "en"). English is the fallback and is always
683
+ * complete, so a partial translation degrades to English strings rather than to blank UI.
684
+ */
685
+ /** A locale tag, e.g. "en", "zh-CN". */
686
+ type Locale = string;
687
+ /** The strings of one locale, keyed by a stable id the plugin chooses. */
688
+ type Messages = Record<string, string>;
689
+ /** Every locale a plugin ships, keyed by tag. `en` is required as the fallback. */
690
+ type MessageBundle = Record<Locale, Messages> & {
691
+ en: Messages;
692
+ };
693
+ /** The default locale, used when a host does not say otherwise. */
694
+ declare const DEFAULT_LOCALE = "en";
695
+ /**
696
+ * Pick the messages for a locale: exact match, then the base language, then English.
697
+ *
698
+ * "zh-CN" therefore finds a "zh-CN" bundle, falls back to "zh", and finally to English — a host
699
+ * asking for a regional variant nobody translated still gets the language.
700
+ */
701
+ declare function resolveMessages(bundle: MessageBundle, locale?: Locale): Messages;
702
+ /** Look up one string, falling back to English and finally to the key itself. */
703
+ declare function translate(bundle: MessageBundle, locale: Locale | undefined, key: string): string;
704
+ /**
705
+ * A lookup function bound to one bundle and locale.
706
+ *
707
+ * Plugins render many strings per node, so resolving the bundle once and closing over it keeps
708
+ * the per-string cost to a map lookup.
709
+ */
710
+ declare function translator(bundle: MessageBundle, locale?: Locale): (key: string) => string;
711
+ /** The locales a bundle actually carries. */
712
+ declare function availableLocales(bundle: MessageBundle): Locale[];
713
+
630
714
  //#endregion
631
715
  //#region src/export-image.d.ts
632
716
  /**
@@ -760,6 +844,14 @@ interface BuildSystemPromptOptions {
760
844
  base?: string;
761
845
  registry?: CardRegistry;
762
846
  plugins?: AIGuiPlugin[];
847
+ /**
848
+ * The locale to write the guidance in, as a BCP-47 tag, e.g. "zh-CN".
849
+ *
850
+ * A product whose persona says "always answer in Chinese" ends up appending English rules to
851
+ * it, which reads as a contradiction. Plugins fall back to English for locales they have not
852
+ * been translated into.
853
+ */
854
+ locale?: string;
763
855
  }
764
856
  /**
765
857
  * Assembles the LLM system-prompt guidance: an optional base, the registered
@@ -830,4 +922,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
830
922
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
831
923
 
832
924
  //#endregion
833
- 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, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, OutcomeTone, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, actionOutcome, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
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 };
package/dist/index.js CHANGED
@@ -1697,6 +1697,115 @@ function normalizeLineEndings(src) {
1697
1697
  return src.replace(/\r\n|\r/g, "\n");
1698
1698
  }
1699
1699
 
1700
+ //#endregion
1701
+ //#region src/plugin-styles.ts
1702
+ /**
1703
+ * The stylesheet every renderer needs regardless of which plugins are loaded.
1704
+ *
1705
+ * Model output is written without knowing the viewport, so a wide table, a long code line or a
1706
+ * diagram sized for a desktop will otherwise push the page sideways on a phone. Each block is
1707
+ * made to scroll inside its own box instead of widening the column that holds it.
1708
+ */
1709
+ const baseCss = [
1710
+ "[data-aigui-renderer]{max-width:100%}",
1711
+ "[data-aigui-renderer] img,[data-aigui-renderer] svg,[data-aigui-renderer] video,[data-aigui-renderer] canvas{max-width:100%;height:auto}",
1712
+ "[data-aigui-renderer] pre{max-width:100%;overflow-x:auto}",
1713
+ "[data-aigui-renderer] code{overflow-wrap:anywhere}",
1714
+ "[data-aigui-renderer] pre code{overflow-wrap:normal}",
1715
+ "[data-aigui-renderer] table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse}",
1716
+ "[data-aigui-renderer] p,[data-aigui-renderer] li,[data-aigui-renderer] h1,[data-aigui-renderer] h2,[data-aigui-renderer] h3{overflow-wrap:break-word}",
1717
+ "[data-aigui-renderer] a{overflow-wrap:anywhere}",
1718
+ "[data-aigui-renderer] [data-aigui-chart],[data-aigui-renderer] [data-aigui-mermaid],[data-aigui-renderer] [data-aigui-map],[data-aigui-renderer] [data-aigui-molecule]{max-width:100%;overflow-x:auto}"
1719
+ ].join("");
1720
+ /**
1721
+ * Collect the stylesheets of the given plugins, base styles first.
1722
+ *
1723
+ * Plugins declare `css` but cannot inject it themselves — they never see the document. Each
1724
+ * plugin appears once even if it is passed twice, and later plugins of the same name win, which
1725
+ * matches how `collectNodeRenderers` resolves duplicates.
1726
+ */
1727
+ function collectPluginStyles(plugins) {
1728
+ const styles = new Map();
1729
+ styles.set("base", baseCss);
1730
+ for (const plugin of plugins ?? []) {
1731
+ const css = plugin?.css?.trim();
1732
+ if (!css) continue;
1733
+ if (css.startsWith("@import") && !/@import\s+url\(|@import\s+["'](?:https?:)?\/\//.test(css)) continue;
1734
+ styles.set(plugin.name, css);
1735
+ }
1736
+ return [...styles].map(([name, css]) => ({
1737
+ name,
1738
+ css
1739
+ }));
1740
+ }
1741
+ const STYLE_ATTR = "data-aigui-style";
1742
+ /**
1743
+ * Put the plugins' stylesheets in the document, once each.
1744
+ *
1745
+ * Called on every render by every renderer on the page, so it must be idempotent: a stylesheet
1746
+ * already present is left alone rather than duplicated. No-ops without a document, which is what
1747
+ * server-side rendering gets.
1748
+ */
1749
+ function injectPluginStyles(plugins, doc) {
1750
+ const target = doc ?? (typeof document === "undefined" ? void 0 : document);
1751
+ if (!target?.head) return;
1752
+ for (const { name, css } of collectPluginStyles(plugins)) {
1753
+ if (target.querySelector(`style[${STYLE_ATTR}="${CSS_ESCAPE(name)}"]`)) continue;
1754
+ const el = target.createElement("style");
1755
+ el.setAttribute(STYLE_ATTR, name);
1756
+ el.textContent = css;
1757
+ target.head.appendChild(el);
1758
+ }
1759
+ }
1760
+ /** Quote a plugin name for use inside an attribute selector. */
1761
+ function CSS_ESCAPE(name) {
1762
+ return name.replace(/["\\]/g, "\\$&");
1763
+ }
1764
+
1765
+ //#endregion
1766
+ //#region src/i18n.ts
1767
+ /** The default locale, used when a host does not say otherwise. */
1768
+ const DEFAULT_LOCALE = "en";
1769
+ /**
1770
+ * Pick the messages for a locale: exact match, then the base language, then English.
1771
+ *
1772
+ * "zh-CN" therefore finds a "zh-CN" bundle, falls back to "zh", and finally to English — a host
1773
+ * asking for a regional variant nobody translated still gets the language.
1774
+ */
1775
+ function resolveMessages(bundle, locale) {
1776
+ const en = bundle.en ?? {};
1777
+ if (!locale) return en;
1778
+ const exact = bundle[locale];
1779
+ if (exact) return {
1780
+ ...en,
1781
+ ...exact
1782
+ };
1783
+ const base = locale.split("-")[0];
1784
+ const language = base && base !== locale ? bundle[base] : void 0;
1785
+ return language ? {
1786
+ ...en,
1787
+ ...language
1788
+ } : en;
1789
+ }
1790
+ /** Look up one string, falling back to English and finally to the key itself. */
1791
+ function translate(bundle, locale, key) {
1792
+ return resolveMessages(bundle, locale)[key] ?? key;
1793
+ }
1794
+ /**
1795
+ * A lookup function bound to one bundle and locale.
1796
+ *
1797
+ * Plugins render many strings per node, so resolving the bundle once and closing over it keeps
1798
+ * the per-string cost to a map lookup.
1799
+ */
1800
+ function translator(bundle, locale) {
1801
+ const messages = resolveMessages(bundle, locale);
1802
+ return (key) => messages[key] ?? key;
1803
+ }
1804
+ /** The locales a bundle actually carries. */
1805
+ function availableLocales(bundle) {
1806
+ return Object.keys(bundle);
1807
+ }
1808
+
1700
1809
  //#endregion
1701
1810
  //#region src/export-image.ts
1702
1811
  /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
@@ -2372,7 +2481,7 @@ function buildSystemPrompt(options = {}) {
2372
2481
  const cardSpec = options.registry?.toPromptSpec();
2373
2482
  if (cardSpec) parts.push(cardSpec);
2374
2483
  for (const plugin of options.plugins ?? []) {
2375
- const spec = typeof plugin.promptSpec === "function" ? plugin.promptSpec() : plugin.promptSpec;
2484
+ const spec = typeof plugin.promptSpec === "function" ? plugin.promptSpec(options.locale) : plugin.promptSpec;
2376
2485
  if (spec) parts.push(spec);
2377
2486
  }
2378
2487
  return parts.join("\n\n");
@@ -2663,4 +2772,4 @@ function delay(ms, signal) {
2663
2772
  }
2664
2773
 
2665
2774
  //#endregion
2666
- 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, DebugEmitter, Renderer, StreamRouter, actionOutcome, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/core",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
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",