lazy-gravity 0.6.1 → 0.7.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.
@@ -30,6 +30,56 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
30
30
  const hasActiveChat = title.length > 0 && title !== 'Agent';
31
31
  return { title: title || '(Untitled)', hasActiveChat };
32
32
  })()`;
33
+ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
34
+ const panel = document.querySelector('.antigravity-agent-side-panel');
35
+ if (!panel) {
36
+ return {
37
+ panelFound: false,
38
+ title: '',
39
+ hasActiveChat: false,
40
+ hasLoadingIndicator: false,
41
+ hasRenderableContent: false,
42
+ renderablePreview: [],
43
+ };
44
+ }
45
+ const header = panel.querySelector('div[class*="border-b"]');
46
+ const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
47
+ const title = titleEl ? (titleEl.textContent || '').trim() : '';
48
+ const hasActiveChat = title.length > 0 && title !== 'Agent';
49
+
50
+ const bodyCandidates = Array.from(panel.querySelectorAll(
51
+ '[data-message-author-role], [data-message-role], .rendered-markdown, .prose'
52
+ ));
53
+ const renderablePreview = bodyCandidates.filter((el) => {
54
+ if (!(el instanceof HTMLElement)) return false;
55
+ const text = (el.textContent || '').trim();
56
+ if (el.offsetParent === null || text.length === 0) return false;
57
+ if (/^\\/\\*\\s*Copied from /i.test(text)) return false;
58
+ return true;
59
+ }).slice(0, 5).map((el) => ({
60
+ tag: el.tagName,
61
+ className: el.className || '',
62
+ text: ((el.textContent || '').trim()).slice(0, 160),
63
+ }));
64
+ const hasRenderableContent = renderablePreview.length > 0;
65
+
66
+ const hasLoadingIndicator = Boolean(
67
+ panel.querySelector(
68
+ '[role="progressbar"], ' +
69
+ 'svg[class*="animate-spin"], div[class*="animate-spin"], ' +
70
+ 'svg[class*="spinner"], div[class*="spinner"], div[class*="loading"]'
71
+ )
72
+ );
73
+
74
+ return {
75
+ panelFound: true,
76
+ title: title || '(Untitled)',
77
+ hasActiveChat,
78
+ hasLoadingIndicator,
79
+ hasRenderableContent,
80
+ renderablePreview,
81
+ };
82
+ })()`;
33
83
  /**
34
84
  * Script to find the Past Conversations button and return its coordinates.
35
85
  * We use coordinates so that the actual click is done via CDP Input.dispatchMouseEvent,
@@ -432,6 +482,10 @@ class ChatSessionService {
432
482
  static ACTIVATE_SESSION_MAX_WAIT_MS = 30000;
433
483
  static ACTIVATE_SESSION_RETRY_INTERVAL_MS = 800;
434
484
  static LIST_SESSIONS_TARGET = 20;
485
+ static HYDRATE_RETRY_DELAY_MS = 700;
486
+ static REOPEN_RETRY_ATTEMPTS = 4;
487
+ static REOPEN_NEW_CHAT_DELAY_MS = 400;
488
+ static REOPEN_HISTORY_DELAY_MS = 1000;
435
489
  /**
436
490
  * List recent sessions by opening the Past Conversations panel.
437
491
  *
@@ -556,6 +610,25 @@ class ChatSessionService {
556
610
  type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
557
611
  });
558
612
  }
613
+ async dispatchNewConversationShortcut(cdpService) {
614
+ const modifiers = process.platform === 'darwin' ? 8 : 10;
615
+ await cdpService.call('Input.dispatchKeyEvent', {
616
+ type: 'keyDown',
617
+ key: 'L',
618
+ code: 'KeyL',
619
+ modifiers,
620
+ windowsVirtualKeyCode: 76,
621
+ nativeVirtualKeyCode: 76,
622
+ });
623
+ await cdpService.call('Input.dispatchKeyEvent', {
624
+ type: 'keyUp',
625
+ key: 'L',
626
+ code: 'KeyL',
627
+ modifiers,
628
+ windowsVirtualKeyCode: 76,
629
+ nativeVirtualKeyCode: 76,
630
+ });
631
+ }
559
632
  /**
560
633
  * Start a new chat session in the Antigravity UI.
561
634
  *
@@ -597,27 +670,23 @@ class ChatSessionService {
597
670
  if (!btnState.enabled) {
598
671
  return { ok: true };
599
672
  }
600
- // cursor: pointer -> click via CDP Input API coordinates
601
- await cdpService.call('Input.dispatchMouseEvent', {
602
- type: 'mouseMoved', x: btnState.x, y: btnState.y,
603
- });
604
- await cdpService.call('Input.dispatchMouseEvent', {
605
- type: 'mousePressed', x: btnState.x, y: btnState.y,
606
- button: 'left', clickCount: 1,
607
- });
608
- await cdpService.call('Input.dispatchMouseEvent', {
609
- type: 'mouseReleased', x: btnState.x, y: btnState.y,
610
- button: 'left', clickCount: 1,
611
- });
612
- // Wait for UI to update after click
673
+ // Prefer the keyboard shortcut because some Antigravity builds bind hover tips to the button target.
674
+ await this.dispatchNewConversationShortcut(cdpService);
675
+ // Wait for UI to update after shortcut
613
676
  await new Promise(r => setTimeout(r, 1500));
614
677
  // Check if button changed to not-allowed (evidence that a new chat was opened)
615
678
  const afterState = await this.getNewChatButtonState(cdpService, contexts);
616
679
  if (afterState.found && !afterState.enabled) {
617
680
  return { ok: true };
618
681
  }
619
- // Button still enabled -> click may not have worked
620
- return { ok: false, error: 'Clicked new chat button but state did not change' };
682
+ // Fallback for older builds where the shortcut is not wired.
683
+ await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
684
+ await new Promise(r => setTimeout(r, 1500));
685
+ const afterFallback = await this.getNewChatButtonState(cdpService, contexts);
686
+ if (afterFallback.found && !afterFallback.enabled) {
687
+ return { ok: true };
688
+ }
689
+ return { ok: false, error: 'New conversation shortcut and button click did not change state' };
621
690
  }
622
691
  catch (error) {
623
692
  const message = error instanceof Error ? error.message : String(error);
@@ -655,6 +724,104 @@ class ChatSessionService {
655
724
  return { title: '(Failed to retrieve)', hasActiveChat: false };
656
725
  }
657
726
  }
727
+ async getCurrentSessionViewState(cdpService) {
728
+ try {
729
+ const contexts = cdpService.getContexts();
730
+ for (const ctx of contexts) {
731
+ try {
732
+ const result = await cdpService.call('Runtime.evaluate', {
733
+ expression: GET_SESSION_VIEW_STATE_SCRIPT,
734
+ returnByValue: true,
735
+ contextId: ctx.id,
736
+ });
737
+ const value = result?.result?.value;
738
+ const hasPanel = value?.panelFound === true;
739
+ const looksUseful = Boolean(hasPanel ||
740
+ value?.hasLoadingIndicator ||
741
+ value?.hasRenderableContent ||
742
+ (typeof value?.title === 'string' && value.title.trim().length > 0));
743
+ if (value && looksUseful) {
744
+ return {
745
+ title: value.title,
746
+ hasActiveChat: value.hasActiveChat ?? false,
747
+ panelFound: value.panelFound ?? false,
748
+ hasLoadingIndicator: value.hasLoadingIndicator ?? false,
749
+ hasRenderableContent: value.hasRenderableContent ?? false,
750
+ renderablePreview: Array.isArray(value.renderablePreview) ? value.renderablePreview : [],
751
+ };
752
+ }
753
+ }
754
+ catch (_) { /* try next context */ }
755
+ }
756
+ }
757
+ catch (_) { /* fall through */ }
758
+ return {
759
+ title: '(Failed to retrieve)',
760
+ hasActiveChat: false,
761
+ panelFound: false,
762
+ hasLoadingIndicator: false,
763
+ hasRenderableContent: false,
764
+ renderablePreview: [],
765
+ };
766
+ }
767
+ async refreshSessionViewIfStuck(cdpService, title) {
768
+ const state = await this.getCurrentSessionViewState(cdpService);
769
+ if (state.title.trim() !== title.trim()) {
770
+ return { ok: false, error: `Current title mismatch before refresh (expected="${title}", actual="${state.title}")` };
771
+ }
772
+ if (!state.hasLoadingIndicator && state.hasRenderableContent) {
773
+ return { ok: true };
774
+ }
775
+ const bounce = await this.recoverSessionViewWithNewConversationBounce(cdpService, title);
776
+ if (bounce.ok) {
777
+ return bounce;
778
+ }
779
+ return {
780
+ ok: false,
781
+ error: `Session "${title}" still appears stuck after new-conversation recovery ` +
782
+ `(${bounce.error || 'unknown'})`,
783
+ };
784
+ }
785
+ async recoverSessionViewWithNewConversationBounce(cdpService, title, options) {
786
+ const state = await this.getCurrentSessionViewState(cdpService);
787
+ if (state.title.trim() === title.trim() && !state.hasLoadingIndicator && state.hasRenderableContent) {
788
+ return { ok: true };
789
+ }
790
+ const maxAttempts = options?.maxAttempts ?? ChatSessionService.REOPEN_RETRY_ATTEMPTS;
791
+ const newChatDelayMs = options?.newChatDelayMs ?? ChatSessionService.REOPEN_NEW_CHAT_DELAY_MS;
792
+ const reopenDelayMs = options?.reopenDelayMs ?? ChatSessionService.REOPEN_HISTORY_DELAY_MS;
793
+ let lastError = `Session "${title}" still appears stuck before recovery`;
794
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
795
+ const newChat = await this.startNewChat(cdpService);
796
+ if (!newChat.ok) {
797
+ lastError =
798
+ `Attempt ${attempt}/${maxAttempts}: failed to open a fresh new conversation before reopening "${title}": ` +
799
+ `${newChat.error || 'unknown'}`;
800
+ continue;
801
+ }
802
+ await new Promise((resolve) => setTimeout(resolve, newChatDelayMs));
803
+ const reopened = await this.activateSessionByTitle(cdpService, title, {
804
+ maxWaitMs: 8000,
805
+ retryIntervalMs: 300,
806
+ allowVisibilityWarmupMs: 1000,
807
+ });
808
+ if (!reopened.ok) {
809
+ lastError =
810
+ `Attempt ${attempt}/${maxAttempts}: failed to reopen "${title}" after new conversation: ` +
811
+ `${reopened.error || 'unknown'}`;
812
+ continue;
813
+ }
814
+ await new Promise((resolve) => setTimeout(resolve, reopenDelayMs));
815
+ const after = await this.getCurrentSessionViewState(cdpService);
816
+ if (after.title.trim() === title.trim() && (!after.hasLoadingIndicator || after.hasRenderableContent)) {
817
+ return { ok: true };
818
+ }
819
+ lastError =
820
+ `Attempt ${attempt}/${maxAttempts}: session "${title}" still appears stuck after reopening ` +
821
+ `(loading=${after.hasLoadingIndicator}, content=${after.hasRenderableContent}, actual="${after.title}")`;
822
+ }
823
+ return { ok: false, error: lastError };
824
+ }
658
825
  /**
659
826
  * Activate an existing chat by title.
660
827
  * Returns ok:false if the target chat cannot be located or verified.
@@ -669,12 +836,14 @@ class ChatSessionService {
669
836
  }
670
837
  const maxWaitMs = options?.maxWaitMs ?? ChatSessionService.ACTIVATE_SESSION_MAX_WAIT_MS;
671
838
  const retryIntervalMs = options?.retryIntervalMs ?? ChatSessionService.ACTIVATE_SESSION_RETRY_INTERVAL_MS;
839
+ const allowVisibilityWarmupMs = options?.allowVisibilityWarmupMs ?? 0;
672
840
  let usedPastConversations = false;
673
841
  let directResult = { ok: false, error: 'not attempted' };
674
842
  let pastResult = null;
675
843
  let clicked = false;
676
- const startedAt = Date.now();
844
+ let startedAt = Date.now();
677
845
  let attempts = 0;
846
+ let warmupConsumed = false;
678
847
  while (Date.now() - startedAt <= maxWaitMs) {
679
848
  attempts += 1;
680
849
  directResult = await this.tryActivateByDirectSidePanel(cdpService, title);
@@ -704,8 +873,21 @@ class ChatSessionService {
704
873
  await new Promise((resolve) => setTimeout(resolve, 500));
705
874
  const after = await this.getCurrentSessionInfo(cdpService);
706
875
  if (after.title.trim() === title.trim()) {
876
+ if (usedPastConversations) {
877
+ await this.closePanelWithEscape(cdpService);
878
+ }
707
879
  return { ok: true };
708
880
  }
881
+ if (!warmupConsumed && allowVisibilityWarmupMs > 0 && after.title.trim() === 'Agent') {
882
+ warmupConsumed = true;
883
+ startedAt = Date.now();
884
+ await new Promise((resolve) => setTimeout(resolve, allowVisibilityWarmupMs));
885
+ return this.activateSessionByTitle(cdpService, title, {
886
+ maxWaitMs,
887
+ retryIntervalMs,
888
+ allowVisibilityWarmupMs: 0,
889
+ });
890
+ }
709
891
  // If direct side-panel activation hit the wrong row, try the explicit Past Conversations flow.
710
892
  if (!usedPastConversations) {
711
893
  const viaPast = await this.tryActivateByPastConversations(cdpService, title);
@@ -713,6 +895,7 @@ class ChatSessionService {
713
895
  await new Promise((resolve) => setTimeout(resolve, 500));
714
896
  const afterPast = await this.getCurrentSessionInfo(cdpService);
715
897
  if (afterPast.title.trim() === title.trim()) {
898
+ await this.closePanelWithEscape(cdpService);
716
899
  return { ok: true };
717
900
  }
718
901
  return {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ResponseMonitor = exports.RESPONSE_SELECTORS = void 0;
4
+ exports.captureResponseMonitorBaseline = captureResponseMonitorBaseline;
4
5
  const logger_1 = require("../utils/logger");
5
6
  const assistantDomExtractor_1 = require("./assistantDomExtractor");
6
7
  /** Lean DOM selectors for response extraction */
