opendevbrowser 0.0.12 → 0.0.15

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -28
  3. package/dist/chunk-JVBMT2O5.js +7173 -0
  4. package/dist/chunk-JVBMT2O5.js.map +1 -0
  5. package/dist/cli/index.js +2486 -589
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1057 -194
  8. package/dist/index.js.map +1 -1
  9. package/dist/opendevbrowser.js +1057 -194
  10. package/dist/opendevbrowser.js.map +1 -1
  11. package/extension/dist/annotate-content.css +237 -0
  12. package/extension/dist/annotate-content.js +934 -0
  13. package/extension/dist/background.js +1194 -32
  14. package/extension/dist/logging.js +50 -0
  15. package/extension/dist/ops/dom-bridge.js +355 -0
  16. package/extension/dist/ops/ops-runtime.js +1249 -0
  17. package/extension/dist/ops/ops-session-store.js +189 -0
  18. package/extension/dist/ops/redaction.js +52 -0
  19. package/extension/dist/ops/snapshot-builder.js +4 -0
  20. package/extension/dist/ops/snapshot-shared.js +220 -0
  21. package/extension/dist/popup.js +370 -25
  22. package/extension/dist/relay-settings.js +1 -0
  23. package/extension/dist/services/CDPRouter.js +501 -103
  24. package/extension/dist/services/ConnectionManager.js +464 -57
  25. package/extension/dist/services/NativePortManager.js +182 -0
  26. package/extension/dist/services/RelayClient.js +227 -26
  27. package/extension/dist/services/TabManager.js +81 -0
  28. package/extension/dist/services/TargetSessionMap.js +146 -0
  29. package/extension/dist/services/cdp-router-commands.js +203 -0
  30. package/extension/dist/services/url-restrictions.js +41 -0
  31. package/extension/dist/types.js +3 -1
  32. package/extension/manifest.json +17 -3
  33. package/extension/popup.html +144 -0
  34. package/package.json +2 -2
  35. package/skills/AGENTS.md +34 -62
  36. package/skills/data-extraction/SKILL.md +95 -103
  37. package/skills/form-testing/SKILL.md +75 -82
  38. package/skills/login-automation/SKILL.md +76 -66
  39. package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
  40. package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
  41. package/dist/chunk-WTFSMBVH.js +0 -2815
  42. package/dist/chunk-WTFSMBVH.js.map +0 -1
  43. package/extension/dist/popup.jsx +0 -150
