@btraut/browser-bridge 0.13.2 → 0.15.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;
@@ -182,6 +181,14 @@ var readSitePermissionsMode = async () => {
182
181
  );
183
182
  });
184
183
  };
184
+ var writeSitePermissionsMode = async (mode) => {
185
+ return await new Promise((resolve) => {
186
+ chrome.storage.local.set(
187
+ { [SITE_PERMISSIONS_MODE_KEY]: mode },
188
+ () => resolve()
189
+ );
190
+ });
191
+ };
185
192
  var readPermissionPromptWaitMs = async () => {
186
193
  return await new Promise((resolve) => {
187
194
  chrome.storage.local.get(
@@ -204,27 +211,18 @@ var readPermissionPromptWaitMs = async () => {
204
211
  );
205
212
  });
206
213
  };
207
- var readDebuggerCapabilityEnabled = async () => {
214
+ var writeDebuggerCapabilityEnabled = async (enabled) => {
215
+ void enabled;
208
216
  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
- }
217
+ chrome.storage.local.set(
218
+ { [DEBUGGER_CAPABILITY_ENABLED_KEY]: true },
219
+ () => resolve()
225
220
  );
226
221
  });
227
222
  };
223
+ var getAllowlistedSites = async () => {
224
+ return await readAllowlistRaw();
225
+ };
228
226
  var isSiteAllowed = async (siteKey) => {
229
227
  const key = normalizeSiteKey(siteKey);
230
228
  const allowlist = await readAllowlistRaw();
@@ -251,6 +249,15 @@ var touchSiteLastUsed = async (siteKey, now = /* @__PURE__ */ new Date()) => {
251
249
  allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };
252
250
  await writeAllowlistRaw(allowlist);
253
251
  };
252
+ var revokeSite = async (siteKey) => {
253
+ const key = normalizeSiteKey(siteKey);
254
+ const allowlist = await readAllowlistRaw();
255
+ if (!allowlist[key]) {
256
+ return;
257
+ }
258
+ delete allowlist[key];
259
+ await writeAllowlistRaw(allowlist);
260
+ };
254
261
 
255
262
  // packages/extension/src/permission-prompt.ts
256
263
  var PERMISSION_PROMPT_PORT_NAME = "permission_prompt";
@@ -436,6 +443,691 @@ var PermissionPromptController = class {
436
443
  }
437
444
  };
438
445
 
