@mcp-b/char 0.1.2 → 0.1.4

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.
@@ -90,8 +90,7 @@ function mergeHostContext(current, patch) {
90
90
  styles: mergeStyles(previous.styles, patch.styles),
91
91
  containerDimensions: shallowMerge(previous.containerDimensions, patch.containerDimensions),
92
92
  deviceCapabilities: shallowMerge(previous.deviceCapabilities, patch.deviceCapabilities),
93
- safeAreaInsets: shallowMerge(previous.safeAreaInsets, patch.safeAreaInsets),
94
- hostCapabilities: shallowMerge(previous.hostCapabilities, patch.hostCapabilities)
93
+ safeAreaInsets: shallowMerge(previous.safeAreaInsets, patch.safeAreaInsets)
95
94
  };
96
95
  }
97
96
 
@@ -503,25 +502,6 @@ const DISPLAY_MODES = [
503
502
  "pip"
504
503
  ];
505
504
  /**
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
505
  * Runtime guard for the message envelope emitted by the iframe.
526
506
  */
527
507
  function isCharIframeMessageData(value) {
@@ -660,7 +640,7 @@ function getDeviceCapabilities() {
660
640
  */
661
641
  var CharAgentElement = class extends HTMLElement {
662
642
  static observedAttributes = [
663
- "dev-mode",
643
+ "publishable-key",
664
644
  "enable-debug-tools",
665
645
  "display-mode",
666
646
  "api-base"
@@ -710,12 +690,12 @@ var CharAgentElement = class extends HTMLElement {
710
690
  return;
711
691
  }
712
692
  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);
693
+ const apiBase = this._resolveApiBaseOverride() ?? WEBMCP_PRODUCTION_API_BASE;
715
694
  const iframeOrigin = this._resolveIframeOrigin(apiBase);
716
695
  const iframe = this._createIframe(apiBase);
717
696
  this._iframe = iframe;
718
- if (resolvedDevMode?.anthropicApiKey) this._pendingAuth = { anthropicApiKey: resolvedDevMode.anthropicApiKey };
697
+ const publishableKey = this.publishableKey ?? this.getAttribute("publishable-key") ?? void 0;
698
+ if (publishableKey) this._pendingAuth = { publishableKey };
719
699
  this._proxy = new CharIframeProxy(iframe, iframeOrigin);
720
700
  this._proxy.start();
721
701
  this._messageListener = this._createMessageListener(iframe, iframeOrigin);
@@ -924,15 +904,21 @@ var CharAgentElement = class extends HTMLElement {
924
904
  /**
925
905
  * Reacts to observed attribute updates after iframe readiness.
926
906
  * Attributes that affect iframe boot configuration are intentionally ignored
927
- * after mount (`dev-mode`, `enable-debug-tools`, `api-base`).
907
+ * after mount (`enable-debug-tools`, `api-base`).
928
908
  */
929
909
  attributeChangedCallback(name, _oldValue, newValue) {
930
910
  if (!this._iframe || !this._iframeReady) return;
931
911
  switch (name) {
912
+ case "publishable-key":
913
+ if (newValue) {
914
+ this._pendingAuth = { publishableKey: newValue };
915
+ const iframeOrigin = this._getIframeOrigin();
916
+ if (iframeOrigin) this._postAuth(iframeOrigin);
917
+ }
918
+ break;
932
919
  case "display-mode":
933
920
  this.setHostContext({ displayMode: resolveDisplayModeFromAttribute(newValue) });
934
921
  break;
935
- case "dev-mode":
936
922
  case "enable-debug-tools":
937
923
  case "api-base": break;
938
924
  }
@@ -943,23 +929,17 @@ var CharAgentElement = class extends HTMLElement {
943
929
  * The token is stored as a JavaScript property (not as a DOM attribute),
944
930
  * preventing exposure to DOM inspection and session replay tools.
945
931
  *
946
- * @param options - Authentication payload for either ID token or ticket auth.
932
+ * @param options - Authentication payload.
947
933
  * @returns `true` when the payload is accepted; `false` when validation fails.
948
934
  */
949
935
  connect(options) {
950
- if (!options?.idToken && !options?.ticketAuth) {
951
- this._emitCharError("MISSING_TOKEN", "connect() requires either idToken or ticketAuth parameter");
952
- return false;
953
- }
954
- if (options?.idToken && !options?.ticketAuth && !options?.clientId) {
955
- this._emitCharError("MISSING_CLIENT_ID", "connect() requires clientId when using idToken authentication");
936
+ if (!options?.publishableKey) {
937
+ this._emitCharError("MISSING_PUBLISHABLE_KEY", "connect() requires publishableKey");
956
938
  return false;
957
939
  }
958
940
  this._pendingAuth = {
959
- idToken: options.ticketAuth ? void 0 : options.idToken,
960
- clientId: options.ticketAuth ? void 0 : options.clientId,
961
- organizationId: options.ticketAuth ? void 0 : options.organizationId,
962
- ticketAuth: options.ticketAuth
941
+ publishableKey: options.publishableKey,
942
+ idToken: options.idToken
963
943
  };
964
944
  const iframeOrigin = this._getIframeOrigin();
965
945
  if (this._iframeReady && iframeOrigin) this._postAuth(iframeOrigin);
@@ -978,7 +958,7 @@ var CharAgentElement = class extends HTMLElement {
978
958
  }
979
959
  /**
980
960
  * Disconnect from the Char agent.
981
- * Clears the authentication token.
961
+ * Clears pending auth state and posts a disconnect message to the iframe.
982
962
  *
983
963
  * @returns `true` when the disconnect message was sent; `false` when the iframe was not ready.
984
964
  */
@@ -1042,11 +1022,6 @@ var CharAgentElement = class extends HTMLElement {
1042
1022
  };
1043
1023
  const safeAreaInsets = getSafeAreaInsets();
1044
1024
  const displayMode = resolveDisplayModeFromAttribute(displayModeAttr) ?? "inline";
1045
- const hostCapabilities = {
1046
- supportedDisplayModes: [...DISPLAY_MODES],
1047
- supportsOpenLink: true,
1048
- supportsTeardown: true
1049
- };
1050
1025
  const context = {
1051
1026
  theme,
1052
1027
  styles: { variables: vars },
@@ -1057,9 +1032,7 @@ var CharAgentElement = class extends HTMLElement {
1057
1032
  timeZone,
1058
1033
  platform: detectPlatform(),
1059
1034
  deviceCapabilities: getDeviceCapabilities(),
1060
- safeAreaInsets,
1061
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1062
- hostCapabilities
1035
+ safeAreaInsets
1063
1036
  };
1064
1037
  this._hostContext = context;
1065
1038
  if (!this._iframe?.contentWindow) {
@@ -1102,17 +1075,15 @@ var CharAgentElement = class extends HTMLElement {
1102
1075
  */
1103
1076
  _postAuth(iframeOrigin) {
1104
1077
  if (!this._pendingAuth || !this._iframe?.contentWindow) return;
1105
- const message = this._pendingAuth.ticketAuth ? {
1106
- type: "char-auth",
1107
- ticketAuth: this._pendingAuth.ticketAuth
1108
- } : this._pendingAuth.anthropicApiKey ? {
1109
- type: "char-auth",
1110
- anthropicApiKey: this._pendingAuth.anthropicApiKey
1111
- } : {
1078
+ if (!this._pendingAuth.publishableKey) {
1079
+ console.error("[Char] _postAuth called but _pendingAuth has no publishableKey");
1080
+ this._pendingAuth = null;
1081
+ return;
1082
+ }
1083
+ const message = {
1112
1084
  type: "char-auth",
1113
- idToken: this._pendingAuth.idToken,
1114
- clientId: this._pendingAuth.clientId,
1115
- organizationId: this._pendingAuth.organizationId
1085
+ publishableKey: this._pendingAuth.publishableKey,
1086
+ idToken: this._pendingAuth.idToken
1116
1087
  };
1117
1088
  this._iframe.contentWindow.postMessage(message, iframeOrigin);
1118
1089
  }
@@ -1259,36 +1230,6 @@ var CharAgentElement = class extends HTMLElement {
1259
1230
  this._containerResizeObserver.observe(this);
1260
1231
  }
1261
1232
  /**
1262
- * Resolve devMode from property (React 19) or attribute.
1263
- *
1264
- * @returns Parsed dev-mode configuration when valid.
1265
- */
1266
- _resolveDevMode() {
1267
- if (this.devMode && typeof this.devMode === "object") return this.devMode;
1268
- const devModeAttr = this.getAttribute("dev-mode");
1269
- if (!devModeAttr) return void 0;
1270
- try {
1271
- const parsed = JSON.parse(devModeAttr);
1272
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1273
- const msg = `dev-mode attribute must be a JSON object, got: ${Array.isArray(parsed) ? "array" : typeof parsed}`;
1274
- console.warn(`[Char] ${msg}`);
1275
- this._emitCharError("INVALID_DEV_MODE", msg);
1276
- return;
1277
- }
1278
- const obj = parsed;
1279
- return {
1280
- anthropicApiKey: typeof obj.anthropicApiKey === "string" ? obj.anthropicApiKey : void 0,
1281
- openaiApiKey: typeof obj.openaiApiKey === "string" ? obj.openaiApiKey : void 0,
1282
- useLocalApi: typeof obj.useLocalApi === "boolean" ? obj.useLocalApi : void 0
1283
- };
1284
- } catch (e) {
1285
- const msg = `Failed to parse dev-mode attribute as JSON: ${e instanceof Error ? e.message : String(e)}`;
1286
- console.warn(`[Char] ${msg}`);
1287
- this._emitCharError("INVALID_DEV_MODE", msg);
1288
- return;
1289
- }
1290
- }
1291
- /**
1292
1233
  * Resolve apiBase override from property (React 19) or attribute.
1293
1234
  *
1294
1235
  * @returns Sanitized API base URL without trailing slash.
@@ -1317,22 +1258,36 @@ registerChar();
1317
1258
  //#region src/shell-component.ts
1318
1259
  const SHELL_OPEN_CHANGE_EVENT = "char-shell-open-change";
1319
1260
  const SHELL_Z_INDEX = 2147483e3;
1320
- const DEFAULT_FULLSCREEN_BREAKPOINT = 1024;
1321
- const DEFAULT_PANEL_WIDTH = 420;
1322
- const DEFAULT_PIP_WIDTH = 300;
1323
- const DEFAULT_PIP_HEIGHT = 96;
1324
- const DEFAULT_PIP_POSITION = "bottom-center";
1261
+ const FULLSCREEN_BREAKPOINT = 1024;
1262
+ const PANEL_WIDTH = 420;
1263
+ const PIP_WIDTH = 400;
1264
+ const PIP_HEIGHT = 60;
1265
+ const PIP_POSITION = "bottom-center";
1325
1266
  const SHELL_HEALTH_TIMEOUT_MS = 1e4;
1326
- const PANEL_BORDER = "1px solid rgba(15, 23, 42, 0.12)";
1327
1267
  const SAFE_BOTTOM = "calc(16px + env(safe-area-inset-bottom, 0px))";
1328
- const SHELL_LAYOUT_TRANSITION = "opacity 280ms ease, width 220ms ease, min-width 220ms ease, flex-basis 220ms ease";
1268
+ const THEME_LIGHT = {
1269
+ background: "oklch(1 0 0)",
1270
+ foreground: "oklch(0.141 0.005 285.823)",
1271
+ border: "oklch(0.92 0.004 286.32)",
1272
+ mutedForeground: "oklch(0.552 0.016 285.938)",
1273
+ primary: "oklch(0.21 0.006 285.885)"
1274
+ };
1275
+ const THEME_DARK = {
1276
+ background: "oklch(0.141 0.005 285.823)",
1277
+ foreground: "oklch(0.985 0 0)",
1278
+ border: "oklch(1 0 0 / 10%)",
1279
+ mutedForeground: "oklch(0.705 0.015 286.067)",
1280
+ primary: "oklch(0.92 0.004 286.32)"
1281
+ };
1282
+ const BODY_MARGIN_TRANSITION = "margin-right 220ms ease";
1283
+ const SHELL_LAYOUT_TRANSITION = "opacity 220ms ease, width 220ms ease, min-width 220ms ease, flex-basis 220ms ease";
1329
1284
  const SHELL_REVEAL_FADE_MS = 280;
1330
1285
  const PIP_MIN_COLLAPSED_WIDTH = 70;
1331
1286
  const PIP_MIN_COLLAPSED_HEIGHT = 30;
1332
1287
  const PIP_MAX_MEASURED_HEIGHT = 320;
1333
1288
  const PIP_COLLAPSE_DELAY_MS = 400;
1334
1289
  const PIP_PILL_HEIGHT = 40;
1335
- const PIP_MORPH_TRANSITION = "width 280ms ease, height 280ms ease, bottom 280ms ease, border-radius 280ms ease, opacity 200ms ease, transform 280ms ease";
1290
+ const PIP_MORPH_TRANSITION = "width 280ms ease, height 280ms ease, bottom 280ms ease, border-radius 280ms ease, opacity 280ms ease, transform 280ms ease";
1336
1291
  const PIP_MORPH_DURATION_MS = 280;
1337
1292
  const PIP_SCROLL_HIDE_OFFSET = 100;
1338
1293
  const PIP_COLLAPSED_WIDTH_RATIO = .75;
@@ -1340,23 +1295,14 @@ const PIP_SWIPE_THRESHOLD = 50;
1340
1295
  const PIP_SCROLL_DIRECTION_THRESHOLD = 40;
1341
1296
  const WHEEL_DELTA_LINE_MULTIPLIER = 15;
1342
1297
  const WHEEL_DELTA_PAGE_MULTIPLIER = 300;
1343
- function normalizePositiveInteger(value, fallback) {
1344
- if (!Number.isFinite(value) || value <= 0) {
1345
- console.warn(`[Char] Invalid value ${value}, using fallback ${fallback}`);
1346
- return fallback;
1347
- }
1348
- return Math.round(value);
1349
- }
1350
- function parsePositiveIntegerAttribute(value, fallback) {
1351
- if (value === null || value.trim() === "") return fallback;
1352
- const parsed = Number.parseInt(value, 10);
1353
- if (!Number.isFinite(parsed)) return fallback;
1354
- return normalizePositiveInteger(parsed, fallback);
1355
- }
1356
- function normalizePipPosition(value) {
1357
- if (value === "bottom-right" || value === "bottom-left") return value;
1358
- return DEFAULT_PIP_POSITION;
1359
- }
1298
+ const PANEL_MIN_WIDTH = 320;
1299
+ const PANEL_MAX_WIDTH = 700;
1300
+ const RESIZE_NUB_WIDTH = 6;
1301
+ const EDGE_TAB_WIDTH = 44;
1302
+ const EDGE_TAB_HEIGHT = 48;
1303
+ const EDGE_TAB_DRAG_THRESHOLD_PX = 4;
1304
+ const ICON_MESSAGE = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>`;
1305
+ const ICON_CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`;
1360
1306
  function getPipPositionStyles(position) {
1361
1307
  if (position === "bottom-right") return {
1362
1308
  left: "auto",
@@ -1413,25 +1359,28 @@ function sanitizeShellHostContext(hostContext) {
1413
1359
  var CharAgentShellElement = class extends HTMLElement {
1414
1360
  static observedAttributes = [
1415
1361
  "open",
1416
- "fullscreen-breakpoint",
1417
- "panel-width",
1418
- "pip-position",
1419
- "pip-width",
1420
- "pip-height",
1421
- "dev-mode",
1422
1362
  "enable-debug-tools",
1423
1363
  "api-base"
1424
1364
  ];
1425
1365
  _agent = null;
1426
1366
  _open = false;
1427
- _fullscreenBreakpoint = DEFAULT_FULLSCREEN_BREAKPOINT;
1428
- _panelWidth = DEFAULT_PANEL_WIDTH;
1429
- _pipWidth = DEFAULT_PIP_WIDTH;
1430
- _pipHeight = DEFAULT_PIP_HEIGHT;
1431
- _pipPosition = DEFAULT_PIP_POSITION;
1367
+ _savedBodyTransition = null;
1368
+ _bodyMarginTimeoutId = null;
1369
+ _currentPanelWidth = PANEL_WIDTH;
1370
+ _resizeNubElement = null;
1371
+ _edgeTabElement = null;
1372
+ _resizeDragging = false;
1373
+ _resizeDragOverlay = null;
1374
+ _resizeStartX = 0;
1375
+ _resizeStartWidth = 0;
1376
+ _edgeTabDragStartX = 0;
1377
+ _edgeTabDragging = false;
1432
1378
  _isNarrowViewport = false;
1433
1379
  _mediaQuery = null;
1434
1380
  _mediaQueryHandler = null;
1381
+ _themeObserver = null;
1382
+ _themeMediaQuery = null;
1383
+ _themeMediaHandler = null;
1435
1384
  _pendingConnectOptions = null;
1436
1385
  _pendingHostContext = {};
1437
1386
  _isApplyingOpenAttribute = false;
@@ -1449,6 +1398,7 @@ var CharAgentShellElement = class extends HTMLElement {
1449
1398
  _pipAnimationTimeoutId = null;
1450
1399
  _pendingPipMeasuredHeight = null;
1451
1400
  _tapOutsideListener = null;
1401
+ _lastAppliedDisplayMode = null;
1452
1402
  _pipHiddenByScroll = false;
1453
1403
  _scrollListener = null;
1454
1404
  _scrollRafId = null;
@@ -1468,75 +1418,36 @@ var CharAgentShellElement = class extends HTMLElement {
1468
1418
  set open(value) {
1469
1419
  this.setOpen(value);
1470
1420
  }
1471
- get fullscreenBreakpoint() {
1472
- return this._fullscreenBreakpoint;
1473
- }
1474
- set fullscreenBreakpoint(value) {
1475
- const normalized = normalizePositiveInteger(value, DEFAULT_FULLSCREEN_BREAKPOINT);
1476
- if (normalized === this._fullscreenBreakpoint) return;
1477
- this._fullscreenBreakpoint = normalized;
1478
- this.setAttribute("fullscreen-breakpoint", String(normalized));
1479
- this._setupViewportObserver();
1480
- this._applyDisplayModeAndLayout();
1481
- }
1482
- get panelWidth() {
1483
- return this._panelWidth;
1484
- }
1485
- set panelWidth(value) {
1486
- const normalized = normalizePositiveInteger(value, DEFAULT_PANEL_WIDTH);
1487
- if (normalized === this._panelWidth) return;
1488
- this._panelWidth = normalized;
1489
- this.setAttribute("panel-width", String(normalized));
1490
- this._applyDisplayModeAndLayout();
1491
- }
1492
- get pipWidth() {
1493
- return this._pipWidth;
1494
- }
1495
- set pipWidth(value) {
1496
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_WIDTH);
1497
- if (normalized === this._pipWidth) return;
1498
- this._pipWidth = normalized;
1499
- this.setAttribute("pip-width", String(normalized));
1500
- this._applyDisplayModeAndLayout();
1501
- }
1502
- get pipHeight() {
1503
- return this._pipHeight;
1504
- }
1505
- set pipHeight(value) {
1506
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_HEIGHT);
1507
- if (normalized === this._pipHeight) return;
1508
- this._pipHeight = normalized;
1509
- this.setAttribute("pip-height", String(normalized));
1510
- this._applyDisplayModeAndLayout();
1511
- }
1512
- get pipPosition() {
1513
- return this._pipPosition;
1514
- }
1515
- set pipPosition(value) {
1516
- const normalized = normalizePipPosition(value);
1517
- if (normalized === this._pipPosition) return;
1518
- this._pipPosition = normalized;
1519
- this.setAttribute("pip-position", normalized);
1520
- this._applyDisplayModeAndLayout();
1521
- }
1522
1421
  connectedCallback() {
1523
1422
  this._upgradeProperty("open");
1524
- this._upgradeProperty("fullscreenBreakpoint");
1525
- this._upgradeProperty("panelWidth");
1526
- this._upgradeProperty("pipPosition");
1527
- this._upgradeProperty("pipWidth");
1528
- this._upgradeProperty("pipHeight");
1529
- this._upgradeProperty("devMode");
1530
1423
  this._upgradeProperty("apiBase");
1531
1424
  registerChar();
1532
1425
  this._ensureAgent();
1533
1426
  this._readConfigFromAttributes();
1534
1427
  this._syncForwardedAttributes();
1535
1428
  this._setupViewportObserver();
1429
+ this._setupThemeObserver();
1536
1430
  this._applyDisplayModeAndLayout();
1537
1431
  this._flushPendingConnectAndContext();
1538
1432
  }
1539
1433
  disconnectedCallback() {
1434
+ this._removeBodyMargin();
1435
+ if (this._bodyMarginTimeoutId !== null) {
1436
+ window.clearTimeout(this._bodyMarginTimeoutId);
1437
+ this._bodyMarginTimeoutId = null;
1438
+ }
1439
+ this._removeResizeDragOverlay();
1440
+ this._teardownThemeObserver();
1441
+ document.removeEventListener("mousemove", this._onEdgeTabMouseMove);
1442
+ document.removeEventListener("mouseup", this._onEdgeTabMouseUp);
1443
+ document.removeEventListener("touchmove", this._onEdgeTabTouchMove);
1444
+ document.removeEventListener("touchend", this._onEdgeTabTouchEnd);
1445
+ this._removeEdgeTab();
1446
+ this._hideResizeNub();
1447
+ if (this._resizeNubElement) {
1448
+ this._resizeNubElement.remove();
1449
+ this._resizeNubElement = null;
1450
+ }
1540
1451
  this._stopHealthWait();
1541
1452
  this._cancelRevealFade();
1542
1453
  this._teardownViewportObserver();
@@ -1565,48 +1476,6 @@ var CharAgentShellElement = class extends HTMLElement {
1565
1476
  });
1566
1477
  return;
1567
1478
  }
1568
- case "fullscreen-breakpoint": {
1569
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_FULLSCREEN_BREAKPOINT);
1570
- if (next !== this._fullscreenBreakpoint) {
1571
- this._fullscreenBreakpoint = next;
1572
- this._setupViewportObserver();
1573
- this._applyDisplayModeAndLayout();
1574
- }
1575
- return;
1576
- }
1577
- case "panel-width": {
1578
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PANEL_WIDTH);
1579
- if (next !== this._panelWidth) {
1580
- this._panelWidth = next;
1581
- this._applyDisplayModeAndLayout();
1582
- }
1583
- return;
1584
- }
1585
- case "pip-width": {
1586
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_WIDTH);
1587
- if (next !== this._pipWidth) {
1588
- this._pipWidth = next;
1589
- this._applyDisplayModeAndLayout();
1590
- }
1591
- return;
1592
- }
1593
- case "pip-height": {
1594
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_HEIGHT);
1595
- if (next !== this._pipHeight) {
1596
- this._pipHeight = next;
1597
- this._applyDisplayModeAndLayout();
1598
- }
1599
- return;
1600
- }
1601
- case "pip-position": {
1602
- const next = normalizePipPosition(newValue);
1603
- if (next !== this._pipPosition) {
1604
- this._pipPosition = next;
1605
- this._applyDisplayModeAndLayout();
1606
- }
1607
- return;
1608
- }
1609
- case "dev-mode":
1610
1479
  case "enable-debug-tools":
1611
1480
  case "api-base":
1612
1481
  this._syncForwardedAttributes();
@@ -1616,7 +1485,7 @@ var CharAgentShellElement = class extends HTMLElement {
1616
1485
  /**
1617
1486
  * Connects shell authentication and starts availability health checks.
1618
1487
  *
1619
- * @param options - Authentication payload for either ID token or ticket auth.
1488
+ * @param options - Authentication payload (`publishableKey` + optional `idToken`).
1620
1489
  * @returns `true` when connect is accepted by the inner element.
1621
1490
  */
1622
1491
  connect(options) {
@@ -1724,15 +1593,10 @@ var CharAgentShellElement = class extends HTMLElement {
1724
1593
  this._attachForwardedListeners();
1725
1594
  }
1726
1595
  _primeAgentBootConfig(agent) {
1727
- for (const name of [
1728
- "dev-mode",
1729
- "enable-debug-tools",
1730
- "api-base"
1731
- ]) {
1596
+ for (const name of ["enable-debug-tools", "api-base"]) {
1732
1597
  const value = this.getAttribute(name);
1733
1598
  if (value !== null) agent.setAttribute(name, value);
1734
1599
  }
1735
- agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1736
1600
  agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1737
1601
  }
1738
1602
  _attachForwardedListeners() {
@@ -1755,29 +1619,19 @@ var CharAgentShellElement = class extends HTMLElement {
1755
1619
  }
1756
1620
  _readConfigFromAttributes() {
1757
1621
  this._open = parseBooleanAttribute(this.getAttribute("open"));
1758
- this._fullscreenBreakpoint = parsePositiveIntegerAttribute(this.getAttribute("fullscreen-breakpoint"), DEFAULT_FULLSCREEN_BREAKPOINT);
1759
- this._panelWidth = parsePositiveIntegerAttribute(this.getAttribute("panel-width"), DEFAULT_PANEL_WIDTH);
1760
- this._pipWidth = parsePositiveIntegerAttribute(this.getAttribute("pip-width"), DEFAULT_PIP_WIDTH);
1761
- this._pipHeight = parsePositiveIntegerAttribute(this.getAttribute("pip-height"), DEFAULT_PIP_HEIGHT);
1762
- this._pipPosition = normalizePipPosition(this.getAttribute("pip-position"));
1763
1622
  }
1764
1623
  _syncForwardedAttributes() {
1765
1624
  if (!this._agent) return;
1766
- for (const name of [
1767
- "dev-mode",
1768
- "enable-debug-tools",
1769
- "api-base"
1770
- ]) {
1625
+ for (const name of ["enable-debug-tools", "api-base"]) {
1771
1626
  const value = this.getAttribute(name);
1772
1627
  if (value === null) this._agent.removeAttribute(name);
1773
1628
  else this._agent.setAttribute(name, value);
1774
1629
  }
1775
- this._agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1776
1630
  this._agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1777
1631
  }
1778
1632
  _setupViewportObserver() {
1779
1633
  if (typeof window === "undefined") return;
1780
- const query = `(max-width: ${this._fullscreenBreakpoint - 1}px)`;
1634
+ const query = `(max-width: ${FULLSCREEN_BREAKPOINT - 1}px)`;
1781
1635
  if (this._mediaQuery?.media === query) {
1782
1636
  this._isNarrowViewport = this._mediaQuery.matches;
1783
1637
  return;
@@ -1815,38 +1669,558 @@ var CharAgentShellElement = class extends HTMLElement {
1815
1669
  if (displayMode !== "pip") {
1816
1670
  if (this._pipMeasuredHeight !== null) this._pipMeasuredHeight = null;
1817
1671
  }
1672
+ const prevMode = this._lastAppliedDisplayMode;
1673
+ this._lastAppliedDisplayMode = displayMode;
1818
1674
  this._agent.setAttribute("display-mode", displayMode);
1819
1675
  this._applyHostLayout(displayMode);
1820
- this._applyAgentLayout(displayMode);
1676
+ this._applyAgentLayout(displayMode, prevMode);
1677
+ }
1678
+ _isParentFlex() {
1679
+ const parent = this.parentElement;
1680
+ if (!parent) return false;
1681
+ const display = getComputedStyle(parent).display;
1682
+ return display === "flex" || display === "inline-flex";
1683
+ }
1684
+ _isDarkMode() {
1685
+ const themeAttr = document.documentElement.getAttribute("data-theme");
1686
+ if (themeAttr === "dark") return true;
1687
+ if (themeAttr === "light") return false;
1688
+ return document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
1689
+ }
1690
+ /**
1691
+ * Read a `--char-color-*` variable from the shell element. If the host
1692
+ * page has set it (directly or via inheritance) use that value; otherwise
1693
+ * fall back to the char design-system default for the current light/dark mode.
1694
+ */
1695
+ _resolveColor(token) {
1696
+ const host = getComputedStyle(this).getPropertyValue({
1697
+ background: "--char-color-background",
1698
+ foreground: "--char-color-foreground",
1699
+ border: "--char-color-border",
1700
+ mutedForeground: "--char-color-muted-foreground",
1701
+ primary: "--char-color-primary"
1702
+ }[token]).trim();
1703
+ if (host) return host;
1704
+ return (this._isDarkMode() ? THEME_DARK : THEME_LIGHT)[token];
1705
+ }
1706
+ _panelBorder() {
1707
+ return `1px solid ${this._resolveColor("border")}`;
1708
+ }
1709
+ _setupThemeObserver() {
1710
+ const onThemeChange = () => {
1711
+ this._applyEdgeTabVisual();
1712
+ this._refreshResizeNubColors();
1713
+ if (this._open && !this._isNarrowViewport) {
1714
+ if (this._isParentFlex()) setStyleValue(this, "border-left", this._panelBorder());
1715
+ else if (this._agent) setStyleValue(this._agent, "border-left", this._panelBorder());
1716
+ }
1717
+ };
1718
+ this._themeObserver = new MutationObserver(onThemeChange);
1719
+ this._themeObserver.observe(document.documentElement, {
1720
+ attributes: true,
1721
+ attributeFilter: [
1722
+ "class",
1723
+ "style",
1724
+ "data-theme"
1725
+ ]
1726
+ });
1727
+ this._themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
1728
+ this._themeMediaHandler = onThemeChange;
1729
+ this._themeMediaQuery.addEventListener("change", this._themeMediaHandler);
1730
+ }
1731
+ _teardownThemeObserver() {
1732
+ if (this._themeObserver) {
1733
+ this._themeObserver.disconnect();
1734
+ this._themeObserver = null;
1735
+ }
1736
+ if (this._themeMediaQuery && this._themeMediaHandler) {
1737
+ this._themeMediaQuery.removeEventListener("change", this._themeMediaHandler);
1738
+ this._themeMediaQuery = null;
1739
+ this._themeMediaHandler = null;
1740
+ }
1821
1741
  }
1822
1742
  _applyHostLayout(displayMode) {
1743
+ const useFlexLayout = this._isParentFlex();
1823
1744
  if (displayMode === "inline" && !this._isNarrowViewport) {
1824
- setStyleValue(this, "position", "relative");
1825
- setStyleValue(this, "flex", `0 0 ${this._panelWidth}px`);
1826
- setStyleValue(this, "width", `${this._panelWidth}px`);
1827
- setStyleValue(this, "min-width", `${this._panelWidth}px`);
1828
- setStyleValue(this, "height", "100%");
1829
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1830
- setStyleValue(this, "border-left", PANEL_BORDER);
1831
- setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1745
+ if (useFlexLayout) {
1746
+ setStyleValue(this, "position", "relative");
1747
+ setStyleValue(this, "flex", `0 0 ${this._currentPanelWidth}px`);
1748
+ setStyleValue(this, "width", `${this._currentPanelWidth}px`);
1749
+ setStyleValue(this, "min-width", `${this._currentPanelWidth}px`);
1750
+ setStyleValue(this, "height", "100%");
1751
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1752
+ setStyleValue(this, "border-left", this._panelBorder());
1753
+ setStyleValue(this, "transition", this._resizeDragging ? "none" : SHELL_LAYOUT_TRANSITION);
1754
+ resetStyleValue(this, "overflow");
1755
+ resetStyleValue(this, "pointer-events");
1756
+ this._removeBodyMargin();
1757
+ } else {
1758
+ setStyleValue(this, "position", "fixed");
1759
+ setStyleValue(this, "width", "0px");
1760
+ setStyleValue(this, "height", "0px");
1761
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1762
+ resetStyleValue(this, "flex");
1763
+ resetStyleValue(this, "min-width");
1764
+ resetStyleValue(this, "border-left");
1765
+ resetStyleValue(this, "overflow");
1766
+ resetStyleValue(this, "pointer-events");
1767
+ this._applyBodyMargin(this._currentPanelWidth);
1768
+ }
1769
+ this._showResizeNub();
1770
+ this._showEdgeTab();
1771
+ if (useFlexLayout) this._syncEdgeTabWithPanelTransition();
1772
+ } else if (displayMode === "fullscreen") {
1773
+ if (useFlexLayout) {
1774
+ setStyleValue(this, "position", "relative");
1775
+ setStyleValue(this, "flex", "0 0 0px");
1776
+ setStyleValue(this, "width", "0px");
1777
+ setStyleValue(this, "min-width", "0px");
1778
+ setStyleValue(this, "height", "0px");
1779
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1780
+ resetStyleValue(this, "border-left");
1781
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1782
+ resetStyleValue(this, "overflow");
1783
+ resetStyleValue(this, "pointer-events");
1784
+ } else {
1785
+ setStyleValue(this, "position", "relative");
1786
+ resetStyleValue(this, "flex");
1787
+ resetStyleValue(this, "min-width");
1788
+ resetStyleValue(this, "border-left");
1789
+ resetStyleValue(this, "overflow");
1790
+ resetStyleValue(this, "pointer-events");
1791
+ }
1792
+ this._removeBodyMargin();
1793
+ this._hideResizeNub();
1794
+ this._hideEdgeTab();
1795
+ } else {
1796
+ if (useFlexLayout) {
1797
+ setStyleValue(this, "position", "relative");
1798
+ setStyleValue(this, "flex", "0 0 0px");
1799
+ setStyleValue(this, "width", "0px");
1800
+ setStyleValue(this, "min-width", "0px");
1801
+ setStyleValue(this, "height", "0px");
1802
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1803
+ resetStyleValue(this, "border-left");
1804
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1805
+ resetStyleValue(this, "overflow");
1806
+ resetStyleValue(this, "pointer-events");
1807
+ } else {
1808
+ setStyleValue(this, "position", "relative");
1809
+ resetStyleValue(this, "flex");
1810
+ resetStyleValue(this, "min-width");
1811
+ resetStyleValue(this, "border-left");
1812
+ resetStyleValue(this, "overflow");
1813
+ resetStyleValue(this, "pointer-events");
1814
+ }
1815
+ this._removeBodyMargin();
1816
+ this._hideResizeNub();
1817
+ this._showEdgeTab();
1818
+ }
1819
+ }
1820
+ _applyBodyMargin(width) {
1821
+ const body = document.body;
1822
+ if (this._savedBodyTransition === null) this._savedBodyTransition = body.style.transition || "";
1823
+ if (!this._resizeDragging) body.style.transition = BODY_MARGIN_TRANSITION;
1824
+ body.style.marginRight = `${width}px`;
1825
+ }
1826
+ _removeBodyMargin() {
1827
+ const body = document.body;
1828
+ body.style.marginRight = "";
1829
+ if (this._bodyMarginTimeoutId !== null) {
1830
+ window.clearTimeout(this._bodyMarginTimeoutId);
1831
+ this._bodyMarginTimeoutId = null;
1832
+ }
1833
+ if (this._savedBodyTransition !== null) {
1834
+ const saved = this._savedBodyTransition;
1835
+ this._bodyMarginTimeoutId = window.setTimeout(() => {
1836
+ this._bodyMarginTimeoutId = null;
1837
+ if (body.style.transition === BODY_MARGIN_TRANSITION) body.style.transition = saved || "";
1838
+ }, 220);
1839
+ this._savedBodyTransition = null;
1840
+ }
1841
+ }
1842
+ _ensureResizeNub() {
1843
+ if (this._resizeNubElement) return this._resizeNubElement;
1844
+ const nub = document.createElement("div");
1845
+ nub.setAttribute("role", "separator");
1846
+ nub.setAttribute("aria-orientation", "vertical");
1847
+ nub.setAttribute("aria-label", "Resize panel");
1848
+ nub.style.cssText = [
1849
+ "position:fixed",
1850
+ "top:0",
1851
+ `width:${RESIZE_NUB_WIDTH}px`,
1852
+ "height:100dvh",
1853
+ "cursor:col-resize",
1854
+ `z-index:${SHELL_Z_INDEX + 1}`,
1855
+ "background:transparent",
1856
+ "user-select:none",
1857
+ "-webkit-user-select:none",
1858
+ "touch-action:none"
1859
+ ].join(";");
1860
+ const indicator = document.createElement("div");
1861
+ indicator.style.cssText = [
1862
+ "position:absolute",
1863
+ "top:0",
1864
+ "left:50%",
1865
+ "transform:translateX(-50%)",
1866
+ "width:2px",
1867
+ "height:100%",
1868
+ "border-radius:1px",
1869
+ "transition:background 150ms ease, width 150ms ease",
1870
+ "pointer-events:none"
1871
+ ].join(";");
1872
+ nub.appendChild(indicator);
1873
+ this._applyResizeNubColors(indicator);
1874
+ nub.addEventListener("mouseenter", () => {
1875
+ const dark = this._isDarkMode();
1876
+ indicator.style.background = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.15)";
1877
+ indicator.style.width = "3px";
1878
+ });
1879
+ nub.addEventListener("mouseleave", () => {
1880
+ if (!this._resizeDragging) {
1881
+ this._applyResizeNubColors(indicator);
1882
+ indicator.style.width = "2px";
1883
+ }
1884
+ });
1885
+ nub.addEventListener("mousedown", this._onResizeStart);
1886
+ nub.addEventListener("touchstart", this._onResizeTouchStart, { passive: false });
1887
+ this._resizeNubElement = nub;
1888
+ return nub;
1889
+ }
1890
+ _applyResizeNubColors(indicator) {
1891
+ const el = indicator ?? this._resizeNubElement?.querySelector("div");
1892
+ if (!el) return;
1893
+ const dark = this._isDarkMode();
1894
+ el.style.background = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)";
1895
+ }
1896
+ _refreshResizeNubColors() {
1897
+ this._applyResizeNubColors();
1898
+ }
1899
+ _showResizeNub() {
1900
+ const nub = this._ensureResizeNub();
1901
+ if (!nub.parentNode) document.body.appendChild(nub);
1902
+ setStyleValue(nub, "right", `${this._currentPanelWidth - Math.floor(RESIZE_NUB_WIDTH / 2)}px`);
1903
+ setStyleValue(nub, "display", "block");
1904
+ }
1905
+ _hideResizeNub() {
1906
+ if (this._resizeNubElement) setStyleValue(this._resizeNubElement, "display", "none");
1907
+ }
1908
+ _ensureEdgeTab() {
1909
+ if (this._edgeTabElement) return this._edgeTabElement;
1910
+ const tab = document.createElement("button");
1911
+ tab.setAttribute("type", "button");
1912
+ tab.setAttribute("aria-label", "Open assistant");
1913
+ tab.style.cssText = [
1914
+ "position:fixed",
1915
+ "top:50%",
1916
+ "right:0px",
1917
+ "transform:translateY(-50%)",
1918
+ `z-index:${SHELL_Z_INDEX + 2}`,
1919
+ `width:${EDGE_TAB_WIDTH}px`,
1920
+ `height:${EDGE_TAB_HEIGHT}px`,
1921
+ "display:flex",
1922
+ "align-items:center",
1923
+ "justify-content:center",
1924
+ "border-right:none",
1925
+ "border-radius:12px 0 0 12px",
1926
+ "padding:0",
1927
+ "outline:none",
1928
+ "transition:transform 200ms ease,background 150ms ease,color 150ms ease,box-shadow 200ms ease,border-color 150ms ease",
1929
+ "user-select:none",
1930
+ "-webkit-user-select:none"
1931
+ ].join(";");
1932
+ const dot = document.createElement("span");
1933
+ dot.style.cssText = [
1934
+ "position:absolute",
1935
+ "top:8px",
1936
+ "right:10px",
1937
+ "width:7px",
1938
+ "height:7px",
1939
+ "border-radius:50%",
1940
+ "transition:opacity 200ms ease"
1941
+ ].join(";");
1942
+ tab.appendChild(dot);
1943
+ const icon = document.createElement("span");
1944
+ icon.style.cssText = "display:flex;align-items:center;justify-content:center;pointer-events:none;";
1945
+ icon.innerHTML = ICON_MESSAGE;
1946
+ tab.appendChild(icon);
1947
+ tab.addEventListener("mouseenter", () => {
1948
+ const dark = this._isDarkMode();
1949
+ if (this._open && !this._isNarrowViewport) {
1950
+ tab.style.background = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
1951
+ tab.style.boxShadow = "none";
1952
+ tab.style.color = this._resolveColor("foreground");
1953
+ tab.style.cursor = "col-resize";
1954
+ } else {
1955
+ tab.style.transform = "translateY(-50%) translateX(-3px)";
1956
+ tab.style.boxShadow = dark ? "-4px 0 16px rgba(0,0,0,0.3)" : "-4px 0 16px rgba(0,0,0,0.12)";
1957
+ tab.style.color = this._resolveColor("foreground");
1958
+ tab.style.cursor = "pointer";
1959
+ }
1960
+ });
1961
+ tab.addEventListener("mouseleave", () => {
1962
+ this._applyEdgeTabVisual();
1963
+ });
1964
+ tab.addEventListener("mousedown", this._onEdgeTabMouseDown);
1965
+ tab.addEventListener("touchstart", this._onEdgeTabTouchStart, { passive: false });
1966
+ this._edgeTabElement = tab;
1967
+ return tab;
1968
+ }
1969
+ /** Apply visual style (colors, shadow) based on open state — no position change. */
1970
+ _applyEdgeTabVisual() {
1971
+ if (!this._edgeTabElement) return;
1972
+ const tab = this._edgeTabElement;
1973
+ const icon = tab.querySelector("span:last-child");
1974
+ const dot = tab.querySelector("span:first-child");
1975
+ const dark = this._isDarkMode();
1976
+ const bg = this._resolveColor("background");
1977
+ const borderColor = this._resolveColor("border");
1978
+ const mutedFg = this._resolveColor("mutedForeground");
1979
+ const primary = this._resolveColor("primary");
1980
+ tab.style.border = `1px solid ${borderColor}`;
1981
+ tab.style.borderRight = "none";
1982
+ if (this._open && !this._isNarrowViewport) {
1983
+ tab.setAttribute("aria-label", "Close assistant");
1984
+ tab.setAttribute("aria-expanded", "true");
1985
+ if (icon) icon.innerHTML = ICON_CHEVRON_RIGHT;
1986
+ if (dot) dot.style.opacity = "0";
1987
+ tab.style.background = bg;
1988
+ tab.style.boxShadow = "none";
1989
+ tab.style.backdropFilter = "none";
1990
+ tab.style.setProperty("-webkit-backdrop-filter", "none");
1991
+ tab.style.color = mutedFg;
1992
+ tab.style.cursor = "col-resize";
1993
+ tab.style.transform = "translateY(-50%)";
1994
+ } else {
1995
+ tab.setAttribute("aria-label", "Open assistant");
1996
+ tab.setAttribute("aria-expanded", "false");
1997
+ if (icon) icon.innerHTML = ICON_MESSAGE;
1998
+ if (dot) {
1999
+ dot.style.opacity = "1";
2000
+ dot.style.background = primary;
2001
+ }
2002
+ tab.style.background = dark ? "color-mix(in oklch, " + bg + ", white 4%)" : "color-mix(in oklch, " + bg + ", transparent 5%)";
2003
+ tab.style.boxShadow = dark ? "-2px 0 12px rgba(0,0,0,0.25)" : "-2px 0 12px rgba(0,0,0,0.08)";
2004
+ tab.style.backdropFilter = "blur(8px)";
2005
+ tab.style.setProperty("-webkit-backdrop-filter", "blur(8px)");
2006
+ tab.style.color = mutedFg;
2007
+ tab.style.cursor = "pointer";
2008
+ tab.style.transform = "translateY(-50%)";
2009
+ }
2010
+ }
2011
+ /** Set the tab's `right` position immediately (no transition). */
2012
+ _positionEdgeTab(rightPx) {
2013
+ if (this._edgeTabElement) this._edgeTabElement.style.right = `${rightPx}px`;
2014
+ }
2015
+ _showEdgeTab() {
2016
+ const tab = this._ensureEdgeTab();
2017
+ if (!tab.parentNode) document.body.appendChild(tab);
2018
+ setStyleValue(tab, "display", "flex");
2019
+ if (this._open && !this._isNarrowViewport) this._positionEdgeTab(this._currentPanelWidth - 1);
2020
+ else this._positionEdgeTab(0);
2021
+ this._applyEdgeTabVisual();
2022
+ }
2023
+ _hideEdgeTab() {
2024
+ if (this._edgeTabElement) setStyleValue(this._edgeTabElement, "display", "none");
2025
+ }
2026
+ _removeEdgeTab() {
2027
+ if (this._edgeTabElement) {
2028
+ this._edgeTabElement.remove();
2029
+ this._edgeTabElement = null;
2030
+ }
2031
+ }
2032
+ _onEdgeTabMouseDown = (e) => {
2033
+ e.preventDefault();
2034
+ this._edgeTabDragStartX = e.clientX;
2035
+ this._edgeTabDragging = false;
2036
+ document.addEventListener("mousemove", this._onEdgeTabMouseMove);
2037
+ document.addEventListener("mouseup", this._onEdgeTabMouseUp);
2038
+ };
2039
+ _onEdgeTabTouchStart = (e) => {
2040
+ if (e.touches.length !== 1) return;
2041
+ e.preventDefault();
2042
+ this._edgeTabDragStartX = e.touches[0].clientX;
2043
+ this._edgeTabDragging = false;
2044
+ document.addEventListener("touchmove", this._onEdgeTabTouchMove, { passive: false });
2045
+ document.addEventListener("touchend", this._onEdgeTabTouchEnd);
2046
+ };
2047
+ _onEdgeTabMouseMove = (e) => {
2048
+ const dx = Math.abs(e.clientX - this._edgeTabDragStartX);
2049
+ if (!this._edgeTabDragging && dx >= EDGE_TAB_DRAG_THRESHOLD_PX) {
2050
+ this._edgeTabDragging = true;
2051
+ this._promoteEdgeTabDragToResize(this._edgeTabDragStartX);
2052
+ }
2053
+ };
2054
+ _onEdgeTabTouchMove = (e) => {
2055
+ if (e.touches.length !== 1) return;
2056
+ e.preventDefault();
2057
+ const dx = Math.abs(e.touches[0].clientX - this._edgeTabDragStartX);
2058
+ if (!this._edgeTabDragging && dx >= EDGE_TAB_DRAG_THRESHOLD_PX) {
2059
+ this._edgeTabDragging = true;
2060
+ this._promoteEdgeTabDragToResize(this._edgeTabDragStartX);
2061
+ }
2062
+ };
2063
+ _onEdgeTabMouseUp = (_e) => {
2064
+ document.removeEventListener("mousemove", this._onEdgeTabMouseMove);
2065
+ document.removeEventListener("mouseup", this._onEdgeTabMouseUp);
2066
+ if (!this._edgeTabDragging) this.toggleOpen();
2067
+ };
2068
+ _onEdgeTabTouchEnd = (_e) => {
2069
+ document.removeEventListener("touchmove", this._onEdgeTabTouchMove);
2070
+ document.removeEventListener("touchend", this._onEdgeTabTouchEnd);
2071
+ if (!this._edgeTabDragging) this.toggleOpen();
2072
+ };
2073
+ /**
2074
+ * Once the edge tab drag crosses the threshold, hand off to the existing
2075
+ * resize machinery so the panel tracks the pointer smoothly.
2076
+ */
2077
+ _promoteEdgeTabDragToResize(startX) {
2078
+ if (!this._open || this._isNarrowViewport) {
2079
+ this._edgeTabDragging = false;
1832
2080
  return;
1833
2081
  }
1834
- setStyleValue(this, "position", "relative");
1835
- setStyleValue(this, "flex", "0 0 0px");
1836
- setStyleValue(this, "width", "0px");
1837
- setStyleValue(this, "min-width", "0px");
1838
- setStyleValue(this, "height", "0px");
1839
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1840
- resetStyleValue(this, "border-left");
1841
- setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
2082
+ this._resizeDragging = true;
2083
+ this._resizeStartX = startX;
2084
+ this._resizeStartWidth = this._currentPanelWidth;
2085
+ this._showResizeDragOverlay();
2086
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2087
+ setStyleValue(this, "transition", "none");
2088
+ document.body.style.transition = "none";
2089
+ document.body.style.cursor = "col-resize";
2090
+ document.body.style.userSelect = "none";
2091
+ document.addEventListener("mousemove", this._onResizeMove);
2092
+ document.addEventListener("mouseup", this._onResizeEnd);
2093
+ document.addEventListener("touchmove", this._onResizeTouchMove, { passive: false });
2094
+ document.addEventListener("touchend", this._onResizeTouchEnd);
2095
+ }
2096
+ /**
2097
+ * Run a rAF loop that reads the shell element's actual rendered width
2098
+ * and pins the edge tab to it. Used during flex layout transitions so
2099
+ * the tab tracks the animated flex-basis exactly, frame by frame.
2100
+ */
2101
+ _syncEdgeTabWithPanelTransition() {
2102
+ if (!this._edgeTabElement) return;
2103
+ const start = performance.now();
2104
+ const maxDuration = 300;
2105
+ const tick = () => {
2106
+ if (!this._edgeTabElement || !this._open) return;
2107
+ if (this._isParentFlex()) {
2108
+ const width = this.getBoundingClientRect().width;
2109
+ this._positionEdgeTab(width - 1);
2110
+ }
2111
+ if (performance.now() - start < maxDuration) requestAnimationFrame(tick);
2112
+ };
2113
+ requestAnimationFrame(tick);
2114
+ }
2115
+ _showResizeDragOverlay() {
2116
+ if (this._resizeDragOverlay) return;
2117
+ const overlay = document.createElement("div");
2118
+ overlay.style.cssText = [
2119
+ "position:fixed",
2120
+ "top:0",
2121
+ "left:0",
2122
+ "width:100vw",
2123
+ "height:100vh",
2124
+ "cursor:col-resize",
2125
+ `z-index:${SHELL_Z_INDEX + 1}`,
2126
+ "background:transparent",
2127
+ "user-select:none",
2128
+ "-webkit-user-select:none"
2129
+ ].join(";");
2130
+ document.body.appendChild(overlay);
2131
+ this._resizeDragOverlay = overlay;
2132
+ }
2133
+ _removeResizeDragOverlay() {
2134
+ if (this._resizeDragOverlay) {
2135
+ this._resizeDragOverlay.remove();
2136
+ this._resizeDragOverlay = null;
2137
+ }
2138
+ }
2139
+ _onResizeStart = (e) => {
2140
+ e.preventDefault();
2141
+ this._resizeDragging = true;
2142
+ this._resizeStartX = e.clientX;
2143
+ this._resizeStartWidth = this._currentPanelWidth;
2144
+ this._showResizeDragOverlay();
2145
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2146
+ setStyleValue(this, "transition", "none");
2147
+ document.body.style.transition = "none";
2148
+ document.body.style.cursor = "col-resize";
2149
+ document.body.style.userSelect = "none";
2150
+ document.addEventListener("mousemove", this._onResizeMove);
2151
+ document.addEventListener("mouseup", this._onResizeEnd);
2152
+ };
2153
+ _onResizeTouchStart = (e) => {
2154
+ if (e.touches.length !== 1) return;
2155
+ e.preventDefault();
2156
+ const touch = e.touches[0];
2157
+ this._resizeDragging = true;
2158
+ this._resizeStartX = touch.clientX;
2159
+ this._resizeStartWidth = this._currentPanelWidth;
2160
+ this._showResizeDragOverlay();
2161
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2162
+ setStyleValue(this, "transition", "none");
2163
+ document.body.style.transition = "none";
2164
+ document.addEventListener("touchmove", this._onResizeTouchMove, { passive: false });
2165
+ document.addEventListener("touchend", this._onResizeTouchEnd);
2166
+ };
2167
+ _onResizeMove = (e) => {
2168
+ if (!this._resizeDragging) return;
2169
+ const delta = this._resizeStartX - e.clientX;
2170
+ this._applyResizeWidth(this._resizeStartWidth + delta);
2171
+ };
2172
+ _onResizeTouchMove = (e) => {
2173
+ if (!this._resizeDragging || e.touches.length !== 1) return;
2174
+ e.preventDefault();
2175
+ const delta = this._resizeStartX - e.touches[0].clientX;
2176
+ this._applyResizeWidth(this._resizeStartWidth + delta);
2177
+ };
2178
+ _applyResizeWidth(rawWidth) {
2179
+ const width = Math.max(PANEL_MIN_WIDTH, Math.min(rawWidth, PANEL_MAX_WIDTH));
2180
+ this._currentPanelWidth = width;
2181
+ if (this._isParentFlex()) {
2182
+ setStyleValue(this, "flex", `0 0 ${width}px`);
2183
+ setStyleValue(this, "width", `${width}px`);
2184
+ setStyleValue(this, "min-width", `${width}px`);
2185
+ } else {
2186
+ if (this._agent) setStyleValue(this._agent, "width", `${width}px`);
2187
+ document.body.style.marginRight = `${width}px`;
2188
+ }
2189
+ if (this._resizeNubElement) {
2190
+ const rightPos = width - Math.floor(RESIZE_NUB_WIDTH / 2);
2191
+ setStyleValue(this._resizeNubElement, "right", `${rightPos}px`);
2192
+ }
2193
+ if (this._edgeTabElement) this._edgeTabElement.style.right = `${width - 1}px`;
2194
+ }
2195
+ _onResizeEnd = (e) => {
2196
+ this._finishResize(e.clientX);
2197
+ document.removeEventListener("mousemove", this._onResizeMove);
2198
+ document.removeEventListener("mouseup", this._onResizeEnd);
2199
+ };
2200
+ _onResizeTouchEnd = (e) => {
2201
+ const x = e.changedTouches[0]?.clientX ?? this._resizeStartX;
2202
+ this._finishResize(x);
2203
+ document.removeEventListener("touchmove", this._onResizeTouchMove);
2204
+ document.removeEventListener("touchend", this._onResizeTouchEnd);
2205
+ };
2206
+ _finishResize(endX) {
2207
+ this._resizeDragging = false;
2208
+ this._removeResizeDragOverlay();
2209
+ document.body.style.cursor = "";
2210
+ document.body.style.userSelect = "";
2211
+ document.body.style.transition = "";
2212
+ const delta = this._resizeStartX - endX;
2213
+ const finalWidth = this._resizeStartWidth + delta;
2214
+ this._currentPanelWidth = Math.max(PANEL_MIN_WIDTH, Math.min(finalWidth, PANEL_MAX_WIDTH));
2215
+ this._applyDisplayModeAndLayout();
1842
2216
  }
1843
2217
  _composePipTransform(opts) {
1844
2218
  const parts = [];
1845
- if (this._pipPosition === "bottom-center") parts.push("translateX(-50%)");
2219
+ if (PIP_POSITION === "bottom-center") parts.push("translateX(-50%)");
1846
2220
  if (opts.scrollHide) parts.push(`translateY(${PIP_SCROLL_HIDE_OFFSET}px)`);
1847
2221
  return parts.length > 0 ? parts.join(" ") : "none";
1848
2222
  }
1849
- _applyAgentLayout(displayMode) {
2223
+ _applyAgentLayout(displayMode, prevMode) {
1850
2224
  if (!this._agent) return;
1851
2225
  if (displayMode === "inline" && !this._isNarrowViewport) {
1852
2226
  this._detachPipInteractionListeners();
@@ -1854,20 +2228,32 @@ var CharAgentShellElement = class extends HTMLElement {
1854
2228
  this._pipExpanded = false;
1855
2229
  this._pipAnimating = false;
1856
2230
  this._hidePipPill();
1857
- setStyleValue(this._agent, "position", "relative");
1858
- resetStyleValue(this._agent, "top");
1859
- resetStyleValue(this._agent, "right");
1860
- resetStyleValue(this._agent, "bottom");
1861
- resetStyleValue(this._agent, "left");
2231
+ if (this._isParentFlex()) {
2232
+ setStyleValue(this._agent, "position", "relative");
2233
+ resetStyleValue(this._agent, "top");
2234
+ resetStyleValue(this._agent, "right");
2235
+ resetStyleValue(this._agent, "bottom");
2236
+ resetStyleValue(this._agent, "left");
2237
+ setStyleValue(this._agent, "z-index", "auto");
2238
+ setStyleValue(this._agent, "width", "100%");
2239
+ setStyleValue(this._agent, "height", "100%");
2240
+ } else {
2241
+ setStyleValue(this._agent, "position", "fixed");
2242
+ setStyleValue(this._agent, "top", "0");
2243
+ setStyleValue(this._agent, "right", "0");
2244
+ resetStyleValue(this._agent, "bottom");
2245
+ resetStyleValue(this._agent, "left");
2246
+ setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
2247
+ setStyleValue(this._agent, "width", `${this._currentPanelWidth}px`);
2248
+ setStyleValue(this._agent, "height", "100dvh");
2249
+ setStyleValue(this._agent, "border-left", this._panelBorder());
2250
+ }
1862
2251
  resetStyleValue(this._agent, "transform");
1863
2252
  resetStyleValue(this._agent, "max-width");
1864
2253
  resetStyleValue(this._agent, "max-height");
1865
- setStyleValue(this._agent, "z-index", "auto");
1866
- setStyleValue(this._agent, "width", "100%");
1867
- setStyleValue(this._agent, "height", "100%");
1868
2254
  setStyleValue(this._agent, "border-radius", "0");
1869
2255
  setStyleValue(this._agent, "overflow", "hidden");
1870
- resetStyleValue(this._agent, "transition");
2256
+ setStyleValue(this._agent, "transition", this._resizeDragging ? "none" : "width 220ms ease");
1871
2257
  resetStyleValue(this._agent, "opacity");
1872
2258
  resetStyleValue(this._agent, "pointer-events");
1873
2259
  return;
@@ -1896,15 +2282,14 @@ var CharAgentShellElement = class extends HTMLElement {
1896
2282
  resetStyleValue(this._agent, "pointer-events");
1897
2283
  return;
1898
2284
  }
1899
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1900
- const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, this._pipWidth);
1901
- const collapsedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, Math.round(this._pipWidth * PIP_COLLAPSED_WIDTH_RATIO));
2285
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
2286
+ const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, PIP_WIDTH);
2287
+ const collapsedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, Math.round(PIP_WIDTH * PIP_COLLAPSED_WIDTH_RATIO));
1902
2288
  const targetWidth = this._pipExpanded ? expandedWidth : collapsedWidth;
1903
- const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, this._pipHeight);
1904
- const hasExplicitPipHeight = this.hasAttribute("pip-height");
2289
+ const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, PIP_HEIGHT);
1905
2290
  const measuredHeightFloor = typeof this._pipMeasuredHeight === "number" ? this._pipMeasuredHeight : 0;
1906
2291
  const expandedHeight = Math.max(collapsedHeight, measuredHeightFloor);
1907
- const collapsedPipHeight = hasExplicitPipHeight ? collapsedHeight : PIP_PILL_HEIGHT;
2292
+ const collapsedPipHeight = PIP_PILL_HEIGHT;
1908
2293
  const heightDelta = expandedHeight - collapsedPipHeight;
1909
2294
  let targetHeight;
1910
2295
  let targetBottom;
@@ -1915,13 +2300,14 @@ var CharAgentShellElement = class extends HTMLElement {
1915
2300
  targetHeight = collapsedPipHeight;
1916
2301
  targetBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1917
2302
  }
2303
+ const enteringFromPanel = prevMode != null && prevMode !== "pip" && this._healthState === "healthy";
1918
2304
  const composedTransform = this._composePipTransform({ scrollHide: this._pipHiddenByScroll });
2305
+ if (enteringFromPanel) setStyleValue(this._agent, "transition", "none");
1919
2306
  setStyleValue(this._agent, "position", "fixed");
1920
2307
  resetStyleValue(this._agent, "top");
1921
2308
  setStyleValue(this._agent, "right", pipPositionStyles.right);
1922
2309
  setStyleValue(this._agent, "left", pipPositionStyles.left);
1923
2310
  setStyleValue(this._agent, "bottom", targetBottom);
1924
- setStyleValue(this._agent, "transform", composedTransform);
1925
2311
  setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1926
2312
  setStyleValue(this._agent, "width", `min(${targetWidth}px, calc(100vw - 24px))`);
1927
2313
  setStyleValue(this._agent, "height", `${targetHeight}px`);
@@ -1929,7 +2315,22 @@ var CharAgentShellElement = class extends HTMLElement {
1929
2315
  setStyleValue(this._agent, "max-width", "100vw");
1930
2316
  setStyleValue(this._agent, "border-radius", "16px");
1931
2317
  setStyleValue(this._agent, "overflow", "hidden");
1932
- setStyleValue(this._agent, "transition", this._healthState === "healthy" ? PIP_MORPH_TRANSITION : "none");
2318
+ resetStyleValue(this._agent, "border-left");
2319
+ if (enteringFromPanel) {
2320
+ setStyleValue(this._agent, "transform", this._composePipTransform({ scrollHide: true }));
2321
+ setStyleValue(this._agent, "opacity", "0");
2322
+ this._agent.offsetHeight;
2323
+ requestAnimationFrame(() => {
2324
+ if (!this._agent) return;
2325
+ setStyleValue(this._agent, "transition", PIP_MORPH_TRANSITION);
2326
+ setStyleValue(this._agent, "transform", composedTransform);
2327
+ setStyleValue(this._agent, "opacity", "1");
2328
+ });
2329
+ } else {
2330
+ setStyleValue(this._agent, "transform", composedTransform);
2331
+ setStyleValue(this._agent, "transition", this._healthState === "healthy" ? PIP_MORPH_TRANSITION : "none");
2332
+ resetStyleValue(this._agent, "opacity");
2333
+ }
1933
2334
  if (this._healthState === "healthy") {
1934
2335
  const pill = this._ensurePipPill();
1935
2336
  if (!pill.parentNode) this.appendChild(pill);
@@ -1938,6 +2339,16 @@ var CharAgentShellElement = class extends HTMLElement {
1938
2339
  this._attachScrollHideListener();
1939
2340
  this._attachSwipeGestureListeners();
1940
2341
  this._attachWheelFallbackListener();
2342
+ if (enteringFromPanel) {
2343
+ pill.style.opacity = "0";
2344
+ pill.style.pointerEvents = "none";
2345
+ pill.style.transition = "none";
2346
+ requestAnimationFrame(() => {
2347
+ pill.style.transition = PIP_MORPH_TRANSITION;
2348
+ pill.style.opacity = "1";
2349
+ pill.style.pointerEvents = "";
2350
+ });
2351
+ }
1941
2352
  this._applyPipVisualState();
1942
2353
  }
1943
2354
  }
@@ -1947,7 +2358,7 @@ var CharAgentShellElement = class extends HTMLElement {
1947
2358
  pill.setAttribute("role", "button");
1948
2359
  pill.setAttribute("aria-label", "Open assistant");
1949
2360
  pill.setAttribute("tabindex", "0");
1950
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
2361
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
1951
2362
  pill.style.cssText = [
1952
2363
  "position:fixed",
1953
2364
  `left:${pipPositionStyles.left}`,
@@ -2333,6 +2744,7 @@ var CharAgentShellElement = class extends HTMLElement {
2333
2744
  this._pipExpanded = false;
2334
2745
  this._pipAnimating = false;
2335
2746
  this._hidePipPill();
2747
+ this._hideEdgeTab();
2336
2748
  if (this._agent) this._agent.removeAttribute("display-mode");
2337
2749
  this._resetHostLayoutStyles();
2338
2750
  this._resetAgentLayoutStyles();
@@ -2378,7 +2790,7 @@ var CharAgentShellElement = class extends HTMLElement {
2378
2790
  this._revealFrameId = null;
2379
2791
  setStyleValue(this, "opacity", "1");
2380
2792
  window.setTimeout(() => {
2381
- if (this._healthState === "healthy") resetStyleValue(this, "will-change");
2793
+ resetStyleValue(this, "will-change");
2382
2794
  }, SHELL_REVEAL_FADE_MS);
2383
2795
  });
2384
2796
  }
@@ -2430,6 +2842,7 @@ var CharAgentShellElement = class extends HTMLElement {
2430
2842
  return;
2431
2843
  }
2432
2844
  this._open = nextOpen;
2845
+ if (!nextOpen) this._currentPanelWidth = PANEL_WIDTH;
2433
2846
  if (options.reflect) this._reflectOpenAttribute(nextOpen);
2434
2847
  this._applyDisplayModeAndLayout();
2435
2848
  if (options.emit) this.dispatchEvent(new CustomEvent(SHELL_OPEN_CHANGE_EVENT, {