opendevbrowser 0.0.11 → 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -28
  3. package/dist/chunk-JVBMT2O5.js +7173 -0
  4. package/dist/chunk-JVBMT2O5.js.map +1 -0
  5. package/dist/cli/index.js +3690 -275
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1080 -2857
  8. package/dist/index.js.map +1 -1
  9. package/dist/opendevbrowser.js +1080 -2857
  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 +1291 -8
  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 +398 -21
  22. package/extension/dist/relay-settings.js +3 -1
  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/icons/icon128.png +0 -0
  33. package/extension/icons/icon16.png +0 -0
  34. package/extension/icons/icon32.png +0 -0
  35. package/extension/icons/icon48.png +0 -0
  36. package/extension/manifest.json +17 -3
  37. package/extension/popup.html +469 -65
  38. package/package.json +2 -2
  39. package/skills/AGENTS.md +34 -61
  40. package/skills/data-extraction/SKILL.md +95 -103
  41. package/skills/form-testing/SKILL.md +75 -82
  42. package/skills/login-automation/SKILL.md +76 -66
  43. package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
  44. package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
  45. package/dist/chunk-R5VUZEUU.js +0 -128
  46. package/dist/chunk-R5VUZEUU.js.map +0 -1
  47. package/extension/dist/popup.jsx +0 -150
@@ -1,32 +1,1315 @@
1
1
  import { ConnectionManager } from "./services/ConnectionManager.js";
2
+ import { NativePortManager } from "./services/NativePortManager.js";
3
+ import { DEFAULT_AUTO_CONNECT, DEFAULT_AUTO_PAIR, DEFAULT_DISCOVERY_PORT, DEFAULT_NATIVE_ENABLED, DEFAULT_PAIRING_ENABLED, DEFAULT_RELAY_PORT } from "./relay-settings.js";
4
+ import { logError } from "./logging.js";
5
+ import { OpsRuntime } from "./ops/ops-runtime.js";
2
6
  const connection = new ConnectionManager();