446
+ // packages/extension/src/permissions-request.ts
447
+ var PERMISSIONS_REQUEST_PORT_NAME = "permissions_request_prompt";
448
+ var defaultMakeRequestId2 = () => {
449
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
450
+ return crypto.randomUUID();
451
+ }
452
+ return `perm-change-${Date.now()}-${Math.random().toString(16).slice(2)}`;
453
+ };
454
+ var defaultOpenWindow2 = async (url) => {
455
+ return await new Promise((resolve, reject) => {
456
+ chrome.windows.create(
457
+ {
458
+ type: "popup",
459
+ url,
460
+ focused: true,
461
+ width: 500,
462
+ height: 470
463
+ },
464
+ (win) => {
465
+ const err = chrome.runtime.lastError;
466
+ if (err) {
467
+ reject(new Error(err.message));
468
+ return;
469
+ }
470
+ const windowId = win?.id;
471
+ if (typeof windowId !== "number") {
472
+ reject(new Error("Prompt window id missing."));
473
+ return;
474
+ }
475
+ resolve(windowId);
476
+ }
477
+ );
478
+ });
479
+ };
480
+ var defaultCloseWindow2 = async (windowId) => {
481
+ return await new Promise((resolve) => {
482
+ chrome.windows.remove(windowId, () => resolve());
483
+ });
484
+ };
485
+ var delay2 = async (ms) => {
486
+ return await new Promise((resolve) => {
487
+ setTimeout(resolve, ms);
488
+ });
489
+ };
490
+ var normalizeSite = (site) => site.trim().toLowerCase();
491
+ var describeChange = (state) => {
492
+ switch (state.kind) {
493
+ case "allow_site":
494
+ return `Allow Browser Bridge actions on ${state.site ?? "this site"}.`;
495
+ case "revoke_site":
496
+ return `Revoke Browser Bridge actions on ${state.site ?? "this site"}.`;
497
+ case "set_mode":
498
+ return state.mode === "bypass" ? "Switch Browser Bridge to bypass mode." : "Switch Browser Bridge to granular mode.";
499
+ }
500
+ };
501
+ var buildWarning = (kind, mode) => {
502
+ if (kind === "set_mode" && mode === "bypass") {
503
+ return "Bypass mode lets the agent act on any website without asking first.";
504
+ }
505
+ return void 0;
506
+ };
507
+ var PermissionsRequestController = class {
508
+ constructor(deps) {
509
+ this.stateByRequestId = /* @__PURE__ */ new Map();
510
+ this.stateByWindowId = /* @__PURE__ */ new Map();
511
+ this.deps = {
512
+ openWindow: deps?.openWindow ?? defaultOpenWindow2,
513
+ closeWindow: deps?.closeWindow ?? defaultCloseWindow2,
514
+ getDefaultWaitMs: deps?.getDefaultWaitMs ?? readPermissionPromptWaitMs,
515
+ makeRequestId: deps?.makeRequestId ?? defaultMakeRequestId2,
516
+ now: deps?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
517
+ allowSite: deps?.allowSite ?? allowSiteAlways,
518
+ revokeSite: deps?.revokeSite ?? revokeSite,
519
+ setMode: deps?.setMode ?? writeSitePermissionsMode
520
+ };
521
+ }
522
+ async requestChange(request) {
523
+ const state = await this.createState(request);
524
+ const waitMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : await this.deps.getDefaultWaitMs();
525
+ const decision = await this.waitForDecisionOrTimeout(state, waitMs);
526
+ if (!decision) {
527
+ return {
528
+ request_id: state.requestId,
529
+ kind: state.kind,
530
+ status: "timed_out",
531
+ requested_at: state.requestedAt,
532
+ ...state.site ? { site: state.site } : {},
533
+ ...state.mode ? { mode: state.mode } : {},
534
+ ...state.source ? { source: state.source } : {},
535
+ ...state.warning ? { warning: state.warning } : {},
536
+ message: "Permission change request timed out waiting for approval."
537
+ };
538
+ }
539
+ return {
540
+ request_id: state.requestId,
541
+ kind: state.kind,
542
+ status: decision === "approve" ? "approved" : "denied",
543
+ requested_at: state.requestedAt,
544
+ ...state.site ? { site: state.site } : {},
545
+ ...state.mode ? { mode: state.mode } : {},
546
+ ...state.source ? { source: state.source } : {},
547
+ ...state.warning ? { warning: state.warning } : {},
548
+ message: decision === "approve" ? describeChange(state) : "Permission change request was denied."
549
+ };
550
+ }
551
+ listPendingRequests() {
552
+ return [...this.stateByRequestId.values()].filter((state) => state.decided === null).map((state) => ({
553
+ request_id: state.requestId,
554
+ kind: state.kind,
555
+ status: "pending",
556
+ requested_at: state.requestedAt,
557
+ ...state.site ? { site: state.site } : {},
558
+ ...state.mode ? { mode: state.mode } : {},
559
+ ...state.source ? { source: state.source } : {},
560
+ ...state.warning ? { warning: state.warning } : {}
561
+ })).sort((a, b) => a.requested_at.localeCompare(b.requested_at));
562
+ }
563
+ handleConnect(port) {
564
+ if (!port || typeof port !== "object") {
565
+ return;
566
+ }
567
+ const p = port;
568
+ if (p.name !== PERMISSIONS_REQUEST_PORT_NAME) {
569
+ return;
570
+ }
571
+ const onMessage = p.onMessage;
572
+ if (!onMessage || typeof onMessage.addListener !== "function") {
573
+ return;
574
+ }
575
+ onMessage.addListener((message) => {
576
+ void this.handlePortMessage(message).catch((error) => {
577
+ console.error(
578
+ "PermissionsRequestController handlePortMessage failed:",
579
+ error
580
+ );
581
+ });
582
+ });
583
+ }
584
+ handleWindowRemoved(windowId) {
585
+ const state = this.stateByWindowId.get(windowId);
586
+ if (!state) {
587
+ return;
588
+ }
589
+ this.cleanupState(state);
590
+ }
591
+ async createState(request) {
592
+ const requestId = this.deps.makeRequestId();
593
+ const requestedAt = this.deps.now();
594
+ const kind = request.kind;
595
+ const warning = buildWarning(kind, request.mode);
596
+ const state = {
597
+ requestId,
598
+ kind,
599
+ requestedAt,
600
+ site: request.site ? normalizeSite(request.site) : void 0,
601
+ mode: request.mode,
602
+ source: request.source,
603
+ warning,
604
+ windowId: null,
605
+ decided: null,
606
+ waiters: /* @__PURE__ */ new Set()
607
+ };
608
+ this.stateByRequestId.set(requestId, state);
609
+ const url = this.buildPromptUrl(state);
610
+ const windowId = await this.deps.openWindow(url);
611
+ state.windowId = windowId;
612
+ this.stateByWindowId.set(windowId, state);
613
+ if (state.decided) {
614
+ await this.deps.closeWindow(windowId);
615
+ this.cleanupState(state);
616
+ }
617
+ return state;
618
+ }
619
+ buildPromptUrl(state) {
620
+ const base = chrome.runtime.getURL("permissions-request.html");
621
+ const url = new URL(base);
622
+ url.searchParams.set("requestId", state.requestId);
623
+ url.searchParams.set("kind", state.kind);
624
+ url.searchParams.set("requestedAt", state.requestedAt);
625
+ if (state.site) {
626
+ url.searchParams.set("site", state.site);
627
+ }
628
+ if (state.mode) {
629
+ url.searchParams.set("mode", state.mode);
630
+ }
631
+ if (state.source) {
632
+ url.searchParams.set("source", state.source);
633
+ }
634
+ if (state.warning) {
635
+ url.searchParams.set("warning", state.warning);
636
+ }
637
+ if (state.kind === "set_mode" && state.mode === "bypass") {
638
+ url.searchParams.set("requireAcknowledge", "1");
639
+ }
640
+ return url.toString();
641
+ }
642
+ async waitForDecisionOrTimeout(state, waitMs) {
643
+ if (state.decided) {
644
+ return state.decided;
645
+ }
646
+ let waiter = null;
647
+ const decisionPromise = new Promise((resolve) => {
648
+ waiter = resolve;
649
+ state.waiters.add(resolve);
650
+ });
651
+ const winner = await Promise.race([
652
+ decisionPromise,
653
+ delay2(waitMs).then(() => null)
654
+ ]);
655
+ if (winner === null && waiter) {
656
+ state.waiters.delete(waiter);
657
+ }
658
+ return winner;
659
+ }
660
+ async handlePortMessage(message) {
661
+ if (!message || typeof message !== "object") {
662
+ return;
663
+ }
664
+ const m = message;
665
+ if (m.type !== "decision") {
666
+ return;
667
+ }
668
+ const requestId = m.requestId;
669
+ const decision = m.decision;
670
+ if (typeof requestId !== "string" || requestId.length === 0) {
671
+ return;
672
+ }
673
+ if (decision !== "approve" && decision !== "deny") {
674
+ return;
675
+ }
676
+ const state = this.stateByRequestId.get(requestId);
677
+ if (!state) {
678
+ return;
679
+ }
680
+ state.decided = decision;
681
+ if (decision === "approve") {
682
+ await this.applyApprovedChange(state);
683
+ }
684
+ for (const waiter of state.waiters) {
685
+ waiter(decision);
686
+ }
687
+ state.waiters.clear();
688
+ if (typeof state.windowId === "number") {
689
+ await this.deps.closeWindow(state.windowId);
690
+ this.cleanupState(state);
691
+ }
692
+ }
693
+ async applyApprovedChange(state) {
694
+ if (state.kind === "allow_site") {
695
+ if (!state.site) {
696
+ throw new Error("allow_site request is missing site.");
697
+ }
698
+ await this.deps.allowSite(state.site);
699
+ return;
700
+ }
701
+ if (state.kind === "revoke_site") {
702
+ if (!state.site) {
703
+ throw new Error("revoke_site request is missing site.");
704
+ }
705
+ await this.deps.revokeSite(state.site);
706
+ return;
707
+ }
708
+ if (!state.mode) {
709
+ throw new Error("set_mode request is missing mode.");
710
+ }
711
+ await this.deps.setMode(state.mode);
712
+ }
713
+ cleanupState(state) {
714
+ this.stateByRequestId.delete(state.requestId);
715
+ if (typeof state.windowId === "number") {
716
+ this.stateByWindowId.delete(state.windowId);
717
+ }
718
+ }
719
+ };
720
+
721
+ // packages/extension/src/restricted-url.ts
722
+ var RESTRICTED_URL_PREFIXES = [
723
+ "chrome://",
724
+ "chrome-extension://",
725
+ "chrome-devtools://",
726
+ "devtools://",
727
+ "edge://",
728
+ "brave://",
729
+ "view-source:"
730
+ ];
731
+ var isRestrictedUrl = (url) => {
732
+ if (!url || typeof url !== "string") {
733
+ return false;
734
+ }
735
+ const lowered = url.toLowerCase();
736
+ if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
737
+ return true;
738
+ }
739
+ try {
740
+ const parsed = new URL(url);
741
+ if (parsed.hostname === "chromewebstore.google.com") {
742
+ return true;
743
+ }
744
+ if (parsed.hostname === "chrome.google.com") {
745
+ return parsed.pathname.startsWith("/webstore");
746
+ }
747
+ } catch (error) {
748
+ console.debug("Ignoring invalid URL in restriction check.", error);
749
+ }
750
+ return false;
751
+ };
752
+ var getRestrictedUrlKind = (url) => {
753
+ const lowered = url.toLowerCase();
754
+ if (lowered.startsWith("chrome-extension://")) {
755
+ return "extension_internal";
756
+ }
757
+ if (lowered.startsWith("chrome://") || lowered.startsWith("edge://") || lowered.startsWith("brave://")) {
758
+ return "browser_internal";
759
+ }
760
+ if (lowered.includes("chromewebstore.google.com") || lowered.includes("chrome.google.com/webstore")) {
761
+ return "webstore";
762
+ }
763
+ return "restricted_url";
764
+ };
765
+ var getAlternativeCommands = (url) => {
766
+ const lowered = url.toLowerCase();
767
+ if (lowered.startsWith("chrome-extension://") || lowered.startsWith("chrome://extensions")) {
768
+ return ["browser-bridge diagnostics doctor", "browser-bridge dev info"];
769
+ }
770
+ return ["browser-bridge dev info", "browser-bridge diagnostics doctor"];
771
+ };
772
+ var buildRestrictedUrlError = (options) => {
773
+ const alternatives = getAlternativeCommands(options.url);
774
+ const operationLabel = options.operation === "navigate" ? "Navigation" : options.operation === "screenshot" ? "Screenshots" : options.operation === "debugger" ? "Debugger attach" : "This action";
775
+ return {
776
+ code: "NOT_SUPPORTED",
777
+ message: `${operationLabel} is not supported for browser internal URLs.`,
778
+ retryable: false,
779
+ details: {
780
+ reason: "restricted_internal_url",
781
+ url: options.url,
782
+ url_kind: getRestrictedUrlKind(options.url),
783
+ rationale: "Chrome restricts extension automation on internal browser surfaces (for example chrome:// and chrome-extension://).",
784
+ action: options.action,
785
+ next_step: alternatives[0],
786
+ alternatives
787
+ }
788
+ };
789
+ };
790
+
791
+ // packages/extension/src/tab-resolution.ts
792
+ var invalidTabIdError = (message) => ({
793
+ code: "INVALID_ARGUMENT",
794
+ message,
795
+ retryable: false
796
+ });
797
+ var readOptionalTabId = (params, message = "tab_id must be a number when provided.") => {
798
+ const tabId = params.tab_id;
799
+ if (tabId !== void 0 && typeof tabId !== "number") {
800
+ return {
801
+ ok: false,
802
+ error: invalidTabIdError(message)
803
+ };
804
+ }
805
+ return {
806
+ ok: true,
807
+ tabId: typeof tabId === "number" ? tabId : void 0
808
+ };
809
+ };
810
+ var readRequiredTabId = (params, message = "tab_id must be a number.") => {
811
+ const tabId = params.tab_id;
812
+ if (typeof tabId !== "number") {
813
+ return {
814
+ ok: false,
815
+ error: invalidTabIdError(message)
816
+ };
817
+ }
818
+ return {
819
+ ok: true,
820
+ tabId
821
+ };
822
+ };
823
+ var resolveOptionalTabId = async (params, deps, message = "tab_id must be a number when provided.") => {
824
+ const parsed = readOptionalTabId(params, message);
825
+ if (!parsed.ok) {
826
+ return parsed;
827
+ }
828
+ return {
829
+ ok: true,
830
+ tabId: parsed.tabId ?? await deps.getDefaultTabId()
831
+ };
832
+ };
833
+ var requireTab = async (tabId, getTab2) => {
834
+ try {
835
+ return {
836
+ ok: true,
837
+ tab: await getTab2(tabId)
838
+ };
839
+ } catch {
840
+ return {
841
+ ok: false,
842
+ error: {
843
+ code: "TAB_NOT_FOUND",
844
+ message: `tab_id ${tabId} was not found.`,
845
+ retryable: false,
846
+ details: { tab_id: tabId }
847
+ }
848
+ };
849
+ }
850
+ };
851
+
852
+ // packages/extension/src/action-permissions.ts
853
+ var GATED_ACTIONS = /* @__PURE__ */ new Set([
854
+ "drive.navigate",
855
+ "drive.go_back",
856
+ "drive.go_forward",
857
+ "drive.click",
858
+ "drive.hover",
859
+ "drive.select",
860
+ "drive.type",
861
+ "drive.fill_form",
862
+ "drive.drag",
863
+ "drive.handle_dialog",
864
+ "drive.key",
865
+ "drive.key_press",
866
+ "drive.scroll",
867
+ "drive.screenshot",
868
+ "drive.wait_for"
869
+ ]);
870
+ var gateDriveAction = async (options) => {
871
+ const { action, params, getDefaultTabId: getDefaultTabId2, getTab: getTab2, permissionPrompts: permissionPrompts2 } = options;
872
+ if (!GATED_ACTIONS.has(action)) {
873
+ return { ok: true, siteKey: null, touchOnSuccess: false };
874
+ }
875
+ let siteKey = null;
876
+ if (action === "drive.navigate") {
877
+ const url = params.url;
878
+ if (typeof url !== "string" || url.length === 0) {
879
+ return { ok: true, siteKey: null, touchOnSuccess: false };
880
+ }
881
+ if (isRestrictedUrl(url)) {
882
+ return {
883
+ ok: false,
884
+ error: buildRestrictedUrlError({
885
+ url,
886
+ operation: "navigate",
887
+ action
888
+ })
889
+ };
890
+ }
891
+ siteKey = siteKeyFromUrl(url);
892
+ if (!siteKey) {
893
+ return {
894
+ ok: false,
895
+ error: {
896
+ code: "INVALID_ARGUMENT",
897
+ message: "Unable to resolve site permission key for url.",
898
+ retryable: false,
899
+ details: { url }
900
+ }
901
+ };
902
+ }
903
+ } else {
904
+ const parsedTabId = readOptionalTabId(params);
905
+ if (!parsedTabId.ok) {
906
+ return { ok: true, siteKey: null, touchOnSuccess: false };
907
+ }
908
+ const resolvedTabId = parsedTabId.tabId ?? await getDefaultTabId2();
909
+ const tab = await getTab2(resolvedTabId);
910
+ const url = tab.url;
911
+ if (typeof url !== "string" || url.length === 0) {
912
+ return {
913
+ ok: false,
914
+ error: {
915
+ code: "FAILED_PRECONDITION",
916
+ message: "Active tab URL is unavailable for permission gating.",
917
+ retryable: false,
918
+ details: { tab_id: resolvedTabId }
919
+ }
920
+ };
921
+ }
922
+ if (isRestrictedUrl(url)) {
923
+ return {
924
+ ok: false,
925
+ error: buildRestrictedUrlError({
926
+ url,
927
+ operation: action === "drive.screenshot" ? "screenshot" : "action",
928
+ action
929
+ })
930
+ };
931
+ }
932
+ siteKey = siteKeyFromUrl(url);
933
+ if (!siteKey) {
934
+ return {
935
+ ok: false,
936
+ error: {
937
+ code: "FAILED_PRECONDITION",
938
+ message: "Unable to resolve site permission key for active tab.",
939
+ retryable: false,
940
+ details: { url, tab_id: resolvedTabId }
941
+ }
942
+ };
943
+ }
944
+ }
945
+ if (await readSitePermissionsMode() === "bypass") {
946
+ return { ok: true, siteKey, touchOnSuccess: false };
947
+ }
948
+ if (await isSiteAllowed(siteKey)) {
949
+ return { ok: true, siteKey, touchOnSuccess: true };
950
+ }
951
+ const decision = await permissionPrompts2.requestPermission({
952
+ siteKey,
953
+ action
954
+ });
955
+ if (decision.kind === "timed_out") {
956
+ return {
957
+ ok: false,
958
+ error: {
959
+ code: "PERMISSION_PROMPT_TIMEOUT",
960
+ message: `Permission prompt timed out for ${siteKey}.`,
961
+ retryable: true,
962
+ details: {
963
+ reason: "prompt_timed_out",
964
+ site: siteKey,
965
+ action,
966
+ wait_ms: decision.waitMs
967
+ }
968
+ }
969
+ };
970
+ }
971
+ if (decision.kind === "deny") {
972
+ return {
973
+ ok: false,
974
+ error: {
975
+ code: "PERMISSION_DENIED",
976
+ message: `User denied Browser Bridge permission for ${siteKey}.`,
977
+ retryable: false,
978
+ details: {
979
+ reason: "user_denied",
980
+ site: siteKey,
981
+ action,
982
+ 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."
983
+ }
984
+ }
985
+ };
986
+ }
987
+ if (decision.kind === "allow_always") {
988
+ await allowSiteAlways(siteKey);
989
+ return { ok: true, siteKey, touchOnSuccess: true };
990
+ }
991
+ return { ok: true, siteKey, touchOnSuccess: false };
992
+ };
993
+
994
+ // packages/extension/src/popup-trigger-state.ts
995
+ var popupTriggerStateChanged = (before, after) => {
996
+ if (!before || !after) {
997
+ return before !== after;
998
+ }
999
+ return before.ariaExpanded !== after.ariaExpanded || before.dataState !== after.dataState || before.open !== after.open;
1000
+ };
1001
+ var coercePopupTriggerState = (value) => {
1002
+ if (!value || typeof value !== "object") {
1003
+ return void 0;
1004
+ }
1005
+ const record = value;
1006
+ if (record.kind !== "popup_trigger") {
1007
+ return void 0;
1008
+ }
1009
+ const ariaHasPopup = typeof record.ariaHasPopup === "string" ? record.ariaHasPopup : void 0;
1010
+ const ariaExpanded = typeof record.ariaExpanded === "string" ? record.ariaExpanded : void 0;
1011
+ const dataState = typeof record.dataState === "string" ? record.dataState : void 0;
1012
+ const open = typeof record.open === "boolean" ? record.open : void 0;
1013
+ return {
1014
+ kind: "popup_trigger",
1015
+ ...ariaHasPopup ? { ariaHasPopup } : {},
1016
+ ...ariaExpanded !== void 0 ? { ariaExpanded } : {},
1017
+ ...dataState !== void 0 ? { dataState } : {},
1018
+ ...open !== void 0 ? { open } : {}
1019
+ };
1020
+ };
1021
+
1022
+ // packages/extension/src/popup-click-verification.ts
1023
+ var POPUP_TRIGGER_CLICK_SETTLE_MS = 50;
1024
+ var POPUP_TRIGGER_RECHECK_MS = 125;
1025
+ var readPopupTriggerAfterClick = async (options) => {
1026
+ const after = await options.resolveLocatorPoint(options.locator);
1027
+ if (!after.ok) {
1028
+ if (shouldTreatPostClickReadErrorAsSuccess(after.error)) {
1029
+ return {
1030
+ ok: false,
1031
+ error: {
1032
+ code: "NOT_FOUND",
1033
+ message: "Popup trigger disappeared after click.",
1034
+ retryable: false,
1035
+ details: { reason: "popup_trigger_disappeared" }
1036
+ }
1037
+ };
1038
+ }
1039
+ return after;
1040
+ }
1041
+ return after;
1042
+ };
1043
+ var shouldTreatPostClickReadErrorAsSuccess = (error) => {
1044
+ return error.code === "LOCATOR_NOT_FOUND" || error.code === "NOT_FOUND" || error.retryable === true && (error.code === "TIMEOUT" || error.details?.reason === "transient_tab_channel_error");
1045
+ };
1046
+ var verifyPopupTriggerClick = async (options) => {
1047
+ if (options.prepareTarget) {
1048
+ await options.prepareTarget();
1049
+ }
1050
+ try {
1051
+ await options.dispatchCdpClick(
1052
+ options.point.x,
1053
+ options.point.y,
1054
+ options.clickCount
1055
+ );
1056
+ } catch (error) {
1057
+ return { ok: false, error: options.mapDispatchError(error) };
1058
+ }
1059
+ await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
1060
+ const after = await readPopupTriggerAfterClick(options);
1061
+ if (!after.ok) {
1062
+ if (after.error.details?.reason === "popup_trigger_disappeared") {
1063
+ return { ok: true };
1064
+ }
1065
+ return after;
1066
+ }
1067
+ if (popupTriggerStateChanged(
1068
+ options.point.targetState ?? null,
1069
+ after.point.targetState ?? null
1070
+ )) {
1071
+ return { ok: true };
1072
+ }
1073
+ if (options.clickCount === 1) {
1074
+ await options.delayMs(POPUP_TRIGGER_RECHECK_MS);
1075
+ const lateAfter = await readPopupTriggerAfterClick(options);
1076
+ if (!lateAfter.ok) {
1077
+ if (lateAfter.error.details?.reason === "popup_trigger_disappeared") {
1078
+ return { ok: true };
1079
+ }
1080
+ return lateAfter;
1081
+ }
1082
+ if (popupTriggerStateChanged(
1083
+ options.point.targetState ?? null,
1084
+ lateAfter.point.targetState ?? null
1085
+ )) {
1086
+ return { ok: true };
1087
+ }
1088
+ try {
1089
+ await options.dispatchCdpClick(
1090
+ options.point.x,
1091
+ options.point.y,
1092
+ options.clickCount
1093
+ );
1094
+ } catch (error) {
1095
+ return { ok: false, error: options.mapDispatchError(error) };
1096
+ }
1097
+ await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
1098
+ const retryAfter = await readPopupTriggerAfterClick(options);
1099
+ if (!retryAfter.ok) {
1100
+ if (retryAfter.error.details?.reason === "popup_trigger_disappeared") {
1101
+ return { ok: true };
1102
+ }
1103
+ return retryAfter;
1104
+ }
1105
+ if (popupTriggerStateChanged(
1106
+ options.point.targetState ?? null,
1107
+ retryAfter.point.targetState ?? null
1108
+ )) {
1109
+ return { ok: true };
1110
+ }
1111
+ }
1112
+ return {
1113
+ ok: false,
1114
+ error: {
1115
+ code: "FAILED_PRECONDITION",
1116
+ message: "Click focused the popup trigger but did not change its open state.",
1117
+ retryable: false,
1118
+ details: {
1119
+ reason: "click_state_unchanged",
1120
+ control: "popup_trigger",
1121
+ aria_haspopup: options.point.targetState?.ariaHasPopup,
1122
+ aria_expanded_before: options.point.targetState?.ariaExpanded,
1123
+ aria_expanded_after: after.point.targetState?.ariaExpanded,
1124
+ data_state_before: options.point.targetState?.dataState,
1125
+ data_state_after: after.point.targetState?.dataState
1126
+ }
1127
+ }
1128
+ };
1129
+ };
1130
+
439
1131
  // packages/extension/src/drive-reliability.ts
