@runtypelabs/persona 3.21.2 → 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.
- package/README.md +67 -0
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CWPIj66R.d.cts → types-BZVr1YOV.d.cts} +10 -0
- package/dist/animations/{types-CWPIj66R.d.ts → types-BZVr1YOV.d.ts} +10 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/index.cjs +50 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -6
- package/dist/index.d.ts +474 -6
- package/dist/index.global.js +98 -88
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +48 -41
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.cjs +1875 -0
- package/dist/smart-dom-reader.d.cts +4521 -0
- package/dist/smart-dom-reader.d.ts +4521 -0
- package/dist/smart-dom-reader.js +1848 -0
- package/dist/theme-editor.cjs +2282 -90
- package/dist/theme-editor.d.cts +348 -1
- package/dist/theme-editor.d.ts +348 -1
- package/dist/theme-editor.js +2267 -90
- package/package.json +9 -2
- package/src/client.test.ts +165 -0
- package/src/client.ts +144 -23
- package/src/components/composer-parts.test.ts +34 -0
- package/src/components/composer-parts.ts +9 -6
- package/src/index.ts +26 -0
- package/src/session.test.ts +258 -0
- package/src/session.ts +886 -30
- package/src/session.webmcp.test.ts +815 -0
- package/src/smart-dom-reader.test.ts +135 -0
- package/src/smart-dom-reader.ts +135 -0
- package/src/theme-editor/color-utils.test.ts +59 -0
- package/src/theme-editor/color-utils.ts +38 -2
- package/src/theme-editor/index.ts +35 -0
- package/src/theme-editor/webmcp/coerce.test.ts +86 -0
- package/src/theme-editor/webmcp/coerce.ts +286 -0
- package/src/theme-editor/webmcp/index.ts +45 -0
- package/src/theme-editor/webmcp/summary.ts +324 -0
- package/src/theme-editor/webmcp/tools.test.ts +205 -0
- package/src/theme-editor/webmcp/tools.ts +795 -0
- package/src/theme-editor/webmcp/types.ts +87 -0
- package/src/types.ts +186 -0
- package/src/ui.composer-keyboard.test.ts +229 -0
- package/src/ui.ts +127 -5
- package/src/utils/composer-history.test.ts +128 -0
- package/src/utils/composer-history.ts +113 -0
- package/src/utils/message-fingerprint.test.ts +20 -0
- package/src/utils/message-fingerprint.ts +2 -0
- package/src/utils/smart-dom-adapter.test.ts +257 -0
- package/src/utils/smart-dom-adapter.ts +217 -0
- package/{LICENSE → src/vendor/smart-dom-reader/LICENSE} +2 -2
- package/src/vendor/smart-dom-reader/README.md +61 -0
- package/src/vendor/smart-dom-reader/index.d.ts +476 -0
- package/src/vendor/smart-dom-reader/index.js +1618 -0
- package/src/webmcp-bridge.test.ts +429 -0
- package/src/webmcp-bridge.ts +547 -0
package/dist/theme-editor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
4296
|
-
*
|
|
4297
|
-
*
|
|
4298
|
-
*
|
|
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
|
|
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
|
|
4306
|
-
const url = `${
|
|
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
|
-
|
|
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
|
|
4742
|
+
var _a2, _b, _c;
|
|
4334
4743
|
return {
|
|
4335
4744
|
role: message.role,
|
|
4336
|
-
content: (_c = (_b = (
|
|
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
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: (
|
|
7551
|
+
agentName: (_c = event.message.agentMetadata.agentName) != null ? _c : "",
|
|
7056
7552
|
status: "running",
|
|
7057
|
-
currentIteration: (
|
|
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.
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
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.
|
|
7078
|
-
|
|
7079
|
-
|
|
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
|
-
(
|
|
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
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
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
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
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 (
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
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.
|
|
7796
|
-
|
|
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),
|
|
@@ -8033,18 +8640,386 @@ var AgentWidgetSession = class _AgentWidgetSession {
|
|
|
8033
8640
|
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
|
|
8034
8641
|
this.setStreaming(false);
|
|
8035
8642
|
this.abortController = null;
|
|
8036
|
-
if (!isAbortError) {
|
|
8037
|
-
(_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
|
|
8038
|
-
_k,
|
|
8039
|
-
error instanceof Error ? error : new Error(String(error))
|
|
8040
|
-
);
|
|
8041
|
-
}
|
|
8643
|
+
if (!isAbortError) {
|
|
8644
|
+
(_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
|
|
8645
|
+
_k,
|
|
8646
|
+
error instanceof Error ? error : new Error(String(error))
|
|
8647
|
+
);
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
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();
|
|
8042
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++;
|
|
8043
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.
|
|
8978
|
-
(
|
|
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
|
}
|
|
@@ -10565,12 +11611,7 @@ var createSendButton = (config) => {
|
|
|
10565
11611
|
if (useIcon) {
|
|
10566
11612
|
if (sendIcon && stopIcon) {
|
|
10567
11613
|
const next = mode === "stop" ? stopIcon : sendIcon;
|
|
10568
|
-
|
|
10569
|
-
if (prev.parentNode === button) {
|
|
10570
|
-
button.replaceChild(next, prev);
|
|
10571
|
-
} else {
|
|
10572
|
-
button.appendChild(next);
|
|
10573
|
-
}
|
|
11614
|
+
button.replaceChildren(next);
|
|
10574
11615
|
}
|
|
10575
11616
|
} else {
|
|
10576
11617
|
button.textContent = mode === "stop" ? stopLabel : sendLabel;
|
|
@@ -11506,11 +12547,11 @@ var createTypingIndicator = () => {
|
|
|
11506
12547
|
container.appendChild(srOnly);
|
|
11507
12548
|
return container;
|
|
11508
12549
|
};
|
|
11509
|
-
var renderLoadingIndicatorWithFallback = (
|
|
12550
|
+
var renderLoadingIndicatorWithFallback = (location2, customRenderer, widgetConfig) => {
|
|
11510
12551
|
const context = {
|
|
11511
12552
|
config: widgetConfig != null ? widgetConfig : {},
|
|
11512
12553
|
streaming: true,
|
|
11513
|
-
location,
|
|
12554
|
+
location: location2,
|
|
11514
12555
|
defaultRenderer: createTypingIndicator
|
|
11515
12556
|
};
|
|
11516
12557
|
if (customRenderer) {
|
|
@@ -16133,7 +17174,7 @@ function buildDropOverlay(dropCfg) {
|
|
|
16133
17174
|
return overlay;
|
|
16134
17175
|
}
|
|
16135
17176
|
var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
16136
|
-
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;
|
|
16137
17178
|
if (mount == null) {
|
|
16138
17179
|
throw new Error(
|
|
16139
17180
|
'createAgentExperience: mount must be a non-null HTMLElement (e.g. pass document.getElementById("my-root") after the node exists).'
|
|
@@ -16889,7 +17930,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
16889
17930
|
btn.style.cursor = "not-allowed";
|
|
16890
17931
|
});
|
|
16891
17932
|
}
|
|
16892
|
-
|
|
17933
|
+
if (approvalMessage.approval.toolType === "webmcp") {
|
|
17934
|
+
session.resolveWebMcpApproval(messageId, decision);
|
|
17935
|
+
} else {
|
|
17936
|
+
session.resolveApproval(approvalMessage.approval, decision);
|
|
17937
|
+
}
|
|
16893
17938
|
});
|
|
16894
17939
|
let artifactPaneApi = null;
|
|
16895
17940
|
let artifactPanelResizeObs = null;
|
|
@@ -19224,17 +20269,77 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
19224
20269
|
}
|
|
19225
20270
|
textarea.value = "";
|
|
19226
20271
|
textarea.style.height = "auto";
|
|
20272
|
+
resetHistoryNavigation();
|
|
19227
20273
|
session.sendMessage(value, { contentParts });
|
|
19228
20274
|
if (hasAttachments) {
|
|
19229
20275
|
attachmentManager.clearAttachments();
|
|
19230
20276
|
}
|
|
19231
20277
|
};
|
|
19232
|
-
const
|
|
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
|
+
}
|
|
19233
20324
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
20325
|
+
if (session.isStreaming()) {
|
|
20326
|
+
event.preventDefault();
|
|
20327
|
+
return;
|
|
20328
|
+
}
|
|
20329
|
+
resetHistoryNavigation();
|
|
19234
20330
|
event.preventDefault();
|
|
19235
20331
|
sendButton.click();
|
|
19236
20332
|
}
|
|
19237
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
|
+
};
|
|
19238
20343
|
const handleInputPaste = async (event) => {
|
|
19239
20344
|
var _a2;
|
|
19240
20345
|
if (((_a2 = config.attachments) == null ? void 0 : _a2.enabled) !== true || !attachmentManager) return;
|
|
@@ -19907,8 +21012,11 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
19907
21012
|
if (composerForm) {
|
|
19908
21013
|
composerForm.addEventListener("submit", handleSubmit);
|
|
19909
21014
|
}
|
|
19910
|
-
textarea == null ? void 0 : textarea.addEventListener("keydown",
|
|
21015
|
+
textarea == null ? void 0 : textarea.addEventListener("keydown", handleComposerKeydown);
|
|
21016
|
+
textarea == null ? void 0 : textarea.addEventListener("input", handleComposerInput);
|
|
19911
21017
|
textarea == null ? void 0 : textarea.addEventListener("paste", handleInputPaste);
|
|
21018
|
+
const escStopDoc = (_P = mount.ownerDocument) != null ? _P : document;
|
|
21019
|
+
escStopDoc.addEventListener("keydown", handleEscStop, true);
|
|
19912
21020
|
const ATTACHMENT_DROP_ACTIVE_CLASS = "persona-attachment-drop-active";
|
|
19913
21021
|
let attachmentFileDragDepth = 0;
|
|
19914
21022
|
const clearAttachmentDropVisual = () => {
|
|
@@ -19968,8 +21076,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
19968
21076
|
if (composerForm) {
|
|
19969
21077
|
composerForm.removeEventListener("submit", handleSubmit);
|
|
19970
21078
|
}
|
|
19971
|
-
textarea == null ? void 0 : textarea.removeEventListener("keydown",
|
|
21079
|
+
textarea == null ? void 0 : textarea.removeEventListener("keydown", handleComposerKeydown);
|
|
21080
|
+
textarea == null ? void 0 : textarea.removeEventListener("input", handleComposerInput);
|
|
19972
21081
|
textarea == null ? void 0 : textarea.removeEventListener("paste", handleInputPaste);
|
|
21082
|
+
escStopDoc.removeEventListener("keydown", handleEscStop, true);
|
|
19973
21083
|
});
|
|
19974
21084
|
destroyCallbacks.push(() => {
|
|
19975
21085
|
container.removeEventListener("dragenter", handleAttachmentDragEnterCapture, attachmentDropCapture);
|
|
@@ -19994,7 +21104,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
19994
21104
|
}
|
|
19995
21105
|
const controller = {
|
|
19996
21106
|
update(nextConfig) {
|
|
19997
|
-
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,
|
|
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;
|
|
19998
21108
|
const previousToolCallConfig = config.toolCall;
|
|
19999
21109
|
const previousMessageActions = config.messageActions;
|
|
20000
21110
|
const previousLayoutMessages = (_a2 = config.layout) == null ? void 0 : _a2.messages;
|
|
@@ -20268,7 +21378,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
20268
21378
|
}
|
|
20269
21379
|
}
|
|
20270
21380
|
const layoutShowTitle = (_T2 = (_S2 = config.layout) == null ? void 0 : _S2.header) == null ? void 0 : _T2.showTitle;
|
|
20271
|
-
const layoutShowSubtitle = (
|
|
21381
|
+
const layoutShowSubtitle = (_V2 = (_U2 = config.layout) == null ? void 0 : _U2.header) == null ? void 0 : _V2.showSubtitle;
|
|
20272
21382
|
if (headerTitle) {
|
|
20273
21383
|
headerTitle.style.display = layoutShowTitle === false ? "none" : "";
|
|
20274
21384
|
}
|
|
@@ -21193,6 +22303,10 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
21193
22303
|
if (!(approvalMessage == null ? void 0 : approvalMessage.approval)) {
|
|
21194
22304
|
throw new Error(`Approval not found: ${approvalId}`);
|
|
21195
22305
|
}
|
|
22306
|
+
if (approvalMessage.approval.toolType === "webmcp") {
|
|
22307
|
+
session.resolveWebMcpApproval(approvalMessage.id, decision);
|
|
22308
|
+
return;
|
|
22309
|
+
}
|
|
21196
22310
|
return session.resolveApproval(approvalMessage.approval, decision);
|
|
21197
22311
|
},
|
|
21198
22312
|
getMessages() {
|
|
@@ -21294,7 +22408,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
21294
22408
|
}
|
|
21295
22409
|
}
|
|
21296
22410
|
};
|
|
21297
|
-
const shouldExposeDebugApi = ((
|
|
22411
|
+
const shouldExposeDebugApi = ((_Q = runtimeOptions == null ? void 0 : runtimeOptions.debugTools) != null ? _Q : false) || Boolean(config.debug);
|
|
21298
22412
|
if (shouldExposeDebugApi && typeof window !== "undefined") {
|
|
21299
22413
|
const previousDebug = window.AgentWidgetBrowser;
|
|
21300
22414
|
const debugApi = {
|
|
@@ -21397,9 +22511,9 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
21397
22511
|
const voiceKey = `${persistConfig.keyPrefix}widget-voice`;
|
|
21398
22512
|
const voiceModeKey = `${persistConfig.keyPrefix}widget-voice-mode`;
|
|
21399
22513
|
if (storage) {
|
|
21400
|
-
const wasOpen = ((
|
|
21401
|
-
const wasVoiceActive = ((
|
|
21402
|
-
const wasInVoiceMode = ((
|
|
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";
|
|
21403
22517
|
if (wasOpen) {
|
|
21404
22518
|
setTimeout(() => {
|
|
21405
22519
|
controller.open();
|
|
@@ -21416,7 +22530,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
21416
22530
|
}, 100);
|
|
21417
22531
|
}, 0);
|
|
21418
22532
|
}
|
|
21419
|
-
if ((
|
|
22533
|
+
if ((_U = persistConfig.persist) == null ? void 0 : _U.openState) {
|
|
21420
22534
|
eventBus.on("widget:opened", () => {
|
|
21421
22535
|
storage.setItem(openKey, "true");
|
|
21422
22536
|
});
|
|
@@ -21424,7 +22538,7 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
|
|
|
21424
22538
|
storage.setItem(openKey, "false");
|
|
21425
22539
|
});
|
|
21426
22540
|
}
|
|
21427
|
-
if ((
|
|
22541
|
+
if ((_V = persistConfig.persist) == null ? void 0 : _V.voiceState) {
|
|
21428
22542
|
eventBus.on("voice:state", (event) => {
|
|
21429
22543
|
storage.setItem(voiceKey, event.active ? "true" : "false");
|
|
21430
22544
|
});
|
|
@@ -22575,6 +23689,1054 @@ function createThemePreview(container, initialOptions) {
|
|
|
22575
23689
|
}
|
|
22576
23690
|
};
|
|
22577
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
|
+
}
|
|
22578
24740
|
export {
|
|
22579
24741
|
ADVANCED_TOKENS_SECTION,
|
|
22580
24742
|
ALL_ROLES,
|
|
@@ -22588,6 +24750,8 @@ export {
|
|
|
22588
24750
|
COMPONENT_SHAPE_SECTIONS,
|
|
22589
24751
|
CONFIGURE_SECTIONS,
|
|
22590
24752
|
CONFIGURE_SUB_GROUPS,
|
|
24753
|
+
CONTRAST_PAIRS,
|
|
24754
|
+
CSS_NAMED_COLORS,
|
|
22591
24755
|
DEVICE_DIMENSIONS,
|
|
22592
24756
|
HOME_SUGGESTION_CHIPS,
|
|
22593
24757
|
INTERFACE_ROLES_SECTION,
|
|
@@ -22595,6 +24759,7 @@ export {
|
|
|
22595
24759
|
MOCK_WORKSPACE_CONTENT,
|
|
22596
24760
|
PALETTE_SECTION,
|
|
22597
24761
|
PREVIEW_STORAGE_ADAPTER,
|
|
24762
|
+
RADIUS_PRESETS,
|
|
22598
24763
|
ROLE_ASSISTANT_MESSAGES,
|
|
22599
24764
|
ROLE_BORDERS,
|
|
22600
24765
|
ROLE_FAMILIES,
|
|
@@ -22626,11 +24791,19 @@ export {
|
|
|
22626
24791
|
buildPreviewConfigWithMessages,
|
|
22627
24792
|
buildShellCss,
|
|
22628
24793
|
buildSrcdoc,
|
|
24794
|
+
buildSummary,
|
|
22629
24795
|
buildTranscriptStreamFrames,
|
|
24796
|
+
coerceColor,
|
|
24797
|
+
coerceFamily,
|
|
24798
|
+
coerceIntensity,
|
|
24799
|
+
coerceRadius,
|
|
24800
|
+
coerceRoundnessStyle,
|
|
24801
|
+
coerceScheme,
|
|
22630
24802
|
convertFromPx,
|
|
22631
24803
|
convertToPx,
|
|
22632
24804
|
createPreviewMessages,
|
|
22633
24805
|
createPreviewTranscriptEntry,
|
|
24806
|
+
createThemeEditorTools,
|
|
22634
24807
|
createThemePreview,
|
|
22635
24808
|
detectRoleAssignment,
|
|
22636
24809
|
escapeHtml2 as escapeHtml,
|
|
@@ -22647,9 +24820,13 @@ export {
|
|
|
22647
24820
|
paletteColorPath,
|
|
22648
24821
|
parseCssValue,
|
|
22649
24822
|
presetStreamsText,
|
|
24823
|
+
quickContrastWarnings,
|
|
22650
24824
|
resolveRoleAssignment,
|
|
22651
24825
|
resolveThemeColorPath,
|
|
24826
|
+
rgbToHex,
|
|
24827
|
+
runContrastChecks,
|
|
22652
24828
|
scopeSection,
|
|
22653
24829
|
tokenRefDisplayName,
|
|
24830
|
+
toolResult,
|
|
22654
24831
|
wcagContrastRatio
|
|
22655
24832
|
};
|