teamshare-bridge 0.21.36 → 0.21.38

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.
@@ -622,9 +622,20 @@ async function cmdBrowse(flags) {
622
622
  let doneRetries = 0;
623
623
  /** How often to re-check which tab is active (ms). */
624
624
  const TAB_CHECK_MS = 2000;
625
+ /**
626
+ * Background tab-watch interval (ms). The main loop only calls
627
+ * `refreshTabIfNeeded` once per iteration and an iteration can include a
628
+ * 30-120s vision call, so a separate watcher probes the active tab while the
629
+ * loop is busy and raises `tabSwitchPending`.
630
+ */
631
+ const TAB_WATCH_MS = 1500;
625
632
  let lastTabCheck = 0;
626
633
  let consecutiveScreenshotFailures = 0;
627
634
  let reattachFailures = 0;
635
+ /** Set by the background watcher when a different tab became active. */
636
+ let tabSwitchPending = false;
637
+ /** Guards against overlapping `pickActiveTab` probes from the watcher. */
638
+ let tabProbeInFlight = false;
628
639
  // Latency observability: rolling average of vision-call latency plus
629
640
  // abort/timeout/stall counts, so the operator can see how slow the model is.
630
641
  const llmStats = { calls: 0, ok: 0, screenAborts: 0, timeouts: 0, stalls: 0, totalMs: 0 };
@@ -655,6 +666,9 @@ async function cmdBrowse(flags) {
655
666
  codeConfirmedOptimal = false;
656
667
  consecutiveFailures = 0;
657
668
  consecutiveScreenshotFailures = 0;
669
+ // The switch we were following is now applied.
670
+ tabSwitchPending = false;
671
+ page._tabSwitchedAt = undefined;
658
672
  console.log(`[browse] Re-attached to tab: ${page.title} (${page.targetId.slice(0, 8)})`);
659
673
  }
660
674
  /**
@@ -695,6 +709,27 @@ async function cmdBrowse(flags) {
695
709
  return false;
696
710
  }
697
711
  }
712
+ /**
713
+ * Background watcher: the human (or the site/tool action) can change the
714
+ * active tab at any moment, and the main loop cannot notice while it is
715
+ * inside a long vision call. Probe the active tab every `TAB_WATCH_MS` and
716
+ * flag a switch; the vision heartbeat aborts the in-flight call so the loop
717
+ * re-binds to the newly active tab immediately. We never activate a tab.
718
+ */
719
+ const tabWatcher = setInterval(() => {
720
+ if (stopped || page.disconnected || tabProbeInFlight)
721
+ return;
722
+ tabProbeInFlight = true;
723
+ void (0, cdp_1.pickActiveTab)(port, page.targetId)
724
+ .then((active) => {
725
+ if (active && active.id !== page.targetId) {
726
+ tabSwitchPending = true;
727
+ page._tabSwitchedAt = Date.now();
728
+ }
729
+ })
730
+ .catch(() => { })
731
+ .finally(() => { tabProbeInFlight = false; });
732
+ }, TAB_WATCH_MS);
698
733
  process.on('SIGINT', () => {
699
734
  console.log('\n[browse] Stopped by user');
700
735
  stopped = true;
@@ -703,9 +738,17 @@ async function cmdBrowse(flags) {
703
738
  console.log(`[browse] Agent started. ${continuous ? 'Running until Ctrl+C.' : `Max ${maxActions} actions.`} Press Ctrl+C to stop.\n`);
704
739
  while (!stopped && actionCount < maxActions) {
705
740
  try {
706
- // Follow the human's active tab: re-attach if the bound tab was closed or
707
- // the user switched to another one.
708
- await refreshTabIfNeeded();
741
+ // Follow the active tab: re-attach if the bound tab was closed, or if the
742
+ // watcher flagged a switch (force skips the throttle so we re-bind at once
743
+ // rather than working a stale tab for the rest of this iteration).
744
+ if (tabSwitchPending) {
745
+ tabSwitchPending = false;
746
+ page._tabSwitchedAt = undefined;
747
+ await refreshTabIfNeeded(true);
748
+ }
749
+ else {
750
+ await refreshTabIfNeeded();
751
+ }
709
752
  // Take screenshot and extract elements in parallel
710
753
  const [screenshot, elements, pageContext] = await Promise.all([
711
754
  captureScreenshot(page, quality, SCREENSHOT_MAX_EDGE),
@@ -778,10 +821,14 @@ async function cmdBrowse(flags) {
778
821
  fatalAuthError = true;
779
822
  break;
780
823
  }
781
- // The screen changed while the LLM was thinking: its answer is stale.
782
- // Bail out of the retry loop and re-analyze the NEW screen right away
783
- // (speed matters: a 30s backoff here livelocked on dynamic pages).
784
- if (result.error === 'screen_changed') {
824
+ // The screen changed (or a new tab became active) while the LLM was
825
+ // thinking: its answer is stale. Bail out of the retry loop and
826
+ // re-analyze the active tab right away (speed matters: a 30s backoff
827
+ // here livelocked on dynamic pages).
828
+ if (result.error === 'screen_changed' || result.error === 'tab_switch') {
829
+ if (result.error === 'tab_switch') {
830
+ console.log('[browse] Active tab changed during analysis — re-analyzing the new tab');
831
+ }
785
832
  screenChangedDuringLlm = true;
786
833
  llmStats.screenAborts++;
787
834
  logLlmStats();
@@ -1267,6 +1314,7 @@ Do NOT simplify variable names or change the function signature. Only optimize t
1267
1314
  await sleep(intervalMs);
1268
1315
  }
1269
1316
  }
1317
+ clearInterval(tabWatcher);
1270
1318
  if (actionCount >= maxActions && !continuous) {
1271
1319
  console.log(`[browse] Reached max actions (${maxActions})`);
1272
1320
  }
@@ -1285,12 +1333,14 @@ Do NOT simplify variable names or change the function signature. Only optimize t
1285
1333
  /**
1286
1334
  * Connect directly to a page's WebSocket from /json/list.
1287
1335
  * This avoids the Target.attachToTarget complexity.
1336
+ *
1337
+ * Uses the ACTIVE-tab picker (`resolveActivePage`): the loop must bind to the
1338
+ * tab the human/site has focused right now, not the first entry of /json/list.
1339
+ * `resolveActivePage` falls back to the general attachable-page picker (which
1340
+ * may open a blank tab) only when no page can be probed.
1288
1341
  */
1289
1342
  async function connectToPage(port, preferTargetId) {
1290
- // Shared picker (cdp.ts): prefers a real page, accepts about:blank/new-tab,
1291
- // and opens a blank tab if none exists - so a Chrome launched on the NTP is
1292
- // still attachable. (Was a local chrome:// filter that could never match.)
1293
- const pageTarget = await (0, cdp_1.resolveAttachablePage)(port, preferTargetId);
1343
+ const pageTarget = await (0, cdp_1.resolveActivePage)(port, preferTargetId);
1294
1344
  const wsUrl = pageTarget.webSocketDebuggerUrl;
1295
1345
  return new Promise((resolve, reject) => {
1296
1346
  const ws = new ws_1.default(wsUrl);
@@ -3396,6 +3446,14 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
3396
3446
  let heartbeatTicks = 0;
3397
3447
  const heartbeat = setInterval(async () => {
3398
3448
  const elapsed = Math.round((Date.now() - startTime) / 1000);
3449
+ // The human/page made a different tab active: this answer belongs to the
3450
+ // old tab, so abort and let the loop re-bind to the active one.
3451
+ if (page?._tabSwitchedAt && page._tabSwitchedAt > startTime) {
3452
+ console.log(`[browse] Active tab switched (${elapsed}s) — aborting LLM`);
3453
+ abortReason = 'tab_switch';
3454
+ controller.abort();
3455
+ return;
3456
+ }
3399
3457
  // Check for screen change via CDP navigation event or MutationObserver flag
3400
3458
  let screenChanged = false;
3401
3459
  if (page?._cdpNavigated && page._cdpNavigated > startTime) {
@@ -3512,11 +3570,19 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
3512
3570
  catch (err) {
3513
3571
  if (err instanceof Error && err.name === 'AbortError') {
3514
3572
  const elapsed = Math.round((Date.now() - startTime) / 1000);
3515
- if (abortReason === 'screen_change') {
3516
- // The page changed while we were waiting: the answer is already stale.
3517
- // Signal the caller to re-analyze right away (no backoff).
3518
- console.log(`[browse] LLM aborted at ${elapsed}s screen changed, re-analyzing now`);
3519
- return { actions: [], attempt, durationMs: Date.now() - startTime, error: 'screen_changed' };
3573
+ if (abortReason === 'screen_change' || abortReason === 'tab_switch') {
3574
+ // The page changed (or a new tab became active) while we were waiting:
3575
+ // the answer is already stale. Signal the caller to re-analyze the
3576
+ // active tab right away (no backoff).
3577
+ console.log(abortReason === 'tab_switch'
3578
+ ? `[browse] LLM aborted at ${elapsed}s — active tab changed, re-analyzing now`
3579
+ : `[browse] LLM aborted at ${elapsed}s — screen changed, re-analyzing now`);
3580
+ return {
3581
+ actions: [],
3582
+ attempt,
3583
+ durationMs: Date.now() - startTime,
3584
+ error: abortReason === 'tab_switch' ? 'tab_switch' : 'screen_changed',
3585
+ };
3520
3586
  }
3521
3587
  console.error(`[browse] LLM request timed out (${Math.round(timeoutMs / 1000)}s)`);
3522
3588
  return { actions: [], attempt, durationMs: Date.now() - startTime, error: 'timeout' };
@@ -3564,6 +3630,12 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
3564
3630
  // wipes it) must also abort - check `_cdpNavigated` too, as in the main call.
3565
3631
  const heartbeat = setInterval(async () => {
3566
3632
  const elapsed = Math.round((Date.now() - startTime) / 1000);
3633
+ if (page?._tabSwitchedAt && page._tabSwitchedAt > startTime) {
3634
+ console.log(`[browse] Active tab switched in opt pass (${elapsed}s) — aborting`);
3635
+ optAbortReason = 'screen_change';
3636
+ controller.abort();
3637
+ return;
3638
+ }
3567
3639
  if (page?._cdpNavigated && page._cdpNavigated > startTime) {
3568
3640
  console.log(`[browse] CDP navigation detected in opt pass (${elapsed}s) — aborting`);
3569
3641
  optAbortReason = 'screen_change';