@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
@@ -1613,9 +1613,29 @@ function normalizeColorValue(value) {
1613
1613
  function isValidHex(value) {
1614
1614
  return /^#[0-9A-Fa-f]{6}$/.test(value);
1615
1615
  }
1616
+ function rgbToHex(value) {
1617
+ const match = value.trim().toLowerCase().match(/^rgba?\(([^)]+)\)$/);
1618
+ if (!match) return null;
1619
+ const parts = match[1].split(",").map((p) => p.trim());
1620
+ if (parts.length < 3) return null;
1621
+ const channel = (raw) => {
1622
+ const isPct = raw.endsWith("%");
1623
+ const n = parseFloat(isPct ? raw.slice(0, -1) : raw);
1624
+ if (!Number.isFinite(n)) return NaN;
1625
+ return Math.max(0, Math.min(255, Math.round(isPct ? n / 100 * 255 : n)));
1626
+ };
1627
+ const r = channel(parts[0]);
1628
+ const g = channel(parts[1]);
1629
+ const b = channel(parts[2]);
1630
+ if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b)) return null;
1631
+ const toHex = (n) => n.toString(16).padStart(2, "0");
1632
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
1633
+ }
1616
1634
  function wcagContrastRatio(hex1, hex2) {
1617
1635
  const luminance = (hex) => {
1618
- const norm = normalizeColorValue(hex);
1636
+ var _a;
1637
+ const normalized = normalizeColorValue(hex);
1638
+ const norm = normalized.startsWith("rgb") ? (_a = rgbToHex(normalized)) != null ? _a : "#000000" : normalized;
1619
1639
  const channels = [1, 3, 5].map((i) => {
1620
1640
  const v = parseInt(norm.slice(i, i + 2), 16) / 255;
1621
1641
  return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
@@ -1627,7 +1647,9 @@ function wcagContrastRatio(hex1, hex2) {
1627
1647
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1628
1648
  }
1629
1649
  function hexToHsl(hex) {
1630
- const normalized = normalizeColorValue(hex);
1650
+ var _a;
1651
+ const normalizedValue = normalizeColorValue(hex);
1652
+ const normalized = normalizedValue.startsWith("rgb") ? (_a = rgbToHex(normalizedValue)) != null ? _a : "#000000" : normalizedValue;
1631
1653
  const r = parseInt(normalized.slice(1, 3), 16) / 255;
1632
1654
  const g = parseInt(normalized.slice(3, 5), 16) / 255;
1633
1655
  const b = parseInt(normalized.slice(5, 7), 16) / 255;
@@ -3319,6 +3341,337 @@ var resolveSanitizer = (option) => {
3319
3341
  return createDefaultSanitizer();
3320
3342
  };
3321
3343
 
3344
+ // src/webmcp-bridge.ts
3345
+ var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
3346
+ var WEBMCP_TOOL_PREFIX = "webmcp:";
3347
+ var log = {
3348
+ warn(message, ...rest) {
3349
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
3350
+ console.warn(`[Persona/WebMCP] ${message}`, ...rest);
3351
+ }
3352
+ }
3353
+ };
3354
+ var WebMcpBridge = class {
3355
+ constructor(config) {
3356
+ this.config = config;
3357
+ /** `true` once the polyfill has been (idempotently) installed. */
3358
+ this.installed = false;
3359
+ /** Memoizes the one-shot async install so concurrent callers share it. */
3360
+ this.readyPromise = null;
3361
+ /**
3362
+ * Warn-once latch for a present-but-incompatible `document.modelContext`
3363
+ * (some other / older WebMCP polyfill squatting the global). `getModelContext`
3364
+ * is hit on every snapshot + execute, so we log the diagnostic only once.
3365
+ */
3366
+ this.incompatibleContextWarned = false;
3367
+ var _a;
3368
+ this.confirmHandler = (_a = config.onConfirm) != null ? _a : null;
3369
+ this.timeoutMs = DEFAULT_TOOL_TIMEOUT_MS;
3370
+ }
3371
+ /**
3372
+ * Override the confirm handler post-construction. Used by `ui.ts` to wire
3373
+ * the in-panel approval bubble after the client has been built (the widget
3374
+ * lifecycle constructs the client before the panel renders).
3375
+ */
3376
+ setConfirmHandler(handler) {
3377
+ this.confirmHandler = handler;
3378
+ }
3379
+ /**
3380
+ * `true` when the bridge can both snapshot the registry AND execute returned
3381
+ * tool calls — i.e. the polyfill is installed and `document.modelContext`
3382
+ * exposes the consumer surface (`getTools` / `executeTool`). Native browsers
3383
+ * that ship `document.modelContext` satisfy this too.
3384
+ *
3385
+ * Synchronous and best-effort: returns `false` until the lazy install has
3386
+ * resolved (see `ensureReady`). The snapshot/execute paths await readiness
3387
+ * themselves, so this is purely an advisory check for callers.
3388
+ */
3389
+ isOperational() {
3390
+ if (this.config.enabled !== true) return false;
3391
+ if (!this.installed) return false;
3392
+ return this.getModelContext() !== null;
3393
+ }
3394
+ /**
3395
+ * Per-turn snapshot for `dispatch.clientTools[]`. Returns the JSON-only
3396
+ * surface — `execute` stays client-side, reached later via `executeToolCall`.
3397
+ *
3398
+ * Async because the strict polyfill's `getTools()` is async. Both payload
3399
+ * builders in `client.ts` already `await`, so this adds no new ceremony.
3400
+ */
3401
+ async snapshotForDispatch() {
3402
+ await this.ensureReady();
3403
+ if (this.config.enabled !== true) return [];
3404
+ const mc = this.getModelContext();
3405
+ if (!mc) return [];
3406
+ let infos;
3407
+ try {
3408
+ infos = await mc.getTools();
3409
+ } catch (err) {
3410
+ log.warn("getTools() threw \u2014 shipping an empty WebMCP snapshot.", err);
3411
+ return [];
3412
+ }
3413
+ const pageOrigin = typeof location !== "undefined" ? location.origin : "";
3414
+ return infos.filter((info) => this.passesClientAllowlist(info.name)).map((info) => {
3415
+ const def = {
3416
+ name: info.name,
3417
+ description: info.description,
3418
+ origin: "webmcp",
3419
+ ...pageOrigin ? { pageOrigin } : {}
3420
+ };
3421
+ const schema = parseSchema(info.inputSchema);
3422
+ if (schema) def.parametersSchema = schema;
3423
+ return def;
3424
+ });
3425
+ }
3426
+ /**
3427
+ * Execute a `webmcp:<name>` tool call returned by the agent and return the
3428
+ * normalized MCP-shaped result for `/resume`.
3429
+ *
3430
+ * Failure modes — all return `{ isError: true, content: [...] }` rather than
3431
+ * throwing, so the dispatch can resume cleanly:
3432
+ * - bridge not operational
3433
+ * - tool not in registry (e.g. unmounted between snapshot and call)
3434
+ * - tool excluded by the client allowlist
3435
+ * - user declined the confirm gate
3436
+ * - `execute()` threw or failed schema validation
3437
+ * - `execute()` exceeded the 30s timeout
3438
+ * - `signal` fired (session-level `cancel()`)
3439
+ *
3440
+ * When `signal` is provided, abort is honored at three points: before the
3441
+ * confirm bubble renders, after the user approves but before `execute()`
3442
+ * runs, and (via a combined `AbortController`) during `execute()` itself.
3443
+ * Honoring abort BEFORE the confirm prevents a late approval after `cancel()`
3444
+ * from firing a host-page side effect with no matching `/resume`.
3445
+ */
3446
+ async executeToolCall(wireToolName, args, signal) {
3447
+ await this.ensureReady();
3448
+ if (this.config.enabled !== true) {
3449
+ return errorResult(
3450
+ "WebMCP is not enabled on this widget."
3451
+ );
3452
+ }
3453
+ const mc = this.getModelContext();
3454
+ if (!mc) {
3455
+ const present = typeof document !== "undefined" && Boolean(document.modelContext);
3456
+ return errorResult(
3457
+ 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)."
3458
+ );
3459
+ }
3460
+ const bareName = stripWebMcpPrefix(wireToolName);
3461
+ let infos;
3462
+ try {
3463
+ infos = await mc.getTools();
3464
+ } catch (err) {
3465
+ const message = err instanceof Error ? err.message : String(err);
3466
+ return errorResult(`Failed to read WebMCP registry: ${message}`);
3467
+ }
3468
+ const info = infos.find((candidate) => candidate.name === bareName);
3469
+ if (!info) {
3470
+ return errorResult(
3471
+ `WebMCP tool not registered on this page: ${bareName}`
3472
+ );
3473
+ }
3474
+ if (!this.passesClientAllowlist(bareName)) {
3475
+ return errorResult(
3476
+ `WebMCP tool not allowed by client allowlist: ${bareName}`
3477
+ );
3478
+ }
3479
+ if (signal == null ? void 0 : signal.aborted) {
3480
+ return errorResult("Aborted by cancel()");
3481
+ }
3482
+ const gateInfo = {
3483
+ toolName: bareName,
3484
+ args,
3485
+ description: info.description,
3486
+ reason: "gate"
3487
+ };
3488
+ if (!await this.requestConfirm(gateInfo)) {
3489
+ return errorResult("User declined the tool call.");
3490
+ }
3491
+ if (signal == null ? void 0 : signal.aborted) {
3492
+ return errorResult("Aborted by cancel()");
3493
+ }
3494
+ const controller = new AbortController();
3495
+ let timedOut = false;
3496
+ const timer = setTimeout(() => {
3497
+ timedOut = true;
3498
+ controller.abort();
3499
+ }, this.timeoutMs);
3500
+ const onAbort = () => controller.abort();
3501
+ if (signal) {
3502
+ if (signal.aborted) controller.abort();
3503
+ else signal.addEventListener("abort", onAbort, { once: true });
3504
+ }
3505
+ try {
3506
+ const raw = await mc.executeTool(info, safeStringifyArgs(args), {
3507
+ signal: controller.signal
3508
+ });
3509
+ return normalizeSerializedResult(raw);
3510
+ } catch (err) {
3511
+ if (timedOut) {
3512
+ return errorResult(
3513
+ `WebMCP tool '${bareName}' timed out after ${this.timeoutMs}ms`
3514
+ );
3515
+ }
3516
+ if (signal == null ? void 0 : signal.aborted) {
3517
+ return errorResult("Aborted by cancel()");
3518
+ }
3519
+ const message = err instanceof Error ? err.message : String(err);
3520
+ return errorResult(message);
3521
+ } finally {
3522
+ clearTimeout(timer);
3523
+ if (signal) signal.removeEventListener("abort", onAbort);
3524
+ }
3525
+ }
3526
+ /**
3527
+ * Lazily install `@mcp-b/webmcp-polyfill` the first time the bridge needs the
3528
+ * registry. Idempotent and memoized. Dynamic import keeps the polyfill out of
3529
+ * the main bundle and prevents it from installing `document.modelContext` for
3530
+ * widget consumers that never enable WebMCP.
3531
+ *
3532
+ * Producer pages should still install the polyfill themselves (or import it)
3533
+ * before registering tools — Persona's install is a fallback, and a page that
3534
+ * registers tools at load before Persona's first dispatch needs the global to
3535
+ * already exist.
3536
+ */
3537
+ ensureReady() {
3538
+ if (this.config.enabled !== true) return Promise.resolve();
3539
+ if (!this.readyPromise) {
3540
+ this.readyPromise = this.install();
3541
+ }
3542
+ return this.readyPromise;
3543
+ }
3544
+ async install() {
3545
+ try {
3546
+ const mod = await import("@mcp-b/webmcp-polyfill");
3547
+ mod.initializeWebMCPPolyfill();
3548
+ this.installed = true;
3549
+ } catch (err) {
3550
+ log.warn(
3551
+ "Failed to load @mcp-b/webmcp-polyfill \u2014 WebMCP consumption disabled.",
3552
+ err
3553
+ );
3554
+ this.installed = false;
3555
+ }
3556
+ }
3557
+ /**
3558
+ * Read the consumer surface off `document.modelContext`, returning `null`
3559
+ * when it is absent or doesn't expose the producer-preview API we rely on.
3560
+ */
3561
+ getModelContext() {
3562
+ if (typeof document === "undefined") return null;
3563
+ const mc = document.modelContext;
3564
+ if (!mc || typeof mc !== "object") {
3565
+ return null;
3566
+ }
3567
+ const core = mc;
3568
+ if (typeof core.getTools !== "function" || typeof core.executeTool !== "function") {
3569
+ if (!this.incompatibleContextWarned) {
3570
+ this.incompatibleContextWarned = true;
3571
+ log.warn(
3572
+ "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)."
3573
+ );
3574
+ }
3575
+ return null;
3576
+ }
3577
+ return mc;
3578
+ }
3579
+ async requestConfirm(info) {
3580
+ var _a;
3581
+ const handler = (_a = this.confirmHandler) != null ? _a : defaultBrowserConfirmHandler;
3582
+ try {
3583
+ return await handler(info);
3584
+ } catch (err) {
3585
+ log.warn(
3586
+ `Confirm handler threw for WebMCP tool '${info.toolName}'; declining.`,
3587
+ err
3588
+ );
3589
+ return false;
3590
+ }
3591
+ }
3592
+ passesClientAllowlist(toolName) {
3593
+ const list = this.config.allowlist;
3594
+ if (!list || list.length === 0) return true;
3595
+ return list.some((pattern) => matchesGlob(toolName, pattern));
3596
+ }
3597
+ };
3598
+ var stripWebMcpPrefix = (name) => name.startsWith(WEBMCP_TOOL_PREFIX) ? name.slice(WEBMCP_TOOL_PREFIX.length) : name;
3599
+ var isWebMcpToolName = (name) => name.startsWith(WEBMCP_TOOL_PREFIX);
3600
+ var matchesGlob = (name, pattern) => {
3601
+ if (pattern === "*") return true;
3602
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
3603
+ const regex = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
3604
+ return regex.test(name);
3605
+ };
3606
+ var parseSchema = (raw) => {
3607
+ if (raw === void 0 || raw === "") return void 0;
3608
+ try {
3609
+ const parsed = JSON.parse(raw);
3610
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
3611
+ } catch {
3612
+ return void 0;
3613
+ }
3614
+ };
3615
+ var normalizeSerializedResult = (raw) => {
3616
+ if (raw === null || raw === void 0) {
3617
+ return { content: [{ type: "text", text: "" }] };
3618
+ }
3619
+ let parsed;
3620
+ try {
3621
+ parsed = JSON.parse(raw);
3622
+ } catch {
3623
+ return { content: [{ type: "text", text: raw }] };
3624
+ }
3625
+ if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.content)) {
3626
+ return parsed;
3627
+ }
3628
+ const text = typeof parsed === "string" ? parsed : safeStringify(parsed);
3629
+ return { content: [{ type: "text", text }] };
3630
+ };
3631
+ var errorResult = (message) => ({
3632
+ isError: true,
3633
+ content: [{ type: "text", text: message }]
3634
+ });
3635
+ var defaultBrowserConfirmHandler = async (info) => {
3636
+ if (typeof window === "undefined" || typeof window.confirm !== "function") {
3637
+ return false;
3638
+ }
3639
+ const argsPreview = previewArgs(info.args);
3640
+ const prompt = `Allow the AI to call ${info.toolName}` + (argsPreview ? `
3641
+
3642
+ Arguments:
3643
+ ${argsPreview}` : "") + (info.description ? `
3644
+
3645
+ ${info.description}` : "");
3646
+ return window.confirm(prompt);
3647
+ };
3648
+ var previewArgs = (args) => {
3649
+ if (args === void 0 || args === null) return "";
3650
+ try {
3651
+ const json = JSON.stringify(args, null, 2);
3652
+ return json.length > 500 ? json.slice(0, 500) + "\u2026" : json;
3653
+ } catch {
3654
+ return String(args);
3655
+ }
3656
+ };
3657
+ var safeStringifyArgs = (args) => {
3658
+ if (args === void 0) return "{}";
3659
+ try {
3660
+ const json = JSON.stringify(args);
3661
+ return json === void 0 ? "{}" : json;
3662
+ } catch {
3663
+ return "{}";
3664
+ }
3665
+ };
3666
+ var safeStringify = (value) => {
3667
+ if (value === void 0) return "";
3668
+ try {
3669
+ return JSON.stringify(value);
3670
+ } catch {
3671
+ return String(value);
3672
+ }
3673
+ };
3674
+
3322
3675
  // src/utils/formatting.ts
3323
3676
  import { parse as parsePartialJson, STR, OBJ } from "partial-json";
3324
3677
  var unescapeJsonString = (str) => {
@@ -3753,7 +4106,7 @@ var AgentWidgetClient = class {
3753
4106
  // Client token mode properties
3754
4107
  this.clientSession = null;
3755
4108
  this.sessionInitPromise = null;
3756
- var _a, _b, _c;
4109
+ var _a, _b, _c, _d;
3757
4110
  this.apiUrl = (_a = config.apiUrl) != null ? _a : DEFAULT_ENDPOINT;
3758
4111
  this.headers = {
3759
4112
  "Content-Type": "application/json",
@@ -3766,6 +4119,7 @@ var AgentWidgetClient = class {
3766
4119
  this.customFetch = config.customFetch;
3767
4120
  this.parseSSEEvent = config.parseSSEEvent;
3768
4121
  this.getHeaders = config.getHeaders;
4122
+ this.webMcpBridge = ((_d = config.webmcp) == null ? void 0 : _d.enabled) === true ? new WebMcpBridge(config.webmcp) : null;
3769
4123
  }
3770
4124
  /**
3771
4125
  * Set callback for capturing raw SSE events
@@ -3773,6 +4127,39 @@ var AgentWidgetClient = class {
3773
4127
  setSSEEventCallback(callback) {
3774
4128
  this.onSSEEvent = callback;
3775
4129
  }
4130
+ /**
4131
+ * WebMCP: wire (or replace) the confirm-bubble handler. Called from
4132
+ * `ui.ts` once the widget panel is built and the approval-bubble
4133
+ * chrome is ready to render.
4134
+ */
4135
+ setWebMcpConfirmHandler(handler) {
4136
+ var _a;
4137
+ (_a = this.webMcpBridge) == null ? void 0 : _a.setConfirmHandler(handler);
4138
+ }
4139
+ /**
4140
+ * WebMCP: `true` when the bridge installed the polyfill and can both
4141
+ * snapshot the page registry and execute returned `webmcp:*` tool calls.
4142
+ * `false` for any guard miss (no `document.modelContext`, polyfill not yet
4143
+ * installed, or `config.webmcp.enabled` not set).
4144
+ */
4145
+ isWebMcpOperational() {
4146
+ var _a;
4147
+ return ((_a = this.webMcpBridge) == null ? void 0 : _a.isOperational()) === true;
4148
+ }
4149
+ /**
4150
+ * WebMCP: execute a returned `webmcp:<name>` tool call against the page's
4151
+ * registry and return the normalized MCP-shaped result for `/resume`. The
4152
+ * bridge handles confirm-bubble gating, the 30s timeout, error
4153
+ * normalization, and `signal`-driven abort — callers never see throws.
4154
+ *
4155
+ * Returns `null` when WebMCP is not enabled on this client (signal to the
4156
+ * session that it should fall back to the legacy local-tool resume path,
4157
+ * if any).
4158
+ */
4159
+ executeWebMcpToolCall(wireToolName, args, signal) {
4160
+ if (!this.webMcpBridge) return null;
4161
+ return this.webMcpBridge.executeToolCall(wireToolName, args, signal);
4162
+ }
3776
4163
  /**
3777
4164
  * Get the current SSE event callback (used to preserve across client recreation)
3778
4165
  */
@@ -3797,7 +4184,7 @@ var AgentWidgetClient = class {
3797
4184
  getClientApiUrl(endpoint) {
3798
4185
  var _a;
3799
4186
  const baseUrl = ((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "").replace(/\/v1\/dispatch$/, "")) || DEFAULT_CLIENT_API_BASE;
3800
- return endpoint === "init" ? `${baseUrl}/v1/client/init` : `${baseUrl}/v1/client/chat`;
4187
+ return `${baseUrl}/v1/client/${endpoint}`;
3801
4188
  }
3802
4189
  /**
3803
4190
  * Get the current client session (if any)
@@ -4070,7 +4457,10 @@ var AgentWidgetClient = class {
4070
4457
  // Include metadata/context from middleware if present (excluding sessionId)
4071
4458
  ...sanitizedMetadata && Object.keys(sanitizedMetadata).length > 0 && { metadata: sanitizedMetadata },
4072
4459
  ...basePayload.inputs && Object.keys(basePayload.inputs).length > 0 && { inputs: basePayload.inputs },
4073
- ...basePayload.context && { context: basePayload.context }
4460
+ ...basePayload.context && { context: basePayload.context },
4461
+ // Forward per-turn WebMCP tools snapshotted by buildPayload(). The
4462
+ // client-token chat endpoint accepts the same shape as /v1/dispatch.
4463
+ ...basePayload.clientTools && basePayload.clientTools.length > 0 && { clientTools: basePayload.clientTools }
4074
4464
  };
4075
4465
  if (this.debug) {
4076
4466
  console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
@@ -4292,18 +4682,31 @@ var AgentWidgetClient = class {
4292
4682
  * (client-executed) tools. Used by the built-in `ask_user_question`
4293
4683
  * answer-pill sheet, but generic enough for any LOCAL tool.
4294
4684
  *
4295
- * Posts to the upstream `/resume` endpoint (the dispatch URL with
4296
- * `/dispatch` replaced by `/resume` — works for both direct-to-Runtype
4297
- * and the persona proxy) and returns the raw Response so the caller can
4298
- * pipe its SSE body through `connectStream()`.
4685
+ * Routes by mode:
4686
+ * - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
4687
+ * session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
4688
+ * with the active `sessionId` in the body and no Bearer key — a browser
4689
+ * client-token page holds no secret. `clientTools` are already persisted
4690
+ * server-side from the dispatch turn, so only `toolOutputs` is re-sent.
4691
+ * - **dispatch / proxy mode**: POST `${apiUrl}/resume` — Runtype mounts
4692
+ * resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
4693
+ * and proxies follow the same shape (`/api/chat/dispatch/resume`).
4694
+ *
4695
+ * Returns the raw Response so the caller can pipe its SSE body through
4696
+ * `connectStream()`.
4299
4697
  *
4300
4698
  * @param executionId - The paused execution id carried on `step_await`.
4301
- * @param toolOutputs - Map keyed by tool name → the tool's result value.
4699
+ * @param toolOutputs - Map keyed by per-call `toolCallId` (core#3878),
4700
+ * falling back to tool name for legacy servers → the tool's result value.
4302
4701
  */
4303
4702
  async resumeFlow(executionId, toolOutputs, options) {
4304
4703
  var _a, _b;
4305
- const trimmed = ((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "")) || DEFAULT_CLIENT_API_BASE;
4306
- const url = `${trimmed}/resume`;
4704
+ const isClientToken = this.isClientTokenMode();
4705
+ const url = isClientToken ? this.getClientApiUrl("resume") : `${((_a = this.config.apiUrl) == null ? void 0 : _a.replace(/\/+$/, "")) || DEFAULT_CLIENT_API_BASE}/resume`;
4706
+ let resumeSessionId;
4707
+ if (isClientToken) {
4708
+ resumeSessionId = (await this.initSession()).sessionId;
4709
+ }
4307
4710
  let headers = {
4308
4711
  "Content-Type": "application/json",
4309
4712
  ...this.headers
@@ -4311,17 +4714,23 @@ var AgentWidgetClient = class {
4311
4714
  if (this.getHeaders) {
4312
4715
  Object.assign(headers, await this.getHeaders());
4313
4716
  }
4717
+ const body = {
4718
+ executionId,
4719
+ toolOutputs,
4720
+ streamResponse: (_b = options == null ? void 0 : options.streamResponse) != null ? _b : true
4721
+ };
4722
+ if (resumeSessionId) {
4723
+ body.sessionId = resumeSessionId;
4724
+ }
4314
4725
  return fetch(url, {
4315
4726
  method: "POST",
4316
4727
  headers,
4317
- body: JSON.stringify({
4318
- executionId,
4319
- toolOutputs,
4320
- streamResponse: (_b = options == null ? void 0 : options.streamResponse) != null ? _b : true
4321
- })
4728
+ body: JSON.stringify(body),
4729
+ signal: options == null ? void 0 : options.signal
4322
4730
  });
4323
4731
  }
4324
4732
  async buildAgentPayload(messages) {
4733
+ var _a;
4325
4734
  if (!this.config.agent) {
4326
4735
  throw new Error("Agent configuration required for agent mode");
4327
4736
  }
@@ -4330,10 +4739,10 @@ var AgentWidgetClient = class {
4330
4739
  const timeB = new Date(b.createdAt).getTime();
4331
4740
  return timeA - timeB;
4332
4741
  }).map((message) => {
4333
- var _a, _b, _c;
4742
+ var _a2, _b, _c;
4334
4743
  return {
4335
4744
  role: message.role,
4336
- content: (_c = (_b = (_a = message.contentParts) != null ? _a : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4745
+ content: (_c = (_b = (_a2 = message.contentParts) != null ? _a2 : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4337
4746
  createdAt: message.createdAt
4338
4747
  };
4339
4748
  });
@@ -4346,6 +4755,10 @@ var AgentWidgetClient = class {
4346
4755
  ...this.config.agentOptions
4347
4756
  }
4348
4757
  };
4758
+ const webMcpSnapshot = await ((_a = this.webMcpBridge) == null ? void 0 : _a.snapshotForDispatch());
4759
+ if (webMcpSnapshot && webMcpSnapshot.length > 0) {
4760
+ payload.clientTools = webMcpSnapshot;
4761
+ }
4349
4762
  if (this.contextProviders.length) {
4350
4763
  const contextAggregate = {};
4351
4764
  await Promise.all(
@@ -4372,16 +4785,17 @@ var AgentWidgetClient = class {
4372
4785
  return payload;
4373
4786
  }
4374
4787
  async buildPayload(messages) {
4788
+ var _a;
4375
4789
  const normalizedMessages = messages.slice().filter(hasValidContent).sort((a, b) => {
4376
4790
  const timeA = new Date(a.createdAt).getTime();
4377
4791
  const timeB = new Date(b.createdAt).getTime();
4378
4792
  return timeA - timeB;
4379
4793
  }).map((message) => {
4380
- var _a, _b, _c;
4794
+ var _a2, _b, _c;
4381
4795
  return {
4382
4796
  role: message.role,
4383
4797
  // Priority: contentParts (multi-modal) > llmContent (explicit LLM content) > rawContent (structured parsers) > content (display)
4384
- content: (_c = (_b = (_a = message.contentParts) != null ? _a : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4798
+ content: (_c = (_b = (_a2 = message.contentParts) != null ? _a2 : message.llmContent) != null ? _b : message.rawContent) != null ? _c : message.content,
4385
4799
  createdAt: message.createdAt
4386
4800
  };
4387
4801
  });
@@ -4389,6 +4803,10 @@ var AgentWidgetClient = class {
4389
4803
  messages: normalizedMessages,
4390
4804
  ...this.config.flowId && { flowId: this.config.flowId }
4391
4805
  };
4806
+ const webMcpSnapshot = await ((_a = this.webMcpBridge) == null ? void 0 : _a.snapshotForDispatch());
4807
+ if (webMcpSnapshot && webMcpSnapshot.length > 0) {
4808
+ payload.clientTools = webMcpSnapshot;
4809
+ }
4392
4810
  if (this.contextProviders.length) {
4393
4811
  const contextAggregate = {};
4394
4812
  await Promise.all(
@@ -4419,7 +4837,11 @@ var AgentWidgetClient = class {
4419
4837
  config: this.config
4420
4838
  });
4421
4839
  if (result && typeof result === "object") {
4422
- return result;
4840
+ const next = result;
4841
+ if (payload.clientTools !== void 0 && !("clientTools" in next)) {
4842
+ next.clientTools = payload.clientTools;
4843
+ }
4844
+ return next;
4423
4845
  }
4424
4846
  } catch (error) {
4425
4847
  if (typeof console !== "undefined") {
@@ -5056,7 +5478,8 @@ var AgentWidgetClient = class {
5056
5478
  toolContext.byCall.delete(callKey);
5057
5479
  }
5058
5480
  } else if (payloadType === "step_await" && payload.awaitReason === "local_tool_required" && payload.toolName) {
5059
- const toolId = (_X = payload.toolId) != null ? _X : `local-${nextSequence()}`;
5481
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.length > 0 ? payload.toolCallId : void 0;
5482
+ const toolId = (_X = toolCallId != null ? toolCallId : payload.toolId) != null ? _X : `local-${nextSequence()}`;
5060
5483
  const toolMessage = ensureToolMessage(toolId);
5061
5484
  const tool = (_Y = toolMessage.toolCall) != null ? _Y : { id: toolId, status: "pending" };
5062
5485
  tool.name = payload.toolName;
@@ -5070,7 +5493,11 @@ var AgentWidgetClient = class {
5070
5493
  toolMessage.agentMetadata = {
5071
5494
  ...toolMessage.agentMetadata,
5072
5495
  executionId: (_ca = payload.executionId) != null ? _ca : (_ba = toolMessage.agentMetadata) == null ? void 0 : _ba.executionId,
5073
- awaitingLocalTool: true
5496
+ awaitingLocalTool: true,
5497
+ // Only set when the server emitted a real per-call id; its presence
5498
+ // is what tells session.ts to batch + key `/resume` by id rather
5499
+ // than by tool name (which can't represent two same-tool calls).
5500
+ ...toolCallId ? { webMcpToolCallId: toolCallId } : {}
5074
5501
  };
5075
5502
  emitMessage(toolMessage);
5076
5503
  } else if (payloadType === "text_start") {
@@ -7019,6 +7446,15 @@ function isVoiceSupported(config) {
7019
7446
  }
7020
7447
 
7021
7448
  // src/session.ts
7449
+ function buildDispatchErrorContent(error, override) {
7450
+ const err = error instanceof Error ? error : new Error(String(error));
7451
+ if (typeof override === "string") return override;
7452
+ if (typeof override === "function") return override(err);
7453
+ 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.";
7454
+ return err.message ? `${base}
7455
+
7456
+ _Details: ${err.message}_` : base;
7457
+ }
7022
7458
  var AgentWidgetSession = class _AgentWidgetSession {
7023
7459
  constructor(config = {}, callbacks) {
7024
7460
  this.config = config;
@@ -7033,6 +7469,62 @@ var AgentWidgetSession = class _AgentWidgetSession {
7033
7469
  this.agentExecution = null;
7034
7470
  this.artifacts = /* @__PURE__ */ new Map();
7035
7471
  this.selectedArtifactId = null;
7472
+ // WebMCP dedupe — keys are `${executionId}:${toolCallId}` so they're
7473
+ // naturally scoped to a single dispatch. A later dispatch (new executionId)
7474
+ // that happens to recycle a `toolCall.id` never collides with prior entries,
7475
+ // and a stale re-emit from an in-flight prior dispatch stays blocked because
7476
+ // its executionId is still in the set.
7477
+ //
7478
+ // webMcpInflightKeys — currently executing; cleared on completion of
7479
+ // EITHER /resume success OR /resume throw. Blocks
7480
+ // concurrent re-fire during the resolve round-trip.
7481
+ // webMcpResolvedKeys — /resume HTTP returned 2xx; not cleared on a new
7482
+ // dispatch (executionId scoping makes that
7483
+ // unnecessary). Blocks stale step_await re-emits
7484
+ // for the same execution.
7485
+ //
7486
+ // If `/resume` throws (network error, server 5xx), we DO want a retry path:
7487
+ // the dispatch is recoverable. Such a tool stays in neither set, so a
7488
+ // subsequent re-emit will re-trigger.
7489
+ this.webMcpInflightKeys = /* @__PURE__ */ new Set();
7490
+ this.webMcpResolvedKeys = /* @__PURE__ */ new Set();
7491
+ // Per-resolve AbortControllers, kept in a set so multiple `webmcp:*`
7492
+ // step_await resolves in one turn never abort one another. The shared
7493
+ // `this.abortController` is intentionally NOT used by resolveWebMcpToolCall:
7494
+ // in a CHAINED turn (tool A → /resume → tool B, where the server emits B's
7495
+ // step_await inside A's resume SSE stream) the shared controller is still
7496
+ // piping A's resume stream — the very stream that just delivered B. Aborting
7497
+ // it mid-chain (the prior shared-controller pre-abort) tore that stream down,
7498
+ // so B never reached execute() and its /resume was never POSTed, pausing the
7499
+ // dispatch forever. cancel(), clearMessages(), hydrateMessages(), and
7500
+ // sendMessage() iterate this set to tear every in-flight resolve down on a
7501
+ // real stop / new turn.
7502
+ this.webMcpResolveControllers = /* @__PURE__ */ new Set();
7503
+ // Bumped on every teardown / new-turn boundary (cancel, clearMessages,
7504
+ // hydrateMessages, sendMessage). A resolveWebMcpToolCall deferred via
7505
+ // queueMicrotask captures the epoch at queue time and bails if it changed,
7506
+ // so a resolve queued just before a teardown can't escape it by installing a
7507
+ // fresh controller after the set was already cleared.
7508
+ this.webMcpEpoch = 0;
7509
+ // WebMCP native approval-bubble gate. When no custom `webmcp.onConfirm` is
7510
+ // supplied, the bridge's confirm handler routes here: we inject an
7511
+ // approval-variant message and park the bridge on a Promise that resolves
7512
+ // when the user clicks Approve/Deny (see requestWebMcpApproval /
7513
+ // resolveWebMcpApproval). Resolvers are keyed by the approval message id.
7514
+ this.webMcpApprovalResolvers = /* @__PURE__ */ new Map();
7515
+ this.webMcpApprovalSeq = 0;
7516
+ // Parallel local-tool batching (core#3878). A single model turn can emit
7517
+ // multiple `step_await(local_tool_required)` events for ONE paused
7518
+ // executionId — including two PARALLEL calls to the SAME tool ("add SHOE-001
7519
+ // and SHOE-007"). Those collapse to an identical `toolId`/`index` and differ
7520
+ // only by the per-call `webMcpToolCallId`. We collect all awaits for an
7521
+ // executionId that arrive in the same stream tick, then post ONE `/resume`
7522
+ // keyed by `webMcpToolCallId` — NOT one `/resume` per tool keyed by name
7523
+ // (which collides for same-tool calls, and whose concurrent posts on one
7524
+ // execution raced → the second 404'd → the turn hung). Keyed by executionId;
7525
+ // `seen` dedupes duplicate step_await re-emits within a batch. Cleared on
7526
+ // every teardown via `abortWebMcpResolves`.
7527
+ this.webMcpAwaitBatches = /* @__PURE__ */ new Map();
7036
7528
  // Voice support
7037
7529
  this.voiceProvider = null;
7038
7530
  this.voiceActive = false;
@@ -7044,17 +7536,21 @@ var AgentWidgetSession = class _AgentWidgetSession {
7044
7536
  // so browser TTS doesn't double-speak them
7045
7537
  this.ttsSpokenMessageIds = /* @__PURE__ */ new Set();
7046
7538
  this.handleEvent = (event) => {
7047
- var _a, _b, _c, _d, _e, _f, _g;
7539
+ var _a, _b, _c, _d, _e, _f, _g, _h;
7048
7540
  if (event.type === "message") {
7049
7541
  this.upsertMessage(event.message);
7050
- if ((_a = event.message.agentMetadata) == null ? void 0 : _a.executionId) {
7542
+ const tc = event.message.toolCall;
7543
+ if (((_a = event.message.agentMetadata) == null ? void 0 : _a.awaitingLocalTool) === true && (tc == null ? void 0 : tc.name) && isWebMcpToolName(tc.name)) {
7544
+ this.enqueueWebMcpAwait(event.message);
7545
+ }
7546
+ if ((_b = event.message.agentMetadata) == null ? void 0 : _b.executionId) {
7051
7547
  if (!this.agentExecution) {
7052
7548
  this.agentExecution = {
7053
7549
  executionId: event.message.agentMetadata.executionId,
7054
7550
  agentId: "",
7055
- agentName: (_b = event.message.agentMetadata.agentName) != null ? _b : "",
7551
+ agentName: (_c = event.message.agentMetadata.agentName) != null ? _c : "",
7056
7552
  status: "running",
7057
- currentIteration: (_c = event.message.agentMetadata.iteration) != null ? _c : 0,
7553
+ currentIteration: (_d = event.message.agentMetadata.iteration) != null ? _d : 0,
7058
7554
  maxTurns: 0
7059
7555
  };
7060
7556
  } else if (event.message.agentMetadata.iteration !== void 0) {
@@ -7066,20 +7562,30 @@ var AgentWidgetSession = class _AgentWidgetSession {
7066
7562
  if (event.status === "connecting") {
7067
7563
  this.setStreaming(true);
7068
7564
  } else if (event.status === "idle" || event.status === "error") {
7069
- this.setStreaming(false);
7070
- this.abortController = null;
7071
- if (((_d = this.agentExecution) == null ? void 0 : _d.status) === "running") {
7072
- this.agentExecution.status = event.status === "error" ? "error" : "complete";
7565
+ if (this.webMcpResolveControllers.size === 0) {
7566
+ this.setStreaming(false);
7567
+ this.abortController = null;
7568
+ }
7569
+ const webMcpPending = this.webMcpAwaitBatches.size > 0 || this.webMcpResolveControllers.size > 0;
7570
+ if (((_e = this.agentExecution) == null ? void 0 : _e.status) === "running") {
7571
+ if (event.status === "error") {
7572
+ this.agentExecution.status = "error";
7573
+ } else if (!webMcpPending) {
7574
+ this.agentExecution.status = "complete";
7575
+ }
7073
7576
  }
7577
+ this.scheduleWebMcpBatchFlush();
7074
7578
  }
7075
7579
  } else if (event.type === "error") {
7076
7580
  this.setStatus("error");
7077
- this.setStreaming(false);
7078
- this.abortController = null;
7079
- if (((_e = this.agentExecution) == null ? void 0 : _e.status) === "running") {
7581
+ if (this.webMcpResolveControllers.size === 0) {
7582
+ this.setStreaming(false);
7583
+ this.abortController = null;
7584
+ }
7585
+ if (((_f = this.agentExecution) == null ? void 0 : _f.status) === "running") {
7080
7586
  this.agentExecution.status = "error";
7081
7587
  }
7082
- (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(_f, event.error);
7588
+ (_h = (_g = this.callbacks).onError) == null ? void 0 : _h.call(_g, event.error);
7083
7589
  } else if (event.type === "artifact_start" || event.type === "artifact_delta" || event.type === "artifact_update" || event.type === "artifact_complete") {
7084
7590
  this.applyArtifactStreamEvent(event);
7085
7591
  }
@@ -7094,6 +7600,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7094
7600
  });
7095
7601
  this.messages = this.sortMessages(this.messages);
7096
7602
  this.client = new AgentWidgetClient(config);
7603
+ this.wireDefaultWebMcpConfirm();
7097
7604
  for (const rec of (_b = config.initialArtifacts) != null ? _b : []) {
7098
7605
  this.artifacts.set(rec.id, { ...rec, status: "complete" });
7099
7606
  }
@@ -7451,9 +7958,13 @@ var AgentWidgetSession = class _AgentWidgetSession {
7451
7958
  return this.client.submitNPSFeedback(rating, comment);
7452
7959
  }
7453
7960
  updateConfig(next) {
7961
+ this.abortWebMcpResolves();
7962
+ this.webMcpInflightKeys.clear();
7963
+ this.webMcpResolvedKeys.clear();
7454
7964
  const prevSSECallback = this.client.getSSEEventCallback();
7455
7965
  this.config = { ...this.config, ...next };
7456
7966
  this.client = new AgentWidgetClient(this.config);
7967
+ this.wireDefaultWebMcpConfirm();
7457
7968
  if (prevSSECallback) {
7458
7969
  this.client.setSSEEventCallback(prevSSECallback);
7459
7970
  }
@@ -7659,6 +8170,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7659
8170
  if (!input && (!(options == null ? void 0 : options.contentParts) || options.contentParts.length === 0)) return;
7660
8171
  this.stopSpeaking();
7661
8172
  (_a = this.abortController) == null ? void 0 : _a.abort();
8173
+ this.abortWebMcpResolves();
7662
8174
  const userMessageId = generateUserMessageId();
7663
8175
  const assistantMessageId = generateAssistantMessageId();
7664
8176
  const userMessage = {
@@ -7692,15 +8204,21 @@ var AgentWidgetSession = class _AgentWidgetSession {
7692
8204
  } catch (error) {
7693
8205
  const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
7694
8206
  if (!isAbortError) {
7695
- const fallback = {
7696
- id: assistantMessageId,
7697
- // Use the pre-generated ID for fallback too
7698
- role: "assistant",
7699
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7700
- content: "It looks like the proxy isn't returning a real response yet. Here's a sample message so you can continue testing locally.",
7701
- sequence: this.nextSequence()
7702
- };
7703
- this.appendMessage(fallback);
8207
+ const content = buildDispatchErrorContent(
8208
+ error,
8209
+ this.config.errorMessage
8210
+ );
8211
+ if (content) {
8212
+ const fallback = {
8213
+ id: assistantMessageId,
8214
+ // Use the pre-generated ID for fallback too
8215
+ role: "assistant",
8216
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8217
+ content,
8218
+ sequence: this.nextSequence()
8219
+ };
8220
+ this.appendMessage(fallback);
8221
+ }
7704
8222
  }
7705
8223
  this.setStatus("idle");
7706
8224
  this.setStreaming(false);
@@ -7745,21 +8263,32 @@ var AgentWidgetSession = class _AgentWidgetSession {
7745
8263
  this.handleEvent
7746
8264
  );
7747
8265
  } catch (error) {
7748
- const fallback = {
7749
- id: assistantMessageId,
7750
- role: "assistant",
7751
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7752
- content: "It looks like the proxy isn't returning a real response yet. Here's a sample message so you can continue testing locally.",
7753
- sequence: this.nextSequence()
7754
- };
7755
- this.appendMessage(fallback);
8266
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8267
+ if (!isAbortError) {
8268
+ const content = buildDispatchErrorContent(
8269
+ error,
8270
+ this.config.errorMessage
8271
+ );
8272
+ if (content) {
8273
+ const fallback = {
8274
+ id: assistantMessageId,
8275
+ role: "assistant",
8276
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8277
+ content,
8278
+ sequence: this.nextSequence()
8279
+ };
8280
+ this.appendMessage(fallback);
8281
+ }
8282
+ }
7756
8283
  this.setStatus("idle");
7757
8284
  this.setStreaming(false);
7758
8285
  this.abortController = null;
7759
- if (error instanceof Error) {
7760
- (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(_b, error);
7761
- } else {
7762
- (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(_d, new Error(String(error)));
8286
+ if (!isAbortError) {
8287
+ if (error instanceof Error) {
8288
+ (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(_b, error);
8289
+ } else {
8290
+ (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(_d, new Error(String(error)));
8291
+ }
7763
8292
  }
7764
8293
  }
7765
8294
  }
@@ -7792,14 +8321,92 @@ var AgentWidgetSession = class _AgentWidgetSession {
7792
8321
  );
7793
8322
  } catch (error) {
7794
8323
  this.setStatus("error");
7795
- this.setStreaming(false);
7796
- this.abortController = null;
8324
+ if (this.webMcpResolveControllers.size === 0) {
8325
+ this.setStreaming(false);
8326
+ this.abortController = null;
8327
+ }
7797
8328
  (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
7798
8329
  _b,
7799
8330
  error instanceof Error ? error : new Error(String(error))
7800
8331
  );
7801
8332
  }
7802
8333
  }
8334
+ /**
8335
+ * Install the native approval-bubble confirm handler on the WebMCP bridge
8336
+ * when the integrator hasn't supplied a custom `webmcp.onConfirm`. Without
8337
+ * this, the bridge falls back to a blunt `window.confirm`. Safe to call
8338
+ * repeatedly (e.g. after the client is re-created in `updateConfig`).
8339
+ */
8340
+ wireDefaultWebMcpConfirm() {
8341
+ const webmcp = this.config.webmcp;
8342
+ if ((webmcp == null ? void 0 : webmcp.enabled) === true && !webmcp.onConfirm) {
8343
+ this.client.setWebMcpConfirmHandler(
8344
+ (info) => this.requestWebMcpApproval(info)
8345
+ );
8346
+ }
8347
+ }
8348
+ /**
8349
+ * Default WebMCP confirm gate: render Persona's native in-panel approval
8350
+ * bubble and resolve when the user clicks Approve/Deny. Returns immediately
8351
+ * with `true` when `webmcp.autoApprove(info)` opts the tool out of the gate
8352
+ * (e.g. a read-only catalog search), so no bubble is shown. The bridge
8353
+ * awaits this Promise before executing the page tool.
8354
+ */
8355
+ requestWebMcpApproval(info) {
8356
+ var _a, _b, _c;
8357
+ try {
8358
+ if (((_b = (_a = this.config.webmcp) == null ? void 0 : _a.autoApprove) == null ? void 0 : _b.call(_a, info)) === true) {
8359
+ return Promise.resolve(true);
8360
+ }
8361
+ } catch {
8362
+ }
8363
+ const approval = {
8364
+ id: `webmcp-${++this.webMcpApprovalSeq}`,
8365
+ status: "pending",
8366
+ agentId: "",
8367
+ executionId: "",
8368
+ toolName: info.toolName,
8369
+ toolType: "webmcp",
8370
+ description: (_c = info.description) != null ? _c : `Allow the assistant to run ${info.toolName}?`,
8371
+ parameters: info.args
8372
+ };
8373
+ const approvalMessageId = `approval-${approval.id}`;
8374
+ this.upsertMessage({
8375
+ id: approvalMessageId,
8376
+ role: "assistant",
8377
+ content: "",
8378
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8379
+ streaming: false,
8380
+ variant: "approval",
8381
+ approval
8382
+ });
8383
+ return new Promise((resolve) => {
8384
+ this.webMcpApprovalResolvers.set(approvalMessageId, resolve);
8385
+ });
8386
+ }
8387
+ /**
8388
+ * Resolve a pending WebMCP approval bubble (from the Approve/Deny click in
8389
+ * `ui.ts`). Updates the bubble to its resolved state and unblocks the
8390
+ * bridge Promise parked in `requestWebMcpApproval`. No-op if already
8391
+ * resolved (double-click, re-render).
8392
+ */
8393
+ resolveWebMcpApproval(approvalMessageId, decision) {
8394
+ const resolve = this.webMcpApprovalResolvers.get(approvalMessageId);
8395
+ if (!resolve) return;
8396
+ this.webMcpApprovalResolvers.delete(approvalMessageId);
8397
+ const existing = this.messages.find((m) => m.id === approvalMessageId);
8398
+ if (existing == null ? void 0 : existing.approval) {
8399
+ this.upsertMessage({
8400
+ ...existing,
8401
+ approval: {
8402
+ ...existing.approval,
8403
+ status: decision,
8404
+ resolvedAt: Date.now()
8405
+ }
8406
+ });
8407
+ }
8408
+ resolve(decision === "approved");
8409
+ }
7803
8410
  /**
7804
8411
  * Resolve a tool approval request (approve or deny).
7805
8412
  * Updates the approval message status, calls the API (or custom onDecision),
@@ -8041,10 +8648,378 @@ var AgentWidgetSession = class _AgentWidgetSession {
8041
8648
  }
8042
8649
  }
8043
8650
  }
8651
+ /**
8652
+ * Collect a `webmcp:*` LOCAL-tool `step_await` into a per-executionId batch
8653
+ * and schedule a single deferred flush. Parallel calls (core#3878) emit
8654
+ * several `step_await`s for ONE paused execution within the same stream tick;
8655
+ * buffering them and flushing once lets us post ONE `/resume` keyed by the
8656
+ * per-call `webMcpToolCallId` rather than racing N name-keyed resumes on the
8657
+ * same execution (which 404'd on the second and hung the turn).
8658
+ *
8659
+ * Deferred via `queueMicrotask` (epoch-guarded) for the same reason the old
8660
+ * direct resolve was: handleEvent must return first so the dispatch's
8661
+ * `connectStream` sees end-of-stream and releases the shared abortController
8662
+ * before a resolve grabs it.
8663
+ *
8664
+ * Awaits without an `executionId` or `toolCall.id` can't be batched (no key)
8665
+ * — route them straight to the single-call path, which surfaces the malformed
8666
+ * wire shape via `onError` / an `isError` resume.
8667
+ */
8668
+ enqueueWebMcpAwait(toolMessage) {
8669
+ var _a, _b;
8670
+ const executionId = (_a = toolMessage.agentMetadata) == null ? void 0 : _a.executionId;
8671
+ const callId = (_b = toolMessage.toolCall) == null ? void 0 : _b.id;
8672
+ if (!executionId || !callId) {
8673
+ const queuedEpoch = this.webMcpEpoch;
8674
+ queueMicrotask(() => {
8675
+ if (queuedEpoch !== this.webMcpEpoch) return;
8676
+ void this.resolveWebMcpToolCall(toolMessage);
8677
+ });
8678
+ return;
8679
+ }
8680
+ let batch = this.webMcpAwaitBatches.get(executionId);
8681
+ if (!batch) {
8682
+ batch = { snapshots: [], seen: /* @__PURE__ */ new Set() };
8683
+ this.webMcpAwaitBatches.set(executionId, batch);
8684
+ }
8685
+ if (batch.seen.has(callId)) return;
8686
+ batch.seen.add(callId);
8687
+ batch.snapshots.push(toolMessage);
8688
+ }
8689
+ /**
8690
+ * Flush every buffered local-tool await batch, one `/resume` per executionId.
8691
+ * Called once a stream ends (`status: idle` / `error`) — by then all parallel
8692
+ * `step_await`s the stream carried have been collected, even if split across
8693
+ * SSE chunks. Deferred via `queueMicrotask` (epoch-guarded) so the idle
8694
+ * handler returns first and the stream's end-of-stream teardown (streaming /
8695
+ * abortController) settles before a resolve grabs them — the same ordering the
8696
+ * single-call resolve always relied on.
8697
+ */
8698
+ scheduleWebMcpBatchFlush() {
8699
+ if (this.webMcpAwaitBatches.size === 0) return;
8700
+ const queuedEpoch = this.webMcpEpoch;
8701
+ queueMicrotask(() => {
8702
+ if (queuedEpoch !== this.webMcpEpoch) return;
8703
+ for (const executionId of [...this.webMcpAwaitBatches.keys()]) {
8704
+ this.flushWebMcpAwaitBatch(executionId);
8705
+ }
8706
+ });
8707
+ }
8708
+ /**
8709
+ * Run a buffered batch of local-tool awaits for one executionId. Size 1
8710
+ * (single call, or distinct-tool turns that happened to arrive alone) takes
8711
+ * the original single-call path; size >1 (parallel calls) takes the batched
8712
+ * path that posts ONE `/resume`. The batch is removed from the map up front
8713
+ * so any later sibling re-emit (e.g. from a re-pause) forms a fresh batch
8714
+ * rather than mutating one already in flight.
8715
+ */
8716
+ flushWebMcpAwaitBatch(executionId) {
8717
+ const batch = this.webMcpAwaitBatches.get(executionId);
8718
+ if (!batch) return;
8719
+ this.webMcpAwaitBatches.delete(executionId);
8720
+ const { snapshots } = batch;
8721
+ if (snapshots.length === 1) {
8722
+ void this.resolveWebMcpToolCall(snapshots[0]);
8723
+ } else if (snapshots.length > 1) {
8724
+ void this.resolveWebMcpToolCallBatch(executionId, snapshots);
8725
+ }
8726
+ }
8727
+ /**
8728
+ * Resolve TWO OR MORE parallel local-tool awaits sharing one paused
8729
+ * executionId with a SINGLE `/resume` (core#3878). Each call is executed
8730
+ * against the page registry concurrently — every gated call renders its own
8731
+ * native approval bubble, and a sibling's confirm Promise never blocks
8732
+ * another's execution. Outputs are keyed by per-call `webMcpToolCallId`
8733
+ * (server prefers it over tool name; name-keying remains the fallback for
8734
+ * legacy single/distinct-tool turns), so two calls to the SAME tool no longer
8735
+ * collide. The server is tolerant: any call we omit (declined-after-abort,
8736
+ * dedupe, exec failure) simply re-pauses and is retried on its re-emit.
8737
+ *
8738
+ * Mirrors `resolveWebMcpToolCall`'s dedupe / abort / streaming machinery, but
8739
+ * shares one resume POST and marks every resolved key on that POST's HTTP OK.
8740
+ */
8741
+ async resolveWebMcpToolCallBatch(executionId, snapshots) {
8742
+ var _a, _b, _c;
8743
+ const claimedKeys = [];
8744
+ const controllers = [];
8745
+ const resumeController = new AbortController();
8746
+ this.webMcpResolveControllers.add(resumeController);
8747
+ this.setStreaming(true);
8748
+ const executed = await Promise.all(
8749
+ snapshots.map(async (toolMessage) => {
8750
+ var _a2, _b2, _c2, _d, _e, _f, _g;
8751
+ const wireToolName = (_a2 = toolMessage.toolCall) == null ? void 0 : _a2.name;
8752
+ const callId = (_b2 = toolMessage.toolCall) == null ? void 0 : _b2.id;
8753
+ if (!wireToolName || !callId) return null;
8754
+ const dedupeKey = `${executionId}:${callId}`;
8755
+ if (this.webMcpInflightKeys.has(dedupeKey) || this.webMcpResolvedKeys.has(dedupeKey)) {
8756
+ return null;
8757
+ }
8758
+ this.webMcpInflightKeys.add(dedupeKey);
8759
+ claimedKeys.push(dedupeKey);
8760
+ this.upsertMessage({
8761
+ ...toolMessage,
8762
+ agentMetadata: {
8763
+ ...toolMessage.agentMetadata,
8764
+ awaitingLocalTool: false
8765
+ }
8766
+ });
8767
+ const controller = new AbortController();
8768
+ this.webMcpResolveControllers.add(controller);
8769
+ controllers.push(controller);
8770
+ const resumeKey = (_d = (_c2 = toolMessage.agentMetadata) == null ? void 0 : _c2.webMcpToolCallId) != null ? _d : wireToolName;
8771
+ const execPromise = this.client.executeWebMcpToolCall(
8772
+ wireToolName,
8773
+ (_e = toolMessage.toolCall) == null ? void 0 : _e.args,
8774
+ controller.signal
8775
+ );
8776
+ let output;
8777
+ if (!execPromise) {
8778
+ output = {
8779
+ isError: true,
8780
+ content: [
8781
+ { type: "text", text: "WebMCP not enabled on this widget." }
8782
+ ]
8783
+ };
8784
+ } else {
8785
+ try {
8786
+ output = await execPromise;
8787
+ } catch (error) {
8788
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8789
+ if (!isAbortError) {
8790
+ (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(
8791
+ _f,
8792
+ error instanceof Error ? error : new Error(String(error))
8793
+ );
8794
+ }
8795
+ this.webMcpInflightKeys.delete(dedupeKey);
8796
+ return null;
8797
+ }
8798
+ }
8799
+ if (controller.signal.aborted) {
8800
+ this.webMcpInflightKeys.delete(dedupeKey);
8801
+ return null;
8802
+ }
8803
+ return { dedupeKey, resumeKey, output };
8804
+ })
8805
+ );
8806
+ try {
8807
+ const ready = executed.filter(
8808
+ (r) => r !== null
8809
+ );
8810
+ if (ready.length === 0) return;
8811
+ const toolOutputs = {};
8812
+ for (const r of ready) {
8813
+ toolOutputs[r.resumeKey] = r.output;
8814
+ }
8815
+ const response = await this.client.resumeFlow(executionId, toolOutputs, {
8816
+ signal: resumeController.signal
8817
+ });
8818
+ if (!response.ok) {
8819
+ const errorData = await response.json().catch(() => null);
8820
+ throw new Error((_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Resume failed: ${response.status}`);
8821
+ }
8822
+ for (const r of ready) {
8823
+ this.webMcpResolvedKeys.add(r.dedupeKey);
8824
+ }
8825
+ if (response.body) {
8826
+ await this.connectStream(response.body, { allowReentry: true });
8827
+ }
8828
+ } catch (error) {
8829
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8830
+ if (!isAbortError) {
8831
+ (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
8832
+ _b,
8833
+ error instanceof Error ? error : new Error(String(error))
8834
+ );
8835
+ }
8836
+ } finally {
8837
+ for (const key of claimedKeys) {
8838
+ this.webMcpInflightKeys.delete(key);
8839
+ }
8840
+ for (const controller of controllers) {
8841
+ this.webMcpResolveControllers.delete(controller);
8842
+ }
8843
+ this.webMcpResolveControllers.delete(resumeController);
8844
+ if (this.webMcpResolveControllers.size === 0 && !this.abortController) {
8845
+ this.setStreaming(false);
8846
+ }
8847
+ }
8848
+ }
8849
+ /**
8850
+ * Resolve a paused `webmcp:*` LOCAL tool call by executing it against the
8851
+ * host page's tool registry and posting the result to `/resume`.
8852
+ *
8853
+ * Triggered automatically from `handleEvent` when a `step_await`-derived
8854
+ * message arrives with a `webmcp:` prefix — the user does not click a
8855
+ * pill; the bridge's confirm-bubble gate is the only interactive surface.
8856
+ *
8857
+ * Idempotent on the message's `toolCall.id`: re-emits of the same step_await
8858
+ * (e.g. from message coalescing) won't double-fire `tool.execute`. Failure
8859
+ * modes — declined, timed out, throw, unknown tool — all resolve into a
8860
+ * `{ isError: true, content: [...] }` payload that resumes the dispatch
8861
+ * cleanly so the agent can recover.
8862
+ */
8863
+ async resolveWebMcpToolCall(toolMessage) {
8864
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
8865
+ const executionId = (_a = toolMessage.agentMetadata) == null ? void 0 : _a.executionId;
8866
+ const wireToolName = (_b = toolMessage.toolCall) == null ? void 0 : _b.name;
8867
+ const toolCallId = (_c = toolMessage.toolCall) == null ? void 0 : _c.id;
8868
+ if (!executionId) {
8869
+ (_e = (_d = this.callbacks).onError) == null ? void 0 : _e.call(
8870
+ _d,
8871
+ new Error(
8872
+ "WebMCP step_await missing executionId \u2014 dispatch left paused."
8873
+ )
8874
+ );
8875
+ return;
8876
+ }
8877
+ if (!wireToolName) return;
8878
+ if (!toolCallId) {
8879
+ const malformedKey = `${executionId}:__no_tool_id__:${wireToolName}`;
8880
+ if (this.webMcpInflightKeys.has(malformedKey) || this.webMcpResolvedKeys.has(malformedKey)) {
8881
+ return;
8882
+ }
8883
+ this.webMcpInflightKeys.add(malformedKey);
8884
+ try {
8885
+ await this.resumeWithToolOutput(executionId, wireToolName, {
8886
+ isError: true,
8887
+ content: [
8888
+ {
8889
+ type: "text",
8890
+ text: "WebMCP step_await missing toolCall.id \u2014 cannot execute the page tool."
8891
+ }
8892
+ ]
8893
+ });
8894
+ this.webMcpResolvedKeys.add(malformedKey);
8895
+ } catch (error) {
8896
+ (_g = (_f = this.callbacks).onError) == null ? void 0 : _g.call(
8897
+ _f,
8898
+ error instanceof Error ? error : new Error(String(error))
8899
+ );
8900
+ } finally {
8901
+ this.webMcpInflightKeys.delete(malformedKey);
8902
+ }
8903
+ return;
8904
+ }
8905
+ const dedupeKey = `${executionId}:${toolCallId}`;
8906
+ if (this.webMcpInflightKeys.has(dedupeKey) || this.webMcpResolvedKeys.has(dedupeKey)) {
8907
+ return;
8908
+ }
8909
+ this.webMcpInflightKeys.add(dedupeKey);
8910
+ this.upsertMessage({
8911
+ ...toolMessage,
8912
+ agentMetadata: {
8913
+ ...toolMessage.agentMetadata,
8914
+ awaitingLocalTool: false
8915
+ }
8916
+ });
8917
+ const resolveController = new AbortController();
8918
+ this.webMcpResolveControllers.add(resolveController);
8919
+ const { signal } = resolveController;
8920
+ this.setStreaming(true);
8921
+ const args = (_h = toolMessage.toolCall) == null ? void 0 : _h.args;
8922
+ const execPromise = this.client.executeWebMcpToolCall(
8923
+ wireToolName,
8924
+ args,
8925
+ signal
8926
+ );
8927
+ try {
8928
+ let resumeOutput;
8929
+ if (!execPromise) {
8930
+ resumeOutput = {
8931
+ isError: true,
8932
+ content: [
8933
+ { type: "text", text: "WebMCP not enabled on this widget." }
8934
+ ]
8935
+ };
8936
+ } else {
8937
+ resumeOutput = await execPromise;
8938
+ }
8939
+ if (signal.aborted) {
8940
+ return;
8941
+ }
8942
+ const resumeKey = (_j = (_i = toolMessage.agentMetadata) == null ? void 0 : _i.webMcpToolCallId) != null ? _j : wireToolName;
8943
+ await this.resumeWithToolOutput(executionId, resumeKey, resumeOutput, {
8944
+ onHttpOk: () => {
8945
+ this.webMcpResolvedKeys.add(dedupeKey);
8946
+ },
8947
+ signal
8948
+ });
8949
+ } catch (error) {
8950
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8951
+ if (!isAbortError) {
8952
+ (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
8953
+ _k,
8954
+ error instanceof Error ? error : new Error(String(error))
8955
+ );
8956
+ }
8957
+ } finally {
8958
+ this.webMcpInflightKeys.delete(dedupeKey);
8959
+ this.webMcpResolveControllers.delete(resolveController);
8960
+ if (this.webMcpResolveControllers.size === 0 && !this.abortController) {
8961
+ this.setStreaming(false);
8962
+ }
8963
+ }
8964
+ }
8965
+ /**
8966
+ * POST `/resume` with a SINGLE tool's output and pipe the resulting SSE
8967
+ * stream back through `connectStream`. Shared by every single-call local-tool
8968
+ * resolve path (ask_user_question and single WebMCP calls). Parallel WebMCP
8969
+ * calls use `resolveWebMcpToolCallBatch`, which posts one resume for many.
8970
+ *
8971
+ * `resumeKey` is the `toolOutputs` map key: the per-call `webMcpToolCallId`
8972
+ * for WebMCP (core#3878), or the tool name for ask_user_question / legacy
8973
+ * servers. `onHttpOk` runs synchronously between the HTTP-status check and the
8974
+ * stream pipe; it lets the WebMCP resolve path commit the dedupe flag at
8975
+ * "server accepted the answer" rather than "stream finished cleanly".
8976
+ */
8977
+ async resumeWithToolOutput(executionId, resumeKey, output, options) {
8978
+ var _a, _b;
8979
+ const response = await this.client.resumeFlow(
8980
+ executionId,
8981
+ { [resumeKey]: output },
8982
+ { signal: options == null ? void 0 : options.signal }
8983
+ );
8984
+ if (!response.ok) {
8985
+ const errorData = await response.json().catch(() => null);
8986
+ throw new Error((_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Resume failed: ${response.status}`);
8987
+ }
8988
+ (_b = options == null ? void 0 : options.onHttpOk) == null ? void 0 : _b.call(options);
8989
+ if (response.body) {
8990
+ await this.connectStream(response.body, { allowReentry: true });
8991
+ } else if (this.webMcpResolveControllers.size === 0) {
8992
+ this.setStreaming(false);
8993
+ this.abortController = null;
8994
+ }
8995
+ }
8996
+ /**
8997
+ * Tear down every in-flight WebMCP resolve and advance the epoch. Each
8998
+ * resolve owns a dedicated AbortController (chained/parallel resolves don't
8999
+ * share one), so we abort them individually; the aborts propagate into the
9000
+ * bridge's execute race and into each `/resume` fetch signal. Bumping
9001
+ * `webMcpEpoch` strands any resolve still deferred in a queued microtask —
9002
+ * it captured the prior epoch and bails before installing a fresh
9003
+ * controller, so it can't escape this teardown. Called from every stop /
9004
+ * new-turn boundary (cancel, clearMessages, hydrateMessages, sendMessage).
9005
+ */
9006
+ abortWebMcpResolves() {
9007
+ for (const controller of this.webMcpResolveControllers) {
9008
+ controller.abort();
9009
+ }
9010
+ this.webMcpResolveControllers.clear();
9011
+ for (const approvalMessageId of [...this.webMcpApprovalResolvers.keys()]) {
9012
+ this.resolveWebMcpApproval(approvalMessageId, "denied");
9013
+ }
9014
+ this.webMcpAwaitBatches.clear();
9015
+ this.webMcpEpoch++;
9016
+ }
8044
9017
  cancel() {
8045
9018
  var _a;
8046
9019
  (_a = this.abortController) == null ? void 0 : _a.abort();
8047
9020
  this.abortController = null;
9021
+ this.abortWebMcpResolves();
9022
+ this.webMcpInflightKeys.clear();
8048
9023
  this.stopSpeaking();
8049
9024
  this.stopVoicePlayback();
8050
9025
  this.setStreaming(false);
@@ -8055,9 +9030,12 @@ var AgentWidgetSession = class _AgentWidgetSession {
8055
9030
  this.stopSpeaking();
8056
9031
  (_a = this.abortController) == null ? void 0 : _a.abort();
8057
9032
  this.abortController = null;
9033
+ this.abortWebMcpResolves();
8058
9034
  this.messages = [];
8059
9035
  this.agentExecution = null;
8060
9036
  this.clearArtifactState();
9037
+ this.webMcpInflightKeys.clear();
9038
+ this.webMcpResolvedKeys.clear();
8061
9039
  this.setStreaming(false);
8062
9040
  this.setStatus("idle");
8063
9041
  this.callbacks.onMessagesChanged([...this.messages]);
@@ -8174,6 +9152,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
8174
9152
  var _a;
8175
9153
  (_a = this.abortController) == null ? void 0 : _a.abort();
8176
9154
  this.abortController = null;
9155
+ this.abortWebMcpResolves();
9156
+ this.webMcpInflightKeys.clear();
9157
+ this.webMcpResolvedKeys.clear();
8177
9158
  this.messages = this.sortMessages(
8178
9159
  messages.map((message) => {
8179
9160
  var _a2;
@@ -8303,7 +9284,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
8303
9284
  return;
8304
9285
  }
8305
9286
  this.messages = this.messages.map((existing, idx) => {
8306
- var _a;
9287
+ var _a, _b, _c, _d, _e, _f;
8307
9288
  if (idx !== index) return existing;
8308
9289
  const merged = { ...existing, ...withSequence };
8309
9290
  if (((_a = existing.agentMetadata) == null ? void 0 : _a.askUserQuestionAnswered) === true && withSequence.agentMetadata) {
@@ -8318,6 +9299,18 @@ var AgentWidgetSession = class _AgentWidgetSession {
8318
9299
  awaitingLocalTool: false
8319
9300
  };
8320
9301
  }
9302
+ const reTcName = (_b = withSequence.toolCall) == null ? void 0 : _b.name;
9303
+ const reExecId = (_c = withSequence.agentMetadata) == null ? void 0 : _c.executionId;
9304
+ const reTcId = (_d = withSequence.toolCall) == null ? void 0 : _d.id;
9305
+ if (reTcName && isWebMcpToolName(reTcName) && reExecId && reTcId && ((_e = withSequence.agentMetadata) == null ? void 0 : _e.awaitingLocalTool) === true) {
9306
+ const reKey = `${reExecId}:${reTcId}`;
9307
+ if (this.webMcpInflightKeys.has(reKey) || this.webMcpResolvedKeys.has(reKey)) {
9308
+ merged.agentMetadata = {
9309
+ ...(_f = merged.agentMetadata) != null ? _f : {},
9310
+ awaitingLocalTool: false
9311
+ };
9312
+ }
9313
+ }
8321
9314
  return merged;
8322
9315
  });
8323
9316
  this.messages = this.sortMessages(this.messages);
@@ -8955,9 +9948,60 @@ var morphMessages = (container, newContent, options = {}) => {
8955
9948
  });
8956
9949
  };
8957
9950
 
9951
+ // src/utils/composer-history.ts
9952
+ var INITIAL_HISTORY_STATE = {
9953
+ index: -1,
9954
+ draft: ""
9955
+ };
9956
+ function navigateComposerHistory(input) {
9957
+ const { direction, history, currentValue, atStart, state } = input;
9958
+ const inHistory = state.index !== -1;
9959
+ if (history.length === 0) {
9960
+ return { handled: false, state };
9961
+ }
9962
+ if (direction === "up") {
9963
+ if (!inHistory && !atStart) {
9964
+ return { handled: false, state };
9965
+ }
9966
+ if (!inHistory) {
9967
+ const index = history.length - 1;
9968
+ return {
9969
+ handled: true,
9970
+ value: history[index],
9971
+ state: { index, draft: currentValue }
9972
+ };
9973
+ }
9974
+ if (state.index > 0) {
9975
+ const index = state.index - 1;
9976
+ return {
9977
+ handled: true,
9978
+ value: history[index],
9979
+ state: { index, draft: state.draft }
9980
+ };
9981
+ }
9982
+ return { handled: true, state };
9983
+ }
9984
+ if (!inHistory) {
9985
+ return { handled: false, state };
9986
+ }
9987
+ if (state.index < history.length - 1) {
9988
+ const index = state.index + 1;
9989
+ return {
9990
+ handled: true,
9991
+ value: history[index],
9992
+ state: { index, draft: state.draft }
9993
+ };
9994
+ }
9995
+ return {
9996
+ handled: true,
9997
+ value: state.draft,
9998
+ state: { ...INITIAL_HISTORY_STATE }
9999
+ };
10000
+ }
10001
+
8958
10002
  // src/utils/message-fingerprint.ts
8959
10003
  function computeMessageFingerprint(message, configVersion) {
8960
- 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;
10004
+ 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;
8961
10005
  return [
8962
10006
  message.id,
8963
10007
  message.role,
@@ -8974,8 +10018,10 @@ function computeMessageFingerprint(message, configVersion) {
8974
10018
  (_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 : "",
8975
10019
  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,
8976
10020
  (_A = (_z = (_y = message.reasoning) == null ? void 0 : _y.chunks) == null ? void 0 : _z.length) != null ? _A : 0,
8977
- (_C = (_B = message.contentParts) == null ? void 0 : _B.length) != null ? _C : 0,
8978
- (_D = message.stopReason) != null ? _D : "",
10021
+ (_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,
10022
+ (_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 : "",
10023
+ (_K = (_J = message.contentParts) == null ? void 0 : _J.length) != null ? _K : 0,
10024
+ (_L = message.stopReason) != null ? _L : "",
8979
10025
  configVersion
8980
10026
  ].join("\0");
8981
10027
  }
@@ -11501,11 +12547,11 @@ var createTypingIndicator = () => {
11501
12547
  container.appendChild(srOnly);
11502
12548
  return container;
11503
12549
  };
11504
- var renderLoadingIndicatorWithFallback = (location, customRenderer, widgetConfig) => {
12550
+ var renderLoadingIndicatorWithFallback = (location2, customRenderer, widgetConfig) => {
11505
12551
  const context = {
11506
12552
  config: widgetConfig != null ? widgetConfig : {},
11507
12553
  streaming: true,
11508
- location,
12554
+ location: location2,
11509
12555
  defaultRenderer: createTypingIndicator
11510
12556
  };
11511
12557
  if (customRenderer) {
@@ -16128,7 +17174,7 @@ function buildDropOverlay(dropCfg) {
16128
17174
  return overlay;
16129
17175
  }
16130
17176
  var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
16131
- 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;
17177
+ 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;
16132
17178
  if (mount == null) {
16133
17179
  throw new Error(
16134
17180
  'createAgentExperience: mount must be a non-null HTMLElement (e.g. pass document.getElementById("my-root") after the node exists).'
@@ -16884,7 +17930,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
16884
17930
  btn.style.cursor = "not-allowed";
16885
17931
  });
16886
17932
  }
16887
- session.resolveApproval(approvalMessage.approval, decision);
17933
+ if (approvalMessage.approval.toolType === "webmcp") {
17934
+ session.resolveWebMcpApproval(messageId, decision);
17935
+ } else {
17936
+ session.resolveApproval(approvalMessage.approval, decision);
17937
+ }
16888
17938
  });
16889
17939
  let artifactPaneApi = null;
16890
17940
  let artifactPanelResizeObs = null;
@@ -19219,17 +20269,77 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19219
20269
  }
19220
20270
  textarea.value = "";
19221
20271
  textarea.style.height = "auto";
20272
+ resetHistoryNavigation();
19222
20273
  session.sendMessage(value, { contentParts });
19223
20274
  if (hasAttachments) {
19224
20275
  attachmentManager.clearAttachments();
19225
20276
  }
19226
20277
  };
19227
- const handleInputEnter = (event) => {
20278
+ const historyNavigationEnabled = () => {
20279
+ var _a2;
20280
+ return ((_a2 = config.features) == null ? void 0 : _a2.composerHistory) !== false;
20281
+ };
20282
+ let composerHistoryState = { ...INITIAL_HISTORY_STATE };
20283
+ let suppressHistoryReset = false;
20284
+ const resetHistoryNavigation = () => {
20285
+ composerHistoryState = { ...INITIAL_HISTORY_STATE };
20286
+ };
20287
+ const getUserMessageHistory = () => session.getMessages().filter((message) => message.role === "user").map((message) => {
20288
+ var _a2;
20289
+ return (_a2 = message.content) != null ? _a2 : "";
20290
+ }).filter((text) => text.length > 0);
20291
+ const applyHistoryValue = (value) => {
20292
+ if (!textarea) return;
20293
+ suppressHistoryReset = true;
20294
+ textarea.value = value;
20295
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
20296
+ suppressHistoryReset = false;
20297
+ const end = textarea.value.length;
20298
+ textarea.setSelectionRange(end, end);
20299
+ };
20300
+ const handleComposerInput = () => {
20301
+ if (suppressHistoryReset) return;
20302
+ resetHistoryNavigation();
20303
+ };
20304
+ const handleComposerKeydown = (event) => {
20305
+ if (!textarea) return;
20306
+ if (historyNavigationEnabled() && (event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey && !event.isComposing) {
20307
+ const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0;
20308
+ const result = navigateComposerHistory({
20309
+ direction: event.key === "ArrowUp" ? "up" : "down",
20310
+ history: getUserMessageHistory(),
20311
+ currentValue: textarea.value,
20312
+ atStart,
20313
+ state: composerHistoryState
20314
+ });
20315
+ composerHistoryState = result.state;
20316
+ if (result.handled) {
20317
+ event.preventDefault();
20318
+ if (result.value !== void 0) {
20319
+ applyHistoryValue(result.value);
20320
+ }
20321
+ return;
20322
+ }
20323
+ }
19228
20324
  if (event.key === "Enter" && !event.shiftKey) {
20325
+ if (session.isStreaming()) {
20326
+ event.preventDefault();
20327
+ return;
20328
+ }
20329
+ resetHistoryNavigation();
19229
20330
  event.preventDefault();
19230
20331
  sendButton.click();
19231
20332
  }
19232
20333
  };
20334
+ const handleEscStop = (event) => {
20335
+ if (event.key !== "Escape" || event.isComposing) return;
20336
+ if (!session.isStreaming()) return;
20337
+ if (!event.composedPath().includes(container)) return;
20338
+ session.cancel();
20339
+ resetHistoryNavigation();
20340
+ event.preventDefault();
20341
+ event.stopImmediatePropagation();
20342
+ };
19233
20343
  const handleInputPaste = async (event) => {
19234
20344
  var _a2;
19235
20345
  if (((_a2 = config.attachments) == null ? void 0 : _a2.enabled) !== true || !attachmentManager) return;
@@ -19902,8 +21012,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19902
21012
  if (composerForm) {
19903
21013
  composerForm.addEventListener("submit", handleSubmit);
19904
21014
  }
19905
- textarea == null ? void 0 : textarea.addEventListener("keydown", handleInputEnter);
21015
+ textarea == null ? void 0 : textarea.addEventListener("keydown", handleComposerKeydown);
21016
+ textarea == null ? void 0 : textarea.addEventListener("input", handleComposerInput);
19906
21017
  textarea == null ? void 0 : textarea.addEventListener("paste", handleInputPaste);
21018
+ const escStopDoc = (_P = mount.ownerDocument) != null ? _P : document;
21019
+ escStopDoc.addEventListener("keydown", handleEscStop, true);
19907
21020
  const ATTACHMENT_DROP_ACTIVE_CLASS = "persona-attachment-drop-active";
19908
21021
  let attachmentFileDragDepth = 0;
19909
21022
  const clearAttachmentDropVisual = () => {
@@ -19963,8 +21076,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19963
21076
  if (composerForm) {
19964
21077
  composerForm.removeEventListener("submit", handleSubmit);
19965
21078
  }
19966
- textarea == null ? void 0 : textarea.removeEventListener("keydown", handleInputEnter);
21079
+ textarea == null ? void 0 : textarea.removeEventListener("keydown", handleComposerKeydown);
21080
+ textarea == null ? void 0 : textarea.removeEventListener("input", handleComposerInput);
19967
21081
  textarea == null ? void 0 : textarea.removeEventListener("paste", handleInputPaste);
21082
+ escStopDoc.removeEventListener("keydown", handleEscStop, true);
19968
21083
  });
19969
21084
  destroyCallbacks.push(() => {
19970
21085
  container.removeEventListener("dragenter", handleAttachmentDragEnterCapture, attachmentDropCapture);
@@ -19989,7 +21104,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
19989
21104
  }
19990
21105
  const controller = {
19991
21106
  update(nextConfig) {
19992
- 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;
21107
+ 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;
19993
21108
  const previousToolCallConfig = config.toolCall;
19994
21109
  const previousMessageActions = config.messageActions;
19995
21110
  const previousLayoutMessages = (_a2 = config.layout) == null ? void 0 : _a2.messages;
@@ -20263,7 +21378,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
20263
21378
  }
20264
21379
  }
20265
21380
  const layoutShowTitle = (_T2 = (_S2 = config.layout) == null ? void 0 : _S2.header) == null ? void 0 : _T2.showTitle;
20266
- const layoutShowSubtitle = (_V = (_U2 = config.layout) == null ? void 0 : _U2.header) == null ? void 0 : _V.showSubtitle;
21381
+ const layoutShowSubtitle = (_V2 = (_U2 = config.layout) == null ? void 0 : _U2.header) == null ? void 0 : _V2.showSubtitle;
20267
21382
  if (headerTitle) {
20268
21383
  headerTitle.style.display = layoutShowTitle === false ? "none" : "";
20269
21384
  }
@@ -21188,6 +22303,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21188
22303
  if (!(approvalMessage == null ? void 0 : approvalMessage.approval)) {
21189
22304
  throw new Error(`Approval not found: ${approvalId}`);
21190
22305
  }
22306
+ if (approvalMessage.approval.toolType === "webmcp") {
22307
+ session.resolveWebMcpApproval(approvalMessage.id, decision);
22308
+ return;
22309
+ }
21191
22310
  return session.resolveApproval(approvalMessage.approval, decision);
21192
22311
  },
21193
22312
  getMessages() {
@@ -21289,7 +22408,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21289
22408
  }
21290
22409
  }
21291
22410
  };
21292
- const shouldExposeDebugApi = ((_P = runtimeOptions == null ? void 0 : runtimeOptions.debugTools) != null ? _P : false) || Boolean(config.debug);
22411
+ const shouldExposeDebugApi = ((_Q = runtimeOptions == null ? void 0 : runtimeOptions.debugTools) != null ? _Q : false) || Boolean(config.debug);
21293
22412
  if (shouldExposeDebugApi && typeof window !== "undefined") {
21294
22413
  const previousDebug = window.AgentWidgetBrowser;
21295
22414
  const debugApi = {
@@ -21392,9 +22511,9 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21392
22511
  const voiceKey = `${persistConfig.keyPrefix}widget-voice`;
21393
22512
  const voiceModeKey = `${persistConfig.keyPrefix}widget-voice-mode`;
21394
22513
  if (storage) {
21395
- const wasOpen = ((_Q = persistConfig.persist) == null ? void 0 : _Q.openState) && storage.getItem(openKey) === "true";
21396
- const wasVoiceActive = ((_R = persistConfig.persist) == null ? void 0 : _R.voiceState) && storage.getItem(voiceKey) === "true";
21397
- const wasInVoiceMode = ((_S = persistConfig.persist) == null ? void 0 : _S.voiceState) && storage.getItem(voiceModeKey) === "true";
22514
+ const wasOpen = ((_R = persistConfig.persist) == null ? void 0 : _R.openState) && storage.getItem(openKey) === "true";
22515
+ const wasVoiceActive = ((_S = persistConfig.persist) == null ? void 0 : _S.voiceState) && storage.getItem(voiceKey) === "true";
22516
+ const wasInVoiceMode = ((_T = persistConfig.persist) == null ? void 0 : _T.voiceState) && storage.getItem(voiceModeKey) === "true";
21398
22517
  if (wasOpen) {
21399
22518
  setTimeout(() => {
21400
22519
  controller.open();
@@ -21411,7 +22530,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21411
22530
  }, 100);
21412
22531
  }, 0);
21413
22532
  }
21414
- if ((_T = persistConfig.persist) == null ? void 0 : _T.openState) {
22533
+ if ((_U = persistConfig.persist) == null ? void 0 : _U.openState) {
21415
22534
  eventBus.on("widget:opened", () => {
21416
22535
  storage.setItem(openKey, "true");
21417
22536
  });
@@ -21419,7 +22538,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
21419
22538
  storage.setItem(openKey, "false");
21420
22539
  });
21421
22540
  }
21422
- if ((_U = persistConfig.persist) == null ? void 0 : _U.voiceState) {
22541
+ if ((_V = persistConfig.persist) == null ? void 0 : _V.voiceState) {
21423
22542
  eventBus.on("voice:state", (event) => {
21424
22543
  storage.setItem(voiceKey, event.active ? "true" : "false");
21425
22544
  });
@@ -22570,6 +23689,1054 @@ function createThemePreview(container, initialOptions) {
22570
23689
  }
22571
23690
  };
22572
23691
  }
23692
+
23693
+ // src/theme-editor/webmcp/types.ts
23694
+ function toolResult(payload) {
23695
+ return {
23696
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
23697
+ structuredContent: payload
23698
+ };
23699
+ }
23700
+
23701
+ // src/theme-editor/webmcp/coerce.ts
23702
+ var CSS_NAMED_COLORS = {
23703
+ black: "#000000",
23704
+ white: "#ffffff",
23705
+ red: "#ff0000",
23706
+ green: "#008000",
23707
+ lime: "#00ff00",
23708
+ blue: "#0000ff",
23709
+ yellow: "#ffff00",
23710
+ cyan: "#00ffff",
23711
+ aqua: "#00ffff",
23712
+ magenta: "#ff00ff",
23713
+ fuchsia: "#ff00ff",
23714
+ silver: "#c0c0c0",
23715
+ gray: "#808080",
23716
+ grey: "#808080",
23717
+ maroon: "#800000",
23718
+ olive: "#808000",
23719
+ purple: "#800080",
23720
+ teal: "#008080",
23721
+ navy: "#000080",
23722
+ orange: "#ffa500",
23723
+ pink: "#ffc0cb",
23724
+ hotpink: "#ff69b4",
23725
+ gold: "#ffd700",
23726
+ indigo: "#4b0082",
23727
+ violet: "#ee82ee",
23728
+ brown: "#a52a2a",
23729
+ beige: "#f5f5dc",
23730
+ ivory: "#fffff0",
23731
+ khaki: "#f0e68c",
23732
+ coral: "#ff7f50",
23733
+ salmon: "#fa8072",
23734
+ tomato: "#ff6347",
23735
+ crimson: "#dc143c",
23736
+ turquoise: "#40e0d0",
23737
+ lavender: "#e6e6fa",
23738
+ plum: "#dda0dd",
23739
+ orchid: "#da70d6",
23740
+ tan: "#d2b48c",
23741
+ chocolate: "#d2691e",
23742
+ sienna: "#a0522d",
23743
+ slategray: "#708090",
23744
+ slategrey: "#708090",
23745
+ steelblue: "#4682b4",
23746
+ royalblue: "#4169e1",
23747
+ dodgerblue: "#1e90ff",
23748
+ skyblue: "#87ceeb",
23749
+ lightblue: "#add8e6",
23750
+ midnightblue: "#191970",
23751
+ forestgreen: "#228b22",
23752
+ seagreen: "#2e8b57",
23753
+ limegreen: "#32cd32",
23754
+ olivedrab: "#6b8e23",
23755
+ darkgreen: "#006400",
23756
+ emerald: "#50c878",
23757
+ mint: "#3eb489",
23758
+ goldenrod: "#daa520",
23759
+ firebrick: "#b22222",
23760
+ darkred: "#8b0000",
23761
+ indianred: "#cd5c5c",
23762
+ deeppink: "#ff1493",
23763
+ mediumpurple: "#9370db",
23764
+ rebeccapurple: "#663399",
23765
+ darkviolet: "#9400d3",
23766
+ slateblue: "#6a5acd",
23767
+ cornflowerblue: "#6495ed",
23768
+ teal2: "#008080",
23769
+ charcoal: "#36454f",
23770
+ graphite: "#3b3b3b",
23771
+ transparent: "transparent"
23772
+ };
23773
+ 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*)?\)$/;
23774
+ function isValidRgb(value) {
23775
+ return RGB_RE.test(value);
23776
+ }
23777
+ function coerceColor(input) {
23778
+ if (typeof input !== "string" || input.trim() === "") {
23779
+ throw new Error('Color must be a non-empty string (e.g. "#2563eb" or "blue").');
23780
+ }
23781
+ const trimmed = input.trim().toLowerCase();
23782
+ const named = CSS_NAMED_COLORS[trimmed];
23783
+ if (named) return named;
23784
+ const normalized = normalizeColorValue(trimmed);
23785
+ if (isValidHex(normalized) || normalized === "transparent" || isValidRgb(normalized)) {
23786
+ return normalized;
23787
+ }
23788
+ throw new Error(
23789
+ `"${input}" is not a recognized color. Pass a hex value like "#ef4444" or a CSS color name (e.g. ${Object.keys(
23790
+ CSS_NAMED_COLORS
23791
+ ).slice(0, 6).join(", ")}).`
23792
+ );
23793
+ }
23794
+ var ROLE_FAMILY_NAMES = ROLE_FAMILIES.map(
23795
+ (f) => f === "gray" ? "neutral" : f
23796
+ );
23797
+ var FAMILY_SYNONYMS = {
23798
+ ...Object.fromEntries(ROLE_FAMILY_NAMES.map((f) => [f, f])),
23799
+ gray: "neutral",
23800
+ grey: "neutral"
23801
+ };
23802
+ function coerceFamily(input, allowNeutral = true) {
23803
+ const key = String(input != null ? input : "").trim().toLowerCase();
23804
+ const family = FAMILY_SYNONYMS[key];
23805
+ if (!family || !allowNeutral && family === "neutral") {
23806
+ const valid = allowNeutral ? "primary, secondary, accent, neutral" : "primary, secondary, accent";
23807
+ throw new Error(`Unknown color family "${input}". Valid families: ${valid}.`);
23808
+ }
23809
+ return family;
23810
+ }
23811
+ function coerceIntensity(input) {
23812
+ const key = String(input != null ? input : "solid").trim().toLowerCase();
23813
+ if (key === "solid" || key === "soft") return key;
23814
+ throw new Error(`Unknown intensity "${input}". Valid intensities: solid, soft.`);
23815
+ }
23816
+ function coerceScheme(input) {
23817
+ const key = String(input != null ? input : "").trim().toLowerCase();
23818
+ if (key === "light" || key === "dark" || key === "auto") return key;
23819
+ if (key === "system") return "auto";
23820
+ throw new Error(`Unknown color scheme "${input}". Valid: light, dark, auto.`);
23821
+ }
23822
+ var ROUNDNESS_SYNONYMS = {
23823
+ sharp: "sharp",
23824
+ square: "sharp",
23825
+ none: "sharp",
23826
+ default: "default",
23827
+ normal: "default",
23828
+ rounded: "rounded",
23829
+ round: "rounded",
23830
+ soft: "rounded",
23831
+ pill: "pill",
23832
+ circle: "pill",
23833
+ full: "pill"
23834
+ };
23835
+ function coerceRoundnessStyle(input) {
23836
+ const key = String(input != null ? input : "").trim().toLowerCase();
23837
+ const style = ROUNDNESS_SYNONYMS[key];
23838
+ if (!style) {
23839
+ throw new Error(`Unknown roundness "${input}". Valid: sharp, default, rounded, pill.`);
23840
+ }
23841
+ return style;
23842
+ }
23843
+ function coerceRadius(input) {
23844
+ if (typeof input === "number" && Number.isFinite(input)) {
23845
+ return `${input}px`;
23846
+ }
23847
+ if (typeof input === "string" && input.trim() !== "") {
23848
+ const trimmed = input.trim();
23849
+ if (trimmed === "9999px" || /^(100%|9999px)$/.test(trimmed)) return "9999px";
23850
+ const parsed = parseCssValue(trimmed);
23851
+ return formatCssValue(parsed.value, parsed.unit);
23852
+ }
23853
+ throw new Error('Radius must be a number (px) or a CSS length string like "0.5rem".');
23854
+ }
23855
+ function refsFromTypographyField(fieldId) {
23856
+ for (const section of STYLE_SECTIONS) {
23857
+ const field = section.fields.find((f) => f.id === fieldId);
23858
+ if (field == null ? void 0 : field.options) {
23859
+ const map = {};
23860
+ for (const opt of field.options) {
23861
+ const key = opt.value.split(".").pop();
23862
+ if (key) map[key] = opt.value;
23863
+ }
23864
+ return map;
23865
+ }
23866
+ }
23867
+ return {};
23868
+ }
23869
+ var FAMILY_BASE = refsFromTypographyField("typo-font-family");
23870
+ var SIZE_BASE = refsFromTypographyField("typo-font-size");
23871
+ var WEIGHT_BASE = refsFromTypographyField("typo-font-weight");
23872
+ var LINE_BASE = refsFromTypographyField("typo-line-height");
23873
+ var FONT_FAMILY_REFS = {
23874
+ ...FAMILY_BASE,
23875
+ monospace: FAMILY_BASE.mono
23876
+ };
23877
+ var FONT_SIZE_REFS = {
23878
+ ...SIZE_BASE,
23879
+ small: SIZE_BASE.sm,
23880
+ md: SIZE_BASE.base,
23881
+ medium: SIZE_BASE.base,
23882
+ large: SIZE_BASE.lg
23883
+ };
23884
+ var FONT_WEIGHT_REFS = {
23885
+ ...WEIGHT_BASE,
23886
+ "400": WEIGHT_BASE.normal,
23887
+ "500": WEIGHT_BASE.medium,
23888
+ "600": WEIGHT_BASE.semibold,
23889
+ "700": WEIGHT_BASE.bold
23890
+ };
23891
+ var LINE_HEIGHT_REFS = {
23892
+ ...LINE_BASE,
23893
+ "1.25": LINE_BASE.tight,
23894
+ "1.5": LINE_BASE.normal,
23895
+ "1.625": LINE_BASE.relaxed
23896
+ };
23897
+ function coerceTypographyRef(input, refs, label) {
23898
+ const key = String(input != null ? input : "").trim().toLowerCase();
23899
+ const ref = refs[key];
23900
+ if (!ref) {
23901
+ const valid = [...new Set(Object.values(refs).map((r) => r.split(".").pop()))].join(", ");
23902
+ throw new Error(`Unknown ${label} "${input}". Valid: ${valid}.`);
23903
+ }
23904
+ return ref;
23905
+ }
23906
+
23907
+ // src/theme-editor/webmcp/summary.ts
23908
+ var RADIUS_PATH_PREFIX = "theme.palette.radius.";
23909
+ function buildRadiusPresets() {
23910
+ var _a;
23911
+ const presets = {
23912
+ pill: { sm: "9999px", md: "9999px", lg: "9999px", xl: "9999px", full: "9999px" }
23913
+ };
23914
+ for (const section of STYLE_SECTIONS) {
23915
+ for (const preset of (_a = section.presets) != null ? _a : []) {
23916
+ const match = preset.id.match(/^radius-(\w+)$/);
23917
+ if (!match) continue;
23918
+ const radius = {};
23919
+ for (const [path, value] of Object.entries(preset.values)) {
23920
+ if (path.startsWith(RADIUS_PATH_PREFIX) && typeof value === "string") {
23921
+ radius[path.slice(RADIUS_PATH_PREFIX.length)] = value;
23922
+ }
23923
+ }
23924
+ presets[match[1]] = radius;
23925
+ }
23926
+ }
23927
+ return presets;
23928
+ }
23929
+ var RADIUS_PRESETS = buildRadiusPresets();
23930
+ var RADIUS_KEYS = ["sm", "md", "lg", "xl", "full"];
23931
+ function resolveColor(state, path, prefix = "theme", depth = 0) {
23932
+ if (depth > 6) return null;
23933
+ const raw = state.get(`${prefix}.${path}`);
23934
+ if (typeof raw !== "string" || raw === "") {
23935
+ return prefix === "darkTheme" ? resolveColor(state, path, "theme", depth) : null;
23936
+ }
23937
+ if (raw.startsWith("#") || raw.startsWith("rgb") || raw === "transparent") return raw;
23938
+ if (raw.startsWith("palette.") || raw.startsWith("semantic.") || raw.startsWith("components.")) {
23939
+ return resolveColor(state, raw, prefix, depth + 1);
23940
+ }
23941
+ return null;
23942
+ }
23943
+ function refSuffix(state, path) {
23944
+ const raw = state.get(path);
23945
+ if (typeof raw !== "string") return "unknown";
23946
+ const parts = raw.split(".");
23947
+ return parts[parts.length - 1] || String(raw);
23948
+ }
23949
+ function roleKey(roleId) {
23950
+ return roleId.replace(/^role-/, "");
23951
+ }
23952
+ function detectRoundness(radius) {
23953
+ for (const [style, preset] of Object.entries(RADIUS_PRESETS)) {
23954
+ if (RADIUS_KEYS.every((k) => radius[k] === preset[k])) return style;
23955
+ }
23956
+ return "custom";
23957
+ }
23958
+ function buildSummary(state) {
23959
+ var _a, _b;
23960
+ const radius = {};
23961
+ for (const k of RADIUS_KEYS) {
23962
+ radius[k] = String((_a = state.get(`theme.palette.radius.${k}`)) != null ? _a : "");
23963
+ }
23964
+ const roles = {};
23965
+ for (const role of ALL_ROLES) {
23966
+ roles[roleKey(role.roleId)] = detectRoleAssignment(
23967
+ (p) => state.get(`theme.${p}`),
23968
+ role
23969
+ );
23970
+ }
23971
+ return {
23972
+ brand: {
23973
+ primary: asColor(state.get("theme.palette.colors.primary.500")),
23974
+ secondary: asColor(state.get("theme.palette.colors.secondary.500")),
23975
+ accent: asColor(state.get("theme.palette.colors.accent.500"))
23976
+ },
23977
+ roles,
23978
+ typography: {
23979
+ fontFamily: refSuffix(state, "theme.semantic.typography.fontFamily"),
23980
+ fontSize: refSuffix(state, "theme.semantic.typography.fontSize"),
23981
+ fontWeight: refSuffix(state, "theme.semantic.typography.fontWeight"),
23982
+ lineHeight: refSuffix(state, "theme.semantic.typography.lineHeight")
23983
+ },
23984
+ roundness: { style: detectRoundness(radius), radius },
23985
+ colorScheme: String((_b = state.get("colorScheme")) != null ? _b : "light"),
23986
+ history: {
23987
+ index: state.getHistoryIndex(),
23988
+ canUndo: state.canUndo(),
23989
+ canRedo: state.canRedo()
23990
+ }
23991
+ };
23992
+ }
23993
+ function asColor(value) {
23994
+ return typeof value === "string" && value !== "" ? value : null;
23995
+ }
23996
+ var CONTRAST_PAIRS = [
23997
+ { key: "user-message", label: "User message text", fg: "components.message.user.text", bg: "components.message.user.background" },
23998
+ { key: "assistant-message", label: "Assistant message text", fg: "components.message.assistant.text", bg: "components.message.assistant.background" },
23999
+ { key: "header", label: "Header title", fg: "components.header.titleForeground", bg: "components.header.background" },
24000
+ { key: "primary-button", label: "Primary button label", fg: "components.button.primary.foreground", bg: "components.button.primary.background" },
24001
+ { key: "input", label: "Input placeholder", fg: "components.input.placeholder", bg: "components.input.background" },
24002
+ { key: "link", label: "Link text", fg: "components.markdown.link.foreground", bg: "semantic.colors.background" },
24003
+ { key: "scroll", label: "Scroll-to-bottom icon", fg: "components.scrollToBottom.foreground", bg: "components.scrollToBottom.background" },
24004
+ { key: "body", label: "Body text on background", fg: "semantic.colors.text", bg: "semantic.colors.background" },
24005
+ { key: "surface", label: "Body text on surface", fg: "semantic.colors.text", bg: "semantic.colors.surface" }
24006
+ ];
24007
+ function roleContrastPairKeys(role) {
24008
+ const targets = new Set(role.targets.map((t) => t.path));
24009
+ return CONTRAST_PAIRS.filter((p) => targets.has(p.fg) || targets.has(p.bg)).map((p) => p.key);
24010
+ }
24011
+ var CONTRAST_THRESHOLDS = { AA: 4.5, AAA: 7 };
24012
+ function round2(n) {
24013
+ return Math.round(n * 100) / 100;
24014
+ }
24015
+ function suggestShade(state, fgPath, bgHex, threshold, prefix) {
24016
+ const raw = state.get(`${prefix}.${fgPath}`);
24017
+ if (typeof raw !== "string") return null;
24018
+ const m = raw.match(/^palette\.colors\.(\w+)\.(\d+)$/);
24019
+ if (!m) return null;
24020
+ const family = m[1];
24021
+ const currentIdx = SHADE_KEYS.indexOf(m[2]);
24022
+ let best = null;
24023
+ let bestDistance = Infinity;
24024
+ SHADE_KEYS.forEach((shade, idx) => {
24025
+ const hex = state.get(`${prefix}.palette.colors.${family}.${shade}`);
24026
+ if (typeof hex !== "string" || !(hex.startsWith("#") || hex.startsWith("rgb"))) return;
24027
+ if (wcagContrastRatio(hex, bgHex) >= threshold) {
24028
+ const distance = currentIdx >= 0 ? Math.abs(idx - currentIdx) : idx;
24029
+ if (distance < bestDistance) {
24030
+ bestDistance = distance;
24031
+ best = `palette.colors.${family}.${shade}`;
24032
+ }
24033
+ }
24034
+ });
24035
+ return best;
24036
+ }
24037
+ function runContrastChecks(state, level = "AA", variant = "both", pairKeys) {
24038
+ const threshold = CONTRAST_THRESHOLDS[level];
24039
+ const variants = variant === "both" ? ["light", "dark"] : [variant];
24040
+ const pairs = pairKeys ? CONTRAST_PAIRS.filter((p) => pairKeys.includes(p.key)) : CONTRAST_PAIRS;
24041
+ const checks = [];
24042
+ for (const v of variants) {
24043
+ const prefix = v === "light" ? "theme" : "darkTheme";
24044
+ for (const pair of pairs) {
24045
+ const fg = resolveColor(state, pair.fg, prefix);
24046
+ const bg = resolveColor(state, pair.bg, prefix);
24047
+ if (!fg || !bg) continue;
24048
+ const ratio = round2(wcagContrastRatio(fg, bg));
24049
+ const passes = ratio >= threshold;
24050
+ const check = {
24051
+ pair: pair.key,
24052
+ label: pair.label,
24053
+ variant: v,
24054
+ fg,
24055
+ bg,
24056
+ ratio,
24057
+ threshold,
24058
+ passes
24059
+ };
24060
+ if (!passes) {
24061
+ const suggestion = suggestShade(state, pair.fg, bg, threshold, prefix);
24062
+ if (suggestion) check.suggestion = suggestion;
24063
+ }
24064
+ checks.push(check);
24065
+ }
24066
+ }
24067
+ return { level, checks, failures: checks.filter((c) => !c.passes) };
24068
+ }
24069
+ function quickContrastWarnings(state, pairKeys, variant = "light", level = "AA") {
24070
+ if (pairKeys.length === 0) return [];
24071
+ const report = runContrastChecks(state, level, variant, pairKeys);
24072
+ return report.failures.map((f) => ({
24073
+ code: "contrast",
24074
+ pair: f.pair,
24075
+ variant: f.variant,
24076
+ ratio: f.ratio,
24077
+ threshold: f.threshold,
24078
+ 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.` : "."}`
24079
+ }));
24080
+ }
24081
+
24082
+ // src/theme-editor/webmcp/tools.ts
24083
+ var ROLE_ALIASES = {};
24084
+ for (const role of ALL_ROLES) {
24085
+ const key = roleKey(role.roleId);
24086
+ ROLE_ALIASES[key] = role;
24087
+ ROLE_ALIASES[role.roleId] = role;
24088
+ }
24089
+ Object.assign(ROLE_ALIASES, {
24090
+ surface: ROLE_ALIASES["surfaces"],
24091
+ background: ROLE_ALIASES["surfaces"],
24092
+ backgrounds: ROLE_ALIASES["surfaces"],
24093
+ user: ROLE_ALIASES["user-messages"],
24094
+ "user-message": ROLE_ALIASES["user-messages"],
24095
+ assistant: ROLE_ALIASES["assistant-messages"],
24096
+ "assistant-message": ROLE_ALIASES["assistant-messages"],
24097
+ actions: ROLE_ALIASES["primary-actions"],
24098
+ buttons: ROLE_ALIASES["primary-actions"],
24099
+ composer: ROLE_ALIASES["input"],
24100
+ links: ROLE_ALIASES["links-focus"],
24101
+ focus: ROLE_ALIASES["links-focus"],
24102
+ border: ROLE_ALIASES["borders"],
24103
+ dividers: ROLE_ALIASES["borders"],
24104
+ scroll: ROLE_ALIASES["scroll-to-bottom"]
24105
+ });
24106
+ function coerceRole(input) {
24107
+ const key = String(input != null ? input : "").trim().toLowerCase();
24108
+ const role = ROLE_ALIASES[key];
24109
+ if (!role) {
24110
+ const valid = ALL_ROLES.map((r) => roleKey(r.roleId)).join(", ");
24111
+ throw new Error(`Unknown role "${input}". Valid roles: ${valid}.`);
24112
+ }
24113
+ return role;
24114
+ }
24115
+ function buildFieldIndex() {
24116
+ const index = /* @__PURE__ */ new Map();
24117
+ const addSections = (sections) => {
24118
+ for (const section of sections) {
24119
+ for (const field of section.fields) {
24120
+ if (!index.has(field.id)) index.set(field.id, field);
24121
+ }
24122
+ }
24123
+ };
24124
+ for (const tab of ALL_TABS) addSections(tab.sections);
24125
+ for (const group of CONFIGURE_SUB_GROUPS) addSections(group.sections);
24126
+ return index;
24127
+ }
24128
+ var FEATURE_PATHS = {
24129
+ voice: "voiceRecognition.enabled",
24130
+ artifacts: "features.artifacts.enabled",
24131
+ attachments: "attachments.enabled",
24132
+ toolCalls: "features.showToolCalls",
24133
+ reasoning: "features.showReasoning",
24134
+ feedback: "messageActions.enabled"
24135
+ };
24136
+ var LAYOUT_PATHS = {
24137
+ avatars: "layout.messages.avatar.show",
24138
+ timestamps: "layout.messages.timestamp.show",
24139
+ showHeader: "layout.showHeader",
24140
+ messageStyle: "layout.messages.layout"
24141
+ };
24142
+ var LAUNCHER_POSITIONS = ["bottom-right", "bottom-left", "top-right", "top-left"];
24143
+ var MESSAGE_STYLES = ["bubble", "flat", "minimal"];
24144
+ var COPY_PATHS = {
24145
+ title: "copy.welcomeTitle",
24146
+ subtitle: "copy.welcomeSubtitle",
24147
+ placeholder: "copy.inputPlaceholder",
24148
+ sendLabel: "copy.sendButtonLabel"
24149
+ };
24150
+ function createThemeEditorTools(state, options) {
24151
+ var _a;
24152
+ let editTarget = (_a = options == null ? void 0 : options.editTarget) != null ? _a : "both";
24153
+ let fieldIndex = null;
24154
+ const rec = (input) => input && typeof input === "object" ? input : {};
24155
+ const expandScoped = (themeRelPath, value, target = editTarget) => {
24156
+ const out = {};
24157
+ if (target === "light" || target === "both") out[`theme.${themeRelPath}`] = value;
24158
+ if (target === "dark" || target === "both") out[`darkTheme.${themeRelPath}`] = value;
24159
+ return out;
24160
+ };
24161
+ const filterByEditTarget = (writes) => {
24162
+ if (editTarget === "both") return writes;
24163
+ const out = {};
24164
+ for (const [k, v] of Object.entries(writes)) {
24165
+ if (editTarget === "light" && k.startsWith("theme.")) out[k] = v;
24166
+ else if (editTarget === "dark" && k.startsWith("darkTheme.")) out[k] = v;
24167
+ }
24168
+ return out;
24169
+ };
24170
+ const warnVariant = () => editTarget === "dark" ? "dark" : "light";
24171
+ const result = (applied, warnings = []) => toolResult({ ok: true, summary: buildSummary(state), warnings, applied });
24172
+ const getThemeOverview = {
24173
+ name: "get_theme_overview",
24174
+ title: "Get current theme & what is editable",
24175
+ 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.",
24176
+ annotations: { readOnlyHint: true },
24177
+ inputSchema: {
24178
+ type: "object",
24179
+ properties: {
24180
+ verbosity: {
24181
+ type: "string",
24182
+ enum: ["summary", "full"],
24183
+ description: "Use 'full' to also include the field-id index for set_theme_fields."
24184
+ }
24185
+ },
24186
+ additionalProperties: false
24187
+ },
24188
+ execute(input) {
24189
+ const { verbosity } = rec(input);
24190
+ const payload = {
24191
+ summary: buildSummary(state),
24192
+ availableRoles: ALL_ROLES.map((r) => ({
24193
+ role: roleKey(r.roleId),
24194
+ helper: r.helper
24195
+ })),
24196
+ availableFamilies: ROLE_FAMILY_NAMES,
24197
+ presets: THEME_EDITOR_PRESETS.map((p) => {
24198
+ var _a2;
24199
+ return {
24200
+ id: p.id,
24201
+ name: p.name,
24202
+ description: p.description,
24203
+ tags: (_a2 = p.tags) != null ? _a2 : []
24204
+ };
24205
+ }),
24206
+ tools: [
24207
+ { tool: "set_brand_colors", hint: "Recolor the palette (primary/secondary/accent) \u2014 auto-generates shade scales." },
24208
+ { tool: "assign_color_role", hint: "Recolor a region (header, user/assistant messages, actions, input, links, borders, surfaces, scroll) with a family + intensity." },
24209
+ { tool: "set_typography", hint: "Set font family, size, weight, line height." },
24210
+ { tool: "set_roundness", hint: "Set corner roundness (sharp/default/rounded/pill) or granular radii." },
24211
+ { tool: "set_color_scheme", hint: "Set light/dark/auto and which variant edits target." },
24212
+ { tool: "apply_preset", hint: "Apply a complete built-in preset." },
24213
+ { tool: "configure_widget", hint: "Toggle launcher position, features, and layout." },
24214
+ { tool: "set_copy_and_suggestions", hint: "Set welcome copy, placeholder, and suggestion chips." },
24215
+ { tool: "set_theme_fields", hint: "Advanced escape hatch: set any field by id or dot-path." },
24216
+ { tool: "check_contrast", hint: "Audit WCAG contrast across key text/background pairs." },
24217
+ { tool: "manage_session", hint: "Undo, redo, reset, or export the theme." }
24218
+ ]
24219
+ };
24220
+ if (verbosity === "full") {
24221
+ fieldIndex != null ? fieldIndex : fieldIndex = buildFieldIndex();
24222
+ payload.fieldIndex = Array.from(fieldIndex.values()).map((f) => {
24223
+ var _a2;
24224
+ return {
24225
+ id: f.id,
24226
+ path: f.path,
24227
+ type: f.type,
24228
+ label: f.label,
24229
+ options: (_a2 = f.options) == null ? void 0 : _a2.map((o) => o.value)
24230
+ };
24231
+ });
24232
+ }
24233
+ return toolResult(payload);
24234
+ }
24235
+ };
24236
+ const setBrandColors = {
24237
+ name: "set_brand_colors",
24238
+ title: "Set brand colors",
24239
+ 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").',
24240
+ inputSchema: {
24241
+ type: "object",
24242
+ properties: {
24243
+ primary: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." },
24244
+ secondary: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." },
24245
+ accent: { type: "string", description: "Hex, rgb()/rgba(), or CSS color name." }
24246
+ },
24247
+ additionalProperties: false
24248
+ },
24249
+ execute(input) {
24250
+ const args = rec(input);
24251
+ const families = ["primary", "secondary", "accent"];
24252
+ const writes = {};
24253
+ const applied = {};
24254
+ for (const family of families) {
24255
+ if (args[family] === void 0) continue;
24256
+ const base = coerceColor(args[family]);
24257
+ applied[family] = base;
24258
+ const scale = generateColorScale(base);
24259
+ for (const shade of SHADE_KEYS) {
24260
+ const value = scale[shade];
24261
+ if (value === void 0) continue;
24262
+ Object.assign(writes, expandScoped(`palette.colors.${family}.${shade}`, value));
24263
+ }
24264
+ }
24265
+ if (Object.keys(applied).length === 0) {
24266
+ throw new Error("Provide at least one of: primary, secondary, accent.");
24267
+ }
24268
+ state.setBatch(writes);
24269
+ const warnings = quickContrastWarnings(
24270
+ state,
24271
+ ["primary-button", "user-message"],
24272
+ warnVariant()
24273
+ );
24274
+ return result(applied, warnings);
24275
+ }
24276
+ };
24277
+ const assignColorRole = {
24278
+ name: "assign_color_role",
24279
+ title: "Assign a color family to an interface role",
24280
+ 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).",
24281
+ inputSchema: {
24282
+ type: "object",
24283
+ properties: {
24284
+ role: { type: "string", description: 'Interface role, e.g. "header" or "user-messages".' },
24285
+ family: { type: "string", enum: ROLE_FAMILY_NAMES },
24286
+ intensity: { type: "string", enum: ["solid", "soft"], description: "Defaults to 'solid'." }
24287
+ },
24288
+ required: ["role", "family"],
24289
+ additionalProperties: false
24290
+ },
24291
+ execute(input) {
24292
+ const args = rec(input);
24293
+ const role = coerceRole(args.role);
24294
+ const family = coerceFamily(args.family, true);
24295
+ const intensity = coerceIntensity(args.intensity);
24296
+ const writes = filterByEditTarget(resolveRoleAssignment(family, intensity, role));
24297
+ const tokensWritten = Object.keys(writes).length;
24298
+ state.setBatch(writes);
24299
+ const warnings = quickContrastWarnings(state, roleContrastPairKeys(role), warnVariant());
24300
+ return result(
24301
+ { role: roleKey(role.roleId), family, intensity, tokensWritten },
24302
+ warnings
24303
+ );
24304
+ }
24305
+ };
24306
+ const setTypography = {
24307
+ name: "set_typography",
24308
+ title: "Set typography",
24309
+ 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).",
24310
+ inputSchema: {
24311
+ type: "object",
24312
+ properties: {
24313
+ fontFamily: { type: "string" },
24314
+ fontSize: { type: "string" },
24315
+ fontWeight: { type: ["string", "number"] },
24316
+ lineHeight: { type: ["string", "number"] }
24317
+ },
24318
+ additionalProperties: false
24319
+ },
24320
+ execute(input) {
24321
+ const args = rec(input);
24322
+ const writes = {};
24323
+ const applied = {};
24324
+ const apply = (key, refs) => {
24325
+ var _a2;
24326
+ if (args[key] === void 0) return;
24327
+ const ref = coerceTypographyRef(args[key], refs, key);
24328
+ applied[key] = (_a2 = ref.split(".").pop()) != null ? _a2 : ref;
24329
+ Object.assign(writes, expandScoped(`semantic.typography.${key}`, ref));
24330
+ };
24331
+ apply("fontFamily", FONT_FAMILY_REFS);
24332
+ apply("fontSize", FONT_SIZE_REFS);
24333
+ apply("fontWeight", FONT_WEIGHT_REFS);
24334
+ apply("lineHeight", LINE_HEIGHT_REFS);
24335
+ if (Object.keys(applied).length === 0) {
24336
+ throw new Error("Provide at least one of: fontFamily, fontSize, fontWeight, lineHeight.");
24337
+ }
24338
+ state.setBatch(writes);
24339
+ return result(applied);
24340
+ }
24341
+ };
24342
+ const setRoundness = {
24343
+ name: "set_roundness",
24344
+ title: "Set corner roundness",
24345
+ 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`.",
24346
+ inputSchema: {
24347
+ type: "object",
24348
+ properties: {
24349
+ style: { type: "string", enum: ["sharp", "default", "rounded", "pill"] },
24350
+ radius: {
24351
+ type: "object",
24352
+ description: "Granular overrides (px number or CSS length).",
24353
+ properties: {
24354
+ sm: { type: ["string", "number"] },
24355
+ md: { type: ["string", "number"] },
24356
+ lg: { type: ["string", "number"] },
24357
+ xl: { type: ["string", "number"] },
24358
+ full: { type: ["string", "number"] }
24359
+ },
24360
+ additionalProperties: false
24361
+ }
24362
+ },
24363
+ additionalProperties: false
24364
+ },
24365
+ execute(input) {
24366
+ const args = rec(input);
24367
+ const writes = {};
24368
+ const applied = {};
24369
+ if (args.style !== void 0) {
24370
+ const style = coerceRoundnessStyle(args.style);
24371
+ applied.style = style;
24372
+ for (const [key, value] of Object.entries(RADIUS_PRESETS[style])) {
24373
+ Object.assign(writes, expandScoped(`palette.radius.${key}`, value));
24374
+ }
24375
+ }
24376
+ if (args.radius !== void 0) {
24377
+ const radius = rec(args.radius);
24378
+ const overrides = {};
24379
+ for (const key of ["sm", "md", "lg", "xl", "full"]) {
24380
+ if (radius[key] === void 0) continue;
24381
+ const value = coerceRadius(radius[key]);
24382
+ overrides[key] = value;
24383
+ Object.assign(writes, expandScoped(`palette.radius.${key}`, value));
24384
+ }
24385
+ applied.radius = overrides;
24386
+ }
24387
+ if (Object.keys(writes).length === 0) {
24388
+ throw new Error("Provide `style` (sharp|default|rounded|pill) or a `radius` object.");
24389
+ }
24390
+ state.setBatch(writes);
24391
+ return result(applied);
24392
+ }
24393
+ };
24394
+ const setColorScheme = {
24395
+ name: "set_color_scheme",
24396
+ title: "Set color scheme",
24397
+ 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).",
24398
+ inputSchema: {
24399
+ type: "object",
24400
+ properties: {
24401
+ scheme: { type: "string", enum: ["light", "dark", "auto"] },
24402
+ editTarget: { type: "string", enum: ["light", "dark", "both"] }
24403
+ },
24404
+ additionalProperties: false
24405
+ },
24406
+ execute(input) {
24407
+ const args = rec(input);
24408
+ const applied = {};
24409
+ if (args.scheme !== void 0) {
24410
+ const scheme = coerceScheme(args.scheme);
24411
+ state.set("colorScheme", scheme);
24412
+ applied.scheme = scheme;
24413
+ }
24414
+ if (args.editTarget !== void 0) {
24415
+ const t = String(args.editTarget).trim().toLowerCase();
24416
+ if (t !== "light" && t !== "dark" && t !== "both") {
24417
+ throw new Error(`Unknown editTarget "${args.editTarget}". Valid: light, dark, both.`);
24418
+ }
24419
+ editTarget = t;
24420
+ applied.editTarget = t;
24421
+ }
24422
+ if (Object.keys(applied).length === 0) {
24423
+ throw new Error("Provide `scheme` and/or `editTarget`.");
24424
+ }
24425
+ return toolResult({ ok: true, summary: buildSummary(state), warnings: [], applied, editTarget });
24426
+ }
24427
+ };
24428
+ const applyPreset = {
24429
+ name: "apply_preset",
24430
+ title: "Apply a built-in theme preset",
24431
+ description: "Apply a complete built-in preset, replacing theme tokens. Call get_theme_overview to list preset ids.",
24432
+ inputSchema: {
24433
+ type: "object",
24434
+ properties: { presetId: { type: "string" } },
24435
+ required: ["presetId"],
24436
+ additionalProperties: false
24437
+ },
24438
+ execute(input) {
24439
+ const { presetId } = rec(input);
24440
+ const preset = getThemeEditorPreset(String(presetId != null ? presetId : ""));
24441
+ if (!preset) {
24442
+ const valid = THEME_EDITOR_PRESETS.map((p) => p.id).join(", ");
24443
+ throw new Error(`Unknown preset "${presetId}". Valid presets: ${valid}.`);
24444
+ }
24445
+ const theme = createTheme(preset.theme, { validate: false });
24446
+ const config = { ...state.getConfig() };
24447
+ if (preset.darkTheme) {
24448
+ config.darkTheme = createTheme(preset.darkTheme, { validate: false });
24449
+ }
24450
+ if (preset.toolCall) config.toolCall = preset.toolCall;
24451
+ state.setFullConfig(config, theme);
24452
+ const warnings = quickContrastWarnings(state, ["body", "assistant-message"], "light");
24453
+ return result({ appliedPreset: { id: preset.id, name: preset.name } }, warnings);
24454
+ }
24455
+ };
24456
+ const configureWidget = {
24457
+ name: "configure_widget",
24458
+ title: "Configure launcher, features, and layout",
24459
+ 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.",
24460
+ inputSchema: {
24461
+ type: "object",
24462
+ properties: {
24463
+ launcherPosition: { type: "string", enum: LAUNCHER_POSITIONS },
24464
+ features: {
24465
+ type: "object",
24466
+ properties: {
24467
+ voice: { type: "boolean" },
24468
+ artifacts: { type: "boolean" },
24469
+ attachments: { type: "boolean" },
24470
+ toolCalls: { type: "boolean" },
24471
+ reasoning: { type: "boolean" },
24472
+ feedback: { type: "boolean" }
24473
+ },
24474
+ additionalProperties: false
24475
+ },
24476
+ layout: {
24477
+ type: "object",
24478
+ properties: {
24479
+ avatars: { type: "boolean" },
24480
+ timestamps: { type: "boolean" },
24481
+ showHeader: { type: "boolean" },
24482
+ messageStyle: { type: "string", enum: MESSAGE_STYLES }
24483
+ },
24484
+ additionalProperties: false
24485
+ }
24486
+ },
24487
+ additionalProperties: false
24488
+ },
24489
+ execute(input) {
24490
+ var _a2, _b, _c;
24491
+ const args = rec(input);
24492
+ const writes = {};
24493
+ const applied = {};
24494
+ if (args.launcherPosition !== void 0) {
24495
+ const pos = String(args.launcherPosition);
24496
+ if (!LAUNCHER_POSITIONS.includes(pos)) {
24497
+ throw new Error(`Unknown launcherPosition "${pos}". Valid: ${LAUNCHER_POSITIONS.join(", ")}.`);
24498
+ }
24499
+ writes["launcher.position"] = pos;
24500
+ applied.launcherPosition = pos;
24501
+ }
24502
+ const features = rec(args.features);
24503
+ for (const [key, path] of Object.entries(FEATURE_PATHS)) {
24504
+ if (features[key] === void 0) continue;
24505
+ writes[path] = Boolean(features[key]);
24506
+ (_a2 = applied.features) != null ? _a2 : applied.features = {};
24507
+ applied.features[key] = Boolean(features[key]);
24508
+ }
24509
+ const layout = rec(args.layout);
24510
+ for (const [key, path] of Object.entries(LAYOUT_PATHS)) {
24511
+ if (layout[key] === void 0) continue;
24512
+ if (key === "messageStyle") {
24513
+ const style = String(layout[key]);
24514
+ if (!MESSAGE_STYLES.includes(style)) {
24515
+ throw new Error(`Unknown messageStyle "${style}". Valid: ${MESSAGE_STYLES.join(", ")}.`);
24516
+ }
24517
+ writes[path] = style;
24518
+ (_b = applied.layout) != null ? _b : applied.layout = {};
24519
+ applied.layout[key] = style;
24520
+ } else {
24521
+ writes[path] = Boolean(layout[key]);
24522
+ (_c = applied.layout) != null ? _c : applied.layout = {};
24523
+ applied.layout[key] = Boolean(layout[key]);
24524
+ }
24525
+ }
24526
+ if (Object.keys(writes).length === 0) {
24527
+ throw new Error("Provide launcherPosition, features, and/or layout.");
24528
+ }
24529
+ state.setBatch(writes);
24530
+ return result(applied);
24531
+ }
24532
+ };
24533
+ const setCopyAndSuggestions = {
24534
+ name: "set_copy_and_suggestions",
24535
+ title: "Set welcome copy and suggestion chips",
24536
+ 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).",
24537
+ inputSchema: {
24538
+ type: "object",
24539
+ properties: {
24540
+ title: { type: "string" },
24541
+ subtitle: { type: "string" },
24542
+ placeholder: { type: "string" },
24543
+ sendLabel: { type: "string" },
24544
+ suggestions: { type: "array", items: { type: "string" } }
24545
+ },
24546
+ additionalProperties: false
24547
+ },
24548
+ execute(input) {
24549
+ const args = rec(input);
24550
+ const writes = {};
24551
+ const applied = {};
24552
+ for (const [key, path] of Object.entries(COPY_PATHS)) {
24553
+ if (args[key] === void 0) continue;
24554
+ writes[path] = String(args[key]);
24555
+ applied[key] = String(args[key]);
24556
+ }
24557
+ if (args.suggestions !== void 0) {
24558
+ if (!Array.isArray(args.suggestions)) {
24559
+ throw new Error("`suggestions` must be an array of strings.");
24560
+ }
24561
+ const chips = args.suggestions.filter((s) => typeof s === "string");
24562
+ writes["suggestionChips"] = chips;
24563
+ applied.suggestions = chips;
24564
+ }
24565
+ if (Object.keys(writes).length === 0) {
24566
+ throw new Error("Provide at least one of: title, subtitle, placeholder, sendLabel, suggestions.");
24567
+ }
24568
+ state.setBatch(writes);
24569
+ return result(applied);
24570
+ }
24571
+ };
24572
+ const setThemeFields = {
24573
+ name: "set_theme_fields",
24574
+ title: "Set theme fields by id or path (advanced)",
24575
+ 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.',
24576
+ inputSchema: {
24577
+ type: "object",
24578
+ properties: {
24579
+ updates: {
24580
+ type: "array",
24581
+ items: {
24582
+ type: "object",
24583
+ properties: {
24584
+ field: { type: "string", description: "Field id or dot-path." },
24585
+ value: { type: ["string", "number", "boolean"] }
24586
+ },
24587
+ required: ["field", "value"],
24588
+ additionalProperties: false
24589
+ }
24590
+ }
24591
+ },
24592
+ required: ["updates"],
24593
+ additionalProperties: false
24594
+ },
24595
+ execute(input) {
24596
+ var _a2;
24597
+ const { updates } = rec(input);
24598
+ if (!Array.isArray(updates) || updates.length === 0) {
24599
+ throw new Error("`updates` must be a non-empty array of { field, value }.");
24600
+ }
24601
+ fieldIndex != null ? fieldIndex : fieldIndex = buildFieldIndex();
24602
+ const writes = {};
24603
+ const report = [];
24604
+ for (const raw of updates) {
24605
+ const entry = rec(raw);
24606
+ const fieldKey = String((_a2 = entry.field) != null ? _a2 : "");
24607
+ try {
24608
+ const def = fieldIndex.get(fieldKey);
24609
+ const path = def ? def.path : fieldKey;
24610
+ if (!def && !/^(theme|darkTheme)\.|\./.test(path)) {
24611
+ throw new Error(
24612
+ `Unknown field "${fieldKey}". Pass a known field id or a dot-path (e.g. theme.palette.radius.md).`
24613
+ );
24614
+ }
24615
+ const value = coerceFieldValue(def, entry.value);
24616
+ if (def && path.startsWith("theme.")) {
24617
+ const scoped = expandScoped(path.slice("theme.".length), value);
24618
+ Object.assign(writes, scoped);
24619
+ report.push({ field: fieldKey, resolvedPath: Object.keys(scoped), ok: true });
24620
+ } else {
24621
+ writes[path] = value;
24622
+ report.push({ field: fieldKey, resolvedPath: path, ok: true });
24623
+ }
24624
+ } catch (err) {
24625
+ report.push({ field: fieldKey, ok: false, error: err.message });
24626
+ }
24627
+ }
24628
+ const okWrites = report.filter((r) => r.ok);
24629
+ if (Object.keys(writes).length > 0) state.setBatch(writes);
24630
+ return toolResult({
24631
+ ok: okWrites.length > 0,
24632
+ summary: buildSummary(state),
24633
+ warnings: [],
24634
+ applied: { updates: report }
24635
+ });
24636
+ }
24637
+ };
24638
+ const checkContrast = {
24639
+ name: "check_contrast",
24640
+ title: "Check accessibility contrast",
24641
+ 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.",
24642
+ annotations: { readOnlyHint: true },
24643
+ inputSchema: {
24644
+ type: "object",
24645
+ properties: {
24646
+ level: { type: "string", enum: ["AA", "AAA"], description: "Defaults to 'AA'." },
24647
+ variant: { type: "string", enum: ["light", "dark", "both"], description: "Defaults to 'both'." }
24648
+ },
24649
+ additionalProperties: false
24650
+ },
24651
+ execute(input) {
24652
+ const args = rec(input);
24653
+ const level = args.level === "AAA" ? "AAA" : "AA";
24654
+ const variant = args.variant === "light" || args.variant === "dark" ? args.variant : "both";
24655
+ const report = runContrastChecks(state, level, variant);
24656
+ return toolResult({
24657
+ level: report.level,
24658
+ passing: report.checks.length - report.failures.length,
24659
+ total: report.checks.length,
24660
+ checks: report.checks,
24661
+ failures: report.failures
24662
+ });
24663
+ }
24664
+ };
24665
+ const manageSession = {
24666
+ name: "manage_session",
24667
+ title: "Undo, redo, reset, or export the theme",
24668
+ 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.',
24669
+ inputSchema: {
24670
+ type: "object",
24671
+ properties: { action: { type: "string", enum: ["undo", "redo", "reset", "export"] } },
24672
+ required: ["action"],
24673
+ additionalProperties: false
24674
+ },
24675
+ execute(input) {
24676
+ const { action } = rec(input);
24677
+ switch (action) {
24678
+ case "undo":
24679
+ state.undo();
24680
+ return result({ action: "undo" });
24681
+ case "redo":
24682
+ state.redo();
24683
+ return result({ action: "redo" });
24684
+ case "reset":
24685
+ state.resetToDefaults();
24686
+ return result({ action: "reset" });
24687
+ case "export":
24688
+ return toolResult({ ok: true, snapshot: state.exportSnapshot() });
24689
+ default:
24690
+ throw new Error(`Unknown action "${action}". Valid: undo, redo, reset, export.`);
24691
+ }
24692
+ }
24693
+ };
24694
+ return [
24695
+ getThemeOverview,
24696
+ setBrandColors,
24697
+ assignColorRole,
24698
+ setTypography,
24699
+ setRoundness,
24700
+ setColorScheme,
24701
+ applyPreset,
24702
+ configureWidget,
24703
+ setCopyAndSuggestions,
24704
+ setThemeFields,
24705
+ checkContrast,
24706
+ manageSession
24707
+ ];
24708
+ }
24709
+ function coerceFieldValue(def, value) {
24710
+ if (!def) return value;
24711
+ switch (def.type) {
24712
+ case "color":
24713
+ return def.parseValue ? def.parseValue(coerceColor(value)) : coerceColor(value);
24714
+ case "toggle":
24715
+ return typeof value === "boolean" ? value : value === "true" || value === 1;
24716
+ case "slider": {
24717
+ const num = Number(value);
24718
+ if (!Number.isFinite(num)) throw new Error(`"${def.id}" expects a number.`);
24719
+ if (def.slider) {
24720
+ const { min, max } = def.slider;
24721
+ if (num < min || num > max) {
24722
+ throw new Error(`"${def.id}" must be between ${min} and ${max}.`);
24723
+ }
24724
+ }
24725
+ return def.parseValue ? def.parseValue(num) : num;
24726
+ }
24727
+ case "select": {
24728
+ const str = String(value);
24729
+ if (def.options && !def.options.some((o) => o.value === str)) {
24730
+ throw new Error(
24731
+ `"${def.id}" must be one of: ${def.options.map((o) => o.value).join(", ")}.`
24732
+ );
24733
+ }
24734
+ return def.parseValue ? def.parseValue(str) : str;
24735
+ }
24736
+ default:
24737
+ return def.parseValue ? def.parseValue(value) : value;
24738
+ }
24739
+ }
22573
24740
  export {
22574
24741
  ADVANCED_TOKENS_SECTION,
22575
24742
  ALL_ROLES,
@@ -22583,6 +24750,8 @@ export {
22583
24750
  COMPONENT_SHAPE_SECTIONS,
22584
24751
  CONFIGURE_SECTIONS,
22585
24752
  CONFIGURE_SUB_GROUPS,
24753
+ CONTRAST_PAIRS,
24754
+ CSS_NAMED_COLORS,
22586
24755
  DEVICE_DIMENSIONS,
22587
24756
  HOME_SUGGESTION_CHIPS,
22588
24757
  INTERFACE_ROLES_SECTION,
@@ -22590,6 +24759,7 @@ export {
22590
24759
  MOCK_WORKSPACE_CONTENT,
22591
24760
  PALETTE_SECTION,
22592
24761
  PREVIEW_STORAGE_ADAPTER,
24762
+ RADIUS_PRESETS,
22593
24763
  ROLE_ASSISTANT_MESSAGES,
22594
24764
  ROLE_BORDERS,
22595
24765
  ROLE_FAMILIES,
@@ -22621,11 +24791,19 @@ export {
22621
24791
  buildPreviewConfigWithMessages,
22622
24792
  buildShellCss,
22623
24793
  buildSrcdoc,
24794
+ buildSummary,
22624
24795
  buildTranscriptStreamFrames,
24796
+ coerceColor,
24797
+ coerceFamily,
24798
+ coerceIntensity,
24799
+ coerceRadius,
24800
+ coerceRoundnessStyle,
24801
+ coerceScheme,
22625
24802
  convertFromPx,
22626
24803
  convertToPx,
22627
24804
  createPreviewMessages,
22628
24805
  createPreviewTranscriptEntry,
24806
+ createThemeEditorTools,
22629
24807
  createThemePreview,
22630
24808
  detectRoleAssignment,
22631
24809
  escapeHtml2 as escapeHtml,
@@ -22642,9 +24820,13 @@ export {
22642
24820
  paletteColorPath,
22643
24821
  parseCssValue,
22644
24822
  presetStreamsText,
24823
+ quickContrastWarnings,
22645
24824
  resolveRoleAssignment,
22646
24825
  resolveThemeColorPath,
24826
+ rgbToHex,
24827
+ runContrastChecks,
22647
24828
  scopeSection,
22648
24829
  tokenRefDisplayName,
24830
+ toolResult,
22649
24831
  wcagContrastRatio
22650
24832
  };