@skippr/live-agent-sdk 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,14 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
1
9
  // src/components/LiveAgent.tsx
2
10
  import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
3
- import { useCallback as useCallback6, useEffect as useEffect12, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
11
+ import { useCallback as useCallback7, useEffect as useEffect14, useMemo as useMemo5, useRef as useRef8, useState as useState9 } from "react";
4
12
 
5
13
  // src/context/LiveAgentContext.tsx
6
14
  import { createContext } from "react";
@@ -165,7 +173,14 @@ async function exchangeForBearerToken(appKey, userToken) {
165
173
  const { token } = await resp.json();
166
174
  return token;
167
175
  }
168
- function useSession({ agentId, authToken, appKey, userToken }) {
176
+ function useSession({
177
+ agentId,
178
+ captureMode = "screenshare",
179
+ agentControls,
180
+ authToken,
181
+ appKey,
182
+ userToken
183
+ }) {
169
184
  const [connection, setConnection] = useState2(null);
170
185
  const [shouldConnect, setShouldConnect] = useState2(false);
171
186
  const [isStarting, setIsStarting] = useState2(false);
@@ -193,6 +208,7 @@ function useSession({ agentId, authToken, appKey, userToken }) {
193
208
  stale = true;
194
209
  };
195
210
  }, [authToken, appKey, userToken]);
211
+ const highlightOptIn = agentControls?.highlight;
196
212
  const startSession = useCallback2(async () => {
197
213
  if (!bearerToken) {
198
214
  setError("No auth token available");
@@ -202,20 +218,27 @@ function useSession({ agentId, authToken, appKey, userToken }) {
202
218
  setError("");
203
219
  setErrorCode(null);
204
220
  let screenStream = null;
205
- try {
206
- screenStream = await navigator.mediaDevices.getDisplayMedia({
207
- video: { displaySurface: "browser" }
208
- });
209
- } catch {
210
- screenStream = null;
221
+ if (captureMode === "screenshare") {
222
+ try {
223
+ screenStream = await navigator.mediaDevices.getDisplayMedia({
224
+ video: { displaySurface: "browser" }
225
+ });
226
+ } catch {
227
+ screenStream = null;
228
+ }
211
229
  }
230
+ const requestAgentControls = highlightOptIn === true ? { highlight: true } : undefined;
212
231
  const headers = { Authorization: `Bearer ${bearerToken}` };
213
232
  try {
214
233
  const createResp = await fetch(`${API_URL2}/v1/sessions`, {
215
234
  method: "POST",
216
235
  credentials: "omit",
217
236
  headers: { "Content-Type": "application/json", ...headers },
218
- body: JSON.stringify({ agentId })
237
+ body: JSON.stringify({
238
+ agentId,
239
+ captureMode,
240
+ agentControls: requestAgentControls
241
+ })
219
242
  });
220
243
  if (!createResp.ok) {
221
244
  const body = await createResp.json().catch(() => null);
@@ -250,7 +273,7 @@ function useSession({ agentId, authToken, appKey, userToken }) {
250
273
  } finally {
251
274
  setIsStarting(false);
252
275
  }
253
- }, [agentId, bearerToken]);
276
+ }, [agentId, captureMode, highlightOptIn, bearerToken]);
254
277
  const disconnect = useCallback2(async () => {
255
278
  if (sessionId && bearerToken) {
256
279
  try {
@@ -283,6 +306,16 @@ function useSession({ agentId, authToken, appKey, userToken }) {
283
306
  pendingScreenStream
284
307
  };
285
308
  }
309
+
310
+ // src/lib/constants.ts
311
+ var SIDEBAR_WIDTH = 360;
312
+ var WIDGET_ROOT_ID = "skippr-sdk-root";
313
+ var REF_ATTR = "data-skippr-ref";
314
+ var PRIVATE_ATTR = "data-skippr-private";
315
+ var DOM_SNAPSHOT_TOPIC = "skippr.dom-snapshot";
316
+ var DOM_EVENTS_TOPIC = "skippr.dom-events";
317
+ var HIGHLIGHT_TOPIC = "skippr.highlight";
318
+ var NAME_MAX_CHARS = 80;
286
319
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/createLucideIcon.js
287
320
  import { forwardRef as forwardRef2, createElement as createElement3 } from "react";
288
321
 
@@ -513,7 +546,7 @@ var __iconNode16 = [
513
546
  ];
514
547
  var Send = createLucideIcon("send", __iconNode16);
515
548
  // src/hooks/useAgentVoiceState.ts
516
- import { useVoiceAssistant } from "@livekit/components-react";
549
+ import { useVoiceAssistant } from "@livekit/components-react/hooks";
517
550
  function useAgentVoiceState() {
518
551
  const { state } = useVoiceAssistant();
519
552
  return {
@@ -535,7 +568,7 @@ function useLiveAgent() {
535
568
  }
536
569
 
537
570
  // src/hooks/useMediaControls.ts
538
- import { useLocalParticipant } from "@livekit/components-react";
571
+ import { useLocalParticipant } from "@livekit/components-react/hooks";
539
572
  import { ScreenSharePresets } from "livekit-client";
540
573
  import { useCallback as useCallback3 } from "react";
541
574
  var SCREEN_SHARE_OPTIONS = {
@@ -682,7 +715,7 @@ function SpeakingBars() {
682
715
  }
683
716
 
684
717
  // src/components/AutoStartMedia.tsx
685
- import { useConnectionState, useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react";
718
+ import { useConnectionState, useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react/hooks";
686
719
  import { ConnectionState, Track } from "livekit-client";
687
720
  import { useEffect as useEffect3, useRef } from "react";
688
721
  function AutoStartMedia({ pendingScreenStream }) {
@@ -711,8 +744,1105 @@ function AutoStartMedia({ pendingScreenStream }) {
711
744
  return null;
712
745
  }
713
746
 
747
+ // src/components/DomCapture.tsx
748
+ import { useConnectionState as useConnectionState2, useLocalParticipant as useLocalParticipant3 } from "@livekit/components-react/hooks";
749
+ import { ConnectionState as ConnectionState2, ScreenSharePresets as ScreenSharePresets2, Track as Track2 } from "livekit-client";
750
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
751
+
752
+ // src/capture/a11yUtils.ts
753
+ var ROLE_BY_TAG = {
754
+ a: "link",
755
+ button: "button",
756
+ input: "input",
757
+ select: "select",
758
+ textarea: "textarea",
759
+ label: "label",
760
+ nav: "navigation",
761
+ main: "main",
762
+ header: "header",
763
+ footer: "footer",
764
+ aside: "complementary",
765
+ form: "form",
766
+ h1: "heading",
767
+ h2: "heading",
768
+ h3: "heading",
769
+ h4: "heading",
770
+ h5: "heading",
771
+ h6: "heading",
772
+ img: "image",
773
+ ul: "list",
774
+ ol: "list",
775
+ li: "listitem",
776
+ table: "table",
777
+ tr: "row",
778
+ td: "cell",
779
+ th: "columnheader",
780
+ video: "video",
781
+ iframe: "iframe",
782
+ canvas: "canvas",
783
+ audio: "audio",
784
+ embed: "embed",
785
+ object: "object"
786
+ };
787
+ function inferRole(element) {
788
+ const explicitRole = element.getAttribute("role");
789
+ if (explicitRole)
790
+ return explicitRole;
791
+ const tagName = element.tagName.toLowerCase();
792
+ if (tagName === "input") {
793
+ const inputType = element.type;
794
+ if (inputType === "checkbox")
795
+ return "checkbox";
796
+ if (inputType === "radio")
797
+ return "radio";
798
+ if (inputType === "submit" || inputType === "button")
799
+ return "button";
800
+ return "input";
801
+ }
802
+ return ROLE_BY_TAG[tagName] ?? null;
803
+ }
804
+ function accessibleName(element, options = {}) {
805
+ const ariaLabel = element.getAttribute("aria-label");
806
+ if (ariaLabel)
807
+ return ariaLabel.trim().slice(0, NAME_MAX_CHARS);
808
+ if (!options.quick) {
809
+ const labelledBy = element.getAttribute("aria-labelledby");
810
+ if (labelledBy) {
811
+ const labelElement = document.getElementById(labelledBy);
812
+ if (labelElement)
813
+ return (labelElement.textContent || "").trim().slice(0, NAME_MAX_CHARS);
814
+ }
815
+ if (element.tagName === "INPUT" || element.tagName === "SELECT" || element.tagName === "TEXTAREA") {
816
+ const elementId = element.id;
817
+ if (elementId) {
818
+ const associatedLabel = document.querySelector(`label[for="${CSS.escape(elementId)}"]`);
819
+ if (associatedLabel?.textContent) {
820
+ return associatedLabel.textContent.trim().slice(0, NAME_MAX_CHARS);
821
+ }
822
+ }
823
+ const placeholder = element.getAttribute("placeholder");
824
+ if (placeholder)
825
+ return placeholder.trim().slice(0, NAME_MAX_CHARS);
826
+ }
827
+ }
828
+ if (element.tagName === "IMG") {
829
+ const altText = element.getAttribute("alt");
830
+ if (altText)
831
+ return altText.trim().slice(0, NAME_MAX_CHARS);
832
+ }
833
+ const titleAttr = element.getAttribute("title");
834
+ if (titleAttr)
835
+ return titleAttr.trim().slice(0, NAME_MAX_CHARS);
836
+ const textContent = (element.textContent || "").replace(/\s+/g, " ").trim();
837
+ if (textContent)
838
+ return textContent.slice(0, NAME_MAX_CHARS);
839
+ return "";
840
+ }
841
+
842
+ // src/capture/a11yTree.ts
843
+ var REF_PREFIX = "r-";
844
+ var VALUE_MAX_CHARS = 80;
845
+ var HREF_MAX_CHARS = 60;
846
+ var PAGE_CONTENT_MAX_BYTES = 14000;
847
+ var NODE_LIMIT = 1500;
848
+ var SENSITIVE_IFRAME_HOSTS = [
849
+ "js.stripe.com",
850
+ "checkout.stripe.com",
851
+ "connect.stripe.com",
852
+ "plaid.com",
853
+ "production.plaid.com",
854
+ "cdn.plaid.com",
855
+ "auth0.com",
856
+ "login.auth0.com",
857
+ "recaptcha.net",
858
+ "www.google.com",
859
+ "hcaptcha.com",
860
+ "newassets.hcaptcha.com",
861
+ "challenges.cloudflare.com"
862
+ ];
863
+ var MASK_AUTOCOMPLETE_VALUES = new Set([
864
+ "cc-number",
865
+ "cc-exp",
866
+ "cc-exp-month",
867
+ "cc-exp-year",
868
+ "cc-csc",
869
+ "cc-name",
870
+ "cc-given-name",
871
+ "cc-family-name",
872
+ "current-password",
873
+ "new-password",
874
+ "one-time-code"
875
+ ]);
876
+ var MASK_INPUT_TYPES = new Set(["password", "hidden"]);
877
+ var MASK_NAME_PATTERN = /password|secret|token|api[_-]?key|ssn|tax[_-]?id|social[_-]?security|cvv|cvc|pin/i;
878
+ var refCounter = 0;
879
+ function generateNextRef() {
880
+ refCounter += 1;
881
+ return `${REF_PREFIX}${refCounter}`;
882
+ }
883
+ function advanceCounterPastExisting(ref) {
884
+ const parsedRefNumber = Number.parseInt(ref.slice(REF_PREFIX.length), 10);
885
+ if (Number.isFinite(parsedRefNumber) && parsedRefNumber > refCounter) {
886
+ refCounter = parsedRefNumber;
887
+ }
888
+ }
889
+ function intersectsViewport(rect, viewportWidth, viewportHeight) {
890
+ if (rect.width <= 0 || rect.height <= 0)
891
+ return false;
892
+ if (rect.bottom <= 0 || rect.top >= viewportHeight)
893
+ return false;
894
+ if (rect.right <= 0 || rect.left >= viewportWidth)
895
+ return false;
896
+ return true;
897
+ }
898
+ function isVisible(element, rect) {
899
+ if (!intersectsViewport(rect, window.innerWidth, window.innerHeight))
900
+ return false;
901
+ const style = window.getComputedStyle(element);
902
+ if (style.visibility === "hidden" || style.display === "none")
903
+ return false;
904
+ if (Number.parseFloat(style.opacity || "1") <= 0.01)
905
+ return false;
906
+ return true;
907
+ }
908
+ function isInteractive(element, role) {
909
+ if (role && role !== "heading" && role !== "image" && role !== "list" && role !== "listitem") {
910
+ const interactiveRoles = [
911
+ "button",
912
+ "link",
913
+ "input",
914
+ "select",
915
+ "textarea",
916
+ "checkbox",
917
+ "radio",
918
+ "tab",
919
+ "menuitem",
920
+ "option",
921
+ "switch",
922
+ "slider"
923
+ ];
924
+ if (interactiveRoles.includes(role))
925
+ return true;
926
+ }
927
+ if (element.tabIndex >= 0)
928
+ return true;
929
+ if (element.hasAttribute("onclick") || element.hasAttribute("contenteditable"))
930
+ return true;
931
+ return false;
932
+ }
933
+ function isBlindRegion(role) {
934
+ return role === "video" || role === "iframe" || role === "canvas" || role === "audio" || role === "embed" || role === "object";
935
+ }
936
+ function getIframeHost(element) {
937
+ const src = element.getAttribute("src");
938
+ if (!src)
939
+ return null;
940
+ try {
941
+ return new URL(src, window.location.href).host;
942
+ } catch {
943
+ return null;
944
+ }
945
+ }
946
+ function isSensitiveIframe(element) {
947
+ const host = getIframeHost(element);
948
+ if (!host)
949
+ return false;
950
+ return SENSITIVE_IFRAME_HOSTS.some((sensitiveHost) => host === sensitiveHost || host.endsWith(`.${sensitiveHost}`));
951
+ }
952
+ function isSameOriginIframe(element) {
953
+ const src = element.getAttribute("src");
954
+ if (!src)
955
+ return true;
956
+ try {
957
+ return new URL(src, window.location.href).origin === window.location.origin;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+ function nearestHeadingText(element) {
963
+ let currentNode = element;
964
+ while (currentNode && currentNode !== document.body) {
965
+ let previousSibling = currentNode.previousElementSibling;
966
+ while (previousSibling) {
967
+ if (/^H[1-6]$/.test(previousSibling.tagName) && previousSibling.textContent) {
968
+ return previousSibling.textContent.trim().slice(0, NAME_MAX_CHARS);
969
+ }
970
+ previousSibling = previousSibling.previousElementSibling;
971
+ }
972
+ currentNode = currentNode.parentElement;
973
+ }
974
+ return null;
975
+ }
976
+ function shouldMaskValue(input) {
977
+ if (input.tagName === "INPUT") {
978
+ const inputElement = input;
979
+ if (MASK_INPUT_TYPES.has(inputElement.type))
980
+ return true;
981
+ const autocompleteAttr = (inputElement.getAttribute("autocomplete") || "").toLowerCase().trim();
982
+ if (MASK_AUTOCOMPLETE_VALUES.has(autocompleteAttr))
983
+ return true;
984
+ }
985
+ const fieldName = (input.getAttribute("name") || "").trim();
986
+ const fieldId = (input.getAttribute("id") || "").trim();
987
+ if (MASK_NAME_PATTERN.test(fieldName) || MASK_NAME_PATTERN.test(fieldId))
988
+ return true;
989
+ return false;
990
+ }
991
+ function escapeAttributeValue(value) {
992
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ");
993
+ }
994
+ function extractAttrs(element, role) {
995
+ const attributes = [];
996
+ if (role === "link") {
997
+ const href = element.getAttribute("href");
998
+ if (href)
999
+ attributes.push(`href="${escapeAttributeValue(href.slice(0, HREF_MAX_CHARS))}"`);
1000
+ }
1001
+ if (role === "input" || role === "checkbox" || role === "radio") {
1002
+ const input = element;
1003
+ if (input.type && input.type !== "text")
1004
+ attributes.push(`type="${input.type}"`);
1005
+ if (shouldMaskValue(input)) {
1006
+ attributes.push("valueMasked");
1007
+ } else if (input.value && role === "input") {
1008
+ attributes.push(`value="${escapeAttributeValue(input.value.slice(0, VALUE_MAX_CHARS))}"`);
1009
+ }
1010
+ if (input.checked)
1011
+ attributes.push("checked");
1012
+ }
1013
+ if (role === "textarea") {
1014
+ const textarea = element;
1015
+ if (shouldMaskValue(textarea)) {
1016
+ attributes.push("valueMasked");
1017
+ } else if (textarea.value) {
1018
+ attributes.push(`value="${escapeAttributeValue(textarea.value.slice(0, VALUE_MAX_CHARS))}"`);
1019
+ }
1020
+ }
1021
+ if (role === "select") {
1022
+ const select = element;
1023
+ const selectedOption = select.options[select.selectedIndex];
1024
+ if (selectedOption?.value?.trim()) {
1025
+ attributes.push(`selected="${escapeAttributeValue(selectedOption.value.slice(0, VALUE_MAX_CHARS))}"`);
1026
+ }
1027
+ }
1028
+ if (role === "heading") {
1029
+ const headingLevel = element.tagName.match(/^H([1-6])$/)?.[1];
1030
+ if (headingLevel)
1031
+ attributes.push(`level="${headingLevel}"`);
1032
+ }
1033
+ if (role === "video" || role === "audio") {
1034
+ const mediaElement = element;
1035
+ try {
1036
+ if (Number.isFinite(mediaElement.currentTime)) {
1037
+ attributes.push(`currentTime="${mediaElement.currentTime.toFixed(1)}"`);
1038
+ }
1039
+ if (Number.isFinite(mediaElement.duration) && mediaElement.duration > 0) {
1040
+ attributes.push(`duration="${mediaElement.duration.toFixed(1)}"`);
1041
+ }
1042
+ attributes.push(mediaElement.paused ? "paused" : "playing");
1043
+ if (mediaElement.muted)
1044
+ attributes.push("muted");
1045
+ const mediaSrc = mediaElement.currentSrc || mediaElement.getAttribute("src");
1046
+ if (mediaSrc) {
1047
+ try {
1048
+ attributes.push(`srcHost="${new URL(mediaSrc, window.location.href).host}"`);
1049
+ } catch {}
1050
+ }
1051
+ } catch {}
1052
+ }
1053
+ if (role === "iframe") {
1054
+ const host = getIframeHost(element);
1055
+ if (host)
1056
+ attributes.push(`srcHost="${host}"`);
1057
+ if (isSensitiveIframe(element)) {
1058
+ attributes.push("sensitive");
1059
+ } else if (!isSameOriginIframe(element)) {
1060
+ attributes.push("cross-origin");
1061
+ }
1062
+ const iframeRect = element.getBoundingClientRect();
1063
+ attributes.push(`size="${Math.round(iframeRect.width)}x${Math.round(iframeRect.height)}"`);
1064
+ }
1065
+ if (role === "canvas") {
1066
+ const canvas = element;
1067
+ attributes.push(`size="${canvas.width}x${canvas.height}"`);
1068
+ const heading = nearestHeadingText(element);
1069
+ if (heading)
1070
+ attributes.push(`nearestHeading="${escapeAttributeValue(heading)}"`);
1071
+ }
1072
+ if (role === "image") {
1073
+ const image = element;
1074
+ const imageSrc = image.getAttribute("src") || "";
1075
+ if (/\.gif(\?|$)/i.test(imageSrc))
1076
+ attributes.push("animated");
1077
+ }
1078
+ const isNativelyDisabled = "disabled" in element && element.disabled === true;
1079
+ if (isNativelyDisabled || element.getAttribute("aria-disabled") === "true") {
1080
+ attributes.push("disabled");
1081
+ }
1082
+ return attributes;
1083
+ }
1084
+ function ensureStableRef(element) {
1085
+ const existingRef = element.getAttribute(REF_ATTR);
1086
+ if (existingRef) {
1087
+ advanceCounterPastExisting(existingRef);
1088
+ return existingRef;
1089
+ }
1090
+ const newRef = generateNextRef();
1091
+ element.setAttribute(REF_ATTR, newRef);
1092
+ return newRef;
1093
+ }
1094
+ function formatNodeAsLine(entry) {
1095
+ const indent = " ".repeat(entry.depth);
1096
+ const namePart = entry.name ? ` "${escapeAttributeValue(entry.name)}"` : "";
1097
+ const attrsPart = entry.attrs.length > 0 ? ` ${entry.attrs.join(" ")}` : "";
1098
+ return `${indent}${entry.role}${namePart} ${entry.ref}${attrsPart}`;
1099
+ }
1100
+ function shouldSkipSubtree(element) {
1101
+ if (element.id === WIDGET_ROOT_ID)
1102
+ return true;
1103
+ if (element.hasAttribute(PRIVATE_ATTR))
1104
+ return true;
1105
+ const tagName = element.tagName;
1106
+ if (tagName === "SCRIPT" || tagName === "STYLE" || tagName === "NOSCRIPT")
1107
+ return true;
1108
+ return false;
1109
+ }
1110
+ function shouldEmitElement(element, role) {
1111
+ if (role === null)
1112
+ return false;
1113
+ return isInteractive(element, role) || role === "heading" || role === "image" || role === "label" || isBlindRegion(role);
1114
+ }
1115
+ function getSameOriginIframeBody(element) {
1116
+ if (element.tagName !== "IFRAME")
1117
+ return null;
1118
+ if (!isSameOriginIframe(element) || isSensitiveIframe(element))
1119
+ return null;
1120
+ try {
1121
+ return element.contentDocument?.body ?? null;
1122
+ } catch {
1123
+ return null;
1124
+ }
1125
+ }
1126
+ function walkAndCollect(element, depth, entries) {
1127
+ if (shouldSkipSubtree(element))
1128
+ return;
1129
+ const rect = element.getBoundingClientRect();
1130
+ const role = inferRole(element);
1131
+ const isVisibleAndEmittable = role !== null && isVisible(element, rect) && shouldEmitElement(element, role);
1132
+ let childDepth = depth;
1133
+ if (isVisibleAndEmittable && role) {
1134
+ const ref = ensureStableRef(element);
1135
+ entries.push({
1136
+ depth,
1137
+ role,
1138
+ name: accessibleName(element),
1139
+ ref,
1140
+ attrs: extractAttrs(element, role),
1141
+ area: rect.width * rect.height
1142
+ });
1143
+ childDepth = depth + 1;
1144
+ }
1145
+ if (role === "iframe") {
1146
+ const iframeBody = getSameOriginIframeBody(element);
1147
+ if (iframeBody) {
1148
+ for (const child of Array.from(iframeBody.children)) {
1149
+ walkAndCollect(child, childDepth, entries);
1150
+ }
1151
+ }
1152
+ return;
1153
+ }
1154
+ if (role && isBlindRegion(role))
1155
+ return;
1156
+ for (const child of Array.from(element.children)) {
1157
+ walkAndCollect(child, childDepth, entries);
1158
+ }
1159
+ }
1160
+ function dropSmallestAreaEntriesUntilUnderLimit(entries) {
1161
+ if (entries.length <= NODE_LIMIT)
1162
+ return entries;
1163
+ const indexedEntries = entries.map((entry, index2) => ({ entry, index: index2 }));
1164
+ const entriesByAreaAscending = indexedEntries.slice(1).sort((a, b) => a.entry.area - b.entry.area);
1165
+ const indicesToDrop = new Set(entriesByAreaAscending.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
1166
+ const keptEntries = [];
1167
+ for (let i = 0;i < entries.length; i++) {
1168
+ if (!indicesToDrop.has(i))
1169
+ keptEntries.push(entries[i]);
1170
+ }
1171
+ return keptEntries;
1172
+ }
1173
+ function buildAccessibilityTree() {
1174
+ const collectedEntries = [];
1175
+ const pageTitle = document.title || window.location.pathname;
1176
+ collectedEntries.push({
1177
+ depth: 0,
1178
+ role: "page",
1179
+ name: pageTitle.slice(0, NAME_MAX_CHARS),
1180
+ ref: `${REF_PREFIX}0`,
1181
+ attrs: [],
1182
+ area: window.innerWidth * window.innerHeight
1183
+ });
1184
+ if (document.body) {
1185
+ for (const child of Array.from(document.body.children)) {
1186
+ walkAndCollect(child, 1, collectedEntries);
1187
+ }
1188
+ }
1189
+ const exceededNodeLimit = collectedEntries.length > NODE_LIMIT;
1190
+ const finalEntries = exceededNodeLimit ? dropSmallestAreaEntriesUntilUnderLimit(collectedEntries) : collectedEntries;
1191
+ const treeLines = finalEntries.map(formatNodeAsLine);
1192
+ let pageContent = treeLines.join(`
1193
+ `);
1194
+ const exceededByteLimit = pageContent.length > PAGE_CONTENT_MAX_BYTES;
1195
+ if (exceededByteLimit) {
1196
+ pageContent = `${pageContent.slice(0, PAGE_CONTENT_MAX_BYTES)}
1197
+ <!-- truncated -->`;
1198
+ }
1199
+ return {
1200
+ pageContent,
1201
+ viewport: { width: window.innerWidth, height: window.innerHeight },
1202
+ refCount: finalEntries.length - 1,
1203
+ truncated: exceededNodeLimit || exceededByteLimit
1204
+ };
1205
+ }
1206
+
1207
+ // src/capture/domEvents.ts
1208
+ var MUTATION_DEBOUNCE_MS = 250;
1209
+ var textEncoder = new TextEncoder;
1210
+ function isInsideWidget(target) {
1211
+ if (!(target instanceof Node))
1212
+ return false;
1213
+ let node = target;
1214
+ while (node) {
1215
+ if (node instanceof Element && node.id === WIDGET_ROOT_ID)
1216
+ return true;
1217
+ node = node.parentNode;
1218
+ }
1219
+ return false;
1220
+ }
1221
+ function findNearestRef(element) {
1222
+ let currentNode = element;
1223
+ while (currentNode) {
1224
+ const ref = currentNode.getAttribute(REF_ATTR);
1225
+ if (ref)
1226
+ return { ref, node: currentNode };
1227
+ currentNode = currentNode.parentElement;
1228
+ }
1229
+ return null;
1230
+ }
1231
+ function installDomEventListeners(localParticipant, options = {}) {
1232
+ const { onTriggerEvent } = options;
1233
+ function publishDomEvent(event) {
1234
+ try {
1235
+ localParticipant.publishData(textEncoder.encode(JSON.stringify(event)), {
1236
+ reliable: true,
1237
+ topic: DOM_EVENTS_TOPIC
1238
+ }).catch(() => {
1239
+ return;
1240
+ });
1241
+ } catch {}
1242
+ if (onTriggerEvent) {
1243
+ try {
1244
+ onTriggerEvent();
1245
+ } catch {}
1246
+ }
1247
+ }
1248
+ const onClick = (event) => {
1249
+ if (isInsideWidget(event.target))
1250
+ return;
1251
+ if (!(event.target instanceof Element))
1252
+ return;
1253
+ const matchedRef = findNearestRef(event.target);
1254
+ const targetElement = matchedRef?.node ?? event.target;
1255
+ publishDomEvent({
1256
+ type: "click",
1257
+ ref: matchedRef?.ref ?? null,
1258
+ role: inferRole(targetElement) ?? targetElement.tagName.toLowerCase(),
1259
+ name: accessibleName(targetElement, { quick: true }),
1260
+ timestamp: Date.now()
1261
+ });
1262
+ };
1263
+ document.addEventListener("click", onClick, true);
1264
+ const onSubmit = (event) => {
1265
+ if (isInsideWidget(event.target))
1266
+ return;
1267
+ if (!(event.target instanceof HTMLFormElement))
1268
+ return;
1269
+ const submittedForm = event.target;
1270
+ publishDomEvent({
1271
+ type: "submit",
1272
+ ref: submittedForm.getAttribute(REF_ATTR),
1273
+ formName: (submittedForm.name || submittedForm.id || "").slice(0, NAME_MAX_CHARS),
1274
+ action: (submittedForm.action || "").slice(0, NAME_MAX_CHARS),
1275
+ timestamp: Date.now()
1276
+ });
1277
+ };
1278
+ document.addEventListener("submit", onSubmit, true);
1279
+ const originalPushState = history.pushState;
1280
+ const originalReplaceState = history.replaceState;
1281
+ let previousUrl = window.location.href;
1282
+ const emitNavigationIfChanged = () => {
1283
+ const nextUrl = window.location.href;
1284
+ if (nextUrl === previousUrl)
1285
+ return;
1286
+ publishDomEvent({
1287
+ type: "navigation",
1288
+ from: previousUrl,
1289
+ to: nextUrl,
1290
+ title: document.title.slice(0, NAME_MAX_CHARS),
1291
+ timestamp: Date.now()
1292
+ });
1293
+ previousUrl = nextUrl;
1294
+ };
1295
+ history.pushState = function patchedPushState(...args) {
1296
+ originalPushState.apply(this, args);
1297
+ queueMicrotask(emitNavigationIfChanged);
1298
+ };
1299
+ history.replaceState = function patchedReplaceState(...args) {
1300
+ originalReplaceState.apply(this, args);
1301
+ queueMicrotask(emitNavigationIfChanged);
1302
+ };
1303
+ const onPopState = () => emitNavigationIfChanged();
1304
+ const onHashChange = () => emitNavigationIfChanged();
1305
+ window.addEventListener("popstate", onPopState);
1306
+ window.addEventListener("hashchange", onHashChange);
1307
+ let pendingAddedRefs = new Set;
1308
+ let pendingRemovedRefs = new Set;
1309
+ let pendingMutationCount = 0;
1310
+ let mutationFlushTimer = null;
1311
+ const flushPendingMutations = () => {
1312
+ mutationFlushTimer = null;
1313
+ if (pendingMutationCount === 0)
1314
+ return;
1315
+ publishDomEvent({
1316
+ type: "mutation",
1317
+ summary: `${pendingMutationCount} subtree changes`,
1318
+ addedRefs: Array.from(pendingAddedRefs),
1319
+ removedRefs: Array.from(pendingRemovedRefs),
1320
+ timestamp: Date.now()
1321
+ });
1322
+ pendingAddedRefs = new Set;
1323
+ pendingRemovedRefs = new Set;
1324
+ pendingMutationCount = 0;
1325
+ };
1326
+ const collectDescendantRefs = (node, targetSet) => {
1327
+ if (!(node instanceof Element))
1328
+ return;
1329
+ if (node.id === WIDGET_ROOT_ID)
1330
+ return;
1331
+ const ref = node.getAttribute(REF_ATTR);
1332
+ if (ref)
1333
+ targetSet.add(ref);
1334
+ for (const child of Array.from(node.children))
1335
+ collectDescendantRefs(child, targetSet);
1336
+ };
1337
+ const observer = new MutationObserver((mutations) => {
1338
+ let observedCount = 0;
1339
+ for (const mutation of mutations) {
1340
+ if (mutation.type !== "childList")
1341
+ continue;
1342
+ if (mutation.target instanceof Element && isInsideWidget(mutation.target))
1343
+ continue;
1344
+ if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0)
1345
+ continue;
1346
+ for (const added of Array.from(mutation.addedNodes)) {
1347
+ collectDescendantRefs(added, pendingAddedRefs);
1348
+ }
1349
+ for (const removed of Array.from(mutation.removedNodes)) {
1350
+ collectDescendantRefs(removed, pendingRemovedRefs);
1351
+ }
1352
+ observedCount += 1;
1353
+ }
1354
+ if (observedCount === 0)
1355
+ return;
1356
+ pendingMutationCount += observedCount;
1357
+ if (mutationFlushTimer)
1358
+ clearTimeout(mutationFlushTimer);
1359
+ mutationFlushTimer = setTimeout(flushPendingMutations, MUTATION_DEBOUNCE_MS);
1360
+ });
1361
+ observer.observe(document.body, { childList: true, subtree: true });
1362
+ return () => {
1363
+ document.removeEventListener("click", onClick, true);
1364
+ document.removeEventListener("submit", onSubmit, true);
1365
+ window.removeEventListener("popstate", onPopState);
1366
+ window.removeEventListener("hashchange", onHashChange);
1367
+ history.pushState = originalPushState;
1368
+ history.replaceState = originalReplaceState;
1369
+ if (mutationFlushTimer)
1370
+ clearTimeout(mutationFlushTimer);
1371
+ observer.disconnect();
1372
+ };
1373
+ }
1374
+
1375
+ // src/capture/snapdom.ts
1376
+ var cachedSnapdomModule = null;
1377
+ function loadSnapdom() {
1378
+ if (!cachedSnapdomModule)
1379
+ cachedSnapdomModule = import("@zumer/snapdom");
1380
+ return cachedSnapdomModule;
1381
+ }
1382
+ async function snapToCanvas(element, options = {}) {
1383
+ const snapdomModule = await loadSnapdom();
1384
+ return snapdomModule.snapdom.toCanvas(element, options);
1385
+ }
1386
+
1387
+ // src/components/DomCapture.tsx
1388
+ var SNAPSHOT_INTERVAL_MS = 2000;
1389
+ var A11Y_PUBLISH_INTERVAL_MS = 2000;
1390
+ var CAPTURE_PRESET = ScreenSharePresets2.h1080fps30;
1391
+ var CAPTURE_FPS = CAPTURE_PRESET.encoding.maxFramerate ?? 30;
1392
+ var CAPTURE_BITRATE = 4000000;
1393
+ var DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES = 14000;
1394
+ var MAX_CANVAS_DPR = 1.5;
1395
+ var CONSECUTIVE_FAILURES_BEFORE_REPORT = 3;
1396
+ var textEncoder2 = new TextEncoder;
1397
+ function shouldIncludeInSnapshot(element) {
1398
+ if (!(element instanceof Element))
1399
+ return true;
1400
+ if (element.id === WIDGET_ROOT_ID)
1401
+ return false;
1402
+ if (element.hasAttribute?.(PRIVATE_ATTR))
1403
+ return false;
1404
+ return true;
1405
+ }
1406
+ function createPublishingCanvas() {
1407
+ const dprBoost = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
1408
+ const canvas = document.createElement("canvas");
1409
+ canvas.width = Math.round(CAPTURE_PRESET.width * dprBoost);
1410
+ canvas.height = Math.round(CAPTURE_PRESET.height * dprBoost);
1411
+ const ctx = canvas.getContext("2d");
1412
+ if (!ctx)
1413
+ return null;
1414
+ ctx.fillStyle = "#ffffff";
1415
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1416
+ return { canvas, ctx };
1417
+ }
1418
+ function getCanvasCaptureStream(canvas, fps) {
1419
+ const captureStreamFn = canvas.captureStream;
1420
+ if (typeof captureStreamFn !== "function") {
1421
+ console.error("canvas.captureStream is not supported in this browser");
1422
+ return null;
1423
+ }
1424
+ return captureStreamFn.call(canvas, fps);
1425
+ }
1426
+ async function paintViewportSnapshot(canvas, ctx) {
1427
+ const dpr = window.devicePixelRatio || 1;
1428
+ const snapshotCanvas = await snapToCanvas(document.documentElement, {
1429
+ filter: shouldIncludeInSnapshot,
1430
+ filterMode: "remove",
1431
+ backgroundColor: "#ffffff",
1432
+ fast: true,
1433
+ dpr
1434
+ });
1435
+ const sourceX = window.scrollX * dpr;
1436
+ const sourceY = window.scrollY * dpr;
1437
+ const sourceWidth = window.innerWidth * dpr;
1438
+ const sourceHeight = window.innerHeight * dpr;
1439
+ const fitScale = Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight);
1440
+ const destWidth = sourceWidth * fitScale;
1441
+ const destHeight = sourceHeight * fitScale;
1442
+ const destX = (canvas.width - destWidth) / 2;
1443
+ const destY = (canvas.height - destHeight) / 2;
1444
+ ctx.fillStyle = "#ffffff";
1445
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1446
+ ctx.drawImage(snapshotCanvas, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
1447
+ }
1448
+ async function gzipIfBeneficial(snapshotBytes) {
1449
+ if (typeof CompressionStream === "undefined" || snapshotBytes.byteLength <= DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES) {
1450
+ return { bytes: snapshotBytes, compressionFlag: 0 };
1451
+ }
1452
+ try {
1453
+ const blob = new Blob([new Uint8Array(snapshotBytes)]);
1454
+ const stream = blob.stream().pipeThrough(new CompressionStream("gzip"));
1455
+ const compressedBytes = new Uint8Array(await new Response(stream).arrayBuffer());
1456
+ return { bytes: compressedBytes, compressionFlag: 1 };
1457
+ } catch {
1458
+ return { bytes: snapshotBytes, compressionFlag: 0 };
1459
+ }
1460
+ }
1461
+ async function buildDomSnapshotFrame() {
1462
+ const a11yTree = buildAccessibilityTree();
1463
+ const snapshotJson = JSON.stringify({
1464
+ pageContent: a11yTree.pageContent,
1465
+ viewport: a11yTree.viewport,
1466
+ truncated: a11yTree.truncated,
1467
+ url: window.location.href,
1468
+ title: document.title,
1469
+ capturedAt: new Date().toISOString()
1470
+ });
1471
+ const snapshotBytes = textEncoder2.encode(snapshotJson);
1472
+ const { bytes, compressionFlag } = await gzipIfBeneficial(snapshotBytes);
1473
+ const frame = new Uint8Array(bytes.byteLength + 1);
1474
+ frame[0] = compressionFlag;
1475
+ frame.set(bytes, 1);
1476
+ return frame;
1477
+ }
1478
+ function reportCaptureError(localParticipant) {
1479
+ try {
1480
+ localParticipant.publishData(textEncoder2.encode(JSON.stringify({ type: "capture_error" })), {
1481
+ reliable: true,
1482
+ topic: DOM_SNAPSHOT_TOPIC
1483
+ }).catch(() => {
1484
+ return;
1485
+ });
1486
+ } catch {}
1487
+ }
1488
+ async function unpublishAndStopTrack(localParticipant, videoTrack) {
1489
+ try {
1490
+ await localParticipant.unpublishTrack(videoTrack);
1491
+ } catch {}
1492
+ videoTrack.stop();
1493
+ }
1494
+ function DomCapture() {
1495
+ const { localParticipant } = useLocalParticipant3();
1496
+ const connectionState = useConnectionState2();
1497
+ const didStartRef = useRef2(false);
1498
+ useEffect4(() => {
1499
+ if (didStartRef.current)
1500
+ return;
1501
+ if (connectionState !== ConnectionState2.Connected)
1502
+ return;
1503
+ const canvasAndCtx = createPublishingCanvas();
1504
+ if (!canvasAndCtx)
1505
+ return;
1506
+ const { canvas, ctx } = canvasAndCtx;
1507
+ const canvasStream = getCanvasCaptureStream(canvas, CAPTURE_FPS);
1508
+ if (!canvasStream)
1509
+ return;
1510
+ const videoTrack = canvasStream.getVideoTracks()[0] ?? null;
1511
+ if (!videoTrack)
1512
+ return;
1513
+ videoTrack.contentHint = "text";
1514
+ didStartRef.current = true;
1515
+ let cancelled = false;
1516
+ let snapshotInFlight = false;
1517
+ let a11yPublishInFlight = false;
1518
+ let consecutiveCaptureFailures = 0;
1519
+ localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
1520
+ localParticipant.publishTrack(videoTrack, {
1521
+ source: Track2.Source.ScreenShare,
1522
+ videoEncoding: {
1523
+ maxBitrate: CAPTURE_BITRATE,
1524
+ maxFramerate: CAPTURE_FPS
1525
+ },
1526
+ simulcast: false
1527
+ }).catch((error) => {
1528
+ console.error("Failed to publish DOM capture track:", error);
1529
+ for (const track of canvasStream.getTracks())
1530
+ track.stop();
1531
+ });
1532
+ const tickSnapshot = async () => {
1533
+ if (cancelled || snapshotInFlight)
1534
+ return;
1535
+ snapshotInFlight = true;
1536
+ try {
1537
+ await paintViewportSnapshot(canvas, ctx);
1538
+ if (cancelled)
1539
+ return;
1540
+ consecutiveCaptureFailures = 0;
1541
+ } catch {
1542
+ if (cancelled)
1543
+ return;
1544
+ consecutiveCaptureFailures += 1;
1545
+ if (consecutiveCaptureFailures === CONSECUTIVE_FAILURES_BEFORE_REPORT) {
1546
+ reportCaptureError(localParticipant);
1547
+ }
1548
+ } finally {
1549
+ snapshotInFlight = false;
1550
+ }
1551
+ };
1552
+ const tickA11yPublish = async () => {
1553
+ if (cancelled || a11yPublishInFlight)
1554
+ return;
1555
+ a11yPublishInFlight = true;
1556
+ try {
1557
+ const frame = await buildDomSnapshotFrame();
1558
+ if (cancelled)
1559
+ return;
1560
+ await localParticipant.publishData(frame, {
1561
+ reliable: true,
1562
+ topic: DOM_SNAPSHOT_TOPIC
1563
+ });
1564
+ } catch {} finally {
1565
+ a11yPublishInFlight = false;
1566
+ }
1567
+ };
1568
+ const cleanupDomEventListeners = installDomEventListeners(localParticipant, {
1569
+ onTriggerEvent: () => {
1570
+ tickA11yPublish();
1571
+ tickSnapshot();
1572
+ }
1573
+ });
1574
+ tickSnapshot();
1575
+ tickA11yPublish();
1576
+ const snapshotTimer = setInterval(tickSnapshot, SNAPSHOT_INTERVAL_MS);
1577
+ const a11yPublishTimer = setInterval(tickA11yPublish, A11Y_PUBLISH_INTERVAL_MS);
1578
+ return () => {
1579
+ cancelled = true;
1580
+ didStartRef.current = false;
1581
+ clearInterval(snapshotTimer);
1582
+ clearInterval(a11yPublishTimer);
1583
+ cleanupDomEventListeners();
1584
+ unpublishAndStopTrack(localParticipant, videoTrack);
1585
+ for (const track of canvasStream.getTracks())
1586
+ track.stop();
1587
+ };
1588
+ }, [connectionState, localParticipant]);
1589
+ return null;
1590
+ }
1591
+
1592
+ // src/components/HighlightOverlay.tsx
1593
+ import { useDataChannel } from "@livekit/components-react/hooks";
1594
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef3, useState as useState3 } from "react";
1595
+ import { jsx as jsx2, jsxs as jsxs2, Fragment as Fragment2 } from "react/jsx-runtime";
1596
+ var TOOLTIP_MAX_WIDTH = 280;
1597
+ var TOOLTIP_GAP = 8;
1598
+ var VIEWPORT_MARGIN = 8;
1599
+ var TOOLTIP_TOP_THRESHOLD = 48;
1600
+ var Z_INDEX = 2147483646;
1601
+ var RING_INNER = "oklab(0.585 0.0288678 -0.231205 / 0.95)";
1602
+ var RING_OUTER = "oklab(0.585 0.0288678 -0.231205 / 0.25)";
1603
+ var textDecoder = new TextDecoder;
1604
+ function parseHighlightMessage(payload) {
1605
+ try {
1606
+ const json = JSON.parse(textDecoder.decode(payload));
1607
+ if (!json || typeof json !== "object")
1608
+ return null;
1609
+ if (json.type === "show" && typeof json.ref === "string") {
1610
+ return {
1611
+ type: "show",
1612
+ ref: json.ref,
1613
+ message: typeof json.message === "string" ? json.message : ""
1614
+ };
1615
+ }
1616
+ if (json.type === "clear")
1617
+ return { type: "clear" };
1618
+ return null;
1619
+ } catch {
1620
+ return null;
1621
+ }
1622
+ }
1623
+ function safeGetIframeDocument(iframe) {
1624
+ try {
1625
+ return iframe.contentDocument;
1626
+ } catch {
1627
+ return null;
1628
+ }
1629
+ }
1630
+ function safeQuerySelector(doc, selector) {
1631
+ try {
1632
+ return doc.querySelector(selector);
1633
+ } catch {
1634
+ return null;
1635
+ }
1636
+ }
1637
+ function safeQueryAllIframes(doc) {
1638
+ try {
1639
+ return Array.from(doc.querySelectorAll("iframe"));
1640
+ } catch {
1641
+ return [];
1642
+ }
1643
+ }
1644
+ function findElementInDocumentTree(rootDoc, selector) {
1645
+ const hit = safeQuerySelector(rootDoc, selector);
1646
+ if (hit)
1647
+ return hit;
1648
+ for (const iframe of safeQueryAllIframes(rootDoc)) {
1649
+ const innerDoc = safeGetIframeDocument(iframe);
1650
+ if (!innerDoc)
1651
+ continue;
1652
+ const nested = findElementInDocumentTree(innerDoc, selector);
1653
+ if (nested)
1654
+ return nested;
1655
+ }
1656
+ return null;
1657
+ }
1658
+ function findElementByRef(ref) {
1659
+ const selector = `[${REF_ATTR}="${CSS.escape(ref)}"]`;
1660
+ return findElementInDocumentTree(document, selector);
1661
+ }
1662
+ function findIframeHostingDocument(targetDoc) {
1663
+ const queue = [document];
1664
+ while (queue.length > 0) {
1665
+ const doc = queue.shift();
1666
+ if (!doc)
1667
+ continue;
1668
+ for (const iframe of safeQueryAllIframes(doc)) {
1669
+ if (safeGetIframeDocument(iframe) === targetDoc)
1670
+ return iframe;
1671
+ const innerDoc = safeGetIframeDocument(iframe);
1672
+ if (innerDoc)
1673
+ queue.push(innerDoc);
1674
+ }
1675
+ }
1676
+ return null;
1677
+ }
1678
+ function getRectInTopViewport(el) {
1679
+ let rect = el.getBoundingClientRect();
1680
+ let ownerDoc = el.ownerDocument;
1681
+ while (ownerDoc && ownerDoc !== document) {
1682
+ const hostingIframe = findIframeHostingDocument(ownerDoc);
1683
+ if (!hostingIframe)
1684
+ return rect;
1685
+ const iframeRect = hostingIframe.getBoundingClientRect();
1686
+ rect = new DOMRect(rect.left + iframeRect.left, rect.top + iframeRect.top, rect.width, rect.height);
1687
+ ownerDoc = hostingIframe.ownerDocument;
1688
+ }
1689
+ return rect;
1690
+ }
1691
+ function findScrollableAncestor(el) {
1692
+ const win = el.ownerDocument?.defaultView ?? window;
1693
+ let node = el.parentElement;
1694
+ while (node) {
1695
+ const overflow = win.getComputedStyle(node).overflow;
1696
+ if (/auto|scroll|overlay/.test(overflow))
1697
+ return node;
1698
+ node = node.parentElement;
1699
+ }
1700
+ return el.ownerDocument?.documentElement ?? document.documentElement;
1701
+ }
1702
+ function HighlightOverlay() {
1703
+ const [overlayState, setOverlayState] = useState3(null);
1704
+ const targetElementRef = useRef3(null);
1705
+ const pendingFrameRef = useRef3(null);
1706
+ const clearOverlay = useCallback4(() => {
1707
+ targetElementRef.current = null;
1708
+ setOverlayState(null);
1709
+ }, []);
1710
+ const recomputeRect = useCallback4(() => {
1711
+ pendingFrameRef.current = null;
1712
+ const target = targetElementRef.current;
1713
+ if (!target)
1714
+ return;
1715
+ if (!target.isConnected) {
1716
+ clearOverlay();
1717
+ return;
1718
+ }
1719
+ const rect2 = getRectInTopViewport(target);
1720
+ setOverlayState((prev) => prev ? { ...prev, rect: rect2 } : prev);
1721
+ }, [clearOverlay]);
1722
+ const scheduleRecompute = useCallback4(() => {
1723
+ if (pendingFrameRef.current !== null)
1724
+ return;
1725
+ pendingFrameRef.current = window.requestAnimationFrame(recomputeRect);
1726
+ }, [recomputeRect]);
1727
+ const onHighlightMessage = useCallback4((msg) => {
1728
+ const parsed = parseHighlightMessage(msg.payload);
1729
+ if (!parsed)
1730
+ return;
1731
+ if (parsed.type === "clear") {
1732
+ clearOverlay();
1733
+ return;
1734
+ }
1735
+ const target = findElementByRef(parsed.ref);
1736
+ if (!target) {
1737
+ clearOverlay();
1738
+ return;
1739
+ }
1740
+ targetElementRef.current = target;
1741
+ setOverlayState({
1742
+ ref: parsed.ref,
1743
+ message: parsed.message ?? "",
1744
+ rect: getRectInTopViewport(target)
1745
+ });
1746
+ }, [clearOverlay]);
1747
+ useDataChannel(HIGHLIGHT_TOPIC, onHighlightMessage);
1748
+ useEffect5(() => {
1749
+ if (!overlayState)
1750
+ return;
1751
+ const target = targetElementRef.current;
1752
+ if (!target)
1753
+ return;
1754
+ const onScroll = () => scheduleRecompute();
1755
+ const onResize = () => scheduleRecompute();
1756
+ const watchedWindows = new Set;
1757
+ watchedWindows.add(window);
1758
+ const ownerWindow = target.ownerDocument?.defaultView;
1759
+ if (ownerWindow && ownerWindow !== window)
1760
+ watchedWindows.add(ownerWindow);
1761
+ for (const win of watchedWindows) {
1762
+ win.addEventListener("scroll", onScroll, { capture: true, passive: true });
1763
+ win.addEventListener("resize", onResize, { passive: true });
1764
+ }
1765
+ const resizeObserver = new ResizeObserver(scheduleRecompute);
1766
+ resizeObserver.observe(target);
1767
+ const scrollContainer = findScrollableAncestor(target);
1768
+ const mutationObserver = new MutationObserver(() => {
1769
+ if (!targetElementRef.current?.isConnected) {
1770
+ clearOverlay();
1771
+ return;
1772
+ }
1773
+ scheduleRecompute();
1774
+ });
1775
+ mutationObserver.observe(scrollContainer, { childList: true, subtree: true });
1776
+ return () => {
1777
+ for (const win of watchedWindows) {
1778
+ win.removeEventListener("scroll", onScroll, { capture: true });
1779
+ win.removeEventListener("resize", onResize);
1780
+ }
1781
+ resizeObserver.disconnect();
1782
+ mutationObserver.disconnect();
1783
+ if (pendingFrameRef.current !== null) {
1784
+ window.cancelAnimationFrame(pendingFrameRef.current);
1785
+ pendingFrameRef.current = null;
1786
+ }
1787
+ };
1788
+ }, [overlayState, scheduleRecompute, clearOverlay]);
1789
+ if (!overlayState)
1790
+ return null;
1791
+ const { rect, message } = overlayState;
1792
+ const ringStyle = {
1793
+ position: "fixed",
1794
+ left: `${rect.left}px`,
1795
+ top: `${rect.top}px`,
1796
+ width: `${rect.width}px`,
1797
+ height: `${rect.height}px`,
1798
+ pointerEvents: "none",
1799
+ zIndex: Z_INDEX,
1800
+ boxShadow: `0 0 0 2px ${RING_INNER}, 0 0 0 6px ${RING_OUTER}`,
1801
+ borderRadius: "4px",
1802
+ transition: "none"
1803
+ };
1804
+ const showTooltipBelow = rect.top < TOOLTIP_TOP_THRESHOLD;
1805
+ const tooltipTop = showTooltipBelow ? rect.bottom + TOOLTIP_GAP : rect.top - TOOLTIP_GAP;
1806
+ const tooltipTransform = showTooltipBelow ? "translateY(0)" : "translateY(-100%)";
1807
+ const tooltipLeft = Math.min(Math.max(rect.left, VIEWPORT_MARGIN), window.innerWidth - TOOLTIP_MAX_WIDTH - VIEWPORT_MARGIN);
1808
+ const tooltipStyle = {
1809
+ position: "fixed",
1810
+ left: `${tooltipLeft}px`,
1811
+ top: `${tooltipTop}px`,
1812
+ transform: tooltipTransform,
1813
+ maxWidth: `${TOOLTIP_MAX_WIDTH}px`,
1814
+ pointerEvents: "none",
1815
+ zIndex: Z_INDEX,
1816
+ background: "rgba(45, 43, 61, 0.96)",
1817
+ color: "#ffffff",
1818
+ padding: "6px 10px",
1819
+ borderRadius: "8px",
1820
+ fontSize: "13px",
1821
+ lineHeight: "1.35",
1822
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, sans-serif',
1823
+ boxShadow: "0 6px 20px rgba(0, 0, 0, 0.25)",
1824
+ whiteSpace: "normal",
1825
+ wordWrap: "break-word"
1826
+ };
1827
+ return /* @__PURE__ */ jsxs2(Fragment2, {
1828
+ children: [
1829
+ /* @__PURE__ */ jsx2("div", {
1830
+ "data-skippr-private": "true",
1831
+ style: ringStyle,
1832
+ "aria-hidden": "true"
1833
+ }),
1834
+ message && /* @__PURE__ */ jsx2("div", {
1835
+ "data-skippr-private": "true",
1836
+ style: tooltipStyle,
1837
+ role: "tooltip",
1838
+ children: message
1839
+ })
1840
+ ]
1841
+ });
1842
+ }
1843
+
714
1844
  // src/components/MinimizedBubble.tsx
715
- import { useEffect as useEffect4 } from "react";
1845
+ import { useEffect as useEffect6 } from "react";
716
1846
 
717
1847
  // src/lib/utils.ts
718
1848
  import { clsx } from "clsx";
@@ -723,12 +1853,12 @@ function cn(...inputs) {
723
1853
 
724
1854
  // src/components/Logo.tsx
725
1855
  import { useId } from "react";
726
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1856
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
727
1857
  function Logo({ className }) {
728
1858
  const reactId = useId().replace(/:/g, "");
729
1859
  const clipId = `skippr-logo-clip-${reactId}`;
730
1860
  const gradientId = `skippr-logo-gradient-${reactId}`;
731
- return /* @__PURE__ */ jsxs2("svg", {
1861
+ return /* @__PURE__ */ jsxs3("svg", {
732
1862
  width: "1em",
733
1863
  height: "1em",
734
1864
  viewBox: "0 0 30 30",
@@ -738,14 +1868,14 @@ function Logo({ className }) {
738
1868
  "aria-label": "Skippr",
739
1869
  className,
740
1870
  children: [
741
- /* @__PURE__ */ jsxs2("g", {
1871
+ /* @__PURE__ */ jsxs3("g", {
742
1872
  clipPath: `url(#${clipId})`,
743
1873
  children: [
744
- /* @__PURE__ */ jsx2("path", {
1874
+ /* @__PURE__ */ jsx3("path", {
745
1875
  d: "M0 10C0 4.47715 4.47715 0 10 0H20C25.5228 0 30 4.47715 30 10V20C30 25.5228 25.5228 30 20 30H10C4.47715 30 0 25.5228 0 20V10Z",
746
1876
  fill: "#2D2D3F"
747
1877
  }),
748
- /* @__PURE__ */ jsx2("rect", {
1878
+ /* @__PURE__ */ jsx3("rect", {
749
1879
  x: "7.83325",
750
1880
  y: "14.9404",
751
1881
  width: "12.4083",
@@ -754,11 +1884,11 @@ function Logo({ className }) {
754
1884
  transform: "rotate(-45 7.83325 14.9404)",
755
1885
  fill: "#52FFF9"
756
1886
  }),
757
- /* @__PURE__ */ jsx2("path", {
1887
+ /* @__PURE__ */ jsx3("path", {
758
1888
  d: "M18.8975 12.5928C20.2728 12.5928 21.3877 13.647 21.3877 14.9474C21.3877 16.2479 20.2728 17.3021 18.8975 17.3021L11.4269 17.3021C10.0516 17.3021 8.93665 16.2479 8.93665 14.9474C8.93665 13.647 10.0516 12.5928 11.4269 12.5928L18.8975 12.5928Z",
759
1889
  fill: `url(#${gradientId})`
760
1890
  }),
761
- /* @__PURE__ */ jsx2("rect", {
1891
+ /* @__PURE__ */ jsx3("rect", {
762
1892
  x: "10.1665",
763
1893
  y: "20.4404",
764
1894
  width: "12.4083",
@@ -769,9 +1899,9 @@ function Logo({ className }) {
769
1899
  })
770
1900
  ]
771
1901
  }),
772
- /* @__PURE__ */ jsxs2("defs", {
1902
+ /* @__PURE__ */ jsxs3("defs", {
773
1903
  children: [
774
- /* @__PURE__ */ jsxs2("linearGradient", {
1904
+ /* @__PURE__ */ jsxs3("linearGradient", {
775
1905
  id: gradientId,
776
1906
  x1: "18.9237",
777
1907
  y1: "16.9997",
@@ -779,19 +1909,19 @@ function Logo({ className }) {
779
1909
  y2: "14.1904",
780
1910
  gradientUnits: "userSpaceOnUse",
781
1911
  children: [
782
- /* @__PURE__ */ jsx2("stop", {
1912
+ /* @__PURE__ */ jsx3("stop", {
783
1913
  offset: "0.473958",
784
1914
  stopColor: "white"
785
1915
  }),
786
- /* @__PURE__ */ jsx2("stop", {
1916
+ /* @__PURE__ */ jsx3("stop", {
787
1917
  offset: "1",
788
1918
  stopColor: "#52FFF9"
789
1919
  })
790
1920
  ]
791
1921
  }),
792
- /* @__PURE__ */ jsx2("clipPath", {
1922
+ /* @__PURE__ */ jsx3("clipPath", {
793
1923
  id: clipId,
794
- children: /* @__PURE__ */ jsx2("rect", {
1924
+ children: /* @__PURE__ */ jsx3("rect", {
795
1925
  width: "30",
796
1926
  height: "30",
797
1927
  fill: "white"
@@ -804,18 +1934,18 @@ function Logo({ className }) {
804
1934
  }
805
1935
 
806
1936
  // src/components/ui/tooltip.tsx
807
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1937
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
808
1938
  var ALIGN_CLASSES = {
809
1939
  center: "skippr:left-1/2 skippr:-translate-x-1/2",
810
1940
  start: "skippr:left-0",
811
1941
  end: "skippr:right-0"
812
1942
  };
813
1943
  function Tooltip({ label, children, position = "top", align = "center" }) {
814
- return /* @__PURE__ */ jsxs3("span", {
1944
+ return /* @__PURE__ */ jsxs4("span", {
815
1945
  className: "skippr:relative skippr:inline-flex skippr:group",
816
1946
  children: [
817
1947
  children,
818
- /* @__PURE__ */ jsx3("span", {
1948
+ /* @__PURE__ */ jsx4("span", {
819
1949
  className: cn("skippr:pointer-events-none skippr:absolute skippr:z-10", "skippr:whitespace-nowrap skippr:rounded-md skippr:bg-foreground skippr:px-2 skippr:py-1", "skippr:text-[11px] skippr:text-background skippr:font-medium", "skippr:opacity-0 skippr:scale-95 skippr:transition-all skippr:duration-150", "skippr:group-hover:opacity-100 skippr:group-hover:scale-100", "skippr:group-focus-within:opacity-100 skippr:group-focus-within:scale-100", ALIGN_CLASSES[align], position === "top" && "skippr:bottom-full skippr:mb-1.5", position === "bottom" && "skippr:top-full skippr:mt-1.5"),
820
1950
  "aria-hidden": "true",
821
1951
  children: label
@@ -825,66 +1955,67 @@ function Tooltip({ label, children, position = "top", align = "center" }) {
825
1955
  }
826
1956
 
827
1957
  // src/components/MinimizedBubble.tsx
828
- import { jsx as jsx4, jsxs as jsxs4, Fragment as Fragment2 } from "react/jsx-runtime";
1958
+ import { jsx as jsx5, jsxs as jsxs5, Fragment as Fragment3 } from "react/jsx-runtime";
829
1959
  var BUBBLE_BUTTON = "skippr:flex skippr:size-12 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-[14px] skippr:shadow-[0_4px_16px_rgba(45,43,61,0.45),0_2px_4px_rgba(0,0,0,0.1)] skippr:transition-all skippr:hover:-translate-y-0.5 skippr:active:translate-y-0";
830
1960
  function ConnectedBubbleContent() {
831
- const { expandPanel, disconnect, position } = useLiveAgent();
1961
+ const { expandPanel, disconnect, position, captureMode } = useLiveAgent();
832
1962
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
833
1963
  const tooltipAlign = position === "right" ? "end" : "start";
834
- return /* @__PURE__ */ jsxs4(Fragment2, {
1964
+ const showScreenShareToggle = captureMode === "screenshare";
1965
+ return /* @__PURE__ */ jsxs5(Fragment3, {
835
1966
  children: [
836
- /* @__PURE__ */ jsx4(Tooltip, {
1967
+ /* @__PURE__ */ jsx5(Tooltip, {
837
1968
  label: isMuted ? "Unmute" : "Mute",
838
1969
  align: tooltipAlign,
839
- children: /* @__PURE__ */ jsx4("button", {
1970
+ children: /* @__PURE__ */ jsx5("button", {
840
1971
  type: "button",
841
1972
  onClick: toggleMute,
842
1973
  "aria-label": isMuted ? "Unmute" : "Mute",
843
1974
  className: cn(BUBBLE_BUTTON, isMuted ? "skippr:bg-destructive skippr:text-destructive-foreground skippr:hover:bg-destructive/90" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
844
- children: isMuted ? /* @__PURE__ */ jsx4(MicOff, {
1975
+ children: isMuted ? /* @__PURE__ */ jsx5(MicOff, {
845
1976
  className: "skippr:size-5"
846
- }) : /* @__PURE__ */ jsx4(Mic, {
1977
+ }) : /* @__PURE__ */ jsx5(Mic, {
847
1978
  className: "skippr:size-5"
848
1979
  })
849
1980
  })
850
1981
  }),
851
- /* @__PURE__ */ jsx4(Tooltip, {
1982
+ showScreenShareToggle && /* @__PURE__ */ jsx5(Tooltip, {
852
1983
  label: isScreenSharing ? "Stop sharing" : "Share screen",
853
1984
  align: tooltipAlign,
854
- children: /* @__PURE__ */ jsx4("button", {
1985
+ children: /* @__PURE__ */ jsx5("button", {
855
1986
  type: "button",
856
1987
  onClick: toggleScreenShare,
857
1988
  "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
858
1989
  className: cn(BUBBLE_BUTTON, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
859
- children: isScreenSharing ? /* @__PURE__ */ jsx4(MonitorOff, {
1990
+ children: isScreenSharing ? /* @__PURE__ */ jsx5(MonitorOff, {
860
1991
  className: "skippr:size-5"
861
- }) : /* @__PURE__ */ jsx4(Monitor, {
1992
+ }) : /* @__PURE__ */ jsx5(Monitor, {
862
1993
  className: "skippr:size-5"
863
1994
  })
864
1995
  })
865
1996
  }),
866
- /* @__PURE__ */ jsx4(Tooltip, {
1997
+ /* @__PURE__ */ jsx5(Tooltip, {
867
1998
  label: "End session",
868
1999
  align: tooltipAlign,
869
- children: /* @__PURE__ */ jsx4("button", {
2000
+ children: /* @__PURE__ */ jsx5("button", {
870
2001
  type: "button",
871
2002
  onClick: () => disconnect(),
872
2003
  "aria-label": "End session",
873
2004
  className: cn(BUBBLE_BUTTON, "skippr:bg-destructive skippr:text-destructive-foreground skippr:hover:bg-destructive/90"),
874
- children: /* @__PURE__ */ jsx4(PhoneOff, {
2005
+ children: /* @__PURE__ */ jsx5(PhoneOff, {
875
2006
  className: "skippr:size-5"
876
2007
  })
877
2008
  })
878
2009
  }),
879
- /* @__PURE__ */ jsx4(Tooltip, {
2010
+ /* @__PURE__ */ jsx5(Tooltip, {
880
2011
  label: "Open chat & transcript",
881
2012
  align: tooltipAlign,
882
- children: /* @__PURE__ */ jsx4("button", {
2013
+ children: /* @__PURE__ */ jsx5("button", {
883
2014
  type: "button",
884
2015
  onClick: expandPanel,
885
2016
  "aria-label": "Open chat & transcript",
886
2017
  className: cn(BUBBLE_BUTTON, "skippr:bg-bubble skippr:hover:brightness-110"),
887
- children: /* @__PURE__ */ jsx4(Logo, {
2018
+ children: /* @__PURE__ */ jsx5(Logo, {
888
2019
  className: "skippr:size-7"
889
2020
  })
890
2021
  })
@@ -895,19 +2026,19 @@ function ConnectedBubbleContent() {
895
2026
  function IdleBubbleContent() {
896
2027
  const { expandPanel, position } = useLiveAgent();
897
2028
  const tooltipAlign = position === "right" ? "end" : "start";
898
- return /* @__PURE__ */ jsx4(Tooltip, {
2029
+ return /* @__PURE__ */ jsx5(Tooltip, {
899
2030
  label: "Open Skippr assistant",
900
2031
  align: tooltipAlign,
901
- children: /* @__PURE__ */ jsxs4("button", {
2032
+ children: /* @__PURE__ */ jsxs5("button", {
902
2033
  type: "button",
903
2034
  onClick: expandPanel,
904
2035
  "aria-label": "Skippr assistant",
905
2036
  className: cn(BUBBLE_BUTTON, "skippr:relative skippr:bg-bubble skippr:hover:brightness-110"),
906
2037
  children: [
907
- /* @__PURE__ */ jsx4(Logo, {
2038
+ /* @__PURE__ */ jsx5(Logo, {
908
2039
  className: "skippr:relative skippr:z-10 skippr:size-7"
909
2040
  }),
910
- /* @__PURE__ */ jsx4("span", {
2041
+ /* @__PURE__ */ jsx5("span", {
911
2042
  className: "skippr:absolute skippr:-inset-[3px] skippr:animate-pulse skippr:rounded-[17px] skippr:border-2 skippr:border-bubble/50"
912
2043
  })
913
2044
  ]
@@ -919,18 +2050,18 @@ function WelcomeBubble({
919
2050
  position,
920
2051
  onDismiss
921
2052
  }) {
922
- useEffect4(() => {
2053
+ useEffect6(() => {
923
2054
  const timer = setTimeout(onDismiss, 5000);
924
2055
  return () => clearTimeout(timer);
925
2056
  }, [onDismiss]);
926
- return /* @__PURE__ */ jsxs4("button", {
2057
+ return /* @__PURE__ */ jsxs5("button", {
927
2058
  type: "button",
928
2059
  className: cn("skippr:absolute skippr:bottom-full skippr:mb-3", "skippr:max-w-64 skippr:rounded-xl skippr:bg-card skippr:shadow-lg", "skippr:border skippr:border-border skippr:px-4 skippr:py-3", "skippr:text-sm skippr:text-foreground skippr:leading-relaxed skippr:text-left", "skippr:animate-[skippr-fade-in_0.3s_ease-out]", "skippr:cursor-pointer", position === "right" ? "skippr:right-0" : "skippr:left-0"),
929
2060
  onClick: onDismiss,
930
2061
  "aria-label": "Dismiss",
931
2062
  children: [
932
2063
  message,
933
- /* @__PURE__ */ jsx4("span", {
2064
+ /* @__PURE__ */ jsx5("span", {
934
2065
  className: cn("skippr:absolute skippr:top-full skippr:size-2.5", "skippr:border-l skippr:border-t skippr:border-border skippr:bg-card", "skippr:rotate-[225deg]", position === "right" ? "skippr:right-5" : "skippr:left-5", "skippr:-mt-[5px]")
935
2066
  })
936
2067
  ]
@@ -943,27 +2074,27 @@ function MinimizedBubble({
943
2074
  }) {
944
2075
  const { isConnected, isStarting, position } = useLiveAgent();
945
2076
  const inSession = isConnected || isStarting;
946
- return /* @__PURE__ */ jsxs4("div", {
2077
+ return /* @__PURE__ */ jsxs5("div", {
947
2078
  className: cn("skippr:fixed skippr:bottom-6 skippr:z-[9999]", "skippr:flex skippr:items-center skippr:gap-2", position === "right" ? "skippr:right-6" : "skippr:left-6"),
948
2079
  children: [
949
- welcomeMessage && !inSession && !welcomeDismissed && /* @__PURE__ */ jsx4(WelcomeBubble, {
2080
+ welcomeMessage && !inSession && !welcomeDismissed && /* @__PURE__ */ jsx5(WelcomeBubble, {
950
2081
  message: welcomeMessage,
951
2082
  position,
952
2083
  onDismiss: onDismissWelcome
953
2084
  }),
954
- inSession ? /* @__PURE__ */ jsx4(ConnectedBubbleContent, {}) : /* @__PURE__ */ jsx4(IdleBubbleContent, {})
2085
+ inSession ? /* @__PURE__ */ jsx5(ConnectedBubbleContent, {}) : /* @__PURE__ */ jsx5(IdleBubbleContent, {})
955
2086
  ]
956
2087
  });
957
2088
  }
958
2089
 
959
2090
  // src/components/Sidebar.tsx
960
- import { useEffect as useEffect11 } from "react";
2091
+ import { useEffect as useEffect13 } from "react";
961
2092
 
962
2093
  // src/hooks/useCombinedMessages.ts
963
2094
  import { useMemo as useMemo4 } from "react";
964
2095
 
965
2096
  // src/hooks/useChatMessages.ts
966
- import { useChat, useLocalParticipant as useLocalParticipant3 } from "@livekit/components-react";
2097
+ import { useChat, useLocalParticipant as useLocalParticipant4 } from "@livekit/components-react/hooks";
967
2098
  import { useMemo as useMemo2 } from "react";
968
2099
 
969
2100
  // src/lib/filterSystemMessages.ts
@@ -975,7 +2106,7 @@ function filterSystemMessages(messages) {
975
2106
  // src/hooks/useChatMessages.ts
976
2107
  function useChatMessages() {
977
2108
  const { chatMessages: rawMessages, send, isSending } = useChat();
978
- const { localParticipant } = useLocalParticipant3();
2109
+ const { localParticipant } = useLocalParticipant4();
979
2110
  const localIdentity = localParticipant.identity;
980
2111
  const chatMessages = useMemo2(() => {
981
2112
  const sortedMessages = rawMessages.map((msg) => ({
@@ -991,11 +2122,11 @@ function useChatMessages() {
991
2122
  }
992
2123
 
993
2124
  // src/hooks/useStreamingTranscript.ts
994
- import { useLocalParticipant as useLocalParticipant4, useTranscriptions } from "@livekit/components-react";
2125
+ import { useLocalParticipant as useLocalParticipant5, useTranscriptions } from "@livekit/components-react/hooks";
995
2126
  import { useMemo as useMemo3 } from "react";
996
2127
  function useStreamingTranscript() {
997
2128
  const transcriptions = useTranscriptions();
998
- const { localParticipant } = useLocalParticipant4();
2129
+ const { localParticipant } = useLocalParticipant5();
999
2130
  const localIdentity = localParticipant.identity;
1000
2131
  const transcriptMessages = useMemo3(() => filterSystemMessages(transcriptions.filter((stream) => stream.text.trim().length > 0).map((stream) => ({
1001
2132
  id: stream.streamInfo.id,
@@ -1039,15 +2170,15 @@ function useCombinedMessages() {
1039
2170
  }
1040
2171
 
1041
2172
  // src/hooks/usePhaseUpdates.ts
1042
- import { useCallback as useCallback4 } from "react";
2173
+ import { useCallback as useCallback5 } from "react";
1043
2174
 
1044
2175
  // src/hooks/useAgentState.ts
1045
- import { useRemoteParticipants } from "@livekit/components-react";
1046
- import { useEffect as useEffect5, useState as useState3 } from "react";
2176
+ import { useRemoteParticipants } from "@livekit/components-react/hooks";
2177
+ import { useEffect as useEffect7, useState as useState4 } from "react";
1047
2178
  function useAgentState(attributeKey, parse, initial) {
1048
- const [value, setValue] = useState3(initial);
2179
+ const [value, setValue] = useState4(initial);
1049
2180
  const remoteParticipants = useRemoteParticipants();
1050
- useEffect5(() => {
2181
+ useEffect7(() => {
1051
2182
  const agentParticipant = remoteParticipants.find((p) => p.attributes?.[attributeKey]);
1052
2183
  if (agentParticipant) {
1053
2184
  const attr = agentParticipant.attributes?.[attributeKey];
@@ -1093,13 +2224,13 @@ function parsePhases(json) {
1093
2224
  return null;
1094
2225
  }
1095
2226
  function usePhaseUpdates() {
1096
- const parse = useCallback4(parsePhases, []);
2227
+ const parse = useCallback5(parsePhases, []);
1097
2228
  const phases = useAgentState("phases", parse, []);
1098
2229
  return { phases };
1099
2230
  }
1100
2231
 
1101
2232
  // src/hooks/useSessionRemaining.ts
1102
- import { useEffect as useEffect6, useRef as useRef2, useState as useState4 } from "react";
2233
+ import { useEffect as useEffect8, useRef as useRef4, useState as useState5 } from "react";
1103
2234
 
1104
2235
  // src/lib/format.ts
1105
2236
  function formatTime(seconds) {
@@ -1115,9 +2246,9 @@ function parseNumber(s) {
1115
2246
  // src/hooks/useSessionRemaining.ts
1116
2247
  function useSessionRemaining() {
1117
2248
  const maxCallDuration = useAgentState("maxCallDuration", parseNumber, null);
1118
- const endTimeRef = useRef2(null);
1119
- const [remaining, setRemaining] = useState4(null);
1120
- useEffect6(() => {
2249
+ const endTimeRef = useRef4(null);
2250
+ const [remaining, setRemaining] = useState5(null);
2251
+ useEffect8(() => {
1121
2252
  if (maxCallDuration === null || endTimeRef.current !== null)
1122
2253
  return;
1123
2254
  const endTime = Date.now() + maxCallDuration * 1000;
@@ -1133,14 +2264,11 @@ function useSessionRemaining() {
1133
2264
  return remaining;
1134
2265
  }
1135
2266
 
1136
- // src/lib/constants.ts
1137
- var SIDEBAR_WIDTH = 360;
1138
-
1139
2267
  // src/hooks/useElapsedSeconds.ts
1140
- import { useEffect as useEffect7, useState as useState5 } from "react";
2268
+ import { useEffect as useEffect9, useState as useState6 } from "react";
1141
2269
  function useElapsedSeconds(isRunning) {
1142
- const [elapsed, setElapsed] = useState5(0);
1143
- useEffect7(() => {
2270
+ const [elapsed, setElapsed] = useState6(0);
2271
+ useEffect9(() => {
1144
2272
  if (!isRunning) {
1145
2273
  setElapsed(0);
1146
2274
  return;
@@ -1156,50 +2284,50 @@ function useElapsedSeconds(isRunning) {
1156
2284
  }
1157
2285
 
1158
2286
  // src/components/ChatHeader.tsx
1159
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2287
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1160
2288
  function ChatHeader() {
1161
2289
  const { isConnected, minimizePanel, minimizable } = useLiveAgent();
1162
2290
  const elapsed = useElapsedSeconds(isConnected);
1163
- return /* @__PURE__ */ jsxs5("header", {
2291
+ return /* @__PURE__ */ jsxs6("header", {
1164
2292
  className: "skippr:sticky skippr:top-0 skippr:z-10 skippr:flex skippr:shrink-0 skippr:items-center skippr:justify-between skippr:border-b skippr:border-border skippr:bg-primary skippr:px-4 skippr:py-3",
1165
2293
  children: [
1166
- /* @__PURE__ */ jsx5("p", {
2294
+ /* @__PURE__ */ jsx6("p", {
1167
2295
  className: "skippr:text-sm skippr:font-semibold skippr:text-primary-foreground",
1168
2296
  children: "Skippr"
1169
2297
  }),
1170
- /* @__PURE__ */ jsxs5("div", {
2298
+ /* @__PURE__ */ jsxs6("div", {
1171
2299
  className: "skippr:flex skippr:items-center skippr:gap-2",
1172
2300
  children: [
1173
- isConnected && /* @__PURE__ */ jsxs5("div", {
2301
+ isConnected && /* @__PURE__ */ jsxs6("div", {
1174
2302
  className: "skippr:flex skippr:items-center skippr:gap-1.5 skippr:rounded-full skippr:bg-primary-foreground/20 skippr:px-2.5 skippr:py-1",
1175
2303
  children: [
1176
- /* @__PURE__ */ jsxs5("span", {
2304
+ /* @__PURE__ */ jsxs6("span", {
1177
2305
  className: "skippr:relative skippr:flex skippr:size-1.5",
1178
2306
  children: [
1179
- /* @__PURE__ */ jsx5("span", {
2307
+ /* @__PURE__ */ jsx6("span", {
1180
2308
  className: "skippr:absolute skippr:inline-flex skippr:h-full skippr:w-full skippr:animate-ping skippr:rounded-full skippr:bg-red-400 skippr:opacity-75"
1181
2309
  }),
1182
- /* @__PURE__ */ jsx5("span", {
2310
+ /* @__PURE__ */ jsx6("span", {
1183
2311
  className: "skippr:relative skippr:inline-flex skippr:size-1.5 skippr:rounded-full skippr:bg-red-400"
1184
2312
  })
1185
2313
  ]
1186
2314
  }),
1187
- /* @__PURE__ */ jsx5("span", {
2315
+ /* @__PURE__ */ jsx6("span", {
1188
2316
  className: "skippr:text-[10px] skippr:font-medium skippr:text-primary-foreground",
1189
2317
  children: "REC"
1190
2318
  }),
1191
- /* @__PURE__ */ jsx5("span", {
2319
+ /* @__PURE__ */ jsx6("span", {
1192
2320
  className: "skippr:text-[10px] skippr:font-mono skippr:text-primary-foreground",
1193
2321
  children: formatTime(elapsed)
1194
2322
  })
1195
2323
  ]
1196
2324
  }),
1197
- minimizable && /* @__PURE__ */ jsx5("button", {
2325
+ minimizable && /* @__PURE__ */ jsx6("button", {
1198
2326
  type: "button",
1199
2327
  onClick: minimizePanel,
1200
2328
  "aria-label": "Minimize",
1201
2329
  className: "skippr:flex skippr:size-6 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-md skippr:text-primary-foreground/70 skippr:transition-colors skippr:hover:bg-primary-foreground/10 skippr:hover:text-primary-foreground",
1202
- children: /* @__PURE__ */ jsx5(Minimize2, {
2330
+ children: /* @__PURE__ */ jsx6(Minimize2, {
1203
2331
  className: "skippr:size-3.5"
1204
2332
  })
1205
2333
  })
@@ -1210,26 +2338,26 @@ function ChatHeader() {
1210
2338
  }
1211
2339
 
1212
2340
  // src/components/LoadingDots.tsx
1213
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2341
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1214
2342
  function LoadingDots({ label }) {
1215
- return /* @__PURE__ */ jsxs6("div", {
2343
+ return /* @__PURE__ */ jsxs7("div", {
1216
2344
  className: "skippr:flex skippr:items-center skippr:gap-2 skippr:py-4",
1217
2345
  children: [
1218
- /* @__PURE__ */ jsxs6("div", {
2346
+ /* @__PURE__ */ jsxs7("div", {
1219
2347
  className: "skippr:flex skippr:gap-1",
1220
2348
  children: [
1221
- /* @__PURE__ */ jsx6("span", {
2349
+ /* @__PURE__ */ jsx7("span", {
1222
2350
  className: "skippr:size-1.5 skippr:rounded-full skippr:bg-muted-foreground/40 skippr:animate-bounce skippr:[animation-delay:0ms]"
1223
2351
  }),
1224
- /* @__PURE__ */ jsx6("span", {
2352
+ /* @__PURE__ */ jsx7("span", {
1225
2353
  className: "skippr:size-1.5 skippr:rounded-full skippr:bg-muted-foreground/40 skippr:animate-bounce skippr:[animation-delay:150ms]"
1226
2354
  }),
1227
- /* @__PURE__ */ jsx6("span", {
2355
+ /* @__PURE__ */ jsx7("span", {
1228
2356
  className: "skippr:size-1.5 skippr:rounded-full skippr:bg-muted-foreground/40 skippr:animate-bounce skippr:[animation-delay:300ms]"
1229
2357
  })
1230
2358
  ]
1231
2359
  }),
1232
- /* @__PURE__ */ jsx6("p", {
2360
+ /* @__PURE__ */ jsx7("p", {
1233
2361
  className: "skippr:text-xs skippr:text-muted-foreground",
1234
2362
  children: label
1235
2363
  })
@@ -1238,11 +2366,11 @@ function LoadingDots({ label }) {
1238
2366
  }
1239
2367
 
1240
2368
  // src/components/LoginFlow.tsx
1241
- import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef3, useState as useState6 } from "react";
2369
+ import { useCallback as useCallback6, useEffect as useEffect10, useRef as useRef5, useState as useState7 } from "react";
1242
2370
 
1243
2371
  // src/components/ui/button.tsx
1244
2372
  import { forwardRef as forwardRef3 } from "react";
1245
- import { jsx as jsx7 } from "react/jsx-runtime";
2373
+ import { jsx as jsx8 } from "react/jsx-runtime";
1246
2374
  var variantClasses = {
1247
2375
  default: "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90",
1248
2376
  destructive: "skippr:bg-destructive skippr:text-white skippr:hover:bg-destructive/90",
@@ -1261,7 +2389,7 @@ var sizeClasses = {
1261
2389
  "icon-lg": "skippr:size-10"
1262
2390
  };
1263
2391
  var Button = forwardRef3(({ className, variant = "default", size = "default", ...props }, ref) => {
1264
- return /* @__PURE__ */ jsx7("button", {
2392
+ return /* @__PURE__ */ jsx8("button", {
1265
2393
  className: cn("skippr:inline-flex skippr:items-center skippr:justify-center skippr:gap-2 skippr:whitespace-nowrap skippr:rounded-md skippr:text-sm skippr:font-medium skippr:ring-offset-background skippr:transition-all skippr:cursor-pointer skippr:focus-visible:outline-none skippr:focus-visible:ring-2 skippr:focus-visible:ring-ring skippr:focus-visible:ring-offset-2 skippr:disabled:pointer-events-none skippr:disabled:opacity-50 skippr:shrink-0 skippr:[&_svg]:pointer-events-none skippr:[&_svg:not([class*='size-'])]:size-4 skippr:[&_svg]:shrink-0", variantClasses[variant], sizeClasses[size], className),
1266
2394
  ref,
1267
2395
  ...props
@@ -1270,28 +2398,28 @@ var Button = forwardRef3(({ className, variant = "default", size = "default", ..
1270
2398
  Button.displayName = "Button";
1271
2399
 
1272
2400
  // src/components/LoginFlow.tsx
1273
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2401
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1274
2402
  var OTP_LENGTH = 6;
1275
2403
  var DIGIT_KEYS = ["d0", "d1", "d2", "d3", "d4", "d5"];
1276
2404
  function LoginFlow({ requestOtp, verifyOtp, error, isSubmitting }) {
1277
- const [step, setStep] = useState6("email");
1278
- const [email, setEmail] = useState6("");
1279
- const handleRequestOtp = useCallback5(async (emailValue) => {
2405
+ const [step, setStep] = useState7("email");
2406
+ const [email, setEmail] = useState7("");
2407
+ const handleRequestOtp = useCallback6(async (emailValue) => {
1280
2408
  const success = await requestOtp(emailValue);
1281
2409
  if (success)
1282
2410
  setStep("otp");
1283
2411
  }, [requestOtp]);
1284
- const handleVerifyOtp = useCallback5(async (code) => {
2412
+ const handleVerifyOtp = useCallback6(async (code) => {
1285
2413
  await verifyOtp(email, code);
1286
2414
  }, [verifyOtp, email]);
1287
- const handleBack = useCallback5(() => {
2415
+ const handleBack = useCallback6(() => {
1288
2416
  setStep("email");
1289
2417
  }, []);
1290
- const handleResend = useCallback5(async () => {
2418
+ const handleResend = useCallback6(async () => {
1291
2419
  await requestOtp(email);
1292
2420
  }, [requestOtp, email]);
1293
2421
  if (step === "otp") {
1294
- return /* @__PURE__ */ jsx8(OtpStep, {
2422
+ return /* @__PURE__ */ jsx9(OtpStep, {
1295
2423
  email,
1296
2424
  onSubmit: handleVerifyOtp,
1297
2425
  onResend: handleResend,
@@ -1300,7 +2428,7 @@ function LoginFlow({ requestOtp, verifyOtp, error, isSubmitting }) {
1300
2428
  isSubmitting
1301
2429
  });
1302
2430
  }
1303
- return /* @__PURE__ */ jsx8(EmailStep, {
2431
+ return /* @__PURE__ */ jsx9(EmailStep, {
1304
2432
  email,
1305
2433
  onEmailChange: setEmail,
1306
2434
  onSubmit: handleRequestOtp,
@@ -1314,30 +2442,30 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
1314
2442
  if (email.trim())
1315
2443
  onSubmit(email.trim());
1316
2444
  }
1317
- return /* @__PURE__ */ jsxs7("div", {
2445
+ return /* @__PURE__ */ jsxs8("div", {
1318
2446
  className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:px-4 skippr:py-4",
1319
2447
  children: [
1320
- /* @__PURE__ */ jsxs7("div", {
2448
+ /* @__PURE__ */ jsxs8("div", {
1321
2449
  className: "skippr:mb-4 skippr:text-center",
1322
2450
  children: [
1323
- /* @__PURE__ */ jsx8(Mail, {
2451
+ /* @__PURE__ */ jsx9(Mail, {
1324
2452
  className: "skippr:mx-auto skippr:mb-2 skippr:size-6 skippr:text-primary"
1325
2453
  }),
1326
- /* @__PURE__ */ jsx8("p", {
2454
+ /* @__PURE__ */ jsx9("p", {
1327
2455
  className: "skippr:text-sm skippr:font-medium skippr:text-foreground",
1328
2456
  children: "Sign in to continue"
1329
2457
  }),
1330
- /* @__PURE__ */ jsx8("p", {
2458
+ /* @__PURE__ */ jsx9("p", {
1331
2459
  className: "skippr:mt-1 skippr:text-xs skippr:text-muted-foreground",
1332
2460
  children: "Your email will be used to identify you across sessions"
1333
2461
  })
1334
2462
  ]
1335
2463
  }),
1336
- /* @__PURE__ */ jsxs7("form", {
2464
+ /* @__PURE__ */ jsxs8("form", {
1337
2465
  onSubmit: handleSubmit,
1338
2466
  className: "skippr:flex skippr:flex-col skippr:gap-3",
1339
2467
  children: [
1340
- /* @__PURE__ */ jsx8("input", {
2468
+ /* @__PURE__ */ jsx9("input", {
1341
2469
  type: "email",
1342
2470
  placeholder: "you@example.com",
1343
2471
  value: email,
@@ -1346,15 +2474,15 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
1346
2474
  required: true,
1347
2475
  className: "skippr:w-full skippr:rounded-md skippr:border skippr:border-border skippr:bg-background skippr:px-3 skippr:py-2 skippr:text-sm skippr:text-foreground skippr:placeholder-muted-foreground skippr:outline-none focus:skippr:ring-2 focus:skippr:ring-primary/30 focus:skippr:border-primary disabled:skippr:opacity-50"
1348
2476
  }),
1349
- /* @__PURE__ */ jsx8(Button, {
2477
+ /* @__PURE__ */ jsx9(Button, {
1350
2478
  type: "submit",
1351
2479
  disabled: isSubmitting || !email.trim(),
1352
2480
  className: "skippr:w-full",
1353
- children: isSubmitting ? /* @__PURE__ */ jsx8(LoaderCircle, {
2481
+ children: isSubmitting ? /* @__PURE__ */ jsx9(LoaderCircle, {
1354
2482
  className: "skippr:size-4 skippr:animate-spin"
1355
2483
  }) : "Continue"
1356
2484
  }),
1357
- error && /* @__PURE__ */ jsx8("p", {
2485
+ error && /* @__PURE__ */ jsx9("p", {
1358
2486
  className: "skippr:text-xs skippr:text-center skippr:text-destructive",
1359
2487
  children: error
1360
2488
  })
@@ -1364,30 +2492,30 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
1364
2492
  });
1365
2493
  }
1366
2494
  function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1367
- const [digits, setDigits] = useState6(Array(OTP_LENGTH).fill(""));
1368
- const [resendCooldown, setResendCooldown] = useState6(0);
1369
- const inputRefs = useRef3([]);
1370
- const submittedRef = useRef3(false);
1371
- useEffect8(() => {
2495
+ const [digits, setDigits] = useState7(Array(OTP_LENGTH).fill(""));
2496
+ const [resendCooldown, setResendCooldown] = useState7(0);
2497
+ const inputRefs = useRef5([]);
2498
+ const submittedRef = useRef5(false);
2499
+ useEffect10(() => {
1372
2500
  inputRefs.current[0]?.focus();
1373
2501
  }, []);
1374
- useEffect8(() => {
2502
+ useEffect10(() => {
1375
2503
  if (error)
1376
2504
  submittedRef.current = false;
1377
2505
  }, [error]);
1378
- useEffect8(() => {
2506
+ useEffect10(() => {
1379
2507
  if (resendCooldown <= 0)
1380
2508
  return;
1381
2509
  const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000);
1382
2510
  return () => clearTimeout(timer);
1383
2511
  }, [resendCooldown]);
1384
- const submitCode = useCallback5((code) => {
2512
+ const submitCode = useCallback6((code) => {
1385
2513
  if (submittedRef.current || isSubmitting)
1386
2514
  return;
1387
2515
  submittedRef.current = true;
1388
2516
  onSubmit(code);
1389
2517
  }, [onSubmit, isSubmitting]);
1390
- const handleDigitChange = useCallback5((index2, value) => {
2518
+ const handleDigitChange = useCallback6((index2, value) => {
1391
2519
  const digit = value.replace(/\D/g, "").slice(-1);
1392
2520
  const newDigits = [...digits];
1393
2521
  newDigits[index2] = digit;
@@ -1401,12 +2529,12 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1401
2529
  submitCode(code);
1402
2530
  }
1403
2531
  }, [digits, submitCode]);
1404
- const handleKeyDown = useCallback5((index2, e) => {
2532
+ const handleKeyDown = useCallback6((index2, e) => {
1405
2533
  if (e.key === "Backspace" && !digits[index2] && index2 > 0) {
1406
2534
  inputRefs.current[index2 - 1]?.focus();
1407
2535
  }
1408
2536
  }, [digits]);
1409
- const handlePaste = useCallback5((e) => {
2537
+ const handlePaste = useCallback6((e) => {
1410
2538
  e.preventDefault();
1411
2539
  const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, OTP_LENGTH);
1412
2540
  if (pasted.length > 0) {
@@ -1434,22 +2562,22 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1434
2562
  submittedRef.current = false;
1435
2563
  inputRefs.current[0]?.focus();
1436
2564
  }
1437
- return /* @__PURE__ */ jsxs7("div", {
2565
+ return /* @__PURE__ */ jsxs8("div", {
1438
2566
  className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:px-4 skippr:py-4",
1439
2567
  children: [
1440
- /* @__PURE__ */ jsxs7("div", {
2568
+ /* @__PURE__ */ jsxs8("div", {
1441
2569
  className: "skippr:mb-4 skippr:text-center",
1442
2570
  children: [
1443
- /* @__PURE__ */ jsx8("p", {
2571
+ /* @__PURE__ */ jsx9("p", {
1444
2572
  className: "skippr:text-sm skippr:font-medium skippr:text-foreground",
1445
2573
  children: "Enter verification code"
1446
2574
  }),
1447
- /* @__PURE__ */ jsxs7("p", {
2575
+ /* @__PURE__ */ jsxs8("p", {
1448
2576
  className: "skippr:mt-1 skippr:text-xs skippr:text-muted-foreground",
1449
2577
  children: [
1450
2578
  "We sent a 6-digit code to",
1451
2579
  " ",
1452
- /* @__PURE__ */ jsx8("span", {
2580
+ /* @__PURE__ */ jsx9("span", {
1453
2581
  className: "skippr:font-medium skippr:text-foreground",
1454
2582
  children: email
1455
2583
  })
@@ -1457,13 +2585,13 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1457
2585
  })
1458
2586
  ]
1459
2587
  }),
1460
- /* @__PURE__ */ jsxs7("form", {
2588
+ /* @__PURE__ */ jsxs8("form", {
1461
2589
  onSubmit: handleSubmit,
1462
2590
  className: "skippr:flex skippr:flex-col skippr:gap-3",
1463
2591
  children: [
1464
- /* @__PURE__ */ jsx8("div", {
2592
+ /* @__PURE__ */ jsx9("div", {
1465
2593
  className: "skippr:flex skippr:justify-center skippr:gap-1.5",
1466
- children: digits.map((digit, index2) => /* @__PURE__ */ jsx8("input", {
2594
+ children: digits.map((digit, index2) => /* @__PURE__ */ jsx9("input", {
1467
2595
  ref: (el) => {
1468
2596
  inputRefs.current[index2] = el;
1469
2597
  },
@@ -1478,29 +2606,29 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1478
2606
  className: "skippr:h-10 skippr:w-10 skippr:rounded-md skippr:border skippr:border-border skippr:bg-background skippr:text-center skippr:text-sm skippr:font-semibold skippr:text-foreground skippr:outline-none focus:skippr:ring-2 focus:skippr:ring-primary/30 focus:skippr:border-primary disabled:skippr:opacity-50"
1479
2607
  }, DIGIT_KEYS[index2]))
1480
2608
  }),
1481
- error && /* @__PURE__ */ jsx8("p", {
2609
+ error && /* @__PURE__ */ jsx9("p", {
1482
2610
  className: "skippr:text-xs skippr:text-center skippr:text-destructive",
1483
2611
  children: error
1484
2612
  }),
1485
- /* @__PURE__ */ jsx8(Button, {
2613
+ /* @__PURE__ */ jsx9(Button, {
1486
2614
  type: "submit",
1487
2615
  disabled: isSubmitting || digits.join("").length !== OTP_LENGTH,
1488
2616
  className: "skippr:w-full",
1489
- children: isSubmitting ? /* @__PURE__ */ jsx8(LoaderCircle, {
2617
+ children: isSubmitting ? /* @__PURE__ */ jsx9(LoaderCircle, {
1490
2618
  className: "skippr:size-4 skippr:animate-spin"
1491
2619
  }) : "Verify"
1492
2620
  }),
1493
- /* @__PURE__ */ jsxs7("div", {
2621
+ /* @__PURE__ */ jsxs8("div", {
1494
2622
  className: "skippr:flex skippr:items-center skippr:justify-between skippr:text-xs",
1495
2623
  children: [
1496
- /* @__PURE__ */ jsx8("button", {
2624
+ /* @__PURE__ */ jsx9("button", {
1497
2625
  type: "button",
1498
2626
  onClick: onBack,
1499
2627
  disabled: isSubmitting,
1500
2628
  className: "skippr:text-muted-foreground hover:skippr:text-foreground skippr:transition-colors disabled:skippr:opacity-50",
1501
2629
  children: "Change email"
1502
2630
  }),
1503
- /* @__PURE__ */ jsx8("button", {
2631
+ /* @__PURE__ */ jsx9("button", {
1504
2632
  type: "button",
1505
2633
  onClick: handleResend,
1506
2634
  disabled: isSubmitting || resendCooldown > 0,
@@ -1516,50 +2644,50 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1516
2644
  }
1517
2645
 
1518
2646
  // src/components/MeetingControls.tsx
1519
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2647
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1520
2648
  var CONTROL_BUTTON = "skippr:flex skippr:size-11 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-full skippr:transition-colors";
1521
- function MeetingControls({ onHangUp }) {
2649
+ function MeetingControls({ onHangUp, showScreenShareToggle = true }) {
1522
2650
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
1523
- return /* @__PURE__ */ jsxs8("div", {
2651
+ return /* @__PURE__ */ jsxs9("div", {
1524
2652
  className: "skippr:shrink-0 skippr:border-t skippr:border-border skippr:bg-background skippr:px-4 skippr:py-4",
1525
2653
  children: [
1526
- /* @__PURE__ */ jsxs8("div", {
2654
+ /* @__PURE__ */ jsxs9("div", {
1527
2655
  className: "skippr:flex skippr:items-center skippr:justify-center skippr:gap-3",
1528
2656
  children: [
1529
- /* @__PURE__ */ jsx9("button", {
2657
+ /* @__PURE__ */ jsx10("button", {
1530
2658
  type: "button",
1531
2659
  onClick: toggleMute,
1532
2660
  "aria-label": isMuted ? "Unmute" : "Mute",
1533
2661
  className: cn(CONTROL_BUTTON, isMuted ? "skippr:bg-destructive/15 skippr:text-destructive skippr:hover:bg-destructive/25" : "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80"),
1534
- children: isMuted ? /* @__PURE__ */ jsx9(MicOff, {
2662
+ children: isMuted ? /* @__PURE__ */ jsx10(MicOff, {
1535
2663
  className: "skippr:size-5"
1536
- }) : /* @__PURE__ */ jsx9(Mic, {
2664
+ }) : /* @__PURE__ */ jsx10(Mic, {
1537
2665
  className: "skippr:size-5"
1538
2666
  })
1539
2667
  }),
1540
- /* @__PURE__ */ jsx9("button", {
2668
+ showScreenShareToggle && /* @__PURE__ */ jsx10("button", {
1541
2669
  type: "button",
1542
2670
  onClick: toggleScreenShare,
1543
2671
  "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
1544
2672
  className: cn(CONTROL_BUTTON, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80"),
1545
- children: isScreenSharing ? /* @__PURE__ */ jsx9(MonitorOff, {
2673
+ children: isScreenSharing ? /* @__PURE__ */ jsx10(MonitorOff, {
1546
2674
  className: "skippr:size-5"
1547
- }) : /* @__PURE__ */ jsx9(Monitor, {
2675
+ }) : /* @__PURE__ */ jsx10(Monitor, {
1548
2676
  className: "skippr:size-5"
1549
2677
  })
1550
2678
  }),
1551
- /* @__PURE__ */ jsx9("button", {
2679
+ /* @__PURE__ */ jsx10("button", {
1552
2680
  type: "button",
1553
2681
  onClick: onHangUp,
1554
2682
  "aria-label": "End session",
1555
2683
  className: cn(CONTROL_BUTTON, "skippr:bg-destructive skippr:text-destructive-foreground skippr:hover:bg-destructive/90"),
1556
- children: /* @__PURE__ */ jsx9(PhoneOff, {
2684
+ children: /* @__PURE__ */ jsx10(PhoneOff, {
1557
2685
  className: "skippr:size-5"
1558
2686
  })
1559
2687
  })
1560
2688
  ]
1561
2689
  }),
1562
- /* @__PURE__ */ jsx9("p", {
2690
+ /* @__PURE__ */ jsx10("p", {
1563
2691
  className: "skippr:mt-3 skippr:text-center skippr:text-[10px] skippr:text-muted-foreground",
1564
2692
  children: "Powered by Skippr"
1565
2693
  })
@@ -1568,17 +2696,17 @@ function MeetingControls({ onHangUp }) {
1568
2696
  }
1569
2697
 
1570
2698
  // src/components/MessageList.tsx
1571
- import { useEffect as useEffect10, useRef as useRef5 } from "react";
2699
+ import { useEffect as useEffect12, useRef as useRef7 } from "react";
1572
2700
 
1573
2701
  // src/components/ChatInput.tsx
1574
- import { useEffect as useEffect9, useRef as useRef4, useState as useState7 } from "react";
1575
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2702
+ import { useEffect as useEffect11, useRef as useRef6, useState as useState8 } from "react";
2703
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1576
2704
  var MAX_INPUT_HEIGHT = 60;
1577
2705
  function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
1578
- const [inputText, setInputText] = useState7("");
1579
- const textareaRef = useRef4(null);
2706
+ const [inputText, setInputText] = useState8("");
2707
+ const textareaRef = useRef6(null);
1580
2708
  const canSend = inputText.trim().length > 0 && !isSendingChat;
1581
- useEffect9(() => {
2709
+ useEffect11(() => {
1582
2710
  if (autoFocus)
1583
2711
  textareaRef.current?.focus();
1584
2712
  }, [autoFocus]);
@@ -1609,13 +2737,13 @@ function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
1609
2737
  handleSubmit(e);
1610
2738
  }
1611
2739
  }
1612
- return /* @__PURE__ */ jsx10("form", {
2740
+ return /* @__PURE__ */ jsx11("form", {
1613
2741
  onSubmit: handleSubmit,
1614
2742
  className: "skippr:border-t skippr:border-border skippr:p-3",
1615
- children: /* @__PURE__ */ jsxs9("div", {
2743
+ children: /* @__PURE__ */ jsxs10("div", {
1616
2744
  className: "skippr:flex skippr:items-center skippr:gap-2 skippr:rounded-xl skippr:bg-background skippr:ring-1 skippr:ring-foreground/10 skippr:px-3 skippr:py-2",
1617
2745
  children: [
1618
- /* @__PURE__ */ jsx10("textarea", {
2746
+ /* @__PURE__ */ jsx11("textarea", {
1619
2747
  ref: textareaRef,
1620
2748
  rows: 1,
1621
2749
  value: inputText,
@@ -1625,12 +2753,12 @@ function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
1625
2753
  className: "skippr:flex-1 skippr:resize-none skippr:overflow-y-auto skippr:bg-transparent skippr:text-sm skippr:leading-5 skippr:text-foreground skippr:placeholder:text-muted-foreground skippr:outline-none",
1626
2754
  style: { maxHeight: `${MAX_INPUT_HEIGHT}px` }
1627
2755
  }),
1628
- /* @__PURE__ */ jsx10("button", {
2756
+ /* @__PURE__ */ jsx11("button", {
1629
2757
  type: "submit",
1630
2758
  disabled: !canSend,
1631
2759
  "aria-label": "Send message",
1632
2760
  className: cn("skippr:flex skippr:size-8 skippr:shrink-0 skippr:items-center skippr:justify-center skippr:rounded-lg skippr:transition-colors", canSend ? "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90" : "skippr:bg-muted-foreground/20 skippr:text-muted-foreground/60"),
1633
- children: /* @__PURE__ */ jsx10(Send, {
2761
+ children: /* @__PURE__ */ jsx11(Send, {
1634
2762
  className: "skippr:size-3.5"
1635
2763
  })
1636
2764
  })
@@ -1640,7 +2768,7 @@ function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
1640
2768
  }
1641
2769
 
1642
2770
  // src/components/ChatMessage.tsx
1643
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2771
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1644
2772
  function formatTimestamp(ts) {
1645
2773
  return new Date(ts).toLocaleTimeString("en-US", {
1646
2774
  hour: "numeric",
@@ -1650,23 +2778,23 @@ function formatTimestamp(ts) {
1650
2778
  }
1651
2779
  function ChatMessage({ message }) {
1652
2780
  const isAgent = message.role === "assistant";
1653
- return /* @__PURE__ */ jsxs10("div", {
2781
+ return /* @__PURE__ */ jsxs11("div", {
1654
2782
  className: cn("skippr:flex skippr:gap-2", isAgent ? "skippr:items-start" : "skippr:justify-end"),
1655
2783
  children: [
1656
- isAgent && /* @__PURE__ */ jsx11("div", {
2784
+ isAgent && /* @__PURE__ */ jsx12("div", {
1657
2785
  className: "skippr:mt-0.5 skippr:flex skippr:size-7 skippr:shrink-0 skippr:items-center skippr:justify-center skippr:rounded-md skippr:bg-primary",
1658
- children: /* @__PURE__ */ jsx11(Sparkles, {
2786
+ children: /* @__PURE__ */ jsx12(Sparkles, {
1659
2787
  className: "skippr:size-3.5 skippr:text-primary-foreground"
1660
2788
  })
1661
2789
  }),
1662
- /* @__PURE__ */ jsxs10("div", {
2790
+ /* @__PURE__ */ jsxs11("div", {
1663
2791
  className: cn("skippr:flex skippr:max-w-[80%] skippr:flex-col", isAgent ? "skippr:items-start" : "skippr:items-end"),
1664
2792
  children: [
1665
- /* @__PURE__ */ jsx11("div", {
2793
+ /* @__PURE__ */ jsx12("div", {
1666
2794
  className: cn("skippr:rounded-2xl skippr:px-4 skippr:py-2.5 skippr:text-sm skippr:leading-relaxed", isAgent ? "skippr:border skippr:border-border skippr:bg-card skippr:text-foreground" : "skippr:bg-primary skippr:text-primary-foreground"),
1667
2795
  children: message.content
1668
2796
  }),
1669
- message.timestamp && /* @__PURE__ */ jsx11("span", {
2797
+ message.timestamp && /* @__PURE__ */ jsx12("span", {
1670
2798
  className: "skippr:mt-1 skippr:px-1 skippr:text-[10px] skippr:text-muted-foreground/60",
1671
2799
  children: formatTimestamp(message.timestamp)
1672
2800
  })
@@ -1677,33 +2805,33 @@ function ChatMessage({ message }) {
1677
2805
  }
1678
2806
 
1679
2807
  // src/components/TypingIndicator.tsx
1680
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2808
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1681
2809
  function TypingIndicator() {
1682
- return /* @__PURE__ */ jsxs11("div", {
2810
+ return /* @__PURE__ */ jsxs12("div", {
1683
2811
  className: "skippr:flex skippr:items-start skippr:gap-2 skippr:animate-skippr-tab-fade",
1684
2812
  children: [
1685
- /* @__PURE__ */ jsx12("div", {
2813
+ /* @__PURE__ */ jsx13("div", {
1686
2814
  className: "skippr:mt-0.5 skippr:flex skippr:size-7 skippr:shrink-0 skippr:items-center skippr:justify-center skippr:rounded-md skippr:bg-primary",
1687
- children: /* @__PURE__ */ jsx12(Sparkles, {
2815
+ children: /* @__PURE__ */ jsx13(Sparkles, {
1688
2816
  className: "skippr:size-3.5 skippr:text-primary-foreground"
1689
2817
  })
1690
2818
  }),
1691
- /* @__PURE__ */ jsxs11("div", {
2819
+ /* @__PURE__ */ jsxs12("div", {
1692
2820
  className: "skippr:inline-flex skippr:items-center skippr:gap-1 skippr:rounded-2xl skippr:border skippr:border-primary/20 skippr:bg-primary/10 skippr:px-4 skippr:py-2 skippr:text-xs skippr:text-primary",
1693
2821
  children: [
1694
- /* @__PURE__ */ jsx12("span", {
2822
+ /* @__PURE__ */ jsx13("span", {
1695
2823
  children: "Agent is analyzing your screen"
1696
2824
  }),
1697
- /* @__PURE__ */ jsxs11("span", {
2825
+ /* @__PURE__ */ jsxs12("span", {
1698
2826
  className: "skippr:inline-flex skippr:items-center skippr:gap-[2px]",
1699
2827
  children: [
1700
- /* @__PURE__ */ jsx12("span", {
2828
+ /* @__PURE__ */ jsx13("span", {
1701
2829
  className: "skippr:size-1 skippr:rounded-full skippr:bg-primary skippr:animate-skippr-thinking-dot skippr:[animation-delay:0ms]"
1702
2830
  }),
1703
- /* @__PURE__ */ jsx12("span", {
2831
+ /* @__PURE__ */ jsx13("span", {
1704
2832
  className: "skippr:size-1 skippr:rounded-full skippr:bg-primary skippr:animate-skippr-thinking-dot skippr:[animation-delay:200ms]"
1705
2833
  }),
1706
- /* @__PURE__ */ jsx12("span", {
2834
+ /* @__PURE__ */ jsx13("span", {
1707
2835
  className: "skippr:size-1 skippr:rounded-full skippr:bg-primary skippr:animate-skippr-thinking-dot skippr:[animation-delay:400ms]"
1708
2836
  })
1709
2837
  ]
@@ -1715,7 +2843,7 @@ function TypingIndicator() {
1715
2843
  }
1716
2844
 
1717
2845
  // src/components/MessageList.tsx
1718
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2846
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1719
2847
  function MessageList({
1720
2848
  messages,
1721
2849
  isStreaming,
@@ -1723,31 +2851,31 @@ function MessageList({
1723
2851
  isSendingChat,
1724
2852
  autoFocus = false
1725
2853
  }) {
1726
- const scrollRef = useRef5(null);
2854
+ const scrollRef = useRef7(null);
1727
2855
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
1728
- useEffect10(() => {
2856
+ useEffect12(() => {
1729
2857
  scrollRef.current?.scrollIntoView({ behavior: "smooth" });
1730
2858
  }, [messages.length, lastMessage?.content]);
1731
2859
  const showTyping = isStreaming && lastMessage?.role === "assistant" && lastMessage.content === "";
1732
- return /* @__PURE__ */ jsxs12("div", {
2860
+ return /* @__PURE__ */ jsxs13("div", {
1733
2861
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col",
1734
2862
  children: [
1735
- /* @__PURE__ */ jsxs12("div", {
2863
+ /* @__PURE__ */ jsxs13("div", {
1736
2864
  className: "skippr:min-h-0 skippr:flex-1 skippr:space-y-4 skippr:overflow-y-auto skippr:p-4",
1737
2865
  children: [
1738
- messages.length === 0 && !showTyping && /* @__PURE__ */ jsx13(LoadingDots, {
2866
+ messages.length === 0 && !showTyping && /* @__PURE__ */ jsx14(LoadingDots, {
1739
2867
  label: "Waiting for conversation to begin..."
1740
2868
  }),
1741
- messages.map((message) => /* @__PURE__ */ jsx13(ChatMessage, {
2869
+ messages.map((message) => /* @__PURE__ */ jsx14(ChatMessage, {
1742
2870
  message
1743
2871
  }, message.id)),
1744
- showTyping && /* @__PURE__ */ jsx13(TypingIndicator, {}),
1745
- /* @__PURE__ */ jsx13("div", {
2872
+ showTyping && /* @__PURE__ */ jsx14(TypingIndicator, {}),
2873
+ /* @__PURE__ */ jsx14("div", {
1746
2874
  ref: scrollRef
1747
2875
  })
1748
2876
  ]
1749
2877
  }),
1750
- /* @__PURE__ */ jsx13(ChatInput, {
2878
+ /* @__PURE__ */ jsx14(ChatInput, {
1751
2879
  sendChatMessage,
1752
2880
  isSendingChat,
1753
2881
  autoFocus
@@ -1757,49 +2885,49 @@ function MessageList({
1757
2885
  }
1758
2886
 
1759
2887
  // src/components/SessionAgenda.tsx
1760
- import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2888
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1761
2889
  function SessionAgenda({ phases, hasStarted }) {
1762
2890
  if (phases.length === 0 || !hasStarted) {
1763
- return /* @__PURE__ */ jsx14("div", {
2891
+ return /* @__PURE__ */ jsx15("div", {
1764
2892
  className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
1765
- children: /* @__PURE__ */ jsx14(LoadingDots, {
2893
+ children: /* @__PURE__ */ jsx15(LoadingDots, {
1766
2894
  label: "Waiting for agenda to load..."
1767
2895
  })
1768
2896
  });
1769
2897
  }
1770
- return /* @__PURE__ */ jsx14("div", {
2898
+ return /* @__PURE__ */ jsx15("div", {
1771
2899
  className: "skippr:flex-1 skippr:overflow-y-auto skippr:px-4 skippr:py-4",
1772
- children: /* @__PURE__ */ jsx14("div", {
2900
+ children: /* @__PURE__ */ jsx15("div", {
1773
2901
  className: "skippr:space-y-1",
1774
2902
  children: phases.map((phase) => {
1775
2903
  const isActive = phase.status === "active";
1776
2904
  const isCompleted = phase.status === "completed";
1777
- return /* @__PURE__ */ jsxs13("div", {
2905
+ return /* @__PURE__ */ jsxs14("div", {
1778
2906
  className: cn("skippr:flex skippr:items-start skippr:gap-2.5 skippr:rounded-lg skippr:p-2 skippr:transition-colors", isActive && "skippr:bg-primary/10"),
1779
2907
  children: [
1780
- /* @__PURE__ */ jsx14("div", {
2908
+ /* @__PURE__ */ jsx15("div", {
1781
2909
  className: "skippr:mt-0.5",
1782
- children: isCompleted ? /* @__PURE__ */ jsx14(CircleCheck, {
2910
+ children: isCompleted ? /* @__PURE__ */ jsx15(CircleCheck, {
1783
2911
  className: "skippr:size-4 skippr:text-chart-3"
1784
- }) : isActive ? /* @__PURE__ */ jsx14(Circle, {
2912
+ }) : isActive ? /* @__PURE__ */ jsx15(Circle, {
1785
2913
  className: "skippr:size-4 skippr:fill-primary/30 skippr:text-primary"
1786
- }) : /* @__PURE__ */ jsx14(Circle, {
2914
+ }) : /* @__PURE__ */ jsx15(Circle, {
1787
2915
  className: "skippr:size-4 skippr:text-muted-foreground/30"
1788
2916
  })
1789
2917
  }),
1790
- /* @__PURE__ */ jsxs13("div", {
2918
+ /* @__PURE__ */ jsxs14("div", {
1791
2919
  className: "skippr:min-w-0 skippr:flex-1",
1792
2920
  children: [
1793
- /* @__PURE__ */ jsx14("p", {
2921
+ /* @__PURE__ */ jsx15("p", {
1794
2922
  className: cn("skippr:text-sm", isCompleted && "skippr:text-muted-foreground skippr:line-through", isActive && "skippr:font-medium skippr:text-foreground", phase.status === "pending" && "skippr:text-muted-foreground"),
1795
2923
  children: phase.name
1796
2924
  }),
1797
- phase.highlights.length > 0 && /* @__PURE__ */ jsx14("ul", {
2925
+ phase.highlights.length > 0 && /* @__PURE__ */ jsx15("ul", {
1798
2926
  className: "skippr:mt-1 skippr:space-y-0.5",
1799
- children: phase.highlights.map((text) => /* @__PURE__ */ jsxs13("li", {
2927
+ children: phase.highlights.map((text) => /* @__PURE__ */ jsxs14("li", {
1800
2928
  className: cn("skippr:flex skippr:items-center skippr:gap-1.5 skippr:text-[11px] skippr:leading-tight", isCompleted ? "skippr:text-muted-foreground/40 skippr:line-through" : "skippr:text-muted-foreground/70"),
1801
2929
  children: [
1802
- /* @__PURE__ */ jsx14("span", {
2930
+ /* @__PURE__ */ jsx15("span", {
1803
2931
  className: "skippr:size-1 skippr:shrink-0 skippr:rounded-full skippr:bg-current"
1804
2932
  }),
1805
2933
  text
@@ -1816,12 +2944,12 @@ function SessionAgenda({ phases, hasStarted }) {
1816
2944
  }
1817
2945
 
1818
2946
  // src/components/SessionWarningBanner.tsx
1819
- import { jsx as jsx15 } from "react/jsx-runtime";
2947
+ import { jsx as jsx16 } from "react/jsx-runtime";
1820
2948
  var SESSION_WARNING_THRESHOLD_SECS = 60;
1821
2949
  function SessionWarningBanner({ remaining }) {
1822
2950
  if (remaining === null || remaining <= 0 || remaining > SESSION_WARNING_THRESHOLD_SECS)
1823
2951
  return null;
1824
- return /* @__PURE__ */ jsx15("div", {
2952
+ return /* @__PURE__ */ jsx16("div", {
1825
2953
  "data-testid": "session-warning-banner",
1826
2954
  className: "skippr:bg-red-50 skippr:px-4 skippr:py-1.5 skippr:text-center skippr:text-xs skippr:font-medium skippr:text-red-700",
1827
2955
  children: "Session ending soon"
@@ -1829,24 +2957,24 @@ function SessionWarningBanner({ remaining }) {
1829
2957
  }
1830
2958
 
1831
2959
  // src/components/StartSessionPrompt.tsx
1832
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2960
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1833
2961
  function StartSessionPrompt({
1834
2962
  onStartSession,
1835
2963
  isStarting,
1836
2964
  error,
1837
2965
  label = "Talk to Skippr"
1838
2966
  }) {
1839
- return /* @__PURE__ */ jsxs14("div", {
2967
+ return /* @__PURE__ */ jsxs15("div", {
1840
2968
  className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:items-center skippr:justify-center skippr:gap-3 skippr:px-4",
1841
2969
  children: [
1842
- /* @__PURE__ */ jsx16("button", {
2970
+ /* @__PURE__ */ jsx17("button", {
1843
2971
  type: "button",
1844
2972
  onClick: onStartSession,
1845
2973
  disabled: isStarting,
1846
2974
  className: "skippr:cursor-pointer skippr:rounded-xl skippr:bg-primary skippr:px-8 skippr:py-3 skippr:text-sm skippr:font-medium skippr:text-primary-foreground skippr:transition-all skippr:hover:bg-primary/90 skippr:disabled:cursor-not-allowed skippr:disabled:opacity-60",
1847
2975
  children: isStarting ? "Starting..." : label
1848
2976
  }),
1849
- error && /* @__PURE__ */ jsx16("p", {
2977
+ error && /* @__PURE__ */ jsx17("p", {
1850
2978
  className: "skippr:text-xs skippr:text-destructive",
1851
2979
  children: error
1852
2980
  })
@@ -1855,7 +2983,7 @@ function StartSessionPrompt({
1855
2983
  }
1856
2984
 
1857
2985
  // src/components/Sidebar.tsx
1858
- import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment3 } from "react/jsx-runtime";
2986
+ import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment4 } from "react/jsx-runtime";
1859
2987
  function Sidebar({
1860
2988
  hideControls = false,
1861
2989
  hideHeader = false,
@@ -1878,11 +3006,12 @@ function Sidebar({
1878
3006
  isAuthSubmitting,
1879
3007
  sidebarTab: activeTab,
1880
3008
  setSidebarTab: setActiveTab,
1881
- autoFocusChat
3009
+ autoFocusChat,
3010
+ captureMode
1882
3011
  } = useLiveAgent();
1883
3012
  const isFloating = variant === "floating";
1884
3013
  const isSidebar = variant === "sidebar";
1885
- useEffect11(() => {
3014
+ useEffect13(() => {
1886
3015
  if (!isSidebar)
1887
3016
  return;
1888
3017
  const prop = position === "right" ? "marginRight" : "marginLeft";
@@ -1896,22 +3025,22 @@ function Sidebar({
1896
3025
  document.body.style.transition = "";
1897
3026
  };
1898
3027
  }, [isSidebar, isPanelOpen, position]);
1899
- return /* @__PURE__ */ jsxs15("div", {
3028
+ return /* @__PURE__ */ jsxs16("div", {
1900
3029
  className: cn("skippr:fixed skippr:z-[9999]", "skippr:bg-card", "skippr:flex skippr:flex-col", "skippr:overflow-hidden", isFloating && "skippr:border skippr:border-border skippr:bottom-[88px] skippr:h-[calc(100vh-112px)] skippr:rounded-2xl skippr:shadow-[0_8px_30px_rgba(0,0,0,0.16),0_4px_12px_rgba(0,0,0,0.08)]", isFloating && (position === "right" ? "skippr:right-6" : "skippr:left-6"), isFloating && "skippr:transition-[opacity,transform] skippr:duration-300 skippr:ease-in-out", isFloating && (position === "right" ? "skippr:origin-bottom-right" : "skippr:origin-bottom-left"), isFloating && !isPanelOpen && "skippr:scale-0 skippr:opacity-0 skippr:pointer-events-none", isFloating && isPanelOpen && "skippr:scale-100 skippr:opacity-100", isSidebar && "skippr:top-0 skippr:h-full", isSidebar && "skippr:transition-[width] skippr:duration-300 skippr:ease-in-out", isSidebar && position === "right" && "skippr:right-0 skippr:border-l skippr:border-l-border", isSidebar && position === "left" && "skippr:left-0 skippr:border-r skippr:border-r-border", isSidebar && !isPanelOpen && "skippr:w-0 skippr:border-0"),
1901
3030
  style: { width: isPanelOpen ? SIDEBAR_WIDTH : undefined },
1902
3031
  children: [
1903
- !hideHeader && /* @__PURE__ */ jsx17(ChatHeader, {}),
1904
- !isAuthenticated && isValidating ? /* @__PURE__ */ jsx17("div", {
3032
+ !hideHeader && /* @__PURE__ */ jsx18(ChatHeader, {}),
3033
+ !isAuthenticated && isValidating ? /* @__PURE__ */ jsx18("div", {
1905
3034
  className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
1906
- children: /* @__PURE__ */ jsx17(LoadingDots, {
3035
+ children: /* @__PURE__ */ jsx18(LoadingDots, {
1907
3036
  label: "Loading..."
1908
3037
  })
1909
- }) : !isAuthenticated ? /* @__PURE__ */ jsx17(LoginFlow, {
3038
+ }) : !isAuthenticated ? /* @__PURE__ */ jsx18(LoginFlow, {
1910
3039
  requestOtp,
1911
3040
  verifyOtp,
1912
3041
  error: authError,
1913
3042
  isSubmitting: isAuthSubmitting
1914
- }) : /* @__PURE__ */ jsx17(AuthenticatedContent, {
3043
+ }) : /* @__PURE__ */ jsx18(AuthenticatedContent, {
1915
3044
  isConnected,
1916
3045
  onStartSession: startSession,
1917
3046
  onDisconnect: disconnect,
@@ -1921,7 +3050,8 @@ function Sidebar({
1921
3050
  onTabChange: setActiveTab,
1922
3051
  hideControls,
1923
3052
  startSessionLabel,
1924
- autoFocusChat
3053
+ autoFocusChat,
3054
+ showScreenShareToggle: captureMode === "screenshare"
1925
3055
  })
1926
3056
  ]
1927
3057
  });
@@ -1936,52 +3066,53 @@ function AuthenticatedContent({
1936
3066
  onTabChange,
1937
3067
  hideControls,
1938
3068
  startSessionLabel,
1939
- autoFocusChat
3069
+ autoFocusChat,
3070
+ showScreenShareToggle
1940
3071
  }) {
1941
- return /* @__PURE__ */ jsxs15(Fragment3, {
3072
+ return /* @__PURE__ */ jsxs16(Fragment4, {
1942
3073
  children: [
1943
- isConnected && /* @__PURE__ */ jsx17(ConnectedBanner, {}),
1944
- /* @__PURE__ */ jsxs15("div", {
3074
+ isConnected && /* @__PURE__ */ jsx18(ConnectedBanner, {}),
3075
+ /* @__PURE__ */ jsxs16("div", {
1945
3076
  className: "skippr:flex skippr:gap-2 skippr:border-b skippr:border-border skippr:px-3 skippr:py-2",
1946
3077
  children: [
1947
- /* @__PURE__ */ jsxs15("button", {
3078
+ /* @__PURE__ */ jsxs16("button", {
1948
3079
  type: "button",
1949
3080
  className: cn("skippr:relative skippr:inline-flex skippr:cursor-pointer skippr:items-center skippr:gap-1.5 skippr:rounded-lg skippr:px-3 skippr:py-2 skippr:text-sm skippr:font-medium skippr:transition-all", activeTab === "chat" ? "skippr:text-foreground" : "skippr:text-muted-foreground skippr:hover:text-foreground"),
1950
3081
  onClick: () => onTabChange("chat"),
1951
3082
  children: [
1952
- /* @__PURE__ */ jsx17(MessageCircle, {
3083
+ /* @__PURE__ */ jsx18(MessageCircle, {
1953
3084
  className: "skippr:size-3.5"
1954
3085
  }),
1955
3086
  "Chat",
1956
- activeTab === "chat" && /* @__PURE__ */ jsx17("span", {
3087
+ activeTab === "chat" && /* @__PURE__ */ jsx18("span", {
1957
3088
  className: "skippr:absolute skippr:-bottom-2 skippr:left-3 skippr:right-3 skippr:h-0.5 skippr:rounded-full skippr:bg-foreground"
1958
3089
  })
1959
3090
  ]
1960
3091
  }),
1961
- /* @__PURE__ */ jsxs15("button", {
3092
+ /* @__PURE__ */ jsxs16("button", {
1962
3093
  type: "button",
1963
3094
  className: cn("skippr:relative skippr:inline-flex skippr:cursor-pointer skippr:items-center skippr:gap-1.5 skippr:rounded-lg skippr:px-3 skippr:py-2 skippr:text-sm skippr:font-medium skippr:transition-all", activeTab === "agenda" ? "skippr:text-foreground" : "skippr:text-muted-foreground skippr:hover:text-foreground"),
1964
3095
  onClick: () => onTabChange("agenda"),
1965
3096
  children: [
1966
- /* @__PURE__ */ jsx17(Calendar, {
3097
+ /* @__PURE__ */ jsx18(Calendar, {
1967
3098
  className: "skippr:size-3.5"
1968
3099
  }),
1969
3100
  "Agenda",
1970
- activeTab === "agenda" && /* @__PURE__ */ jsx17("span", {
3101
+ activeTab === "agenda" && /* @__PURE__ */ jsx18("span", {
1971
3102
  className: "skippr:absolute skippr:-bottom-2 skippr:left-3 skippr:right-3 skippr:h-0.5 skippr:rounded-full skippr:bg-foreground"
1972
3103
  })
1973
3104
  ]
1974
3105
  })
1975
3106
  ]
1976
3107
  }),
1977
- /* @__PURE__ */ jsx17("div", {
3108
+ /* @__PURE__ */ jsx18("div", {
1978
3109
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col",
1979
- children: isConnected || isStarting ? /* @__PURE__ */ jsx17(ConnectedBody, {
3110
+ children: isConnected || isStarting ? /* @__PURE__ */ jsx18(ConnectedBody, {
1980
3111
  activeTab,
1981
3112
  autoFocusChat
1982
- }) : /* @__PURE__ */ jsx17("div", {
3113
+ }) : /* @__PURE__ */ jsx18("div", {
1983
3114
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col skippr:animate-skippr-tab-fade",
1984
- children: /* @__PURE__ */ jsx17(StartSessionPrompt, {
3115
+ children: /* @__PURE__ */ jsx18(StartSessionPrompt, {
1985
3116
  onStartSession,
1986
3117
  isStarting,
1987
3118
  error,
@@ -1989,15 +3120,16 @@ function AuthenticatedContent({
1989
3120
  })
1990
3121
  }, `${activeTab}-empty`)
1991
3122
  }),
1992
- isConnected && !hideControls && /* @__PURE__ */ jsx17(MeetingControls, {
1993
- onHangUp: onDisconnect
3123
+ isConnected && !hideControls && /* @__PURE__ */ jsx18(MeetingControls, {
3124
+ onHangUp: onDisconnect,
3125
+ showScreenShareToggle
1994
3126
  })
1995
3127
  ]
1996
3128
  });
1997
3129
  }
1998
3130
  function ConnectedBanner() {
1999
3131
  const remaining = useSessionRemaining();
2000
- return /* @__PURE__ */ jsx17(SessionWarningBanner, {
3132
+ return /* @__PURE__ */ jsx18(SessionWarningBanner, {
2001
3133
  remaining
2002
3134
  });
2003
3135
  }
@@ -2008,17 +3140,17 @@ function ConnectedBody({
2008
3140
  const { allMessages, agentState, sendChatMessage, isSendingChat } = useCombinedMessages();
2009
3141
  const { phases } = usePhaseUpdates();
2010
3142
  if (activeTab === "agenda") {
2011
- return /* @__PURE__ */ jsx17("div", {
3143
+ return /* @__PURE__ */ jsx18("div", {
2012
3144
  className: "skippr:min-h-0 skippr:flex-1 skippr:overflow-y-auto skippr:animate-skippr-tab-fade",
2013
- children: /* @__PURE__ */ jsx17(SessionAgenda, {
3145
+ children: /* @__PURE__ */ jsx18(SessionAgenda, {
2014
3146
  phases,
2015
3147
  hasStarted: allMessages.length > 0 || agentState === "speaking"
2016
3148
  })
2017
3149
  }, "agenda");
2018
3150
  }
2019
- return /* @__PURE__ */ jsx17("div", {
3151
+ return /* @__PURE__ */ jsx18("div", {
2020
3152
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col skippr:animate-skippr-tab-fade",
2021
- children: /* @__PURE__ */ jsx17(MessageList, {
3153
+ children: /* @__PURE__ */ jsx18(MessageList, {
2022
3154
  messages: allMessages,
2023
3155
  isStreaming: agentState === "speaking",
2024
3156
  sendChatMessage,
@@ -2029,45 +3161,55 @@ function ConnectedBody({
2029
3161
  }
2030
3162
 
2031
3163
  // src/components/SidebarTrigger.tsx
2032
- import { jsx as jsx18 } from "react/jsx-runtime";
3164
+ import { jsx as jsx19 } from "react/jsx-runtime";
2033
3165
  function SidebarTrigger() {
2034
3166
  const { isPanelOpen, togglePanel, minimizePanel, minimizable, position, isMinimized } = useLiveAgent();
2035
3167
  if (isMinimized)
2036
3168
  return null;
2037
3169
  const handleClick = isPanelOpen && minimizable ? minimizePanel : togglePanel;
2038
- return /* @__PURE__ */ jsx18("button", {
3170
+ return /* @__PURE__ */ jsx19("button", {
2039
3171
  type: "button",
2040
3172
  onClick: handleClick,
2041
3173
  title: isPanelOpen ? "Close chat" : "Open chat",
2042
3174
  "aria-label": isPanelOpen ? "Close chat" : "Open chat",
2043
3175
  className: cn("skippr:fixed skippr:bottom-6 skippr:z-[9998]", "skippr:flex skippr:size-12 skippr:items-center skippr:justify-center", "skippr:rounded-[14px] skippr:bg-bubble skippr:text-white", "skippr:shadow-[0_4px_16px_rgba(45,43,61,0.45),0_2px_4px_rgba(0,0,0,0.1)] skippr:transition-all", "skippr:cursor-pointer skippr:hover:brightness-110 skippr:hover:-translate-y-0.5 skippr:active:translate-y-0", position === "right" ? "skippr:right-6" : "skippr:left-6"),
2044
- children: isPanelOpen ? /* @__PURE__ */ jsx18(ChevronDown, {
3176
+ children: isPanelOpen ? /* @__PURE__ */ jsx19(ChevronDown, {
2045
3177
  className: "skippr:size-5"
2046
- }) : /* @__PURE__ */ jsx18(Logo, {
3178
+ }) : /* @__PURE__ */ jsx19(Logo, {
2047
3179
  className: "skippr:size-7"
2048
3180
  })
2049
3181
  });
2050
3182
  }
2051
3183
 
2052
3184
  // src/components/LiveAgent.tsx
2053
- import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
2054
- function LiveAgent({
2055
- agentId,
2056
- authToken: authTokenProp,
2057
- appKey,
2058
- userToken,
2059
- position = "right",
2060
- variant = "floating",
2061
- minimizable = true,
2062
- defaultOpen = false,
2063
- welcomeMessage,
2064
- hideControls = false,
2065
- hideHeader = false,
2066
- startSessionLabel = "Talk to Skippr",
2067
- autoFocusChat = true,
2068
- showAgentStateBanner = true,
2069
- children
2070
- }) {
3185
+ import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
3186
+ function LiveAgent(props) {
3187
+ const {
3188
+ agentId,
3189
+ authToken: authTokenProp,
3190
+ appKey,
3191
+ userToken,
3192
+ position = "right",
3193
+ variant = "floating",
3194
+ minimizable = true,
3195
+ defaultOpen = false,
3196
+ welcomeMessage,
3197
+ hideControls = false,
3198
+ hideHeader = false,
3199
+ startSessionLabel = "Talk to Skippr",
3200
+ autoFocusChat = true,
3201
+ showAgentStateBanner = true,
3202
+ children
3203
+ } = props;
3204
+ const captureMode = props.captureMode ?? "screenshare";
3205
+ let agentControls;
3206
+ if ("agentControls" in props && props.agentControls) {
3207
+ if (captureMode === "auto") {
3208
+ agentControls = props.agentControls;
3209
+ } else {
3210
+ console.warn('[Skippr] agentControls requires captureMode: "auto"');
3211
+ }
3212
+ }
2071
3213
  const auth = useAuth({ appKey });
2072
3214
  const effectiveAuthToken = authTokenProp || auth.authToken || undefined;
2073
3215
  const {
@@ -2081,16 +3223,18 @@ function LiveAgent({
2081
3223
  pendingScreenStream
2082
3224
  } = useSession({
2083
3225
  agentId,
3226
+ captureMode,
3227
+ agentControls,
2084
3228
  authToken: effectiveAuthToken,
2085
3229
  appKey,
2086
3230
  userToken
2087
3231
  });
2088
- const [isPanelOpen, setIsPanelOpen] = useState8(defaultOpen);
2089
- const [isMinimized, setIsMinimized] = useState8(minimizable && !defaultOpen);
2090
- const [sidebarTab, setSidebarTab] = useState8("agenda");
2091
- const [welcomeDismissed, setWelcomeDismissed] = useState8(false);
2092
- const dismissWelcome = useCallback6(() => setWelcomeDismissed(true), []);
2093
- const [currentPosition, setCurrentPosition] = useState8(() => {
3232
+ const [isPanelOpen, setIsPanelOpen] = useState9(defaultOpen);
3233
+ const [isMinimized, setIsMinimized] = useState9(minimizable && !defaultOpen);
3234
+ const [sidebarTab, setSidebarTab] = useState9("agenda");
3235
+ const [welcomeDismissed, setWelcomeDismissed] = useState9(false);
3236
+ const dismissWelcome = useCallback7(() => setWelcomeDismissed(true), []);
3237
+ const [currentPosition, setCurrentPosition] = useState9(() => {
2094
3238
  try {
2095
3239
  const saved = localStorage.getItem("skippr_widget_position");
2096
3240
  if (saved === "left" || saved === "right")
@@ -2098,20 +3242,20 @@ function LiveAgent({
2098
3242
  } catch {}
2099
3243
  return position;
2100
3244
  });
2101
- const setPositionWithPersist = useCallback6((pos) => {
3245
+ const setPositionWithPersist = useCallback7((pos) => {
2102
3246
  setCurrentPosition(pos);
2103
3247
  try {
2104
3248
  localStorage.setItem("skippr_widget_position", pos);
2105
3249
  } catch {}
2106
3250
  }, []);
2107
- const openPanel = useCallback6(() => setIsPanelOpen(true), []);
2108
- const closePanel = useCallback6(() => setIsPanelOpen(false), []);
2109
- const togglePanel = useCallback6(() => setIsPanelOpen((prev) => !prev), []);
2110
- const expandPanel = useCallback6(() => {
3251
+ const openPanel = useCallback7(() => setIsPanelOpen(true), []);
3252
+ const closePanel = useCallback7(() => setIsPanelOpen(false), []);
3253
+ const togglePanel = useCallback7(() => setIsPanelOpen((prev) => !prev), []);
3254
+ const expandPanel = useCallback7(() => {
2111
3255
  setIsMinimized(false);
2112
3256
  setIsPanelOpen(true);
2113
3257
  }, []);
2114
- const minimizePanel = useCallback6(() => {
3258
+ const minimizePanel = useCallback7(() => {
2115
3259
  if (!minimizable)
2116
3260
  return;
2117
3261
  setIsMinimized(true);
@@ -2119,8 +3263,8 @@ function LiveAgent({
2119
3263
  }, [minimizable]);
2120
3264
  const isConnected = connection !== null;
2121
3265
  const isAuthenticated = !!userToken || !!authTokenProp || auth.isAuthenticated;
2122
- const prevConnectionRef = useRef6(connection);
2123
- useEffect12(() => {
3266
+ const prevConnectionRef = useRef8(connection);
3267
+ useEffect14(() => {
2124
3268
  const connectionChanged = prevConnectionRef.current !== connection;
2125
3269
  prevConnectionRef.current = connection;
2126
3270
  if (connectionChanged && minimizable) {
@@ -2157,7 +3301,9 @@ function LiveAgent({
2157
3301
  isAuthSubmitting: auth.isSubmitting,
2158
3302
  sidebarTab,
2159
3303
  setSidebarTab,
2160
- autoFocusChat
3304
+ autoFocusChat,
3305
+ captureMode,
3306
+ agentControls
2161
3307
  }), [
2162
3308
  connection,
2163
3309
  shouldConnect,
@@ -2186,32 +3332,41 @@ function LiveAgent({
2186
3332
  auth.logout,
2187
3333
  auth.isSubmitting,
2188
3334
  sidebarTab,
2189
- autoFocusChat
3335
+ autoFocusChat,
3336
+ captureMode,
3337
+ agentControls
2190
3338
  ]);
2191
- return /* @__PURE__ */ jsx19(LiveAgentContext.Provider, {
3339
+ return /* @__PURE__ */ jsx20(LiveAgentContext.Provider, {
2192
3340
  value: ctx,
2193
- children: /* @__PURE__ */ jsxs16(LiveKitRoom, {
3341
+ children: /* @__PURE__ */ jsxs17(LiveKitRoom, {
2194
3342
  serverUrl: connection?.livekitUrl,
2195
3343
  token: connection?.token,
2196
3344
  connect: shouldConnect,
2197
3345
  audio: true,
2198
3346
  onDisconnected: disconnect,
2199
3347
  children: [
2200
- connection && /* @__PURE__ */ jsx19(RoomAudioRenderer, {}),
2201
- showAgentStateBanner && /* @__PURE__ */ jsx19(AgentStateBanner, {}),
2202
- connection && /* @__PURE__ */ jsx19(AutoStartMedia, {
3348
+ connection && /* @__PURE__ */ jsx20(RoomAudioRenderer, {}),
3349
+ connection && captureMode === "screenshare" && /* @__PURE__ */ jsx20(AutoStartMedia, {
2203
3350
  pendingScreenStream
2204
3351
  }),
2205
- isMinimized && /* @__PURE__ */ jsx19(MinimizedBubble, {
2206
- welcomeMessage,
2207
- welcomeDismissed,
2208
- onDismissWelcome: dismissWelcome
2209
- }),
2210
- /* @__PURE__ */ jsx19(SidebarTrigger, {}),
2211
- /* @__PURE__ */ jsx19(Sidebar, {
2212
- hideControls,
2213
- hideHeader,
2214
- startSessionLabel
3352
+ connection && captureMode === "auto" && /* @__PURE__ */ jsx20(DomCapture, {}),
3353
+ connection && captureMode === "auto" && agentControls?.highlight && /* @__PURE__ */ jsx20(HighlightOverlay, {}),
3354
+ /* @__PURE__ */ jsxs17("div", {
3355
+ id: WIDGET_ROOT_ID,
3356
+ children: [
3357
+ showAgentStateBanner && /* @__PURE__ */ jsx20(AgentStateBanner, {}),
3358
+ isMinimized && /* @__PURE__ */ jsx20(MinimizedBubble, {
3359
+ welcomeMessage,
3360
+ welcomeDismissed,
3361
+ onDismissWelcome: dismissWelcome
3362
+ }),
3363
+ /* @__PURE__ */ jsx20(SidebarTrigger, {}),
3364
+ /* @__PURE__ */ jsx20(Sidebar, {
3365
+ hideControls,
3366
+ hideHeader,
3367
+ startSessionLabel
3368
+ })
3369
+ ]
2215
3370
  }),
2216
3371
  children
2217
3372
  ]
@@ -2219,9 +3374,9 @@ function LiveAgent({
2219
3374
  });
2220
3375
  }
2221
3376
  // src/hooks/useIsLocalSpeaking.ts
2222
- import { useIsSpeaking, useLocalParticipant as useLocalParticipant5 } from "@livekit/components-react";
3377
+ import { useIsSpeaking, useLocalParticipant as useLocalParticipant6 } from "@livekit/components-react/hooks";
2223
3378
  function useIsLocalSpeaking() {
2224
- const { localParticipant } = useLocalParticipant5();
3379
+ const { localParticipant } = useLocalParticipant6();
2225
3380
  return useIsSpeaking(localParticipant);
2226
3381
  }
2227
3382
  export {