@btraut/browser-bridge 0.13.2 → 0.14.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.
@@ -102,7 +102,6 @@ var DEFAULT_PERMISSION_PROMPT_WAIT_MS = 3e4;
102
102
  var SITE_PERMISSIONS_MODE_KEY = "sitePermissionsMode";
103
103
  var DEBUGGER_CAPABILITY_ENABLED_KEY = "debuggerCapabilityEnabled";
104
104
  var DEFAULT_SITE_PERMISSIONS_MODE = "granular";
105
- var DEFAULT_DEBUGGER_CAPABILITY_ENABLED = false;
106
105
  var siteKeyFromUrl = (rawUrl) => {
107
106
  if (!rawUrl || typeof rawUrl !== "string") {
108
107
  return null;
@@ -204,24 +203,12 @@ var readPermissionPromptWaitMs = async () => {
204
203
  );
205
204
  });
206
205
  };
207
- var readDebuggerCapabilityEnabled = async () => {
206
+ var writeDebuggerCapabilityEnabled = async (enabled) => {
207
+ void enabled;
208
208
  return await new Promise((resolve) => {
209
- chrome.storage.local.get(
210
- [DEBUGGER_CAPABILITY_ENABLED_KEY],
211
- (result) => {
212
- const raw = result?.[DEBUGGER_CAPABILITY_ENABLED_KEY];
213
- if (typeof raw === "boolean") {
214
- resolve(raw);
215
- return;
216
- }
217
- try {
218
- chrome.storage.local.set({
219
- [DEBUGGER_CAPABILITY_ENABLED_KEY]: DEFAULT_DEBUGGER_CAPABILITY_ENABLED
220
- });
221
- } catch {
222
- }
223
- resolve(DEFAULT_DEBUGGER_CAPABILITY_ENABLED);
224
- }
209
+ chrome.storage.local.set(
210
+ { [DEBUGGER_CAPABILITY_ENABLED_KEY]: true },
211
+ () => resolve()
225
212
  );
226
213
  });
227
214
  };
@@ -436,6 +423,416 @@ var PermissionPromptController = class {
436
423
  }
437
424
  };
438
425
 
426
+ // packages/extension/src/restricted-url.ts
427
+ var RESTRICTED_URL_PREFIXES = [
428
+ "chrome://",
429
+ "chrome-extension://",
430
+ "chrome-devtools://",
431
+ "devtools://",
432
+ "edge://",
433
+ "brave://",
434
+ "view-source:"
435
+ ];
436
+ var isRestrictedUrl = (url) => {
437
+ if (!url || typeof url !== "string") {
438
+ return false;
439
+ }
440
+ const lowered = url.toLowerCase();
441
+ if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
442
+ return true;
443
+ }
444
+ try {
445
+ const parsed = new URL(url);
446
+ if (parsed.hostname === "chromewebstore.google.com") {
447
+ return true;
448
+ }
449
+ if (parsed.hostname === "chrome.google.com") {
450
+ return parsed.pathname.startsWith("/webstore");
451
+ }
452
+ } catch (error) {
453
+ console.debug("Ignoring invalid URL in restriction check.", error);
454
+ }
455
+ return false;
456
+ };
457
+ var getRestrictedUrlKind = (url) => {
458
+ const lowered = url.toLowerCase();
459
+ if (lowered.startsWith("chrome-extension://")) {
460
+ return "extension_internal";
461
+ }
462
+ if (lowered.startsWith("chrome://") || lowered.startsWith("edge://") || lowered.startsWith("brave://")) {
463
+ return "browser_internal";
464
+ }
465
+ if (lowered.includes("chromewebstore.google.com") || lowered.includes("chrome.google.com/webstore")) {
466
+ return "webstore";
467
+ }
468
+ return "restricted_url";
469
+ };
470
+ var getAlternativeCommands = (url) => {
471
+ const lowered = url.toLowerCase();
472
+ if (lowered.startsWith("chrome-extension://") || lowered.startsWith("chrome://extensions")) {
473
+ return ["browser-bridge diagnostics doctor", "browser-bridge dev info"];
474
+ }
475
+ return ["browser-bridge dev info", "browser-bridge diagnostics doctor"];
476
+ };
477
+ var buildRestrictedUrlError = (options) => {
478
+ const alternatives = getAlternativeCommands(options.url);
479
+ const operationLabel = options.operation === "navigate" ? "Navigation" : options.operation === "screenshot" ? "Screenshots" : options.operation === "debugger" ? "Debugger attach" : "This action";
480
+ return {
481
+ code: "NOT_SUPPORTED",
482
+ message: `${operationLabel} is not supported for browser internal URLs.`,
483
+ retryable: false,
484
+ details: {
485
+ reason: "restricted_internal_url",
486
+ url: options.url,
487
+ url_kind: getRestrictedUrlKind(options.url),
488
+ rationale: "Chrome restricts extension automation on internal browser surfaces (for example chrome:// and chrome-extension://).",
489
+ action: options.action,
490
+ next_step: alternatives[0],
491
+ alternatives
492
+ }
493
+ };
494
+ };
495
+
496
+ // packages/extension/src/tab-resolution.ts
497
+ var invalidTabIdError = (message) => ({
498
+ code: "INVALID_ARGUMENT",
499
+ message,
500
+ retryable: false
501
+ });
502
+ var readOptionalTabId = (params, message = "tab_id must be a number when provided.") => {
503
+ const tabId = params.tab_id;
504
+ if (tabId !== void 0 && typeof tabId !== "number") {
505
+ return {
506
+ ok: false,
507
+ error: invalidTabIdError(message)
508
+ };
509
+ }
510
+ return {
511
+ ok: true,
512
+ tabId: typeof tabId === "number" ? tabId : void 0
513
+ };
514
+ };
515
+ var readRequiredTabId = (params, message = "tab_id must be a number.") => {
516
+ const tabId = params.tab_id;
517
+ if (typeof tabId !== "number") {
518
+ return {
519
+ ok: false,
520
+ error: invalidTabIdError(message)
521
+ };
522
+ }
523
+ return {
524
+ ok: true,
525
+ tabId
526
+ };
527
+ };
528
+ var resolveOptionalTabId = async (params, deps, message = "tab_id must be a number when provided.") => {
529
+ const parsed = readOptionalTabId(params, message);
530
+ if (!parsed.ok) {
531
+ return parsed;
532
+ }
533
+ return {
534
+ ok: true,
535
+ tabId: parsed.tabId ?? await deps.getDefaultTabId()
536
+ };
537
+ };
538
+ var requireTab = async (tabId, getTab2) => {
539
+ try {
540
+ return {
541
+ ok: true,
542
+ tab: await getTab2(tabId)
543
+ };
544
+ } catch {
545
+ return {
546
+ ok: false,
547
+ error: {
548
+ code: "TAB_NOT_FOUND",
549
+ message: `tab_id ${tabId} was not found.`,
550
+ retryable: false,
551
+ details: { tab_id: tabId }
552
+ }
553
+ };
554
+ }
555
+ };
556
+
557
+ // packages/extension/src/action-permissions.ts
558
+ var GATED_ACTIONS = /* @__PURE__ */ new Set([
559
+ "drive.navigate",
560
+ "drive.go_back",
561
+ "drive.go_forward",
562
+ "drive.click",
563
+ "drive.hover",
564
+ "drive.select",
565
+ "drive.type",
566
+ "drive.fill_form",
567
+ "drive.drag",
568
+ "drive.handle_dialog",
569
+ "drive.key",
570
+ "drive.key_press",
571
+ "drive.scroll",
572
+ "drive.screenshot",
573
+ "drive.wait_for"
574
+ ]);
575
+ var gateDriveAction = async (options) => {
576
+ const { action, params, getDefaultTabId: getDefaultTabId2, getTab: getTab2, permissionPrompts: permissionPrompts2 } = options;
577
+ if (!GATED_ACTIONS.has(action)) {
578
+ return { ok: true, siteKey: null, touchOnSuccess: false };
579
+ }
580
+ let siteKey = null;
581
+ if (action === "drive.navigate") {
582
+ const url = params.url;
583
+ if (typeof url !== "string" || url.length === 0) {
584
+ return { ok: true, siteKey: null, touchOnSuccess: false };
585
+ }
586
+ if (isRestrictedUrl(url)) {
587
+ return {
588
+ ok: false,
589
+ error: buildRestrictedUrlError({
590
+ url,
591
+ operation: "navigate",
592
+ action
593
+ })
594
+ };
595
+ }
596
+ siteKey = siteKeyFromUrl(url);
597
+ if (!siteKey) {
598
+ return {
599
+ ok: false,
600
+ error: {
601
+ code: "INVALID_ARGUMENT",
602
+ message: "Unable to resolve site permission key for url.",
603
+ retryable: false,
604
+ details: { url }
605
+ }
606
+ };
607
+ }
608
+ } else {
609
+ const parsedTabId = readOptionalTabId(params);
610
+ if (!parsedTabId.ok) {
611
+ return { ok: true, siteKey: null, touchOnSuccess: false };
612
+ }
613
+ const resolvedTabId = parsedTabId.tabId ?? await getDefaultTabId2();
614
+ const tab = await getTab2(resolvedTabId);
615
+ const url = tab.url;
616
+ if (typeof url !== "string" || url.length === 0) {
617
+ return {
618
+ ok: false,
619
+ error: {
620
+ code: "FAILED_PRECONDITION",
621
+ message: "Active tab URL is unavailable for permission gating.",
622
+ retryable: false,
623
+ details: { tab_id: resolvedTabId }
624
+ }
625
+ };
626
+ }
627
+ if (isRestrictedUrl(url)) {
628
+ return {
629
+ ok: false,
630
+ error: buildRestrictedUrlError({
631
+ url,
632
+ operation: action === "drive.screenshot" ? "screenshot" : "action",
633
+ action
634
+ })
635
+ };
636
+ }
637
+ siteKey = siteKeyFromUrl(url);
638
+ if (!siteKey) {
639
+ return {
640
+ ok: false,
641
+ error: {
642
+ code: "FAILED_PRECONDITION",
643
+ message: "Unable to resolve site permission key for active tab.",
644
+ retryable: false,
645
+ details: { url, tab_id: resolvedTabId }
646
+ }
647
+ };
648
+ }
649
+ }
650
+ if (await readSitePermissionsMode() === "bypass") {
651
+ return { ok: true, siteKey, touchOnSuccess: false };
652
+ }
653
+ if (await isSiteAllowed(siteKey)) {
654
+ return { ok: true, siteKey, touchOnSuccess: true };
655
+ }
656
+ const decision = await permissionPrompts2.requestPermission({
657
+ siteKey,
658
+ action
659
+ });
660
+ if (decision.kind === "timed_out") {
661
+ return {
662
+ ok: false,
663
+ error: {
664
+ code: "PERMISSION_PROMPT_TIMEOUT",
665
+ message: `Permission prompt timed out for ${siteKey}.`,
666
+ retryable: true,
667
+ details: {
668
+ reason: "prompt_timed_out",
669
+ site: siteKey,
670
+ action,
671
+ wait_ms: decision.waitMs
672
+ }
673
+ }
674
+ };
675
+ }
676
+ if (decision.kind === "deny") {
677
+ return {
678
+ ok: false,
679
+ error: {
680
+ code: "PERMISSION_DENIED",
681
+ message: `User denied Browser Bridge permission for ${siteKey}.`,
682
+ retryable: false,
683
+ details: {
684
+ reason: "user_denied",
685
+ site: siteKey,
686
+ action,
687
+ next_step: "Ask the user to approve the permission prompt (Allow/Always allow) or allow the site in the extension options page, then retry the command."
688
+ }
689
+ }
690
+ };
691
+ }
692
+ if (decision.kind === "allow_always") {
693
+ await allowSiteAlways(siteKey);
694
+ return { ok: true, siteKey, touchOnSuccess: true };
695
+ }
696
+ return { ok: true, siteKey, touchOnSuccess: false };
697
+ };
698
+
699
+ // packages/extension/src/popup-trigger-state.ts
700
+ var popupTriggerStateChanged = (before, after) => {
701
+ if (!before || !after) {
702
+ return before !== after;
703
+ }
704
+ return before.ariaExpanded !== after.ariaExpanded || before.dataState !== after.dataState || before.open !== after.open;
705
+ };
706
+ var coercePopupTriggerState = (value) => {
707
+ if (!value || typeof value !== "object") {
708
+ return void 0;
709
+ }
710
+ const record = value;
711
+ if (record.kind !== "popup_trigger") {
712
+ return void 0;
713
+ }
714
+ const ariaHasPopup = typeof record.ariaHasPopup === "string" ? record.ariaHasPopup : void 0;
715
+ const ariaExpanded = typeof record.ariaExpanded === "string" ? record.ariaExpanded : void 0;
716
+ const dataState = typeof record.dataState === "string" ? record.dataState : void 0;
717
+ const open = typeof record.open === "boolean" ? record.open : void 0;
718
+ return {
719
+ kind: "popup_trigger",
720
+ ...ariaHasPopup ? { ariaHasPopup } : {},
721
+ ...ariaExpanded !== void 0 ? { ariaExpanded } : {},
722
+ ...dataState !== void 0 ? { dataState } : {},
723
+ ...open !== void 0 ? { open } : {}
724
+ };
725
+ };
726
+
727
+ // packages/extension/src/popup-click-verification.ts
728
+ var POPUP_TRIGGER_CLICK_SETTLE_MS = 50;
729
+ var POPUP_TRIGGER_RECHECK_MS = 125;
730
+ var readPopupTriggerAfterClick = async (options) => {
731
+ const after = await options.resolveLocatorPoint(options.locator);
732
+ if (!after.ok) {
733
+ if (shouldTreatPostClickReadErrorAsSuccess(after.error)) {
734
+ return {
735
+ ok: false,
736
+ error: {
737
+ code: "NOT_FOUND",
738
+ message: "Popup trigger disappeared after click.",
739
+ retryable: false,
740
+ details: { reason: "popup_trigger_disappeared" }
741
+ }
742
+ };
743
+ }
744
+ return after;
745
+ }
746
+ return after;
747
+ };
748
+ var shouldTreatPostClickReadErrorAsSuccess = (error) => {
749
+ return error.code === "LOCATOR_NOT_FOUND" || error.code === "NOT_FOUND" || error.retryable === true && (error.code === "TIMEOUT" || error.details?.reason === "transient_tab_channel_error");
750
+ };
751
+ var verifyPopupTriggerClick = async (options) => {
752
+ if (options.prepareTarget) {
753
+ await options.prepareTarget();
754
+ }
755
+ try {
756
+ await options.dispatchCdpClick(
757
+ options.point.x,
758
+ options.point.y,
759
+ options.clickCount
760
+ );
761
+ } catch (error) {
762
+ return { ok: false, error: options.mapDispatchError(error) };
763
+ }
764
+ await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
765
+ const after = await readPopupTriggerAfterClick(options);
766
+ if (!after.ok) {
767
+ if (after.error.details?.reason === "popup_trigger_disappeared") {
768
+ return { ok: true };
769
+ }
770
+ return after;
771
+ }
772
+ if (popupTriggerStateChanged(
773
+ options.point.targetState ?? null,
774
+ after.point.targetState ?? null
775
+ )) {
776
+ return { ok: true };
777
+ }
778
+ if (options.clickCount === 1) {
779
+ await options.delayMs(POPUP_TRIGGER_RECHECK_MS);
780
+ const lateAfter = await readPopupTriggerAfterClick(options);
781
+ if (!lateAfter.ok) {
782
+ if (lateAfter.error.details?.reason === "popup_trigger_disappeared") {
783
+ return { ok: true };
784
+ }
785
+ return lateAfter;
786
+ }
787
+ if (popupTriggerStateChanged(
788
+ options.point.targetState ?? null,
789
+ lateAfter.point.targetState ?? null
790
+ )) {
791
+ return { ok: true };
792
+ }
793
+ try {
794
+ await options.dispatchCdpClick(
795
+ options.point.x,
796
+ options.point.y,
797
+ options.clickCount
798
+ );
799
+ } catch (error) {
800
+ return { ok: false, error: options.mapDispatchError(error) };
801
+ }
802
+ await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
803
+ const retryAfter = await readPopupTriggerAfterClick(options);
804
+ if (!retryAfter.ok) {
805
+ if (retryAfter.error.details?.reason === "popup_trigger_disappeared") {
806
+ return { ok: true };
807
+ }
808
+ return retryAfter;
809
+ }
810
+ if (popupTriggerStateChanged(
811
+ options.point.targetState ?? null,
812
+ retryAfter.point.targetState ?? null
813
+ )) {
814
+ return { ok: true };
815
+ }
816
+ }
817
+ return {
818
+ ok: false,
819
+ error: {
820
+ code: "FAILED_PRECONDITION",
821
+ message: "Click focused the popup trigger but did not change its open state.",
822
+ retryable: false,
823
+ details: {
824
+ reason: "click_state_unchanged",
825
+ control: "popup_trigger",
826
+ aria_haspopup: options.point.targetState?.ariaHasPopup,
827
+ aria_expanded_before: options.point.targetState?.ariaExpanded,
828
+ aria_expanded_after: after.point.targetState?.ariaExpanded,
829
+ data_state_before: options.point.targetState?.dataState,
830
+ data_state_after: after.point.targetState?.dataState
831
+ }
832
+ }
833
+ };
834
+ };
835
+
439
836
  // packages/extension/src/drive-reliability.ts
