opencode-browser-annotation-plugin 0.3.2 → 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 +127 -40
- 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
|
@@ -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-status-row { display: flex; align-items: center; gap:
|
|
99
|
-
.oc-
|
|
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; }
|
|
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); }
|
|
@@ -244,6 +262,44 @@
|
|
|
244
262
|
return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
245
263
|
}
|
|
246
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
|
+
|
|
247
303
|
// ---------- picking ----------
|
|
248
304
|
|
|
249
305
|
function pickTarget(e) {
|
|
@@ -282,8 +338,13 @@
|
|
|
282
338
|
e.stopPropagation();
|
|
283
339
|
e.stopImmediatePropagation();
|
|
284
340
|
const rect = hit.el.getBoundingClientRect();
|
|
341
|
+
const meta = elementMeta(hit.el, hit.inShadow);
|
|
285
342
|
stopPicking();
|
|
286
|
-
|
|
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
|
+
});
|
|
287
348
|
}
|
|
288
349
|
|
|
289
350
|
function onKey(e) {
|
|
@@ -330,12 +391,18 @@
|
|
|
330
391
|
}
|
|
331
392
|
}
|
|
332
393
|
|
|
394
|
+
function setPickActive(on) {
|
|
395
|
+
const btn = root && root.getElementById("oc-pick");
|
|
396
|
+
if (btn) btn.classList.toggle("active", on);
|
|
397
|
+
}
|
|
398
|
+
|
|
333
399
|
function startPicking() {
|
|
334
400
|
ensureUI();
|
|
335
401
|
closePopup();
|
|
336
402
|
picking = true;
|
|
337
403
|
document.documentElement.style.cursor = "crosshair";
|
|
338
404
|
root.getElementById("oc-hint").style.display = "flex";
|
|
405
|
+
setPickActive(true);
|
|
339
406
|
}
|
|
340
407
|
|
|
341
408
|
function stopPicking() {
|
|
@@ -344,6 +411,7 @@
|
|
|
344
411
|
if (root) {
|
|
345
412
|
hl().style.display = "none";
|
|
346
413
|
root.getElementById("oc-hint").style.display = "none";
|
|
414
|
+
setPickActive(false);
|
|
347
415
|
}
|
|
348
416
|
}
|
|
349
417
|
|
|
@@ -356,7 +424,7 @@
|
|
|
356
424
|
}
|
|
357
425
|
}
|
|
358
426
|
|
|
359
|
-
function openPopup(element, rect) {
|
|
427
|
+
function openPopup(element, rect, thumb) {
|
|
360
428
|
ensureUI();
|
|
361
429
|
closePopup();
|
|
362
430
|
const el = document.createElement("div");
|
|
@@ -383,7 +451,7 @@
|
|
|
383
451
|
setTimeout(() => ta.focus(), 30);
|
|
384
452
|
|
|
385
453
|
el.querySelector(".oc-x").addEventListener("click", closePopup);
|
|
386
|
-
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 });
|
|
387
455
|
el.querySelector(".oc-add").addEventListener("click", () => {
|
|
388
456
|
if (!ta.value.trim()) return ta.focus();
|
|
389
457
|
pending.push(build());
|
|
@@ -444,7 +512,13 @@
|
|
|
444
512
|
<div class="oc-foot-bar">
|
|
445
513
|
<div class="oc-status-row">
|
|
446
514
|
<div class="oc-status" id="oc-status">…</div>
|
|
447
|
-
<
|
|
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>
|
|
448
522
|
</div>
|
|
449
523
|
<button class="btn primary oc-submit" id="oc-submit" disabled>${ICON.send}<span>Submit to agent</span></button>
|
|
450
524
|
<div class="oc-toast" id="oc-toast"></div>
|
|
@@ -452,8 +526,13 @@
|
|
|
452
526
|
root.appendChild(sb);
|
|
453
527
|
sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
|
|
454
528
|
sb.querySelector("#oc-pick").addEventListener("click", () => startPicking());
|
|
455
|
-
sb.querySelector("#oc-
|
|
456
|
-
|
|
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");
|
|
457
536
|
});
|
|
458
537
|
sb.querySelector("#oc-submit").addEventListener("click", () => {
|
|
459
538
|
submit(pending, false);
|
|
@@ -502,11 +581,11 @@
|
|
|
502
581
|
pending.forEach((a, i) => {
|
|
503
582
|
const card = document.createElement("div");
|
|
504
583
|
card.className = "oc-card";
|
|
584
|
+
const thumb = a.thumb ? `<img class="oc-thumb" src="${a.thumb}" alt="">` : "";
|
|
505
585
|
card.innerHTML = `
|
|
506
|
-
<
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
</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>
|
|
510
589
|
<div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>`;
|
|
511
590
|
list.appendChild(card);
|
|
512
591
|
});
|
|
@@ -535,13 +614,8 @@
|
|
|
535
614
|
sessions = Array.isArray(res.data.sessions) ? res.data.sessions : [];
|
|
536
615
|
autoSessionID = res.data.sessionID || null;
|
|
537
616
|
renderSessions();
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
el.textContent = "Connected";
|
|
541
|
-
} else {
|
|
542
|
-
el.className = "oc-status warn";
|
|
543
|
-
el.textContent = "Connected — send a message in OpenCode first";
|
|
544
|
-
}
|
|
617
|
+
el.className = "oc-status good";
|
|
618
|
+
el.textContent = "Connected";
|
|
545
619
|
} else {
|
|
546
620
|
el.className = "oc-status bad";
|
|
547
621
|
el.textContent = "Not connected — check the SSH tunnel";
|
|
@@ -551,22 +625,33 @@
|
|
|
551
625
|
|
|
552
626
|
function renderSessions() {
|
|
553
627
|
if (!root) return;
|
|
554
|
-
const
|
|
555
|
-
|
|
628
|
+
const menu = root.getElementById("oc-dd-menu");
|
|
629
|
+
const lbl = root.getElementById("oc-dd-lbl");
|
|
630
|
+
if (!menu || !lbl) return;
|
|
556
631
|
// If the chosen target vanished, fall back to auto.
|
|
557
632
|
if (targetSessionID && !sessions.some((s) => s.id === targetSessionID)) targetSessionID = null;
|
|
558
633
|
const current = effectiveTarget();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
}),
|
|
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" : ""}` })),
|
|
566
637
|
);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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)";
|
|
570
655
|
}
|
|
571
656
|
|
|
572
657
|
function toast(text, bad) {
|
|
@@ -593,7 +678,9 @@
|
|
|
593
678
|
if (!annotations.length) return;
|
|
594
679
|
toast("Submitting…");
|
|
595
680
|
const sessionID = effectiveTarget() || undefined;
|
|
596
|
-
|
|
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) => {
|
|
597
684
|
if (!res) {
|
|
598
685
|
toast("Extension error", true);
|
|
599
686
|
return;
|
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",
|