@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.
package/dist/index.js CHANGED
@@ -485,8 +485,7 @@ function mergeHostContext(current, patch) {
485
485
  styles: mergeStyles(previous.styles, patch.styles),
486
486
  containerDimensions: shallowMerge(previous.containerDimensions, patch.containerDimensions),
487
487
  deviceCapabilities: shallowMerge(previous.deviceCapabilities, patch.deviceCapabilities),
488
- safeAreaInsets: shallowMerge(previous.safeAreaInsets, patch.safeAreaInsets),
489
- hostCapabilities: shallowMerge(previous.hostCapabilities, patch.hostCapabilities)
488
+ safeAreaInsets: shallowMerge(previous.safeAreaInsets, patch.safeAreaInsets)
490
489
  };
491
490
  }
492
491
 
@@ -521,25 +520,6 @@ const DISPLAY_MODES = [
521
520
  "pip"
522
521
  ];
523
522
  /**
524
- * Normalizes an API key by trimming whitespace and collapsing empty strings.
525
- */
526
- function normalizeApiKey(value) {
527
- const trimmed = value?.trim();
528
- return trimmed ? trimmed : void 0;
529
- }
530
- /**
531
- * Produces a sanitized dev-mode configuration and drops empty payloads.
532
- */
533
- function resolveDevMode(devMode) {
534
- if (!devMode) return void 0;
535
- const normalized = {
536
- anthropicApiKey: normalizeApiKey(devMode.anthropicApiKey),
537
- openaiApiKey: normalizeApiKey(devMode.openaiApiKey),
538
- useLocalApi: devMode.useLocalApi === true ? true : void 0
539
- };
540
- return !!normalized.anthropicApiKey || !!normalized.openaiApiKey || !!normalized.useLocalApi ? normalized : void 0;
541
- }
542
- /**
543
523
  * Runtime guard for the message envelope emitted by the iframe.
544
524
  */
