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
@@ -69,6 +69,7 @@ const ensureRoot = () => {
69
69
  <div class="odb-title">Annotate</div>
70
70
  <div class="odb-actions">
71
71
  <button class="odb-btn odb-btn-ghost" data-action="copy">Copy</button>
72
+ <button class="odb-btn odb-btn-ghost" data-action="send">Send</button>
72
73
  <button class="odb-btn odb-btn-ghost" data-action="cancel">Cancel</button>
73
74
  <button class="odb-btn odb-btn-primary" data-action="submit">Submit</button>
74
75
  <button class="odb-btn odb-btn-icon" data-action="close" aria-label="Close">×</button>
@@ -132,6 +133,12 @@ const ensureRoot = () => {
132
133
  setCopyFeedback("Copy failed");
133
134
  });
134
135
  }
136
+ if (action === "send") {
137
+ sendPayload(undefined, "annotate_all", "Annotation payload").catch((error) => {
138
+ logError("annotation.send_payload", error, { code: "annotation_send_failed" });
139
+ setCopyFeedback("Send failed");
140
+ });
141
+ }
135
142
  if (action === "cancel") {
136
143
  cancelSession();
137
144
  }
@@ -275,9 +282,15 @@ const handleClick = (event) => {
275
282
  return;
276
283
  event.preventDefault();
277
284
  event.stopPropagation();
278
- const target = state.hoverEl;
285
+ const clickedTarget = event.target instanceof Element && !isUiElement(event.target)
286
+ ? event.target
287
+ : null;
288
+ const target = clickedTarget ?? state.hoverEl;
279
289
  if (!target)
280
290
  return;
291
+ state.hoverEl = target;
292
+ state.hoverChain = buildAncestorChain(target);
293
+ state.hoverIndex = 0;
281
294
  if (event.shiftKey) {
282
295
  toggleSelection(target);
283
296
  }
@@ -372,17 +385,36 @@ const createNote = (element, id) => {
372
385
  note.innerHTML = `
373
386
  <div class="odb-note-header">
374
387
  <span>${describeElement(element)}</span>
375
- <button class="odb-note-close" aria-label="Remove">x</button>
388
+ <div class="odb-actions">
389
+ <button class="odb-btn odb-btn-ghost" data-role="copy-item" type="button">Copy</button>
390
+ <button class="odb-btn odb-btn-ghost" data-role="send-item" type="button">Send</button>
391
+ <button class="odb-note-close" data-role="remove-item" aria-label="Remove" type="button">x</button>
392
+ </div>
376
393
  </div>
377
394
  <textarea class="odb-note-input" rows="3" placeholder="Add annotation..."></textarea>
378
395
  `;
379
- const close = note.querySelector("button");
396
+ const close = note.querySelector("button[data-role='remove-item']");
380
397
  close.addEventListener("click", () => {
381
398
  note.remove();
382
399
  state.selections.delete(id);
383
400
  updateCount();
384
401
  scheduleConnectorUpdate();
385
402
  });
403
+ const copyButton = note.querySelector("button[data-role='copy-item']");
404
+ copyButton.addEventListener("click", () => {
405
+ void copyPayload([id], copyButton).catch((error) => {
406
+ logError("annotation.copy_item_payload", error, { code: "annotation_copy_item_failed", extra: { id } });
407
+ setButtonFeedback(copyButton, "Copy failed");
408
+ });
409
+ });
410
+ const sendButton = note.querySelector("button[data-role='send-item']");
411
+ sendButton.addEventListener("click", () => {
412
+ const selection = state.selections.get(id);
413
+ void sendPayload([id], "annotate_item", selection ? describeAnnotationItem(buildAnnotationItem(selection)) : "Annotation item", sendButton).catch((error) => {
414
+ logError("annotation.send_item_payload", error, { code: "annotation_send_item_failed", extra: { id } });
415
+ setButtonFeedback(sendButton, "Send failed");
416
+ });
417
+ });
386
418
  const textarea = note.querySelector("textarea");
387
419
  textarea.addEventListener("input", () => {
388
420
  const selection = state.selections.get(id);
@@ -495,7 +527,7 @@ const updateConnectors = () => {
495
527
  state.connectorLayer.appendChild(line);
496
528
  }
497
529
  };
498
- const buildPayload = async () => {
530
+ const buildCompletePayload = async () => {
499
531
  const url = window.location.href;
500
532
  const title = document.title;
501
533
  const timestamp = new Date().toISOString();
@@ -518,6 +550,15 @@ const buildPayload = async () => {
518
550
  annotations
519
551
  };
520
552
  };
553
+ const buildPayload = async (annotationIds) => {
554
+ const payload = await buildCompletePayload();
555
+ if (!annotationIds || annotationIds.length === 0) {
556
+ return payload;
557
+ }
558
+ return filterAnnotationPayload(payload, annotationIds, {
559
+ includeScreenshots: Boolean(payload.screenshots?.length)
560
+ });
561
+ };
521
562
  const extractBase64 = (dataUrl) => {
522
563
  if (!dataUrl.includes(","))
523
564
  return dataUrl;
@@ -841,12 +882,75 @@ const mergeOptions = (options) => {
841
882
  context: options?.context ?? DEFAULT_OPTIONS.context
842
883
  };
843
884
  };
844
- const copyPayload = async () => {
845
- const payload = await buildPayload();
885
+ const filterAnnotationPayload = (payload, annotationIds, options = {}) => {
886
+ const includeScreenshots = options.includeScreenshots ?? true;
887
+ const wanted = new Set(annotationIds);
888
+ const annotations = payload.annotations.filter((annotation) => wanted.has(annotation.id));
889
+ const screenshotIds = new Set(annotations
890
+ .map((annotation) => annotation.screenshotId)
891
+ .filter((value) => typeof value === "string" && value.length > 0));
892
+ const filtered = {
893
+ ...payload,
894
+ screenshotMode: includeScreenshots ? payload.screenshotMode : "none",
895
+ annotations: annotations.map((annotation) => {
896
+ if (includeScreenshots) {
897
+ return annotation;
898
+ }
899
+ const { screenshotId, ...next } = annotation;
900
+ void screenshotId;
901
+ return next;
902
+ })
903
+ };
904
+ if (includeScreenshots) {
905
+ filtered.screenshots = payload.screenshots?.filter((screenshot) => screenshotIds.has(screenshot.id));
906
+ }
907
+ else {
908
+ delete filtered.screenshots;
909
+ }
910
+ return filtered;
911
+ };
912
+ const describeAnnotationItem = (item) => {
913
+ const selector = item.selector?.trim().length ? item.selector : item.tag;
914
+ const label = item.note?.trim().length ? item.note.trim() : item.text?.trim();
915
+ return label ? `${selector} — ${label}` : selector;
916
+ };
917
+ const copyPayload = async (annotationIds, button) => {
918
+ const payload = await buildPayload(annotationIds);
846
919
  const text = JSON.stringify(payload);
847
920
  await writeClipboard(text);
921
+ if (button) {
922
+ setButtonFeedback(button, "Copied");
923
+ return;
924
+ }
848
925
  setCopyFeedback("Copied");
849
926
  };
927
+ const sendPayload = async (annotationIds, source, label, button) => {
928
+ const payload = await buildPayload(annotationIds);
929
+ await new Promise((resolve, reject) => {
930
+ chrome.runtime.sendMessage({
931
+ type: "annotation:sendPayload",
932
+ payload,
933
+ source,
934
+ label
935
+ }, (response) => {
936
+ const lastError = chrome.runtime.lastError;
937
+ if (lastError) {
938
+ reject(new Error(lastError.message));
939
+ return;
940
+ }
941
+ if (!response || response.ok !== true) {
942
+ reject(new Error(response?.error?.message ?? "Send failed"));
943
+ return;
944
+ }
945
+ resolve();
946
+ });
947
+ });
948
+ if (button) {
949
+ setButtonFeedback(button, "Sent");
950
+ return;
951
+ }
952
+ setCopyFeedback("Sent");
953
+ };
850
954
  const writeClipboard = async (value) => {
851
955
  if (navigator.clipboard?.writeText) {
852
956
  try {
@@ -874,17 +978,23 @@ const setCopyFeedback = (label) => {
874
978
  const button = state.copyButton;
875
979
  if (!button)
876
980
  return;
877
- const original = button.dataset.originalLabel ?? button.textContent ?? "Copy";
981
+ setButtonFeedback(button, label, true);
982
+ };
983
+ const setButtonFeedback = (button, label, useSharedTimer = false) => {
984
+ const original = button.dataset.originalLabel ?? button.textContent ?? button.getAttribute("aria-label") ?? "Action";
878
985
  if (!button.dataset.originalLabel) {
879
986
  button.dataset.originalLabel = original;
880
987
  }
881
988
  button.textContent = label;
882
- if (state.copyTimeout !== null) {
989
+ if (useSharedTimer && state.copyTimeout !== null) {
883
990
  window.clearTimeout(state.copyTimeout);
884
991
  }
885
- state.copyTimeout = window.setTimeout(() => {
992
+ const restore = window.setTimeout(() => {
886
993
  button.textContent = original;
887
994
  }, 1500);
995
+ if (useSharedTimer) {
996
+ state.copyTimeout = restore;
997
+ }
888
998
  };
889
999
  const bootstrap = () => {
890
1000
  if (window.__odbAnnotate) {
@@ -0,0 +1,163 @@
1
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
2
+ const readString = (value) => typeof value === "string" && value.trim().length > 0 ? value : null;
3
+ const formatCanvasUrl = (documentId, page) => {
4
+ const safePath = page.path && page.path.trim().length > 0 ? page.path : page.id;
5
+ return `canvas://${documentId}${safePath.startsWith("/") ? safePath : `/${safePath}`}`;
6
+ };
7
+ const readCanvasNodeText = (node) => {
8
+ const raw = node.props.text ?? node.metadata.text ?? node.name;
9
+ if (raw === undefined || raw === null) {
10
+ return undefined;
11
+ }
12
+ const text = typeof raw === "string" ? raw.trim() : String(raw).trim();
13
+ return text.length > 0 ? text.slice(0, 240) : undefined;
14
+ };
15
+ const formatCanvasStyleValue = (value) => {
16
+ if (typeof value === "number" && Number.isFinite(value)) {
17
+ return `${value}px`;
18
+ }
19
+ if (typeof value === "string" && value.trim().length > 0) {
20
+ return value;
21
+ }
22
+ return undefined;
23
+ };
24
+ const buildCanvasStyles = (node) => {
25
+ const style = isRecord(node.style) ? node.style : {};
26
+ return {
27
+ color: formatCanvasStyleValue(style.color),
28
+ backgroundColor: formatCanvasStyleValue(style.backgroundColor),
29
+ fontSize: formatCanvasStyleValue(style.fontSize),
30
+ fontFamily: formatCanvasStyleValue(style.fontFamily),
31
+ fontWeight: formatCanvasStyleValue(style.fontWeight),
32
+ lineHeight: formatCanvasStyleValue(style.lineHeight),
33
+ display: formatCanvasStyleValue(style.display),
34
+ position: formatCanvasStyleValue(style.position ?? "absolute")
35
+ };
36
+ };
37
+ const buildCanvasAttributes = (node) => {
38
+ const propsAttributes = isRecord(node.props.attributes) ? node.props.attributes : {};
39
+ const result = {
40
+ "data-node-id": node.id,
41
+ "data-canvas-kind": node.kind
42
+ };
43
+ for (const [key, value] of Object.entries(propsAttributes)) {
44
+ const next = readString(value);
45
+ if (next) {
46
+ result[key] = next;
47
+ }
48
+ }
49
+ const tagName = readString(node.props.tagName);
50
+ if (tagName) {
51
+ result["data-tag-name"] = tagName;
52
+ }
53
+ return result;
54
+ };
55
+ const resolveCanvasTag = (node) => {
56
+ return readString(node.props.tagName)?.toLowerCase()
57
+ ?? readString(isRecord(node.metadata.codeSync) ? node.metadata.codeSync.tagName : null)?.toLowerCase()
58
+ ?? node.kind;
59
+ };
60
+ export function stripAnnotationPayloadScreenshots(payload) {
61
+ const { screenshots, annotations, ...rest } = payload;
62
+ void screenshots;
63
+ return {
64
+ ...rest,
65
+ screenshotMode: "none",
66
+ annotations: annotations.map((annotation) => {
67
+ const { screenshotId, ...next } = annotation;
68
+ void screenshotId;
69
+ return next;
70
+ })
71
+ };
72
+ }
73
+ export function filterAnnotationPayload(payload, annotationIds, options = {}) {
74
+ const includeScreenshots = options.includeScreenshots ?? true;
75
+ const wanted = new Set(annotationIds);
76
+ const annotations = payload.annotations.filter((annotation) => wanted.has(annotation.id));
77
+ if (annotations.length === payload.annotations.length && includeScreenshots) {
78
+ return payload;
79
+ }
80
+ const screenshotIds = new Set(annotations
81
+ .map((annotation) => annotation.screenshotId)
82
+ .filter((value) => typeof value === "string" && value.length > 0));
83
+ const filtered = {
84
+ ...payload,
85
+ screenshotMode: includeScreenshots ? payload.screenshotMode : "none",
86
+ annotations: annotations.map((annotation) => {
87
+ if (includeScreenshots) {
88
+ return annotation;
89
+ }
90
+ const { screenshotId, ...next } = annotation;
91
+ void screenshotId;
92
+ return next;
93
+ })
94
+ };
95
+ if (includeScreenshots) {
96
+ filtered.screenshots = payload.screenshots?.filter((screenshot) => screenshotIds.has(screenshot.id));
97
+ return filtered;
98
+ }
99
+ delete filtered.screenshots;
100
+ return filtered;
101
+ }
102
+ export function describeAnnotationItem(item) {
103
+ const selector = item.selector?.trim().length ? item.selector : item.tag;
104
+ const label = item.note?.trim().length ? item.note.trim() : item.text?.trim();
105
+ return label ? `${selector} — ${label}` : selector;
106
+ }
107
+ export function formatDispatchSourceLabel(source) {
108
+ switch (source) {
109
+ case "annotate_item":
110
+ return "annotation item";
111
+ case "annotate_all":
112
+ return "annotation payload";
113
+ case "popup_item":
114
+ return "popup annotation item";
115
+ case "popup_all":
116
+ return "popup annotation payload";
117
+ case "canvas_item":
118
+ return "canvas annotation item";
119
+ case "canvas_all":
120
+ return "canvas annotation payload";
121
+ default:
122
+ return "annotation payload";
123
+ }
124
+ }
125
+ export function buildCanvasAnnotationPayload(options) {
126
+ const nodesById = new Map(options.page.nodes.map((node) => [node.id, node]));
127
+ const annotations = options.drafts.flatMap((draft) => {
128
+ const node = nodesById.get(draft.nodeId);
129
+ if (!node) {
130
+ return [];
131
+ }
132
+ const tag = resolveCanvasTag(node);
133
+ return [{
134
+ id: node.id,
135
+ selector: `[data-node-id="${node.id}"]`,
136
+ tag,
137
+ idAttr: node.id,
138
+ classes: [`canvas-node`, `canvas-${node.kind}`],
139
+ text: readCanvasNodeText(node),
140
+ rect: {
141
+ x: node.rect.x,
142
+ y: node.rect.y,
143
+ width: node.rect.width,
144
+ height: node.rect.height
145
+ },
146
+ attributes: buildCanvasAttributes(node),
147
+ a11y: {
148
+ role: readString(isRecord(node.metadata.accessibility) ? node.metadata.accessibility.role : null) ?? undefined,
149
+ label: readString(isRecord(node.metadata.accessibility) ? node.metadata.accessibility.label : null) ?? undefined
150
+ },
151
+ styles: buildCanvasStyles(node),
152
+ note: readString(draft.note) ?? undefined
153
+ }];
154
+ });
155
+ return {
156
+ url: formatCanvasUrl(options.document.documentId, options.page),
157
+ title: `${options.document.title} • ${options.page.name}`,
158
+ timestamp: new Date().toISOString(),
159
+ context: options.context,
160
+ screenshotMode: "none",
161
+ annotations
162
+ };
163
+ }
@@ -3,10 +3,17 @@ import { NativePortManager } from "./services/NativePortManager.js";
3
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
4
  import { logError } from "./logging.js";
5
5
  import { OpsRuntime } from "./ops/ops-runtime.js";
6
+ import { CanvasRuntime } from "./canvas/canvas-runtime.js";
7
+ import { formatDispatchSourceLabel, stripAnnotationPayloadScreenshots } from "./annotation-payload.js";
6
8
  const connection = new ConnectionManager();
9
+ const canvasRuntime = new CanvasRuntime({
10
+ send: (message) => connection.sendCanvasMessage(message)
11
+ });
7
12
  const opsRuntime = new OpsRuntime({
8
13
  send: (message) => connection.sendOpsMessage(message),
9
- cdp: connection.getCdpRouter()
14
+ cdp: connection.getCdpRouter(),
15
+ getCanvasPageState: (targetId) => canvasRuntime.getPageStateByTargetId(targetId),
16
+ performCanvasPageAction: (targetId, action, selector) => canvasRuntime.performPageAction(targetId, action, selector)
10
17
  });
11
18
  const nativePort = new NativePortManager({
12
19
  onMessage: (payload) => {
@@ -31,10 +38,13 @@ const ANNOTATION_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
31
38
  const ANNOTATION_REQUEST_TIMEOUT_MS = 120_000;
32
39
  const LAST_ANNOTATION_META_KEY = "annotationLastMeta";
33
40
  const LAST_ANNOTATION_PAYLOAD_KEY = "annotationLastPayloadSansScreenshots";
41
+ const LAST_AGENT_ANNOTATION_META_KEY = "annotationAgentMeta";
42
+ const LAST_AGENT_ANNOTATION_PAYLOAD_KEY = "annotationAgentPayloadSansScreenshots";
34
43
  const BADGE_CONNECTED_DOT_COLOR = "#16a34a";
35
44
  const BADGE_DISCONNECTED_DOT_COLOR = "#dc2626";
36
45
  const annotationSessions = new Map();
37
46
  let lastAnnotationFull = null;
47
+ let lastAgentAnnotationFull = null;
38
48
  connection.onAnnotationCommand((command) => {
39
49
  handleRelayAnnotationCommand(command).catch((error) => {
40
50
  logError("annotation.relay_command", error, { code: "annotation_command_failed" });
@@ -43,6 +53,9 @@ connection.onAnnotationCommand((command) => {
43
53
  connection.onOpsMessage((message) => {
44
54
  opsRuntime.handleMessage(message);
45
55
  });
56
+ connection.onCanvasMessage((message) => {
57
+ canvasRuntime.handleMessage(message);
58
+ });
46
59
  const RESTRICTED_PROTOCOLS = new Set([
47
60
  "chrome:",
48
61
  "chrome-extension:",
@@ -156,6 +169,8 @@ const buildRelayHealthNote = (health) => {
156
169
  return "Annotation channel disconnected. Keep the extension open and retry.";
157
170
  case "ops_disconnected":
158
171
  return "Ops channel disconnected. Start a new session and retry.";
172
+ case "canvas_disconnected":
173
+ return "Canvas channel disconnected. Reopen the design canvas command and retry.";
159
174
  case "cdp_disconnected":
160
175
  return "No CDP clients connected. Start a session and retry.";
161
176
  case "relay_down":
@@ -303,6 +318,7 @@ const fetchRelayHealth = async (port) => {
303
318
  const cdpConnected = data.cdpConnected === true;
304
319
  const annotationConnected = data.annotationConnected === true;
305
320
  const opsConnected = data.opsConnected === true;
321
+ const canvasConnected = data.canvasConnected === true;
306
322
  const pairingRequired = data.pairingRequired === true;
307
323
  const ok = extensionConnected && handshake;
308
324
  return {
@@ -313,6 +329,7 @@ const fetchRelayHealth = async (port) => {
313
329
  cdpConnected,
314
330
  annotationConnected,
315
331
  opsConnected,
332
+ canvasConnected,
316
333
  pairingRequired
317
334
  };
318
335
  }
@@ -675,19 +692,7 @@ const validatePayloadSize = (payload) => {
675
692
  const generateAnnotationRequestId = () => {
676
693
  return crypto.randomUUID();
677
694
  };
678
- const stripScreenshots = (payload) => {
679
- const { screenshots, annotations, ...rest } = payload;
680
- void screenshots;
681
- return {
682
- ...rest,
683
- annotations: annotations.map((item) => {
684
- const { screenshotId, ...restItem } = item;
685
- void screenshotId;
686
- return restItem;
687
- })
688
- };
689
- };
690
- const buildLastAnnotationMeta = (requestId, response, hasFullPayloadInMemory) => {
695
+ const buildLastAnnotationMeta = (requestId, response, hasFullPayloadInMemory, extras = {}) => {
691
696
  const payload = response.payload;
692
697
  const annotationCount = payload ? payload.annotations.length : undefined;
693
698
  const screenshotCount = payload?.screenshots?.length ?? 0;
@@ -695,6 +700,8 @@ const buildLastAnnotationMeta = (requestId, response, hasFullPayloadInMemory) =>
695
700
  requestId,
696
701
  status: response.status,
697
702
  error: response.error,
703
+ source: extras.source,
704
+ label: extras.label,
698
705
  url: payload?.url,
699
706
  title: payload?.title,
700
707
  timestamp: payload?.timestamp,
@@ -712,6 +719,12 @@ const persistLastAnnotation = async (meta, payload) => {
712
719
  [LAST_ANNOTATION_PAYLOAD_KEY]: payload
713
720
  });
714
721
  };
722
+ const persistAgentAnnotation = async (meta, payload) => {
723
+ await setStorage({
724
+ [LAST_AGENT_ANNOTATION_META_KEY]: meta,
725
+ [LAST_AGENT_ANNOTATION_PAYLOAD_KEY]: payload
726
+ });
727
+ };
715
728
  const loadPersistedLastAnnotation = async () => {
716
729
  const data = await new Promise((resolve) => {
717
730
  chrome.storage.local.get([LAST_ANNOTATION_META_KEY, LAST_ANNOTATION_PAYLOAD_KEY], (items) => resolve(items));
@@ -722,6 +735,77 @@ const loadPersistedLastAnnotation = async () => {
722
735
  const payload = payloadRecord && typeof payloadRecord === "object" ? payloadRecord : null;
723
736
  return { meta, payload };
724
737
  };
738
+ const loadPersistedAgentAnnotation = async () => {
739
+ const data = await new Promise((resolve) => {
740
+ chrome.storage.local.get([LAST_AGENT_ANNOTATION_META_KEY, LAST_AGENT_ANNOTATION_PAYLOAD_KEY], (items) => resolve(items));
741
+ });
742
+ const metaRecord = data[LAST_AGENT_ANNOTATION_META_KEY];
743
+ const payloadRecord = data[LAST_AGENT_ANNOTATION_PAYLOAD_KEY];
744
+ const meta = metaRecord && typeof metaRecord === "object" ? metaRecord : null;
745
+ const payload = payloadRecord && typeof payloadRecord === "object" ? payloadRecord : null;
746
+ return { meta, payload };
747
+ };
748
+ const isAnnotationPayload = (value) => {
749
+ if (!value || typeof value !== "object") {
750
+ return false;
751
+ }
752
+ const payload = value;
753
+ return typeof payload.url === "string"
754
+ && typeof payload.timestamp === "string"
755
+ && typeof payload.screenshotMode === "string"
756
+ && Array.isArray(payload.annotations);
757
+ };
758
+ const storeAgentAnnotationPayload = async (payload, source, label) => {
759
+ if (!validatePayloadSize(payload)) {
760
+ throw new Error("Annotation payload exceeded size limits.");
761
+ }
762
+ const response = {
763
+ version: 1,
764
+ requestId: crypto.randomUUID(),
765
+ status: "ok",
766
+ payload
767
+ };
768
+ const meta = buildLastAnnotationMeta(response.requestId, response, true, {
769
+ source,
770
+ label: label?.trim().length ? label.trim() : formatDispatchSourceLabel(source)
771
+ });
772
+ lastAgentAnnotationFull = { meta, payload };
773
+ await persistAgentAnnotation(meta, stripAnnotationPayloadScreenshots(payload));
774
+ return meta;
775
+ };
776
+ const loadAgentAnnotationPayload = async (includeScreenshots) => {
777
+ if (includeScreenshots && lastAgentAnnotationFull) {
778
+ return {
779
+ version: 1,
780
+ requestId: crypto.randomUUID(),
781
+ status: "ok",
782
+ payload: lastAgentAnnotationFull.payload
783
+ };
784
+ }
785
+ if (lastAgentAnnotationFull) {
786
+ return {
787
+ version: 1,
788
+ requestId: crypto.randomUUID(),
789
+ status: "ok",
790
+ payload: stripAnnotationPayloadScreenshots(lastAgentAnnotationFull.payload)
791
+ };
792
+ }
793
+ const stored = await loadPersistedAgentAnnotation();
794
+ if (stored.payload) {
795
+ return {
796
+ version: 1,
797
+ requestId: crypto.randomUUID(),
798
+ status: "ok",
799
+ payload: stored.payload
800
+ };
801
+ }
802
+ return {
803
+ version: 1,
804
+ requestId: crypto.randomUUID(),
805
+ status: "error",
806
+ error: { code: "payload_unavailable", message: "No agent-dispatched annotation payload available." }
807
+ };
808
+ };
725
809
  async function handleNativePortMessage(payload) {
726
810
  if (!payload || typeof payload !== "object") {
727
811
  return;
@@ -746,6 +830,14 @@ const handleRelayAnnotationCommand = async (command, transport = "relay") => {
746
830
  await cancelAnnotationSession(payload.requestId, transport);
747
831
  return;
748
832
  }
833
+ if (payload.command === "fetch_stored") {
834
+ const stored = await loadAgentAnnotationPayload(payload.options?.includeScreenshots === true);
835
+ sendAnnotationResponse({
836
+ ...stored,
837
+ requestId: payload.requestId
838
+ }, transport);
839
+ return;
840
+ }
749
841
  try {
750
842
  await startAnnotationSession(payload, transport);
751
843
  sendAnnotationEvent({
@@ -805,7 +897,7 @@ const handleAnnotationComplete = (requestId, payload) => {
805
897
  const meta = buildLastAnnotationMeta(requestId, response, true);
806
898
  lastAnnotationFull = { meta, payload };
807
899
  const storageMeta = { ...meta, hasFullPayloadInMemory: false };
808
- const sanitizedPayload = stripScreenshots(payload);
900
+ const sanitizedPayload = stripAnnotationPayloadScreenshots(payload);
809
901
  persistLastAnnotation(storageMeta, sanitizedPayload).catch((error) => {
810
902
  logError("annotation.persist_sanitized_payload", error, { code: "annotation_persist_failed" });
811
903
  });
@@ -1237,9 +1329,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1237
1329
  if (message.includeScreenshots) {
1238
1330
  const response = {
1239
1331
  type: "annotation:payloadResult",
1240
- payload: null,
1332
+ payload: stored.payload,
1241
1333
  meta: storedMeta,
1242
- source: "none",
1334
+ source: stored.payload ? "storage" : "none",
1243
1335
  warning: "Full payload not available; screenshots may have been dropped."
1244
1336
  };
1245
1337
  sendResponse(response);
@@ -1248,7 +1340,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1248
1340
  if (lastAnnotationFull) {
1249
1341
  const response = {
1250
1342
  type: "annotation:payloadResult",
1251
- payload: stripScreenshots(lastAnnotationFull.payload),
1343
+ payload: stripAnnotationPayloadScreenshots(lastAnnotationFull.payload),
1252
1344
  meta: { ...lastAnnotationFull.meta, hasFullPayloadInMemory: true },
1253
1345
  source: "memory"
1254
1346
  };
@@ -1285,6 +1377,39 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1285
1377
  });
1286
1378
  return true;
1287
1379
  }
1380
+ if (message.type === "annotation:sendPayload") {
1381
+ (async () => {
1382
+ if (!isAnnotationPayload(message.payload)) {
1383
+ const response = {
1384
+ type: "annotation:sendPayloadResult",
1385
+ ok: false,
1386
+ meta: null,
1387
+ error: { code: "invalid_request", message: "Invalid annotation payload." }
1388
+ };
1389
+ sendResponse(response);
1390
+ return;
1391
+ }
1392
+ const meta = await storeAgentAnnotationPayload(message.payload, message.source ?? "popup_all", message.label);
1393
+ const response = {
1394
+ type: "annotation:sendPayloadResult",
1395
+ ok: true,
1396
+ meta: { ...meta, hasFullPayloadInMemory: true }
1397
+ };
1398
+ sendResponse(response);
1399
+ })().catch((error) => {
1400
+ const response = {
1401
+ type: "annotation:sendPayloadResult",
1402
+ ok: false,
1403
+ meta: null,
1404
+ error: {
1405
+ code: "payload_too_large",
1406
+ message: error instanceof Error ? error.message : "Annotation dispatch failed."
1407
+ }
1408
+ };
1409
+ sendResponse(response);
1410
+ });
1411
+ return true;
1412
+ }
1288
1413
  if (message.type === "annotation:capture") {
1289
1414
  (async () => {
1290
1415
  const tab = sender.tab;
@@ -1319,3 +1444,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1319
1444
  }
1320
1445
  return false;
1321
1446
  });
1447
+ chrome.runtime.onConnect.addListener((port) => {
1448
+ if (port.name === "canvas-page") {
1449
+ canvasRuntime.attachPort(port);
1450
+ }
1451
+ });