opencode-browser-annotation-plugin 0.3.0 → 0.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/dist/plugin.js +54 -52
- package/extension/background.js +1 -1
- package/extension/manifest.json +18 -5
- package/extension/overlay.js +59 -32
- 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.1",
|
|
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
|
@@ -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,9 +94,11 @@
|
|
|
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; }
|
|
98
|
+
.oc-target-row { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
|
|
99
|
+
.oc-target-label { font-size: 11px; font-weight: 600; color: #8b90a0; flex: none; }
|
|
100
|
+
.oc-select { flex: 1; min-width: 0; font: inherit; font-size: 12px; color: #e6e8ee; background: #14151b; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; padding: 6px 8px; }
|
|
101
|
+
.oc-select:focus { outline: none; border-color: #4c8dff; }
|
|
109
102
|
.oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
|
|
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); }
|
|
@@ -125,6 +118,24 @@
|
|
|
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
|
+
function effectiveTarget() {
|
|
126
|
+
return targetSessionID || autoSessionID;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function targetTitle() {
|
|
130
|
+
const id = effectiveTarget();
|
|
131
|
+
const s = sessions.find((x) => x.id === id);
|
|
132
|
+
return s ? s.title : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function targetLabel() {
|
|
136
|
+
const t = targetTitle();
|
|
137
|
+
return t ? ` · → ${escapeHtml(t)}` : "";
|
|
138
|
+
}
|
|
128
139
|
|
|
129
140
|
// ---------- metadata ----------
|
|
130
141
|
|
|
@@ -297,7 +308,6 @@
|
|
|
297
308
|
function openPopup(element, rect) {
|
|
298
309
|
ensureUI();
|
|
299
310
|
closePopup();
|
|
300
|
-
const state = { mode: "act" };
|
|
301
311
|
const el = document.createElement("div");
|
|
302
312
|
el.className = "oc-popup oc-surface";
|
|
303
313
|
el.innerHTML = `
|
|
@@ -308,11 +318,7 @@
|
|
|
308
318
|
</div>
|
|
309
319
|
<div class="oc-body">
|
|
310
320
|
<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>
|
|
321
|
+
<div class="oc-hintline">Cmd/Ctrl+Enter to send${targetLabel()}</div>
|
|
316
322
|
</div>
|
|
317
323
|
<div class="oc-foot">
|
|
318
324
|
<button class="btn oc-add">${ICON.plus}<span>Add to list</span></button>
|
|
@@ -326,13 +332,7 @@
|
|
|
326
332
|
setTimeout(() => ta.focus(), 30);
|
|
327
333
|
|
|
328
334
|
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 });
|
|
335
|
+
const build = () => ({ instruction: ta.value.trim(), page: { url: location.href, title: document.title }, element });
|
|
336
336
|
el.querySelector(".oc-add").addEventListener("click", () => {
|
|
337
337
|
if (!ta.value.trim()) return ta.focus();
|
|
338
338
|
pending.push(build());
|
|
@@ -390,12 +390,19 @@
|
|
|
390
390
|
</div>
|
|
391
391
|
<div class="oc-list" id="oc-list"></div>
|
|
392
392
|
<div class="oc-foot-bar">
|
|
393
|
+
<label class="oc-target-row">
|
|
394
|
+
<span class="oc-target-label">Session</span>
|
|
395
|
+
<select class="oc-select" id="oc-session"></select>
|
|
396
|
+
</label>
|
|
393
397
|
<div class="oc-status" id="oc-status">…</div>
|
|
394
398
|
<button class="btn primary oc-submit" id="oc-submit" disabled>${ICON.send}<span>Submit to agent</span></button>
|
|
395
399
|
<div class="oc-toast" id="oc-toast"></div>
|
|
396
400
|
</div>`;
|
|
397
401
|
root.appendChild(sb);
|
|
398
402
|
sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
|
|
403
|
+
sb.querySelector("#oc-session").addEventListener("change", (e) => {
|
|
404
|
+
targetSessionID = e.target.value || null;
|
|
405
|
+
});
|
|
399
406
|
sb.querySelector("#oc-submit").addEventListener("click", () => {
|
|
400
407
|
submit(pending, false);
|
|
401
408
|
});
|
|
@@ -443,15 +450,12 @@
|
|
|
443
450
|
pending.forEach((a, i) => {
|
|
444
451
|
const card = document.createElement("div");
|
|
445
452
|
card.className = "oc-card";
|
|
446
|
-
const modeIcon = a.mode === "queue" ? ICON.layers : ICON.bolt;
|
|
447
|
-
const modeName = a.mode === "queue" ? "Queue" : "Act now";
|
|
448
453
|
card.innerHTML = `
|
|
449
454
|
<div class="oc-card-head">
|
|
450
455
|
<span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
|
|
451
456
|
<button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
|
|
452
457
|
</div>
|
|
453
|
-
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div
|
|
454
|
-
<div class="oc-tag">${modeIcon}<span>${modeName}</span></div>`;
|
|
458
|
+
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>`;
|
|
455
459
|
list.appendChild(card);
|
|
456
460
|
});
|
|
457
461
|
const btn = root.getElementById("oc-submit");
|
|
@@ -476,10 +480,12 @@
|
|
|
476
480
|
return;
|
|
477
481
|
}
|
|
478
482
|
if (res.ok && res.data?.ok) {
|
|
479
|
-
|
|
480
|
-
|
|
483
|
+
sessions = Array.isArray(res.data.sessions) ? res.data.sessions : [];
|
|
484
|
+
autoSessionID = res.data.sessionID || null;
|
|
485
|
+
renderSessions();
|
|
486
|
+
if (sessions.length || res.data.activeSession) {
|
|
481
487
|
el.className = "oc-status good";
|
|
482
|
-
el.textContent =
|
|
488
|
+
el.textContent = "Connected";
|
|
483
489
|
} else {
|
|
484
490
|
el.className = "oc-status warn";
|
|
485
491
|
el.textContent = "Connected — send a message in OpenCode first";
|
|
@@ -491,6 +497,26 @@
|
|
|
491
497
|
});
|
|
492
498
|
}
|
|
493
499
|
|
|
500
|
+
function renderSessions() {
|
|
501
|
+
if (!root) return;
|
|
502
|
+
const sel = root.getElementById("oc-session");
|
|
503
|
+
if (!sel) return;
|
|
504
|
+
// If the chosen target vanished, fall back to auto.
|
|
505
|
+
if (targetSessionID && !sessions.some((s) => s.id === targetSessionID)) targetSessionID = null;
|
|
506
|
+
const current = effectiveTarget();
|
|
507
|
+
const opts = [`<option value="">Auto (last active)</option>`].concat(
|
|
508
|
+
sessions.map((s) => {
|
|
509
|
+
const active = s.id === autoSessionID ? " • active" : "";
|
|
510
|
+
const label = `${(s.title || s.id).slice(0, 40)}${active}`;
|
|
511
|
+
const selected = s.id === current ? " selected" : "";
|
|
512
|
+
return `<option value="${escapeHtml(s.id)}"${selected}>${escapeHtml(label)}</option>`;
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
// Keep "Auto" selected when no explicit target.
|
|
516
|
+
sel.innerHTML = opts.join("");
|
|
517
|
+
if (!targetSessionID) sel.value = "";
|
|
518
|
+
}
|
|
519
|
+
|
|
494
520
|
function toast(text, bad) {
|
|
495
521
|
if (!root) return;
|
|
496
522
|
// Prefer the sidebar's toast when it is open; otherwise use a standalone
|
|
@@ -514,7 +540,8 @@
|
|
|
514
540
|
function submit(annotations, quick) {
|
|
515
541
|
if (!annotations.length) return;
|
|
516
542
|
toast("Submitting…");
|
|
517
|
-
|
|
543
|
+
const sessionID = effectiveTarget() || undefined;
|
|
544
|
+
chrome.runtime.sendMessage({ type: "oc-submit", annotations, sessionID }, (res) => {
|
|
518
545
|
if (chrome.runtime.lastError || !res) {
|
|
519
546
|
toast("Extension error", true);
|
|
520
547
|
return;
|
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.1",
|
|
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",
|