@skippr/live-agent-sdk 0.96.0 → 0.98.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.
@@ -394,6 +394,12 @@ var CHAT_SURFACE = {
394
394
  Room: "room"
395
395
  };
396
396
  var DEFAULT_CHAT_SURFACE = CHAT_SURFACE.Line;
397
+ var ACTION_PLAN_MODE = {
398
+ Always: "always",
399
+ Auto: "auto",
400
+ Never: "never"
401
+ };
402
+ var DEFAULT_ACTION_PLAN_MODE = ACTION_PLAN_MODE.Auto;
397
403
  var LAUNCHER_MODE = {
398
404
  Avatar: "avatar",
399
405
  Strip: "strip",
@@ -2182,7 +2188,11 @@ function isBuddyActive(appearance) {
2182
2188
  return appearance?.buddy?.enabled === true;
2183
2189
  }
2184
2190
  function buddyEffectiveControls(base) {
2185
- const next = { ...base ?? {}, micMode: MIC_MODE.PushToTalk };
2191
+ const next = {
2192
+ ...base ?? {},
2193
+ micMode: MIC_MODE.PushToTalk,
2194
+ actionPlanMode: ACTION_PLAN_MODE.Never
2195
+ };
2186
2196
  delete next.chat;
2187
2197
  return next;
2188
2198
  }
@@ -4058,6 +4068,9 @@ function useSession({
4058
4068
  enabledControls.highlight = true;
4059
4069
  if (agentControls?.actions === true)
4060
4070
  enabledControls.actions = true;
4071
+ if (agentControls?.actionPlanMode) {
4072
+ enabledControls.actionPlanMode = agentControls.actionPlanMode;
4073
+ }
4061
4074
  const micMode = agentControls?.micMode;
4062
4075
  if (isRequestResponseMicMode(micMode))
4063
4076
  enabledControls.micMode = micMode;
@@ -4963,6 +4976,7 @@ function accessibleName(element, options = {}) {
4963
4976
  var REF_PREFIX = "r-";
4964
4977
  var VALUE_MAX_CHARS = 80;
4965
4978
  var HREF_MAX_CHARS = 60;
4979
+ var TABLE_ROW_MAX_CHARS = 160;
4966
4980
  var PAGE_CONTENT_MAX_BYTES = 14000;
4967
4981
  var NODE_LIMIT = 1500;
4968
4982
  var SENSITIVE_IFRAME_HOSTS = [
@@ -5308,8 +5322,9 @@ function ensureStableRef(element) {
5308
5322
  function formatNodeAsLine(entry) {
5309
5323
  const indent = " ".repeat(entry.depth);
5310
5324
  const namePart = entry.name ? ` "${escapeAttributeValue(entry.name)}"` : "";
5325
+ const refPart = entry.ref ? ` ${entry.ref}` : "";
5311
5326
  const attrsPart = entry.attrs.length > 0 ? ` ${entry.attrs.join(" ")}` : "";
5312
- return `${indent}${entry.role}${namePart} ${entry.ref}${attrsPart}`;
5327
+ return `${indent}${entry.role}${namePart}${refPart}${attrsPart}`;
5313
5328
  }
5314
5329
  function shouldSkipSubtree(element) {
5315
5330
  if (element.id === WIDGET_ROOT_ID)
@@ -5337,13 +5352,66 @@ function getSameOriginIframeBody(element) {
5337
5352
  return null;
5338
5353
  }
5339
5354
  }
5340
- function walkAndCollect(element, depth, entries) {
5355
+ function normalizeText(text) {
5356
+ return (text ?? "").replace(/\s+/g, " ").trim();
5357
+ }
5358
+ function formatTableRowText(row) {
5359
+ const cellValues = Array.from(row.children).filter((cell) => cell.tagName === "TD" || cell.tagName === "TH").map((cell) => normalizeText(cell.textContent).slice(0, VALUE_MAX_CHARS));
5360
+ return cellValues.join(" | ").slice(0, TABLE_ROW_MAX_CHARS).trim();
5361
+ }
5362
+ function isTableHeaderRow(row) {
5363
+ return Array.from(row.children).some((cell) => cell.tagName === "TH");
5364
+ }
5365
+ function directTextOfElement(element) {
5366
+ let combinedText = "";
5367
+ for (const childNode of Array.from(element.childNodes)) {
5368
+ if (childNode.nodeType === Node.TEXT_NODE)
5369
+ combinedText += `${childNode.textContent} `;
5370
+ }
5371
+ return normalizeText(combinedText);
5372
+ }
5373
+ var REFLESS_TEXT_ENTRY_REF = "";
5374
+ function collectRefLessTextLeaf(text, depth, rect, entries) {
5375
+ entries.push({
5376
+ depth,
5377
+ role: "text",
5378
+ name: text.slice(0, VALUE_MAX_CHARS),
5379
+ ref: REFLESS_TEXT_ENTRY_REF,
5380
+ attrs: [],
5381
+ area: rect.width * rect.height
5382
+ });
5383
+ }
5384
+ function collectTableRowAsSingleEntry(row, depth, rect, entries) {
5385
+ const rowText = formatTableRowText(row);
5386
+ if (!rowText)
5387
+ return false;
5388
+ entries.push({
5389
+ depth,
5390
+ role: "row",
5391
+ name: rowText,
5392
+ ref: ensureStableRef(row),
5393
+ attrs: isTableHeaderRow(row) ? ["header"] : [],
5394
+ area: rect.width * rect.height
5395
+ });
5396
+ for (const child of Array.from(row.children)) {
5397
+ walkAndCollect(child, depth + 1, entries, true);
5398
+ }
5399
+ return true;
5400
+ }
5401
+ function walkAndCollect(element, depth, entries, suppressTextLeaves = false) {
5341
5402
  if (shouldSkipSubtree(element))
5342
5403
  return;
5343
5404
  const rect = element.getBoundingClientRect();
5405
+ if (element.tagName === "TR") {
5406
+ if (!isVisible(element, rect))
5407
+ return;
5408
+ if (collectTableRowAsSingleEntry(element, depth, rect, entries))
5409
+ return;
5410
+ }
5344
5411
  const role = inferRole(element) ?? (isCursorClickable(element, rect) ? "button" : null);
5345
5412
  const isVisibleAndEmittable = role !== null && isVisible(element, rect) && shouldEmitElement(element, role);
5346
5413
  let childDepth = depth;
5414
+ let childSuppressTextLeaves = suppressTextLeaves;
5347
5415
  if (isVisibleAndEmittable && role) {
5348
5416
  const ref = ensureStableRef(element);
5349
5417
  const name = accessibleName(element);
@@ -5356,12 +5424,20 @@ function walkAndCollect(element, depth, entries) {
5356
5424
  area: rect.width * rect.height
5357
5425
  });
5358
5426
  childDepth = depth + 1;
5427
+ const nameAlreadyCoversSubtreeText = name !== "";
5428
+ if (nameAlreadyCoversSubtreeText)
5429
+ childSuppressTextLeaves = true;
5430
+ } else if (!suppressTextLeaves) {
5431
+ const ownText = directTextOfElement(element);
5432
+ if (ownText && isVisible(element, rect)) {
5433
+ collectRefLessTextLeaf(ownText, depth, rect, entries);
5434
+ }
5359
5435
  }
5360
5436
  if (role === "iframe") {
5361
5437
  const iframeBody = getSameOriginIframeBody(element);
5362
5438
  if (iframeBody) {
5363
5439
  for (const child of Array.from(iframeBody.children)) {
5364
- walkAndCollect(child, childDepth, entries);
5440
+ walkAndCollect(child, childDepth, entries, childSuppressTextLeaves);
5365
5441
  }
5366
5442
  }
5367
5443
  return;
@@ -5369,15 +5445,19 @@ function walkAndCollect(element, depth, entries) {
5369
5445
  if (role && isBlindRegion(role))
5370
5446
  return;
5371
5447
  for (const child of Array.from(element.children)) {
5372
- walkAndCollect(child, childDepth, entries);
5448
+ walkAndCollect(child, childDepth, entries, childSuppressTextLeaves);
5373
5449
  }
5374
5450
  }
5375
- function dropSmallestAreaEntriesUntilUnderLimit(entries) {
5451
+ var TRUNCATION_DROP_ORDER_BY_ROLE = { text: 0, row: 1 };
5452
+ function truncationDropOrder(entry) {
5453
+ return TRUNCATION_DROP_ORDER_BY_ROLE[entry.role] ?? 2;
5454
+ }
5455
+ function dropInformationalEntriesFirstUntilUnderLimit(entries) {
5376
5456
  if (entries.length <= NODE_LIMIT)
5377
5457
  return entries;
5378
5458
  const indexedEntries = entries.map((entry, index2) => ({ entry, index: index2 }));
5379
- const entriesByAreaAscending = indexedEntries.slice(1).sort((a, b) => a.entry.area - b.entry.area);
5380
- const indicesToDrop = new Set(entriesByAreaAscending.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
5459
+ const entriesMostDroppableFirst = indexedEntries.slice(1).sort((a, b) => truncationDropOrder(a.entry) - truncationDropOrder(b.entry) || a.entry.area - b.entry.area);
5460
+ const indicesToDrop = new Set(entriesMostDroppableFirst.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
5381
5461
  const keptEntries = [];
5382
5462
  for (let i = 0;i < entries.length; i++) {
5383
5463
  if (!indicesToDrop.has(i))
@@ -5402,7 +5482,7 @@ function buildAccessibilityTree() {
5402
5482
  }
5403
5483
  }
5404
5484
  const exceededNodeLimit = collectedEntries.length > NODE_LIMIT;
5405
- const finalEntries = exceededNodeLimit ? dropSmallestAreaEntriesUntilUnderLimit(collectedEntries) : collectedEntries;
5485
+ const finalEntries = exceededNodeLimit ? dropInformationalEntriesFirstUntilUnderLimit(collectedEntries) : collectedEntries;
5406
5486
  const treeLines = finalEntries.map(formatNodeAsLine);
5407
5487
  let pageContent = treeLines.join(`
5408
5488
  `);
@@ -5777,21 +5857,46 @@ function paintCursorOverlay(ctx, transform, brand = DEFAULT_BRAND_PALETTE) {
5777
5857
  }
5778
5858
 
5779
5859
  // src/capture/snapdom.ts
5860
+ var UNLOADABLE_IMAGE_PLACEHOLDER_FILL = "#e2e8f0";
5861
+ function placeholderForUnloadableImage({
5862
+ width = 100,
5863
+ height = 100
5864
+ }) {
5865
+ const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="100%" height="100%" fill="${UNLOADABLE_IMAGE_PLACEHOLDER_FILL}"/></svg>`;
5866
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(placeholderSvg)}`;
5867
+ }
5780
5868
  var cachedSnapdomModule = null;
5781
5869
  function loadSnapdom() {
5782
- if (!cachedSnapdomModule)
5870
+ if (!cachedSnapdomModule) {
5783
5871
  cachedSnapdomModule = import("@zumer/snapdom");
5872
+ }
5784
5873
  return cachedSnapdomModule;
5785
5874
  }
5875
+ function preCacheSnapshotResources() {
5876
+ loadSnapdom().then(({ preCache }) => preCache(document, { embedFonts: true })).catch(() => {
5877
+ return;
5878
+ });
5879
+ }
5786
5880
  async function snapToCanvas(element, options = {}) {
5787
5881
  const snapdomModule = await loadSnapdom();
5788
- return snapdomModule.snapdom.toCanvas(element, options);
5882
+ const optionsWithEngineDefaults = {
5883
+ embedFonts: true,
5884
+ fallbackURL: placeholderForUnloadableImage,
5885
+ ...options,
5886
+ snap: snapdomModule.snapdom
5887
+ };
5888
+ return snapdomModule.snapdom.toCanvas(element, optionsWithEngineDefaults);
5789
5889
  }
5790
5890
 
5791
5891
  // src/components/DomCapture.tsx
5792
5892
  var SNAPSHOT_INTERVAL_MS = 3000;
5793
5893
  var A11Y_PUBLISH_INTERVAL_MS = 2000;
5794
5894
  var FIRST_SNAPSHOT_DELAY_MS = 400;
5895
+ var RASTERIZE_TIMEOUT_MS = 1e4;
5896
+ var FULL_QUALITY_TIMEOUTS_BEFORE_COOLDOWN = 1;
5897
+ var FAST_CAPTURES_BEFORE_FULL_QUALITY_RETRY = 10;
5898
+ var RASTERIZE_TIMED_OUT = Symbol("rasterize-timed-out");
5899
+ var SCROLL_SETTLE_MS = 300;
5795
5900
  var CAPTURE_PRESET = ScreenSharePresets2.h1080fps30;
5796
5901
  var CAPTURE_FPS = CAPTURE_PRESET.encoding.maxFramerate ?? 30;
5797
5902
  var CAPTURE_BITRATE = 4000000;
@@ -5799,15 +5904,20 @@ var DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES = 14000;
5799
5904
  var MAX_CANVAS_DPR = 1.5;
5800
5905
  var CONSECUTIVE_FAILURES_BEFORE_REPORT = 3;
5801
5906
  var textEncoder4 = new TextEncoder;
5907
+ var SNAPSHOT_EXCLUDE_SELECTORS = [`#${WIDGET_ROOT_ID}`, `[${PRIVATE_ATTR}]`];
5908
+ function isCapturableIframe(element) {
5909
+ return isSameOriginIframe(element) && !isSensitiveIframe(element);
5910
+ }
5802
5911
  function shouldIncludeInSnapshot(element) {
5803
- if (!(element instanceof Element))
5804
- return true;
5805
- if (element.id === WIDGET_ROOT_ID)
5806
- return false;
5807
- if (element.hasAttribute?.(PRIVATE_ATTR))
5912
+ if (element instanceof HTMLIFrameElement && !isCapturableIframe(element))
5808
5913
  return false;
5809
5914
  return true;
5810
5915
  }
5916
+ function shouldIncludeInFirstFrame(element) {
5917
+ if (element.tagName === "IMG")
5918
+ return false;
5919
+ return shouldIncludeInSnapshot(element);
5920
+ }
5811
5921
  function createPublishingCanvas() {
5812
5922
  const dprBoost = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
5813
5923
  const canvas = document.createElement("canvas");
@@ -5828,22 +5938,23 @@ function getCanvasCaptureStream(canvas, fps) {
5828
5938
  }
5829
5939
  return captureStreamFn.call(canvas, fps);
5830
5940
  }
5831
- async function rasterizeViewportToPageLayer(width, height) {
5832
- const dpr = window.devicePixelRatio || 1;
5941
+ async function rasterizeViewportToPageLayer(width, height, { skipCrossOriginImageSettling, includeImages }) {
5942
+ const requestedDpr = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
5943
+ const viewportWidthAtCaptureStart = window.innerWidth;
5833
5944
  const snapshotCanvas = await snapToCanvas(document.documentElement, {
5834
- filter: shouldIncludeInSnapshot,
5835
- filterMode: "remove",
5945
+ exclude: SNAPSHOT_EXCLUDE_SELECTORS,
5946
+ excludeMode: "remove",
5947
+ filter: includeImages ? shouldIncludeInSnapshot : shouldIncludeInFirstFrame,
5948
+ filterMode: "hide",
5836
5949
  backgroundColor: "#ffffff",
5837
- fast: false,
5838
- dpr
5950
+ fast: skipCrossOriginImageSettling,
5951
+ dpr: requestedDpr,
5952
+ clip: "viewport"
5839
5953
  });
5840
- const sourceX = window.scrollX * dpr;
5841
- const sourceY = window.scrollY * dpr;
5842
- const sourceWidth = window.innerWidth * dpr;
5843
- const sourceHeight = window.innerHeight * dpr;
5844
- const fitScale = Math.min(width / sourceWidth, height / sourceHeight);
5845
- const destWidth = sourceWidth * fitScale;
5846
- const destHeight = sourceHeight * fitScale;
5954
+ const canvasPixelsPerCssPixel = viewportWidthAtCaptureStart > 0 ? snapshotCanvas.width / viewportWidthAtCaptureStart : requestedDpr;
5955
+ const fitScale = Math.min(width / snapshotCanvas.width, height / snapshotCanvas.height);
5956
+ const destWidth = snapshotCanvas.width * fitScale;
5957
+ const destHeight = snapshotCanvas.height * fitScale;
5847
5958
  const destX = (width - destWidth) / 2;
5848
5959
  const destY = (height - destHeight) / 2;
5849
5960
  const layer = document.createElement("canvas");
@@ -5854,10 +5965,14 @@ async function rasterizeViewportToPageLayer(width, height) {
5854
5965
  throw new Error("Failed to get 2D context for page layer");
5855
5966
  layerCtx.fillStyle = "#ffffff";
5856
5967
  layerCtx.fillRect(0, 0, width, height);
5857
- layerCtx.drawImage(snapshotCanvas, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
5968
+ layerCtx.drawImage(snapshotCanvas, destX, destY, destWidth, destHeight);
5858
5969
  return {
5859
5970
  canvas: layer,
5860
- viewportToCanvas: { scale: dpr * fitScale, offsetX: destX, offsetY: destY }
5971
+ viewportToCanvas: {
5972
+ scale: canvasPixelsPerCssPixel * fitScale,
5973
+ offsetX: destX,
5974
+ offsetY: destY
5975
+ }
5861
5976
  };
5862
5977
  }
5863
5978
  function compositePageAndCursorOverlay(ctx, canvas, cachedPage, brand) {
@@ -5881,6 +5996,12 @@ async function gzipIfBeneficial(snapshotBytes) {
5881
5996
  return { bytes: snapshotBytes, compressionFlag: 0 };
5882
5997
  }
5883
5998
  }
5999
+ function frameWithCompressionFlag(compressionFlag, payload) {
6000
+ const frame = new Uint8Array(payload.byteLength + 1);
6001
+ frame[0] = compressionFlag;
6002
+ frame.set(payload, 1);
6003
+ return frame;
6004
+ }
5884
6005
  async function buildDomSnapshotFrame() {
5885
6006
  const a11yTree = buildAccessibilityTree();
5886
6007
  const snapshotJson = JSON.stringify({
@@ -5893,14 +6014,12 @@ async function buildDomSnapshotFrame() {
5893
6014
  });
5894
6015
  const snapshotBytes = textEncoder4.encode(snapshotJson);
5895
6016
  const { bytes, compressionFlag } = await gzipIfBeneficial(snapshotBytes);
5896
- const frame = new Uint8Array(bytes.byteLength + 1);
5897
- frame[0] = compressionFlag;
5898
- frame.set(bytes, 1);
5899
- return frame;
6017
+ return frameWithCompressionFlag(compressionFlag, bytes);
5900
6018
  }
5901
6019
  function reportCaptureError(localParticipant) {
5902
6020
  try {
5903
- localParticipant.publishData(textEncoder4.encode(JSON.stringify({ type: "capture_error" })), {
6021
+ const payload = textEncoder4.encode(JSON.stringify({ type: "capture_error" }));
6022
+ localParticipant.publishData(frameWithCompressionFlag(0, payload), {
5904
6023
  reliable: true,
5905
6024
  topic: DOM_SNAPSHOT_TOPIC
5906
6025
  }).catch(() => {
@@ -5943,12 +6062,18 @@ function DomCapture({ pushOrTapMicMode = false }) {
5943
6062
  return;
5944
6063
  videoTrack.contentHint = "text";
5945
6064
  didStartRef.current = true;
6065
+ preCacheSnapshotResources();
5946
6066
  let cancelled = false;
5947
6067
  let snapshotInFlight = false;
5948
6068
  let a11yPublishInFlight = false;
5949
6069
  let a11yPublishPending = false;
5950
6070
  let consecutiveCaptureFailures = 0;
5951
6071
  let cachedPage = null;
6072
+ let rasterizeSequence = 0;
6073
+ let lastCommittedSequence = 0;
6074
+ let firstFrameCommitted = false;
6075
+ let capturesUntilFullQualityRetry = 0;
6076
+ let consecutiveFullQualityTimeouts = 0;
5952
6077
  if (!pushOrTapMicMode && !localParticipant.isMicrophoneEnabled) {
5953
6078
  localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
5954
6079
  }
@@ -5964,16 +6089,61 @@ function DomCapture({ pushOrTapMicMode = false }) {
5964
6089
  for (const track of canvasStream.getTracks())
5965
6090
  track.stop();
5966
6091
  });
5967
- const refreshPageAndComposite = async () => {
5968
- if (cancelled || snapshotInFlight || pausedRef.current)
6092
+ const commitPageLayerIfNewest = (sequence, page) => {
6093
+ if (cancelled || pausedRef.current || sequence <= lastCommittedSequence)
6094
+ return false;
6095
+ lastCommittedSequence = sequence;
6096
+ cachedPage = page;
6097
+ compositePageAndCursorOverlay(ctx, canvas, page, brandRef.current);
6098
+ firstFrameCommitted = true;
6099
+ return true;
6100
+ };
6101
+ const rasterizeAndCommitWaitingOutTimeout = async (options) => {
6102
+ const sequence = ++rasterizeSequence;
6103
+ const captureInProgress = rasterizeViewportToPageLayer(canvas.width, canvas.height, options);
6104
+ const captureOrTimeout = await Promise.race([
6105
+ captureInProgress,
6106
+ new Promise((resolve) => setTimeout(() => resolve(RASTERIZE_TIMED_OUT), RASTERIZE_TIMEOUT_MS))
6107
+ ]);
6108
+ if (captureOrTimeout === RASTERIZE_TIMED_OUT) {
6109
+ captureInProgress.then((lateCapture) => commitPageLayerIfNewest(sequence, lateCapture)).catch(() => {
6110
+ return;
6111
+ });
6112
+ return { timedOut: true };
6113
+ }
6114
+ commitPageLayerIfNewest(sequence, captureOrTimeout);
6115
+ return { timedOut: false };
6116
+ };
6117
+ let followUpRefreshQueued = false;
6118
+ const refreshPageAndComposite = async ({ queueWhenBusy = false } = {}) => {
6119
+ if (cancelled || snapshotInFlight || pausedRef.current) {
6120
+ if (snapshotInFlight && !cancelled && queueWhenBusy)
6121
+ followUpRefreshQueued = true;
5969
6122
  return;
6123
+ }
5970
6124
  snapshotInFlight = true;
5971
6125
  try {
5972
- cachedPage = await rasterizeViewportToPageLayer(canvas.width, canvas.height);
5973
- if (cancelled || pausedRef.current)
5974
- return;
5975
- compositePageAndCursorOverlay(ctx, canvas, cachedPage, brandRef.current);
6126
+ const isImagelessFirstFrame = !firstFrameCommitted;
6127
+ const useFullQuality = firstFrameCommitted && capturesUntilFullQualityRetry === 0;
6128
+ const { timedOut } = await rasterizeAndCommitWaitingOutTimeout({
6129
+ skipCrossOriginImageSettling: !useFullQuality,
6130
+ includeImages: !isImagelessFirstFrame
6131
+ });
6132
+ if (isImagelessFirstFrame && firstFrameCommitted)
6133
+ followUpRefreshQueued = true;
5976
6134
  consecutiveCaptureFailures = 0;
6135
+ if (useFullQuality) {
6136
+ if (timedOut) {
6137
+ consecutiveFullQualityTimeouts += 1;
6138
+ if (consecutiveFullQualityTimeouts >= FULL_QUALITY_TIMEOUTS_BEFORE_COOLDOWN) {
6139
+ capturesUntilFullQualityRetry = FAST_CAPTURES_BEFORE_FULL_QUALITY_RETRY;
6140
+ }
6141
+ } else {
6142
+ consecutiveFullQualityTimeouts = 0;
6143
+ }
6144
+ } else if (capturesUntilFullQualityRetry > 0) {
6145
+ capturesUntilFullQualityRetry -= 1;
6146
+ }
5977
6147
  } catch {
5978
6148
  if (cancelled)
5979
6149
  return;
@@ -5983,6 +6153,10 @@ function DomCapture({ pushOrTapMicMode = false }) {
5983
6153
  }
5984
6154
  } finally {
5985
6155
  snapshotInFlight = false;
6156
+ if (followUpRefreshQueued && !cancelled) {
6157
+ followUpRefreshQueued = false;
6158
+ refreshPageAndComposite();
6159
+ }
5986
6160
  }
5987
6161
  };
5988
6162
  let overlayAnimationFrame = null;
@@ -6048,20 +6222,30 @@ function DomCapture({ pushOrTapMicMode = false }) {
6048
6222
  });
6049
6223
  const cleanupSnapshotRequest = onSnapshotRequest(() => {
6050
6224
  tickA11yPublish();
6051
- refreshPageAndComposite();
6225
+ refreshPageAndComposite({ queueWhenBusy: true });
6052
6226
  });
6227
+ let scrollSettleTimer = null;
6228
+ const refreshOnceScrollSettles = () => {
6229
+ if (scrollSettleTimer)
6230
+ clearTimeout(scrollSettleTimer);
6231
+ scrollSettleTimer = setTimeout(() => {
6232
+ tickA11yPublish();
6233
+ refreshPageAndComposite({ queueWhenBusy: true });
6234
+ }, SCROLL_SETTLE_MS);
6235
+ };
6236
+ window.addEventListener("scroll", refreshOnceScrollSettles, { passive: true, capture: true });
6053
6237
  let snapshotTimer = null;
6054
6238
  const scheduleNextSnapshot = (delay) => {
6055
6239
  if (cancelled)
6056
6240
  return;
6057
- snapshotTimer = setTimeout(async () => {
6058
- await refreshPageAndComposite();
6241
+ snapshotTimer = setTimeout(() => {
6242
+ refreshPageAndComposite();
6059
6243
  scheduleNextSnapshot(SNAPSHOT_INTERVAL_MS);
6060
6244
  }, delay);
6061
6245
  };
6062
6246
  scheduleNextSnapshot(FIRST_SNAPSHOT_DELAY_MS);
6063
6247
  tickA11yPublish();
6064
- const a11yPublishTimer = setInterval(tickA11yPublish, A11Y_PUBLISH_INTERVAL_MS);
6248
+ const a11yPublishTimer = setInterval(() => void tickA11yPublish(), A11Y_PUBLISH_INTERVAL_MS);
6065
6249
  freezeCaptureRef.current = () => {
6066
6250
  pausedRef.current = true;
6067
6251
  drawPausedOverlay(canvas, ctx);
@@ -6078,6 +6262,9 @@ function DomCapture({ pushOrTapMicMode = false }) {
6078
6262
  resumeCaptureRef.current = null;
6079
6263
  if (snapshotTimer)
6080
6264
  clearTimeout(snapshotTimer);
6265
+ if (scrollSettleTimer)
6266
+ clearTimeout(scrollSettleTimer);
6267
+ window.removeEventListener("scroll", refreshOnceScrollSettles, { capture: true });
6081
6268
  clearInterval(a11yPublishTimer);
6082
6269
  if (overlayAnimationFrame !== null)
6083
6270
  cancelAnimationFrame(overlayAnimationFrame);