440
1132
  var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
441
1133
  "receiving end does not exist",
@@ -443,6 +1135,9 @@ var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
443
1135
  "the message port closed before a response was received",
444
1136
  "extension port is moved into back/forward cache"
445
1137
  ];
1138
+ var CONTENT_SCRIPT_RECOVERY_ERROR_PATTERNS = [
1139
+ "receiving end does not exist"
1140
+ ];
446
1141
  var TAB_CHANNEL_RETRY_DELAYS_MS = [120, 200, 320, 500, 750, 1e3, 1200];
447
1142
  var normalizePathname = (pathname) => {
448
1143
  if (pathname.length === 0) {
@@ -462,6 +1157,34 @@ var isTransientTabChannelError = (message) => {
462
1157
  (pattern) => normalized.includes(pattern)
463
1158
  );
464
1159
  };
1160
+ var canInjectContentScriptForUrl = (url) => {
1161
+ if (typeof url !== "string" || url.trim().length === 0) {
1162
+ return false;
1163
+ }
1164
+ const normalized = url.toLowerCase();
1165
+ if (normalized.startsWith("chrome://") || normalized.startsWith("chrome-extension://") || normalized.startsWith("devtools://") || normalized.startsWith("edge://") || normalized.startsWith("about:") || normalized.startsWith("file://")) {
1166
+ return false;
1167
+ }
1168
+ return normalized.startsWith("http://") || normalized.startsWith("https://");
1169
+ };
1170
+ var shouldReinjectContentScript = (message, tabUrl) => {
1171
+ if (typeof message !== "string") {
1172
+ return false;
1173
+ }
1174
+ const normalized = message.toLowerCase();
1175
+ return CONTENT_SCRIPT_RECOVERY_ERROR_PATTERNS.some(
1176
+ (pattern) => normalized.includes(pattern)
1177
+ ) && canInjectContentScriptForUrl(tabUrl);
1178
+ };
1179
+ var shouldRetryTabChannelFailure = (action, error) => {
1180
+ if (!error || typeof error !== "object") {
1181
+ return false;
1182
+ }
1183
+ if (isTransientTabChannelError(error.message)) {
1184
+ return true;
1185
+ }
1186
+ return action === "drive.wait_for" && error.code === "TIMEOUT" && error.retryable === true && typeof error.message === "string" && error.message.includes("Timed out waiting for content response");
1187
+ };
465
1188
  var getTabChannelRetryDelayMs = (attempt) => {
466
1189
  if (!Number.isInteger(attempt) || attempt < 1) {
467
1190
  return void 0;
@@ -544,23 +1267,242 @@ var ConnectionStateTracker = class {
544
1267
  }
545
1268
  getStatus() {
546
1269
  return {
547
- state: this.state,
548
- endpoint: this.endpoint,
549
- ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
550
- reconnect_delay_ms: this.reconnectDelayMs,
551
- retry_at: this.retryAt,
552
- last_connected_at: this.lastConnectedAt,
553
- last_disconnected_at: this.lastDisconnectedAt,
554
- last_error_at: this.lastErrorAt,
555
- last_error_message: this.lastErrorMessage,
556
- consecutive_failures: this.consecutiveFailures
1270
+ state: this.state,
1271
+ endpoint: this.endpoint,
1272
+ ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
1273
+ reconnect_delay_ms: this.reconnectDelayMs,
1274
+ retry_at: this.retryAt,
1275
+ last_connected_at: this.lastConnectedAt,
1276
+ last_disconnected_at: this.lastDisconnectedAt,
1277
+ last_error_at: this.lastErrorAt,
1278
+ last_error_message: this.lastErrorMessage,
1279
+ consecutive_failures: this.consecutiveFailures
1280
+ };
1281
+ }
1282
+ };
1283
+
1284
+ // packages/extension/src/debugger-dispatch.ts
1285
+ var dispatchDebuggerRequest = async (message, deps, responders) => {
1286
+ const { respondAck, respondError } = responders;
1287
+ try {
1288
+ switch (message.action) {
1289
+ case "debugger.attach": {
1290
+ const parsedTabId = readRequiredTabId(
1291
+ message.params ?? {}
1292
+ );
1293
+ if (!parsedTabId.ok) {
1294
+ respondError(parsedTabId.error);
1295
+ return;
1296
+ }
1297
+ const error = await deps.ensureDebuggerAttached(parsedTabId.tabId);
1298
+ if (error) {
1299
+ respondError(error);
1300
+ return;
1301
+ }
1302
+ respondAck({ ok: true });
1303
+ return;
1304
+ }
1305
+ case "debugger.detach": {
1306
+ const parsedTabId = readRequiredTabId(
1307
+ message.params ?? {}
1308
+ );
1309
+ if (!parsedTabId.ok) {
1310
+ respondError(parsedTabId.error);
1311
+ return;
1312
+ }
1313
+ const error = await deps.detachDebugger(parsedTabId.tabId);
1314
+ if (error) {
1315
+ respondError(error);
1316
+ return;
1317
+ }
1318
+ respondAck({ ok: true });
1319
+ return;
1320
+ }
1321
+ case "debugger.command": {
1322
+ const params = message.params ?? {};
1323
+ const parsedTabId = readRequiredTabId(
1324
+ params
1325
+ );
1326
+ if (!parsedTabId.ok) {
1327
+ respondError(parsedTabId.error);
1328
+ return;
1329
+ }
1330
+ if (typeof params.method !== "string" || params.method.length === 0) {
1331
+ respondError({
1332
+ code: "INVALID_ARGUMENT",
1333
+ message: "method must be a non-empty string.",
1334
+ retryable: false
1335
+ });
1336
+ return;
1337
+ }
1338
+ const session = deps.getSession(parsedTabId.tabId);
1339
+ if (session?.attachPromise) {
1340
+ try {
1341
+ await session.attachPromise;
1342
+ } catch (error) {
1343
+ const info = deps.mapDebuggerErrorMessage(
1344
+ error instanceof Error ? error.message : "Debugger attach failed."
1345
+ );
1346
+ deps.clearDebuggerSession(parsedTabId.tabId);
1347
+ respondError(info);
1348
+ return;
1349
+ }
1350
+ }
1351
+ const attachedSession = deps.getSession(parsedTabId.tabId);
1352
+ if (!attachedSession?.attached) {
1353
+ respondError({
1354
+ code: "FAILED_PRECONDITION",
1355
+ message: "Debugger is not attached to the requested tab.",
1356
+ retryable: false
1357
+ });
1358
+ return;
1359
+ }
1360
+ try {
1361
+ const result = await deps.sendDebuggerCommand(
1362
+ parsedTabId.tabId,
1363
+ params.method,
1364
+ params.params,
1365
+ deps.debuggerCommandTimeoutMs
1366
+ );
1367
+ deps.touchDebuggerSession(parsedTabId.tabId);
1368
+ respondAck(result);
1369
+ } catch (error) {
1370
+ const info = deps.mapDebuggerErrorMessage(
1371
+ error instanceof Error ? error.message : "Debugger command failed."
1372
+ );
1373
+ respondError(info);
1374
+ }
1375
+ return;
1376
+ }
1377
+ default:
1378
+ respondError({
1379
+ code: "NOT_IMPLEMENTED",
1380
+ message: `${message.action} not implemented in extension yet.`,
1381
+ retryable: false
1382
+ });
1383
+ }
1384
+ } catch (error) {
1385
+ const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
1386
+ respondError({
1387
+ code: "INSPECT_UNAVAILABLE",
1388
+ message: messageText,
1389
+ retryable: false
1390
+ });
1391
+ }
1392
+ };
1393
+
1394
+ // packages/extension/src/screenshot-errors.ts
1395
+ var isCaptureVisibleTabRateLimitedMessage = (message) => {
1396
+ const normalized = message.toLowerCase();
1397
+ const hasCaptureSignal = normalized.includes("capturevisibletab");
1398
+ const hasRateSignal = normalized.includes("max_capture_visible_tab_calls_per_second") || normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
1399
+ return hasCaptureSignal && hasRateSignal;
1400
+ };
1401
+ var isCaptureVisibleTabPermissionMessage = (message) => {
1402
+ const normalized = message.toLowerCase();
1403
+ const hasCaptureSignal = normalized.includes("capturevisibletab");
1404
+ const hasPermissionSignal = normalized.includes("permission is required") || normalized.includes("requires permission") || normalized.includes("requires either");
1405
+ const hasPermissionTarget = normalized.includes("all_urls") || normalized.includes("activetab");
1406
+ return hasCaptureSignal && hasPermissionSignal && hasPermissionTarget;
1407
+ };
1408
+ var isCaptureVisibleTabRateLimitedError = (error) => error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
1409
+ var isCaptureVisibleTabPermissionError = (error) => error instanceof Error && isCaptureVisibleTabPermissionMessage(error.message);
1410
+ var mapScreenshotCaptureError = (error, fallbackMessage) => {
1411
+ const message = error instanceof Error && error.message ? error.message : fallbackMessage;
1412
+ if (isCaptureVisibleTabRateLimitedError(error)) {
1413
+ return {
1414
+ code: "RATE_LIMITED",
1415
+ message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
1416
+ retryable: true,
1417
+ details: {
1418
+ reason: "capture_visible_tab_rate_limited",
1419
+ original_message: message
1420
+ }
1421
+ };
1422
+ }
1423
+ if (isCaptureVisibleTabPermissionError(error)) {
1424
+ return {
1425
+ code: "PERMISSION_REQUIRED",
1426
+ message: "Screenshot capture requires captureVisibleTab permission (<all_urls> or activeTab).",
1427
+ retryable: false,
1428
+ details: {
1429
+ reason: "capture_visible_tab_permission_required",
1430
+ required_any_of: ["<all_urls>", "activeTab"],
1431
+ next_step: "Reload the Browser Bridge extension in chrome://extensions and retry screenshot capture.",
1432
+ original_message: message
1433
+ }
1434
+ };
1435
+ }
1436
+ return {
1437
+ code: "ARTIFACT_IO_ERROR",
1438
+ message,
1439
+ retryable: false
1440
+ };
1441
+ };
1442
+
1443
+ // packages/extension/src/core-endpoint-config.ts
1444
+ var DEFAULT_CORE_HOST = "127.0.0.1";
1445
+ var DEFAULT_CORE_PORT = 3210;
1446
+ var LEGACY_CORE_PORT_KEY = "corePort";
1447
+ var hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1448
+ var readCoreEndpointConfig = async () => ({
1449
+ host: DEFAULT_CORE_HOST,
1450
+ port: DEFAULT_CORE_PORT,
1451
+ portSource: "default"
1452
+ });
1453
+ var clearLegacyCorePort = async (storage) => {
1454
+ return await new Promise((resolve) => {
1455
+ storage.get([LEGACY_CORE_PORT_KEY], (items) => {
1456
+ if (!hasOwn(items, LEGACY_CORE_PORT_KEY)) {
1457
+ resolve(false);
1458
+ return;
1459
+ }
1460
+ storage.remove([LEGACY_CORE_PORT_KEY], () => resolve(true));
1461
+ });
1462
+ });
1463
+ };
1464
+
1465
+ // packages/extension/src/tab-activation.ts
1466
+ var resolveTabActivationOutcome = ({
1467
+ tabId,
1468
+ windowId,
1469
+ activated,
1470
+ focusErrorMessage,
1471
+ windowFocused
1472
+ }) => {
1473
+ if (!activated) {
1474
+ return {
1475
+ ok: false,
1476
+ error: {
1477
+ code: "FAILED_PRECONDITION",
1478
+ message: `Failed to activate tab_id ${tabId}.`,
1479
+ retryable: true,
1480
+ details: { tab_id: tabId }
1481
+ }
557
1482
  };
558
1483
  }
1484
+ const warnings = [];
1485
+ if (typeof windowId === "number") {
1486
+ if (focusErrorMessage) {
1487
+ warnings.push(
1488
+ `Activated tab_id ${tabId}, but failed to focus window_id ${windowId}: ${focusErrorMessage}`
1489
+ );
1490
+ } else if (windowFocused === false) {
1491
+ warnings.push(
1492
+ `Activated tab_id ${tabId}, but window_id ${windowId} did not report focused state.`
1493
+ );
1494
+ }
1495
+ }
1496
+ return {
1497
+ ok: true,
1498
+ result: {
1499
+ ok: true,
1500
+ ...warnings.length > 0 ? { warnings } : {}
1501
+ }
1502
+ };
559
1503
  };
560
1504
 
561
1505
  // packages/extension/src/background.ts
562
- var DEFAULT_CORE_PORT = 3210;
563
- var CORE_PORT_KEY = "corePort";
564
1506
  var CORE_WS_PATH = "/drive";
565
1507
  var CORE_HEALTH_PATH = "/health";
566
1508
  var CORE_HEALTH_TIMEOUT_MS = 1200;
@@ -598,33 +1540,29 @@ var BASE_NEGOTIATED_CAPABILITIES = Object.freeze({
598
1540
  "drive.tab_list": true,
599
1541
  "drive.tab_activate": true,
600
1542
  "drive.tab_close": true,
601
- "drive.ping": true
1543
+ "drive.set_debugger_capability": true,
1544
+ "drive.ping": true,
1545
+ "permissions.list": true,
1546
+ "permissions.get_mode": true,
1547
+ "permissions.list_pending_requests": true,
1548
+ "permissions.request_allow_site": true,
1549
+ "permissions.request_revoke_site": true,
1550
+ "permissions.request_set_mode": true
602
1551
  });
603
1552
  var DEBUGGER_CAPABILITY_ACTIONS = [
604
1553
  "debugger.attach",
605
1554
  "debugger.detach",
606
1555
  "debugger.command"
607
1556
  ];
608
- var buildNegotiatedCapabilities = (debuggerCapabilityEnabled) => {
1557
+ var buildNegotiatedCapabilities = () => {
609
1558
  const capabilities = {
610
1559
  ...BASE_NEGOTIATED_CAPABILITIES
611
1560
  };
612
1561
  for (const action of DEBUGGER_CAPABILITY_ACTIONS) {
613
- capabilities[action] = debuggerCapabilityEnabled;
1562
+ capabilities[action] = true;
614
1563
  }
615
1564
  return capabilities;
616
1565
  };
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
1566
  var getAgentTabBootstrapUrl = () => {
629
1567
  return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_BOOTSTRAP_PATH) : AGENT_TAB_BOOTSTRAP_PATH;
630
1568
  };
@@ -682,20 +1620,33 @@ var delayMs = async (ms) => {
682
1620
  self.setTimeout(resolve, ms);
683
1621
  });
684
1622
  };
