opendevbrowser 0.0.16 → 0.0.17

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 (210) hide show
  1. package/README.md +51 -27
  2. package/dist/browser/annotation-manager.d.ts +3 -0
  3. package/dist/browser/annotation-manager.d.ts.map +1 -1
  4. package/dist/browser/browser-manager.d.ts +6 -1
  5. package/dist/browser/browser-manager.d.ts.map +1 -1
  6. package/dist/browser/canvas-client.d.ts +53 -0
  7. package/dist/browser/canvas-client.d.ts.map +1 -0
  8. package/dist/browser/canvas-code-sync-manager.d.ts +79 -0
  9. package/dist/browser/canvas-code-sync-manager.d.ts.map +1 -0
  10. package/dist/browser/canvas-manager.d.ts +94 -0
  11. package/dist/browser/canvas-manager.d.ts.map +1 -0
  12. package/dist/browser/canvas-runtime-preview-bridge.d.ts +20 -0
  13. package/dist/browser/canvas-runtime-preview-bridge.d.ts.map +1 -0
  14. package/dist/browser/canvas-session-sync-manager.d.ts +21 -0
  15. package/dist/browser/canvas-session-sync-manager.d.ts.map +1 -0
  16. package/dist/browser/manager-types.d.ts +13 -1
  17. package/dist/browser/manager-types.d.ts.map +1 -1
  18. package/dist/browser/ops-browser-manager.d.ts +11 -1
  19. package/dist/browser/ops-browser-manager.d.ts.map +1 -1
  20. package/dist/canvas/code-sync/apply-tsx.d.ts +23 -0
  21. package/dist/canvas/code-sync/apply-tsx.d.ts.map +1 -0
  22. package/dist/canvas/code-sync/graph.d.ts +5 -0
  23. package/dist/canvas/code-sync/graph.d.ts.map +1 -0
  24. package/dist/canvas/code-sync/hash.d.ts +3 -0
  25. package/dist/canvas/code-sync/hash.d.ts.map +1 -0
  26. package/dist/canvas/code-sync/import.d.ts +18 -0
  27. package/dist/canvas/code-sync/import.d.ts.map +1 -0
  28. package/dist/canvas/code-sync/manifest.d.ts +5 -0
  29. package/dist/canvas/code-sync/manifest.d.ts.map +1 -0
  30. package/dist/canvas/code-sync/tsx-adapter.d.ts +8 -0
  31. package/dist/canvas/code-sync/tsx-adapter.d.ts.map +1 -0
  32. package/dist/canvas/code-sync/types.d.ts +152 -0
  33. package/dist/canvas/code-sync/types.d.ts.map +1 -0
  34. package/dist/canvas/code-sync/write.d.ts +9 -0
  35. package/dist/canvas/code-sync/write.d.ts.map +1 -0
  36. package/dist/canvas/document-store.d.ts +81 -0
  37. package/dist/canvas/document-store.d.ts.map +1 -0
  38. package/dist/canvas/export.d.ts +12 -0
  39. package/dist/canvas/export.d.ts.map +1 -0
  40. package/dist/canvas/repo-store.d.ts +10 -0
  41. package/dist/canvas/repo-store.d.ts.map +1 -0
  42. package/dist/canvas/surface-palette.d.ts +15 -0
  43. package/dist/canvas/surface-palette.d.ts.map +1 -0
  44. package/dist/canvas/types.d.ts +255 -0
  45. package/dist/canvas/types.d.ts.map +1 -0
  46. package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js +7 -0
  47. package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js.map +1 -0
  48. package/dist/{chunk-ST7CO5FA.js → chunk-5J3IFL3X.js} +11577 -13539
  49. package/dist/chunk-5J3IFL3X.js.map +1 -0
  50. package/dist/chunk-D633UO34.js +8149 -0
  51. package/dist/chunk-D633UO34.js.map +1 -0
  52. package/dist/{chunk-7W3SPXIB.js → chunk-FUSXMW3G.js} +4 -1
  53. package/dist/chunk-TBUCZX4A.js +34 -0
  54. package/dist/chunk-TBUCZX4A.js.map +1 -0
  55. package/dist/chunk-V7KUDHDG.js +276 -0
  56. package/dist/chunk-V7KUDHDG.js.map +1 -0
  57. package/dist/chunk-Y2KL55OG.js +59 -0
  58. package/dist/chunk-Y2KL55OG.js.map +1 -0
  59. package/dist/cli/args.d.ts +3 -3
  60. package/dist/cli/args.d.ts.map +1 -1
  61. package/dist/cli/commands/annotate.d.ts +11 -0
  62. package/dist/cli/commands/annotate.d.ts.map +1 -1
  63. package/dist/cli/commands/canvas.d.ts +45 -0
  64. package/dist/cli/commands/canvas.d.ts.map +1 -0
  65. package/dist/cli/commands/devtools/perf.d.ts.map +1 -1
  66. package/dist/cli/commands/devtools/screenshot.d.ts +1 -0
  67. package/dist/cli/commands/devtools/screenshot.d.ts.map +1 -1
  68. package/dist/cli/commands/dom/attr.d.ts.map +1 -1
  69. package/dist/cli/commands/dom/checked.d.ts.map +1 -1
  70. package/dist/cli/commands/dom/enabled.d.ts.map +1 -1
  71. package/dist/cli/commands/dom/html.d.ts.map +1 -1
  72. package/dist/cli/commands/dom/text.d.ts.map +1 -1
  73. package/dist/cli/commands/dom/value.d.ts.map +1 -1
  74. package/dist/cli/commands/dom/visible.d.ts.map +1 -1
  75. package/dist/cli/commands/export/clone-component.d.ts +9 -0
  76. package/dist/cli/commands/export/clone-component.d.ts.map +1 -1
  77. package/dist/cli/commands/export/clone-page.d.ts +8 -0
  78. package/dist/cli/commands/export/clone-page.d.ts.map +1 -1
  79. package/dist/cli/commands/interact/check.d.ts.map +1 -1
  80. package/dist/cli/commands/interact/click.d.ts.map +1 -1
  81. package/dist/cli/commands/interact/hover.d.ts.map +1 -1
  82. package/dist/cli/commands/interact/press.d.ts.map +1 -1
  83. package/dist/cli/commands/interact/scroll-into-view.d.ts.map +1 -1
  84. package/dist/cli/commands/interact/scroll.d.ts.map +1 -1
  85. package/dist/cli/commands/interact/select.d.ts.map +1 -1
  86. package/dist/cli/commands/interact/type.d.ts.map +1 -1
  87. package/dist/cli/commands/interact/uncheck.d.ts.map +1 -1
  88. package/dist/cli/commands/native.d.ts +12 -1
  89. package/dist/cli/commands/native.d.ts.map +1 -1
  90. package/dist/cli/commands/nav/goto.d.ts.map +1 -1
  91. package/dist/cli/commands/nav/snapshot.d.ts.map +1 -1
  92. package/dist/cli/commands/nav/wait.d.ts.map +1 -1
  93. package/dist/cli/commands/serve.d.ts +5 -0
  94. package/dist/cli/commands/serve.d.ts.map +1 -1
  95. package/dist/cli/commands/session/connect.d.ts.map +1 -1
  96. package/dist/cli/commands/status.d.ts +5 -0
  97. package/dist/cli/commands/status.d.ts.map +1 -1
  98. package/dist/cli/daemon-commands.d.ts.map +1 -1
  99. package/dist/cli/help.d.ts +5 -0
  100. package/dist/cli/help.d.ts.map +1 -1
  101. package/dist/cli/index.js +724 -163
  102. package/dist/cli/index.js.map +1 -1
  103. package/dist/cli/remote-canvas-manager.d.ts +8 -0
  104. package/dist/cli/remote-canvas-manager.d.ts.map +1 -0
  105. package/dist/cli/remote-manager.d.ts +3 -1
  106. package/dist/cli/remote-manager.d.ts.map +1 -1
  107. package/dist/cli/remote-relay.d.ts +2 -0
  108. package/dist/cli/remote-relay.d.ts.map +1 -1
  109. package/dist/cli/utils/parse.d.ts +1 -0
  110. package/dist/cli/utils/parse.d.ts.map +1 -1
  111. package/dist/core/bootstrap.d.ts.map +1 -1
  112. package/dist/core/types.d.ts +2 -0
  113. package/dist/core/types.d.ts.map +1 -1
  114. package/dist/fs-UMRKOBNN.js +7 -0
  115. package/dist/fs-UMRKOBNN.js.map +1 -0
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +192 -67
  118. package/dist/index.js.map +1 -1
  119. package/dist/{macros-NUBRM44Y.js → macros-ND2M7LWU.js} +2 -2
  120. package/dist/opendevbrowser.d.ts.map +1 -1
  121. package/dist/opendevbrowser.js +192 -67
  122. package/dist/opendevbrowser.js.map +1 -1
  123. package/dist/providers/index.d.ts.map +1 -1
  124. package/dist/providers/shopping/index.d.ts.map +1 -1
  125. package/dist/providers-G3LRHQXX.js +121 -0
  126. package/dist/providers-G3LRHQXX.js.map +1 -0
  127. package/dist/relay/protocol.d.ts +85 -3
  128. package/dist/relay/protocol.d.ts.map +1 -1
  129. package/dist/relay/relay-server.d.ts +14 -1
  130. package/dist/relay/relay-server.d.ts.map +1 -1
  131. package/dist/relay/relay-types.d.ts +3 -0
  132. package/dist/relay/relay-types.d.ts.map +1 -1
  133. package/dist/runtime-factory-BICHDPE7.js +13 -0
  134. package/dist/runtime-factory-BICHDPE7.js.map +1 -0
  135. package/dist/tools/annotate.d.ts.map +1 -1
  136. package/dist/tools/canvas.d.ts +4 -0
  137. package/dist/tools/canvas.d.ts.map +1 -0
  138. package/dist/tools/check.d.ts.map +1 -1
  139. package/dist/tools/click.d.ts.map +1 -1
  140. package/dist/tools/clone_component.d.ts.map +1 -1
  141. package/dist/tools/clone_page.d.ts.map +1 -1
  142. package/dist/tools/connect.d.ts.map +1 -1
  143. package/dist/tools/deps.d.ts +2 -0
  144. package/dist/tools/deps.d.ts.map +1 -1
  145. package/dist/tools/dom_get_html.d.ts.map +1 -1
  146. package/dist/tools/dom_get_text.d.ts.map +1 -1
  147. package/dist/tools/get_attr.d.ts.map +1 -1
  148. package/dist/tools/get_value.d.ts.map +1 -1
  149. package/dist/tools/goto.d.ts.map +1 -1
  150. package/dist/tools/hover.d.ts.map +1 -1
  151. package/dist/tools/index.d.ts.map +1 -1
  152. package/dist/tools/is_checked.d.ts.map +1 -1
  153. package/dist/tools/is_enabled.d.ts.map +1 -1
  154. package/dist/tools/is_visible.d.ts.map +1 -1
  155. package/dist/tools/launch.d.ts.map +1 -1
  156. package/dist/tools/macro_resolve.d.ts.map +1 -1
  157. package/dist/tools/perf.d.ts.map +1 -1
  158. package/dist/tools/press.d.ts.map +1 -1
  159. package/dist/tools/product_video_run.d.ts.map +1 -1
  160. package/dist/tools/research_run.d.ts.map +1 -1
  161. package/dist/tools/response.d.ts +4 -1
  162. package/dist/tools/response.d.ts.map +1 -1
  163. package/dist/tools/screenshot.d.ts.map +1 -1
  164. package/dist/tools/scroll.d.ts.map +1 -1
  165. package/dist/tools/scroll_into_view.d.ts.map +1 -1
  166. package/dist/tools/select.d.ts.map +1 -1
  167. package/dist/tools/shopping_run.d.ts.map +1 -1
  168. package/dist/tools/snapshot.d.ts.map +1 -1
  169. package/dist/tools/type.d.ts.map +1 -1
  170. package/dist/tools/uncheck.d.ts.map +1 -1
  171. package/dist/tools/wait.d.ts.map +1 -1
  172. package/dist/tools/workflow-runtime.d.ts +1 -2
  173. package/dist/tools/workflow-runtime.d.ts.map +1 -1
  174. package/extension/canvas.html +636 -0
  175. package/extension/dist/annotate-content.css +15 -6
  176. package/extension/dist/annotate-content.js +119 -9
  177. package/extension/dist/annotation-payload.js +163 -0
  178. package/extension/dist/background.js +148 -18
  179. package/extension/dist/canvas/canvas-runtime.js +1061 -0
  180. package/extension/dist/canvas/model.js +213 -0
  181. package/extension/dist/canvas/viewport-fit.js +67 -0
  182. package/extension/dist/canvas-page.js +1801 -0
  183. package/extension/dist/ops/dom-bridge.js +116 -3
  184. package/extension/dist/ops/ops-runtime.js +508 -44
  185. package/extension/dist/ops/ops-session-store.js +21 -114
  186. package/extension/dist/ops/target-session-coordinator.js +157 -0
  187. package/extension/dist/popup.js +155 -31
  188. package/extension/dist/services/ConnectionManager.js +17 -0
  189. package/extension/dist/services/RelayClient.js +9 -0
  190. package/extension/dist/services/TabManager.js +35 -12
  191. package/extension/dist/types.js +2 -0
  192. package/extension/manifest.json +1 -1
  193. package/extension/popup.html +52 -0
  194. package/package.json +6 -4
  195. package/skills/AGENTS.md +5 -2
  196. package/skills/opendevbrowser-best-practices/SKILL.md +71 -3
  197. package/skills/opendevbrowser-best-practices/artifacts/canvas-governance-playbook.md +141 -0
  198. package/skills/opendevbrowser-best-practices/artifacts/command-channel-reference.md +113 -17
  199. package/skills/opendevbrowser-best-practices/assets/templates/canvas-blocker-checklist.json +70 -0
  200. package/skills/opendevbrowser-best-practices/assets/templates/canvas-feedback-eval.json +73 -0
  201. package/skills/opendevbrowser-best-practices/assets/templates/canvas-generation-plan.v1.json +67 -0
  202. package/skills/opendevbrowser-best-practices/assets/templates/canvas-handshake-example.json +126 -0
  203. package/skills/opendevbrowser-best-practices/assets/templates/robustness-checklist.json +57 -0
  204. package/skills/opendevbrowser-best-practices/assets/templates/surface-audit-checklist.json +7 -3
  205. package/skills/opendevbrowser-best-practices/scripts/odb-workflow.sh +26 -0
  206. package/skills/opendevbrowser-best-practices/scripts/run-robustness-audit.sh +82 -1
  207. package/skills/opendevbrowser-best-practices/scripts/validate-skill-assets.sh +225 -84
  208. package/dist/chunk-ST7CO5FA.js.map +0 -1
  209. /package/dist/{chunk-7W3SPXIB.js.map → chunk-FUSXMW3G.js.map} +0 -0
  210. /package/dist/{macros-NUBRM44Y.js.map → macros-ND2M7LWU.js.map} +0 -0
