@different-ai/opencode-browser 4.3.2 → 4.4.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.
@@ -140,6 +140,7 @@ async function runInPage(tabId, command, args) {
140
140
  async function pageOps(command, args) {
141
141
  const options = args || {}
142
142
  const MAX_DEPTH = 6
143
+ const DEFAULT_TIMEOUT_MS = 2000
143
144
 
144
145
  function safeString(value) {
145
146
  return typeof value === "string" ? value : ""
@@ -157,6 +158,48 @@ async function pageOps(command, args) {
157
158
  return parts.length ? parts : [selector.trim()].filter(Boolean)
158
159
  }
159
160
 
161
+ function stripQuotes(value) {
162
+ return safeString(value).replace(/^['"]|['"]$/g, "")
163
+ }
164
+
165
+ function normalizeText(value) {
166
+ return safeString(value).replace(/\s+/g, " ").trim().toLowerCase()
167
+ }
168
+
169
+ function matchesText(value, target) {
170
+ if (!target) return false
171
+ const normTarget = normalizeText(target)
172
+ if (!normTarget) return false
173
+ const normValue = normalizeText(value)
174
+ return normValue === normTarget || normValue.includes(normTarget)
175
+ }
176
+
177
+ function normalizeLocatorKey(key) {
178
+ if (key === "css") return "css"
179
+ if (key === "label" || key === "field") return "label"
180
+ if (key === "aria" || key === "aria-label") return "aria"
181
+ if (key === "placeholder") return "placeholder"
182
+ if (key === "name") return "name"
183
+ if (key === "role") return "role"
184
+ if (key === "text") return "text"
185
+ if (key === "id") return "id"
186
+ return null
187
+ }
188
+
189
+ function parseLocator(raw) {
190
+ const trimmed = safeString(raw).trim()
191
+ if (!trimmed) return { kind: "css", value: "", raw: "" }
192
+ const match = trimmed.match(/^([a-zA-Z_-]+)\s*(=|:)\s*(.+)$/)
193
+ if (match) {
194
+ const key = match[1].toLowerCase()
195
+ const kind = normalizeLocatorKey(key)
196
+ if (kind) {
197
+ return { kind, value: stripQuotes(match[3]), raw: trimmed }
198
+ }
199
+ }
200
+ return { kind: "css", value: trimmed, raw: trimmed }
201
+ }
202
+
160
203
  function isVisible(el) {
161
204
  if (!el) return false
162
205
  const rect = el.getBoundingClientRect()
@@ -206,19 +249,135 @@ async function pageOps(command, args) {
206
249
  return out
207
250
  }
208
251
 
209
- function resolveMatches(selectors, index) {
252
+ function getAriaLabelledByText(el) {
253
+ const ids = safeString(el?.getAttribute?.("aria-labelledby")).split(/\s+/).filter(Boolean)
254
+ if (!ids.length) return ""
255
+ const parts = []
256
+ for (const id of ids) {
257
+ const ref = document.getElementById(id)
258
+ if (ref) parts.push(ref.innerText || ref.textContent || "")
259
+ }
260
+ return parts.join(" ")
261
+ }
262
+
263
+ function findByAttribute(attr, target, allowedTags) {
264
+ if (!target) return []
265
+ const nodes = deepQuerySelectorAll(`[${attr}]`, document)
266
+ return nodes.filter((el) => {
267
+ if (Array.isArray(allowedTags) && allowedTags.length && !allowedTags.includes(el.tagName)) return false
268
+ return matchesText(el.getAttribute(attr), target)
269
+ })
270
+ }
271
+
272
+ function findByLabelText(target) {
273
+ if (!target) return []
274
+ const results = []
275
+ const seen = new Set()
276
+ const labels = deepQuerySelectorAll("label", document)
277
+ for (const label of labels) {
278
+ if (!matchesText(label.innerText || label.textContent || "", target)) continue
279
+ const control = label.control || label.querySelector("input, textarea, select")
280
+ if (control && !seen.has(control)) {
281
+ seen.add(control)
282
+ results.push(control)
283
+ }
284
+ }
285
+ const labelled = deepQuerySelectorAll("[aria-labelledby]", document)
286
+ for (const el of labelled) {
287
+ if (!matchesText(getAriaLabelledByText(el), target)) continue
288
+ if (!seen.has(el)) {
289
+ seen.add(el)
290
+ results.push(el)
291
+ }
292
+ }
293
+ return results
294
+ }
295
+
296
+ function findByRole(target) {
297
+ if (!target) return []
298
+ const nodes = deepQuerySelectorAll("[role]", document)
299
+ return nodes.filter((el) => matchesText(el.getAttribute("role"), target))
300
+ }
301
+
302
+ function findByName(target) {
303
+ return findByAttribute("name", target)
304
+ }
305
+
306
+ function findByText(target) {
307
+ if (!target) return []
308
+ const results = []
309
+ const seen = new Set()
310
+ const candidates = deepQuerySelectorAll(
311
+ "button, a, label, option, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem']",
312
+ document
313
+ )
314
+ for (const el of candidates) {
315
+ if (!matchesText(el.innerText || el.textContent || "", target)) continue
316
+ if (!seen.has(el)) {
317
+ seen.add(el)
318
+ results.push(el)
319
+ }
320
+ }
321
+ const inputs = deepQuerySelectorAll("input[type='button'], input[type='submit'], input[type='reset']", document)
322
+ for (const el of inputs) {
323
+ if (!matchesText(el.value || "", target)) continue
324
+ if (!seen.has(el)) {
325
+ seen.add(el)
326
+ results.push(el)
327
+ }
328
+ }
329
+ return results
330
+ }
331
+
332
+ function resolveLocator(locator) {
333
+ if (locator.kind === "css") {
334
+ const value = safeString(locator.value)
335
+ if (!value) return []
336
+ return deepQuerySelectorAll(value, document)
337
+ }
338
+
339
+ if (locator.kind === "label") return findByLabelText(locator.value)
340
+ if (locator.kind === "aria") return findByAttribute("aria-label", locator.value)
341
+ if (locator.kind === "placeholder") return findByAttribute("placeholder", locator.value, ["INPUT", "TEXTAREA"])
342
+ if (locator.kind === "name") return findByName(locator.value)
343
+ if (locator.kind === "role") return findByRole(locator.value)
344
+ if (locator.kind === "text") return findByText(locator.value)
345
+
346
+ if (locator.kind === "id") {
347
+ const idValue = safeString(locator.value).trim()
348
+ if (!idValue) return []
349
+ const escaped = window.CSS && window.CSS.escape ? window.CSS.escape(idValue) : idValue.replace(/[^a-zA-Z0-9_-]/g, "\\$&")
350
+ return deepQuerySelectorAll(`#${escaped}`, document)
351
+ }
352
+
353
+ return []
354
+ }
355
+
356
+ function resolveMatchesOnce(selectors, index) {
210
357
  for (const sel of selectors) {
211
- const s = safeString(sel)
212
- if (!s) continue
213
- const matches = deepQuerySelectorAll(s, document)
358
+ const locator = parseLocator(sel)
359
+ if (!locator.value) continue
360
+ const matches = resolveLocator(locator)
214
361
  if (!matches.length) continue
215
362
  const visible = matches.filter(isVisible)
216
- const chosen = visible[index] || matches[index]
217
- return { selectorUsed: s, matches, chosen }
363
+ const chosen = visible[index] || matches[index] || null
364
+ return { selectorUsed: locator.raw, matches, chosen }
218
365
  }
219
366
  return { selectorUsed: selectors[0] || "", matches: [], chosen: null }
220
367
  }
221
368
 
369
+ async function resolveMatches(selectors, index, timeoutMs, pollMs) {
370
+ let match = resolveMatchesOnce(selectors, index)
371
+ if (timeoutMs > 0) {
372
+ const start = Date.now()
373
+ while (!match.matches.length && Date.now() - start < timeoutMs) {
374
+ await new Promise((r) => setTimeout(r, pollMs))
375
+ match = resolveMatchesOnce(selectors, index)
376
+ }
377
+ }
378
+ return match
379
+ }
380
+
222
381
  function clickElement(el) {
223
382
  try {
224
383
  el.scrollIntoView({ block: "center", inline: "center" })
@@ -333,14 +492,14 @@ async function pageOps(command, args) {
333
492
  const mode = typeof options.mode === "string" && options.mode ? options.mode : "text"
334
493
  const selectors = normalizeSelectorList(options.selector)
335
494
  const index = Number.isFinite(options.index) ? options.index : 0
336
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0
495
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS
337
496
  const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 200
338
497
  const limit = Number.isFinite(options.limit) ? options.limit : mode === "page_text" ? 20000 : 50
339
498
  const pattern = typeof options.pattern === "string" ? options.pattern : null
340
499
  const flags = typeof options.flags === "string" ? options.flags : "i"
341
500
 
342
501
  if (command === "click") {
343
- const match = resolveMatches(selectors, index)
502
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
344
503
  if (!match.chosen) {
345
504
  return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
346
505
  }
@@ -351,7 +510,7 @@ async function pageOps(command, args) {
351
510
  if (command === "type") {
352
511
  const text = options.text
353
512
  const shouldClear = !!options.clear
354
- const match = resolveMatches(selectors, index)
513
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
355
514
  if (!match.chosen) {
356
515
  return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
357
516
  }
@@ -393,7 +552,7 @@ async function pageOps(command, args) {
393
552
  const value = typeof options.value === "string" ? options.value : null
394
553
  const label = typeof options.label === "string" ? options.label : null
395
554
  const optionIndex = Number.isFinite(options.optionIndex) ? options.optionIndex : null
396
- const match = resolveMatches(selectors, index)
555
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
397
556
  if (!match.chosen) {
398
557
  return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
399
558
  }
@@ -453,7 +612,7 @@ async function pageOps(command, args) {
453
612
  const scrollX = Number.isFinite(options.x) ? options.x : 0
454
613
  const scrollY = Number.isFinite(options.y) ? options.y : 0
455
614
  if (selectors.length) {
456
- const match = resolveMatches(selectors, index)
615
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
457
616
  if (!match.chosen) {
458
617
  return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
459
618
  }
@@ -469,12 +628,7 @@ async function pageOps(command, args) {
469
628
  if (command === "query") {
470
629
  if (mode === "page_text") {
471
630
  if (selectors.length && timeoutMs > 0) {
472
- const start = Date.now()
473
- while (Date.now() - start < timeoutMs) {
474
- const match = resolveMatches(selectors, index)
475
- if (match.matches.length) break
476
- await new Promise((r) => setTimeout(r, pollMs))
477
- }
631
+ await resolveMatches(selectors, index, timeoutMs, pollMs)
478
632
  }
479
633
  return { ok: true, value: getPageText(limit, pattern, flags) }
480
634
  }
@@ -483,16 +637,7 @@ async function pageOps(command, args) {
483
637
  return { ok: false, error: "Selector is required" }
484
638
  }
485
639
 
486
- let match = resolveMatches(selectors, index)
487
- if (timeoutMs > 0) {
488
- const start = Date.now()
489
- while (Date.now() - start < timeoutMs) {
490
- match = resolveMatches(selectors, index)
491
- if (mode === "exists" && match.matches.length) break
492
- if (mode !== "exists" && match.chosen) break
493
- await new Promise((r) => setTimeout(r, pollMs))
494
- }
495
- }
640
+ const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
496
641
 
497
642
  if (mode === "exists") {
498
643
  return {
@@ -586,35 +731,35 @@ async function toolNavigate({ url, tabId }) {
586
731
  return { tabId: tab.id, content: `Navigated to ${url}` }
587
732
  }
588
733
 
589
- async function toolClick({ selector, tabId, index = 0 }) {
734
+ async function toolClick({ selector, tabId, index = 0, timeoutMs, pollMs }) {
590
735
  if (!selector) throw new Error("Selector is required")
591
736
  const tab = await getTabById(tabId)
592
737
 
593
- const result = await runInPage(tab.id, "click", { selector, index })
738
+ const result = await runInPage(tab.id, "click", { selector, index, timeoutMs, pollMs })
594
739
  if (!result?.ok) throw new Error(result?.error || "Click failed")
595
740
  const used = result.selectorUsed || selector
596
741
  return { tabId: tab.id, content: `Clicked ${used}` }
597
742
  }
598
743
 
599
- async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
744
+ async function toolType({ selector, text, tabId, clear = false, index = 0, timeoutMs, pollMs }) {
600
745
  if (!selector) throw new Error("Selector is required")
601
746
  if (text === undefined) throw new Error("Text is required")
602
747
  const tab = await getTabById(tabId)
603
748
 
604
- const result = await runInPage(tab.id, "type", { selector, text, clear, index })
749
+ const result = await runInPage(tab.id, "type", { selector, text, clear, index, timeoutMs, pollMs })
605
750
  if (!result?.ok) throw new Error(result?.error || "Type failed")
606
751
  const used = result.selectorUsed || selector
607
752
  return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
608
753
  }
609
754
 
610
- async function toolSelect({ selector, value, label, optionIndex, tabId, index = 0 }) {
755
+ async function toolSelect({ selector, value, label, optionIndex, tabId, index = 0, timeoutMs, pollMs }) {
611
756
  if (!selector) throw new Error("Selector is required")
612
757
  if (value === undefined && label === undefined && optionIndex === undefined) {
613
758
  throw new Error("value, label, or optionIndex is required")
614
759
  }
615
760
  const tab = await getTabById(tabId)
616
761
 
617
- const result = await runInPage(tab.id, "select", { selector, value, label, optionIndex, index })
762
+ const result = await runInPage(tab.id, "select", { selector, value, label, optionIndex, index, timeoutMs, pollMs })
618
763
  if (!result?.ok) throw new Error(result?.error || "Select failed")
619
764
  const used = result.selectorUsed || selector
620
765
  const valueText = result.value ? String(result.value) : ""
@@ -820,10 +965,10 @@ async function toolQuery({
820
965
  return { tabId: tab.id, content: typeof result.value === "string" ? result.value : JSON.stringify(result.value) }
821
966
  }
822
967
 
823
- async function toolScroll({ x = 0, y = 0, selector, tabId }) {
968
+ async function toolScroll({ x = 0, y = 0, selector, tabId, timeoutMs, pollMs }) {
824
969
  const tab = await getTabById(tabId)
825
970
 
826
- const result = await runInPage(tab.id, "scroll", { x, y, selector })
971
+ const result = await runInPage(tab.id, "scroll", { x, y, selector, timeoutMs, pollMs })
827
972
  if (!result?.ok) throw new Error(result?.error || "Scroll failed")
828
973
  const target = result.selectorUsed ? `to ${result.selectorUsed}` : `by (${x}, ${y})`
829
974
  return { tabId: tab.id, content: `Scrolled ${target}` }
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
- "version": "4.3.2",
4
+ "short_name": "OpenCode Browser",
5
+ "version": "4.4.0",
5
6
  "description": "Browser automation for OpenCode",
7
+ "homepage_url": "https://github.com/different-ai/opencode-browser",
8
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9KICDxZh63m3QeQX13b37J/p7eG+cIEk4UDv+Phr7W/rqruy8DtdLFZ3WC37JYXo2Qk2iTWzIuBYjnc367RMY4Khu3f5hRyp67nvujuNY+0ACkrNGJk/viTodcW3CDcib9gijQQmPI1ZFkbec3qG0CN1hFxyn6pkdQZ8rnU9uo8ctntHIXrcfaHvQNT3AIVQfe8UjWPgalB0YCq9RamPVfMu6bG+6QLZGSKbvYDn0o5f8A/JZkm6K0MIii2EH61Oszhb/9GzZOciKqRZN+0mo23xbWHDp4ofDFyfQyMi83D3nDNldVGPSkIYlQWZaKHalaCoBRK+gzz7uuFoGrcsQIDAQAB",
6
9
  "permissions": [
7
10
  "tabs",
8
11
  "activeTab",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "4.3.2",
3
+ "version": "4.4.0",
4
4
  "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "build": "bun build src/plugin.ts --target=node --outfile=dist/plugin.js",
23
23
  "install": "node bin/cli.js install",
24
24
  "uninstall": "node bin/cli.js uninstall",
25
- "status": "node bin/cli.js status"
25
+ "status": "node bin/cli.js status",
26
+ "tool-test": "bun bin/tool-test.ts"
26
27
  },
27
28
  "keywords": [
28
29
  "opencode",
@@ -45,6 +46,9 @@
45
46
  "peerDependencies": {
46
47
  "@opencode-ai/plugin": "*"
47
48
  },
49
+ "dependencies": {
50
+ "agent-browser": "^0.4.0"
51
+ },
48
52
  "devDependencies": {
49
53
  "@opencode-ai/plugin": "*",
50
54
  "bun-types": "*"