1623
+ var ensureTabContentScript = async (tabId) => {
1624
+ try {
1625
+ const tab = await getTab(tabId);
1626
+ const url = typeof tab.url === "string" ? tab.url : void 0;
1627
+ if (!canInjectContentScriptForUrl(url)) {
1628
+ return false;
1629
+ }
1630
+ await wrapChromeVoid(
1631
+ (callback) => chrome.scripting.executeScript(
1632
+ {
1633
+ target: { tabId },
1634
+ files: ["dist/content.js"]
1635
+ },
1636
+ () => callback()
1637
+ )
1638
+ );
1639
+ return true;
1640
+ } catch (error) {
1641
+ console.debug("Failed to re-inject content script.", { tabId, error });
1642
+ return false;
1643
+ }
1644
+ };
685
1645
  var CAPTURE_VISIBLE_TAB_MIN_INTERVAL_MS = 400;
686
1646
  var CAPTURE_VISIBLE_TAB_MAX_RETRIES = 3;
687
1647
  var CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS = 500;
688
1648
  var captureVisibleTabQueue = Promise.resolve();
689
1649
  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
1650
  var randomJitterMs = (maxMs) => {
700
1651
  return Math.floor(Math.random() * Math.max(1, maxMs));
701
1652
  };
@@ -737,25 +1688,6 @@ var captureVisibleTabWithThrottle = async (windowId) => {
737
1688
  throw new Error("captureVisibleTab failed unexpectedly.");
738
1689
  });
