@different-ai/opencode-browser 3.0.0 → 4.0.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.
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Chrome Native Messaging host for OpenCode Browser.
5
+ // Speaks length-prefixed JSON over stdin/stdout and forwards messages to the local broker over a unix socket.
6
+
7
+ const net = require("net");
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+ const { spawn } = require("child_process");
12
+
13
+ const BASE_DIR = path.join(os.homedir(), ".opencode-browser");
14
+ const SOCKET_PATH = path.join(BASE_DIR, "broker.sock");
15
+ const BROKER_PATH = path.join(BASE_DIR, "broker.cjs");
16
+
17
+ fs.mkdirSync(BASE_DIR, { recursive: true });
18
+
19
+ function createJsonLineParser(onMessage) {
20
+ let buffer = "";
21
+ return (chunk) => {
22
+ buffer += chunk.toString("utf8");
23
+ while (true) {
24
+ const idx = buffer.indexOf("\n");
25
+ if (idx === -1) return;
26
+ const line = buffer.slice(0, idx);
27
+ buffer = buffer.slice(idx + 1);
28
+ if (!line.trim()) continue;
29
+ try {
30
+ onMessage(JSON.parse(line));
31
+ } catch {
32
+ // ignore
33
+ }
34
+ }
35
+ };
36
+ }
37
+
38
+ function writeJsonLine(socket, msg) {
39
+ socket.write(JSON.stringify(msg) + "\n");
40
+ }
41
+
42
+ function maybeStartBroker() {
43
+ try {
44
+ if (!fs.existsSync(BROKER_PATH)) return;
45
+ const child = spawn(process.execPath, [BROKER_PATH], { detached: true, stdio: "ignore" });
46
+ child.unref();
47
+ } catch {
48
+ // ignore
49
+ }
50
+ }
51
+
52
+ async function connectToBroker() {
53
+ return await new Promise((resolve, reject) => {
54
+ const socket = net.createConnection(SOCKET_PATH);
55
+ socket.once("connect", () => resolve(socket));
56
+ socket.once("error", (err) => reject(err));
57
+ });
58
+ }
59
+
60
+ async function ensureBroker() {
61
+ try {
62
+ return await connectToBroker();
63
+ } catch {
64
+ maybeStartBroker();
65
+ for (let i = 0; i < 50; i++) {
66
+ await new Promise((r) => setTimeout(r, 100));
67
+ try {
68
+ return await connectToBroker();
69
+ } catch {}
70
+ }
71
+ throw new Error("Could not connect to broker");
72
+ }
73
+ }
74
+
75
+ // --- Native messaging framing ---
76
+ let stdinBuffer = Buffer.alloc(0);
77
+
78
+ function writeNativeMessage(obj) {
79
+ try {
80
+ const payload = Buffer.from(JSON.stringify(obj), "utf8");
81
+ const header = Buffer.alloc(4);
82
+ header.writeUInt32LE(payload.length, 0);
83
+ process.stdout.write(Buffer.concat([header, payload]));
84
+ } catch (e) {
85
+ console.error("[native-host] write error", e);
86
+ }
87
+ }
88
+
89
+ function onStdinData(chunk, onMessage) {
90
+ stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
91
+ while (stdinBuffer.length >= 4) {
92
+ const len = stdinBuffer.readUInt32LE(0);
93
+ if (stdinBuffer.length < 4 + len) return;
94
+ const body = stdinBuffer.slice(4, 4 + len);
95
+ stdinBuffer = stdinBuffer.slice(4 + len);
96
+ try {
97
+ onMessage(JSON.parse(body.toString("utf8")));
98
+ } catch {
99
+ // ignore
100
+ }
101
+ }
102
+ }
103
+
104
+ (async () => {
105
+ const broker = await ensureBroker();
106
+ broker.setNoDelay(true);
107
+ broker.on("data", createJsonLineParser((msg) => {
108
+ if (msg && msg.type === "to_extension" && msg.message) {
109
+ writeNativeMessage(msg.message);
110
+ }
111
+ }));
112
+
113
+ broker.on("close", () => {
114
+ process.exit(0);
115
+ });
116
+
117
+ broker.on("error", () => {
118
+ process.exit(1);
119
+ });
120
+
121
+ writeJsonLine(broker, { type: "hello", role: "native-host" });
122
+
123
+ process.stdin.on("data", (chunk) =>
124
+ onStdinData(chunk, (message) => {
125
+ // Forward extension-origin messages to broker.
126
+ writeJsonLine(broker, { type: "from_extension", message });
127
+ })
128
+ );
129
+
130
+ process.stdin.on("end", () => {
131
+ try {
132
+ broker.end();
133
+ } catch {}
134
+ process.exit(0);
135
+ });
136
+ })();
@@ -1,306 +1,351 @@
1
- const PLUGIN_URL = "ws://localhost:19222";
2
- const KEEPALIVE_ALARM = "keepalive";
1
+ const NATIVE_HOST_NAME = "com.opencode.browser_automation"
2
+ const KEEPALIVE_ALARM = "keepalive"
3
3
 
