opencode-browser-annotation-plugin 0.2.1 → 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,32 +1,26 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Annotation",
4
- "version": "0.2.1",
5
- "description": "Alt+A to annotate elements and send them to your OpenCode agent. Text and element metadata only.",
6
- "permissions": [
7
- "activeTab",
8
- "scripting",
9
- "storage"
10
- ],
11
- "host_permissions": [
12
- "http://127.0.0.1/*",
13
- "http://localhost/*"
14
- ],
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
+ "permissions": ["activeTab", "scripting", "storage"],
7
+ "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
15
8
  "background": {
16
9
  "service_worker": "background.js",
17
10
  "type": "module"
18
11
  },
19
12
  "action": {
20
- "default_title": "Toggle OpenCode Annotation (Alt+A)"
13
+ "default_title": "OpenCode Annotation Alt+A to pick, Alt+Shift+A for list"
21
14
  },
22
15
  "options_page": "options.html",
23
16
  "commands": {
24
- "toggle-overlay": {
25
- "suggested_key": {
26
- "default": "Alt+A",
27
- "mac": "Alt+A"
28
- },
29
- "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"
30
24
  }
31
25
  }
32
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,23 +1,21 @@
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.
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
- window.dispatchEvent(new CustomEvent("oc-annotation-toggle"));
12
- return;
13
- }
13
+ if (window.__ocAnnotationInjected) return;
14
14
  window.__ocAnnotationInjected = true;
15
15
 
16
- const SIDEBAR_W = 348;
17
-
18
16
  const ICON = {
19
17
  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>',
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>',
21
19
  close:
22
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>',
23
21
  trash:
@@ -28,6 +26,8 @@
28
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>',
29
27
  send:
30
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
31
  logo:
32
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
33
  };
@@ -35,126 +35,102 @@
35
35
  const STYLE = `
36
36
  :host { all: initial; }
37
37
  * { box-sizing: border-box; }
38
- #oc-layer { position: fixed; inset: 0; pointer-events: none; z-index: 1; }
39
- #oc-highlight {
40
- position: fixed; display: none; pointer-events: none; z-index: 2;
38
+ .oc-hl {
39
+ position: fixed; display: none; pointer-events: none; z-index: 2147483644;
41
40
  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);
43
- transition: all .04s ease-out;
41
+ border-radius: 3px; box-shadow: 0 0 0 1px rgba(0,0,0,.4); transition: all .04s ease-out;
44
42
  }
45
- #oc-hint {
46
- position: fixed; top: 18px; left: calc(50% - ${SIDEBAR_W / 2}px);
47
- transform: translateX(-50%); z-index: 3;
43
+ .oc-hint {
44
+ position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 2147483646;
48
45
  display: none; gap: 10px; align-items: center; pointer-events: none;
49
46
  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);
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);
52
49
  }
53
- #oc-hint .key { font-size: 11px; background: rgba(255,255,255,.14); padding: 2px 7px; border-radius: 5px; }
50
+ .oc-hint .key { font-size: 11px; background: rgba(255,255,255,.14); padding: 2px 7px; border-radius: 5px; }
54
51
 
55
- #oc-sidebar {
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;
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);
59
56
  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);
62
- }
63
- #oc-sidebar header {
64
- display: flex; align-items: center; gap: 9px;
65
- padding: 14px 14px; border-bottom: 1px solid rgba(255,255,255,.07);
66
- }
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;
73
- }
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; }
78
- button.primary {
79
- font: inherit; font-weight: 600; width: 100%;
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;
83
- transition: background .15s, transform .05s;
84
- }
85
- button.primary svg { width: 16px; height: 16px; }
86
- button.primary:hover:not(:disabled) { background: #3670f0; }
87
- button.primary:active:not(:disabled) { transform: translateY(1px); }
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
-
96
- .oc-card {
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);
99
- }
100
- .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
101
- .oc-desc {
102
- font: 11.5px ui-monospace, monospace; color: #aeb4c6;
103
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
104
- background: rgba(255,255,255,.06); padding: 3px 8px; border-radius: 6px; flex: 1;
105
57
  }
106
- .oc-card textarea {
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;
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;
109
69
  background: #14151b; color: #e6e8ee;
110
70
  }
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); }
113
- .oc-modes { display: flex; gap: 6px; margin-top: 8px; }
114
- .oc-mode {
115
- font: inherit; font-size: 12px; font-weight: 600; flex: 1;
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;
119
- }
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; }
120
76
  .oc-mode svg { width: 13px; height: 13px; }
121
77
  .oc-mode:hover { background: rgba(255,255,255,.09); color: #cfd3df; }
122
78
  .oc-mode.sel { background: rgba(76,141,255,.16); color: #7aa9ff; border-color: rgba(76,141,255,.4); }
123
79
 
124
- #oc-sidebar footer { padding: 12px 14px 14px; border-top: 1px solid rgba(255,255,255,.07); position: relative; }
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; }
125
109
  .oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 8px; color: #9298aa; }
126
110
  .oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #5b6070; flex: none; }
127
111
  .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
-
135
- .oc-toast {
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);
139
- transition: opacity .2s, transform .2s; pointer-events: none;
140
- box-shadow: 0 10px 30px rgba(0,0,0,.5); border: 1px solid rgba(255,255,255,.08);
141
- }
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); }
142
116
  .oc-toast.show { opacity: 1; transform: translateY(0); }
143
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; }
144
119
  `;