739
1690
  };
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
1691
  var parseDataUrl = (dataUrl) => {
760
1692
  const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
761
1693
  if (!match) {
@@ -801,40 +1733,6 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
801
1733
  bitmap.close();
802
1734
  }
803
1735
  };
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
1736
  var readDebuggerIdleTimeoutMs = async () => {
839
1737
  return await new Promise((resolve) => {
840
1738
  chrome.storage.local.get(
@@ -857,36 +1755,6 @@ var readDebuggerIdleTimeoutMs = async () => {
857
1755
  );
858
1756
  });
859
1757
  };
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
1758
  var mapDebuggerErrorMessage = (message, fallbackCode = "INSPECT_UNAVAILABLE") => {
891
1759
  const normalized = message.toLowerCase();
892
1760
  if (normalized.includes("already attached") || normalized.includes("another debugger") || normalized.includes("attached to this target")) {
@@ -943,6 +1811,25 @@ var buildTabInfo = (tab) => {
943
1811
  last_active_at: ensureLastActiveAt(tabId)
944
1812
  };
945
1813
  };
1814
+ var tabRecencyScore = (tab) => {
1815
+ const parsed = Date.parse(tab.last_active_at);
1816
+ return Number.isFinite(parsed) ? parsed : -Infinity;
1817
+ };
1818
+ var compareTabsForReport = (a, b) => {
1819
+ const aActive = a.active === true ? 1 : 0;
1820
+ const bActive = b.active === true ? 1 : 0;
1821
+ if (aActive !== bActive) {
1822
+ return bActive - aActive;
1823
+ }
1824
+ const recencyDelta = tabRecencyScore(b) - tabRecencyScore(a);
1825
+ if (recencyDelta !== 0) {
1826
+ return recencyDelta;
1827
+ }
1828
+ if (a.window_id !== b.window_id) {
1829
+ return a.window_id - b.window_id;
1830
+ }
1831
+ return a.tab_id - b.tab_id;
1832
+ };
946
1833
  var queryTabs = async () => {
947
1834
  const tabs = await wrapChromeCallback(
948
1835
  (callback) => chrome.tabs.query({}, callback)
@@ -954,6 +1841,7 @@ var queryTabs = async () => {
954
1841
  result.push(info);
955
1842
  }
956
1843
  }
1844
+ result.sort(compareTabsForReport);
957
1845
  return result;
958
1846
  };
959
1847
  var getTab = async (tabId) => {
@@ -961,6 +1849,21 @@ var getTab = async (tabId) => {
961
1849
  (callback) => chrome.tabs.get(tabId, callback)
962
1850
  );
963
1851
  };
1852
+ var getWindow = async (windowId) => {
1853
+ return await wrapChromeCallback(
1854
+ (callback) => chrome.windows.get(windowId, callback)
1855
+ );
1856
+ };
1857
+ var withResolvedTabTarget = async (tabId, result) => {
1858
+ const payload = result && typeof result === "object" && !Array.isArray(result) ? { ...result } : {};
1859
+ const tab = await getTab(tabId).catch(() => void 0);
1860
+ const windowId = tab && typeof tab.windowId === "number" ? tab.windowId : void 0;
1861
+ return {
1862
+ ...payload,
1863
+ tab_id: tabId,
1864
+ ...typeof windowId === "number" ? { window_id: windowId } : {}
1865
+ };
1866
+ };
964
1867
  var getActiveTabId = async () => {
965
1868
  const tabs = await wrapChromeCallback(
966
1869
  (callback) => chrome.tabs.query({ active: true, lastFocusedWindow: true }, callback)
@@ -1194,12 +2097,19 @@ var sendToTab = async (tabId, action, params, options) => {
1194
2097
  });
1195
2098
  });
1196
2099
  };
2100
+ let attemptedContentRecovery = false;
1197
2101
  for (let attempt = 1; ; attempt += 1) {
1198
2102
  const result = await attemptSend();
1199
2103
  if (result.ok) {
1200
2104
  return result;
1201
2105
  }
1202
- if (!isTransientTabChannelError(result.error?.message)) {
2106
+ if (!attemptedContentRecovery && shouldReinjectContentScript(
2107
+ result.error.message,
2108
+ (await getTab(tabId).catch(() => void 0))?.url
2109
+ )) {
2110
+ attemptedContentRecovery = await ensureTabContentScript(tabId);
2111
+ }
2112
+ if (!shouldRetryTabChannelFailure(action, result.error)) {
1203
2113
  return result;
1204
2114
  }
1205
2115
  const retryDelayMs = getTabChannelRetryDelayMs(attempt);
@@ -1351,15 +2261,15 @@ var DriveSocket = class {
1351
2261
  if (this.reconnectTimer !== null) {
1352
2262
  return;
1353
2263
  }
1354
- const delay2 = this.reconnectDelayMs;
1355
- this.connection.markBackoff(delay2);
2264
+ const delay3 = this.reconnectDelayMs;
2265
+ this.connection.markBackoff(delay3);
1356
2266
  this.reconnectTimer = self.setTimeout(() => {
1357
2267
  this.reconnectTimer = null;
1358
2268
  this.connection.markConnecting();
1359
2269
  void this.connect().catch((error) => {
1360
2270
  this.recordConnectionFailure("reconnect", error);
1361
2271
  });
1362
- }, delay2);
2272
+ }, delay3);
1363
2273
  this.reconnectDelayMs = Math.min(
1364
2274
  this.maxReconnectDelayMs,
1365
2275
  this.reconnectDelayMs * 2
@@ -1480,7 +2390,6 @@ var DriveSocket = class {
1480
2390
  async sendHello() {
1481
2391
  const manifest = chrome.runtime.getManifest();
1482
2392
  const endpoint = await readCoreEndpointConfig();
1483
- const debuggerCapabilityEnabled = await readDebuggerCapabilityEnabled();
1484
2393
  let tabs = [];
1485
2394
  try {
1486
2395
  tabs = await queryTabs();
@@ -1489,9 +2398,10 @@ var DriveSocket = class {
1489
2398
  tabs = [];
1490
2399
  }
1491
2400
  const params = {
2401
+ extension_id: chrome.runtime.id,
1492
2402
  version: manifest.version,
1493
2403
  protocol_version: DRIVE_WS_PROTOCOL_VERSION,
1494
- capabilities: buildNegotiatedCapabilities(debuggerCapabilityEnabled),
2404
+ capabilities: buildNegotiatedCapabilities(),
1495
2405
  core_host: endpoint.host,
1496
2406
  core_port: endpoint.port,
1497
2407
  core_port_source: endpoint.portSource,
@@ -1567,24 +2477,18 @@ var DriveSocket = class {
1567
2477
  }
1568
2478
  }
1569
2479
  async refreshDebuggerCapabilityState() {
1570
- const enabled = await readDebuggerCapabilityEnabled();
1571
- if (!enabled) {
1572
- await this.detachAllDebuggerSessions();
1573
- }
1574
2480
  this.refreshCapabilities();
1575
2481
  }
1576
2482
  async handleDebuggerCapabilityChange(enabled) {
1577
- if (!enabled) {
1578
- await this.detachAllDebuggerSessions();
1579
- }
2483
+ void enabled;
1580
2484
  this.refreshCapabilities();
1581
2485
  }
1582
2486
  async handleRequest(message) {
1583
- let driveMessage = null;
2487
+ let requestMessage = null;
1584
2488
  let gatedSiteKey = null;
1585
2489
  let touchGatedSiteOnSuccess = false;
1586
2490
  const respondOk = (result) => {
1587
- if (!driveMessage) {
2491
+ if (!requestMessage) {
1588
2492
  return;
1589
2493
  }
1590
2494
  if (touchGatedSiteOnSuccess && gatedSiteKey) {
@@ -1593,20 +2497,20 @@ var DriveSocket = class {
1593
2497
  });
1594
2498
  }
1595
2499
  const response = {
1596
- id: driveMessage.id,
1597
- action: driveMessage.action,
2500
+ id: requestMessage.id,
2501
+ action: requestMessage.action,
1598
2502
  status: "ok",
1599
2503
  result
1600
2504
  };
1601
2505
  this.sendMessage(response);
1602
2506
  };
1603
2507
  const respondError = (error) => {
1604
- if (!driveMessage) {
2508
+ if (!requestMessage) {
1605
2509
  return;
1606
2510
  }
1607
2511
  const response = {
1608
- id: driveMessage.id,
1609
- action: driveMessage.action,
2512
+ id: requestMessage.id,
2513
+ action: requestMessage.action,
1610
2514
  status: "error",
1611
2515
  error: sanitizeDriveErrorInfo(error)
1612
2516
  };
@@ -1616,179 +2520,161 @@ var DriveSocket = class {
1616
2520
  if (!message || typeof message !== "object" || typeof message.id !== "string" || typeof message.action !== "string") {
1617
2521
  return;
1618
2522
  }
1619
- 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
- }
2523
+ const action = message.action;
2524
+ if (action.startsWith("debugger.")) {
1629
2525
  await this.handleDebuggerRequest(message);
1630
2526
  return;
1631
2527
  }
1632
- if (!message.action.startsWith("drive.")) {
1633
- return;
1634
- }
1635
- 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
- }
2528
+ requestMessage = message;
2529
+ if (action.startsWith("permissions.")) {
1658
2530
  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
- };
2531
+ const rawTimeoutMs = params.timeout_ms;
2532
+ const timeoutMs = typeof rawTimeoutMs === "number" && Number.isFinite(rawTimeoutMs) && rawTimeoutMs > 0 ? Math.floor(rawTimeoutMs) : void 0;
2533
+ const rawSource = params.source;
2534
+ const source = rawSource === "cli" || rawSource === "mcp" || rawSource === "api" ? rawSource : void 0;
2535
+ switch (action) {
2536
+ case "permissions.list": {
2537
+ const allowlist = await getAllowlistedSites();
2538
+ const sites = Object.entries(allowlist).map(([site, entry]) => ({
2539
+ site,
2540
+ created_at: entry.createdAt,
2541
+ last_used_at: entry.lastUsedAt
2542
+ })).sort((a, b) => a.site.localeCompare(b.site));
2543
+ respondOk({ sites });
2544
+ return;
1675
2545
  }
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
- };
2546
+ case "permissions.get_mode": {
2547
+ respondOk({
2548
+ mode: await readSitePermissionsMode()
2549
+ });
2550
+ return;
1687
2551
  }
1688
- } else {
1689
- const tabId = params.tab_id;
1690
- if (tabId !== void 0 && typeof tabId !== "number") {
1691
- return { ok: true, siteKey: null, touchOnSuccess: false };
2552
+ case "permissions.list_pending_requests": {
2553
+ respondOk({
2554
+ requests: permissionsRequests.listPendingRequests()
2555
+ });
2556
+ return;
1692
2557
  }
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.",
2558
+ case "permissions.request_allow_site": {
2559
+ const site = params.site;
2560
+ if (typeof site !== "string" || site.trim().length === 0) {
2561
+ respondError({
2562
+ code: "INVALID_ARGUMENT",
2563
+ message: "site must be a non-empty string.",
1702
2564
  retryable: false,
1703
- details: { tab_id: resolvedTabId }
1704
- }
1705
- };
2565
+ details: { field: "site" }
2566
+ });
2567
+ return;
2568
+ }
2569
+ respondOk(
2570
+ await permissionsRequests.requestChange({
2571
+ kind: "allow_site",
2572
+ site,
2573
+ timeoutMs,
2574
+ source
2575
+ })
2576
+ );
2577
+ return;
1706
2578
  }
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,
2579
+ case "permissions.request_revoke_site": {
2580
+ const site = params.site;
2581
+ if (typeof site !== "string" || site.trim().length === 0) {
2582
+ respondError({
2583
+ code: "INVALID_ARGUMENT",
2584
+ message: "site must be a non-empty string.",
1714
2585
  retryable: false,
1715
- details: { url }
1716
- }
1717
- };
2586
+ details: { field: "site" }
2587
+ });
2588
+ return;
2589
+ }
2590
+ respondOk(
2591
+ await permissionsRequests.requestChange({
2592
+ kind: "revoke_site",
2593
+ site,
2594
+ timeoutMs,
2595
+ source
2596
+ })
2597
+ );
2598
+ return;
1718
2599
  }
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.",
2600
+ case "permissions.request_set_mode": {
2601
+ const mode = params.mode;
2602
+ if (mode !== "granular" && mode !== "bypass") {
2603
+ respondError({
2604
+ code: "INVALID_ARGUMENT",
2605
+ message: "mode must be granular or bypass.",
1726
2606
  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
- }
2607
+ details: { field: "mode" }
2608
+ });
2609
+ return;
1771
2610
  }
1772
- };
1773
- }
1774
- if (decision.kind === "allow_always") {
1775
- await allowSiteAlways(siteKey);
1776
- return { ok: true, siteKey, touchOnSuccess: true };
2611
+ respondOk(
2612
+ await permissionsRequests.requestChange({
2613
+ kind: "set_mode",
2614
+ mode,
2615
+ timeoutMs,
2616
+ source
2617
+ })
2618
+ );
2619
+ return;
2620
+ }
1777
2621
  }
1778
- return { ok: true, siteKey, touchOnSuccess: false };
1779
- };
1780
- const gated = await gateDriveAction();
2622
+ }
2623
+ if (!action.startsWith("drive.")) {
2624
+ return;
2625
+ }
2626
+ const gated = await gateDriveAction({
2627
+ action,
2628
+ params: message.params ?? {},
2629
+ getDefaultTabId,
2630
+ getTab,
2631
+ permissionPrompts
2632
+ });
1781
2633
  if (!gated.ok) {
1782
2634
  respondError(gated.error);
1783
2635
  return;
1784
2636
  }
