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 +12 -9
- package/extension/background.js +21 -19
- package/extension/manifest.json +12 -18
- package/extension/options.html +1 -1
- package/extension/overlay.js +326 -299
- package/package.json +1 -1
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
|
|
77
|
-
|
|
78
|
-
3. In the
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
package/extension/background.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Background service worker.
|
|
2
|
-
// -
|
|
3
|
-
// -
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
43
|
+
void sendToOverlay(tab, "oc-toggle-sidebar");
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
package/extension/manifest.json
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Annotation",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Alt+A to annotate
|
|
6
|
-
"permissions": [
|
|
7
|
-
|
|
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": "
|
|
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
|
-
"
|
|
25
|
-
"suggested_key": {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
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
|
}
|
package/extension/options.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span id="saved"></span>
|
|
38
38
|
|
|
39
39
|
<div class="hint">
|
|
40
|
-
<p><strong>
|
|
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>
|
package/extension/overlay.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
// Overlay content script
|
|
2
|
-
//
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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="
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
position: fixed; top: 18px; left:
|
|
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,
|
|
51
|
-
box-shadow: 0 8px 24px rgba(0,0,0,.
|
|
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
|
-
|
|
50
|
+
.oc-hint .key { font-size: 11px; background: rgba(255,255,255,.14); padding: 2px 7px; border-radius: 5px; }
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
107
|
-
|
|
108
|
-
|
|
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-
|
|
112
|
-
.oc-
|
|
113
|
-
|
|
114
|
-
.oc-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
130
|
-
.oc-
|
|
131
|
-
.oc-
|
|
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
|
|
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
|
-
// ----------
|
|
129
|
+
// ---------- metadata ----------
|
|
154
130
|
|
|
155
131
|
function bestTestId(el) {
|
|
156
|
-
for (const
|
|
157
|
-
const v = el.getAttribute && el.getAttribute(
|
|
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
|
|
174
|
-
if (
|
|
175
|
-
const sibs = Array.from(
|
|
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
|
|
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(
|
|
209
|
-
return
|
|
184
|
+
for (const k of Object.keys(m)) if (m[k] === undefined) delete m[k];
|
|
185
|
+
return m;
|
|
210
186
|
}
|
|
211
187
|
|
|
212
|
-
function
|
|
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
|
-
|
|
227
|
-
|
|
194
|
+
function escapeHtml(s) {
|
|
195
|
+
return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[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;
|
|
203
|
+
if (n === host) return null;
|
|
233
204
|
if (!(n instanceof Element)) continue;
|
|
234
|
-
if (root && root.contains(n)) return null;
|
|
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
|
|
242
|
-
|
|
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
|
-
|
|
259
|
-
if (box) box.style.display = "none";
|
|
221
|
+
box.style.display = "none";
|
|
260
222
|
return;
|
|
261
223
|
}
|
|
262
|
-
|
|
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
|
-
|
|
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 (
|
|
279
|
-
else
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
282
|
+
if (root) {
|
|
283
|
+
hl().style.display = "none";
|
|
284
|
+
root.getElementById("oc-hint").style.display = "none";
|
|
285
|
+
}
|
|
297
286
|
}
|
|
298
287
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
if (last) last.focus();
|
|
412
|
+
refreshStatus();
|
|
413
|
+
if (!statusTimer) statusTimer = setInterval(refreshStatus, 15000);
|
|
315
414
|
}
|
|
316
415
|
|
|
317
|
-
function
|
|
318
|
-
|
|
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 (
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
list.appendChild(
|
|
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
|
-
|
|
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(
|
|
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
|
-
<
|
|
339
|
-
<div class="oc-
|
|
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
|
|
351
|
-
|
|
352
|
-
? `Submit ${
|
|
353
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
526
|
+
toast(parts.length ? parts.join(", ") : "Submitted");
|
|
527
|
+
if (!quick) {
|
|
528
|
+
pending = [];
|
|
529
|
+
renderCards();
|
|
530
|
+
}
|
|
412
531
|
refreshStatus();
|
|
413
532
|
} else {
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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",
|