@different-ai/opencode-browser 2.1.0 → 4.0.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.
- package/README.md +43 -93
- package/bin/broker.cjs +290 -0
- package/bin/cli.js +231 -186
- package/bin/native-host.cjs +136 -0
- package/extension/background.js +240 -174
- package/extension/manifest.json +3 -2
- package/package.json +8 -5
- package/src/plugin.ts +226 -623
package/extension/background.js
CHANGED
|
@@ -1,285 +1,351 @@
|
|
|
1
|
-
const
|
|
2
|
-
const KEEPALIVE_ALARM = "keepalive"
|
|
1
|
+
const NATIVE_HOST_NAME = "com.opencode.browser_automation"
|
|
2
|
+
const KEEPALIVE_ALARM = "keepalive"
|
|
3
3
|
|
|
4
|
-
let
|
|
5
|
-
let isConnected = false
|
|
4
|
+
let port = null
|
|
5
|
+
let isConnected = false
|
|
6
|
+
let connectionAttempts = 0
|
|
6
7
|
|
|
7
|
-
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
|
|
8
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
|
|
8
9
|
|
|
9
10
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
10
11
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
11
|
-
if (!isConnected)
|
|
12
|
-
console.log("[OpenCode] Alarm triggered reconnect");
|
|
13
|
-
connect();
|
|
14
|
-
}
|
|
12
|
+
if (!isConnected) connect()
|
|
15
13
|
}
|
|
16
|
-
})
|
|
14
|
+
})
|
|
17
15
|
|
|
18
16
|
function connect() {
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ws = null;
|
|
17
|
+
if (port) {
|
|
18
|
+
try { port.disconnect() } catch {}
|
|
19
|
+
port = null
|
|
23
20
|
}
|
|
24
|
-
|
|
21
|
+
|
|
25
22
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
port = chrome.runtime.connectNative(NATIVE_HOST_NAME)
|
|
24
|
+
|
|
25
|
+
port.onMessage.addListener((message) => {
|
|
26
|
+
handleMessage(message).catch((e) => {
|
|
27
|
+
console.error("[OpenCode] Message handler error:", e)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
port.onDisconnect.addListener(() => {
|
|
32
|
+
isConnected = false
|
|
33
|
+
port = null
|
|
34
|
+
updateBadge(false)
|
|
35
|
+
|
|
36
|
+
const err = chrome.runtime.lastError
|
|
37
|
+
if (err?.message) {
|
|
38
|
+
// Usually means native host not installed or crashed
|
|
39
|
+
connectionAttempts++
|
|
40
|
+
if (connectionAttempts === 1) {
|
|
41
|
+
console.log("[OpenCode] Native host not available. Run: npx @different-ai/opencode-browser install")
|
|
42
|
+
} else if (connectionAttempts % 20 === 0) {
|
|
43
|
+
console.log("[OpenCode] Still waiting for native host...")
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ws = null;
|
|
47
|
-
updateBadge(false);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
ws.onerror = (err) => {
|
|
51
|
-
console.error("[OpenCode] WebSocket error");
|
|
52
|
-
isConnected = false;
|
|
53
|
-
updateBadge(false);
|
|
54
|
-
};
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
isConnected = true
|
|
49
|
+
connectionAttempts = 0
|
|
50
|
+
updateBadge(true)
|
|
55
51
|
} catch (e) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
isConnected = false
|
|
53
|
+
updateBadge(false)
|
|
54
|
+
console.error("[OpenCode] connectNative failed:", e)
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
function updateBadge(connected) {
|
|
63
|
-
chrome.action.setBadgeText({ text: connected ? "ON" : "" })
|
|
64
|
-
chrome.action.setBadgeBackgroundColor({ color: connected ? "#22c55e" : "#ef4444" })
|
|
59
|
+
chrome.action.setBadgeText({ text: connected ? "ON" : "" })
|
|
60
|
+
chrome.action.setBadgeBackgroundColor({ color: connected ? "#22c55e" : "#ef4444" })
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
function send(message) {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
if (!port) return false
|
|
65
|
+
try {
|
|
66
|
+
port.postMessage(message)
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
71
70
|
}
|
|
72
|
-
return false;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
async function handleMessage(message) {
|
|
74
|
+
if (!message || typeof message !== "object") return
|
|
75
|
+
|
|
76
76
|
if (message.type === "tool_request") {
|
|
77
|
-
await handleToolRequest(message)
|
|
77
|
+
await handleToolRequest(message)
|
|
78
78
|
} else if (message.type === "ping") {
|
|
79
|
-
send({ type: "pong" })
|
|
79
|
+
send({ type: "pong" })
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function handleToolRequest(request) {
|
|
84
|
-
const { id, tool, args } = request
|
|
85
|
-
|
|
84
|
+
const { id, tool, args } = request
|
|
85
|
+
|
|
86
86
|
try {
|
|
87
|
-
const result = await executeTool(tool, args || {})
|
|
88
|
-
send({ type: "tool_response", id, result
|
|
87
|
+
const result = await executeTool(tool, args || {})
|
|
88
|
+
send({ type: "tool_response", id, result })
|
|
89
89
|
} catch (error) {
|
|
90
|
-
send({
|
|
90
|
+
send({
|
|
91
|
+
type: "tool_response",
|
|
92
|
+
id,
|
|
93
|
+
error: { content: error?.message || String(error) },
|
|
94
|
+
})
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
async function executeTool(toolName, args) {
|
|
95
99
|
const tools = {
|
|
100
|
+
get_active_tab: toolGetActiveTab,
|
|
101
|
+
get_tabs: toolGetTabs,
|
|
96
102
|
navigate: toolNavigate,
|
|
97
103
|
click: toolClick,
|
|
98
104
|
type: toolType,
|
|
99
105
|
screenshot: toolScreenshot,
|
|
100
106
|
snapshot: toolSnapshot,
|
|
101
|
-
get_tabs: toolGetTabs,
|
|
102
107
|
execute_script: toolExecuteScript,
|
|
103
108
|
scroll: toolScroll,
|
|
104
|
-
wait: toolWait
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const fn = tools[toolName]
|
|
108
|
-
if (!fn) throw new Error(`Unknown tool: ${toolName}`)
|
|
109
|
-
return await fn(args)
|
|
109
|
+
wait: toolWait,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fn = tools[toolName]
|
|
113
|
+
if (!fn) throw new Error(`Unknown tool: ${toolName}`)
|
|
114
|
+
return await fn(args)
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
async function getActiveTab() {
|
|
113
|
-
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
|
114
|
-
if (!tab?.id) throw new Error("No active tab found")
|
|
115
|
-
return tab
|
|
118
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
|
119
|
+
if (!tab?.id) throw new Error("No active tab found")
|
|
120
|
+
return tab
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async function getTabById(tabId) {
|
|
119
|
-
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab()
|
|
124
|
+
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function toolGetActiveTab() {
|
|
128
|
+
const tab = await getActiveTab()
|
|
129
|
+
return { tabId: tab.id, content: { tabId: tab.id, url: tab.url, title: tab.title } }
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
async function toolNavigate({ url, tabId }) {
|
|
123
|
-
if (!url) throw new Error("URL is required")
|
|
124
|
-
const tab = await getTabById(tabId)
|
|
125
|
-
await chrome.tabs.update(tab.id, { url })
|
|
126
|
-
|
|
133
|
+
if (!url) throw new Error("URL is required")
|
|
134
|
+
const tab = await getTabById(tabId)
|
|
135
|
+
await chrome.tabs.update(tab.id, { url })
|
|
136
|
+
|
|
127
137
|
await new Promise((resolve) => {
|
|
128
138
|
const listener = (updatedTabId, info) => {
|
|
129
139
|
if (updatedTabId === tab.id && info.status === "complete") {
|
|
130
|
-
chrome.tabs.onUpdated.removeListener(listener)
|
|
131
|
-
resolve()
|
|
140
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
141
|
+
resolve()
|
|
132
142
|
}
|
|
133
|
-
}
|
|
134
|
-
chrome.tabs.onUpdated.addListener(listener)
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
}
|
|
144
|
+
chrome.tabs.onUpdated.addListener(listener)
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
147
|
+
resolve()
|
|
148
|
+
}, 30000)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return { tabId: tab.id, content: `Navigated to ${url}` }
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
async function toolClick({ selector, tabId }) {
|
|
142
|
-
if (!selector) throw new Error("Selector is required")
|
|
143
|
-
const tab = await getTabById(tabId)
|
|
144
|
-
|
|
155
|
+
if (!selector) throw new Error("Selector is required")
|
|
156
|
+
const tab = await getTabById(tabId)
|
|
157
|
+
|
|
145
158
|
const result = await chrome.scripting.executeScript({
|
|
146
159
|
target: { tabId: tab.id },
|
|
147
160
|
func: (sel) => {
|
|
148
|
-
const el = document.querySelector(sel)
|
|
149
|
-
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
150
|
-
el.click()
|
|
151
|
-
return { success: true }
|
|
161
|
+
const el = document.querySelector(sel)
|
|
162
|
+
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
163
|
+
el.click()
|
|
164
|
+
return { success: true }
|
|
152
165
|
},
|
|
153
|
-
args: [selector]
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
157
|
-
return `Clicked ${selector}
|
|
166
|
+
args: [selector],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
170
|
+
return { tabId: tab.id, content: `Clicked ${selector}` }
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
async function toolType({ selector, text, tabId, clear = false }) {
|
|
161
|
-
if (!selector) throw new Error("Selector is required")
|
|
162
|
-
if (text === undefined) throw new Error("Text is required")
|
|
163
|
-
const tab = await getTabById(tabId)
|
|
164
|
-
|
|
174
|
+
if (!selector) throw new Error("Selector is required")
|
|
175
|
+
if (text === undefined) throw new Error("Text is required")
|
|
176
|
+
const tab = await getTabById(tabId)
|
|
177
|
+
|
|
165
178
|
const result = await chrome.scripting.executeScript({
|
|
166
179
|
target: { tabId: tab.id },
|
|
167
180
|
func: (sel, txt, shouldClear) => {
|
|
168
|
-
const el = document.querySelector(sel)
|
|
169
|
-
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
170
|
-
el.focus()
|
|
171
|
-
if (shouldClear) el.value = ""
|
|
181
|
+
const el = document.querySelector(sel)
|
|
182
|
+
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
183
|
+
el.focus()
|
|
184
|
+
if (shouldClear && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) el.value = ""
|
|
185
|
+
|
|
172
186
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
173
|
-
el.value = el.value + txt
|
|
174
|
-
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
175
|
-
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
187
|
+
el.value = el.value + txt
|
|
188
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
189
|
+
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
176
190
|
} else if (el.isContentEditable) {
|
|
177
|
-
document.execCommand("insertText", false, txt)
|
|
191
|
+
document.execCommand("insertText", false, txt)
|
|
178
192
|
}
|
|
179
|
-
return { success: true }
|
|
193
|
+
return { success: true }
|
|
180
194
|
},
|
|
181
|
-
args: [selector, text, clear]
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
185
|
-
return `Typed "${text}" into ${selector}
|
|
195
|
+
args: [selector, text, clear],
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
199
|
+
return { tabId: tab.id, content: `Typed "${text}" into ${selector}` }
|
|
186
200
|
}
|
|
187
201
|
|
|
188
202
|
async function toolScreenshot({ tabId }) {
|
|
189
|
-
const tab = await getTabById(tabId)
|
|
190
|
-
|
|
203
|
+
const tab = await getTabById(tabId)
|
|
204
|
+
const png = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
|
|
205
|
+
return { tabId: tab.id, content: png }
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
async function toolSnapshot({ tabId }) {
|
|
194
|
-
const tab = await getTabById(tabId)
|
|
195
|
-
|
|
209
|
+
const tab = await getTabById(tabId)
|
|
210
|
+
|
|
196
211
|
const result = await chrome.scripting.executeScript({
|
|
197
212
|
target: { tabId: tab.id },
|
|
198
213
|
func: () => {
|
|
199
214
|
function getName(el) {
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
return (
|
|
216
|
+
el.getAttribute("aria-label") ||
|
|
217
|
+
el.getAttribute("alt") ||
|
|
218
|
+
el.getAttribute("title") ||
|
|
219
|
+
el.getAttribute("placeholder") ||
|
|
220
|
+
el.innerText?.slice(0, 100) ||
|
|
221
|
+
""
|
|
222
|
+
)
|
|
203
223
|
}
|
|
204
|
-
|
|
224
|
+
|
|
205
225
|
function build(el, depth = 0, uid = 0) {
|
|
206
|
-
if (depth > 10) return { nodes: [], nextUid: uid }
|
|
207
|
-
const nodes = []
|
|
208
|
-
const style = window.getComputedStyle(el)
|
|
209
|
-
if (style.display === "none" || style.visibility === "hidden") return { nodes: [], nextUid: uid }
|
|
210
|
-
|
|
211
|
-
const isInteractive =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
if (depth > 10) return { nodes: [], nextUid: uid }
|
|
227
|
+
const nodes = []
|
|
228
|
+
const style = window.getComputedStyle(el)
|
|
229
|
+
if (style.display === "none" || style.visibility === "hidden") return { nodes: [], nextUid: uid }
|
|
230
|
+
|
|
231
|
+
const isInteractive =
|
|
232
|
+
["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
|
|
233
|
+
el.getAttribute("onclick") ||
|
|
234
|
+
el.getAttribute("role") === "button" ||
|
|
235
|
+
el.isContentEditable
|
|
236
|
+
const rect = el.getBoundingClientRect()
|
|
237
|
+
|
|
215
238
|
if (rect.width > 0 && rect.height > 0 && (isInteractive || el.innerText?.trim())) {
|
|
216
|
-
const node = {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
const node = {
|
|
240
|
+
uid: `e${uid}`,
|
|
241
|
+
role: el.getAttribute("role") || el.tagName.toLowerCase(),
|
|
242
|
+
name: getName(el).slice(0, 200),
|
|
243
|
+
tag: el.tagName.toLowerCase(),
|
|
244
|
+
}
|
|
245
|
+
if (el.href) node.href = el.href
|
|
246
|
+
if (el.tagName === "INPUT") {
|
|
247
|
+
node.type = el.type
|
|
248
|
+
node.value = el.value
|
|
249
|
+
}
|
|
250
|
+
if (el.id) node.selector = `#${el.id}`
|
|
221
251
|
else if (el.className && typeof el.className === "string") {
|
|
222
|
-
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
223
|
-
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}
|
|
252
|
+
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
253
|
+
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}`
|
|
224
254
|
}
|
|
225
|
-
nodes.push(node)
|
|
226
|
-
uid
|
|
255
|
+
nodes.push(node)
|
|
256
|
+
uid++
|
|
227
257
|
}
|
|
228
|
-
|
|
258
|
+
|
|
229
259
|
for (const child of el.children) {
|
|
230
|
-
const r = build(child, depth + 1, uid)
|
|
231
|
-
nodes.push(...r.nodes)
|
|
232
|
-
uid = r.nextUid
|
|
260
|
+
const r = build(child, depth + 1, uid)
|
|
261
|
+
nodes.push(...r.nodes)
|
|
262
|
+
uid = r.nextUid
|
|
233
263
|
}
|
|
234
|
-
return { nodes, nextUid: uid }
|
|
264
|
+
return { nodes, nextUid: uid }
|
|
235
265
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
266
|
+
|
|
267
|
+
function getAllLinks() {
|
|
268
|
+
const links = []
|
|
269
|
+
const seen = new Set()
|
|
270
|
+
document.querySelectorAll("a[href]").forEach((a) => {
|
|
271
|
+
const href = a.href
|
|
272
|
+
if (href && !seen.has(href) && !href.startsWith("javascript:")) {
|
|
273
|
+
seen.add(href)
|
|
274
|
+
const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || ""
|
|
275
|
+
links.push({ href, text })
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
return links.slice(0, 100)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
url: location.href,
|
|
283
|
+
title: document.title,
|
|
284
|
+
nodes: build(document.body).nodes.slice(0, 500),
|
|
285
|
+
links: getAllLinks(),
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
242
291
|
}
|
|
243
292
|
|
|
244
293
|
async function toolGetTabs() {
|
|
245
|
-
const tabs = await chrome.tabs.query({})
|
|
246
|
-
|
|
294
|
+
const tabs = await chrome.tabs.query({})
|
|
295
|
+
const out = tabs.map((t) => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }))
|
|
296
|
+
return { content: JSON.stringify(out, null, 2) }
|
|
247
297
|
}
|
|
248
298
|
|
|
249
299
|
async function toolExecuteScript({ code, tabId }) {
|
|
250
|
-
if (!code) throw new Error("Code is required")
|
|
251
|
-
const tab = await getTabById(tabId)
|
|
252
|
-
const result = await chrome.scripting.executeScript({
|
|
253
|
-
|
|
300
|
+
if (!code) throw new Error("Code is required")
|
|
301
|
+
const tab = await getTabById(tabId)
|
|
302
|
+
const result = await chrome.scripting.executeScript({
|
|
303
|
+
target: { tabId: tab.id },
|
|
304
|
+
func: new Function(code),
|
|
305
|
+
})
|
|
306
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result) }
|
|
254
307
|
}
|
|
255
308
|
|
|
256
309
|
async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
257
|
-
const tab = await getTabById(tabId)
|
|
258
|
-
const sel = selector || null
|
|
259
|
-
|
|
310
|
+
const tab = await getTabById(tabId)
|
|
311
|
+
const sel = selector || null
|
|
312
|
+
|
|
260
313
|
await chrome.scripting.executeScript({
|
|
261
314
|
target: { tabId: tab.id },
|
|
262
315
|
func: (scrollX, scrollY, sel) => {
|
|
263
|
-
if (sel) {
|
|
264
|
-
|
|
316
|
+
if (sel) {
|
|
317
|
+
const el = document.querySelector(sel)
|
|
318
|
+
if (el) {
|
|
319
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
window.scrollBy(scrollX, scrollY)
|
|
265
324
|
},
|
|
266
|
-
args: [x, y, sel]
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
return `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}
|
|
325
|
+
args: [x, y, sel],
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
return { tabId: tab.id, content: `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}` }
|
|
270
329
|
}
|
|
271
330
|
|
|
272
|
-
async function toolWait({ ms = 1000 }) {
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
async function toolWait({ ms = 1000, tabId }) {
|
|
332
|
+
if (typeof tabId === "number") {
|
|
333
|
+
// keep tabId in response for ownership purposes
|
|
334
|
+
}
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
336
|
+
return { tabId, content: `Waited ${ms}ms` }
|
|
275
337
|
}
|
|
276
338
|
|
|
277
|
-
chrome.runtime.onInstalled.addListener(() => connect())
|
|
278
|
-
chrome.runtime.onStartup.addListener(() => connect())
|
|
339
|
+
chrome.runtime.onInstalled.addListener(() => connect())
|
|
340
|
+
chrome.runtime.onStartup.addListener(() => connect())
|
|
279
341
|
chrome.action.onClicked.addListener(() => {
|
|
280
|
-
connect()
|
|
281
|
-
chrome.notifications.create({
|
|
282
|
-
|
|
283
|
-
|
|
342
|
+
connect()
|
|
343
|
+
chrome.notifications.create({
|
|
344
|
+
type: "basic",
|
|
345
|
+
iconUrl: "icons/icon128.png",
|
|
346
|
+
title: "OpenCode Browser",
|
|
347
|
+
message: isConnected ? "Connected" : "Reconnecting...",
|
|
348
|
+
})
|
|
349
|
+
})
|
|
284
350
|
|
|
285
|
-
connect()
|
|
351
|
+
connect()
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Automation",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"description": "Browser automation for OpenCode",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripting",
|
|
10
10
|
"storage",
|
|
11
11
|
"notifications",
|
|
12
|
-
"alarms"
|
|
12
|
+
"alarms",
|
|
13
|
+
"nativeMessaging"
|
|
13
14
|
],
|
|
14
15
|
"host_permissions": [
|
|
15
16
|
"<all_urls>"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@different-ai/opencode-browser",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Browser automation plugin for OpenCode
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opencode-browser": "./bin/cli.js"
|
|
@@ -13,19 +13,22 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bin",
|
|
16
|
-
"src",
|
|
16
|
+
"src/plugin.ts",
|
|
17
17
|
"extension",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"install
|
|
21
|
+
"install": "node bin/cli.js install",
|
|
22
|
+
"uninstall": "node bin/cli.js uninstall",
|
|
23
|
+
"status": "node bin/cli.js status"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
24
26
|
"opencode",
|
|
25
27
|
"browser",
|
|
26
28
|
"automation",
|
|
27
29
|
"chrome",
|
|
28
|
-
"plugin"
|
|
30
|
+
"plugin",
|
|
31
|
+
"native-messaging"
|
|
29
32
|
],
|
|
30
33
|
"author": "Benjamin Shafii",
|
|
31
34
|
"license": "MIT",
|