opencode-browser-annotation-plugin 0.1.3 → 0.2.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 +34 -15
- package/dist/plugin.js +121 -48
- package/extension/background.js +44 -38
- package/extension/manifest.json +24 -9
- package/extension/options.html +25 -46
- package/extension/overlay.js +521 -0
- package/package.json +1 -1
- package/extension/content.js +0 -126
- package/extension/popup.html +0 -88
- package/extension/popup.js +0 -71
package/README.md
CHANGED
|
@@ -12,16 +12,16 @@ the code from the selector/DOM context you send.
|
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
Desktop Chrome (extension)
|
|
15
|
-
└─ pick element
|
|
16
|
-
└─ POST http://127.0.0.1:39517/annotations (via ssh -
|
|
15
|
+
└─ Alt+A → overlay + sidebar → pick element, type instruction, Act/Queue → [Submit]
|
|
16
|
+
└─ POST http://127.0.0.1:39517/annotations (via ssh -L when remote)
|
|
17
17
|
OpenCode host (plugin)
|
|
18
18
|
└─ HTTP server on 127.0.0.1 → injects a turn into the active session
|
|
19
19
|
└─ agent receives the instruction + element metadata and acts
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The plugin binds to `127.0.0.1` only. When OpenCode is on a remote host, an
|
|
23
|
-
`ssh -
|
|
24
|
-
SSH provides the auth and encryption.
|
|
23
|
+
`ssh -L` local forward carries the extension's POST from your desktop to it — no
|
|
24
|
+
public port, and SSH provides the auth and encryption.
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
@@ -51,16 +51,19 @@ Until it is on the Chrome Web Store, load it unpacked:
|
|
|
51
51
|
|
|
52
52
|
### 3. Tunnel (when OpenCode is remote)
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
The extension (on your desktop) needs to reach the plugin server (on the host),
|
|
55
|
+
so use a **local forward**. Run on your desktop, in a separate window:
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
ssh -N -
|
|
58
|
+
ssh -N -L 39517:127.0.0.1:39517 you@host
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
If you also drive the browser from the agent (desktop-drive),
|
|
61
|
+
If you also drive the browser from the agent (desktop-drive on 9333), that is the
|
|
62
|
+
opposite direction (host → desktop), so it uses a reverse forward. You can run
|
|
63
|
+
both at once:
|
|
61
64
|
|
|
62
65
|
```bash
|
|
63
|
-
ssh -N -
|
|
66
|
+
ssh -N -L 39517:127.0.0.1:39517 -R 9333:127.0.0.1:9333 you@host
|
|
64
67
|
```
|
|
65
68
|
|
|
66
69
|
If OpenCode runs on the same machine as your browser, no tunnel is needed — the
|
|
@@ -70,10 +73,15 @@ extension's **Settings** page.
|
|
|
70
73
|
## Use
|
|
71
74
|
|
|
72
75
|
1. Send at least one message in OpenCode so the plugin knows the active session.
|
|
73
|
-
2.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
2. Press **Alt+A** on any page to open the annotation overlay (or click the
|
|
77
|
+
toolbar icon). Press Alt+A again or Esc to close it.
|
|
78
|
+
3. In the sidebar, click **Select element**, hover to highlight, and click the
|
|
79
|
+
element you want (works inside shadow DOM).
|
|
80
|
+
4. Type an instruction in the card. Choose **Act now** (agent responds
|
|
81
|
+
immediately) or **Queue** (held for context until the next Act). Repeat for
|
|
82
|
+
more elements.
|
|
83
|
+
5. Click **Submit to agent**. The sidebar footer shows the connection and active
|
|
84
|
+
session; a toast confirms how many were sent vs queued.
|
|
77
85
|
|
|
78
86
|
## Payload
|
|
79
87
|
|
|
@@ -81,18 +89,23 @@ The extension POSTs to `/annotations`:
|
|
|
81
89
|
|
|
82
90
|
```json
|
|
83
91
|
{
|
|
84
|
-
"extensionVersion": "0.
|
|
92
|
+
"extensionVersion": "0.2.0",
|
|
85
93
|
"annotations": [
|
|
86
94
|
{
|
|
95
|
+
"mode": "act",
|
|
87
96
|
"instruction": "Make this button larger and blue",
|
|
88
97
|
"page": { "url": "https://example.com/app", "title": "My App" },
|
|
89
98
|
"element": {
|
|
90
99
|
"selector": "button.cta",
|
|
91
100
|
"tag": "BUTTON",
|
|
92
|
-
"
|
|
101
|
+
"id": "signup",
|
|
102
|
+
"testId": "signup-cta",
|
|
93
103
|
"role": "button",
|
|
94
104
|
"ariaLabel": "Sign up",
|
|
105
|
+
"classes": ["cta", "primary"],
|
|
106
|
+
"text": "Sign up",
|
|
95
107
|
"bounds": { "x": 100, "y": 200, "width": 120, "height": 40 },
|
|
108
|
+
"inShadow": false,
|
|
96
109
|
"html": "<button class=\"cta\">Sign up</button>"
|
|
97
110
|
}
|
|
98
111
|
}
|
|
@@ -100,7 +113,13 @@ The extension POSTs to `/annotations`:
|
|
|
100
113
|
}
|
|
101
114
|
```
|
|
102
115
|
|
|
103
|
-
`
|
|
116
|
+
`mode` is `"act"` (respond now, flushing any queued) or `"queue"` (hold for
|
|
117
|
+
context until the next act). The plugin surfaces the most code-locatable
|
|
118
|
+
identifiers first (testId, id, name, role) and treats the CSS path and bounds as
|
|
119
|
+
weak hints.
|
|
120
|
+
|
|
121
|
+
`GET /status` returns `{ ok, activeSession, sessionID, sessionTitle, queued, host, port }`
|
|
122
|
+
for a quick health check and to show the target session.
|
|
104
123
|
|
|
105
124
|
## Develop
|
|
106
125
|
|
package/dist/plugin.js
CHANGED
|
@@ -3,10 +3,13 @@ import { createServer } from "node:http";
|
|
|
3
3
|
* opencode-browser-annotation-plugin
|
|
4
4
|
*
|
|
5
5
|
* Runs a loopback HTTP server on the OpenCode host. A browser extension POSTs
|
|
6
|
-
* annotations (a typed instruction plus selected-element metadata) to it
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* annotations (a typed instruction plus selected-element metadata) to it. When
|
|
7
|
+
* the browser runs on a separate desktop, the extension reaches this server over
|
|
8
|
+
* an `ssh -L` local forward (desktop -> host).
|
|
9
|
+
*
|
|
10
|
+
* On receipt the plugin injects a new user turn into the most recently active
|
|
11
|
+
* OpenCode session so the agent responds. Annotations may be acted on
|
|
12
|
+
* immediately ("act") or queued and flushed with a later act ("queue").
|
|
10
13
|
*
|
|
11
14
|
* Scope: text + element metadata only. No screenshots, no image/vision.
|
|
12
15
|
*/
|
|
@@ -23,52 +26,79 @@ function envPort() {
|
|
|
23
26
|
function truncate(value, max) {
|
|
24
27
|
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
25
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Surface the most code-locatable identifiers first: test ids and framework
|
|
31
|
+
* hooks are what map to source, not pixel bounds.
|
|
32
|
+
*/
|
|
26
33
|
function formatElement(el) {
|
|
27
34
|
if (!el)
|
|
28
|
-
return "
|
|
35
|
+
return " (no element captured)";
|
|
29
36
|
const lines = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (el.
|
|
33
|
-
lines.push(
|
|
37
|
+
const tag = el.tag ? el.tag.toLowerCase() : "element";
|
|
38
|
+
lines.push(` Element: <${tag}>`);
|
|
39
|
+
if (el.testId)
|
|
40
|
+
lines.push(` data-testid: ${el.testId}`);
|
|
41
|
+
if (el.id)
|
|
42
|
+
lines.push(` id: ${el.id}`);
|
|
43
|
+
if (el.name)
|
|
44
|
+
lines.push(` name: ${el.name}`);
|
|
34
45
|
if (el.role)
|
|
35
|
-
lines.push(
|
|
46
|
+
lines.push(` role: ${el.role}`);
|
|
36
47
|
if (el.ariaLabel)
|
|
37
|
-
lines.push(
|
|
48
|
+
lines.push(` aria-label: ${el.ariaLabel}`);
|
|
49
|
+
if (el.classes && el.classes.length)
|
|
50
|
+
lines.push(` classes: ${el.classes.slice(0, 8).join(" ")}`);
|
|
51
|
+
if (el.href)
|
|
52
|
+
lines.push(` href: ${el.href}`);
|
|
53
|
+
if (el.src)
|
|
54
|
+
lines.push(` src: ${el.src}`);
|
|
38
55
|
if (el.text)
|
|
39
|
-
lines.push(
|
|
56
|
+
lines.push(` text: ${JSON.stringify(truncate(el.text.trim(), 200))}`);
|
|
57
|
+
if (el.selector)
|
|
58
|
+
lines.push(` css path: ${el.selector}`);
|
|
59
|
+
const context = [];
|
|
60
|
+
if (el.inShadow)
|
|
61
|
+
context.push("inside a shadow DOM");
|
|
62
|
+
if (el.inIframe)
|
|
63
|
+
context.push(`inside an iframe${el.framePath ? ` (${el.framePath})` : ""}`);
|
|
64
|
+
if (context.length)
|
|
65
|
+
lines.push(` context: ${context.join(", ")}`);
|
|
40
66
|
if (el.bounds) {
|
|
41
67
|
const b = el.bounds;
|
|
42
|
-
lines.push(
|
|
68
|
+
lines.push(` viewport bounds: ${Math.round(b.width)}×${Math.round(b.height)} at (${Math.round(b.x)}, ${Math.round(b.y)})`);
|
|
43
69
|
}
|
|
44
70
|
if (el.html)
|
|
45
|
-
lines.push(
|
|
46
|
-
return lines.
|
|
71
|
+
lines.push(` outer html: ${truncate(el.html.trim(), 500)}`);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
function annotationBlock(a, index, total) {
|
|
75
|
+
const label = total > 1 ? ` ${index + 1}` : "";
|
|
76
|
+
const page = a.page ?? {};
|
|
77
|
+
const instruction = (a.instruction ?? "").trim() || "(no instruction text)";
|
|
78
|
+
return [
|
|
79
|
+
`### Annotation${label}`,
|
|
80
|
+
`Instruction: ${instruction}`,
|
|
81
|
+
page.url ? `Page: ${page.title ? `${page.title} — ` : ""}${page.url}` : "",
|
|
82
|
+
"Selected element:",
|
|
83
|
+
formatElement(a.element),
|
|
84
|
+
]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("\n");
|
|
47
87
|
}
|
|
48
88
|
function buildPrompt(annotations) {
|
|
49
89
|
const header = annotations.length > 1
|
|
50
|
-
? `The user
|
|
51
|
-
: "The user
|
|
52
|
-
const blocks = annotations.map((a, i) =>
|
|
53
|
-
const n = annotations.length > 1 ? ` ${i + 1}` : "";
|
|
54
|
-
const page = a.page ?? {};
|
|
55
|
-
const instruction = (a.instruction ?? "").trim() || "(no instruction text)";
|
|
56
|
-
return [
|
|
57
|
-
`## Annotation${n}`,
|
|
58
|
-
`Instruction: ${instruction}`,
|
|
59
|
-
page.url ? `Page: ${page.title ? `${page.title} — ` : ""}${page.url}` : "",
|
|
60
|
-
"Selected element:",
|
|
61
|
-
formatElement(a.element),
|
|
62
|
-
]
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join("\n");
|
|
65
|
-
});
|
|
90
|
+
? `The user made ${annotations.length} annotations in the browser. Address each one.`
|
|
91
|
+
: "The user made an annotation in the browser.";
|
|
92
|
+
const blocks = annotations.map((a, i) => annotationBlock(a, i, annotations.length));
|
|
66
93
|
return [
|
|
67
94
|
header,
|
|
68
95
|
"",
|
|
69
96
|
...blocks,
|
|
70
97
|
"",
|
|
71
|
-
"
|
|
98
|
+
"Guidance:",
|
|
99
|
+
"- Locate the code for each element using the most stable identifier available (data-testid, id, name, role, then unique class or text). Treat the CSS path and viewport bounds as weak hints only.",
|
|
100
|
+
"- Confirm the element actually exists in this codebase before editing; if you cannot find it, say so instead of guessing.",
|
|
101
|
+
"- No screenshot is attached; reason from the metadata and the code.",
|
|
72
102
|
].join("\n");
|
|
73
103
|
}
|
|
74
104
|
async function readJsonBody(req, limitBytes = 2_000_000) {
|
|
@@ -112,38 +142,71 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
112
142
|
const host = envHost();
|
|
113
143
|
const port = envPort();
|
|
114
144
|
let activeSessionID = null;
|
|
145
|
+
let activeSessionTitle = null;
|
|
115
146
|
let server = null;
|
|
147
|
+
const queued = [];
|
|
116
148
|
const log = (level, message, extra) => {
|
|
117
149
|
void client.app
|
|
118
150
|
.log({ body: { service: "browser-annotation", level, message, extra } })
|
|
119
151
|
.catch(() => { });
|
|
120
152
|
};
|
|
121
|
-
async function
|
|
122
|
-
if (!activeSessionID) {
|
|
123
|
-
return { ok: false, error: "No active OpenCode session yet. Send a message in OpenCode first." };
|
|
124
|
-
}
|
|
153
|
+
async function injectPrompt(annotations) {
|
|
125
154
|
try {
|
|
126
|
-
// promptAsync injects the turn without blocking on the agent's full
|
|
127
|
-
// response, so the extension gets a prompt acknowledgement.
|
|
128
155
|
await client.session.promptAsync({
|
|
129
156
|
path: { id: activeSessionID },
|
|
130
157
|
query: { directory },
|
|
131
158
|
body: { parts: [{ type: "text", text: buildPrompt(annotations) }] },
|
|
132
159
|
});
|
|
133
|
-
return { ok: true
|
|
160
|
+
return { ok: true };
|
|
134
161
|
}
|
|
135
162
|
catch (error) {
|
|
136
|
-
|
|
137
|
-
return { ok: false, error: message, sessionID: activeSessionID };
|
|
163
|
+
return { ok: false, error: error instanceof Error ? error.message : "session.prompt failed" };
|
|
138
164
|
}
|
|
139
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Queue annotations are held until an "act" annotation arrives (or all-queue
|
|
168
|
+
* submits nothing yet). An act annotation flushes the queue plus itself.
|
|
169
|
+
*/
|
|
170
|
+
async function handleSubmit(annotations) {
|
|
171
|
+
if (!activeSessionID) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: "No active OpenCode session yet. Send a message in OpenCode first.",
|
|
175
|
+
injected: 0,
|
|
176
|
+
queued: queued.length,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const toQueue = annotations.filter((a) => a.mode === "queue");
|
|
180
|
+
const toAct = annotations.filter((a) => a.mode !== "queue");
|
|
181
|
+
queued.push(...toQueue);
|
|
182
|
+
if (toAct.length === 0) {
|
|
183
|
+
return { ok: true, injected: 0, queued: queued.length, sessionID: activeSessionID };
|
|
184
|
+
}
|
|
185
|
+
const batch = [...queued, ...toAct];
|
|
186
|
+
queued.length = 0;
|
|
187
|
+
const result = await injectPrompt(batch);
|
|
188
|
+
if (!result.ok) {
|
|
189
|
+
// Re-queue so nothing is lost.
|
|
190
|
+
queued.unshift(...batch.filter((a) => a.mode === "queue"));
|
|
191
|
+
return { ok: false, error: result.error, injected: 0, queued: queued.length, sessionID: activeSessionID };
|
|
192
|
+
}
|
|
193
|
+
return { ok: true, injected: batch.length, queued: queued.length, sessionID: activeSessionID };
|
|
194
|
+
}
|
|
140
195
|
function handle(req, res) {
|
|
141
196
|
if (req.method === "OPTIONS") {
|
|
142
197
|
sendJson(res, 204, {});
|
|
143
198
|
return;
|
|
144
199
|
}
|
|
145
200
|
if (req.method === "GET" && req.url === "/status") {
|
|
146
|
-
sendJson(res, 200, {
|
|
201
|
+
sendJson(res, 200, {
|
|
202
|
+
ok: true,
|
|
203
|
+
activeSession: Boolean(activeSessionID),
|
|
204
|
+
sessionID: activeSessionID,
|
|
205
|
+
sessionTitle: activeSessionTitle,
|
|
206
|
+
queued: queued.length,
|
|
207
|
+
host,
|
|
208
|
+
port,
|
|
209
|
+
});
|
|
147
210
|
return;
|
|
148
211
|
}
|
|
149
212
|
if (req.method === "POST" && req.url === "/annotations") {
|
|
@@ -155,13 +218,15 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
155
218
|
sendJson(res, 400, { ok: false, error: "No annotations in payload." });
|
|
156
219
|
return;
|
|
157
220
|
}
|
|
158
|
-
const result = await
|
|
221
|
+
const result = await handleSubmit(annotations);
|
|
159
222
|
if (result.ok) {
|
|
160
|
-
log("info", `
|
|
161
|
-
|
|
223
|
+
log("info", `Annotations: injected ${result.injected}, queued ${result.queued}`, {
|
|
224
|
+
sessionID: result.sessionID,
|
|
225
|
+
});
|
|
226
|
+
sendJson(res, 200, result);
|
|
162
227
|
}
|
|
163
228
|
else {
|
|
164
|
-
log("warn", `Annotation
|
|
229
|
+
log("warn", `Annotation submit failed: ${result.error}`);
|
|
165
230
|
sendJson(res, 409, result);
|
|
166
231
|
}
|
|
167
232
|
})
|
|
@@ -198,9 +263,17 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
198
263
|
activeSessionID = input.sessionID;
|
|
199
264
|
},
|
|
200
265
|
event: async ({ event }) => {
|
|
201
|
-
if (event.type === "session.
|
|
202
|
-
|
|
266
|
+
if (event.type === "session.updated") {
|
|
267
|
+
const info = event.properties.info;
|
|
268
|
+
if (info.id === activeSessionID && typeof info.title === "string") {
|
|
269
|
+
activeSessionTitle = info.title;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (event.type === "session.deleted") {
|
|
273
|
+
if (event.properties.info.id === activeSessionID) {
|
|
203
274
|
activeSessionID = null;
|
|
275
|
+
activeSessionTitle = null;
|
|
276
|
+
}
|
|
204
277
|
}
|
|
205
278
|
},
|
|
206
279
|
};
|
package/extension/background.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Background service worker
|
|
2
|
-
// the
|
|
3
|
-
//
|
|
1
|
+
// Background service worker.
|
|
2
|
+
// - Toggles the annotation overlay on Alt+A (command) or toolbar click.
|
|
3
|
+
// - Performs all network I/O to the plugin endpoint (status + submit), because
|
|
4
|
+
// page CSP can block a content script from fetching localhost. Results are
|
|
5
|
+
// returned to the overlay via message responses.
|
|
4
6
|
|
|
5
7
|
const DEFAULT_ENDPOINT = "http://127.0.0.1:39517";
|
|
6
8
|
const EXTENSION_VERSION = chrome.runtime.getManifest().version;
|
|
@@ -10,47 +12,52 @@ async function getEndpoint() {
|
|
|
10
12
|
return (endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
async function
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
async function toggleOverlay(tab) {
|
|
16
|
+
if (!tab?.id) return;
|
|
17
|
+
try {
|
|
18
|
+
// The overlay content script listens for this and toggles itself. If it is
|
|
19
|
+
// not injected yet, inject it first, then it opens on load.
|
|
20
|
+
const [{ result } = {}] = await chrome.scripting.executeScript({
|
|
21
|
+
target: { tabId: tab.id },
|
|
22
|
+
func: () => Boolean(window.__ocAnnotationInjected),
|
|
23
|
+
});
|
|
24
|
+
if (!result) {
|
|
25
|
+
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["overlay.js"] });
|
|
26
|
+
} else {
|
|
27
|
+
await chrome.tabs.sendMessage(tab.id, { type: "oc-toggle" });
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// e.g. chrome:// pages or the web store cannot be injected.
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
async
|
|
19
|
-
|
|
20
|
-
}
|
|
34
|
+
chrome.commands.onCommand.addListener(async (command) => {
|
|
35
|
+
if (command !== "toggle-overlay") return;
|
|
36
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
37
|
+
await toggleOverlay(tab);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
chrome.action.onClicked.addListener((tab) => {
|
|
41
|
+
void toggleOverlay(tab);
|
|
42
|
+
});
|
|
21
43
|
|
|
22
44
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
23
45
|
(async () => {
|
|
24
|
-
if (msg?.type === "oc-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
sendResponse({ ok: true, annotations: await getAnnotations() });
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (msg?.type === "oc-clear") {
|
|
38
|
-
await setAnnotations([]);
|
|
39
|
-
sendResponse({ ok: true });
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (msg?.type === "oc-remove" && typeof msg.index === "number") {
|
|
44
|
-
const list = await getAnnotations();
|
|
45
|
-
list.splice(msg.index, 1);
|
|
46
|
-
await setAnnotations(list);
|
|
47
|
-
sendResponse({ ok: true, annotations: list });
|
|
46
|
+
if (msg?.type === "oc-status") {
|
|
47
|
+
try {
|
|
48
|
+
const endpoint = await getEndpoint();
|
|
49
|
+
const res = await fetch(`${endpoint}/status`, { method: "GET" });
|
|
50
|
+
const data = await res.json().catch(() => ({}));
|
|
51
|
+
sendResponse({ ok: res.ok, endpoint, data });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
sendResponse({ ok: false, error: error?.message || "unreachable" });
|
|
54
|
+
}
|
|
48
55
|
return;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
if (msg?.type === "oc-submit") {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
59
|
+
const annotations = Array.isArray(msg.annotations) ? msg.annotations : [];
|
|
60
|
+
if (annotations.length === 0) {
|
|
54
61
|
sendResponse({ ok: false, error: "No annotations to submit." });
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
@@ -59,12 +66,11 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
59
66
|
const res = await fetch(`${endpoint}/annotations`, {
|
|
60
67
|
method: "POST",
|
|
61
68
|
headers: { "content-type": "application/json" },
|
|
62
|
-
body: JSON.stringify({ extensionVersion: EXTENSION_VERSION, annotations
|
|
69
|
+
body: JSON.stringify({ extensionVersion: EXTENSION_VERSION, annotations }),
|
|
63
70
|
});
|
|
64
71
|
const data = await res.json().catch(() => ({}));
|
|
65
72
|
if (res.ok && data.ok) {
|
|
66
|
-
|
|
67
|
-
sendResponse({ ok: true, count: data.count });
|
|
73
|
+
sendResponse({ ok: true, injected: data.injected ?? 0, queued: data.queued ?? 0, sessionID: data.sessionID });
|
|
68
74
|
} else {
|
|
69
75
|
sendResponse({ ok: false, error: data.error || `HTTP ${res.status}` });
|
|
70
76
|
}
|
package/extension/manifest.json
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Annotation",
|
|
4
|
-
"version": "0.1
|
|
5
|
-
"description": "
|
|
6
|
-
"permissions": [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "Alt+A to annotate elements and send them to your OpenCode agent. Text and element metadata only.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"activeTab",
|
|
8
|
+
"scripting",
|
|
9
|
+
"storage"
|
|
10
|
+
],
|
|
11
|
+
"host_permissions": [
|
|
12
|
+
"http://127.0.0.1/*",
|
|
13
|
+
"http://localhost/*"
|
|
14
|
+
],
|
|
12
15
|
"background": {
|
|
13
16
|
"service_worker": "background.js",
|
|
14
17
|
"type": "module"
|
|
15
18
|
},
|
|
16
|
-
"
|
|
19
|
+
"action": {
|
|
20
|
+
"default_title": "Toggle OpenCode Annotation (Alt+A)"
|
|
21
|
+
},
|
|
22
|
+
"options_page": "options.html",
|
|
23
|
+
"commands": {
|
|
24
|
+
"toggle-overlay": {
|
|
25
|
+
"suggested_key": {
|
|
26
|
+
"default": "Alt+A",
|
|
27
|
+
"mac": "Alt+A"
|
|
28
|
+
},
|
|
29
|
+
"description": "Toggle the annotation overlay"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
17
32
|
}
|
package/extension/options.html
CHANGED
|
@@ -3,65 +3,44 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<style>
|
|
6
|
+
:root { color-scheme: light; }
|
|
6
7
|
body {
|
|
7
|
-
font
|
|
8
|
-
max-width:
|
|
9
|
-
margin: 24px auto;
|
|
10
|
-
padding: 0 16px;
|
|
11
|
-
color: #1a1a1a;
|
|
12
|
-
}
|
|
13
|
-
label {
|
|
14
|
-
display: block;
|
|
15
|
-
font-weight: 600;
|
|
16
|
-
margin-bottom: 6px;
|
|
8
|
+
font: 14px/1.5 system-ui, -apple-system, sans-serif;
|
|
9
|
+
max-width: 540px; margin: 40px auto; padding: 0 20px; color: #1c1d21;
|
|
17
10
|
}
|
|
11
|
+
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
12
|
+
p.sub { color: #6a6d75; margin: 0 0 24px; }
|
|
13
|
+
label { display: block; font-weight: 650; margin-bottom: 6px; }
|
|
18
14
|
input {
|
|
19
|
-
width: 100%;
|
|
20
|
-
|
|
21
|
-
padding: 8px;
|
|
22
|
-
border: 1px solid #ccc;
|
|
23
|
-
border-radius: 6px;
|
|
24
|
-
box-sizing: border-box;
|
|
15
|
+
width: 100%; box-sizing: border-box; font: inherit;
|
|
16
|
+
padding: 10px 12px; border: 1px solid rgba(0,0,0,.15); border-radius: 10px;
|
|
25
17
|
}
|
|
18
|
+
input:focus { outline: none; border-color: #2b7cff; box-shadow: 0 0 0 3px rgba(43,124,255,.15); }
|
|
26
19
|
button {
|
|
27
|
-
font: inherit;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
border: 1px solid #2b7cff;
|
|
31
|
-
border-radius: 6px;
|
|
32
|
-
background: #2b7cff;
|
|
33
|
-
color: #fff;
|
|
34
|
-
cursor: pointer;
|
|
35
|
-
}
|
|
36
|
-
p {
|
|
37
|
-
color: #555;
|
|
38
|
-
font-size: 13px;
|
|
39
|
-
line-height: 1.5;
|
|
40
|
-
}
|
|
41
|
-
code {
|
|
42
|
-
background: #f2f2f2;
|
|
43
|
-
padding: 1px 4px;
|
|
44
|
-
border-radius: 4px;
|
|
45
|
-
}
|
|
46
|
-
#saved {
|
|
47
|
-
color: #0a0;
|
|
48
|
-
font-size: 13px;
|
|
49
|
-
margin-left: 8px;
|
|
20
|
+
font: inherit; font-weight: 600; margin-top: 16px;
|
|
21
|
+
padding: 9px 16px; border-radius: 10px; cursor: pointer;
|
|
22
|
+
color: #fff; background: #2b7cff; border: 1px solid #2b7cff;
|
|
50
23
|
}
|
|
24
|
+
button:hover { background: #1e6ef0; }
|
|
25
|
+
.hint { color: #6a6d75; font-size: 13px; line-height: 1.6; margin-top: 18px; }
|
|
26
|
+
code { background: #f0f1f4; padding: 2px 6px; border-radius: 6px; font: 12.5px ui-monospace, monospace; }
|
|
27
|
+
#saved { color: #178048; font-size: 13px; margin-left: 10px; }
|
|
51
28
|
</style>
|
|
52
29
|
</head>
|
|
53
30
|
<body>
|
|
54
|
-
<h1>OpenCode Annotation
|
|
31
|
+
<h1>OpenCode Annotation</h1>
|
|
32
|
+
<p class="sub">Send element annotations to your OpenCode agent. Text and metadata only.</p>
|
|
33
|
+
|
|
55
34
|
<label for="endpoint">Plugin endpoint</label>
|
|
56
35
|
<input id="endpoint" type="text" placeholder="http://127.0.0.1:39517" />
|
|
57
|
-
<p>
|
|
58
|
-
The URL of the plugin's HTTP server. When OpenCode runs on a remote host,
|
|
59
|
-
forward the port with a reverse tunnel and point this at the local end:
|
|
60
|
-
<br />
|
|
61
|
-
<code>ssh -N -R 39517:127.0.0.1:39517 you@server</code>
|
|
62
|
-
</p>
|
|
63
36
|
<button id="save">Save</button>
|
|
64
37
|
<span id="saved"></span>
|
|
38
|
+
|
|
39
|
+
<div class="hint">
|
|
40
|
+
<p><strong>Shortcut:</strong> press <code>Alt+A</code> on any page to toggle the annotation overlay.</p>
|
|
41
|
+
<p>When OpenCode runs on a remote host, forward the port from your desktop and point this at the local end:</p>
|
|
42
|
+
<p><code>ssh -N -L 39517:127.0.0.1:39517 you@host</code></p>
|
|
43
|
+
</div>
|
|
65
44
|
<script src="options.js"></script>
|
|
66
45
|
</body>
|
|
67
46
|
</html>
|