145
120
 
146
121
  let host = null;
147
122
  let root = null;
148
123
  let picking = false;
149
- let annotations = [];
124
+ let popupEl = null;
125
+ let pending = []; // annotations added to the sidebar list
126
+ let sidebarOpen = false;
150
127
  let statusTimer = null;
151
- let visible = false;
152
128
 
153
- // ---------- element metadata ----------
129
+ // ---------- metadata ----------
154
130
 
155
131
  function bestTestId(el) {
156
- for (const attr of ["data-testid", "data-test", "data-test-id", "data-cy", "data-qa"]) {
157
- 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);
158
134
  if (v) return v;
159
135
  }
160
136
  return undefined;
@@ -170,9 +146,9 @@
170
146
  if (node.classList && node.classList.length) {
171
147
  sel += "." + Array.from(node.classList).slice(0, 3).map((c) => CSS.escape(c)).join(".");
172
148
  }
173
- const parent = node.parentElement;
174
- if (parent) {
175
- 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);
176
152
  if (sibs.length > 1) sel += `:nth-of-type(${sibs.indexOf(node) + 1})`;
177
153
  }
178
154
  parts.unshift(sel);
@@ -188,7 +164,7 @@
188
164
  function elementMeta(el, inShadow) {
189
165
  const r = el.getBoundingClientRect();
190
166
  const classes = el.classList ? Array.from(el.classList) : [];
191
- const meta = {
167
+ const m = {
192
168
  selector: cssPath(el),
193
169
  tag: el.tagName,
194
170
  id: el.id || undefined,
@@ -205,61 +181,48 @@
205
181
  inIframe: window.top !== window.self,
206
182
  html: (el.outerHTML || "").slice(0, 800),
207
183
  };
208
- for (const k of Object.keys(meta)) if (meta[k] === undefined) delete meta[k];
209
- return meta;
184
+ for (const k of Object.keys(m)) if (m[k] === undefined) delete m[k];
185
+ return m;
210
186
  }
211
187
 
212
- function shortDescriptor(el) {
188
+ function descriptor(el) {
213
189
  const tag = (el.tag || "el").toLowerCase();
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
- : "";
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]}` : "";
223
191
  return `${tag}${id}`;
224
192
  }
225
193
 
226
- // ---------- picking (layer is always pointer-events:none; read the real
227
- // element from the event's composed path, which pierces shadow DOM) ----------
194
+ function escapeHtml(s) {
195
+ return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
196
+ }
197
+
198
+ // ---------- picking ----------
228
199
 
229
200
  function pickTarget(e) {
230
201
  const path = e.composedPath ? e.composedPath() : [e.target];
231
202
  for (const n of path) {
232
- if (n === host) return null; // our UI wrapper
203
+ if (n === host) return null;
233
204
  if (!(n instanceof Element)) continue;
234
- if (root && root.contains(n)) return null; // hovering our own UI
205
+ if (root && root.contains(n)) return null;
235
206
  const inShadow = n.getRootNode && n.getRootNode() instanceof ShadowRoot;
236
207
  return { el: n, inShadow };
237
208
  }
238
209
  return null;
239
210
  }
240
211
 
241
- function positionHighlight(el) {
242
- const box = root.getElementById("oc-highlight");
243
- if (!box) return;
244
- const r = el.getBoundingClientRect();
245
- Object.assign(box.style, {
246
- display: "block",
247
- left: `${r.left}px`,
248
- top: `${r.top}px`,
249
- width: `${r.width}px`,
250
- height: `${r.height}px`,
251
- });
212
+ function hl() {
213
+ return root.getElementById("oc-hl");
252
214
  }
253
215
 
254
216
  function onMove(e) {
255
217
  if (!picking) return;
256
218
  const hit = pickTarget(e);
219
+ const box = hl();
257
220
  if (!hit) {
258
- const box = root.getElementById("oc-highlight");
259
- if (box) box.style.display = "none";
221
+ box.style.display = "none";
260
222
  return;
261
223
  }
262
- positionHighlight(hit.el);
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` });
263
226
  }
