opencode-browser-annotation-plugin 0.2.0 → 0.3.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/README.md CHANGED
@@ -73,15 +73,18 @@ extension's **Settings** page.
73
73
  ## Use
74
74
 
75
75
  1. Send at least one message in OpenCode so the plugin knows the active session.
76
- 2. Press **Alt+A** on any page to open the annotation overlay (or click the
77
- toolbar icon). Press Alt+A again or Esc to close it.
78
- 3. In the sidebar, click **Select element**, hover to highlight, and click the
79
- element you want (works inside shadow DOM).
80
- 4. Type an instruction in the card. Choose **Act now** (agent responds
81
- immediately) or **Queue** (held for context until the next Act). Repeat for
82
- more elements.
83
- 5. Click **Submit to agent**. The sidebar footer shows the connection and active
84
- session; a toast confirms how many were sent vs queued.
76
+ 2. Press **Alt+A** on any page, then click an element (highlight follows the
77
+ cursor; works inside shadow DOM). A popup opens next to it.
78
+ 3. In the popup, type an instruction and choose **Act now** (agent responds
79
+ immediately) or **Queue** (held for context until the next Act). Then either:
80
+ - **Send** (or Cmd/Ctrl+Enter) submit this one right away, no sidebar needed.
81
+ - **Add to list** stash it in the sidebar to batch with others.
82
+ 4. Press **Alt+Shift+A** (or click the toolbar icon) to open the **list sidebar**,
83
+ review pending annotations, and **Submit** them together. The footer shows the
84
+ connection status; a toast confirms how many were sent vs queued.
85
+
86
+ Shortcuts: `Alt+A` picks an element, `Alt+Shift+A` toggles the list. If they do
87
+ nothing after loading an unpacked build, set them at `chrome://extensions/shortcuts`.
85
88
 
86
89
  ## Payload
87
90
 
@@ -1,8 +1,9 @@
1
1
  // Background service worker.
2
- // - Toggles the annotation overlay on Alt+A (command) or toolbar click.
3
- // - Performs all network I/O to the plugin endpoint (status + submit), because
4
- // page CSP can block a content script from fetching localhost. Results are
5
- // returned to the overlay via message responses.
2
+ // - Alt+A -> start element picking
3
+ // - Alt+Shift+A -> toggle the annotation-list sidebar
4
+ // - toolbar click -> toggle the sidebar
5
+ // Performs all network I/O to the plugin endpoint (status + submit), because
6
+ // page CSP can block a content script from fetching localhost.
6
7
 
7
8
  const DEFAULT_ENDPOINT = "http://127.0.0.1:39517";
8
9
  const EXTENSION_VERSION = chrome.runtime.getManifest().version;
@@ -12,33 +13,34 @@ async function getEndpoint() {
12
13
  return (endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
13
14
  }
14
15
 
15
- async function toggleOverlay(tab) {
16
+ async function ensureInjected(tabId) {
17
+ const [{ result } = {}] = await chrome.scripting.executeScript({
18
+ target: { tabId },
19
+ func: () => Boolean(window.__ocAnnotationInjected),
20
+ });
21
+ if (!result) {
22
+ await chrome.scripting.executeScript({ target: { tabId }, files: ["overlay.js"] });
23
+ }
24
+ }
25
+
26
+ async function sendToOverlay(tab, type) {
16
27
  if (!tab?.id) return;
17
28
  try {
18
- // The overlay content script listens for this and toggles itself. If it is
19
- // not injected yet, inject it first, then it opens on load.
20
- const [{ result } = {}] = await chrome.scripting.executeScript({
21
- target: { tabId: tab.id },
22
- func: () => Boolean(window.__ocAnnotationInjected),
23
- });
24
- if (!result) {
25
- await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["overlay.js"] });
26
- } else {
27
- await chrome.tabs.sendMessage(tab.id, { type: "oc-toggle" });
28
- }
29
+ await ensureInjected(tab.id);
30
+ await chrome.tabs.sendMessage(tab.id, { type });
29
31
  } catch {
30
32
  // e.g. chrome:// pages or the web store cannot be injected.
31
33
  }
32
34
  }
33
35
 
34
36
  chrome.commands.onCommand.addListener(async (command) => {
35
- if (command !== "toggle-overlay") return;
36
37
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
37
- await toggleOverlay(tab);
38
+ if (command === "select-element") await sendToOverlay(tab, "oc-pick");
39
+ else if (command === "toggle-sidebar") await sendToOverlay(tab, "oc-toggle-sidebar");
38
40
  });
39
41
 
40
42
  chrome.action.onClicked.addListener((tab) => {
41
- void toggleOverlay(tab);
43
+ void sendToOverlay(tab, "oc-toggle-sidebar");
42
44
  });
43
45
 
44
46
  chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Annotation",
4
- "version": "0.2.0",
5
- "description": "Alt+A to annotate elements and send them to your OpenCode agent. Text and element metadata only.",
4
+ "version": "0.3.0",
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": ["activeTab", "scripting", "storage"],
7
7
  "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
8
8
  "background": {
@@ -10,16 +10,17 @@
10
10
  "type": "module"
11
11
  },