3
- chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
7
+ const opsRuntime = new OpsRuntime({
8
+ send: (message) => connection.sendOpsMessage(message),
9
+ cdp: connection.getCdpRouter()
10
+ });
11
+ const nativePort = new NativePortManager({
12
+ onMessage: (payload) => {
13
+ handleNativePortMessage(payload).catch((error) => {
14
+ logError("native_port.message", error, { code: "native_message_failed" });
15
+ });
16
+ },
17
+ onDisconnect: () => {
18
+ updateBadge(getEffectiveStatus());
19
+ }
20
+ });
21
+ let autoConnectInFlight = false;
22
+ let statusNoteOverride = null;
23
+ let retryScheduled = false;
24
+ let retryDelayMs = 5000;
25
+ let nativeEnabled = DEFAULT_NATIVE_ENABLED;
26
+ const RETRY_ALARM_NAME = "opendevbrowser-auto-connect";
27
+ const RETRY_MAX_MS = 60_000;
28
+ const ANNOTATION_CONTENT_SCRIPT = "dist/annotate-content.js";
29
+ const ANNOTATION_CONTENT_STYLE = "dist/annotate-content.css";
30
+ const ANNOTATION_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
31
+ const ANNOTATION_REQUEST_TIMEOUT_MS = 120_000;
32
+ const LAST_ANNOTATION_META_KEY = "annotationLastMeta";
33
+ const LAST_ANNOTATION_PAYLOAD_KEY = "annotationLastPayloadSansScreenshots";
34
+ const annotationSessions = new Map();
35
+ let lastAnnotationFull = null;
36
+ connection.onAnnotationCommand((command) => {
37
+ handleRelayAnnotationCommand(command).catch((error) => {
38
+ logError("annotation.relay_command", error, { code: "annotation_command_failed" });
39
+ });
40
+ });
41
+ connection.onOpsMessage((message) => {
42
+ opsRuntime.handleMessage(message);
43
+ });
44
+ const RESTRICTED_PROTOCOLS = new Set([
45
+ "chrome:",
46
+ "chrome-extension:",
47
+ "chrome-search:",
48
+ "chrome-untrusted:",
49
+ "devtools:",
50
+ "chrome-devtools:",
51
+ "edge:",
52
+ "brave:"
53
+ ]);
54
+ const updateBadge = (status) => {
55
+ const isConnected = status === "connected";
56
+ chrome.action.setBadgeText({ text: isConnected ? "ON" : "OFF" });
57
+ chrome.action.setBadgeBackgroundColor({
58
+ color: isConnected ? "#20d5c6" : "#5b667a"
59
+ });
60
+ };
61
+ const getEffectiveStatus = () => {
62
+ if (connection.getStatus() === "connected") {
63
+ return "connected";
64
+ }
65
+ if (nativeEnabled && nativePort.isConnected()) {
66
+ return "connected";
67
+ }
68
+ return "disconnected";
69
+ };
70
+ const buildStatusMessage = async () => {
71
+ const error = connection.getLastError();
72
+ const relayStatus = connection.getStatus();
73
+ const status = getEffectiveStatus();
74
+ let note = error?.message;
75
+ let relayHealth = null;
76
+ const isNativeEnabled = nativeEnabled;
77
+ let nativeHealth = isNativeEnabled ? nativePort.getHealth() : null;
78
+ if (isNativeEnabled && nativePort.isConnected()) {
79
+ try {
80
+ await nativePort.ping(1000);
81
+ }
82
+ catch (error) {
83
+ logError("native_port.ping", error, { code: "native_ping_failed" });
84
+ }
85
+ nativeHealth = nativePort.getHealth();
86
+ }
87
+ if (!error) {
88
+ if (relayStatus === "connected") {
89
+ const identity = connection.getRelayIdentity();
90
+ if (identity.relayPort && identity.instanceId) {
91
+ note = `Connected to 127.0.0.1:${identity.relayPort} (relay ${identity.instanceId.slice(0, 8)})`;
92
+ }
93
+ else if (identity.relayPort) {
94
+ note = `Connected to 127.0.0.1:${identity.relayPort}`;
95
+ }
96
+ relayHealth = await connection.relayHealthCheck();
97
+ }
98
+ else if (nativeHealth?.status === "connected") {
99
+ note = "Connected via native host.";
100
+ }
101
+ else {
102
+ const stored = await new Promise((resolve) => {
103
+ chrome.storage.local.get(["relayPort"], (items) => resolve(items));
104
+ });
105
+ const port = parsePort(stored.relayPort) ?? DEFAULT_RELAY_PORT;
106
+ relayHealth = await fetchRelayHealth(port);
107
+ note = statusNoteOverride ?? buildRelayHealthNote(relayHealth);
108
+ if (!statusNoteOverride && isNativeEnabled && nativeHealth?.status === "error") {
109
+ note = buildNativeHealthNote(nativeHealth);
110
+ }
111
+ }
112
+ }
113
+ if (!error) {
114
+ const relayNotice = connection.getRelayNotice();
115
+ if (relayNotice) {
116
+ note = relayNotice;
117
+ }
118
+ }
119
+ return {
120
+ type: "status",
121
+ status,
122
+ note,
123
+ relayHealth,
124
+ nativeHealth,
125
+ nativeEnabled: isNativeEnabled
126
+ };
127
+ };
128
+ const setStorage = (items) => {
129
+ return new Promise((resolve) => {
130
+ chrome.storage.local.set(items, () => resolve());
131
+ });
132
+ };
133
+ const setStatusNoteOverride = (note) => {
134
+ statusNoteOverride = note;
135
+ };
136
+ const buildRelayHealthNote = (health) => {
137
+ if (!health) {
138
+ return "Relay unreachable. Start the daemon and retry.";
139
+ }
140
+ switch (health.reason) {
141
+ case "pairing_invalid":
142
+ return "Pairing token mismatch. Update the token and reconnect.";
143
+ case "pairing_required":
144
+ return "Pairing required. Enable auto-pair or set the token.";
145
+ case "handshake_incomplete":
146
+ return "Extension handshake pending. Keep the relay running and retry.";
147
+ case "extension_disconnected":
148
+ return "Extension not connected to relay. Click Connect.";
149
+ case "annotation_disconnected":
150
+ return "Annotation channel disconnected. Keep the extension open and retry.";
151
+ case "ops_disconnected":
152
+ return "Ops channel disconnected. Start a new session and retry.";
153
+ case "cdp_disconnected":
154
+ return "No CDP clients connected. Start a session and retry.";
155
+ case "relay_down":
156
+ return "Relay down. Start the daemon and retry.";
157
+ default:
158
+ return "Local relay only. Tokens stay on-device.";
159
+ }
160
+ };
161
+ const buildNativeHealthNote = (health) => {
162
+ if (health.status === "connected") {
163
+ return "Native host connected.";
164
+ }
165
+ switch (health.error) {
166
+ case "host_not_installed":
167
+ return "Native host not installed. Run `opendevbrowser native install <extension-id>`.";
168
+ case "host_forbidden":
169
+ return "Native host forbidden. Verify the extension ID matches the manifest.";
170
+ case "host_disconnect":
171
+ return "Native host disconnected. Restart the host.";
172
+ case "host_timeout":
173
+ return "Native host ping timed out.";
174
+ case "host_message_too_large":
175
+ return "Native host rejected message size.";
176
+ default:
177
+ return "Native host unavailable.";
178
+ }
179
+ };
180
+ const clearRetry = () => {
181
+ retryScheduled = false;
182
+ retryDelayMs = 5000;
183
+ if (chrome.alarms?.clear) {
184
+ chrome.alarms.clear(RETRY_ALARM_NAME);
185
+ }
186
+ };
187
+ const scheduleRetry = () => {
188
+ if (retryScheduled) {
189
+ return;
190
+ }
191
+ retryScheduled = true;
192
+ const delayMs = retryDelayMs;
193
+ retryDelayMs = Math.min(retryDelayMs * 2, RETRY_MAX_MS);
194
+ if (chrome.alarms?.create) {
195
+ chrome.alarms.create(RETRY_ALARM_NAME, { when: Date.now() + delayMs });
196
+ return;
197
+ }
198
+ setTimeout(() => {
199
+ retryScheduled = false;
200
+ autoConnect().catch((error) => {
201
+ logError("auto_connect.retry", error, { code: "auto_connect_failed" });
202
+ });
203
+ }, delayMs);
204
+ };
205
+ const attemptNativeConnect = async () => {
206
+ if (!nativeEnabled) {
207
+ return false;
208
+ }
209
+ const connected = await nativePort.connect();
210
+ if (!connected) {
211
+ return false;
212
+ }
213
+ try {
214
+ await nativePort.ping(1500);
215
+ }
216
+ catch (error) {
217
+ logError("native_port.ping", error, { code: "native_ping_failed" });
218
+ }
219
+ updateBadge(getEffectiveStatus());
220
+ return nativePort.isConnected();
221
+ };
222
+ const parsePort = (value) => {
223
+ if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
224
+ return value;
225
+ }
226
+ if (typeof value === "string" && value.trim()) {
227
+ const parsed = Number(value);
228
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
229
+ return parsed;
230
+ }
231
+ }
232
+ return null;
233
+ };
234
+ const parseEpoch = (value) => {
235
+ if (typeof value === "number" && Number.isFinite(value)) {
236
+ return value;
237
+ }
238
+ return null;
239
+ };
240
+ const isWebStoreUrl = (url) => {
241
+ if (url.hostname === "chromewebstore.google.com") {
242
+ return true;
243
+ }
244
+ if (url.hostname === "chrome.google.com" && url.pathname.startsWith("/webstore")) {
245
+ return true;
246
+ }
247
+ return false;
248
+ };
249
+ const getRestrictionMessage = (url) => {
250
+ if (RESTRICTED_PROTOCOLS.has(url.protocol)) {
251
+ return "Active tab uses a restricted URL scheme. Open a normal http(s) page and retry.";
252
+ }
253
+ if (isWebStoreUrl(url)) {
254
+ return "Chrome Web Store tabs cannot be annotated. Open a normal tab and retry.";
255
+ }
256
+ return null;
257
+ };
258
+ const fetchRelayConfig = async (port) => {
259
+ try {
260
+ const response = await fetch(`http://127.0.0.1:${port}/config`, {
261
+ method: "GET",
262
+ headers: { "Accept": "application/json" }
263
+ });
264
+ if (!response.ok) {
265
+ return null;
266
+ }
267
+ const data = await response.json();
268
+ const relayPort = parsePort(data.relayPort);
269
+ if (!relayPort) {
270
+ return null;
271
+ }
272
+ const pairingRequired = typeof data.pairingRequired === "boolean" ? data.pairingRequired : true;
273
+ const instanceId = typeof data.instanceId === "string" ? data.instanceId : null;
274
+ const epoch = parseEpoch(data.epoch);
275
+ return { relayPort, pairingRequired, instanceId, epoch };
276
+ }
277
+ catch (error) {
278
+ logError("relay.config_fetch", error, { code: "relay_config_fetch_failed", extra: { port } });
279
+ return null;
280
+ }
281
+ };
282
+ const fetchRelayHealth = async (port) => {
283
+ try {
284
+ const response = await fetch(`http://127.0.0.1:${port}/status`, {
285
+ method: "GET",
286
+ headers: { "Accept": "application/json" }
287
+ });
288
+ if (!response.ok) {
289
+ return null;
290
+ }
291
+ const data = await response.json();
292
+ if (data.health && typeof data.health === "object") {
293
+ return data.health;
294
+ }
295
+ const extensionConnected = data.extensionConnected === true;
296
+ const handshake = data.extensionHandshakeComplete === true;
297
+ const cdpConnected = data.cdpConnected === true;
298
+ const annotationConnected = data.annotationConnected === true;
299
+ const opsConnected = data.opsConnected === true;
300
+ const pairingRequired = data.pairingRequired === true;
301
+ const ok = extensionConnected && handshake;
302
+ return {
303
+ ok,
304
+ reason: ok ? "ok" : (extensionConnected ? "handshake_incomplete" : "extension_disconnected"),
305
+ extensionConnected,
306
+ extensionHandshakeComplete: handshake,
307
+ cdpConnected,
308
+ annotationConnected,
309
+ opsConnected,
310
+ pairingRequired
311
+ };
312
+ }
313
+ catch (error) {
314
+ logError("relay.health_fetch", error, { code: "relay_health_fetch_failed", extra: { port } });
315
+ return null;
316
+ }
317
+ };
318
+ const sendAnnotationResponse = (payload, transport = "relay") => {
319
+ if (transport === "popup") {
320
+ return;
321
+ }
322
+ const response = { type: "annotationResponse", payload };
323
+ if (transport === "native") {
324
+ nativePort.send(response);
325
+ return;
326
+ }
327
+ connection.sendAnnotationResponse(response);
328
+ };
329
+ const sendAnnotationEvent = (payload, transport = "relay") => {
330
+ if (transport === "popup") {
331
+ return;
332
+ }
333
+ const event = { type: "annotationEvent", payload };
334
+ if (transport === "native") {
335
+ nativePort.send(event);
336
+ return;
337
+ }
338
+ connection.sendAnnotationEvent(event);
339
+ };
340
+ const startAnnotationTimeout = (requestId, transport) => {
341
+ return setTimeout(() => {
342
+ const session = annotationSessions.get(requestId);
343
+ if (!session)
344
+ return;
345
+ annotationSessions.delete(requestId);
346
+ const response = {
347
+ version: 1,
348
+ requestId,
349
+ status: "error",
350
+ error: { code: "timeout", message: "Annotation request timed out." }
351
+ };
352
+ const meta = buildLastAnnotationMeta(requestId, response, false);
353
+ lastAnnotationFull = null;
354
+ persistLastAnnotation(meta, null).catch((error) => {
355
+ logError("annotation.persist_timeout_meta", error, { code: "annotation_persist_failed" });
356
+ });
357
+ sendAnnotationResponse(response, transport);
358
+ }, ANNOTATION_REQUEST_TIMEOUT_MS);
359
+ };
360
+ const getTab = async (tabId) => {
361
+ try {
362
+ return await chrome.tabs.get(tabId);
363
+ }
364
+ catch (error) {
365
+ logError("tabs.get", error, { code: "tab_lookup_failed", extra: { tabId } });
366
+ return null;
367
+ }
368
+ };
369
+ const createTab = async (url) => {
370
+ return await new Promise((resolve, reject) => {
371
+ chrome.tabs.create({ url, active: true }, (tab) => {
372
+ const lastError = chrome.runtime.lastError;
373
+ if (lastError) {
374
+ reject(new Error(lastError.message));
375
+ return;
376
+ }
377
+ if (!tab) {
378
+ reject(new Error("Tab creation failed"));
379
+ return;
380
+ }
381
+ resolve(tab);
382
+ });
383
+ });
384
+ };
385
+ const updateTabUrl = async (tabId, url) => {
386
+ return await new Promise((resolve, reject) => {
387
+ chrome.tabs.update(tabId, { url, active: true }, (tab) => {
388
+ const lastError = chrome.runtime.lastError;
389
+ if (lastError) {
390
+ reject(new Error(lastError.message));
391
+ return;
392
+ }
393
+ if (!tab) {
394
+ reject(new Error("Tab update failed"));
395
+ return;
396
+ }
397
+ resolve(tab);
398
+ });
399
+ });
400
+ };
401
+ const waitForTabComplete = async (tabId, timeoutMs = 10000) => {
402
+ const tab = await getTab(tabId);
403
+ if (tab?.status === "complete")
404
+ return;
405
+ await new Promise((resolve, reject) => {
406
+ let settled = false;
407
+ const timeout = setTimeout(() => {
408
+ if (settled)
409
+ return;
410
+ settled = true;
411
+ chrome.tabs.onUpdated.removeListener(listener);
412
+ reject(new Error("Tab load timeout"));
413
+ }, timeoutMs);
414
+ const listener = (updatedId, changeInfo) => {
415
+ if (updatedId !== tabId)
416
+ return;
417
+ if (changeInfo.status === "complete") {
418
+ if (settled)
419
+ return;
420
+ settled = true;
421
+ clearTimeout(timeout);
422
+ chrome.tabs.onUpdated.removeListener(listener);
423
+ resolve();
424
+ }
425
+ };
426
+ chrome.tabs.onUpdated.addListener(listener);
427
+ });
428
+ };
429
+ const getActiveTab = async () => {
430
+ const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
431
+ return tabs[0] ?? null;
432
+ };
433
+ const resolveAnnotationTab = async (command) => {
434
+ if (typeof command.tabId === "number") {
435
+ if (command.url) {
436
+ const updated = await updateTabUrl(command.tabId, command.url);
437
+ await waitForTabComplete(command.tabId);
438
+ return updated;
439
+ }
440
+ const existing = await getTab(command.tabId);
441
+ if (!existing) {
442
+ throw new Error("Target tab unavailable");
443
+ }
444
+ return existing;
445
+ }
446
+ if (command.url) {
447
+ const created = await createTab(command.url);
448
+ if (typeof created.id === "number") {
449
+ await waitForTabComplete(created.id);
450
+ }
451
+ return created;
452
+ }
453
+ const active = await getActiveTab();
454
+ if (!active) {
455
+ throw new Error("No active tab available");
456
+ }
457
+ return active;
458
+ };
459
+ const isRestrictedTab = (tab) => {
460
+ const rawUrl = tab.url ?? tab.pendingUrl ?? "";
461
+ if (!rawUrl)
462
+ return "Active tab URL unavailable.";
463
+ let parsed = null;
464
+ try {
465
+ parsed = new URL(rawUrl);
466
+ }
467
+ catch (error) {
468
+ logError("annotation.parse_tab_url", error, { code: "tab_url_parse_failed" });
469
+ return "Active tab URL is invalid.";
470
+ }
471
+ return getRestrictionMessage(parsed);
472
+ };
473
+ const injectAnnotationAssets = async (tabId) => {
474
+ await new Promise((resolve, reject) => {
475
+ chrome.scripting.insertCSS({ target: { tabId }, files: [ANNOTATION_CONTENT_STYLE] }, () => {
476
+ const lastError = chrome.runtime.lastError;
477
+ if (lastError) {
478
+ reject(new Error(lastError.message));
479
+ return;
480
+ }
481
+ resolve();
482
+ });
483
+ });
484
+ await new Promise((resolve, reject) => {
485
+ chrome.scripting.executeScript({ target: { tabId }, files: [ANNOTATION_CONTENT_SCRIPT] }, () => {
486
+ const lastError = chrome.runtime.lastError;
487
+ if (lastError) {
488
+ reject(new Error(lastError.message));
489
+ return;
490
+ }
491
+ resolve();
492
+ });
493
+ });
494
+ };
495
+ const sendMessageToTab = async (tabId, message) => {
496
+ return await new Promise((resolve, reject) => {
497
+ chrome.tabs.sendMessage(tabId, message, (response) => {
498
+ const lastError = chrome.runtime.lastError;
499
+ if (lastError) {
500
+ reject(new Error(lastError.message));
501
+ return;
502
+ }
503
+ resolve(response);
504
+ });
505
+ });
506
+ };
507
+ const isMissingAnnotationReceiverError = (error) => {
508
+ if (!(error instanceof Error)) {
509
+ return false;
510
+ }
511
+ return error.message.includes("Receiving end does not exist");
512
+ };
513
+ const sleep = async (ms) => {
514
+ await new Promise((resolve) => setTimeout(resolve, ms));
515
+ };
516
+ const pingAnnotation = async (tabId) => {
517
+ const response = await sendMessageToTab(tabId, { type: "annotation:ping" });
518
+ const ok = typeof response === "object" && response !== null && response.ok === true;
519
+ if (!ok) {
520
+ throw new Error("Annotation ping failed");
521
+ }
522
+ };
523
+ const ensureAnnotationInjected = async (tabId) => {
524
+ try {
525
+ await pingAnnotation(tabId);
526
+ return;
527
+ }
528
+ catch (error) {
529
+ // Initial ping can fail before content script injection; avoid noisy logs for missing receivers.
530
+ if (!isMissingAnnotationReceiverError(error)) {
531
+ logError("annotation.ping", error, { code: "annotation_ping_failed", extra: { tabId } });
532
+ }
533
+ }
534
+ const backoff = [150, 400];
535
+ let lastError = null;
536
+ for (let attempt = 0; attempt <= backoff.length; attempt += 1) {
537
+ try {
538
+ await injectAnnotationAssets(tabId);
539
+ await pingAnnotation(tabId);
540
+ return;
541
+ }
542
+ catch (error) {
543
+ lastError = error;
544
+ logError("annotation.inject", error, { code: "annotation_injection_failed", extra: { tabId, attempt } });
545
+ if (attempt < backoff.length) {
546
+ const delay = backoff[attempt] ?? 0;
547
+ await sleep(delay);
548
+ }
549
+ }
550
+ }
551
+ if (lastError instanceof Error) {
552
+ throw lastError;
553
+ }
554
+ throw new Error("Annotation injection failed");
555
+ };
556
+ const probeAnnotationInjected = async () => {
557
+ const active = await getActiveTab();
558
+ if (!active || typeof active.id !== "number") {
559
+ return { injected: false, detail: "No active tab available." };
560
+ }
561
+ const restricted = isRestrictedTab(active);
562
+ if (restricted) {
563
+ return { injected: false, detail: restricted };
564
+ }
565
+ try {
566
+ await pingAnnotation(active.id);
567
+ return { injected: true };
568
+ }
569
+ catch (error) {
570
+ const message = error instanceof Error ? error.message : "Annotation not injected.";
571
+ return { injected: false, detail: message };
572
+ }
573
+ };
574
+ const toggleAnnotationUi = async () => {
575
+ const tab = await getActiveTab();
576
+ if (!tab || typeof tab.id !== "number") {
577
+ return;
578
+ }
579
+ const restricted = isRestrictedTab(tab);
580
+ if (restricted) {
581
+ return;
582
+ }
583
+ await ensureAnnotationInjected(tab.id);
584
+ await sendMessageToTab(tab.id, { type: "annotation:toggle" });
585
+ };
586
+ const startAnnotationSession = async (command, transport) => {
587
+ const requestId = command.requestId;
588
+ if (annotationSessions.has(requestId)) {
589
+ sendAnnotationResponse({
590
+ version: 1,
591
+ requestId,
592
+ status: "error",
593
+ error: { code: "invalid_request", message: "Duplicate annotation requestId." }
594
+ }, transport);
595
+ return;
596
+ }
597
+ const tab = await resolveAnnotationTab(command);
598
+ const restricted = isRestrictedTab(tab);
599
+ if (restricted) {
600
+ sendAnnotationResponse({
601
+ version: 1,
602
+ requestId,
603
+ status: "error",
604
+ error: { code: "restricted_url", message: restricted }
605
+ }, transport);
606
+ return;
607
+ }
608
+ if (typeof tab.id !== "number") {
609
+ sendAnnotationResponse({
610
+ version: 1,
611
+ requestId,
612
+ status: "error",
613
+ error: { code: "invalid_request", message: "Target tab missing id." }
614
+ }, transport);
615
+ return;
616
+ }
617
+ let timeoutId = null;
618
+ try {
619
+ await ensureAnnotationInjected(tab.id);
620
+ timeoutId = startAnnotationTimeout(requestId, transport);
621
+ annotationSessions.set(requestId, {
622
+ requestId,
623
+ tabId: tab.id,
624
+ options: command.options,
625
+ createdAt: Date.now(),
626
+ timeoutId,
627
+ transport
628
+ });
629
+ await sendMessageToTab(tab.id, {
630
+ type: "annotation:start",
631
+ requestId,
632
+ options: command.options ?? {},
633
+ url: command.url
634
+ });
635
+ }
636
+ catch (error) {
637
+ if (timeoutId !== null) {
638
+ clearTimeout(timeoutId);
639
+ }
640
+ annotationSessions.delete(requestId);
641
+ throw error instanceof Error ? error : new Error("Annotation injection failed");
642
+ }
643
+ };
644
+ const cancelAnnotationSession = async (requestId, transport) => {
645
+ const session = annotationSessions.get(requestId);
646
+ if (!session) {
647
+ sendAnnotationResponse({
648
+ version: 1,
649
+ requestId,
650
+ status: "cancelled",
651
+ error: { code: "cancelled", message: "Annotation session not active." }
652
+ }, transport);
653
+ return;
654
+ }
655
+ clearTimeout(session.timeoutId);
656
+ annotationSessions.delete(requestId);
657
+ await sendMessageToTab(session.tabId, { type: "annotation:cancel", requestId });
658
+ sendAnnotationResponse({
659
+ version: 1,
660
+ requestId,
661
+ status: "cancelled",
662
+ error: { code: "cancelled", message: "Annotation cancelled." }
663
+ }, session.transport);
664
+ };
665
+ const validatePayloadSize = (payload) => {
666
+ const size = new TextEncoder().encode(JSON.stringify(payload)).length;
667
+ return size <= ANNOTATION_MAX_PAYLOAD_BYTES;
668
+ };
669
+ const generateAnnotationRequestId = () => {
670
+ return crypto.randomUUID();
671
+ };
672
+ const stripScreenshots = (payload) => {
673
+ const { screenshots, annotations, ...rest } = payload;
674
+ void screenshots;
675
+ return {
676
+ ...rest,
677
+ annotations: annotations.map((item) => {
678
+ const { screenshotId, ...restItem } = item;
679
+ void screenshotId;
680
+ return restItem;
681
+ })
682
+ };
683
+ };
684
+ const buildLastAnnotationMeta = (requestId, response, hasFullPayloadInMemory) => {
685
+ const payload = response.payload;
686
+ const annotationCount = payload ? payload.annotations.length : undefined;
687
+ const screenshotCount = payload?.screenshots?.length ?? 0;
688
+ return {
689
+ requestId,
690
+ status: response.status,
691
+ error: response.error,
692
+ url: payload?.url,
693
+ title: payload?.title,
694
+ timestamp: payload?.timestamp,
695
+ annotationCount,
696
+ screenshotCount: payload ? screenshotCount : undefined,
697
+ screenshotMode: payload?.screenshotMode,
698
+ storedAt: Date.now(),
699
+ hasScreenshots: screenshotCount > 0,
700
+ hasFullPayloadInMemory
701
+ };
702
+ };
703
+ const persistLastAnnotation = async (meta, payload) => {
704
+ await setStorage({
705
+ [LAST_ANNOTATION_META_KEY]: meta,
706
+ [LAST_ANNOTATION_PAYLOAD_KEY]: payload
707
+ });
708
+ };
709
+ const loadPersistedLastAnnotation = async () => {
710
+ const data = await new Promise((resolve) => {
711
+ chrome.storage.local.get([LAST_ANNOTATION_META_KEY, LAST_ANNOTATION_PAYLOAD_KEY], (items) => resolve(items));
712
+ });
713
+ const metaRecord = data[LAST_ANNOTATION_META_KEY];
714
+ const payloadRecord = data[LAST_ANNOTATION_PAYLOAD_KEY];
715
+ const meta = metaRecord && typeof metaRecord === "object" ? metaRecord : null;
716
+ const payload = payloadRecord && typeof payloadRecord === "object" ? payloadRecord : null;
717
+ return { meta, payload };
718
+ };
719
+ async function handleNativePortMessage(payload) {
720
+ if (!payload || typeof payload !== "object") {
721
+ return;
722
+ }
723
+ const record = payload;
724
+ if (record.type === "annotationCommand") {
725
+ await handleNativeAnnotationCommand(record);
726
+ }
727
+ }
728
+ const handleRelayAnnotationCommand = async (command, transport = "relay") => {
729
+ const payload = command.payload;
730
+ if (!payload || payload.version !== 1 || typeof payload.requestId !== "string") {
731
+ sendAnnotationResponse({
732
+ version: 1,
733
+ requestId: payload?.requestId ?? "unknown",
734
+ status: "error",
735
+ error: { code: "invalid_request", message: "Invalid annotation command." }
736
+ }, transport);
737
+ return;
738
+ }
739
+ if (payload.command === "cancel") {
740
+ await cancelAnnotationSession(payload.requestId, transport);
741
+ return;
742
+ }
743
+ try {
744
+ await startAnnotationSession(payload, transport);
745
+ sendAnnotationEvent({
746
+ version: 1,
747
+ requestId: payload.requestId,
748
+ event: "ready",
749
+ message: "Annotation session started."
750
+ }, transport);
751
+ }
752
+ catch (error) {
753
+ const detail = error instanceof Error ? error.message : "Annotation start failed.";
754
+ sendAnnotationResponse({
755
+ version: 1,
756
+ requestId: payload.requestId,
757
+ status: "error",
758
+ error: { code: "injection_failed", message: detail }
759
+ }, transport);
760
+ }
761
+ };
762
+ const handleNativeAnnotationCommand = async (command) => {
763
+ await handleRelayAnnotationCommand(command, "native");
764
+ };
765
+ const finalizeAnnotationSession = (requestId) => {
766
+ const session = annotationSessions.get(requestId);
767
+ if (!session)
768
+ return null;
769
+ clearTimeout(session.timeoutId);
770
+ annotationSessions.delete(requestId);
771
+ return session;
772
+ };
773
+ const handleAnnotationComplete = (requestId, payload) => {
774
+ const session = finalizeAnnotationSession(requestId);
775
+ if (!session) {
776
+ return;
777
+ }
778
+ if (!validatePayloadSize(payload)) {
779
+ const response = {
780
+ version: 1,
781
+ requestId,
782
+ status: "error",
783
+ error: { code: "payload_too_large", message: "Annotation payload exceeded size limits." }
784
+ };
785
+ const meta = buildLastAnnotationMeta(requestId, response, false);
786
+ lastAnnotationFull = null;
787
+ persistLastAnnotation(meta, null).catch((error) => {
788
+ logError("annotation.persist_payload_too_large", error, { code: "annotation_persist_failed" });
789
+ });
790
+ sendAnnotationResponse(response, session.transport);
791
+ return;
792
+ }
793
+ const response = {
794
+ version: 1,
795
+ requestId,
796
+ status: "ok",
797
+ payload
798
+ };
799
+ const meta = buildLastAnnotationMeta(requestId, response, true);
800
+ lastAnnotationFull = { meta, payload };
801
+ const storageMeta = { ...meta, hasFullPayloadInMemory: false };
802
+ const sanitizedPayload = stripScreenshots(payload);
803
+ persistLastAnnotation(storageMeta, sanitizedPayload).catch((error) => {
804
+ logError("annotation.persist_sanitized_payload", error, { code: "annotation_persist_failed" });
805
+ });
806
+ sendAnnotationResponse(response, session.transport);
807
+ };
808
+ const handleAnnotationError = (requestId, error) => {
809
+ const session = finalizeAnnotationSession(requestId);
810
+ if (!session)
811
+ return;
812
+ const response = {
813
+ version: 1,
814
+ requestId,
815
+ status: "error",
816
+ error
817
+ };
818
+ const meta = buildLastAnnotationMeta(requestId, response, false);
819
+ lastAnnotationFull = null;
820
+ persistLastAnnotation(meta, null).catch((error) => {
821
+ logError("annotation.persist_error_meta", error, { code: "annotation_persist_failed" });
822
+ });
823
+ sendAnnotationResponse(response, session.transport);
824
+ };
825
+ const handleAnnotationCancelled = (requestId) => {
826
+ const session = finalizeAnnotationSession(requestId);
827
+ if (!session)
828
+ return;
829
+ const response = {
830
+ version: 1,
831
+ requestId,
832
+ status: "cancelled",
833
+ error: { code: "cancelled", message: "Annotation cancelled." }
834
+ };
835
+ const meta = buildLastAnnotationMeta(requestId, response, false);
836
+ lastAnnotationFull = null;
837
+ persistLastAnnotation(meta, null).catch((error) => {
838
+ logError("annotation.persist_cancel_meta", error, { code: "annotation_persist_failed" });
839
+ });
840
+ sendAnnotationResponse(response, session.transport);
841
+ };
842
+ const captureVisibleTab = async (tab) => {
843
+ return await new Promise((resolve, reject) => {
844
+ chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" }, (dataUrl) => {
845
+ const lastError = chrome.runtime.lastError;
846
+ if (lastError) {
847
+ reject(new Error(lastError.message));
848
+ return;
849
+ }
850
+ if (!dataUrl) {
851
+ reject(new Error("Capture failed"));
852
+ return;
853
+ }
854
+ resolve(dataUrl);
855
+ });
856
+ });
857
+ };
858
+ const fetchTokenFromPlugin = async (port) => {
859
+ try {
860
+ const response = await fetch(`http://127.0.0.1:${port}/pair`, {
861
+ method: "GET",
862
+ headers: { "Accept": "application/json" }
863
+ });
864
+ if (!response.ok) {
865
+ return null;
866
+ }
867
+ const data = await response.json();
868
+ if (typeof data.token !== "string") {
869
+ return null;
870
+ }
871
+ return {
872
+ token: data.token,
873
+ instanceId: typeof data.instanceId === "string" ? data.instanceId : null,
874
+ epoch: parseEpoch(data.epoch)
875
+ };
876
+ }
877
+ catch (error) {
878
+ logError("relay.token_fetch", error, { code: "relay_pair_fetch_failed", extra: { port } });
879
+ return null;
880
+ }
881
+ };
882
+ const clearStoredRelayState = async () => {
883
+ await setStorage({
884
+ relayPort: null,
885
+ relayInstanceId: null,
886
+ relayEpoch: null,
887
+ pairingToken: null,
888
+ tokenEpoch: null
889
+ });
890
+ };
891
+ const attemptAutoConnect = async () => {
892
+ const data = await new Promise((resolve) => {
893
+ chrome.storage.local.get([
894
+ "autoConnect",
895
+ "autoPair",
896
+ "pairingEnabled",
897
+ "pairingToken",
898
+ "relayPort",
899
+ "relayInstanceId",
900
+ "relayEpoch",
901
+ "tokenEpoch",
902
+ "nativeEnabled"
903
+ ], (items) => {
904
+ resolve(items);
905
+ });
906
+ });
907
+ const autoConnect = typeof data.autoConnect === "boolean" ? data.autoConnect : DEFAULT_AUTO_CONNECT;
908
+ if (!autoConnect || connection.getStatus() === "connected") {
909
+ clearRetry();
910
+ return;
911
+ }
912
+ const autoPair = typeof data.autoPair === "boolean" ? data.autoPair : DEFAULT_AUTO_PAIR;
913
+ const pairingEnabled = typeof data.pairingEnabled === "boolean" ? data.pairingEnabled : DEFAULT_PAIRING_ENABLED;
914
+ const storedRelayPort = parsePort(data.relayPort) ?? DEFAULT_RELAY_PORT;
915
+ let storedPairingToken = typeof data.pairingToken === "string" ? data.pairingToken : null;
916
+ nativeEnabled = typeof data.nativeEnabled === "boolean" ? data.nativeEnabled : DEFAULT_NATIVE_ENABLED;
917
+ if (typeof data.nativeEnabled !== "boolean") {
918
+ await setStorage({ nativeEnabled });
919
+ }
920
+ if (autoPair && pairingEnabled) {
921
+ let config = await fetchRelayConfig(DEFAULT_DISCOVERY_PORT);
922
+ if (!config && storedRelayPort !== DEFAULT_DISCOVERY_PORT) {
923
+ config = await fetchRelayConfig(storedRelayPort);
924
+ }
925
+ const storedRelayEpoch = parseEpoch(data.relayEpoch);
926
+ const storedRelayInstanceId = typeof data.relayInstanceId === "string" ? data.relayInstanceId : null;
927
+ const storedTokenEpoch = parseEpoch(data.tokenEpoch);
928
+ if (!config) {
929
+ setStatusNoteOverride("Relay config unreachable. Start the daemon and retry.");
930
+ scheduleRetry();
931
+ return;
932
+ }
933
+ const relayPort = config.relayPort ?? storedRelayPort;
934
+ const configEpoch = config.epoch ?? null;
935
+ const hasEpoch = config.epoch !== null;
936
+ if (config.relayPort) {
937
+ await setStorage({
938
+ relayPort: config.relayPort,
939
+ relayInstanceId: config.instanceId,
940
+ relayEpoch: config.epoch
941
+ });
942
+ }
943
+ if (hasEpoch && storedRelayEpoch !== null && storedRelayEpoch !== configEpoch) {
944
+ await clearStoredRelayState();
945
+ storedPairingToken = null;
946
+ setStatusNoteOverride("Relay restarted. Refresh the connection.");
947
+ }
948
+ if (config.instanceId && storedRelayInstanceId && config.instanceId !== storedRelayInstanceId) {
949
+ await clearStoredRelayState();
950
+ storedPairingToken = null;
951
+ setStatusNoteOverride("Relay instance mismatch. Open the popup and click Connect.");
952
+ }
953
+ if (hasEpoch && storedTokenEpoch !== null && storedTokenEpoch !== configEpoch) {
954
+ await setStorage({ pairingToken: null, tokenEpoch: null });
955
+ storedPairingToken = null;
956
+ }
957
+ if (hasEpoch && storedTokenEpoch === null && storedPairingToken) {
958
+ await setStorage({ pairingToken: null, tokenEpoch: null });
959
+ storedPairingToken = null;
960
+ }
961
+ const pairingRequired = config.pairingRequired ?? true;
962
+ if (pairingRequired) {
963
+ if (!storedPairingToken) {
964
+ const fetched = await fetchTokenFromPlugin(relayPort);
965
+ if (!fetched) {
966
+ setStatusNoteOverride("Auto-pair failed. Start the daemon and retry.");
967
+ scheduleRetry();
968
+ return;
969
+ }
970
+ if (config.instanceId && fetched.instanceId && config.instanceId !== fetched.instanceId) {
971
+ console.warn("[opendevbrowser] Relay instance mismatch during auto-pair. Retrying later.");
972
+ setStatusNoteOverride("Relay instance mismatch. Open the popup and click Connect.");
973
+ return;
974
+ }
975
+ const tokenEpoch = fetched.epoch ?? configEpoch;
976
+ await setStorage({ pairingToken: fetched.token, tokenEpoch });
977
+ }
978
+ }
979
+ }
980
+ await connection.connect();
981
+ if (connection.getStatus() !== "connected") {
982
+ const nativeConnected = await attemptNativeConnect();
983
+ if (nativeConnected) {
984
+ setStatusNoteOverride("Connected via native host.");
985
+ clearRetry();
986
+ return;
987
+ }
988
+ if (nativeEnabled) {
989
+ setStatusNoteOverride(buildNativeHealthNote(nativePort.getHealth()));
990
+ }
991
+ scheduleRetry();
992
+ return;
993
+ }
994
+ nativePort.disconnect();
995
+ setStatusNoteOverride(null);
996
+ clearRetry();
997
+ };
998
+ const autoConnect = async () => {
999
+ if (autoConnectInFlight) {
1000
+ return;
1001
+ }
1002
+ autoConnectInFlight = true;
1003
+ try {
1004
+ await attemptAutoConnect();
1005
+ }
1006
+ catch (error) {
1007
+ logError("auto_connect.attempt", error, { code: "auto_connect_failed" });
1008
+ connection.disconnect();
1009
+ nativePort.disconnect();
1010
+ }
1011
+ finally {
1012
+ autoConnectInFlight = false;
1013
+ }
1014
+ };
1015
+ connection.onStatus((status) => {
1016
+ const effectiveStatus = status === "connected" ? "connected" : (nativeEnabled && nativePort.isConnected()) ? "connected" : "disconnected";
1017
+ updateBadge(effectiveStatus);
1018
+ if (status === "connected") {
1019
+ nativePort.disconnect();
1020
+ setStatusNoteOverride(null);
1021
+ clearRetry();
1022
+ }
1023
+ if (status === "disconnected" && !nativePort.isConnected()) {
1024
+ for (const session of annotationSessions.values()) {
1025
+ clearTimeout(session.timeoutId);
1026
+ }
1027
+ annotationSessions.clear();
1028
+ }
1029
+ });
1030
+ updateBadge(getEffectiveStatus());
1031
+ chrome.runtime.onStartup.addListener(() => {
1032
+ autoConnect().catch((error) => {
1033
+ logError("auto_connect.startup", error, { code: "auto_connect_failed" });
1034
+ });
1035
+ });
1036
+ chrome.runtime.onInstalled.addListener(() => {
1037
+ autoConnect().catch((error) => {
1038
+ logError("auto_connect.installed", error, { code: "auto_connect_failed" });
1039
+ });
1040
+ });
1041
+ if (chrome.alarms?.onAlarm) {
1042
+ chrome.alarms.onAlarm.addListener((alarm) => {
1043
+ if (alarm.name === RETRY_ALARM_NAME) {
1044
+ retryScheduled = false;
1045
+ autoConnect().catch((error) => {
1046
+ logError("auto_connect.alarm", error, { code: "auto_connect_failed" });
1047
+ });
1048
+ }
1049
+ });
1050
+ }
1051
+ if (chrome.commands?.onCommand) {
1052
+ chrome.commands.onCommand.addListener((command) => {
1053
+ if (command === "toggle-annotation") {
1054
+ toggleAnnotationUi().catch((error) => {
1055
+ logError("annotation.toggle", error, { code: "annotation_toggle_failed" });
1056
+ });
1057
+ }
1058
+ });
1059
+ }
1060
+ autoConnect().catch((error) => {
1061
+ logError("auto_connect.startup", error, { code: "auto_connect_failed" });
1062
+ });
1063
+ chrome.storage.onChanged.addListener((changes, area) => {
1064
+ if (area !== "local") {
1065
+ return;
1066
+ }
1067
+ if (changes.nativeEnabled) {
1068
+ nativeEnabled = changes.nativeEnabled.newValue === true;
1069
+ if (!nativeEnabled) {
1070
+ nativePort.disconnect();
1071
+ }
1072
+ updateBadge(getEffectiveStatus());
1073
+ }
1074
+ if (changes.autoConnect?.newValue === true) {
1075
+ autoConnect().catch((error) => {
1076
+ logError("auto_connect.setting", error, { code: "auto_connect_failed" });
1077
+ });
1078
+ }
1079
+ if (changes.pairingToken) {
1080
+ autoConnect().catch((error) => {
1081
+ logError("auto_connect.pairing_token", error, { code: "auto_connect_failed" });
1082
+ });
1083
+ }
1084
+ });
1085
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
4
1086
  const respond = (status) => {
5
1087
  sendResponse(status);
6
1088
  };
