@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.
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,22 +1093,15 @@ var CharAgentElement = class extends HTMLElement {
1120
1093
  */
1121
1094
  _postAuth(iframeOrigin) {
1122
1095
  if (!this._pendingAuth || !this._iframe?.contentWindow) return;
1123
- if (!this._pendingAuth.ticketAuth && !this._pendingAuth.anthropicApiKey && !this._pendingAuth.idToken) {
1124
- console.error("[Char] _postAuth called but _pendingAuth has no auth credentials");
1096
+ if (!this._pendingAuth.publishableKey) {
1097
+ console.error("[Char] _postAuth called but _pendingAuth has no publishableKey");
1125
1098
  this._pendingAuth = null;
1126
1099
  return;
1127
1100
  }
1128
- const message = this._pendingAuth.ticketAuth ? {
1129
- type: "char-auth",
1130
- ticketAuth: this._pendingAuth.ticketAuth
1131
- } : this._pendingAuth.anthropicApiKey ? {
1101
+ const message = {
1132
1102
  type: "char-auth",
1133
- anthropicApiKey: this._pendingAuth.anthropicApiKey
1134
- } : {
1135
- type: "char-auth",
1136
- idToken: this._pendingAuth.idToken,
1137
- clientId: this._pendingAuth.clientId,
1138
- organizationId: this._pendingAuth.organizationId
1103
+ publishableKey: this._pendingAuth.publishableKey,
1104
+ idToken: this._pendingAuth.idToken
1139
1105
  };
1140
1106
  this._iframe.contentWindow.postMessage(message, iframeOrigin);
1141
1107
  }
@@ -1282,36 +1248,6 @@ var CharAgentElement = class extends HTMLElement {
1282
1248
  this._containerResizeObserver.observe(this);
1283
1249
  }
1284
1250
  /**
1285
- * Resolve devMode from property (React 19) or attribute.
1286
- *
1287
- * @returns Parsed dev-mode configuration when valid.
1288
- */
1289
- _resolveDevMode() {
1290
- if (this.devMode && typeof this.devMode === "object") return this.devMode;
1291
- const devModeAttr = this.getAttribute("dev-mode");
1292
- if (!devModeAttr) return void 0;
1293
- try {
1294
- const parsed = JSON.parse(devModeAttr);
1295
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1296
- const msg = `dev-mode attribute must be a JSON object, got: ${Array.isArray(parsed) ? "array" : typeof parsed}`;
1297
- console.warn(`[Char] ${msg}`);
1298
- this._emitCharError("INVALID_DEV_MODE", msg);
1299
- return;
1300
- }
1301
- const obj = parsed;
1302
- return {
1303
- anthropicApiKey: typeof obj.anthropicApiKey === "string" ? obj.anthropicApiKey : void 0,
1304
- openaiApiKey: typeof obj.openaiApiKey === "string" ? obj.openaiApiKey : void 0,
1305
- useLocalApi: typeof obj.useLocalApi === "boolean" ? obj.useLocalApi : void 0
1306
- };
1307
- } catch (e) {
1308
- const msg = `Failed to parse dev-mode attribute as JSON: ${e instanceof Error ? e.message : String(e)}`;
1309
- console.warn(`[Char] ${msg}`);
1310
- this._emitCharError("INVALID_DEV_MODE", msg);
1311
- return;
1312
- }
1313
- }
1314
- /**
1315
1251
  * Resolve apiBase override from property (React 19) or attribute.
1316
1252
  *
1317
1253
  * @returns Sanitized API base URL without trailing slash.
@@ -1340,22 +1276,36 @@ registerChar();
1340
1276
  //#region src/shell-component.ts
1341
1277
  const SHELL_OPEN_CHANGE_EVENT = "char-shell-open-change";
1342
1278
  const SHELL_Z_INDEX = 2147483e3;
1343
- const DEFAULT_FULLSCREEN_BREAKPOINT = 1024;
1344
- const DEFAULT_PANEL_WIDTH = 420;
1345
- const DEFAULT_PIP_WIDTH = 300;
1346
- const DEFAULT_PIP_HEIGHT = 96;
1347
- 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";
1348
1284
  const SHELL_HEALTH_TIMEOUT_MS = 1e4;
1349
- const PANEL_BORDER = "1px solid rgba(15, 23, 42, 0.12)";
1350
1285
  const SAFE_BOTTOM = "calc(16px + env(safe-area-inset-bottom, 0px))";
1351
- 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";
1352
1302
  const SHELL_REVEAL_FADE_MS = 280;
1353
1303
  const PIP_MIN_COLLAPSED_WIDTH = 70;
1354
1304
  const PIP_MIN_COLLAPSED_HEIGHT = 30;
1355
1305
  const PIP_MAX_MEASURED_HEIGHT = 320;
1356
1306
  const PIP_COLLAPSE_DELAY_MS = 400;
1357
1307
  const PIP_PILL_HEIGHT = 40;
1358
- 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";
1359
1309
  const PIP_MORPH_DURATION_MS = 280;
1360
1310
  const PIP_SCROLL_HIDE_OFFSET = 100;
1361
1311
  const PIP_COLLAPSED_WIDTH_RATIO = .75;
@@ -1363,23 +1313,14 @@ const PIP_SWIPE_THRESHOLD = 50;
1363
1313
  const PIP_SCROLL_DIRECTION_THRESHOLD = 40;
1364
1314
  const WHEEL_DELTA_LINE_MULTIPLIER = 15;
1365
1315
  const WHEEL_DELTA_PAGE_MULTIPLIER = 300;
1366
- function normalizePositiveInteger(value, fallback) {
1367
- if (!Number.isFinite(value) || value <= 0) {
1368
- console.warn(`[Char] Invalid value ${value}, using fallback ${fallback}`);
1369
- return fallback;
1370
- }
1371
- return Math.round(value);
1372
- }
1373
- function parsePositiveIntegerAttribute(value, fallback) {
1374
- if (value === null || value.trim() === "") return fallback;
1375
- const parsed = Number.parseInt(value, 10);
1376
- if (!Number.isFinite(parsed)) return fallback;
1377
- return normalizePositiveInteger(parsed, fallback);
1378
- }
1379
- function normalizePipPosition(value) {
1380
- if (value === "bottom-right" || value === "bottom-left") return value;
1381
- return DEFAULT_PIP_POSITION;
1382
- }
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>`;
1383
1324
  function getPipPositionStyles(position) {
1384
1325
  if (position === "bottom-right") return {
1385
1326
  left: "auto",
@@ -1436,25 +1377,28 @@ function sanitizeShellHostContext(hostContext) {
1436
1377
  var CharAgentShellElement = class extends HTMLElement {
1437
1378
  static observedAttributes = [
1438
1379
  "open",
1439
- "fullscreen-breakpoint",
1440
- "panel-width",
1441
- "pip-position",
1442
- "pip-width",
1443
- "pip-height",
1444
- "dev-mode",
1445
1380
  "enable-debug-tools",
1446
1381
  "api-base"
1447
1382
  ];
1448
1383
  _agent = null;
1449
1384
  _open = false;
1450
- _fullscreenBreakpoint = DEFAULT_FULLSCREEN_BREAKPOINT;
1451
- _panelWidth = DEFAULT_PANEL_WIDTH;
1452
- _pipWidth = DEFAULT_PIP_WIDTH;
1453
- _pipHeight = DEFAULT_PIP_HEIGHT;
1454
- _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;
1455
1396
  _isNarrowViewport = false;
1456
1397
  _mediaQuery = null;
1457
1398
  _mediaQueryHandler = null;
1399
+ _themeObserver = null;
1400
+ _themeMediaQuery = null;
1401
+ _themeMediaHandler = null;
1458
1402
  _pendingConnectOptions = null;
1459
1403
  _pendingHostContext = {};
1460
1404
  _isApplyingOpenAttribute = false;
@@ -1472,6 +1416,7 @@ var CharAgentShellElement = class extends HTMLElement {
1472
1416
  _pipAnimationTimeoutId = null;
1473
1417
  _pendingPipMeasuredHeight = null;
1474
1418
  _tapOutsideListener = null;
1419
+ _lastAppliedDisplayMode = null;
1475
1420
  _pipHiddenByScroll = false;
1476
1421
  _scrollListener = null;
1477
1422
  _scrollRafId = null;
@@ -1491,75 +1436,36 @@ var CharAgentShellElement = class extends HTMLElement {
1491
1436
  set open(value) {
1492
1437
  this.setOpen(value);
1493
1438
  }
1494
- get fullscreenBreakpoint() {
1495
- return this._fullscreenBreakpoint;
1496
- }
1497
- set fullscreenBreakpoint(value) {
1498
- const normalized = normalizePositiveInteger(value, DEFAULT_FULLSCREEN_BREAKPOINT);
1499
- if (normalized === this._fullscreenBreakpoint) return;
1500
- this._fullscreenBreakpoint = normalized;
1501
- this.setAttribute("fullscreen-breakpoint", String(normalized));
1502
- this._setupViewportObserver();
1503
- this._applyDisplayModeAndLayout();
1504
- }
1505
- get panelWidth() {
1506
- return this._panelWidth;
1507
- }
1508
- set panelWidth(value) {
1509
- const normalized = normalizePositiveInteger(value, DEFAULT_PANEL_WIDTH);
1510
- if (normalized === this._panelWidth) return;
1511
- this._panelWidth = normalized;
1512
- this.setAttribute("panel-width", String(normalized));
1513
- this._applyDisplayModeAndLayout();
1514
- }
1515
- get pipWidth() {
1516
- return this._pipWidth;
1517
- }
1518
- set pipWidth(value) {
1519
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_WIDTH);
1520
- if (normalized === this._pipWidth) return;
1521
- this._pipWidth = normalized;
1522
- this.setAttribute("pip-width", String(normalized));
1523
- this._applyDisplayModeAndLayout();
1524
- }
1525
- get pipHeight() {
1526
- return this._pipHeight;
1527
- }
1528
- set pipHeight(value) {
1529
- const normalized = normalizePositiveInteger(value, DEFAULT_PIP_HEIGHT);
1530
- if (normalized === this._pipHeight) return;
1531
- this._pipHeight = normalized;
1532
- this.setAttribute("pip-height", String(normalized));
1533
- this._applyDisplayModeAndLayout();
1534
- }
1535
- get pipPosition() {
1536
- return this._pipPosition;
1537
- }
1538
- set pipPosition(value) {
1539
- const normalized = normalizePipPosition(value);
1540
- if (normalized === this._pipPosition) return;
1541
- this._pipPosition = normalized;
1542
- this.setAttribute("pip-position", normalized);
1543
- this._applyDisplayModeAndLayout();
1544
- }
1545
1439
  connectedCallback() {
1546
1440
  this._upgradeProperty("open");
1547
- this._upgradeProperty("fullscreenBreakpoint");
1548
- this._upgradeProperty("panelWidth");
1549
- this._upgradeProperty("pipPosition");
1550
- this._upgradeProperty("pipWidth");
1551
- this._upgradeProperty("pipHeight");
1552
- this._upgradeProperty("devMode");
1553
1441
  this._upgradeProperty("apiBase");
1554
1442
  registerChar();
1555
1443
  this._ensureAgent();
1556
1444
  this._readConfigFromAttributes();
1557
1445
  this._syncForwardedAttributes();
1558
1446
  this._setupViewportObserver();
1447
+ this._setupThemeObserver();
1559
1448
  this._applyDisplayModeAndLayout();
1560
1449
  this._flushPendingConnectAndContext();
1561
1450
  }
1562
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
+ }
1563
1469
  this._stopHealthWait();
1564
1470
  this._cancelRevealFade();
1565
1471
  this._teardownViewportObserver();
@@ -1588,48 +1494,6 @@ var CharAgentShellElement = class extends HTMLElement {
1588
1494
  });
1589
1495
  return;
1590
1496
  }
1591
- case "fullscreen-breakpoint": {
1592
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_FULLSCREEN_BREAKPOINT);
1593
- if (next !== this._fullscreenBreakpoint) {
1594
- this._fullscreenBreakpoint = next;
1595
- this._setupViewportObserver();
1596
- this._applyDisplayModeAndLayout();
1597
- }
1598
- return;
1599
- }
1600
- case "panel-width": {
1601
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PANEL_WIDTH);
1602
- if (next !== this._panelWidth) {
1603
- this._panelWidth = next;
1604
- this._applyDisplayModeAndLayout();
1605
- }
1606
- return;
1607
- }
1608
- case "pip-width": {
1609
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_WIDTH);
1610
- if (next !== this._pipWidth) {
1611
- this._pipWidth = next;
1612
- this._applyDisplayModeAndLayout();
1613
- }
1614
- return;
1615
- }
1616
- case "pip-height": {
1617
- const next = parsePositiveIntegerAttribute(newValue, DEFAULT_PIP_HEIGHT);
1618
- if (next !== this._pipHeight) {
1619
- this._pipHeight = next;
1620
- this._applyDisplayModeAndLayout();
1621
- }
1622
- return;
1623
- }
1624
- case "pip-position": {
1625
- const next = normalizePipPosition(newValue);
1626
- if (next !== this._pipPosition) {
1627
- this._pipPosition = next;
1628
- this._applyDisplayModeAndLayout();
1629
- }
1630
- return;
1631
- }
1632
- case "dev-mode":
1633
1497
  case "enable-debug-tools":
1634
1498
  case "api-base":
1635
1499
  this._syncForwardedAttributes();
@@ -1639,7 +1503,7 @@ var CharAgentShellElement = class extends HTMLElement {
1639
1503
  /**
1640
1504
  * Connects shell authentication and starts availability health checks.
1641
1505
  *
1642
- * @param options - Authentication payload for either ID token or ticket auth.
1506
+ * @param options - Authentication payload (`publishableKey` + optional `idToken`).
1643
1507
  * @returns `true` when connect is accepted by the inner element.
1644
1508
  */
1645
1509
  connect(options) {
@@ -1747,15 +1611,10 @@ var CharAgentShellElement = class extends HTMLElement {
1747
1611
  this._attachForwardedListeners();
1748
1612
  }
1749
1613
  _primeAgentBootConfig(agent) {
1750
- for (const name of [
1751
- "dev-mode",
1752
- "enable-debug-tools",
1753
- "api-base"
1754
- ]) {
1614
+ for (const name of ["enable-debug-tools", "api-base"]) {
1755
1615
  const value = this.getAttribute(name);
1756
1616
  if (value !== null) agent.setAttribute(name, value);
1757
1617
  }
1758
- agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1759
1618
  agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1760
1619
  }
1761
1620
  _attachForwardedListeners() {
@@ -1778,29 +1637,19 @@ var CharAgentShellElement = class extends HTMLElement {
1778
1637
  }
1779
1638
  _readConfigFromAttributes() {
1780
1639
  this._open = parseBooleanAttribute(this.getAttribute("open"));
1781
- this._fullscreenBreakpoint = parsePositiveIntegerAttribute(this.getAttribute("fullscreen-breakpoint"), DEFAULT_FULLSCREEN_BREAKPOINT);
1782
- this._panelWidth = parsePositiveIntegerAttribute(this.getAttribute("panel-width"), DEFAULT_PANEL_WIDTH);
1783
- this._pipWidth = parsePositiveIntegerAttribute(this.getAttribute("pip-width"), DEFAULT_PIP_WIDTH);
1784
- this._pipHeight = parsePositiveIntegerAttribute(this.getAttribute("pip-height"), DEFAULT_PIP_HEIGHT);
1785
- this._pipPosition = normalizePipPosition(this.getAttribute("pip-position"));
1786
1640
  }
1787
1641
  _syncForwardedAttributes() {
1788
1642
  if (!this._agent) return;
1789
- for (const name of [
1790
- "dev-mode",
1791
- "enable-debug-tools",
1792
- "api-base"
1793
- ]) {
1643
+ for (const name of ["enable-debug-tools", "api-base"]) {
1794
1644
  const value = this.getAttribute(name);
1795
1645
  if (value === null) this._agent.removeAttribute(name);
1796
1646
  else this._agent.setAttribute(name, value);
1797
1647
  }
1798
- this._agent.devMode = this.devMode && typeof this.devMode === "object" ? this.devMode : void 0;
1799
1648
  this._agent.apiBase = typeof this.apiBase === "string" ? this.apiBase : void 0;
1800
1649
  }
1801
1650
  _setupViewportObserver() {
1802
1651
  if (typeof window === "undefined") return;
1803
- const query = `(max-width: ${this._fullscreenBreakpoint - 1}px)`;
1652
+ const query = `(max-width: ${FULLSCREEN_BREAKPOINT - 1}px)`;
1804
1653
  if (this._mediaQuery?.media === query) {
1805
1654
  this._isNarrowViewport = this._mediaQuery.matches;
1806
1655
  return;
@@ -1838,38 +1687,558 @@ var CharAgentShellElement = class extends HTMLElement {
1838
1687
  if (displayMode !== "pip") {
1839
1688
  if (this._pipMeasuredHeight !== null) this._pipMeasuredHeight = null;
1840
1689
  }
1690
+ const prevMode = this._lastAppliedDisplayMode;
1691
+ this._lastAppliedDisplayMode = displayMode;
1841
1692
  this._agent.setAttribute("display-mode", displayMode);
1842
1693
  this._applyHostLayout(displayMode);
1843
- 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
+ }
1844
1759
  }
1845
1760
  _applyHostLayout(displayMode) {
1761
+ const useFlexLayout = this._isParentFlex();
1846
1762
  if (displayMode === "inline" && !this._isNarrowViewport) {
1847
- setStyleValue(this, "position", "relative");
1848
- setStyleValue(this, "flex", `0 0 ${this._panelWidth}px`);
1849
- setStyleValue(this, "width", `${this._panelWidth}px`);
1850
- setStyleValue(this, "min-width", `${this._panelWidth}px`);
1851
- setStyleValue(this, "height", "100%");
1852
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1853
- setStyleValue(this, "border-left", PANEL_BORDER);
1854
- 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;
1855
2098
  return;
1856
2099
  }
1857
- setStyleValue(this, "position", "relative");
1858
- setStyleValue(this, "flex", "0 0 0px");
1859
- setStyleValue(this, "width", "0px");
1860
- setStyleValue(this, "min-width", "0px");
1861
- setStyleValue(this, "height", "0px");
1862
- setStyleValue(this, "z-index", String(SHELL_Z_INDEX));
1863
- resetStyleValue(this, "border-left");
1864
- 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();
1865
2234
  }
1866
2235
  _composePipTransform(opts) {
1867
2236
  const parts = [];
1868
- if (this._pipPosition === "bottom-center") parts.push("translateX(-50%)");
2237
+ if (PIP_POSITION === "bottom-center") parts.push("translateX(-50%)");
1869
2238
  if (opts.scrollHide) parts.push(`translateY(${PIP_SCROLL_HIDE_OFFSET}px)`);
1870
2239
  return parts.length > 0 ? parts.join(" ") : "none";
1871
2240
  }
1872
- _applyAgentLayout(displayMode) {
2241
+ _applyAgentLayout(displayMode, prevMode) {
1873
2242
  if (!this._agent) return;
1874
2243
  if (displayMode === "inline" && !this._isNarrowViewport) {
1875
2244
  this._detachPipInteractionListeners();
@@ -1877,20 +2246,32 @@ var CharAgentShellElement = class extends HTMLElement {
1877
2246
  this._pipExpanded = false;
1878
2247
  this._pipAnimating = false;
1879
2248
  this._hidePipPill();
1880
- setStyleValue(this._agent, "position", "relative");
1881
- resetStyleValue(this._agent, "top");
1882
- resetStyleValue(this._agent, "right");
1883
- resetStyleValue(this._agent, "bottom");
1884
- 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
+ }
1885
2269
  resetStyleValue(this._agent, "transform");
1886
2270
  resetStyleValue(this._agent, "max-width");
1887
2271
  resetStyleValue(this._agent, "max-height");
1888
- setStyleValue(this._agent, "z-index", "auto");
1889
- setStyleValue(this._agent, "width", "100%");
1890
- setStyleValue(this._agent, "height", "100%");
1891
2272
  setStyleValue(this._agent, "border-radius", "0");
1892
2273
  setStyleValue(this._agent, "overflow", "hidden");
1893
- resetStyleValue(this._agent, "transition");
2274
+ setStyleValue(this._agent, "transition", this._resizeDragging ? "none" : "width 220ms ease");
1894
2275
  resetStyleValue(this._agent, "opacity");
1895
2276
  resetStyleValue(this._agent, "pointer-events");
1896
2277
  return;
@@ -1919,15 +2300,14 @@ var CharAgentShellElement = class extends HTMLElement {
1919
2300
  resetStyleValue(this._agent, "pointer-events");
1920
2301
  return;
1921
2302
  }
1922
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
1923
- const expandedWidth = Math.max(PIP_MIN_COLLAPSED_WIDTH, this._pipWidth);
1924
- 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));
1925
2306
  const targetWidth = this._pipExpanded ? expandedWidth : collapsedWidth;
1926
- const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, this._pipHeight);
1927
- const hasExplicitPipHeight = this.hasAttribute("pip-height");
2307
+ const collapsedHeight = Math.max(PIP_MIN_COLLAPSED_HEIGHT, PIP_HEIGHT);
1928
2308
  const measuredHeightFloor = typeof this._pipMeasuredHeight === "number" ? this._pipMeasuredHeight : 0;
1929
2309
  const expandedHeight = Math.max(collapsedHeight, measuredHeightFloor);
1930
- const collapsedPipHeight = hasExplicitPipHeight ? collapsedHeight : PIP_PILL_HEIGHT;
2310
+ const collapsedPipHeight = PIP_PILL_HEIGHT;
1931
2311
  const heightDelta = expandedHeight - collapsedPipHeight;
1932
2312
  let targetHeight;
1933
2313
  let targetBottom;
@@ -1938,13 +2318,14 @@ var CharAgentShellElement = class extends HTMLElement {
1938
2318
  targetHeight = collapsedPipHeight;
1939
2319
  targetBottom = heightDelta > 0 ? `calc(${heightDelta}px + 16px + env(safe-area-inset-bottom, 0px))` : SAFE_BOTTOM;
1940
2320
  }
2321
+ const enteringFromPanel = prevMode != null && prevMode !== "pip" && this._healthState === "healthy";
1941
2322
  const composedTransform = this._composePipTransform({ scrollHide: this._pipHiddenByScroll });
2323
+ if (enteringFromPanel) setStyleValue(this._agent, "transition", "none");
1942
2324
  setStyleValue(this._agent, "position", "fixed");
1943
2325
  resetStyleValue(this._agent, "top");
1944
2326
  setStyleValue(this._agent, "right", pipPositionStyles.right);
1945
2327
  setStyleValue(this._agent, "left", pipPositionStyles.left);
1946
2328
  setStyleValue(this._agent, "bottom", targetBottom);
1947
- setStyleValue(this._agent, "transform", composedTransform);
1948
2329
  setStyleValue(this._agent, "z-index", String(SHELL_Z_INDEX));
1949
2330
  setStyleValue(this._agent, "width", `min(${targetWidth}px, calc(100vw - 24px))`);
1950
2331
  setStyleValue(this._agent, "height", `${targetHeight}px`);
@@ -1952,7 +2333,22 @@ var CharAgentShellElement = class extends HTMLElement {
1952
2333
  setStyleValue(this._agent, "max-width", "100vw");
1953
2334
  setStyleValue(this._agent, "border-radius", "16px");
1954
2335
  setStyleValue(this._agent, "overflow", "hidden");
1955
- 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
+ }
1956
2352
  if (this._healthState === "healthy") {
1957
2353
  const pill = this._ensurePipPill();
1958
2354
  if (!pill.parentNode) this.appendChild(pill);
@@ -1961,6 +2357,16 @@ var CharAgentShellElement = class extends HTMLElement {
1961
2357
  this._attachScrollHideListener();
1962
2358
  this._attachSwipeGestureListeners();
1963
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
+ }
1964
2370
  this._applyPipVisualState();
1965
2371
  }
1966
2372
  }
@@ -1970,7 +2376,7 @@ var CharAgentShellElement = class extends HTMLElement {
1970
2376
  pill.setAttribute("role", "button");
1971
2377
  pill.setAttribute("aria-label", "Open assistant");
1972
2378
  pill.setAttribute("tabindex", "0");
1973
- const pipPositionStyles = getPipPositionStyles(this._pipPosition);
2379
+ const pipPositionStyles = getPipPositionStyles(PIP_POSITION);
1974
2380
  pill.style.cssText = [
1975
2381
  "position:fixed",
1976
2382
  `left:${pipPositionStyles.left}`,
@@ -2356,6 +2762,7 @@ var CharAgentShellElement = class extends HTMLElement {
2356
2762
  this._pipExpanded = false;
2357
2763
  this._pipAnimating = false;
2358
2764
  this._hidePipPill();
2765
+ this._hideEdgeTab();
2359
2766
  if (this._agent) this._agent.removeAttribute("display-mode");
2360
2767
  this._resetHostLayoutStyles();
2361
2768
  this._resetAgentLayoutStyles();
@@ -2401,7 +2808,7 @@ var CharAgentShellElement = class extends HTMLElement {
2401
2808
  this._revealFrameId = null;
2402
2809
  setStyleValue(this, "opacity", "1");
2403
2810
  window.setTimeout(() => {
2404
- if (this._healthState === "healthy") resetStyleValue(this, "will-change");
2811
+ resetStyleValue(this, "will-change");
2405
2812
  }, SHELL_REVEAL_FADE_MS);
2406
2813
  });
2407
2814
  }
@@ -2453,6 +2860,7 @@ var CharAgentShellElement = class extends HTMLElement {
2453
2860
  return;
2454
2861
  }
2455
2862
  this._open = nextOpen;
2863
+ if (!nextOpen) this._currentPanelWidth = PANEL_WIDTH;
2456
2864
  if (options.reflect) this._reflectOpenAttribute(nextOpen);
2457
2865
  this._applyDisplayModeAndLayout();
2458
2866
  if (options.emit) this.dispatchEvent(new CustomEvent(SHELL_OPEN_CHANGE_EVENT, {
@@ -2526,6 +2934,16 @@ function registerCharShell(tagName = "char-agent-shell") {
2526
2934
  }
2527
2935
  registerCharShell();
2528
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
+
2529
2947
  //#endregion
2530
2948
  export { DEFAULT_AVAILABLE_DISPLAY_MODES, isDisplayMode, isExpandedDisplayMode, registerChar, registerCharShell, resolvePolicyDisplayMode, resolveSupportedDisplayMode };
2531
2949
  //# sourceMappingURL=index.js.map