1785
2637
  gatedSiteKey = gated.siteKey;
1786
2638
  touchGatedSiteOnSuccess = gated.touchOnSuccess;
2639
+ const resolveActionTabId = async (params) => await resolveOptionalTabId(params, { getDefaultTabId });
1787
2640
  switch (message.action) {
1788
2641
  case "drive.ping": {
1789
2642
  respondOk({ ok: true });
1790
2643
  return;
1791
2644
  }
2645
+ case "drive.set_debugger_capability": {
2646
+ const params = message.params ?? {};
2647
+ const enabled = typeof params.enabled === "boolean" ? params.enabled : true;
2648
+ const expectedExtensionId = params.extension_id;
2649
+ if (expectedExtensionId !== void 0 && typeof expectedExtensionId !== "string") {
2650
+ respondError({
2651
+ code: "INVALID_ARGUMENT",
2652
+ message: "extension_id must be a string when provided.",
2653
+ retryable: false
2654
+ });
2655
+ return;
2656
+ }
2657
+ if (typeof expectedExtensionId === "string" && expectedExtensionId.length > 0 && expectedExtensionId !== chrome.runtime.id) {
2658
+ respondError({
2659
+ code: "FAILED_PRECONDITION",
2660
+ message: "Connected extension id does not match the requested extension.",
2661
+ retryable: false,
2662
+ details: {
2663
+ expected_extension_id: expectedExtensionId,
2664
+ connected_extension_id: chrome.runtime.id
2665
+ }
2666
+ });
2667
+ return;
2668
+ }
2669
+ await writeDebuggerCapabilityEnabled(enabled);
2670
+ await this.refreshDebuggerCapabilityState();
2671
+ respondOk({
2672
+ ok: true,
2673
+ enabled,
2674
+ extension_id: chrome.runtime.id
2675
+ });
2676
+ return;
2677
+ }
1792
2678
  case "drive.navigate": {
1793
2679
  const params = message.params ?? {};
1794
2680
  const url = params.url;
@@ -1800,21 +2686,38 @@ var DriveSocket = class {
1800
2686
  });
1801
2687
  return;
1802
2688
  }
1803
- let tabId = params.tab_id;
1804
- if (tabId !== void 0 && typeof tabId !== "number") {
2689
+ const tabTarget = await resolveActionTabId(params);
2690
+ if (!tabTarget.ok) {
2691
+ respondError(tabTarget.error);
2692
+ return;
2693
+ }
2694
+ const tabId = tabTarget.tabId;
2695
+ const requestedWaitMode = params.wait;
2696
+ if (requestedWaitMode !== void 0 && requestedWaitMode !== "none" && requestedWaitMode !== "domcontentloaded" && requestedWaitMode !== "networkidle") {
1805
2697
  respondError({
1806
2698
  code: "INVALID_ARGUMENT",
1807
- message: "tab_id must be a number when provided.",
1808
- retryable: false
2699
+ message: `Unsupported wait mode: ${String(requestedWaitMode)}.`,
2700
+ retryable: false,
2701
+ details: {
2702
+ field: "wait",
2703
+ supported_wait_modes: [
2704
+ "none",
2705
+ "domcontentloaded",
2706
+ "networkidle"
2707
+ ],
2708
+ mapped_wait_mode: "domcontentloaded"
2709
+ }
1809
2710
  });
1810
2711
  return;
1811
2712
  }
1812
- if (tabId === void 0) {
1813
- tabId = await getDefaultTabId();
1814
- }
1815
- const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
2713
+ const waitMode = requestedWaitMode === "none" ? "none" : "domcontentloaded";
1816
2714
  const domContentLoadedSignal = waitMode === "domcontentloaded" ? waitForDomContentLoaded(tabId, 3e4) : null;
1817
2715
  const warnings = [];
2716
+ if (requestedWaitMode === "networkidle") {
2717
+ warnings.push(
2718
+ "wait=networkidle is mapped to domcontentloaded in this runtime."
2719
+ );
2720
+ }
1818
2721
  await wrapChromeVoid(
1819
2722
  (callback) => chrome.tabs.update(tabId, { url }, () => callback())
1820
2723
  );
@@ -1841,39 +2744,30 @@ var DriveSocket = class {
1841
2744
  if (tabId === agentTabId) {
1842
2745
  void refreshAgentTabBranding(tabId);
1843
2746
  }
1844
- respondOk({
1845
- ok: true,
1846
- ...warnings.length > 0 ? { warnings } : {}
1847
- });
2747
+ respondOk(
2748
+ await withResolvedTabTarget(tabId, {
2749
+ ok: true,
2750
+ ...warnings.length > 0 ? { warnings } : {}
2751
+ })
2752
+ );
1848
2753
  return;
1849
2754
  }
1850
2755
  case "drive.go_back":
1851
2756
  case "drive.go_forward": {
1852
2757
  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
- });
2758
+ const tabTarget = await resolveActionTabId(params);
2759
+ if (!tabTarget.ok) {
2760
+ respondError(tabTarget.error);
1860
2761
  return;
1861
2762
  }
1862
- if (tabId === void 0) {
1863
- tabId = await getDefaultTabId();
1864
- }
2763
+ const tabId = tabTarget.tabId;
1865
2764
  const navigationSignal = waitForHistoryNavigationSignal(
1866
2765
  tabId,
1867
2766
  HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS
1868
2767
  );
1869
- const result = await sendToTab(
1870
- tabId,
1871
- message.action,
1872
- void 0,
1873
- {
1874
- timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
1875
- }
1876
- );
2768
+ const result = await sendToTab(tabId, message.action, void 0, {
2769
+ timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
2770
+ });
1877
2771
  if (!result.ok && result.error.code !== "TIMEOUT") {
1878
2772
  respondError(result.error);
1879
2773
  return;
@@ -1899,9 +2793,11 @@ var DriveSocket = class {
1899
2793
  if (tabId === agentTabId) {
1900
2794
  void refreshAgentTabBranding(tabId);
1901
2795
  }
1902
- respondOk({
1903
- ok: true
1904
- });
2796
+ respondOk(
2797
+ await withResolvedTabTarget(tabId, {
2798
+ ok: true
2799
+ })
2800
+ );
1905
2801
  return;
1906
2802
  }
1907
2803
  case "drive.tab_list": {
@@ -1911,44 +2807,66 @@ var DriveSocket = class {
1911
2807
  return;
1912
2808
  }
1913
2809
  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
- });
2810
+ const parsedTabId = readRequiredTabId(
2811
+ message.params ?? {}
2812
+ );
2813
+ if (!parsedTabId.ok) {
2814
+ respondError(parsedTabId.error);
1921
2815
  return;
1922
2816
  }
1923
- const tab = await getTab(tabId);
2817
+ const tabId = parsedTabId.tabId;
2818
+ const tabLookup = await requireTab(tabId, getTab);
2819
+ if (!tabLookup.ok) {
2820
+ respondError(tabLookup.error);
2821
+ return;
2822
+ }
2823
+ const tab = tabLookup.tab;
1924
2824
  await wrapChromeVoid(
1925
2825
  (callback) => chrome.tabs.update(tabId, { active: true }, () => callback())
1926
2826
  );
1927
2827
  const windowId = tab.windowId;
2828
+ let focusErrorMessage;
1928
2829
  if (typeof windowId === "number") {
1929
- await wrapChromeVoid(
1930
- (callback) => chrome.windows.update(
1931
- windowId,
1932
- { focused: true },
1933
- () => callback()
1934
- )
1935
- );
2830
+ try {
2831
+ await wrapChromeVoid(
2832
+ (callback) => chrome.windows.update(
2833
+ windowId,
2834
+ { focused: true },
2835
+ () => callback()
2836
+ )
2837
+ );
2838
+ } catch (error) {
2839
+ focusErrorMessage = error instanceof Error ? error.message : "Unknown focus error.";
2840
+ }
2841
+ }
2842
+ const activatedTab = await getTab(tabId).catch(() => void 0);
2843
+ const focusedWindow = typeof windowId === "number" ? await getWindow(windowId).catch(() => void 0) : void 0;
2844
+ const windowFocused = focusedWindow && typeof focusedWindow.focused === "boolean" ? focusedWindow.focused : void 0;
2845
+ const outcome = resolveTabActivationOutcome({
2846
+ tabId,
2847
+ windowId: typeof windowId === "number" ? windowId : void 0,
2848
+ activated: Boolean(activatedTab?.active === true),
2849
+ focusErrorMessage,
2850
+ windowFocused
2851
+ });
2852
+ if (!outcome.ok) {
2853
+ respondError(outcome.error);
2854
+ return;
1936
2855
  }
1937
2856
  markTabActive(tabId);
1938
- respondOk({ ok: true });
2857
+ respondOk(outcome.result);
1939
2858
  this.sendTabReport();
1940
2859
  return;
1941
2860
  }
1942
2861
  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
- });
2862
+ const parsedTabId = readRequiredTabId(
2863
+ message.params ?? {}
2864
+ );
2865
+ if (!parsedTabId.ok) {
2866
+ respondError(parsedTabId.error);
1950
2867
  return;
1951
2868
  }
2869
+ const tabId = parsedTabId.tabId;
1952
2870
  await wrapChromeVoid(
1953
2871
  (callback) => chrome.tabs.remove(tabId, () => callback())
1954
2872
  );
@@ -1962,8 +2880,8 @@ var DriveSocket = class {
1962
2880
  }
