@runtypelabs/persona 3.21.3 → 3.22.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.
Files changed (57) hide show
  1. package/README.md +67 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CWPIj66R.d.cts → types-BZVr1YOV.d.cts} +10 -0
  5. package/dist/animations/{types-CWPIj66R.d.ts → types-BZVr1YOV.d.ts} +10 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/index.cjs +50 -43
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +474 -6
  11. package/dist/index.d.ts +474 -6
  12. package/dist/index.global.js +98 -88
  13. package/dist/index.global.js.map +1 -1
  14. package/dist/index.js +48 -41
  15. package/dist/index.js.map +1 -1
  16. package/dist/smart-dom-reader.cjs +1875 -0
  17. package/dist/smart-dom-reader.d.cts +4521 -0
  18. package/dist/smart-dom-reader.d.ts +4521 -0
  19. package/dist/smart-dom-reader.js +1848 -0
  20. package/dist/theme-editor.cjs +2281 -84
  21. package/dist/theme-editor.d.cts +348 -1
  22. package/dist/theme-editor.d.ts +348 -1
  23. package/dist/theme-editor.js +2260 -78
  24. package/package.json +9 -2
  25. package/src/client.test.ts +165 -0
  26. package/src/client.ts +144 -23
  27. package/src/index.ts +26 -0
  28. package/src/session.test.ts +258 -0
  29. package/src/session.ts +886 -30
  30. package/src/session.webmcp.test.ts +815 -0
  31. package/src/smart-dom-reader.test.ts +135 -0
  32. package/src/smart-dom-reader.ts +135 -0
  33. package/src/theme-editor/color-utils.test.ts +59 -0
  34. package/src/theme-editor/color-utils.ts +38 -2
  35. package/src/theme-editor/index.ts +35 -0
  36. package/src/theme-editor/webmcp/coerce.test.ts +86 -0
  37. package/src/theme-editor/webmcp/coerce.ts +286 -0
  38. package/src/theme-editor/webmcp/index.ts +45 -0
  39. package/src/theme-editor/webmcp/summary.ts +324 -0
  40. package/src/theme-editor/webmcp/tools.test.ts +205 -0
  41. package/src/theme-editor/webmcp/tools.ts +795 -0
  42. package/src/theme-editor/webmcp/types.ts +87 -0
  43. package/src/types.ts +186 -0
  44. package/src/ui.composer-keyboard.test.ts +229 -0
  45. package/src/ui.ts +127 -5
  46. package/src/utils/composer-history.test.ts +128 -0
  47. package/src/utils/composer-history.ts +113 -0
  48. package/src/utils/message-fingerprint.test.ts +20 -0
  49. package/src/utils/message-fingerprint.ts +2 -0
  50. package/src/utils/smart-dom-adapter.test.ts +257 -0
  51. package/src/utils/smart-dom-adapter.ts +217 -0
  52. package/{LICENSE → src/vendor/smart-dom-reader/LICENSE} +2 -2
  53. package/src/vendor/smart-dom-reader/README.md +61 -0
  54. package/src/vendor/smart-dom-reader/index.d.ts +476 -0
  55. package/src/vendor/smart-dom-reader/index.js +1618 -0
  56. package/src/webmcp-bridge.test.ts +429 -0
  57. package/src/webmcp-bridge.ts +547 -0
