opencode-browser-annotation-plugin 0.3.0 → 0.3.2
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/dist/plugin.js +54 -52
- package/extension/background.js +1 -1
- package/extension/manifest.json +18 -5
- package/extension/overlay.js +120 -44
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -7,9 +7,9 @@ import { createServer } from "node:http";
|
|
|
7
7
|
* the browser runs on a separate desktop, the extension reaches this server over
|
|
8
8
|
* an `ssh -L` local forward (desktop -> host).
|
|
9
9
|
*
|
|
10
|
-
* On receipt the plugin injects a new user turn into the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* On receipt the plugin injects a new user turn into the chosen OpenCode session
|
|
11
|
+
* (the extension may target any session by id; otherwise the most recently
|
|
12
|
+
* active one) so the agent responds.
|
|
13
13
|
*
|
|
14
14
|
* Scope: text + element metadata only. No screenshots, no image/vision.
|
|
15
15
|
*/
|
|
@@ -142,18 +142,35 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
142
142
|
const host = envHost();
|
|
143
143
|
const port = envPort();
|
|
144
144
|
let activeSessionID = null;
|
|
145
|
-
let activeSessionTitle = null;
|
|
146
145
|
let server = null;
|
|
147
|
-
const queued = [];
|
|
148
146
|
const log = (level, message, extra) => {
|
|
149
147
|
void client.app
|
|
150
148
|
.log({ body: { service: "browser-annotation", level, message, extra } })
|
|
151
149
|
.catch(() => { });
|
|
152
150
|
};
|
|
153
|
-
|
|
151
|
+
/**
|
|
152
|
+
* All sessions live in this one OpenCode process, so the plugin can address
|
|
153
|
+
* any of them by id — a single HTTP port/tunnel does not limit targeting.
|
|
154
|
+
* Returns top-level sessions (no parentID), most-recently-updated first.
|
|
155
|
+
*/
|
|
156
|
+
async function listSessions() {
|
|
157
|
+
try {
|
|
158
|
+
const res = (await client.session.list({ query: { directory } }));
|
|
159
|
+
const rows = Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : [];
|
|
160
|
+
return rows
|
|
161
|
+
.filter((s) => s && typeof s.id === "string" && !s.parentID)
|
|
162
|
+
.map((s) => ({ id: s.id, title: typeof s.title === "string" ? s.title : s.id, updated: s.time?.updated ?? 0 }))
|
|
163
|
+
.sort((a, b) => b.updated - a.updated);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
log("warn", `session.list failed: ${error instanceof Error ? error.message : "unknown"}`);
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function injectPrompt(sessionID, annotations) {
|
|
154
171
|
try {
|
|
155
172
|
await client.session.promptAsync({
|
|
156
|
-
path: { id:
|
|
173
|
+
path: { id: sessionID },
|
|
157
174
|
query: { directory },
|
|
158
175
|
body: { parts: [{ type: "text", text: buildPrompt(annotations) }] },
|
|
159
176
|
});
|
|
@@ -163,34 +180,26 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
163
180
|
return { ok: false, error: error instanceof Error ? error.message : "session.prompt failed" };
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
async function handleSubmit(annotations, requestedSessionID) {
|
|
184
|
+
// Prefer the explicitly targeted session; fall back to the last active one.
|
|
185
|
+
let targetID = requestedSessionID || activeSessionID;
|
|
186
|
+
if (requestedSessionID) {
|
|
187
|
+
const sessions = await listSessions();
|
|
188
|
+
if (!sessions.some((s) => s.id === requestedSessionID)) {
|
|
189
|
+
return { ok: false, error: "Target session no longer exists.", injected: 0 };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!targetID) {
|
|
172
193
|
return {
|
|
173
194
|
ok: false,
|
|
174
|
-
error: "No
|
|
195
|
+
error: "No target session. Send a message in OpenCode first, or pick a session.",
|
|
175
196
|
injected: 0,
|
|
176
|
-
queued: queued.length,
|
|
177
197
|
};
|
|
178
198
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 };
|
|
199
|
+
const result = await injectPrompt(targetID, annotations);
|
|
200
|
+
if (!result.ok)
|
|
201
|
+
return { ok: false, error: result.error, injected: 0, sessionID: targetID };
|
|
202
|
+
return { ok: true, injected: annotations.length, sessionID: targetID };
|
|
194
203
|
}
|
|
195
204
|
function handle(req, res) {
|
|
196
205
|
if (req.method === "OPTIONS") {
|
|
@@ -198,14 +207,17 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
198
207
|
return;
|
|
199
208
|
}
|
|
200
209
|
if (req.method === "GET" && req.url === "/status") {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
void listSessions().then((sessions) => {
|
|
211
|
+
const active = sessions.find((s) => s.id === activeSessionID);
|
|
212
|
+
sendJson(res, 200, {
|
|
213
|
+
ok: true,
|
|
214
|
+
activeSession: Boolean(activeSessionID),
|
|
215
|
+
sessionID: activeSessionID,
|
|
216
|
+
sessionTitle: active?.title ?? null,
|
|
217
|
+
sessions,
|
|
218
|
+
host,
|
|
219
|
+
port,
|
|
220
|
+
});
|
|
209
221
|
});
|
|
210
222
|
return;
|
|
211
223
|
}
|
|
@@ -218,11 +230,9 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
218
230
|
sendJson(res, 400, { ok: false, error: "No annotations in payload." });
|
|
219
231
|
return;
|
|
220
232
|
}
|
|
221
|
-
const result = await handleSubmit(annotations);
|
|
233
|
+
const result = await handleSubmit(annotations, payload.sessionID);
|
|
222
234
|
if (result.ok) {
|
|
223
|
-
log("info", `
|
|
224
|
-
sessionID: result.sessionID,
|
|
225
|
-
});
|
|
235
|
+
log("info", `Injected ${result.injected} annotation(s)`, { sessionID: result.sessionID });
|
|
226
236
|
sendJson(res, 200, result);
|
|
227
237
|
}
|
|
228
238
|
else {
|
|
@@ -263,17 +273,9 @@ export const BrowserAnnotationPlugin = async ({ client, directory }) => {
|
|
|
263
273
|
activeSessionID = input.sessionID;
|
|
264
274
|
},
|
|
265
275
|
event: async ({ event }) => {
|
|
266
|
-
if (event.type === "session.
|
|
267
|
-
|
|
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) {
|
|
276
|
+
if (event.type === "session.deleted") {
|
|
277
|
+
if (event.properties.info.id === activeSessionID)
|
|
274
278
|
activeSessionID = null;
|
|
275
|
-
activeSessionTitle = null;
|
|
276
|
-
}
|
|
277
279
|
}
|
|
278
280
|
},
|
|
279
281
|
};
|
package/extension/background.js
CHANGED
|
@@ -68,7 +68,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
68
68
|
const res = await fetch(`${endpoint}/annotations`, {
|
|
69
69
|
method: "POST",
|
|
70
70
|
headers: { "content-type": "application/json" },
|
|
71
|
-
body: JSON.stringify({ extensionVersion: EXTENSION_VERSION, annotations }),
|
|
71
|
+
body: JSON.stringify({ extensionVersion: EXTENSION_VERSION, annotations, sessionID: msg.sessionID }),
|
|
72
72
|
});
|
|
73
73
|
const data = await res.json().catch(() => ({}));
|
|
74
74
|
if (res.ok && data.ok) {
|
package/extension/manifest.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Annotation",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"description": "Alt+A to annotate an element, Alt+Shift+A for the annotation list. Sends element metadata to your OpenCode agent.",
|
|
6
|
-
"permissions": [
|
|
7
|
-
|
|
6
|
+
"permissions": [
|
|
7
|
+
"activeTab",
|
|
8
|
+
"scripting",
|
|
9
|
+
"storage"
|
|
10
|
+
],
|
|
11
|
+
"host_permissions": [
|
|
12
|
+
"http://127.0.0.1/*",
|
|
13
|
+
"http://localhost/*"
|
|
14
|
+
],
|
|
8
15
|
"background": {
|
|
9
16
|
"service_worker": "background.js",
|
|
10
17
|
"type": "module"
|
|
@@ -15,11 +22,17 @@
|
|
|
15
22
|
"options_page": "options.html",
|
|
16
23
|
"commands": {
|
|
17
24
|
"select-element": {
|
|
18
|
-
"suggested_key": {
|
|
25
|
+
"suggested_key": {
|
|
26
|
+
"default": "Alt+A",
|
|
27
|
+
"mac": "Alt+A"
|
|
28
|
+
},
|
|
19
29
|
"description": "Pick an element to annotate"
|
|
20
30
|
},
|
|
21
31
|
"toggle-sidebar": {
|
|
22
|
-
"suggested_key": {
|
|
32
|
+
"suggested_key": {
|
|
33
|
+
"default": "Alt+Shift+A",
|
|
34
|
+
"mac": "Alt+Shift+A"
|
|
35
|
+
},
|
|
23
36
|
"description": "Toggle the annotation list sidebar"
|
|
24
37
|
}
|
|
25
38
|
}
|
package/extension/overlay.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// Alt+A -> start element picking (background sends "oc-pick")
|
|
6
6
|
// Alt+Shift+A -> toggle the list sidebar ("oc-toggle-sidebar")
|
|
7
7
|
// Picking an element opens a floating popup near it (onUI-style): type an
|
|
8
|
-
// instruction,
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// instruction, then Add (to the sidebar list) or Send (submit immediately). The
|
|
9
|
+
// sidebar is a floating rounded card holding pending annotations for batch
|
|
10
|
+
// submit, with a session-target picker.
|
|
11
11
|
|
|
12
12
|
(() => {
|
|
13
13
|
if (window.__ocAnnotationInjected) return;
|
|
@@ -20,10 +20,6 @@
|
|
|
20
20
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>',
|
|
21
21
|
trash:
|
|
22
22
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
|
23
|
-
bolt:
|
|
24
|
-
'<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
|
25
|
-
layers:
|
|
26
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
|
|
27
23
|
send:
|
|
28
24
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
|
|
29
25
|
plus:
|
|
@@ -71,11 +67,6 @@
|
|
|
71
67
|
textarea.oc-ta::placeholder { color: #616678; }
|
|
72
68
|
textarea.oc-ta:focus { outline: none; border-color: #4c8dff; box-shadow: 0 0 0 3px rgba(76,141,255,.18); }
|
|
73
69
|
|
|
74
|
-
.oc-modes { display: flex; gap: 6px; margin-top: 9px; }
|
|
75
|
-
.oc-mode { font: inherit; font-size: 12px; font-weight: 600; flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 8px; border-radius: 8px; cursor: pointer; background: rgba(255,255,255,.05); color: #9298aa; border: 1px solid transparent; }
|
|
76
|
-
.oc-mode svg { width: 13px; height: 13px; }
|
|
77
|
-
.oc-mode:hover { background: rgba(255,255,255,.09); color: #cfd3df; }
|
|
78
|
-
.oc-mode.sel { background: rgba(76,141,255,.16); color: #7aa9ff; border-color: rgba(76,141,255,.4); }
|
|
79
70
|
|
|
80
71
|
.btn { font: inherit; font-weight: 600; font-size: 12.5px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px 12px; border-radius: 9px; cursor: pointer; border: 1px solid rgba(255,255,255,.14); background: rgba(255,255,255,.05); color: #e6e8ee; transition: background .15s, transform .05s; }
|
|
81
72
|
.btn svg { width: 15px; height: 15px; }
|
|
@@ -103,10 +94,12 @@
|
|
|
103
94
|
.oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
104
95
|
.oc-desc { font: 11px ui-monospace, monospace; color: #aeb4c6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: rgba(255,255,255,.06); padding: 2px 7px; border-radius: 6px; flex: 1; }
|
|
105
96
|
.oc-card .oc-text { font-size: 12.5px; color: #d4d7e0; white-space: pre-wrap; word-break: break-word; }
|
|
106
|
-
.oc-card .oc-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: #9298aa; margin-top: 6px; }
|
|
107
|
-
.oc-card .oc-tag svg { width: 12px; height: 12px; }
|
|
108
97
|
.oc-foot-bar { padding: 11px 12px; border-top: 1px solid rgba(255,255,255,.07); position: relative; }
|
|
109
|
-
.oc-status {
|
|
98
|
+
.oc-status-row { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
|
|
99
|
+
.oc-select { flex: none; max-width: 150px; font: inherit; font-size: 11.5px; color: #cfd3df; background: #14151b; border: 1px solid rgba(255,255,255,.12); border-radius: 7px; padding: 4px 6px; }
|
|
100
|
+
.oc-select:focus { outline: none; border-color: #4c8dff; }
|
|
101
|
+
.oc-status { flex: 1; min-width: 0; font-size: 12px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
|
|
102
|
+
.oc-status span, .oc-status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
110
103
|
.oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #5b6070; flex: none; }
|
|
111
104
|
.oc-status.good::before { background: #34d399; box-shadow: 0 0 8px rgba(52,211,153,.6); }
|
|
112
105
|
.oc-status.warn::before { background: #fbbf24; } .oc-status.bad::before { background: #f87171; } .oc-status.checking::before { background: #60a5fa; }
|
|
@@ -125,6 +118,62 @@
|
|
|
125
118
|
let pending = []; // annotations added to the sidebar list
|
|
126
119
|
let sidebarOpen = false;
|
|
127
120
|
let statusTimer = null;
|
|
121
|
+
let sessions = []; // [{ id, title, updated }]
|
|
122
|
+
let targetSessionID = null; // user-chosen target; null = auto (last active)
|
|
123
|
+
let autoSessionID = null; // the plugin's last-active session
|
|
124
|
+
|
|
125
|
+
// A content script keeps running after its extension is reloaded/updated, but
|
|
126
|
+
// its chrome.* calls then throw "Extension context invalidated". Guard every
|
|
127
|
+
// message so a stale overlay fails quietly instead of spamming errors, and
|
|
128
|
+
// tear itself down once the context is gone.
|
|
129
|
+
function contextAlive() {
|
|
130
|
+
try {
|
|
131
|
+
return Boolean(chrome.runtime && chrome.runtime.id);
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sendMsg(payload, cb) {
|
|
138
|
+
if (!contextAlive()) {
|
|
139
|
+
teardown();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
chrome.runtime.sendMessage(payload, (res) => {
|
|
144
|
+
if (chrome.runtime.lastError) {
|
|
145
|
+
cb(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
cb(res);
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
teardown();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function teardown() {
|
|
156
|
+
if (statusTimer) {
|
|
157
|
+
clearInterval(statusTimer);
|
|
158
|
+
statusTimer = null;
|
|
159
|
+
}
|
|
160
|
+
if (host) host.style.display = "none";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function effectiveTarget() {
|
|
164
|
+
return targetSessionID || autoSessionID;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function targetTitle() {
|
|
168
|
+
const id = effectiveTarget();
|
|
169
|
+
const s = sessions.find((x) => x.id === id);
|
|
170
|
+
return s ? s.title : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function targetLabel() {
|
|
174
|
+
const t = targetTitle();
|
|
175
|
+
return t ? ` · → ${escapeHtml(t)}` : "";
|
|
176
|
+
}
|
|
128
177
|
|
|
129
178
|
// ---------- metadata ----------
|
|
130
179
|
|
|
@@ -266,6 +315,19 @@
|
|
|
266
315
|
document.addEventListener("mousemove", onMove, true);
|
|
267
316
|
document.addEventListener("click", onClick, true);
|
|
268
317
|
document.addEventListener("keydown", onKey, true);
|
|
318
|
+
|
|
319
|
+
// Keep keystrokes typed inside our UI from reaching the page's global
|
|
320
|
+
// hotkeys (e.g. GitHub's "s"/"f"). Capture key events at the document before
|
|
321
|
+
// page listeners run; if the event originates inside our shadow UI, stop it
|
|
322
|
+
// (Esc is handled by onKey above, which runs first in capture order here).
|
|
323
|
+
const contain = (e) => {
|
|
324
|
+
if (e.composedPath && e.composedPath().includes(host)) {
|
|
325
|
+
e.stopImmediatePropagation();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
for (const type of ["keydown", "keyup", "keypress"]) {
|
|
329
|
+
document.addEventListener(type, contain, true);
|
|
330
|
+
}
|
|
269
331
|
}
|
|
270
332
|
|
|
271
333
|
function startPicking() {
|
|
@@ -297,7 +359,6 @@
|
|
|
297
359
|
function openPopup(element, rect) {
|
|
298
360
|
ensureUI();
|
|
299
361
|
closePopup();
|
|
300
|
-
const state = { mode: "act" };
|
|
301
362
|
const el = document.createElement("div");
|
|
302
363
|
el.className = "oc-popup oc-surface";
|
|
303
364
|
el.innerHTML = `
|
|
@@ -308,11 +369,7 @@
|
|
|
308
369
|
</div>
|
|
309
370
|
<div class="oc-body">
|
|
310
371
|
<textarea class="oc-ta" rows="3" placeholder="Describe the change or ask about this element…"></textarea>
|
|
311
|
-
<div class="oc-
|
|
312
|
-
<button class="oc-mode sel" data-mode="act">${ICON.bolt}<span>Act now</span></button>
|
|
313
|
-
<button class="oc-mode" data-mode="queue">${ICON.layers}<span>Queue</span></button>
|
|
314
|
-
</div>
|
|
315
|
-
<div class="oc-hintline">Cmd/Ctrl+Enter to send</div>
|
|
372
|
+
<div class="oc-hintline">Cmd/Ctrl+Enter to send${targetLabel()}</div>
|
|
316
373
|
</div>
|
|
317
374
|
<div class="oc-foot">
|
|
318
375
|
<button class="btn oc-add">${ICON.plus}<span>Add to list</span></button>
|
|
@@ -326,13 +383,7 @@
|
|
|
326
383
|
setTimeout(() => ta.focus(), 30);
|
|
327
384
|
|
|
328
385
|
el.querySelector(".oc-x").addEventListener("click", closePopup);
|
|
329
|
-
|
|
330
|
-
b.addEventListener("click", () => {
|
|
331
|
-
state.mode = b.dataset.mode;
|
|
332
|
-
el.querySelectorAll(".oc-mode").forEach((x) => x.classList.toggle("sel", x === b));
|
|
333
|
-
}),
|
|
334
|
-
);
|
|
335
|
-
const build = () => ({ instruction: ta.value.trim(), mode: state.mode, page: { url: location.href, title: document.title }, element });
|
|
386
|
+
const build = () => ({ instruction: ta.value.trim(), page: { url: location.href, title: document.title }, element });
|
|
336
387
|
el.querySelector(".oc-add").addEventListener("click", () => {
|
|
337
388
|
if (!ta.value.trim()) return ta.focus();
|
|
338
389
|
pending.push(build());
|
|
@@ -386,16 +437,24 @@
|
|
|
386
437
|
<span class="logo">${ICON.logo}</span>
|
|
387
438
|
<span class="title">Annotations</span>
|
|
388
439
|
<span class="badge" id="oc-badge">0</span>
|
|
440
|
+
<button class="iconbtn" id="oc-pick" title="Select element (Alt+A)">${ICON.target}</button>
|
|
389
441
|
<button class="iconbtn" id="oc-close" title="Close (Alt+Shift+A)">${ICON.close}</button>
|
|
390
442
|
</div>
|
|
391
443
|
<div class="oc-list" id="oc-list"></div>
|
|
392
444
|
<div class="oc-foot-bar">
|
|
393
|
-
<div class="oc-status
|
|
445
|
+
<div class="oc-status-row">
|
|
446
|
+
<div class="oc-status" id="oc-status">…</div>
|
|
447
|
+
<select class="oc-select" id="oc-session" title="Target session"></select>
|
|
448
|
+
</div>
|
|
394
449
|
<button class="btn primary oc-submit" id="oc-submit" disabled>${ICON.send}<span>Submit to agent</span></button>
|
|
395
450
|
<div class="oc-toast" id="oc-toast"></div>
|
|
396
451
|
</div>`;
|
|
397
452
|
root.appendChild(sb);
|
|
398
453
|
sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
|
|
454
|
+
sb.querySelector("#oc-pick").addEventListener("click", () => startPicking());
|
|
455
|
+
sb.querySelector("#oc-session").addEventListener("change", (e) => {
|
|
456
|
+
targetSessionID = e.target.value || null;
|
|
457
|
+
});
|
|
399
458
|
sb.querySelector("#oc-submit").addEventListener("click", () => {
|
|
400
459
|
submit(pending, false);
|
|
401
460
|
});
|
|
@@ -443,15 +502,12 @@
|
|
|
443
502
|
pending.forEach((a, i) => {
|
|
444
503
|
const card = document.createElement("div");
|
|
445
504
|
card.className = "oc-card";
|
|
446
|
-
const modeIcon = a.mode === "queue" ? ICON.layers : ICON.bolt;
|
|
447
|
-
const modeName = a.mode === "queue" ? "Queue" : "Act now";
|
|
448
505
|
card.innerHTML = `
|
|
449
506
|
<div class="oc-card-head">
|
|
450
507
|
<span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
|
|
451
508
|
<button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
|
|
452
509
|
</div>
|
|
453
|
-
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div
|
|
454
|
-
<div class="oc-tag">${modeIcon}<span>${modeName}</span></div>`;
|
|
510
|
+
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>`;
|
|
455
511
|
list.appendChild(card);
|
|
456
512
|
});
|
|
457
513
|
const btn = root.getElementById("oc-submit");
|
|
@@ -469,17 +525,19 @@
|
|
|
469
525
|
if (!el) return;
|
|
470
526
|
el.className = "oc-status checking";
|
|
471
527
|
el.textContent = "Checking connection…";
|
|
472
|
-
|
|
473
|
-
if (
|
|
528
|
+
sendMsg({ type: "oc-status" }, (res) => {
|
|
529
|
+
if (!res) {
|
|
474
530
|
el.className = "oc-status bad";
|
|
475
531
|
el.textContent = "Extension error";
|
|
476
532
|
return;
|
|
477
533
|
}
|
|
478
534
|
if (res.ok && res.data?.ok) {
|
|
479
|
-
|
|
480
|
-
|
|
535
|
+
sessions = Array.isArray(res.data.sessions) ? res.data.sessions : [];
|
|
536
|
+
autoSessionID = res.data.sessionID || null;
|
|
537
|
+
renderSessions();
|
|
538
|
+
if (sessions.length || res.data.activeSession) {
|
|
481
539
|
el.className = "oc-status good";
|
|
482
|
-
el.textContent =
|
|
540
|
+
el.textContent = "Connected";
|
|
483
541
|
} else {
|
|
484
542
|
el.className = "oc-status warn";
|
|
485
543
|
el.textContent = "Connected — send a message in OpenCode first";
|
|
@@ -491,6 +549,26 @@
|
|
|
491
549
|
});
|
|
492
550
|
}
|
|
493
551
|
|
|
552
|
+
function renderSessions() {
|
|
553
|
+
if (!root) return;
|
|
554
|
+
const sel = root.getElementById("oc-session");
|
|
555
|
+
if (!sel) return;
|
|
556
|
+
// If the chosen target vanished, fall back to auto.
|
|
557
|
+
if (targetSessionID && !sessions.some((s) => s.id === targetSessionID)) targetSessionID = null;
|
|
558
|
+
const current = effectiveTarget();
|
|
559
|
+
const opts = [`<option value="">Auto (last active)</option>`].concat(
|
|
560
|
+
sessions.map((s) => {
|
|
561
|
+
const active = s.id === autoSessionID ? " • active" : "";
|
|
562
|
+
const label = `${(s.title || s.id).slice(0, 40)}${active}`;
|
|
563
|
+
const selected = s.id === current ? " selected" : "";
|
|
564
|
+
return `<option value="${escapeHtml(s.id)}"${selected}>${escapeHtml(label)}</option>`;
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
// Keep "Auto" selected when no explicit target.
|
|
568
|
+
sel.innerHTML = opts.join("");
|
|
569
|
+
if (!targetSessionID) sel.value = "";
|
|
570
|
+
}
|
|
571
|
+
|
|
494
572
|
function toast(text, bad) {
|
|
495
573
|
if (!root) return;
|
|
496
574
|
// Prefer the sidebar's toast when it is open; otherwise use a standalone
|
|
@@ -514,16 +592,14 @@
|
|
|
514
592
|
function submit(annotations, quick) {
|
|
515
593
|
if (!annotations.length) return;
|
|
516
594
|
toast("Submitting…");
|
|
517
|
-
|
|
518
|
-
|
|
595
|
+
const sessionID = effectiveTarget() || undefined;
|
|
596
|
+
sendMsg({ type: "oc-submit", annotations, sessionID }, (res) => {
|
|
597
|
+
if (!res) {
|
|
519
598
|
toast("Extension error", true);
|
|
520
599
|
return;
|
|
521
600
|
}
|
|
522
601
|
if (res.ok) {
|
|
523
|
-
|
|
524
|
-
if (res.injected) parts.push(`${res.injected} sent`);
|
|
525
|
-
if (res.queued) parts.push(`${res.queued} queued`);
|
|
526
|
-
toast(parts.length ? parts.join(", ") : "Submitted");
|
|
602
|
+
toast(res.injected ? `${res.injected} sent to agent` : "Submitted");
|
|
527
603
|
if (!quick) {
|
|
528
604
|
pending = [];
|
|
529
605
|
renderCards();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-browser-annotation-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|