1963
2881
  case "drive.handle_dialog": {
1964
2882
  const params = message.params ?? {};
1965
- const action = params.action;
1966
- if (action !== "accept" && action !== "dismiss") {
2883
+ const action2 = params.action;
2884
+ if (action2 !== "accept" && action2 !== "dismiss") {
1967
2885
  respondError({
1968
2886
  code: "INVALID_ARGUMENT",
1969
2887
  message: "action must be accept or dismiss.",
@@ -1980,18 +2898,12 @@ var DriveSocket = class {
1980
2898
  });
1981
2899
  return;
1982
2900
  }
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
- });
2901
+ const tabTarget = await resolveActionTabId(params);
2902
+ if (!tabTarget.ok) {
2903
+ respondError(tabTarget.error);
1990
2904
  return;
1991
2905
  }
1992
- if (tabId === void 0) {
1993
- tabId = await getDefaultTabId();
1994
- }
2906
+ const tabId = tabTarget.tabId;
1995
2907
  const error = await this.ensureDebuggerAttached(tabId);
1996
2908
  if (error) {
1997
2909
  respondError(error);
@@ -2002,13 +2914,13 @@ var DriveSocket = class {
2002
2914
  tabId,
2003
2915
  "Page.handleJavaScriptDialog",
2004
2916
  {
2005
- accept: action === "accept",
2917
+ accept: action2 === "accept",
2006
2918
  ...promptText ? { promptText } : {}
2007
2919
  },
2008
2920
  DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
2009
2921
  );
2010
2922
  this.touchDebuggerSession(tabId);
2011
- respondOk({ ok: true });
2923
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2012
2924
  } catch (error2) {
2013
2925
  const info = mapDebuggerErrorMessage(
2014
2926
  error2 instanceof Error ? error2.message : "Dialog handling failed."
@@ -2019,18 +2931,12 @@ var DriveSocket = class {
2019
2931
  }
2020
2932
  case "drive.click": {
2021
2933
  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
- });
2934
+ const tabTarget = await resolveActionTabId(params);
2935
+ if (!tabTarget.ok) {
2936
+ respondError(tabTarget.error);
2029
2937
  return;
2030
2938
  }
2031
- if (tabId === void 0) {
2032
- tabId = await getDefaultTabId();
2033
- }
2939
+ const tabId = tabTarget.tabId;
2034
2940
  const clickCount = params.click_count;
2035
2941
  const count = typeof clickCount === "number" && Number.isFinite(clickCount) ? Math.max(1, Math.floor(clickCount)) : 1;
2036
2942
  const error = await this.ensureDebuggerAttached(tabId);
@@ -2046,31 +2952,48 @@ var DriveSocket = class {
2046
2952
  respondError(pointResult.error);
2047
2953
  return;
2048
2954
  }
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
- }
2955
+ const { x, y, targetState } = pointResult.point;
2956
+ if (targetState) {
2957
+ const verified = await verifyPopupTriggerClick({
2958
+ clickCount: count,
2959
+ locator: params.locator,
2960
+ point: pointResult.point,
2961
+ prepareTarget: async () => await this.focusLocator(tabId, params.locator),
2962
+ resolveLocatorPoint: async (locator) => await this.resolveLocatorPoint(tabId, locator),
2963
+ dispatchCdpClick: async (clickX, clickY, clickCount2) => await this.dispatchCdpClick(tabId, clickX, clickY, clickCount2),
2964
+ mapDispatchError: (error2) => mapDebuggerErrorMessage(
2965
+ error2 instanceof Error ? error2.message : "Click dispatch failed."
2966
+ ),
2967
+ delayMs
2968
+ });
2969
+ if (!verified.ok) {
2970
+ respondError(verified.error);
2971
+ return;
2972
+ }
2973
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2974
+ return;
2975
+ }
2976
+ try {
2977
+ await this.dispatchCdpClick(tabId, x, y, count);
2978
+ } catch (error2) {
2979
+ respondError(
2980
+ mapDebuggerErrorMessage(
2981
+ error2 instanceof Error ? error2.message : "Click dispatch failed."
2982
+ )
2055
2983
  );
2056
- }, 0);
2057
- respondOk({ ok: true });
2984
+ return;
2985
+ }
2986
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2058
2987
  return;
2059
2988
  }
2060
2989
  case "drive.hover": {
2061
2990
  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
- });
2991
+ const tabTarget = await resolveActionTabId(params);
2992
+ if (!tabTarget.ok) {
2993
+ respondError(tabTarget.error);
2069
2994
  return;
2070
2995
  }
2071
- if (tabId === void 0) {
2072
- tabId = await getDefaultTabId();
2073
- }
2996
+ const tabId = tabTarget.tabId;
2074
2997
  const error = await this.ensureDebuggerAttached(tabId);
2075
2998
  if (error) {
2076
2999
  respondError(error);
@@ -2091,15 +3014,17 @@ var DriveSocket = class {
2091
3014
  if (waitMs > 0) {
2092
3015
  await delayMs(waitMs);
2093
3016
  }
2094
- const snapshot = await sendToTab(
2095
- tabId,
2096
- "drive.snapshot_html"
2097
- );
3017
+ const snapshot = await sendToTab(tabId, "drive.snapshot_html");
2098
3018
  if (!snapshot.ok) {
2099
3019
  respondError(snapshot.error);
2100
3020
  return;
2101
3021
  }
2102
- respondOk(snapshot.result ?? { format: "html", snapshot: "" });
3022
+ respondOk(
3023
+ await withResolvedTabTarget(
3024
+ tabId,
3025
+ snapshot.result ?? { format: "html", snapshot: "" }
3026
+ )
3027
+ );
2103
3028
  } catch (error2) {
2104
3029
  const info = mapDebuggerErrorMessage(
2105
3030
  error2 instanceof Error ? error2.message : "Hover dispatch failed."
@@ -2110,35 +3035,23 @@ var DriveSocket = class {
2110
3035
  }
2111
3036
  case "drive.drag": {
2112
3037
  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
- });
3038
+ const tabTarget = await resolveActionTabId(params);
3039
+ if (!tabTarget.ok) {
3040
+ respondError(tabTarget.error);
2120
3041
  return;
2121
3042
  }
2122
- if (tabId === void 0) {
2123
- tabId = await getDefaultTabId();
2124
- }
3043
+ const tabId = tabTarget.tabId;
2125
3044
  const error = await this.ensureDebuggerAttached(tabId);
2126
3045
  if (error) {
2127
3046
  respondError(error);
2128
3047
  return;
2129
3048
  }
2130
- const fromResult = await this.resolveLocatorPoint(
2131
- tabId,
2132
- params.from
2133
- );
3049
+ const fromResult = await this.resolveLocatorPoint(tabId, params.from);
2134
3050
  if (!fromResult.ok) {
2135
3051
  respondError(fromResult.error);
2136
3052
  return;
2137
3053
  }
2138
- const toResult = await this.resolveLocatorPoint(
2139
- tabId,
2140
- params.to
2141
- );
3054
+ const toResult = await this.resolveLocatorPoint(tabId, params.to);
2142
3055
  if (!toResult.ok) {
2143
3056
  respondError(toResult.error);
2144
3057
  return;
@@ -2151,7 +3064,7 @@ var DriveSocket = class {
2151
3064
  toResult.point,
2152
3065
  steps
2153
3066
  );
2154
- respondOk({ ok: true });
3067
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2155
3068
  } catch (error2) {
2156
3069
  const info = mapDebuggerErrorMessage(
2157
3070
  error2 instanceof Error ? error2.message : "Drag dispatch failed."
@@ -2171,30 +3084,20 @@ var DriveSocket = class {
2171
3084
  });
2172
3085
  return;
2173
3086
  }
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
- });
3087
+ const tabTarget = await resolveActionTabId(params);
3088
+ if (!tabTarget.ok) {
3089
+ respondError(tabTarget.error);
2181
3090
  return;
2182
3091
  }
2183
- if (tabId === void 0) {
2184
- tabId = await getDefaultTabId();
2185
- }
3092
+ const tabId = tabTarget.tabId;
2186
3093
  const error = await this.ensureDebuggerAttached(tabId);
2187
3094
  if (error) {
2188
3095
  respondError(error);
2189
3096
  return;
2190
3097
  }
2191
3098
  try {
2192
- await this.dispatchCdpKeyPress(
2193
- tabId,
2194
- key,
2195
- params.modifiers
2196
- );
2197
- respondOk({ ok: true });
3099
+ await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
3100
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2198
3101
  } catch (error2) {
2199
3102
  const info = mapDebuggerErrorMessage(
2200
3103
  error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
@@ -2214,18 +3117,12 @@ var DriveSocket = class {
2214
3117
  });
2215
3118
  return;
2216
3119
  }
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
- });
3120
+ const tabTarget = await resolveActionTabId(params);
3121
+ if (!tabTarget.ok) {
3122
+ respondError(tabTarget.error);
2224
3123
  return;
2225
3124
  }
2226
- if (tabId === void 0) {
2227
- tabId = await getDefaultTabId();
2228
- }
3125
+ const tabId = tabTarget.tabId;
2229
3126
  const count = typeof params.repeat === "number" && Number.isFinite(params.repeat) ? Math.max(1, Math.min(50, Math.floor(params.repeat))) : 1;
2230
3127
  const error = await this.ensureDebuggerAttached(tabId);
2231
3128
  if (error) {
@@ -2234,13 +3131,9 @@ var DriveSocket = class {
2234
3131
  }
2235
3132
  try {
2236
3133
  for (let i = 0; i < count; i += 1) {
2237
- await this.dispatchCdpKeyPress(
2238
- tabId,
2239
- key,
2240
- params.modifiers
2241
- );
3134
+ await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
2242
3135
  }
2243
- respondOk({ ok: true });
3136
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2244
3137
  } catch (error2) {
2245
3138
  const info = mapDebuggerErrorMessage(
2246
3139
  error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
@@ -2260,18 +3153,12 @@ var DriveSocket = class {
2260
3153
  });
2261
3154
  return;
2262
3155
  }
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
- });
3156
+ const tabTarget = await resolveActionTabId(params);
3157
+ if (!tabTarget.ok) {
3158
+ respondError(tabTarget.error);
2270
3159
  return;
2271
3160
  }
2272
- if (tabId === void 0) {
2273
- tabId = await getDefaultTabId();
2274
- }
3161
+ const tabId = tabTarget.tabId;
2275
3162
  const error = await this.ensureDebuggerAttached(tabId);
2276
3163
  if (error) {
2277
3164
  respondError(error);
@@ -2287,23 +3174,17 @@ var DriveSocket = class {
2287
3174
  respondError(result.error);
2288
3175
  return;
2289
3176
  }
2290
- respondOk({ ok: true });
3177
+ respondOk(await withResolvedTabTarget(tabId, { ok: true }));
2291
3178
  return;
2292
3179
  }
2293
3180
  case "drive.select": {
2294
3181
  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
- });
3182
+ const tabTarget = await resolveActionTabId(params);
3183
+ if (!tabTarget.ok) {
3184
+ respondError(tabTarget.error);
2302
3185
  return;
2303
3186
  }
2304
- if (tabId === void 0) {
2305
- tabId = await getDefaultTabId();
2306
- }
3187
+ const tabId = tabTarget.tabId;
2307
3188
  const error = await this.ensureDebuggerAttached(tabId);
2308
3189
  if (error) {
2309
3190
  respondError(error);
@@ -2331,16 +3212,17 @@ var DriveSocket = class {
2331
3212
  respondError(info);
2332
3213
  return;
2333
3214
  }
2334
- const selectResult = await sendToTab(
2335
- tabId,
2336
- "drive.select",
2337
- params
2338
- );
3215
+ const selectResult = await sendToTab(tabId, "drive.select", params);
2339
3216
  if (!selectResult.ok) {
2340
3217
  respondError(selectResult.error);
2341
3218
  return;
2342
3219
  }
2343
- respondOk(selectResult.result ?? { ok: true });
3220
+ respondOk(
3221
+ await withResolvedTabTarget(
3222
+ tabId,
3223
+ selectResult.result ?? { ok: true }
3224
+ )
3225
+ );
2344
3226
  return;
2345
3227
  }
2346
3228
  case "drive.fill_form": {
@@ -2354,18 +3236,12 @@ var DriveSocket = class {
2354
3236
  });
2355
3237
  return;
2356
3238
  }
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
- });
3239
+ const tabTarget = await resolveActionTabId(params);
3240
+ if (!tabTarget.ok) {
3241
+ respondError(tabTarget.error);
2364
3242
  return;
2365
3243
  }
2366
- if (tabId === void 0) {
2367
- tabId = await getDefaultTabId();
2368
- }
3244
+ const tabId = tabTarget.tabId;
2369
3245
  const error = await this.ensureDebuggerAttached(tabId);
