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,35 +1,218 @@
1
- import { DEFAULT_AUTO_PAIR, DEFAULT_DISCOVERY_PORT, DEFAULT_PAIRING_ENABLED, DEFAULT_PAIRING_TOKEN, DEFAULT_RELAY_PORT } from "./relay-settings.js";
1
+ import { DEFAULT_AUTO_CONNECT, DEFAULT_AUTO_PAIR, DEFAULT_DISCOVERY_PORT, DEFAULT_NATIVE_ENABLED, DEFAULT_PAIRING_ENABLED, DEFAULT_PAIRING_TOKEN, DEFAULT_RELAY_PORT } from "./relay-settings.js";
2
+ import { logError } from "./logging.js";
2
3
  const statusEl = document.getElementById("status");
3
4
  const statusIndicator = document.getElementById("statusIndicator");
5
+ const statusPill = document.getElementById("statusPill");
6
+ const statusNote = document.getElementById("statusNote");
4
7
  const toggleButton = document.getElementById("toggle");
8
+ const healthRelay = document.getElementById("healthRelay");
9
+ const healthHandshake = document.getElementById("healthHandshake");
10
+ const healthAnnotation = document.getElementById("healthAnnotation");
11
+ const healthInjected = document.getElementById("healthInjected");
12
+ const healthCdp = document.getElementById("healthCdp");
13
+ const healthPairing = document.getElementById("healthPairing");
14
+ const healthNative = document.getElementById("healthNative");
15
+ const healthNote = document.getElementById("healthNote");
5
16
  const relayPortInput = document.getElementById("relayPort");
6
17
  const pairingTokenInput = document.getElementById("pairingToken");
7
18
  const pairingEnabledInput = document.getElementById("pairingEnabled");
8
19
  const autoPairInput = document.getElementById("autoPair");
