opencode-browser-annotation-plugin 0.5.1 → 0.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.
- package/README.md +56 -16
- package/dist/plugin.js +8 -14
- package/extension/manifest.json +1 -1
- package/extension/overlay.js +35 -2
- package/package.json +2 -1
- package/skills/browser-annotation/SKILL.md +60 -0
package/README.md
CHANGED
|
@@ -41,7 +41,23 @@ Configure with environment variables if needed:
|
|
|
41
41
|
- `OPENCODE_ANNOTATION_PORT` (default `39517`)
|
|
42
42
|
- `OPENCODE_ANNOTATION_HOST` (default `127.0.0.1`)
|
|
43
43
|
|
|
44
|
-
### 2.
|
|
44
|
+
### 2. Skill (on the OpenCode host)
|
|
45
|
+
|
|
46
|
+
The agent learns how to interpret annotations from a bundled skill, so the
|
|
47
|
+
annotation payload stays lean (no repeated instructions). Copy it into your
|
|
48
|
+
OpenCode skills directory:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mkdir -p ~/.config/opencode/skills/browser-annotation
|
|
52
|
+
cp node_modules/opencode-browser-annotation-plugin/skills/browser-annotation/SKILL.md \
|
|
53
|
+
~/.config/opencode/skills/browser-annotation/SKILL.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Restart OpenCode. The agent auto-loads this skill when an annotation arrives
|
|
57
|
+
(the payload identifies itself and references the skill). Verify with
|
|
58
|
+
`opencode debug skill`.
|
|
59
|
+
|
|
60
|
+
### 3. Extension (in your desktop Chrome)
|
|
45
61
|
|
|
46
62
|
Until it is on the Chrome Web Store, load it unpacked:
|
|
47
63
|
|
|
@@ -49,7 +65,7 @@ Until it is on the Chrome Web Store, load it unpacked:
|
|
|
49
65
|
2. Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select `extension/`.
|
|
50
66
|
3. Use a dedicated Chrome profile — the same debug profile you drive with the agent is a good choice.
|
|
51
67
|
|
|
52
|
-
###
|
|
68
|
+
### 4. Tunnel (when OpenCode is remote)
|
|
53
69
|
|
|
54
70
|
The extension (on your desktop) needs to reach the plugin server (on the host),
|
|
55
71
|
so use a **local forward**. Run on your desktop, in a separate window:
|
|
@@ -72,30 +88,48 @@ extension's **Settings** page.
|
|
|
72
88
|
|
|
73
89
|
## Use
|
|
74
90
|
|
|
75
|
-
1. Send at least one message in OpenCode so the plugin knows the active session
|
|
91
|
+
1. Send at least one message in OpenCode so the plugin knows the active session
|
|
92
|
+
(or pick a target session in the sidebar).
|
|
76
93
|
2. Press **Alt+A** on any page, then click an element (highlight follows the
|
|
77
94
|
cursor; works inside shadow DOM). A popup opens next to it.
|
|
78
|
-
3. In the popup, type an instruction
|
|
79
|
-
immediately) or **Queue** (held for context until the next Act). Then either:
|
|
95
|
+
3. In the popup, type an instruction, then either:
|
|
80
96
|
- **Send** (or Cmd/Ctrl+Enter) — submit this one right away, no sidebar needed.
|
|
81
97
|
- **Add to list** — stash it in the sidebar to batch with others.
|
|
82
98
|
4. Press **Alt+Shift+A** (or click the toolbar icon) to open the **list sidebar**,
|
|
83
|
-
review pending annotations
|
|
84
|
-
|
|
99
|
+
review pending annotations (each with a thumbnail), pick the **target session**,
|
|
100
|
+
and **Submit** them together. The footer shows a steady connection status.
|
|
85
101
|
|
|
86
102
|
Shortcuts: `Alt+A` picks an element, `Alt+Shift+A` toggles the list. If they do
|
|
87
103
|
nothing after loading an unpacked build, set them at `chrome://extensions/shortcuts`.
|
|
88
104
|
|
|
105
|
+
## How the agent handles it
|
|
106
|
+
|
|
107
|
+
When you submit, the plugin injects a short user turn into the chosen session
|
|
108
|
+
that identifies itself as a browser annotation and references the
|
|
109
|
+
`browser-annotation` skill. The agent loads that skill (installed in step 2) to
|
|
110
|
+
interpret the element metadata and locate the code — so the interpretation rules
|
|
111
|
+
live once in the skill, not in every message.
|
|
112
|
+
|
|
113
|
+
If your agents use a project `AGENTS.md`, you can make routing explicit by adding
|
|
114
|
+
a line such as:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
When a turn is a browser annotation (it says so and references the
|
|
118
|
+
`browser-annotation` skill), load that skill and use the element metadata
|
|
119
|
+
(component path, data-testid/id/role, ancestors, nearest region, text) to locate
|
|
120
|
+
and change the corresponding code. Confirm the element exists before editing.
|
|
121
|
+
```
|
|
122
|
+
|
|
89
123
|
## Payload
|
|
90
124
|
|
|
91
125
|
The extension POSTs to `/annotations`:
|
|
92
126
|
|
|
93
127
|
```json
|
|
94
128
|
{
|
|
95
|
-
"extensionVersion": "0.
|
|
129
|
+
"extensionVersion": "0.6.0",
|
|
130
|
+
"sessionID": "ses_...",
|
|
96
131
|
"annotations": [
|
|
97
132
|
{
|
|
98
|
-
"mode": "act",
|
|
99
133
|
"instruction": "Make this button larger and blue",
|
|
100
134
|
"page": { "url": "https://example.com/app", "title": "My App" },
|
|
101
135
|
"element": {
|
|
@@ -107,22 +141,28 @@ The extension POSTs to `/annotations`:
|
|
|
107
141
|
"ariaLabel": "Sign up",
|
|
108
142
|
"classes": ["cta", "primary"],
|
|
109
143
|
"text": "Sign up",
|
|
144
|
+
"componentPath": "App > Header > SignupButton",
|
|
145
|
+
"framework": "react",
|
|
146
|
+
"landmark": "header[role=banner]",
|
|
147
|
+
"ancestors": ["div.actions", "header[role=banner]"],
|
|
110
148
|
"bounds": { "x": 100, "y": 200, "width": 120, "height": 40 },
|
|
111
149
|
"inShadow": false,
|
|
112
|
-
"html": "<button class=\"cta\"
|
|
150
|
+
"html": "<button class=\"cta\" id=\"signup\">"
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
]
|
|
116
154
|
}
|
|
117
155
|
```
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
The plugin surfaces the most code-locatable identifiers first (component path,
|
|
158
|
+
testId, id, name, role, ancestors, region) and treats the CSS path and bounds as
|
|
159
|
+
weak hints. Build-generated/hashed class names are filtered out, text is
|
|
160
|
+
whitespace-collapsed, and `html` is the element's opening tag with secret-looking
|
|
161
|
+
attribute values redacted. `sessionID` is optional; without it the plugin targets
|
|
162
|
+
the last-active session.
|
|
123
163
|
|
|
124
|
-
`GET /status` returns `{ ok, activeSession, sessionID, sessionTitle,
|
|
125
|
-
for a quick health check and to
|
|
164
|
+
`GET /status` returns `{ ok, activeSession, sessionID, sessionTitle, sessions, host, port }`
|
|
165
|
+
for a quick health check and to list/target sessions.
|
|
126
166
|
|
|
127
167
|
## Develop
|
|
128
168
|
|
package/dist/plugin.js
CHANGED
|
@@ -74,7 +74,7 @@ function formatElement(el) {
|
|
|
74
74
|
lines.push(` viewport bounds: ${Math.round(b.width)}×${Math.round(b.height)} at (${Math.round(b.x)}, ${Math.round(b.y)})`);
|
|
75
75
|
}
|
|
76
76
|
if (el.html)
|
|
77
|
-
lines.push(`
|
|
77
|
+
lines.push(` opening tag: ${truncate(el.html.trim(), 500)}`);
|
|
78
78
|
return lines.join("\n");
|
|
79
79
|
}
|
|
80
80
|
function annotationBlock(a, index, total) {
|
|
@@ -92,21 +92,15 @@ function annotationBlock(a, index, total) {
|
|
|
92
92
|
.join("\n");
|
|
93
93
|
}
|
|
94
94
|
function buildPrompt(annotations) {
|
|
95
|
+
// The static interpretation rules live in the `browser-annotation` skill, not
|
|
96
|
+
// in every payload. The header marks this as a browser annotation and tells
|
|
97
|
+
// the agent to use that skill, so the rules are stated once (in the skill),
|
|
98
|
+
// never repeated per message.
|
|
95
99
|
const header = annotations.length > 1
|
|
96
|
-
? `
|
|
97
|
-
: "
|
|
100
|
+
? `Browser annotations (${annotations.length}) — handle using the \`browser-annotation\` skill. Address each one.`
|
|
101
|
+
: "Browser annotation — handle using the `browser-annotation` skill.";
|
|
98
102
|
const blocks = annotations.map((a, i) => annotationBlock(a, i, annotations.length));
|
|
99
|
-
return [
|
|
100
|
-
header,
|
|
101
|
-
"",
|
|
102
|
-
...blocks,
|
|
103
|
-
"",
|
|
104
|
-
"Guidance:",
|
|
105
|
-
"- Locate the code for each element using the most stable identifier available: framework component path first if given, then data-testid, id, name, role, then the ancestor chain and nearest region to disambiguate, then unique class or text. Treat the CSS path and viewport bounds as weak hints only.",
|
|
106
|
-
"- Use the ancestors and nearest region to find the right component/file when the element itself is generic (e.g. a bare button that appears many times).",
|
|
107
|
-
"- Confirm the element actually exists in this codebase before editing; if you cannot find it, say so instead of guessing.",
|
|
108
|
-
"- No screenshot is attached; reason from the metadata and the code.",
|
|
109
|
-
].join("\n");
|
|
103
|
+
return [header, "", ...blocks].join("\n");
|
|
110
104
|
}
|
|
111
105
|
async function readJsonBody(req, limitBytes = 2_000_000) {
|
|
112
106
|
return await new Promise((resolve, reject) => {
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Annotation",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Alt+A to annotate an element, Alt+Shift+A for the annotation list. Sends element metadata to your OpenCode agent.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"activeTab",
|
package/extension/overlay.js
CHANGED
|
@@ -338,6 +338,39 @@
|
|
|
338
338
|
return undefined;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// Collapse runs of whitespace to single spaces and trim, so a container's
|
|
342
|
+
// textContent doesn't arrive as a wall of newlines.
|
|
343
|
+
function cleanText(s) {
|
|
344
|
+
const t = (s || "").replace(/\s+/g, " ").trim();
|
|
345
|
+
return t ? t.slice(0, 300) : undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Names/attributes whose values may be secrets and must never be sent.
|
|
349
|
+
const SECRET_ATTR = /(token|csrf|authenticity|secret|api[-_]?key|password|session|auth|nonce|signature)/i;
|
|
350
|
+
|
|
351
|
+
// For the outer HTML, send only the element's own opening tag (not the whole
|
|
352
|
+
// subtree), with secret-looking attribute values redacted. This avoids leaking
|
|
353
|
+
// CSRF tokens / API keys and keeps the payload small for large containers.
|
|
354
|
+
function safeOpenTag(el) {
|
|
355
|
+
try {
|
|
356
|
+
const tag = el.tagName.toLowerCase();
|
|
357
|
+
const attrs = [];
|
|
358
|
+
for (const a of Array.from(el.attributes || [])) {
|
|
359
|
+
let v = a.value;
|
|
360
|
+
if (SECRET_ATTR.test(a.name) || (a.name === "value" && /password|hidden/i.test(el.getAttribute("type") || ""))) {
|
|
361
|
+
v = "[redacted]";
|
|
362
|
+
} else if (v && v.length > 120) {
|
|
363
|
+
v = v.slice(0, 120) + "…";
|
|
364
|
+
}
|
|
365
|
+
attrs.push(v === "" ? a.name : `${a.name}="${v}"`);
|
|
366
|
+
}
|
|
367
|
+
const open = `<${tag}${attrs.length ? " " + attrs.join(" ") : ""}>`;
|
|
368
|
+
return open.slice(0, 600);
|
|
369
|
+
} catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
341
374
|
function elementMeta(el, inShadow) {
|
|
342
375
|
const r = el.getBoundingClientRect();
|
|
343
376
|
const fw = frameworkComponents(el);
|
|
@@ -350,7 +383,7 @@
|
|
|
350
383
|
role: el.getAttribute("role") || undefined,
|
|
351
384
|
ariaLabel: el.getAttribute("aria-label") || undefined,
|
|
352
385
|
classes: cleanClasses(el.classList ? Array.from(el.classList) : []),
|
|
353
|
-
text: (el.textContent
|
|
386
|
+
text: cleanText(el.textContent),
|
|
354
387
|
href: el.getAttribute("href") || undefined,
|
|
355
388
|
src: el.getAttribute("src") || undefined,
|
|
356
389
|
bounds: { x: r.left, y: r.top, width: r.width, height: r.height },
|
|
@@ -360,7 +393,7 @@
|
|
|
360
393
|
landmark: nearestLandmark(el),
|
|
361
394
|
componentPath: fw ? fw.path : undefined,
|
|
362
395
|
framework: fw ? fw.framework : undefined,
|
|
363
|
-
html: (el
|
|
396
|
+
html: safeOpenTag(el),
|
|
364
397
|
};
|
|
365
398
|
for (const k of Object.keys(m)) if (m[k] === undefined) delete m[k];
|
|
366
399
|
if (Array.isArray(m.ancestors) && m.ancestors.length === 0) delete m.ancestors;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-browser-annotation-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Select an element in your browser, type an instruction, and send it to your OpenCode agent over a loopback + SSH tunnel. Text and element metadata only (no screenshots).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
30
|
"extension",
|
|
31
|
+
"skills",
|
|
31
32
|
"README.md"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-annotation
|
|
3
|
+
description: >-
|
|
4
|
+
Interpret and act on browser annotations sent by the
|
|
5
|
+
opencode-browser-annotation extension. Load this whenever a message says it is
|
|
6
|
+
a "Browser annotation" or references the `browser-annotation` skill, or when a
|
|
7
|
+
turn contains a "Selected element:" block with fields like data-testid,
|
|
8
|
+
ancestors, nearest region, or a component path. It explains how to turn that
|
|
9
|
+
element metadata into the correct code location and change.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Browser annotations
|
|
13
|
+
|
|
14
|
+
Messages from the `opencode-browser-annotation` extension describe a DOM element
|
|
15
|
+
the user selected in their browser plus an instruction. Your job is to map that
|
|
16
|
+
element to the right code and carry out the instruction. The payload is text and
|
|
17
|
+
element metadata only — no screenshot is attached.
|
|
18
|
+
|
|
19
|
+
## How to locate the code
|
|
20
|
+
|
|
21
|
+
Use the most stable identifier available, in this order:
|
|
22
|
+
|
|
23
|
+
1. **Framework component path** (e.g. `App > Dashboard > SignupCard`) — the
|
|
24
|
+
strongest hint. Find that component's source file first.
|
|
25
|
+
2. **`data-testid` / `id` / `name` / `role`** on the element itself — usually
|
|
26
|
+
maps directly to source.
|
|
27
|
+
3. **Ancestors (nearest first) + nearest region** — use these to disambiguate a
|
|
28
|
+
generic element (e.g. a bare `<button>` that appears many times). They tell
|
|
29
|
+
you which container/feature/file the element belongs to.
|
|
30
|
+
4. **Unique class or text** — human-authored class names and the element's text
|
|
31
|
+
are good locators. Build-generated/hashed class names are already filtered
|
|
32
|
+
out; treat any remaining opaque tokens as weak.
|
|
33
|
+
5. **CSS path and viewport bounds** — weak hints only; do not rely on them.
|
|
34
|
+
|
|
35
|
+
## Rules
|
|
36
|
+
|
|
37
|
+
- Confirm the element actually exists in this codebase before editing. If you
|
|
38
|
+
cannot find it, say so instead of guessing or inventing a file.
|
|
39
|
+
- Reason from the metadata and the actual code; there is no image.
|
|
40
|
+
- The `opening tag` field is the element's own opening tag with secret-looking
|
|
41
|
+
attribute values redacted — do not expect a full HTML subtree, and never try
|
|
42
|
+
to recover redacted values.
|
|
43
|
+
- If several annotations arrive together, address each one.
|
|
44
|
+
- Treat the page URL as context for which route/screen the user was on.
|
|
45
|
+
|
|
46
|
+
## Field reference
|
|
47
|
+
|
|
48
|
+
- `Element` — tag name.
|
|
49
|
+
- `data-testid`, `id`, `name`, `role`, `aria-label` — element identifiers.
|
|
50
|
+
- `classes` — authored class names (hashed/build-generated ones removed).
|
|
51
|
+
- `text` — the element's visible text (whitespace-collapsed, truncated).
|
|
52
|
+
- `href` / `src` — link or media source when present.
|
|
53
|
+
- `<framework> components` — component hierarchy from React/Preact/Vue when
|
|
54
|
+
detectable.
|
|
55
|
+
- `nearest region` — closest semantic landmark/region ancestor.
|
|
56
|
+
- `ancestors (nearest first)` — parent chain signatures, shadow DOM pierced.
|
|
57
|
+
- `css path` — a positional selector; weak.
|
|
58
|
+
- `context` — notes such as "inside a shadow DOM" or "inside an iframe".
|
|
59
|
+
- `viewport bounds` — on-screen size/position; weak.
|
|
60
|
+
- `opening tag` — redacted opening tag of the element.
|