2370
3246
  if (error) {
2371
3247
  respondError(error);
@@ -2426,13 +3302,9 @@ var DriveSocket = class {
2426
3302
  filled += 1;
2427
3303
  continue;
2428
3304
  }
2429
- const fallback = await sendToTab(
2430
- tabId,
2431
- "drive.fill_form",
2432
- {
2433
- fields: [field]
2434
- }
2435
- );
3305
+ const fallback = await sendToTab(tabId, "drive.fill_form", {
3306
+ fields: [field]
3307
+ });
2436
3308
  if (!fallback.ok) {
2437
3309
  errors.push(
2438
3310
  `Field ${index} could not be filled: ${fallback.error.message}`
@@ -2451,39 +3323,32 @@ var DriveSocket = class {
2451
3323
  }
2452
3324
  errors.push(`Field ${index} could not be filled.`);
2453
3325
  }
2454
- respondOk({
2455
- filled,
2456
- attempted: fields.length,
2457
- errors: errors.length > 0 ? errors : []
2458
- });
3326
+ respondOk(
3327
+ await withResolvedTabTarget(tabId, {
3328
+ filled,
3329
+ attempted: fields.length,
3330
+ errors: errors.length > 0 ? errors : []
3331
+ })
3332
+ );
2459
3333
  return;
2460
3334
  }
2461
3335
  case "drive.scroll":
2462
3336
  case "drive.wait_for": {
2463
3337
  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
- });
3338
+ const tabTarget = await resolveActionTabId(params);
3339
+ if (!tabTarget.ok) {
3340
+ respondError(tabTarget.error);
2471
3341
  return;
2472
3342
  }
2473
- if (tabId === void 0) {
2474
- tabId = await getDefaultTabId();
2475
- }
3343
+ const tabId = tabTarget.tabId;
2476
3344
  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
- );
3345
+ const result = await sendToTab(tabId, message.action, params, {
3346
+ timeoutMs
3347
+ });
2485
3348
  if (result.ok) {
2486
- respondOk(result.result ?? { ok: true });
3349
+ respondOk(
3350
+ await withResolvedTabTarget(tabId, result.result ?? { ok: true })
3351
+ );
2487
3352
  } else {
2488
3353
  respondError(result.error);
2489
3354
  }
@@ -2491,30 +3356,25 @@ var DriveSocket = class {
2491
3356
  }
2492
3357
  case "drive.screenshot": {
2493
3358
  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
- });
3359
+ const tabTarget = await resolveActionTabId(params);
3360
+ if (!tabTarget.ok) {
3361
+ respondError(tabTarget.error);
2501
3362
  return;
2502
3363
  }
2503
- if (tabId === void 0) {
2504
- tabId = await getDefaultTabId();
2505
- }
3364
+ const tabId = tabTarget.tabId;
2506
3365
  const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
2507
3366
  const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
2508
3367
  const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
2509
3368
  const tab = await getTab(tabId);
2510
3369
  const url = tab.url;
2511
3370
  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
- });
3371
+ respondError(
3372
+ buildRestrictedUrlError({
3373
+ url,
3374
+ operation: "screenshot",
3375
+ action: "drive.screenshot"
3376
+ })
3377
+ );
2518
3378
  return;
2519
3379
  }
2520
3380
  const windowId = tab.windowId;
@@ -2679,7 +3539,7 @@ var DriveSocket = class {
2679
3539
  format,
2680
3540
  quality
2681
3541
  );
2682
- respondOk(rendered);
3542
+ respondOk(await withResolvedTabTarget(tabId, rendered));
2683
3543
  } catch (error) {
2684
3544
  respondError(
2685
3545
  mapScreenshotCaptureError(
@@ -2786,7 +3646,12 @@ var DriveSocket = class {
2786
3646
  srcW2,
2787
3647
  srcH2
2788
3648
  );
2789
- respondOk(await canvasToResult(cropCanvas2));
3649
+ respondOk(
3650
+ await withResolvedTabTarget(
3651
+ tabId,
3652
+ await canvasToResult(cropCanvas2)
3653
+ )
3654
+ );
2790
3655
  } finally {
2791
3656
  bitmap.close();
2792
3657
  }
@@ -2816,7 +3681,12 @@ var DriveSocket = class {
2816
3681
  srcW,
2817
3682
  srcH
2818
3683
  );
2819
- respondOk(await canvasToResult(cropCanvas));
3684
+ respondOk(
3685
+ await withResolvedTabTarget(
3686
+ tabId,
3687
+ await canvasToResult(cropCanvas)
3688
+ )
3689
+ );
2820
3690
  } catch (error) {
2821
3691
  respondError(
2822
3692
  mapScreenshotCaptureError(
@@ -2835,7 +3705,12 @@ var DriveSocket = class {
2835
3705
  try {
2836
3706
  const metaInfo = await getMetaInfo();
2837
3707
  const canvas = await captureFullPageCanvas(metaInfo);
2838
- respondOk(await canvasToResult(canvas));
3708
+ respondOk(
3709
+ await withResolvedTabTarget(
3710
+ tabId,
3711
+ await canvasToResult(canvas)
3712
+ )
3713
+ );
2839
3714
  } catch (error) {
2840
3715
  respondError(
2841
3716
  mapScreenshotCaptureError(
@@ -2973,7 +3848,23 @@ var DriveSocket = class {
2973
3848
  }
2974
3849
  };
2975
3850
  }
2976
- return { ok: true, point: { x, y } };
3851
+ const targetState = coercePopupTriggerState(record.target_state);
3852
+ return {
3853
+ ok: true,
3854
+ point: {
3855
+ x,
3856
+ y,
3857
+ ...targetState ? { targetState } : {}
3858
+ }
3859
+ };
3860
+ }
3861
+ async focusLocator(tabId, locator) {
3862
+ const focused = await sendToTab(tabId, "drive.focus_locator", {
3863
+ locator
3864
+ });
3865
+ if (!focused.ok) {
3866
+ throw new Error(focused.error.message);
3867
+ }
2977
3868
  }
2978
3869
  async performCdpType(tabId, options) {
2979
3870
  const targetPoint = await sendToTab(tabId, "drive.type_target_point", {
@@ -3163,132 +4054,25 @@ var DriveSocket = class {
3163
4054
  error: sanitizeDriveErrorInfo(error)
3164
4055
  });
3165
4056
  };
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
- });
4057
+ await dispatchDebuggerRequest(
4058
+ message,
4059
+ {
4060
+ getSession: (tabId) => this.debuggerSessions.get(tabId),
4061
+ ensureDebuggerAttached: async (tabId) => await this.ensureDebuggerAttached(tabId),
4062
+ detachDebugger: async (tabId) => await this.detachDebugger(tabId),
4063
+ sendDebuggerCommand: async (tabId, method, params, timeoutMs) => await this.sendDebuggerCommand(tabId, method, params, timeoutMs),
4064
+ touchDebuggerSession: (tabId) => this.touchDebuggerSession(tabId),
4065
+ clearDebuggerSession: (tabId) => this.clearDebuggerSession(tabId),
4066
+ mapDebuggerErrorMessage,
4067
+ debuggerCommandTimeoutMs: DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
4068
+ },
4069
+ {
4070
+ respondAck,
4071
+ respondError
3278
4072
  }
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
- }
4073
+ );
3287
4074
  }
3288
4075
  async handleDebuggerEvent(source, method, params) {
3289
- if (!await readDebuggerCapabilityEnabled()) {
3290
- return;
3291
- }
3292
4076
  const tabId = source.tabId;
3293
4077
  if (typeof tabId !== "number") {
3294
4078
  return;
@@ -3307,9 +4091,6 @@ var DriveSocket = class {
3307
4091
  return;
3308
4092
  }
3309
4093
  this.clearDebuggerSession(tabId);
3310
- if (!await readDebuggerCapabilityEnabled()) {
3311
- return;
3312
- }
3313
4094
  this.sendDebuggerEvent({
3314
4095
  tab_id: tabId,
3315
4096
  method: "Debugger.detached",
@@ -3402,12 +4183,11 @@ var DriveSocket = class {
3402
4183
  const tab = await getTab(tabId);
3403
4184
  const url = typeof tab.url === "string" ? tab.url : void 0;
3404
4185
  if (isRestrictedUrl(url)) {
3405
- return {
3406
- code: "NOT_SUPPORTED",
3407
- message: "Debugger cannot attach to restricted pages.",
3408
- retryable: false,
3409
- details: { url }
3410
- };
4186
+ return buildRestrictedUrlError({
4187
+ url: url ?? "about:blank",
4188
+ operation: "debugger",
4189
+ action: "debugger.attach"
4190
+ });
3411
4191
  }
3412
4192
  } catch (error) {
3413
4193
  return mapDebuggerErrorMessage(
@@ -3536,11 +4316,25 @@ var DebuggerTimeoutError = class extends Error {
3536
4316
  };
3537
4317
  var socket = new DriveSocket();
3538
4318
  var permissionPrompts = new PermissionPromptController();
4319
+ var permissionsRequests = new PermissionsRequestController();
3539
4320
  chrome.runtime.onConnect.addListener((port) => {
3540
4321
  permissionPrompts.handleConnect(port);
4322
+ permissionsRequests.handleConnect(port);
3541
4323
  });
3542
4324
  chrome.windows.onRemoved.addListener((windowId) => {
3543
4325
  permissionPrompts.handleWindowRemoved(windowId);
4326
+ permissionsRequests.handleWindowRemoved(windowId);
4327
+ });
4328
+ chrome.windows.onFocusChanged.addListener((windowId) => {
4329
+ if (windowId === chrome.windows.WINDOW_ID_NONE) {
4330
+ return;
4331
+ }
4332
+ void queryActiveTabIdInWindow(windowId).then((tabId) => {
4333
+ markTabActive(tabId);
4334
+ socket.sendTabReport();
4335
+ }).catch(() => {
4336
+ socket.sendTabReport();
4337
+ });
3544
4338
  });
3545
4339
  chrome.tabs.onActivated.addListener((activeInfo) => {
3546
4340
  markTabActive(activeInfo.tabId);
@@ -3615,26 +4409,23 @@ chrome.storage.onChanged.addListener(
3615
4409
  if (areaName !== "local") {
3616
4410
  return;
3617
4411
  }
3618
- const debuggerChange = changes[DEBUGGER_CAPABILITY_ENABLED_KEY];
3619
- if (!debuggerChange) {
4412
+ const corePortChange = changes[LEGACY_CORE_PORT_KEY];
4413
+ if (!corePortChange) {
3620
4414
  return;
3621
4415
  }
3622
- if (typeof debuggerChange.newValue === "boolean") {
3623
- void socket.handleDebuggerCapabilityChange(debuggerChange.newValue).catch((error) => {
3624
- console.error(
3625
- "DriveSocket handleDebuggerCapabilityChange failed:",
3626
- error
3627
- );
4416
+ let work = Promise.resolve();
4417
+ if (corePortChange) {
4418
+ work = work.then(async () => {
4419
+ await clearLegacyCorePort(chrome.storage.local);
3628
4420
  });
3629
- return;
3630
4421
  }
3631
- void socket.refreshDebuggerCapabilityState().catch((error) => {
3632
- console.error(
3633
- "DriveSocket refreshDebuggerCapabilityState failed:",
3634
- error
3635
- );
4422
+ void work.catch((error) => {
4423
+ console.error("DriveSocket storage change handling failed:", error);
3636
4424
  });
3637
4425
  }
3638
4426
  );
4427
+ void clearLegacyCorePort(chrome.storage.local).catch((error) => {
4428
+ console.warn("Failed to clear legacy corePort storage.", error);
4429
+ });
3639
4430
  socket.start();
3640
4431
  //# sourceMappingURL=background.js.map