@different-ai/opencode-browser 4.3.0 → 4.3.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.
- package/README.md +23 -2
- package/dist/plugin.js +19 -0
- package/extension/background.js +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,20 +4,32 @@ Browser automation plugin for [OpenCode](https://github.com/opencode-ai/opencode
|
|
|
4
4
|
|
|
5
5
|
Control your real Chromium browser (Chrome/Brave/Arc/Edge) using your existing profile (logins, cookies, bookmarks). No DevTools Protocol, no security prompts.
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
https://github.com/user-attachments/assets/1496b3b3-419b-436c-b412-8cda2fed83d6
|
|
9
|
+
|
|
10
|
+
|
|
7
11
|
## Why this architecture
|
|
8
12
|
|
|
9
13
|
This version is optimized for reliability and predictable multi-session behavior:
|
|
10
|
-
|
|
14
|
+
- **No MCP** -> just opencode plugin
|
|
11
15
|
- **No WebSocket port** → no port conflicts
|
|
12
16
|
- **Chrome Native Messaging** between extension and a local host process
|
|
13
17
|
- A local **broker** multiplexes multiple OpenCode plugin sessions and enforces **per-tab ownership**
|
|
14
18
|
|
|
15
19
|
## Installation
|
|
16
20
|
|
|
21
|
+
> Help me improve this!
|
|
22
|
+
|
|
17
23
|
```bash
|
|
18
|
-
|
|
24
|
+
bunx @different-ai/opencode-browser@latest install
|
|
19
25
|
```
|
|
20
26
|
|
|
27
|
+
|
|
28
|
+
https://github.com/user-attachments/assets/d5767362-fbf3-4023-858b-90f06d9f0b25
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
The installer will:
|
|
22
34
|
|
|
23
35
|
1. Copy the extension to `~/.opencode-browser/extension/`
|
|
@@ -62,6 +74,7 @@ Core primitives:
|
|
|
62
74
|
- `browser_query` (modes: `text`, `value`, `list`, `exists`, `page_text`; optional `timeoutMs`/`pollMs`)
|
|
63
75
|
- `browser_click`
|
|
64
76
|
- `browser_type`
|
|
77
|
+
- `browser_select`
|
|
65
78
|
- `browser_scroll`
|
|
66
79
|
- `browser_wait`
|
|
67
80
|
|
|
@@ -70,6 +83,14 @@ Diagnostics:
|
|
|
70
83
|
- `browser_screenshot`
|
|
71
84
|
- `browser_version`
|
|
72
85
|
|
|
86
|
+
## Roadmap
|
|
87
|
+
|
|
88
|
+
- [ ] Add tab management tools (`browser_set_active_tab`, `browser_close_tab`)
|
|
89
|
+
- [ ] Add navigation helpers (`browser_back`, `browser_forward`, `browser_reload`)
|
|
90
|
+
- [ ] Add keyboard input tool (`browser_key`)
|
|
91
|
+
- [ ] Add download support (`browser_download`, `browser_list_downloads`)
|
|
92
|
+
- [ ] Add upload support (`browser_set_file_input`)
|
|
93
|
+
|
|
73
94
|
## Troubleshooting
|
|
74
95
|
|
|
75
96
|
**Extension says native host not available**
|
package/dist/plugin.js
CHANGED
|
@@ -12562,6 +12562,25 @@ var plugin = async (ctx) => {
|
|
|
12562
12562
|
return toolResultText(data, `Typed "${text}" into ${selector}`);
|
|
12563
12563
|
}
|
|
12564
12564
|
}),
|
|
12565
|
+
browser_select: tool({
|
|
12566
|
+
description: "Select an option in a native select element",
|
|
12567
|
+
args: {
|
|
12568
|
+
selector: schema.string(),
|
|
12569
|
+
value: schema.string().optional(),
|
|
12570
|
+
label: schema.string().optional(),
|
|
12571
|
+
optionIndex: schema.number().optional(),
|
|
12572
|
+
index: schema.number().optional(),
|
|
12573
|
+
tabId: schema.number().optional()
|
|
12574
|
+
},
|
|
12575
|
+
async execute({ selector, value, label, optionIndex, index, tabId }, ctx2) {
|
|
12576
|
+
const data = await brokerRequest("tool", {
|
|
12577
|
+
tool: "select",
|
|
12578
|
+
args: { selector, value, label, optionIndex, index, tabId }
|
|
12579
|
+
});
|
|
12580
|
+
const summary = value ?? label ?? (optionIndex != null ? String(optionIndex) : "option");
|
|
12581
|
+
return toolResultText(data, `Selected ${summary} in ${selector}`);
|
|
12582
|
+
}
|
|
12583
|
+
}),
|
|
12565
12584
|
browser_screenshot: tool({
|
|
12566
12585
|
description: "Take a screenshot of the current page. Returns base64 image data URL.",
|
|
12567
12586
|
args: {
|
package/extension/background.js
CHANGED
|
@@ -104,6 +104,7 @@ async function executeTool(toolName, args) {
|
|
|
104
104
|
navigate: toolNavigate,
|
|
105
105
|
click: toolClick,
|
|
106
106
|
type: toolType,
|
|
107
|
+
select: toolSelect,
|
|
107
108
|
screenshot: toolScreenshot,
|
|
108
109
|
snapshot: toolSnapshot,
|
|
109
110
|
query: toolQuery,
|
|
@@ -253,6 +254,12 @@ async function pageOps(command, args) {
|
|
|
253
254
|
return false
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
function setSelectValue(el, value) {
|
|
258
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, "value")?.set
|
|
259
|
+
if (setter) setter.call(el, value)
|
|
260
|
+
else el.value = value
|
|
261
|
+
}
|
|
262
|
+
|
|
256
263
|
function getInputValues() {
|
|
257
264
|
const out = []
|
|
258
265
|
const nodes = document.querySelectorAll("input, textarea")
|
|
@@ -382,6 +389,66 @@ async function pageOps(command, args) {
|
|
|
382
389
|
return { ok: false, error: `Element is not typable: ${match.selectorUsed} (${tag.toLowerCase()})` }
|
|
383
390
|
}
|
|
384
391
|
|
|
392
|
+
if (command === "select") {
|
|
393
|
+
const value = typeof options.value === "string" ? options.value : null
|
|
394
|
+
const label = typeof options.label === "string" ? options.label : null
|
|
395
|
+
const optionIndex = Number.isFinite(options.optionIndex) ? options.optionIndex : null
|
|
396
|
+
const match = resolveMatches(selectors, index)
|
|
397
|
+
if (!match.chosen) {
|
|
398
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tag = match.chosen.tagName
|
|
402
|
+
if (tag !== "SELECT") {
|
|
403
|
+
return { ok: false, error: `Element is not a select: ${match.selectorUsed} (${tag.toLowerCase()})` }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (value === null && label === null && optionIndex === null) {
|
|
407
|
+
return { ok: false, error: "value, label, or optionIndex is required" }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const selectEl = match.chosen
|
|
411
|
+
const optionList = Array.from(selectEl.options || [])
|
|
412
|
+
let option = null
|
|
413
|
+
|
|
414
|
+
if (value !== null) {
|
|
415
|
+
option = optionList.find((opt) => opt.value === value)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!option && label !== null) {
|
|
419
|
+
const target = label.trim()
|
|
420
|
+
option = optionList.find((opt) => (opt.label || opt.textContent || "").trim() === target)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!option && optionIndex !== null) {
|
|
424
|
+
option = optionList[optionIndex]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!option) {
|
|
428
|
+
return { ok: false, error: "Option not found" }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
selectEl.scrollIntoView({ block: "center", inline: "center" })
|
|
433
|
+
} catch {}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
selectEl.focus()
|
|
437
|
+
} catch {}
|
|
438
|
+
|
|
439
|
+
setSelectValue(selectEl, option.value)
|
|
440
|
+
option.selected = true
|
|
441
|
+
selectEl.dispatchEvent(new Event("input", { bubbles: true }))
|
|
442
|
+
selectEl.dispatchEvent(new Event("change", { bubbles: true }))
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
selectorUsed: match.selectorUsed,
|
|
447
|
+
value: selectEl.value,
|
|
448
|
+
label: (option.label || option.textContent || "").trim(),
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
385
452
|
if (command === "scroll") {
|
|
386
453
|
const scrollX = Number.isFinite(options.x) ? options.x : 0
|
|
387
454
|
const scrollY = Number.isFinite(options.y) ? options.y : 0
|
|
@@ -540,6 +607,22 @@ async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
|
|
|
540
607
|
return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
|
|
541
608
|
}
|
|
542
609
|
|
|
610
|
+
async function toolSelect({ selector, value, label, optionIndex, tabId, index = 0 }) {
|
|
611
|
+
if (!selector) throw new Error("Selector is required")
|
|
612
|
+
if (value === undefined && label === undefined && optionIndex === undefined) {
|
|
613
|
+
throw new Error("value, label, or optionIndex is required")
|
|
614
|
+
}
|
|
615
|
+
const tab = await getTabById(tabId)
|
|
616
|
+
|
|
617
|
+
const result = await runInPage(tab.id, "select", { selector, value, label, optionIndex, index })
|
|
618
|
+
if (!result?.ok) throw new Error(result?.error || "Select failed")
|
|
619
|
+
const used = result.selectorUsed || selector
|
|
620
|
+
const valueText = result.value ? String(result.value) : ""
|
|
621
|
+
const labelText = result.label ? String(result.label) : ""
|
|
622
|
+
const summary = labelText && valueText && labelText !== valueText ? `${labelText} (${valueText})` : labelText || valueText
|
|
623
|
+
return { tabId: tab.id, content: `Selected ${summary || "option"} in ${used}` }
|
|
624
|
+
}
|
|
625
|
+
|
|
543
626
|
async function toolScreenshot({ tabId }) {
|
|
544
627
|
const tab = await getTabById(tabId)
|
|
545
628
|
const png = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
|