@@ -42,6 +42,8 @@ __export(theme_editor_exports, {
42
42
  COMPONENT_SHAPE_SECTIONS: () => COMPONENT_SHAPE_SECTIONS,
43
43
  CONFIGURE_SECTIONS: () => CONFIGURE_SECTIONS,
44
44
  CONFIGURE_SUB_GROUPS: () => CONFIGURE_SUB_GROUPS,
45
+ CONTRAST_PAIRS: () => CONTRAST_PAIRS,
46
+ CSS_NAMED_COLORS: () => CSS_NAMED_COLORS,
45
47
  DEVICE_DIMENSIONS: () => DEVICE_DIMENSIONS,
46
48
  HOME_SUGGESTION_CHIPS: () => HOME_SUGGESTION_CHIPS,
47
49
  INTERFACE_ROLES_SECTION: () => INTERFACE_ROLES_SECTION,
@@ -49,6 +51,7 @@ __export(theme_editor_exports, {
49
51
  MOCK_WORKSPACE_CONTENT: () => MOCK_WORKSPACE_CONTENT,
50
52
  PALETTE_SECTION: () => PALETTE_SECTION,
51
53
  PREVIEW_STORAGE_ADAPTER: () => PREVIEW_STORAGE_ADAPTER,
54
+ RADIUS_PRESETS: () => RADIUS_PRESETS,
52
55
  ROLE_ASSISTANT_MESSAGES: () => ROLE_ASSISTANT_MESSAGES,
53
56
  ROLE_BORDERS: () => ROLE_BORDERS,
54
57
  ROLE_FAMILIES: () => ROLE_FAMILIES,
@@ -80,11 +83,19 @@ __export(theme_editor_exports, {
80
83
  buildPreviewConfigWithMessages: () => buildPreviewConfigWithMessages,
81
84
  buildShellCss: () => buildShellCss,
82
85
  buildSrcdoc: () => buildSrcdoc,
86
+ buildSummary: () => buildSummary,
83
87
  buildTranscriptStreamFrames: () => buildTranscriptStreamFrames,
88
+ coerceColor: () => coerceColor,
89
+ coerceFamily: () => coerceFamily,
90
+ coerceIntensity: () => coerceIntensity,
91
+ coerceRadius: () => coerceRadius,
92
+ coerceRoundnessStyle: () => coerceRoundnessStyle,
93
+ coerceScheme: () => coerceScheme,
84
94
  convertFromPx: () => convertFromPx,
85
95
  convertToPx: () => convertToPx,
86
96
  createPreviewMessages: () => createPreviewMessages,
87
97
  createPreviewTranscriptEntry: () => createPreviewTranscriptEntry,
98
+ createThemeEditorTools: () => createThemeEditorTools,
88
99
  createThemePreview: () => createThemePreview,
89
100
  detectRoleAssignment: () => detectRoleAssignment,
90
101
  escapeHtml: () => escapeHtml2,
@@ -101,10 +112,14 @@ __export(theme_editor_exports, {
101
112
  paletteColorPath: () => paletteColorPath,
102
113
  parseCssValue: () => parseCssValue,
103
114
  presetStreamsText: () => presetStreamsText,
115
+ quickContrastWarnings: () => quickContrastWarnings,
104
116
  resolveRoleAssignment: () => resolveRoleAssignment,
105
117
  resolveThemeColorPath: () => resolveThemeColorPath,
118
+ rgbToHex: () => rgbToHex,
119
+ runContrastChecks: () => runContrastChecks,
106
120
  scopeSection: () => scopeSection,
107
121
  tokenRefDisplayName: () => tokenRefDisplayName,
122
+ toolResult: () => toolResult,
108
123
  wcagContrastRatio: () => wcagContrastRatio
109
124
  });
110
125
  module.exports = __toCommonJS(theme_editor_exports);
@@ -1724,9 +1739,29 @@ function normalizeColorValue(value) {
1724
1739
  function isValidHex(value) {
1725
1740
  return /^#[0-9A-Fa-f]{6}$/.test(value);
1726
1741
  }
1742
+ function rgbToHex(value) {
1743
+ const match = value.trim().toLowerCase().match(/^rgba?\(([^)]+)\)$/);
1744
+ if (!match) return null;
1745
+ const parts = match[1].split(",").map((p) => p.trim());
1746
+ if (parts.length < 3) return null;
1747
+ const channel = (raw) => {
1748
+ const isPct = raw.endsWith("%");
1749
+ const n = parseFloat(isPct ? raw.slice(0, -1) : raw);
1750
+ if (!Number.isFinite(n)) return NaN;
1751
+ return Math.max(0, Math.min(255, Math.round(isPct ? n / 100 * 255 : n)));
1752
+ };
1753
+ const r = channel(parts[0]);
1754
+ const g = channel(parts[1]);
1755
+ const b = channel(parts[2]);
1756
+ if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b)) return null;
1757
+ const toHex = (n) => n.toString(16).padStart(2, "0");
1758
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
1759
+ }
1727
1760
  function wcagContrastRatio(hex1, hex2) {
1728
1761
  const luminance = (hex) => {
1729
- const norm = normalizeColorValue(hex);
1762
+ var _a;
1763
+ const normalized = normalizeColorValue(hex);
1764
+ const norm = normalized.startsWith("rgb") ? (_a = rgbToHex(normalized)) != null ? _a : "#000000" : normalized;
1730
1765
  const channels = [1, 3, 5].map((i) => {
1731
1766
  const v = parseInt(norm.slice(i, i + 2), 16) / 255;
1732
1767
  return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
@@ -1738,7 +1773,9 @@ function wcagContrastRatio(hex1, hex2) {
1738
1773
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1739
1774
  }
1740
1775
  function hexToHsl(hex) {
1741
- const normalized = normalizeColorValue(hex);
1776
+ var _a;
1777
+ const normalizedValue = normalizeColorValue(hex);
1778
+ const normalized = normalizedValue.startsWith("rgb") ? (_a = rgbToHex(normalizedValue)) != null ? _a : "#000000" : normalizedValue;
1742
1779
  const r = parseInt(normalized.slice(1, 3), 16) / 255;
1743
1780
  const g = parseInt(normalized.slice(3, 5), 16) / 255;
1744
1781
  const b = parseInt(normalized.slice(5, 7), 16) / 255;
@@ -3430,6 +3467,337 @@ var resolveSanitizer = (option) => {
3430
3467
  return createDefaultSanitizer();
3431
3468
  };
3432
3469
 
3470
+ // src/webmcp-bridge.ts
3471
+ var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
3472
+ var WEBMCP_TOOL_PREFIX = "webmcp:";
3473
+ var log = {
3474
+ warn(message, ...rest) {
3475
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
3476
+ console.warn(`[Persona/WebMCP] ${message}`, ...rest);
3477
+ }
3478
+ }
3479
+ };
3480
+ var WebMcpBridge = class {
3481
+ constructor(config) {
3482
+ this.config = config;
3483
+ /** `true` once the polyfill has been (idempotently) installed. */
3484
+ this.installed = false;
3485
+ /** Memoizes the one-shot async install so concurrent callers share it. */
3486
+ this.readyPromise = null;
3487
+ /**
3488
+ * Warn-once latch for a present-but-incompatible `document.modelContext`
3489
+ * (some other / older WebMCP polyfill squatting the global). `getModelContext`
3490
+ * is hit on every snapshot + execute, so we log the diagnostic only once.
3491
+ */
3492
+ this.incompatibleContextWarned = false;
3493
+ var _a;
3494
+ this.confirmHandler = (_a = config.onConfirm) != null ? _a : null;
3495
+ this.timeoutMs = DEFAULT_TOOL_TIMEOUT_MS;
3496
+ }
3497
+ /**
3498
+ * Override the confirm handler post-construction. Used by `ui.ts` to wire
3499
+ * the in-panel approval bubble after the client has been built (the widget
3500
+ * lifecycle constructs the client before the panel renders).
3501
+ */
3502
+ setConfirmHandler(handler) {
3503
+ this.confirmHandler = handler;
3504
+ }
3505
+ /**
3506
+ * `true` when the bridge can both snapshot the registry AND execute returned
3507
+ * tool calls — i.e. the polyfill is installed and `document.modelContext`
3508
+ * exposes the consumer surface (`getTools` / `executeTool`). Native browsers
3509
+ * that ship `document.modelContext` satisfy this too.
3510
+ *
3511
+ * Synchronous and best-effort: returns `false` until the lazy install has
3512
+ * resolved (see `ensureReady`). The snapshot/execute paths await readiness
3513
+ * themselves, so this is purely an advisory check for callers.
3514
+ */
3515
+ isOperational() {
3516
+ if (this.config.enabled !== true) return false;
3517
+ if (!this.installed) return false;
3518
+ return this.getModelContext() !== null;
3519
+ }
3520
+ /**
3521
+ * Per-turn snapshot for `dispatch.clientTools[]`. Returns the JSON-only
3522
+ * surface — `execute` stays client-side, reached later via `executeToolCall`.
3523
+ *
3524
+ * Async because the strict polyfill's `getTools()` is async. Both payload
3525
+ * builders in `client.ts` already `await`, so this adds no new ceremony.
3526
+ */
3527
+ async snapshotForDispatch() {
3528
+ await this.ensureReady();
3529
+ if (this.config.enabled !== true) return [];
3530
+ const mc = this.getModelContext();
3531
+ if (!mc) return [];
3532
+ let infos;
3533
+ try {
3534
+ infos = await mc.getTools();
3535
+ } catch (err) {
3536
+ log.warn("getTools() threw \u2014 shipping an empty WebMCP snapshot.", err);
3537
+ return [];
3538
+ }
3539
+ const pageOrigin = typeof location !== "undefined" ? location.origin : "";
3540
+ return infos.filter((info) => this.passesClientAllowlist(info.name)).map((info) => {
3541
+ const def = {
3542
+ name: info.name,
3543
+ description: info.description,
3544
+ origin: "webmcp",
3545
+ ...pageOrigin ? { pageOrigin } : {}
3546
+ };
3547
+ const schema = parseSchema(info.inputSchema);
3548
+ if (schema) def.parametersSchema = schema;
3549
+ return def;
3550
+ });
3551
+ }
3552
+ /**
3553
+ * Execute a `webmcp:<name>` tool call returned by the agent and return the
3554
+ * normalized MCP-shaped result for `/resume`.
3555
+ *
3556
+ * Failure modes — all return `{ isError: true, content: [...] }` rather than
3557
+ * throwing, so the dispatch can resume cleanly:
3558
+ * - bridge not operational
3559
+ * - tool not in registry (e.g. unmounted between snapshot and call)
3560
+ * - tool excluded by the client allowlist
3561
+ * - user declined the confirm gate
3562
+ * - `execute()` threw or failed schema validation
3563
+ * - `execute()` exceeded the 30s timeout
3564
+ * - `signal` fired (session-level `cancel()`)
3565
+ *
3566
+ * When `signal` is provided, abort is honored at three points: before the
3567
+ * confirm bubble renders, after the user approves but before `execute()`
3568
+ * runs, and (via a combined `AbortController`) during `execute()` itself.
3569
+ * Honoring abort BEFORE the confirm prevents a late approval after `cancel()`
3570
+ * from firing a host-page side effect with no matching `/resume`.
3571
+ */
3572
+ async executeToolCall(wireToolName, args, signal) {
3573
+ await this.ensureReady();
3574
+ if (this.config.enabled !== true) {
3575
+ return errorResult(
3576
+ "WebMCP is not enabled on this widget."
3577
+ );
3578
+ }
3579
+ const mc = this.getModelContext();
3580
+ if (!mc) {
3581
+ const present = typeof document !== "undefined" && Boolean(document.modelContext);
3582
+ return errorResult(
3583
+ present ? "WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill)." : "WebMCP bridge is not operational on this page (document.modelContext not available)."
3584
+ );
3585
+ }
3586
+ const bareName = stripWebMcpPrefix(wireToolName);
3587
+ let infos;
3588
+ try {
3589
+ infos = await mc.getTools();
3590
+ } catch (err) {
3591
+ const message = err instanceof Error ? err.message : String(err);
3592
+ return errorResult(`Failed to read WebMCP registry: ${message}`);
3593
+ }
3594
+ const info = infos.find((candidate) => candidate.name === bareName);
3595
+ if (!info) {
3596
+ return errorResult(
3597
+ `WebMCP tool not registered on this page: ${bareName}`
3598
+ );
3599
+ }
3600
+ if (!this.passesClientAllowlist(bareName)) {
3601
+ return errorResult(
3602
+ `WebMCP tool not allowed by client allowlist: ${bareName}`
3603
+ );
3604
+ }
3605
+ if (signal == null ? void 0 : signal.aborted) {
3606
+ return errorResult("Aborted by cancel()");
3607
+ }
3608
+ const gateInfo = {
3609
+ toolName: bareName,
3610
+ args,
3611
+ description: info.description,
3612
+ reason: "gate"
3613
+ };
3614
+ if (!await this.requestConfirm(gateInfo)) {
3615
+ return errorResult("User declined the tool call.");
3616
+ }
3617
+ if (signal == null ? void 0 : signal.aborted) {
3618
+ return errorResult("Aborted by cancel()");
3619
+ }
3620
+ const controller = new AbortController();
3621
+ let timedOut = false;
3622
+ const timer = setTimeout(() => {
3623
+ timedOut = true;
3624
+ controller.abort();
3625
+ }, this.timeoutMs);
3626
+ const onAbort = () => controller.abort();
3627
+ if (signal) {
3628
+ if (signal.aborted) controller.abort();
3629
+ else signal.addEventListener("abort", onAbort, { once: true });
3630
+ }
3631
+ try {
3632
+ const raw = await mc.executeTool(info, safeStringifyArgs(args), {
3633
+ signal: controller.signal
3634
+ });
3635
+ return normalizeSerializedResult(raw);
3636
+ } catch (err) {
3637
+ if (timedOut) {
3638
+ return errorResult(
3639
+ `WebMCP tool '${bareName}' timed out after ${this.timeoutMs}ms`
3640
+ );
3641
+ }
3642
+ if (signal == null ? void 0 : signal.aborted) {
3643
+ return errorResult("Aborted by cancel()");
3644
+ }
3645
+ const message = err instanceof Error ? err.message : String(err);
3646
+ return errorResult(message);
3647
+ } finally {
3648
+ clearTimeout(timer);
3649
+ if (signal) signal.removeEventListener("abort", onAbort);
3650
+ }
3651
+ }
3652
+ /**
3653
+ * Lazily install `@mcp-b/webmcp-polyfill` the first time the bridge needs the
3654
+ * registry. Idempotent and memoized. Dynamic import keeps the polyfill out of
3655
+ * the main bundle and prevents it from installing `document.modelContext` for
3656
+ * widget consumers that never enable WebMCP.
3657
+ *
3658
+ * Producer pages should still install the polyfill themselves (or import it)
3659
+ * before registering tools — Persona's install is a fallback, and a page that
3660
+ * registers tools at load before Persona's first dispatch needs the global to
3661
+ * already exist.
3662
+ */
3663
+ ensureReady() {
3664
+ if (this.config.enabled !== true) return Promise.resolve();
3665
+ if (!this.readyPromise) {
3666
+ this.readyPromise = this.install();
3667
+ }
3668
+ return this.readyPromise;
3669
+ }
3670
+ async install() {
3671
+ try {
3672
+ const mod = await import("@mcp-b/webmcp-polyfill");
3673
+ mod.initializeWebMCPPolyfill();
3674
+ this.installed = true;
3675
+ } catch (err) {
3676
+ log.warn(
3677
+ "Failed to load @mcp-b/webmcp-polyfill \u2014 WebMCP consumption disabled.",
3678
+ err
3679
+ );
3680
+ this.installed = false;
3681
+ }
3682
+ }
3683
+ /**
3684
+ * Read the consumer surface off `document.modelContext`, returning `null`
3685
+ * when it is absent or doesn't expose the producer-preview API we rely on.
3686
+ */
3687
+ getModelContext() {
3688
+ if (typeof document === "undefined") return null;
3689
+ const mc = document.modelContext;
3690
+ if (!mc || typeof mc !== "object") {
3691
+ return null;
3692
+ }
3693
+ const core = mc;
3694
+ if (typeof core.getTools !== "function" || typeof core.executeTool !== "function") {
3695
+ if (!this.incompatibleContextWarned) {
3696
+ this.incompatibleContextWarned = true;
3697
+ log.warn(
3698
+ "document.modelContext is present but does not expose getTools()/executeTool() \u2014 WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill)."
3699
+ );
3700
+ }
3701
+ return null;
3702
+ }
3703
+ return mc;
3704
+ }
3705
+ async requestConfirm(info) {
3706
+ var _a;
3707
+ const handler = (_a = this.confirmHandler) != null ? _a : defaultBrowserConfirmHandler;
3708
+ try {
3709
+ return await handler(info);
3710
+ } catch (err) {
3711
+ log.warn(
3712
+ `Confirm handler threw for WebMCP tool '${info.toolName}'; declining.`,
3713
+ err
3714
+ );
3715
+ return false;
3716
+ }
3717
+ }
3718
+ passesClientAllowlist(toolName) {
3719
+ const list = this.config.allowlist;
3720
+ if (!list || list.length === 0) return true;
3721
+ return list.some((pattern) => matchesGlob(toolName, pattern));
3722
+ }
3723
+ };
3724
+ var stripWebMcpPrefix = (name) => name.startsWith(WEBMCP_TOOL_PREFIX) ? name.slice(WEBMCP_TOOL_PREFIX.length) : name;
3725
+ var isWebMcpToolName = (name) => name.startsWith(WEBMCP_TOOL_PREFIX);
3726
+ var matchesGlob = (name, pattern) => {
3727
+ if (pattern === "*") return true;
3728
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
3729
+ const regex = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
3730
+ return regex.test(name);
3731
+ };
3732
+ var parseSchema = (raw) => {
3733
+ if (raw === void 0 || raw === "") return void 0;
3734
+ try {
3735
+ const parsed = JSON.parse(raw);
3736
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
3737
+ } catch {
3738
+ return void 0;
3739
+ }
3740
+ };
3741
+ var normalizeSerializedResult = (raw) => {
3742
+ if (raw === null || raw === void 0) {
3743
+ return { content: [{ type: "text", text: "" }] };
3744
+ }
3745
+ let parsed;
3746
+ try {
3747
+ parsed = JSON.parse(raw);
3748
+ } catch {
3749
+ return { content: [{ type: "text", text: raw }] };
3750
+ }
3751
+ if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.content)) {
3752
+ return parsed;
3753
+ }
3754
+ const text = typeof parsed === "string" ? parsed : safeStringify(parsed);
3755
+ return { content: [{ type: "text", text }] };
3756
+ };
3757
+ var errorResult = (message) => ({
3758
+ isError: true,
3759
+ content: [{ type: "text", text: message }]
3760
+ });
3761
+ var defaultBrowserConfirmHandler = async (info) => {
3762
+ if (typeof window === "undefined" || typeof window.confirm !== "function") {
3763
+ return false;
3764
+ }
3765
+ const argsPreview = previewArgs(info.args);
3766
+ const prompt = `Allow the AI to call ${info.toolName}` + (argsPreview ? `
3767
+
3768
+ Arguments:
3769
+ ${argsPreview}` : "") + (info.description ? `
3770
+
3771
+ ${info.description}` : "");
3772
+ return window.confirm(prompt);
3773
+ };
3774
+ var previewArgs = (args) => {
3775
+ if (args === void 0 || args === null) return "";
3776
+ try {
3777
+ const json = JSON.stringify(args, null, 2);
3778
+ return json.length > 500 ? json.slice(0, 500) + "\u2026" : json;
3779
+ } catch {
3780
+ return String(args);
3781
+ }
3782
+ };
3783
+ var safeStringifyArgs = (args) => {
3784
+ if (args === void 0) return "{}";
3785
+ try {
3786
+ const json = JSON.stringify(args);
3787
+ return json === void 0 ? "{}" : json;
3788
+ } catch {
3789
+ return "{}";
3790
+ }
3791
+ };
3792
+ var safeStringify = (value) => {
3793
+ if (value === void 0) return "";
3794
+ try {
3795
+ return JSON.stringify(value);
3796
+ } catch {
3797
+ return String(value);
3798
+ }
3799
+ };
3800
+
3433
3801
  // src/utils/formatting.ts
3434
3802
  var import_partial_json = require("partial-json");
3435
3803
  var unescapeJsonString = (str) => {
@@ -3864,7 +4232,7 @@ var AgentWidgetClient = class {
3864
4232
  // Client token mode properties
3865
4233
  this.clientSession = null;
3866
4234
  this.sessionInitPromise = null;
3867
- var _a, _b, _c;
4235
+ var _a, _b, _c, _d;
3868
4236
  this.apiUrl = (_a = config.apiUrl) != null ? _a : DEFAULT_ENDPOINT;
3869
4237
  this.headers = {
3870
4238
  "Content-Type": "application/json",
@@ -3877,6 +4245,7 @@ var AgentWidgetClient = class {
3877
4245
  this.customFetch = config.customFetch;
3878
4246
  this.parseSSEEvent = config.parseSSEEvent;
3879
4247
  this.getHeaders = config.getHeaders;
4248
+ this.webMcpBridge = ((_d = config.webmcp) == null ? void 0 : _d.enabled) === true ? new WebMcpBridge(config.webmcp) : null;
3880
4249
  }
3881
4250
  /**
3882
4251
  * Set callback for capturing raw SSE events
@@ -3884,6 +4253,39 @@ var AgentWidgetClient = class {
3884
4253
  setSSEEventCallback(callback) {
3885
4254
  this.onSSEEvent = callback;
3886
4255
  }
4256
+ /**
4257
+ * WebMCP: wire (or replace) the confirm-bubble handler. Called from
4258
+ * `ui.ts` once the widget panel is built and the approval-bubble
4259
+ * chrome is ready to render.
4260
+ */
4261
+ setWebMcpConfirmHandler(handler) {
4262
+ var _a;
4263
+ (_a = this.webMcpBridge) == null ? void 0 : _a.setConfirmHandler(handler);
4264
+ }
4265
+ /**
4266
+ * WebMCP: `true` when the bridge installed the polyfill and can both
4267
+ * snapshot the page registry and execute returned `webmcp:*` tool calls.
4268
+ * `false` for any guard miss (no `document.modelContext`, polyfill not yet
4269
+ * installed, or `config.webmcp.enabled` not set).
4270
+ */
4271
+ isWebMcpOperational() {
4272
+ var _a;
4273
+ return ((_a = this.webMcpBridge) == null ? void 0 : _a.isOperational()) === true;
4274
+ }
4275
+ /**
4276
+ * WebMCP: execute a returned `webmcp:<name>` tool call against the page's
4277
+ * registry and return the normalized MCP-shaped result for `/resume`. The
4278
+ * bridge handles confirm-bubble gating, the 30s timeout, error
4279
+ * normalization, and `signal`-driven abort — callers never see throws.
4280
+ *
4281
+ * Returns `null` when WebMCP is not enabled on this client (signal to the
4282
+ * session that it should fall back to the legacy local-tool resume path,
4283
+ * if any).
4284
+ */
4285
+ executeWebMcpToolCall(wireToolName, args, signal) {
4286
+ if (!this.webMcpBridge) return null;
4287
+ return this.webMcpBridge.executeToolCall(wireToolName, args, signal);
4288
+ }
3887
4289
  /**
3888
4290
  * Get the current SSE event callback (used to preserve across client recreation)
3889
4291
  */
@@ -3908,7 +4310,7 @@ var AgentWidgetClient = class {
3908
4310
  getClientApiUrl(endpoint) {
3909
4311
  var _a;
3910
4312
  const baseUrl = ((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "").replace(/\/v1\/dispatch$/, "")) || DEFAULT_CLIENT_API_BASE;
3911
- return endpoint === "init" ? `${baseUrl}/v1/client/init` : `${baseUrl}/v1/client/chat`;
4313
+ return `${baseUrl}/v1/client/${endpoint}`;
3912
4314
  }
3913
4315
  /**
3914
4316
  * Get the current client session (if any)
@@ -4181,7 +4583,10 @@ var AgentWidgetClient = class {
4181
4583
  // Include metadata/context from middleware if present (excluding sessionId)
4182
4584
  ...sanitizedMetadata && Object.keys(sanitizedMetadata).length > 0 && { metadata: sanitizedMetadata },
4183
4585
  ...basePayload.inputs && Object.keys(basePayload.inputs).length > 0 && { inputs: basePayload.inputs },
4184
- ...basePayload.context && { context: basePayload.context }
4586
+ ...basePayload.context && { context: basePayload.context },
4587
+ // Forward per-turn WebMCP tools snapshotted by buildPayload(). The
4588
+ // client-token chat endpoint accepts the same shape as /v1/dispatch.
4589
+ ...basePayload.clientTools && basePayload.clientTools.length > 0 && { clientTools: basePayload.clientTools }
4185
4590
  };
4186
4591
  if (this.debug) {
4187
4592
  console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
@@ -4403,18 +4808,31 @@ var AgentWidgetClient = class {
4403
4808
  * (client-executed) tools. Used by the built-in `ask_user_question`
4404
4809
  * answer-pill sheet, but generic enough for any LOCAL tool.
4405
4810
  *
4406
- * Posts to the upstream `/resume` endpoint (the dispatch URL with
4407
- * `/dispatch` replaced by `/resume` — works for both direct-to-Runtype
4408
- * and the persona proxy) and returns the raw Response so the caller can
4409
- * pipe its SSE body through `connectStream()`.
4811
+ * Routes by mode:
4812
+ * - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
4813
+ * session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
4814
+ * with the active `sessionId` in the body and no Bearer key — a browser
4815
+ * client-token page holds no secret. `clientTools` are already persisted
4816
+ * server-side from the dispatch turn, so only `toolOutputs` is re-sent.
4817
+ * - **dispatch / proxy mode**: POST `${apiUrl}/resume` — Runtype mounts
4818
+ * resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
4819
+ * and proxies follow the same shape (`/api/chat/dispatch/resume`).
4820
+ *
4821
+ * Returns the raw Response so the caller can pipe its SSE body through
4822
+ * `connectStream()`.
4410
4823
  *
4411
4824
  * @param executionId - The paused execution id carried on `step_await`.
4412
- * @param toolOutputs - Map keyed by tool name → the tool's result value.
4825
+ * @param toolOutputs - Map keyed by per-call `toolCallId` (core#3878),
4826
+ * falling back to tool name for legacy servers → the tool's result value.
4413
4827
  */
4414
4828
  async resumeFlow(executionId, toolOutputs, options) {
4415
4829
  var _a, _b;
4416
- const trimmed = ((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "")) || DEFAULT_CLIENT_API_BASE;
4417
- const url = `${trimmed}/resume`;
4830
+ const isClientToken = this.isClientTokenMode();
4831
+ const url = isClientToken ? this.getClientApiUrl("resume") : `${((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "")) || DEFAULT_CLIENT_API_BASE}/resume`;
4832
+ let resumeSessionId;
4833
+ if (isClientToken) {
4834
+ resumeSessionId = (await this.initSession()).sessionId;
4835
+ }
4418
4836
  let headers = {
4419
4837
  "Content-Type": "application/json",
4420
4838
  ...this.headers
@@ -4422,17 +4840,23 @@ var AgentWidgetClient = class {
4422
4840
  if (this.getHeaders) {
4423
4841
  Object.assign(headers, await this.getHeaders());
4424
4842
  }
4843
+ const body = {
4844
+ executionId,
4845
+ toolOutputs,
4846
+ streamResponse: (_b = options == null ? void 0 : options.streamResponse) != null ? _b : true
4847
+ };
4848
+ if (resumeSessionId) {
4849
+ body.sessionId = resumeSessionId;
4850
+ }
4425
4851
  return fetch(url, {
4426
4852
  method: "POST",
4427
4853
  headers,
4428
- body: JSON.stringify({
4429
- executionId,
4430
- toolOutputs,
4431
- streamResponse: (_b = options == null ? void 0 : options.streamResponse) != null ? _b : true
4432
- })
4854
+ body: JSON.stringify(body),
4855
+ signal: options == null ? void 0 : options.signal
4433
4856
  });
4434
4857
  }
4435
4858
  async buildAgentPayload(messages) {
4859
+ var _a;
4436
4860
  if (!this.config.agent) {
4437
4861
  throw new Error("Agent configuration required for agent mode");
4438
4862
  }
@@ -4441,10 +4865,10 @@ var AgentWidgetClient = class {
4441
4865
  const timeB = new Date(b.createdAt).getTime();
4442
4866
  return timeA - timeB;
4443
4867
  }).map((message) => {
4444
- var _a, _b, _c;
4868
+ var _a2, _b, _c;
4445
4869
  return {
4446
4870
  role: message.role,
4447
- content: (_c = (_b = (_a = message.contentParts) != null ? _a : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4871
+ content: (_c = (_b = (_a2 = message.contentParts) != null ? _a2 : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4448
4872
  createdAt: message.createdAt
4449
4873
  };
4450
4874
  });
@@ -4457,6 +4881,10 @@ var AgentWidgetClient = class {
4457
4881
  ...this.config.agentOptions
4458
4882
  }
4459
4883
  };
4884
+ const webMcpSnapshot = await ((_a = this.webMcpBridge) == null ? void 0 : _a.snapshotForDispatch());
4885
+ if (webMcpSnapshot && webMcpSnapshot.length > 0) {
4886
+ payload.clientTools = webMcpSnapshot;
4887
+ }
4460
4888
  if (this.contextProviders.length) {
4461
4889
  const contextAggregate = {};
4462
4890
  await Promise.all(
@@ -4483,16 +4911,17 @@ var AgentWidgetClient = class {
4483
4911
  return payload;
4484
4912
  }
4485
4913
  async buildPayload(messages) {
4914
+ var _a;
4486
4915
  const normalizedMessages = messages.slice().filter(hasValidContent).sort((a, b) => {
4487
4916
  const timeA = new Date(a.createdAt).getTime();
4488
4917
  const timeB = new Date(b.createdAt).getTime();
4489
4918
  return timeA - timeB;
4490
4919
  }).map((message) => {
4491
- var _a, _b, _c;
4920
+ var _a2, _b, _c;
4492
4921
  return {
4493
4922
  role: message.role,
4494
4923
  // Priority: contentParts (multi-modal) > llmContent (explicit LLM content) > rawContent (structured parsers) > content (display)
4495
- content: (_c = (_b = (_a = message.contentParts) != null ? _a : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4924
+ content: (_c = (_b = (_a2 = message.contentParts) != null ? _a2 : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4496
4925
  createdAt: message.createdAt
4497
4926
  };
4498
4927
  });
@@ -4500,6 +4929,10 @@ var AgentWidgetClient = class {
4500
4929
  messages: normalizedMessages,
4501
4930
  ...this.config.flowId && { flowId: this.config.flowId }
4502
4931
  };
4932
+ const webMcpSnapshot = await ((_a = this.webMcpBridge) == null ? void 0 : _a.snapshotForDispatch());
4933
+ if (webMcpSnapshot && webMcpSnapshot.length > 0) {
4934
+ payload.clientTools = webMcpSnapshot;
4935
+ }
4503
4936
  if (this.contextProviders.length) {
4504
4937
  const contextAggregate = {};
4505
4938
  await Promise.all(
@@ -4530,7 +4963,11 @@ var AgentWidgetClient = class {
4530
4963
  config: this.config
4531
4964
  });
4532
4965
  if (result && typeof result === "object") {
4533
- return result;
4966
+ const next = result;
4967
+ if (payload.clientTools !== void 0 && !("clientTools" in next)) {
4968
+ next.clientTools = payload.clientTools;
4969
+ }
4970
+ return next;
4534
4971
  }
4535
4972
  } catch (error) {
4536
4973
  if (typeof console !== "undefined") {
@@ -5167,7 +5604,8 @@ var AgentWidgetClient = class {
5167
5604
  toolContext.byCall.delete(callKey);
5168
5605
  }
5169
5606
  } else if (payloadType === "step_await" && payload.awaitReason === "local_tool_required" && payload.toolName) {
5170
- const toolId = (_X = payload.toolId) != null ? _X : `local-${nextSequence()}`;
5607
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.length > 0 ? payload.toolCallId : void 0;
5608
+ const toolId = (_X = toolCallId != null ? toolCallId : payload.toolId) != null ? _X : `local-${nextSequence()}`;
5171
5609
  const toolMessage = ensureToolMessage(toolId);
5172
5610
  const tool = (_Y = toolMessage.toolCall) != null ? _Y : { id: toolId, status: "pending" };
5173
5611
  tool.name = payload.toolName;
@@ -5181,7 +5619,11 @@ var AgentWidgetClient = class {
5181
5619
  toolMessage.agentMetadata = {
5182
5620
  ...toolMessage.agentMetadata,
5183
5621
  executionId: (_ca = payload.executionId) != null ? _ca : (_ba = toolMessage.agentMetadata) == null ? void 0 : _ba.executionId,
5184
- awaitingLocalTool: true
5622
+ awaitingLocalTool: true,
5623
+ // Only set when the server emitted a real per-call id; its presence
5624
+ // is what tells session.ts to batch + key `/resume` by id rather
5625
+ // than by tool name (which can't represent two same-tool calls).
5626
+ ...toolCallId ? { webMcpToolCallId: toolCallId } : {}
5185
5627
  };
5186
5628
  emitMessage(toolMessage);
5187
5629
  } else if (payloadType === "text_start") {
@@ -7130,6 +7572,15 @@ function isVoiceSupported(config) {
7130
7572
  }
7131
7573
 
7132
7574
  // src/session.ts
7575
+ function buildDispatchErrorContent(error, override) {
7576
+ const err = error instanceof Error ? error : new Error(String(error));
7577
+ if (typeof override === "string") return override;
7578
+ if (typeof override === "function") return override(err);
7579
+ const base = "Sorry \u2014 I couldn't reach the assistant. The chat service didn't respond. Please check that your proxy or backend is running and reachable, then try again.";
7580
+ return err.message ? `${base}
7581
+
7582
+ _Details: ${err.message}_` : base;
7583
+ }
7133
7584
  var AgentWidgetSession = class _AgentWidgetSession {
7134
7585
  constructor(config = {}, callbacks) {
7135
7586
  this.config = config;
@@ -7144,6 +7595,62 @@ var AgentWidgetSession = class _AgentWidgetSession {
7144
7595
  this.agentExecution = null;
7145
7596
  this.artifacts = /* @__PURE__ */ new Map();
7146
7597
  this.selectedArtifactId = null;
7598
+ // WebMCP dedupe — keys are `${executionId}:${toolCallId}` so they're
7599
+ // naturally scoped to a single dispatch. A later dispatch (new executionId)
7600
+ // that happens to recycle a `toolCall.id` never collides with prior entries,
7601
+ // and a stale re-emit from an in-flight prior dispatch stays blocked because
7602
+ // its executionId is still in the set.
7603
+ //
7604
+ // webMcpInflightKeys — currently executing; cleared on completion of
7605
+ // EITHER /resume success OR /resume throw. Blocks
7606
+ // concurrent re-fire during the resolve round-trip.
7607
+ // webMcpResolvedKeys — /resume HTTP returned 2xx; not cleared on a new
7608
+ // dispatch (executionId scoping makes that
7609
+ // unnecessary). Blocks stale step_await re-emits
7610
+ // for the same execution.
7611
+ //
7612
+ // If `/resume` throws (network error, server 5xx), we DO want a retry path:
7613
+ // the dispatch is recoverable. Such a tool stays in neither set, so a
7614
+ // subsequent re-emit will re-trigger.
7615
+ this.webMcpInflightKeys = /* @__PURE__ */ new Set();
7616
+ this.webMcpResolvedKeys = /* @__PURE__ */ new Set();
7617
+ // Per-resolve AbortControllers, kept in a set so multiple `webmcp:*`
7618
+ // step_await resolves in one turn never abort one another. The shared
7619
+ // `this.abortController` is intentionally NOT used by resolveWebMcpToolCall:
7620
+ // in a CHAINED turn (tool A → /resume → tool B, where the server emits B's
7621
+ // step_await inside A's resume SSE stream) the shared controller is still
7622
+ // piping A's resume stream — the very stream that just delivered B. Aborting
7623
+ // it mid-chain (the prior shared-controller pre-abort) tore that stream down,
7624
+ // so B never reached execute() and its /resume was never POSTed, pausing the
7625
+ // dispatch forever. cancel(), clearMessages(), hydrateMessages(), and
7626
+ // sendMessage() iterate this set to tear every in-flight resolve down on a
7627
+ // real stop / new turn.
7628
+ this.webMcpResolveControllers = /* @__PURE__ */ new Set();
7629
+ // Bumped on every teardown / new-turn boundary (cancel, clearMessages,
7630
+ // hydrateMessages, sendMessage). A resolveWebMcpToolCall deferred via
7631
+ // queueMicrotask captures the epoch at queue time and bails if it changed,
7632
+ // so a resolve queued just before a teardown can't escape it by installing a
7633
+ // fresh controller after the set was already cleared.
7634
+ this.webMcpEpoch = 0;
7635
+ // WebMCP native approval-bubble gate. When no custom `webmcp.onConfirm` is
7636
+ // supplied, the bridge's confirm handler routes here: we inject an
7637
+ // approval-variant message and park the bridge on a Promise that resolves
7638
+ // when the user clicks Approve/Deny (see requestWebMcpApproval /
7639
+ // resolveWebMcpApproval). Resolvers are keyed by the approval message id.
7640
+ this.webMcpApprovalResolvers = /* @__PURE__ */ new Map();
7641
+ this.webMcpApprovalSeq = 0;
7642
+ // Parallel local-tool batching (core#3878). A single model turn can emit
7643
+ // multiple `step_await(local_tool_required)` events for ONE paused
7644
+ // executionId — including two PARALLEL calls to the SAME tool ("add SHOE-001
7645
+ // and SHOE-007"). Those collapse to an identical `toolId`/`index` and differ
7646
+ // only by the per-call `webMcpToolCallId`. We collect all awaits for an
7647
+ // executionId that arrive in the same stream tick, then post ONE `/resume`
7648
+ // keyed by `webMcpToolCallId` — NOT one `/resume` per tool keyed by name
7649
+ // (which collides for same-tool calls, and whose concurrent posts on one
7650
+ // execution raced → the second 404'd → the turn hung). Keyed by executionId;
7651
+ // `seen` dedupes duplicate step_await re-emits within a batch. Cleared on
7652
+ // every teardown via `abortWebMcpResolves`.
7653
+ this.webMcpAwaitBatches = /* @__PURE__ */ new Map();
7147
7654
  // Voice support
7148
7655
  this.voiceProvider = null;
7149
7656
  this.voiceActive = false;
@@ -7155,17 +7662,21 @@ var AgentWidgetSession = class _AgentWidgetSession {
7155
7662
  // so browser TTS doesn't double-speak them
7156
7663
  this.ttsSpokenMessageIds = /* @__PURE__ */ new Set();
7157
7664
  this.handleEvent = (event) => {
7158
- var _a, _b, _c, _d, _e, _f, _g;
7665
+ var _a, _b, _c, _d, _e, _f, _g, _h;
7159
7666
  if (event.type === "message") {
7160
7667
  this.upsertMessage(event.message);
7161
- if ((_a = event.message.agentMetadata) == null ? void 0 : _a.executionId) {
7668
+ const tc = event.message.toolCall;
7669
+ if (((_a = event.message.agentMetadata) == null ? void 0 : _a.awaitingLocalTool) === true && (tc == null ? void 0 : tc.name) && isWebMcpToolName(tc.name)) {
7670
+ this.enqueueWebMcpAwait(event.message);
7671
+ }
7672
+ if ((_b = event.message.agentMetadata) == null ? void 0 : _b.executionId) {
7162
7673
  if (!this.agentExecution) {
7163
7674
  this.agentExecution = {
7164
7675
  executionId: event.message.agentMetadata.executionId,
7165
7676
  agentId: "",
7166
- agentName: (_b = event.message.agentMetadata.agentName) != null ? _b : "",
7677
+ agentName: (_c = event.message.agentMetadata.agentName) != null ? _c : "",
7167
7678
  status: "running",
7168
- currentIteration: (_c = event.message.agentMetadata.iteration) != null ? _c : 0,
7679
+ currentIteration: (_d = event.message.agentMetadata.iteration) != null ? _d : 0,
7169
7680
  maxTurns: 0
7170
7681
  };
7171
7682
  } else if (event.message.agentMetadata.iteration !== void 0) {
@@ -7177,20 +7688,30 @@ var AgentWidgetSession = class _AgentWidgetSession {
7177
7688
  if (event.status === "connecting") {
7178
7689
  this.setStreaming(true);
7179
7690
  } else if (event.status === "idle" || event.status === "error") {
7180
- this.setStreaming(false);
7181
- this.abortController = null;
7182
- if (((_d = this.agentExecution) == null ? void 0 : _d.status) === "running") {
7183
- this.agentExecution.status = event.status === "error" ? "error" : "complete";
7691
+ if (this.webMcpResolveControllers.size === 0) {
7692
+ this.setStreaming(false);
7693
+ this.abortController = null;
7184
7694
  }
7695
+ const webMcpPending = this.webMcpAwaitBatches.size > 0 || this.webMcpResolveControllers.size > 0;
7696
+ if (((_e = this.agentExecution) == null ? void 0 : _e.status) === "running") {
7697
+ if (event.status === "error") {
7698
+ this.agentExecution.status = "error";
7699
+ } else if (!webMcpPending) {
7700
+ this.agentExecution.status = "complete";
7701
+ }
7702
+ }
7703
+ this.scheduleWebMcpBatchFlush();
7185
7704
  }
7186
7705
  } else if (event.type === "error") {
7187
7706
  this.setStatus("error");
7188
- this.setStreaming(false);
7189
- this.abortController = null;
7190
- if (((_e = this.agentExecution) == null ? void 0 : _e.status) === "running") {
7707
+ if (this.webMcpResolveControllers.size === 0) {
7708
+ this.setStreaming(false);
7709
+ this.abortController = null;
7710
+ }
7711
+ if (((_f = this.agentExecution) == null ? void 0 : _f.status) === "running") {
7191
7712
  this.agentExecution.status = "error";
7192
7713
  }
7193
- (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(_f, event.error);
7714
+ (_h = (_g = this.callbacks).onError) == null ? void 0 : _h.call(_g, event.error);
7194
7715
  } else if (event.type === "artifact_start" || event.type === "artifact_delta" || event.type === "artifact_update" || event.type === "artifact_complete") {
7195
7716
  this.applyArtifactStreamEvent(event);
7196
7717
  }
@@ -7205,6 +7726,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7205
7726
  });
7206
7727
  this.messages = this.sortMessages(this.messages);
7207
7728
  this.client = new AgentWidgetClient(config);
7729
+ this.wireDefaultWebMcpConfirm();
7208
7730
  for (const rec of (_b = config.initialArtifacts) != null ? _b : []) {
7209
7731
  this.artifacts.set(rec.id, { ...rec, status: "complete" });
7210
7732
  }
@@ -7562,9 +8084,13 @@ var AgentWidgetSession = class _AgentWidgetSession {
7562
8084
  return this.client.submitNPSFeedback(rating, comment);
7563
8085
  }
7564
8086
  updateConfig(next) {
8087
+ this.abortWebMcpResolves();
8088
+ this.webMcpInflightKeys.clear();
8089
+ this.webMcpResolvedKeys.clear();
7565
8090
  const prevSSECallback = this.client.getSSEEventCallback();
7566
8091
  this.config = { ...this.config, ...next };
7567
8092
  this.client = new AgentWidgetClient(this.config);
8093
+ this.wireDefaultWebMcpConfirm();
7568
8094
  if (prevSSECallback) {
7569
8095
  this.client.setSSEEventCallback(prevSSECallback);
7570
8096
  }
@@ -7770,6 +8296,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7770
8296
  if (!input && (!(options == null ? void 0 : options.contentParts) || options.contentParts.length === 0)) return;
7771
8297
  this.stopSpeaking();
7772
8298
  (_a = this.abortController) == null ? void 0 : _a.abort();
8299
+ this.abortWebMcpResolves();
7773
8300
  const userMessageId = generateUserMessageId();
7774
8301
  const assistantMessageId = generateAssistantMessageId();
7775
8302
  const userMessage = {
@@ -7803,15 +8330,21 @@ var AgentWidgetSession = class _AgentWidgetSession {
7803
8330
  } catch (error) {
7804
8331
  const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
7805
8332
  if (!isAbortError) {
7806
- const fallback = {
7807
- id: assistantMessageId,
7808
- // Use the pre-generated ID for fallback too
7809
- role: "assistant",
7810
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7811
- content: "It looks like the proxy isn't returning a real response yet. Here's a sample message so you can continue testing locally.",
7812
- sequence: this.nextSequence()
7813
- };
7814
- this.appendMessage(fallback);
8333
+ const content = buildDispatchErrorContent(
8334
+ error,
8335
+ this.config.errorMessage
8336
+ );
8337
+ if (content) {
8338
+ const fallback = {
8339
+ id: assistantMessageId,
8340
+ // Use the pre-generated ID for fallback too
8341
+ role: "assistant",
8342
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8343
+ content,
8344
+ sequence: this.nextSequence()
8345
+ };
8346
+ this.appendMessage(fallback);
8347
+ }
7815
8348
  }
7816
8349
  this.setStatus("idle");
7817
8350
  this.setStreaming(false);
@@ -7856,21 +8389,32 @@ var AgentWidgetSession = class _AgentWidgetSession {
7856
8389
  this.handleEvent
7857
8390
  );
7858
8391
  } catch (error) {
7859
- const fallback = {
7860
- id: assistantMessageId,
7861
- role: "assistant",
7862
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7863
- content: "It looks like the proxy isn't returning a real response yet. Here's a sample message so you can continue testing locally.",
7864
- sequence: this.nextSequence()
7865
- };
7866
- this.appendMessage(fallback);
8392
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8393
+ if (!isAbortError) {
8394
+ const content = buildDispatchErrorContent(
8395
+ error,
8396
+ this.config.errorMessage
8397
+ );
8398
+ if (content) {
8399
+ const fallback = {
8400
+ id: assistantMessageId,
8401
+ role: "assistant",
8402
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8403
+ content,
8404
+ sequence: this.nextSequence()
8405
+ };
8406
+ this.appendMessage(fallback);
8407
+ }
8408
+ }
7867
8409
  this.setStatus("idle");
7868
8410
  this.setStreaming(false);
7869
8411
  this.abortController = null;
7870
- if (error instanceof Error) {
7871
- (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(_b, error);
7872
- } else {
7873
- (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(_d, new Error(String(error)));
8412
+ if (!isAbortError) {
8413
+ if (error instanceof Error) {
8414
+ (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(_b, error);
8415
+ } else {
8416
+ (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(_d, new Error(String(error)));
8417
+ }
7874
8418
  }
7875
8419
  }
7876
8420
  }
@@ -7903,14 +8447,92 @@ var AgentWidgetSession = class _AgentWidgetSession {
7903
8447
  );
7904
8448
  } catch (error) {
7905
8449
  this.setStatus("error");
7906
- this.setStreaming(false);
7907
- this.abortController = null;
8450
+ if (this.webMcpResolveControllers.size === 0) {
8451
+ this.setStreaming(false);
8452
+ this.abortController = null;
8453
+ }
7908
8454
  (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
7909
8455
  _b,
7910
8456
  error instanceof Error ? error : new Error(String(error))
7911
8457
  );
7912
8458
  }
7913
8459
  }
8460
+ /**
8461
+ * Install the native approval-bubble confirm handler on the WebMCP bridge
8462
+ * when the integrator hasn't supplied a custom `webmcp.onConfirm`. Without
8463
+ * this, the bridge falls back to a blunt `window.confirm`. Safe to call
8464
+ * repeatedly (e.g. after the client is re-created in `updateConfig`).
8465
+ */
8466
+ wireDefaultWebMcpConfirm() {
8467
+ const webmcp = this.config.webmcp;
8468
+ if ((webmcp == null ? void 0 : webmcp.enabled) === true && !webmcp.onConfirm) {
8469
+ this.client.setWebMcpConfirmHandler(
8470
+ (info) => this.requestWebMcpApproval(info)
8471
+ );
8472
+ }
8473
+ }
8474
+ /**
8475
+ * Default WebMCP confirm gate: render Persona's native in-panel approval
8476
+ * bubble and resolve when the user clicks Approve/Deny. Returns immediately
8477
+ * with `true` when `webmcp.autoApprove(info)` opts the tool out of the gate
8478
+ * (e.g. a read-only catalog search), so no bubble is shown. The bridge
8479
+ * awaits this Promise before executing the page tool.
8480
+ */
8481
+ requestWebMcpApproval(info) {
8482
+ var _a, _b, _c;
8483
+ try {
8484
+ if (((_b = (_a = this.config.webmcp) == null ? void 0 : _a.autoApprove) == null ? void 0 : _b.call(_a, info)) === true) {
8485
+ return Promise.resolve(true);
8486
+ }
8487
+ } catch {
8488
+ }
8489
+ const approval = {
8490
+ id: `webmcp-${++this.webMcpApprovalSeq}`,
8491
+ status: "pending",
8492
+ agentId: "",
8493
+ executionId: "",
8494
+ toolName: info.toolName,
8495
+ toolType: "webmcp",
8496
+ description: (_c = info.description) != null ? _c : `Allow the assistant to run ${info.toolName}?`,
8497
+ parameters: info.args
8498
+ };
8499
+ const approvalMessageId = `approval-${approval.id}`;
8500
+ this.upsertMessage({
8501
+ id: approvalMessageId,
8502
+ role: "assistant",
8503
+ content: "",
8504
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8505
+ streaming: false,
8506
+ variant: "approval",
8507
+ approval
8508
+ });
8509
+ return new Promise((resolve) => {
8510
+ this.webMcpApprovalResolvers.set(approvalMessageId, resolve);
8511
+ });
8512
+ }
8513
+ /**
8514
+ * Resolve a pending WebMCP approval bubble (from the Approve/Deny click in
8515
+ * `ui.ts`). Updates the bubble to its resolved state and unblocks the
8516
+ * bridge Promise parked in `requestWebMcpApproval`. No-op if already
8517
+ * resolved (double-click, re-render).
8518
+ */
8519
+ resolveWebMcpApproval(approvalMessageId, decision) {
8520
+ const resolve = this.webMcpApprovalResolvers.get(approvalMessageId);
8521
+ if (!resolve) return;
8522
+ this.webMcpApprovalResolvers.delete(approvalMessageId);
8523
+ const existing = this.messages.find((m) => m.id === approvalMessageId);
8524
+ if (existing == null ? void 0 : existing.approval) {
8525
+ this.upsertMessage({
8526
+ ...existing,
8527
+ approval: {
8528
+ ...existing.approval,
8529
+ status: decision,
8530
+ resolvedAt: Date.now()
8531
+ }
8532
+ });
8533
+ }
8534
+ resolve(decision === "approved");
8535
+ }
7914
8536
  /**
7915
8537
  * Resolve a tool approval request (approve or deny).
7916
8538
  * Updates the approval message status, calls the API (or custom onDecision),
@@ -8144,18 +8766,386 @@ var AgentWidgetSession = class _AgentWidgetSession {
8144
8766
  const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8145
8767
  this.setStreaming(false);
8146
8768
  this.abortController = null;
8147
- if (!isAbortError) {
8148
- (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
8149
- _k,
8150
- error instanceof Error ? error : new Error(String(error))
8151
- );
8152
- }
8769
+ if (!isAbortError) {
8770
+ (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
8771
+ _k,
8772
+ error instanceof Error ? error : new Error(String(error))
8773
+ );
8774
+ }
8775
+ }
8776
+ }
8777
+ /**
8778
+ * Collect a `webmcp:*` LOCAL-tool `step_await` into a per-executionId batch
8779
+ * and schedule a single deferred flush. Parallel calls (core#3878) emit
8780
+ * several `step_await`s for ONE paused execution within the same stream tick;
8781
+ * buffering them and flushing once lets us post ONE `/resume` keyed by the
8782
+ * per-call `webMcpToolCallId` rather than racing N name-keyed resumes on the
8783
+ * same execution (which 404'd on the second and hung the turn).
8784
+ *
8785
+ * Deferred via `queueMicrotask` (epoch-guarded) for the same reason the old
8786
+ * direct resolve was: handleEvent must return first so the dispatch's
8787
+ * `connectStream` sees end-of-stream and releases the shared abortController
8788
+ * before a resolve grabs it.
8789
+ *
8790
+ * Awaits without an `executionId` or `toolCall.id` can't be batched (no key)
8791
+ * — route them straight to the single-call path, which surfaces the malformed
8792
+ * wire shape via `onError` / an `isError` resume.
8793
+ */
8794
+ enqueueWebMcpAwait(toolMessage) {
8795
+ var _a, _b;
8796
+ const executionId = (_a = toolMessage.agentMetadata) == null ? void 0 : _a.executionId;
8797
+ const callId = (_b = toolMessage.toolCall) == null ? void 0 : _b.id;
8798
+ if (!executionId || !callId) {
8799
+ const queuedEpoch = this.webMcpEpoch;
8800
+ queueMicrotask(() => {
8801
+ if (queuedEpoch !== this.webMcpEpoch) return;
8802
+ void this.resolveWebMcpToolCall(toolMessage);
8803
+ });
8804
+ return;
8805
+ }
8806
+ let batch = this.webMcpAwaitBatches.get(executionId);
8807
+ if (!batch) {
8808
+ batch = { snapshots: [], seen: /* @__PURE__ */ new Set() };
8809
+ this.webMcpAwaitBatches.set(executionId, batch);
8810
+ }
8811
+ if (batch.seen.has(callId)) return;
8812
+ batch.seen.add(callId);
8813
+ batch.snapshots.push(toolMessage);
8814
+ }
8815
+ /**
8816
+ * Flush every buffered local-tool await batch, one `/resume` per executionId.
8817
+ * Called once a stream ends (`status: idle` / `error`) — by then all parallel
8818
+ * `step_await`s the stream carried have been collected, even if split across
8819
+ * SSE chunks. Deferred via `queueMicrotask` (epoch-guarded) so the idle
8820
+ * handler returns first and the stream's end-of-stream teardown (streaming /
8821
+ * abortController) settles before a resolve grabs them — the same ordering the
8822
+ * single-call resolve always relied on.
8823
+ */
8824
+ scheduleWebMcpBatchFlush() {
8825
+ if (this.webMcpAwaitBatches.size === 0) return;
8826
+ const queuedEpoch = this.webMcpEpoch;
8827
+ queueMicrotask(() => {
8828
+ if (queuedEpoch !== this.webMcpEpoch) return;
8829
+ for (const executionId of [...this.webMcpAwaitBatches.keys()]) {
8830
+ this.flushWebMcpAwaitBatch(executionId);
8831
+ }
8832
+ });
8833
+ }
8834
+ /**
8835
+ * Run a buffered batch of local-tool awaits for one executionId. Size 1
8836
+ * (single call, or distinct-tool turns that happened to arrive alone) takes
8837
+ * the original single-call path; size >1 (parallel calls) takes the batched
8838
+ * path that posts ONE `/resume`. The batch is removed from the map up front
8839
+ * so any later sibling re-emit (e.g. from a re-pause) forms a fresh batch
8840
+ * rather than mutating one already in flight.
8841
+ */
8842
+ flushWebMcpAwaitBatch(executionId) {
8843
+ const batch = this.webMcpAwaitBatches.get(executionId);
8844
+ if (!batch) return;
8845
+ this.webMcpAwaitBatches.delete(executionId);
8846
+ const { snapshots } = batch;
8847
+ if (snapshots.length === 1) {
8848
+ void this.resolveWebMcpToolCall(snapshots[0]);
8849
+ } else if (snapshots.length > 1) {
8850
+ void this.resolveWebMcpToolCallBatch(executionId, snapshots);
8851
+ }
8852
+ }
8853
+ /**
8854
+ * Resolve TWO OR MORE parallel local-tool awaits sharing one paused
8855
+ * executionId with a SINGLE `/resume` (core#3878). Each call is executed
8856
+ * against the page registry concurrently — every gated call renders its own
8857
+ * native approval bubble, and a sibling's confirm Promise never blocks
8858
+ * another's execution. Outputs are keyed by per-call `webMcpToolCallId`
8859
+ * (server prefers it over tool name; name-keying remains the fallback for
8860
+ * legacy single/distinct-tool turns), so two calls to the SAME tool no longer
8861
+ * collide. The server is tolerant: any call we omit (declined-after-abort,
8862
+ * dedupe, exec failure) simply re-pauses and is retried on its re-emit.
8863
+ *
8864
+ * Mirrors `resolveWebMcpToolCall`'s dedupe / abort / streaming machinery, but
8865
+ * shares one resume POST and marks every resolved key on that POST's HTTP OK.
8866
+ */
8867
+ async resolveWebMcpToolCallBatch(executionId, snapshots) {
8868
+ var _a, _b, _c;
8869
+ const claimedKeys = [];
8870
+ const controllers = [];
8871
+ const resumeController = new AbortController();
8872
+ this.webMcpResolveControllers.add(resumeController);
8873
+ this.setStreaming(true);
8874
+ const executed = await Promise.all(
8875
+ snapshots.map(async (toolMessage) => {
8876
+ var _a2, _b2, _c2, _d, _e, _f, _g;
8877
+ const wireToolName = (_a2 = toolMessage.toolCall) == null ? void 0 : _a2.name;
8878
+ const callId = (_b2 = toolMessage.toolCall) == null ? void 0 : _b2.id;
8879
+ if (!wireToolName || !callId) return null;
8880
+ const dedupeKey = `${executionId}:${callId}`;
8881
+ if (this.webMcpInflightKeys.has(dedupeKey) || this.webMcpResolvedKeys.has(dedupeKey)) {
8882
+ return null;
8883
+ }
8884
+ this.webMcpInflightKeys.add(dedupeKey);
8885
+ claimedKeys.push(dedupeKey);
8886
+ this.upsertMessage({
8887
+ ...toolMessage,
8888
+ agentMetadata: {
8889
+ ...toolMessage.agentMetadata,
8890
+ awaitingLocalTool: false
8891
+ }
8892
+ });
8893
+ const controller = new AbortController();
8894
+ this.webMcpResolveControllers.add(controller);
8895
+ controllers.push(controller);
8896
+ const resumeKey = (_d = (_c2 = toolMessage.agentMetadata) == null ? void 0 : _c2.webMcpToolCallId) != null ? _d : wireToolName;
8897
+ const execPromise = this.client.executeWebMcpToolCall(
8898
+ wireToolName,
8899
+ (_e = toolMessage.toolCall) == null ? void 0 : _e.args,
8900
+ controller.signal
8901
+ );
8902
+ let output;
8903
+ if (!execPromise) {
8904
+ output = {
8905
+ isError: true,
8906
+ content: [
8907
+ { type: "text", text: "WebMCP not enabled on this widget." }
8908
+ ]
8909
+ };
8910
+ } else {
8911
+ try {
8912
+ output = await execPromise;
8913
+ } catch (error) {
8914
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8915
+ if (!isAbortError) {
8916
+ (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(
8917
+ _f,
8918
+ error instanceof Error ? error : new Error(String(error))
8919
+ );
8920
+ }
8921
+ this.webMcpInflightKeys.delete(dedupeKey);
8922
+ return null;
8923
+ }
8924
+ }
8925
+ if (controller.signal.aborted) {
8926
+ this.webMcpInflightKeys.delete(dedupeKey);
8927
+ return null;
8928
+ }
8929
+ return { dedupeKey, resumeKey, output };
8930
+ })
8931
+ );
8932
+ try {
8933
+ const ready = executed.filter(
8934
+ (r) => r !== null
8935
+ );
8936
+ if (ready.length === 0) return;
8937
+ const toolOutputs = {};
8938
+ for (const r of ready) {
8939
+ toolOutputs[r.resumeKey] = r.output;
8940
+ }
8941
+ const response = await this.client.resumeFlow(executionId, toolOutputs, {
8942
+ signal: resumeController.signal
8943
+ });
8944
+ if (!response.ok) {
8945
+ const errorData = await response.json().catch(() => null);
8946
+ throw new Error((_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Resume failed: ${response.status}`);
8947
+ }
8948
+ for (const r of ready) {
8949
+ this.webMcpResolvedKeys.add(r.dedupeKey);
8950
+ }
8951
+ if (response.body) {
8952
+ await this.connectStream(response.body, { allowReentry: true });
8953
+ }
8954
+ } catch (error) {
8955
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8956
+ if (!isAbortError) {
8957
+ (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
8958
+ _b,
8959
+ error instanceof Error ? error : new Error(String(error))
8960
+ );
8961
+ }
8962
+ } finally {
8963
+ for (const key of claimedKeys) {
8964
+ this.webMcpInflightKeys.delete(key);
8965
+ }
8966
+ for (const controller of controllers) {
8967
+ this.webMcpResolveControllers.delete(controller);
8968
+ }
8969
+ this.webMcpResolveControllers.delete(resumeController);
8970
+ if (this.webMcpResolveControllers.size === 0 && !this.abortController) {
8971
+ this.setStreaming(false);
8972
+ }
8973
+ }
8974
+ }
8975
+ /**
8976
+ * Resolve a paused `webmcp:*` LOCAL tool call by executing it against the
8977
+ * host page's tool registry and posting the result to `/resume`.
8978
+ *
8979
+ * Triggered automatically from `handleEvent` when a `step_await`-derived
8980
+ * message arrives with a `webmcp:` prefix — the user does not click a
8981
+ * pill; the bridge's confirm-bubble gate is the only interactive surface.
8982
+ *
8983
+ * Idempotent on the message's `toolCall.id`: re-emits of the same step_await
8984
+ * (e.g. from message coalescing) won't double-fire `tool.execute`. Failure
8985
+ * modes — declined, timed out, throw, unknown tool — all resolve into a
8986
+ * `{ isError: true, content: [...] }` payload that resumes the dispatch
8987
+ * cleanly so the agent can recover.
8988
+ */
8989
+ async resolveWebMcpToolCall(toolMessage) {
8990
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
8991
+ const executionId = (_a = toolMessage.agentMetadata) == null ? void 0 : _a.executionId;
8992
+ const wireToolName = (_b = toolMessage.toolCall) == null ? void 0 : _b.name;
8993
+ const toolCallId = (_c = toolMessage.toolCall) == null ? void 0 : _c.id;
8994
+ if (!executionId) {
8995
+ (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(
8996
+ _d,
8997
+ new Error(
8998
+ "WebMCP step_await missing executionId \u2014 dispatch left paused."
8999
+ )
9000
+ );
9001
+ return;
9002
+ }
9003
+ if (!wireToolName) return;
9004
+ if (!toolCallId) {
9005
+ const malformedKey = `${executionId}:__no_tool_id__:${wireToolName}`;
9006
+ if (this.webMcpInflightKeys.has(malformedKey) || this.webMcpResolvedKeys.has(malformedKey)) {
9007
+ return;
9008
+ }
9009
+ this.webMcpInflightKeys.add(malformedKey);
9010
+ try {
9011
+ await this.resumeWithToolOutput(executionId, wireToolName, {
9012
+ isError: true,
9013
+ content: [
9014
+ {
9015
+ type: "text",
9016
+ text: "WebMCP step_await missing toolCall.id \u2014 cannot execute the page tool."
9017
+ }
9018
+ ]
9019
+ });
9020
+ this.webMcpResolvedKeys.add(malformedKey);
9021
+ } catch (error) {
9022
+ (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(
9023
+ _f,
9024
+ error instanceof Error ? error : new Error(String(error))
9025
+ );
9026
+ } finally {
9027
+ this.webMcpInflightKeys.delete(malformedKey);
9028
+ }
9029
+ return;
9030
+ }
9031
+ const dedupeKey = `${executionId}:${toolCallId}`;
9032
+ if (this.webMcpInflightKeys.has(dedupeKey) || this.webMcpResolvedKeys.has(dedupeKey)) {
9033
+ return;
9034
+ }
9035
+ this.webMcpInflightKeys.add(dedupeKey);
9036
+ this.upsertMessage({
9037
+ ...toolMessage,
9038
+ agentMetadata: {
9039
+ ...toolMessage.agentMetadata,
9040
+ awaitingLocalTool: false
9041
+ }
9042
+ });
9043
+ const resolveController = new AbortController();
9044
+ this.webMcpResolveControllers.add(resolveController);
9045
+ const { signal } = resolveController;
9046
+ this.setStreaming(true);
9047
+ const args = (_h = toolMessage.toolCall) == null ? void 0 : _h.args;
9048
+ const execPromise = this.client.executeWebMcpToolCall(
9049
+ wireToolName,
9050
+ args,
9051
+ signal
9052
+ );
9053
+ try {
9054
+ let resumeOutput;
9055
+ if (!execPromise) {
9056
+ resumeOutput = {
9057
+ isError: true,
9058
+ content: [
9059
+ { type: "text", text: "WebMCP not enabled on this widget." }
9060
+ ]
9061
+ };
9062
+ } else {
9063
+ resumeOutput = await execPromise;
9064
+ }
9065
+ if (signal.aborted) {
9066
+ return;
9067
+ }
9068
+ const resumeKey = (_j = (_i = toolMessage.agentMetadata) == null ? void 0 : _i.webMcpToolCallId) != null ? _j : wireToolName;
9069
+ await this.resumeWithToolOutput(executionId, resumeKey, resumeOutput, {
9070
+ onHttpOk: () => {
9071
+ this.webMcpResolvedKeys.add(dedupeKey);
9072
+ },
9073
+ signal
9074
+ });
9075
+ } catch (error) {
9076
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
9077
+ if (!isAbortError) {
9078
+ (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
9079
+ _k,
9080
+ error instanceof Error ? error : new Error(String(error))
9081
+ );
9082
+ }
9083
+ } finally {
9084
+ this.webMcpInflightKeys.delete(dedupeKey);
9085
+ this.webMcpResolveControllers.delete(resolveController);
9086
+ if (this.webMcpResolveControllers.size === 0 && !this.abortController) {
9087
+ this.setStreaming(false);
9088
+ }
9089
+ }
9090
+ }
9091
+ /**
9092
+ * POST `/resume` with a SINGLE tool's output and pipe the resulting SSE
9093
+ * stream back through `connectStream`. Shared by every single-call local-tool
9094
+ * resolve path (ask_user_question and single WebMCP calls). Parallel WebMCP
9095
+ * calls use `resolveWebMcpToolCallBatch`, which posts one resume for many.
9096
+ *
9097
+ * `resumeKey` is the `toolOutputs` map key: the per-call `webMcpToolCallId`
9098
+ * for WebMCP (core#3878), or the tool name for ask_user_question / legacy
9099
+ * servers. `onHttpOk` runs synchronously between the HTTP-status check and the
9100
+ * stream pipe; it lets the WebMCP resolve path commit the dedupe flag at
9101
+ * "server accepted the answer" rather than "stream finished cleanly".
9102
+ */
9103
+ async resumeWithToolOutput(executionId, resumeKey, output, options) {
9104
+ var _a, _b;
9105
+ const response = await this.client.resumeFlow(
9106
+ executionId,
9107
+ { [resumeKey]: output },
9108
+ { signal: options == null ? void 0 : options.signal }
9109
+ );
9110
+ if (!response.ok) {
9111
+ const errorData = await response.json().catch(() => null);
9112
+ throw new Error((_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Resume failed: ${response.status}`);
9113
+ }
9114
+ (_b = options == null ? void 0 : options.onHttpOk) == null ? void 0 : _b.call(options);
9115
+ if (response.body) {
9116
+ await this.connectStream(response.body, { allowReentry: true });
9117
+ } else if (this.webMcpResolveControllers.size === 0) {
9118
+ this.setStreaming(false);
9119
+ this.abortController = null;
9120
+ }
9121
+ }
9122
+ /**
9123
+ * Tear down every in-flight WebMCP resolve and advance the epoch. Each
9124
+ * resolve owns a dedicated AbortController (chained/parallel resolves don't
9125
+ * share one), so we abort them individually; the aborts propagate into the
9126
+ * bridge's execute race and into each `/resume` fetch signal. Bumping
9127
+ * `webMcpEpoch` strands any resolve still deferred in a queued microtask —
9128
+ * it captured the prior epoch and bails before installing a fresh
9129
+ * controller, so it can't escape this teardown. Called from every stop /
9130
+ * new-turn boundary (cancel, clearMessages, hydrateMessages, sendMessage).
9131
+ */
9132
+ abortWebMcpResolves() {
9133
+ for (const controller of this.webMcpResolveControllers) {
9134
+ controller.abort();
9135
+ }
9136
+ this.webMcpResolveControllers.clear();
9137
+ for (const approvalMessageId of [...this.webMcpApprovalResolvers.keys()]) {
9138
+ this.resolveWebMcpApproval(approvalMessageId, "denied");
8153
9139
  }
9140
+ this.webMcpAwaitBatches.clear();
9141
+ this.webMcpEpoch++;
8154
9142
  }
8155
9143
  cancel() {
8156
9144
  var _a;
8157
9145
  (_a = this.abortController) == null ? void 0 : _a.abort();
8158
9146
  this.abortController = null;
9147
+ this.abortWebMcpResolves();
9148
+ this.webMcpInflightKeys.clear();
8159
9149
  this.stopSpeaking();
8160
9150
  this.stopVoicePlayback();
8161
9151
  this.setStreaming(false);
@@ -8166,9 +9156,12 @@ var AgentWidgetSession = class _AgentWidgetSession {
8166
9156
  this.stopSpeaking();
8167
9157
  (_a = this.abortController) == null ? void 0 : _a.abort();
8168
9158
  this.abortController = null;
9159
+ this.abortWebMcpResolves();
8169
9160
  this.messages = [];
8170
9161
  this.agentExecution = null;
8171
9162
  this.clearArtifactState();
9163
+ this.webMcpInflightKeys.clear();
9164
+ this.webMcpResolvedKeys.clear();
8172
9165
  this.setStreaming(false);
8173
9166
  this.setStatus("idle");
8174
9167
  this.callbacks.onMessagesChanged([...this.messages]);
@@ -8285,6 +9278,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
8285
9278
  var _a;
8286
9279
  (_a = this.abortController) == null ? void 0 : _a.abort();
8287
9280
  this.abortController = null;
9281
+ this.abortWebMcpResolves();
9282
+ this.webMcpInflightKeys.clear();
9283
+ this.webMcpResolvedKeys.clear();
8288
9284
  this.messages = this.sortMessages(
8289
9285
  messages.map((message) => {
8290
9286
  var _a2;
@@ -8414,7 +9410,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
8414
9410
  return;
8415
9411
  }
8416
9412
  this.messages = this.messages.map((existing, idx) => {
8417
- var _a;
9413
+ var _a, _b, _c, _d, _e, _f;
8418
9414
  if (idx !== index) return existing;
8419
9415
  const merged = { ...existing, ...withSequence };
8420
9416
  if (((_a = existing.agentMetadata) == null ? void 0 : _a.askUserQuestionAnswered) === true && withSequence.agentMetadata) {
@@ -8429,6 +9425,18 @@ var AgentWidgetSession = class _AgentWidgetSession {
8429
9425
  awaitingLocalTool: false
8430
9426
  };
8431
9427
  }
9428
+ const reTcName = (_b = withSequence.toolCall) == null ? void 0 : _b.name;
9429
+ const reExecId = (_c = withSequence.agentMetadata) == null ? void 0 : _c.executionId;
9430
+ const reTcId = (_d = withSequence.toolCall) == null ? void 0 : _d.id;
9431
+ if (reTcName && isWebMcpToolName(reTcName) && reExecId && reTcId && ((_e = withSequence.agentMetadata) == null ? void 0 : _e.awaitingLocalTool) === true) {
9432
+ const reKey = `${reExecId}:${reTcId}`;
9433
+ if (this.webMcpInflightKeys.has(reKey) || this.webMcpResolvedKeys.has(reKey)) {
9434
+ merged.agentMetadata = {
9435
+ ...(_f = merged.agentMetadata) != null ? _f : {},
9436
+ awaitingLocalTool: false
9437
+ };
9438
+ }
9439
+ }
8432
9440
  return merged;
8433
9441
  });
8434
9442
  this.messages = this.sortMessages(this.messages);
@@ -8952,9 +9960,60 @@ var morphMessages = (container, newContent, options = {}) => {
8952
9960
  });
8953
9961
  };
8954
9962
 
9963
+ // src/utils/composer-history.ts
9964
+ var INITIAL_HISTORY_STATE = {
9965
+ index: -1,
9966
+ draft: ""
9967
+ };
9968
+ function navigateComposerHistory(input) {
9969
+ const { direction, history, currentValue, atStart, state } = input;
9970
+ const inHistory = state.index !== -1;
9971
+ if (history.length === 0) {
9972
+ return { handled: false, state };
9973
+ }
9974
+ if (direction === "up") {
9975
+ if (!inHistory && !atStart) {
9976
+ return { handled: false, state };
9977
+ }
9978
+ if (!inHistory) {
9979
+ const index = history.length - 1;
9980
+ return {
9981
+ handled: true,
9982
+ value: history[index],
9983
+ state: { index, draft: currentValue }
9984
+ };
9985
+ }
9986
+ if (state.index > 0) {
9987
+ const index = state.index - 1;
9988
+ return {
9989
+ handled: true,
9990
+ value: history[index],
9991
+ state: { index, draft: state.draft }
9992
+ };
9993
+ }
9994
+ return { handled: true, state };
9995
+ }
9996
+ if (!inHistory) {
9997
+ return { handled: false, state };
9998
+ }
9999
+ if (state.index < history.length - 1) {
10000
+ const index = state.index + 1;
10001
+ return {
10002
+ handled: true,
10003
+ value: history[index],
10004
+ state: { index, draft: state.draft }
10005
+ };
10006
+ }
10007
+ return {
10008
+ handled: true,
10009
+ value: state.draft,
10010
+ state: { ...INITIAL_HISTORY_STATE }
10011
+ };
10012
+ }
10013
+
8955
10014
  // src/utils/message-fingerprint.ts
8956
10015
  function computeMessageFingerprint(message, configVersion) {
8957
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D;
10016
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L;
8958
10017
  return [
8959
10018
  message.id,
8960
10019
  message.role,
@@ -8971,8 +10030,10 @@ function computeMessageFingerprint(message, configVersion) {
8971
10030
  (_v = (_u = (_t = (_s = message.toolCall) == null ? void 0 : _s.chunks) == null ? void 0 : _t[message.toolCall.chunks.length - 1]) == null ? void 0 : _u.slice(-32)) != null ? _v : "",
8972
10031
  typeof ((_w = message.toolCall) == null ? void 0 : _w.args) === "string" ? message.toolCall.args.length : ((_x = message.toolCall) == null ? void 0 : _x.args) ? JSON.stringify(message.toolCall.args).length : 0,
8973
10032
  (_A = (_z = (_y = message.reasoning) == null ? void 0 : _y.chunks) == null ? void 0 : _z.length) != null ? _A : 0,
8974
- (_C = (_B = message.contentParts) == null ? void 0 : _B.length) != null ? _C : 0,
8975
- (_D = message.stopReason) != null ? _D : "",
10033
+ (_E = (_D = (_C = (_B = message.reasoning) == null ? void 0 : _B.chunks) == null ? void 0 : _C[message.reasoning.chunks.length - 1]) == null ? void 0 : _D.length) != null ? _E : 0,
10034
+ (_I = (_H = (_G = (_F = message.reasoning) == null ? void 0 : _F.chunks) == null ? void 0 : _G[message.reasoning.chunks.length - 1]) == null ? void 0 : _H.slice(-32)) != null ? _I : "",
10035
+ (_K = (_J = message.contentParts) == null ? void 0 : _J.length) != null ? _K : 0,
10036
+ (_L = message.stopReason) != null ? _L : "",
8976
10037
  configVersion
8977
10038
  ].join("\0");
8978
10039
  }
@@ -11498,11 +12559,11 @@ var createTypingIndicator = () => {
11498
12559
  container.appendChild(srOnly);
11499
12560
  return container;
11500
12561
  };
11501
- var renderLoadingIndicatorWithFallback = (location, customRenderer, widgetConfig) => {
12562
+ var renderLoadingIndicatorWithFallback = (location2, customRenderer, widgetConfig) => {
11502
12563
  const context = {
11503
12564
  config: widgetConfig != null ? widgetConfig : {},
11504
12565
  streaming: true,
11505
- location,
12566
+ location: location2,
11506
12567
  defaultRenderer: createTypingIndicator
11507
12568
  };
11508
12569
  if (customRenderer) {
@@ -16125,7 +17186,7 @@ function buildDropOverlay(dropCfg) {
16125
17186
  return overlay;
16126
17187
  }
16127
17188
  var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
16128
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U;
17189
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V;
16129
17190
  if (mount == null) {
16130
17191
  throw new Error(
16131
17192
  'createAgentExperience: mount must be a non-null HTMLElement (e.g. pass document.getElementById("my-root") after the node exists).'
@@ -16881,7 +17942,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
16881
17942
  btn.style.cursor = "not-allowed";
16882
17943
  });
16883
17944
  }
16884
- session.resolveApproval(approvalMessage.approval, decision);
17945
+ if (approvalMessage.approval.toolType === "webmcp") {
17946
+ session.resolveWebMcpApproval(messageId, decision);
17947
+ } else {
17948
+ session.resolveApproval(approvalMessage.approval, decision);
17949
+ }
16885
17950
  });
16886
17951
  let artifactPaneApi = null;
16887
17952
  let artifactPanelResizeObs = null;
@@ -19216,17 +20281,77 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19216
20281
  }
19217
20282
  textarea.value = "";
19218
20283
  textarea.style.height = "auto";
20284
+ resetHistoryNavigation();
19219
20285
  session.sendMessage(value, { contentParts });
19220
20286
  if (hasAttachments) {
19221
20287
  attachmentManager.clearAttachments();
19222
20288
  }
19223
20289
  };
19224
- const handleInputEnter = (event) => {
20290
+ const historyNavigationEnabled = () => {
20291
+ var _a2;
20292
+ return ((_a2 = config.features) == null ? void 0 : _a2.composerHistory) !== false;
20293
+ };
20294
+ let composerHistoryState = { ...INITIAL_HISTORY_STATE };
20295
+ let suppressHistoryReset = false;
20296
+ const resetHistoryNavigation = () => {
20297
+ composerHistoryState = { ...INITIAL_HISTORY_STATE };
20298
+ };
20299
+ const getUserMessageHistory = () => session.getMessages().filter((message) => message.role === "user").map((message) => {
20300
+ var _a2;
20301
+ return (_a2 = message.content) != null ? _a2 : "";
20302
+ }).filter((text) => text.length > 0);
20303
+ const applyHistoryValue = (value) => {
20304
+ if (!textarea) return;
20305
+ suppressHistoryReset = true;
20306
+ textarea.value = value;
20307
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
20308
+ suppressHistoryReset = false;
20309
+ const end = textarea.value.length;
20310
+ textarea.setSelectionRange(end, end);
20311
+ };
20312
+ const handleComposerInput = () => {
20313
+ if (suppressHistoryReset) return;
20314
+ resetHistoryNavigation();
20315
+ };
20316
+ const handleComposerKeydown = (event) => {
20317
+ if (!textarea) return;
20318
+ if (historyNavigationEnabled() && (event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey && !event.isComposing) {
20319
+ const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0;
20320
+ const result = navigateComposerHistory({
20321
+ direction: event.key === "ArrowUp" ? "up" : "down",
20322
+ history: getUserMessageHistory(),
20323
+ currentValue: textarea.value,
20324
+ atStart,
20325
+ state: composerHistoryState
20326
+ });
20327
+ composerHistoryState = result.state;
20328
+ if (result.handled) {
20329
+ event.preventDefault();
20330
+ if (result.value !== void 0) {
20331
+ applyHistoryValue(result.value);
20332
+ }
20333
+ return;
20334
+ }
20335
+ }
19225
20336
  if (event.key === "Enter" && !event.shiftKey) {
20337
+ if (session.isStreaming()) {
20338
+ event.preventDefault();
20339
+ return;
20340
+ }
20341
+ resetHistoryNavigation();
19226
20342
  event.preventDefault();
19227
20343
  sendButton.click();
19228
20344
  }
19229
20345
  };
20346
+ const handleEscStop = (event) => {
20347
+ if (event.key !== "Escape" || event.isComposing) return;
20348
+ if (!session.isStreaming()) return;
20349
+ if (!event.composedPath().includes(container)) return;
20350
+ session.cancel();
20351
+ resetHistoryNavigation();
20352
+ event.preventDefault();
20353
+ event.stopImmediatePropagation();
20354
+ };
19230
20355
  const handleInputPaste = async (event) => {
19231
20356
  var _a2;
19232
20357
  if (((_a2 = config.attachments) == null ? void 0 : _a2.enabled) !== true || !attachmentManager) return;
@@ -19899,8 +21024,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19899
21024
  if (composerForm) {
19900
21025
  composerForm.addEventListener("submit", handleSubmit);
19901
21026
  }
19902
- textarea == null ? void 0 : textarea.addEventListener("keydown", handleInputEnter);
21027
+ textarea == null ? void 0 : textarea.addEventListener("keydown", handleComposerKeydown);
21028
+ textarea == null ? void 0 : textarea.addEventListener("input", handleComposerInput);
19903
21029
  textarea == null ? void 0 : textarea.addEventListener("paste", handleInputPaste);
21030
+ const escStopDoc = (_P = mount.ownerDocument) != null ? _P : document;
21031
+ escStopDoc.addEventListener("keydown", handleEscStop, true);
19904
21032
  const ATTACHMENT_DROP_ACTIVE_CLASS = "persona-attachment-drop-active";
19905
21033
  let attachmentFileDragDepth = 0;
19906
21034
  const clearAttachmentDropVisual = () => {
@@ -19960,8 +21088,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19960
21088
  if (composerForm) {
19961
21089
  composerForm.removeEventListener("submit", handleSubmit);
19962
21090
  }
19963
- textarea == null ? void 0 : textarea.removeEventListener("keydown", handleInputEnter);
21091
+ textarea == null ? void 0 : textarea.removeEventListener("keydown", handleComposerKeydown);
21092
+ textarea == null ? void 0 : textarea.removeEventListener("input", handleComposerInput);
19964
21093
  textarea == null ? void 0 : textarea.removeEventListener("paste", handleInputPaste);
21094
+ escStopDoc.removeEventListener("keydown", handleEscStop, true);
19965
21095
  });
19966
21096
  destroyCallbacks.push(() => {
19967
21097
  container.removeEventListener("dragenter", handleAttachmentDragEnterCapture, attachmentDropCapture);
@@ -19986,7 +21116,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19986
21116
  }
19987
21117
  const controller = {
19988
21118
  update(nextConfig) {
19989
- var _a2, _b2, _c2, _d2, _e2, _f2, _g2, _h2, _i2, _j2, _k2, _l2, _m2, _n2, _o2, _p2, _q2, _r2, _s2, _t2, _u2, _v2, _w2, _x2, _y2, _z2, _A2, _B2, _C2, _D2, _E2, _F2, _G2, _H2, _I2, _J2, _K2, _L2, _M2, _N2, _O2, _P2, _Q2, _R2, _S2, _T2, _U2, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa, _Ga, _Ha, _Ia, _Ja, _Ka, _La, _Ma, _Na, _Oa, _Pa, _Qa, _Ra, _Sa, _Ta, _Ua, _Va, _Wa, _Xa, _Ya, _Za, __a, _$a, _ab, _bb, _cb, _db, _eb;
21119
+ var _a2, _b2, _c2, _d2, _e2, _f2, _g2, _h2, _i2, _j2, _k2, _l2, _m2, _n2, _o2, _p2, _q2, _r2, _s2, _t2, _u2, _v2, _w2, _x2, _y2, _z2, _A2, _B2, _C2, _D2, _E2, _F2, _G2, _H2, _I2, _J2, _K2, _L2, _M2, _N2, _O2, _P2, _Q2, _R2, _S2, _T2, _U2, _V2, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa, _Ga, _Ha, _Ia, _Ja, _Ka, _La, _Ma, _Na, _Oa, _Pa, _Qa, _Ra, _Sa, _Ta, _Ua, _Va, _Wa, _Xa, _Ya, _Za, __a, _$a, _ab, _bb, _cb, _db, _eb;
19990
21120
  const previousToolCallConfig = config.toolCall;
19991
21121
  const previousMessageActions = config.messageActions;
19992
21122
  const previousLayoutMessages = (_a2 = config.layout) == null ? void 0 : _a2.messages;
@@ -20260,7 +21390,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
20260
21390
  }
20261
21391
  }
20262
21392
  const layoutShowTitle = (_T2 = (_S2 = config.layout) == null ? void 0 : _S2.header) == null ? void 0 : _T2.showTitle;
20263
- const layoutShowSubtitle = (_V = (_U2 = config.layout) == null ? void 0 : _U2.header) == null ? void 0 : _V.showSubtitle;
21393
+ const layoutShowSubtitle = (_V2 = (_U2 = config.layout) == null ? void 0 : _U2.header) == null ? void 0 : _V2.showSubtitle;
20264
21394
  if (headerTitle) {
20265
21395
  headerTitle.style.display = layoutShowTitle === false ? "none" : "";
20266
21396
  }
@@ -21185,6 +22315,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21185
22315
  if (!(approvalMessage == null ? void 0 : approvalMessage.approval)) {
21186
22316
  throw new Error(`Approval not found: ${approvalId}`);
21187
22317
  }
22318
+ if (approvalMessage.approval.toolType === "webmcp") {
22319
+ session.resolveWebMcpApproval(approvalMessage.id, decision);
22320
+ return;
22321
+ }
21188
22322
  return session.resolveApproval(approvalMessage.approval, decision);
21189
22323
  },
21190
22324
  getMessages() {
@@ -21286,7 +22420,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21286
22420
  }
21287
22421
  }
21288
22422
  };
21289
- const shouldExposeDebugApi = ((_P = runtimeOptions == null ? void 0 : runtimeOptions.debugTools) != null ? _P : false) || Boolean(config.debug);
22423
+ const shouldExposeDebugApi = ((_Q = runtimeOptions == null ? void 0 : runtimeOptions.debugTools) != null ? _Q : false) || Boolean(config.debug);
21290
22424
  if (shouldExposeDebugApi && typeof window !== "undefined") {
21291
22425
  const previousDebug = window.AgentWidgetBrowser;
21292
22426
  const debugApi = {
@@ -21389,9 +22523,9 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21389
22523
  const voiceKey = `${persistConfig.keyPrefix}widget-voice`;
21390
22524
  const voiceModeKey = `${persistConfig.keyPrefix}widget-voice-mode`;
21391
22525
  if (storage) {
21392
- const wasOpen = ((_Q = persistConfig.persist) == null ? void 0 : _Q.openState) && storage.getItem(openKey) === "true";
21393
- const wasVoiceActive = ((_R = persistConfig.persist) == null ? void 0 : _R.voiceState) && storage.getItem(voiceKey) === "true";
21394
- const wasInVoiceMode = ((_S = persistConfig.persist) == null ? void 0 : _S.voiceState) && storage.getItem(voiceModeKey) === "true";
22526
+ const wasOpen = ((_R = persistConfig.persist) == null ? void 0 : _R.openState) && storage.getItem(openKey) === "true";
22527
+ const wasVoiceActive = ((_S = persistConfig.persist) == null ? void 0 : _S.voiceState) && storage.getItem(voiceKey) === "true";
22528
+ const wasInVoiceMode = ((_T = persistConfig.persist) == null ? void 0 : _T.voiceState) && storage.getItem(voiceModeKey) === "true";
21395
22529
  if (wasOpen) {
21396
22530
  setTimeout(() => {
21397
22531
  controller.open();
@@ -21408,7 +22542,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21408
22542
  }, 100);
21409
22543
  }, 0);
21410
22544
  }
21411
- if ((_T = persistConfig.persist) == null ? void 0 : _T.openState) {
22545
+ if ((_U = persistConfig.persist) == null ? void 0 : _U.openState) {
21412
22546
  eventBus.on("widget:opened", () => {
21413
22547
  storage.setItem(openKey, "true");
21414
22548
  });
@@ -21416,7 +22550,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21416
22550
  storage.setItem(openKey, "false");
21417
22551
  });
21418
22552
  }
21419
- if ((_U = persistConfig.persist) == null ? void 0 : _U.voiceState) {
22553
+ if ((_V = persistConfig.persist) == null ? void 0 : _V.voiceState) {
21420
22554
  eventBus.on("voice:state", (event) => {
21421
22555
  storage.setItem(voiceKey, event.active ? "true" : "false");
21422
22556
  });
@@ -22567,6 +23701,1054 @@ function createThemePreview(container, initialOptions) {
22567
23701
  }
22568
23702
  };
22569
23703
  }
23704
+
23705
+ // src/theme-editor/webmcp/types.ts
23706
+ function toolResult(payload) {
23707
+ return {
23708
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
23709
+ structuredContent: payload
23710
+ };
23711
+ }
23712
+
23713
+ // src/theme-editor/webmcp/coerce.ts
23714
+ var CSS_NAMED_COLORS = {
23715
+ black: "#000000",
23716
+ white: "#ffffff",
23717
+ red: "#ff0000",
23718
+ green: "#008000",
23719
+ lime: "#00ff00",
23720
+ blue: "#0000ff",
23721
+ yellow: "#ffff00",
23722
+ cyan: "#00ffff",
23723
+ aqua: "#00ffff",
23724
+ magenta: "#ff00ff",
23725
+ fuchsia: "#ff00ff",
23726
+ silver: "#c0c0c0",
23727
+ gray: "#808080",
23728
+ grey: "#808080",
23729
+ maroon: "#800000",
23730
+ olive: "#808000",
23731
+ purple: "#800080",
23732
+ teal: "#008080",
23733
+ navy: "#000080",
23734
+ orange: "#ffa500",
23735
+ pink: "#ffc0cb",
23736
+ hotpink: "#ff69b4",
23737
+ gold: "#ffd700",
23738
+ indigo: "#4b0082",
23739
+ violet: "#ee82ee",
23740
+ brown: "#a52a2a",
23741
+ beige: "#f5f5dc",
23742
+ ivory: "#fffff0",
23743
+ khaki: "#f0e68c",
23744
+ coral: "#ff7f50",
23745
+ salmon: "#fa8072",
23746
+ tomato: "#ff6347",
23747
+ crimson: "#dc143c",
23748
+ turquoise: "#40e0d0",
23749
+ lavender: "#e6e6fa",
23750
+ plum: "#dda0dd",
23751
+ orchid: "#da70d6",
23752
+ tan: "#d2b48c",
23753
+ chocolate: "#d2691e",
23754
+ sienna: "#a0522d",
23755
+ slategray: "#708090",
23756
+ slategrey: "#708090",
23757
+ steelblue: "#4682b4",
23758
+ royalblue: "#4169e1",
23759
+ dodgerblue: "#1e90ff",
23760
+ skyblue: "#87ceeb",
23761
+ lightblue: "#add8e6",
23762
+ midnightblue: "#191970",
23763
+ forestgreen: "#228b22",
23764
+ seagreen: "#2e8b57",
23765
+ limegreen: "#32cd32",
23766
+ olivedrab: "#6b8e23",
23767
+ darkgreen: "#006400",
23768
+ emerald: "#50c878",
23769
+ mint: "#3eb489",
23770
+ goldenrod: "#daa520",
23771
+ firebrick: "#b22222",
23772
+ darkred: "#8b0000",
23773
+ indianred: "#cd5c5c",
23774
+ deeppink: "#ff1493",
23775
+ mediumpurple: "#9370db",
23776
+ rebeccapurple: "#663399",
23777
+ darkviolet: "#9400d3",
23778
+ slateblue: "#6a5acd",
23779
+ cornflowerblue: "#6495ed",
23780
+ teal2: "#008080",
23781
+ charcoal: "#36454f",
23782
+ graphite: "#3b3b3b",
23783
+ transparent: "transparent"
23784
+ };
23785
+ var RGB_RE = /^rgba?\(\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*(,\s*(0|1|0?\.\d+|\d{1,3}%)\s*)?\)$/;
23786
+ function isValidRgb(value) {
23787
+ return RGB_RE.test(value);
23788
+ }
23789
+ function coerceColor(input) {
23790
+ if (typeof input !== "string" || input.trim() === "") {
23791
+ throw new Error('Color must be a non-empty string (e.g. "#2563eb" or "blue").');
23792
+ }
23793
+ const trimmed = input.trim().toLowerCase();
23794
+ const named = CSS_NAMED_COLORS[trimmed];
23795
+ if (named) return named;
23796
+ const normalized = normalizeColorValue(trimmed);
23797
+ if (isValidHex(normalized) || normalized === "transparent" || isValidRgb(normalized)) {
23798
+ return normalized;
23799
+ }
23800
+ throw new Error(
23801
+ `"${input}" is not a recognized color. Pass a hex value like "#ef4444" or a CSS color name (e.g. ${Object.keys(
23802
+ CSS_NAMED_COLORS
23803
+ ).slice(0, 6).join(", ")}).`
23804
+ );
23805
+ }
23806
+ var ROLE_FAMILY_NAMES = ROLE_FAMILIES.map(
23807
+ (f) => f === "gray" ? "neutral" : f
23808
+ );
23809
+ var FAMILY_SYNONYMS = {
23810
+ ...Object.fromEntries(ROLE_FAMILY_NAMES.map((f) => [f, f])),
23811
+ gray: "neutral",
23812
+ grey: "neutral"
23813
+ };
23814
+ function coerceFamily(input, allowNeutral = true) {
23815
+ const key = String(input != null ? input : "").trim().toLowerCase();
23816
+ const family = FAMILY_SYNONYMS[key];
23817
+ if (!family || !allowNeutral && family === "neutral") {
23818
+ const valid = allowNeutral ? "primary, secondary, accent, neutral" : "primary, secondary, accent";
23819
+ throw new Error(`Unknown color family "${input}". Valid families: ${valid}.`);
23820
+ }
23821
+ return family;
23822
+ }
23823
+ function coerceIntensity(input) {
23824
+ const key = String(input != null ? input : "solid").trim().toLowerCase();
23825
+ if (key === "solid" || key === "soft") return key;
23826
+ throw new Error(`Unknown intensity "${input}". Valid intensities: solid, soft.`);
23827
+ }
23828
+ function coerceScheme(input) {
23829
+ const key = String(input != null ? input : "").trim().toLowerCase();
23830
+ if (key === "light" || key === "dark" || key === "auto") return key;
23831
+ if (key === "system") return "auto";
23832
+ throw new Error(`Unknown color scheme "${input}". Valid: light, dark, auto.`);
23833
+ }
23834
+ var ROUNDNESS_SYNONYMS = {
23835
+ sharp: "sharp",
23836
+ square: "sharp",
23837
+ none: "sharp",
23838
+ default: "default",
23839
+ normal: "default",
23840
+ rounded: "rounded",
23841
+ round: "rounded",
23842
+ soft: "rounded",
23843
+ pill: "pill",
23844
+ circle: "pill",
23845
+ full: "pill"
23846
+ };
23847
+ function coerceRoundnessStyle(input) {
23848
+ const key = String(input != null ? input : "").trim().toLowerCase();
23849
+ const style = ROUNDNESS_SYNONYMS[key];
23850
+ if (!style) {
23851
+ throw new Error(`Unknown roundness "${input}". Valid: sharp, default, rounded, pill.`);
23852
+ }
23853
+ return style;
23854
+ }
23855
+ function coerceRadius(input) {
23856
+ if (typeof input === "number" && Number.isFinite(input)) {
23857
+ return `${input}px`;
23858
+ }
23859
+ if (typeof input === "string" && input.trim() !== "") {
23860
+ const trimmed = input.trim();
23861
+ if (trimmed === "9999px" || /^(100%|9999px)$/.test(trimmed)) return "9999px";
23862
+ const parsed = parseCssValue(trimmed);
23863
+ return formatCssValue(parsed.value, parsed.unit);
23864
+ }
23865
+ throw new Error('Radius must be a number (px) or a CSS length string like "0.5rem".');
23866
+ }
23867
+ function refsFromTypographyField(fieldId) {
23868
+ for (const section of STYLE_SECTIONS) {
23869
+ const field = section.fields.find((f) => f.id === fieldId);
23870
+ if (field == null ? void 0 : field.options) {
23871
+ const map = {};
23872
+ for (const opt of field.options) {
23873
+ const key = opt.value.split(".").pop();
23874
+ if (key) map[key] = opt.value;
23875
+ }
23876
+ return map;
23877
+ }
23878
+ }
23879
+ return {};
23880
+ }
23881
+ var FAMILY_BASE = refsFromTypographyField("typo-font-family");
23882
+ var SIZE_BASE = refsFromTypographyField("typo-font-size");
23883
+ var WEIGHT_BASE = refsFromTypographyField("typo-font-weight");
23884
+ var LINE_BASE = refsFromTypographyField("typo-line-height");
23885
+ var FONT_FAMILY_REFS = {
23886
+ ...FAMILY_BASE,
23887
+ monospace: FAMILY_BASE.mono
23888
+ };
23889
+ var FONT_SIZE_REFS = {
23890
+ ...SIZE_BASE,
23891
+ small: SIZE_BASE.sm,
23892
+ md: SIZE_BASE.base,
23893
+ medium: SIZE_BASE.base,
23894
+ large: SIZE_BASE.lg
23895
+ };
23896
+ var FONT_WEIGHT_REFS = {
23897
+ ...WEIGHT_BASE,
23898
+ "400": WEIGHT_BASE.normal,
23899
+ "500": WEIGHT_BASE.medium,
23900
+ "600": WEIGHT_BASE.semibold,
23901
+ "700": WEIGHT_BASE.bold
23902
+ };
23903
+ var LINE_HEIGHT_REFS = {
23904
+ ...LINE_BASE,
23905
+ "1.25": LINE_BASE.tight,
23906
+ "1.5": LINE_BASE.normal,
23907
+ "1.625": LINE_BASE.relaxed
23908
+ };
23909
+ function coerceTypographyRef(input, refs, label) {
23910
+ const key = String(input != null ? input : "").trim().toLowerCase();
23911
+ const ref = refs[key];
23912
+ if (!ref) {
23913
+ const valid = [...new Set(Object.values(refs).map((r) => r.split(".").pop()))].join(", ");
23914
+ throw new Error(`Unknown ${label} "${input}". Valid: ${valid}.`);
23915
+ }
23916
+ return ref;
23917
+ }
23918
+
23919
+ // src/theme-editor/webmcp/summary.ts
23920
+ var RADIUS_PATH_PREFIX = "theme.palette.radius.";
23921
+ function buildRadiusPresets() {
23922
+ var _a;
23923
+ const presets = {
23924
+ pill: { sm: "9999px", md: "9999px", lg: "9999px", xl: "9999px", full: "9999px" }
23925
+ };
23926
+ for (const section of STYLE_SECTIONS) {
23927
+ for (const preset of (_a = section.presets) != null ? _a : []) {
23928
+ const match = preset.id.match(/^radius-(\w+)$/);
23929
+ if (!match) continue;
23930
+ const radius = {};
23931
+ for (const [path, value] of Object.entries(preset.values)) {
23932
+ if (path.startsWith(RADIUS_PATH_PREFIX) && typeof value === "string") {
23933
+ radius[path.slice(RADIUS_PATH_PREFIX.length)] = value;
23934
+ }
23935
+ }
23936
+ presets[match[1]] = radius;
23937
+ }
23938
+ }
23939
+ return presets;
23940
+ }
23941
+ var RADIUS_PRESETS = buildRadiusPresets();
23942
+ var RADIUS_KEYS = ["sm", "md", "lg", "xl", "full"];
23943
+ function resolveColor(state, path, prefix = "theme", depth = 0) {
23944
+ if (depth > 6) return null;
23945
+ const raw = state.get(`${prefix}.${path}`);
23946
+ if (typeof raw !== "string" || raw === "") {
23947
+ return prefix === "darkTheme" ? resolveColor(state, path, "theme", depth) : null;
23948
+ }
23949
+ if (raw.startsWith("#") || raw.startsWith("rgb") || raw === "transparent") return raw;
23950
+ if (raw.startsWith("palette.") || raw.startsWith("semantic.") || raw.startsWith("components.")) {
23951
+ return resolveColor(state, raw, prefix, depth + 1);
23952
+ }
23953
+ return null;
23954
+ }
23955
+ function refSuffix(state, path) {
23956
+ const raw = state.get(path);
23957
+ if (typeof raw !== "string") return "unknown";
23958
+ const parts = raw.split(".");
23959
+ return parts[parts.length - 1] || String(raw);
23960
+ }
23961
+ function roleKey(roleId) {
23962
+ return roleId.replace(/^role-/, "");
23963
+ }
23964
+ function detectRoundness(radius) {
23965
+ for (const [style, preset] of Object.entries(RADIUS_PRESETS)) {
23966
+ if (RADIUS_KEYS.every((k) => radius[k] === preset[k])) return style;
23967
+ }
23968
+ return "custom";
23969
+ }
23970
+ function buildSummary(state) {
23971
+ var _a, _b;
23972
+ const radius = {};
23973
+ for (const k of RADIUS_KEYS) {
23974
+ radius[k] = String((_a = state.get(`theme.palette.radius.${k}`)) != null ? _a : "");
23975
+ }
23976
+ const roles = {};
23977
+ for (const role of ALL_ROLES) {
23978
+ roles[roleKey(role.roleId)] = detectRoleAssignment(
23979
+ (p) => state.get(`theme.${p}`),
23980
+ role
23981
+ );
23982
+ }
23983
+ return {
23984
+ brand: {
23985
+ primary: asColor(state.get("theme.palette.colors.primary.500")),
23986
+ secondary: asColor(state.get("theme.palette.colors.secondary.500")),
23987
+ accent: asColor(state.get("theme.palette.colors.accent.500"))
23988
+ },
23989
+ roles,
23990
+ typography: {
23991
+ fontFamily: refSuffix(state, "theme.semantic.typography.fontFamily"),
23992
+ fontSize: refSuffix(state, "theme.semantic.typography.fontSize"),
23993
+ fontWeight: refSuffix(state, "theme.semantic.typography.fontWeight"),
23994
+ lineHeight: refSuffix(state, "theme.semantic.typography.lineHeight")
23995
+ },
23996
+ roundness: { style: detectRoundness(radius), radius },
23997
+ colorScheme: String((_b = state.get("colorScheme")) != null ? _b : "light"),
23998
+ history: {
23999
+ index: state.getHistoryIndex(),
24000
+ canUndo: state.canUndo(),
24001
+ canRedo: state.canRedo()
24002
+ }
24003
+ };
24004
+ }
24005
+ function asColor(value) {
24006
+ return typeof value === "string" && value !== "" ? value : null;
24007
+ }
24008
+ var CONTRAST_PAIRS = [
24009
+ { key: "user-message", label: "User message text", fg: "components.message.user.text", bg: "components.message.user.background" },
24010
+ { key: "assistant-message", label: "Assistant message text", fg: "components.message.assistant.text", bg: "components.message.assistant.background" },
24011
+ { key: "header", label: "Header title", fg: "components.header.titleForeground", bg: "components.header.background" },
24012
+ { key: "primary-button", label: "Primary button label", fg: "components.button.primary.foreground", bg: "components.button.primary.background" },
24013
+ { key: "input", label: "Input placeholder", fg: "components.input.placeholder", bg: "components.input.background" },
24014
+ { key: "link", label: "Link text", fg: "components.markdown.link.foreground", bg: "semantic.colors.background" },
24015
+ { key: "scroll", label: "Scroll-to-bottom icon", fg: "components.scrollToBottom.foreground", bg: "components.scrollToBottom.background" },
24016
+ { key: "body", label: "Body text on background", fg: "semantic.colors.text", bg: "semantic.colors.background" },
24017
+ { key: "surface", label: "Body text on surface", fg: "semantic.colors.text", bg: "semantic.colors.surface" }
24018
+ ];
24019
+ function roleContrastPairKeys(role) {
24020
+ const targets = new Set(role.targets.map((t) => t.path));
24021
+ return CONTRAST_PAIRS.filter((p) => targets.has(p.fg) || targets.has(p.bg)).map((p) => p.key);
24022
+ }
24023
+ var CONTRAST_THRESHOLDS = { AA: 4.5, AAA: 7 };
24024
+ function round2(n) {
24025
+ return Math.round(n * 100) / 100;
24026
+ }
24027
+ function suggestShade(state, fgPath, bgHex, threshold, prefix) {
24028
+ const raw = state.get(`${prefix}.${fgPath}`);
24029
+ if (typeof raw !== "string") return null;
24030
+ const m = raw.match(/^palette\.colors\.(\w+)\.(\d+)$/);
24031
+ if (!m) return null;
24032
+ const family = m[1];
24033
+ const currentIdx = SHADE_KEYS.indexOf(m[2]);
24034
+ let best = null;
24035
+ let bestDistance = Infinity;
24036
+ SHADE_KEYS.forEach((shade, idx) => {
24037
+ const hex = state.get(`${prefix}.palette.colors.${family}.${shade}`);
24038
+ if (typeof hex !== "string" || !(hex.startsWith("#") || hex.startsWith("rgb"))) return;
24039
+ if (wcagContrastRatio(hex, bgHex) >= threshold) {
24040
+ const distance = currentIdx >= 0 ? Math.abs(idx - currentIdx) : idx;
24041
+ if (distance < bestDistance) {
24042
+ bestDistance = distance;
24043
+ best = `palette.colors.${family}.${shade}`;
24044
+ }
24045
+ }
24046
+ });
24047
+ return best;
24048
+ }
24049
+ function runContrastChecks(state, level = "AA", variant = "both", pairKeys) {
24050
+ const threshold = CONTRAST_THRESHOLDS[level];
24051
+ const variants = variant === "both" ? ["light", "dark"] : [variant];
24052
+ const pairs = pairKeys ? CONTRAST_PAIRS.filter((p) => pairKeys.includes(p.key)) : CONTRAST_PAIRS;
24053
+ const checks = [];
24054
+ for (const v of variants) {
24055
+ const prefix = v === "light" ? "theme" : "darkTheme";
24056
+ for (const pair of pairs) {
24057
+ const fg = resolveColor(state, pair.fg, prefix);
24058
+ const bg = resolveColor(state, pair.bg, prefix);
24059
+ if (!fg || !bg) continue;
24060
+ const ratio = round2(wcagContrastRatio(fg, bg));
24061
+ const passes = ratio >= threshold;
24062
+ const check = {
24063
+ pair: pair.key,
24064
+ label: pair.label,
24065
+ variant: v,
24066
+ fg,
24067
+ bg,
24068
+ ratio,
24069
+ threshold,
24070
+ passes
24071
+ };
24072
+ if (!passes) {
24073
+ const suggestion = suggestShade(state, pair.fg, bg, threshold, prefix);
24074
+ if (suggestion) check.suggestion = suggestion;
24075
+ }
24076
+ checks.push(check);
24077
+ }
24078
+ }
24079
+ return { level, checks, failures: checks.filter((c) => !c.passes) };
24080
+ }
24081
+ function quickContrastWarnings(state, pairKeys, variant = "light", level = "AA") {
24082
+ if (pairKeys.length === 0) return [];
24083
+ const report = runContrastChecks(state, level, variant, pairKeys);
24084
+ return report.failures.map((f) => ({
24085
+ code: "contrast",
24086
+ pair: f.pair,
24087
+ variant: f.variant,
24088
+ ratio: f.ratio,
24089
+ threshold: f.threshold,
24090
+ message: `${f.label} (${f.variant}) has a contrast ratio of ${f.ratio}:1, below the ${level} threshold of ${f.threshold}:1${f.suggestion ? `. Try ${f.suggestion} for the foreground.` : "."}`
24091
+ }));
24092
+ }
24093
+
24094
+ // src/theme-editor/webmcp/tools.ts
24095
+ var ROLE_ALIASES = {};
24096
+ for (const role of ALL_ROLES) {
24097
+ const key = roleKey(role.roleId);
24098
+ ROLE_ALIASES[key] = role;
24099
+ ROLE_ALIASES[role.roleId] = role;
24100
+ }
24101
+ Object.assign(ROLE_ALIASES, {
24102
+ surface: ROLE_ALIASES["surfaces"],
24103
+ background: ROLE_ALIASES["surfaces"],
24104
+ backgrounds: ROLE_ALIASES["surfaces"],
24105
+ user: ROLE_ALIASES["user-messages"],
24106
+ "user-message": ROLE_ALIASES["user-messages"],
24107
+ assistant: ROLE_ALIASES["assistant-messages"],
24108
+ "assistant-message": ROLE_ALIASES["assistant-messages"],
24109
+ actions: ROLE_ALIASES["primary-actions"],
24110
+ buttons: ROLE_ALIASES["primary-actions"],
24111
+ composer: ROLE_ALIASES["input"],
24112
+ links: ROLE_ALIASES["links-focus"],
24113
+ focus: ROLE_ALIASES["links-focus"],
24114
+ border: ROLE_ALIASES["borders"],
24115
+ dividers: ROLE_ALIASES["borders"],
24116
+ scroll: ROLE_ALIASES["scroll-to-bottom"]
24117
+ });
24118
+ function coerceRole(input) {
24119
+ const key = String(input != null ? input : "").trim().toLowerCase();
24120
+ const role = ROLE_ALIASES[key];
24121
+ if (!role) {
24122
+ const valid = ALL_ROLES.map((r) => roleKey(r.roleId)).join(", ");
24123
+ throw new Error(`Unknown role "${input}". Valid roles: ${valid}.`);
24124
+ }
24125
+ return role;
24126
+ }
24127
+ function buildFieldIndex() {
24128
+ const index = /* @__PURE__ */ new Map();
24129
+ const addSections = (sections) => {
24130
+ for (const section of sections) {
24131
+ for (const field of section.fields) {
24132
+ if (!index.has(field.id)) index.set(field.id, field);
24133
+ }
24134
+ }
24135
+ };
24136
+ for (const tab of ALL_TABS) addSections(tab.sections);
24137
+ for (const group of CONFIGURE_SUB_GROUPS) addSections(group.sections);
24138
+ return index;
24139
+ }
24140
+ var FEATURE_PATHS = {
24141
+ voice: "voiceRecognition.enabled",
24142
+ artifacts: "features.artifacts.enabled",
24143
+ attachments: "attachments.enabled",
24144
+ toolCalls: "features.showToolCalls",
24145
+ reasoning: "features.showReasoning",
24146
+ feedback: "messageActions.enabled"
24147
+ };
24148
+ var LAYOUT_PATHS = {
24149
+ avatars: "layout.messages.avatar.show",
24150
+ timestamps: "layout.messages.timestamp.show",
24151
+ showHeader: "layout.showHeader",
24152
+ messageStyle: "layout.messages.layout"
24153
+ };
24154
+ var LAUNCHER_POSITIONS = ["bottom-right", "bottom-left", "top-right", "top-left"];
24155
+ var MESSAGE_STYLES = ["bubble", "flat", "minimal"];
24156
+ var COPY_PATHS = {
24157
+ title: "copy.welcomeTitle",
24158
+ subtitle: "copy.welcomeSubtitle",
24159
+ placeholder: "copy.inputPlaceholder",
24160
+ sendLabel: "copy.sendButtonLabel"
24161
+ };
24162
+ function createThemeEditorTools(state, options) {
24163
+ var _a;
24164
+ let editTarget = (_a = options == null ? void 0 : options.editTarget) != null ? _a : "both";
24165
+ let fieldIndex = null;
24166
+ const rec = (input) => input && typeof input === "object" ? input : {};
24167
+ const expandScoped = (themeRelPath, value, target = editTarget) => {
24168
+ const out = {};
24169
+ if (target === "light" || target === "both") out[`theme.${themeRelPath}`] = value;
24170
+ if (target === "dark" || target === "both") out[`darkTheme.${themeRelPath}`] = value;
24171
+ return out;
24172
+ };
24173
+ const filterByEditTarget = (writes) => {
24174
+ if (editTarget === "both") return writes;
24175
+ const out = {};
24176
+ for (const [k, v] of Object.entries(writes)) {
24177
+ if (editTarget === "light" && k.startsWith("theme.")) out[k] = v;
24178
+ else if (editTarget === "dark" && k.startsWith("darkTheme.")) out[k] = v;
24179
+ }
24180
+ return out;
24181
+ };
24182
+ const warnVariant = () => editTarget === "dark" ? "dark" : "light";
24183
+ const result = (applied, warnings = []) => toolResult({ ok: true, summary: buildSummary(state), warnings, applied });
24184
+ const getThemeOverview = {
24185
+ name: "get_theme_overview",
24186
+ title: "Get current theme & what is editable",
24187
+ description: "Read the current widget theme (brand colors, per-role color assignments, typography, roundness, color scheme, undo/redo state), the available presets, and the high-level levers you can change. Call this FIRST before editing.",
24188
+ annotations: { readOnlyHint: true },
24189
+ inputSchema: {
24190
+ type: "object",
24191
+ properties: {
24192
+ verbosity: {
24193
+ type: "string",
24194
+ enum: ["summary", "full"],
24195
+ description: "Use 'full' to also include the field-id index for set_theme_fields."
24196
+ }
24197
+ },
24198
+ additionalProperties: false
24199
+ },
24200
+ execute(input) {
24201
+ const { verbosity } = rec(input);
24202
+ const payload = {
24203
+ summary: buildSummary(state),
24204
+ availableRoles: ALL_ROLES.map((r) => ({
24205
+ role: roleKey(r.roleId),
24206
+ helper: r.helper
24207
+ })),
24208
+ availableFamilies: ROLE_FAMILY_NAMES,
24209
+ presets: THEME_EDITOR_PRESETS.map((p) => {
24210
+ var _a2;
24211
+ return {
24212
+ id: p.id,
24213
+ name: p.name,
24214
+ description: p.description,
24215
+ tags: (_a2 = p.tags) != null ? _a2 : []
24216
+ };
24217
+ }),
24218
+ tools: [
24219
+ { tool: "set_brand_colors", hint: "Recolor the palette (primary/secondary/accent) \u2014 auto-generates shade scales." },
24220
+ { tool: "assign_color_role", hint: "Recolor a region (header, user/assistant messages, actions, input, links, borders, surfaces, scroll) with a family + intensity." },
24221
+ { tool: "set_typography", hint: "Set font family, size, weight, line height." },
24222
+ { tool: "set_roundness", hint: "Set corner roundness (sharp/default/rounded/pill) or granular radii." },
24223
+ { tool: "set_color_scheme", hint: "Set light/dark/auto and which variant edits target." },
24224
+ { tool: "apply_preset", hint: "Apply a complete built-in preset." },
24225
+ { tool: "configure_widget", hint: "Toggle launcher position, features, and layout." },
24226
+ { tool: "set_copy_and_suggestions", hint: "Set welcome copy, placeholder, and suggestion chips." },
24227
+ { tool: "set_theme_fields", hint: "Advanced escape hatch: set any field by id or dot-path." },
24228
+ { tool: "check_contrast", hint: "Audit WCAG contrast across key text/background pairs." },
24229
+ { tool: "manage_session", hint: "Undo, redo, reset, or export the theme." }
24230
+ ]
24231
+ };
24232
+ if (verbosity === "full") {
24233
+ fieldIndex != null ? fieldIndex : fieldIndex = buildFieldIndex();
24234
+ payload.fieldIndex = Array.from(fieldIndex.values()).map((f) => {
24235
+ var _a2;
24236
+ return {
24237
+ id: f.id,
24238
+ path: f.path,
24239
+ type: f.type,
24240
+ label: f.label,
24241
+ options: (_a2 = f.options) == null ? void 0 : _a2.map((o) => o.value)
24242
+ };
24243
+ });
24244
+ }
24245
+ return toolResult(payload);
24246
+ }
24247
+ };
24248
+ const setBrandColors = {
24249
+ name: "set_brand_colors",
24250
+ title: "Set brand colors",
24251
+ description: 'Set one or more brand colors (primary, secondary, accent). Each color auto-generates a full 50\u2013950 shade scale and applies to the light and dark themes (per the current edit target). Accepts hex ("#2563eb", "2563eb", "#18f"), rgb()/rgba() ("rgb(37, 99, 235)"), or CSS color names ("blue", "slateblue").',
24252
+ inputSchema: {
24253
+ type: "object",
24254
+ properties: {
24255
+ primary: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." },
24256
+ secondary: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." },
24257
+ accent: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." }
24258
+ },
24259
+ additionalProperties: false
24260
+ },
24261
+ execute(input) {
24262
+ const args = rec(input);
24263
+ const families = ["primary", "secondary", "accent"];
24264
+ const writes = {};
24265
+ const applied = {};
24266
+ for (const family of families) {
24267
+ if (args[family] === void 0) continue;
24268
+ const base = coerceColor(args[family]);
24269
+ applied[family] = base;
24270
+ const scale = generateColorScale(base);
24271
+ for (const shade of SHADE_KEYS) {
24272
+ const value = scale[shade];
24273
+ if (value === void 0) continue;
24274
+ Object.assign(writes, expandScoped(`palette.colors.${family}.${shade}`, value));
24275
+ }
24276
+ }
24277
+ if (Object.keys(applied).length === 0) {
24278
+ throw new Error("Provide at least one of: primary, secondary, accent.");
24279
+ }
24280
+ state.setBatch(writes);
24281
+ const warnings = quickContrastWarnings(
24282
+ state,
24283
+ ["primary-button", "user-message"],
24284
+ warnVariant()
24285
+ );
24286
+ return result(applied, warnings);
24287
+ }
24288
+ };
24289
+ const assignColorRole = {
24290
+ name: "assign_color_role",
24291
+ title: "Assign a color family to an interface role",
24292
+ description: "Recolor a semantic region of the widget by choosing a palette family and intensity. One call writes all related tokens (background, text, border, icon) consistently. Roles: header, user-messages, assistant-messages, primary-actions, input, links, borders, surfaces, scroll-to-bottom. Families: primary, secondary, accent, neutral. Intensity: solid (bold) or soft (tinted).",
24293
+ inputSchema: {
24294
+ type: "object",
24295
+ properties: {
24296
+ role: { type: "string", description: 'Interface role, e.g. "header" or "user-messages".' },
24297
+ family: { type: "string", enum: ROLE_FAMILY_NAMES },
24298
+ intensity: { type: "string", enum: ["solid", "soft"], description: "Defaults to 'solid'." }
24299
+ },
24300
+ required: ["role", "family"],
24301
+ additionalProperties: false
24302
+ },
24303
+ execute(input) {
24304
+ const args = rec(input);
24305
+ const role = coerceRole(args.role);
24306
+ const family = coerceFamily(args.family, true);
24307
+ const intensity = coerceIntensity(args.intensity);
24308
+ const writes = filterByEditTarget(resolveRoleAssignment(family, intensity, role));
24309
+ const tokensWritten = Object.keys(writes).length;
24310
+ state.setBatch(writes);
24311
+ const warnings = quickContrastWarnings(state, roleContrastPairKeys(role), warnVariant());
24312
+ return result(
24313
+ { role: roleKey(role.roleId), family, intensity, tokensWritten },
24314
+ warnings
24315
+ );
24316
+ }
24317
+ };
24318
+ const setTypography = {
24319
+ name: "set_typography",
24320
+ title: "Set typography",
24321
+ description: "Set font family, base size, weight, and line height in one call. fontFamily: sans|serif|mono. fontSize: xs|sm|base|lg|xl. fontWeight: normal|medium|semibold|bold (or 400\u2013700). lineHeight: tight|normal|relaxed (or 1.25/1.5/1.625).",
24322
+ inputSchema: {
24323
+ type: "object",
24324
+ properties: {
24325
+ fontFamily: { type: "string" },
24326
+ fontSize: { type: "string" },
24327
+ fontWeight: { type: ["string", "number"] },
24328
+ lineHeight: { type: ["string", "number"] }
24329
+ },
24330
+ additionalProperties: false
24331
+ },
24332
+ execute(input) {
24333
+ const args = rec(input);
24334
+ const writes = {};
24335
+ const applied = {};
24336
+ const apply = (key, refs) => {
24337
+ var _a2;
24338
+ if (args[key] === void 0) return;
24339
+ const ref = coerceTypographyRef(args[key], refs, key);
24340
+ applied[key] = (_a2 = ref.split(".").pop()) != null ? _a2 : ref;
24341
+ Object.assign(writes, expandScoped(`semantic.typography.${key}`, ref));
24342
+ };
24343
+ apply("fontFamily", FONT_FAMILY_REFS);
24344
+ apply("fontSize", FONT_SIZE_REFS);
24345
+ apply("fontWeight", FONT_WEIGHT_REFS);
24346
+ apply("lineHeight", LINE_HEIGHT_REFS);
24347
+ if (Object.keys(applied).length === 0) {
24348
+ throw new Error("Provide at least one of: fontFamily, fontSize, fontWeight, lineHeight.");
24349
+ }
24350
+ state.setBatch(writes);
24351
+ return result(applied);
24352
+ }
24353
+ };
24354
+ const setRoundness = {
24355
+ name: "set_roundness",
24356
+ title: "Set corner roundness",
24357
+ description: "Set overall corner roundness with a keyword (sharp, default, rounded, pill) which maps the full radius scale, OR pass granular radius values. Provide at least one of `style` or `radius`.",
24358
+ inputSchema: {
24359
+ type: "object",
24360
+ properties: {
24361
+ style: { type: "string", enum: ["sharp", "default", "rounded", "pill"] },
24362
+ radius: {
24363
+ type: "object",
24364
+ description: "Granular overrides (px number or CSS length).",
24365
+ properties: {
24366
+ sm: { type: ["string", "number"] },
24367
+ md: { type: ["string", "number"] },
24368
+ lg: { type: ["string", "number"] },
24369
+ xl: { type: ["string", "number"] },
24370
+ full: { type: ["string", "number"] }
24371
+ },
24372
+ additionalProperties: false
24373
+ }
24374
+ },
24375
+ additionalProperties: false
24376
+ },
24377
+ execute(input) {
24378
+ const args = rec(input);
24379
+ const writes = {};
24380
+ const applied = {};
24381
+ if (args.style !== void 0) {
24382
+ const style = coerceRoundnessStyle(args.style);
24383
+ applied.style = style;
24384
+ for (const [key, value] of Object.entries(RADIUS_PRESETS[style])) {
24385
+ Object.assign(writes, expandScoped(`palette.radius.${key}`, value));
24386
+ }
24387
+ }
24388
+ if (args.radius !== void 0) {
24389
+ const radius = rec(args.radius);
24390
+ const overrides = {};
24391
+ for (const key of ["sm", "md", "lg", "xl", "full"]) {
24392
+ if (radius[key] === void 0) continue;
24393
+ const value = coerceRadius(radius[key]);
24394
+ overrides[key] = value;
24395
+ Object.assign(writes, expandScoped(`palette.radius.${key}`, value));
24396
+ }
24397
+ applied.radius = overrides;
24398
+ }
24399
+ if (Object.keys(writes).length === 0) {
24400
+ throw new Error("Provide `style` (sharp|default|rounded|pill) or a `radius` object.");
24401
+ }
24402
+ state.setBatch(writes);
24403
+ return result(applied);
24404
+ }
24405
+ };
24406
+ const setColorScheme = {
24407
+ name: "set_color_scheme",
24408
+ title: "Set color scheme",
24409
+ description: "Set the shipped widget color scheme (light, dark, or auto/follow-system). Optionally set `editTarget` to choose which theme variant subsequent styling edits write to (light, dark, or both \u2014 default both).",
24410
+ inputSchema: {
24411
+ type: "object",
24412
+ properties: {
24413
+ scheme: { type: "string", enum: ["light", "dark", "auto"] },
24414
+ editTarget: { type: "string", enum: ["light", "dark", "both"] }
24415
+ },
24416
+ additionalProperties: false
24417
+ },
24418
+ execute(input) {
24419
+ const args = rec(input);
24420
+ const applied = {};
24421
+ if (args.scheme !== void 0) {
24422
+ const scheme = coerceScheme(args.scheme);
24423
+ state.set("colorScheme", scheme);
24424
+ applied.scheme = scheme;
24425
+ }
24426
+ if (args.editTarget !== void 0) {
24427
+ const t = String(args.editTarget).trim().toLowerCase();
24428
+ if (t !== "light" && t !== "dark" && t !== "both") {
24429
+ throw new Error(`Unknown editTarget "${args.editTarget}". Valid: light, dark, both.`);
24430
+ }
24431
+ editTarget = t;
24432
+ applied.editTarget = t;
24433
+ }
24434
+ if (Object.keys(applied).length === 0) {
24435
+ throw new Error("Provide `scheme` and/or `editTarget`.");
24436
+ }
24437
+ return toolResult({ ok: true, summary: buildSummary(state), warnings: [], applied, editTarget });
24438
+ }
24439
+ };
24440
+ const applyPreset = {
24441
+ name: "apply_preset",
24442
+ title: "Apply a built-in theme preset",
24443
+ description: "Apply a complete built-in preset, replacing theme tokens. Call get_theme_overview to list preset ids.",
24444
+ inputSchema: {
24445
+ type: "object",
24446
+ properties: { presetId: { type: "string" } },
24447
+ required: ["presetId"],
24448
+ additionalProperties: false
24449
+ },
24450
+ execute(input) {
24451
+ const { presetId } = rec(input);
24452
+ const preset = getThemeEditorPreset(String(presetId != null ? presetId : ""));
24453
+ if (!preset) {
24454
+ const valid = THEME_EDITOR_PRESETS.map((p) => p.id).join(", ");
24455
+ throw new Error(`Unknown preset "${presetId}". Valid presets: ${valid}.`);
24456
+ }
24457
+ const theme = createTheme(preset.theme, { validate: false });
24458
+ const config = { ...state.getConfig() };
24459
+ if (preset.darkTheme) {
24460
+ config.darkTheme = createTheme(preset.darkTheme, { validate: false });
24461
+ }
24462
+ if (preset.toolCall) config.toolCall = preset.toolCall;
24463
+ state.setFullConfig(config, theme);
24464
+ const warnings = quickContrastWarnings(state, ["body", "assistant-message"], "light");
24465
+ return result({ appliedPreset: { id: preset.id, name: preset.name } }, warnings);
24466
+ }
24467
+ };
24468
+ const configureWidget = {
24469
+ name: "configure_widget",
24470
+ title: "Configure launcher, features, and layout",
24471
+ description: "Toggle non-theme widget configuration. launcherPosition: bottom-right|bottom-left|top-right|top-left. features: { voice, artifacts, attachments, toolCalls, reasoning, feedback } booleans. layout: { avatars, timestamps, showHeader } booleans and messageStyle: bubble|flat|minimal.",
24472
+ inputSchema: {
24473
+ type: "object",
24474
+ properties: {
24475
+ launcherPosition: { type: "string", enum: LAUNCHER_POSITIONS },
24476
+ features: {
24477
+ type: "object",
24478
+ properties: {
24479
+ voice: { type: "boolean" },
24480
+ artifacts: { type: "boolean" },
24481
+ attachments: { type: "boolean" },
24482
+ toolCalls: { type: "boolean" },
24483
+ reasoning: { type: "boolean" },
24484
+ feedback: { type: "boolean" }
24485
+ },
24486
+ additionalProperties: false
24487
+ },
24488
+ layout: {
24489
+ type: "object",
24490
+ properties: {
24491
+ avatars: { type: "boolean" },
24492
+ timestamps: { type: "boolean" },
24493
+ showHeader: { type: "boolean" },
24494
+ messageStyle: { type: "string", enum: MESSAGE_STYLES }
24495
+ },
24496
+ additionalProperties: false
24497
+ }
24498
+ },
24499
+ additionalProperties: false
24500
+ },
24501
+ execute(input) {
24502
+ var _a2, _b, _c;
24503
+ const args = rec(input);
24504
+ const writes = {};
24505
+ const applied = {};
24506
+ if (args.launcherPosition !== void 0) {
24507
+ const pos = String(args.launcherPosition);
24508
+ if (!LAUNCHER_POSITIONS.includes(pos)) {
24509
+ throw new Error(`Unknown launcherPosition "${pos}". Valid: ${LAUNCHER_POSITIONS.join(", ")}.`);
24510
+ }
24511
+ writes["launcher.position"] = pos;
24512
+ applied.launcherPosition = pos;
24513
+ }
24514
+ const features = rec(args.features);
24515
+ for (const [key, path] of Object.entries(FEATURE_PATHS)) {
24516
+ if (features[key] === void 0) continue;
24517
+ writes[path] = Boolean(features[key]);
24518
+ (_a2 = applied.features) != null ? _a2 : applied.features = {};
24519
+ applied.features[key] = Boolean(features[key]);
24520
+ }
24521
+ const layout = rec(args.layout);
24522
+ for (const [key, path] of Object.entries(LAYOUT_PATHS)) {
24523
+ if (layout[key] === void 0) continue;
24524
+ if (key === "messageStyle") {
24525
+ const style = String(layout[key]);
24526
+ if (!MESSAGE_STYLES.includes(style)) {
24527
+ throw new Error(`Unknown messageStyle "${style}". Valid: ${MESSAGE_STYLES.join(", ")}.`);
24528
+ }
24529
+ writes[path] = style;
24530
+ (_b = applied.layout) != null ? _b : applied.layout = {};
24531
+ applied.layout[key] = style;
24532
+ } else {
24533
+ writes[path] = Boolean(layout[key]);
24534
+ (_c = applied.layout) != null ? _c : applied.layout = {};
24535
+ applied.layout[key] = Boolean(layout[key]);
24536
+ }
24537
+ }
24538
+ if (Object.keys(writes).length === 0) {
24539
+ throw new Error("Provide launcherPosition, features, and/or layout.");
24540
+ }
24541
+ state.setBatch(writes);
24542
+ return result(applied);
24543
+ }
24544
+ };
24545
+ const setCopyAndSuggestions = {
24546
+ name: "set_copy_and_suggestions",
24547
+ title: "Set welcome copy and suggestion chips",
24548
+ description: "Set the widget welcome copy and suggestion chips. title/subtitle are the welcome card text; placeholder is the input placeholder; sendLabel is the send button label; suggestions is an array of suggestion-chip strings (replaces the existing list).",
24549
+ inputSchema: {
24550
+ type: "object",
24551
+ properties: {
24552
+ title: { type: "string" },
24553
+ subtitle: { type: "string" },
24554
+ placeholder: { type: "string" },
24555
+ sendLabel: { type: "string" },
24556
+ suggestions: { type: "array", items: { type: "string" } }
24557
+ },
24558
+ additionalProperties: false
24559
+ },
24560
+ execute(input) {
24561
+ const args = rec(input);
24562
+ const writes = {};
24563
+ const applied = {};
24564
+ for (const [key, path] of Object.entries(COPY_PATHS)) {
24565
+ if (args[key] === void 0) continue;
24566
+ writes[path] = String(args[key]);
24567
+ applied[key] = String(args[key]);
24568
+ }
24569
+ if (args.suggestions !== void 0) {
24570
+ if (!Array.isArray(args.suggestions)) {
24571
+ throw new Error("`suggestions` must be an array of strings.");
24572
+ }
24573
+ const chips = args.suggestions.filter((s) => typeof s === "string");
24574
+ writes["suggestionChips"] = chips;
24575
+ applied.suggestions = chips;
24576
+ }
24577
+ if (Object.keys(writes).length === 0) {
24578
+ throw new Error("Provide at least one of: title, subtitle, placeholder, sendLabel, suggestions.");
24579
+ }
24580
+ state.setBatch(writes);
24581
+ return result(applied);
24582
+ }
24583
+ };
24584
+ const setThemeFields = {
24585
+ name: "set_theme_fields",
24586
+ title: "Set theme fields by id or path (advanced)",
24587
+ description: 'Advanced escape hatch: set individual editor fields by field id (see get_theme_overview verbosity:"full") \u2014 theme field ids follow the current edit target (light/dark/both) \u2014 or by raw dot-path (theme.* / darkTheme.* / a config path), which is written as-is. Use only when a higher-level tool does not cover the need. Values are validated against the field metadata.',
24588
+ inputSchema: {
24589
+ type: "object",
24590
+ properties: {
24591
+ updates: {
24592
+ type: "array",
24593
+ items: {
24594
+ type: "object",
24595
+ properties: {
24596
+ field: { type: "string", description: "Field id or dot-path." },
24597
+ value: { type: ["string", "number", "boolean"] }
24598
+ },
24599
+ required: ["field", "value"],
24600
+ additionalProperties: false
24601
+ }
24602
+ }
24603
+ },
24604
+ required: ["updates"],
24605
+ additionalProperties: false
24606
+ },
24607
+ execute(input) {
24608
+ var _a2;
24609
+ const { updates } = rec(input);
24610
+ if (!Array.isArray(updates) || updates.length === 0) {
24611
+ throw new Error("`updates` must be a non-empty array of { field, value }.");
24612
+ }
24613
+ fieldIndex != null ? fieldIndex : fieldIndex = buildFieldIndex();
24614
+ const writes = {};
24615
+ const report = [];
24616
+ for (const raw of updates) {
24617
+ const entry = rec(raw);
24618
+ const fieldKey = String((_a2 = entry.field) != null ? _a2 : "");
24619
+ try {
24620
+ const def = fieldIndex.get(fieldKey);
24621
+ const path = def ? def.path : fieldKey;
24622
+ if (!def && !/^(theme|darkTheme)\.|\./.test(path)) {
24623
+ throw new Error(
24624
+ `Unknown field "${fieldKey}". Pass a known field id or a dot-path (e.g. theme.palette.radius.md).`
24625
+ );
24626
+ }
24627
+ const value = coerceFieldValue(def, entry.value);
24628
+ if (def && path.startsWith("theme.")) {
24629
+ const scoped = expandScoped(path.slice("theme.".length), value);
24630
+ Object.assign(writes, scoped);
24631
+ report.push({ field: fieldKey, resolvedPath: Object.keys(scoped), ok: true });
24632
+ } else {
24633
+ writes[path] = value;
24634
+ report.push({ field: fieldKey, resolvedPath: path, ok: true });
24635
+ }
24636
+ } catch (err) {
24637
+ report.push({ field: fieldKey, ok: false, error: err.message });
24638
+ }
24639
+ }
24640
+ const okWrites = report.filter((r) => r.ok);
24641
+ if (Object.keys(writes).length > 0) state.setBatch(writes);
24642
+ return toolResult({
24643
+ ok: okWrites.length > 0,
24644
+ summary: buildSummary(state),
24645
+ warnings: [],
24646
+ applied: { updates: report }
24647
+ });
24648
+ }
24649
+ };
24650
+ const checkContrast = {
24651
+ name: "check_contrast",
24652
+ title: "Check accessibility contrast",
24653
+ description: "Run WCAG contrast checks over the key text/background pairs (message text, header title, input/body text, primary button). Returns each ratio, whether it passes, and a suggested foreground shade for failures.",
24654
+ annotations: { readOnlyHint: true },
24655
+ inputSchema: {
24656
+ type: "object",
24657
+ properties: {
24658
+ level: { type: "string", enum: ["AA", "AAA"], description: "Defaults to 'AA'." },
24659
+ variant: { type: "string", enum: ["light", "dark", "both"], description: "Defaults to 'both'." }
24660
+ },
24661
+ additionalProperties: false
24662
+ },
24663
+ execute(input) {
24664
+ const args = rec(input);
24665
+ const level = args.level === "AAA" ? "AAA" : "AA";
24666
+ const variant = args.variant === "light" || args.variant === "dark" ? args.variant : "both";
24667
+ const report = runContrastChecks(state, level, variant);
24668
+ return toolResult({
24669
+ level: report.level,
24670
+ passing: report.checks.length - report.failures.length,
24671
+ total: report.checks.length,
24672
+ checks: report.checks,
24673
+ failures: report.failures
24674
+ });
24675
+ }
24676
+ };
24677
+ const manageSession = {
24678
+ name: "manage_session",
24679
+ title: "Undo, redo, reset, or export the theme",
24680
+ description: 'Session action. "undo"/"redo" step through edit history; "reset" restores defaults; "export" returns the embeddable theme snapshot (config + theme JSON) with no side effects.',
24681
+ inputSchema: {
24682
+ type: "object",
24683
+ properties: { action: { type: "string", enum: ["undo", "redo", "reset", "export"] } },
24684
+ required: ["action"],
24685
+ additionalProperties: false
24686
+ },
24687
+ execute(input) {
24688
+ const { action } = rec(input);
24689
+ switch (action) {
24690
+ case "undo":
24691
+ state.undo();
24692
+ return result({ action: "undo" });
24693
+ case "redo":
24694
+ state.redo();
24695
+ return result({ action: "redo" });
24696
+ case "reset":
24697
+ state.resetToDefaults();
24698
+ return result({ action: "reset" });
24699
+ case "export":
24700
+ return toolResult({ ok: true, snapshot: state.exportSnapshot() });
24701
+ default:
24702
+ throw new Error(`Unknown action "${action}". Valid: undo, redo, reset, export.`);
24703
+ }
24704
+ }
24705
+ };
24706
+ return [
24707
+ getThemeOverview,
24708
+ setBrandColors,
24709
+ assignColorRole,
24710
+ setTypography,
24711
+ setRoundness,
24712
+ setColorScheme,
24713
+ applyPreset,
24714
+ configureWidget,
24715
+ setCopyAndSuggestions,
24716
+ setThemeFields,
24717
+ checkContrast,
24718
+ manageSession
24719
+ ];
24720
+ }
24721
+ function coerceFieldValue(def, value) {
24722
+ if (!def) return value;
24723
+ switch (def.type) {
24724
+ case "color":
24725
+ return def.parseValue ? def.parseValue(coerceColor(value)) : coerceColor(value);
24726
+ case "toggle":
24727
+ return typeof value === "boolean" ? value : value === "true" || value === 1;
24728
+ case "slider": {
24729
+ const num = Number(value);
24730
+ if (!Number.isFinite(num)) throw new Error(`"${def.id}" expects a number.`);
24731
+ if (def.slider) {
24732
+ const { min, max } = def.slider;
24733
+ if (num < min || num > max) {
24734
+ throw new Error(`"${def.id}" must be between ${min} and ${max}.`);
24735
+ }
24736
+ }
24737
+ return def.parseValue ? def.parseValue(num) : num;
24738
+ }
24739
+ case "select": {
24740
+ const str = String(value);
24741
+ if (def.options && !def.options.some((o) => o.value === str)) {
24742
+ throw new Error(
24743
+ `"${def.id}" must be one of: ${def.options.map((o) => o.value).join(", ")}.`
24744
+ );
24745
+ }
24746
+ return def.parseValue ? def.parseValue(str) : str;
24747
+ }
24748
+ default:
24749
+ return def.parseValue ? def.parseValue(value) : value;
24750
+ }
24751
+ }
22570
24752
  // Annotate the CommonJS export names for ESM import in node:
22571
24753
  0 && (module.exports = {
22572
24754
  ADVANCED_TOKENS_SECTION,
@@ -22581,6 +24763,8 @@ function createThemePreview(container, initialOptions) {
22581
24763
  COMPONENT_SHAPE_SECTIONS,
22582
24764
  CONFIGURE_SECTIONS,
22583
24765
  CONFIGURE_SUB_GROUPS,
24766
+ CONTRAST_PAIRS,
24767
+ CSS_NAMED_COLORS,
22584
24768
  DEVICE_DIMENSIONS,
22585
24769
  HOME_SUGGESTION_CHIPS,
22586
24770
  INTERFACE_ROLES_SECTION,
@@ -22588,6 +24772,7 @@ function createThemePreview(container, initialOptions) {
22588
24772
  MOCK_WORKSPACE_CONTENT,
22589
24773
  PALETTE_SECTION,
22590
24774
  PREVIEW_STORAGE_ADAPTER,
24775
+ RADIUS_PRESETS,
22591
24776
  ROLE_ASSISTANT_MESSAGES,
22592
24777
  ROLE_BORDERS,
22593
24778
  ROLE_FAMILIES,
@@ -22619,11 +24804,19 @@ function createThemePreview(container, initialOptions) {
22619
24804
  buildPreviewConfigWithMessages,
22620
24805
  buildShellCss,
22621
24806
  buildSrcdoc,
24807
+ buildSummary,
22622
24808
  buildTranscriptStreamFrames,
24809
+ coerceColor,
24810
+ coerceFamily,
24811
+ coerceIntensity,
24812
+ coerceRadius,
24813
+ coerceRoundnessStyle,
24814
+ coerceScheme,
22623
24815
  convertFromPx,
22624
24816
  convertToPx,
22625
24817
  createPreviewMessages,
22626
24818
  createPreviewTranscriptEntry,
24819
+ createThemeEditorTools,
22627
24820
  createThemePreview,
22628
24821
  detectRoleAssignment,
22629
24822
  escapeHtml,
@@ -22640,9 +24833,13 @@ function createThemePreview(container, initialOptions) {
22640
24833
  paletteColorPath,
22641
24834
  parseCssValue,
22642
24835
  presetStreamsText,
24836
+ quickContrastWarnings,
22643
24837
  resolveRoleAssignment,
22644
24838
  resolveThemeColorPath,
24839
+ rgbToHex,
24840
+ runContrastChecks,
22645
24841
  scopeSection,
22646
24842
  tokenRefDisplayName,
24843
+ toolResult,
22647
24844
  wcagContrastRatio
22648
24845
  });