545
525
  function isCharIframeMessageData(value) {
@@ -678,7 +658,7 @@ function getDeviceCapabilities() {
678
658
  */
679
659
  var CharAgentElement = class extends HTMLElement {
680
660
  static observedAttributes = [
681
- "dev-mode",
661
+ "publishable-key",
682
662
  "enable-debug-tools",
683
663
  "display-mode",
684
664
  "api-base"
@@ -728,12 +708,12 @@ var CharAgentElement = class extends HTMLElement {
728
708
  return;
729
709
  }
730
710
  if (!this.style.display) this.style.display = "block";
731
- const resolvedDevMode = resolveDevMode(this._resolveDevMode());
732
- const apiBase = this._resolveApiBaseOverride() ?? (resolvedDevMode?.useLocalApi ? window.location.origin : WEBMCP_PRODUCTION_API_BASE);
711
+ const apiBase = this._resolveApiBaseOverride() ?? WEBMCP_PRODUCTION_API_BASE;
733
712
  const iframeOrigin = this._resolveIframeOrigin(apiBase);
734
713
  const iframe = this._createIframe(apiBase);
735
714
  this._iframe = iframe;
736
- if (resolvedDevMode?.anthropicApiKey) this._pendingAuth = { anthropicApiKey: resolvedDevMode.anthropicApiKey };
715
+ const publishableKey = this.publishableKey ?? this.getAttribute("publishable-key") ?? void 0;
716
+ if (publishableKey) this._pendingAuth = { publishableKey };
737
717
  this._proxy = new CharIframeProxy(iframe, iframeOrigin);
738
718
  this._proxy.start();
739
719
  this._messageListener = this._createMessageListener(iframe, iframeOrigin);
@@ -942,15 +922,21 @@ var CharAgentElement = class extends HTMLElement {
942
922
  /**
943
923
  * Reacts to observed attribute updates after iframe readiness.
944
924
  * Attributes that affect iframe boot configuration are intentionally ignored
945
- * after mount (`dev-mode`, `enable-debug-tools`, `api-base`).
925
+ * after mount (`enable-debug-tools`, `api-base`).
946
926
  */
947
927
  attributeChangedCallback(name, _oldValue, newValue) {
948
928
  if (!this._iframe || !this._iframeReady) return;
949
929
  switch (name) {
930
+ case "publishable-key":
931
+ if (newValue) {
932
+ this._pendingAuth = { publishableKey: newValue };
933
+ const iframeOrigin = this._getIframeOrigin();
934
+ if (iframeOrigin) this._postAuth(iframeOrigin);
935
+ }
936
+ break;
950
937
  case "display-mode":
951
938
  this.setHostContext({ displayMode: resolveDisplayModeFromAttribute(newValue) });
952
939
  break;
953
- case "dev-mode":
954
940
  case "enable-debug-tools":
955
941
  case "api-base": break;
956
942
  }
@@ -961,23 +947,17 @@ var CharAgentElement = class extends HTMLElement {
961
947
  * The token is stored as a JavaScript property (not as a DOM attribute),
962
948
  * preventing exposure to DOM inspection and session replay tools.
963
949
  *
964
- * @param options - Authentication payload for either ID token or ticket auth.
950
+ * @param options - Authentication payload.
965
951
  * @returns `true` when the payload is accepted; `false` when validation fails.
966
952
  */
967
953
  connect(options) {
968
- if (!options?.idToken && !options?.ticketAuth) {
969
- this._emitCharError("MISSING_TOKEN", "connect() requires either idToken or ticketAuth parameter");
970
- return false;
971
- }
972
- if (options?.idToken && !options?.ticketAuth && !options?.clientId) {
973
- this._emitCharError("MISSING_CLIENT_ID", "connect() requires clientId when using idToken authentication");
954
+ if (!options?.publishableKey) {
955
+ this._emitCharError("MISSING_PUBLISHABLE_KEY", "connect() requires publishableKey");
974
956
  return false;
975
957
  }
976
958
  this._pendingAuth = {
977
- idToken: options.ticketAuth ? void 0 : options.idToken,
978
- clientId: options.ticketAuth ? void 0 : options.clientId,
979
- organizationId: options.ticketAuth ? void 0 : options.organizationId,
980
- ticketAuth: options.ticketAuth
959
+ publishableKey: options.publishableKey,
960
+ idToken: options.idToken
981
961
  };
982
962
  const iframeOrigin = this._getIframeOrigin();
983
963
  if (this._iframeReady && iframeOrigin) this._postAuth(iframeOrigin);
@@ -996,7 +976,7 @@ var CharAgentElement = class extends HTMLElement {
996
976
  }
997
977
  /**
998
978
  * Disconnect from the Char agent.
999
- * Clears the authentication token.
979
+ * Clears pending auth state and posts a disconnect message to the iframe.
1000
980
  *
1001
981
  * @returns `true` when the disconnect message was sent; `false` when the iframe was not ready.
1002
982
  */
@@ -1060,11 +1040,6 @@ var CharAgentElement = class extends HTMLElement {
1060
1040
  };
1061
1041
  const safeAreaInsets = getSafeAreaInsets();
1062
1042
  const displayMode = resolveDisplayModeFromAttribute(displayModeAttr) ?? "inline";
1063
- const hostCapabilities = {
1064
- supportedDisplayModes: [...DISPLAY_MODES],
1065
- supportsOpenLink: true,
1066
- supportsTeardown: true
1067
- };
1068
1043
  const context = {
1069
1044
  theme,
1070
1045
  styles: { variables: vars },
@@ -1075,9 +1050,7 @@ var CharAgentElement = class extends HTMLElement {
1075
1050
  timeZone,
1076
1051
  platform: detectPlatform(),
1077
1052
  deviceCapabilities: getDeviceCapabilities(),
1078
- safeAreaInsets,
1079
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1080
- hostCapabilities
1053
+ safeAreaInsets
1081
1054
  };
1082
1055
  this._hostContext = context;
1083
1056
  if (!this._iframe?.contentWindow) {
@@ -1120,17 +1093,15 @@ var CharAgentElement = class extends HTMLElement {
1120
1093
  */
1121
1094
  _postAuth(iframeOrigin) {
1122
1095
  if (!this._pendingAuth || !this._iframe?.contentWindow) return;
1123
- const message = this._pendingAuth.ticketAuth ? {
1124
- type: "char-auth",
1125
- ticketAuth: this._pendingAuth.ticketAuth
1126
- } : this._pendingAuth.anthropicApiKey ? {
1127
- type: "char-auth",
1128
- anthropicApiKey: this._pendingAuth.anthropicApiKey
1129
- } : {
1096
+ if (!this._pendingAuth.publishableKey) {
1097
+ console.error("[Char] _postAuth called but _pendingAuth has no publishableKey");
1098
+ this._pendingAuth = null;
1099
+ return;
1100
+ }
1101
+ const message = {
1130
1102
  type: "char-auth",
1131
- idToken: this._pendingAuth.idToken,
1132
- clientId: this._pendingAuth.clientId,
1133
- organizationId: this._pendingAuth.organizationId
1103
+ publishableKey: this._pendingAuth.publishableKey,
1104
+ idToken: this._pendingAuth.idToken
1134
1105
  };
1135
1106
  this._iframe.contentWindow.postMessage(message, iframeOrigin);
1136
1107
  }
@@ -1277,36 +1248,6 @@ var CharAgentElement = class extends HTMLElement {
1277
1248
  this._containerResizeObserver.observe(this);
1278
1249
  }
1279
1250
  /**
1280
- * Resolve devMode from property (React 19) or attribute.
1281
- *
1282
- * @returns Parsed dev-mode configuration when valid.
1283
- */
1284
- _resolveDevMode() {
1285
- if (this.devMode && typeof this.devMode === "object") return this.devMode;
1286
- const devModeAttr = this.getAttribute("dev-mode");
1287
- if (!devModeAttr) return void 0;
1288
- try {
1289
- const parsed = JSON.parse(devModeAttr);
1290
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1291
- const msg = `dev-mode attribute must be a JSON object, got: ${Array.isArray(parsed) ? "array" : typeof parsed}`;
1292
- console.warn(`[Char] ${msg}`);
1293
- this._emitCharError("INVALID_DEV_MODE", msg);
1294
- return;
1295
- }
1296
- const obj = parsed;
1297
- return {
1298
- anthropicApiKey: typeof obj.anthropicApiKey === "string" ? obj.anthropicApiKey : void 0,
1299
- openaiApiKey: typeof obj.openaiApiKey === "string" ? obj.openaiApiKey : void 0,
1300
- useLocalApi: typeof obj.useLocalApi === "boolean" ? obj.useLocalApi : void 0
1301
- };
1302
- } catch (e) {
1303
- const msg = `Failed to parse dev-mode attribute as JSON: ${e instanceof Error ? e.message : String(e)}`;
1304
- console.warn(`[Char] ${msg}`);
1305
- this._emitCharError("INVALID_DEV_MODE", msg);
1306
- return;
1307
- }
1308
- }
1309
- /**
1310
1251
  * Resolve apiBase override from property (React 19) or attribute.
1311
1252
  *
1312
1253
  * @returns Sanitized API base URL without trailing slash.
@@ -1335,22 +1276,36 @@ registerChar();
1335
1276
  //#region src/shell-component.ts
1336
1277
  const SHELL_OPEN_CHANGE_EVENT = "char-shell-open-change";
1337
1278
  const SHELL_Z_INDEX = 2147483e3;
1338
- const DEFAULT_FULLSCREEN_BREAKPOINT = 1024;
1339
- const DEFAULT_PANEL_WIDTH = 420;
1340
- const DEFAULT_PIP_WIDTH = 300;
1341
- const DEFAULT_PIP_HEIGHT = 96;
1342
- const DEFAULT_PIP_POSITION = "bottom-center";
1279
+ const FULLSCREEN_BREAKPOINT = 1024;
1280
+ const PANEL_WIDTH = 420;
1281
+ const PIP_WIDTH = 400;
1282
+ const PIP_HEIGHT = 60;
1283
+ const PIP_POSITION = "bottom-center";
1343
1284
  const SHELL_HEALTH_TIMEOUT_MS = 1e4;
1344
- const PANEL_BORDER = "1px solid rgba(15, 23, 42, 0.12)";
1345
1285
  const SAFE_BOTTOM = "calc(16px + env(safe-area-inset-bottom, 0px))";
1346
- const SHELL_LAYOUT_TRANSITION = "opacity 280ms ease, width 220ms ease, min-width 220ms ease, flex-basis 220ms ease";
1286
+ const THEME_LIGHT = {
1287
+ background: "oklch(1 0 0)",
1288
+ foreground: "oklch(0.141 0.005 285.823)",
1289
+ border: "oklch(0.92 0.004 286.32)",
1290
+ mutedForeground: "oklch(0.552 0.016 285.938)",
1291
+ primary: "oklch(0.21 0.006 285.885)"
1292
+ };
1293
+ const THEME_DARK = {
1294
+ background: "oklch(0.141 0.005 285.823)",
1295
+ foreground: "oklch(0.985 0 0)",
1296
+ border: "oklch(1 0 0 / 10%)",
1297
+ mutedForeground: "oklch(0.705 0.015 286.067)",
1298
+ primary: "oklch(0.92 0.004 286.32)"
1299
+ };
1300
+ const BODY_MARGIN_TRANSITION = "margin-right 220ms ease";
1301
+ const SHELL_LAYOUT_TRANSITION = "opacity 220ms ease, width 220ms ease, min-width 220ms ease, flex-basis 220ms ease";
1347
1302
  const SHELL_REVEAL_FADE_MS = 280;
1348
1303
  const PIP_MIN_COLLAPSED_WIDTH = 70;
1349
1304
  const PIP_MIN_COLLAPSED_HEIGHT = 30;
1350
1305
  const PIP_MAX_MEASURED_HEIGHT = 320;
1351
1306
  const PIP_COLLAPSE_DELAY_MS = 400;
1352
1307
  const PIP_PILL_HEIGHT = 40;
1353
- const PIP_MORPH_TRANSITION = "width 280ms ease, height 280ms ease, bottom 280ms ease, border-radius 280ms ease, opacity 200ms ease, transform 280ms ease";
1308
+ const PIP_MORPH_TRANSITION = "width 280ms ease, height 280ms ease, bottom 280ms ease, border-radius 280ms ease, opacity 280ms ease, transform 280ms ease";
1354
1309
  const PIP_MORPH_DURATION_MS = 280;
1355
1310
  const PIP_SCROLL_HIDE_OFFSET = 100;
1356
1311
  const PIP_COLLAPSED_WIDTH_RATIO = .75;
@@ -1358,23 +1313,14 @@ const PIP_SWIPE_THRESHOLD = 50;
1358
1313
  const PIP_SCROLL_DIRECTION_THRESHOLD = 40;
1359
1314
  const WHEEL_DELTA_LINE_MULTIPLIER = 15;
1360
1315
  const WHEEL_DELTA_PAGE_MULTIPLIER = 300;
1361
- function normalizePositiveInteger(value, fallback) {
1362
- if (!Number.isFinite(value) || value <= 0) {
1363
- console.warn(`[Char] Invalid value ${value}, using fallback ${fallback}`);
1364
- return fallback;
1365
- }
1366
- return Math.round(value);
1367
- }
1368
- function parsePositiveIntegerAttribute(value, fallback) {
1369
- if (value === null || value.trim() === "") return fallback;
1370
- const parsed = Number.parseInt(value, 10);
1371
- if (!Number.isFinite(parsed)) return fallback;
1372
- return normalizePositiveInteger(parsed, fallback);
1373
- }
1374
- function normalizePipPosition(value) {
1375
- if (value === "bottom-right" || value === "bottom-left") return value;
1376
- return DEFAULT_PIP_POSITION;
1377
- }
1316
+ const PANEL_MIN_WIDTH = 320;
1317
+ const PANEL_MAX_WIDTH = 700;
1318
+ const RESIZE_NUB_WIDTH = 6;
1319
+ const EDGE_TAB_WIDTH = 44;
1320
+ const EDGE_TAB_HEIGHT = 48;
1321
+ const EDGE_TAB_DRAG_THRESHOLD_PX = 4;
1322
+ 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>`;
1323
+ 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>`;
1378
1324
  function getPipPositionStyles(position) {
1379
1325
  if (position === "bottom-right") return {
1380
1326
  left: "auto",
@@ -1431,25 +1377,28 @@ function sanitizeShellHostContext(hostContext) {
1431
1377
  var CharAgentShellElement = class extends HTMLElement {
1432
1378
  static observedAttributes = [
1433
1379
  "open",
1434
- "fullscreen-breakpoint",
1435
- "panel-width",
1436
- "pip-position",
1437
- "pip-width",
1438
- "pip-height",
1439
- "dev-mode",
1440
1380
  "enable-debug-tools",
1441
1381
  "api-base"
1442
1382
  ];
1443
1383
  _agent = null;
1444
1384
  _open = false;
1445
- _fullscreenBreakpoint = DEFAULT_FULLSCREEN_BREAKPOINT;
1446
- _panelWidth = DEFAULT_PANEL_WIDTH;
1447
- _pipWidth = DEFAULT_PIP_WIDTH;
1448
- _pipHeight = DEFAULT_PIP_HEIGHT;
1449
- _pipPosition = DEFAULT_PIP_POSITION;
1385
+ _savedBodyTransition = null;
1386
+ _bodyMarginTimeoutId = null;
1387
+ _currentPanelWidth = PANEL_WIDTH;
1388
+ _resizeNubElement = null;
1389
+ _edgeTabElement = null;
1390
+ _resizeDragging = false;
1391
+ _resizeDragOverlay = null;
1392
+ _resizeStartX = 0;
1393
+ _resizeStartWidth = 0;
1394
+ _edgeTabDragStartX = 0;
1395
+ _edgeTabDragging = false;
1450
1396
  _isNarrowViewport = false;
1451
1397
  _mediaQuery = null;
1452
1398
  _mediaQueryHandler = null;
1399
+ _themeObserver = null;
1400
+ _themeMediaQuery = null;
1401
+ _themeMediaHandler = null;
1453
1402
  _pendingConnectOptions = null;
1454
1403
  _pendingHostContext = {};
1455
1404
  _isApplyingOpenAttribute = false;
@@ -1467,6 +1416,7 @@ var CharAgentShellElement = class extends HTMLElement {
1467
1416
  _pipAnimationTimeoutId = null;
1468
1417
  _pendingPipMeasuredHeight = null;
1469
1418
  _tapOutsideListener = null;
1419
+ _lastAppliedDisplayMode = null;
1470
1420
  _pipHiddenByScroll = false;
1471
1421
  _scrollListener = null;
1472
1422
  _scrollRafId = null;
@@ -1486,75 +1436,36 @@ var CharAgentShellElement = class extends HTMLElement {
1486
1436
  set open(value) {
1487
1437
  this.setOpen(value);
1488
1438
  }
1489
- get fullscreenBreakpoint() {
1490
- return this._fullscreenBreakpoint;
1491
- }
1492
- set fullscreenBreakpoint(value) {
1493
- const normalized = normalizePositiveInteger(value, DEFAULT_FULLSCREEN_BREAKPOINT);
1494
- if (normalized === this._fullscreenBreakpoint) return;
1495
- this._fullscreenBreakpoint = normalized;
1496
- this.setAttribute("fullscreen-breakpoint", String(normalized));
1497
- this._setupViewportObserver();
1498
- this._applyDisplayModeAndLayout();
1499
- }
1500
- get panelWidth() {
1501
- return this._panelWidth;
1502
- }
1503
- set panelWidth(value) {
1504
- const normalized = normalizePositiveInteger(value, DEFAULT_PANEL_WIDTH);
1505
- if (normalized === this._panelWidth) return;
1506
- this._panelWidth = normalized;
1507
- this.setAttribute("panel-width", String(normalized));
1508
- this._applyDisplayModeAndLayout();
1509
- }
1510
- get pipWidth() {
1511
- return this._pipWidth;
1512
- }
1513
- set pipWidth(value) {
1514
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_WIDTH);
1515
- if (normalized === this._pipWidth) return;
1516
- this._pipWidth = normalized;
1517
- this.setAttribute("pip-width", String(normalized));
1518
- this._applyDisplayModeAndLayout();
1519
- }
1520
- get pipHeight() {
1521
- return this._pipHeight;
1522
- }
1523
- set pipHeight(value) {
1524
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_HEIGHT);
1525
- if (normalized === this._pipHeight) return;
1526
- this._pipHeight = normalized;
1527
- this.setAttribute("pip-height", String(normalized));
1528
- this._applyDisplayModeAndLayout();
1529
- }
1530
- get pipPosition() {
1531
- return this._pipPosition;
1532
- }
1533
- set pipPosition(value) {
1534
- const normalized = normalizePipPosition(value);
1535
- if (normalized === this._pipPosition) return;
1536
- this._pipPosition = normalized;
1537
- this.setAttribute("pip-position", normalized);
1538
- this._applyDisplayModeAndLayout();
1539
- }
1540
1439
  connectedCallback() {
1541
1440
  this._upgradeProperty("open");
1542
- this._upgradeProperty("fullscreenBreakpoint");
1543
- this._upgradeProperty("panelWidth");
1544
- this._upgradeProperty("pipPosition");
1545
- this._upgradeProperty("pipWidth");
1546
- this._upgradeProperty("pipHeight");
1547
- this._upgradeProperty("devMode");
1548
1441
  this._upgradeProperty("apiBase");
1549
1442
  registerChar();
1550
1443
  this._ensureAgent();
1551
1444
  this._readConfigFromAttributes();
1552
1445
  this._syncForwardedAttributes();
1553
1446
  this._setupViewportObserver();
1447
+ this._setupThemeObserver();
1554
1448
  this._applyDisplayModeAndLayout();
1555
1449
  this._flushPendingConnectAndContext();
1556
1450
  }
1557
1451
  disconnectedCallback() {
1452
+ this._removeBodyMargin();
1453
+ if (this._bodyMarginTimeoutId !== null) {
1454
+ window.clearTimeout(this._bodyMarginTimeoutId);
1455
+ this._bodyMarginTimeoutId = null;
1456
+ }
1457
+ this._removeResizeDragOverlay();
1458
+ this._teardownThemeObserver();
1459
+ document.removeEventListener("mousemove", this._onEdgeTabMouseMove);
1460
+ document.removeEventListener("mouseup", this._onEdgeTabMouseUp);
1461
+ document.removeEventListener("touchmove", this._onEdgeTabTouchMove);
1462
+ document.removeEventListener("touchend", this._onEdgeTabTouchEnd);
1463
+ this._removeEdgeTab();
1464
+ this._hideResizeNub();
1465
+ if (this._resizeNubElement) {
1466
+ this._resizeNubElement.remove();
1467
+ this._resizeNubElement = null;
1468
+ }
1558
1469
  this._stopHealthWait();
1559
1470
  this._cancelRevealFade();
1560
1471
  this._teardownViewportObserver();
@@ -1583,48 +1494,6 @@ var CharAgentShellElement = class extends HTMLElement {
1583
1494
  });
1584
1495
  return;
1585
1496
  }
1586
- case "fullscreen-breakpoint": {
1587
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_FULLSCREEN_BREAKPOINT);
1588
- if (next !== this._fullscreenBreakpoint) {
1589
- this._fullscreenBreakpoint = next;
1590
- this._setupViewportObserver();
1591
- this._applyDisplayModeAndLayout();
1592
- }
1593
- return;
1594
- }
1595
- case "panel-width": {
1596
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PANEL_WIDTH);
1597
- if (next !== this._panelWidth) {
1598
- this._panelWidth = next;
1599
- this._applyDisplayModeAndLayout();
1600
- }
1601
- return;
1602
- }
1603
- case "pip-width": {
1604
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_WIDTH);
1605
- if (next !== this._pipWidth) {
1606
- this._pipWidth = next;
1607
- this._applyDisplayModeAndLayout();
1608
- }
1609
- return;
1610
- }
1611
- case "pip-height": {
1612
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_HEIGHT);
1613
- if (next !== this._pipHeight) {
1614
- this._pipHeight = next;
1615
- this._applyDisplayModeAndLayout();
1616
- }
1617
- return;
1618
- }
1619
- case "pip-position": {
1620
- const next = normalizePipPosition(newValue);
1621
- if (next !== this._pipPosition) {
1622
- this._pipPosition = next;
1623
- this._applyDisplayModeAndLayout();
1624
- }
1625
- return;
1626
- }
1627
- case "dev-mode":
1628
1497
  case "enable-debug-tools":
1629
1498
  case "api-base":
1630
1499
  this._syncForwardedAttributes();
@@ -1634,7 +1503,7 @@ var CharAgentShellElement = class extends HTMLElement {
1634
1503
  /**
1635
1504
  * Connects shell authentication and starts availability health checks.
1636
1505
  *
1637
- * @param options - Authentication payload for either ID token or ticket auth.
1506
+ * @param options - Authentication payload (`publishableKey` + optional `idToken`).
1638
1507
  * @returns `true` when connect is accepted by the inner element.
1639
1508
  */
1640
1509
  connect(options) {
@@ -1742,15 +1611,10 @@ var CharAgentShellElement = class extends HTMLElement {
1742
1611
  this._attachForwardedListeners();
1743
1612
  }
1744
1613
  _primeAgentBootConfig(agent) {
1745
- for (const name of [
1746
- "dev-mode",
1747
- "enable-debug-tools",
1748
- "api-base"
1749
- ]) {
1614
+ for (const name of ["enable-debug-tools", "api-base"]) {
1750
1615
  const value = this.getAttribute(name);
1751
1616
  if (value !== null) agent.setAttribute(name, value);
1752
1617
  }
1753
- agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1754
1618
  agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1755
1619
  }
1756
1620
  _attachForwardedListeners() {
@@ -1773,29 +1637,19 @@ var CharAgentShellElement = class extends HTMLElement {
1773
1637
  }
1774
1638
  _readConfigFromAttributes() {
1775
1639
  this._open = parseBooleanAttribute(this.getAttribute("open"));
1776
- this._fullscreenBreakpoint = parsePositiveIntegerAttribute(this.getAttribute("fullscreen-breakpoint"), DEFAULT_FULLSCREEN_BREAKPOINT);
1777
- this._panelWidth = parsePositiveIntegerAttribute(this.getAttribute("panel-width"), DEFAULT_PANEL_WIDTH);
1778
- this._pipWidth = parsePositiveIntegerAttribute(this.getAttribute("pip-width"), DEFAULT_PIP_WIDTH);
1779
- this._pipHeight = parsePositiveIntegerAttribute(this.getAttribute("pip-height"), DEFAULT_PIP_HEIGHT);
1780
- this._pipPosition = normalizePipPosition(this.getAttribute("pip-position"));
1781
1640
  }
1782
1641
  _syncForwardedAttributes() {
1783
1642
  if (!this._agent) return;
1784
- for (const name of [
1785
- "dev-mode",
1786
- "enable-debug-tools",
1787
- "api-base"
1788
- ]) {
1643
+ for (const name of ["enable-debug-tools", "api-base"]) {
1789
1644
  const value = this.getAttribute(name);
1790
1645
  if (value === null) this._agent.removeAttribute(name);
1791
1646
  else this._agent.setAttribute(name, value);
1792
1647
  }
1793
- this._agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1794
1648
  this._agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1795
1649
  }
1796
1650
  _setupViewportObserver() {
1797
1651
  if (typeof window === "undefined") return;
1798
- const query = `(max-width: ${this._fullscreenBreakpoint - 1}px)`;
1652
+ const query = `(max-width: ${FULLSCREEN_BREAKPOINT - 1}px)`;
1799
1653
  if (this._mediaQuery?.media === query) {
1800
1654
  this._isNarrowViewport = this._mediaQuery.matches;
1801
1655
  return;
@@ -1833,38 +1687,558 @@ var CharAgentShellElement = class extends HTMLElement {
1833
1687
  if (displayMode !== "pip") {
1834
1688
  if (this._pipMeasuredHeight !== null) this._pipMeasuredHeight = null;
1835
1689
  }
1690
+ const prevMode = this._lastAppliedDisplayMode;
1691
+ this._lastAppliedDisplayMode = displayMode;
1836
1692
  this._agent.setAttribute("display-mode", displayMode);
1837
1693
  this._applyHostLayout(displayMode);
1838
- this._applyAgentLayout(displayMode);
1694
+ this._applyAgentLayout(displayMode, prevMode);
1695
+ }
1696
+ _isParentFlex() {
1697
+ const parent = this.parentElement;
1698
+ if (!parent) return false;
1699
+ const display = getComputedStyle(parent).display;
1700
+ return display === "flex" || display === "inline-flex";
1701
+ }
1702
+ _isDarkMode() {
1703
+ const themeAttr = document.documentElement.getAttribute("data-theme");
1704
+ if (themeAttr === "dark") return true;
1705
+ if (themeAttr === "light") return false;
1706
+ return document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
1707
+ }
1708
+ /**
1709
+ * Read a `--char-color-*` variable from the shell element. If the host
1710
+ * page has set it (directly or via inheritance) use that value; otherwise
1711
+ * fall back to the char design-system default for the current light/dark mode.
1712
+ */
1713
+ _resolveColor(token) {
1714
+ const host = getComputedStyle(this).getPropertyValue({
1715
+ background: "--char-color-background",
1716
+ foreground: "--char-color-foreground",
1717
+ border: "--char-color-border",
1718
+ mutedForeground: "--char-color-muted-foreground",
1719
+ primary: "--char-color-primary"
1720
+ }[token]).trim();
1721
+ if (host) return host;
1722
+ return (this._isDarkMode() ? THEME_DARK : THEME_LIGHT)[token];
1723
+ }
1724
+ _panelBorder() {
1725
+ return `1px solid ${this._resolveColor("border")}`;
1726
+ }
1727
+ _setupThemeObserver() {
1728
+ const onThemeChange = () => {
1729
+ this._applyEdgeTabVisual();
1730
+ this._refreshResizeNubColors();
1731
+ if (this._open && !this._isNarrowViewport) {
1732
+ if (this._isParentFlex()) setStyleValue(this, "border-left", this._panelBorder());
1733
+ else if (this._agent) setStyleValue(this._agent, "border-left", this._panelBorder());
1734
+ }
1735
+ };
1736
+ this._themeObserver = new MutationObserver(onThemeChange);
1737
+ this._themeObserver.observe(document.documentElement, {
1738
+ attributes: true,
1739
+ attributeFilter: [
1740
+ "class",
1741
+ "style",
1742
+ "data-theme"
1743
+ ]
1744
+ });
1745
+ this._themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
1746
+ this._themeMediaHandler = onThemeChange;
1747
+ this._themeMediaQuery.addEventListener("change", this._themeMediaHandler);
1748
+ }
1749
+ _teardownThemeObserver() {
1750
+ if (this._themeObserver) {
1751
+ this._themeObserver.disconnect();
1752
+ this._themeObserver = null;
1753
+ }
1754
+ if (this._themeMediaQuery && this._themeMediaHandler) {
1755
+ this._themeMediaQuery.removeEventListener("change", this._themeMediaHandler);
1756
+ this._themeMediaQuery = null;
1757
+ this._themeMediaHandler = null;
1758
+ }
1839
1759
  }
1840
1760
  _applyHostLayout(displayMode) {
1761
+ const useFlexLayout = this._isParentFlex();
1841
1762
  if (displayMode === "inline" && !this._isNarrowViewport) {
1842
- setStyleValue(this, "position", "relative");
1843
- setStyleValue(this, "flex", `0 0 ${this._panelWidth}px`);
1844
- setStyleValue(this, "width", `${this._panelWidth}px`);
1845
- setStyleValue(this, "min-width", `${this._panelWidth}px`);
1846
- setStyleValue(this, "height", "100%");
1847
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1848
- setStyleValue(this, "border-left", PANEL_BORDER);
1849
- setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1763
+ if (useFlexLayout) {
1764
+ setStyleValue(this, "position", "relative");
1765
+ setStyleValue(this, "flex", `0 0 ${this._currentPanelWidth}px`);
1766
+ setStyleValue(this, "width", `${this._currentPanelWidth}px`);
1767
+ setStyleValue(this, "min-width", `${this._currentPanelWidth}px`);
1768
+ setStyleValue(this, "height", "100%");
1769
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1770
+ setStyleValue(this, "border-left", this._panelBorder());
1771
+ setStyleValue(this, "transition", this._resizeDragging ? "none" : SHELL_LAYOUT_TRANSITION);
1772
+ resetStyleValue(this, "overflow");
1773
+ resetStyleValue(this, "pointer-events");
1774
+ this._removeBodyMargin();
1775
+ } else {
1776
+ setStyleValue(this, "position", "fixed");
1777
+ setStyleValue(this, "width", "0px");
1778
+ setStyleValue(this, "height", "0px");
1779
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1780
+ resetStyleValue(this, "flex");
1781
+ resetStyleValue(this, "min-width");
1782
+ resetStyleValue(this, "border-left");
1783
+ resetStyleValue(this, "overflow");
1784
+ resetStyleValue(this, "pointer-events");
1785
+ this._applyBodyMargin(this._currentPanelWidth);
1786
+ }
1787
+ this._showResizeNub();
1788
+ this._showEdgeTab();
1789
+ if (useFlexLayout) this._syncEdgeTabWithPanelTransition();
1790
+ } else if (displayMode === "fullscreen") {
1791
+ if (useFlexLayout) {
1792
+ setStyleValue(this, "position", "relative");
1793
+ setStyleValue(this, "flex", "0 0 0px");
1794
+ setStyleValue(this, "width", "0px");
1795
+ setStyleValue(this, "min-width", "0px");
1796
+ setStyleValue(this, "height", "0px");
1797
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1798
+ resetStyleValue(this, "border-left");
1799
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1800
+ resetStyleValue(this, "overflow");
1801
+ resetStyleValue(this, "pointer-events");
1802
+ } else {
1803
+ setStyleValue(this, "position", "relative");
1804
+ resetStyleValue(this, "flex");
1805
+ resetStyleValue(this, "min-width");
1806
+ resetStyleValue(this, "border-left");
1807
+ resetStyleValue(this, "overflow");
1808
+ resetStyleValue(this, "pointer-events");
1809
+ }
1810
+ this._removeBodyMargin();
1811
+ this._hideResizeNub();
1812
+ this._hideEdgeTab();
1813
+ } else {
1814
+ if (useFlexLayout) {
1815
+ setStyleValue(this, "position", "relative");
1816
+ setStyleValue(this, "flex", "0 0 0px");
1817
+ setStyleValue(this, "width", "0px");
1818
+ setStyleValue(this, "min-width", "0px");
1819
+ setStyleValue(this, "height", "0px");
1820
+ setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1821
+ resetStyleValue(this, "border-left");
1822
+ setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
1823
+ resetStyleValue(this, "overflow");
1824
+ resetStyleValue(this, "pointer-events");
1825
+ } else {
1826
+ setStyleValue(this, "position", "relative");
1827
+ resetStyleValue(this, "flex");
1828
+ resetStyleValue(this, "min-width");
1829
+ resetStyleValue(this, "border-left");
1830
+ resetStyleValue(this, "overflow");
1831
+ resetStyleValue(this, "pointer-events");
1832
+ }
1833
+ this._removeBodyMargin();
1834
+ this._hideResizeNub();
1835
+ this._showEdgeTab();
1836
+ }
1837
+ }
1838
+ _applyBodyMargin(width) {
1839
+ const body = document.body;
1840
+ if (this._savedBodyTransition === null) this._savedBodyTransition = body.style.transition || "";
1841
+ if (!this._resizeDragging) body.style.transition = BODY_MARGIN_TRANSITION;
1842
+ body.style.marginRight = `${width}px`;
1843
+ }
1844
+ _removeBodyMargin() {
1845
+ const body = document.body;
1846
+ body.style.marginRight = "";
1847
+ if (this._bodyMarginTimeoutId !== null) {
1848
+ window.clearTimeout(this._bodyMarginTimeoutId);
1849
+ this._bodyMarginTimeoutId = null;
1850
+ }
1851
+ if (this._savedBodyTransition !== null) {
1852
+ const saved = this._savedBodyTransition;
1853
+ this._bodyMarginTimeoutId = window.setTimeout(() => {
1854
+ this._bodyMarginTimeoutId = null;
1855
+ if (body.style.transition === BODY_MARGIN_TRANSITION) body.style.transition = saved || "";
1856
+ }, 220);
1857
+ this._savedBodyTransition = null;
1858
+ }
1859
+ }
1860
+ _ensureResizeNub() {
1861
+ if (this._resizeNubElement) return this._resizeNubElement;
1862
+ const nub = document.createElement("div");
1863
+ nub.setAttribute("role", "separator");
1864
+ nub.setAttribute("aria-orientation", "vertical");
1865
+ nub.setAttribute("aria-label", "Resize panel");
1866
+ nub.style.cssText = [
1867
+ "position:fixed",
1868
+ "top:0",
1869
+ `width:${RESIZE_NUB_WIDTH}px`,
1870
+ "height:100dvh",
1871
+ "cursor:col-resize",
1872
+ `z-index:${SHELL_Z_INDEX + 1}`,
1873
+ "background:transparent",
1874
+ "user-select:none",
1875
+ "-webkit-user-select:none",
1876
+ "touch-action:none"
1877
+ ].join(";");
1878
+ const indicator = document.createElement("div");
1879
+ indicator.style.cssText = [
1880
+ "position:absolute",
1881
+ "top:0",
1882
+ "left:50%",
1883
+ "transform:translateX(-50%)",
1884
+ "width:2px",
1885
+ "height:100%",
1886
+ "border-radius:1px",
1887
+ "transition:background 150ms ease, width 150ms ease",
1888
+ "pointer-events:none"
1889
+ ].join(";");
1890
+ nub.appendChild(indicator);
1891
+ this._applyResizeNubColors(indicator);
1892
+ nub.addEventListener("mouseenter", () => {
1893
+ const dark = this._isDarkMode();
1894
+ indicator.style.background = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.15)";
1895
+ indicator.style.width = "3px";
1896
+ });
1897
+ nub.addEventListener("mouseleave", () => {
1898
+ if (!this._resizeDragging) {
1899
+ this._applyResizeNubColors(indicator);
1900
+ indicator.style.width = "2px";
1901
+ }
1902
+ });
1903
+ nub.addEventListener("mousedown", this._onResizeStart);
1904
+ nub.addEventListener("touchstart", this._onResizeTouchStart, { passive: false });
1905
+ this._resizeNubElement = nub;
1906
+ return nub;
1907
+ }
1908
+ _applyResizeNubColors(indicator) {
1909
+ const el = indicator ?? this._resizeNubElement?.querySelector("div");
1910
+ if (!el) return;
1911
+ const dark = this._isDarkMode();
1912
+ el.style.background = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)";
1913
+ }
1914
+ _refreshResizeNubColors() {
1915
+ this._applyResizeNubColors();
1916
+ }
1917
+ _showResizeNub() {
1918
+ const nub = this._ensureResizeNub();
1919
+ if (!nub.parentNode) document.body.appendChild(nub);
1920
+ setStyleValue(nub, "right", `${this._currentPanelWidth - Math.floor(RESIZE_NUB_WIDTH / 2)}px`);
1921
+ setStyleValue(nub, "display", "block");
1922
+ }
1923
+ _hideResizeNub() {
1924
+ if (this._resizeNubElement) setStyleValue(this._resizeNubElement, "display", "none");
1925
+ }
1926
+ _ensureEdgeTab() {
1927
+ if (this._edgeTabElement) return this._edgeTabElement;
1928
+ const tab = document.createElement("button");
1929
+ tab.setAttribute("type", "button");
1930
+ tab.setAttribute("aria-label", "Open assistant");
1931
+ tab.style.cssText = [
1932
+ "position:fixed",
1933
+ "top:50%",
1934
+ "right:0px",
1935
+ "transform:translateY(-50%)",
1936
+ `z-index:${SHELL_Z_INDEX + 2}`,
1937
+ `width:${EDGE_TAB_WIDTH}px`,
1938
+ `height:${EDGE_TAB_HEIGHT}px`,
1939
+ "display:flex",
1940
+ "align-items:center",
1941
+ "justify-content:center",
1942
+ "border-right:none",
1943
+ "border-radius:12px 0 0 12px",
1944
+ "padding:0",
1945
+ "outline:none",
1946
+ "transition:transform 200ms ease,background 150ms ease,color 150ms ease,box-shadow 200ms ease,border-color 150ms ease",
1947
+ "user-select:none",
1948
+ "-webkit-user-select:none"
1949
+ ].join(";");
1950
+ const dot = document.createElement("span");
1951
+ dot.style.cssText = [
1952
+ "position:absolute",
1953
+ "top:8px",
1954
+ "right:10px",
1955
+ "width:7px",
1956
+ "height:7px",
1957
+ "border-radius:50%",
1958
+ "transition:opacity 200ms ease"
1959
+ ].join(";");
1960
+ tab.appendChild(dot);
1961
+ const icon = document.createElement("span");
1962
+ icon.style.cssText = "display:flex;align-items:center;justify-content:center;pointer-events:none;";
1963
+ icon.innerHTML = ICON_MESSAGE;
1964
+ tab.appendChild(icon);
1965
+ tab.addEventListener("mouseenter", () => {
1966
+ const dark = this._isDarkMode();
1967
+ if (this._open && !this._isNarrowViewport) {
1968
+ tab.style.background = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
1969
+ tab.style.boxShadow = "none";
1970
+ tab.style.color = this._resolveColor("foreground");
1971
+ tab.style.cursor = "col-resize";
1972
+ } else {
1973
+ tab.style.transform = "translateY(-50%) translateX(-3px)";
1974
+ tab.style.boxShadow = dark ? "-4px 0 16px rgba(0,0,0,0.3)" : "-4px 0 16px rgba(0,0,0,0.12)";
1975
+ tab.style.color = this._resolveColor("foreground");
1976
+ tab.style.cursor = "pointer";
1977
+ }
1978
+ });
1979
+ tab.addEventListener("mouseleave", () => {
1980
+ this._applyEdgeTabVisual();
1981
+ });
1982
+ tab.addEventListener("mousedown", this._onEdgeTabMouseDown);
1983
+ tab.addEventListener("touchstart", this._onEdgeTabTouchStart, { passive: false });
1984
+ this._edgeTabElement = tab;
1985
+ return tab;
1986
+ }
1987
+ /** Apply visual style (colors, shadow) based on open state — no position change. */
1988
+ _applyEdgeTabVisual() {
1989
+ if (!this._edgeTabElement) return;
1990
+ const tab = this._edgeTabElement;
1991
+ const icon = tab.querySelector("span:last-child");
1992
+ const dot = tab.querySelector("span:first-child");
1993
+ const dark = this._isDarkMode();
1994
+ const bg = this._resolveColor("background");
1995
+ const borderColor = this._resolveColor("border");
1996
+ const mutedFg = this._resolveColor("mutedForeground");
1997
+ const primary = this._resolveColor("primary");
1998
+ tab.style.border = `1px solid ${borderColor}`;
1999
+ tab.style.borderRight = "none";
2000
+ if (this._open && !this._isNarrowViewport) {
2001
+ tab.setAttribute("aria-label", "Close assistant");
2002
+ tab.setAttribute("aria-expanded", "true");
2003
+ if (icon) icon.innerHTML = ICON_CHEVRON_RIGHT;
2004
+ if (dot) dot.style.opacity = "0";
2005
+ tab.style.background = bg;
2006
+ tab.style.boxShadow = "none";
2007
+ tab.style.backdropFilter = "none";
2008
+ tab.style.setProperty("-webkit-backdrop-filter", "none");
2009
+ tab.style.color = mutedFg;
2010
+ tab.style.cursor = "col-resize";
2011
+ tab.style.transform = "translateY(-50%)";
2012
+ } else {
2013
+ tab.setAttribute("aria-label", "Open assistant");
2014
+ tab.setAttribute("aria-expanded", "false");
2015
+ if (icon) icon.innerHTML = ICON_MESSAGE;
2016
+ if (dot) {
2017
+ dot.style.opacity = "1";
2018
+ dot.style.background = primary;
2019
+ }
2020
+ tab.style.background = dark ? "color-mix(in oklch, " + bg + ", white 4%)" : "color-mix(in oklch, " + bg + ", transparent 5%)";
2021
+ tab.style.boxShadow = dark ? "-2px 0 12px rgba(0,0,0,0.25)" : "-2px 0 12px rgba(0,0,0,0.08)";
2022
+ tab.style.backdropFilter = "blur(8px)";
2023
+ tab.style.setProperty("-webkit-backdrop-filter", "blur(8px)");
2024
+ tab.style.color = mutedFg;
2025
+ tab.style.cursor = "pointer";
2026
+ tab.style.transform = "translateY(-50%)";
2027
+ }
2028
+ }
2029
+ /** Set the tab's `right` position immediately (no transition). */
2030
+ _positionEdgeTab(rightPx) {
2031
+ if (this._edgeTabElement) this._edgeTabElement.style.right = `${rightPx}px`;
2032
+ }
2033
+ _showEdgeTab() {
2034
+ const tab = this._ensureEdgeTab();
2035
+ if (!tab.parentNode) document.body.appendChild(tab);
2036
+ setStyleValue(tab, "display", "flex");
2037
+ if (this._open && !this._isNarrowViewport) this._positionEdgeTab(this._currentPanelWidth - 1);
2038
+ else this._positionEdgeTab(0);
2039
+ this._applyEdgeTabVisual();
2040
+ }
2041
+ _hideEdgeTab() {
2042
+ if (this._edgeTabElement) setStyleValue(this._edgeTabElement, "display", "none");
2043
+ }
2044
+ _removeEdgeTab() {
2045
+ if (this._edgeTabElement) {
2046
+ this._edgeTabElement.remove();
2047
+ this._edgeTabElement = null;
2048
+ }
2049
+ }
2050
+ _onEdgeTabMouseDown = (e) => {
2051
+ e.preventDefault();
2052
+ this._edgeTabDragStartX = e.clientX;
2053
+ this._edgeTabDragging = false;
2054
+ document.addEventListener("mousemove", this._onEdgeTabMouseMove);
2055
+ document.addEventListener("mouseup", this._onEdgeTabMouseUp);
2056
+ };
2057
+ _onEdgeTabTouchStart = (e) => {
2058
+ if (e.touches.length !== 1) return;
2059
+ e.preventDefault();
2060
+ this._edgeTabDragStartX = e.touches[0].clientX;
2061
+ this._edgeTabDragging = false;
2062
+ document.addEventListener("touchmove", this._onEdgeTabTouchMove, { passive: false });
2063
+ document.addEventListener("touchend", this._onEdgeTabTouchEnd);
2064
+ };
2065
+ _onEdgeTabMouseMove = (e) => {
2066
+ const dx = Math.abs(e.clientX - this._edgeTabDragStartX);
2067
+ if (!this._edgeTabDragging && dx >= EDGE_TAB_DRAG_THRESHOLD_PX) {
2068
+ this._edgeTabDragging = true;
2069
+ this._promoteEdgeTabDragToResize(this._edgeTabDragStartX);
2070
+ }
2071
+ };
2072
+ _onEdgeTabTouchMove = (e) => {
2073
+ if (e.touches.length !== 1) return;
2074
+ e.preventDefault();
2075
+ const dx = Math.abs(e.touches[0].clientX - this._edgeTabDragStartX);
2076
+ if (!this._edgeTabDragging && dx >= EDGE_TAB_DRAG_THRESHOLD_PX) {
2077
+ this._edgeTabDragging = true;
2078
+ this._promoteEdgeTabDragToResize(this._edgeTabDragStartX);
2079
+ }
2080
+ };
2081
+ _onEdgeTabMouseUp = (_e) => {
2082
+ document.removeEventListener("mousemove", this._onEdgeTabMouseMove);
2083
+ document.removeEventListener("mouseup", this._onEdgeTabMouseUp);
2084
+ if (!this._edgeTabDragging) this.toggleOpen();
2085
+ };
2086
+ _onEdgeTabTouchEnd = (_e) => {
2087
+ document.removeEventListener("touchmove", this._onEdgeTabTouchMove);
2088
+ document.removeEventListener("touchend", this._onEdgeTabTouchEnd);
2089
+ if (!this._edgeTabDragging) this.toggleOpen();
2090
+ };
2091
+ /**
2092
+ * Once the edge tab drag crosses the threshold, hand off to the existing
2093
+ * resize machinery so the panel tracks the pointer smoothly.
2094
+ */
2095
+ _promoteEdgeTabDragToResize(startX) {
2096
+ if (!this._open || this._isNarrowViewport) {
2097
+ this._edgeTabDragging = false;
1850
2098
  return;
1851
2099
  }
1852
- setStyleValue(this, "position", "relative");
1853
- setStyleValue(this, "flex", "0 0 0px");
1854
- setStyleValue(this, "width", "0px");
1855
- setStyleValue(this, "min-width", "0px");
1856
- setStyleValue(this, "height", "0px");
1857
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1858
- resetStyleValue(this, "border-left");
1859
- setStyleValue(this, "transition", SHELL_LAYOUT_TRANSITION);
2100
+ this._resizeDragging = true;
2101
+ this._resizeStartX = startX;
2102
+ this._resizeStartWidth = this._currentPanelWidth;
2103
+ this._showResizeDragOverlay();
2104
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2105
+ setStyleValue(this, "transition", "none");
2106
+ document.body.style.transition = "none";
2107
+ document.body.style.cursor = "col-resize";
2108
+ document.body.style.userSelect = "none";
2109
+ document.addEventListener("mousemove", this._onResizeMove);
2110
+ document.addEventListener("mouseup", this._onResizeEnd);
2111
+ document.addEventListener("touchmove", this._onResizeTouchMove, { passive: false });
2112
+ document.addEventListener("touchend", this._onResizeTouchEnd);
2113
+ }
2114
+ /**
2115
+ * Run a rAF loop that reads the shell element's actual rendered width
2116
+ * and pins the edge tab to it. Used during flex layout transitions so
2117
+ * the tab tracks the animated flex-basis exactly, frame by frame.
2118
+ */
2119
+ _syncEdgeTabWithPanelTransition() {
2120
+ if (!this._edgeTabElement) return;
2121
+ const start = performance.now();
2122
+ const maxDuration = 300;
2123
+ const tick = () => {
2124
+ if (!this._edgeTabElement || !this._open) return;
2125
+ if (this._isParentFlex()) {
2126
+ const width = this.getBoundingClientRect().width;
2127
+ this._positionEdgeTab(width - 1);
2128
+ }
2129
+ if (performance.now() - start < maxDuration) requestAnimationFrame(tick);
2130
+ };
2131
+ requestAnimationFrame(tick);
2132
+ }
2133
+ _showResizeDragOverlay() {
2134
+ if (this._resizeDragOverlay) return;
2135
+ const overlay = document.createElement("div");
2136
+ overlay.style.cssText = [
2137
+ "position:fixed",
2138
+ "top:0",
2139
+ "left:0",
2140
+ "width:100vw",
2141
+ "height:100vh",
2142
+ "cursor:col-resize",
2143
+ `z-index:${SHELL_Z_INDEX + 1}`,
2144
+ "background:transparent",
2145
+ "user-select:none",
2146
+ "-webkit-user-select:none"
2147
+ ].join(";");
2148
+ document.body.appendChild(overlay);
2149
+ this._resizeDragOverlay = overlay;
2150
+ }
2151
+ _removeResizeDragOverlay() {
2152
+ if (this._resizeDragOverlay) {
2153
+ this._resizeDragOverlay.remove();
2154
+ this._resizeDragOverlay = null;
2155
+ }
2156
+ }
2157
+ _onResizeStart = (e) => {
2158
+ e.preventDefault();
2159
+ this._resizeDragging = true;
2160
+ this._resizeStartX = e.clientX;
2161
+ this._resizeStartWidth = this._currentPanelWidth;
2162
+ this._showResizeDragOverlay();
2163
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2164
+ setStyleValue(this, "transition", "none");
2165
+ document.body.style.transition = "none";
2166
+ document.body.style.cursor = "col-resize";
2167
+ document.body.style.userSelect = "none";
2168
+ document.addEventListener("mousemove", this._onResizeMove);
2169
+ document.addEventListener("mouseup", this._onResizeEnd);
2170
+ };
2171
+ _onResizeTouchStart = (e) => {
2172
+ if (e.touches.length !== 1) return;
2173
+ e.preventDefault();
2174
+ const touch = e.touches[0];
2175
+ this._resizeDragging = true;
2176
+ this._resizeStartX = touch.clientX;
2177
+ this._resizeStartWidth = this._currentPanelWidth;
2178
+ this._showResizeDragOverlay();
2179
+ if (this._agent) setStyleValue(this._agent, "transition", "none");
2180
+ setStyleValue(this, "transition", "none");
2181
+ document.body.style.transition = "none";
2182
+ document.addEventListener("touchmove", this._onResizeTouchMove, { passive: false });
2183
+ document.addEventListener("touchend", this._onResizeTouchEnd);
2184
+ };
2185
+ _onResizeMove = (e) => {
2186
+ if (!this._resizeDragging) return;
2187
+ const delta = this._resizeStartX - e.clientX;
2188
+ this._applyResizeWidth(this._resizeStartWidth + delta);
2189
+ };
2190
+ _onResizeTouchMove = (e) => {
2191
+ if (!this._resizeDragging || e.touches.length !== 1) return;
2192
+ e.preventDefault();
2193
+ const delta = this._resizeStartX - e.touches[0].clientX;
2194
+ this._applyResizeWidth(this._resizeStartWidth + delta);
2195
+ };
2196
+ _applyResizeWidth(rawWidth) {
2197
+ const width = Math.max(PANEL_MIN_WIDTH, Math.min(rawWidth, PANEL_MAX_WIDTH));
2198
+ this._currentPanelWidth = width;
2199
+ if (this._isParentFlex()) {
2200
+ setStyleValue(this, "flex", `0 0 ${width}px`);
2201
+ setStyleValue(this, "width", `${width}px`);
2202
+ setStyleValue(this, "min-width", `${width}px`);
2203
+ } else {
2204
+ if (this._agent) setStyleValue(this._agent, "width", `${width}px`);
2205
+ document.body.style.marginRight = `${width}px`;
2206
+ }
2207
+ if (this._resizeNubElement) {
2208
+ const rightPos = width - Math.floor(RESIZE_NUB_WIDTH / 2);
2209
+ setStyleValue(this._resizeNubElement, "right", `${rightPos}px`);
2210
+ }
2211
+ if (this._edgeTabElement) this._edgeTabElement.style.right = `${width - 1}px`;
2212
+ }
2213
+ _onResizeEnd = (e) => {
2214
+ this._finishResize(e.clientX);
2215
+ document.removeEventListener("mousemove", this._onResizeMove);
2216
+ document.removeEventListener("mouseup", this._onResizeEnd);
2217
+ };
2218
+ _onResizeTouchEnd = (e) => {
2219
+ const x = e.changedTouches[0]?.clientX ?? this._resizeStartX;
2220
+ this._finishResize(x);
2221
+ document.removeEventListener("touchmove", this._onResizeTouchMove);
2222
+ document.removeEventListener("touchend", this._onResizeTouchEnd);
2223
+ };
2224
+ _finishResize(endX) {
2225
+ this._resizeDragging = false;
2226
+ this._removeResizeDragOverlay();
2227
+ document.body.style.cursor = "";
2228
+ document.body.style.userSelect = "";
2229
+ document.body.style.transition = "";
2230
+ const delta = this._resizeStartX - endX;
2231
+ const finalWidth = this._resizeStartWidth + delta;
2232
+ this._currentPanelWidth = Math.max(PANEL_MIN_WIDTH, Math.min(finalWidth, PANEL_MAX_WIDTH));
2233
+ this._applyDisplayModeAndLayout();
1860
2234
  }
1861
2235
  _composePipTransform(opts) {
1862
2236
  const parts = [];
1863
- if (this._pipPosition === "bottom-center") parts.push("translateX(-50%)");
2237
+ if (PIP_POSITION === "bottom-center") parts.push("translateX(-50%)");
1864
2238
  if (opts.scrollHide) parts.push(`translateY(${PIP_SCROLL_HIDE_OFFSET}px)`);
1865
2239
  return parts.length > 0 ? parts.join(" ") : "none";
1866
2240
  }
1867
- _applyAgentLayout(displayMode) {
2241
+ _applyAgentLayout(displayMode, prevMode) {
1868
2242
  if (!this._agent) return;
1869
2243
  if (displayMode === "inline" && !this._isNarrowViewport) {
1870
2244
  this._detachPipInteractionListeners();
@@ -1872,20 +2246,32 @@ var CharAgentShellElement = class extends HTMLElement {
1872
2246
  this._pipExpanded = false;
1873
2247
  this._pipAnimating = false;
1874
2248
  this._hidePipPill();
1875
- setStyleValue(this._agent, "position", "relative");
1876
- resetStyleValue(this._agent, "top");
1877
- resetStyleValue(this._agent, "right");
1878
- resetStyleValue(this._agent, "bottom");
1879
- resetStyleValue(this._agent, "left");
2249
+ if (this._isParentFlex()) {
2250
+ setStyleValue(this._agent, "position", "relative");
2251
+ resetStyleValue(this._agent, "top");
2252
+ resetStyleValue(this._agent, "right");
2253
+ resetStyleValue(this._agent, "bottom");
2254
+ resetStyleValue(this._agent, "left");
2255
+ setStyleValue(this._agent, "z-index", "auto");
2256
+ setStyleValue(this._agent, "width", "100%");
2257
+ setStyleValue(this._agent, "height", "100%");
2258
+ } else {
2259
+ setStyleValue(this._agent, "position", "fixed");
2260
+ setStyleValue(this._agent, "top", "0");
2261
+ setStyleValue(this._agent, "right", "0");
2262
+ resetStyleValue(this._agent, "bottom");
2263
+ resetStyleValue(this._agent, "left");
2264
+ setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
2265
+ setStyleValue(this._agent, "width", `${this._currentPanelWidth}px`);
2266
+ setStyleValue(this._agent, "height", "100dvh");
2267
+ setStyleValue(this._agent, "border-left", this._panelBorder());
2268
+ }
1880
2269
  resetStyleValue(this._agent, "transform");
1881
2270
  resetStyleValue(this._agent, "max-width");
1882
2271
  resetStyleValue(this._agent, "max-height");
1883
- setStyleValue(this._agent, "z-index", "auto");
1884
- setStyleValue(this._agent, "width", "100%");
1885
- setStyleValue(this._agent, "height", "100%");
1886
2272
  setStyleValue(this._agent, "border-radius", "0");
1887
2273
  setStyleValue(this._agent, "overflow", "hidden");
1888
- resetStyleValue(this._agent, "transition");
2274
+ setStyleValue(this._agent, "transition", this._resizeDragging ? "none" : "width 220ms ease");
1889
2275
  resetStyleValue(this._agent, "opacity");
1890
2276
  resetStyleValue(this._agent, "pointer-events");
1891
2277
  return;
@@ -1914,15 +2300,14 @@ var CharAgentShellElement = class extends HTMLElement {
1914
2300
  resetStyleValue(this._agent, "pointer-events");
1915
2301
  return;
1916
2302
  }
1917
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1918
- const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, this._pipWidth);
1919
- const collapsedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, Math.round(this._pipWidth * PIP_COLLAPSED_WIDTH_RATIO));
2303
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
2304
+ const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, PIP_WIDTH);
2305
+ const collapsedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, Math.round(PIP_WIDTH * PIP_COLLAPSED_WIDTH_RATIO));
1920
2306
  const targetWidth = this._pipExpanded ? expandedWidth : collapsedWidth;
1921
- const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, this._pipHeight);
1922
- const hasExplicitPipHeight = this.hasAttribute("pip-height");
2307
+ const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, PIP_HEIGHT);
1923
2308
  const measuredHeightFloor = typeof this._pipMeasuredHeight === "number" ? this._pipMeasuredHeight : 0;
1924
2309
  const expandedHeight = Math.max(collapsedHeight, measuredHeightFloor);
1925
- const collapsedPipHeight = hasExplicitPipHeight ? collapsedHeight : PIP_PILL_HEIGHT;
2310
+ const collapsedPipHeight = PIP_PILL_HEIGHT;
1926
2311
  const heightDelta = expandedHeight - collapsedPipHeight;
1927
2312
  let targetHeight;
1928
2313
  let targetBottom;
@@ -1933,13 +2318,14 @@ var CharAgentShellElement = class extends HTMLElement {
1933
2318
  targetHeight = collapsedPipHeight;
1934
2319
  targetBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1935
2320
  }
2321
+ const enteringFromPanel = prevMode != null && prevMode !== "pip" && this._healthState === "healthy";
1936
2322
  const composedTransform = this._composePipTransform({ scrollHide: this._pipHiddenByScroll });
2323
+ if (enteringFromPanel) setStyleValue(this._agent, "transition", "none");
1937
2324
  setStyleValue(this._agent, "position", "fixed");
1938
2325
  resetStyleValue(this._agent, "top");
1939
2326
  setStyleValue(this._agent, "right", pipPositionStyles.right);
1940
2327
  setStyleValue(this._agent, "left", pipPositionStyles.left);
1941
2328
  setStyleValue(this._agent, "bottom", targetBottom);
1942
- setStyleValue(this._agent, "transform", composedTransform);
1943
2329
  setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1944
2330
  setStyleValue(this._agent, "width", `min(${targetWidth}px, calc(100vw - 24px))`);
1945
2331
  setStyleValue(this._agent, "height", `${targetHeight}px`);
@@ -1947,7 +2333,22 @@ var CharAgentShellElement = class extends HTMLElement {
1947
2333
  setStyleValue(this._agent, "max-width", "100vw");
1948
2334
  setStyleValue(this._agent, "border-radius", "16px");
1949
2335
  setStyleValue(this._agent, "overflow", "hidden");
1950
- setStyleValue(this._agent, "transition", this._healthState === "healthy" ? PIP_MORPH_TRANSITION : "none");
2336
+ resetStyleValue(this._agent, "border-left");
2337
+ if (enteringFromPanel) {
2338
+ setStyleValue(this._agent, "transform", this._composePipTransform({ scrollHide: true }));
2339
+ setStyleValue(this._agent, "opacity", "0");
2340
+ this._agent.offsetHeight;
2341
+ requestAnimationFrame(() => {
2342
+ if (!this._agent) return;
2343
+ setStyleValue(this._agent, "transition", PIP_MORPH_TRANSITION);
2344
+ setStyleValue(this._agent, "transform", composedTransform);
2345
+ setStyleValue(this._agent, "opacity", "1");
2346
+ });
2347
+ } else {
2348
+ setStyleValue(this._agent, "transform", composedTransform);
2349
+ setStyleValue(this._agent, "transition", this._healthState === "healthy" ? PIP_MORPH_TRANSITION : "none");
2350
+ resetStyleValue(this._agent, "opacity");
2351
+ }
1951
2352
  if (this._healthState === "healthy") {
1952
2353
  const pill = this._ensurePipPill();
1953
2354
  if (!pill.parentNode) this.appendChild(pill);
@@ -1956,6 +2357,16 @@ var CharAgentShellElement = class extends HTMLElement {
1956
2357
  this._attachScrollHideListener();
1957
2358
  this._attachSwipeGestureListeners();
1958
2359
  this._attachWheelFallbackListener();
2360
+ if (enteringFromPanel) {
2361
+ pill.style.opacity = "0";
2362
+ pill.style.pointerEvents = "none";
2363
+ pill.style.transition = "none";
2364
+ requestAnimationFrame(() => {
2365
+ pill.style.transition = PIP_MORPH_TRANSITION;
2366
+ pill.style.opacity = "1";
2367
+ pill.style.pointerEvents = "";
2368
+ });
2369
+ }
1959
2370
  this._applyPipVisualState();
1960
2371
  }
1961
2372
  }
@@ -1965,7 +2376,7 @@ var CharAgentShellElement = class extends HTMLElement {
1965
2376
  pill.setAttribute("role", "button");
1966
2377
  pill.setAttribute("aria-label", "Open assistant");
1967
2378
  pill.setAttribute("tabindex", "0");
1968
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
2379
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
1969
2380
  pill.style.cssText = [
1970
2381
  "position:fixed",
1971
2382
  `left:${pipPositionStyles.left}`,
@@ -2351,6 +2762,7 @@ var CharAgentShellElement = class extends HTMLElement {
2351
2762
  this._pipExpanded = false;
2352
2763
  this._pipAnimating = false;
2353
2764
  this._hidePipPill();
2765
+ this._hideEdgeTab();
2354
2766
  if (this._agent) this._agent.removeAttribute("display-mode");
2355
2767
  this._resetHostLayoutStyles();
2356
2768
  this._resetAgentLayoutStyles();
@@ -2396,7 +2808,7 @@ var CharAgentShellElement = class extends HTMLElement {
2396
2808
  this._revealFrameId = null;
2397
2809
  setStyleValue(this, "opacity", "1");
2398
2810
  window.setTimeout(() => {
2399
- if (this._healthState === "healthy") resetStyleValue(this, "will-change");
2811
+ resetStyleValue(this, "will-change");
2400
2812
  }, SHELL_REVEAL_FADE_MS);
2401
2813
  });
2402
2814
  }
@@ -2448,6 +2860,7 @@ var CharAgentShellElement = class extends HTMLElement {
2448
2860
  return;
2449
2861
  }
2450
2862
  this._open = nextOpen;
2863
+ if (!nextOpen) this._currentPanelWidth = PANEL_WIDTH;
2451
2864
  if (options.reflect) this._reflectOpenAttribute(nextOpen);
2452
2865
  this._applyDisplayModeAndLayout();
2453
2866
  if (options.emit) this.dispatchEvent(new CustomEvent(SHELL_OPEN_CHANGE_EVENT, {
@@ -2521,6 +2934,16 @@ function registerCharShell(tagName = "char-agent-shell") {
2521
2934
  }
2522
2935
  registerCharShell();
2523
2936
 
2937
+ //#endregion
2938
+ //#region src/index.ts
2939
+ /**
2940
+ * @packageDocumentation
2941
+ * @mcp-b/char
2942
+ *
2943
+ * Host entry point (auto-registers <char-agent> and <char-agent-shell>).
2944
+ * This package root is intentionally host-only.
2945
+ */
2946
+
2524
2947
  //#endregion
2525
2948
  export { DEFAULT_AVAILABLE_DISPLAY_MODES, isDisplayMode, isExpandedDisplayMode, registerChar, registerCharShell, resolvePolicyDisplayMode, resolveSupportedDisplayMode };
2526
2949
  //# sourceMappingURL=index.js.map