@browserbridge/bbx 1.0.0 → 1.1.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.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
package/manifest.json DELETED
@@ -1,52 +0,0 @@
1
- {
2
- "manifest_version": 3,
3
- "minimum_chrome_version": "114",
4
- "name": "Browser Bridge",
5
- "version": "1.0.0",
6
- "description": "Local bridge between your coding agent and browser. Structured access to DOM, styles, console, network, and reversible patches.",
7
- "icons": {
8
- "16": "packages/extension/assets/icon-16.png",
9
- "32": "packages/extension/assets/icon-32.png",
10
- "48": "packages/extension/assets/icon-48.png",
11
- "128": "packages/extension/assets/icon-128.png"
12
- },
13
- "permissions": [
14
- "activeTab",
15
- "alarms",
16
- "debugger",
17
- "nativeMessaging",
18
- "scripting",
19
- "sidePanel",
20
- "storage",
21
- "tabs",
22
- "offscreen"
23
- ],
24
- "host_permissions": [
25
- "<all_urls>"
26
- ],
27
- "background": {
28
- "service_worker": "packages/extension/src/background.js",
29
- "type": "module"
30
- },
31
- "action": {
32
- "default_title": "Browser Bridge",
33
- "default_popup": "packages/extension/ui/popup.html",
34
- "default_icon": {
35
- "16": "packages/extension/assets/icon-16.png",
36
- "32": "packages/extension/assets/icon-32.png"
37
- }
38
- },
39
- "side_panel": {
40
- "default_path": "packages/extension/ui/sidepanel.html"
41
- },
42
- "web_accessible_resources": [
43
- {
44
- "resources": [
45
- "packages/extension/ui/ui.css"
46
- ],
47
- "matches": [
48
- "<all_urls>"
49
- ]
50
- }
51
- ]
52
- }
@@ -1,459 +0,0 @@
1
- // @ts-check
2
-
3
- import {
4
- ERROR_CODES,
5
- estimateJsonPayloadCost,
6
- getMethodCapability,
7
- getCostClass,
8
- getUtf8ByteLength,
9
- isDebuggerBackedMethod,
10
- } from '../../protocol/src/index.js';
11
-
12
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
13
- /** @typedef {import('../../protocol/src/types.js').Capability} Capability */
14
- /** @typedef {import('../../protocol/src/types.js').ErrorCode} ErrorCode */
15
-
16
- const INTERACTIVE_AX_ROLES = new Set([
17
- 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',
18
- 'listbox', 'menuitem', 'tab', 'switch', 'slider', 'spinbutton',
19
- 'searchbox', 'menuitemcheckbox', 'menuitemradio', 'option'
20
- ]);
21
-
22
- /**
23
- * @param {chrome.tabs.Tab} tab
24
- * @param {string} method
25
- * @returns {{ method: string, tabId: number | null, windowId: number | null, url: string, title: string, status: string }}
26
- */
27
- export function summarizeTabResult(tab, method) {
28
- return {
29
- method,
30
- tabId: typeof tab.id === 'number' ? tab.id : null,
31
- windowId: typeof tab.windowId === 'number' ? tab.windowId : null,
32
- url: tab.url ?? '',
33
- title: tab.title ?? '',
34
- status: tab.status ?? 'unknown'
35
- };
36
- }
37
-
38
- /**
39
- * @param {unknown} prop
40
- * @returns {string}
41
- */
42
- export function axValue(prop) {
43
- if (!prop || typeof prop !== 'object') return '';
44
- const val = /** @type {{ value?: unknown }} */ (prop).value;
45
- return typeof val === 'string' ? val : '';
46
- }
47
-
48
- /**
49
- * @param {unknown} prop
50
- * @returns {boolean}
51
- */
52
- export function axBool(prop) {
53
- if (!prop || typeof prop !== 'object') return false;
54
- return /** @type {{ value?: unknown }} */ (prop).value === true;
55
- }
56
-
57
- /**
58
- * @param {unknown} prop
59
- * @returns {string | null}
60
- */
61
- export function axTristateValue(prop) {
62
- if (!prop || typeof prop !== 'object') return null;
63
- const val = /** @type {{ value?: unknown }} */ (prop).value;
64
- if (val === 'true' || val === true) return 'true';
65
- if (val === 'false' || val === false) return 'false';
66
- if (val === 'mixed') return 'mixed';
67
- return null;
68
- }
69
-
70
- /**
71
- * @param {Record<string, unknown>} node
72
- * @returns {{ nodeId: string, role: string, name: string, description: string, value: string, focused: boolean, required: boolean, checked: string | null, disabled: boolean, interactive: boolean, childIds: string[] }}
73
- */
74
- export function simplifyAXNode(node) {
75
- const role = axValue(node.role);
76
- return {
77
- nodeId: String(node.nodeId ?? ''),
78
- role,
79
- name: axValue(node.name),
80
- description: axValue(node.description),
81
- value: axValue(node.value),
82
- focused: axBool(node.focused),
83
- required: axBool(node.required),
84
- checked: axTristateValue(node.checked),
85
- disabled: axBool(node.disabled),
86
- interactive: INTERACTIVE_AX_ROLES.has(role) || axBool(node.focusable),
87
- childIds: Array.isArray(node.childIds) ? node.childIds.map(String) : []
88
- };
89
- }
90
-
91
- /**
92
- * @param {string} method
93
- * @returns {boolean}
94
- */
95
- export function shouldLogAction(method) {
96
- return ![
97
- 'health.ping',
98
- 'log.tail',
99
- 'skill.get_runtime_context',
100
- 'setup.get_status',
101
- 'setup.install',
102
- 'tabs.list'
103
- ].includes(method);
104
- }
105
-
106
- /**
107
- * Treat page exceptions as part of the error stream so filtered reads return
108
- * runtime failures alongside explicit `console.error` calls.
109
- *
110
- * @param {string} requestedLevel
111
- * @param {string} entryLevel
112
- * @returns {boolean}
113
- */
114
- export function matchesConsoleLevel(requestedLevel, entryLevel) {
115
- if (requestedLevel === entryLevel) {
116
- return true;
117
- }
118
- if (requestedLevel === 'error') {
119
- return entryLevel === 'exception' || entryLevel === 'rejection';
120
- }
121
- return false;
122
- }
123
-
124
- /**
125
- * @param {BridgeResponse} response
126
- * @returns {string}
127
- */
128
- export function summarizeActionResult(response) {
129
- if (!response.ok) {
130
- return response.error.message;
131
- }
132
-
133
- const result = response.result && typeof response.result === 'object'
134
- ? /** @type {Record<string, unknown>} */ (response.result)
135
- : {};
136
-
137
- if (typeof result.patchId === 'string') {
138
- return `Patch ${result.patchId} applied.`;
139
- }
140
-
141
- if (Array.isArray(result.nodes)) {
142
- return `${result.nodes.length} node(s) returned.`;
143
- }
144
-
145
- if (typeof result.image === 'string') {
146
- return 'Partial screenshot captured.';
147
- }
148
-
149
- return 'Completed successfully.';
150
- }
151
-
152
- /**
153
- * Estimate approximate token cost from a bridge response.
154
- *
155
- * @param {BridgeResponse} response
156
- * @returns {{
157
- * responseBytes: number,
158
- * approxTokens: number,
159
- * textBytes: number,
160
- * textApproxTokens: number,
161
- * imageApproxTokens: number,
162
- * imageBytes: number,
163
- * hasScreenshot: boolean,
164
- * nodeCount: number | null
165
- * }}
166
- */
167
- export function estimateResponseTokens(response) {
168
- const payload = response.ok
169
- ? response.result
170
- : { error: response.error };
171
- const estimate = estimateJsonPayloadCost(payload);
172
- const responseBytes = estimate.bytes;
173
- const result = response.ok && response.result && typeof response.result === 'object'
174
- ? /** @type {Record<string, unknown>} */ (response.result)
175
- : null;
176
- const hasScreenshot = result != null && typeof result.image === 'string';
177
- const nodeCount = result != null && Array.isArray(result.nodes) ? result.nodes.length : null;
178
- const textPayload = hasScreenshot && result != null
179
- ? omitScreenshotImage(result)
180
- : payload;
181
- const textEstimate = estimateJsonPayloadCost(textPayload);
182
- const imageTransportBytes = Math.max(0, responseBytes - textEstimate.bytes);
183
- const imageBytes = hasScreenshot && result != null
184
- ? estimateInlineImageBytes(result.image)
185
- : 0;
186
-
187
- return {
188
- responseBytes,
189
- approxTokens: estimate.approxTokens,
190
- textBytes: textEstimate.bytes,
191
- textApproxTokens: textEstimate.approxTokens,
192
- imageApproxTokens: imageTransportBytes === 0 ? 0 : Math.ceil(imageTransportBytes / 4),
193
- imageBytes,
194
- hasScreenshot,
195
- nodeCount,
196
- };
197
- }
198
-
199
- /**
200
- * @param {string} method
201
- * @param {BridgeResponse} response
202
- * @returns {{
203
- * responseBytes: number,
204
- * approxTokens: number,
205
- * textBytes: number,
206
- * textApproxTokens: number,
207
- * imageApproxTokens: number,
208
- * imageBytes: number,
209
- * hasScreenshot: boolean,
210
- * nodeCount: number | null,
211
- * costClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
212
- * textCostClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
213
- * debuggerBacked: boolean
214
- * }}
215
- */
216
- export function getResponseDiagnostics(method, response) {
217
- const estimate = estimateResponseTokens(response);
218
- return {
219
- ...estimate,
220
- costClass: getCostClass(estimate.approxTokens),
221
- textCostClass: getCostClass(estimate.textApproxTokens),
222
- debuggerBacked: isDebuggerBackedMethod(method),
223
- };
224
- }
225
-
226
- /**
227
- * Keep screenshot metadata while excluding the large inline image payload from
228
- * token-oriented UI estimates.
229
- *
230
- * @param {Record<string, unknown>} result
231
- * @returns {Record<string, unknown>}
232
- */
233
- function omitScreenshotImage(result) {
234
- const textPayload = { ...result };
235
- delete textPayload.image;
236
- return textPayload;
237
- }
238
-
239
- /**
240
- * Estimate decoded image bytes for data URLs so the UI can show image size
241
- * without pretending the base64 blob is text-token traffic.
242
- *
243
- * @param {unknown} image
244
- * @returns {number}
245
- */
246
- function estimateInlineImageBytes(image) {
247
- if (typeof image !== 'string' || image.length === 0) {
248
- return 0;
249
- }
250
-
251
- const match = /^data:[^;]+;base64,([A-Za-z0-9+/=\s]+)$/u.exec(image);
252
- if (!match) {
253
- return getUtf8ByteLength(image);
254
- }
255
-
256
- const base64 = match[1].replace(/\s+/gu, '');
257
- if (base64.length === 0) {
258
- return 0;
259
- }
260
-
261
- const padding = base64.endsWith('==')
262
- ? 2
263
- : base64.endsWith('=')
264
- ? 1
265
- : 0;
266
- return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
267
- }
268
-
269
- /**
270
- * Deterministically trim oversized success payloads to fit within an
271
- * approximate token budget. This prefers shrinking large strings and slicing
272
- * top-level result arrays before falling back to a compact continuation payload.
273
- *
274
- * @param {string} method
275
- * @param {BridgeResponse} response
276
- * @param {number | null | undefined} tokenBudget
277
- * @returns {BridgeResponse}
278
- */
279
- export function enforceTokenBudget(method, response, tokenBudget) {
280
- if (!response.ok || typeof tokenBudget !== 'number' || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
281
- return response;
282
- }
283
-
284
- const maxBytes = Math.max(128, Math.floor(tokenBudget * 4));
285
- const responseBytes = estimateJsonPayloadCost(response.result).bytes;
286
- if (responseBytes <= maxBytes) {
287
- return {
288
- ...response,
289
- meta: {
290
- ...response.meta,
291
- budget_applied: false,
292
- budget_truncated: false,
293
- continuation_hint: null,
294
- },
295
- };
296
- }
297
-
298
- const cloned = cloneJsonValue(response.result);
299
- let truncated = false;
300
- let iterations = 0;
301
- const MAX_BUDGET_ITERATIONS = 100;
302
- while (estimateJsonPayloadCost(cloned).bytes > maxBytes && shrinkForBudget(cloned) && iterations < MAX_BUDGET_ITERATIONS) {
303
- truncated = true;
304
- iterations += 1;
305
- }
306
-
307
- let result = cloned;
308
- if (estimateJsonPayloadCost(result).bytes > maxBytes) {
309
- result = {
310
- truncated: true,
311
- continuationHint: `Retry ${method} with a larger token budget or tighter params.`,
312
- };
313
- truncated = true;
314
- }
315
-
316
- return {
317
- ...response,
318
- result,
319
- meta: {
320
- ...response.meta,
321
- budget_applied: true,
322
- budget_truncated: truncated,
323
- continuation_hint: truncated
324
- ? `Retry ${method} with a larger token budget or tighter params.`
325
- : null,
326
- },
327
- };
328
- }
329
-
330
- /**
331
- * @param {unknown} value
332
- * @returns {any}
333
- */
334
- function cloneJsonValue(value) {
335
- return value == null ? value : JSON.parse(JSON.stringify(value));
336
- }
337
-
338
- /**
339
- * @param {any} value
340
- * @returns {boolean}
341
- */
342
- function shrinkForBudget(value) {
343
- if (!value || typeof value !== 'object') {
344
- return false;
345
- }
346
-
347
- if (Array.isArray(value)) {
348
- if (value.length > 1) {
349
- const nextLength = Math.max(1, Math.floor(value.length * 0.75));
350
- value.splice(nextLength);
351
- return true;
352
- }
353
- return value.length === 1 ? shrinkForBudget(value[0]) : false;
354
- }
355
-
356
- for (const key of ['image', 'html', 'text', 'value']) {
357
- if (typeof value[key] === 'string' && value[key].length > 64) {
358
- value[key] = key === 'image'
359
- ? '[omitted image over token budget]'
360
- : `${value[key].slice(0, Math.max(32, Math.floor(value[key].length * 0.75) - 1))}\u2026`;
361
- if (typeof value.truncated !== 'boolean') {
362
- value.truncated = true;
363
- }
364
- return true;
365
- }
366
- }
367
-
368
- for (const key of ['nodes', 'entries', 'tabs', 'patches']) {
369
- if (Array.isArray(value[key]) && value[key].length > 1) {
370
- const originalLength = value[key].length;
371
- const nextLength = Math.max(1, Math.floor(originalLength * 0.75));
372
- value[key].splice(nextLength);
373
- if (typeof value.count !== 'number') {
374
- value.count = originalLength;
375
- }
376
- if (typeof value.total !== 'number') {
377
- value.total = originalLength;
378
- }
379
- value.truncated = true;
380
- return true;
381
- }
382
- }
383
-
384
- for (const entry of Object.values(value)) {
385
- if (shrinkForBudget(entry)) {
386
- if (typeof value.truncated !== 'boolean') {
387
- value.truncated = true;
388
- }
389
- return true;
390
- }
391
- }
392
-
393
- const keys = Object.keys(value);
394
- if (keys.length > 2) {
395
- delete value[keys[keys.length - 1]];
396
- value.truncated = true;
397
- return true;
398
- }
399
-
400
- return false;
401
- }
402
-
403
- /**
404
- * @param {unknown} error
405
- * @returns {string}
406
- */
407
- export function getErrorMessage(error) {
408
- if (typeof error === 'string') {
409
- return error;
410
- }
411
- if (error instanceof Error) {
412
- return error.message;
413
- }
414
- return 'Unexpected extension error.';
415
- }
416
-
417
- /**
418
- * @param {string} message
419
- * @returns {string}
420
- */
421
- export function normalizeRuntimeErrorMessage(message) {
422
- return /^No tab with id[: ]/i.test(message)
423
- ? ERROR_CODES.TAB_MISMATCH
424
- : message;
425
- }
426
-
427
- /**
428
- * @param {{ x?: number, y?: number, width?: number, height?: number, scale?: number }} [rect={}]
429
- * @returns {{ x: number, y: number, width: number, height: number }}
430
- */
431
- export function normalizeCropRect(rect = {}) {
432
- const scale = Number(rect.scale) || 1;
433
- return {
434
- x: Math.max(0, Math.round((rect.x || 0) * scale)),
435
- y: Math.max(0, Math.round((rect.y || 0) * scale)),
436
- width: Math.max(1, Math.round((rect.width || 1) * scale)),
437
- height: Math.max(1, Math.round((rect.height || 1) * scale))
438
- };
439
- }
440
-
441
- /**
442
- * @param {string} url
443
- * @returns {string}
444
- */
445
- export function safeOrigin(url) {
446
- try {
447
- return new URL(url).origin;
448
- } catch {
449
- return '';
450
- }
451
- }
452
-
453
- /**
454
- * @param {string} method
455
- * @returns {Capability | null}
456
- */
457
- export function inferCapability(method) {
458
- return getMethodCapability(/** @type {import('../../protocol/src/types.js').BridgeMethod} */ (method));
459
- }
@@ -1,91 +0,0 @@
1
- // @ts-check
2
-
3
- import { ERROR_CODES } from '../../protocol/src/index.js';
4
-
5
- /**
6
- * @typedef {{
7
- * tabId: number,
8
- * windowId: number,
9
- * title: string,
10
- * url: string
11
- * }} ResolvedTabTarget
12
- */
13
-
14
- /**
15
- * @param {string} url
16
- * @returns {boolean}
17
- */
18
- export function isRestrictedAutomationUrl(url) {
19
- return /^(about:|chrome:|chrome-extension:|chrome-search:|devtools:|edge:|brave:|moz-extension:|view-source:)/i.test(url);
20
- }
21
-
22
- /**
23
- * @param {number | null | undefined} requestTabId
24
- * @param {chrome.tabs.Tab | null | undefined} explicitTab
25
- * @param {chrome.tabs.Tab | null | undefined} activeTab
26
- * @returns {chrome.tabs.Tab | null}
27
- */
28
- export function selectRequestTabCandidate(requestTabId, explicitTab, activeTab) {
29
- if (typeof requestTabId === 'number' && Number.isFinite(requestTabId)) {
30
- return explicitTab ?? null;
31
- }
32
- return activeTab ?? null;
33
- }
34
-
35
- /**
36
- * @param {chrome.tabs.Tab | null | undefined} tab
37
- * @param {number} enabledWindowId
38
- * @param {{ requireScriptable?: boolean }} [options]
39
- * @returns {ResolvedTabTarget}
40
- */
41
- export function resolveWindowScopedTab(tab, enabledWindowId, options = {}) {
42
- const requireScriptable = options.requireScriptable !== false;
43
- if (
44
- typeof tab?.id !== 'number'
45
- || !Number.isFinite(tab.id)
46
- || typeof tab.windowId !== 'number'
47
- ) {
48
- throw new Error(ERROR_CODES.TAB_MISMATCH);
49
- }
50
- if (tab.windowId !== enabledWindowId) {
51
- throw new Error(ERROR_CODES.ACCESS_DENIED);
52
- }
53
- if (typeof tab.url !== 'string' || !tab.url) {
54
- throw new Error(ERROR_CODES.TAB_MISMATCH);
55
- }
56
- if (requireScriptable && isRestrictedAutomationUrl(tab.url)) {
57
- throw new Error(ERROR_CODES.ACCESS_DENIED);
58
- }
59
-
60
- return {
61
- tabId: tab.id,
62
- windowId: tab.windowId,
63
- title: tab.title ?? '',
64
- url: tab.url
65
- };
66
- }
67
-
68
- /**
69
- * @param {chrome.tabs.Tab | null | undefined} tab
70
- * @returns {ResolvedTabTarget | null}
71
- */
72
- export function normalizeRequestedAccessTab(tab) {
73
- if (
74
- typeof tab?.id !== 'number'
75
- || !Number.isFinite(tab.id)
76
- || typeof tab.windowId !== 'number'
77
- || typeof tab.url !== 'string'
78
- || !tab.url
79
- ) {
80
- return null;
81
- }
82
- if (isRestrictedAutomationUrl(tab.url)) {
83
- return null;
84
- }
85
- return {
86
- tabId: tab.id,
87
- windowId: tab.windowId,
88
- title: tab.title ?? '',
89
- url: tab.url
90
- };
91
- }