264
227
 
265
228
  function onClick(e) {
@@ -269,94 +232,241 @@
269
232
  e.preventDefault();
270
233
  e.stopPropagation();
271
234
  e.stopImmediatePropagation();
235
+ const rect = hit.el.getBoundingClientRect();
272
236
  stopPicking();
273
- addCardForElement(elementMeta(hit.el, hit.inShadow));
237
+ openPopup(elementMeta(hit.el, hit.inShadow), rect);
274
238
  }
275
239
 
276
240
  function onKey(e) {
277
241
  if (e.key === "Escape") {
278
- if (picking) stopPicking();
279
- else toggle();
242
+ if (popupEl) closePopup();
243
+ else if (picking) stopPicking();
280
244
  }
281
245
  }
282
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
+
283
271
  function startPicking() {
272
+ ensureUI();
273
+ closePopup();
284
274
  picking = true;
285
275
  document.documentElement.style.cursor = "crosshair";
286
276
  root.getElementById("oc-hint").style.display = "flex";
287
- setPickBtn(true);
288
277
  }
289
278
 
290
279
  function stopPicking() {
291
280
  picking = false;
292
281
  document.documentElement.style.cursor = "";
293
- const box = root.getElementById("oc-highlight");
294
- if (box) box.style.display = "none";
295
- root.getElementById("oc-hint").style.display = "none";
296
- setPickBtn(false);
282
+ if (root) {
283
+ hl().style.display = "none";
284
+ root.getElementById("oc-hint").style.display = "none";
285
+ }
297
286
  }
298
287
 
299
- function setPickBtn(active) {
300
- const btn = root.getElementById("oc-pick");
301
- if (btn) {
302
- btn.innerHTML = `${ICON.target}<span>${active ? "Picking… (Esc)" : "Select element"}</span>`;
303
- 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;
304
294
  }
305
295
  }
306
296
 
307
- // ---------- 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
+ }
308
373
 
309
- function addCardForElement(element) {
310
- 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";
311
411
  renderCards();
312
- const areas = root.querySelectorAll(".oc-card textarea");
313
- const last = areas[areas.length - 1];
314
- if (last) last.focus();
412
+ refreshStatus();
413
+ if (!statusTimer) statusTimer = setInterval(refreshStatus, 15000);
315
414
  }
316
415
 
317
- function escapeHtml(s) {
318
- return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
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();
319
429
  }
320
430
 