@@ -433,6 +434,125 @@ exports.RESPONSE_SELECTORS = {
433
434
  return diag;
434
435
  })()`,
435
436
  };
437
+ /**
438
+ * Prefer the active runtime context first, then fall back to any discovered contexts.
439
+ */
440
+ function getOrderedContextTargets(cdpService) {
441
+ const primaryId = cdpService.getPrimaryContextId?.() ?? null;
442
+ const rawContexts = cdpService.getContexts?.() ?? [];
443
+ const contexts = rawContexts
444
+ .filter((ctx) => ctx && typeof ctx.id === 'number')
445
+ .map((ctx) => ({
446
+ id: ctx.id,
447
+ name: typeof ctx.name === 'string' ? ctx.name : undefined,
448
+ url: typeof ctx.url === 'string' ? ctx.url : undefined,
449
+ }));
450
+ if (contexts.length === 0) {
451
+ return primaryId !== null ? [{ id: primaryId }] : [];
452
+ }
453
+ if (primaryId === null) {
454
+ return contexts;
455
+ }
456
+ const primary = contexts.find((ctx) => ctx.id === primaryId);
457
+ const ordered = primary ? [primary] : [{ id: primaryId }];
458
+ for (const ctx of contexts) {
459
+ if (ctx.id !== primaryId)
460
+ ordered.push(ctx);
461
+ }
462
+ return ordered;
463
+ }
464
+ /**
465
+ * Evaluate an expression across known runtime contexts until one returns an acceptable value.
466
+ */
467
+ async function evaluateAcrossContexts(cdpService, expression, accept, options) {
468
+ const awaitPromise = options?.awaitPromise ?? true;
469
+ const targets = getOrderedContextTargets(cdpService);
470
+ let firstValue = null;
471
+ let lastError = null;
472
+ if (targets.length === 0) {
473
+ const result = await cdpService.call('Runtime.evaluate', {
474
+ expression,
475
+ returnByValue: true,
476
+ awaitPromise,
477
+ });
478
+ return {
479
+ value: (result?.result?.value ?? null),
480
+ contextId: null,
481
+ contextName: null,
482
+ contextUrl: null,
483
+ };
484
+ }
485
+ for (const target of targets) {
486
+ try {
487
+ const result = await cdpService.call('Runtime.evaluate', {
488
+ expression,
489
+ returnByValue: true,
490
+ awaitPromise,
491
+ contextId: target.id,
492
+ });
493
+ const value = (result?.result?.value ?? null);
494
+ const probed = {
495
+ value,
496
+ contextId: target.id,
497
+ contextName: target.name ?? null,
498
+ contextUrl: target.url ?? null,
499
+ };
500
+ if (!firstValue)
501
+ firstValue = probed;
502
+ if (accept(value)) {
503
+ return probed;
504
+ }
505
+ }
506
+ catch (error) {
507
+ lastError = error;
508
+ }
509
+ }
510
+ if (firstValue) {
511
+ return firstValue;
512
+ }
513
+ if (lastError) {
514
+ throw lastError;
515
+ }
516
+ return {
517
+ value: null,
518
+ contextId: null,
519
+ contextName: null,
520
+ contextUrl: null,
521
+ };
522
+ }
523
+ /**
524
+ * Capture the current assistant/output DOM state before sending a new prompt.
525
+ * This avoids races where a fast reply is mistaken for baseline text.
526
+ */
527
+ async function captureResponseMonitorBaseline(cdpService) {
528
+ let text = null;
529
+ try {
530
+ const textResult = await evaluateAcrossContexts(cdpService, exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
531
+ text = typeof textResult.value === 'string' ? textResult.value.trim() || null : null;
532
+ }
533
+ catch {
534
+ text = null;
535
+ }
536
+ const processLogKeys = new Set();
537
+ try {
538
+ const logResult = await evaluateAcrossContexts(cdpService, exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
539
+ const logEntries = logResult.value;
540
+ if (Array.isArray(logEntries)) {
541
+ for (const entry of logEntries) {
542
+ const key = String(entry || '').replace(/\r/g, '').trim().slice(0, 200);
543
+ if (key)
544
+ processLogKeys.add(key);
545
+ }
546
+ }
547
+ }
548
+ catch {
549
+ // best-effort baseline capture
550
+ }
551
+ return {
552
+ text,
553
+ processLogKeys: Array.from(processLogKeys),
554
+ };
555
+ }
436
556
  /**
437
557
  * Lean AI response monitor.
438
558
  *
@@ -452,6 +572,8 @@ class ResponseMonitor {
452
572
  onTimeout;
453
573
  onPhaseChange;
454
574
  onProcessLog;
575
+ initialBaselineText;
576
+ initialSeenProcessLogKeys;
455
577
  pollTimer = null;
456
578
  isRunning = false;
457
579
  lastText = null;
@@ -462,6 +584,7 @@ class ResponseMonitor {
462
584
  quotaDetected = false;
463
585
  seenProcessLogKeys = new Set();
464
586
  structuredDiagLogged = false;
587
+ lastContentContextId = null;
465
588
  // CDP disconnect handling (#48)
466
589
  isPaused = false;
467
590
  onCdpDisconnected = null;
@@ -480,6 +603,8 @@ class ResponseMonitor {
480
603
  this.onTimeout = options.onTimeout;
481
604
  this.onPhaseChange = options.onPhaseChange;
482
605
  this.onProcessLog = options.onProcessLog;
606
+ this.initialBaselineText = options.initialBaselineText;
607
+ this.initialSeenProcessLogKeys = options.initialSeenProcessLogKeys;
483
608
  }
484
609
  /** Start monitoring */
485
610
  async start() {
@@ -508,28 +633,38 @@ class ResponseMonitor {
508
633
  this.quotaDetected = false;
509
634
  this.seenProcessLogKeys = new Set();
510
635
  this.onPhaseChange?.(this.currentPhase, null);
511
- // Capture baseline text
512
- try {
513
- const baseResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
514
- const rawValue = baseResult?.result?.value;
515
- this.baselineText = typeof rawValue === 'string' ? rawValue.trim() || null : null;
516
- }
517
- catch {
518
- this.baselineText = null;
636
+ if (this.initialBaselineText !== undefined) {
637
+ this.baselineText = this.initialBaselineText;
519
638
  }
520
- // Capture baseline process logs as already-seen keys
521
- try {
522
- const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
523
- const logEntries = logResult?.result?.value;
524
- if (Array.isArray(logEntries)) {
525
- this.seenProcessLogKeys = new Set(logEntries
526
- .map((s) => (s || '').replace(/\r/g, '').trim())
527
- .filter((s) => s.length > 0)
528
- .map((s) => s.slice(0, 200)));
639
+ else {
640
+ try {
641
+ const baseResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
642
+ this.baselineText = typeof baseResult.value === 'string' ? baseResult.value.trim() || null : null;
643
+ }
644
+ catch {
645
+ this.baselineText = null;
529
646
  }
530
647
  }
531
- catch {
532
- // baseline capture only
648
+ if (this.initialSeenProcessLogKeys !== undefined) {
649
+ this.seenProcessLogKeys = new Set(this.initialSeenProcessLogKeys
650
+ .map((s) => (s || '').replace(/\r/g, '').trim())
651
+ .filter((s) => s.length > 0)
652
+ .map((s) => s.slice(0, 200)));
653
+ }
654
+ else {
655
+ try {
656
+ const logResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
657
+ const logEntries = logResult.value;
658
+ if (Array.isArray(logEntries)) {
659
+ this.seenProcessLogKeys = new Set(logEntries
660
+ .map((s) => (s || '').replace(/\r/g, '').trim())
661
+ .filter((s) => s.length > 0)
662
+ .map((s) => s.slice(0, 200)));
663
+ }
664
+ }
665
+ catch {
666
+ // baseline capture only
667
+ }
533
668
  }
534
669
  // In structured mode, also capture activity lines from the structured
535
670
  // extraction to align the baseline with polling logic. The PROCESS_LOGS
@@ -538,8 +673,8 @@ class ResponseMonitor {
538
673
  // entries from previous turns leak into the process log as "new" entries.
539
674
  if (this.extractionMode === 'structured') {
540
675
  try {
541
- const structuredBaseline = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
542
- const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline?.result?.value);
676
+ const structuredBaseline = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED, (value) => (0, assistantDomExtractor_1.classifyAssistantSegments)(value).diagnostics.source === 'dom-structured');
677
+ const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline.value);
543
678
  if (baselineClassified.diagnostics.source === 'dom-structured') {
544
679
  for (const line of baselineClassified.activityLines) {
545
680
  const key = (line || '').replace(/\r/g, '').trim().slice(0, 200);
@@ -589,12 +724,15 @@ class ResponseMonitor {
589
724
  /** Click the stop button to interrupt LLM generation */
590
725
  async clickStopButton() {
591
726
  try {
592
- const result = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.CLICK_STOP_BUTTON));
593
- const value = result?.result?.value;
727
+ const result = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.CLICK_STOP_BUTTON, (value) => !!(value && typeof value === 'object' && value.ok));
728
+ const value = result.value;
594
729
  if (this.isRunning) {
595
730
  await this.stop();
596
731
  }
597
- return value ?? { ok: false, error: 'CDP evaluation returned empty' };
732
+ if (value && typeof value.ok === 'boolean') {
733
+ return value;
734
+ }
735
+ return { ok: false, error: 'CDP evaluation returned empty' };
598
736
  }
599
737
  catch (error) {
600
738
  return { ok: false, error: error.message || 'Failed to click stop button' };
@@ -693,17 +831,70 @@ class ResponseMonitor {
693
831
  }
694
832
  }, this.pollIntervalMs);
695
833
  }
696
- buildEvaluateParams(expression) {
697
- const params = {
698
- expression,
699
- returnByValue: true,
834
+ async evaluateAcrossContexts(expression, accept) {
835
+ const result = await evaluateAcrossContexts(this.cdpService, expression, accept, {
700
836
  awaitPromise: true,
701
- };
702
- const contextId = this.cdpService.getPrimaryContextId?.();
703
- if (contextId !== null && contextId !== undefined) {
704
- params.contextId = contextId;
837
+ });
838
+ if (result.contextId !== null
839
+ && accept(result.value)
840
+ && this.lastContentContextId !== result.contextId) {
841
+ this.lastContentContextId = result.contextId;
842
+ logger_1.logger.debug(`[ResponseMonitor] Using context ${result.contextId} (${result.contextName ?? 'unknown'} | ${result.contextUrl ?? 'no-url'})`);
843
+ }
844
+ return result;
845
+ }
846
+ async logStructuredExtractionDiagnostics(payload) {
847
+ try {
848
+ const dumpResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.DUMP_ALL_TEXTS, (value) => Array.isArray(value) && value.length > 0);
849
+ const dumpValue = Array.isArray(dumpResult.value) ? dumpResult.value : [];
850
+ const accepted = dumpValue.filter((entry) => !entry?.skip).slice(0, 5);
851
+ const skipped = dumpValue.filter((entry) => entry?.skip).slice(0, 5);
852
+ logger_1.logger.warn(`[ResponseMonitor:diag] Structured payload invalid — ${dumpValue.length} candidate(s), ` +
853
+ `${accepted.length} accepted, ${skipped.length} skipped ` +
854
+ `(context=${dumpResult.contextId ?? 'none'})`);
855
+ logger_1.logger.debug('[ResponseMonitor:diag] Candidate details:', JSON.stringify({
856
+ payloadType: payload === null ? 'null' : typeof payload,
857
+ contextId: dumpResult.contextId,
858
+ contextUrl: dumpResult.contextUrl,
859
+ totalCandidates: dumpValue.length,
860
+ accepted: accepted.map((entry) => ({
861
+ sel: entry.sel,
862
+ len: entry.len,
863
+ preview: entry.preview,
864
+ })),
865
+ skipped: skipped.map((entry) => ({
866
+ sel: entry.sel,
867
+ skip: entry.skip,
868
+ len: entry.len,
869
+ preview: entry.preview,
870
+ })),
871
+ }));
872
+ }
873
+ catch (error) {
874
+ logger_1.logger.warn('[ResponseMonitor:diag] DUMP_ALL_TEXTS failed:', error);
875
+ }
876
+ try {
877
+ const domResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.DOM_DIAGNOSTIC, (value) => !!value && typeof value === 'object' && (Array.isArray(value.allTextNodes)
878
+ || Array.isArray(value.activityNodes)
879
+ || Array.isArray(value.detailsDump)));
880
+ const domValue = domResult.value;
881
+ logger_1.logger.warn(`[ResponseMonitor:diag] DOM_DIAGNOSTIC — ` +
882
+ `details=${domValue?.detailsCount ?? 0}, ` +
883
+ `activity=${Array.isArray(domValue?.activityNodes) ? domValue.activityNodes.length : 0}, ` +
884
+ `textNodes=${Array.isArray(domValue?.allTextNodes) ? domValue.allTextNodes.length : 0} ` +
885
+ `(context=${domResult.contextId ?? 'none'})`);
886
+ logger_1.logger.debug('[ResponseMonitor:diag] DOM_DIAGNOSTIC details:', JSON.stringify({
887
+ contextId: domResult.contextId,
888
+ contextUrl: domResult.contextUrl,
889
+ detailsCount: domValue?.detailsCount ?? null,
890
+ detailsDump: Array.isArray(domValue?.detailsDump) ? domValue.detailsDump.slice(0, 3) : [],
891
+ activityNodes: Array.isArray(domValue?.activityNodes) ? domValue.activityNodes.slice(0, 5) : [],
892
+ allTextNodes: Array.isArray(domValue?.allTextNodes) ? domValue.allTextNodes.slice(0, 5) : [],
893
+ }));
894
+ }
895
+ catch (error) {
896
+ logger_1.logger.warn('[ResponseMonitor:diag] DOM_DIAGNOSTIC failed:', error);
705
897
  }
706
- return params;
707
898
  }
708
899
  /**
709
900
  * Emit new process log entries, deduplicating against previously seen keys.
@@ -738,20 +929,20 @@ class ResponseMonitor {
738
929
  async poll() {
739
930
  try {
740
931
  // 1. Stop button check
741
- const stopResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.STOP_BUTTON));
742
- const stopValue = stopResult?.result?.value;
932
+ const stopResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.STOP_BUTTON, (value) => !!(value && typeof value === 'object' && value.isGenerating));
933
+ const stopValue = stopResult.value;
743
934
  const isGenerating = !!(stopValue && typeof stopValue === 'object' && stopValue.isGenerating);
744
935
  // 2. Quota error check
745
- const quotaResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.QUOTA_ERROR));
746
- const quotaDetected = quotaResult?.result?.value === true;
936
+ const quotaResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.QUOTA_ERROR, (value) => value === true);
937
+ const quotaDetected = quotaResult.value === true;
747
938
  // 3. Text extraction (structured or legacy)
748
939
  let currentText = null;
749
940
  let structuredHandledLogs = false;
750
941
  if (this.extractionMode === 'structured') {
751
942
  // Structured: use DOM segment extraction with HTML-to-Markdown
752
943
  try {
753
- const structuredResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
754
- const payload = structuredResult?.result?.value;
944
+ const structuredResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED, (value) => (0, assistantDomExtractor_1.classifyAssistantSegments)(value).diagnostics.source === 'dom-structured');
945
+ const payload = structuredResult.value;
755
946
  const classified = (0, assistantDomExtractor_1.classifyAssistantSegments)(payload);
756
947
  if (classified.diagnostics.source === 'dom-structured') {
757
948
  currentText = classified.finalOutputText.trim() || null;
@@ -768,6 +959,7 @@ class ResponseMonitor {
768
959
  else if (!this.structuredDiagLogged) {
769
960
  this.structuredDiagLogged = true;
770
961
  logger_1.logger.warn('[ResponseMonitor:poll] Structured extraction failed — reason:', classified.diagnostics.fallbackReason ?? 'unknown', '| payload type:', typeof payload, '| payload:', payload === null ? 'null' : payload === undefined ? 'undefined' : 'object');
962
+ await this.logStructuredExtractionDiagnostics(payload);
771
963
  }
772
964
  }
773
965
  catch (error) {
@@ -776,19 +968,14 @@ class ResponseMonitor {
776
968
  }
777
969
  // Legacy path (or fallback from structured)
778
970
  if (currentText === null) {
779
- const textResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
780
- const rawText = textResult?.result?.value;
781
- const exceptionDetail = textResult?.result?.exceptionDetails ?? textResult?.exceptionDetails;
782
- if (exceptionDetail) {
783
- logger_1.logger.warn('[ResponseMonitor:poll] RESPONSE_TEXT threw:', exceptionDetail.text ?? JSON.stringify(exceptionDetail).slice(0, 200));
784
- }
785
- currentText = typeof rawText === 'string' ? rawText.trim() || null : null;
971
+ const textResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
972
+ currentText = typeof textResult.value === 'string' ? textResult.value.trim() || null : null;
786
973
  }
787
974
  // 4. Process log extraction — always when structured didn't handle it
788
975
  if (!structuredHandledLogs) {
789
976
  try {
790
- const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
791
- const logEntries = logResult?.result?.value;
977
+ const logResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
978
+ const logEntries = logResult.value;
792
979
  if (Array.isArray(logEntries)) {
793
980
  this.emitNewProcessLogs(logEntries);
794
981
  }