opencode-browser-annotation-plugin 0.2.0 → 0.2.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,10 +1,17 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Annotation",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "Alt+A to annotate elements and send them to your OpenCode agent. Text and element metadata only.",
6
- "permissions": ["activeTab", "scripting", "storage"],
7
- "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
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"
@@ -1,6 +1,6 @@
1
- // Overlay content script: a full-window annotation layer plus a right sidebar,
2
- // all rendered inside a shadow root so the host page's CSS cannot interfere and
3
- // ours cannot leak. Toggled via Alt+A (handled by the background worker).
1
+ // Overlay content script: a picking layer plus a right sidebar, rendered inside
2
+ // a shadow root so the host page's CSS cannot interfere and ours cannot leak.
3
+ // Toggled via Alt+A (background command) or the toolbar icon.
4
4
  //
5
5
  // Flow: pick an element (hover highlight + click, shadow-DOM aware via
6
6
  // composedPath) -> type an instruction in a card -> choose Act/Queue ->
@@ -8,119 +8,147 @@
8
8
 
9
9
  (() => {
10
10
  if (window.__ocAnnotationInjected) {
11
- // Re-injection: just toggle.
12
11
  window.dispatchEvent(new CustomEvent("oc-annotation-toggle"));
13
12
  return;
14
13
  }
15
14
  window.__ocAnnotationInjected = true;
16
15
 
16
+ const SIDEBAR_W = 348;
17
+
18
+ const ICON = {
19
+ target:
20
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="7"/><line x1="12" y1="1" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="1" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="23" y2="12"/></svg>',
21
+ close:
22
+ '<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>',
23
+ trash:
24
+ '<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>',
25
+ bolt:
26
+ '<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>',
27
+ layers:
28
+ '<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>',
29
+ send:
30
+ '<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>',
31
+ logo:
32
+ '<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>',
33
+ };
34
+
17
35
  const STYLE = `
18
36
  :host { all: initial; }
19
- #oc-layer {
20
- position: fixed; inset: 0; pointer-events: none;
21
- cursor: crosshair; background: transparent;
22
- }
37
+ * { box-sizing: border-box; }
38
+ #oc-layer { position: fixed; inset: 0; pointer-events: none; z-index: 1; }
23
39
  #oc-highlight {
24
- position: fixed; display: none; pointer-events: none;
25
- border: 2px solid #2b7cff; background: rgba(43,124,255,0.12);
26
- border-radius: 3px; box-shadow: 0 0 0 1px rgba(255,255,255,0.6);
40
+ position: fixed; display: none; pointer-events: none; z-index: 2;
41
+ border: 2px solid #4c8dff; background: rgba(76,141,255,0.14);
42
+ border-radius: 3px; box-shadow: 0 0 0 1px rgba(0,0,0,.4);
27
43
  transition: all .04s ease-out;
28
44
  }
29
45
  #oc-hint {
30
- position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
46
+ position: fixed; top: 18px; left: calc(50% - ${SIDEBAR_W / 2}px);
47
+ transform: translateX(-50%); z-index: 3;
31
48
  display: none; gap: 10px; align-items: center; pointer-events: none;
32
- font: 13px system-ui, sans-serif; color: #fff;
33
- background: rgba(20,22,28,.92); padding: 8px 14px; border-radius: 999px;
34
- box-shadow: 0 6px 20px rgba(0,0,0,.25);
35
- }
36
- #oc-hint .key {
37
- font-size: 11px; background: rgba(255,255,255,.18);
38
- padding: 2px 7px; border-radius: 5px;
49
+ font: 13px system-ui, sans-serif; color: #e6e8ee;
50
+ background: rgba(20,22,30,.94); padding: 8px 14px; border-radius: 999px;
51
+ box-shadow: 0 8px 24px rgba(0,0,0,.4); border: 1px solid rgba(255,255,255,.08);
39
52
  }
53
+ #oc-hint .key { font-size: 11px; background: rgba(255,255,255,.14); padding: 2px 7px; border-radius: 5px; }
54
+
40
55
  #oc-sidebar {
41
- position: fixed; top: 0; right: 0; height: 100vh; width: 344px;
42
- pointer-events: auto; display: flex; flex-direction: column;
43
- background: #fbfbfd; color: #1c1d21;
44
- font: 14px/1.45 system-ui, -apple-system, sans-serif;
45
- box-shadow: -8px 0 30px rgba(0,0,0,.16);
46
- border-left: 1px solid rgba(0,0,0,.08);
56
+ position: fixed; top: 0; right: 0; height: 100vh; width: ${SIDEBAR_W}px;
57
+ pointer-events: auto; display: flex; flex-direction: column; z-index: 4;
58
+ background: #16171d; color: #e6e8ee;
59
+ font: 13.5px/1.5 system-ui, -apple-system, sans-serif;
60
+ border-left: 1px solid rgba(255,255,255,.08);
61
+ box-shadow: -12px 0 40px rgba(0,0,0,.45);
47
62
  }
48
63
  #oc-sidebar header {
49
- display: flex; align-items: center; justify-content: space-between;
50
- padding: 14px 16px; border-bottom: 1px solid rgba(0,0,0,.07);
64
+ display: flex; align-items: center; gap: 9px;
65
+ padding: 14px 14px; border-bottom: 1px solid rgba(255,255,255,.07);
51
66
  }
52
- #oc-sidebar header .title { font-weight: 650; font-size: 14px; letter-spacing: .2px; }
53
- #oc-close {
54
- border: none; background: transparent; font-size: 16px; color: #8a8d96;
55
- cursor: pointer; border-radius: 6px; width: 28px; height: 28px;
67
+ #oc-sidebar header .logo { width: 18px; height: 18px; color: #4c8dff; flex: none; }
68
+ #oc-sidebar header .title { font-weight: 650; font-size: 13.5px; flex: 1; letter-spacing: .2px; }
69
+ .iconbtn {
70
+ display: inline-flex; align-items: center; justify-content: center;
71
+ border: none; background: transparent; color: #8b90a0; cursor: pointer;
72
+ width: 30px; height: 30px; border-radius: 8px; padding: 0;
56
73
  }
57
- #oc-close:hover { background: rgba(0,0,0,.06); color: #1c1d21; }
58
- .oc-actions { padding: 12px 16px 4px; }
74
+ .iconbtn svg { width: 16px; height: 16px; }
75
+ .iconbtn:hover { background: rgba(255,255,255,.07); color: #e6e8ee; }
76
+
77
+ .oc-actions { padding: 12px 14px 6px; }
59
78
  button.primary {
60
79
  font: inherit; font-weight: 600; width: 100%;
61
- padding: 9px 12px; border-radius: 9px; cursor: pointer;
62
- color: #fff; background: #2b7cff; border: 1px solid #2b7cff;
80
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
81
+ padding: 10px 12px; border-radius: 10px; cursor: pointer;
82
+ color: #fff; background: #3f7dff; border: 1px solid #3f7dff;
63
83
  transition: background .15s, transform .05s;
64
84
  }
65
- button.primary:hover:not(:disabled) { background: #1e6ef0; }
85
+ button.primary svg { width: 16px; height: 16px; }
86
+ button.primary:hover:not(:disabled) { background: #3670f0; }
66
87
  button.primary:active:not(:disabled) { transform: translateY(1px); }
67
- button.primary:disabled { opacity: .45; cursor: default; }
68
- #oc-pick.active { background: #e8551f; border-color: #e8551f; }
69
- #oc-list { flex: 1; overflow-y: auto; padding: 10px 16px; display: flex; flex-direction: column; gap: 10px; }
70
- .oc-empty { color: #8a8d96; font-size: 13px; padding: 20px 4px; text-align: center; }
88
+ button.primary:disabled { opacity: .4; cursor: default; }
89
+ #oc-pick.active { background: #e0632a; border-color: #e0632a; }
90
+
91
+ #oc-list { flex: 1; overflow-y: auto; padding: 10px 14px; display: flex; flex-direction: column; gap: 10px; }
92
+ #oc-list::-webkit-scrollbar { width: 10px; }
93
+ #oc-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); border-radius: 6px; border: 3px solid #16171d; }
94
+ .oc-empty { color: #71768a; font-size: 12.5px; padding: 26px 8px; text-align: center; line-height: 1.6; }
95
+
71
96
  .oc-card {
72
- background: #fff; border: 1px solid rgba(0,0,0,.08); border-radius: 12px;
73
- padding: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.05);
97
+ background: #1f2129; border: 1px solid rgba(255,255,255,.08); border-radius: 12px;
98
+ padding: 10px; box-shadow: 0 1px 2px rgba(0,0,0,.3);
74
99
  }
75
- .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
100
+ .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
76
101
  .oc-desc {
77
- font: 12px ui-monospace, monospace; color: #4a4d57;
102
+ font: 11.5px ui-monospace, monospace; color: #aeb4c6;
78
103
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
79
- background: #f0f1f4; padding: 2px 7px; border-radius: 6px;
104
+ background: rgba(255,255,255,.06); padding: 3px 8px; border-radius: 6px; flex: 1;
80
105
  }
81
- .oc-rm { color: #b02a2a; cursor: pointer; font-size: 13px; padding: 0 2px; }
82
- .oc-rm:hover { color: #e03131; }
83
106
  .oc-card textarea {
84
- width: 100%; box-sizing: border-box; resize: vertical; font: inherit;
85
- border: 1px solid rgba(0,0,0,.12); border-radius: 8px; padding: 7px 9px;
86
- background: #fff; color: #1c1d21;
107
+ width: 100%; resize: vertical; min-height: 46px; font: inherit;
108
+ border: 1px solid rgba(255,255,255,.1); border-radius: 8px; padding: 8px 9px;
109
+ background: #14151b; color: #e6e8ee;
87
110
  }
88
- .oc-card textarea:focus { outline: none; border-color: #2b7cff; box-shadow: 0 0 0 3px rgba(43,124,255,.15); }
111
+ .oc-card textarea::placeholder { color: #616678; }
112
+ .oc-card textarea:focus { outline: none; border-color: #4c8dff; box-shadow: 0 0 0 3px rgba(76,141,255,.18); }
89
113
  .oc-modes { display: flex; gap: 6px; margin-top: 8px; }
90
114
  .oc-mode {
91
115
  font: inherit; font-size: 12px; font-weight: 600; flex: 1;
92
- padding: 5px 8px; border-radius: 7px; cursor: pointer;
93
- background: #f0f1f4; color: #55585f; border: 1px solid transparent;
116
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
117
+ padding: 6px 8px; border-radius: 8px; cursor: pointer;
118
+ background: rgba(255,255,255,.05); color: #9298aa; border: 1px solid transparent;
94
119
  }
95
- .oc-mode:hover { background: #e7e8ec; }
96
- .oc-mode.sel { background: #e8f0ff; color: #1e6ef0; border-color: #bcd4ff; }
97
- #oc-sidebar footer { padding: 12px 16px 14px; border-top: 1px solid rgba(0,0,0,.07); position: relative; }
98
- .oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 7px; }
99
- .oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #b7bac2; flex: none; }
100
- .oc-status.good::before { background: #1faa5a; }
101
- .oc-status.warn::before { background: #e0a020; }
102
- .oc-status.bad::before { background: #e03131; }
103
- .oc-status.checking::before { background: #7aa7ff; }
104
- .oc-status.good { color: #178048; }
105
- .oc-status.warn { color: #9a6a10; }
106
- .oc-status.bad { color: #b02a2a; }
120
+ .oc-mode svg { width: 13px; height: 13px; }
121
+ .oc-mode:hover { background: rgba(255,255,255,.09); color: #cfd3df; }
122
+ .oc-mode.sel { background: rgba(76,141,255,.16); color: #7aa9ff; border-color: rgba(76,141,255,.4); }
123
+
124
+ #oc-sidebar footer { padding: 12px 14px 14px; border-top: 1px solid rgba(255,255,255,.07); position: relative; }
125
+ .oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
126
+ .oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #5b6070; flex: none; }
127
+ .oc-status.good::before { background: #34d399; box-shadow: 0 0 8px rgba(52,211,153,.6); }
128
+ .oc-status.warn::before { background: #fbbf24; }
129
+ .oc-status.bad::before { background: #f87171; }
130
+ .oc-status.checking::before { background: #60a5fa; }
131
+ .oc-status.good { color: #34d399; }
132
+ .oc-status.warn { color: #fbbf24; }
133
+ .oc-status.bad { color: #f87171; }
134
+
107
135
  .oc-toast {
108
- position: absolute; left: 16px; right: 16px; bottom: 60px;
109
- background: #1c1d21; color: #fff; font-size: 12.5px;
110
- padding: 8px 12px; border-radius: 9px; opacity: 0; transform: translateY(6px);
136
+ position: absolute; left: 14px; right: 14px; bottom: 62px;
137
+ background: #0e0f14; color: #fff; font-size: 12.5px;
138
+ padding: 9px 12px; border-radius: 9px; opacity: 0; transform: translateY(6px);
111
139
  transition: opacity .2s, transform .2s; pointer-events: none;
112
- box-shadow: 0 8px 24px rgba(0,0,0,.22);
140
+ box-shadow: 0 10px 30px rgba(0,0,0,.5); border: 1px solid rgba(255,255,255,.08);
113
141
  }
114
142
  .oc-toast.show { opacity: 1; transform: translateY(0); }
115
- .oc-toast.bad { background: #b02a2a; }
143
+ .oc-toast.bad { border-color: rgba(248,113,113,.5); color: #fca5a5; }
116
144
  `;
117
145
 
118
- let host = null; // shadow host element
119
- let root = null; // shadow root
146
+ let host = null;
147
+ let root = null;
120
148
  let picking = false;
121
- let hoverEl = null;
122
- let annotations = []; // { instruction, mode, page, element }
149
+ let annotations = [];
123
150
  let statusTimer = null;
151
+ let visible = false;
124
152
 
125
153
  // ---------- element metadata ----------
126
154
 
@@ -183,24 +211,31 @@
183
211
 
184
212
  function shortDescriptor(el) {
185
213
  const tag = (el.tag || "el").toLowerCase();
186
- const id = el.testId ? `[data-testid=${el.testId}]` : el.id ? `#${el.id}` : el.ariaLabel ? `[${el.ariaLabel}]` : "";
187
- return `<${tag}>${id}`;
214
+ const id = el.testId
215
+ ? `[data-testid=${el.testId}]`
216
+ : el.id
217
+ ? `#${el.id}`
218
+ : el.ariaLabel
219
+ ? `[${el.ariaLabel}]`
220
+ : el.classes && el.classes.length
221
+ ? `.${el.classes[0]}`
222
+ : "";
223
+ return `${tag}${id}`;
188
224
  }
189
225
 
190
- // ---------- picking ----------
226
+ // ---------- picking (layer is always pointer-events:none; read the real
227
+ // element from the event's composed path, which pierces shadow DOM) ----------
191
228
 
192
229
  function pickTarget(e) {
193
- // composedPath surfaces the real element inside shadow roots; e.target only
194
- // gives the shadow host.
195
- const path = e.composedPath ? e.composedPath() : [];
230
+ const path = e.composedPath ? e.composedPath() : [e.target];
196
231
  for (const n of path) {
197
- if (n instanceof Element && n !== host && !(root && root.contains(n))) {
198
- const inShadow = n.getRootNode && n.getRootNode() instanceof ShadowRoot;
199
- return { el: n, inShadow };
200
- }
232
+ if (n === host) return null; // our UI wrapper
233
+ if (!(n instanceof Element)) continue;
234
+ if (root && root.contains(n)) return null; // hovering our own UI
235
+ const inShadow = n.getRootNode && n.getRootNode() instanceof ShadowRoot;
236
+ return { el: n, inShadow };
201
237
  }
202
- const t = e.target;
203
- return t instanceof Element ? { el: t, inShadow: false } : null;
238
+ return null;
204
239
  }
205
240
 
206
241
  function positionHighlight(el) {
@@ -219,10 +254,12 @@
219
254
  function onMove(e) {
220
255
  if (!picking) return;
221
256
  const hit = pickTarget(e);
222
- if (!hit) return;
223
- hoverEl = hit.el;
224
- hoverEl.__ocInShadow = hit.inShadow;
225
- positionHighlight(hoverEl);
257
+ if (!hit) {
258
+ const box = root.getElementById("oc-highlight");
259
+ if (box) box.style.display = "none";
260
+ return;
261
+ }
262
+ positionHighlight(hit.el);
226
263
  }
227
264
 
228
265
  function onClick(e) {
@@ -231,32 +268,30 @@
231
268
  if (!hit) return;
232
269
  e.preventDefault();
233
270
  e.stopPropagation();
271
+ e.stopImmediatePropagation();
234
272
  stopPicking();
235
273
  addCardForElement(elementMeta(hit.el, hit.inShadow));
236
274
  }
237
275
 
238
276
  function onKey(e) {
239
277
  if (e.key === "Escape") {
240
- if (picking) {
241
- stopPicking();
242
- } else {
243
- close();
244
- }
278
+ if (picking) stopPicking();
279
+ else toggle();
245
280
  }
246
281
  }
247
282
 
248
283
  function startPicking() {
249
284
  picking = true;
250
- root.getElementById("oc-layer").style.pointerEvents = "auto";
285
+ document.documentElement.style.cursor = "crosshair";
251
286
  root.getElementById("oc-hint").style.display = "flex";
252
287
  setPickBtn(true);
253
288
  }
254
289
 
255
290
  function stopPicking() {
256
291
  picking = false;
292
+ document.documentElement.style.cursor = "";
257
293
  const box = root.getElementById("oc-highlight");
258
294
  if (box) box.style.display = "none";
259
- root.getElementById("oc-layer").style.pointerEvents = "none";
260
295
  root.getElementById("oc-hint").style.display = "none";
261
296
  setPickBtn(false);
262
297
  }
@@ -264,29 +299,32 @@
264
299
  function setPickBtn(active) {
265
300
  const btn = root.getElementById("oc-pick");
266
301
  if (btn) {
267
- btn.textContent = active ? "Picking… (Esc to cancel)" : "Select element";
302
+ btn.innerHTML = `${ICON.target}<span>${active ? "Picking… (Esc)" : "Select element"}</span>`;
268
303
  btn.classList.toggle("active", active);
269
304
  }
270
305
  }
271
306
 
272
- // ---------- annotation cards ----------
307
+ // ---------- cards ----------
273
308
 
274
309
  function addCardForElement(element) {
275
310
  annotations.push({ instruction: "", mode: "act", page: { url: location.href, title: document.title }, element });
276
311
  renderCards();
277
- // focus the new card's textarea
278
312
  const areas = root.querySelectorAll(".oc-card textarea");
279
313
  const last = areas[areas.length - 1];
280
314
  if (last) last.focus();
281
315
  }
282
316
 
317
+ function escapeHtml(s) {
318
+ return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
319
+ }
320
+
283
321
  function renderCards() {
284
322
  const list = root.getElementById("oc-list");
285
323
  list.innerHTML = "";
286
324
  if (annotations.length === 0) {
287
325
  const empty = document.createElement("div");
288
326
  empty.className = "oc-empty";
289
- empty.textContent = "No annotations yet. Click “Select element”, then pick something on the page.";
327
+ empty.textContent = "No annotations yet. Click “Select element”, then click something on the page.";
290
328
  list.appendChild(empty);
291
329
  }
292
330
  annotations.forEach((a, i) => {
@@ -295,31 +333,29 @@
295
333
  card.innerHTML = `
296
334
  <div class="oc-card-head">
297
335
  <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(shortDescriptor(a.element))}</span>
298
- <span class="oc-rm" data-i="${i}" title="Remove">✕</span>
336
+ <button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
299
337
  </div>
300
338
  <textarea rows="2" placeholder="Instruction for this element…" data-i="${i}">${escapeHtml(a.instruction)}</textarea>
301
- <div class="oc-modes" data-i="${i}">
302
- <button class="oc-mode ${a.mode === "act" ? "sel" : ""}" data-mode="act" data-i="${i}">Act now</button>
303
- <button class="oc-mode ${a.mode === "queue" ? "sel" : ""}" data-mode="queue" data-i="${i}">Queue</button>
339
+ <div class="oc-modes">
340
+ <button class="oc-mode ${a.mode === "act" ? "sel" : ""}" data-mode="act" data-i="${i}">${ICON.bolt}<span>Act now</span></button>
341
+ <button class="oc-mode ${a.mode === "queue" ? "sel" : ""}" data-mode="queue" data-i="${i}">${ICON.layers}<span>Queue</span></button>
304
342
  </div>`;
305
343
  list.appendChild(card);
306
344
  });
307
345
  updateSubmit();
308
346
  }
309
347
 
310
- function escapeHtml(s) {
311
- return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
312
- }
313
-
314
348
  function updateSubmit() {
315
349
  const btn = root.getElementById("oc-submit");
316
350
  btn.disabled = annotations.length === 0;
317
- btn.textContent = annotations.length ? `Submit ${annotations.length} to agent` : "Submit to agent";
351
+ btn.querySelector("span").textContent = annotations.length
352
+ ? `Submit ${annotations.length} to agent`
353
+ : "Submit to agent";
318
354
  }
319
355
 
320
356
  // ---------- status ----------
321
357
 
322
- async function refreshStatus() {
358
+ function refreshStatus() {
323
359
  const el = root.getElementById("oc-status");
324
360
  el.className = "oc-status checking";
325
361
  el.textContent = "Checking connection…";
@@ -331,10 +367,9 @@
331
367
  }
332
368
  if (res.ok && res.data?.ok) {
333
369
  if (res.data.activeSession) {
334
- const name = res.data.sessionTitle || res.data.sessionID || "active session";
335
370
  const q = res.data.queued ? ` · ${res.data.queued} queued` : "";
336
371
  el.className = "oc-status good";
337
- el.textContent = `Connected${name}${q}`;
372
+ el.textContent = `Connected${q}`;
338
373
  } else {
339
374
  el.className = "oc-status warn";
340
375
  el.textContent = "Connected — send a message in OpenCode first";
@@ -390,12 +425,12 @@
390
425
  t.__timer = setTimeout(() => (t.className = "oc-toast"), 4000);
391
426
  }
392
427
 
393
- // ---------- UI construction ----------
428
+ // ---------- UI ----------
394
429
 
395
430
  function buildUI() {
396
431
  host = document.createElement("div");
397
432
  host.id = "oc-annotation-host";
398
- host.style.cssText = "all: initial; position: fixed; inset: 0; z-index: 2147483647; pointer-events: none;";
433
+ host.style.cssText = "all: initial;";
399
434
  root = host.attachShadow({ mode: "open" });
400
435
  root.innerHTML = `
401
436
  <style>${STYLE}</style>
@@ -404,24 +439,23 @@
404
439
  <div id="oc-hint"><span>Hover an element and click to annotate</span><span class="key">Esc</span></div>
405
440
  <aside id="oc-sidebar">
406
441
  <header>
407
- <div class="title">OpenCode Annotation</div>
408
- <button id="oc-close" title="Close (Esc)">✕</button>
442
+ <span class="logo">${ICON.logo}</span>
443
+ <span class="title">OpenCode Annotation</span>
444
+ <button id="oc-close" class="iconbtn" title="Close (Alt+A)">${ICON.close}</button>
409
445
  </header>
410
- <div class="oc-actions"><button id="oc-pick" class="primary">Select element</button></div>
446
+ <div class="oc-actions"><button id="oc-pick" class="primary">${ICON.target}<span>Select element</span></button></div>
411
447
  <div id="oc-list"></div>
412
448
  <footer>
413
449
  <div id="oc-status" class="oc-status">…</div>
414
- <button id="oc-submit" class="primary" disabled>Submit to agent</button>
450
+ <button id="oc-submit" class="primary" disabled>${ICON.send}<span>Submit to agent</span></button>
415
451
  <div id="oc-toast" class="oc-toast"></div>
416
452
  </footer>
417
453
  </aside>`;
418
454
  document.documentElement.appendChild(host);
419
455
 
420
456
  root.getElementById("oc-pick").addEventListener("click", () => (picking ? stopPicking() : startPicking()));
421
- root.getElementById("oc-close").addEventListener("click", close);
457
+ root.getElementById("oc-close").addEventListener("click", () => toggle());
422
458
  root.getElementById("oc-submit").addEventListener("click", submit);
423
-
424
- // event delegation for card controls
425
459
  root.getElementById("oc-list").addEventListener("click", (e) => {
426
460
  const rm = e.target.closest(".oc-rm");
427
461
  if (rm) {
@@ -438,24 +472,28 @@
438
472
 
439
473
  renderCards();
440
474
  refreshStatus();
441
- statusTimer = setInterval(refreshStatus, 15000);
442
475
  }
443
476
 
444
- // ---------- lifecycle ----------
477
+ function pushPage(on) {
478
+ const el = document.documentElement;
479
+ el.style.transition = "margin-right .22s ease";
480
+ el.style.marginRight = on ? `${SIDEBAR_W}px` : "";
481
+ }
445
482
 
446
483
  function open() {
447
- if (host) {
448
- host.style.display = "block";
449
- } else {
450
- buildUI();
451
- }
484
+ if (!host) buildUI();
485
+ else host.style.display = "";
486
+ pushPage(true);
452
487
  document.addEventListener("mousemove", onMove, true);
453
488
  document.addEventListener("click", onClick, true);
454
489
  document.addEventListener("keydown", onKey, true);
490
+ if (!statusTimer) statusTimer = setInterval(refreshStatus, 15000);
491
+ refreshStatus();
455
492
  }
456
493
 
457
494
  function close() {
458
495
  stopPicking();
496
+ pushPage(false);
459
497
  document.removeEventListener("mousemove", onMove, true);
460
498
  document.removeEventListener("click", onClick, true);
461
499
  document.removeEventListener("keydown", onKey, true);
@@ -466,15 +504,10 @@
466
504
  }
467
505
  }
468
506
 
469
- let visible = false;
470
507
  function toggle() {
471
508
  visible = !visible;
472
- if (visible) {
473
- open();
474
- if (statusTimer === null && host) statusTimer = setInterval(refreshStatus, 15000);
475
- } else {
476
- close();
477
- }
509
+ if (visible) open();
510
+ else close();
478
511
  }
479
512
 
480
513
  window.addEventListener("oc-annotation-toggle", toggle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-browser-annotation-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",