9
- if (!statusEl || !statusIndicator || !toggleButton || !relayPortInput || !pairingTokenInput || !pairingEnabledInput || !autoPairInput) {
20
+ const autoConnectInput = document.getElementById("autoConnect");
21
+ const nativeEnabledInput = document.getElementById("nativeEnabled");
22
+ const annotationContextInput = document.getElementById("annotationContext");
23
+ const annotationStartButton = document.getElementById("annotationStart");
24
+ const annotationCopyButton = document.getElementById("annotationCopy");
25
+ const annotationNote = document.getElementById("annotationNote");
26
+ if (!statusEl
27
+ || !statusIndicator
28
+ || !statusPill
29
+ || !statusNote
30
+ || !toggleButton
31
+ || !healthRelay
32
+ || !healthHandshake
33
+ || !healthAnnotation
34
+ || !healthInjected
35
+ || !healthCdp
36
+ || !healthPairing
37
+ || !healthNative
38
+ || !healthNote
39
+ || !relayPortInput
40
+ || !pairingTokenInput
41
+ || !pairingEnabledInput
42
+ || !autoPairInput
43
+ || !autoConnectInput
44
+ || !nativeEnabledInput
45
+ || !annotationContextInput
46
+ || !annotationStartButton
47
+ || !annotationCopyButton
48
+ || !annotationNote) {
10
49
  throw new Error("Popup DOM missing required elements");
11
50
  }
51
+ const defaultNote = "Local relay only. Tokens stay on-device.";
52
+ const defaultAnnotationNote = "No annotations captured yet.";
53
+ const LAST_ANNOTATION_META_KEY = "annotationLastMeta";
54
+ const setNote = (message) => {
55
+ const next = message && message.trim() ? message : defaultNote;
56
+ statusNote.textContent = next;
57
+ };
58
+ const setAnnotationNote = (message) => {
59
+ const next = message && message.trim() ? message : defaultAnnotationNote;
60
+ annotationNote.textContent = next;
61
+ };
12
62
  const setStatus = (status) => {
13
63
  const isConnected = status === "connected";
14
64
  statusEl.textContent = isConnected ? "Connected" : "Disconnected";
15
65
  toggleButton.textContent = isConnected ? "Disconnect" : "Connect";
16
66
  if (isConnected) {
17
67
  statusIndicator.classList.add("connected");
68
+ statusPill.classList.add("connected");
18
69
  }
19
70
  else {
20
71
  statusIndicator.classList.remove("connected");
72
+ statusPill.classList.remove("connected");
73
+ }
74
+ };
75
+ const setHealthValue = (element, value, tone) => {
76
+ element.textContent = value;
77
+ element.dataset.tone = tone;
78
+ };
79
+ const formatReason = (reason) => {
80
+ return reason.replace(/_/g, " ");
81
+ };
82
+ const setHealth = (health) => {
83
+ if (!health) {
84
+ setHealthValue(healthRelay, "Unknown", "off");
85
+ setHealthValue(healthHandshake, "Unknown", "off");
86
+ setHealthValue(healthAnnotation, "Unknown", "off");
87
+ setHealthValue(healthInjected, "Unknown", "off");
88
+ setHealthValue(healthCdp, "Unknown", "off");
89
+ setHealthValue(healthPairing, "Unknown", "off");
90
+ healthNote.textContent = "Health check pending.";
91
+ return;
92
+ }
93
+ setHealthValue(healthRelay, health.ok ? "Online" : "Offline", health.ok ? "ok" : "warn");
94
+ setHealthValue(healthHandshake, health.extensionHandshakeComplete ? "Complete" : (health.extensionConnected ? "Pending" : "Offline"), health.extensionHandshakeComplete ? "ok" : (health.extensionConnected ? "warn" : "off"));
95
+ setHealthValue(healthAnnotation, health.annotationConnected ? "Connected" : "Idle", health.annotationConnected ? "ok" : "off");
96
+ setHealthValue(healthInjected, "Unknown", "off");
97
+ setHealthValue(healthCdp, health.cdpConnected ? "Active" : "Idle", health.cdpConnected ? "ok" : "off");
98
+ setHealthValue(healthPairing, health.pairingRequired ? "Required" : "Not required", health.pairingRequired ? "warn" : "ok");
99
+ if (health.lastHandshakeError) {
100
+ const detail = health.lastHandshakeError.message ? ` ${health.lastHandshakeError.message}` : "";
101
+ healthNote.textContent = `Last handshake error: ${health.lastHandshakeError.code}.${detail}`;
102
+ }
103
+ else if (!health.ok) {
104
+ healthNote.textContent = `Relay health: ${formatReason(health.reason)}.`;
105
+ }
106
+ else {
107
+ healthNote.textContent = "Relay health OK.";
108
+ }
109
+ };
110
+ const setInjectionStatus = (injected, detail) => {
111
+ if (injected === null) {
112
+ setHealthValue(healthInjected, "Unknown", "off");
113
+ return;
114
+ }
115
+ if (injected) {
116
+ setHealthValue(healthInjected, "Injected", "ok");
117
+ return;
118
+ }
119
+ setHealthValue(healthInjected, "Not injected", "warn");
120
+ if (detail && healthNote.textContent === "Relay health OK.") {
121
+ healthNote.textContent = `Annotation UI: ${detail}`;
122
+ }
123
+ };
124
+ const formatNativeHealth = (health) => {
125
+ if (!health) {
126
+ return { label: "Unknown", tone: "off" };
127
+ }
128
+ if (health.status === "connected") {
129
+ return { label: "Connected", tone: "ok" };
130
+ }
131
+ switch (health.error) {
132
+ case "host_not_installed":
133
+ return { label: "Not installed", tone: "warn", note: "Native host not installed." };
134
+ case "host_forbidden":
135
+ return { label: "Forbidden", tone: "warn", note: "Native host forbidden." };
136
+ case "host_timeout":
137
+ return { label: "Timeout", tone: "warn", note: "Native host timed out." };
138
+ case "host_message_too_large":
139
+ return { label: "Too large", tone: "warn", note: "Native host rejected message size." };
140
+ case "host_disconnect":
141
+ return { label: "Disconnected", tone: "off", note: "Native host disconnected." };
142
+ default:
143
+ return { label: "Unavailable", tone: "off", note: "Native host unavailable." };
144
+ }
145
+ };
146
+ const setNativeHealth = (health, enabled) => {
147
+ if (!enabled) {
148
+ setHealthValue(healthNative, "Disabled", "off");
149
+ return;
21
150
  }
151
+ const formatted = formatNativeHealth(health);
152
+ setHealthValue(healthNative, formatted.label, formatted.tone);
153
+ if (formatted.note && healthNote.textContent === "Relay health OK.") {
154
+ healthNote.textContent = `${healthNote.textContent} ${formatted.note}`;
155
+ }
156
+ };
157
+ const applyStatus = (response) => {
158
+ setStatus(response.status);
159
+ setNote(response.note);
160
+ setHealth(response.relayHealth ?? null);
161
+ setNativeHealth(response.nativeHealth ?? null, response.nativeEnabled === true);
22
162
  };
23
163
  const sendMessage = (message) => {
24
- return new Promise((resolve) => {
164
+ return new Promise((resolve, reject) => {
25
165
  chrome.runtime.sendMessage(message, (response) => {
166
+ const lastError = chrome.runtime.lastError;
167
+ if (lastError) {
168
+ reject(new Error(lastError.message));
169
+ return;
170
+ }
171
+ if (!response) {
172
+ reject(new Error("No response from background"));
173
+ return;
174
+ }
26
175
  resolve(response);
27
176
  });
28
177
  });
29
178
  };
179
+ const setStorage = (items) => {
180
+ return new Promise((resolve) => {
181
+ chrome.storage.local.set(items, () => resolve());
182
+ });
183
+ };
184
+ const refreshInjectionStatus = async () => {
185
+ try {
186
+ const response = await sendMessage({ type: "annotation:probe" });
187
+ setInjectionStatus(response.injected, response.detail);
188
+ }
189
+ catch (error) {
190
+ logError("popup.annotation_probe", error, { code: "annotation_probe_failed" });
191
+ setInjectionStatus(null);
192
+ }
193
+ };
30
194
  const refreshStatus = async () => {
31
- const response = await sendMessage({ type: "status" });
32
- setStatus(response.status);
195
+ try {
196
+ const response = await sendMessage({ type: "status" });
197
+ applyStatus(response);
198
+ void refreshInjectionStatus();
199
+ }
200
+ catch (error) {
201
+ logError("popup.status_refresh", error, { code: "status_refresh_failed" });
202
+ setStatus("disconnected");
203
+ const message = error instanceof Error ? error.message : "Background unavailable";
204
+ setNote(message);
205
+ setHealth(null);
206
+ setNativeHealth(null, nativeEnabledInput.checked);
207
+ setInjectionStatus(null);
208
+ }
209
+ };
210
+ const setCopyEnabled = (enabled) => {
211
+ annotationCopyButton.disabled = !enabled;
212
+ };
213
+ const buildPopupAnnotationOptions = () => {
214
+ const context = annotationContextInput.value.trim();
215
+ return context ? { context } : undefined;
33
216
  };
34
217
  const fetchTokenFromPlugin = async (port) => {
35
218
  try {
@@ -41,9 +224,17 @@ const fetchTokenFromPlugin = async (port) => {
41
224
  return null;
42
225
  }
43
226
  const data = await response.json();
44
- return typeof data.token === "string" ? data.token : null;
227
+ if (typeof data.token !== "string") {
228
+ return null;
229
+ }
230
+ return {
231
+ token: data.token,
232
+ instanceId: typeof data.instanceId === "string" ? data.instanceId : null,
233
+ epoch: typeof data.epoch === "number" ? data.epoch : null
234
+ };
45
235
  }
46
- catch {
236
+ catch (error) {
237
+ logError("popup.token_fetch", error, { code: "relay_pair_fetch_failed", extra: { port } });
47
238
  return null;
48
239
  }
49
240
  };
@@ -74,9 +265,12 @@ const fetchRelayConfig = async (port) => {
74
265
  return null;
75
266
  }
76
267
  const pairingRequired = typeof data.pairingRequired === "boolean" ? data.pairingRequired : true;
77
- return { relayPort, pairingRequired };
268
+ const instanceId = typeof data.instanceId === "string" ? data.instanceId : null;
269
+ const epoch = typeof data.epoch === "number" ? data.epoch : null;
270
+ return { relayPort, pairingRequired, instanceId, epoch };
78
271
  }
79
- catch {
272
+ catch (error) {
273
+ logError("popup.relay_config_fetch", error, { code: "relay_config_fetch_failed", extra: { port } });
80
274
  return null;
81
275
  }
82
276
  };
@@ -89,26 +283,37 @@ const applyRelayPort = (port) => {
89
283
  };
90
284
  const loadSettings = async () => {
91
285
  const data = await new Promise((resolve) => {
92
- chrome.storage.local.get(["pairingToken", "pairingEnabled", "relayPort", "autoPair"], (items) => {
286
+ chrome.storage.local.get(["pairingToken", "pairingEnabled", "relayPort", "autoPair", "autoConnect", "nativeEnabled"], (items) => {
93
287
  resolve(items);
94
288
  });
95
289
  });
96
290
  const autoPair = typeof data.autoPair === "boolean" ? data.autoPair : DEFAULT_AUTO_PAIR;
291
+ const autoConnect = typeof data.autoConnect === "boolean" ? data.autoConnect : DEFAULT_AUTO_CONNECT;
292
+ const nativeEnabled = typeof data.nativeEnabled === "boolean" ? data.nativeEnabled : DEFAULT_NATIVE_ENABLED;
97
293
  const pairingEnabled = typeof data.pairingEnabled === "boolean"
98
294
  ? data.pairingEnabled
99
295
  : DEFAULT_PAIRING_ENABLED;
100
296
  const rawToken = typeof data.pairingToken === "string" ? data.pairingToken.trim() : "";
101
297
  const tokenValue = pairingEnabled ? (rawToken || DEFAULT_PAIRING_TOKEN || "") : rawToken;
102
298
  const portValue = parsePort(data.relayPort) ?? DEFAULT_RELAY_PORT;
299
+ autoConnectInput.checked = autoConnect;
103
300
  autoPairInput.checked = autoPair;
301
+ nativeEnabledInput.checked = nativeEnabled;
104
302
  pairingEnabledInput.checked = pairingEnabled;
105
303
  pairingTokenInput.disabled = !pairingEnabled || autoPair;
106
304
  pairingTokenInput.value = tokenValue;
107
305
  relayPortInput.value = Number.isInteger(portValue) ? String(portValue) : String(DEFAULT_RELAY_PORT);
306
+ setNote();
108
307
  const updates = {};
308
+ if (typeof data.autoConnect !== "boolean") {
309
+ updates.autoConnect = autoConnect;
310
+ }
109
311
  if (typeof data.autoPair !== "boolean") {
110
312
  updates.autoPair = autoPair;
111
313
  }
314
+ if (typeof data.nativeEnabled !== "boolean") {
315
+ updates.nativeEnabled = nativeEnabled;
316
+ }
112
317
  if (typeof data.pairingEnabled !== "boolean") {
113
318
  updates.pairingEnabled = pairingEnabled;
114
319
  }
@@ -116,38 +321,173 @@ const loadSettings = async () => {
116
321
  chrome.storage.local.set(updates);
117
322
  }
118
323
  };
324
+ const formatAnnotationSummary = (meta) => {
325
+ if (meta.status !== "ok") {
326
+ if (meta.error?.message) {
327
+ return `Last annotation ${meta.status}: ${meta.error.message}`;
328
+ }
329
+ return `Last annotation ${meta.status}.`;
330
+ }
331
+ const count = meta.annotationCount ?? 0;
332
+ const target = meta.url ? ` on ${meta.url}` : "";
333
+ return `Last annotation: ${count} item${count === 1 ? "" : "s"}${target}.`;
334
+ };
335
+ const refreshLastAnnotationMeta = async () => {
336
+ try {
337
+ const response = await sendMessage({ type: "annotation:lastMeta" });
338
+ const meta = response.meta;
339
+ if (meta && meta.status === "ok") {
340
+ setCopyEnabled(true);
341
+ setAnnotationNote(formatAnnotationSummary(meta));
342
+ return;
343
+ }
344
+ setCopyEnabled(false);
345
+ if (meta) {
346
+ setAnnotationNote(formatAnnotationSummary(meta));
347
+ }
348
+ else {
349
+ setAnnotationNote();
350
+ }
351
+ }
352
+ catch (error) {
353
+ logError("popup.annotation_meta", error, { code: "annotation_meta_failed" });
354
+ setCopyEnabled(false);
355
+ setAnnotationNote("Annotation status unavailable.");
356
+ }
357
+ };
358
+ const copyTextToClipboard = async (text) => {
359
+ try {
360
+ await navigator.clipboard.writeText(text);
361
+ return;
362
+ }
363
+ catch (error) {
364
+ logError("popup.clipboard_async", error, { code: "clipboard_write_failed" });
365
+ }
366
+ const textarea = document.createElement("textarea");
367
+ textarea.value = text;
368
+ textarea.setAttribute("readonly", "true");
369
+ textarea.style.position = "fixed";
370
+ textarea.style.top = "-1000px";
371
+ textarea.style.opacity = "0";
372
+ document.body.appendChild(textarea);
373
+ textarea.select();
374
+ const ok = document.execCommand("copy");
375
+ document.body.removeChild(textarea);
376
+ if (!ok) {
377
+ throw new Error("Clipboard copy failed");
378
+ }
379
+ };
119
380
  const toggle = async () => {
120
381
  const isConnected = statusEl.textContent === "Connected";
382
+ setNote();
121
383
  if (!isConnected && autoPairInput.checked && pairingEnabledInput.checked) {
122
384
  const config = await fetchRelayConfig(DEFAULT_DISCOVERY_PORT);
123
385
  const relayPort = config?.relayPort ?? parsePort(relayPortInput.value) ?? DEFAULT_RELAY_PORT;
124
386
  if (config?.relayPort) {
125
387
  applyRelayPort(config.relayPort);
126
388
  }
389
+ if (config?.relayPort || config?.instanceId || config?.epoch) {
390
+ await setStorage({
391
+ relayPort: config?.relayPort ?? relayPort,
392
+ relayInstanceId: config?.instanceId ?? null,
393
+ relayEpoch: config?.epoch ?? null
394
+ });
395
+ }
127
396
  const pairingRequired = config?.pairingRequired ?? true;
128
397
  if (pairingRequired) {
129
398
  const fetchedToken = await fetchTokenFromPlugin(relayPort);
130
399
  if (fetchedToken) {
131
- pairingTokenInput.value = fetchedToken;
132
- chrome.storage.local.set({ pairingToken: fetchedToken });
400
+ pairingTokenInput.value = fetchedToken.token;
401
+ await setStorage({
402
+ pairingToken: fetchedToken.token,
403
+ tokenEpoch: fetchedToken.epoch ?? config?.epoch ?? null,
404
+ relayInstanceId: config?.instanceId ?? fetchedToken.instanceId ?? null,
405
+ relayEpoch: config?.epoch ?? fetchedToken.epoch ?? null
406
+ });
133
407
  }
134
408
  else {
135
- statusEl.textContent = "Failed to fetch token from plugin";
409
+ setStatus("disconnected");
410
+ setNote("Auto-pair failed. Start the daemon and retry.");
136
411
  setTimeout(() => refreshStatus(), 2000);
137
412
  return;
138
413
  }
139
414
  }
140
415
  }
141
- const response = await sendMessage({
142
- type: isConnected ? "disconnect" : "connect"
143
- });
144
- setStatus(response.status);
416
+ try {
417
+ const response = await sendMessage({
418
+ type: isConnected ? "disconnect" : "connect"
419
+ });
420
+ applyStatus(response);
421
+ void refreshInjectionStatus();
422
+ }
423
+ catch (error) {
424
+ logError("popup.toggle", error, { code: "toggle_failed" });
425
+ setStatus("disconnected");
426
+ const message = error instanceof Error ? error.message : "Background unavailable";
427
+ setNote(message);
428
+ setHealth(null);
429
+ }
145
430
  };
146
431
  toggleButton.addEventListener("click", () => {
147
- toggle().catch(() => {
432
+ toggle().catch((error) => {
433
+ logError("popup.toggle", error, { code: "toggle_failed" });
148
434
  setStatus("disconnected");
149
435
  });
150
436
  });
437
+ annotationStartButton.addEventListener("click", () => {
438
+ (async () => {
439
+ setAnnotationNote("Starting annotation...");
440
+ const options = buildPopupAnnotationOptions();
441
+ const response = await sendMessage({
442
+ type: "annotation:start",
443
+ options
444
+ });
445
+ if (!response.ok) {
446
+ const message = response.error?.message ?? "Annotation start failed.";
447
+ setAnnotationNote(message);
448
+ setCopyEnabled(false);
449
+ return;
450
+ }
451
+ setAnnotationNote("Annotation started. Switch to the tab to select elements.");
452
+ setCopyEnabled(false);
453
+ void refreshInjectionStatus();
454
+ })().catch((error) => {
455
+ const message = error instanceof Error ? error.message : "Annotation start failed.";
456
+ setAnnotationNote(message);
457
+ setCopyEnabled(false);
458
+ });
459
+ });
460
+ annotationCopyButton.addEventListener("click", () => {
461
+ (async () => {
462
+ setAnnotationNote("Preparing annotation payload...");
463
+ let response = await sendMessage({
464
+ type: "annotation:getPayload",
465
+ includeScreenshots: true
466
+ });
467
+ if (!response.payload) {
468
+ response = await sendMessage({
469
+ type: "annotation:getPayload",
470
+ includeScreenshots: false
471
+ });
472
+ }
473
+ if (!response.payload) {
474
+ setAnnotationNote("No completed annotation payload available.");
475
+ setCopyEnabled(false);
476
+ return;
477
+ }
478
+ await copyTextToClipboard(JSON.stringify(response.payload, null, 2));
479
+ if (response.warning) {
480
+ setAnnotationNote(`Copied payload (${response.warning.replace(/\.$/, "")}).`);
481
+ }
482
+ else {
483
+ setAnnotationNote("Copied annotation payload to clipboard.");
484
+ }
485
+ await refreshLastAnnotationMeta();
486
+ })().catch((error) => {
487
+ const message = error instanceof Error ? error.message : "Copy failed.";
488
+ setAnnotationNote(message);
489
+ });
490
+ });
151
491
  autoPairInput.addEventListener("change", () => {
152
492
  const enabled = autoPairInput.checked;
153
493
  pairingTokenInput.disabled = !pairingEnabledInput.checked || enabled;
@@ -156,9 +496,26 @@ autoPairInput.addEventListener("change", () => {
156
496
  pairingTokenInput.placeholder = "Will be fetched automatically";
157
497
  }
158
498
  else {
159
- pairingTokenInput.placeholder = "Enter token or enable Auto-Pair";
499
+ pairingTokenInput.placeholder = "Enter token or enable auto-pair";
500
+ }
501
+ });
502
+ autoConnectInput.addEventListener("change", () => {
503
+ const enabled = autoConnectInput.checked;
504
+ chrome.storage.local.set({ autoConnect: enabled });
505
+ if (enabled && statusEl.textContent !== "Connected") {
506
+ toggle().catch((error) => {
507
+ logError("popup.auto_connect", error, { code: "auto_connect_failed" });
508
+ setStatus("disconnected");
509
+ setNote();
510
+ });
160
511
  }
161
512
  });
513
+ nativeEnabledInput.addEventListener("change", () => {
514
+ const enabled = nativeEnabledInput.checked;
515
+ chrome.storage.local.set({ nativeEnabled: enabled });
516
+ setNativeHealth(null, enabled);
517
+ void refreshStatus();
518
+ });
162
519
  pairingTokenInput.addEventListener("input", () => {
163
520
  const value = pairingTokenInput.value.trim();
164
521
  chrome.storage.local.set({ pairingToken: value });
@@ -185,13 +542,33 @@ relayPortInput.addEventListener("input", () => {
185
542
  chrome.storage.local.set({ relayPort: parsed });
186
543
  }
187
544
  });
188
- refreshStatus().catch(() => {
545
+ refreshStatus().catch((error) => {
546
+ logError("popup.refresh_status", error, { code: "status_refresh_failed" });
189
547
  setStatus("disconnected");
548
+ setNote();
190
549
  });
191
- loadSettings().catch(() => {
550
+ loadSettings().catch((error) => {
551
+ logError("popup.load_settings", error, { code: "settings_load_failed" });
552
+ autoConnectInput.checked = DEFAULT_AUTO_CONNECT;
192
553
  autoPairInput.checked = DEFAULT_AUTO_PAIR;
554
+ nativeEnabledInput.checked = DEFAULT_NATIVE_ENABLED;
193
555
  pairingEnabledInput.checked = DEFAULT_PAIRING_ENABLED;
194
556
  pairingTokenInput.disabled = !DEFAULT_PAIRING_ENABLED;
195
557
  pairingTokenInput.value = DEFAULT_PAIRING_TOKEN || "";
196
558
  relayPortInput.value = String(DEFAULT_RELAY_PORT);
559
+ setNote();
560
+ });
561
+ refreshLastAnnotationMeta().catch((error) => {
562
+ logError("popup.annotation_meta", error, { code: "annotation_meta_failed" });
563
+ setCopyEnabled(false);
564
+ setAnnotationNote();
565
+ });
566
+ chrome.storage.onChanged.addListener((changes, areaName) => {
567
+ if (areaName !== "local")
568
+ return;
569
+ if (!changes[LAST_ANNOTATION_META_KEY])
570
+ return;
571
+ refreshLastAnnotationMeta().catch((error) => {
572
+ logError("popup.annotation_meta", error, { code: "annotation_meta_failed" });
573
+ });
197
574
  });
@@ -2,4 +2,6 @@ export const DEFAULT_RELAY_PORT = 8787;
2
2
  export const DEFAULT_DISCOVERY_PORT = 8787;
3
3
  export const DEFAULT_PAIRING_TOKEN = null;
4
4
  export const DEFAULT_PAIRING_ENABLED = true;
5
- export const DEFAULT_AUTO_PAIR = false;
5
+ export const DEFAULT_AUTO_PAIR = true;
6
+ export const DEFAULT_AUTO_CONNECT = true;
7
+ export const DEFAULT_NATIVE_ENABLED = false;