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