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.
@@ -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, _sender, sendResponse) => {
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();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Annotation",
4
- "version": "0.3.1",
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",
@@ -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, choose Act/Queue, then Add (to the sidebar list) or Send (submit
9
- // immediately). The sidebar is a floating rounded card holding pending
10
- // annotations for batch submit.
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-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
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; }
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: 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; }
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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
- openPopup(elementMeta(hit.el, hit.inShadow), rect);
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
- <label class="oc-target-row">
394
- <span class="oc-target-label">Session</span>
395
- <select class="oc-select" id="oc-session"></select>
396
- </label>
397
- <div class="oc-status" id="oc-status">…</div>
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-session").addEventListener("change", (e) => {
404
- targetSessionID = e.target.value || null;
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
- <div class="oc-card-head">
455
- <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
456
- <button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
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
- chrome.runtime.sendMessage({ type: "oc-status" }, (res) => {
477
- if (chrome.runtime.lastError || !res) {
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
- if (sessions.length || res.data.activeSession) {
487
- el.className = "oc-status good";
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 sel = root.getElementById("oc-session");
503
- if (!sel) return;
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
- 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
- }),
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
- // Keep "Auto" selected when no explicit target.
516
- sel.innerHTML = opts.join("");
517
- if (!targetSessionID) sel.value = "";
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
- chrome.runtime.sendMessage({ type: "oc-submit", annotations, sessionID }, (res) => {
545
- if (chrome.runtime.lastError || !res) {
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
- const parts = [];
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.1",
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",