opencode-browser-annotation-plugin 0.3.1 → 0.4.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/extension/background.js +15 -1
- package/extension/manifest.json +1 -1
- package/extension/overlay.js +190 -54
- package/package.json +1 -1
package/extension/background.js
CHANGED
|
@@ -43,8 +43,22 @@ chrome.action.onClicked.addListener((tab) => {
|
|
|
43
43
|
void sendToOverlay(tab, "oc-toggle-sidebar");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
chrome.runtime.onMessage.addListener((msg,
|
|
46
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
47
47
|
(async () => {
|
|
48
|
+
if (msg?.type === "oc-capture") {
|
|
49
|
+
// Capture the visible tab for the extension's own thumbnail preview only.
|
|
50
|
+
// This image is never sent to the plugin/agent; the content script crops
|
|
51
|
+
// it to the element and keeps it local to the sidebar UI.
|
|
52
|
+
try {
|
|
53
|
+
const windowId = sender?.tab?.windowId;
|
|
54
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "png" });
|
|
55
|
+
sendResponse({ ok: true, dataUrl });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
sendResponse({ ok: false, error: error?.message || "capture failed" });
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
if (msg?.type === "oc-status") {
|
|
49
63
|
try {
|
|
50
64
|
const endpoint = await getEndpoint();
|
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.4.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
|
@@ -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;
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
|
27
27
|
logo:
|
|
28
28
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0H5a2 2 0 0 1-2-2v-4m6 6h10a2 2 0 0 0 2-2v-4"/></svg>',
|
|
29
|
+
caret:
|
|
30
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
const STYLE = `
|
|
@@ -58,6 +60,8 @@
|
|
|
58
60
|
.iconbtn { display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: #8b90a0; cursor: pointer; width: 28px; height: 28px; border-radius: 8px; padding: 0; }
|
|
59
61
|
.iconbtn svg { width: 15px; height: 15px; }
|
|
60
62
|
.iconbtn:hover { background: rgba(255,255,255,.08); color: #e6e8ee; }
|
|
63
|
+
.iconbtn.active { background: #3f7dff; color: #fff; box-shadow: 0 0 0 3px rgba(63,125,255,.28); }
|
|
64
|
+
.iconbtn.active:hover { background: #3670f0; color: #fff; }
|
|
61
65
|
|
|
62
66
|
textarea.oc-ta {
|
|
63
67
|
width: 100%; resize: vertical; min-height: 62px; font: inherit;
|
|
@@ -90,20 +94,34 @@
|
|
|
90
94
|
.oc-list::-webkit-scrollbar { width: 10px; }
|
|
91
95
|
.oc-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); border-radius: 6px; border: 3px solid transparent; background-clip: content-box; }
|
|
92
96
|
.oc-empty { color: #71768a; font-size: 12.5px; padding: 24px 8px; text-align: center; line-height: 1.6; }
|
|
93
|
-
.oc-card { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 11px; padding: 9px 10px; }
|
|
94
|
-
.oc-card-
|
|
95
|
-
.oc-
|
|
97
|
+
.oc-card { position: relative; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 11px; padding: 9px 10px; }
|
|
98
|
+
.oc-card .oc-rm { position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; }
|
|
99
|
+
.oc-card .oc-rm svg { width: 13px; height: 13px; }
|
|
100
|
+
.oc-thumb { width: 100%; height: 96px; object-fit: cover; object-position: top left; background: #0e0f14; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; display: block; margin-bottom: 8px; }
|
|
101
|
+
.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; display: inline-block; max-width: calc(100% - 30px); margin-bottom: 6px; }
|
|
96
102
|
.oc-card .oc-text { font-size: 12.5px; color: #d4d7e0; white-space: pre-wrap; word-break: break-word; }
|
|
97
|
-
.oc-foot-bar { padding:
|
|
98
|
-
.oc-
|
|
99
|
-
.oc-
|
|
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; }
|
|
102
|
-
.oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
|
|
103
|
+
.oc-foot-bar { padding: 12px; border-top: 1px solid rgba(255,255,255,.07); position: relative; }
|
|
104
|
+
.oc-status-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
105
|
+
.oc-status { flex: 1; min-width: 0; font-size: 12px; display: flex; align-items: center; gap: 8px; color: #9298aa; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
103
106
|
.oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #5b6070; flex: none; }
|
|
104
107
|
.oc-status.good::before { background: #34d399; box-shadow: 0 0 8px rgba(52,211,153,.6); }
|
|
105
108
|
.oc-status.warn::before { background: #fbbf24; } .oc-status.bad::before { background: #f87171; } .oc-status.checking::before { background: #60a5fa; }
|
|
106
|
-
.oc-status.good { color: #34d399; } .oc-status.warn { color: #fbbf24; } .oc-status.bad { color: #f87171; }
|
|
109
|
+
.oc-status.good { color: #34d399; } .oc-status.warn { color: #fbbf24; } .oc-status.bad { color: #f87171; } .oc-status.checking { color: #93b4ff; }
|
|
110
|
+
|
|
111
|
+
/* custom session dropdown (opens upward) */
|
|
112
|
+
.oc-dd { position: relative; flex: none; width: 176px; }
|
|
113
|
+
.oc-dd-btn { width: 100%; display: flex; align-items: center; gap: 6px; font: inherit; font-size: 11.5px; color: #cfd3df; background: #14151b; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; padding: 6px 8px; cursor: pointer; }
|
|
114
|
+
.oc-dd-btn:hover { border-color: rgba(255,255,255,.22); }
|
|
115
|
+
.oc-dd-btn .lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
|
116
|
+
.oc-dd-btn .car { width: 12px; height: 12px; flex: none; transition: transform .15s; }
|
|
117
|
+
.oc-dd.open .oc-dd-btn .car { transform: rotate(180deg); }
|
|
118
|
+
.oc-dd-menu { position: absolute; bottom: calc(100% + 6px); right: 0; left: 0; max-height: 240px; overflow-y: auto; background: #1b1d25; border: 1px solid rgba(255,255,255,.14); border-radius: 10px; box-shadow: 0 -12px 34px rgba(0,0,0,.5); padding: 5px; display: none; }
|
|
119
|
+
.oc-dd.open .oc-dd-menu { display: block; }
|
|
120
|
+
.oc-dd-opt { font-size: 12px; color: #cfd3df; padding: 7px 9px; border-radius: 7px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
121
|
+
.oc-dd-opt:hover { background: rgba(255,255,255,.08); }
|
|
122
|
+
.oc-dd-opt.sel { background: rgba(76,141,255,.16); color: #7aa9ff; }
|
|
123
|
+
.oc-dd-menu::-webkit-scrollbar { width: 8px; }
|
|
124
|
+
.oc-dd-menu::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 6px; }
|
|
107
125
|
.oc-submit { width: 100%; }
|
|
108
126
|
.oc-toast { position: absolute; left: 12px; right: 12px; bottom: 58px; background: #0e0f14; color: #fff; font-size: 12.5px; padding: 9px 12px; border-radius: 9px; opacity: 0; transform: translateY(6px); transition: opacity .2s, transform .2s; pointer-events: none; box-shadow: 0 10px 30px rgba(0,0,0,.5); border: 1px solid rgba(255,255,255,.08); }
|
|
109
127
|
.oc-toast.show { opacity: 1; transform: translateY(0); }
|
|
@@ -122,6 +140,44 @@
|
|
|
122
140
|
let targetSessionID = null; // user-chosen target; null = auto (last active)
|
|
123
141
|
let autoSessionID = null; // the plugin's last-active session
|
|
124
142
|
|
|
143
|
+
// A content script keeps running after its extension is reloaded/updated, but
|
|
144
|
+
// its chrome.* calls then throw "Extension context invalidated". Guard every
|
|
145
|
+
// message so a stale overlay fails quietly instead of spamming errors, and
|
|
146
|
+
// tear itself down once the context is gone.
|
|
147
|
+
function contextAlive() {
|
|
148
|
+
try {
|
|
149
|
+
return Boolean(chrome.runtime && chrome.runtime.id);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sendMsg(payload, cb) {
|
|
156
|
+
if (!contextAlive()) {
|
|
157
|
+
teardown();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
chrome.runtime.sendMessage(payload, (res) => {
|
|
162
|
+
if (chrome.runtime.lastError) {
|
|
163
|
+
cb(null);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
cb(res);
|
|
167
|
+
});
|
|
168
|
+
} catch {
|
|
169
|
+
teardown();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function teardown() {
|
|
174
|
+
if (statusTimer) {
|
|
175
|
+
clearInterval(statusTimer);
|
|
176
|
+
statusTimer = null;
|
|
177
|
+
}
|
|
178
|
+
if (host) host.style.display = "none";
|
|
179
|
+
}
|
|
180
|
+
|
|
125
181
|
function effectiveTarget() {
|
|
126
182
|
return targetSessionID || autoSessionID;
|
|
127
183
|
}
|
|
@@ -206,6 +262,44 @@
|
|
|
206
262
|
return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
207
263
|
}
|
|
208
264
|
|
|
265
|
+
// Capture a thumbnail of the selected element for the SIDEBAR LIST ONLY.
|
|
266
|
+
// Never sent to the plugin/agent (stripped before submit). Best-effort: on any
|
|
267
|
+
// failure we simply omit the thumbnail.
|
|
268
|
+
function captureThumb(rect) {
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
sendMsg({ type: "oc-capture" }, (res) => {
|
|
271
|
+
if (!res || !res.ok || !res.dataUrl) return resolve(null);
|
|
272
|
+
const img = new Image();
|
|
273
|
+
img.onload = () => {
|
|
274
|
+
try {
|
|
275
|
+
const dpr = window.devicePixelRatio || 1;
|
|
276
|
+
const pad = 6 * dpr;
|
|
277
|
+
let sx = Math.max(0, rect.left * dpr - pad);
|
|
278
|
+
let sy = Math.max(0, rect.top * dpr - pad);
|
|
279
|
+
let sw = Math.min(img.width - sx, rect.width * dpr + pad * 2);
|
|
280
|
+
let sh = Math.min(img.height - sy, rect.height * dpr + pad * 2);
|
|
281
|
+
if (sw <= 0 || sh <= 0) return resolve(null);
|
|
282
|
+
// cap output size for a compact list thumbnail
|
|
283
|
+
const maxW = 300;
|
|
284
|
+
const scale = Math.min(1, maxW / sw);
|
|
285
|
+
const cw = Math.round(sw * scale);
|
|
286
|
+
const ch = Math.round(sh * scale);
|
|
287
|
+
const c = document.createElement("canvas");
|
|
288
|
+
c.width = cw;
|
|
289
|
+
c.height = ch;
|
|
290
|
+
const ctx = c.getContext("2d");
|
|
291
|
+
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, cw, ch);
|
|
292
|
+
resolve(c.toDataURL("image/png"));
|
|
293
|
+
} catch {
|
|
294
|
+
resolve(null);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
img.onerror = () => resolve(null);
|
|
298
|
+
img.src = res.dataUrl;
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
209
303
|
// ---------- picking ----------
|
|
210
304
|
|
|
211
305
|
function pickTarget(e) {
|
|
@@ -244,8 +338,13 @@
|
|
|
244
338
|
e.stopPropagation();
|
|
245
339
|
e.stopImmediatePropagation();
|
|
246
340
|
const rect = hit.el.getBoundingClientRect();
|
|
341
|
+
const meta = elementMeta(hit.el, hit.inShadow);
|
|
247
342
|
stopPicking();
|
|
248
|
-
|
|
343
|
+
// Capture the thumbnail while the element is still unobscured (before the
|
|
344
|
+
// popup is drawn), then open the popup. Best-effort; thumb may be null.
|
|
345
|
+
requestAnimationFrame(() => {
|
|
346
|
+
captureThumb(rect).then((thumb) => openPopup(meta, rect, thumb));
|
|
347
|
+
});
|
|
249
348
|
}
|
|
250
349
|
|
|
251
350
|
function onKey(e) {
|
|
@@ -277,6 +376,24 @@
|
|
|
277
376
|
document.addEventListener("mousemove", onMove, true);
|
|
278
377
|
document.addEventListener("click", onClick, true);
|
|
279
378
|
document.addEventListener("keydown", onKey, true);
|
|
379
|
+
|
|
380
|
+
// Keep keystrokes typed inside our UI from reaching the page's global
|
|
381
|
+
// hotkeys (e.g. GitHub's "s"/"f"). Capture key events at the document before
|
|
382
|
+
// page listeners run; if the event originates inside our shadow UI, stop it
|
|
383
|
+
// (Esc is handled by onKey above, which runs first in capture order here).
|
|
384
|
+
const contain = (e) => {
|
|
385
|
+
if (e.composedPath && e.composedPath().includes(host)) {
|
|
386
|
+
e.stopImmediatePropagation();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
for (const type of ["keydown", "keyup", "keypress"]) {
|
|
390
|
+
document.addEventListener(type, contain, true);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function setPickActive(on) {
|
|
395
|
+
const btn = root && root.getElementById("oc-pick");
|
|
396
|
+
if (btn) btn.classList.toggle("active", on);
|
|
280
397
|
}
|
|
281
398
|
|
|
282
399
|
function startPicking() {
|
|
@@ -285,6 +402,7 @@
|
|
|
285
402
|
picking = true;
|
|
286
403
|
document.documentElement.style.cursor = "crosshair";
|
|
287
404
|
root.getElementById("oc-hint").style.display = "flex";
|
|
405
|
+
setPickActive(true);
|
|
288
406
|
}
|
|
289
407
|
|
|
290
408
|
function stopPicking() {
|
|
@@ -293,6 +411,7 @@
|
|
|
293
411
|
if (root) {
|
|
294
412
|
hl().style.display = "none";
|
|
295
413
|
root.getElementById("oc-hint").style.display = "none";
|
|
414
|
+
setPickActive(false);
|
|
296
415
|
}
|
|
297
416
|
}
|
|
298
417
|
|
|
@@ -305,7 +424,7 @@
|
|
|
305
424
|
}
|
|
306
425
|
}
|
|
307
426
|
|
|
308
|
-
function openPopup(element, rect) {
|
|
427
|
+
function openPopup(element, rect, thumb) {
|
|
309
428
|
ensureUI();
|
|
310
429
|
closePopup();
|
|
311
430
|
const el = document.createElement("div");
|
|
@@ -332,7 +451,7 @@
|
|
|
332
451
|
setTimeout(() => ta.focus(), 30);
|
|
333
452
|
|
|
334
453
|
el.querySelector(".oc-x").addEventListener("click", closePopup);
|
|
335
|
-
const build = () => ({ instruction: ta.value.trim(), page: { url: location.href, title: document.title }, element });
|
|
454
|
+
const build = () => ({ instruction: ta.value.trim(), page: { url: location.href, title: document.title }, element, thumb });
|
|
336
455
|
el.querySelector(".oc-add").addEventListener("click", () => {
|
|
337
456
|
if (!ta.value.trim()) return ta.focus();
|
|
338
457
|
pending.push(build());
|
|
@@ -386,22 +505,34 @@
|
|
|
386
505
|
<span class="logo">${ICON.logo}</span>
|
|
387
506
|
<span class="title">Annotations</span>
|
|
388
507
|
<span class="badge" id="oc-badge">0</span>
|
|
508
|
+
<button class="iconbtn" id="oc-pick" title="Select element (Alt+A)">${ICON.target}</button>
|
|
389
509
|
<button class="iconbtn" id="oc-close" title="Close (Alt+Shift+A)">${ICON.close}</button>
|
|
390
510
|
</div>
|
|
391
511
|
<div class="oc-list" id="oc-list"></div>
|
|
392
512
|
<div class="oc-foot-bar">
|
|
393
|
-
<
|
|
394
|
-
<
|
|
395
|
-
<
|
|
396
|
-
|
|
397
|
-
|
|
513
|
+
<div class="oc-status-row">
|
|
514
|
+
<div class="oc-status" id="oc-status">…</div>
|
|
515
|
+
<div class="oc-dd" id="oc-dd">
|
|
516
|
+
<button class="oc-dd-btn" id="oc-dd-btn" title="Target session">
|
|
517
|
+
<span class="lbl" id="oc-dd-lbl">Auto (last active)</span>
|
|
518
|
+
<span class="car">${ICON.caret}</span>
|
|
519
|
+
</button>
|
|
520
|
+
<div class="oc-dd-menu" id="oc-dd-menu"></div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
398
523
|
<button class="btn primary oc-submit" id="oc-submit" disabled>${ICON.send}<span>Submit to agent</span></button>
|
|
399
524
|
<div class="oc-toast" id="oc-toast"></div>
|
|
400
525
|
</div>`;
|
|
401
526
|
root.appendChild(sb);
|
|
402
527
|
sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
|
|
403
|
-
sb.querySelector("#oc-
|
|
404
|
-
|
|
528
|
+
sb.querySelector("#oc-pick").addEventListener("click", () => startPicking());
|
|
529
|
+
sb.querySelector("#oc-dd-btn").addEventListener("click", (e) => {
|
|
530
|
+
e.stopPropagation();
|
|
531
|
+
sb.querySelector("#oc-dd").classList.toggle("open");
|
|
532
|
+
});
|
|
533
|
+
root.addEventListener("click", (e) => {
|
|
534
|
+
const dd = sb.querySelector("#oc-dd");
|
|
535
|
+
if (dd && !dd.contains(e.target)) dd.classList.remove("open");
|
|
405
536
|
});
|
|
406
537
|
sb.querySelector("#oc-submit").addEventListener("click", () => {
|
|
407
538
|
submit(pending, false);
|
|
@@ -450,11 +581,11 @@
|
|
|
450
581
|
pending.forEach((a, i) => {
|
|
451
582
|
const card = document.createElement("div");
|
|
452
583
|
card.className = "oc-card";
|
|
584
|
+
const thumb = a.thumb ? `<img class="oc-thumb" src="${a.thumb}" alt="">` : "";
|
|
453
585
|
card.innerHTML = `
|
|
454
|
-
<
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
</div>
|
|
586
|
+
<button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
|
|
587
|
+
${thumb}
|
|
588
|
+
<span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
|
|
458
589
|
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>`;
|
|
459
590
|
list.appendChild(card);
|
|
460
591
|
});
|
|
@@ -473,8 +604,8 @@
|
|
|
473
604
|
if (!el) return;
|
|
474
605
|
el.className = "oc-status checking";
|
|
475
606
|
el.textContent = "Checking connection…";
|
|
476
|
-
|
|
477
|
-
if (
|
|
607
|
+
sendMsg({ type: "oc-status" }, (res) => {
|
|
608
|
+
if (!res) {
|
|
478
609
|
el.className = "oc-status bad";
|
|
479
610
|
el.textContent = "Extension error";
|
|
480
611
|
return;
|
|
@@ -483,13 +614,8 @@
|
|
|
483
614
|
sessions = Array.isArray(res.data.sessions) ? res.data.sessions : [];
|
|
484
615
|
autoSessionID = res.data.sessionID || null;
|
|
485
616
|
renderSessions();
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
el.textContent = "Connected";
|
|
489
|
-
} else {
|
|
490
|
-
el.className = "oc-status warn";
|
|
491
|
-
el.textContent = "Connected — send a message in OpenCode first";
|
|
492
|
-
}
|
|
617
|
+
el.className = "oc-status good";
|
|
618
|
+
el.textContent = "Connected";
|
|
493
619
|
} else {
|
|
494
620
|
el.className = "oc-status bad";
|
|
495
621
|
el.textContent = "Not connected — check the SSH tunnel";
|
|
@@ -499,22 +625,33 @@
|
|
|
499
625
|
|
|
500
626
|
function renderSessions() {
|
|
501
627
|
if (!root) return;
|
|
502
|
-
const
|
|
503
|
-
|
|
628
|
+
const menu = root.getElementById("oc-dd-menu");
|
|
629
|
+
const lbl = root.getElementById("oc-dd-lbl");
|
|
630
|
+
if (!menu || !lbl) return;
|
|
504
631
|
// If the chosen target vanished, fall back to auto.
|
|
505
632
|
if (targetSessionID && !sessions.some((s) => s.id === targetSessionID)) targetSessionID = null;
|
|
506
633
|
const current = effectiveTarget();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
}),
|
|
634
|
+
|
|
635
|
+
const rows = [{ id: "", title: "Auto (last active)" }].concat(
|
|
636
|
+
sessions.map((s) => ({ id: s.id, title: `${s.title || s.id}${s.id === autoSessionID ? " • active" : ""}` })),
|
|
514
637
|
);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
638
|
+
menu.innerHTML = rows
|
|
639
|
+
.map((r) => {
|
|
640
|
+
const selCls = (r.id || null) === targetSessionID ? " sel" : "";
|
|
641
|
+
return `<div class="oc-dd-opt${selCls}" data-id="${escapeHtml(r.id)}" title="${escapeHtml(r.title)}">${escapeHtml(r.title)}</div>`;
|
|
642
|
+
})
|
|
643
|
+
.join("");
|
|
644
|
+
menu.querySelectorAll(".oc-dd-opt").forEach((opt) => {
|
|
645
|
+
opt.addEventListener("click", () => {
|
|
646
|
+
targetSessionID = opt.dataset.id || null;
|
|
647
|
+
root.getElementById("oc-dd").classList.remove("open");
|
|
648
|
+
renderSessions();
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Button label reflects the effective target.
|
|
653
|
+
const chosen = sessions.find((s) => s.id === current);
|
|
654
|
+
lbl.textContent = targetSessionID && chosen ? chosen.title : "Auto (last active)";
|
|
518
655
|
}
|
|
519
656
|
|
|
520
657
|
function toast(text, bad) {
|
|
@@ -541,16 +678,15 @@
|
|
|
541
678
|
if (!annotations.length) return;
|
|
542
679
|
toast("Submitting…");
|
|
543
680
|
const sessionID = effectiveTarget() || undefined;
|
|
544
|
-
|
|
545
|
-
|
|
681
|
+
// Strip the local-only thumbnail; the agent receives text + metadata only.
|
|
682
|
+
const clean = annotations.map(({ thumb, ...rest }) => rest);
|
|
683
|
+
sendMsg({ type: "oc-submit", annotations: clean, sessionID }, (res) => {
|
|
684
|
+
if (!res) {
|
|
546
685
|
toast("Extension error", true);
|
|
547
686
|
return;
|
|
548
687
|
}
|
|
549
688
|
if (res.ok) {
|
|
550
|
-
|
|
551
|
-
if (res.injected) parts.push(`${res.injected} sent`);
|
|
552
|
-
if (res.queued) parts.push(`${res.queued} queued`);
|
|
553
|
-
toast(parts.length ? parts.join(", ") : "Submitted");
|
|
689
|
+
toast(res.injected ? `${res.injected} sent to agent` : "Submitted");
|
|
554
690
|
if (!quick) {
|
|
555
691
|
pending = [];
|
|
556
692
|
renderCards();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-browser-annotation-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|