opencode-browser-annotation-plugin 0.5.0 → 0.5.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Annotation",
4
- "version": "0.5.0",
4
+ "version": "0.5.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
6
  "permissions": [
7
7
  "activeTab",
@@ -139,6 +139,7 @@
139
139
  let sessions = []; // [{ id, title, updated }]
140
140
  let targetSessionID = null; // user-chosen target; null = auto (last active)
141
141
  let autoSessionID = null; // the plugin's last-active session
142
+ let lastStatusState = null; // "good" | "bad" | "checking" | null; avoids flicker
142
143
 
143
144
  // A content script keeps running after its extension is reloaded/updated, but
144
145
  // its chrome.* calls then throw "Extension context invalidated". Guard every
@@ -203,6 +204,25 @@
203
204
  return undefined;
204
205
  }
205
206
 
207
+ // Drop build-generated/hashed class names (CSS Modules, styled-components,
208
+ // Emotion, etc.). They change every build and don't map to source, so they are
209
+ // noise for locating code. Keep human-authored classes like "heading-element".
210
+ function isHashedClass(c) {
211
+ if (!c) return true;
212
+ if (/module__/i.test(c)) return true; // CSS Modules: Foo-module__Bar__hHXUL
213
+ if (/__[A-Za-z0-9]{4,}$/.test(c)) return true; // trailing __hash
214
+ if (/^css-[a-z0-9]{5,}$/i.test(c)) return true; // Emotion: css-1ab2c3
215
+ if (/^sc-[A-Za-z0-9]{5,}$/.test(c)) return true; // styled-components: sc-bdVaJa
216
+ if (/^[A-Za-z0-9]{7,}$/.test(c) && /[0-9]/.test(c) && /[A-Z]/.test(c)) return true; // mixed-case+digit opaque hash
217
+ return false;
218
+ }
219
+
220
+ function cleanClasses(list) {
221
+ if (!list || !list.length) return undefined;
222
+ const kept = list.filter((c) => !isHashedClass(c));
223
+ return kept.length ? kept : undefined;
224
+ }
225
+
206
226
  function cssPath(el) {
207
227
  if (!(el instanceof Element)) return "";
208
228
  if (el.id) return `#${CSS.escape(el.id)}`;
@@ -237,7 +257,8 @@
237
257
  else if (el.id) s += `#${el.id}`;
238
258
  const role = el.getAttribute && el.getAttribute("role");
239
259
  if (role) s += `[role=${role}]`;
240
- if (el.classList && el.classList.length) s += "." + Array.from(el.classList).slice(0, 3).join(".");
260
+ const cls = cleanClasses(el.classList ? Array.from(el.classList) : []);
261
+ if (cls) s += "." + cls.slice(0, 3).join(".");
241
262
  return s;
242
263
  }
243
264
 
@@ -319,7 +340,6 @@
319
340
 