@@ -34,6 +34,7 @@ const TARGET_SCOPED_COMMANDS = new Set([
34
34
  "dom.isVisible",
35
35
  "dom.isEnabled",
36
36
  "dom.isChecked",
37
+ "canvas.applyRuntimePreviewBridge",
37
38
  "export.clonePage",
38
39
  "export.cloneComponent",
39
40
  "devtools.perf",
@@ -42,6 +43,8 @@ const TARGET_SCOPED_COMMANDS = new Set([
42
43
  export class OpsRuntime {
43
44
  sendEnvelope;
44
45
  cdp;
46
+ getCanvasPageState;
47
+ performCanvasPageAction;
45
48
  tabs = new TabManager();
46
49
  dom = new DomBridge();
47
50
  sessions = new OpsSessionStore();
@@ -51,6 +54,8 @@ export class OpsRuntime {
51
54
  constructor(options) {
52
55
  this.sendEnvelope = options.send;
53
56
  this.cdp = options.cdp;
57
+ this.getCanvasPageState = options.getCanvasPageState;
58
+ this.performCanvasPageAction = options.performCanvasPageAction;
54
59
  chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
55
60
  chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
56
61
  chrome.debugger.onEvent.addListener(this.handleDebuggerEvent);
@@ -252,6 +257,9 @@ export class OpsRuntime {
252
257
  case "targets.use":
253
258
  await this.withSession(message, clientId, (session) => this.handleTargetsUse(message, session));
254
259
  return;
260
+ case "targets.registerCanvas":
261
+ await this.withSession(message, clientId, (session) => this.handleTargetsRegisterCanvas(message, session));
262
+ return;
255
263
  case "targets.new":
256
264
  await this.withSession(message, clientId, (session) => this.handleTargetsNew(message, session));
257
265
  return;
@@ -324,6 +332,9 @@ export class OpsRuntime {
324
332
  case "dom.isChecked":
325
333
  await this.withSession(message, clientId, (session) => this.handleDomIsChecked(message, session));
326
334
  return;
335
+ case "canvas.applyRuntimePreviewBridge":
336
+ await this.withSession(message, clientId, (session) => this.handleCanvasRuntimePreviewBridge(message, session));
337
+ return;
327
338
  case "export.clonePage":
328
339
  await this.withSession(message, clientId, (session) => this.handleClonePage(message, session));
329
340
  return;
@@ -370,28 +381,36 @@ export class OpsRuntime {
370
381
  this.sendError(message, buildError("ops_unavailable", "No active tab to attach.", true));
371
382
  return;
372
383
  }
373
- if (activeTab.url) {
374
- const restriction = isRestrictedUrl(activeTab.url);
384
+ const activeTabId = activeTab.id;
385
+ const resolvedTab = startUrl
386
+ ? await this.tabs.waitForTabComplete(activeTabId)
387
+ .catch(() => undefined)
388
+ .then(async () => await this.tabs.getTab(activeTabId) ?? activeTab)
389
+ : activeTab;
390
+ if (resolvedTab.url) {
391
+ const restriction = isRestrictedUrl(resolvedTab.url);
375
392
  if (restriction.restricted) {
376
393
  this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
377
394
  return;
378
395
  }
379
396
  }
380
397
  try {
381
- await this.cdp.attach(activeTab.id);
398
+ await this.cdp.attach(activeTabId);
382
399
  }
383
400
  catch (error) {
384
401
  const detail = error instanceof Error ? error.message : "Debugger attach failed";
385
402
  this.sendError(message, buildError("cdp_attach_failed", detail, false));
386
403
  return;
387
404
  }
388
- await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
405
+ if (!startUrl) {
406
+ await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
407
+ }
389
408
  const leaseId = typeof message.leaseId === "string" && message.leaseId.trim().length > 0
390
409
  ? message.leaseId.trim()
391
410
  : createId();
392
- const session = this.sessions.createSession(clientId, activeTab.id, leaseId, {
393
- url: activeTab.url ?? undefined,
394
- title: activeTab.title ?? undefined
411
+ const session = this.sessions.createSession(clientId, activeTabId, leaseId, {
412
+ url: resolvedTab.url ?? undefined,
413
+ title: resolvedTab.title ?? undefined
395
414
  }, {
396
415
  parallelismPolicy
397
416
  });
@@ -406,8 +425,8 @@ export class OpsRuntime {
406
425
  this.sendResponse(message, {
407
426
  opsSessionId: session.id,
408
427
  activeTargetId: session.activeTargetId,
409
- url: activeTab.url ?? undefined,
410
- title: activeTab.title ?? undefined,
428
+ url: resolvedTab.url ?? undefined,
429
+ title: resolvedTab.title ?? undefined,
411
430
  leaseId: session.leaseId
412
431
  });
413
432
  }
@@ -437,11 +456,12 @@ export class OpsRuntime {
437
456
  const includeUrls = payload.includeUrls === true;
438
457
  const targets = await Promise.all(Array.from(session.targets.values()).map(async (target) => {
439
458
  const tab = await this.tabs.getTab(target.tabId);
459
+ const synthetic = session.syntheticTargets.get(target.targetId);
440
460
  return {
441
461
  targetId: target.targetId,
442
462
  type: "page",
443
- title: tab?.title ?? target.title,
444
- url: includeUrls ? tab?.url ?? target.url : undefined
463
+ title: resolveReportedTargetTitle(target, tab?.title, synthetic),
464
+ url: includeUrls ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined
445
465
  };
446
466
  }));
447
467
  this.sendResponse(message, { activeTargetId: session.activeTargetId || null, targets });
@@ -459,10 +479,66 @@ export class OpsRuntime {
459
479
  await this.tabs.activateTab(target.tabId).catch(() => undefined);
460
480
  }
461
481
  const tab = target ? await this.tabs.getTab(target.tabId) : null;
482
+ const synthetic = target ? session.syntheticTargets.get(target.targetId) : undefined;
462
483
  this.sendResponse(message, {
463
484
  activeTargetId: targetId,
464
- url: tab?.url ?? target?.url,
465
- title: tab?.title ?? target?.title
485
+ url: target ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined,
486
+ title: target ? resolveReportedTargetTitle(target, tab?.title, synthetic) : undefined
487
+ });
488
+ }
489
+ async handleTargetsRegisterCanvas(message, session) {
490
+ const payload = isRecord(message.payload) ? message.payload : {};
491
+ const targetId = typeof payload.targetId === "string" ? payload.targetId.trim() : "";
492
+ if (!targetId) {
493
+ this.sendError(message, buildError("invalid_request", "Missing targetId", false));
494
+ return;
495
+ }
496
+ const tabId = parseTabTargetId(targetId);
497
+ if (tabId === null) {
498
+ this.sendError(message, buildError("invalid_request", "Canvas targetId must be tab-<id>.", false));
499
+ return;
500
+ }
501
+ let tab = await this.tabs.getTab(tabId);
502
+ if (!tab) {
503
+ this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
504
+ return;
505
+ }
506
+ await this.tabs.waitForTabComplete(tabId, 5000).catch(() => undefined);
507
+ tab = await this.tabs.getTab(tabId) ?? tab;
508
+ if (!this.isAllowedCanvasTargetUrl(tab.url)) {
509
+ this.sendError(message, buildError("restricted_url", "Only the extension canvas tab can be registered.", false));
510
+ return;
511
+ }
512
+ const existing = session.targets.get(targetId);
513
+ if (existing) {
514
+ existing.url = tab.url ?? existing.url;
515
+ existing.title = tab.title ?? existing.title;
516
+ session.activeTargetId = targetId;
517
+ this.sendResponse(message, {
518
+ targetId,
519
+ url: existing.url,
520
+ title: existing.title,
521
+ adopted: false
522
+ });
523
+ return;
524
+ }
525
+ try {
526
+ await this.cdp.attach(tabId);
527
+ await this.enableTargetDomains(tabId);
528
+ }
529
+ catch (error) {
530
+ logError("ops.register_canvas_target", error, {
531
+ code: "canvas_target_attach_failed",
532
+ extra: { tabId, targetId }
533
+ });
534
+ }
535
+ const target = this.sessions.addTarget(session.id, tabId, { url: tab.url ?? undefined, title: tab.title ?? undefined });
536
+ session.activeTargetId = target.targetId;
537
+ this.sendResponse(message, {
538
+ targetId: target.targetId,
539
+ url: target.url,
540
+ title: target.title,
541
+ adopted: true
466
542
  });
467
543
  }
468
544
  async handleTargetsNew(message, session) {
@@ -544,11 +620,12 @@ export class OpsRuntime {
544
620
  const pages = await Promise.all(this.sessions.listNamedTargets(session.id).map(async ({ name, targetId }) => {
545
621
  const target = session.targets.get(targetId);
546
622
  const tab = target ? await this.tabs.getTab(target.tabId) : null;
623
+ const synthetic = session.syntheticTargets.get(targetId);
547
624
  return {
548
625
  name,
549
626
  targetId,
550
- url: tab?.url ?? target?.url,
551
- title: tab?.title ?? target?.title
627
+ url: resolveReportedTargetUrl(target, tab?.url, synthetic),
628
+ title: resolveReportedTargetTitle(target, tab?.title, synthetic)
552
629
  };
553
630
  }));
554
631
  this.sendResponse(message, { pages });
@@ -601,6 +678,24 @@ export class OpsRuntime {
601
678
  if (!target)
602
679
  return;
603
680
  await this.tabs.activateTab(target.tabId).catch(() => undefined);
681
+ const targetRecord = session.targets.get(target.targetId);
682
+ const syntheticHtml = decodeHtmlDataUrl(url);
683
+ if (syntheticHtml !== null) {
684
+ const result = await executeInTab(target.tabId, replaceDocumentWithHtmlScript, [{ html: syntheticHtml }]);
685
+ session.refStore.clearTarget(target.targetId);
686
+ session.syntheticTargets.set(target.targetId, {
687
+ url,
688
+ title: typeof result?.title === "string" && result.title.trim().length > 0
689
+ ? result.title
690
+ : targetRecord?.title
691
+ });
692
+ this.sendResponse(message, {
693
+ finalUrl: url,
694
+ status: undefined,
695
+ timingMs: Date.now() - start
696
+ });
697
+ return;
698
+ }
604
699
  const updated = await new Promise((resolve) => {
605
700
  chrome.tabs.update(target.tabId, { url }, (tab) => {
606
701
  resolve(tab ?? null);
@@ -608,10 +703,13 @@ export class OpsRuntime {
608
703
  });
609
704
  await this.tabs.waitForTabComplete(target.tabId, timeoutMs).catch(() => undefined);
610
705
  const refreshed = await this.tabs.getTab(target.tabId);
611
- const targetRecord = session.targets.get(target.targetId);
706
+ session.syntheticTargets.delete(target.targetId);
612
707
  if (targetRecord) {
613
- targetRecord.url = refreshed?.url ?? updated?.url ?? url;
614
- targetRecord.title = refreshed?.title ?? updated?.title ?? targetRecord.title;
708
+ session.targets.set(target.targetId, {
709
+ ...targetRecord,
710
+ url: refreshed?.url ?? updated?.url ?? url,
711
+ title: refreshed?.title ?? updated?.title ?? targetRecord.title
712
+ });
615
713
  }
616
714
  this.sendResponse(message, {
617
715
  finalUrl: refreshed?.url ?? updated?.url ?? url,
@@ -632,7 +730,7 @@ export class OpsRuntime {
632
730
  if (!selector)
633
731
  return;
634
732
  try {
635
- await this.waitForSelector(target.tabId, selector, state, timeoutMs);
733
+ await this.waitForSelector(target, selector, state, timeoutMs);
636
734
  this.sendResponse(message, { timingMs: Date.now() - start });
637
735
  }
638
736
  catch (error) {
@@ -671,10 +769,12 @@ export class OpsRuntime {
671
769
  return;
672
770
  }
673
771
  const tab = await this.tabs.getTab(target.tabId);
772
+ const targetRecord = session.targets.get(target.targetId);
773
+ const synthetic = session.syntheticTargets.get(target.targetId);
674
774
  this.sendResponse(message, {
675
775
  snapshotId: snapshot.snapshotId,
676
- url: tab?.url ?? undefined,
677
- title: tab?.title ?? undefined,
776
+ url: resolveReportedTargetUrl(targetRecord ?? null, tab?.url, synthetic),
777
+ title: resolveReportedTargetTitle(targetRecord ?? null, tab?.title, synthetic),
678
778
  content,
679
779
  truncated,
680
780
  nextCursor,
@@ -692,7 +792,7 @@ export class OpsRuntime {
692
792
  return;
693
793
  const start = Date.now();
694
794
  const before = await this.tabs.getTab(target.tabId);
695
- await this.dom.click(target.tabId, selector);
795
+ await this.runElementAction(target, selector, { type: "click" }, () => this.dom.click(target.tabId, selector));
696
796
  const after = await this.tabs.getTab(target.tabId);
697
797
  const navigated = Boolean(before?.url && after?.url && before.url !== after.url);
698
798
  this.sendResponse(message, { timingMs: Date.now() - start, navigated });
@@ -705,7 +805,7 @@ export class OpsRuntime {
705
805
  if (!target)
706
806
  return;
707
807
  const start = Date.now();
708
- await this.dom.hover(target.tabId, selector);
808
+ await this.runElementAction(target, selector, { type: "hover" }, () => this.dom.hover(target.tabId, selector));
709
809
  this.sendResponse(message, { timingMs: Date.now() - start });
710
810
  }
711
811
  async handlePress(message, session) {
@@ -722,7 +822,7 @@ export class OpsRuntime {
722
822
  if (payload.ref && !selector)
723
823
  return;
724
824
  const start = Date.now();
725
- await this.dom.press(target.tabId, selector, key);
825
+ await this.runCanvasPageAction(target, { type: "press", key }, selector, () => this.dom.press(target.tabId, selector, key));
726
826
  this.sendResponse(message, { timingMs: Date.now() - start });
727
827
  }
728
828
  async handleCheck(message, session, checked) {
@@ -733,7 +833,7 @@ export class OpsRuntime {
733
833
  if (!target)
734
834
  return;
735
835
  const start = Date.now();
736
- await this.dom.setChecked(target.tabId, selector, checked);
836
+ await this.runElementAction(target, selector, { type: "setChecked", checked }, () => this.dom.setChecked(target.tabId, selector, checked));
737
837
  this.sendResponse(message, { timingMs: Date.now() - start });
738
838
  }
739
839
  async handleType(message, session) {
@@ -751,7 +851,7 @@ export class OpsRuntime {
751
851
  if (!target)
752
852
  return;
753
853
  const start = Date.now();
754
- await this.dom.type(target.tabId, selector, text, payload.clear === true, payload.submit === true);
854
+ await this.runElementAction(target, selector, { type: "type", value: text, clear: payload.clear === true, submit: payload.submit === true }, () => this.dom.type(target.tabId, selector, text, payload.clear === true, payload.submit === true));
755
855
  this.sendResponse(message, { timingMs: Date.now() - start });
756
856
  }
757
857
  async handleSelect(message, session) {
@@ -768,7 +868,7 @@ export class OpsRuntime {
768
868
  const target = this.requireActiveTarget(session, message);
769
869
  if (!target)
770
870
  return;
771
- await this.dom.select(target.tabId, selector, values);
871
+ await this.runElementAction(target, selector, { type: "select", values: values }, () => this.dom.select(target.tabId, selector, values));
772
872
  this.sendResponse(message, {});
773
873
  }
774
874
  async handleScroll(message, session) {
@@ -781,7 +881,7 @@ export class OpsRuntime {
781
881
  const target = this.requireActiveTarget(session, message);
782
882
  if (!target)
783
883
  return;
784
- await this.dom.scroll(target.tabId, dy, selector);
884
+ await this.runCanvasPageAction(target, { type: "scroll", dy }, selector ?? null, () => this.dom.scroll(target.tabId, dy, selector));
785
885
  this.sendResponse(message, {});
786
886
  }
787
887
  async handleScrollIntoView(message, session) {
@@ -792,7 +892,7 @@ export class OpsRuntime {
792
892
  if (!target)
793
893
  return;
794
894
  const start = Date.now();
795
- await this.dom.scrollIntoView(target.tabId, selector);
895
+ await this.runElementAction(target, selector, { type: "scrollIntoView" }, () => this.dom.scrollIntoView(target.tabId, selector));
796
896
  this.sendResponse(message, { timingMs: Date.now() - start });
797
897
  }
798
898
  async handleDomGetHtml(message, session) {
@@ -809,7 +909,7 @@ export class OpsRuntime {
809
909
  const target = this.requireActiveTarget(session, message);
810
910
  if (!target)
811
911
  return;
812
- const html = await this.dom.getOuterHtml(target.tabId, selector);
912
+ const html = await this.runElementAction(target, selector, { type: "outerHTML" }, () => this.dom.getOuterHtml(target.tabId, selector));
813
913
  const truncated = html.length > maxChars;
814
914
  const outerHTML = truncated ? html.slice(0, maxChars) : html;
815
915
  this.sendResponse(message, { outerHTML, truncated });
@@ -828,7 +928,7 @@ export class OpsRuntime {
828
928
  const target = this.requireActiveTarget(session, message);
829
929
  if (!target)
830
930
  return;
831
- const text = await this.dom.getInnerText(target.tabId, selector);
931
+ const text = await this.runElementAction(target, selector, { type: "innerText" }, () => this.dom.getInnerText(target.tabId, selector));
832
932
  const truncated = text.length > maxChars;
833
933
  this.sendResponse(message, { text: truncated ? text.slice(0, maxChars) : text, truncated });
834
934
  }
@@ -846,7 +946,7 @@ export class OpsRuntime {
846
946
  const target = this.requireActiveTarget(session, message);
847
947
  if (!target)
848
948
  return;
849
- const value = await this.dom.getAttr(target.tabId, selector, name);
949
+ const value = await this.runElementAction(target, selector, { type: "getAttr", name }, () => this.dom.getAttr(target.tabId, selector, name));
850
950
  this.sendResponse(message, { value });
851
951
  }
852
952
  async handleDomGetValue(message, session) {
@@ -862,7 +962,7 @@ export class OpsRuntime {
862
962
  const target = this.requireActiveTarget(session, message);
863
963
  if (!target)
864
964
  return;
865
- const value = await this.dom.getValue(target.tabId, selector);
965
+ const value = await this.runElementAction(target, selector, { type: "getValue" }, () => this.dom.getValue(target.tabId, selector));
866
966
  this.sendResponse(message, { value });
867
967
  }
868
968
  async handleDomIsVisible(message, session) {
@@ -872,8 +972,11 @@ export class OpsRuntime {
872
972
  const target = this.requireActiveTarget(session, message);
873
973
  if (!target)
874
974
  return;
875
- const visible = await this.dom.isVisible(target.tabId, selector);
876
- this.sendResponse(message, { value: visible });
975
+ const visible = await this.runElementAction(target, selector, { type: "getSelectorState" }, async () => await this.dom.getSelectorState(target.tabId, selector));
976
+ const isVisible = typeof visible === "object" && visible !== null && "visible" in visible
977
+ ? Boolean(visible.visible)
978
+ : Boolean(visible);
979
+ this.sendResponse(message, { value: isVisible });
877
980
  }
878
981
  async handleDomIsEnabled(message, session) {
879
982
  const selector = this.resolveSelector(session, message.payload, message);
@@ -882,7 +985,7 @@ export class OpsRuntime {
882
985
  const target = this.requireActiveTarget(session, message);
883
986
  if (!target)
884
987
  return;
885
- const enabled = await this.dom.isEnabled(target.tabId, selector);
988
+ const enabled = await this.runElementAction(target, selector, { type: "isEnabled" }, () => this.dom.isEnabled(target.tabId, selector));
886
989
  this.sendResponse(message, { value: enabled });
887
990
  }
888
991
  async handleDomIsChecked(message, session) {
@@ -892,14 +995,34 @@ export class OpsRuntime {
892
995
  const target = this.requireActiveTarget(session, message);
893
996
  if (!target)
894
997
  return;
895
- const checked = await this.dom.isChecked(target.tabId, selector);
998
+ const checked = await this.runElementAction(target, selector, { type: "isChecked" }, () => this.dom.isChecked(target.tabId, selector));
896
999
  this.sendResponse(message, { value: checked });
897
1000
  }
1001
+ async handleCanvasRuntimePreviewBridge(message, session) {
1002
+ const payload = isRecord(message.payload) ? message.payload : {};
1003
+ const bindingId = typeof payload.bindingId === "string" ? payload.bindingId.trim() : "";
1004
+ const rootSelector = typeof payload.rootSelector === "string" ? payload.rootSelector.trim() : "";
1005
+ const html = typeof payload.html === "string" ? payload.html : "";
1006
+ if (!bindingId || !rootSelector) {
1007
+ this.sendError(message, buildError("invalid_request", "Missing bindingId or rootSelector", false));
1008
+ return;
1009
+ }
1010
+ const target = this.requireActiveTarget(session, message);
1011
+ if (!target)
1012
+ return;
1013
+ const result = await this.dom.applyRuntimePreviewBridge(target.tabId, bindingId, rootSelector, html);
1014
+ this.sendResponse(message, result);
1015
+ }
898
1016
  async handleClonePage(message, session) {
899
1017
  const payload = isRecord(message.payload) ? message.payload : {};
900
1018
  const target = this.requireActiveTarget(session, message);
901
1019
  if (!target)
902
1020
  return;
1021
+ const canvasCapture = await this.captureCanvasPage(target.tabId, target.targetId);
1022
+ if (canvasCapture) {
1023
+ this.sendResponse(message, { capture: canvasCapture });
1024
+ return;
1025
+ }
903
1026
  const capture = await this.dom.captureDom(target.tabId, "body", {
904
1027
  sanitize: payload.sanitize !== false,
905
1028
  maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
@@ -1062,10 +1185,13 @@ export class OpsRuntime {
1062
1185
  });
1063
1186
  }
1064
1187
  async enableSessionDomains(session) {
1188
+ await this.enableTargetDomains(session.tabId);
1189
+ }
1190
+ async enableTargetDomains(tabId) {
1065
1191
  try {
1066
- await this.cdp.sendCommand({ tabId: session.tabId }, "Runtime.enable", {});
1067
- await this.cdp.sendCommand({ tabId: session.tabId }, "Network.enable", {});
1068
- await this.cdp.sendCommand({ tabId: session.tabId }, "Performance.enable", {});
1192
+ await this.cdp.sendCommand({ tabId }, "Runtime.enable", {});
1193
+ await this.cdp.sendCommand({ tabId }, "Network.enable", {});
1194
+ await this.cdp.sendCommand({ tabId }, "Performance.enable", {});
1069
1195
  }
1070
1196
  catch (error) {
1071
1197
  logError("ops.enable_domains", error, { code: "enable_domains_failed" });
@@ -1297,12 +1423,99 @@ export class OpsRuntime {
1297
1423
  }
1298
1424
  if (target.url) {
1299
1425
  const restriction = isRestrictedUrl(target.url);
1300
- if (restriction.restricted) {
1426
+ if (restriction.restricted && !this.isAllowedCanvasTargetUrl(target.url)) {
1301
1427
  this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
1302
1428
  return null;
1303
1429
  }
1304
1430
  }
1305
- return { tabId: target.tabId, targetId: target.targetId };
1431
+ return { tabId: target.tabId, targetId: target.targetId, url: target.url };
1432
+ }
1433
+ isAllowedCanvasTargetUrl(rawUrl) {
1434
+ if (typeof rawUrl !== "string" || rawUrl.length === 0) {
1435
+ return false;
1436
+ }
1437
+ try {
1438
+ const allowedUrl = chrome.runtime.getURL("canvas.html");
1439
+ return rawUrl === allowedUrl || rawUrl.startsWith(`${allowedUrl}#`) || rawUrl.startsWith(`${allowedUrl}?`);
1440
+ }
1441
+ catch {
1442
+ return false;
1443
+ }
1444
+ }
1445
+ async captureCanvasPage(tabId, targetId) {
1446
+ if (!this.getCanvasPageState) {
1447
+ return null;
1448
+ }
1449
+ const state = this.getCanvasPageState(targetId);
1450
+ if (!state) {
1451
+ return null;
1452
+ }
1453
+ const previewHtml = typeof state.html === "string" && state.html.length > 0
1454
+ ? extractBodyHtml(state.html)
1455
+ : null;
1456
+ const shouldProbeLiveStage = Boolean(state.pendingMutation)
1457
+ || (canvasStateContainsRichMedia(state) && !htmlContainsRichMedia(previewHtml));
1458
+ if (shouldProbeLiveStage) {
1459
+ const liveStageCapture = await this.captureLiveCanvasStage(tabId);
1460
+ if (liveStageCapture) {
1461
+ return liveStageCapture;
1462
+ }
1463
+ const documentCapture = buildCanvasDocumentCapture(state);
1464
+ if (documentCapture) {
1465
+ return documentCapture;
1466
+ }
1467
+ }
1468
+ if (!previewHtml) {
1469
+ return buildCanvasDocumentCapture(state);
1470
+ }
1471
+ return {
1472
+ html: previewHtml,
1473
+ styles: {},
1474
+ warnings: ["canvas_state_capture"],
1475
+ inlineStyles: false
1476
+ };
1477
+ }
1478
+ async runElementAction(target, selector, action, fallback) {
1479
+ return await this.runCanvasPageAction(target, action, selector, fallback);
1480
+ }
1481
+ async runCanvasPageAction(target, action, selector, fallback) {
1482
+ if (!this.isAllowedCanvasTargetUrl(target.url) || !this.performCanvasPageAction) {
1483
+ return await fallback();
1484
+ }
1485
+ return await this.performCanvasPageAction(target.targetId, action, selector ?? null);
1486
+ }
1487
+ async captureLiveCanvasStage(tabId) {
1488
+ try {
1489
+ const results = await chrome.scripting.executeScript({
1490
+ target: { tabId },
1491
+ func: () => {
1492
+ const stage = document.getElementById("canvas-stage-inner");
1493
+ if (!(stage instanceof HTMLElement)) {
1494
+ return null;
1495
+ }
1496
+ const html = stage.innerHTML.trim();
1497
+ if (!html) {
1498
+ return null;
1499
+ }
1500
+ const width = stage.style.width || `${Math.max(stage.scrollWidth, 320)}px`;
1501
+ const height = stage.style.height || `${Math.max(stage.scrollHeight, 240)}px`;
1502
+ return `<body><main data-surface="canvas" style="position:relative;width:${width};min-height:${height};">${html}</main></body>`;
1503
+ }
1504
+ });
1505
+ const html = typeof results[0]?.result === "string" ? results[0].result : null;
1506
+ if (!html) {
1507
+ return null;
1508
+ }
1509
+ return {
1510
+ html,
1511
+ styles: {},
1512
+ warnings: ["canvas_state_capture"],
1513
+ inlineStyles: true
1514
+ };
1515
+ }
1516
+ catch {
1517
+ return null;
1518
+ }
1306
1519
  }
1307
1520
  resolveSelector(session, refOrPayload, message) {
1308
1521
  const ref = typeof refOrPayload === "string"
@@ -1324,10 +1537,10 @@ export class OpsRuntime {
1324
1537
  }
1325
1538
  return entry.selector;
1326
1539
  }
1327
- async waitForSelector(tabId, selector, state, timeoutMs) {
1540
+ async waitForSelector(target, selector, state, timeoutMs) {
1328
1541
  const start = Date.now();
1329
1542
  while (Date.now() - start < timeoutMs) {
1330
- const snapshot = await this.dom.getSelectorState(tabId, selector);
1543
+ const snapshot = await this.runElementAction(target, selector, { type: "getSelectorState" }, () => this.dom.getSelectorState(target.tabId, selector));
1331
1544
  if (state === "attached" && snapshot.attached)
1332
1545
  return;
1333
1546
  if (state === "visible" && snapshot.visible)
@@ -1729,6 +1942,257 @@ const paginate = (lines, startIndex, maxChars) => {
1729
1942
  };
1730
1943
  };
1731
1944
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1945
+ const parseTabTargetId = (targetId) => {
1946
+ const raw = targetId.startsWith("tab-") ? targetId.slice(4) : targetId;
1947
+ const parsed = Number.parseInt(raw, 10);
1948
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1949
+ return null;
1950
+ }
1951
+ return parsed;
1952
+ };
1953
+ const extractBodyHtml = (html) => {
1954
+ const bodyMatch = html.match(/<body\b[^>]*>[\s\S]*<\/body>/i);
1955
+ if (bodyMatch) {
1956
+ return bodyMatch[0];
1957
+ }
1958
+ return html;
1959
+ };
1960
+ const htmlContainsRichMedia = (html) => {
1961
+ return typeof html === "string" && /<(img|video|audio)\b/i.test(html);
1962
+ };
1963
+ const canvasStateContainsRichMedia = (state) => {
1964
+ const document = isRecord(state.document) ? state.document : null;
1965
+ const pages = Array.isArray(document?.pages) ? document.pages : [];
1966
+ const assets = Array.isArray(document?.assets) ? document.assets : [];
1967
+ const assetsById = new Map(assets.flatMap((asset) => typeof asset?.id === "string" ? [[asset.id, asset]] : []));
1968
+ return pages.some((page) => Array.isArray(page?.nodes) && page.nodes.some((node) => nodeContainsRichMedia(node, assetsById)));
1969
+ };
1970
+ const nodeContainsRichMedia = (node, assetsById) => {
1971
+ const tagName = readCanvasMediaTagName(node);
1972
+ if (tagName === "img" || tagName === "video" || tagName === "audio") {
1973
+ return true;
1974
+ }
1975
+ const assetIds = Array.isArray(node.metadata.assetIds)
1976
+ ? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
1977
+ : [];
1978
+ return assetIds.some((assetId) => {
1979
+ const asset = assetsById.get(assetId);
1980
+ const kind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : "";
1981
+ const mime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : "";
1982
+ return kind === "image" || kind === "video" || kind === "audio" || mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/");
1983
+ });
1984
+ };
1985
+ const readCanvasMediaTagName = (node) => {
1986
+ if (typeof node.props.tagName === "string" && node.props.tagName.trim().length > 0) {
1987
+ return node.props.tagName.trim().toLowerCase();
1988
+ }
1989
+ const codeSync = isRecord(node.metadata.codeSync) ? node.metadata.codeSync : null;
1990
+ if (codeSync && typeof codeSync.tagName === "string" && codeSync.tagName.trim().length > 0) {
1991
+ return codeSync.tagName.trim().toLowerCase();
1992
+ }
1993
+ return null;
1994
+ };
1995
+ const buildCanvasDocumentCapture = (state) => {
1996
+ const page = Array.isArray(state.document.pages) ? state.document.pages[0] : null;
1997
+ if (!page || !Array.isArray(page.nodes) || page.nodes.length === 0) {
1998
+ return null;
1999
+ }
2000
+ const { width, height } = computeCanvasDocumentBounds(page.nodes);
2001
+ const nodes = [...page.nodes]
2002
+ .sort(compareCanvasCaptureNodes)
2003
+ .map((node) => renderCanvasDocumentNode(state.document, node))
2004
+ .join("");
2005
+ return {
2006
+ html: `<body><main data-surface="canvas" style="position:relative;width:${width}px;min-height:${height}px;">${nodes}</main></body>`,
2007
+ styles: {},
2008
+ warnings: ["canvas_state_capture"],
2009
+ inlineStyles: true
2010
+ };
2011
+ };
2012
+ const computeCanvasDocumentBounds = (nodes) => {
2013
+ if (nodes.length === 0) {
2014
+ return { width: 1600, height: 1200 };
2015
+ }
2016
+ const maxX = Math.max(...nodes.map((node) => node.rect.x + node.rect.width));
2017
+ const maxY = Math.max(...nodes.map((node) => node.rect.y + node.rect.height));
2018
+ return {
2019
+ width: Math.max(maxX + 240, 1600),
2020
+ height: Math.max(maxY + 240, 1200)
2021
+ };
2022
+ };
2023
+ const compareCanvasCaptureNodes = (left, right) => {
2024
+ const rootOrder = Number(left.parentId !== null) - Number(right.parentId !== null);
2025
+ if (rootOrder !== 0) {
2026
+ return rootOrder;
2027
+ }
2028
+ const areaOrder = (right.rect.width * right.rect.height) - (left.rect.width * left.rect.height);
2029
+ if (areaOrder !== 0) {
2030
+ return areaOrder;
2031
+ }
2032
+ const verticalOrder = left.rect.y - right.rect.y;
2033
+ return verticalOrder !== 0 ? verticalOrder : left.rect.x - right.rect.x;
2034
+ };
2035
+ const renderCanvasDocumentNode = (document, node) => {
2036
+ const media = resolveCanvasDocumentMedia(document, node);
2037
+ const text = escapeCanvasHtml(nodeTextForCapture(node) || node.name);
2038
+ const style = serializeCanvasCaptureStyle({
2039
+ position: "absolute",
2040
+ left: `${node.rect.x}px`,
2041
+ top: `${node.rect.y}px`,
2042
+ width: `${Math.max(node.rect.width, 40)}px`,
2043
+ minHeight: `${Math.max(node.rect.height, readCanvasMediaTagName(node) === "audio" ? 64 : 40)}px`,
2044
+ overflow: "hidden",
2045
+ ...node.style
2046
+ });
2047
+ const title = escapeCanvasAttribute(`${node.kind} • ${node.name}`);
2048
+ if (media?.kind === "image" && media.src) {
2049
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><img src="${escapeCanvasAttribute(media.src)}" alt="${escapeCanvasAttribute(media.alt ?? node.name)}" loading="lazy" draggable="false" style="width:100%;height:100%;object-fit:cover;display:block;" /></div>`;
2050
+ }
2051
+ if (media?.kind === "video" && media.src) {
2052
+ const poster = media.poster ? ` poster="${escapeCanvasAttribute(media.poster)}"` : "";
2053
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><video src="${escapeCanvasAttribute(media.src)}"${poster} muted loop autoplay playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;display:block;"></video></div>`;
2054
+ }
2055
+ if (media?.kind === "audio" && media.src) {
2056
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><audio src="${escapeCanvasAttribute(media.src)}" controls preload="metadata" style="width:100%;display:block;"></audio>${text ? `<div style="margin-top:8px;font:500 12px/1.4 sans-serif;">${text}</div>` : ""}</div>`;
2057
+ }
2058
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}">${text}</div>`;
2059
+ };
2060
+ const nodeTextForCapture = (node) => {
2061
+ const raw = node.props.text ?? node.metadata.text;
2062
+ if (raw !== undefined && raw !== null) {
2063
+ return typeof raw === "string" ? raw : String(raw);
2064
+ }
2065
+ return node.kind === "text" || node.kind === "note" || node.kind === "component-instance"
2066
+ ? node.name
2067
+ : "";
2068
+ };
2069
+ const resolveCanvasDocumentMedia = (document, node) => {
2070
+ const tagName = readCanvasMediaTagName(node);
2071
+ const attributes = isRecord(node.props.attributes) ? node.props.attributes : {};
2072
+ const assetIds = Array.isArray(node.metadata.assetIds)
2073
+ ? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
2074
+ : [];
2075
+ const asset = assetIds.length > 0
2076
+ ? document.assets.find((entry) => entry.id === assetIds[0])
2077
+ : null;
2078
+ const assetKind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : null;
2079
+ const assetMime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : null;
2080
+ const src = typeof node.props.src === "string"
2081
+ ? node.props.src
2082
+ : typeof attributes.src === "string"
2083
+ ? attributes.src
2084
+ : typeof asset?.url === "string"
2085
+ ? asset.url
2086
+ : typeof asset?.repoPath === "string"
2087
+ ? asset.repoPath
2088
+ : null;
2089
+ const poster = typeof node.props.poster === "string"
2090
+ ? node.props.poster
2091
+ : typeof attributes.poster === "string"
2092
+ ? attributes.poster
2093
+ : null;
2094
+ const alt = typeof node.props.alt === "string"
2095
+ ? node.props.alt
2096
+ : typeof attributes.alt === "string"
2097
+ ? attributes.alt
2098
+ : node.name;
2099
+ if (tagName === "img" || assetKind === "image" || assetMime?.startsWith("image/")) {
2100
+ return { kind: "image", src, poster: null, alt };
2101
+ }
2102
+ if (tagName === "video" || assetKind === "video" || assetMime?.startsWith("video/")) {
2103
+ return { kind: "video", src, poster, alt };
2104
+ }
2105
+ if (tagName === "audio" || assetKind === "audio" || assetMime?.startsWith("audio/")) {
2106
+ return { kind: "audio", src, poster: null, alt };
2107
+ }
2108
+ return null;
2109
+ };
2110
+ const serializeCanvasCaptureStyle = (style) => {
2111
+ return Object.entries(style)
2112
+ .flatMap(([key, value]) => {
2113
+ if (typeof value !== "string" && typeof value !== "number") {
2114
+ return [];
2115
+ }
2116
+ const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
2117
+ const cssValue = typeof value === "number" && !CANVAS_CAPTURE_UNITLESS_STYLES.has(key) ? `${value}px` : String(value);
2118
+ return `${cssKey}:${escapeCanvasAttribute(cssValue)};`;
2119
+ })
2120
+ .join("");
2121
+ };
2122
+ const escapeCanvasHtml = (value) => {
2123
+ return value
2124
+ .replaceAll("&", "&amp;")
2125
+ .replaceAll("<", "&lt;")
2126
+ .replaceAll(">", "&gt;");
2127
+ };
2128
+ const escapeCanvasAttribute = (value) => {
2129
+ return escapeCanvasHtml(value)
2130
+ .replaceAll("\"", "&quot;")
2131
+ .replaceAll("'", "&#39;");
2132
+ };
2133
+ const CANVAS_CAPTURE_UNITLESS_STYLES = new Set(["fontWeight", "lineHeight", "opacity", "zIndex"]);
2134
+ const resolveReportedTargetUrl = (target, liveUrl, synthetic) => {
2135
+ if (typeof synthetic?.url === "string" && isHtmlDataUrl(synthetic.url)) {
2136
+ return synthetic.url;
2137
+ }
2138
+ if (typeof target?.url === "string" && isHtmlDataUrl(target.url)) {
2139
+ return target.url;
2140
+ }
2141
+ return liveUrl ?? target?.url;
2142
+ };
2143
+ const resolveReportedTargetTitle = (target, liveTitle, synthetic) => {
2144
+ if (typeof synthetic?.title === "string" && synthetic.title.length > 0) {
2145
+ return synthetic.title;
2146
+ }
2147
+ if (typeof target?.url === "string" && isHtmlDataUrl(target.url) && typeof target.title === "string" && target.title.length > 0) {
2148
+ return target.title;
2149
+ }
2150
+ return liveTitle ?? target?.title;
2151
+ };
2152
+ const isHtmlDataUrl = (url) => {
2153
+ return url.startsWith("data:text/html");
2154
+ };
2155
+ const decodeHtmlDataUrl = (url) => {
2156
+ if (!isHtmlDataUrl(url)) {
2157
+ return null;
2158
+ }
2159
+ const commaIndex = url.indexOf(",");
2160
+ if (commaIndex === -1) {
2161
+ return null;
2162
+ }
2163
+ const metadata = url.slice(0, commaIndex).toLowerCase();
2164
+ const payload = url.slice(commaIndex + 1);
2165
+ if (metadata.includes(";base64")) {
2166
+ const decoded = atob(payload);
2167
+ const bytes = Uint8Array.from(decoded, (char) => char.charCodeAt(0));
2168
+ return new TextDecoder().decode(bytes);
2169
+ }
2170
+ try {
2171
+ return decodeURIComponent(payload);
2172
+ }
2173
+ catch {
2174
+ return payload;
2175
+ }
2176
+ };
2177
+ const executeInTab = async (tabId, func, args) => {
2178
+ return await new Promise((resolve, reject) => {
2179
+ chrome.scripting.executeScript({ target: { tabId }, func: func, args }, (results) => {
2180
+ const lastError = chrome.runtime.lastError;
2181
+ if (lastError) {
2182
+ reject(new Error(lastError.message));
2183
+ return;
2184
+ }
2185
+ const [first] = results ?? [];
2186
+ resolve((first?.result ?? null));
2187
+ });
2188
+ });
2189
+ };
2190
+ function replaceDocumentWithHtmlScript(input) {
2191
+ document.open();
2192
+ document.write(input.html);
2193
+ document.close();
2194
+ return { title: document.title };
2195
+ }
1732
2196
  const withTimeout = async (promise, timeoutMs, message) => {
1733
2197
  return await new Promise((resolve, reject) => {
1734
2198
  const timeoutId = setTimeout(() => {