4
- let ws = null;
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 (ws && ws.readyState === WebSocket.OPEN) return;
20
- if (ws) {
21
- try { ws.close(); } catch {}
22
- ws = null;
17
+ if (port) {
18
+ try { port.disconnect() } catch {}
19
+ port = null
23
20
  }
24
-
21
+
25
22
  try {
26
- ws = new WebSocket(PLUGIN_URL);
27
-
28
- ws.onopen = () => {
29
- console.log("[OpenCode] Connected to plugin");
30
- isConnected = true;
31
- updateBadge(true);
32
- };
33
-
34
- ws.onmessage = async (event) => {
35
- try {
36
- const message = JSON.parse(event.data);
37
- await handleMessage(message);
38
- } catch (e) {
39
- console.error("[OpenCode] Parse error:", e);
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
- ws.onclose = () => {
44
- console.log("[OpenCode] Disconnected");
45
- isConnected = false;
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
- console.error("[OpenCode] Connect failed:", e);
57
- isConnected = false;
58
- updateBadge(false);
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 (ws && ws.readyState === WebSocket.OPEN) {
69
- ws.send(JSON.stringify(message));
70
- return true;
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: { content: result } });
87
+ const result = await executeTool(tool, args || {})
88
+ send({ type: "tool_response", id, result })
89
89
  } catch (error) {
90
- send({ type: "tool_response", id, error: { content: error.message || String(error) } });
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(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
136
- });
137
-
138
- return `Navigated to ${url}`;
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
- return await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
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 el.getAttribute("aria-label") || el.getAttribute("alt") ||
201
- el.getAttribute("title") || el.getAttribute("placeholder") ||
202
- el.innerText?.slice(0, 100) || "";
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 = ["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
212
- el.getAttribute("onclick") || el.getAttribute("role") === "button" || el.isContentEditable;
213
- const rect = el.getBoundingClientRect();
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 = { uid: `e${uid}`, role: el.getAttribute("role") || el.tagName.toLowerCase(),
217
- name: getName(el).slice(0, 200), tag: el.tagName.toLowerCase() };
218
- // Capture href for any element that has one (links, area, base, etc.)
219
- if (el.href) node.href = el.href;
220
- if (el.tagName === "INPUT") { node.type = el.type; node.value = el.value; }
221
- if (el.id) node.selector = `#${el.id}`;
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}`
222
251
  else if (el.className && typeof el.className === "string") {
223
- const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
224
- 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}`
225
254
  }
226
- nodes.push(node);
227
- uid++;
255
+ nodes.push(node)
256
+ uid++
228
257
  }
229
-
258
+
230
259
  for (const child of el.children) {
231
- const r = build(child, depth + 1, uid);
232
- nodes.push(...r.nodes);
233
- uid = r.nextUid;
260
+ const r = build(child, depth + 1, uid)
261
+ nodes.push(...r.nodes)
262
+ uid = r.nextUid
234
263
  }
235
- return { nodes, nextUid: uid };
264
+ return { nodes, nextUid: uid }
236
265
  }
237
-
238
- // Collect all links on the page separately for easy access
266
+
239
267
  function getAllLinks() {
240
- const links = [];
241
- const seen = new Set();
242
- document.querySelectorAll("a[href]").forEach(a => {
243
- const href = a.href;
268
+ const links = []
269
+ const seen = new Set()
270
+ document.querySelectorAll("a[href]").forEach((a) => {
271
+ const href = a.href
244
272
  if (href && !seen.has(href) && !href.startsWith("javascript:")) {
245
- seen.add(href);
246
- const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || "";
247
- links.push({ href, text });
273
+ seen.add(href)
274
+ const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || ""
275
+ links.push({ href, text })
248
276
  }
249
- });
250
- return links.slice(0, 100); // Limit to 100 links
277
+ })
278
+ return links.slice(0, 100)
251
279
  }
252
-
253
- return {
254
- url: location.href,
255
- title: document.title,
280
+
281
+ return {
282
+ url: location.href,
283
+ title: document.title,
256
284
  nodes: build(document.body).nodes.slice(0, 500),
257
- links: getAllLinks()
258
- };
259
- }
260
- });
261
-
262
- return JSON.stringify(result[0]?.result, null, 2);
285
+ links: getAllLinks(),
286
+ }
287
+ },
288
+ })
289
+
290
+ return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
263
291
  }
264
292
 
265
293
  async function toolGetTabs() {
266
- const tabs = await chrome.tabs.query({});
267
- return JSON.stringify(tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })), null, 2);
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) }
268
297
  }
269
298
 
270
299
  async function toolExecuteScript({ code, tabId }) {
271
- if (!code) throw new Error("Code is required");
272
- const tab = await getTabById(tabId);
273
- const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: new Function(code) });
274
- return JSON.stringify(result[0]?.result);
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) }
275
307
  }
276
308
 
277
309
  async function toolScroll({ x = 0, y = 0, selector, tabId }) {
278
- const tab = await getTabById(tabId);
279
- const sel = selector || null;
280
-
310
+ const tab = await getTabById(tabId)
311
+ const sel = selector || null
312
+
281
313
  await chrome.scripting.executeScript({
282
314
  target: { tabId: tab.id },
283
315
  func: (scrollX, scrollY, sel) => {
284
- if (sel) { const el = document.querySelector(sel); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); return; } }
285
- window.scrollBy(scrollX, scrollY);
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)
286
324
  },
287
- args: [x, y, sel]
288
- });
289
-
290
- 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})`}` }
291
329
  }
292
330
 
293
- async function toolWait({ ms = 1000 }) {
294
- await new Promise(resolve => setTimeout(resolve, ms));
295
- return `Waited ${ms}ms`;
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` }
296
337
  }
297
338
 
298
- chrome.runtime.onInstalled.addListener(() => connect());
299
- chrome.runtime.onStartup.addListener(() => connect());
339
+ chrome.runtime.onInstalled.addListener(() => connect())
340
+ chrome.runtime.onStartup.addListener(() => connect())
300
341
  chrome.action.onClicked.addListener(() => {
301
- connect();
302
- chrome.notifications.create({ type: "basic", iconUrl: "icons/icon128.png", title: "OpenCode Browser",
303
- message: isConnected ? "Connected" : "Reconnecting..." });
304
- });
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
+ })
305
350
 
306
- connect();
351
+ connect()