@different-ai/opencode-browser 4.5.1 → 4.6.1

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.
@@ -1,19 +1,200 @@
1
1
  const NATIVE_HOST_NAME = "com.opencode.browser_automation"
2
2
  const KEEPALIVE_ALARM = "keepalive"
3
+ const PERMISSION_HINT = "Click the OpenCode Browser extension icon and approve requested permissions."
4
+ const OPTIONAL_RUNTIME_PERMISSIONS = ["nativeMessaging", "downloads", "debugger"]
5
+ const OPTIONAL_RUNTIME_ORIGINS = ["<all_urls>"]
6
+
7
+ const runtimeManifest = chrome.runtime.getManifest()
8
+ const declaredOptionalPermissions = new Set(runtimeManifest.optional_permissions || [])
9
+ const declaredOptionalOrigins = new Set(runtimeManifest.optional_host_permissions || [])
3
10
 
4
11
  let port = null
5
12
  let isConnected = false
6
13
  let connectionAttempts = 0
14
+ let nativePermissionHintLogged = false
15
+
16
+ // Debugger state management for console/error capture
17
+ const debuggerState = new Map()
18
+ const MAX_LOG_ENTRIES = 1000
19
+
20
+ async function hasPermissions(query) {
21
+ if (!chrome.permissions?.contains) return true
22
+ try {
23
+ return await chrome.permissions.contains(query)
24
+ } catch {
25
+ return false
26
+ }
27
+ }
28
+
29
+ async function hasNativeMessagingPermission() {
30
+ return await hasPermissions({ permissions: ["nativeMessaging"] })
31
+ }
32
+
33
+ async function hasDebuggerPermission() {
34
+ return await hasPermissions({ permissions: ["debugger"] })
35
+ }
36
+
37
+ async function hasDownloadsPermission() {
38
+ return await hasPermissions({ permissions: ["downloads"] })
39
+ }
40
+
41
+ async function hasHostAccessPermission() {
42
+ return await hasPermissions({ origins: ["<all_urls>"] })
43
+ }
44
+
45
+ async function requestOptionalPermissionsFromClick() {
46
+ if (!chrome.permissions?.contains || !chrome.permissions?.request) {
47
+ return { granted: true, requested: false, permissions: [], origins: [] }
48
+ }
49
+
50
+ const permissions = []
51
+ for (const permission of OPTIONAL_RUNTIME_PERMISSIONS) {
52
+ if (!declaredOptionalPermissions.has(permission)) continue
53
+ const granted = await hasPermissions({ permissions: [permission] })
54
+ if (!granted) permissions.push(permission)
55
+ }
56
+
57
+ const origins = []
58
+ for (const origin of OPTIONAL_RUNTIME_ORIGINS) {
59
+ if (!declaredOptionalOrigins.has(origin)) continue
60
+ const granted = await hasPermissions({ origins: [origin] })
61
+ if (!granted) origins.push(origin)
62
+ }
63
+
64
+ if (!permissions.length && !origins.length) {
65
+ return { granted: true, requested: false, permissions, origins }
66
+ }
67
+
68
+ try {
69
+ const granted = await chrome.permissions.request({ permissions, origins })
70
+ return { granted, requested: true, permissions, origins }
71
+ } catch (error) {
72
+ return {
73
+ granted: false,
74
+ requested: true,
75
+ permissions,
76
+ origins,
77
+ error: error?.message || String(error),
78
+ }
79
+ }
80
+ }
81
+
82
+ async function ensureDebuggerAvailable() {
83
+ if (!chrome.debugger?.attach) {
84
+ return {
85
+ ok: false,
86
+ reason: "Debugger API unavailable in this build.",
87
+ }
88
+ }
89
+
90
+ const granted = await hasDebuggerPermission()
91
+ if (!granted) {
92
+ return {
93
+ ok: false,
94
+ reason: `Debugger permission not granted. ${PERMISSION_HINT}`,
95
+ }
96
+ }
97
+
98
+ return { ok: true }
99
+ }
100
+
101
+ async function ensureDownloadsAvailable() {
102
+ if (!chrome.downloads) {
103
+ throw new Error(`Downloads API unavailable in this build. ${PERMISSION_HINT}`)
104
+ }
105
+
106
+ const granted = await hasDownloadsPermission()
107
+ if (!granted) {
108
+ throw new Error(`Downloads permission not granted. ${PERMISSION_HINT}`)
109
+ }
110
+ }
111
+
112
+ async function ensureDebuggerAttached(tabId) {
113
+ const availability = await ensureDebuggerAvailable()
114
+ if (!availability.ok) {
115
+ return {
116
+ attached: false,
117
+ unavailableReason: availability.reason,
118
+ consoleMessages: [],
119
+ pageErrors: [],
120
+ }
121
+ }
122
+
123
+ if (debuggerState.has(tabId)) return debuggerState.get(tabId)
124
+
125
+ const state = { attached: false, consoleMessages: [], pageErrors: [] }
126
+ debuggerState.set(tabId, state)
127
+
128
+ try {
129
+ await chrome.debugger.attach({ tabId }, "1.3")
130
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable")
131
+ state.attached = true
132
+ } catch (e) {
133
+ console.warn("[OpenCode] Failed to attach debugger:", e.message || e)
134
+ }
135
+
136
+ return state
137
+ }
138
+
139
+ if (chrome.debugger?.onEvent) {
140
+ chrome.debugger.onEvent.addListener((source, method, params) => {
141
+ const state = debuggerState.get(source.tabId)
142
+ if (!state) return
143
+
144
+ if (method === "Runtime.consoleAPICalled") {
145
+ if (state.consoleMessages.length >= MAX_LOG_ENTRIES) {
146
+ state.consoleMessages.shift()
147
+ }
148
+ state.consoleMessages.push({
149
+ type: params.type,
150
+ text: params.args.map((a) => a.value ?? a.description ?? "").join(" "),
151
+ timestamp: Date.now(),
152
+ source: params.stackTrace?.callFrames?.[0]?.url,
153
+ line: params.stackTrace?.callFrames?.[0]?.lineNumber,
154
+ })
155
+ }
156
+
157
+ if (method === "Runtime.exceptionThrown") {
158
+ if (state.pageErrors.length >= MAX_LOG_ENTRIES) {
159
+ state.pageErrors.shift()
160
+ }
161
+ state.pageErrors.push({
162
+ message: params.exceptionDetails.text,
163
+ source: params.exceptionDetails.url,
164
+ line: params.exceptionDetails.lineNumber,
165
+ column: params.exceptionDetails.columnNumber,
166
+ stack: params.exceptionDetails.exception?.description,
167
+ timestamp: Date.now(),
168
+ })
169
+ }
170
+ })
171
+ }
172
+
173
+ if (chrome.debugger?.onDetach) {
174
+ chrome.debugger.onDetach.addListener((source) => {
175
+ if (debuggerState.has(source.tabId)) {
176
+ const state = debuggerState.get(source.tabId)
177
+ state.attached = false
178
+ }
179
+ })
180
+ }
181
+
182
+ chrome.tabs.onRemoved.addListener((tabId) => {
183
+ if (debuggerState.has(tabId)) {
184
+ if (chrome.debugger?.detach) chrome.debugger.detach({ tabId }).catch(() => {})
185
+ debuggerState.delete(tabId)
186
+ }
187
+ })
7
188
 