321
431
  function renderCards() {
432
+ if (!root) return;
322
433
  const list = root.getElementById("oc-list");
434
+ const badge = root.getElementById("oc-badge");
435
+ if (badge) badge.textContent = String(pending.length);
323
436
  list.innerHTML = "";
324
- if (annotations.length === 0) {
325
- const empty = document.createElement("div");
326
- empty.className = "oc-empty";
327
- empty.textContent = "No annotations yet. Click “Select element”, then click something on the page.";
328
- 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);
329
442
  }
330
- annotations.forEach((a, i) => {
443
+ pending.forEach((a, i) => {
331
444
  const card = document.createElement("div");
332
445
  card.className = "oc-card";
446
+ const modeIcon = a.mode === "queue" ? ICON.layers : ICON.bolt;
447
+ const modeName = a.mode === "queue" ? "Queue" : "Act now";
333
448
  card.innerHTML = `
334
449
  <div class="oc-card-head">
335
- <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(shortDescriptor(a.element))}</span>
450
+ <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(descriptor(a.element))}</span>
336
451
  <button class="iconbtn oc-rm" data-i="${i}" title="Remove">${ICON.trash}</button>
337
452
  </div>
338
- <textarea rows="2" placeholder="Instruction for this element…" data-i="${i}">${escapeHtml(a.instruction)}</textarea>
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>
342
- </div>`;
453
+ <div class="oc-text">${escapeHtml(a.instruction || "(no instruction)")}</div>
454
+ <div class="oc-tag">${modeIcon}<span>${modeName}</span></div>`;
343
455
  list.appendChild(card);
344
456
  });
345
- updateSubmit();
346
- }
347
-
348
- function updateSubmit() {
349
457
  const btn = root.getElementById("oc-submit");
350
- btn.disabled = annotations.length === 0;
351
- btn.querySelector("span").textContent = annotations.length
352
- ? `Submit ${annotations.length} to agent`
353
- : "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
+ }
354
462
  }
355
463
 
356
- // ---------- status ----------
464
+ // ---------- status + submit ----------
357
465
 
358
466
  function refreshStatus() {
467
+ if (!root) return;
359
468
  const el = root.getElementById("oc-status");
469
+ if (!el) return;
360
470
  el.className = "oc-status checking";
361
471
  el.textContent = "Checking connection…";
362
472
  chrome.runtime.sendMessage({ type: "oc-status" }, (res) => {
@@ -381,141 +491,58 @@
381
491
  });
382
492
  }
383
493
 
384
- // ---------- submit ----------
385
-
386
- function collectInstructions() {
387
- root.querySelectorAll(".oc-card textarea").forEach((ta) => {
388
- const i = Number(ta.dataset.i);
389
- if (annotations[i]) annotations[i].instruction = ta.value.trim();
390
- });
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);
391
512
  }
392
513
 
393
- function submit() {
394
- collectInstructions();
395
- if (annotations.length === 0) return;
396
- const btn = root.getElementById("oc-submit");
397
- btn.disabled = true;
398
- setToast("Submitting…");
514
+ function submit(annotations, quick) {
515
+ if (!annotations.length) return;
516
+ toast("Submitting…");
399
517
  chrome.runtime.sendMessage({ type: "oc-submit", annotations }, (res) => {
400
518
  if (chrome.runtime.lastError || !res) {
401
- setToast("Extension error", true);
402
- updateSubmit();
519
+ toast("Extension error", true);
403
520
  return;
404
521
  }
405
522
  if (res.ok) {
406
523
  const parts = [];
407
524
  if (res.injected) parts.push(`${res.injected} sent`);
408
525
  if (res.queued) parts.push(`${res.queued} queued`);
409
- setToast(parts.length ? parts.join(", ") : "Submitted");
410
- annotations = [];
411
- renderCards();
526
+ toast(parts.length ? parts.join(", ") : "Submitted");
527
+ if (!quick) {
528
+ pending = [];
529
+ renderCards();
530
+ }
412
531
  refreshStatus();
413
532
  } else {
414
- setToast(`Failed: ${res.error || "unknown"}`, true);
415
- updateSubmit();
416
- }
417
- });
418
- }
419
-
420
- function setToast(text, bad) {
421
- const t = root.getElementById("oc-toast");
422
- t.textContent = text;
423
- t.className = "oc-toast show" + (bad ? " bad" : "");
424
- clearTimeout(t.__timer);
425
- t.__timer = setTimeout(() => (t.className = "oc-toast"), 4000);
426
- }
427
-
428
- // ---------- UI ----------
429
-
430
- function buildUI() {
431
- host = document.createElement("div");
432
- host.id = "oc-annotation-host";
433
- host.style.cssText = "all: initial;";
434
- root = host.attachShadow({ mode: "open" });
435
- root.innerHTML = `
436
- <style>${STYLE}</style>
437
- <div id="oc-layer"></div>
438
- <div id="oc-highlight"></div>
439
- <div id="oc-hint"><span>Hover an element and click to annotate</span><span class="key">Esc</span></div>
440
- <aside id="oc-sidebar">
441
- <header>
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>
445
- </header>
446
- <div class="oc-actions"><button id="oc-pick" class="primary">${ICON.target}<span>Select element</span></button></div>
447
- <div id="oc-list"></div>
448
- <footer>
449
- <div id="oc-status" class="oc-status">…</div>
450
- <button id="oc-submit" class="primary" disabled>${ICON.send}<span>Submit to agent</span></button>
451
- <div id="oc-toast" class="oc-toast"></div>
452
- </footer>
453
- </aside>`;
454
- document.documentElement.appendChild(host);
455
-
456
- root.getElementById("oc-pick").addEventListener("click", () => (picking ? stopPicking() : startPicking()));
457
- root.getElementById("oc-close").addEventListener("click", () => toggle());
458
- root.getElementById("oc-submit").addEventListener("click", submit);
459
- root.getElementById("oc-list").addEventListener("click", (e) => {
460
- const rm = e.target.closest(".oc-rm");
461
- if (rm) {
462
- annotations.splice(Number(rm.dataset.i), 1);
463
- renderCards();
464
- return;
465
- }
466
- const mode = e.target.closest(".oc-mode");
467
- if (mode) {
468
- annotations[Number(mode.dataset.i)].mode = mode.dataset.mode;
469
- renderCards();
533
+ toast(`Failed: ${res.error || "unknown"}`, true);
534
+ if (!quick) {
535
+ // keep the list so the user can retry
536
+ renderCards();
537
+ }
470
538
  }
471
539
  });
472
-
473
- renderCards();
474
- refreshStatus();
475
540
  }
476
541
 
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
- }
542
+ // ---------- messages ----------
482
543
 
483
- function open() {
484
- if (!host) buildUI();
485
- else host.style.display = "";
486
- pushPage(true);
487
- document.addEventListener("mousemove", onMove, true);
488
- document.addEventListener("click", onClick, true);
489
- document.addEventListener("keydown", onKey, true);
490
- if (!statusTimer) statusTimer = setInterval(refreshStatus, 15000);
491
- refreshStatus();
492
- }
493
-
494
- function close() {
495
- stopPicking();
496
- pushPage(false);
497
- document.removeEventListener("mousemove", onMove, true);
498
- document.removeEventListener("click", onClick, true);
499
- document.removeEventListener("keydown", onKey, true);
500
- if (host) host.style.display = "none";
501
- if (statusTimer) {
502
- clearInterval(statusTimer);
503
- statusTimer = null;
504
- }
505
- }
506
-
507
- function toggle() {
508
- visible = !visible;
509
- if (visible) open();
510
- else close();
511
- }
512
-
513
- window.addEventListener("oc-annotation-toggle", toggle);
514
544
  chrome.runtime.onMessage.addListener((msg) => {
515
- if (msg?.type === "oc-toggle") toggle();
545
+ if (msg?.type === "oc-pick") startPicking();
546
+ else if (msg?.type === "oc-toggle-sidebar") toggleSidebar();
516
547
  });
517
-
518
- // First injection opens immediately.
519
- visible = true;
520
- open();
521
548
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-browser-annotation-plugin",
3
- "version": "0.2.1",
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",