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