12
12
  "action": {
13
- "default_title": "Toggle OpenCode Annotation (Alt+A)"
13
+ "default_title": "OpenCode Annotation Alt+A to pick, Alt+Shift+A for list"
14
14
  },
15
15
  "options_page": "options.html",
16
16
  "commands": {
17
- "toggle-overlay": {
18
- "suggested_key": {
19
- "default": "Alt+A",
20
- "mac": "Alt+A"
21
- },
22
- "description": "Toggle the annotation overlay"
17
+ "select-element": {
18
+ "suggested_key": { "default": "Alt+A", "mac": "Alt+A" },
19
+ "description": "Pick an element to annotate"
20
+ },
21
+ "toggle-sidebar": {
22
+ "suggested_key": { "default": "Alt+Shift+A", "mac": "Alt+Shift+A" },
23
+ "description": "Toggle the annotation list sidebar"
23
24
  }
24
25
  }
25
26
  }
@@ -37,7 +37,7 @@
37
37
  <span id="saved"></span>
38
38
 
39
39
  <div class="hint">
40
- <p><strong>Shortcut:</strong> press <code>Alt+A</code> on any page to toggle the annotation overlay.</p>
40
+ <p><strong>Shortcuts:</strong> <code>Alt+A</code> to pick an element, <code>Alt+Shift+A</code> to toggle the annotation list. If they do nothing after an unpacked reload, set them at <code>chrome://extensions/shortcuts</code>.</p>
41
41
  <p>When OpenCode runs on a remote host, forward the port from your desktop and point this at the local end:</p>
42
42
  <p><code>ssh -N -L 39517:127.0.0.1:39517 you@host</code></p>
43
43
  </div>
@@ -1,132 +1,136 @@
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. All UI lives inside one shadow root so host-page CSS
2
+ // cannot interfere and ours cannot leak. Nothing pushes the page.
4
3
  //
5
- // Flow: pick an element (hover highlight + click, shadow-DOM aware via
6
- // composedPath) -> type an instruction in a card -> choose Act/Queue ->
7
- // Submit sends all cards to the background worker, which POSTs to the plugin.
4
+ // Interactions:
5
+ // Alt+A -> start element picking (background sends "oc-pick")
6
+ // Alt+Shift+A -> toggle the list sidebar ("oc-toggle-sidebar")
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
11
 
9
12
  (() => {
10
- if (window.__ocAnnotationInjected) {
11
- // Re-injection: just toggle.
12
- window.dispatchEvent(new CustomEvent("oc-annotation-toggle"));
13
- return;
14
- }
13
+ if (window.__ocAnnotationInjected) return;
15
14
  window.__ocAnnotationInjected = true;
16
15
 
16
+ const ICON = {
17
+ target:
18
+ '<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="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/></svg>',
19
+ close:
20
+ '<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>',
21
+ trash:
22
+ '<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>',
23
+ bolt:
24
+ '<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>',
25
+ layers:
26
+ '<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>',
27
+ send:
28
+ '<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>',
29
+ plus:
30
+ '<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>',
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;
37
+ * { box-sizing: border-box; }
38
+ .oc-hl {
39
+ position: fixed; display: none; pointer-events: none; z-index: 2147483644;
40
+ border: 2px solid #4c8dff; background: rgba(76,141,255,0.14);
41
+ border-radius: 3px; box-shadow: 0 0 0 1px rgba(0,0,0,.4); transition: all .04s ease-out;
22
42
  }
23
- #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);
27
- transition: all .04s ease-out;
28
- }
29
- #oc-hint {
30
- position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
43
+ .oc-hint {
44
+ position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 2147483646;
31
45
  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;
39
- }
40
- #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);
47
- }
48
- #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);
51
- }
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;
46
+ font: 13px system-ui, sans-serif; color: #e6e8ee;
47
+ background: rgba(18,20,28,.95); padding: 8px 14px; border-radius: 999px;
48
+ box-shadow: 0 8px 24px rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.08);
56
49
  }