320
341
  function elementMeta(el, inShadow) {
321
342
  const r = el.getBoundingClientRect();
322
- const classes = el.classList ? Array.from(el.classList) : [];
323
343
  const fw = frameworkComponents(el);
324
344
  const m = {
325
345
  selector: cssPath(el),
@@ -329,7 +349,7 @@
329
349
  testId: bestTestId(el),
330
350
  role: el.getAttribute("role") || undefined,
331
351
  ariaLabel: el.getAttribute("aria-label") || undefined,
332
- classes: classes.length ? classes : undefined,
352
+ classes: cleanClasses(el.classList ? Array.from(el.classList) : []),
333
353
  text: (el.textContent || "").trim().slice(0, 500) || undefined,
334
354
  href: el.getAttribute("href") || undefined,
335
355
  src: el.getAttribute("src") || undefined,
@@ -371,18 +391,22 @@
371
391
  const pad = 6 * dpr;
372
392
  let sx = Math.max(0, rect.left * dpr - pad);
373
393
  let sy = Math.max(0, rect.top * dpr - pad);
374
- let sw = Math.min(img.width - sx, rect.width * dpr + pad * 2);
375
- let sh = Math.min(img.height - sy, rect.height * dpr + pad * 2);
394
+ const sw = Math.min(img.width - sx, rect.width * dpr + pad * 2);
395
+ const sh = Math.min(img.height - sy, rect.height * dpr + pad * 2);
376
396
  if (sw <= 0 || sh <= 0) return resolve(null);
377
- // cap output size for a compact list thumbnail
378
- const maxW = 300;
397
+ // Keep the crop at (up to) full captured resolution so it stays sharp
398
+ // on the sidebar's HiDPI display. Only downscale if it's very large,
399
+ // to keep the data URL reasonable.
400
+ const maxW = 900;
379
401
  const scale = Math.min(1, maxW / sw);
380
- const cw = Math.round(sw * scale);
381
- const ch = Math.round(sh * scale);
402
+ const cw = Math.max(1, Math.round(sw * scale));
403
+ const ch = Math.max(1, Math.round(sh * scale));
382
404
  const c = document.createElement("canvas");
383
405
  c.width = cw;
384
406
  c.height = ch;
385
407
  const ctx = c.getContext("2d");
408
+ ctx.imageSmoothingEnabled = true;
409
+ ctx.imageSmoothingQuality = "high";
386
410
  ctx.drawImage(img, sx, sy, sw, sh, 0, 0, cw, ch);
387
411
  resolve(c.toDataURL("image/png"));
388
412
  } catch {
@@ -620,7 +644,7 @@
620
644
  </div>`;
621
645
  root.appendChild(sb);
622
646
  sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
623
- sb.querySelector("#oc-pick").addEventListener("click", () => startPicking());
647
+ sb.querySelector("#oc-pick").addEventListener("click", () => (picking ? stopPicking() : startPicking()));
624
648
  sb.querySelector("#oc-dd-btn").addEventListener("click", (e) => {
625
649
  e.stopPropagation();
626
650
  sb.querySelector("#oc-dd").classList.toggle("open");
@@ -654,6 +678,7 @@
654
678
  clearInterval(statusTimer);
655
679
  statusTimer = null;
656
680
  }
681
+ lastStatusState = null; // re-check cleanly on reopen
657
682
  }
658
683
 
659
684
  function toggleSidebar() {
@@ -693,27 +718,33 @@
693
718
 
694
719
  // ---------- status + submit ----------
695
720
 
721
+ // Apply a status only when it actually changes, so routine polls don't make
722
+ // the text flicker. Only the very first check shows "Checking…".
723
+ function setStatus(state, text) {
724
+ const el = root && root.getElementById("oc-status");
725
+ if (!el) return;
726
+ if (lastStatusState === state) return; // stable during polls
727
+ lastStatusState = state;
728
+ el.className = `oc-status ${state}`;
729
+ el.textContent = text;
730
+ }
731
+
696
732
  function refreshStatus() {
697
733
  if (!root) return;
698
- const el = root.getElementById("oc-status");
699
- if (!el) return;
700
- el.className = "oc-status checking";
701
- el.textContent = "Checking connection…";
734
+ if (lastStatusState === null) setStatus("checking", "Checking connection…");
702
735
  sendMsg({ type: "oc-status" }, (res) => {
703
736
  if (!res) {
704
- el.className = "oc-status bad";
705
- el.textContent = "Extension error";
737
+ setStatus("bad", "Extension error");
706
738
  return;
707
739
  }
708
740
  if (res.ok && res.data?.ok) {
741
+ // Session list updates every poll; the status text stays stable.
709
742
  sessions = Array.isArray(res.data.sessions) ? res.data.sessions : [];
710
743
  autoSessionID = res.data.sessionID || null;
711
744
  renderSessions();
712
- el.className = "oc-status good";
713
- el.textContent = "Connected";
745
+ setStatus("good", "Connected");
714
746
  } else {
715
- el.className = "oc-status bad";
716
- el.textContent = "Not connected — check the SSH tunnel";
747
+ setStatus("bad", "Not connected — check the SSH tunnel");
717
748
  }
718
749
  });
719
750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-browser-annotation-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.5.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",