@different-ai/opencode-browser 4.5.1 → 4.6.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.
@@ -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) {
@@ -609,6 +822,67 @@ async function pageOps(command, args) {
609
822
  }
610
823
  }
611
824
 
825
+ if (command === "set_file_input") {
826
+ const rawFiles = Array.isArray(options.files) ? options.files : options.files ? [options.files] : []
827
+ if (!rawFiles.length) return { ok: false, error: "files is required" }
828
+
829
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
830
+ if (!match.chosen) {
831
+ return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
832
+ }
833
+
834
+ const tag = match.chosen.tagName
835
+ if (tag !== "INPUT" || match.chosen.type !== "file") {
836
+ return { ok: false, error: `Element is not a file input: ${match.selectorUsed} (${tag.toLowerCase()})` }
837
+ }
838
+
839
+ function decodeBase64(value) {
840
+ const raw = safeString(value)
841
+ const b64 = raw.includes(",") ? raw.split(",").pop() : raw
842
+ const binary = atob(b64)
843
+ const bytes = new Uint8Array(binary.length)
844
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
845
+ return bytes
846
+ }
847
+
848
+ const dt = new DataTransfer()
849
+ const names = []
850
+
851
+ for (const fileInfo of rawFiles) {
852
+ const name = safeString(fileInfo?.name) || "upload.bin"
853
+ const mimeType = safeString(fileInfo?.mimeType) || "application/octet-stream"
854
+ const base64 = safeString(fileInfo?.base64)
855
+ if (!base64) return { ok: false, error: "file.base64 is required" }
856
+ const bytes = decodeBase64(base64)
857
+ const file = new File([bytes], name, { type: mimeType, lastModified: Date.now() })
858
+ dt.items.add(file)
859
+ names.push(name)
860
+ }
861
+
862
+ try {
863
+ match.chosen.scrollIntoView({ block: "center", inline: "center" })
864
+ } catch {}
865
+
866
+ try {
867
+ match.chosen.focus()
868
+ } catch {}
869
+
870
+ try {
871
+ match.chosen.files = dt.files
872
+ } catch {
873
+ try {
874
+ Object.defineProperty(match.chosen, "files", { value: dt.files, writable: false })
875
+ } catch {
876
+ return { ok: false, error: "Failed to set file input" }
877
+ }
878
+ }
879
+
880
+ match.chosen.dispatchEvent(new Event("input", { bubbles: true }))
881
+ match.chosen.dispatchEvent(new Event("change", { bubbles: true }))
882
+
883
+ return { ok: true, selectorUsed: match.selectorUsed, count: dt.files.length, names }
884
+ }
885
+
612
886
  if (command === "scroll") {
613
887
  const scrollX = Number.isFinite(options.x) ? options.x : 0
614
888
  const scrollY = Number.isFinite(options.y) ? options.y : 0
@@ -626,6 +900,73 @@ async function pageOps(command, args) {
626
900
  return { ok: true }
627
901
  }
628
902
 
903
+ if (command === "highlight") {
904
+ const duration = Number.isFinite(options.duration) ? options.duration : 3000
905
+ const color = typeof options.color === "string" ? options.color : "#ff0000"
906
+ const showInfo = !!options.showInfo
907
+
908
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
909
+ if (!match.chosen) {
910
+ return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
911
+ }
912
+
913
+ const el = match.chosen
914
+ const rect = el.getBoundingClientRect()
915
+
916
+ // Remove any existing highlight overlay
917
+ const existing = document.getElementById("__opc_highlight_overlay")
918
+ if (existing) existing.remove()
919
+
920
+ // Create overlay
921
+ const overlay = document.createElement("div")
922
+ overlay.id = "__opc_highlight_overlay"
923
+ overlay.style.cssText = `
924
+ position: fixed;
925
+ top: ${rect.top}px;
926
+ left: ${rect.left}px;
927
+ width: ${rect.width}px;
928
+ height: ${rect.height}px;
929
+ border: 3px solid ${color};
930
+ box-shadow: 0 0 10px ${color};
931
+ pointer-events: none;
932
+ z-index: 2147483647;
933
+ transition: opacity 0.3s;
934
+ `
935
+
936
+ if (showInfo) {
937
+ const info = document.createElement("div")
938
+ info.style.cssText = `
939
+ position: absolute;
940
+ top: -25px;
941
+ left: 0;
942
+ background: ${color};
943
+ color: white;
944
+ padding: 2px 8px;
945
+ font-size: 12px;
946
+ font-family: monospace;
947
+ border-radius: 3px;
948
+ white-space: nowrap;
949
+ `
950
+ info.textContent = `${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}`
951
+ overlay.appendChild(info)
952
+ }
953
+
954
+ document.body.appendChild(overlay)
955
+
956
+ setTimeout(() => {
957
+ overlay.style.opacity = "0"
958
+ setTimeout(() => overlay.remove(), 300)
959
+ }, duration)
960
+
961
+ return {
962
+ ok: true,
963
+ selectorUsed: match.selectorUsed,
964
+ highlighted: true,
965
+ tag: el.tagName,
966
+ id: el.id || null,
967
+ }
968
+ }
969
+
629
970
  if (command === "query") {
630
971
  if (mode === "page_text") {
631
972
  if (selectors.length && timeoutMs > 0) {
@@ -986,16 +1327,250 @@ async function toolWait({ ms = 1000, tabId }) {
986
1327
  return { tabId, content: `Waited ${ms}ms` }
987
1328
  }
988
1329
 
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...",
1330
+ function clampNumber(value, min, max, fallback) {
1331
+ const n = Number(value)
1332
+ if (!Number.isFinite(n)) return fallback
1333
+ return Math.min(Math.max(n, min), max)
1334
+ }
1335
+
1336
+ function normalizeDownloadTimeoutMs(value) {
1337
+ return clampNumber(value, 0, 60000, 60000)
1338
+ }
1339
+
1340
+ function waitForNextDownloadCreated(timeoutMs) {
1341
+ const timeout = normalizeDownloadTimeoutMs(timeoutMs)
1342
+ return new Promise((resolve, reject) => {
1343
+ const listener = (item) => {
1344
+ cleanup()
1345
+ resolve(item)
1346
+ }
1347
+
1348
+ const timer = timeout
1349
+ ? setTimeout(() => {
1350
+ cleanup()
1351
+ reject(new Error("Timed out waiting for download to start"))
1352
+ }, timeout)
1353
+ : null
1354
+
1355
+ function cleanup() {
1356
+ chrome.downloads.onCreated.removeListener(listener)
1357
+ if (timer) clearTimeout(timer)
1358
+ }
1359
+
1360
+ chrome.downloads.onCreated.addListener(listener)
998
1361
  })
1362
+ }
1363
+
1364
+ async function getDownloadById(downloadId) {
1365
+ const items = await chrome.downloads.search({ id: downloadId })
1366
+ return items && items.length ? items[0] : null
1367
+ }
1368
+
1369
+ async function waitForDownloadCompletion(downloadId, timeoutMs) {
1370
+ const timeout = normalizeDownloadTimeoutMs(timeoutMs)
1371
+ const pollMs = 200
1372
+ const endAt = Date.now() + timeout
1373
+
1374
+ while (true) {
1375
+ const item = await getDownloadById(downloadId)
1376
+ if (item && (item.state === "complete" || item.state === "interrupted")) return item
1377
+ if (!timeout || Date.now() >= endAt) return item
1378
+ await new Promise((resolve) => setTimeout(resolve, pollMs))
1379
+ }
1380
+ }
1381
+
1382
+ async function toolDownload({
1383
+ url,
1384
+ selector,
1385
+ filename,
1386
+ conflictAction,
1387
+ saveAs = false,
1388
+ wait = false,
1389
+ downloadTimeoutMs,
1390
+ tabId,
1391
+ index = 0,
1392
+ timeoutMs,
1393
+ pollMs,
1394
+ }) {
1395
+ const hasUrl = typeof url === "string" && url.trim()
1396
+ const hasSelector = typeof selector === "string" && selector.trim()
1397
+
1398
+ await ensureDownloadsAvailable()
1399
+
1400
+ if (!hasUrl && !hasSelector) throw new Error("url or selector is required")
1401
+ if (hasUrl && hasSelector) throw new Error("Provide either url or selector, not both")
1402
+
1403
+ let downloadId = null
1404
+
1405
+ if (hasUrl) {
1406
+ const options = { url: url.trim() }
1407
+ if (typeof filename === "string" && filename.trim()) options.filename = filename.trim()
1408
+ if (typeof conflictAction === "string" && conflictAction.trim()) options.conflictAction = conflictAction.trim()
1409
+ if (typeof saveAs === "boolean") options.saveAs = saveAs
1410
+
1411
+ downloadId = await chrome.downloads.download(options)
1412
+ } else {
1413
+ const tab = await getTabById(tabId)
1414
+ const created = waitForNextDownloadCreated(downloadTimeoutMs)
1415
+ const clicked = await runInPage(tab.id, "click", { selector, index, timeoutMs, pollMs })
1416
+ if (!clicked?.ok) throw new Error(clicked?.error || "Click failed")
1417
+ const createdItem = await created
1418
+ downloadId = createdItem?.id
1419
+ }
1420
+
1421
+ if (!Number.isFinite(downloadId)) throw new Error("Download did not start")
1422
+
1423
+ if (!wait) {
1424
+ const item = await getDownloadById(downloadId)
1425
+ return { content: { downloadId, item } }
1426
+ }
1427
+
1428
+ const item = await waitForDownloadCompletion(downloadId, downloadTimeoutMs)
1429
+ return { content: { downloadId, item } }
1430
+ }
1431
+
1432
+ async function toolListDownloads({ limit = 20, state } = {}) {
1433
+ await ensureDownloadsAvailable()
1434
+
1435
+ const limitValue = clampNumber(limit, 1, 200, 20)
1436
+ const query = { orderBy: ["-startTime"], limit: limitValue }
1437
+ if (typeof state === "string" && state.trim()) query.state = state.trim()
1438
+
1439
+ const downloads = await chrome.downloads.search(query)
1440
+ const out = downloads.map((d) => ({
1441
+ id: d.id,
1442
+ url: d.url,
1443
+ filename: d.filename,
1444
+ state: d.state,
1445
+ bytesReceived: d.bytesReceived,
1446
+ totalBytes: d.totalBytes,
1447
+ startTime: d.startTime,
1448
+ endTime: d.endTime,
1449
+ error: d.error,
1450
+ mime: d.mime,
1451
+ }))
1452
+
1453
+ return { content: JSON.stringify({ downloads: out }, null, 2) }
1454
+ }
1455
+
1456
+ async function toolSetFileInput({ selector, tabId, index = 0, timeoutMs, pollMs, files }) {
1457
+ if (!selector) throw new Error("Selector is required")
1458
+ const tab = await getTabById(tabId)
1459
+
1460
+ const result = await runInPage(tab.id, "set_file_input", { selector, index, timeoutMs, pollMs, files })
1461
+ if (!result?.ok) throw new Error(result?.error || "Failed to set file input")
1462
+ const used = result.selectorUsed || selector
1463
+ return { tabId: tab.id, content: JSON.stringify({ selector: used, ...result }, null, 2) }
1464
+ }
1465
+
1466
+ async function toolHighlight({ selector, tabId, index = 0, duration, color, showInfo, timeoutMs, pollMs }) {
1467
+ if (!selector) throw new Error("Selector is required")
1468
+ const tab = await getTabById(tabId)
1469
+
1470
+ const result = await runInPage(tab.id, "highlight", {
1471
+ selector,
1472
+ index,
1473
+ duration,
1474
+ color,
1475
+ showInfo,
1476
+ timeoutMs,
1477
+ pollMs,
1478
+ })
1479
+ if (!result?.ok) throw new Error(result?.error || "Highlight failed")
1480
+ return {
1481
+ tabId: tab.id,
1482
+ content: JSON.stringify({
1483
+ highlighted: true,
1484
+ tag: result.tag,
1485
+ id: result.id,
1486
+ selectorUsed: result.selectorUsed,
1487
+ }),
1488
+ }
1489
+ }
1490
+
1491
+ async function toolConsole({ tabId, clear = false, filter } = {}) {
1492
+ const tab = await getTabById(tabId)
1493
+ const state = await ensureDebuggerAttached(tab.id)
1494
+
1495
+ if (!state.attached) {
1496
+ return {
1497
+ tabId: tab.id,
1498
+ content: JSON.stringify({
1499
+ error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
1500
+ messages: [],
1501
+ }),
1502
+ }
1503
+ }
1504
+
1505
+ let messages = [...state.consoleMessages]
1506
+
1507
+ if (filter && typeof filter === "string") {
1508
+ const filterType = filter.toLowerCase()
1509
+ messages = messages.filter((m) => m.type === filterType)
1510
+ }
1511
+
1512
+ if (clear) {
1513
+ state.consoleMessages = []
1514
+ }
1515
+
1516
+ return {
1517
+ tabId: tab.id,
1518
+ content: JSON.stringify(messages, null, 2),
1519
+ }
1520
+ }
1521
+
1522
+ async function toolErrors({ tabId, clear = false } = {}) {
1523
+ const tab = await getTabById(tabId)
1524
+ const state = await ensureDebuggerAttached(tab.id)
1525
+
1526
+ if (!state.attached) {
1527
+ return {
1528
+ tabId: tab.id,
1529
+ content: JSON.stringify({
1530
+ error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
1531
+ errors: [],
1532
+ }),
1533
+ }
1534
+ }
1535
+
1536
+ const errors = [...state.pageErrors]
1537
+
1538
+ if (clear) {
1539
+ state.pageErrors = []
1540
+ }
1541
+
1542
+ return {
1543
+ tabId: tab.id,
1544
+ content: JSON.stringify(errors, null, 2),
1545
+ }
1546
+ }
1547
+
1548
+ chrome.runtime.onInstalled.addListener(() => connect().catch(() => {}))
1549
+ chrome.runtime.onStartup.addListener(() => connect().catch(() => {}))
1550
+
1551
+ if (chrome.permissions?.onAdded) {
1552
+ chrome.permissions.onAdded.addListener(() => connect().catch(() => {}))
1553
+ }
1554
+
1555
+ chrome.action.onClicked.addListener(async () => {
1556
+ const permissionResult = await requestOptionalPermissionsFromClick()
1557
+ if (!permissionResult.granted) {
1558
+ updateBadge(false)
1559
+ if (permissionResult.error) {
1560
+ console.warn("[OpenCode] Permission request failed:", permissionResult.error)
1561
+ } else {
1562
+ console.warn("[OpenCode] Permission request denied.")
1563
+ }
1564
+ return
1565
+ }
1566
+
1567
+ if (permissionResult.requested) {
1568
+ const requestedPermissions = permissionResult.permissions.join(", ") || "none"
1569
+ const requestedOrigins = permissionResult.origins.join(", ") || "none"
1570
+ console.log(`[OpenCode] Requested permissions -> permissions: ${requestedPermissions}; origins: ${requestedOrigins}`)
1571
+ }
1572
+
1573
+ await connect()
999
1574
  })
1000
1575
 
1001
- connect()
1576
+ connect().catch(() => {})
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
4
  "short_name": "OpenCode Browser",
5
- "version": "4.4.0",
5
+ "version": "4.6.0",
6
6
  "description": "Browser automation for OpenCode",
7
7
  "homepage_url": "https://github.com/different-ai/opencode-browser",
8
8
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9KICDxZh63m3QeQX13b37J/p7eG+cIEk4UDv+Phr7W/rqruy8DtdLFZ3WC37JYXo2Qk2iTWzIuBYjnc367RMY4Khu3f5hRyp67nvujuNY+0ACkrNGJk/viTodcW3CDcib9gijQQmPI1ZFkbec3qG0CN1hFxyn6pkdQZ8rnU9uo8ctntHIXrcfaHvQNT3AIVQfe8UjWPgalB0YCq9RamPVfMu6bG+6QLZGSKbvYDn0o5f8A/JZkm6K0MIii2EH61Oszhb/9GzZOciKqRZN+0mo23xbWHDp4ofDFyfQyMi83D3nDNldVGPSkIYlQWZaKHalaCoBRK+gzz7uuFoGrcsQIDAQAB",
@@ -13,7 +13,9 @@
13
13
  "storage",
14
14
  "notifications",
15
15
  "alarms",
16
- "nativeMessaging"
16
+ "nativeMessaging",
17
+ "downloads",
18
+ "debugger"
17
19
  ],
18
20
  "host_permissions": [
19
21
  "<all_urls>"