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 +12 -9
- package/extension/background.js +21 -19
- package/extension/manifest.json +10 -9
- package/extension/options.html +1 -1
- package/extension/overlay.js +374 -314
- 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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Annotation",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Alt+A to annotate
|
|
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": "
|
|
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
|
-
"
|
|
18
|
-
"suggested_key": {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
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
|
}
|
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,132 +1,136 @@
|
|
|
1
|
-
// Overlay content script
|
|
2
|
-
//
|
|
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
|
-
//
|
|
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
|
-
// 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
position: fixed;
|
|
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: #
|
|
33
|
-
background: rgba(20,
|
|
34
|
-
box-shadow: 0
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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-
|
|
96
|
-
.oc-
|
|
97
|
-
|
|
98
|
-
.oc-
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
.oc-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 {
|
|
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;
|
|
119
|
-
let root = null;
|
|
121
|
+
let host = null;
|
|
122
|
+
let root = null;
|
|
120
123
|
let picking = false;
|
|
121
|
-
let
|
|
122
|
-
let
|
|
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
|
-
// ----------
|
|
129
|
+
// ---------- metadata ----------
|
|
126
130
|
|
|
127
131
|
function bestTestId(el) {
|
|
128
|
-
for (const
|
|
129
|
-
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);
|
|
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
|
|
146
|
-
if (
|
|
147
|
-
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);
|
|
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
|
|
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(
|
|
181
|
-
return
|
|
184
|
+
for (const k of Object.keys(m)) if (m[k] === undefined) delete m[k];
|
|
185
|
+
return m;
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
function
|
|
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
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
// ---------- picking ----------
|
|
191
199
|
|
|
192
200
|
function pickTarget(e) {
|
|
193
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
return t instanceof Element ? { el: t, inShadow: false } : null;
|
|
209
|
+
return null;
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
function
|
|
207
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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 (
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 (
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
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);
|
|
291
442
|
}
|
|
292
|
-
|
|
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(
|
|
298
|
-
<
|
|
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
|
-
<
|
|
301
|
-
<div class="oc-
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function updateSubmit() {
|
|
315
457
|
const btn = root.getElementById("oc-submit");
|
|
316
|
-
btn
|
|
317
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
526
|
+
toast(parts.length ? parts.join(", ") : "Submitted");
|
|
527
|
+
if (!quick) {
|
|
528
|
+
pending = [];
|
|
529
|
+
renderCards();
|
|
530
|
+
}
|
|
377
531
|
refreshStatus();
|
|
378
532
|
} else {
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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",
|