@mcp-b/char 0.1.3 → 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,22 +1075,15 @@ var CharAgentElement = class extends HTMLElement {
1102
1075
  */
1103
1076
  _postAuth(iframeOrigin) {
1104
1077
  if (!this._pendingAuth || !this._iframe?.contentWindow) return;
1105
- if (!this._pendingAuth.ticketAuth && !this._pendingAuth.anthropicApiKey && !this._pendingAuth.idToken) {
1106
- console.error("[Char] _postAuth called but _pendingAuth has no auth credentials");
1078
+ if (!this._pendingAuth.publishableKey) {
1079
+ console.error("[Char] _postAuth called but _pendingAuth has no publishableKey");
1107
1080
  this._pendingAuth = null;
1108
1081
  return;
1109
1082
  }
1110
- const message = this._pendingAuth.ticketAuth ? {
1111
- type: "char-auth",
1112
- ticketAuth: this._pendingAuth.ticketAuth
1113
- } : this._pendingAuth.anthropicApiKey ? {
1083
+ const message = {
1114
1084
  type: "char-auth",
1115
- anthropicApiKey: this._pendingAuth.anthropicApiKey
1116
- } : {
1117
- type: "char-auth",
1118
- idToken: this._pendingAuth.idToken,
1119
- clientId: this._pendingAuth.clientId,
1120
- organizationId: this._pendingAuth.organizationId
1085
+ publishableKey: this._pendingAuth.publishableKey,
1086
+ idToken: this._pendingAuth.idToken
1121
1087
  };
1122
1088
  this._iframe.contentWindow.postMessage(message, iframeOrigin);
1123
1089
  }
@@ -1264,36 +1230,6 @@ var CharAgentElement = class extends HTMLElement {
1264
1230
  this._containerResizeObserver.observe(this);
1265
1231
  }
1266
1232
  /**
1267
- * Resolve devMode from property (React 19) or attribute.
1268
- *
1269
- * @returns Parsed dev-mode configuration when valid.
1270
- */
1271
- _resolveDevMode() {
1272
- if (this.devMode && typeof this.devMode === "object") return this.devMode;
1273
- const devModeAttr = this.getAttribute("dev-mode");
1274
- if (!devModeAttr) return void 0;
1275
- try {
1276
- const parsed = JSON.parse(devModeAttr);
1277
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1278
- const msg = `dev-mode attribute must be a JSON object, got: ${Array.isArray(parsed) ? "array" : typeof parsed}`;
1279
- console.warn(`[Char] ${msg}`);
1280
- this._emitCharError("INVALID_DEV_MODE", msg);
1281
- return;
1282
- }
1283
- const obj = parsed;
1284
- return {
1285
- anthropicApiKey: typeof obj.anthropicApiKey === "string" ? obj.anthropicApiKey : void 0,
1286
- openaiApiKey: typeof obj.openaiApiKey === "string" ? obj.openaiApiKey : void 0,
1287
- useLocalApi: typeof obj.useLocalApi === "boolean" ? obj.useLocalApi : void 0
1288
- };
1289
- } catch (e) {
1290
- const msg = `Failed to parse dev-mode attribute as JSON: ${e instanceof Error ? e.message : String(e)}`;
1291
- console.warn(`[Char] ${msg}`);
1292
- this._emitCharError("INVALID_DEV_MODE", msg);
1293
- return;
1294
- }
1295
- }
1296
- /**
1297
1233
  * Resolve apiBase override from property (React 19) or attribute.
1298
1234
  *
1299
1235
  * @returns Sanitized API base URL without trailing slash.
@@ -1322,22 +1258,36 @@ registerChar();
1322
1258
  //#region src/shell-component.ts
1323
1259
  const SHELL_OPEN_CHANGE_EVENT = "char-shell-open-change";
1324
1260
  const SHELL_Z_INDEX = 2147483e3;
1325
- const DEFAULT_FULLSCREEN_BREAKPOINT = 1024;
1326
- const DEFAULT_PANEL_WIDTH = 420;
1327
- const DEFAULT_PIP_WIDTH = 300;
1328
- const DEFAULT_PIP_HEIGHT = 96;
1329
- 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";
1330
1266
  const SHELL_HEALTH_TIMEOUT_MS = 1e4;
1331
- const PANEL_BORDER = "1px solid rgba(15, 23, 42, 0.12)";
1332
1267
  const SAFE_BOTTOM = "calc(16px + env(safe-area-inset-bottom, 0px))";
1333
- 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";
1334
1284
  const SHELL_REVEAL_FADE_MS = 280;
1335
1285
  const PIP_MIN_COLLAPSED_WIDTH = 70;
1336
1286
  const PIP_MIN_COLLAPSED_HEIGHT = 30;
1337
1287
  const PIP_MAX_MEASURED_HEIGHT = 320;
1338
1288
  const PIP_COLLAPSE_DELAY_MS = 400;
1339
1289
  const PIP_PILL_HEIGHT = 40;
1340
- 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";
1341
1291
  const PIP_MORPH_DURATION_MS = 280;
1342
1292
  const PIP_SCROLL_HIDE_OFFSET = 100;
1343
1293
  const PIP_COLLAPSED_WIDTH_RATIO = .75;
@@ -1345,23 +1295,14 @@ const PIP_SWIPE_THRESHOLD = 50;
1345
1295
  const PIP_SCROLL_DIRECTION_THRESHOLD = 40;
1346
1296
  const WHEEL_DELTA_LINE_MULTIPLIER = 15;
1347
1297
  const WHEEL_DELTA_PAGE_MULTIPLIER = 300;
1348
- function normalizePositiveInteger(value, fallback) {
1349
- if (!Number.isFinite(value) || value <= 0) {
1350
- console.warn(`[Char] Invalid value ${value}, using fallback ${fallback}`);
1351
- return fallback;
1352
- }
1353
- return Math.round(value);
1354
- }
1355
- function parsePositiveIntegerAttribute(value, fallback) {
1356
- if (value === null || value.trim() === "") return fallback;
1357
- const parsed = Number.parseInt(value, 10);
1358
- if (!Number.isFinite(parsed)) return fallback;
1359
- return normalizePositiveInteger(parsed, fallback);
1360
- }
1361
- function normalizePipPosition(value) {
1362
- if (value === "bottom-right" || value === "bottom-left") return value;
1363
- return DEFAULT_PIP_POSITION;
1364
- }
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>`;
1365
1306
  function getPipPositionStyles(position) {
1366
1307
  if (position === "bottom-right") return {
1367
1308
  left: "auto",
@@ -1418,25 +1359,28 @@ function sanitizeShellHostContext(hostContext) {
1418
1359
  var CharAgentShellElement = class extends HTMLElement {
1419
1360
  static observedAttributes = [
1420
1361
  "open",
1421
- "fullscreen-breakpoint",
1422
- "panel-width",
1423
- "pip-position",
1424
- "pip-width",
1425
- "pip-height",
1426
- "dev-mode",
1427
1362
  "enable-debug-tools",
1428
1363
  "api-base"
1429
1364
  ];
1430
1365
  _agent = null;
1431
1366
  _open = false;
1432
- _fullscreenBreakpoint = DEFAULT_FULLSCREEN_BREAKPOINT;
1433
- _panelWidth = DEFAULT_PANEL_WIDTH;
1434
- _pipWidth = DEFAULT_PIP_WIDTH;
1435
- _pipHeight = DEFAULT_PIP_HEIGHT;
1436
- _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;
1437
1378
  _isNarrowViewport = false;
1438
1379
  _mediaQuery = null;
1439
1380
  _mediaQueryHandler = null;
1381
+ _themeObserver = null;
1382
+ _themeMediaQuery = null;
1383
+ _themeMediaHandler = null;
1440
1384
  _pendingConnectOptions = null;
1441
1385
  _pendingHostContext = {};
1442
1386
  _isApplyingOpenAttribute = false;
@@ -1454,6 +1398,7 @@ var CharAgentShellElement = class extends HTMLElement {
1454
1398
  _pipAnimationTimeoutId = null;
1455
1399
  _pendingPipMeasuredHeight = null;
1456
1400
  _tapOutsideListener = null;
1401
+ _lastAppliedDisplayMode = null;
1457
1402
  _pipHiddenByScroll = false;
1458
1403
  _scrollListener = null;
1459
1404
  _scrollRafId = null;
@@ -1473,75 +1418,36 @@ var CharAgentShellElement = class extends HTMLElement {
1473
1418
  set open(value) {
1474
1419
  this.setOpen(value);
1475
1420
  }
1476
- get fullscreenBreakpoint() {
1477
- return this._fullscreenBreakpoint;
1478
- }
1479
- set fullscreenBreakpoint(value) {
1480
- const normalized = normalizePositiveInteger(value, DEFAULT_FULLSCREEN_BREAKPOINT);
1481
- if (normalized === this._fullscreenBreakpoint) return;
1482
- this._fullscreenBreakpoint = normalized;
1483
- this.setAttribute("fullscreen-breakpoint", String(normalized));
1484
- this._setupViewportObserver();
1485
- this._applyDisplayModeAndLayout();
1486
- }
1487
- get panelWidth() {
1488
- return this._panelWidth;
1489
- }
1490
- set panelWidth(value) {
1491
- const normalized = normalizePositiveInteger(value, DEFAULT_PANEL_WIDTH);
1492
- if (normalized === this._panelWidth) return;
1493
- this._panelWidth = normalized;
1494
- this.setAttribute("panel-width", String(normalized));
1495
- this._applyDisplayModeAndLayout();
1496
- }
1497
- get pipWidth() {
1498
- return this._pipWidth;
1499
- }
1500
- set pipWidth(value) {
1501
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_WIDTH);
1502
- if (normalized === this._pipWidth) return;
1503
- this._pipWidth = normalized;
1504
- this.setAttribute("pip-width", String(normalized));
1505
- this._applyDisplayModeAndLayout();
1506
- }
1507
- get pipHeight() {
1508
- return this._pipHeight;
1509
- }
1510
- set pipHeight(value) {
1511
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_HEIGHT);
1512
- if (normalized === this._pipHeight) return;
1513
- this._pipHeight = normalized;
1514
- this.setAttribute("pip-height", String(normalized));
1515
- this._applyDisplayModeAndLayout();
1516
- }
1517
- get pipPosition() {
1518
- return this._pipPosition;
1519
- }
1520
- set pipPosition(value) {
1521
- const normalized = normalizePipPosition(value);
1522
- if (normalized === this._pipPosition) return;
1523
- this._pipPosition = normalized;
1524
- this.setAttribute("pip-position", normalized);
1525
- this._applyDisplayModeAndLayout();
1526
- }
1527
1421
  connectedCallback() {
1528
1422
  this._upgradeProperty("open");
1529
- this._upgradeProperty("fullscreenBreakpoint");
1530
- this._upgradeProperty("panelWidth");
1531
- this._upgradeProperty("pipPosition");
1532
- this._upgradeProperty("pipWidth");
1533
- this._upgradeProperty("pipHeight");
1534
- this._upgradeProperty("devMode");
1535
1423
  this._upgradeProperty("apiBase");
1536
1424
  registerChar();
1537
1425
  this._ensureAgent();
1538
1426
  this._readConfigFromAttributes();
1539
1427
  this._syncForwardedAttributes();
1540
1428
  this._setupViewportObserver();
1429
+ this._setupThemeObserver();
1541
1430
  this._applyDisplayModeAndLayout();
1542
1431
  this._flushPendingConnectAndContext();
1543
1432
  }
1544
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
+ }
1545
1451
  this._stopHealthWait();
1546
1452
  this._cancelRevealFade();
1547
1453
  this._teardownViewportObserver();
@@ -1570,48 +1476,6 @@ var CharAgentShellElement = class extends HTMLElement {
1570
1476
  });
1571
1477
  return;
1572
1478
  }
1573
- case "fullscreen-breakpoint": {
1574
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_FULLSCREEN_BREAKPOINT);
1575
- if (next !== this._fullscreenBreakpoint) {
1576
- this._fullscreenBreakpoint = next;
1577
- this._setupViewportObserver();
1578
- this._applyDisplayModeAndLayout();
1579
- }
1580
- return;
1581
- }
1582
- case "panel-width": {
1583
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PANEL_WIDTH);
1584
- if (next !== this._panelWidth) {
1585
- this._panelWidth = next;
1586
- this._applyDisplayModeAndLayout();
1587
- }
1588
- return;
1589
- }
1590
- case "pip-width": {
1591
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_WIDTH);
1592
- if (next !== this._pipWidth) {
1593
- this._pipWidth = next;
1594
- this._applyDisplayModeAndLayout();
1595
- }
1596
- return;
1597
- }
1598
- case "pip-height": {
1599
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_HEIGHT);
1600
- if (next !== this._pipHeight) {
1601
- this._pipHeight = next;
1602
- this._applyDisplayModeAndLayout();
1603
- }
1604
- return;
1605
- }
1606
- case "pip-position": {
1607
- const next = normalizePipPosition(newValue);
1608
- if (next !== this._pipPosition) {
1609
- this._pipPosition = next;
1610
- this._applyDisplayModeAndLayout();
1611
- }
1612
- return;
1613
- }
1614
- case "dev-mode":
1615
1479
  case "enable-debug-tools":
1616
1480
  case "api-base":
1617
1481
  this._syncForwardedAttributes();
@@ -1621,7 +1485,7 @@ var CharAgentShellElement = class extends HTMLElement {
1621
1485
  /**
1622
1486
  * Connects shell authentication and starts availability health checks.
1623
1487
  *
1624
- * @param options - Authentication payload for either ID token or ticket auth.
1488
+ * @param options - Authentication payload (`publishableKey` + optional `idToken`).
1625
1489
  * @returns `true` when connect is accepted by the inner element.
1626
1490
  */
1627
1491
  connect(options) {
@@ -1729,15 +1593,10 @@ var CharAgentShellElement = class extends HTMLElement {
1729
1593
  this._attachForwardedListeners();
1730
1594
  }
1731
1595
  _primeAgentBootConfig(agent) {
1732
- for (const name of [
1733
- "dev-mode",
1734
- "enable-debug-tools",
1735
- "api-base"
1736
- ]) {
1596
+ for (const name of ["enable-debug-tools", "api-base"]) {
1737
1597
  const value = this.getAttribute(name);
1738
1598
  if (value !== null) agent.setAttribute(name, value);
1739
1599
  }
1740
- agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1741
1600
  agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1742
1601
  }
1743
1602
  _attachForwardedListeners() {
@@ -1760,29 +1619,19 @@ var CharAgentShellElement = class extends HTMLElement {
1760
1619
  }
1761
1620
  _readConfigFromAttributes() {
1762
1621
  this._open = parseBooleanAttribute(this.getAttribute("open"));
1763
- this._fullscreenBreakpoint = parsePositiveIntegerAttribute(this.getAttribute("fullscreen-breakpoint"), DEFAULT_FULLSCREEN_BREAKPOINT);
1764
- this._panelWidth = parsePositiveIntegerAttribute(this.getAttribute("panel-width"), DEFAULT_PANEL_WIDTH);
1765
- this._pipWidth = parsePositiveIntegerAttribute(this.getAttribute("pip-width"), DEFAULT_PIP_WIDTH);
1766
- this._pipHeight = parsePositiveIntegerAttribute(this.getAttribute("pip-height"), DEFAULT_PIP_HEIGHT);
1767
- this._pipPosition = normalizePipPosition(this.getAttribute("pip-position"));
1768
1622
  }
1769
1623
  _syncForwardedAttributes() {
1770
1624
  if (!this._agent) return;
1771
- for (const name of [
1772
- "dev-mode",
1773
- "enable-debug-tools",
1774
- "api-base"
1775
- ]) {
1625
+ for (const name of ["enable-debug-tools", "api-base"]) {
1776
1626
  const value = this.getAttribute(name);
1777
1627
  if (value === null) this._agent.removeAttribute(name);
1778
1628
  else this._agent.setAttribute(name, value);
1779
1629
  }
1780
- this._agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1781
1630
  this._agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1782
1631
  }
1783
1632
  _setupViewportObserver() {
1784
1633
  if (typeof window === "undefined") return;
1785
- const query = `(max-width: ${this._fullscreenBreakpoint - 1}px)`;
1634
+ const query = `(max-width: ${FULLSCREEN_BREAKPOINT - 1}px)`;
1786
1635
  if (this._mediaQuery?.media === query) {
1787
1636
  this._isNarrowViewport = this._mediaQuery.matches;
1788
1637
  return;
@@ -1820,38 +1669,558 @@ var CharAgentShellElement = class extends HTMLElement {
1820
1669
  if (displayMode !== "pip") {
1821
1670
  if (this._pipMeasuredHeight !== null) this._pipMeasuredHeight = null;
1822
1671
  }
1672
+ const prevMode = this._lastAppliedDisplayMode;
1673
+ this._lastAppliedDisplayMode = displayMode;
1823
1674
  this._agent.setAttribute("display-mode", displayMode);
1824
1675
  this._applyHostLayout(displayMode);
1825
- 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
+ }
1826
1741
  }
1827
1742
  _applyHostLayout(displayMode) {
1743
+ const useFlexLayout = this._isParentFlex();
1828
1744
  if (displayMode === "inline" && !this._isNarrowViewport) {
1829
- setStyleValue(this, "position", "relative");
1830
- setStyleValue(this, "flex", `0 0 ${this._panelWidth}px`);
1831
- setStyleValue(this, "width", `${this._panelWidth}px`);
1832
- setStyleValue(this, "min-width", `${this._panelWidth}px`);
1833
- setStyleValue(this, "height", "100%");
1834
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1835
- setStyleValue(this, "border-left", PANEL_BORDER);
1836
- 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;
1837
2080
  return;
1838
2081
  }
1839
- setStyleValue(this, "position", "relative");
1840
- setStyleValue(this, "flex", "0 0 0px");
1841
- setStyleValue(this, "width", "0px");
1842
- setStyleValue(this, "min-width", "0px");
1843
- setStyleValue(this, "height", "0px");
1844
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1845
- resetStyleValue(this, "border-left");
1846
- 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();
1847
2216
  }
1848
2217
  _composePipTransform(opts) {
1849
2218
  const parts = [];
1850
- if (this._pipPosition === "bottom-center") parts.push("translateX(-50%)");
2219
+ if (PIP_POSITION === "bottom-center") parts.push("translateX(-50%)");
1851
2220
  if (opts.scrollHide) parts.push(`translateY(${PIP_SCROLL_HIDE_OFFSET}px)`);
1852
2221
  return parts.length > 0 ? parts.join(" ") : "none";
1853
2222
  }
1854
- _applyAgentLayout(displayMode) {
2223
+ _applyAgentLayout(displayMode, prevMode) {
1855
2224
  if (!this._agent) return;
1856
2225
  if (displayMode === "inline" && !this._isNarrowViewport) {
1857
2226
  this._detachPipInteractionListeners();
@@ -1859,20 +2228,32 @@ var CharAgentShellElement = class extends HTMLElement {
1859
2228
  this._pipExpanded = false;
1860
2229
  this._pipAnimating = false;
1861
2230
  this._hidePipPill();
1862
- setStyleValue(this._agent, "position", "relative");
1863
- resetStyleValue(this._agent, "top");
1864
- resetStyleValue(this._agent, "right");
1865
- resetStyleValue(this._agent, "bottom");
1866
- 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
+ }
1867
2251
  resetStyleValue(this._agent, "transform");
1868
2252
  resetStyleValue(this._agent, "max-width");
1869
2253
  resetStyleValue(this._agent, "max-height");
1870
- setStyleValue(this._agent, "z-index", "auto");
1871
- setStyleValue(this._agent, "width", "100%");
1872
- setStyleValue(this._agent, "height", "100%");
1873
2254
  setStyleValue(this._agent, "border-radius", "0");
1874
2255
  setStyleValue(this._agent, "overflow", "hidden");
1875
- resetStyleValue(this._agent, "transition");
2256
+ setStyleValue(this._agent, "transition", this._resizeDragging ? "none" : "width 220ms ease");
1876
2257
  resetStyleValue(this._agent, "opacity");
1877
2258
  resetStyleValue(this._agent, "pointer-events");
1878
2259
  return;
@@ -1901,15 +2282,14 @@ var CharAgentShellElement = class extends HTMLElement {
1901
2282
  resetStyleValue(this._agent, "pointer-events");
1902
2283
  return;
1903
2284
  }
1904
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1905
- const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, this._pipWidth);
1906
- 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));
1907
2288
  const targetWidth = this._pipExpanded ? expandedWidth : collapsedWidth;
1908
- const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, this._pipHeight);
1909
- const hasExplicitPipHeight = this.hasAttribute("pip-height");
2289
+ const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, PIP_HEIGHT);
1910
2290
  const measuredHeightFloor = typeof this._pipMeasuredHeight === "number" ? this._pipMeasuredHeight : 0;
1911
2291
  const expandedHeight = Math.max(collapsedHeight, measuredHeightFloor);
1912
- const collapsedPipHeight = hasExplicitPipHeight ? collapsedHeight : PIP_PILL_HEIGHT;
2292
+ const collapsedPipHeight = PIP_PILL_HEIGHT;
1913
2293
  const heightDelta = expandedHeight - collapsedPipHeight;
1914
2294
  let targetHeight;
1915
2295
  let targetBottom;
@@ -1920,13 +2300,14 @@ var CharAgentShellElement = class extends HTMLElement {
1920
2300
  targetHeight = collapsedPipHeight;
1921
2301
  targetBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1922
2302
  }
2303
+ const enteringFromPanel = prevMode != null && prevMode !== "pip" && this._healthState === "healthy";
1923
2304
  const composedTransform = this._composePipTransform({ scrollHide: this._pipHiddenByScroll });
2305
+ if (enteringFromPanel) setStyleValue(this._agent, "transition", "none");
1924
2306
  setStyleValue(this._agent, "position", "fixed");
1925
2307
  resetStyleValue(this._agent, "top");
1926
2308
  setStyleValue(this._agent, "right", pipPositionStyles.right);
1927
2309
  setStyleValue(this._agent, "left", pipPositionStyles.left);
1928
2310
  setStyleValue(this._agent, "bottom", targetBottom);
1929
- setStyleValue(this._agent, "transform", composedTransform);
1930
2311
  setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1931
2312
  setStyleValue(this._agent, "width", `min(${targetWidth}px, calc(100vw - 24px))`);
1932
2313
  setStyleValue(this._agent, "height", `${targetHeight}px`);
@@ -1934,7 +2315,22 @@ var CharAgentShellElement = class extends HTMLElement {
1934
2315
  setStyleValue(this._agent, "max-width", "100vw");
1935
2316
  setStyleValue(this._agent, "border-radius", "16px");
1936
2317
  setStyleValue(this._agent, "overflow", "hidden");
1937
- 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
+ }
1938
2334
  if (this._healthState === "healthy") {
1939
2335
  const pill = this._ensurePipPill();
1940
2336
  if (!pill.parentNode) this.appendChild(pill);
@@ -1943,6 +2339,16 @@ var CharAgentShellElement = class extends HTMLElement {
1943
2339
  this._attachScrollHideListener();
1944
2340
  this._attachSwipeGestureListeners();
1945
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
+ }
1946
2352
  this._applyPipVisualState();
1947
2353
  }
1948
2354
  }
@@ -1952,7 +2358,7 @@ var CharAgentShellElement = class extends HTMLElement {
1952
2358
  pill.setAttribute("role", "button");
1953
2359
  pill.setAttribute("aria-label", "Open assistant");
1954
2360
  pill.setAttribute("tabindex", "0");
1955
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
2361
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
1956
2362
  pill.style.cssText = [
1957
2363
  "position:fixed",
1958
2364
  `left:${pipPositionStyles.left}`,
@@ -2338,6 +2744,7 @@ var CharAgentShellElement = class extends HTMLElement {
2338
2744
  this._pipExpanded = false;
2339
2745
  this._pipAnimating = false;
2340
2746
  this._hidePipPill();
2747
+ this._hideEdgeTab();
2341
2748
  if (this._agent) this._agent.removeAttribute("display-mode");
2342
2749
  this._resetHostLayoutStyles();
2343
2750
  this._resetAgentLayoutStyles();
@@ -2383,7 +2790,7 @@ var CharAgentShellElement = class extends HTMLElement {
2383
2790
  this._revealFrameId = null;
2384
2791
  setStyleValue(this, "opacity", "1");
2385
2792
  window.setTimeout(() => {
2386
- if (this._healthState === "healthy") resetStyleValue(this, "will-change");
2793
+ resetStyleValue(this, "will-change");
2387
2794
  }, SHELL_REVEAL_FADE_MS);
2388
2795
  });
2389
2796
  }
@@ -2435,6 +2842,7 @@ var CharAgentShellElement = class extends HTMLElement {
2435
2842
  return;
2436
2843
  }
2437
2844
  this._open = nextOpen;
2845
+ if (!nextOpen) this._currentPanelWidth = PANEL_WIDTH;
2438
2846
  if (options.reflect) this._reflectOpenAttribute(nextOpen);
2439
2847
  this._applyDisplayModeAndLayout();
2440
2848
  if (options.emit) this.dispatchEvent(new CustomEvent(SHELL_OPEN_CHANGE_EVENT, {