440
837
  var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
441
838
  "receiving end does not exist",
@@ -462,6 +859,15 @@ var isTransientTabChannelError = (message) => {
462
859
  (pattern) => normalized.includes(pattern)
463
860
  );
464
861
  };
862
+ var shouldRetryTabChannelFailure = (action, error) => {
863
+ if (!error || typeof error !== "object") {
864
+ return false;
865
+ }
866
+ if (isTransientTabChannelError(error.message)) {
867
+ return true;
868
+ }
869
+ return action === "drive.wait_for" && error.code === "TIMEOUT" && error.retryable === true && typeof error.message === "string" && error.message.includes("Timed out waiting for content response");
870
+ };
465
871
  var getTabChannelRetryDelayMs = (attempt) => {
466
872
  if (!Number.isInteger(attempt) || attempt < 1) {
467
873
  return void 0;
@@ -558,9 +964,228 @@ var ConnectionStateTracker = class {
558
964
  }
559
965
  };
560
966
 
561
- // packages/extension/src/background.ts
967
+ // packages/extension/src/debugger-dispatch.ts
968
+ var dispatchDebuggerRequest = async (message, deps, responders) => {
969
+ const { respondAck, respondError } = responders;
970
+ try {
971
+ switch (message.action) {
972
+ case "debugger.attach": {
973
+ const parsedTabId = readRequiredTabId(
974
+ message.params ?? {}
975
+ );
976
+ if (!parsedTabId.ok) {
977
+ respondError(parsedTabId.error);
978
+ return;
979
+ }
980
+ const error = await deps.ensureDebuggerAttached(parsedTabId.tabId);
981
+ if (error) {
982
+ respondError(error);
983
+ return;
984
+ }
985
+ respondAck({ ok: true });
986
+ return;
987
+ }
988
+ case "debugger.detach": {
989
+ const parsedTabId = readRequiredTabId(
990
+ message.params ?? {}
991
+ );
992
+ if (!parsedTabId.ok) {
993
+ respondError(parsedTabId.error);
994
+ return;
995
+ }
996
+ const error = await deps.detachDebugger(parsedTabId.tabId);
997
+ if (error) {
998
+ respondError(error);
999
+ return;
1000
+ }
1001
+ respondAck({ ok: true });
1002
+ return;
1003
+ }
1004
+ case "debugger.command": {
1005
+ const params = message.params ?? {};
1006
+ const parsedTabId = readRequiredTabId(
1007
+ params
1008
+ );
1009
+ if (!parsedTabId.ok) {
1010
+ respondError(parsedTabId.error);
1011
+ return;
1012
+ }
1013
+ if (typeof params.method !== "string" || params.method.length === 0) {
1014
+ respondError({
1015
+ code: "INVALID_ARGUMENT",
1016
+ message: "method must be a non-empty string.",
1017
+ retryable: false
1018
+ });
1019
+ return;
1020
+ }
1021
+ const session = deps.getSession(parsedTabId.tabId);
1022
+ if (session?.attachPromise) {
1023
+ try {
1024
+ await session.attachPromise;
1025
+ } catch (error) {
1026
+ const info = deps.mapDebuggerErrorMessage(
1027
+ error instanceof Error ? error.message : "Debugger attach failed."
1028
+ );
1029
+ deps.clearDebuggerSession(parsedTabId.tabId);
1030
+ respondError(info);
1031
+ return;
1032
+ }
1033
+ }
1034
+ const attachedSession = deps.getSession(parsedTabId.tabId);
1035
+ if (!attachedSession?.attached) {
1036
+ respondError({
1037
+ code: "FAILED_PRECONDITION",
1038
+ message: "Debugger is not attached to the requested tab.",
1039
+ retryable: false
1040
+ });
1041
+ return;
1042
+ }
1043
+ try {
1044
+ const result = await deps.sendDebuggerCommand(
1045
+ parsedTabId.tabId,
1046
+ params.method,
1047
+ params.params,
1048
+ deps.debuggerCommandTimeoutMs
1049
+ );
1050
+ deps.touchDebuggerSession(parsedTabId.tabId);
1051
+ respondAck(result);
1052
+ } catch (error) {
1053
+ const info = deps.mapDebuggerErrorMessage(
1054
+ error instanceof Error ? error.message : "Debugger command failed."
1055
+ );
1056
+ respondError(info);
1057
+ }
1058
+ return;
1059
+ }
1060
+ default:
1061
+ respondError({
1062
+ code: "NOT_IMPLEMENTED",
1063
+ message: `${message.action} not implemented in extension yet.`,
1064
+ retryable: false
1065
+ });
1066
+ }
1067
+ } catch (error) {
1068
+ const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
1069
+ respondError({
1070
+ code: "INSPECT_UNAVAILABLE",
1071
+ message: messageText,
1072
+ retryable: false
1073
+ });
1074
+ }
1075
+ };
1076
+
1077
+ // packages/extension/src/screenshot-errors.ts
1078
+ var isCaptureVisibleTabRateLimitedMessage = (message) => {
1079
+ const normalized = message.toLowerCase();
1080
+ const hasCaptureSignal = normalized.includes("capturevisibletab");
1081
+ const hasRateSignal = normalized.includes("max_capture_visible_tab_calls_per_second") || normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
1082
+ return hasCaptureSignal && hasRateSignal;
1083
+ };
1084
+ var isCaptureVisibleTabPermissionMessage = (message) => {
1085
+ const normalized = message.toLowerCase();
1086
+ const hasCaptureSignal = normalized.includes("capturevisibletab");
1087
+ const hasPermissionSignal = normalized.includes("permission is required") || normalized.includes("requires permission") || normalized.includes("requires either");
1088
+ const hasPermissionTarget = normalized.includes("all_urls") || normalized.includes("activetab");
1089
+ return hasCaptureSignal && hasPermissionSignal && hasPermissionTarget;
1090
+ };
1091
+ var isCaptureVisibleTabRateLimitedError = (error) => error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
1092
+ var isCaptureVisibleTabPermissionError = (error) => error instanceof Error && isCaptureVisibleTabPermissionMessage(error.message);
1093
+ var mapScreenshotCaptureError = (error, fallbackMessage) => {
1094
+ const message = error instanceof Error && error.message ? error.message : fallbackMessage;
1095
+ if (isCaptureVisibleTabRateLimitedError(error)) {
1096
+ return {
1097
+ code: "RATE_LIMITED",
1098
+ message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
1099
+ retryable: true,
1100
+ details: {
1101
+ reason: "capture_visible_tab_rate_limited",
1102
+ original_message: message
1103
+ }
1104
+ };
1105
+ }
1106
+ if (isCaptureVisibleTabPermissionError(error)) {
1107
+ return {
1108
+ code: "PERMISSION_REQUIRED",
1109
+ message: "Screenshot capture requires captureVisibleTab permission (<all_urls> or activeTab).",
1110
+ retryable: false,
1111
+ details: {
1112
+ reason: "capture_visible_tab_permission_required",
1113
+ required_any_of: ["<all_urls>", "activeTab"],
1114
+ next_step: "Reload the Browser Bridge extension in chrome://extensions and retry screenshot capture.",
1115
+ original_message: message
1116
+ }
1117
+ };
1118
+ }
1119
+ return {
1120
+ code: "ARTIFACT_IO_ERROR",
1121
+ message,
1122
+ retryable: false
1123
+ };
1124
+ };
1125
+
1126
+ // packages/extension/src/core-endpoint-config.ts
1127
+ var DEFAULT_CORE_HOST = "127.0.0.1";
562
1128
  var DEFAULT_CORE_PORT = 3210;
563
- var CORE_PORT_KEY = "corePort";
1129
+ var LEGACY_CORE_PORT_KEY = "corePort";
1130
+ var hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1131
+ var readCoreEndpointConfig = async () => ({
1132
+ host: DEFAULT_CORE_HOST,
1133
+ port: DEFAULT_CORE_PORT,
1134
+ portSource: "default"
1135
+ });
1136
+ var clearLegacyCorePort = async (storage) => {
1137
+ return await new Promise((resolve) => {
1138
+ storage.get([LEGACY_CORE_PORT_KEY], (items) => {
1139
+ if (!hasOwn(items, LEGACY_CORE_PORT_KEY)) {
1140
+ resolve(false);
1141
+ return;
1142
+ }
1143
+ storage.remove([LEGACY_CORE_PORT_KEY], () => resolve(true));
1144
+ });
1145
+ });
1146
+ };
1147
+
1148
+ // packages/extension/src/tab-activation.ts
1149
+ var resolveTabActivationOutcome = ({
1150
+ tabId,
1151
+ windowId,
1152
+ activated,
1153
+ focusErrorMessage,
1154
+ windowFocused
1155
+ }) => {
1156
+ if (!activated) {
1157
+ return {
1158
+ ok: false,
1159
+ error: {
1160
+ code: "FAILED_PRECONDITION",
1161
+ message: `Failed to activate tab_id ${tabId}.`,
1162
+ retryable: true,
1163
+ details: { tab_id: tabId }
1164
+ }
1165
+ };
1166
+ }
1167
+ const warnings = [];
1168
+ if (typeof windowId === "number") {
1169
+ if (focusErrorMessage) {
1170
+ warnings.push(
1171
+ `Activated tab_id ${tabId}, but failed to focus window_id ${windowId}: ${focusErrorMessage}`
1172
+ );
1173
+ } else if (windowFocused === false) {
1174
+ warnings.push(
1175
+ `Activated tab_id ${tabId}, but window_id ${windowId} did not report focused state.`
1176
+ );
1177
+ }
1178
+ }
1179
+ return {
1180
+ ok: true,
1181
+ result: {
1182
+ ok: true,
1183
+ ...warnings.length > 0 ? { warnings } : {}
1184
+ }
1185
+ };
1186
+ };
1187
+
1188
+ // packages/extension/src/background.ts
564
1189
  var CORE_WS_PATH = "/drive";
565
1190
  var CORE_HEALTH_PATH = "/health";
566
1191
  var CORE_HEALTH_TIMEOUT_MS = 1200;
@@ -598,6 +1223,7 @@ var BASE_NEGOTIATED_CAPABILITIES = Object.freeze({
598
1223
  "drive.tab_list": true,
599
1224
  "drive.tab_activate": true,
600
1225
  "drive.tab_close": true,
1226
+ "drive.set_debugger_capability": true,
601
1227
  "drive.ping": true
602
1228
  });
603
1229
  var DEBUGGER_CAPABILITY_ACTIONS = [
@@ -605,26 +1231,15 @@ var DEBUGGER_CAPABILITY_ACTIONS = [
605
1231
  "debugger.detach",
606
1232
  "debugger.command"
607
1233
  ];
608
- var buildNegotiatedCapabilities = (debuggerCapabilityEnabled) => {
1234
+ var buildNegotiatedCapabilities = () => {
609
1235
  const capabilities = {
610
1236
  ...BASE_NEGOTIATED_CAPABILITIES
611
1237
  };
612
1238
  for (const action of DEBUGGER_CAPABILITY_ACTIONS) {
613
- capabilities[action] = debuggerCapabilityEnabled;
1239
+ capabilities[action] = true;
614
1240
  }
615
1241
  return capabilities;
616
1242
  };
617
- var debuggerCapabilityDisabledError = () => {
618
- return {
619
- code: "ATTACH_DENIED",
620
- message: "Debugger capability is disabled. Enable debugger-based inspect in extension options and retry.",
621
- retryable: false,
622
- details: {
623
- reason: "debugger_capability_disabled",
624
- next_step: "Open Browser Bridge extension options, enable debugger-based inspect, then retry."
625
- }
626
- };
627
- };
628
1243
  var getAgentTabBootstrapUrl = () => {
629
1244
  return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_BOOTSTRAP_PATH) : AGENT_TAB_BOOTSTRAP_PATH;
630
1245
  };
@@ -687,15 +1302,6 @@ var CAPTURE_VISIBLE_TAB_MAX_RETRIES = 3;
687
1302
  var CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS = 500;
688
1303
  var captureVisibleTabQueue = Promise.resolve();
689
1304
  var captureVisibleTabLastCallAt = 0;
690
- var isCaptureVisibleTabRateLimitedMessage = (message) => {
691
- const normalized = message.toLowerCase();
692
- const hasCaptureSignal = normalized.includes("capturevisibletab") || normalized.includes("max_capture_visible_tab_calls_per_second");
693
- const hasRateSignal = normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
694
- return hasCaptureSignal && hasRateSignal;
695
- };
696
- var isCaptureVisibleTabRateLimitedError = (error) => {
697
- return error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
698
- };
699
1305
  var randomJitterMs = (maxMs) => {
700
1306
  return Math.floor(Math.random() * Math.max(1, maxMs));
701
1307
  };
@@ -737,25 +1343,6 @@ var captureVisibleTabWithThrottle = async (windowId) => {
737
1343
  throw new Error("captureVisibleTab failed unexpectedly.");
738
1344
  });
739
1345
  };