7
1089
  if (message.type === "status") {
8
- respond({ type: "status", status: connection.getStatus() });
1090
+ (async () => {
1091
+ respond(await buildStatusMessage());
1092
+ })().catch((error) => {
1093
+ logError("popup.status", error, { code: "status_failed" });
1094
+ respond({
1095
+ type: "status",
1096
+ status: connection.getStatus(),
1097
+ note: "Background unavailable"
1098
+ });
1099
+ });
9
1100
  return true;
10
1101
  }
11
1102
  if (message.type === "connect") {
12
1103
  (async () => {
13
1104
  await connection.connect();
14
- respond({ type: "status", status: connection.getStatus() });
15
- })().catch(() => {
1105
+ if (connection.getStatus() !== "connected") {
1106
+ await attemptNativeConnect();
1107
+ }
1108
+ respond(await buildStatusMessage());
1109
+ })().catch((error) => {
1110
+ logError("popup.connect", error, { code: "connect_failed" });
16
1111
  connection.disconnect();
17
- respond({ type: "status", status: connection.getStatus() });
1112
+ nativePort.disconnect();
1113
+ respond({
1114
+ type: "status",
1115
+ status: "disconnected",
1116
+ note: "Connect failed"
1117
+ });
18
1118
  });
19
1119
  return true;
20
1120
  }
