@skippr/live-agent-sdk 0.56.0 → 0.57.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.
@@ -1210,9 +1210,17 @@ var SETTLE_TO_CENTER_MS = 420;
1210
1210
  var ACT_CLICK_DELAY_MS = 400;
1211
1211
  var BASIC_ACT_DELAY_MS = 150;
1212
1212
  var TRACK_GLIDE_MS = 90;
1213
+ var GESTURE_GLIDE_MS = 500;
1214
+ var GESTURE_ACT_DELAY_MS = 200;
1213
1215
  function cursorEntryPoint() {
1214
1216
  return { x: window.innerWidth - 72, y: window.innerHeight - 80 };
1215
1217
  }
1218
+ function gesturePoint(direction) {
1219
+ return {
1220
+ x: window.innerWidth / 2,
1221
+ y: direction === "down" ? window.innerHeight - 96 : 96
1222
+ };
1223
+ }
1216
1224
  function centerOf(rect) {
1217
1225
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
1218
1226
  }
@@ -1295,7 +1303,18 @@ function AgentCursor({
1295
1303
  useLayoutEffect(() => {
1296
1304
  cancelAnimation();
1297
1305
  setClickPulse(0);
1298
- if (!target?.element.isConnected) {
1306
+ if (target?.kind === "gesture" && target.gesture) {
1307
+ setPhase("done");
1308
+ setRingBox(null);
1309
+ const destination = gesturePoint(target.gesture.direction);
1310
+ startGlide(prevCursorRef.current ?? cursorEntryPoint(), destination, GESTURE_GLIDE_MS, () => {
1311
+ if (target.mode === "act") {
1312
+ after(GESTURE_ACT_DELAY_MS, () => target.onAct?.());
1313
+ }
1314
+ });
1315
+ return cancelAnimation;
1316
+ }
1317
+ if (!target?.element?.isConnected) {
1299
1318
  setPhase("idle");
1300
1319
  setRingBox(null);
1301
1320
  setCursor(null);
@@ -1346,11 +1365,11 @@ function AgentCursor({
1346
1365
  return cancelAnimation;
1347
1366
  }, [target, animated]);
1348
1367
  useEffect6(() => {
1349
- if (!target || phase === "idle")
1368
+ if (!target || target.kind === "gesture" || phase === "idle")
1350
1369
  return;
1351
1370
  const track = () => {
1352
1371
  trackFrameRef.current = null;
1353
- if (!target.element.isConnected)
1372
+ if (!target.element?.isConnected)
1354
1373
  return;
1355
1374
  const rect = getRectInTopViewport(target.element);
1356
1375
  setRingBox(rect);
@@ -1424,6 +1443,21 @@ function AgentCursor({
1424
1443
  })
1425
1444
  ]
1426
1445
  }),
1446
+ target.kind === "gesture" && /* @__PURE__ */ jsx("div", {
1447
+ role: "status",
1448
+ className: "skippr:fixed skippr:-translate-x-1/2 skippr:flex skippr:items-center skippr:gap-1.5 skippr:rounded-full skippr:bg-[#2bc0ae] skippr:px-2.5 skippr:py-1 skippr:shadow-[0_4px_12px_rgba(0,0,0,0.12)]",
1449
+ style: {
1450
+ left: `${cursor.x}px`,
1451
+ top: `${cursor.y + 18}px`,
1452
+ zIndex: Z_INDEX,
1453
+ pointerEvents: "none",
1454
+ transition: `left ${glideMs}ms ${GLIDE_EASE}, top ${glideMs}ms ${GLIDE_EASE}`
1455
+ },
1456
+ children: /* @__PURE__ */ jsx("span", {
1457
+ className: "skippr:text-[11px] skippr:font-medium skippr:text-white skippr:whitespace-nowrap",
1458
+ children: target.label
1459
+ })
1460
+ }),
1427
1461
  /* @__PURE__ */ jsx("div", {
1428
1462
  ref: cursorElRef,
1429
1463
  style: {
@@ -1671,6 +1705,24 @@ function isInteractive(element, role) {
1671
1705
  function isBlindRegion(role) {
1672
1706
  return role === "video" || role === "iframe" || role === "canvas" || role === "audio" || role === "embed" || role === "object";
1673
1707
  }
1708
+ var SCROLLABLE_CONTAINER_ROLES = new Set([
1709
+ "listbox",
1710
+ "menu",
1711
+ "dialog",
1712
+ "list",
1713
+ "grid",
1714
+ "tree",
1715
+ "group",
1716
+ "region"
1717
+ ]);
1718
+ function isScrollableContainer(element, role) {
1719
+ if (!SCROLLABLE_CONTAINER_ROLES.has(role))
1720
+ return false;
1721
+ if (element.scrollHeight - element.clientHeight <= 1)
1722
+ return false;
1723
+ const overflowY = window.getComputedStyle(element).overflowY;
1724
+ return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
1725
+ }
1674
1726
  function getIframeHost(element) {
1675
1727
  const src = element.getAttribute("src");
1676
1728
  if (!src)
@@ -1832,6 +1884,8 @@ function extractAttrs(element, role) {
1832
1884
  if (/\.gif(\?|$)/i.test(imageSrc))
1833
1885
  attributes.push("animated");
1834
1886
  }
1887
+ if (isScrollableContainer(element, role))
1888
+ attributes.push("scrollable");
1835
1889
  const isNativelyDisabled = "disabled" in element && element.disabled === true;
1836
1890
  if (isNativelyDisabled || element.getAttribute("aria-disabled") === "true") {
1837
1891
  attributes.push("disabled");
@@ -1867,7 +1921,7 @@ function shouldSkipSubtree(element) {
1867
1921
  function shouldEmitElement(element, role) {
1868
1922
  if (role === null)
1869
1923
  return false;
1870
- return isInteractive(element, role) || role === "heading" || role === "image" || role === "label" || isBlindRegion(role);
1924
+ return isInteractive(element, role) || role === "heading" || role === "image" || role === "label" || isBlindRegion(role) || isScrollableContainer(element, role);
1871
1925
  }
1872
1926
  function getSameOriginIframeBody(element) {
1873
1927
  if (element.tagName !== "IFRAME")
@@ -2872,6 +2926,9 @@ import { useDataChannel as useDataChannel2 } from "@livekit/components-react/hoo
2872
2926
  import { useCallback as useCallback6, useEffect as useEffect12, useRef as useRef8 } from "react";
2873
2927
  var STEP_MAX_WAIT_MS = 5000;
2874
2928
  var textDecoder2 = new TextDecoder;
2929
+ function parseScrollDirection(value) {
2930
+ return value === "down" || value === "up" || value === "top" || value === "bottom" ? value : null;
2931
+ }
2875
2932
  function parsePageAction(payload) {
2876
2933
  try {
2877
2934
  const json = JSON.parse(textDecoder2.decode(payload));
@@ -2886,6 +2943,16 @@ function parsePageAction(payload) {
2886
2943
  return typeof json.ref === "string" && typeof json.value === "string" ? { type: "select", ref: json.ref, value: json.value } : null;
2887
2944
  case "scroll":
2888
2945
  return typeof json.ref === "string" ? { type: "scroll", ref: json.ref } : null;
2946
+ case "hover":
2947
+ return typeof json.ref === "string" ? { type: "hover", ref: json.ref } : null;
2948
+ case "scroll_page": {
2949
+ const direction = parseScrollDirection(json.direction);
2950
+ return direction ? { type: "scroll_page", direction } : null;
2951
+ }
2952
+ case "scroll_in": {
2953
+ const direction = parseScrollDirection(json.direction);
2954
+ return typeof json.ref === "string" && direction ? { type: "scroll_in", ref: json.ref, direction } : null;
2955
+ }
2889
2956
  case "navigate":
2890
2957
  return typeof json.url === "string" ? { type: "navigate", url: json.url } : null;
2891
2958
  default:
@@ -2929,6 +2996,12 @@ function performFill(element, value) {
2929
2996
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2930
2997
  element.focus();
2931
2998
  setReactCompatibleValue(element, value);
2999
+ return;
3000
+ }
3001
+ if (element.isContentEditable) {
3002
+ element.focus();
3003
+ element.textContent = value;
3004
+ element.dispatchEvent(new InputEvent("input", { bubbles: true }));
2932
3005
  }
2933
3006
  }
2934
3007
  function performSelect(element, value) {
@@ -2975,6 +3048,87 @@ function performClick(element) {
2975
3048
  } catch {}
2976
3049
  element.click();
2977
3050
  }
3051
+ function performHover(element) {
3052
+ const rect = element.getBoundingClientRect();
3053
+ const base = {
3054
+ bubbles: true,
3055
+ cancelable: true,
3056
+ composed: true,
3057
+ clientX: rect.left + rect.width / 2,
3058
+ clientY: rect.top + rect.height / 2
3059
+ };
3060
+ try {
3061
+ element.dispatchEvent(new PointerEvent("pointerover", { ...base, pointerId: 1, isPrimary: true }));
3062
+ } catch {}
3063
+ element.dispatchEvent(new MouseEvent("mouseover", base));
3064
+ try {
3065
+ element.dispatchEvent(new PointerEvent("pointerenter", { ...base, pointerId: 1, isPrimary: true }));
3066
+ } catch {}
3067
+ element.dispatchEvent(new MouseEvent("mouseenter", base));
3068
+ }
3069
+ var SCROLL_PAGE_FRACTION = 0.85;
3070
+ function isScrollableY(element) {
3071
+ if (element.scrollHeight - element.clientHeight <= 1)
3072
+ return false;
3073
+ const overflowY = element.ownerDocument.defaultView?.getComputedStyle(element).overflowY;
3074
+ return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
3075
+ }
3076
+ function nearestScrollableContainer(element) {
3077
+ let node = element;
3078
+ while (node && node !== document.body && node !== document.documentElement) {
3079
+ if (isScrollableY(node))
3080
+ return node;
3081
+ node = node.parentElement;
3082
+ }
3083
+ return document.documentElement;
3084
+ }
3085
+ var PAGE_SCROLLER_MAX_DEPTH = 3;
3086
+ function collectShallow(element, depth, out) {
3087
+ for (const child of Array.from(element.children)) {
3088
+ if (child instanceof HTMLElement)
3089
+ out.push(child);
3090
+ if (depth > 1)
3091
+ collectShallow(child, depth - 1, out);
3092
+ }
3093
+ }
3094
+ function pickPageScroller() {
3095
+ const documentScroller = document.scrollingElement;
3096
+ if (documentScroller && documentScroller.scrollHeight - documentScroller.clientHeight > 1) {
3097
+ return documentScroller;
3098
+ }
3099
+ const candidates = [];
3100
+ collectShallow(document.body, PAGE_SCROLLER_MAX_DEPTH, candidates);
3101
+ let largest = null;
3102
+ let largestArea = 0;
3103
+ for (const element of candidates) {
3104
+ if (!isScrollableY(element))
3105
+ continue;
3106
+ const rect = element.getBoundingClientRect();
3107
+ if (rect.bottom <= 0 || rect.top >= window.innerHeight)
3108
+ continue;
3109
+ const area = rect.width * rect.height;
3110
+ if (area > largestArea) {
3111
+ largestArea = area;
3112
+ largest = element;
3113
+ }
3114
+ }
3115
+ return largest ?? documentScroller ?? document.documentElement;
3116
+ }
3117
+ function gestureFor(direction) {
3118
+ return direction === "up" || direction === "top" ? "up" : "down";
3119
+ }
3120
+ function performScroll(container, direction) {
3121
+ if (direction === "top") {
3122
+ container.scrollTo({ top: 0, behavior: "smooth" });
3123
+ return;
3124
+ }
3125
+ if (direction === "bottom") {
3126
+ container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
3127
+ return;
3128
+ }
3129
+ const delta = container.clientHeight * SCROLL_PAGE_FRACTION * (direction === "down" ? 1 : -1);
3130
+ container.scrollBy({ top: delta, behavior: "smooth" });
3131
+ }
2978
3132
  function performAction(action, element) {
2979
3133
  switch (action.type) {
2980
3134
  case "click": {
@@ -2994,6 +3148,12 @@ function performAction(action, element) {
2994
3148
  case "scroll":
2995
3149
  element.scrollIntoView({ behavior: "smooth", block: "center" });
2996
3150
  return;
3151
+ case "hover":
3152
+ performHover(element);
3153
+ return;
3154
+ case "scroll_in":
3155
+ performScroll(nearestScrollableContainer(element), action.direction);
3156
+ return;
2997
3157
  }
2998
3158
  }
2999
3159
  function labelFor(action) {
@@ -3005,7 +3165,11 @@ function labelFor(action) {
3005
3165
  case "select":
3006
3166
  return "Selecting…";
3007
3167
  case "scroll":
3168
+ case "scroll_page":
3169
+ case "scroll_in":
3008
3170
  return "Scrolling…";
3171
+ case "hover":
3172
+ return "Hovering…";
3009
3173
  case "navigate":
3010
3174
  return "Navigating…";
3011
3175
  }
@@ -3023,6 +3187,30 @@ function PageActionHandler() {
3023
3187
  resolve();
3024
3188
  return;
3025
3189
  }
3190
+ if (action.type === "scroll_page") {
3191
+ let scrolled = false;
3192
+ const finishScroll = () => {
3193
+ if (scrolled)
3194
+ return;
3195
+ scrolled = true;
3196
+ resolve();
3197
+ };
3198
+ const fallback2 = setTimeout(finishScroll, STEP_MAX_WAIT_MS);
3199
+ setCursorTarget("act", {
3200
+ kind: "gesture",
3201
+ gesture: { direction: gestureFor(action.direction) },
3202
+ label: labelFor(action),
3203
+ mode: "act",
3204
+ onAct: () => {
3205
+ clearTimeout(fallback2);
3206
+ if (mountedRef.current) {
3207
+ runAgentAction(() => performScroll(pickPageScroller(), action.direction));
3208
+ }
3209
+ finishScroll();
3210
+ }
3211
+ });
3212
+ return;
3213
+ }
3026
3214
  const element = findElementByRef(action.ref);
3027
3215
  if (!element?.isConnected || !isVisible2(element) || isActionBlocked(element)) {
3028
3216
  resolve();