57
- #oc-close:hover { background: rgba(0,0,0,.06); color: #1c1d21; }
58
- .oc-actions { padding: 12px 16px 4px; }
59
- button.primary {
60
- 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;
63
- transition: background .15s, transform .05s;
64
- }
65
- button.primary:hover:not(:disabled) { background: #1e6ef0; }
66
- 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; }
71
- .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);
74
- }
75
- .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
76
- .oc-desc {
77
- font: 12px ui-monospace, monospace; color: #4a4d57;
78
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
79
- background: #f0f1f4; padding: 2px 7px; border-radius: 6px;
80
- }
81
- .oc-rm { color: #b02a2a; cursor: pointer; font-size: 13px; padding: 0 2px; }
82
- .oc-rm:hover { color: #e03131; }
83
- .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;
87
- }
88
- .oc-card textarea:focus { outline: none; border-color: #2b7cff; box-shadow: 0 0 0 3px rgba(43,124,255,.15); }
89
- .oc-modes { display: flex; gap: 6px; margin-top: 8px; }
90
- .oc-mode {
91
- 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;
50
+ .oc-hint .key { font-size: 11px; background: rgba(255,255,255,.14); padding: 2px 7px; border-radius: 5px; }
51
+
52
+ /* shared surface */
53
+ .oc-surface {
54
+ background: rgba(20,22,30,.98); color: #e6e8ee; border: 1px solid rgba(255,255,255,.1);
55
+ border-radius: 14px; box-shadow: 0 18px 50px rgba(0,0,0,.5);
56
+ font: 13.5px/1.5 system-ui, -apple-system, sans-serif;
94
57
  }
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; }
107
- .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);
111
- transition: opacity .2s, transform .2s; pointer-events: none;
112
- box-shadow: 0 8px 24px rgba(0,0,0,.22);
58
+ .oc-head { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border-bottom: 1px solid rgba(255,255,255,.07); }
59
+ .oc-head .logo { width: 17px; height: 17px; color: #4c8dff; flex: none; }
60
+ .oc-head .title { font-weight: 650; font-size: 13px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
61
+ .oc-path { font: 11px ui-monospace, monospace; color: #8b90a0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
62
+ .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; }
63
+ .iconbtn svg { width: 15px; height: 15px; }
64
+ .iconbtn:hover { background: rgba(255,255,255,.08); color: #e6e8ee; }
65
+
66
+ textarea.oc-ta {
67
+ width: 100%; resize: vertical; min-height: 62px; font: inherit;
68
+ border: 1px solid rgba(255,255,255,.12); border-radius: 9px; padding: 9px 10px;
69
+ background: #14151b; color: #e6e8ee;
113
70
  }
71
+ textarea.oc-ta::placeholder { color: #616678; }
72
+ textarea.oc-ta:focus { outline: none; border-color: #4c8dff; box-shadow: 0 0 0 3px rgba(76,141,255,.18); }
73
+
74
+ .oc-modes { display: flex; gap: 6px; margin-top: 9px; }
75
+ .oc-mode { font: inherit; font-size: 12px; font-weight: 600; flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 8px; border-radius: 8px; cursor: pointer; background: rgba(255,255,255,.05); color: #9298aa; border: 1px solid transparent; }
76
+ .oc-mode svg { width: 13px; height: 13px; }
77
+ .oc-mode:hover { background: rgba(255,255,255,.09); color: #cfd3df; }
78
+ .oc-mode.sel { background: rgba(76,141,255,.16); color: #7aa9ff; border-color: rgba(76,141,255,.4); }
79
+
80
+ .btn { font: inherit; font-weight: 600; font-size: 12.5px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px 12px; border-radius: 9px; cursor: pointer; border: 1px solid rgba(255,255,255,.14); background: rgba(255,255,255,.05); color: #e6e8ee; transition: background .15s, transform .05s; }
81
+ .btn svg { width: 15px; height: 15px; }
82
+ .btn:hover:not(:disabled) { background: rgba(255,255,255,.1); }
83
+ .btn:active:not(:disabled) { transform: translateY(1px); }
84
+ .btn:disabled { opacity: .4; cursor: default; }
85
+ .btn.primary { background: #3f7dff; border-color: #3f7dff; color: #fff; }
86
+ .btn.primary:hover:not(:disabled) { background: #3670f0; }
87
+
88
+ /* popup */
89
+ .oc-popup { position: fixed; z-index: 2147483647; width: 320px; pointer-events: auto; }
90
+ .oc-popup .oc-body { padding: 11px 12px; }
91
+ .oc-popup .oc-foot { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid rgba(255,255,255,.07); }
92
+ .oc-popup .oc-foot .btn { flex: 1; }
93
+ .oc-hintline { font-size: 11px; color: #6a6f80; margin-top: 8px; text-align: center; }
94
+
95
+ /* sidebar (floating card at right, not pinned) */
96
+ .oc-sidebar { position: fixed; top: 16px; right: 16px; bottom: 16px; width: 330px; z-index: 2147483645; display: flex; flex-direction: column; pointer-events: auto; overflow: hidden; }
97
+ .oc-sidebar .badge { font-size: 11px; font-weight: 700; color: #7aa9ff; background: rgba(76,141,255,.16); border-radius: 999px; padding: 1px 8px; }
98
+ .oc-list { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 9px; }
99
+ .oc-list::-webkit-scrollbar { width: 10px; }
100
+ .oc-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); border-radius: 6px; border: 3px solid transparent; background-clip: content-box; }
101
+ .oc-empty { color: #71768a; font-size: 12.5px; padding: 24px 8px; text-align: center; line-height: 1.6; }
102
+ .oc-card { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 11px; padding: 9px 10px; }
103
+ .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
104
+ .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; }
105
+ .oc-card .oc-text { font-size: 12.5px; color: #d4d7e0; white-space: pre-wrap; word-break: break-word; }
106
+ .oc-card .oc-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: #9298aa; margin-top: 6px; }
107
+ .oc-card .oc-tag svg { width: 12px; height: 12px; }
108
+ .oc-foot-bar { padding: 11px 12px; border-top: 1px solid rgba(255,255,255,.07); position: relative; }
109
+ .oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
110
+ .oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #5b6070; flex: none; }
111
+ .oc-status.good::before { background: #34d399; box-shadow: 0 0 8px rgba(52,211,153,.6); }
112
+ .oc-status.warn::before { background: #fbbf24; } .oc-status.bad::before { background: #f87171; } .oc-status.checking::before { background: #60a5fa; }
113
+ .oc-status.good { color: #34d399; } .oc-status.warn { color: #fbbf24; } .oc-status.bad { color: #f87171; }
114
+ .oc-submit { width: 100%; }
115
+ .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); }
114
116
  .oc-toast.show { opacity: 1; transform: translateY(0); }
115
- .oc-toast.bad { background: #b02a2a; }
117
+ .oc-toast.bad { border-color: rgba(248,113,113,.5); color: #fca5a5; }
118
+ .oc-toast-float { position: fixed; left: auto; right: 20px; bottom: 20px; width: auto; max-width: 320px; z-index: 2147483647; }
116
119
  `;
117
120
 
118
- let host = null; // shadow host element
119
- let root = null; // shadow root
121
+ let host = null;
122
+ let root = null;
120
123
  let picking = false;
121
- let hoverEl = null;
122
- let annotations = []; // { instruction, mode, page, element }
124
+ let popupEl = null;
125
+ let pending = []; // annotations added to the sidebar list
126
+ let sidebarOpen = false;
123
127
  let statusTimer = null;
124
128
 
125
- // ---------- element metadata ----------
129
+ // ---------- metadata ----------
126
130
 
127
131
  function bestTestId(el) {
128
- for (const attr of ["data-testid", "data-test", "data-test-id", "data-cy", "data-qa"]) {
129
- const v = el.getAttribute && el.getAttribute(attr);
132
+ for (const a of ["data-testid", "data-test", "data-test-id", "data-cy", "data-qa"]) {
133
+ const v = el.getAttribute && el.getAttribute(a);
130
134
  if (v) return v;
131
135
  }
132
136
  return undefined;
@@ -142,9 +146,9 @@
142
146
  if (node.classList && node.classList.length) {
143
147
  sel += "." + Array.from(node.classList).slice(0, 3).map((c) => CSS.escape(c)).join(".");
144
148
  }
145
- const parent = node.parentElement;
146
- if (parent) {
147
- const sibs = Array.from(parent.children).filter((c) => c.nodeName === node.nodeName);
149
+ const p = node.parentElement;
150
+ if (p) {
151
+ const sibs = Array.from(p.children).filter((c) => c.nodeName === node.nodeName);
148
152
  if (sibs.length > 1) sel += `:nth-of-type(${sibs.indexOf(node) + 1})`;
149
153
  }
150
154
  parts.unshift(sel);
@@ -160,7 +164,7 @@
160
164
  function elementMeta(el, inShadow) {
161
165
  const r = el.getBoundingClientRect();
162
166
  const classes = el.classList ? Array.from(el.classList) : [];
163
- const meta = {
167
+ const m = {
164
168
  selector: cssPath(el),
165
169
  tag: el.tagName,
166
170
  id: el.id || undefined,
@@ -177,52 +181,48 @@
177
181
  inIframe: window.top !== window.self,
178
182
  html: (el.outerHTML || "").slice(0, 800),
179
183
  };
180
- for (const k of Object.keys(meta)) if (meta[k] === undefined) delete meta[k];
181
- return meta;
184
+ for (const k of Object.keys(m)) if (m[k] === undefined) delete m[k];
185
+ return m;
182
186
  }
183
187
 
184
- function shortDescriptor(el) {
188
+ function descriptor(el) {
185
189
  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}`;
190
+ const id = el.testId ? `[data-testid=${el.testId}]` : el.id ? `#${el.id}` : el.ariaLabel ? `[${el.ariaLabel}]` : el.classes && el.classes.length ? `.${el.classes[0]}` : "";
191
+ return `${tag}${id}`;
192
+ }
193
+
194
+ function escapeHtml(s) {
195
+ return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
188
196
  }
189
197
 
190
198
  // ---------- picking ----------
191
199
 
192
200
  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() : [];
201
+ const path = e.composedPath ? e.composedPath() : [e.target];
196
202
  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
- }
203
+ if (n === host) return null;
204
+ if (!(n instanceof Element)) continue;
205
+ if (root && root.contains(n)) return null;
206
+ const inShadow = n.getRootNode && n.getRootNode() instanceof ShadowRoot;
207
+ return { el: n, inShadow };
201
208
  }
202
- const t = e.target;
203
- return t instanceof Element ? { el: t, inShadow: false } : null;
209
+ return null;
204
210
  }
205
211
 
206
- function positionHighlight(el) {
207
- const box = root.getElementById("oc-highlight");
208
- if (!box) return;
209
- const r = el.getBoundingClientRect();
210
- Object.assign(box.style, {
211
- display: "block",
212
- left: `${r.left}px`,
213
- top: `${r.top}px`,
214
- width: `${r.width}px`,
215
- height: `${r.height}px`,
216
- });
212
+ function hl() {
213
+ return root.getElementById("oc-hl");
217
214
  }
218
215
 
219
216
  function onMove(e) {
220
217
  if (!picking) return;
221
218
  const hit = pickTarget(e);
222
- if (!hit) return;
223
- hoverEl = hit.el;
224
- hoverEl.__ocInShadow = hit.inShadow;
225
- positionHighlight(hoverEl);
219
+ const box = hl();
220
+ if (!hit) {
221
+ box.style.display = "none";
222
+ return;
223
+ }
224
+ const r = hit.el.getBoundingClientRect();
225
+ Object.assign(box.style, { display: "block", left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
226
226
  }
227
227
 
228
228
  function onClick(e) {
@@ -231,96 +231,242 @@
231
231
  if (!hit) return;
232
232
  e.preventDefault();
233
233
  e.stopPropagation();
234
+ e.stopImmediatePropagation();
235
+ const rect = hit.el.getBoundingClientRect();
234
236
  stopPicking();
235
- addCardForElement(elementMeta(hit.el, hit.inShadow));
237
+ openPopup(elementMeta(hit.el, hit.inShadow), rect);
236
238
  }
237
239
 
238
240
  function onKey(e) {
239
241
  if (e.key === "Escape") {
240
- if (picking) {
241
- stopPicking();
242
- } else {
243
- close();
244
- }
242
+ if (popupEl) closePopup();
243
+ else if (picking) stopPicking();
245
244
  }
246
245
  }
247
246
 
247
+ function ensureUI() {
248
+ if (host) return;
249
+ host = document.createElement("div");
250
+ host.id = "oc-annotation-host";
251
+ host.style.cssText = "all: initial;";
252
+ root = host.attachShadow({ mode: "open" });
253
+ const style = document.createElement("style");
254
+ style.textContent = STYLE;
255
+ root.appendChild(style);
256
+ const hlBox = document.createElement("div");
257
+ hlBox.className = "oc-hl";
258
+ hlBox.id = "oc-hl";
259
+ root.appendChild(hlBox);
260
+ const hint = document.createElement("div");
261
+ hint.className = "oc-hint";
262
+ hint.id = "oc-hint";
263
+ hint.innerHTML = `<span>Click an element to annotate</span><span class="key">Esc</span>`;
264
+ root.appendChild(hint);
265
+ document.documentElement.appendChild(host);
266
+ document.addEventListener("mousemove", onMove, true);
267
+ document.addEventListener("click", onClick, true);
268
+ document.addEventListener("keydown", onKey, true);
269
+ }
270
+
248
271
  function startPicking() {
272
+ ensureUI();
273
+ closePopup();
249
274
  picking = true;
250
- root.getElementById("oc-layer").style.pointerEvents = "auto";
275
+ document.documentElement.style.cursor = "crosshair";
251
276
  root.getElementById("oc-hint").style.display = "flex";
252
- setPickBtn(true);
253
277
  }
254
278
 
255
279
  function stopPicking() {
256
280
  picking = false;
257
- const box = root.getElementById("oc-highlight");
258
- if (box) box.style.display = "none";
259
- root.getElementById("oc-layer").style.pointerEvents = "none";
260
- root.getElementById("oc-hint").style.display = "none";
261
- setPickBtn(false);
281
+ document.documentElement.style.cursor = "";
282
+ if (root) {
283
+ hl().style.display = "none";
284
+ root.getElementById("oc-hint").style.display = "none";
285
+ }
262
286
  }
263
287
 
264
- function setPickBtn(active) {
265
- const btn = root.getElementById("oc-pick");
266
- if (btn) {
267
- btn.textContent = active ? "Picking… (Esc to cancel)" : "Select element";
268
- btn.classList.toggle("active", active);
288
+ // ---------- popup (onUI-style, positioned near element) ----------
289
+
290
+ function closePopup() {
291
+ if (popupEl) {
292
+ popupEl.remove();
293
+ popupEl = null;
269
294
  }
270
295
  }
271
296
 
272
- // ---------- annotation cards ----------
297
+ function openPopup(element, rect) {
298
+ ensureUI();
299
+ closePopup();
300
+ const state = { mode: "act" };
301
+ const el = document.createElement("div");
302
+ el.className = "oc-popup oc-surface";
303
+ el.innerHTML = `
304
+ <div class="oc-head">
305
+ <span class="logo">${ICON.logo}</span>
306
+ <span class="oc-path" title="${escapeHtml(element.selector || "")}">${escapeHtml(descriptor(element))}</span>
307
+ <button class="iconbtn oc-x" title="Cancel (Esc)">${ICON.close}</button>
308
+ </div>
309
+ <div class="oc-body">
310
+ <textarea class="oc-ta" rows="3" placeholder="Describe the change or ask about this element…"></textarea>
311
+ <div class="oc-modes">
312
+ <button class="oc-mode sel" data-mode="act">${ICON.bolt}<span>Act now</span></button>
313
+ <button class="oc-mode" data-mode="queue">${ICON.layers}<span>Queue</span></button>
314
+ </div>
315
+ <div class="oc-hintline">Cmd/Ctrl+Enter to send</div>
316
+ </div>
317
+ <div class="oc-foot">
318
+ <button class="btn oc-add">${ICON.plus}<span>Add to list</span></button>
319
+ <button class="btn primary oc-send">${ICON.send}<span>Send</span></button>
320
+ </div>`;
321
+ root.appendChild(el);
322
+ popupEl = el;
323
+ positionPopup(el, rect);
324
+
325
+ const ta = el.querySelector(".oc-ta");
326
+ setTimeout(() => ta.focus(), 30);
327
+
328
+ el.querySelector(".oc-x").addEventListener("click", closePopup);
329
+ el.querySelectorAll(".oc-mode").forEach((b) =>
330
+ b.addEventListener("click", () => {
331
+ state.mode = b.dataset.mode;
332
+ el.querySelectorAll(".oc-mode").forEach((x) => x.classList.toggle("sel", x === b));
333
+ }),
334
+ );
335
+ const build = () => ({ instruction: ta.value.trim(), mode: state.mode, page: { url: location.href, title: document.title }, element });
336
+ el.querySelector(".oc-add").addEventListener("click", () => {
337
+ if (!ta.value.trim()) return ta.focus();
338
+ pending.push(build());
339
+ closePopup();
340
+ openSidebar();
341
+ });
342
+ el.querySelector(".oc-send").addEventListener("click", () => {
343
+ if (!ta.value.trim()) return ta.focus();
344
+ const one = build();
345
+ closePopup();
346
+ submit([one], true);
347
+ });
348
+ ta.addEventListener("keydown", (e) => {
349
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
350
+ e.preventDefault();
351
+ if (ta.value.trim()) {
352
+ const one = build();
353
+ closePopup();
354
+ submit([one], true);
355
+ }
356
+ }
357
+ });
358
+ }
359
+
360
+ function positionPopup(el, rect) {
361
+ const w = 320;
362
+ const r = el.getBoundingClientRect();
363
+ const h = r.height || 220;
364
+ let left = rect.left + rect.width + 14;
365
+ let top = rect.top;
366
+ if (left + w > window.innerWidth - 16) left = rect.left - w - 14;
367
+ if (left < 16) left = 16;
368
+ if (top + h > window.innerHeight - 16) top = window.innerHeight - h - 16;
369
+ if (top < 16) top = 16;
370
+ el.style.left = `${left}px`;
371
+ el.style.top = `${top}px`;
372
+ }
273
373
 
274
- function addCardForElement(element) {
275
- annotations.push({ instruction: "", mode: "act", page: { url: location.href, title: document.title }, element });
374
+ // ---------- sidebar ----------
375
+
376
+ function openSidebar() {
377
+ ensureUI();
378
+ sidebarOpen = true;
379
+ let sb = root.getElementById("oc-sidebar");
380
+ if (!sb) {
381
+ sb = document.createElement("aside");
382
+ sb.className = "oc-sidebar oc-surface";
383
+ sb.id = "oc-sidebar";
384
+ sb.innerHTML = `
385
+ <div class="oc-head">
386
+ <span class="logo">${ICON.logo}</span>
387
+ <span class="title">Annotations</span>
388
+ <span class="badge" id="oc-badge">0</span>
389
+ <button class="iconbtn" id="oc-close" title="Close (Alt+Shift+A)">${ICON.close}</button>
390
+ </div>
391
+ <div class="oc-list" id="oc-list"></div>
392
+ <div class="oc-foot-bar">
393
+ <div class="oc-status" id="oc-status">…</div>
394
+ <button class="btn primary oc-submit" id="oc-submit" disabled>${ICON.send}<span>Submit to agent</span></button>
395
+ <div class="oc-toast" id="oc-toast"></div>
396
+ </div>`;
397
+ root.appendChild(sb);
398
+ sb.querySelector("#oc-close").addEventListener("click", closeSidebar);
399
+ sb.querySelector("#oc-submit").addEventListener("click", () => {
400
+ submit(pending, false);
401
+ });
402
+ sb.querySelector("#oc-list").addEventListener("click", (e) => {
403
+ const rm = e.target.closest(".oc-rm");
404
+ if (rm) {
405
+ pending.splice(Number(rm.dataset.i), 1);
406
+ renderCards();
407
+ }
408
+ });
409
+ }
410
+ sb.style.display = "flex";
276
411
  renderCards();
277
- // focus the new card's textarea
278
- const areas = root.querySelectorAll(".oc-card textarea");
279
- const last = areas[areas.length - 1];
280
- if (last) last.focus();
412
+ refreshStatus();
413
+ if (!statusTimer) statusTimer = setInterval(refreshStatus, 15000);
414
+ }
415
+
416
+ function closeSidebar() {
417
+ sidebarOpen = false;
418
+ const sb = root && root.getElementById("oc-sidebar");
419
+ if (sb) sb.style.display = "none";
420
+ if (statusTimer) {
421
+ clearInterval(statusTimer);
422
+ statusTimer = null;
423
+ }
424
+ }
425
+
426
+ function toggleSidebar() {
427
+ if (sidebarOpen) closeSidebar();
428
+ else openSidebar();
281
429
  }
282
430
 
283
431
  function renderCards() {
432
+ if (!root) return;
284
433
  const list = root.getElementById("oc-list");
434
+ const badge = root.getElementById("oc-badge");
435
+ if (badge) badge.textContent = String(pending.length);
285
436
  list.innerHTML = "";
286
- if (annotations.length === 0) {
287
- const empty = document.createElement("div");
288
- empty.className = "oc-empty";
289
- empty.textContent = "No annotations yet. Click “Select element”, then pick something on the page.";
290
- list.appendChild(empty);
437
+ if (pending.length === 0) {
438
+ const e = document.createElement("div");
439
+ e.className = "oc-empty";
440
+ e.textContent = "No annotations yet. Press Alt+A, click an element, and “Add to list”.";
441
+ list.appendChild(e);
291
442
  }
292
- annotations.forEach((a, i) => {
443
+ pending.forEach((a, i) => {
293
444
  const card = document.createElement("div");
294
445
  card.className = "oc-card";
446
+ const modeIcon = a.mode === "queue" ? ICON.layers : ICON.bolt;
447
+ const modeName = a.mode === "queue" ? "Queue" : "Act now";
295
448
  card.innerHTML = `
296
449
  <div class="oc-card-head">
297
- <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>
450
+ <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
451
+ <button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
299
452
  </div>
300
- <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>
304
- </div>`;
453
+ <div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>
454
+ <div class="oc-tag">${modeIcon}<span>${modeName}</span></div>`;
305
455
  list.appendChild(card);
306
456
  });
307
- updateSubmit();
308
- }
309
-
310
- function escapeHtml(s) {
311
- return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
312
- }
313
-
314
- function updateSubmit() {
315
457
  const btn = root.getElementById("oc-submit");
316
- btn.disabled = annotations.length === 0;
317
- btn.textContent = annotations.length ? `Submit ${annotations.length} to agent` : "Submit to agent";
458
+ if (btn) {
459
+ btn.disabled = pending.length === 0;
460
+ btn.querySelector("span").textContent = pending.length ? `Submit ${pending.length} to agent` : "Submit to agent";
461
+ }
318
462
  }
319
463
 
320
- // ---------- status ----------
464
+ // ---------- status + submit ----------
321
465
 
322
- async function refreshStatus() {
466
+ function refreshStatus() {
467
+ if (!root) return;
323
468
  const el = root.getElementById("oc-status");
469
+ if (!el) return;
324
470
  el.className = "oc-status checking";
325
471
  el.textContent = "Checking connection…";
326
472
  chrome.runtime.sendMessage({ type: "oc-status" }, (res) => {
@@ -331,10 +477,9 @@
331
477
  }
332
478
  if (res.ok && res.data?.ok) {
333
479
  if (res.data.activeSession) {
334
- const name = res.data.sessionTitle || res.data.sessionID || "active session";
335
480
  const q = res.data.queued ? ` · ${res.data.queued} queued` : "";
336
481
  el.className = "oc-status good";
337
- el.textContent = `Connected${name}${q}`;
482
+ el.textContent = `Connected${q}`;
338
483
  } else {
339
484
  el.className = "oc-status warn";
340
485
  el.textContent = "Connected — send a message in OpenCode first";
@@ -346,143 +491,58 @@
346
491
  });
347
492
  }
348
493
 
349
- // ---------- submit ----------
350
-
351
- function collectInstructions() {
352
- root.querySelectorAll(".oc-card textarea").forEach((ta) => {
353
- const i = Number(ta.dataset.i);
354
- if (annotations[i]) annotations[i].instruction = ta.value.trim();
355
- });
494
+ function toast(text, bad) {
495
+ if (!root) return;
496
+ // Prefer the sidebar's toast when it is open; otherwise use a standalone
497
+ // floating toast so a quick Send never forces the sidebar open.
498
+ const sb = root.getElementById("oc-sidebar");
499
+ let t = sb && sb.style.display !== "none" ? root.getElementById("oc-toast") : root.getElementById("oc-toast-float");
500
+ if (!t) {
501
+ t = document.createElement("div");
502
+ t.className = "oc-toast oc-toast-float";
503
+ t.id = "oc-toast-float";
504
+ root.appendChild(t);
505
+ }
506
+ t.textContent = text;
507
+ t.className = (t.id === "oc-toast-float" ? "oc-toast oc-toast-float" : "oc-toast") + " show" + (bad ? " bad" : "");
508
+ clearTimeout(t.__timer);
509
+ t.__timer = setTimeout(() => {
510
+ t.className = t.id === "oc-toast-float" ? "oc-toast oc-toast-float" : "oc-toast";
511
+ }, 4000);
356
512
  }
357
513
 
358
- function submit() {
359
- collectInstructions();
360
- if (annotations.length === 0) return;
361
- const btn = root.getElementById("oc-submit");
362
- btn.disabled = true;
363
- setToast("Submitting…");
514
+ function submit(annotations, quick) {
515
+ if (!annotations.length) return;
516
+ toast("Submitting…");
364
517
  chrome.runtime.sendMessage({ type: "oc-submit", annotations }, (res) => {
365
518
  if (chrome.runtime.lastError || !res) {
366
- setToast("Extension error", true);
367
- updateSubmit();
519
+ toast("Extension error", true);
368
520
  return;
369
521
  }
370
522
  if (res.ok) {
371
523
  const parts = [];
372
524
  if (res.injected) parts.push(`${res.injected} sent`);
373
525
  if (res.queued) parts.push(`${res.queued} queued`);
374
- setToast(parts.length ? parts.join(", ") : "Submitted");
375
- annotations = [];
376
- renderCards();
526
+ toast(parts.length ? parts.join(", ") : "Submitted");
527
+ if (!quick) {
528
+ pending = [];
529
+ renderCards();
530
+ }
377
531
  refreshStatus();
378
532
  } else {
379
- setToast(`Failed: ${res.error || "unknown"}`, true);
380
- updateSubmit();
381
- }
382
- });
383
- }
384
-
385
- function setToast(text, bad) {
386
- const t = root.getElementById("oc-toast");
387
- t.textContent = text;
388
- t.className = "oc-toast show" + (bad ? " bad" : "");
389
- clearTimeout(t.__timer);
390
- t.__timer = setTimeout(() => (t.className = "oc-toast"), 4000);
391
- }
392
-
393
- // ---------- UI construction ----------
394
-
395
- function buildUI() {
396
- host = document.createElement("div");
397
- host.id = "oc-annotation-host";
398
- host.style.cssText = "all: initial; position: fixed; inset: 0; z-index: 2147483647; pointer-events: none;";
399
- root = host.attachShadow({ mode: "open" });
400
- root.innerHTML = `
401
- <style>${STYLE}</style>
402
- <div id="oc-layer"></div>
403
- <div id="oc-highlight"></div>
404
- <div id="oc-hint"><span>Hover an element and click to annotate</span><span class="key">Esc</span></div>
405
- <aside id="oc-sidebar">
406
- <header>
407
- <div class="title">OpenCode Annotation</div>
408
- <button id="oc-close" title="Close (Esc)">✕</button>
409
- </header>
410
- <div class="oc-actions"><button id="oc-pick" class="primary">Select element</button></div>
411
- <div id="oc-list"></div>
412
- <footer>
413
- <div id="oc-status" class="oc-status">…</div>
414
- <button id="oc-submit" class="primary" disabled>Submit to agent</button>
415
- <div id="oc-toast" class="oc-toast"></div>
416
- </footer>
417
- </aside>`;
418
- document.documentElement.appendChild(host);
419
-
420
- root.getElementById("oc-pick").addEventListener("click", () => (picking ? stopPicking() : startPicking()));
421
- root.getElementById("oc-close").addEventListener("click", close);
422
- root.getElementById("oc-submit").addEventListener("click", submit);
423
-
424
- // event delegation for card controls
425
- root.getElementById("oc-list").addEventListener("click", (e) => {
426
- const rm = e.target.closest(".oc-rm");
427
- if (rm) {
428
- annotations.splice(Number(rm.dataset.i), 1);
429
- renderCards();
430
- return;
431
- }
432
- const mode = e.target.closest(".oc-mode");
433
- if (mode) {
434
- annotations[Number(mode.dataset.i)].mode = mode.dataset.mode;
435
- renderCards();
533
+ toast(`Failed: ${res.error || "unknown"}`, true);
534
+ if (!quick) {
535
+ // keep the list so the user can retry
536
+ renderCards();
537
+ }
436
538
  }
437
539
  });
438
-
439
- renderCards();
440
- refreshStatus();
441
- statusTimer = setInterval(refreshStatus, 15000);
442
- }
443
-
444
- // ---------- lifecycle ----------
445
-
446
- function open() {
447
- if (host) {
448
- host.style.display = "block";
449
- } else {
450
- buildUI();
451
- }
452
- document.addEventListener("mousemove", onMove, true);
453
- document.addEventListener("click", onClick, true);
454
- document.addEventListener("keydown", onKey, true);
455
- }
456
-
457
- function close() {
458
- stopPicking();
459
- document.removeEventListener("mousemove", onMove, true);
460
- document.removeEventListener("click", onClick, true);
461
- document.removeEventListener("keydown", onKey, true);
462
- if (host) host.style.display = "none";
463
- if (statusTimer) {
464
- clearInterval(statusTimer);
465
- statusTimer = null;
466
- }
467
540
  }
468
541
 
469
- let visible = false;
470
- function toggle() {
471
- visible = !visible;
472
- if (visible) {
473
- open();
474
- if (statusTimer === null && host) statusTimer = setInterval(refreshStatus, 15000);
475
- } else {
476
- close();
477
- }
478
- }
542
+ // ---------- messages ----------
479
543
 
480
- window.addEventListener("oc-annotation-toggle", toggle);
481
544
  chrome.runtime.onMessage.addListener((msg) => {
482
- if (msg?.type === "oc-toggle") toggle();
545
+ if (msg?.type === "oc-pick") startPicking();
546
+ else if (msg?.type === "oc-toggle-sidebar") toggleSidebar();
483
547
  });
484
-
485
- // First injection opens immediately.
486
- visible = true;
487
- open();
488
548
  })();
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.3.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",