21
1121
  if (message.type === "disconnect") {
22
1122
  (async () => {
23
1123
  await connection.disconnect();
24
- respond({ type: "status", status: connection.getStatus() });
25
- })().catch(() => {
1124
+ nativePort.disconnect();
1125
+ connection.clearLastError();
1126
+ respond(await buildStatusMessage());
1127
+ })().catch((error) => {
1128
+ logError("popup.disconnect", error, { code: "disconnect_failed" });
26
1129
  connection.disconnect();
27
- respond({ type: "status", status: connection.getStatus() });
1130
+ nativePort.disconnect();
1131
+ connection.clearLastError();
1132
+ respond({
1133
+ type: "status",
1134
+ status: "disconnected",
1135
+ note: "Disconnect failed"
1136
+ });
1137
+ });
1138
+ return true;
1139
+ }
1140
+ if (message.type === "annotation:start") {
1141
+ (async () => {
1142
+ const requestId = generateAnnotationRequestId();
1143
+ const response = {
1144
+ type: "annotation:startResult",
1145
+ requestId,
1146
+ ok: false
1147
+ };
1148
+ try {
1149
+ const active = await getActiveTab();
1150
+ if (!active) {
1151
+ response.error = { code: "invalid_request", message: "No active tab available." };
1152
+ sendResponse(response);
1153
+ return;
1154
+ }
1155
+ const restricted = isRestrictedTab(active);
1156
+ if (restricted) {
1157
+ response.error = { code: "restricted_url", message: restricted };
1158
+ sendResponse(response);
1159
+ return;
1160
+ }
1161
+ if (typeof active.id !== "number") {
1162
+ response.error = { code: "invalid_request", message: "Target tab missing id." };
1163
+ sendResponse(response);
1164
+ return;
1165
+ }
1166
+ await startAnnotationSession({
1167
+ version: 1,
1168
+ requestId,
1169
+ command: "start",
1170
+ tabId: active.id,
1171
+ options: message.options
1172
+ }, "popup");
1173
+ if (!annotationSessions.has(requestId)) {
1174
+ response.error = { code: "injection_failed", message: "Annotation session failed to start." };
1175
+ sendResponse(response);
1176
+ return;
1177
+ }
1178
+ response.ok = true;
1179
+ sendResponse(response);
1180
+ }
1181
+ catch (error) {
1182
+ const detail = error instanceof Error ? error.message : "Annotation start failed.";
1183
+ response.error = { code: "injection_failed", message: detail };
1184
+ sendResponse(response);
1185
+ }
1186
+ })();
1187
+ return true;
1188
+ }
1189
+ if (message.type === "annotation:lastMeta") {
1190
+ (async () => {
1191
+ if (lastAnnotationFull) {
1192
+ const meta = { ...lastAnnotationFull.meta, hasFullPayloadInMemory: true };
1193
+ const response = { type: "annotation:lastMetaResult", meta };
1194
+ sendResponse(response);
1195
+ return;
1196
+ }
1197
+ const stored = await loadPersistedLastAnnotation();
1198
+ const meta = stored.meta ? { ...stored.meta, hasFullPayloadInMemory: false } : null;
1199
+ const response = { type: "annotation:lastMetaResult", meta };
1200
+ sendResponse(response);
1201
+ })().catch(() => {
1202
+ const response = { type: "annotation:lastMetaResult", meta: null };
1203
+ sendResponse(response);
28
1204
  });
29
1205
  return true;
30
1206
  }