740
- var mapScreenshotCaptureError = (error, fallbackMessage) => {
741
- const message = error instanceof Error && error.message ? error.message : fallbackMessage;
742
- if (isCaptureVisibleTabRateLimitedError(error)) {
743
- return {
744
- code: "RATE_LIMITED",
745
- message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
746
- retryable: true,
747
- details: {
748
- reason: "capture_visible_tab_rate_limited",
749
- original_message: message
750
- }
751
- };
752
- }
753
- return {
754
- code: "ARTIFACT_IO_ERROR",
755
- message,
756
- retryable: false
757
- };
758
- };
759
1346
  var parseDataUrl = (dataUrl) => {
760
1347
  const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
761
1348
  if (!match) {
@@ -801,40 +1388,6 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
801
1388
  bitmap.close();
802
1389
  }
803
1390
  };
804
- var readCoreEndpointConfig = async () => {
805
- return await new Promise((resolve) => {
806
- chrome.storage.local.get(
807
- [CORE_PORT_KEY],
808
- (result) => {
809
- const raw = result?.[CORE_PORT_KEY];
810
- if (typeof raw === "number" && Number.isFinite(raw)) {
811
- resolve({
812
- host: "127.0.0.1",
813
- port: raw,
814
- portSource: "storage"
815
- });
816
- return;
817
- }
818
- if (typeof raw === "string") {
819
- const parsed = Number(raw);
820
- if (Number.isFinite(parsed)) {
821
- resolve({
822
- host: "127.0.0.1",
823
- port: parsed,
824
- portSource: "storage"
825
- });
826
- return;
827
- }
828
- }
829
- resolve({
830
- host: "127.0.0.1",
831
- port: DEFAULT_CORE_PORT,
832
- portSource: "default"
833
- });
834
- }
835
- );
836
- });
837
- };
838
1391
  var readDebuggerIdleTimeoutMs = async () => {
839
1392
  return await new Promise((resolve) => {
840
1393
  chrome.storage.local.get(
@@ -857,36 +1410,6 @@ var readDebuggerIdleTimeoutMs = async () => {
857
1410
  );
858
1411
  });
859
1412
  };
860
- var RESTRICTED_URL_PREFIXES = [
861
- "chrome://",
862
- "chrome-extension://",
863
- "chrome-devtools://",
864
- "devtools://",
865
- "edge://",
866
- "brave://",
867
- "view-source:"
868
- ];
869
- var isRestrictedUrl = (url) => {
870
- if (!url || typeof url !== "string") {
871
- return false;
872
- }
873
- const lowered = url.toLowerCase();
874
- if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
875
- return true;
876
- }
877
- try {
878
- const parsed = new URL(url);
879
- if (parsed.hostname === "chromewebstore.google.com") {
880
- return true;
881
- }
882
- if (parsed.hostname === "chrome.google.com") {
883
- return parsed.pathname.startsWith("/webstore");
884
- }
885
- } catch (error) {
886
- console.debug("Ignoring invalid URL in restriction check.", error);
887
- }
888
- return false;
889
- };
890
1413
  var mapDebuggerErrorMessage = (message, fallbackCode = "INSPECT_UNAVAILABLE") => {
891
1414
  const normalized = message.toLowerCase();
892
1415
  if (normalized.includes("already attached") || normalized.includes("another debugger") || normalized.includes("attached to this target")) {
@@ -943,6 +1466,25 @@ var buildTabInfo = (tab) => {
943
1466
  last_active_at: ensureLastActiveAt(tabId)
944
1467
  };
945
1468
  };
1469
+ var tabRecencyScore = (tab) => {
1470
+ const parsed = Date.parse(tab.last_active_at);
1471
+ return Number.isFinite(parsed) ? parsed : -Infinity;
1472
+ };
1473
+ var compareTabsForReport = (a, b) => {
1474
+ const aActive = a.active === true ? 1 : 0;
1475
+ const bActive = b.active === true ? 1 : 0;
1476
+ if (aActive !== bActive) {
1477
+ return bActive - aActive;
1478
+ }
1479
+ const recencyDelta = tabRecencyScore(b) - tabRecencyScore(a);
1480
+ if (recencyDelta !== 0) {
1481
+ return recencyDelta;
1482
+ }
1483
+ if (a.window_id !== b.window_id) {
1484
+ return a.window_id - b.window_id;
1485
+ }
1486
+ return a.tab_id - b.tab_id;
1487
+ };
946
1488
  var queryTabs = async () => {
947
1489
  const tabs = await wrapChromeCallback(
948
1490
  (callback) => chrome.tabs.query({}, callback)
@@ -954,6 +1496,7 @@ var queryTabs = async () => {
954
1496
  result.push(info);
955
1497
  }
956
1498
  }
1499
+ result.sort(compareTabsForReport);
957
1500
  return result;
958
1501
  };
959
1502
  var getTab = async (tabId) => {
@@ -961,6 +1504,21 @@ var getTab = async (tabId) => {
961
1504
  (callback) => chrome.tabs.get(tabId, callback)
962
1505
  );
963
1506
  };
1507
+ var getWindow = async (windowId) => {
1508
+ return await wrapChromeCallback(
1509
+ (callback) => chrome.windows.get(windowId, callback)
1510
+ );
1511
+ };
1512
+ var withResolvedTabTarget = async (tabId, result) => {
1513
+ const payload = result && typeof result === "object" && !Array.isArray(result) ? { ...result } : {};
1514
+ const tab = await getTab(tabId).catch(() => void 0);
1515
+ const windowId = tab && typeof tab.windowId === "number" ? tab.windowId : void 0;
1516
+ return {
1517
+ ...payload,
1518
+ tab_id: tabId,
1519
+ ...typeof windowId === "number" ? { window_id: windowId } : {}
1520
+ };
1521
+ };
964
1522
  var getActiveTabId = async () => {
965
1523
  const tabs = await wrapChromeCallback(
966
1524
  (callback) => chrome.tabs.query({ active: true, lastFocusedWindow: true }, callback)
@@ -1199,7 +1757,7 @@ var sendToTab = async (tabId, action, params, options) => {
1199
1757
  if (result.ok) {
1200
1758
  return result;
1201
1759
  }
1202
- if (!isTransientTabChannelError(result.error?.message)) {
1760
+ if (!shouldRetryTabChannelFailure(action, result.error)) {
1203
1761
  return result;
1204
1762
  }
1205
1763
  const retryDelayMs = getTabChannelRetryDelayMs(attempt);
@@ -1480,7 +2038,6 @@ var DriveSocket = class {
1480
2038
  async sendHello() {
1481
2039
  const manifest = chrome.runtime.getManifest();
1482
2040
  const endpoint = await readCoreEndpointConfig();
1483
- const debuggerCapabilityEnabled = await readDebuggerCapabilityEnabled();
1484
2041
  let tabs = [];
1485
2042
  try {
1486
2043
  tabs = await queryTabs();
@@ -1489,9 +2046,10 @@ var DriveSocket = class {
1489
2046
  tabs = [];
1490
2047
  }
1491
2048
  const params = {
2049
+ extension_id: chrome.runtime.id,
1492
2050
  version: manifest.version,
1493
2051
  protocol_version: DRIVE_WS_PROTOCOL_VERSION,
1494
- capabilities: buildNegotiatedCapabilities(debuggerCapabilityEnabled),
2052
+ capabilities: buildNegotiatedCapabilities(),
1495
2053
  core_host: endpoint.host,
1496
2054
  core_port: endpoint.port,
1497
2055
  core_port_source: endpoint.portSource,
@@ -1567,16 +2125,10 @@ var DriveSocket = class {
1567
2125
  }
1568
2126
  }
1569
2127
  async refreshDebuggerCapabilityState() {
1570
- const enabled = await readDebuggerCapabilityEnabled();
1571
- if (!enabled) {
1572
- await this.detachAllDebuggerSessions();
1573
- }
1574
2128
  this.refreshCapabilities();
1575
2129
  }
1576
2130
  async handleDebuggerCapabilityChange(enabled) {
1577
- if (!enabled) {
1578
- await this.detachAllDebuggerSessions();
1579
- }
2131
+ void enabled;
1580
2132
  this.refreshCapabilities();
1581
2133
  }
1582
2134
  async handleRequest(message) {
@@ -1617,15 +2169,6 @@ var DriveSocket = class {
1617
2169
  return;
1618
2170
  }
1619
2171
  if (message.action.startsWith("debugger.")) {
1620
- if (!await readDebuggerCapabilityEnabled()) {
1621
- this.sendMessage({
1622
- id: message.id,
1623
- action: message.action,
1624
- status: "error",
1625
- error: sanitizeDriveErrorInfo(debuggerCapabilityDisabledError())
1626
- });
1627
- return;
1628
- }
1629
2172
  await this.handleDebuggerRequest(message);
1630
2173
  return;
1631
2174
  }
@@ -1633,162 +2176,58 @@ var DriveSocket = class {
1633
2176
  return;
1634
2177
  }
1635
2178
  driveMessage = message;
1636
- const gatedActions = /* @__PURE__ */ new Set([
1637
- "drive.navigate",
1638
- "drive.go_back",
1639
- "drive.go_forward",
1640
- "drive.click",
1641
- "drive.hover",
1642
- "drive.select",
1643
- "drive.type",
1644
- "drive.fill_form",
1645
- "drive.drag",
1646
- "drive.handle_dialog",
1647
- "drive.key",
1648
- "drive.key_press",
1649
- "drive.scroll",
1650
- "drive.screenshot",
1651
- "drive.wait_for"
1652
- ]);
1653
- const gateDriveAction = async () => {
1654
- const action = message.action;
1655
- if (!gatedActions.has(action)) {
1656
- return { ok: true, siteKey: null, touchOnSuccess: false };
1657
- }
1658
- const params = message.params ?? {};
1659
- let siteKey = null;
1660
- if (action === "drive.navigate") {
1661
- const url = params.url;
1662
- if (typeof url !== "string" || url.length === 0) {
1663
- return { ok: true, siteKey: null, touchOnSuccess: false };
1664
- }
1665
- if (isRestrictedUrl(url)) {
1666
- return {
1667
- ok: false,
1668
- error: {
1669
- code: "NOT_SUPPORTED",
1670
- message: "Navigation is not supported for this URL.",
1671
- retryable: false,
1672
- details: { url }
1673
- }
1674
- };
1675
- }
1676
- siteKey = siteKeyFromUrl(url);
1677
- if (!siteKey) {
1678
- return {
1679
- ok: false,
1680
- error: {
1681
- code: "INVALID_ARGUMENT",
1682
- message: "Unable to resolve site permission key for url.",
1683
- retryable: false,
1684
- details: { url }
1685
- }
1686
- };
1687
- }
1688
- } else {
1689
- const tabId = params.tab_id;
1690
- if (tabId !== void 0 && typeof tabId !== "number") {
1691
- return { ok: true, siteKey: null, touchOnSuccess: false };
1692
- }
1693
- const resolvedTabId = typeof tabId === "number" ? tabId : await getDefaultTabId();
1694
- const tab = await getTab(resolvedTabId);
1695
- const url = tab.url;
1696
- if (typeof url !== "string" || url.length === 0) {
1697
- return {
1698
- ok: false,
1699
- error: {
1700
- code: "FAILED_PRECONDITION",
1701
- message: "Active tab URL is unavailable for permission gating.",
1702
- retryable: false,
1703
- details: { tab_id: resolvedTabId }
1704
- }
1705
- };
1706
- }
1707
- if (isRestrictedUrl(url)) {
1708
- const message2 = action === "drive.screenshot" ? "Screenshots are not supported for this URL." : "This action is not supported for this URL.";
1709
- return {
1710
- ok: false,
1711
- error: {
1712
- code: "NOT_SUPPORTED",
1713
- message: message2,
1714
- retryable: false,
1715
- details: { url }
1716
- }
1717
- };
1718
- }
1719
- siteKey = siteKeyFromUrl(url);
1720
- if (!siteKey) {
1721
- return {
1722
- ok: false,
1723
- error: {
1724
- code: "FAILED_PRECONDITION",
1725
- message: "Unable to resolve site permission key for active tab.",
1726
- retryable: false,
1727
- details: { url, tab_id: resolvedTabId }
1728
- }
1729
- };
1730
- }
1731
- }
1732
- if (await readSitePermissionsMode() === "bypass") {
1733
- return { ok: true, siteKey, touchOnSuccess: false };
1734
- }
1735
- if (await isSiteAllowed(siteKey)) {
1736
- return { ok: true, siteKey, touchOnSuccess: true };
1737
- }
1738
- const decision = await permissionPrompts.requestPermission({
1739
- siteKey,
1740
- action
1741
- });
1742
- if (decision.kind === "timed_out") {
1743
- return {
1744
- ok: false,
1745
- error: {
1746
- code: "PERMISSION_PROMPT_TIMEOUT",
1747
- message: `Permission prompt timed out for ${siteKey}.`,
1748
- retryable: true,
1749
- details: {
1750
- reason: "prompt_timed_out",
1751
- site: siteKey,
1752
- action,
1753
- wait_ms: decision.waitMs
1754
- }
1755
- }
1756
- };
1757
- }
1758
- if (decision.kind === "deny") {
1759
- return {
1760
- ok: false,
1761
- error: {
1762
- code: "PERMISSION_DENIED",
1763
- message: `User denied Browser Bridge permission for ${siteKey}.`,
1764
- retryable: false,
1765
- details: {
1766
- reason: "user_denied",
1767
- site: siteKey,
1768
- action,
1769
- next_step: "Ask the user to approve the permission prompt (Allow/Always allow) or allow the site in the extension options page, then retry the command."
1770
- }
1771
- }
1772
- };
1773
- }
1774
- if (decision.kind === "allow_always") {
1775
- await allowSiteAlways(siteKey);
1776
- return { ok: true, siteKey, touchOnSuccess: true };
1777
- }
1778
- return { ok: true, siteKey, touchOnSuccess: false };
1779
- };
1780
- const gated = await gateDriveAction();
2179
+ const gated = await gateDriveAction({
2180
+ action: message.action,
2181
+ params: message.params ?? {},
2182
+ getDefaultTabId,
2183
+ getTab,
2184
+ permissionPrompts
2185
+ });
1781
2186
  if (!gated.ok) {
1782
2187
  respondError(gated.error);
1783
2188
  return;
1784
2189
  }
1785
2190
  gatedSiteKey = gated.siteKey;
1786
2191
  touchGatedSiteOnSuccess = gated.touchOnSuccess;
2192
+ const resolveActionTabId = async (params) => await resolveOptionalTabId(params, { getDefaultTabId });
1787
2193
  switch (message.action) {
1788
2194
  case "drive.ping": {
1789
2195
  respondOk({ ok: true });
1790
2196
  return;
1791
2197
  }
2198
+ case "drive.set_debugger_capability": {
2199
+ const params = message.params ?? {};
2200
+ const enabled = typeof params.enabled === "boolean" ? params.enabled : true;
2201
+ const expectedExtensionId = params.extension_id;
2202
+ if (expectedExtensionId !== void 0 && typeof expectedExtensionId !== "string") {
2203
+ respondError({
2204
+ code: "INVALID_ARGUMENT",
2205
+ message: "extension_id must be a string when provided.",
2206
+ retryable: false
2207
+ });
2208
+ return;
2209
+ }
2210
+ if (typeof expectedExtensionId === "string" && expectedExtensionId.length > 0 && expectedExtensionId !== chrome.runtime.id) {
2211
+ respondError({
2212
+ code: "FAILED_PRECONDITION",
2213
+ message: "Connected extension id does not match the requested extension.",
2214
+ retryable: false,
2215
+ details: {
2216
+ expected_extension_id: expectedExtensionId,
2217
+ connected_extension_id: chrome.runtime.id
2218
+ }
2219
+ });
2220
+ return;
2221
+ }
2222
+ await writeDebuggerCapabilityEnabled(enabled);
2223
+ await this.refreshDebuggerCapabilityState();
2224
+ respondOk({
2225
+ ok: true,
2226
+ enabled,
2227
+ extension_id: chrome.runtime.id
2228
+ });
2229
+ return;
2230
+ }
1792
2231
  case "drive.navigate": {
1793
2232
  const params = message.params ?? {};
1794
2233
  const url = params.url;
@@ -1800,21 +2239,38 @@ var DriveSocket = class {
1800
2239
  });
1801
2240
  return;
1802
2241
  }
1803
- let tabId = params.tab_id;
1804
- if (tabId !== void 0 && typeof tabId !== "number") {
2242
+ const tabTarget = await resolveActionTabId(params);
2243
+ if (!tabTarget.ok) {
2244
+ respondError(tabTarget.error);
2245
+ return;
2246
+ }
2247
+ const tabId = tabTarget.tabId;
2248
+ const requestedWaitMode = params.wait;
2249
+ if (requestedWaitMode !== void 0 && requestedWaitMode !== "none" && requestedWaitMode !== "domcontentloaded" && requestedWaitMode !== "networkidle") {
1805
2250
  respondError({
1806
2251
  code: "INVALID_ARGUMENT",
1807
- message: "tab_id must be a number when provided.",
1808
- retryable: false
2252
+ message: `Unsupported wait mode: ${String(requestedWaitMode)}.`,
2253
+ retryable: false,
2254
+ details: {
2255
+ field: "wait",
2256
+ supported_wait_modes: [
2257
+ "none",
2258
+ "domcontentloaded",
2259
+ "networkidle"
2260
+ ],
2261
+ mapped_wait_mode: "domcontentloaded"
2262
+ }
1809
2263
  });
1810
2264
  return;
1811
2265
  }
1812
- if (tabId === void 0) {
1813
- tabId = await getDefaultTabId();
1814
- }
1815
- const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
2266
+ const waitMode = requestedWaitMode === "none" ? "none" : "domcontentloaded";
1816
2267
  const domContentLoadedSignal = waitMode === "domcontentloaded" ? waitForDomContentLoaded(tabId, 3e4) : null;
1817
2268
  const warnings = [];
2269
+ if (requestedWaitMode === "networkidle") {
2270
+ warnings.push(
2271
+ "wait=networkidle is mapped to domcontentloaded in this runtime."
2272
+ );
2273
+ }
1818
2274
  await wrapChromeVoid(
1819
2275
  (callback) => chrome.tabs.update(tabId, { url }, () => callback())
1820
2276
  );
@@ -1841,39 +2297,30 @@ var DriveSocket = class {
1841
2297
  if (tabId === agentTabId) {
1842
2298
  void refreshAgentTabBranding(tabId);
1843
2299
  }
1844
- respondOk({
1845
- ok: true,
1846
- ...warnings.length > 0 ? { warnings } : {}
1847
- });
2300
+ respondOk(
2301
+ await withResolvedTabTarget(tabId, {
2302
+ ok: true,
2303
+ ...warnings.length > 0 ? { warnings } : {}
2304
+ })
2305
+ );
1848
2306
  return;
1849
2307
  }
1850
2308
  case "drive.go_back":
1851
2309
  case "drive.go_forward": {
1852
2310
  const params = message.params ?? {};
1853
- let tabId = params.tab_id;
1854
- if (tabId !== void 0 && typeof tabId !== "number") {
1855
- respondError({
1856
- code: "INVALID_ARGUMENT",
1857
- message: "tab_id must be a number when provided.",
1858
- retryable: false
1859
- });
2311
+ const tabTarget = await resolveActionTabId(params);
2312
+ if (!tabTarget.ok) {
2313
+ respondError(tabTarget.error);
1860
2314
  return;
1861
2315
  }
1862
- if (tabId === void 0) {
1863
- tabId = await getDefaultTabId();
1864
- }
2316
+ const tabId = tabTarget.tabId;
1865
2317
  const navigationSignal = waitForHistoryNavigationSignal(
1866
2318
  tabId,
1867
2319
  HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS
1868
2320
  );
1869
- const result = await sendToTab(
1870
- tabId,
1871
- message.action,
1872
- void 0,
1873
- {
1874
- timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
1875
- }
1876
- );
2321
+ const result = await sendToTab(tabId, message.action, void 0, {
2322
+ timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
2323
+ });
1877
2324
  if (!result.ok && result.error.code !== "TIMEOUT") {
1878
2325
  respondError(result.error);
1879
2326
  return;
@@ -1899,9 +2346,11 @@ var DriveSocket = class {
1899
2346
  if (tabId === agentTabId) {
1900
2347
  void refreshAgentTabBranding(tabId);
1901
2348
  }
1902
- respondOk({
1903
- ok: true
1904
- });
2349
+ respondOk(
2350
+ await withResolvedTabTarget(tabId, {
2351
+ ok: true
2352
+ })
2353
+ );
1905
2354
  return;
1906
2355
  }
1907
2356
  case "drive.tab_list": {
@@ -1911,44 +2360,66 @@ var DriveSocket = class {
1911
2360
  return;
1912
2361
  }
1913
2362
  case "drive.tab_activate": {
1914
- const tabId = message.params?.tab_id;
1915
- if (typeof tabId !== "number") {
1916
- respondError({
1917
- code: "INVALID_ARGUMENT",
1918
- message: "tab_id must be a number.",
1919
- retryable: false
1920
- });
2363
+ const parsedTabId = readRequiredTabId(
2364
+ message.params ?? {}
2365
+ );
2366
+ if (!parsedTabId.ok) {
2367
+ respondError(parsedTabId.error);
1921
2368
  return;
1922
2369
  }
1923
- const tab = await getTab(tabId);
2370
+ const tabId = parsedTabId.tabId;
2371
+ const tabLookup = await requireTab(tabId, getTab);
2372
+ if (!tabLookup.ok) {
2373
+ respondError(tabLookup.error);
2374
+ return;
2375
+ }
2376
+ const tab = tabLookup.tab;
1924
2377
  await wrapChromeVoid(
1925
2378
  (callback) => chrome.tabs.update(tabId, { active: true }, () => callback())
1926
2379
  );
1927
2380
  const windowId = tab.windowId;
2381
+ let focusErrorMessage;
1928
2382
  if (typeof windowId === "number") {
1929
- await wrapChromeVoid(
1930
- (callback) => chrome.windows.update(
1931
- windowId,
1932
- { focused: true },
1933
- () => callback()
1934
- )
1935
- );
2383
+ try {
2384
+ await wrapChromeVoid(
2385
+ (callback) => chrome.windows.update(
2386
+ windowId,
2387
+ { focused: true },
2388
+ () => callback()
2389
+ )
2390
+ );
2391
+ } catch (error) {
2392
+ focusErrorMessage = error instanceof Error ? error.message : "Unknown focus error.";
2393
+ }
2394
+ }
2395
+ const activatedTab = await getTab(tabId).catch(() => void 0);
2396
+ const focusedWindow = typeof windowId === "number" ? await getWindow(windowId).catch(() => void 0) : void 0;
2397
+ const windowFocused = focusedWindow && typeof focusedWindow.focused === "boolean" ? focusedWindow.focused : void 0;
2398
+ const outcome = resolveTabActivationOutcome({
2399
+ tabId,
2400
+ windowId: typeof windowId === "number" ? windowId : void 0,
2401
+ activated: Boolean(activatedTab?.active === true),
2402
+ focusErrorMessage,
2403
+ windowFocused
2404
+ });
2405
+ if (!outcome.ok) {
2406
+ respondError(outcome.error);
2407
+ return;
1936
2408
  }
1937
2409
  markTabActive(tabId);
1938
- respondOk({ ok: true });
2410
+ respondOk(outcome.result);
1939
2411
  this.sendTabReport();
1940
2412
  return;
1941
2413
  }
1942
2414
  case "drive.tab_close": {
1943
- const tabId = message.params?.tab_id;
1944
- if (typeof tabId !== "number") {
1945
- respondError({
1946
- code: "INVALID_ARGUMENT",
1947
- message: "tab_id must be a number.",
1948
- retryable: false
1949
- });
2415
+ const parsedTabId = readRequiredTabId(
2416
+ message.params ?? {}
2417
+ );
2418
+ if (!parsedTabId.ok) {
2419
+ respondError(parsedTabId.error);
1950
2420
  return;
1951
2421
  }
2422
+ const tabId = parsedTabId.tabId;
1952
2423
  await wrapChromeVoid(
1953
2424
  (callback) => chrome.tabs.remove(tabId, () => callback())
1954
2425
  );
@@ -1980,18 +2451,12 @@ var DriveSocket = class {
1980
2451
  });
1981
2452
  return;
1982
2453
  }
1983
- let tabId = params.tab_id;
1984
- if (tabId !== void 0 && typeof tabId !== "number") {
1985
- respondError({
1986
- code: "INVALID_ARGUMENT",
1987
- message: "tab_id must be a number when provided.",
1988
- retryable: false
1989
- });
2454
+ const tabTarget = await resolveActionTabId(params);
2455
+ if (!tabTarget.ok) {
2456
+ respondError(tabTarget.error);
1990
2457
  return;
1991
2458
  }
1992
- if (tabId === void 0) {
1993
- tabId = await getDefaultTabId();
1994
- }
2459
+ const tabId = tabTarget.tabId;
1995
2460
  const error = await this.ensureDebuggerAttached(tabId);
1996
2461
  if (error) {
1997
2462
  respondError(error);
@@ -2008,7 +2473,7 @@ var DriveSocket = class {
2008
2473
  DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
2009
2474
  );
2010
2475
  this.touchDebuggerSession(tabId);
2011
- respondOk({ ok: true });
2476
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2012
2477
  } catch (error2) {
2013
2478
  const info = mapDebuggerErrorMessage(
2014
2479
  error2 instanceof Error ? error2.message : "Dialog handling failed."
@@ -2019,18 +2484,12 @@ var DriveSocket = class {
2019
2484
  }
2020
2485
  case "drive.click": {
2021
2486
  const params = message.params ?? {};
2022
- let tabId = params.tab_id;
2023
- if (tabId !== void 0 && typeof tabId !== "number") {
2024
- respondError({
2025
- code: "INVALID_ARGUMENT",
2026
- message: "tab_id must be a number when provided.",
2027
- retryable: false
2028
- });
2487
+ const tabTarget = await resolveActionTabId(params);
2488
+ if (!tabTarget.ok) {
2489
+ respondError(tabTarget.error);
2029
2490
  return;
2030
2491
  }
2031
- if (tabId === void 0) {
2032
- tabId = await getDefaultTabId();
2033
- }
2492
+ const tabId = tabTarget.tabId;
2034
2493
  const clickCount = params.click_count;
2035
2494
  const count = typeof clickCount === "number" && Number.isFinite(clickCount) ? Math.max(1, Math.floor(clickCount)) : 1;
2036
2495
  const error = await this.ensureDebuggerAttached(tabId);
@@ -2046,31 +2505,48 @@ var DriveSocket = class {
2046
2505
  respondError(pointResult.error);
2047
2506
  return;
2048
2507
  }
2049
- const { x, y } = pointResult.point;
2050
- self.setTimeout(() => {
2051
- void this.dispatchCdpClick(tabId, x, y, count).catch(
2052
- (error2) => {
2053
- console.debug("Deferred CDP click failed.", error2);
2054
- }
2508
+ const { x, y, targetState } = pointResult.point;
2509
+ if (targetState) {
2510
+ const verified = await verifyPopupTriggerClick({
2511
+ clickCount: count,
2512
+ locator: params.locator,
2513
+ point: pointResult.point,
2514
+ prepareTarget: async () => await this.focusLocator(tabId, params.locator),
2515
+ resolveLocatorPoint: async (locator) => await this.resolveLocatorPoint(tabId, locator),
2516
+ dispatchCdpClick: async (clickX, clickY, clickCount2) => await this.dispatchCdpClick(tabId, clickX, clickY, clickCount2),
2517
+ mapDispatchError: (error2) => mapDebuggerErrorMessage(
2518
+ error2 instanceof Error ? error2.message : "Click dispatch failed."
2519
+ ),
2520
+ delayMs
2521
+ });
2522
+ if (!verified.ok) {
2523
+ respondError(verified.error);
2524
+ return;
2525
+ }
2526
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2527
+ return;
2528
+ }
2529
+ try {
2530
+ await this.dispatchCdpClick(tabId, x, y, count);
2531
+ } catch (error2) {
2532
+ respondError(
2533
+ mapDebuggerErrorMessage(
2534
+ error2 instanceof Error ? error2.message : "Click dispatch failed."
2535
+ )
2055
2536
  );
2056
- }, 0);
2057
- respondOk({ ok: true });
2537
+ return;
2538
+ }
2539
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2058
2540
  return;
2059
2541
  }
2060
2542
  case "drive.hover": {
2061
2543
  const params = message.params ?? {};
2062
- let tabId = params.tab_id;
2063
- if (tabId !== void 0 && typeof tabId !== "number") {
2064
- respondError({
2065
- code: "INVALID_ARGUMENT",
2066
- message: "tab_id must be a number when provided.",
2067
- retryable: false
2068
- });
2544
+ const tabTarget = await resolveActionTabId(params);
2545
+ if (!tabTarget.ok) {
2546
+ respondError(tabTarget.error);
2069
2547
  return;
2070
2548
  }
2071
- if (tabId === void 0) {
2072
- tabId = await getDefaultTabId();
2073
- }
2549
+ const tabId = tabTarget.tabId;
2074
2550
  const error = await this.ensureDebuggerAttached(tabId);
2075
2551
  if (error) {
2076
2552
  respondError(error);
@@ -2091,15 +2567,17 @@ var DriveSocket = class {
2091
2567
  if (waitMs > 0) {
2092
2568
  await delayMs(waitMs);
2093
2569
  }
2094
- const snapshot = await sendToTab(
2095
- tabId,
2096
- "drive.snapshot_html"
2097
- );
2570
+ const snapshot = await sendToTab(tabId, "drive.snapshot_html");
2098
2571
  if (!snapshot.ok) {
2099
2572
  respondError(snapshot.error);
2100
2573
  return;
2101
2574
  }
2102
- respondOk(snapshot.result ?? { format: "html", snapshot: "" });
2575
+ respondOk(
2576
+ await withResolvedTabTarget(
2577
+ tabId,
2578
+ snapshot.result ?? { format: "html", snapshot: "" }
2579
+ )
2580
+ );
2103
2581
  } catch (error2) {
2104
2582
  const info = mapDebuggerErrorMessage(
2105
2583
  error2 instanceof Error ? error2.message : "Hover dispatch failed."
@@ -2110,35 +2588,23 @@ var DriveSocket = class {
2110
2588
  }
2111
2589
  case "drive.drag": {
2112
2590
  const params = message.params ?? {};
2113
- let tabId = params.tab_id;
2114
- if (tabId !== void 0 && typeof tabId !== "number") {
2115
- respondError({
2116
- code: "INVALID_ARGUMENT",
2117
- message: "tab_id must be a number when provided.",
2118
- retryable: false
2119
- });
2591
+ const tabTarget = await resolveActionTabId(params);
2592
+ if (!tabTarget.ok) {
2593
+ respondError(tabTarget.error);
2120
2594
  return;
2121
2595
  }
2122
- if (tabId === void 0) {
2123
- tabId = await getDefaultTabId();
2124
- }
2596
+ const tabId = tabTarget.tabId;
2125
2597
  const error = await this.ensureDebuggerAttached(tabId);
2126
2598
  if (error) {
2127
2599
  respondError(error);
2128
2600
  return;
2129
2601
  }
2130
- const fromResult = await this.resolveLocatorPoint(
2131
- tabId,
2132
- params.from
2133
- );
2602
+ const fromResult = await this.resolveLocatorPoint(tabId, params.from);
2134
2603
  if (!fromResult.ok) {
2135
2604
  respondError(fromResult.error);
2136
2605
  return;
2137
2606
  }
2138
- const toResult = await this.resolveLocatorPoint(
2139
- tabId,
2140
- params.to
2141
- );
2607
+ const toResult = await this.resolveLocatorPoint(tabId, params.to);
2142
2608
  if (!toResult.ok) {
2143
2609
  respondError(toResult.error);
2144
2610
  return;
@@ -2151,7 +2617,7 @@ var DriveSocket = class {
2151
2617
  toResult.point,
2152
2618
  steps
2153
2619
  );
2154
- respondOk({ ok: true });
2620
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2155
2621
  } catch (error2) {
2156
2622
  const info = mapDebuggerErrorMessage(
2157
2623
  error2 instanceof Error ? error2.message : "Drag dispatch failed."
@@ -2171,30 +2637,20 @@ var DriveSocket = class {
2171
2637
  });
2172
2638
  return;
2173
2639
  }
2174
- let tabId = params.tab_id;
2175
- if (tabId !== void 0 && typeof tabId !== "number") {
2176
- respondError({
2177
- code: "INVALID_ARGUMENT",
2178
- message: "tab_id must be a number when provided.",
2179
- retryable: false
2180
- });
2640
+ const tabTarget = await resolveActionTabId(params);
2641
+ if (!tabTarget.ok) {
2642
+ respondError(tabTarget.error);
2181
2643
  return;
2182
2644
  }
2183
- if (tabId === void 0) {
2184
- tabId = await getDefaultTabId();
2185
- }
2645
+ const tabId = tabTarget.tabId;
2186
2646
  const error = await this.ensureDebuggerAttached(tabId);
2187
2647
  if (error) {
2188
2648
  respondError(error);
2189
2649
  return;
2190
2650
  }
2191
2651
  try {
2192
- await this.dispatchCdpKeyPress(
2193
- tabId,
2194
- key,
2195
- params.modifiers
2196
- );
2197
- respondOk({ ok: true });
2652
+ await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
2653
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2198
2654
  } catch (error2) {
2199
2655
  const info = mapDebuggerErrorMessage(
2200
2656
  error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
@@ -2214,18 +2670,12 @@ var DriveSocket = class {
2214
2670
  });
2215
2671
  return;
2216
2672
  }
2217
- let tabId = params.tab_id;
2218
- if (tabId !== void 0 && typeof tabId !== "number") {
2219
- respondError({
2220
- code: "INVALID_ARGUMENT",
2221
- message: "tab_id must be a number when provided.",
2222
- retryable: false
2223
- });
2673
+ const tabTarget = await resolveActionTabId(params);
2674
+ if (!tabTarget.ok) {
2675
+ respondError(tabTarget.error);
2224
2676
  return;
2225
2677
  }
2226
- if (tabId === void 0) {
2227
- tabId = await getDefaultTabId();
2228
- }
2678
+ const tabId = tabTarget.tabId;
2229
2679
  const count = typeof params.repeat === "number" && Number.isFinite(params.repeat) ? Math.max(1, Math.min(50, Math.floor(params.repeat))) : 1;
2230
2680
  const error = await this.ensureDebuggerAttached(tabId);
2231
2681
  if (error) {
@@ -2234,13 +2684,9 @@ var DriveSocket = class {
2234
2684
  }
2235
2685
  try {
2236
2686
  for (let i = 0; i < count; i += 1) {
2237
- await this.dispatchCdpKeyPress(
2238
- tabId,
2239
- key,
2240
- params.modifiers
2241
- );
2687
+ await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
2242
2688
  }
2243
- respondOk({ ok: true });
2689
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2244
2690
  } catch (error2) {
2245
2691
  const info = mapDebuggerErrorMessage(
2246
2692
  error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
@@ -2260,18 +2706,12 @@ var DriveSocket = class {
2260
2706
  });
2261
2707
  return;
2262
2708
  }
2263
- let tabId = params.tab_id;
2264
- if (tabId !== void 0 && typeof tabId !== "number") {
2265
- respondError({
2266
- code: "INVALID_ARGUMENT",
2267
- message: "tab_id must be a number when provided.",
2268
- retryable: false
2269
- });
2709
+ const tabTarget = await resolveActionTabId(params);
2710
+ if (!tabTarget.ok) {
2711
+ respondError(tabTarget.error);
2270
2712
  return;
2271
2713
  }
2272
- if (tabId === void 0) {
2273
- tabId = await getDefaultTabId();
2274
- }
2714
+ const tabId = tabTarget.tabId;
2275
2715
  const error = await this.ensureDebuggerAttached(tabId);
2276
2716
  if (error) {
2277
2717
  respondError(error);
@@ -2287,23 +2727,17 @@ var DriveSocket = class {
2287
2727
  respondError(result.error);
2288
2728
  return;
2289
2729
  }
2290
- respondOk({ ok: true });
2730
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2291
2731
  return;
2292
2732
  }
2293
2733
  case "drive.select": {
2294
2734
  const params = message.params ?? {};
2295
- let tabId = params.tab_id;
2296
- if (tabId !== void 0 && typeof tabId !== "number") {
2297
- respondError({
2298
- code: "INVALID_ARGUMENT",
2299
- message: "tab_id must be a number when provided.",
2300
- retryable: false
2301
- });
2735
+ const tabTarget = await resolveActionTabId(params);
2736
+ if (!tabTarget.ok) {
2737
+ respondError(tabTarget.error);
2302
2738
  return;
2303
2739
  }
2304
- if (tabId === void 0) {
2305
- tabId = await getDefaultTabId();
2306
- }
2740
+ const tabId = tabTarget.tabId;
2307
2741
  const error = await this.ensureDebuggerAttached(tabId);
2308
2742
  if (error) {
2309
2743
  respondError(error);
@@ -2331,16 +2765,17 @@ var DriveSocket = class {
2331
2765
  respondError(info);
2332
2766
  return;
2333
2767
  }
2334
- const selectResult = await sendToTab(
2335
- tabId,
2336
- "drive.select",
2337
- params
2338
- );
2768
+ const selectResult = await sendToTab(tabId, "drive.select", params);
2339
2769
  if (!selectResult.ok) {
2340
2770
  respondError(selectResult.error);
2341
2771
  return;
2342
2772
  }
2343
- respondOk(selectResult.result ?? { ok: true });
2773
+ respondOk(
2774
+ await withResolvedTabTarget(
2775
+ tabId,
2776
+ selectResult.result ?? { ok: true }
2777
+ )
2778
+ );
2344
2779
  return;
2345
2780
  }
2346
2781
  case "drive.fill_form": {
@@ -2354,18 +2789,12 @@ var DriveSocket = class {
2354
2789
  });
2355
2790
  return;
2356
2791
  }
2357
- let tabId = params.tab_id;
2358
- if (tabId !== void 0 && typeof tabId !== "number") {
2359
- respondError({
2360
- code: "INVALID_ARGUMENT",
2361
- message: "tab_id must be a number when provided.",
2362
- retryable: false
2363
- });
2792
+ const tabTarget = await resolveActionTabId(params);
2793
+ if (!tabTarget.ok) {
2794
+ respondError(tabTarget.error);
2364
2795
  return;
2365
2796
  }
2366
- if (tabId === void 0) {
2367
- tabId = await getDefaultTabId();
2368
- }
2797
+ const tabId = tabTarget.tabId;
2369
2798
  const error = await this.ensureDebuggerAttached(tabId);
2370
2799
  if (error) {
2371
2800
  respondError(error);
@@ -2426,13 +2855,9 @@ var DriveSocket = class {
2426
2855
  filled += 1;
2427
2856
  continue;
2428
2857
  }
2429
- const fallback = await sendToTab(
2430
- tabId,
2431
- "drive.fill_form",
2432
- {
2433
- fields: [field]
2434
- }
2435
- );
2858
+ const fallback = await sendToTab(tabId, "drive.fill_form", {
2859
+ fields: [field]
2860
+ });
2436
2861
  if (!fallback.ok) {
2437
2862
  errors.push(
2438
2863
  `Field ${index} could not be filled: ${fallback.error.message}`
@@ -2451,39 +2876,32 @@ var DriveSocket = class {
2451
2876
  }
2452
2877
  errors.push(`Field ${index} could not be filled.`);
2453
2878
  }
2454
- respondOk({
2455
- filled,
2456
- attempted: fields.length,
2457
- errors: errors.length > 0 ? errors : []
2458
- });
2879
+ respondOk(
2880
+ await withResolvedTabTarget(tabId, {
2881
+ filled,
2882
+ attempted: fields.length,
2883
+ errors: errors.length > 0 ? errors : []
2884
+ })
2885
+ );
2459
2886
  return;
2460
2887
  }
2461
2888
  case "drive.scroll":
2462
2889
  case "drive.wait_for": {
2463
2890
  const params = message.params ?? {};
2464
- let tabId = params.tab_id;
2465
- if (tabId !== void 0 && typeof tabId !== "number") {
2466
- respondError({
2467
- code: "INVALID_ARGUMENT",
2468
- message: "tab_id must be a number when provided.",
2469
- retryable: false
2470
- });
2891
+ const tabTarget = await resolveActionTabId(params);
2892
+ if (!tabTarget.ok) {
2893
+ respondError(tabTarget.error);
2471
2894
  return;
2472
2895
  }
2473
- if (tabId === void 0) {
2474
- tabId = await getDefaultTabId();
2475
- }
2896
+ const tabId = tabTarget.tabId;
2476
2897
  const timeoutMs = message.action === "drive.wait_for" && typeof params.timeout_ms === "number" && Number.isFinite(params.timeout_ms) ? Math.max(1, Math.floor(params.timeout_ms) + 1e3) : void 0;
2477
- const result = await sendToTab(
2478
- tabId,
2479
- message.action,
2480
- params,
2481
- {
2482
- timeoutMs
2483
- }
2484
- );
2898
+ const result = await sendToTab(tabId, message.action, params, {
2899
+ timeoutMs
2900
+ });
2485
2901
  if (result.ok) {
2486
- respondOk(result.result ?? { ok: true });
2902
+ respondOk(
2903
+ await withResolvedTabTarget(tabId, result.result ?? { ok: true })
2904
+ );
2487
2905
  } else {
2488
2906
  respondError(result.error);
2489
2907
  }
@@ -2491,30 +2909,25 @@ var DriveSocket = class {
2491
2909
  }
2492
2910
  case "drive.screenshot": {
2493
2911
  const params = message.params ?? {};
2494
- let tabId = params.tab_id;
2495
- if (tabId !== void 0 && typeof tabId !== "number") {
2496
- respondError({
2497
- code: "INVALID_ARGUMENT",
2498
- message: "tab_id must be a number when provided.",
2499
- retryable: false
2500
- });
2912
+ const tabTarget = await resolveActionTabId(params);
2913
+ if (!tabTarget.ok) {
2914
+ respondError(tabTarget.error);
2501
2915
  return;
2502
2916
  }
2503
- if (tabId === void 0) {
2504
- tabId = await getDefaultTabId();
2505
- }
2917
+ const tabId = tabTarget.tabId;
2506
2918
  const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
2507
2919
  const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
2508
2920
  const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
2509
2921
  const tab = await getTab(tabId);
2510
2922
  const url = tab.url;
2511
2923
  if (typeof url === "string" && isRestrictedUrl(url)) {
2512
- respondError({
2513
- code: "NOT_SUPPORTED",
2514
- message: "Screenshots are not supported for this URL.",
2515
- retryable: false,
2516
- details: { url }
2517
- });
2924
+ respondError(
2925
+ buildRestrictedUrlError({
2926
+ url,
2927
+ operation: "screenshot",
2928
+ action: "drive.screenshot"
2929
+ })
2930
+ );
2518
2931
  return;
2519
2932
  }
2520
2933
  const windowId = tab.windowId;
@@ -2679,7 +3092,7 @@ var DriveSocket = class {
2679
3092
  format,
2680
3093
  quality
2681
3094
  );
2682
- respondOk(rendered);
3095
+ respondOk(await withResolvedTabTarget(tabId, rendered));
2683
3096
  } catch (error) {
2684
3097
  respondError(
2685
3098
  mapScreenshotCaptureError(
@@ -2786,7 +3199,12 @@ var DriveSocket = class {
2786
3199
  srcW2,
2787
3200
  srcH2
2788
3201
  );
2789
- respondOk(await canvasToResult(cropCanvas2));
3202
+ respondOk(
3203
+ await withResolvedTabTarget(
3204
+ tabId,
3205
+ await canvasToResult(cropCanvas2)
3206
+ )
3207
+ );
2790
3208
  } finally {
2791
3209
  bitmap.close();
2792
3210
  }
@@ -2816,7 +3234,12 @@ var DriveSocket = class {
2816
3234
  srcW,
2817
3235
  srcH
2818
3236
  );
2819
- respondOk(await canvasToResult(cropCanvas));
3237
+ respondOk(
3238
+ await withResolvedTabTarget(
3239
+ tabId,
3240
+ await canvasToResult(cropCanvas)
3241
+ )
3242
+ );
2820
3243
  } catch (error) {
2821
3244
  respondError(
2822
3245
  mapScreenshotCaptureError(
@@ -2835,7 +3258,12 @@ var DriveSocket = class {
2835
3258
  try {
2836
3259
  const metaInfo = await getMetaInfo();
2837
3260
  const canvas = await captureFullPageCanvas(metaInfo);
2838
- respondOk(await canvasToResult(canvas));
3261
+ respondOk(
3262
+ await withResolvedTabTarget(
3263
+ tabId,
3264
+ await canvasToResult(canvas)
3265
+ )
3266
+ );
2839
3267
  } catch (error) {
2840
3268
  respondError(
2841
3269
  mapScreenshotCaptureError(
@@ -2973,7 +3401,23 @@ var DriveSocket = class {
2973
3401
  }
2974
3402
  };
2975
3403
  }
2976
- return { ok: true, point: { x, y } };
3404
+ const targetState = coercePopupTriggerState(record.target_state);
3405
+ return {
3406
+ ok: true,
3407
+ point: {
3408
+ x,
3409
+ y,
3410
+ ...targetState ? { targetState } : {}
3411
+ }
3412
+ };
3413
+ }
3414
+ async focusLocator(tabId, locator) {
3415
+ const focused = await sendToTab(tabId, "drive.focus_locator", {
3416
+ locator
3417
+ });
3418
+ if (!focused.ok) {
3419
+ throw new Error(focused.error.message);
3420
+ }
2977
3421
  }
2978
3422
  async performCdpType(tabId, options) {
2979
3423
  const targetPoint = await sendToTab(tabId, "drive.type_target_point", {
@@ -3163,132 +3607,25 @@ var DriveSocket = class {
3163
3607
  error: sanitizeDriveErrorInfo(error)
3164
3608
  });
3165
3609
  };
3166
- try {
3167
- switch (message.action) {
3168
- case "debugger.attach": {
3169
- const params = message.params ?? {};
3170
- const tabId = params.tab_id;
3171
- if (typeof tabId !== "number") {
3172
- respondError({
3173
- code: "INVALID_ARGUMENT",
3174
- message: "tab_id must be a number.",
3175
- retryable: false
3176
- });
3177
- return;
3178
- }
3179
- const error = await this.ensureDebuggerAttached(tabId);
3180
- if (error) {
3181
- respondError(error);
3182
- return;
3183
- }
3184
- respondAck({ ok: true });
3185
- return;
3186
- }
3187
- case "debugger.detach": {
3188
- const params = message.params ?? {};
3189
- const tabId = params.tab_id;
3190
- if (typeof tabId !== "number") {
3191
- respondError({
3192
- code: "INVALID_ARGUMENT",
3193
- message: "tab_id must be a number.",
3194
- retryable: false
3195
- });
3196
- return;
3197
- }
3198
- const error = await this.detachDebugger(tabId);
3199
- if (error) {
3200
- respondError(error);
3201
- return;
3202
- }
3203
- respondAck({ ok: true });
3204
- return;
3205
- }
3206
- case "debugger.command": {
3207
- const params = message.params ?? {};
3208
- const tabId = params.tab_id;
3209
- if (typeof tabId !== "number") {
3210
- respondError({
3211
- code: "INVALID_ARGUMENT",
3212
- message: "tab_id must be a number.",
3213
- retryable: false
3214
- });
3215
- return;
3216
- }
3217
- if (typeof params.method !== "string" || params.method.length === 0) {
3218
- respondError({
3219
- code: "INVALID_ARGUMENT",
3220
- message: "method must be a non-empty string.",
3221
- retryable: false
3222
- });
3223
- return;
3224
- }
3225
- const session = this.debuggerSessions.get(tabId);
3226
- if (session?.attachPromise) {
3227
- try {
3228
- await session.attachPromise;
3229
- } catch (error) {
3230
- const info = mapDebuggerErrorMessage(
3231
- error instanceof Error ? error.message : "Debugger attach failed."
3232
- );
3233
- this.clearDebuggerSession(tabId);
3234
- respondError(info);
3235
- return;
3236
- }
3237
- }
3238
- const attachedSession = this.debuggerSessions.get(tabId);
3239
- if (!attachedSession?.attached) {
3240
- respondError({
3241
- code: "FAILED_PRECONDITION",
3242
- message: "Debugger is not attached to the requested tab.",
3243
- retryable: false
3244
- });
3245
- return;
3246
- }
3247
- try {
3248
- const result = await this.sendDebuggerCommand(
3249
- tabId,
3250
- params.method,
3251
- params.params,
3252
- DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
3253
- );
3254
- this.touchDebuggerSession(tabId);
3255
- respondAck(result);
3256
- } catch (error) {
3257
- if (error instanceof DebuggerTimeoutError) {
3258
- respondError({
3259
- code: "TIMEOUT",
3260
- message: error.message,
3261
- retryable: true
3262
- });
3263
- return;
3264
- }
3265
- const info = mapDebuggerErrorMessage(
3266
- error instanceof Error ? error.message : "Debugger command failed."
3267
- );
3268
- respondError(info);
3269
- }
3270
- return;
3271
- }
3272
- default:
3273
- respondError({
3274
- code: "NOT_IMPLEMENTED",
3275
- message: `${message.action} not implemented in extension yet.`,
3276
- retryable: false
3277
- });
3610
+ await dispatchDebuggerRequest(
3611
+ message,
3612
+ {
3613
+ getSession: (tabId) => this.debuggerSessions.get(tabId),
3614
+ ensureDebuggerAttached: async (tabId) => await this.ensureDebuggerAttached(tabId),
3615
+ detachDebugger: async (tabId) => await this.detachDebugger(tabId),
3616
+ sendDebuggerCommand: async (tabId, method, params, timeoutMs) => await this.sendDebuggerCommand(tabId, method, params, timeoutMs),
3617
+ touchDebuggerSession: (tabId) => this.touchDebuggerSession(tabId),
3618
+ clearDebuggerSession: (tabId) => this.clearDebuggerSession(tabId),
3619
+ mapDebuggerErrorMessage,
3620
+ debuggerCommandTimeoutMs: DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
3621
+ },
3622
+ {
3623
+ respondAck,
3624
+ respondError
3278
3625
  }
3279
- } catch (error) {
3280
- const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
3281
- respondError({
3282
- code: "INSPECT_UNAVAILABLE",
3283
- message: messageText,
3284
- retryable: false
3285
- });
3286
- }
3626
+ );
3287
3627
  }
3288
3628
  async handleDebuggerEvent(source, method, params) {
3289
- if (!await readDebuggerCapabilityEnabled()) {
3290
- return;
3291
- }
3292
3629
  const tabId = source.tabId;
3293
3630
  if (typeof tabId !== "number") {
3294
3631
  return;
@@ -3307,9 +3644,6 @@ var DriveSocket = class {
3307
3644
  return;
3308
3645
  }
3309
3646
  this.clearDebuggerSession(tabId);
3310
- if (!await readDebuggerCapabilityEnabled()) {
3311
- return;
3312
- }
3313
3647
  this.sendDebuggerEvent({
3314
3648
  tab_id: tabId,
3315
3649
  method: "Debugger.detached",
@@ -3402,12 +3736,11 @@ var DriveSocket = class {
3402
3736
  const tab = await getTab(tabId);
3403
3737
  const url = typeof tab.url === "string" ? tab.url : void 0;
3404
3738
  if (isRestrictedUrl(url)) {
3405
- return {
3406
- code: "NOT_SUPPORTED",
3407
- message: "Debugger cannot attach to restricted pages.",
3408
- retryable: false,
3409
- details: { url }
3410
- };
3739
+ return buildRestrictedUrlError({
3740
+ url: url ?? "about:blank",
3741
+ operation: "debugger",
3742
+ action: "debugger.attach"
3743
+ });
3411
3744
  }
3412
3745
  } catch (error) {
3413
3746
  return mapDebuggerErrorMessage(
@@ -3542,6 +3875,17 @@ chrome.runtime.onConnect.addListener((port) => {
3542
3875
  chrome.windows.onRemoved.addListener((windowId) => {
3543
3876
  permissionPrompts.handleWindowRemoved(windowId);
3544
3877
  });
3878
+ chrome.windows.onFocusChanged.addListener((windowId) => {
3879
+ if (windowId === chrome.windows.WINDOW_ID_NONE) {
3880
+ return;
3881
+ }
3882
+ void queryActiveTabIdInWindow(windowId).then((tabId) => {
3883
+ markTabActive(tabId);
3884
+ socket.sendTabReport();
3885
+ }).catch(() => {
3886
+ socket.sendTabReport();
3887
+ });
3888
+ });
3545
3889
  chrome.tabs.onActivated.addListener((activeInfo) => {
3546
3890
  markTabActive(activeInfo.tabId);
3547
3891
  socket.sendTabReport();
@@ -3615,26 +3959,23 @@ chrome.storage.onChanged.addListener(
3615
3959
  if (areaName !== "local") {
3616
3960
  return;
3617
3961
  }
3618
- const debuggerChange = changes[DEBUGGER_CAPABILITY_ENABLED_KEY];
3619
- if (!debuggerChange) {
3962
+ const corePortChange = changes[LEGACY_CORE_PORT_KEY];
3963
+ if (!corePortChange) {
3620
3964
  return;
3621
3965
  }
3622
- if (typeof debuggerChange.newValue === "boolean") {
3623
- void socket.handleDebuggerCapabilityChange(debuggerChange.newValue).catch((error) => {
3624
- console.error(
3625
- "DriveSocket handleDebuggerCapabilityChange failed:",
3626
- error
3627
- );
3966
+ let work = Promise.resolve();
3967
+ if (corePortChange) {
3968
+ work = work.then(async () => {
3969
+ await clearLegacyCorePort(chrome.storage.local);
3628
3970
  });
3629
- return;
3630
3971
  }
3631
- void socket.refreshDebuggerCapabilityState().catch((error) => {
3632
- console.error(
3633
- "DriveSocket refreshDebuggerCapabilityState failed:",
3634
- error
3635
- );
3972
+ void work.catch((error) => {
3973
+ console.error("DriveSocket storage change handling failed:", error);
3636
3974
  });
3637
3975
  }
3638
3976
  );
3977
+ void clearLegacyCorePort(chrome.storage.local).catch((error) => {
3978
+ console.warn("Failed to clear legacy corePort storage.", error);
3979
+ });
3639
3980
  socket.start();
3640
3981
  //# sourceMappingURL=background.js.map