8
189
  chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
9
190
 
10
191
  chrome.alarms.onAlarm.addListener((alarm) => {
11
192
  if (alarm.name === KEEPALIVE_ALARM) {
12
- if (!isConnected) connect()
193
+ if (!isConnected) connect().catch(() => {})
13
194
  }
14
195
  })
15
196
 
16
- function connect() {
197
+ async function connect() {
17
198
  if (port) {
18
199
  try {
19
200
  port.disconnect()
@@ -21,6 +202,19 @@ function connect() {
21
202
  port = null
22
203
  }
23
204
 
205
+ const nativeMessagingAllowed = await hasNativeMessagingPermission()
206
+ if (!nativeMessagingAllowed) {
207
+ isConnected = false
208
+ updateBadge(false)
209
+ if (!nativePermissionHintLogged) {
210
+ nativePermissionHintLogged = true
211
+ console.log(`[OpenCode] Native messaging permission not granted. ${PERMISSION_HINT}`)
212
+ }
213
+ return
214
+ }
215
+
216
+ nativePermissionHintLogged = false
217
+
24
218
  try {
25
219
  port = chrome.runtime.connectNative(NATIVE_HOST_NAME)
26
220
 
@@ -111,6 +305,12 @@ async function executeTool(toolName, args) {
111
305
  query: toolQuery,
112
306
  scroll: toolScroll,
113
307
  wait: toolWait,
308
+ download: toolDownload,
309
+ list_downloads: toolListDownloads,
310
+ set_file_input: toolSetFileInput,
311
+ highlight: toolHighlight,
312
+ console: toolConsole,
313
+ errors: toolErrors,
114
314
  }
115
315
 
116
316
  const fn = tools[toolName]
@@ -129,13 +329,26 @@ async function getTabById(tabId) {
129
329
  }
130
330
 
131
331
  async function runInPage(tabId, command, args) {
132
- const result = await chrome.scripting.executeScript({
133
- target: { tabId },
134
- func: pageOps,
135
- args: [command, args || {}],
136
- world: "ISOLATED",
137
- })
138
- return result[0]?.result
332
+ const hasHostAccess = await hasHostAccessPermission()
333
+ if (!hasHostAccess) {
334
+ throw new Error(`Site access permission not granted. ${PERMISSION_HINT}`)
335
+ }
336
+
337
+ try {
338
+ const result = await chrome.scripting.executeScript({
339
+ target: { tabId },
340
+ func: pageOps,
341
+ args: [command, args || {}],
342
+ world: "ISOLATED",
343
+ })
344
+ return result[0]?.result
345
+ } catch (error) {
346
+ const message = error?.message || String(error)
347
+ if (message.includes("Cannot access contents of the page")) {
348
+ throw new Error(`Site access permission not granted for this page. ${PERMISSION_HINT}`)
349
+ }
350
+ throw error
351
+ }
139
352
  }
140
353
 
141
354
  async function pageOps(command, args) {
@@ -309,7 +522,7 @@ async function pageOps(command, args) {
309
522
  const results = []
310
523
  const seen = new Set()
311
524
  const candidates = deepQuerySelectorAll(
312
- "button, a, label, option, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem']",
525
+ "button, a, label, option, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem'], [role='option'], [role='listitem'], [role='row'], [tabindex]",
313
526
  document
314
527
  )
315
528
  for (const el of candidates) {
@@ -319,6 +532,23 @@ async function pageOps(command, args) {
319
532
  results.push(el)
320
533
  }
321
534
  }
535
+
536
+ const generic = deepQuerySelectorAll("div, span, li, article", document)
537
+ for (const el of generic) {
538
+ if (!matchesText(el.innerText || el.textContent || "", target)) continue
539
+ const style = window.getComputedStyle(el)
540
+ const likelyInteractive =
541
+ !!el.getAttribute("onclick") ||
542
+ !!el.getAttribute("role") ||
543
+ el.tabIndex >= 0 ||
544
+ style.cursor === "pointer"
545
+ if (!likelyInteractive) continue
546
+ if (!seen.has(el)) {
547
+ seen.add(el)
548
+ results.push(el)
549
+ }
550
+ }
551
+
322
552
  const inputs = deepQuerySelectorAll("input[type='button'], input[type='submit'], input[type='reset']", document)
323
553
  for (const el of inputs) {
324
554
  if (!matchesText(el.value || "", target)) continue
@@ -609,6 +839,67 @@ async function pageOps(command, args) {
609
839
  }
610
840
  }
611
841
 
842
+ if (command === "set_file_input") {
843
+ const rawFiles = Array.isArray(options.files) ? options.files : options.files ? [options.files] : []
844
+ if (!rawFiles.length) return { ok: false, error: "files is required" }
845
+
846
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
847
+ if (!match.chosen) {
848
+ return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
849
+ }
850
+
851
+ const tag = match.chosen.tagName
852
+ if (tag !== "INPUT" || match.chosen.type !== "file") {
853
+ return { ok: false, error: `Element is not a file input: ${match.selectorUsed} (${tag.toLowerCase()})` }
854
+ }
855
+
856
+ function decodeBase64(value) {
857
+ const raw = safeString(value)
858
+ const b64 = raw.includes(",") ? raw.split(",").pop() : raw
859
+ const binary = atob(b64)
860
+ const bytes = new Uint8Array(binary.length)
861
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
862
+ return bytes
863
+ }
864
+
865
+ const dt = new DataTransfer()
866
+ const names = []
867
+
868
+ for (const fileInfo of rawFiles) {
869
+ const name = safeString(fileInfo?.name) || "upload.bin"
870
+ const mimeType = safeString(fileInfo?.mimeType) || "application/octet-stream"
871
+ const base64 = safeString(fileInfo?.base64)
872
+ if (!base64) return { ok: false, error: "file.base64 is required" }
873
+ const bytes = decodeBase64(base64)
874
+ const file = new File([bytes], name, { type: mimeType, lastModified: Date.now() })
875
+ dt.items.add(file)
876
+ names.push(name)
877
+ }
878
+
879
+ try {
880
+ match.chosen.scrollIntoView({ block: "center", inline: "center" })
881
+ } catch {}
882
+
883
+ try {
884
+ match.chosen.focus()
885
+ } catch {}
886
+
887
+ try {
888
+ match.chosen.files = dt.files
889
+ } catch {
890
+ try {
891
+ Object.defineProperty(match.chosen, "files", { value: dt.files, writable: false })
892
+ } catch {
893
+ return { ok: false, error: "Failed to set file input" }
894
+ }
895
+ }
896
+
897
+ match.chosen.dispatchEvent(new Event("input", { bubbles: true }))
898
+ match.chosen.dispatchEvent(new Event("change", { bubbles: true }))
899
+
900
+ return { ok: true, selectorUsed: match.selectorUsed, count: dt.files.length, names }
901
+ }
902
+
612
903
  if (command === "scroll") {
613
904
  const scrollX = Number.isFinite(options.x) ? options.x : 0
614
905
  const scrollY = Number.isFinite(options.y) ? options.y : 0
@@ -617,6 +908,21 @@ async function pageOps(command, args) {
617
908
  if (!match.chosen) {
618
909
  return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
619
910
  }
911
+ if (scrollX || scrollY) {
912
+ try {
913
+ if (typeof match.chosen.scrollBy === "function") {
914
+ match.chosen.scrollBy({ left: scrollX, top: scrollY, behavior: "smooth" })
915
+ } else {
916
+ match.chosen.scrollLeft = Number(match.chosen.scrollLeft || 0) + scrollX
917
+ match.chosen.scrollTop = Number(match.chosen.scrollTop || 0) + scrollY
918
+ }
919
+ } catch {
920
+ match.chosen.scrollLeft = Number(match.chosen.scrollLeft || 0) + scrollX
921
+ match.chosen.scrollTop = Number(match.chosen.scrollTop || 0) + scrollY
922
+ }
923
+ return { ok: true, selectorUsed: match.selectorUsed, elementScroll: { x: scrollX, y: scrollY } }
924
+ }
925
+
620
926
  try {
621
927
  match.chosen.scrollIntoView({ behavior: "smooth", block: "center" })
622
928
  } catch {}
@@ -626,6 +932,73 @@ async function pageOps(command, args) {
626
932
  return { ok: true }
627
933
  }
628
934
 
935
+ if (command === "highlight") {
936
+ const duration = Number.isFinite(options.duration) ? options.duration : 3000
937
+ const color = typeof options.color === "string" ? options.color : "#ff0000"
938
+ const showInfo = !!options.showInfo
939
+
940
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
941
+ if (!match.chosen) {
942
+ return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
943
+ }
944
+
945
+ const el = match.chosen
946
+ const rect = el.getBoundingClientRect()
947
+
948
+ // Remove any existing highlight overlay
949
+ const existing = document.getElementById("__opc_highlight_overlay")
950
+ if (existing) existing.remove()
951
+
952
+ // Create overlay
953
+ const overlay = document.createElement("div")
954
+ overlay.id = "__opc_highlight_overlay"
955
+ overlay.style.cssText = `
956
+ position: fixed;
957
+ top: ${rect.top}px;
958
+ left: ${rect.left}px;
959
+ width: ${rect.width}px;
960
+ height: ${rect.height}px;
961
+ border: 3px solid ${color};
962
+ box-shadow: 0 0 10px ${color};
963
+ pointer-events: none;
964
+ z-index: 2147483647;
965
+ transition: opacity 0.3s;
966
+ `
967
+
968
+ if (showInfo) {
969
+ const info = document.createElement("div")
970
+ info.style.cssText = `
971
+ position: absolute;
972
+ top: -25px;
973
+ left: 0;
974
+ background: ${color};
975
+ color: white;
976
+ padding: 2px 8px;
977
+ font-size: 12px;
978
+ font-family: monospace;
979
+ border-radius: 3px;
980
+ white-space: nowrap;
981
+ `
982
+ info.textContent = `${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}`
983
+ overlay.appendChild(info)
984
+ }
985
+
986
+ document.body.appendChild(overlay)
987
+
988
+ setTimeout(() => {
989
+ overlay.style.opacity = "0"
990
+ setTimeout(() => overlay.remove(), 300)
991
+ }, duration)
992
+
993
+ return {
994
+ ok: true,
995
+ selectorUsed: match.selectorUsed,
996
+ highlighted: true,
997
+ tag: el.tagName,
998
+ id: el.id || null,
999
+ }
1000
+ }
1001
+
629
1002
  if (command === "query") {
630
1003
  if (mode === "page_text") {
631
1004
  if (selectors.length && timeoutMs > 0) {
@@ -986,16 +1359,250 @@ async function toolWait({ ms = 1000, tabId }) {
986
1359
  return { tabId, content: `Waited ${ms}ms` }
987
1360
  }
988
1361
 
989
- chrome.runtime.onInstalled.addListener(() => connect())
990
- chrome.runtime.onStartup.addListener(() => connect())
991
- chrome.action.onClicked.addListener(() => {
992
- connect()
993
- chrome.notifications.create({
994
- type: "basic",
995
- iconUrl: "icons/icon128.png",
996
- title: "OpenCode Browser",
997
- message: isConnected ? "Connected" : "Reconnecting...",
1362
+ function clampNumber(value, min, max, fallback) {
1363
+ const n = Number(value)
1364
+ if (!Number.isFinite(n)) return fallback
1365
+ return Math.min(Math.max(n, min), max)
1366
+ }
1367
+
1368
+ function normalizeDownloadTimeoutMs(value) {
1369
+ return clampNumber(value, 0, 60000, 60000)
1370
+ }
1371
+
1372
+ function waitForNextDownloadCreated(timeoutMs) {
1373
+ const timeout = normalizeDownloadTimeoutMs(timeoutMs)
1374
+ return new Promise((resolve, reject) => {
1375
+ const listener = (item) => {
1376
+ cleanup()
1377
+ resolve(item)
1378
+ }
1379
+
1380
+ const timer = timeout
1381
+ ? setTimeout(() => {
1382
+ cleanup()
1383
+ reject(new Error("Timed out waiting for download to start"))
1384
+ }, timeout)
1385
+ : null
1386
+
1387
+ function cleanup() {
1388
+ chrome.downloads.onCreated.removeListener(listener)
1389
+ if (timer) clearTimeout(timer)
1390
+ }
1391
+
1392
+ chrome.downloads.onCreated.addListener(listener)
1393
+ })
1394
+ }
1395
+
1396
+ async function getDownloadById(downloadId) {
1397
+ const items = await chrome.downloads.search({ id: downloadId })
1398
+ return items && items.length ? items[0] : null
1399
+ }
1400
+
1401
+ async function waitForDownloadCompletion(downloadId, timeoutMs) {
1402
+ const timeout = normalizeDownloadTimeoutMs(timeoutMs)
1403
+ const pollMs = 200
1404
+ const endAt = Date.now() + timeout
1405
+
1406
+ while (true) {
1407
+ const item = await getDownloadById(downloadId)
1408
+ if (item && (item.state === "complete" || item.state === "interrupted")) return item
1409
+ if (!timeout || Date.now() >= endAt) return item
1410
+ await new Promise((resolve) => setTimeout(resolve, pollMs))
1411
+ }
1412
+ }
1413
+
1414
+ async function toolDownload({
1415
+ url,
1416
+ selector,
1417
+ filename,
1418
+ conflictAction,
1419
+ saveAs = false,
1420
+ wait = false,
1421
+ downloadTimeoutMs,
1422
+ tabId,
1423
+ index = 0,
1424
+ timeoutMs,
1425
+ pollMs,
1426
+ }) {
1427
+ const hasUrl = typeof url === "string" && url.trim()
1428
+ const hasSelector = typeof selector === "string" && selector.trim()
1429
+
1430
+ await ensureDownloadsAvailable()
1431
+
1432
+ if (!hasUrl && !hasSelector) throw new Error("url or selector is required")
1433
+ if (hasUrl && hasSelector) throw new Error("Provide either url or selector, not both")
1434
+
1435
+ let downloadId = null
1436
+
1437
+ if (hasUrl) {
1438
+ const options = { url: url.trim() }
1439
+ if (typeof filename === "string" && filename.trim()) options.filename = filename.trim()
1440
+ if (typeof conflictAction === "string" && conflictAction.trim()) options.conflictAction = conflictAction.trim()
1441
+ if (typeof saveAs === "boolean") options.saveAs = saveAs
1442
+
1443
+ downloadId = await chrome.downloads.download(options)
1444
+ } else {
1445
+ const tab = await getTabById(tabId)
1446
+ const created = waitForNextDownloadCreated(downloadTimeoutMs)
1447
+ const clicked = await runInPage(tab.id, "click", { selector, index, timeoutMs, pollMs })
1448
+ if (!clicked?.ok) throw new Error(clicked?.error || "Click failed")
1449
+ const createdItem = await created
1450
+ downloadId = createdItem?.id
1451
+ }
1452
+
1453
+ if (!Number.isFinite(downloadId)) throw new Error("Download did not start")
1454
+
1455
+ if (!wait) {
1456
+ const item = await getDownloadById(downloadId)
1457
+ return { content: { downloadId, item } }
1458
+ }
1459
+
1460
+ const item = await waitForDownloadCompletion(downloadId, downloadTimeoutMs)
1461
+ return { content: { downloadId, item } }
1462
+ }
1463
+
1464
+ async function toolListDownloads({ limit = 20, state } = {}) {
1465
+ await ensureDownloadsAvailable()
1466
+
1467
+ const limitValue = clampNumber(limit, 1, 200, 20)
1468
+ const query = { orderBy: ["-startTime"], limit: limitValue }
1469
+ if (typeof state === "string" && state.trim()) query.state = state.trim()
1470
+
1471
+ const downloads = await chrome.downloads.search(query)
1472
+ const out = downloads.map((d) => ({
1473
+ id: d.id,
1474
+ url: d.url,
1475
+ filename: d.filename,
1476
+ state: d.state,
1477
+ bytesReceived: d.bytesReceived,
1478
+ totalBytes: d.totalBytes,
1479
+ startTime: d.startTime,
1480
+ endTime: d.endTime,
1481
+ error: d.error,
1482
+ mime: d.mime,
1483
+ }))
1484
+
1485
+ return { content: JSON.stringify({ downloads: out }, null, 2) }
1486
+ }
1487
+
1488
+ async function toolSetFileInput({ selector, tabId, index = 0, timeoutMs, pollMs, files }) {
1489
+ if (!selector) throw new Error("Selector is required")
1490
+ const tab = await getTabById(tabId)
1491
+
1492
+ const result = await runInPage(tab.id, "set_file_input", { selector, index, timeoutMs, pollMs, files })
1493
+ if (!result?.ok) throw new Error(result?.error || "Failed to set file input")
1494
+ const used = result.selectorUsed || selector
1495
+ return { tabId: tab.id, content: JSON.stringify({ selector: used, ...result }, null, 2) }
1496
+ }
1497
+
1498
+ async function toolHighlight({ selector, tabId, index = 0, duration, color, showInfo, timeoutMs, pollMs }) {
1499
+ if (!selector) throw new Error("Selector is required")
1500
+ const tab = await getTabById(tabId)
1501
+
1502
+ const result = await runInPage(tab.id, "highlight", {
1503
+ selector,
1504
+ index,
1505
+ duration,
1506
+ color,
1507
+ showInfo,
1508
+ timeoutMs,
1509
+ pollMs,
998
1510
  })
1511
+ if (!result?.ok) throw new Error(result?.error || "Highlight failed")
1512
+ return {
1513
+ tabId: tab.id,
1514
+ content: JSON.stringify({
1515
+ highlighted: true,
1516
+ tag: result.tag,
1517
+ id: result.id,
1518
+ selectorUsed: result.selectorUsed,
1519
+ }),
1520
+ }
1521
+ }
1522
+
1523
+ async function toolConsole({ tabId, clear = false, filter } = {}) {
1524
+ const tab = await getTabById(tabId)
1525
+ const state = await ensureDebuggerAttached(tab.id)
1526
+
1527
+ if (!state.attached) {
1528
+ return {
1529
+ tabId: tab.id,
1530
+ content: JSON.stringify({
1531
+ error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
1532
+ messages: [],
1533
+ }),
1534
+ }
1535
+ }
1536
+
1537
+ let messages = [...state.consoleMessages]
1538
+
1539
+ if (filter && typeof filter === "string") {
1540
+ const filterType = filter.toLowerCase()
1541
+ messages = messages.filter((m) => m.type === filterType)
1542
+ }
1543
+
1544
+ if (clear) {
1545
+ state.consoleMessages = []
1546
+ }
1547
+
1548
+ return {
1549
+ tabId: tab.id,
1550
+ content: JSON.stringify(messages, null, 2),
1551
+ }
1552
+ }
1553
+
1554
+ async function toolErrors({ tabId, clear = false } = {}) {
1555
+ const tab = await getTabById(tabId)
1556
+ const state = await ensureDebuggerAttached(tab.id)
1557
+
1558
+ if (!state.attached) {
1559
+ return {
1560
+ tabId: tab.id,
1561
+ content: JSON.stringify({
1562
+ error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
1563
+ errors: [],
1564
+ }),
1565
+ }
1566
+ }
1567
+
1568
+ const errors = [...state.pageErrors]
1569
+
1570
+ if (clear) {
1571
+ state.pageErrors = []
1572
+ }
1573
+
1574
+ return {
1575
+ tabId: tab.id,
1576
+ content: JSON.stringify(errors, null, 2),
1577
+ }
1578
+ }
1579
+
1580
+ chrome.runtime.onInstalled.addListener(() => connect().catch(() => {}))
1581
+ chrome.runtime.onStartup.addListener(() => connect().catch(() => {}))
1582
+
1583
+ if (chrome.permissions?.onAdded) {
1584
+ chrome.permissions.onAdded.addListener(() => connect().catch(() => {}))
1585
+ }
1586
+
1587
+ chrome.action.onClicked.addListener(async () => {
1588
+ const permissionResult = await requestOptionalPermissionsFromClick()
1589
+ if (!permissionResult.granted) {
1590
+ updateBadge(false)
1591
+ if (permissionResult.error) {
1592
+ console.warn("[OpenCode] Permission request failed:", permissionResult.error)
1593
+ } else {
1594
+ console.warn("[OpenCode] Permission request denied.")
1595
+ }
1596
+ return
1597
+ }
1598
+
1599
+ if (permissionResult.requested) {
1600
+ const requestedPermissions = permissionResult.permissions.join(", ") || "none"
1601
+ const requestedOrigins = permissionResult.origins.join(", ") || "none"
1602
+ console.log(`[OpenCode] Requested permissions -> permissions: ${requestedPermissions}; origins: ${requestedOrigins}`)
1603
+ }
1604
+
1605
+ await connect()
999
1606
  })
1000
1607
 
1001
- connect()
1608
+ connect().catch(() => {})