@@ -0,0 +1,146 @@
1
+ export class TargetSessionMap {
2
+ tabTargets = new Map();
3
+ sessionsById = new Map();
4
+ sessionByTarget = new Map();
5
+ rootWaiters = new Map();
6
+ registerRootTab(tabId, targetInfo, sessionId) {
7
+ const record = { tabId, targetInfo, rootSessionId: sessionId };
8
+ this.tabTargets.set(tabId, record);
9
+ const session = {
10
+ kind: "root",
11
+ sessionId,
12
+ tabId,
13
+ targetId: targetInfo.targetId,
14
+ debuggerSession: { tabId },
15
+ targetInfo
16
+ };
17
+ this.sessionsById.set(sessionId, session);
18
+ this.sessionByTarget.set(targetInfo.targetId, sessionId);
19
+ this.resolveRootWaiters(tabId, session);
20
+ return session;
21
+ }
22
+ registerChildSession(tabId, targetInfo, sessionId) {
23
+ const session = {
24
+ kind: "child",
25
+ sessionId,
26
+ tabId,
27
+ targetId: targetInfo.targetId,
28
+ debuggerSession: { tabId, sessionId },
29
+ targetInfo
30
+ };
31
+ this.sessionsById.set(sessionId, session);
32
+ this.sessionByTarget.set(targetInfo.targetId, sessionId);
33
+ return session;
34
+ }
35
+ getBySessionId(sessionId) {
36
+ return this.sessionsById.get(sessionId) ?? null;
37
+ }
38
+ hasSession(sessionId) {
39
+ return this.sessionsById.has(sessionId);
40
+ }
41
+ getByTargetId(targetId) {
42
+ const sessionId = this.sessionByTarget.get(targetId);
43
+ if (!sessionId) {
44
+ return null;
45
+ }
46
+ return this.sessionsById.get(sessionId) ?? null;
47
+ }
48
+ getByTabId(tabId) {
49
+ return this.tabTargets.get(tabId) ?? null;
50
+ }
51
+ async waitForRootSession(tabId, timeoutMs = 2000) {
52
+ const existing = this.getByTabId(tabId);
53
+ if (existing) {
54
+ const session = this.sessionsById.get(existing.rootSessionId);
55
+ if (session) {
56
+ return session;
57
+ }
58
+ }
59
+ return await new Promise((resolve, reject) => {
60
+ const timeoutId = setTimeout(() => {
61
+ this.rejectRootWaiter(tabId, timeoutId);
62
+ reject(new Error("Target attach timeout"));
63
+ }, timeoutMs);
64
+ const entry = { resolve, reject, timeoutId };
65
+ const waiters = this.rootWaiters.get(tabId) ?? [];
66
+ waiters.push(entry);
67
+ this.rootWaiters.set(tabId, waiters);
68
+ });
69
+ }
70
+ listTargetInfos() {
71
+ const rootTargets = Array.from(this.tabTargets.values()).map((record) => record.targetInfo);
72
+ const childTargets = Array.from(this.sessionsById.values())
73
+ .filter((session) => session.kind === "child" && session.targetInfo)
74
+ .map((session) => session.targetInfo);
75
+ return [...rootTargets, ...childTargets];
76
+ }
77
+ listTabIds() {
78
+ return Array.from(this.tabTargets.keys());
79
+ }
80
+ listSessionIds() {
81
+ return Array.from(this.sessionsById.keys());
82
+ }
83
+ removeByTabId(tabId) {
84
+ const record = this.tabTargets.get(tabId) ?? null;
85
+ if (!record) {
86
+ return null;
87
+ }
88
+ for (const [sessionId, session] of this.sessionsById.entries()) {
89
+ if (session.tabId === tabId) {
90
+ this.sessionsById.delete(sessionId);
91
+ }
92
+ }
93
+ for (const [targetId, sessionId] of this.sessionByTarget.entries()) {
94
+ const session = this.sessionsById.get(sessionId);
95
+ if (!session || session.tabId === tabId) {
96
+ this.sessionByTarget.delete(targetId);
97
+ }
98
+ }
99
+ this.tabTargets.delete(tabId);
100
+ return record;
101
+ }
102
+ removeBySessionId(sessionId) {
103
+ const session = this.sessionsById.get(sessionId) ?? null;
104
+ if (!session) {
105
+ return null;
106
+ }
107
+ if (session.kind === "root") {
108
+ this.removeByTabId(session.tabId);
109
+ return session;
110
+ }
111
+ this.sessionsById.delete(sessionId);
112
+ this.sessionByTarget.delete(session.targetId);
113
+ return session;
114
+ }
115
+ removeByTargetId(targetId) {
116
+ const sessionId = this.sessionByTarget.get(targetId);
117
+ if (!sessionId) {
118
+ return null;
119
+ }
120
+ return this.removeBySessionId(sessionId);
121
+ }
122
+ resolveRootWaiters(tabId, session) {
123
+ const waiters = this.rootWaiters.get(tabId);
124
+ if (!waiters || waiters.length === 0) {
125
+ return;
126
+ }
127
+ this.rootWaiters.delete(tabId);
128
+ for (const waiter of waiters) {
129
+ clearTimeout(waiter.timeoutId);
130
+ waiter.resolve(session);
131
+ }
132
+ }
133
+ rejectRootWaiter(tabId, timeoutId) {
134
+ const waiters = this.rootWaiters.get(tabId);
135
+ if (!waiters || waiters.length === 0) {
136
+ return;
137
+ }
138
+ const remaining = waiters.filter((waiter) => waiter.timeoutId !== timeoutId);
139
+ if (remaining.length === 0) {
140
+ this.rootWaiters.delete(tabId);
141
+ }
142
+ else {
143
+ this.rootWaiters.set(tabId, remaining);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,203 @@
1
+ export async function handleSetDiscoverTargets(ctx, commandId, params) {
2
+ const discover = params.discover === true;
3
+ const shouldEmit = discover && !ctx.discoverTargets;
4
+ ctx.setDiscoverTargets(discover);
5
+ if (shouldEmit) {
6
+ for (const targetInfo of ctx.sessions.listTargetInfos()) {
7
+ ctx.emitTargetCreated(targetInfo);
8
+ }
9
+ }
10
+ ctx.respond(commandId, {});
11
+ }
12
+ export async function handleSetAutoAttach(ctx, commandId, params, sessionId) {
13
+ if (params.flatten === false) {
14
+ ctx.respondError(commandId, ctx.flatSessionError, sessionId);
15
+ return;
16
+ }
17
+ if (sessionId) {
18
+ ctx.respond(commandId, {}, sessionId);
19
+ return;
20
+ }
21
+ const autoAttach = params.autoAttach === true;
22
+ const waitForDebuggerOnStart = params.waitForDebuggerOnStart === true;
23
+ ctx.setAutoAttachOptions({ autoAttach, waitForDebuggerOnStart, flatten: true, filter: params.filter });
24
+ if (autoAttach && !sessionId) {
25
+ ctx.resetRootAttached();
26
+ }
27
+ try {
28
+ for (const debuggee of ctx.debuggees.values()) {
29
+ await ctx.applyAutoAttach(debuggee);
30
+ }
31
+ }
32
+ catch (error) {
33
+ ctx.respondError(commandId, getErrorMessage(error));
34
+ return;
35
+ }
36
+ if (!autoAttach) {
37
+ ctx.emitRootDetached();
38
+ }
39
+ else {
40
+ for (const targetInfo of ctx.sessions.listTargetInfos()) {
41
+ ctx.emitRootAttached(targetInfo);
42
+ }
43
+ }
44
+ ctx.respond(commandId, {});
45
+ }
46
+ export async function handleCreateTarget(ctx, commandId, params) {
47
+ const url = typeof params.url === "string" ? params.url : undefined;
48
+ const background = params.background === true;
49
+ let createdTabId = null;
50
+ try {
51
+ const tab = await ctx.tabManager.createTab(url, !background);
52
+ if (typeof tab.id !== "number") {
53
+ throw new Error("Target.createTarget did not yield a tab id");
54
+ }
55
+ createdTabId = tab.id;
56
+ await ctx.tabManager.waitForTabComplete(tab.id);
57
+ await ctx.attach(tab.id);
58
+ await ctx.sessions.waitForRootSession(tab.id);
59
+ await ctx.sendCommand({ tabId: tab.id }, "Target.getTargets", {});
60
+ const targetInfo = await ctx.registerRootTab(tab.id);
61
+ if (ctx.discoverTargets) {
62
+ ctx.emitTargetCreated(targetInfo);
63
+ }
64
+ if (ctx.autoAttachOptions.autoAttach) {
65
+ ctx.emitRootAttached(targetInfo);
66
+ }
67
+ if (!background) {
68
+ ctx.updatePrimaryTab(tab.id);
69
+ }
70
+ ctx.respond(commandId, { targetId: targetInfo.targetId });
71
+ }
72
+ catch (error) {
73
+ if (createdTabId !== null) {
74
+ const debuggee = ctx.debuggees.get(createdTabId) ?? null;
75
+ ctx.detachTabState(createdTabId);
76
+ if (debuggee) {
77
+ await ctx.safeDetach(debuggee);
78
+ }
79
+ try {
80
+ await ctx.tabManager.closeTab(createdTabId);
81
+ }
82
+ catch {
83
+ // Best-effort cleanup for partially created targets.
84
+ }
85
+ }
86
+ ctx.respondError(commandId, getErrorMessage(error));
87
+ }
88
+ }
89
+ export async function handleCloseTarget(ctx, commandId, params) {
90
+ const targetId = typeof params.targetId === "string" ? params.targetId : null;
91
+ if (!targetId) {
92
+ ctx.respondError(commandId, "Missing targetId");
93
+ return;
94
+ }
95
+ const session = ctx.sessions.getByTargetId(targetId);
96
+ if (!session || session.kind !== "root") {
97
+ ctx.respondError(commandId, "Target not found");
98
+ return;
99
+ }
100
+ try {
101
+ const debuggee = ctx.debuggees.get(session.tabId) ?? null;
102
+ ctx.detachTabState(session.tabId);
103
+ if (debuggee) {
104
+ await ctx.safeDetach(debuggee);
105
+ }
106
+ await ctx.tabManager.closeTab(session.tabId);
107
+ ctx.respond(commandId, { success: true });
108
+ }
109
+ catch (error) {
110
+ ctx.respondError(commandId, getErrorMessage(error));
111
+ }
112
+ }
113
+ export async function handleActivateTarget(ctx, commandId, params) {
114
+ const targetId = typeof params.targetId === "string" ? params.targetId : null;
115
+ if (!targetId) {
116
+ ctx.respondError(commandId, "Missing targetId");
117
+ return;
118
+ }
119
+ const session = ctx.sessions.getByTargetId(targetId);
120
+ if (!session || session.kind !== "root") {
121
+ ctx.respondError(commandId, "Target not found");
122
+ return;
123
+ }
124
+ try {
125
+ await ctx.tabManager.activateTab(session.tabId);
126
+ ctx.updatePrimaryTab(session.tabId);
127
+ ctx.respond(commandId, {});
128
+ }
129
+ catch (error) {
130
+ ctx.respondError(commandId, getErrorMessage(error));
131
+ }
132
+ }
133
+ export async function handleAttachToTarget(ctx, commandId, params, sessionId) {
134
+ const targetId = typeof params.targetId === "string" ? params.targetId : null;
135
+ if (!targetId) {
136
+ ctx.respondError(commandId, "Missing targetId", sessionId);
137
+ return;
138
+ }
139
+ if (params.flatten === false) {
140
+ ctx.respondError(commandId, ctx.flatSessionError, sessionId);
141
+ return;
142
+ }
143
+ const targetSession = ctx.sessions.getByTargetId(targetId);
144
+ if (targetSession && targetSession.kind === "root") {
145
+ ctx.respond(commandId, { sessionId: targetSession.sessionId }, sessionId);
146
+ return;
147
+ }
148
+ const session = sessionId ? ctx.sessions.getBySessionId(sessionId) : null;
149
+ if (sessionId && !session) {
150
+ ctx.respondError(commandId, `Unknown sessionId: ${sessionId}`, sessionId);
151
+ return;
152
+ }
153
+ const debuggee = session?.debuggerSession ?? ctx.getPrimaryDebuggee();
154
+ if (!debuggee) {
155
+ ctx.respondError(commandId, "No tab attached", sessionId);
156
+ return;
157
+ }
158
+ try {
159
+ const result = await ctx.sendCommand(debuggee, "Target.attachToTarget", { targetId, flatten: true });
160
+ const record = isRecord(result) ? result : {};
161
+ const childSessionId = typeof record.sessionId === "string" ? record.sessionId : null;
162
+ if (childSessionId) {
163
+ const targetInfo = {
164
+ targetId,
165
+ type: "page",
166
+ browserContextId: "default"
167
+ };
168
+ ctx.sessions.registerChildSession(debuggee.tabId, targetInfo, childSessionId);
169
+ }
170
+ ctx.respond(commandId, result);
171
+ }
172
+ catch (error) {
173
+ ctx.respondError(commandId, getErrorMessage(error), sessionId);
174
+ }
175
+ }
176
+ export async function handleRoutedCommand(ctx, commandId, method, params, sessionId) {
177
+ const session = sessionId ? ctx.sessions.getBySessionId(sessionId) : null;
178
+ if (sessionId && !session) {
179
+ ctx.respondError(commandId, `Unknown sessionId: ${sessionId}`, sessionId);
180
+ return;
181
+ }
182
+ const debuggee = session?.debuggerSession ?? ctx.getPrimaryDebuggee();
183
+ if (!debuggee) {
184
+ ctx.respondError(commandId, "No tab attached", sessionId);
185
+ return;
186
+ }
187
+ try {
188
+ const result = await ctx.sendCommand(debuggee, method, params);
189
+ ctx.respond(commandId, result, sessionId);
190
+ }
191
+ catch (error) {
192
+ ctx.respondError(commandId, getErrorMessage(error), sessionId);
193
+ }
194
+ }
195
+ const isRecord = (value) => {
196
+ return typeof value === "object" && value !== null;
197
+ };
198
+ const getErrorMessage = (error) => {
199
+ if (error instanceof Error) {
200
+ return error.message;
201
+ }
202
+ return "Unknown error";
203
+ };
@@ -0,0 +1,41 @@
1
+ const RESTRICTED_PROTOCOLS = new Set([
2
+ "chrome:",
3
+ "chrome-extension:",
4
+ "chrome-search:",
5
+ "chrome-untrusted:",
6
+ "devtools:",
7
+ "chrome-devtools:",
8
+ "edge:",
9
+ "brave:"
10
+ ]);
11
+ const isWebStoreUrl = (url) => {
12
+ if (url.hostname === "chromewebstore.google.com") {
13
+ return true;
14
+ }
15
+ if (url.hostname === "chrome.google.com" && url.pathname.startsWith("/webstore")) {
16
+ return true;
17
+ }
18
+ return false;
19
+ };
20
+ export const getRestrictionMessage = (url) => {
21
+ if (RESTRICTED_PROTOCOLS.has(url.protocol)) {
22
+ return "Active tab uses a restricted URL scheme. Focus a normal http(s) tab and retry.";
23
+ }
24
+ if (isWebStoreUrl(url)) {
25
+ return "Chrome Web Store tabs cannot be debugged. Open a normal tab and retry.";
26
+ }
27
+ return null;
28
+ };
29
+ export const isRestrictedUrl = (rawUrl) => {
30
+ try {
31
+ const url = new URL(rawUrl);
32
+ const message = getRestrictionMessage(url);
33
+ if (message) {
34
+ return { restricted: true, message };
35
+ }
36
+ return { restricted: false };
37
+ }
38
+ catch {
39
+ return { restricted: true, message: "Unable to parse tab URL." };
40
+ }
41
+ };
@@ -1 +1,3 @@
1
- export {};
1
+ export const OPS_PROTOCOL_VERSION = "1";
2
+ export const MAX_OPS_PAYLOAD_BYTES = 12 * 1024 * 1024;
3
+ export const MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenDevBrowser Relay",
4
- "version": "0.0.12",
4
+ "version": "0.0.15",
5
5
  "description": "Optional bridge to reuse existing Chrome tabs with OpenDevBrowser.",
6
6
  "permissions": [
7
7
  "debugger",
8
+ "alarms",
8
9
  "tabs",
9
- "storage"
10
+ "storage",
11
+ "scripting",
12
+ "activeTab",
13
+ "nativeMessaging"
10
14
  ],
11
15
  "host_permissions": [
12
16
  "http://127.0.0.1/*",
13
- "http://localhost/*"
17
+ "http://localhost/*",
18
+ "<all_urls>"
14
19
  ],
15
20
  "icons": {
16
21
  "16": "icons/icon16.png",
@@ -30,5 +35,14 @@
30
35
  "background": {
31
36
  "service_worker": "dist/background.js",
32
37
  "type": "module"
38
+ },
39
+ "commands": {
40
+ "toggle-annotation": {
41
+ "suggested_key": {
42
+ "default": "Ctrl+Shift+P",
43
+ "mac": "Command+Shift+P"
44
+ },
45
+ "description": "Toggle annotation UI"
46
+ }
33
47
  }
34
48
  }
@@ -285,12 +285,97 @@
285
285
  transform: translateY(0);
286
286
  }
287
287
 
288
+ .secondary {
289
+ width: 100%;
290
+ padding: 9px 10px;
291
+ border-radius: 12px;
292
+ border: 1px solid var(--stroke);
293
+ background: var(--panel-strong);
294
+ color: var(--text);
295
+ font-size: 11px;
296
+ font-weight: 600;
297
+ letter-spacing: 0.02em;
298
+ cursor: pointer;
299
+ transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
300
+ }
301
+
302
+ .secondary:hover {
303
+ transform: translateY(-1px);
304
+ border-color: rgba(32, 213, 198, 0.4);
305
+ box-shadow: 0 10px 22px rgba(32, 213, 198, 0.12);
306
+ }
307
+
308
+ .secondary:active {
309
+ transform: translateY(0);
310
+ }
311
+
312
+ .secondary:disabled {
313
+ opacity: 0.6;
314
+ cursor: not-allowed;
315
+ box-shadow: none;
316
+ transform: none;
317
+ }
318
+
288
319
  .field-note {
289
320
  font-size: 10px;
290
321
  color: var(--muted);
291
322
  margin-top: 6px;
292
323
  }
293
324
 
325
+ .panel-title {
326
+ font-size: 10px;
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.24em;
329
+ color: var(--muted);
330
+ }
331
+
332
+ .health-grid {
333
+ display: grid;
334
+ grid-template-columns: repeat(2, minmax(0, 1fr));
335
+ gap: 8px;
336
+ }
337
+
338
+ .health-item {
339
+ padding: 8px 10px;
340
+ border-radius: 10px;
341
+ border: 1px solid var(--stroke);
342
+ background: var(--panel-strong);
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 4px;
346
+ }
347
+
348
+ .health-label {
349
+ font-size: 9px;
350
+ text-transform: uppercase;
351
+ letter-spacing: 0.2em;
352
+ color: var(--muted);
353
+ }
354
+
355
+ .health-value {
356
+ font-size: 12px;
357
+ font-weight: 600;
358
+ color: var(--text);
359
+ }
360
+
361
+ .health-value[data-tone="ok"] {
362
+ color: var(--accent);
363
+ }
364
+
365
+ .health-value[data-tone="warn"] {
366
+ color: #f6c56f;
367
+ }
368
+
369
+ .health-value[data-tone="off"] {
370
+ color: var(--muted);
371
+ }
372
+
373
+ .button-row {
374
+ display: grid;
375
+ grid-template-columns: repeat(2, minmax(0, 1fr));
376
+ gap: 10px;
377
+ }
378
+
294
379
  @media (prefers-reduced-motion: reduce) {
295
380
  * {
296
381
  transition: none !important;
@@ -332,6 +417,17 @@
332
417
  </label>
333
418
  </div>
334
419
 
420
+ <div class="row">
421
+ <div>
422
+ <div class="row-title">Native fallback (experimental)</div>
423
+ <div class="row-subtitle">Use native messaging host when relay is down</div>
424
+ </div>
425
+ <label class="switch">
426
+ <input id="nativeEnabled" type="checkbox" />
427
+ <span class="slider"></span>
428
+ </label>
429
+ </div>
430
+
335
431
  <div class="row">
336
432
  <div>
337
433
  <div class="row-title">Auto-pair</div>
@@ -361,6 +457,54 @@
361
457
  </div>
362
458
  </section>
363
459
 
460
+ <section class="panel">
461
+ <div class="panel-title">Diagnostics</div>
462
+ <div class="health-grid">
463
+ <div class="health-item">
464
+ <div class="health-label">Relay</div>
465
+ <div id="healthRelay" class="health-value" data-tone="off">Unknown</div>
466
+ </div>
467
+ <div class="health-item">
468
+ <div class="health-label">Handshake</div>
469
+ <div id="healthHandshake" class="health-value" data-tone="off">Unknown</div>
470
+ </div>
471
+ <div class="health-item">
472
+ <div class="health-label">Annotate</div>
473
+ <div id="healthAnnotation" class="health-value" data-tone="off">Unknown</div>
474
+ </div>
475
+ <div class="health-item">
476
+ <div class="health-label">Injected</div>
477
+ <div id="healthInjected" class="health-value" data-tone="off">Unknown</div>
478
+ </div>
479
+ <div class="health-item">
480
+ <div class="health-label">CDP</div>
481
+ <div id="healthCdp" class="health-value" data-tone="off">Unknown</div>
482
+ </div>
483
+ <div class="health-item">
484
+ <div class="health-label">Pairing</div>
485
+ <div id="healthPairing" class="health-value" data-tone="off">Unknown</div>
486
+ </div>
487
+ <div class="health-item">
488
+ <div class="health-label">Native</div>
489
+ <div id="healthNative" class="health-value" data-tone="off">Unknown</div>
490
+ </div>
491
+ </div>
492
+ <div id="healthNote" class="field-note">Health check pending.</div>
493
+ </section>
494
+
495
+ <section class="panel">
496
+ <div class="panel-title">Annotation</div>
497
+ <div class="field">
498
+ <label for="annotationContext">Request</label>
499
+ <input id="annotationContext" type="text" placeholder="Optional context for this review" />
500
+ <div id="annotationNote" class="field-note">No annotations captured yet.</div>
501
+ </div>
502
+ <div class="button-row">
503
+ <button id="annotationStart" class="secondary">Annotate</button>
504
+ <button id="annotationCopy" class="secondary" disabled>Copy payload</button>
505
+ </div>
506
+ </section>
507
+
364
508
  <button id="toggle" class="primary">Connect</button>
365
509
  </div>
366
510
  <script type="module" src="dist/popup.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opendevbrowser",
3
- "version": "0.0.12",
3
+ "version": "0.0.15",
4
4
  "description": "OpenCode plugin for browser automation via CDP with snapshot-refs-actions workflow",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "lint": "eslint \"{src,tests}/**/*.ts\"",
42
42
  "test": "vitest run --coverage",
43
43
  "extension:sync": "node scripts/sync-extension-version.mjs",
44
- "extension:build": "npm run extension:sync && tsc -p extension/tsconfig.json",
44
+ "extension:build": "npm run extension:sync && tsc -p extension/tsconfig.json && node scripts/copy-extension-assets.mjs",
45
45
  "extension:pack": "cd extension && zip -r ../opendevbrowser-extension.zip manifest.json popup.html dist/ icons/",
46
46
  "version:check": "node scripts/verify-versions.mjs",
47
47
  "prepack": "npm run version:check && npm run build && npm run extension:build"