1207
+ if (message.type === "annotation:probe") {
1208
+ (async () => {
1209
+ const result = await probeAnnotationInjected();
1210
+ sendResponse({ type: "annotation:probeResult", ...result });
1211
+ })().catch((error) => {
1212
+ logError("annotation.probe", error, { code: "annotation_probe_failed" });
1213
+ sendResponse({ type: "annotation:probeResult", injected: false, detail: "Probe failed." });
1214
+ });
1215
+ return true;
1216
+ }
1217
+ if (message.type === "annotation:getPayload") {
1218
+ (async () => {
1219
+ if (message.includeScreenshots && lastAnnotationFull) {
1220
+ const response = {
1221
+ type: "annotation:payloadResult",
1222
+ payload: lastAnnotationFull.payload,
1223
+ meta: { ...lastAnnotationFull.meta, hasFullPayloadInMemory: true },
1224
+ source: "memory"
1225
+ };
1226
+ sendResponse(response);
1227
+ return;
1228
+ }
1229
+ const stored = await loadPersistedLastAnnotation();
1230
+ const storedMeta = stored.meta ? { ...stored.meta, hasFullPayloadInMemory: false } : null;
1231
+ if (message.includeScreenshots) {
1232
+ const response = {
1233
+ type: "annotation:payloadResult",
1234
+ payload: null,
1235
+ meta: storedMeta,
1236
+ source: "none",
1237
+ warning: "Full payload not available; screenshots may have been dropped."
1238
+ };
1239
+ sendResponse(response);
1240
+ return;
1241
+ }
1242
+ if (lastAnnotationFull) {
1243
+ const response = {
1244
+ type: "annotation:payloadResult",
1245
+ payload: stripScreenshots(lastAnnotationFull.payload),
1246
+ meta: { ...lastAnnotationFull.meta, hasFullPayloadInMemory: true },
1247
+ source: "memory"
1248
+ };
1249
+ sendResponse(response);
1250
+ return;
1251
+ }
1252
+ if (stored.payload) {
1253
+ const response = {
1254
+ type: "annotation:payloadResult",
1255
+ payload: stored.payload,
1256
+ meta: storedMeta,
1257
+ source: "storage"
1258
+ };
1259
+ sendResponse(response);
1260
+ return;
1261
+ }
1262
+ const response = {
1263
+ type: "annotation:payloadResult",
1264
+ payload: null,
1265
+ meta: storedMeta,
1266
+ source: "none",
1267
+ warning: "No stored annotation payload."
1268
+ };
1269
+ sendResponse(response);
1270
+ })().catch(() => {
1271
+ const response = {
1272
+ type: "annotation:payloadResult",
1273
+ payload: null,
1274
+ meta: null,
1275
+ source: "none",
1276
+ warning: "Background unavailable."
1277
+ };
1278
+ sendResponse(response);
1279
+ });
1280
+ return true;
1281
+ }
1282
+ if (message.type === "annotation:capture") {
1283
+ (async () => {
1284
+ const tab = sender.tab;
1285
+ if (!tab) {
1286
+ sendResponse({ ok: false, error: "No tab for capture" });
1287
+ return;
1288
+ }
1289
+ try {
1290
+ const dataUrl = await captureVisibleTab(tab);
1291
+ sendResponse({ ok: true, dataUrl });
1292
+ }
1293
+ catch (error) {
1294
+ sendResponse({ ok: false, error: error instanceof Error ? error.message : "Capture failed" });
1295
+ }
1296
+ })();
1297
+ return true;
1298
+ }
1299
+ if (message.type === "annotation:complete") {
1300
+ handleAnnotationComplete(message.requestId, message.payload);
1301
+ sendResponse({ ok: true });
1302
+ return true;
1303
+ }
1304
+ if (message.type === "annotation:cancelled") {
1305
+ handleAnnotationCancelled(message.requestId);
1306
+ sendResponse({ ok: true });
1307
+ return true;
1308
+ }
1309
+ if (message.type === "annotation:error") {
1310
+ handleAnnotationError(message.requestId, message.error);
1311
+ sendResponse({ ok: true });
1312
+ return true;
1313
+ }
31
1314
  return false;
32
1315
  });