@mcp-b/char 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +76 -362
  2. package/dist/custom-elements.json +2180 -0
  3. package/dist/display-mode-policy.d.ts +82 -0
  4. package/dist/display-mode-policy.d.ts.map +1 -0
  5. package/dist/display-mode-policy.js +87 -0
  6. package/dist/display-mode-policy.js.map +1 -0
  7. package/dist/index.d.ts +707 -326
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2370 -15550
  10. package/dist/index.js.map +1 -1
  11. package/dist/shell-component.d.ts +379 -0
  12. package/dist/shell-component.d.ts.map +1 -0
  13. package/dist/shell-component.js +2504 -0
  14. package/dist/shell-component.js.map +1 -0
  15. package/dist/tsdoc-metadata.json +11 -0
  16. package/dist/utils.d.ts +161 -0
  17. package/dist/utils.d.ts.map +1 -0
  18. package/dist/utils.js +393 -0
  19. package/dist/utils.js.map +1 -0
  20. package/dist/web-component-standalone.iife.js +1255 -2358
  21. package/dist/web-component-standalone.iife.js.map +1 -1
  22. package/dist/web-component.d.ts +381 -180
  23. package/dist/web-component.d.ts.map +1 -1
  24. package/dist/web-component.js +1137 -15768
  25. package/dist/web-component.js.map +1 -1
  26. package/package.json +23 -115
  27. package/THIRD_PARTY_NOTICES.md +0 -52
  28. package/dist/VoiceHandoffPanel-CIFIJSDs.js +0 -244
  29. package/dist/VoiceHandoffPanel-CIFIJSDs.js.map +0 -1
  30. package/dist/button-BLnLZvxR.js +0 -105
  31. package/dist/button-BLnLZvxR.js.map +0 -1
  32. package/dist/realtimekit.d.ts +0 -15
  33. package/dist/realtimekit.d.ts.map +0 -1
  34. package/dist/realtimekit.js +0 -89
  35. package/dist/realtimekit.js.map +0 -1
  36. package/dist/styles/globals.css +0 -2
  37. package/dist/styles.d.ts +0 -2
  38. package/dist/web-component-standalone.css +0 -37
  39. package/dist/web-component-standalone.css.map +0 -1
@@ -0,0 +1,2504 @@
1
+ /*! @mcp-b/char | Copyright (c) 2025 Kukumis Inc. All rights reserved. | UNLICENSED */
2
+ //#region src/display-mode-policy.ts
3
+ /**
4
+ * Default display modes advertised by Char hosts.
5
+ *
6
+ * Order matters for fallback behavior. When a preferred mode is unavailable,
7
+ * the first available mode is selected.
8
+ *
9
+ * @public
10
+ */
11
+ const DEFAULT_AVAILABLE_DISPLAY_MODES = [
12
+ "inline",
13
+ "fullscreen",
14
+ "pip"
15
+ ];
16
+ /**
17
+ * Runtime guard for Char display mode values.
18
+ *
19
+ * @param value - Raw value from host attributes, events, or external input.
20
+ * @returns `true` when the value is one of `inline`, `fullscreen`, or `pip`.
21
+ *
22
+ * @public
23
+ */
24
+ function isDisplayMode(value) {
25
+ return value === "inline" || value === "fullscreen" || value === "pip";
26
+ }
27
+ /**
28
+ * Resolves a preferred display mode against host-supported modes.
29
+ *
30
+ * An empty `availableModes` array means "no constraint" — the preferred mode
31
+ * is returned unconditionally. This is intentional: it allows callers to opt
32
+ * out of mode filtering without a separate code path.
33
+ *
34
+ * @param preferred - Preferred mode requested by host policy.
35
+ * @param availableModes - Supported modes advertised by the host.
36
+ * @returns Preferred mode when supported, otherwise the first available mode.
37
+ *
38
+ * @public
39
+ */
40
+ function resolveSupportedDisplayMode(preferred, availableModes = DEFAULT_AVAILABLE_DISPLAY_MODES) {
41
+ if (availableModes.length === 0) return preferred;
42
+ if (availableModes.includes(preferred)) return preferred;
43
+ console.debug(`[Char] Display mode "${preferred}" not in available modes [${availableModes.join(", ")}], using "${availableModes[0]}"`);
44
+ return availableModes[0];
45
+ }
46
+ /**
47
+ * Resolves Char's standard host display-mode policy.
48
+ *
49
+ * Policy:
50
+ * - Closed state to `pip`
51
+ * - Open on desktop to `inline`
52
+ * - Open on narrow viewport to `fullscreen`
53
+ *
54
+ * The resolved mode is then constrained to `availableModes`.
55
+ *
56
+ * @param options - Host orchestration inputs: `open`, `isNarrowViewport`,
57
+ * and optional `availableModes`.
58
+ * @returns Final mode that satisfies policy and availability constraints.
59
+ *
60
+ * @public
61
+ */
62
+ function resolvePolicyDisplayMode(options) {
63
+ const availableModes = options.availableModes ?? DEFAULT_AVAILABLE_DISPLAY_MODES;
64
+ return resolveSupportedDisplayMode(options.open ? options.isNarrowViewport ? "fullscreen" : "inline" : "pip", availableModes);
65
+ }
66
+
67
+ //#endregion
68
+ //#region src/utils/host-context-merge.ts
69
+ function mergeStyles(current, patch) {
70
+ if (!patch) return current;
71
+ return {
72
+ ...current,
73
+ ...patch,
74
+ variables: patch.variables ?? current?.variables,
75
+ fonts: patch.fonts ?? current?.fonts
76
+ };
77
+ }
78
+ function shallowMerge(current, patch) {
79
+ if (!patch) return current;
80
+ return {
81
+ ...current,
82
+ ...patch
83
+ };
84
+ }
85
+ function mergeHostContext(current, patch) {
86
+ const previous = current ?? {};
87
+ return {
88
+ ...previous,
89
+ ...patch,
90
+ styles: mergeStyles(previous.styles, patch.styles),
91
+ containerDimensions: shallowMerge(previous.containerDimensions, patch.containerDimensions),
92
+ deviceCapabilities: shallowMerge(previous.deviceCapabilities, patch.deviceCapabilities),
93
+ safeAreaInsets: shallowMerge(previous.safeAreaInsets, patch.safeAreaInsets),
94
+ hostCapabilities: shallowMerge(previous.hostCapabilities, patch.hostCapabilities)
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/utils/parse-boolean-attribute.ts
100
+ /**
101
+ * Attribute helper treating any value except `null` and `'false'` as enabled.
102
+ */
103
+ function parseBooleanAttribute(value) {
104
+ return value !== null && value !== "false";
105
+ }
106
+
107
+ //#endregion
108
+ //#region src/host/char-iframe-proxy.ts
109
+ /**
110
+ * CharIframeProxy
111
+ *
112
+ * Host-side relay that bridges MCP server-to-client responses from the page's
113
+ * TabServerTransport back to the iframe (embedded agent).
114
+ *
115
+ * Client-to-server messages (iframe → host) are handled directly by
116
+ * TabServerTransport via allowedOrigins, so no relay is needed in that
117
+ * direction.
118
+ *
119
+ * Zero runtime dependencies — uses only browser postMessage APIs.
120
+ */
121
+ var CharIframeProxy = class {
122
+ _cleanup = [];
123
+ constructor(_iframe, _iframeOrigin, _channelId = "mcp-default") {
124
+ this._iframe = _iframe;
125
+ this._iframeOrigin = _iframeOrigin;
126
+ this._channelId = _channelId;
127
+ }
128
+ start() {
129
+ const fromTab = (event) => {
130
+ if (event.source !== window) return;
131
+ const d = event.data;
132
+ if (!d || d.channel !== this._channelId || d.type !== "mcp" || d.direction !== "server-to-client") return;
133
+ if (d._proxied) return;
134
+ if (!this._iframe.contentWindow) {
135
+ console.error("[CharIframeProxy] Cannot relay to iframe: contentWindow is null");
136
+ return;
137
+ }
138
+ this._iframe.contentWindow.postMessage(d, this._iframeOrigin);
139
+ };
140
+ window.addEventListener("message", fromTab);
141
+ this._cleanup = [() => window.removeEventListener("message", fromTab)];
142
+ }
143
+ destroy() {
144
+ for (const fn of this._cleanup) fn();
145
+ this._cleanup.length = 0;
146
+ }
147
+ };
148
+
149
+ //#endregion
150
+ //#region src/utils/constants.ts
151
+ const WEBMCP_PRODUCTION_API_BASE = "https://app.usechar.ai";
152
+
153
+ //#endregion
154
+ //#region src/utils/css-sanitizer.ts
155
+ /**
156
+ * Maximum allowed length for CSS custom property values transported to iframe.
157
+ *
158
+ * NOTE: This value and the blocked-token policy intentionally mirror
159
+ * `packages/shared-types/src/schemas/embed-bridge.ts` so validation happens
160
+ * both before transport (host) and at contract parse-time (iframe).
161
+ *
162
+ * CONTROL_CHAR_PATTERN here is intentionally more permissive than
163
+ * embed-bridge.ts: it allows tabs/newlines/CR/FF because
164
+ * normalizeCssFormattingWhitespace converts them to spaces before the
165
+ * control-char check. The iframe-side schema (embed-bridge.ts) uses
166
+ * the stricter pattern as defense-in-depth for values that bypass
167
+ * host-side sanitization.
168
+ */
169
+ const MAX_CSS_VARIABLE_VALUE_LENGTH = 1024;
170
+ const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000b\u000e-\u001f\u007f]/u;
171
+ const CSS_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
172
+ const CSS_FORMATTING_WHITESPACE_PATTERN = /[\t\n\r\f]+/gu;
173
+ /**
174
+ * Normalizes CSS values for dangerous-token scanning.
175
+ * Lowercases and removes CSS comments/whitespace/control chars to catch obfuscation.
176
+ */
177
+ function normalizeForSecurityScan(value) {
178
+ return value.replace(CSS_COMMENT_PATTERN, "").toLowerCase().replace(/[\s\u0000-\u001f\u007f]+/gu, "");
179
+ }
180
+ /**
181
+ * Collapses CSS formatting whitespace (tabs, newlines, carriage returns, form feeds)
182
+ * into single spaces. Applied to the output value before trimming, so multiline
183
+ * host-provided values (e.g., font stacks) become single-line without losing
184
+ * meaningful token boundaries. This also ensures split-token obfuscation
185
+ * (e.g., "java\nscript:") is collapsed before the security scan.
186
+ */
187
+ function normalizeCssFormattingWhitespace(value) {
188
+ return value.replace(CSS_FORMATTING_WHITESPACE_PATTERN, " ");
189
+ }
190
+ /**
191
+ * Sanitizes host-provided CSS variable values before iframe transport.
192
+ * Returns `undefined` for unsafe or invalid values.
193
+ *
194
+ * @param value - The raw CSS property value.
195
+ * @param variableName - Optional variable name for diagnostic logging.
196
+ */
197
+ function sanitizeCssVariableValue(value, variableName) {
198
+ const trimmed = normalizeCssFormattingWhitespace(value).trim();
199
+ if (!trimmed) return void 0;
200
+ if (trimmed.length > MAX_CSS_VARIABLE_VALUE_LENGTH) {
201
+ console.warn(`[Char] CSS variable ${variableName ?? "(unknown)"} rejected: value exceeds max length (${trimmed.length} > ${MAX_CSS_VARIABLE_VALUE_LENGTH})`);
202
+ return;
203
+ }
204
+ if (CONTROL_CHAR_PATTERN.test(trimmed)) {
205
+ console.warn(`[Char] CSS variable ${variableName ?? "(unknown)"} rejected: contains control characters`);
206
+ return;
207
+ }
208
+ const blockedToken = getBlockedCssVariableToken(normalizeForSecurityScan(trimmed));
209
+ if (blockedToken) return warnAndReject(variableName, blockedToken);
210
+ return trimmed;
211
+ }
212
+ function getBlockedCssVariableToken(normalizedValue) {
213
+ if (normalizedValue.includes("javascript:")) return "javascript:";
214
+ if (normalizedValue.includes("vbscript:")) return "vbscript:";
215
+ if (normalizedValue.includes("expression(")) return "expression(";
216
+ if (normalizedValue.includes("@import")) return "@import";
217
+ if (normalizedValue.includes("-moz-binding")) return "-moz-binding";
218
+ if (normalizedValue.includes("url(")) return "url(";
219
+ }
220
+ function warnAndReject(variableName, blockedToken) {
221
+ console.warn(`[Char] CSS variable ${variableName ?? "(unknown)"} rejected: contains blocked token "${blockedToken}"`);
222
+ }
223
+
224
+ //#endregion
225
+ //#region src/utils/css-resolver.ts
226
+ /**
227
+ * CSS var() Resolver
228
+ *
229
+ * Recursively resolves CSS custom property references (`var(--x, fallback)`)
230
+ * to concrete values using getComputedStyle(). Necessary because `var()`
231
+ * references are not valid across iframe boundaries.
232
+ *
233
+ * Handles nested var() references, fallbacks, and cycle detection.
234
+ */
235
+ const MAX_CSS_VAR_RESOLVE_DEPTH = 12;
236
+ function findMatchingParen(value, openParenIndex) {
237
+ let depth = 1;
238
+ for (let i = openParenIndex + 1; i < value.length; i++) {
239
+ const char = value[i];
240
+ if (char === "(") depth += 1;
241
+ else if (char === ")") depth -= 1;
242
+ if (depth === 0) return i;
243
+ }
244
+ return -1;
245
+ }
246
+ function splitTopLevelComma(value) {
247
+ let depth = 0;
248
+ for (let i = 0; i < value.length; i++) {
249
+ const char = value[i];
250
+ if (char === "(") depth += 1;
251
+ else if (char === ")") depth = Math.max(0, depth - 1);
252
+ else if (char === "," && depth === 0) return [value.slice(0, i), value.slice(i + 1)];
253
+ }
254
+ return [value, void 0];
255
+ }
256
+ function resolveFallback(fallback, computed, seen, depth) {
257
+ if (!fallback) return void 0;
258
+ return resolveCssValue(fallback, computed, seen, depth + 1) || void 0;
259
+ }
260
+ function resolveCssVariable(variableName, fallback, computed, seen, depth) {
261
+ if (depth > MAX_CSS_VAR_RESOLVE_DEPTH) {
262
+ console.warn(`[Char] CSS variable resolution depth exceeded for "${variableName}". Possible circular reference.`);
263
+ return fallback?.trim();
264
+ }
265
+ if (!variableName.startsWith("--") || seen.has(variableName)) return resolveFallback(fallback, computed, seen, depth);
266
+ const nextSeen = new Set(seen);
267
+ nextSeen.add(variableName);
268
+ const rawValue = computed.getPropertyValue(variableName).trim();
269
+ if (rawValue) {
270
+ const resolvedRaw = resolveCssValue(rawValue, computed, nextSeen, depth + 1);
271
+ if (resolvedRaw) return resolvedRaw;
272
+ }
273
+ return resolveFallback(fallback, computed, seen, depth);
274
+ }
275
+ function resolveCssValue(value, computed, seen, depth = 0) {
276
+ const trimmed = value.trim();
277
+ if (!trimmed || !trimmed.includes("var(") || depth > MAX_CSS_VAR_RESOLVE_DEPTH) return trimmed;
278
+ let cursor = 0;
279
+ let resolved = "";
280
+ let replacedAny = false;
281
+ while (cursor < trimmed.length) {
282
+ const varStart = trimmed.indexOf("var(", cursor);
283
+ if (varStart === -1) {
284
+ resolved += trimmed.slice(cursor);
285
+ break;
286
+ }
287
+ resolved += trimmed.slice(cursor, varStart);
288
+ const openParenIndex = varStart + 3;
289
+ const closeParenIndex = findMatchingParen(trimmed, openParenIndex);
290
+ if (closeParenIndex === -1) {
291
+ resolved += trimmed.slice(varStart);
292
+ break;
293
+ }
294
+ const [rawVariableName, rawFallback] = splitTopLevelComma(trimmed.slice(openParenIndex + 1, closeParenIndex));
295
+ const variableName = rawVariableName.trim();
296
+ const fallback = rawFallback?.trim();
297
+ const replacement = variableName ? resolveCssVariable(variableName, fallback, computed, seen, depth + 1) : void 0;
298
+ if (replacement !== void 0) {
299
+ resolved += replacement;
300
+ replacedAny = true;
301
+ } else resolved += trimmed.slice(varStart, closeParenIndex + 1);
302
+ cursor = closeParenIndex + 1;
303
+ }
304
+ const normalized = resolved.trim();
305
+ if (!replacedAny || !normalized.includes("var(")) return normalized;
306
+ return resolveCssValue(normalized, computed, seen, depth + 1);
307
+ }
308
+
309
+ //#endregion
310
+ //#region src/utils/css-variables.ts
311
+ /**
312
+ * Public CSS variable names for the Char widget.
313
+ *
314
+ * These are the external API — host pages set these to customise the widget.
315
+ * We maintain static lists because `getComputedStyle()` cannot enumerate
316
+ * custom properties.
317
+ *
318
+ * Source of truth:
319
+ * - `src/styles/globals.css` (`--char-*`)
320
+ * - WebMCP ext-apps UI spec (`--color-*`, `--font-*`, etc.)
321
+ */
322
+ const CHAR_PUBLIC_VARIABLE_NAMES = [
323
+ "--char-color-background",
324
+ "--char-color-foreground",
325
+ "--char-color-card",
326
+ "--char-color-card-foreground",
327
+ "--char-color-popover",
328
+ "--char-color-popover-foreground",
329
+ "--char-color-primary",
330
+ "--char-color-primary-foreground",
331
+ "--char-color-secondary",
332
+ "--char-color-secondary-foreground",
333
+ "--char-color-muted",
334
+ "--char-color-muted-foreground",
335
+ "--char-color-accent",
336
+ "--char-color-accent-foreground",
337
+ "--char-color-destructive",
338
+ "--char-color-destructive-foreground",
339
+ "--char-color-border",
340
+ "--char-color-success",
341
+ "--char-color-warning",
342
+ "--char-color-error",
343
+ "--char-color-input",
344
+ "--char-color-ring",
345
+ "--char-user-bubble-bg",
346
+ "--char-user-bubble-text",
347
+ "--char-assistant-bubble-bg",
348
+ "--char-assistant-bubble-text",
349
+ "--char-composer-bg",
350
+ "--char-composer-border",
351
+ "--char-composer-text",
352
+ "--char-composer-placeholder",
353
+ "--char-composer-button-bg",
354
+ "--char-composer-button-text",
355
+ "--char-tool-bg",
356
+ "--char-tool-border",
357
+ "--char-tool-text",
358
+ "--char-tool-header-bg",
359
+ "--char-tool-approve-bg",
360
+ "--char-tool-approve-text",
361
+ "--char-tool-deny-bg",
362
+ "--char-tool-deny-text",
363
+ "--char-code-bg",
364
+ "--char-code-text",
365
+ "--char-code-header-bg",
366
+ "--char-radius",
367
+ "--char-radius-sm",
368
+ "--char-radius-md",
369
+ "--char-radius-lg",
370
+ "--char-radius-xl",
371
+ "--char-radius-2xl",
372
+ "--char-radius-3xl",
373
+ "--char-radius-full",
374
+ "--char-spacing-unit",
375
+ "--char-font-sans",
376
+ "--char-font-mono",
377
+ "--char-font-size-xs",
378
+ "--char-font-size-sm",
379
+ "--char-font-size-base",
380
+ "--char-font-size-lg",
381
+ "--char-duration-fast",
382
+ "--char-duration-normal",
383
+ "--char-duration-slow",
384
+ "--char-easing-default",
385
+ "--char-easing-spring",
386
+ "--char-z-base",
387
+ "--char-z-content",
388
+ "--char-z-overlay",
389
+ "--char-z-max",
390
+ "--char-shadow-sm",
391
+ "--char-shadow-md",
392
+ "--char-shadow-lg",
393
+ "--char-blur-sm",
394
+ "--char-blur-md",
395
+ "--char-blur-lg"
396
+ ];
397
+ const MCP_UI_STYLE_VARIABLE_NAMES = [
398
+ "--color-background-primary",
399
+ "--color-background-secondary",
400
+ "--color-background-tertiary",
401
+ "--color-background-inverse",
402
+ "--color-background-ghost",
403
+ "--color-background-info",
404
+ "--color-background-danger",
405
+ "--color-background-success",
406
+ "--color-background-warning",
407
+ "--color-background-disabled",
408
+ "--color-text-primary",
409
+ "--color-text-secondary",
410
+ "--color-text-tertiary",
411
+ "--color-text-inverse",
412
+ "--color-text-ghost",
413
+ "--color-text-info",
414
+ "--color-text-danger",
415
+ "--color-text-success",
416
+ "--color-text-warning",
417
+ "--color-text-disabled",
418
+ "--color-border-primary",
419
+ "--color-border-secondary",
420
+ "--color-border-tertiary",
421
+ "--color-border-inverse",
422
+ "--color-border-ghost",
423
+ "--color-border-info",
424
+ "--color-border-danger",
425
+ "--color-border-success",
426
+ "--color-border-warning",
427
+ "--color-border-disabled",
428
+ "--color-ring-primary",
429
+ "--color-ring-secondary",
430
+ "--color-ring-inverse",
431
+ "--color-ring-info",
432
+ "--color-ring-danger",
433
+ "--color-ring-success",
434
+ "--color-ring-warning",
435
+ "--font-sans",
436
+ "--font-mono",
437
+ "--font-weight-normal",
438
+ "--font-weight-medium",
439
+ "--font-weight-semibold",
440
+ "--font-weight-bold",
441
+ "--font-text-xs-size",
442
+ "--font-text-sm-size",
443
+ "--font-text-md-size",
444
+ "--font-text-lg-size",
445
+ "--font-heading-xs-size",
446
+ "--font-heading-sm-size",
447
+ "--font-heading-md-size",
448
+ "--font-heading-lg-size",
449
+ "--font-heading-xl-size",
450
+ "--font-heading-2xl-size",
451
+ "--font-heading-3xl-size",
452
+ "--font-text-xs-line-height",
453
+ "--font-text-sm-line-height",
454
+ "--font-text-md-line-height",
455
+ "--font-text-lg-line-height",
456
+ "--font-heading-xs-line-height",
457
+ "--font-heading-sm-line-height",
458
+ "--font-heading-md-line-height",
459
+ "--font-heading-lg-line-height",
460
+ "--font-heading-xl-line-height",
461
+ "--font-heading-2xl-line-height",
462
+ "--font-heading-3xl-line-height",
463
+ "--border-radius-xs",
464
+ "--border-radius-sm",
465
+ "--border-radius-md",
466
+ "--border-radius-lg",
467
+ "--border-radius-xl",
468
+ "--border-radius-full",
469
+ "--border-width-regular",
470
+ "--shadow-hairline",
471
+ "--shadow-sm",
472
+ "--shadow-md",
473
+ "--shadow-lg"
474
+ ];
475
+ const CHAR_CSS_VARIABLE_NAMES = [...CHAR_PUBLIC_VARIABLE_NAMES, ...MCP_UI_STYLE_VARIABLE_NAMES];
476
+
477
+ //#endregion
478
+ //#region src/utils/display-mode.ts
479
+ function resolveDisplayModeFromAttribute(displayModeAttr) {
480
+ if (!isDisplayMode(displayModeAttr)) return;
481
+ return displayModeAttr;
482
+ }
483
+
484
+ //#endregion
485
+ //#region src/web-component.tsx
486
+ /**
487
+ * Shared options used by custom events emitted from the host element.
488
+ */
489
+ const CHAR_CUSTOM_EVENT_OPTIONS = {
490
+ bubbles: true,
491
+ composed: true
492
+ };
493
+ /**
494
+ * Inline iframe styles used to make the host element a transparent container.
495
+ */
496
+ const IFRAME_STYLE = "width:100%;height:100%;border:none;display:block;";
497
+ /**
498
+ * Display mode values advertised to the embedded runtime.
499
+ */
500
+ const DISPLAY_MODES = [
501
+ "inline",
502
+ "fullscreen",
503
+ "pip"
504
+ ];
505
+ /**
506
+ * Normalizes an API key by trimming whitespace and collapsing empty strings.
507
+ */
508
+ function normalizeApiKey(value) {
509
+ const trimmed = value?.trim();
510
+ return trimmed ? trimmed : void 0;
511
+ }
512
+ /**
513
+ * Produces a sanitized dev-mode configuration and drops empty payloads.
514
+ */
515
+ function resolveDevMode(devMode) {
516
+ if (!devMode) return void 0;
517
+ const normalized = {
518
+ anthropicApiKey: normalizeApiKey(devMode.anthropicApiKey),
519
+ openaiApiKey: normalizeApiKey(devMode.openaiApiKey),
520
+ useLocalApi: devMode.useLocalApi === true ? true : void 0
521
+ };
522
+ return !!normalized.anthropicApiKey || !!normalized.openaiApiKey || !!normalized.useLocalApi ? normalized : void 0;
523
+ }
524
+ /**
525
+ * Runtime guard for the message envelope emitted by the iframe.
526
+ */
527
+ function isCharIframeMessageData(value) {
528
+ if (!value || typeof value !== "object") return false;
529
+ const maybeType = value.type;
530
+ return typeof maybeType === "string" && maybeType.startsWith("char-");
531
+ }
532
+ function getStringOrUndefined(value) {
533
+ return typeof value === "string" ? value : void 0;
534
+ }
535
+ function getNumberOrUndefined(value) {
536
+ return typeof value === "number" ? value : void 0;
537
+ }
538
+ /**
539
+ * Get the iframe src URL.
540
+ * Points to the SaaS app's static /embed/ entrypoint with the parent origin as a query parameter.
541
+ *
542
+ * @param apiBase - Public base URL for the Char SaaS application.
543
+ * @param parentOrigin - Origin of the host page embedding the widget.
544
+ * @returns Fully qualified iframe URL targeting the embed entrypoint.
545
+ */
546
+ function getIframeSrc(apiBase, parentOrigin) {
547
+ return `${apiBase}/embed/?parentOrigin=${encodeURIComponent(parentOrigin)}`;
548
+ }
549
+ /**
550
+ * Extract host CSS variable values from a host element.
551
+ * Includes both `--char-*` and MCP UI style `--color-*`/`--font-*` tokens.
552
+ * Uses a static list since getComputedStyle() cannot enumerate custom properties.
553
+ * Values containing `var()` are resolved to concrete values before transport
554
+ * so the iframe can safely apply them in its own CSS scope.
555
+ *
556
+ * @param el - Host `<char-agent>` element.
557
+ * @returns A snapshot of resolved CSS custom properties for the iframe.
558
+ */
559
+ function extractCharVariables(el) {
560
+ const computed = getComputedStyle(el);
561
+ const vars = {};
562
+ for (const name of CHAR_CSS_VARIABLE_NAMES) {
563
+ const rawValue = computed.getPropertyValue(name).trim();
564
+ if (rawValue) {
565
+ const sanitizedValue = sanitizeCssVariableValue(resolveCssValue(rawValue, computed, new Set([name])) || rawValue, name);
566
+ if (sanitizedValue !== void 0) vars[name] = sanitizedValue;
567
+ }
568
+ }
569
+ return vars;
570
+ }
571
+ /**
572
+ * Detect whether the host page is in dark mode.
573
+ * Checks the data-theme attribute, the `dark` class on <html>, and the prefers-color-scheme media query.
574
+ *
575
+ * @returns `true` when the host page should be treated as dark mode.
576
+ */
577
+ function isDarkMode() {
578
+ if (typeof document === "undefined") return false;
579
+ const themeAttr = document.documentElement.getAttribute("data-theme");
580
+ if (themeAttr === "dark") return true;
581
+ if (themeAttr === "light") return false;
582
+ return document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
583
+ }
584
+ /**
585
+ * Deep equality check using JSON.stringify. Sufficient for the flat/nested
586
+ * structures in CharHostContext (no circular refs, no functions).
587
+ *
588
+ * @param a - First value.
589
+ * @param b - Second value.
590
+ * @returns `true` when both values serialize identically.
591
+ */
592
+ function deepEqual(a, b) {
593
+ return JSON.stringify(a) === JSON.stringify(b);
594
+ }
595
+ /**
596
+ * Parses a CSS pixel value into a finite number.
597
+ */
598
+ function parsePxToNumber(value) {
599
+ const parsed = Number.parseFloat(value);
600
+ return Number.isFinite(parsed) ? parsed : 0;
601
+ }
602
+ /**
603
+ * Reads safe-area inset values using `env(safe-area-inset-*)`.
604
+ */
605
+ function getSafeAreaInsets() {
606
+ if (typeof document === "undefined") return void 0;
607
+ const probe = document.createElement("div");
608
+ probe.style.cssText = [
609
+ "position:fixed",
610
+ "visibility:hidden",
611
+ "pointer-events:none",
612
+ "inset:0 auto auto 0",
613
+ "padding-top:env(safe-area-inset-top)",
614
+ "padding-right:env(safe-area-inset-right)",
615
+ "padding-bottom:env(safe-area-inset-bottom)",
616
+ "padding-left:env(safe-area-inset-left)"
617
+ ].join(";");
618
+ document.documentElement.appendChild(probe);
619
+ const computed = getComputedStyle(probe);
620
+ const insets = {
621
+ top: parsePxToNumber(computed.paddingTop),
622
+ right: parsePxToNumber(computed.paddingRight),
623
+ bottom: parsePxToNumber(computed.paddingBottom),
624
+ left: parsePxToNumber(computed.paddingLeft)
625
+ };
626
+ probe.remove();
627
+ return insets;
628
+ }
629
+ /**
630
+ * Identifies the host platform category from the user agent.
631
+ * Note: 'desktop' detection not yet implemented; non-mobile UAs resolve to 'web'.
632
+ */
633
+ function detectPlatform() {
634
+ if (typeof navigator === "undefined") return "web";
635
+ const ua = navigator.userAgent.toLowerCase();
636
+ if (/(iphone|ipad|ipod|android|mobile)/.test(ua)) return "mobile";
637
+ return "web";
638
+ }
639
+ /**
640
+ * Captures lightweight host device capability hints.
641
+ */
642
+ function getDeviceCapabilities() {
643
+ if (typeof window === "undefined" || typeof navigator === "undefined") return {};
644
+ return {
645
+ touch: "ontouchstart" in window || navigator.maxTouchPoints > 0,
646
+ hover: window.matchMedia("(hover: hover)").matches,
647
+ pointerFine: window.matchMedia("(pointer: fine)").matches
648
+ };
649
+ }
650
+ /**
651
+ * `<char-agent>` custom element.
652
+ *
653
+ * Creates an iframe pointing to the SaaS app's /embed/ entrypoint and relays
654
+ * auth, styles, dark mode, display mode, and MCP messages via postMessage.
655
+ *
656
+ * Uses a unified `char-context` message with diffing (only changed fields
657
+ * are sent) instead of separate messages per concern.
658
+ *
659
+ * @public
660
+ */
661
+ var CharAgentElement = class extends HTMLElement {
662
+ static observedAttributes = [
663
+ "dev-mode",
664
+ "enable-debug-tools",
665
+ "display-mode",
666
+ "api-base"
667
+ ];
668
+ /** Active iframe element owned by this custom element instance. */
669
+ _iframe = null;
670
+ /** MCP relay between host window and embed iframe. */
671
+ _proxy = null;
672
+ /** Indicates whether the iframe has emitted `char-ready`. */
673
+ _iframeReady = false;
674
+ /** Bound window message handler for iframe-originated events. */
675
+ _messageListener = null;
676
+ /** Observer that tracks host theme mutations. */
677
+ _darkModeObserver = null;
678
+ /** Media query object for `prefers-color-scheme`. */
679
+ _darkModeMediaQuery = null;
680
+ /** Registered media query callback. */
681
+ _darkModeMediaHandler = null;
682
+ /** Observer that tracks host style/class mutations. */
683
+ _styleObserver = null;
684
+ /** Observer that tracks container size changes. */
685
+ _containerResizeObserver = null;
686
+ /** In-flight teardown request waiting for iframe acknowledgement. */
687
+ _teardownPending = null;
688
+ /** Monotonic sequence used to construct teardown request IDs. */
689
+ _teardownSequence = 0;
690
+ /** Re-entrancy guard used during disconnect lifecycle. */
691
+ _isDisconnecting = false;
692
+ /** Last host context snapshot used for delta emission. */
693
+ _hostContext = {};
694
+ /** Pending credentials that are flushed when the iframe is ready. */
695
+ _pendingAuth = null;
696
+ /**
697
+ * Mount lifecycle: builds iframe runtime, starts proxying, and starts host observers.
698
+ * The existing runtime is reused when reconnecting during fast detach/reattach cycles.
699
+ */
700
+ connectedCallback() {
701
+ if (this._hasActiveRuntime()) {
702
+ this._isDisconnecting = false;
703
+ this._teardownSequence = 0;
704
+ if (this._teardownPending) {
705
+ window.clearTimeout(this._teardownPending.timeoutId);
706
+ this._teardownPending.resolve();
707
+ this._teardownPending = null;
708
+ }
709
+ if (this._iframe && !this.contains(this._iframe)) this.appendChild(this._iframe);
710
+ return;
711
+ }
712
+ if (!this.style.display) this.style.display = "block";
713
+ const resolvedDevMode = resolveDevMode(this._resolveDevMode());
714
+ const apiBase = this._resolveApiBaseOverride() ?? (resolvedDevMode?.useLocalApi ? window.location.origin : WEBMCP_PRODUCTION_API_BASE);
715
+ const iframeOrigin = this._resolveIframeOrigin(apiBase);
716
+ const iframe = this._createIframe(apiBase);
717
+ this._iframe = iframe;
718
+ this._proxy = new CharIframeProxy(iframe, iframeOrigin);
719
+ this._proxy.start();
720
+ this._messageListener = this._createMessageListener(iframe, iframeOrigin);
721
+ window.addEventListener("message", this._messageListener);
722
+ this._observeDarkMode();
723
+ this._observeStyleChanges();
724
+ this._observeContainerDimensions();
725
+ this.appendChild(iframe);
726
+ }
727
+ /**
728
+ * Determines whether this instance already has an initialized runtime.
729
+ */
730
+ _hasActiveRuntime() {
731
+ return !!(this._iframe || this._proxy || this._messageListener);
732
+ }
733
+ /**
734
+ * Parses and validates the iframe origin from the configured API base URL.
735
+ *
736
+ * @throws Error When `apiBase` is not a valid URL.
737
+ */
738
+ _resolveIframeOrigin(apiBase) {
739
+ try {
740
+ return new URL(apiBase).origin;
741
+ } catch {
742
+ throw new Error(`[Char] Invalid api-base: "${apiBase}". Must be a valid URL.`);
743
+ }
744
+ }
745
+ /**
746
+ * Creates a fully configured iframe element for the embed runtime.
747
+ */
748
+ _createIframe(apiBase) {
749
+ const iframe = document.createElement("iframe");
750
+ iframe.src = getIframeSrc(apiBase, window.location.origin);
751
+ iframe.style.cssText = IFRAME_STYLE;
752
+ iframe.title = "Char AI Assistant";
753
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
754
+ iframe.setAttribute("allow", "microphone");
755
+ return iframe;
756
+ }
757
+ /**
758
+ * Creates the message listener that filters by source/origin and dispatches typed events.
759
+ */
760
+ _createMessageListener(iframe, iframeOrigin) {
761
+ return (event) => {
762
+ if (event.source !== iframe.contentWindow) return;
763
+ if (event.origin !== iframeOrigin) return;
764
+ if (!isCharIframeMessageData(event.data)) return;
765
+ this._handleIframeMessage(event.data, iframeOrigin);
766
+ };
767
+ }
768
+ /**
769
+ * Routes validated iframe messages to local handlers and host DOM events.
770
+ */
771
+ _handleIframeMessage(data, iframeOrigin) {
772
+ switch (data.type) {
773
+ case "char-ready":
774
+ this._iframeReady = true;
775
+ this._sendInitialContext(iframeOrigin);
776
+ return;
777
+ case "char-initialized":
778
+ this._emitCharEvent("char-initialized");
779
+ return;
780
+ case "char-size-changed": {
781
+ const width = getNumberOrUndefined(data.width);
782
+ const height = getNumberOrUndefined(data.height);
783
+ const rawSizeMode = getStringOrUndefined(data.displayMode);
784
+ const sizeDisplayMode = rawSizeMode && DISPLAY_MODES.includes(rawSizeMode) ? rawSizeMode : void 0;
785
+ if (width !== void 0 || height !== void 0) {
786
+ const detail = {
787
+ width,
788
+ height,
789
+ displayMode: sizeDisplayMode
790
+ };
791
+ this._emitCharEventWithDetail("char-size-changed", detail);
792
+ }
793
+ return;
794
+ }
795
+ case "char-request-display-mode": {
796
+ const rawMode = getStringOrUndefined(data.mode);
797
+ const detail = { mode: rawMode && DISPLAY_MODES.includes(rawMode) ? rawMode : void 0 };
798
+ this._emitCharEventWithDetail("char-request-display-mode", detail);
799
+ return;
800
+ }
801
+ case "char-close":
802
+ this._emitCharEvent("char-close");
803
+ return;
804
+ case "char-error":
805
+ this._emitCharEventWithDetail("char-error", {
806
+ code: getStringOrUndefined(data.code) ?? "UNKNOWN",
807
+ message: getStringOrUndefined(data.message)
808
+ });
809
+ return;
810
+ case "char-open-link": {
811
+ const requestId = getStringOrUndefined(data.requestId);
812
+ const url = getStringOrUndefined(data.url);
813
+ if (requestId && url) this._handleOpenLinkRequest(requestId, url, iframeOrigin);
814
+ else if (requestId) this._iframe?.contentWindow?.postMessage({
815
+ type: "char-open-link-result",
816
+ requestId,
817
+ ok: false,
818
+ error: "Missing url in char-open-link message"
819
+ }, iframeOrigin);
820
+ else console.error("[Char] Received char-open-link without requestId");
821
+ return;
822
+ }
823
+ case "char-teardown-complete":
824
+ this._resolvePendingTeardown(getStringOrUndefined(data.requestId));
825
+ return;
826
+ default:
827
+ console.warn(`[Char] Unrecognized iframe message type: "${data.type}"`);
828
+ return;
829
+ }
830
+ }
831
+ /**
832
+ * Emits a host-facing custom event without detail payload.
833
+ */
834
+ _emitCharEvent(type) {
835
+ this.dispatchEvent(new CustomEvent(type, CHAR_CUSTOM_EVENT_OPTIONS));
836
+ }
837
+ /**
838
+ * Emits a host-facing custom event with detail payload.
839
+ */
840
+ _emitCharEventWithDetail(type, detail) {
841
+ this.dispatchEvent(new CustomEvent(type, {
842
+ ...CHAR_CUSTOM_EVENT_OPTIONS,
843
+ detail
844
+ }));
845
+ }
846
+ /**
847
+ * Emits a standardized `char-error` event.
848
+ */
849
+ _emitCharError(code, message) {
850
+ const detail = {
851
+ code,
852
+ message
853
+ };
854
+ this._emitCharEventWithDetail("char-error", detail);
855
+ }
856
+ /**
857
+ * Unmount lifecycle: requests iframe teardown and releases all host resources.
858
+ */
859
+ disconnectedCallback() {
860
+ if (this._isDisconnecting) return;
861
+ this._isDisconnecting = true;
862
+ try {
863
+ const iframeOrigin = this._getIframeOrigin();
864
+ if (this._iframeReady && iframeOrigin) {
865
+ this._requestTeardown(iframeOrigin, "unmount").catch((err) => console.error("[Char] Teardown failed:", err)).finally(() => {
866
+ if (this.isConnected) {
867
+ this._isDisconnecting = false;
868
+ return;
869
+ }
870
+ this._finalizeDisconnect();
871
+ });
872
+ return;
873
+ }
874
+ } catch (err) {
875
+ console.error("[Char] Error during disconnect:", err);
876
+ }
877
+ this._finalizeDisconnect();
878
+ }
879
+ /**
880
+ * Releases iframe, observers, listeners, and pending teardown state.
881
+ */
882
+ _finalizeDisconnect() {
883
+ if (this._proxy) {
884
+ this._proxy.destroy();
885
+ this._proxy = null;
886
+ }
887
+ if (this._messageListener) {
888
+ window.removeEventListener("message", this._messageListener);
889
+ this._messageListener = null;
890
+ }
891
+ if (this._darkModeObserver) {
892
+ this._darkModeObserver.disconnect();
893
+ this._darkModeObserver = null;
894
+ }
895
+ if (this._darkModeMediaQuery && this._darkModeMediaHandler) {
896
+ this._darkModeMediaQuery.removeEventListener("change", this._darkModeMediaHandler);
897
+ this._darkModeMediaQuery = null;
898
+ this._darkModeMediaHandler = null;
899
+ }
900
+ if (this._styleObserver) {
901
+ this._styleObserver.disconnect();
902
+ this._styleObserver = null;
903
+ }
904
+ if (this._containerResizeObserver) {
905
+ this._containerResizeObserver.disconnect();
906
+ this._containerResizeObserver = null;
907
+ }
908
+ if (this._iframe) {
909
+ this._iframe.remove();
910
+ this._iframe = null;
911
+ }
912
+ if (this._teardownPending) {
913
+ window.clearTimeout(this._teardownPending.timeoutId);
914
+ this._teardownPending.resolve();
915
+ this._teardownPending = null;
916
+ }
917
+ this._iframeReady = false;
918
+ this._pendingAuth = null;
919
+ this._hostContext = {};
920
+ this._teardownSequence = 0;
921
+ this._isDisconnecting = false;
922
+ }
923
+ /**
924
+ * Reacts to observed attribute updates after iframe readiness.
925
+ * Attributes that affect iframe boot configuration are intentionally ignored
926
+ * after mount (`dev-mode`, `enable-debug-tools`, `api-base`).
927
+ */
928
+ attributeChangedCallback(name, _oldValue, newValue) {
929
+ if (!this._iframe || !this._iframeReady) return;
930
+ switch (name) {
931
+ case "display-mode":
932
+ this.setHostContext({ displayMode: resolveDisplayModeFromAttribute(newValue) });
933
+ break;
934
+ case "dev-mode":
935
+ case "enable-debug-tools":
936
+ case "api-base": break;
937
+ }
938
+ }
939
+ /**
940
+ * Connect to the Char agent with authentication.
941
+ *
942
+ * The token is stored as a JavaScript property (not as a DOM attribute),
943
+ * preventing exposure to DOM inspection and session replay tools.
944
+ *
945
+ * @param options - Authentication payload for either ID token or ticket auth.
946
+ * @returns `true` when the payload is accepted; `false` when validation fails.
947
+ */
948
+ connect(options) {
949
+ if (!options?.idToken && !options?.ticketAuth) {
950
+ this._emitCharError("MISSING_TOKEN", "connect() requires either idToken or ticketAuth parameter");
951
+ return false;
952
+ }
953
+ if (options?.idToken && !options?.ticketAuth && !options?.clientId) {
954
+ this._emitCharError("MISSING_CLIENT_ID", "connect() requires clientId when using idToken authentication");
955
+ return false;
956
+ }
957
+ this._pendingAuth = {
958
+ idToken: options.ticketAuth ? void 0 : options.idToken,
959
+ clientId: options.ticketAuth ? void 0 : options.clientId,
960
+ organizationId: options.ticketAuth ? void 0 : options.organizationId,
961
+ ticketAuth: options.ticketAuth
962
+ };
963
+ const iframeOrigin = this._getIframeOrigin();
964
+ if (this._iframeReady && iframeOrigin) this._postAuth(iframeOrigin);
965
+ return true;
966
+ }
967
+ /**
968
+ * Convenience method for declarative wrappers.
969
+ * Applies auth when provided, otherwise clears auth.
970
+ *
971
+ * @param options - Auth payload to connect, or `null` to disconnect.
972
+ * @returns Result from `connect()` or `disconnect()`.
973
+ */
974
+ setAuth(options) {
975
+ if (!options) return this.disconnect();
976
+ return this.connect(options);
977
+ }
978
+ /**
979
+ * Disconnect from the Char agent.
980
+ * Clears the authentication token.
981
+ *
982
+ * @returns `true` when the disconnect message was sent; `false` when the iframe was not ready.
983
+ */
984
+ disconnect() {
985
+ this._pendingAuth = null;
986
+ const iframeOrigin = this._getIframeOrigin();
987
+ if (this._iframeReady && this._iframe && iframeOrigin) {
988
+ this._iframe.contentWindow?.postMessage({ type: "char-disconnect" }, iframeOrigin);
989
+ return true;
990
+ }
991
+ return false;
992
+ }
993
+ /**
994
+ * Resolves the current iframe origin when mounted.
995
+ */
996
+ _getIframeOrigin() {
997
+ if (!this._iframe?.src) return null;
998
+ try {
999
+ return new URL(this._iframe.src).origin;
1000
+ } catch {
1001
+ console.error("[Char] Failed to parse iframe origin from src:", this._iframe.src);
1002
+ return null;
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Update the host context sent to the iframe.
1007
+ * Only changed fields are transmitted (diffing pattern).
1008
+ *
1009
+ * @param hostContext - Partial host context patch to merge and emit.
1010
+ */
1011
+ setHostContext(hostContext) {
1012
+ const changes = {};
1013
+ const nextContext = mergeHostContext(this._hostContext, hostContext);
1014
+ let hasChanges = false;
1015
+ for (const key of Object.keys(hostContext)) {
1016
+ const oldValue = this._hostContext[key];
1017
+ const newValue = nextContext[key];
1018
+ if (deepEqual(oldValue, newValue)) continue;
1019
+ changes[key] = hostContext[key];
1020
+ hasChanges = true;
1021
+ }
1022
+ if (hasChanges) {
1023
+ this._hostContext = nextContext;
1024
+ this._sendContextDelta(changes);
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Build and send the full initial context after iframe signals char-ready.
1029
+ *
1030
+ * @param iframeOrigin - Trusted origin used for postMessage target filtering.
1031
+ */
1032
+ _sendInitialContext(iframeOrigin) {
1033
+ const vars = extractCharVariables(this);
1034
+ const theme = isDarkMode() ? "dark" : "light";
1035
+ const displayModeAttr = this.getAttribute("display-mode");
1036
+ const locale = typeof navigator !== "undefined" ? navigator.language : void 0;
1037
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1038
+ const containerDimensions = {
1039
+ width: this.clientWidth,
1040
+ height: this.clientHeight
1041
+ };
1042
+ const safeAreaInsets = getSafeAreaInsets();
1043
+ const displayMode = resolveDisplayModeFromAttribute(displayModeAttr);
1044
+ const hostCapabilities = {
1045
+ supportedDisplayModes: [...DISPLAY_MODES],
1046
+ supportsOpenLink: true,
1047
+ supportsTeardown: true
1048
+ };
1049
+ const context = {
1050
+ theme,
1051
+ styles: { variables: vars },
1052
+ displayMode,
1053
+ availableDisplayModes: [...DISPLAY_MODES],
1054
+ containerDimensions,
1055
+ locale,
1056
+ timeZone,
1057
+ platform: detectPlatform(),
1058
+ deviceCapabilities: getDeviceCapabilities(),
1059
+ safeAreaInsets,
1060
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1061
+ hostCapabilities
1062
+ };
1063
+ this._hostContext = context;
1064
+ if (!this._iframe?.contentWindow) {
1065
+ console.error("[Char] iframe signaled char-ready but contentWindow is null");
1066
+ this._iframeReady = false;
1067
+ this._emitCharError("IFRAME_CONTENT_WINDOW_NULL", "iframe signaled char-ready but contentWindow is null");
1068
+ return;
1069
+ }
1070
+ this._iframe.contentWindow.postMessage({
1071
+ type: "char-context",
1072
+ ...context
1073
+ }, iframeOrigin);
1074
+ if (parseBooleanAttribute(this.getAttribute("enable-debug-tools"))) this._iframe?.contentWindow?.postMessage({
1075
+ type: "char-debug-tools",
1076
+ enabled: true
1077
+ }, iframeOrigin);
1078
+ if (this._pendingAuth) this._postAuth(iframeOrigin);
1079
+ }
1080
+ /**
1081
+ * Send a context delta to the iframe.
1082
+ *
1083
+ * @param changes - Minimal context patch derived from diffing.
1084
+ */
1085
+ _sendContextDelta(changes) {
1086
+ if (!this._iframeReady || !this._iframe?.contentWindow) return;
1087
+ const iframeOrigin = this._getIframeOrigin();
1088
+ if (!iframeOrigin) {
1089
+ console.warn("[Char] Cannot send context delta: iframe origin is null");
1090
+ return;
1091
+ }
1092
+ this._iframe.contentWindow.postMessage({
1093
+ type: "char-context",
1094
+ ...changes
1095
+ }, iframeOrigin);
1096
+ }
1097
+ /**
1098
+ * Post authentication credentials to the iframe.
1099
+ *
1100
+ * @param iframeOrigin - Trusted origin used for postMessage target filtering.
1101
+ */
1102
+ _postAuth(iframeOrigin) {
1103
+ if (!this._pendingAuth || !this._iframe?.contentWindow) return;
1104
+ const message = this._pendingAuth.ticketAuth ? {
1105
+ type: "char-auth",
1106
+ ticketAuth: this._pendingAuth.ticketAuth
1107
+ } : {
1108
+ type: "char-auth",
1109
+ idToken: this._pendingAuth.idToken,
1110
+ clientId: this._pendingAuth.clientId,
1111
+ organizationId: this._pendingAuth.organizationId
1112
+ };
1113
+ this._iframe.contentWindow.postMessage(message, iframeOrigin);
1114
+ }
1115
+ /**
1116
+ * Requests graceful iframe teardown and resolves once acknowledged or timed out.
1117
+ *
1118
+ * @param iframeOrigin - Trusted origin used for postMessage target filtering.
1119
+ * @param reason - Lifecycle reason for teardown.
1120
+ * @returns Promise resolved when teardown flow completes.
1121
+ */
1122
+ _requestTeardown(iframeOrigin, reason) {
1123
+ if (!this._iframeReady || !this._iframe?.contentWindow) return Promise.resolve();
1124
+ if (this._teardownPending) {
1125
+ window.clearTimeout(this._teardownPending.timeoutId);
1126
+ this._teardownPending.resolve();
1127
+ this._teardownPending = null;
1128
+ }
1129
+ const requestId = `teardown-${Date.now()}-${++this._teardownSequence}`;
1130
+ return new Promise((resolve) => {
1131
+ const timeoutId = window.setTimeout(() => {
1132
+ if (this._teardownPending?.requestId === requestId) {
1133
+ console.debug("[Char] Teardown timed out after 1000ms, requestId:", requestId);
1134
+ this._teardownPending = null;
1135
+ resolve();
1136
+ }
1137
+ }, 1e3);
1138
+ this._teardownPending = {
1139
+ requestId,
1140
+ timeoutId,
1141
+ resolve: () => {
1142
+ window.clearTimeout(timeoutId);
1143
+ resolve();
1144
+ }
1145
+ };
1146
+ this._iframe?.contentWindow?.postMessage({
1147
+ type: "char-teardown",
1148
+ requestId,
1149
+ reason
1150
+ }, iframeOrigin);
1151
+ });
1152
+ }
1153
+ /**
1154
+ * Completes an in-flight teardown promise when the iframe acknowledges it.
1155
+ *
1156
+ * @param requestId - Optional request identifier to match against pending teardown.
1157
+ */
1158
+ _resolvePendingTeardown(requestId) {
1159
+ const pending = this._teardownPending;
1160
+ if (!pending) return;
1161
+ if (requestId && requestId !== pending.requestId) return;
1162
+ this._teardownPending = null;
1163
+ pending.resolve();
1164
+ }
1165
+ /**
1166
+ * Handles open-link requests from the iframe with protocol allowlisting.
1167
+ * This allowlist intentionally mirrors the iframe-side pre-filter.
1168
+ *
1169
+ * @param requestId - Link request identifier supplied by the iframe.
1170
+ * @param url - Requested URL string.
1171
+ * @param iframeOrigin - Trusted origin used for postMessage target filtering.
1172
+ */
1173
+ _handleOpenLinkRequest(requestId, url, iframeOrigin) {
1174
+ let ok = false;
1175
+ let error;
1176
+ try {
1177
+ const parsed = new URL(url, window.location.href);
1178
+ const protocol = parsed.protocol.toLowerCase();
1179
+ if (!(protocol === "http:" || protocol === "https:" || protocol === "mailto:" || protocol === "tel:")) throw new Error(`Unsupported URL protocol: ${protocol}`);
1180
+ ok = window.open(parsed.toString(), "_blank", "noopener,noreferrer") !== null || protocol === "mailto:" || protocol === "tel:";
1181
+ if (!ok) error = "Popup blocked by browser";
1182
+ this.dispatchEvent(new CustomEvent("char-open-link", {
1183
+ ...CHAR_CUSTOM_EVENT_OPTIONS,
1184
+ detail: {
1185
+ requestId,
1186
+ url: parsed.toString(),
1187
+ ok
1188
+ }
1189
+ }));
1190
+ } catch (err) {
1191
+ error = err instanceof Error ? err.message : String(err);
1192
+ console.error("[Char] open-link request failed:", err);
1193
+ }
1194
+ this._iframe?.contentWindow?.postMessage({
1195
+ type: "char-open-link-result",
1196
+ requestId,
1197
+ ok,
1198
+ error
1199
+ }, iframeOrigin);
1200
+ }
1201
+ /**
1202
+ * Observe dark mode changes and forward to iframe via unified context.
1203
+ */
1204
+ _observeDarkMode() {
1205
+ const syncThemeAndStyles = () => {
1206
+ if (!this._iframeReady) return;
1207
+ this.setHostContext({
1208
+ theme: isDarkMode() ? "dark" : "light",
1209
+ styles: { variables: extractCharVariables(this) }
1210
+ });
1211
+ };
1212
+ this._darkModeObserver = new MutationObserver(syncThemeAndStyles);
1213
+ this._darkModeObserver.observe(document.documentElement, {
1214
+ attributes: true,
1215
+ attributeFilter: [
1216
+ "class",
1217
+ "style",
1218
+ "data-theme"
1219
+ ]
1220
+ });
1221
+ this._darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
1222
+ this._darkModeMediaHandler = syncThemeAndStyles;
1223
+ this._darkModeMediaQuery.addEventListener("change", this._darkModeMediaHandler);
1224
+ }
1225
+ /**
1226
+ * Observe style attribute changes on <char-agent> to detect CSS variable updates.
1227
+ * Re-extracts variables and sends delta via unified context.
1228
+ */
1229
+ _observeStyleChanges() {
1230
+ this._styleObserver = new MutationObserver(() => {
1231
+ if (!this._iframeReady) return;
1232
+ this.setHostContext({ styles: { variables: extractCharVariables(this) } });
1233
+ });
1234
+ this._styleObserver.observe(this, {
1235
+ attributes: true,
1236
+ attributeFilter: ["style", "class"]
1237
+ });
1238
+ }
1239
+ /**
1240
+ * Observe host element dimensions to provide containerDimensions context.
1241
+ */
1242
+ _observeContainerDimensions() {
1243
+ this._containerResizeObserver = new ResizeObserver((entries) => {
1244
+ if (!this._iframeReady) return;
1245
+ const entry = entries[0];
1246
+ if (!entry) return;
1247
+ this.setHostContext({
1248
+ containerDimensions: {
1249
+ width: Math.round(entry.contentRect.width),
1250
+ height: Math.round(entry.contentRect.height)
1251
+ },
1252
+ safeAreaInsets: getSafeAreaInsets()
1253
+ });
1254
+ });
1255
+ this._containerResizeObserver.observe(this);
1256
+ }
1257
+ /**
1258
+ * Resolve devMode from property (React 19) or attribute.
1259
+ *
1260
+ * @returns Parsed dev-mode configuration when valid.
1261
+ */
1262
+ _resolveDevMode() {
1263
+ if (this.devMode && typeof this.devMode === "object") return this.devMode;
1264
+ const devModeAttr = this.getAttribute("dev-mode");
1265
+ if (!devModeAttr) return void 0;
1266
+ try {
1267
+ const parsed = JSON.parse(devModeAttr);
1268
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1269
+ const msg = `dev-mode attribute must be a JSON object, got: ${Array.isArray(parsed) ? "array" : typeof parsed}`;
1270
+ console.warn(`[Char] ${msg}`);
1271
+ this._emitCharError("INVALID_DEV_MODE", msg);
1272
+ return;
1273
+ }
1274
+ const obj = parsed;
1275
+ return {
1276
+ anthropicApiKey: typeof obj.anthropicApiKey === "string" ? obj.anthropicApiKey : void 0,
1277
+ openaiApiKey: typeof obj.openaiApiKey === "string" ? obj.openaiApiKey : void 0,
1278
+ useLocalApi: typeof obj.useLocalApi === "boolean" ? obj.useLocalApi : void 0
1279
+ };
1280
+ } catch (e) {
1281
+ const msg = `Failed to parse dev-mode attribute as JSON: ${e instanceof Error ? e.message : String(e)}`;
1282
+ console.warn(`[Char] ${msg}`);
1283
+ this._emitCharError("INVALID_DEV_MODE", msg);
1284
+ return;
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Resolve apiBase override from property (React 19) or attribute.
1289
+ *
1290
+ * @returns Sanitized API base URL without trailing slash.
1291
+ */
1292
+ _resolveApiBaseOverride() {
1293
+ const trimmed = ((typeof this.apiBase === "string" ? this.apiBase : this.getAttribute("api-base")) ?? void 0)?.trim();
1294
+ if (!trimmed) return void 0;
1295
+ return trimmed.replace(/\/+$/, "");
1296
+ }
1297
+ };
1298
+ /**
1299
+ * Register the <char-agent> custom element.
1300
+ * Called automatically when importing this module.
1301
+ *
1302
+ * @param tagName - Optional custom tag name override.
1303
+ *
1304
+ * @public
1305
+ */
1306
+ function registerChar(tagName = "char-agent") {
1307
+ if (typeof window === "undefined") return;
1308
+ if (!customElements.get(tagName)) customElements.define(tagName, CharAgentElement);
1309
+ }
1310
+ registerChar();
1311
+
1312
+ //#endregion
1313
+ //#region src/shell-component.ts
1314
+ const SHELL_OPEN_CHANGE_EVENT = "char-shell-open-change";
1315
+ const SHELL_Z_INDEX = 2147483e3;
1316
+ const DEFAULT_FULLSCREEN_BREAKPOINT = 1024;
1317
+ const DEFAULT_PANEL_WIDTH = 420;
1318
+ const DEFAULT_PIP_WIDTH = 300;
1319
+ const DEFAULT_PIP_HEIGHT = 96;
1320
+ const DEFAULT_PIP_POSITION = "bottom-center";
1321
+ const SHELL_HEALTH_TIMEOUT_MS = 1e4;
1322
+ const PANEL_BORDER = "1px solid rgba(15, 23, 42, 0.12)";
1323
+ const SAFE_BOTTOM = "calc(16px + env(safe-area-inset-bottom, 0px))";
1324
+ const SHELL_LAYOUT_TRANSITION = "opacity 280ms ease, width 220ms ease, min-width 220ms ease, flex-basis 220ms ease";
1325
+ const SHELL_REVEAL_FADE_MS = 280;
1326
+ const PIP_MIN_COLLAPSED_WIDTH = 70;
1327
+ const PIP_MIN_COLLAPSED_HEIGHT = 30;
1328
+ const PIP_MAX_MEASURED_HEIGHT = 320;
1329
+ const PIP_COLLAPSE_DELAY_MS = 400;
1330
+ const PIP_PILL_HEIGHT = 40;
1331
+ const PIP_MORPH_TRANSITION = "width 280ms ease, height 280ms ease, bottom 280ms ease, border-radius 280ms ease, opacity 200ms ease, transform 280ms ease";
1332
+ const PIP_MORPH_DURATION_MS = 280;
1333
+ const PIP_SCROLL_HIDE_OFFSET = 100;
1334
+ const PIP_COLLAPSED_WIDTH_RATIO = .75;
1335
+ const PIP_SWIPE_THRESHOLD = 50;
1336
+ const PIP_SCROLL_DIRECTION_THRESHOLD = 40;
1337
+ const WHEEL_DELTA_LINE_MULTIPLIER = 15;
1338
+ const WHEEL_DELTA_PAGE_MULTIPLIER = 300;
1339
+ function normalizePositiveInteger(value, fallback) {
1340
+ if (!Number.isFinite(value) || value <= 0) {
1341
+ console.warn(`[Char] Invalid value ${value}, using fallback ${fallback}`);
1342
+ return fallback;
1343
+ }
1344
+ return Math.round(value);
1345
+ }
1346
+ function parsePositiveIntegerAttribute(value, fallback) {
1347
+ if (value === null || value.trim() === "") return fallback;
1348
+ const parsed = Number.parseInt(value, 10);
1349
+ if (!Number.isFinite(parsed)) return fallback;
1350
+ return normalizePositiveInteger(parsed, fallback);
1351
+ }
1352
+ function normalizePipPosition(value) {
1353
+ if (value === "bottom-right" || value === "bottom-left") return value;
1354
+ return DEFAULT_PIP_POSITION;
1355
+ }
1356
+ function getPipPositionStyles(position) {
1357
+ if (position === "bottom-right") return {
1358
+ left: "auto",
1359
+ right: "16px",
1360
+ transform: "none"
1361
+ };
1362
+ if (position === "bottom-left") return {
1363
+ left: "16px",
1364
+ right: "auto",
1365
+ transform: "none"
1366
+ };
1367
+ return {
1368
+ left: "50%",
1369
+ right: "auto",
1370
+ transform: "translateX(-50%)"
1371
+ };
1372
+ }
1373
+ function setStyleValue(el, name, value) {
1374
+ el.style.setProperty(name, value);
1375
+ }
1376
+ function resetStyleValue(el, name) {
1377
+ el.style.removeProperty(name);
1378
+ }
1379
+ function applyOpenAttribute(el, open) {
1380
+ if (open) {
1381
+ el.setAttribute("open", "");
1382
+ return;
1383
+ }
1384
+ el.removeAttribute("open");
1385
+ }
1386
+ /**
1387
+ * Strips shell-owned mode fields from host context patches.
1388
+ * `char-agent-shell` owns `displayMode` and `availableDisplayModes`.
1389
+ */
1390
+ function sanitizeShellHostContext(hostContext) {
1391
+ const { displayMode: _displayMode, availableDisplayModes: _availableModes, ...rest } = hostContext;
1392
+ return rest;
1393
+ }
1394
+ /**
1395
+ * `<char-agent-shell>` custom element.
1396
+ *
1397
+ * Opinionated host shell that composes a persistent inner `<char-agent>` and
1398
+ * owns responsive mode policy plus host layout orchestration.
1399
+ * This shell does not render chat controls, prompts, or composer UI.
1400
+ * Visible assistant UI is always iframe-owned.
1401
+ *
1402
+ * Policy:
1403
+ * - closed to `pip`
1404
+ * - open desktop to `inline`
1405
+ * - open narrow viewport to `fullscreen`
1406
+ *
1407
+ * @public
1408
+ */
1409
+ var CharAgentShellElement = class extends HTMLElement {
1410
+ static observedAttributes = [
1411
+ "open",
1412
+ "fullscreen-breakpoint",
1413
+ "panel-width",
1414
+ "pip-position",
1415
+ "pip-width",
1416
+ "pip-height",
1417
+ "dev-mode",
1418
+ "enable-debug-tools",
1419
+ "api-base"
1420
+ ];
1421
+ _agent = null;
1422
+ _open = false;
1423
+ _fullscreenBreakpoint = DEFAULT_FULLSCREEN_BREAKPOINT;
1424
+ _panelWidth = DEFAULT_PANEL_WIDTH;
1425
+ _pipWidth = DEFAULT_PIP_WIDTH;
1426
+ _pipHeight = DEFAULT_PIP_HEIGHT;
1427
+ _pipPosition = DEFAULT_PIP_POSITION;
1428
+ _isNarrowViewport = false;
1429
+ _mediaQuery = null;
1430
+ _mediaQueryHandler = null;
1431
+ _pendingConnectOptions = null;
1432
+ _pendingHostContext = {};
1433
+ _isApplyingOpenAttribute = false;
1434
+ _forwardedListenersAttached = false;
1435
+ _healthState = "idle";
1436
+ _healthTimeoutId = null;
1437
+ _pipMeasuredHeight = null;
1438
+ _revealFrameId = null;
1439
+ _pipExpanded = false;
1440
+ _pipPillElement = null;
1441
+ _pipCollapseTimerId = null;
1442
+ _hoverMediaQuery = null;
1443
+ _pipListenersAttached = false;
1444
+ _pipAnimating = false;
1445
+ _pipAnimationTimeoutId = null;
1446
+ _pendingPipMeasuredHeight = null;
1447
+ _tapOutsideListener = null;
1448
+ _pipHiddenByScroll = false;
1449
+ _scrollListener = null;
1450
+ _scrollRafId = null;
1451
+ _lastScrollEl = null;
1452
+ _lastScrollTop = null;
1453
+ _scrollDelta = 0;
1454
+ _swipeTouchStartY = null;
1455
+ _swipeStartAtTop = false;
1456
+ _swipeStartAtBottom = false;
1457
+ _swipeTouchStartListener = null;
1458
+ _swipeTouchEndListener = null;
1459
+ _wheelListener = null;
1460
+ _wheelDelta = 0;
1461
+ get open() {
1462
+ return this._open;
1463
+ }
1464
+ set open(value) {
1465
+ this.setOpen(value);
1466
+ }
1467
+ get fullscreenBreakpoint() {
1468
+ return this._fullscreenBreakpoint;
1469
+ }
1470
+ set fullscreenBreakpoint(value) {
1471
+ const normalized = normalizePositiveInteger(value, DEFAULT_FULLSCREEN_BREAKPOINT);
1472
+ if (normalized === this._fullscreenBreakpoint) return;
1473
+ this._fullscreenBreakpoint = normalized;
1474
+ this.setAttribute("fullscreen-breakpoint", String(normalized));
1475
+ this._setupViewportObserver();
1476
+ this._applyDisplayModeAndLayout();
1477
+ }
1478
+ get panelWidth() {
1479
+ return this._panelWidth;
1480
+ }
1481
+ set panelWidth(value) {
1482
+ const normalized = normalizePositiveInteger(value, DEFAULT_PANEL_WIDTH);
1483
+ if (normalized === this._panelWidth) return;
1484
+ this._panelWidth = normalized;
1485
+ this.setAttribute("panel-width", String(normalized));
1486
+ this._applyDisplayModeAndLayout();
1487
+ }
1488
+ get pipWidth() {
1489
+ return this._pipWidth;
1490
+ }
1491
+ set pipWidth(value) {
1492
+ const normalized = normalizePositiveInteger(value, DEFAULT_PIP_WIDTH);
1493
+ if (normalized === this._pipWidth) return;
1494
+ this._pipWidth = normalized;
1495
+ this.setAttribute("pip-width", String(normalized));
1496
+ this._applyDisplayModeAndLayout();
1497
+ }
1498
+ get pipHeight() {
1499
+ return this._pipHeight;
1500
+ }
1501
+ set pipHeight(value) {
1502
+ const normalized = normalizePositiveInteger(value, DEFAULT_PIP_HEIGHT);
1503
+ if (normalized === this._pipHeight) return;
1504
+ this._pipHeight = normalized;
1505
+ this.setAttribute("pip-height", String(normalized));
1506
+ this._applyDisplayModeAndLayout();
1507
+ }
1508
+ get pipPosition() {
1509
+ return this._pipPosition;
1510
+ }
1511
+ set pipPosition(value) {
1512
+ const normalized = normalizePipPosition(value);
1513
+ if (normalized === this._pipPosition) return;
1514
+ this._pipPosition = normalized;
1515
+ this.setAttribute("pip-position", normalized);
1516
+ this._applyDisplayModeAndLayout();
1517
+ }
1518
+ connectedCallback() {
1519
+ this._upgradeProperty("open");
1520
+ this._upgradeProperty("fullscreenBreakpoint");
1521
+ this._upgradeProperty("panelWidth");
1522
+ this._upgradeProperty("pipPosition");
1523
+ this._upgradeProperty("pipWidth");
1524
+ this._upgradeProperty("pipHeight");
1525
+ this._upgradeProperty("devMode");
1526
+ this._upgradeProperty("apiBase");
1527
+ registerChar();
1528
+ this._ensureAgent();
1529
+ this._readConfigFromAttributes();
1530
+ this._syncForwardedAttributes();
1531
+ this._setupViewportObserver();
1532
+ this._applyDisplayModeAndLayout();
1533
+ this._flushPendingConnectAndContext();
1534
+ }
1535
+ disconnectedCallback() {
1536
+ this._stopHealthWait();
1537
+ this._cancelRevealFade();
1538
+ this._teardownViewportObserver();
1539
+ this._detachForwardedListeners();
1540
+ this._detachPipInteractionListeners();
1541
+ this._clearPipCollapseTimer();
1542
+ if (this._pipAnimationTimeoutId !== null) {
1543
+ window.clearTimeout(this._pipAnimationTimeoutId);
1544
+ this._pipAnimationTimeoutId = null;
1545
+ }
1546
+ this._pipAnimating = false;
1547
+ if (this._pipPillElement) {
1548
+ this._pipPillElement.remove();
1549
+ this._pipPillElement = null;
1550
+ }
1551
+ this._hoverMediaQuery = null;
1552
+ }
1553
+ attributeChangedCallback(name, _oldValue, newValue) {
1554
+ switch (name) {
1555
+ case "open": {
1556
+ if (this._isApplyingOpenAttribute) return;
1557
+ const nextOpen = parseBooleanAttribute(newValue);
1558
+ this._setOpenInternal(nextOpen, {
1559
+ emit: true,
1560
+ reflect: false
1561
+ });
1562
+ return;
1563
+ }
1564
+ case "fullscreen-breakpoint": {
1565
+ const next = parsePositiveIntegerAttribute(newValue, DEFAULT_FULLSCREEN_BREAKPOINT);
1566
+ if (next !== this._fullscreenBreakpoint) {
1567
+ this._fullscreenBreakpoint = next;
1568
+ this._setupViewportObserver();
1569
+ this._applyDisplayModeAndLayout();
1570
+ }
1571
+ return;
1572
+ }
1573
+ case "panel-width": {
1574
+ const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PANEL_WIDTH);
1575
+ if (next !== this._panelWidth) {
1576
+ this._panelWidth = next;
1577
+ this._applyDisplayModeAndLayout();
1578
+ }
1579
+ return;
1580
+ }
1581
+ case "pip-width": {
1582
+ const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_WIDTH);
1583
+ if (next !== this._pipWidth) {
1584
+ this._pipWidth = next;
1585
+ this._applyDisplayModeAndLayout();
1586
+ }
1587
+ return;
1588
+ }
1589
+ case "pip-height": {
1590
+ const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_HEIGHT);
1591
+ if (next !== this._pipHeight) {
1592
+ this._pipHeight = next;
1593
+ this._applyDisplayModeAndLayout();
1594
+ }
1595
+ return;
1596
+ }
1597
+ case "pip-position": {
1598
+ const next = normalizePipPosition(newValue);
1599
+ if (next !== this._pipPosition) {
1600
+ this._pipPosition = next;
1601
+ this._applyDisplayModeAndLayout();
1602
+ }
1603
+ return;
1604
+ }
1605
+ case "dev-mode":
1606
+ case "enable-debug-tools":
1607
+ case "api-base":
1608
+ this._syncForwardedAttributes();
1609
+ return;
1610
+ }
1611
+ }
1612
+ /**
1613
+ * Connects shell authentication and starts availability health checks.
1614
+ *
1615
+ * @param options - Authentication payload for either ID token or ticket auth.
1616
+ * @returns `true` when connect is accepted by the inner element.
1617
+ */
1618
+ connect(options) {
1619
+ if (this._healthState === "unavailable") this._recoverFromUnavailable();
1620
+ if (!this._agent) {
1621
+ this._pendingConnectOptions = options;
1622
+ this._startHealthWait();
1623
+ return true;
1624
+ }
1625
+ const ok = this._agent.connect(options);
1626
+ if (ok) {
1627
+ this._pendingConnectOptions = options;
1628
+ this._startHealthWait();
1629
+ }
1630
+ return ok;
1631
+ }
1632
+ /**
1633
+ * Clears authentication and disconnects the inner `<char-agent>`.
1634
+ *
1635
+ * @returns `true` when disconnect dispatch succeeds.
1636
+ */
1637
+ disconnect() {
1638
+ this._pendingConnectOptions = null;
1639
+ this._stopHealthWait();
1640
+ if (this._healthState !== "unavailable") this._healthState = "idle";
1641
+ return this._agent ? this._agent.disconnect() : true;
1642
+ }
1643
+ /**
1644
+ * Convenience method for declarative wrappers.
1645
+ * Applies auth when provided, otherwise clears auth.
1646
+ *
1647
+ * @param options - Auth payload to connect, or `null` to disconnect.
1648
+ * @returns Result from `connect()` or `disconnect()`.
1649
+ */
1650
+ setAuth(options) {
1651
+ if (!options) return this.disconnect();
1652
+ return this.connect(options);
1653
+ }
1654
+ /**
1655
+ * Merges and forwards host context to the inner `<char-agent>`.
1656
+ * Shell-owned fields (`displayMode`, `availableDisplayModes`) are ignored.
1657
+ *
1658
+ * @param hostContext - Partial host context patch to forward.
1659
+ */
1660
+ setHostContext(hostContext) {
1661
+ const sanitizedContext = sanitizeShellHostContext(hostContext);
1662
+ if (Object.keys(sanitizedContext).length === 0) return;
1663
+ if (!this._agent) {
1664
+ this._pendingHostContext = mergeHostContext(this._pendingHostContext, sanitizedContext);
1665
+ return;
1666
+ }
1667
+ this._agent.setHostContext(sanitizedContext);
1668
+ }
1669
+ /**
1670
+ * Sets shell open state.
1671
+ *
1672
+ * @param open - Next desired open state.
1673
+ */
1674
+ setOpen(open) {
1675
+ if (this._healthState === "unavailable" && open) {
1676
+ this.dispatchEvent(new CustomEvent("char-error", {
1677
+ bubbles: true,
1678
+ composed: true,
1679
+ detail: {
1680
+ code: "SHELL_UNAVAILABLE",
1681
+ message: "Cannot open shell: agent is unavailable"
1682
+ }
1683
+ }));
1684
+ return;
1685
+ }
1686
+ this._setOpenInternal(Boolean(open), {
1687
+ emit: true,
1688
+ reflect: true
1689
+ });
1690
+ }
1691
+ /**
1692
+ * Toggles shell open state.
1693
+ */
1694
+ toggleOpen() {
1695
+ this._setOpenInternal(!this._open, {
1696
+ emit: true,
1697
+ reflect: true
1698
+ });
1699
+ }
1700
+ _upgradeProperty(propertyName) {
1701
+ if (!Object.hasOwn(this, propertyName)) return;
1702
+ const value = this[propertyName];
1703
+ delete this[propertyName];
1704
+ this[propertyName] = value;
1705
+ }
1706
+ _ensureAgent() {
1707
+ if (this._agent) {
1708
+ if (!this.contains(this._agent)) this.appendChild(this._agent);
1709
+ this._attachForwardedListeners();
1710
+ return;
1711
+ }
1712
+ const agent = document.createElement("char-agent");
1713
+ agent.style.display = "block";
1714
+ agent.style.width = "100%";
1715
+ agent.style.height = "100%";
1716
+ agent.style.border = "none";
1717
+ this._primeAgentBootConfig(agent);
1718
+ this._agent = agent;
1719
+ this.appendChild(agent);
1720
+ this._attachForwardedListeners();
1721
+ }
1722
+ _primeAgentBootConfig(agent) {
1723
+ for (const name of [
1724
+ "dev-mode",
1725
+ "enable-debug-tools",
1726
+ "api-base"
1727
+ ]) {
1728
+ const value = this.getAttribute(name);
1729
+ if (value !== null) agent.setAttribute(name, value);
1730
+ }
1731
+ agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1732
+ agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1733
+ }
1734
+ _attachForwardedListeners() {
1735
+ if (!this._agent || this._forwardedListenersAttached) return;
1736
+ this._agent.addEventListener("char-request-display-mode", this._handleRequestDisplayMode);
1737
+ this._agent.addEventListener("char-close", this._handleClose);
1738
+ this._agent.addEventListener("char-initialized", this._handleInitialized);
1739
+ this._agent.addEventListener("char-error", this._handleInnerError);
1740
+ this._agent.addEventListener("char-size-changed", this._handleInnerSizeChanged);
1741
+ this._forwardedListenersAttached = true;
1742
+ }
1743
+ _detachForwardedListeners() {
1744
+ if (!this._agent || !this._forwardedListenersAttached) return;
1745
+ this._agent.removeEventListener("char-request-display-mode", this._handleRequestDisplayMode);
1746
+ this._agent.removeEventListener("char-close", this._handleClose);
1747
+ this._agent.removeEventListener("char-initialized", this._handleInitialized);
1748
+ this._agent.removeEventListener("char-error", this._handleInnerError);
1749
+ this._agent.removeEventListener("char-size-changed", this._handleInnerSizeChanged);
1750
+ this._forwardedListenersAttached = false;
1751
+ }
1752
+ _readConfigFromAttributes() {
1753
+ this._open = parseBooleanAttribute(this.getAttribute("open"));
1754
+ this._fullscreenBreakpoint = parsePositiveIntegerAttribute(this.getAttribute("fullscreen-breakpoint"), DEFAULT_FULLSCREEN_BREAKPOINT);
1755
+ this._panelWidth = parsePositiveIntegerAttribute(this.getAttribute("panel-width"), DEFAULT_PANEL_WIDTH);
1756
+ this._pipWidth = parsePositiveIntegerAttribute(this.getAttribute("pip-width"), DEFAULT_PIP_WIDTH);
1757
+ this._pipHeight = parsePositiveIntegerAttribute(this.getAttribute("pip-height"), DEFAULT_PIP_HEIGHT);
1758
+ this._pipPosition = normalizePipPosition(this.getAttribute("pip-position"));
1759
+ }
1760
+ _syncForwardedAttributes() {
1761
+ if (!this._agent) return;
1762
+ for (const name of [
1763
+ "dev-mode",
1764
+ "enable-debug-tools",
1765
+ "api-base"
1766
+ ]) {
1767
+ const value = this.getAttribute(name);
1768
+ if (value === null) this._agent.removeAttribute(name);
1769
+ else this._agent.setAttribute(name, value);
1770
+ }
1771
+ this._agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1772
+ this._agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1773
+ }
1774
+ _setupViewportObserver() {
1775
+ if (typeof window === "undefined") return;
1776
+ const query = `(max-width: ${this._fullscreenBreakpoint - 1}px)`;
1777
+ if (this._mediaQuery?.media === query) {
1778
+ this._isNarrowViewport = this._mediaQuery.matches;
1779
+ return;
1780
+ }
1781
+ this._teardownViewportObserver();
1782
+ this._mediaQuery = window.matchMedia(query);
1783
+ this._isNarrowViewport = this._mediaQuery.matches;
1784
+ this._mediaQueryHandler = (event) => {
1785
+ this._isNarrowViewport = event.matches;
1786
+ this._applyDisplayModeAndLayout();
1787
+ };
1788
+ this._mediaQuery.addEventListener("change", this._mediaQueryHandler);
1789
+ }
1790
+ _teardownViewportObserver() {
1791
+ if (this._mediaQuery && this._mediaQueryHandler) this._mediaQuery.removeEventListener("change", this._mediaQueryHandler);
1792
+ this._mediaQuery = null;
1793
+ this._mediaQueryHandler = null;
1794
+ }
1795
+ _applyDisplayModeAndLayout() {
1796
+ if (!this._agent) return;
1797
+ if (this._healthState === "unavailable") {
1798
+ this._applyUnavailableLayout();
1799
+ return;
1800
+ }
1801
+ if (this._healthState !== "healthy") {
1802
+ this._applyPreReadyLayout();
1803
+ return;
1804
+ }
1805
+ this.hidden = false;
1806
+ const displayMode = resolvePolicyDisplayMode({
1807
+ open: this._open,
1808
+ isNarrowViewport: this._isNarrowViewport,
1809
+ availableModes: DEFAULT_AVAILABLE_DISPLAY_MODES
1810
+ });
1811
+ if (displayMode !== "pip") {
1812
+ if (this._pipMeasuredHeight !== null) this._pipMeasuredHeight = null;
1813
+ }
1814
+ this._agent.setAttribute("display-mode", displayMode);
1815
+ this._applyHostLayout(displayMode);
1816
+ this._applyAgentLayout(displayMode);
1817
+ }
1818
+ _applyHostLayout(displayMode) {
1819
+ if (displayMode === "inline" && !this._isNarrowViewport) {
1820
+ setStyleValue(this, "position", "relative");
1821
+ setStyleValue(this, "flex", `0 0 ${this._panelWidth}px`);
1822
+ setStyleValue(this, "width", `${this._panelWidth}px`);
1823
+ setStyleValue(this, "min-width", `${this._panelWidth}px`);
1824
+ setStyleValue(this, "height", "100%");
1825
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1826
+ setStyleValue(this, "border-left", PANEL_BORDER);
1827
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1828
+ return;
1829
+ }
1830
+ setStyleValue(this, "position", "relative");
1831
+ setStyleValue(this, "flex", "0 0 0px");
1832
+ setStyleValue(this, "width", "0px");
1833
+ setStyleValue(this, "min-width", "0px");
1834
+ setStyleValue(this, "height", "0px");
1835
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1836
+ resetStyleValue(this, "border-left");
1837
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1838
+ }
1839
+ _composePipTransform(opts) {
1840
+ const parts = [];
1841
+ if (this._pipPosition === "bottom-center") parts.push("translateX(-50%)");
1842
+ if (opts.scrollHide) parts.push(`translateY(${PIP_SCROLL_HIDE_OFFSET}px)`);
1843
+ return parts.length > 0 ? parts.join(" ") : "none";
1844
+ }
1845
+ _applyAgentLayout(displayMode) {
1846
+ if (!this._agent) return;
1847
+ if (displayMode === "inline" && !this._isNarrowViewport) {
1848
+ this._detachPipInteractionListeners();
1849
+ this._clearPipCollapseTimer();
1850
+ this._pipExpanded = false;
1851
+ this._pipAnimating = false;
1852
+ this._hidePipPill();
1853
+ setStyleValue(this._agent, "position", "relative");
1854
+ resetStyleValue(this._agent, "top");
1855
+ resetStyleValue(this._agent, "right");
1856
+ resetStyleValue(this._agent, "bottom");
1857
+ resetStyleValue(this._agent, "left");
1858
+ resetStyleValue(this._agent, "transform");
1859
+ resetStyleValue(this._agent, "max-width");
1860
+ resetStyleValue(this._agent, "max-height");
1861
+ setStyleValue(this._agent, "z-index", "auto");
1862
+ setStyleValue(this._agent, "width", "100%");
1863
+ setStyleValue(this._agent, "height", "100%");
1864
+ setStyleValue(this._agent, "border-radius", "0");
1865
+ setStyleValue(this._agent, "overflow", "hidden");
1866
+ resetStyleValue(this._agent, "transition");
1867
+ resetStyleValue(this._agent, "opacity");
1868
+ resetStyleValue(this._agent, "pointer-events");
1869
+ return;
1870
+ }
1871
+ if (displayMode === "fullscreen") {
1872
+ this._detachPipInteractionListeners();
1873
+ this._clearPipCollapseTimer();
1874
+ this._pipExpanded = false;
1875
+ this._pipAnimating = false;
1876
+ this._hidePipPill();
1877
+ setStyleValue(this._agent, "position", "fixed");
1878
+ setStyleValue(this._agent, "top", "0");
1879
+ setStyleValue(this._agent, "right", "0");
1880
+ setStyleValue(this._agent, "bottom", "0");
1881
+ setStyleValue(this._agent, "left", "0");
1882
+ resetStyleValue(this._agent, "transform");
1883
+ resetStyleValue(this._agent, "max-width");
1884
+ resetStyleValue(this._agent, "max-height");
1885
+ setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1886
+ setStyleValue(this._agent, "width", "100vw");
1887
+ setStyleValue(this._agent, "height", "100dvh");
1888
+ setStyleValue(this._agent, "border-radius", "0");
1889
+ setStyleValue(this._agent, "overflow", "hidden");
1890
+ resetStyleValue(this._agent, "transition");
1891
+ resetStyleValue(this._agent, "opacity");
1892
+ resetStyleValue(this._agent, "pointer-events");
1893
+ return;
1894
+ }
1895
+ const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1896
+ const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, this._pipWidth);
1897
+ const collapsedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, Math.round(this._pipWidth * PIP_COLLAPSED_WIDTH_RATIO));
1898
+ const targetWidth = this._pipExpanded ? expandedWidth : collapsedWidth;
1899
+ const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, this._pipHeight);
1900
+ const hasExplicitPipHeight = this.hasAttribute("pip-height");
1901
+ const measuredHeightFloor = typeof this._pipMeasuredHeight === "number" ? this._pipMeasuredHeight : 0;
1902
+ const expandedHeight = Math.max(collapsedHeight, measuredHeightFloor);
1903
+ const collapsedPipHeight = hasExplicitPipHeight ? collapsedHeight : PIP_PILL_HEIGHT;
1904
+ const heightDelta = expandedHeight - collapsedPipHeight;
1905
+ let targetHeight;
1906
+ let targetBottom;
1907
+ if (this._pipExpanded) {
1908
+ targetHeight = expandedHeight;
1909
+ targetBottom = SAFE_BOTTOM;
1910
+ } else {
1911
+ targetHeight = collapsedPipHeight;
1912
+ targetBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1913
+ }
1914
+ const composedTransform = this._composePipTransform({ scrollHide: this._pipHiddenByScroll });
1915
+ setStyleValue(this._agent, "position", "fixed");
1916
+ resetStyleValue(this._agent, "top");
1917
+ setStyleValue(this._agent, "right", pipPositionStyles.right);
1918
+ setStyleValue(this._agent, "left", pipPositionStyles.left);
1919
+ setStyleValue(this._agent, "bottom", targetBottom);
1920
+ setStyleValue(this._agent, "transform", composedTransform);
1921
+ setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1922
+ setStyleValue(this._agent, "width", `min(${targetWidth}px, calc(100vw - 24px))`);
1923
+ setStyleValue(this._agent, "height", `${targetHeight}px`);
1924
+ setStyleValue(this._agent, "max-height", "calc(100dvh - 24px - env(safe-area-inset-bottom, 0px))");
1925
+ setStyleValue(this._agent, "max-width", "100vw");
1926
+ setStyleValue(this._agent, "border-radius", "16px");
1927
+ setStyleValue(this._agent, "overflow", "hidden");
1928
+ setStyleValue(this._agent, "transition", this._healthState === "healthy" ? PIP_MORPH_TRANSITION : "none");
1929
+ if (this._healthState === "healthy") {
1930
+ const pill = this._ensurePipPill();
1931
+ if (!pill.parentNode) this.appendChild(pill);
1932
+ this._updatePipPillPosition(collapsedWidth, heightDelta, collapsedPipHeight);
1933
+ this._attachPipInteractionListeners();
1934
+ this._attachScrollHideListener();
1935
+ this._attachSwipeGestureListeners();
1936
+ this._attachWheelFallbackListener();
1937
+ this._applyPipVisualState();
1938
+ }
1939
+ }
1940
+ _ensurePipPill() {
1941
+ if (this._pipPillElement) return this._pipPillElement;
1942
+ const pill = document.createElement("div");
1943
+ pill.setAttribute("role", "button");
1944
+ pill.setAttribute("aria-label", "Open assistant");
1945
+ pill.setAttribute("tabindex", "0");
1946
+ const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1947
+ pill.style.cssText = [
1948
+ "position:fixed",
1949
+ `left:${pipPositionStyles.left}`,
1950
+ `right:${pipPositionStyles.right}`,
1951
+ `z-index:${SHELL_Z_INDEX + 1}`,
1952
+ "background:transparent",
1953
+ "border-radius:16px",
1954
+ "cursor:pointer",
1955
+ "user-select:none",
1956
+ "-webkit-user-select:none"
1957
+ ].join(";");
1958
+ this._pipPillElement = pill;
1959
+ return pill;
1960
+ }
1961
+ _updatePipPillPosition(pipWidth, heightDelta, pillHeight) {
1962
+ if (!this._pipPillElement) return;
1963
+ const collapsedBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1964
+ setStyleValue(this._pipPillElement, "bottom", collapsedBottom);
1965
+ setStyleValue(this._pipPillElement, "width", `min(${pipWidth}px, calc(100vw - 24px))`);
1966
+ setStyleValue(this._pipPillElement, "height", `${pillHeight}px`);
1967
+ setStyleValue(this._pipPillElement, "transform", this._composePipTransform({ scrollHide: this._pipHiddenByScroll }));
1968
+ setStyleValue(this._pipPillElement, "transition", "transform 280ms ease, width 280ms ease, opacity 200ms ease");
1969
+ }
1970
+ _applyPipVisualState() {
1971
+ if (!this._agent || !this._pipPillElement) return;
1972
+ if (this._pipHiddenByScroll) {
1973
+ setStyleValue(this._agent, "opacity", "0");
1974
+ setStyleValue(this._agent, "pointer-events", "none");
1975
+ setStyleValue(this._pipPillElement, "opacity", "0");
1976
+ setStyleValue(this._pipPillElement, "pointer-events", "none");
1977
+ return;
1978
+ }
1979
+ setStyleValue(this._agent, "opacity", "1");
1980
+ if (this._pipExpanded) {
1981
+ setStyleValue(this._agent, "pointer-events", "auto");
1982
+ setStyleValue(this._pipPillElement, "pointer-events", "none");
1983
+ } else {
1984
+ setStyleValue(this._agent, "pointer-events", "none");
1985
+ setStyleValue(this._pipPillElement, "pointer-events", "auto");
1986
+ }
1987
+ }
1988
+ _setPipExpanded(expanded) {
1989
+ this._pipExpanded = expanded;
1990
+ this._clearPipCollapseTimer();
1991
+ this._applyPipVisualState();
1992
+ if (this._agent?.getAttribute("display-mode") === "pip") this._applyAgentLayout("pip");
1993
+ this._pipAnimating = true;
1994
+ this._pendingPipMeasuredHeight = null;
1995
+ if (this._pipAnimationTimeoutId !== null) window.clearTimeout(this._pipAnimationTimeoutId);
1996
+ this._pipAnimationTimeoutId = window.setTimeout(() => {
1997
+ this._pipAnimating = false;
1998
+ this._pipAnimationTimeoutId = null;
1999
+ this._flushPendingPipMeasuredHeight();
2000
+ }, PIP_MORPH_DURATION_MS);
2001
+ if (expanded) this._attachTapOutsideListener();
2002
+ else this._detachTapOutsideListener();
2003
+ }
2004
+ _flushPendingPipMeasuredHeight() {
2005
+ const pending = this._pendingPipMeasuredHeight;
2006
+ this._pendingPipMeasuredHeight = null;
2007
+ if (pending === null || pending === this._pipMeasuredHeight) return;
2008
+ this._pipMeasuredHeight = pending;
2009
+ if (this._agent?.getAttribute("display-mode") === "pip") this._applyAgentLayout("pip");
2010
+ }
2011
+ _isHoverDevice() {
2012
+ if (!this._hoverMediaQuery) {
2013
+ if (typeof window === "undefined") return false;
2014
+ this._hoverMediaQuery = window.matchMedia("(hover: hover)");
2015
+ }
2016
+ return this._hoverMediaQuery.matches;
2017
+ }
2018
+ _attachPipInteractionListeners() {
2019
+ if (this._pipListenersAttached || !this._agent || !this._pipPillElement) return;
2020
+ this._pipListenersAttached = true;
2021
+ const pill = this._pipPillElement;
2022
+ const agent = this._agent;
2023
+ if (this._isHoverDevice()) {
2024
+ pill.addEventListener("pointerenter", this._handlePipPointerEnter);
2025
+ agent.addEventListener("pointerenter", this._handlePipPointerEnter);
2026
+ pill.addEventListener("pointerleave", this._handlePipPointerLeave);
2027
+ agent.addEventListener("pointerleave", this._handlePipPointerLeave);
2028
+ }
2029
+ pill.addEventListener("click", this._handlePillClick);
2030
+ pill.addEventListener("keydown", this._handlePillKeydown);
2031
+ }
2032
+ _detachPipInteractionListeners() {
2033
+ if (!this._pipListenersAttached) return;
2034
+ this._pipListenersAttached = false;
2035
+ const pill = this._pipPillElement;
2036
+ const agent = this._agent;
2037
+ if (pill) {
2038
+ pill.removeEventListener("pointerenter", this._handlePipPointerEnter);
2039
+ pill.removeEventListener("pointerleave", this._handlePipPointerLeave);
2040
+ pill.removeEventListener("click", this._handlePillClick);
2041
+ pill.removeEventListener("keydown", this._handlePillKeydown);
2042
+ }
2043
+ if (agent) {
2044
+ agent.removeEventListener("pointerenter", this._handlePipPointerEnter);
2045
+ agent.removeEventListener("pointerleave", this._handlePipPointerLeave);
2046
+ }
2047
+ this._detachTapOutsideListener();
2048
+ this._detachScrollHideListener();
2049
+ this._detachSwipeGestureListeners();
2050
+ this._detachWheelFallbackListener();
2051
+ }
2052
+ _schedulePipCollapse() {
2053
+ this._clearPipCollapseTimer();
2054
+ this._pipCollapseTimerId = window.setTimeout(() => {
2055
+ this._pipCollapseTimerId = null;
2056
+ this._setPipExpanded(false);
2057
+ }, PIP_COLLAPSE_DELAY_MS);
2058
+ }
2059
+ _clearPipCollapseTimer() {
2060
+ if (this._pipCollapseTimerId !== null) {
2061
+ window.clearTimeout(this._pipCollapseTimerId);
2062
+ this._pipCollapseTimerId = null;
2063
+ }
2064
+ }
2065
+ _attachTapOutsideListener() {
2066
+ if (this._tapOutsideListener || this._isHoverDevice()) return;
2067
+ this._tapOutsideListener = (e) => {
2068
+ const target = e.target;
2069
+ if (!target) return;
2070
+ if (this._pipPillElement?.contains(target)) return;
2071
+ if (this._agent?.contains(target)) return;
2072
+ this._setPipExpanded(false);
2073
+ };
2074
+ document.addEventListener("click", this._tapOutsideListener, true);
2075
+ }
2076
+ _detachTapOutsideListener() {
2077
+ if (this._tapOutsideListener) {
2078
+ document.removeEventListener("click", this._tapOutsideListener, true);
2079
+ this._tapOutsideListener = null;
2080
+ }
2081
+ }
2082
+ _handlePipPointerEnter = () => {
2083
+ this._clearPipCollapseTimer();
2084
+ if (!this._pipExpanded) this._setPipExpanded(true);
2085
+ };
2086
+ _handlePipPointerLeave = () => {
2087
+ if (this._pipExpanded) this._schedulePipCollapse();
2088
+ };
2089
+ _attachScrollHideListener() {
2090
+ if (this._scrollListener || typeof window === "undefined") return;
2091
+ this._scrollListener = (e) => {
2092
+ const target = e.target;
2093
+ let scrollEl = null;
2094
+ if (target === document || target === document.documentElement) scrollEl = document.scrollingElement || document.documentElement;
2095
+ else if (target instanceof Element) scrollEl = target;
2096
+ if (!scrollEl) return;
2097
+ if (scrollEl.clientHeight < window.innerHeight * .3) return;
2098
+ if (this._scrollRafId !== null) return;
2099
+ this._scrollRafId = requestAnimationFrame(() => {
2100
+ this._scrollRafId = null;
2101
+ this._evaluateScrollPosition(scrollEl);
2102
+ });
2103
+ };
2104
+ document.addEventListener("scroll", this._scrollListener, {
2105
+ capture: true,
2106
+ passive: true
2107
+ });
2108
+ }
2109
+ _evaluateScrollPosition(el) {
2110
+ const scrollTop = el.scrollTop;
2111
+ const maxScroll = el.scrollHeight - el.clientHeight;
2112
+ if (el !== this._lastScrollEl) {
2113
+ this._lastScrollEl = el;
2114
+ this._lastScrollTop = scrollTop;
2115
+ this._scrollDelta = 0;
2116
+ return;
2117
+ }
2118
+ if (scrollTop <= 0) {
2119
+ this._lastScrollTop = 0;
2120
+ this._scrollDelta = 0;
2121
+ if (this._pipHiddenByScroll) {
2122
+ this._pipHiddenByScroll = false;
2123
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2124
+ this._applyPipVisualState();
2125
+ this._applyAgentLayout("pip");
2126
+ }
2127
+ }
2128
+ return;
2129
+ }
2130
+ if (maxScroll > 0 && scrollTop >= maxScroll - 2) {
2131
+ this._lastScrollTop = scrollTop;
2132
+ this._scrollDelta = 0;
2133
+ if (!this._pipHiddenByScroll) {
2134
+ this._pipHiddenByScroll = true;
2135
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2136
+ this._applyPipVisualState();
2137
+ this._applyAgentLayout("pip");
2138
+ }
2139
+ }
2140
+ return;
2141
+ }
2142
+ if (this._lastScrollTop === null) {
2143
+ this._lastScrollTop = scrollTop;
2144
+ return;
2145
+ }
2146
+ const delta = scrollTop - this._lastScrollTop;
2147
+ this._lastScrollTop = scrollTop;
2148
+ if (delta > 0 && this._scrollDelta < 0 || delta < 0 && this._scrollDelta > 0) this._scrollDelta = 0;
2149
+ this._scrollDelta += delta;
2150
+ if (this._scrollDelta > PIP_SCROLL_DIRECTION_THRESHOLD && !this._pipHiddenByScroll) {
2151
+ this._pipHiddenByScroll = true;
2152
+ this._scrollDelta = 0;
2153
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2154
+ this._applyPipVisualState();
2155
+ this._applyAgentLayout("pip");
2156
+ }
2157
+ } else if (this._scrollDelta < -PIP_SCROLL_DIRECTION_THRESHOLD && this._pipHiddenByScroll) {
2158
+ this._pipHiddenByScroll = false;
2159
+ this._scrollDelta = 0;
2160
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2161
+ this._applyPipVisualState();
2162
+ this._applyAgentLayout("pip");
2163
+ }
2164
+ }
2165
+ }
2166
+ _detachScrollHideListener() {
2167
+ if (this._scrollListener) {
2168
+ document.removeEventListener("scroll", this._scrollListener, true);
2169
+ this._scrollListener = null;
2170
+ }
2171
+ if (this._scrollRafId !== null) {
2172
+ cancelAnimationFrame(this._scrollRafId);
2173
+ this._scrollRafId = null;
2174
+ }
2175
+ this._pipHiddenByScroll = false;
2176
+ this._lastScrollEl = null;
2177
+ this._lastScrollTop = null;
2178
+ this._scrollDelta = 0;
2179
+ }
2180
+ _attachSwipeGestureListeners() {
2181
+ if (this._swipeTouchStartListener || typeof window === "undefined") return;
2182
+ this._swipeTouchStartListener = (e) => {
2183
+ if (e.touches.length !== 1) return;
2184
+ this._swipeTouchStartY = e.touches[0].clientY;
2185
+ const scrollEl = document.scrollingElement || document.documentElement;
2186
+ this._swipeStartAtTop = scrollEl.scrollTop <= 0;
2187
+ this._swipeStartAtBottom = scrollEl.scrollHeight <= scrollEl.clientHeight || scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 1;
2188
+ };
2189
+ this._swipeTouchEndListener = (e) => {
2190
+ if (this._swipeTouchStartY === null) return;
2191
+ if (e.changedTouches.length === 0) return;
2192
+ const deltaY = e.changedTouches[0].clientY - this._swipeTouchStartY;
2193
+ this._swipeTouchStartY = null;
2194
+ if (Math.abs(deltaY) < PIP_SWIPE_THRESHOLD) return;
2195
+ if (deltaY < -PIP_SWIPE_THRESHOLD && this._swipeStartAtTop && this._pipHiddenByScroll) {
2196
+ this._pipHiddenByScroll = false;
2197
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2198
+ this._applyPipVisualState();
2199
+ this._applyAgentLayout("pip");
2200
+ }
2201
+ return;
2202
+ }
2203
+ if (deltaY > PIP_SWIPE_THRESHOLD && this._swipeStartAtBottom && !this._pipHiddenByScroll) {
2204
+ this._pipHiddenByScroll = true;
2205
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2206
+ this._applyPipVisualState();
2207
+ this._applyAgentLayout("pip");
2208
+ }
2209
+ }
2210
+ };
2211
+ document.addEventListener("touchstart", this._swipeTouchStartListener, {
2212
+ capture: true,
2213
+ passive: true
2214
+ });
2215
+ document.addEventListener("touchend", this._swipeTouchEndListener, {
2216
+ capture: true,
2217
+ passive: true
2218
+ });
2219
+ }
2220
+ _detachSwipeGestureListeners() {
2221
+ if (this._swipeTouchStartListener) {
2222
+ document.removeEventListener("touchstart", this._swipeTouchStartListener, true);
2223
+ this._swipeTouchStartListener = null;
2224
+ }
2225
+ if (this._swipeTouchEndListener) {
2226
+ document.removeEventListener("touchend", this._swipeTouchEndListener, true);
2227
+ this._swipeTouchEndListener = null;
2228
+ }
2229
+ this._swipeTouchStartY = null;
2230
+ }
2231
+ _attachWheelFallbackListener() {
2232
+ if (this._wheelListener || typeof window === "undefined") return;
2233
+ this._wheelListener = (e) => {
2234
+ const scrollEl = document.scrollingElement || document.documentElement;
2235
+ if (scrollEl.scrollHeight > scrollEl.clientHeight) return;
2236
+ let deltaY = e.deltaY;
2237
+ if (e.deltaMode === WheelEvent.DOM_DELTA_LINE) deltaY *= WHEEL_DELTA_LINE_MULTIPLIER;
2238
+ else if (e.deltaMode === WheelEvent.DOM_DELTA_PAGE) deltaY *= WHEEL_DELTA_PAGE_MULTIPLIER;
2239
+ if (deltaY > 0 && this._wheelDelta < 0 || deltaY < 0 && this._wheelDelta > 0) this._wheelDelta = 0;
2240
+ this._wheelDelta += deltaY;
2241
+ if (this._wheelDelta > PIP_SCROLL_DIRECTION_THRESHOLD && !this._pipHiddenByScroll) {
2242
+ this._pipHiddenByScroll = true;
2243
+ this._wheelDelta = 0;
2244
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2245
+ this._applyPipVisualState();
2246
+ this._applyAgentLayout("pip");
2247
+ }
2248
+ } else if (this._wheelDelta < -PIP_SCROLL_DIRECTION_THRESHOLD && this._pipHiddenByScroll) {
2249
+ this._pipHiddenByScroll = false;
2250
+ this._wheelDelta = 0;
2251
+ if (this._agent?.getAttribute("display-mode") === "pip") {
2252
+ this._applyPipVisualState();
2253
+ this._applyAgentLayout("pip");
2254
+ }
2255
+ }
2256
+ };
2257
+ document.addEventListener("wheel", this._wheelListener, {
2258
+ capture: true,
2259
+ passive: true
2260
+ });
2261
+ }
2262
+ _detachWheelFallbackListener() {
2263
+ if (this._wheelListener) {
2264
+ document.removeEventListener("wheel", this._wheelListener, true);
2265
+ this._wheelListener = null;
2266
+ }
2267
+ this._wheelDelta = 0;
2268
+ }
2269
+ _handlePillClick = () => {
2270
+ if (this._isHoverDevice()) this.setOpen(true);
2271
+ else if (this._pipExpanded) this.setOpen(true);
2272
+ else this._setPipExpanded(true);
2273
+ };
2274
+ _handlePillKeydown = (event) => {
2275
+ const key = event.key;
2276
+ if (key === "Enter" || key === " ") {
2277
+ event.preventDefault();
2278
+ this.setOpen(true);
2279
+ }
2280
+ };
2281
+ _hidePipPill() {
2282
+ if (this._pipPillElement) {
2283
+ setStyleValue(this._pipPillElement, "opacity", "0");
2284
+ setStyleValue(this._pipPillElement, "pointer-events", "none");
2285
+ }
2286
+ }
2287
+ _resetHostLayoutStyles() {
2288
+ for (const name of [
2289
+ "position",
2290
+ "flex",
2291
+ "width",
2292
+ "min-width",
2293
+ "height",
2294
+ "z-index",
2295
+ "border-left",
2296
+ "transition",
2297
+ "opacity",
2298
+ "will-change"
2299
+ ]) resetStyleValue(this, name);
2300
+ }
2301
+ _resetAgentLayoutStyles() {
2302
+ if (!this._agent) return;
2303
+ for (const name of [
2304
+ "position",
2305
+ "top",
2306
+ "right",
2307
+ "bottom",
2308
+ "left",
2309
+ "transform",
2310
+ "z-index",
2311
+ "width",
2312
+ "height",
2313
+ "max-width",
2314
+ "max-height",
2315
+ "border-radius",
2316
+ "overflow",
2317
+ "transition",
2318
+ "opacity",
2319
+ "pointer-events"
2320
+ ]) resetStyleValue(this._agent, name);
2321
+ }
2322
+ _applyUnavailableLayout() {
2323
+ this._cancelRevealFade();
2324
+ this._detachPipInteractionListeners();
2325
+ this._detachScrollHideListener();
2326
+ this._detachSwipeGestureListeners();
2327
+ this._detachWheelFallbackListener();
2328
+ this._clearPipCollapseTimer();
2329
+ this._pipExpanded = false;
2330
+ this._pipAnimating = false;
2331
+ this._hidePipPill();
2332
+ if (this._agent) this._agent.removeAttribute("display-mode");
2333
+ this._resetHostLayoutStyles();
2334
+ this._resetAgentLayoutStyles();
2335
+ this.hidden = true;
2336
+ }
2337
+ _applyPreReadyLayout() {
2338
+ this._applyUnavailableLayout();
2339
+ }
2340
+ _startHealthWait() {
2341
+ if (typeof window === "undefined") return;
2342
+ this._stopHealthWait();
2343
+ this._applyPreReadyLayout();
2344
+ this._healthState = "waiting";
2345
+ this._healthTimeoutId = window.setTimeout(() => {
2346
+ if (this._healthState !== "waiting") return;
2347
+ this._setUnavailable("Initialization timed out before the embedded agent became ready.");
2348
+ }, SHELL_HEALTH_TIMEOUT_MS);
2349
+ }
2350
+ _stopHealthWait() {
2351
+ if (this._healthTimeoutId === null || typeof window === "undefined") return;
2352
+ window.clearTimeout(this._healthTimeoutId);
2353
+ this._healthTimeoutId = null;
2354
+ }
2355
+ _markHealthy() {
2356
+ this._stopHealthWait();
2357
+ this._healthState = "healthy";
2358
+ this.hidden = false;
2359
+ this._applyDisplayModeAndLayout();
2360
+ this._startRevealFade();
2361
+ }
2362
+ _cancelRevealFade() {
2363
+ if (this._revealFrameId !== null && typeof window !== "undefined") window.cancelAnimationFrame(this._revealFrameId);
2364
+ this._revealFrameId = null;
2365
+ resetStyleValue(this, "opacity");
2366
+ resetStyleValue(this, "will-change");
2367
+ }
2368
+ _startRevealFade() {
2369
+ if (typeof window === "undefined") return;
2370
+ this._cancelRevealFade();
2371
+ setStyleValue(this, "opacity", "0");
2372
+ setStyleValue(this, "will-change", "opacity");
2373
+ this._revealFrameId = window.requestAnimationFrame(() => {
2374
+ this._revealFrameId = null;
2375
+ setStyleValue(this, "opacity", "1");
2376
+ window.setTimeout(() => {
2377
+ if (this._healthState === "healthy") resetStyleValue(this, "will-change");
2378
+ }, SHELL_REVEAL_FADE_MS);
2379
+ });
2380
+ }
2381
+ _setUnavailable(message) {
2382
+ this._stopHealthWait();
2383
+ this._healthState = "unavailable";
2384
+ this._pendingConnectOptions = null;
2385
+ this._pipMeasuredHeight = null;
2386
+ this._setOpenInternal(false, {
2387
+ emit: true,
2388
+ reflect: true
2389
+ });
2390
+ this._applyUnavailableLayout();
2391
+ this.dispatchEvent(new CustomEvent("char-error", {
2392
+ bubbles: true,
2393
+ composed: true,
2394
+ detail: {
2395
+ code: "SHELL_UNAVAILABLE",
2396
+ message
2397
+ }
2398
+ }));
2399
+ }
2400
+ _recoverFromUnavailable() {
2401
+ if (this._healthState !== "unavailable") return;
2402
+ this._healthState = "idle";
2403
+ this._pipMeasuredHeight = null;
2404
+ this._detachForwardedListeners();
2405
+ if (this._agent) {
2406
+ this._agent.remove();
2407
+ this._agent = null;
2408
+ }
2409
+ this._forwardedListenersAttached = false;
2410
+ this._ensureAgent();
2411
+ this._syncForwardedAttributes();
2412
+ this._applyPreReadyLayout();
2413
+ const agent = this._agent;
2414
+ if (Object.keys(this._pendingHostContext).length > 0) {
2415
+ if (agent) agent.setHostContext(this._pendingHostContext);
2416
+ this._pendingHostContext = {};
2417
+ }
2418
+ }
2419
+ _setOpenInternal(nextOpen, options) {
2420
+ if (this._healthState === "unavailable" && nextOpen) {
2421
+ this._reflectOpenAttribute(false);
2422
+ return;
2423
+ }
2424
+ if (nextOpen === this._open) {
2425
+ if (options.reflect) this._reflectOpenAttribute(nextOpen);
2426
+ return;
2427
+ }
2428
+ this._open = nextOpen;
2429
+ if (options.reflect) this._reflectOpenAttribute(nextOpen);
2430
+ this._applyDisplayModeAndLayout();
2431
+ if (options.emit) this.dispatchEvent(new CustomEvent(SHELL_OPEN_CHANGE_EVENT, {
2432
+ bubbles: true,
2433
+ composed: true,
2434
+ detail: { open: nextOpen }
2435
+ }));
2436
+ }
2437
+ _reflectOpenAttribute(open) {
2438
+ this._isApplyingOpenAttribute = true;
2439
+ applyOpenAttribute(this, open);
2440
+ this._isApplyingOpenAttribute = false;
2441
+ }
2442
+ _flushPendingConnectAndContext() {
2443
+ if (!this._agent) return;
2444
+ if (this._pendingConnectOptions) {
2445
+ if (this._agent.connect(this._pendingConnectOptions)) this._startHealthWait();
2446
+ }
2447
+ if (Object.keys(this._pendingHostContext).length > 0) {
2448
+ this._agent.setHostContext(this._pendingHostContext);
2449
+ this._pendingHostContext = {};
2450
+ }
2451
+ }
2452
+ _handleRequestDisplayMode = (event) => {
2453
+ if (this._healthState === "unavailable") return;
2454
+ const detail = event.detail;
2455
+ if (!isDisplayMode(detail?.mode)) return;
2456
+ if (detail.mode === "pip") {
2457
+ this.setOpen(false);
2458
+ return;
2459
+ }
2460
+ this.setOpen(true);
2461
+ };
2462
+ _handleClose = () => {
2463
+ this.setOpen(false);
2464
+ };
2465
+ _handleInitialized = () => {
2466
+ this._markHealthy();
2467
+ };
2468
+ _handleInnerError = (event) => {
2469
+ if (this._healthState !== "waiting") return;
2470
+ const detail = event.detail;
2471
+ this._setUnavailable(detail?.message ?? "Embedded agent initialization failed before it became available.");
2472
+ };
2473
+ _handleInnerSizeChanged = (event) => {
2474
+ if (!this._agent || this._agent.getAttribute("display-mode") !== "pip") return;
2475
+ const detail = event.detail;
2476
+ if (detail?.displayMode !== "pip") return;
2477
+ const reportedHeight = detail?.height;
2478
+ if (typeof reportedHeight !== "number" || !Number.isFinite(reportedHeight) || reportedHeight <= 0) return;
2479
+ const nextMeasuredHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, Math.min(Math.round(reportedHeight), PIP_MAX_MEASURED_HEIGHT));
2480
+ if (this._pipAnimating) {
2481
+ this._pendingPipMeasuredHeight = nextMeasuredHeight;
2482
+ return;
2483
+ }
2484
+ if (this._pipMeasuredHeight === nextMeasuredHeight) return;
2485
+ this._pipMeasuredHeight = nextMeasuredHeight;
2486
+ this._applyAgentLayout("pip");
2487
+ };
2488
+ };
2489
+ /**
2490
+ * Registers the `<char-agent-shell>` custom element.
2491
+ *
2492
+ * @param tagName - Optional custom tag name override.
2493
+ *
2494
+ * @public
2495
+ */
2496
+ function registerCharShell(tagName = "char-agent-shell") {
2497
+ if (typeof window === "undefined") return;
2498
+ if (!customElements.get(tagName)) customElements.define(tagName, CharAgentShellElement);
2499
+ }
2500
+ registerCharShell();
2501
+
2502
+ //#endregion
2503
+ export { CharAgentShellElement, registerCharShell };
2504
+ //# sourceMappingURL=shell-component.js.map