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
@@ -0,0 +1,1061 @@
1
+ import { CANVAS_PROTOCOL_VERSION, MAX_CANVAS_PAYLOAD_BYTES } from "../types.js";
2
+ import { logError } from "../logging.js";
3
+ import { TabManager } from "../services/TabManager.js";
4
+ import { TargetSessionCoordinator, createCoordinatorId } from "../ops/target-session-coordinator.js";
5
+ import { normalizeCanvasSessionSummary, normalizeCanvasTargetStateSummaries } from "./model.js";
6
+ const OVERLAY_STYLE = `
7
+ #opendevbrowser-canvas-style,
8
+ .opendevbrowser-canvas-highlight {
9
+ box-sizing: border-box;
10
+ }
11
+ .opendevbrowser-canvas-highlight {
12
+ outline: 2px solid #20d5c6 !important;
13
+ outline-offset: 3px !important;
14
+ }
15
+ .opendevbrowser-canvas-overlay {
16
+ position: fixed;
17
+ top: 16px;
18
+ right: 16px;
19
+ z-index: 2147483647;
20
+ max-width: 320px;
21
+ padding: 12px 14px;
22
+ border-radius: 14px;
23
+ border: 1px solid rgba(255,255,255,0.16);
24
+ background: rgba(7,17,29,0.92);
25
+ color: #f3f6fb;
26
+ font: 12px/1.4 "Segoe UI", sans-serif;
27
+ box-shadow: 0 18px 40px rgba(0,0,0,0.3);
28
+ }
29
+ .opendevbrowser-canvas-overlay strong {
30
+ display: block;
31
+ margin-bottom: 4px;
32
+ }
33
+ `;
34
+ export class CanvasRuntime {
35
+ sendEnvelope;
36
+ tabs = new TabManager();
37
+ sessions = new TargetSessionCoordinator();
38
+ pagePorts = new Map();
39
+ pendingPageActions = new Map();
40
+ constructor(options) {
41
+ this.sendEnvelope = options.send;
42
+ }
43
+ attachPort(port) {
44
+ if (port.name !== "canvas-page") {
45
+ return;
46
+ }
47
+ const tabId = port.sender?.tab?.id;
48
+ if (typeof tabId !== "number") {
49
+ port.disconnect();
50
+ return;
51
+ }
52
+ let ports = this.pagePorts.get(tabId);
53
+ if (!ports) {
54
+ ports = new Set();
55
+ this.pagePorts.set(tabId, ports);
56
+ }
57
+ ports.add(port);
58
+ port.onDisconnect.addListener(() => {
59
+ const current = this.pagePorts.get(tabId);
60
+ current?.delete(port);
61
+ if (current && current.size === 0) {
62
+ this.pagePorts.delete(tabId);
63
+ }
64
+ for (const [requestId, pending] of this.pendingPageActions.entries()) {
65
+ if (pending.tabId !== tabId) {
66
+ continue;
67
+ }
68
+ clearTimeout(pending.timeoutId);
69
+ pending.reject(new Error("Canvas page disconnected before action completed."));
70
+ this.pendingPageActions.delete(requestId);
71
+ }
72
+ });
73
+ port.onMessage.addListener((message) => {
74
+ const record = isRecord(message) ? message : null;
75
+ if (!record) {
76
+ return;
77
+ }
78
+ this.handlePagePortMessage(tabId, record);
79
+ });
80
+ this.postCanvasState(port, this.getPageStateByTabId(tabId), "canvas-page:init");
81
+ }
82
+ getPageStateByTargetId(targetId) {
83
+ try {
84
+ return this.getPageStateByTabId(parseTargetId(targetId));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ async performPageAction(targetId, action, selector, timeoutMs = 2500) {
91
+ const tabId = parseTargetId(targetId);
92
+ const port = await this.waitForPagePort(tabId, timeoutMs);
93
+ return await new Promise((resolve, reject) => {
94
+ const requestId = crypto.randomUUID();
95
+ const timeoutId = setTimeout(() => {
96
+ this.pendingPageActions.delete(requestId);
97
+ reject(new Error("Canvas page action timed out."));
98
+ }, timeoutMs);
99
+ this.pendingPageActions.set(requestId, { tabId, resolve, reject, timeoutId });
100
+ try {
101
+ port.postMessage({
102
+ type: "canvas-page-action-request",
103
+ requestId,
104
+ selector: selector ?? null,
105
+ action
106
+ });
107
+ }
108
+ catch (error) {
109
+ clearTimeout(timeoutId);
110
+ this.pendingPageActions.delete(requestId);
111
+ reject(error instanceof Error ? error : new Error("Canvas page action failed."));
112
+ }
113
+ });
114
+ }
115
+ handleMessage(message) {
116
+ if (message.type === "canvas_hello") {
117
+ this.handleHello(message);
118
+ return;
119
+ }
120
+ if (message.type === "canvas_ping") {
121
+ this.handlePing(message);
122
+ return;
123
+ }
124
+ if (message.type === "canvas_event" && message.event === "canvas_client_disconnected") {
125
+ this.handleClientDisconnected(message);
126
+ return;
127
+ }
128
+ if (message.type === "canvas_request") {
129
+ void this.handleRequest(message).catch((error) => {
130
+ logError("canvas.handle_request", error, { code: "canvas_request_failed", extra: { command: message.command } });
131
+ this.sendError(message, normalizeCanvasError(error));
132
+ });
133
+ }
134
+ }
135
+ handleHello(message) {
136
+ if (message.version !== CANVAS_PROTOCOL_VERSION) {
137
+ this.sendError({ requestId: "canvas_hello", clientId: message.clientId, canvasSessionId: undefined }, {
138
+ code: "not_supported",
139
+ message: "Unsupported canvas protocol version.",
140
+ retryable: false,
141
+ details: { supported: [CANVAS_PROTOCOL_VERSION], received: message.version }
142
+ });
143
+ return;
144
+ }
145
+ const ack = {
146
+ type: "canvas_hello_ack",
147
+ version: CANVAS_PROTOCOL_VERSION,
148
+ clientId: message.clientId,
149
+ maxPayloadBytes: MAX_CANVAS_PAYLOAD_BYTES,
150
+ capabilities: [
151
+ "canvas.tab.open",
152
+ "canvas.tab.close",
153
+ "canvas.tab.sync",
154
+ "canvas.overlay.mount",
155
+ "canvas.overlay.unmount",
156
+ "canvas.overlay.select",
157
+ "canvas.overlay.sync"
158
+ ]
159
+ };
160
+ this.sendEnvelope(ack);
161
+ }
162
+ handlePing(message) {
163
+ const pong = {
164
+ type: "canvas_pong",
165
+ id: message.id,
166
+ clientId: message.clientId
167
+ };
168
+ this.sendEnvelope(pong);
169
+ }
170
+ handleClientDisconnected(message) {
171
+ const clientId = message.clientId;
172
+ if (!clientId) {
173
+ return;
174
+ }
175
+ for (const session of this.sessions.listOwnedBy(clientId)) {
176
+ void this.closeRuntimeSession(session, "client_disconnected");
177
+ }
178
+ }
179
+ handlePagePortMessage(tabId, message) {
180
+ if (message.type === "canvas-page-action-response") {
181
+ const pending = this.pendingPageActions.get(message.requestId);
182
+ if (!pending) {
183
+ return;
184
+ }
185
+ clearTimeout(pending.timeoutId);
186
+ this.pendingPageActions.delete(message.requestId);
187
+ if (message.ok) {
188
+ pending.resolve(message.value);
189
+ }
190
+ else {
191
+ pending.reject(new Error(message.error || "Canvas page action failed."));
192
+ }
193
+ return;
194
+ }
195
+ const session = this.sessions.getByTabId(tabId);
196
+ if (message.type === "canvas-page-ready" || message.type === "canvas-page-request-state") {
197
+ this.broadcastCanvasState(tabId, "canvas-page:init");
198
+ return;
199
+ }
200
+ if (!session) {
201
+ return;
202
+ }
203
+ if (message.type === "canvas-page-view-state") {
204
+ this.mergeEditorState(session, message.viewport, message.selection);
205
+ this.broadcastCanvasState(tabId, "canvas-page:update");
206
+ return;
207
+ }
208
+ if (message.type === "canvas-page-patch-request") {
209
+ if (!Array.isArray(message.patches) || message.patches.length === 0 || typeof message.baseRevision !== "number") {
210
+ return;
211
+ }
212
+ this.mergeEditorState(session, undefined, message.selection);
213
+ session.pendingMutation = true;
214
+ this.broadcastCanvasState(tabId, "canvas-page:update");
215
+ this.sendEvent({
216
+ type: "canvas_event",
217
+ clientId: session.ownerClientId,
218
+ canvasSessionId: session.id,
219
+ event: "canvas_patch_requested",
220
+ payload: {
221
+ targetId: session.designTabTargetId,
222
+ documentId: session.document.documentId,
223
+ baseRevision: message.baseRevision,
224
+ patches: message.patches,
225
+ selection: session.selection
226
+ }
227
+ });
228
+ }
229
+ }
230
+ async handleRequest(message) {
231
+ switch (message.command) {
232
+ case "canvas.tab.open":
233
+ this.sendResponse(message, await this.openTab(message));
234
+ return;
235
+ case "canvas.tab.close":
236
+ this.sendResponse(message, await this.closeTab(message));
237
+ return;
238
+ case "canvas.tab.sync":
239
+ this.sendResponse(message, await this.syncTab(message));
240
+ return;
241
+ case "canvas.overlay.mount":
242
+ this.sendResponse(message, await this.mountOverlay(message));
243
+ return;
244
+ case "canvas.overlay.unmount":
245
+ this.sendResponse(message, await this.unmountOverlay(message));
246
+ return;
247
+ case "canvas.overlay.select":
248
+ this.sendResponse(message, await this.selectOverlay(message));
249
+ return;
250
+ case "canvas.overlay.sync":
251
+ this.sendResponse(message, await this.syncOverlay(message));
252
+ return;
253
+ default:
254
+ this.sendError(message, {
255
+ code: "not_supported",
256
+ message: `Unsupported canvas command: ${message.command}`,
257
+ retryable: false
258
+ });
259
+ }
260
+ }
261
+ async openTab(message) {
262
+ const record = requireRecord(message.payload, "payload");
263
+ const document = requireCanvasDocument(record.document);
264
+ const previewMode = requireEnum(record.previewMode, "previewMode", ["focused", "pinned", "background"]);
265
+ const tab = await this.createTab(chrome.runtime.getURL("canvas.html"), previewMode);
266
+ const tabId = requireTabId(tab);
267
+ const session = this.createOrReplaceSession(message, tabId, document, previewMode, record);
268
+ this.broadcastCanvasState(tabId, "canvas-page:init");
269
+ this.sendEvent({
270
+ type: "canvas_event",
271
+ clientId: message.clientId,
272
+ canvasSessionId: session.id,
273
+ event: "canvas_session_created",
274
+ payload: { tabId, targetId: session.designTabTargetId }
275
+ });
276
+ return {
277
+ targetId: session.designTabTargetId,
278
+ previewState: session.previewState
279
+ };
280
+ }
281
+ async closeTab(message) {
282
+ const session = this.requireSessionForMessage(message);
283
+ const record = requireRecord(message.payload, "payload");
284
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
285
+ const targetId = formatTargetId(tabId);
286
+ this.broadcastCanvasState(tabId, "canvas-page:closed", { reason: "target_closed" });
287
+ session.designTabTargetId = null;
288
+ session.pendingMutation = false;
289
+ this.pagePorts.delete(tabId);
290
+ this.sessions.removeTarget(session.id, targetId);
291
+ await this.tabs.closeTab(tabId);
292
+ this.sendEvent({
293
+ type: "canvas_event",
294
+ clientId: session.ownerClientId,
295
+ canvasSessionId: session.id,
296
+ event: "canvas_target_closed",
297
+ payload: { targetId, tabId }
298
+ });
299
+ if (session.targets.size === 0) {
300
+ this.sessions.delete(session.id);
301
+ this.sendEvent({
302
+ type: "canvas_event",
303
+ clientId: session.ownerClientId,
304
+ canvasSessionId: session.id,
305
+ event: "canvas_session_closed",
306
+ payload: { reason: "target_closed" }
307
+ });
308
+ }
309
+ return {
310
+ ok: true,
311
+ targetId,
312
+ targetIds: [...session.targets.keys()],
313
+ releasedTargetIds: [targetId],
314
+ previewState: "background"
315
+ };
316
+ }
317
+ async syncTab(message) {
318
+ const session = this.requireSessionForMessage(message);
319
+ const record = requireRecord(message.payload, "payload");
320
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
321
+ const existingTab = await this.tabs.getTab(tabId);
322
+ if (!existingTab) {
323
+ throw new Error(`Canvas target is unavailable: ${tabId}`);
324
+ }
325
+ const summary = normalizeCanvasSessionSummary(record.summary);
326
+ session.document = requireCanvasDocument(record.document);
327
+ session.documentRevision = optionalNumber(record.documentRevision);
328
+ session.html = requireRenderedHtml(record);
329
+ session.summary = summary;
330
+ session.previewTargets = normalizeCanvasTargetStateSummaries(record.targets ?? summary.targets);
331
+ session.overlayMounts = parseOverlayMounts(record.overlayMounts ?? summary.overlayMounts);
332
+ session.feedback = parseFeedbackEvents(record.feedback);
333
+ session.feedbackCursor = optionalString(record.feedbackCursor) ?? lastFeedbackCursor(session.feedback);
334
+ session.pendingMutation = false;
335
+ this.mergeEditorState(session, normalizeViewport(record.viewport), normalizeSelection(record.selection));
336
+ session.previewState = normalizePreviewState(record.previewState) ?? session.previewState;
337
+ session.previewMode = normalizePreviewState(record.previewMode) ?? session.previewMode;
338
+ this.broadcastCanvasState(tabId, "canvas-page:update");
339
+ return {
340
+ ok: true,
341
+ targetId: formatTargetId(tabId),
342
+ previewState: session.previewState
343
+ };
344
+ }
345
+ async mountOverlay(message) {
346
+ const session = this.requireSessionForMessage(message);
347
+ const record = requireRecord(message.payload, "payload");
348
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
349
+ const prototypeId = optionalString(record.prototypeId) ?? "default";
350
+ await insertCss(tabId, OVERLAY_STYLE);
351
+ const mountId = `mount_${crypto.randomUUID()}`;
352
+ const result = await executeInTab(tabId, mountOverlayScript, [{
353
+ mountId,
354
+ cssText: OVERLAY_STYLE,
355
+ title: session.document.title,
356
+ prototypeId,
357
+ selection: session.selection
358
+ }]);
359
+ const mount = {
360
+ mountId,
361
+ targetId: formatTargetId(tabId),
362
+ mountedAt: new Date().toISOString()
363
+ };
364
+ session.overlayMounts = dedupeOverlayMounts([...session.overlayMounts, mount]);
365
+ this.broadcastIfDesignTab(session);
366
+ return {
367
+ mountId,
368
+ targetId: formatTargetId(tabId),
369
+ previewState: "background",
370
+ overlayState: result?.previewState ?? "mounted",
371
+ capabilities: { selection: true, guides: true }
372
+ };
373
+ }
374
+ async unmountOverlay(message) {
375
+ const session = this.requireSessionForMessage(message);
376
+ const record = requireRecord(message.payload, "payload");
377
+ const mountId = requireString(record.mountId, "mountId");
378
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
379
+ await executeInTab(tabId, unmountOverlayScript, [mountId]);
380
+ session.overlayMounts = session.overlayMounts.filter((mount) => mount.mountId !== mountId);
381
+ this.broadcastIfDesignTab(session);
382
+ return {
383
+ ok: true,
384
+ mountId,
385
+ previewState: "background",
386
+ overlayState: "idle"
387
+ };
388
+ }
389
+ async selectOverlay(message) {
390
+ const session = this.requireSessionForMessage(message);
391
+ const record = requireRecord(message.payload, "payload");
392
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
393
+ const selectionHint = isRecord(record.selectionHint) ? record.selectionHint : {};
394
+ const nodeId = optionalString(record.nodeId);
395
+ const selection = await executeInTab(tabId, selectOverlayScript, [{ selectionHint, nodeId }]);
396
+ if (typeof nodeId === "string") {
397
+ session.selection = {
398
+ pageId: session.document.pages[0]?.id ?? null,
399
+ nodeId,
400
+ targetId: formatTargetId(tabId),
401
+ updatedAt: new Date().toISOString()
402
+ };
403
+ this.broadcastIfDesignTab(session);
404
+ }
405
+ return {
406
+ targetId: formatTargetId(tabId),
407
+ selection
408
+ };
409
+ }
410
+ async syncOverlay(message) {
411
+ const session = this.requireSessionForMessage(message);
412
+ const record = requireRecord(message.payload, "payload");
413
+ const tabId = parseTargetId(requireString(record.targetId, "targetId"));
414
+ const mountId = requireString(record.mountId, "mountId");
415
+ await insertCss(tabId, OVERLAY_STYLE);
416
+ const result = await executeInTab(tabId, syncOverlayScript, [{
417
+ mountId,
418
+ title: session.document.title,
419
+ selection: session.selection
420
+ }]);
421
+ return {
422
+ ok: true,
423
+ mountId,
424
+ targetId: formatTargetId(tabId),
425
+ overlayState: result?.overlayState ?? "mounted"
426
+ };
427
+ }
428
+ createOrReplaceSession(message, tabId, document, previewMode, record) {
429
+ const canvasSessionId = resolveCanvasSessionId(message, record);
430
+ const clientId = requireString(message.clientId, "clientId");
431
+ const requestedLeaseId = optionalString(message.leaseId);
432
+ const summary = normalizeCanvasSessionSummary(record.summary);
433
+ const existing = this.sessions.get(canvasSessionId);
434
+ if (existing) {
435
+ if (existing.ownerClientId !== clientId || (requestedLeaseId && existing.leaseId !== requestedLeaseId)) {
436
+ throw new Error("Canvas session ownership mismatch.");
437
+ }
438
+ if (existing.designTabTargetId && existing.designTabTargetId !== formatTargetId(tabId)) {
439
+ void this.tabs.closeTab(parseTargetId(existing.designTabTargetId)).catch(() => undefined);
440
+ }
441
+ existing.designTabTargetId = formatTargetId(tabId);
442
+ existing.document = document;
443
+ existing.documentRevision = optionalNumber(record.documentRevision);
444
+ existing.html = requireRenderedHtml(record);
445
+ existing.summary = summary;
446
+ existing.previewTargets = normalizeCanvasTargetStateSummaries(record.targets ?? summary.targets);
447
+ existing.overlayMounts = parseOverlayMounts(record.overlayMounts ?? summary.overlayMounts);
448
+ existing.feedback = parseFeedbackEvents(record.feedback);
449
+ existing.feedbackCursor = optionalString(record.feedbackCursor) ?? lastFeedbackCursor(existing.feedback);
450
+ existing.previewMode = previewMode;
451
+ existing.previewState = previewMode;
452
+ existing.pendingMutation = false;
453
+ this.sessions.addTarget(existing.id, tabId, { title: document.title, url: chrome.runtime.getURL("canvas.html") });
454
+ this.sessions.setActiveTarget(existing.id, formatTargetId(tabId));
455
+ return existing;
456
+ }
457
+ const leaseId = requestedLeaseId ?? createCoordinatorId();
458
+ const session = this.sessions.createSession(clientId, tabId, leaseId, {
459
+ title: document.title,
460
+ url: chrome.runtime.getURL("canvas.html")
461
+ }, {
462
+ designTabTargetId: formatTargetId(tabId),
463
+ document,
464
+ documentRevision: optionalNumber(record.documentRevision),
465
+ html: requireRenderedHtml(record),
466
+ summary,
467
+ previewMode,
468
+ previewState: previewMode,
469
+ previewTargets: normalizeCanvasTargetStateSummaries(record.targets ?? summary.targets),
470
+ overlayMounts: parseOverlayMounts(record.overlayMounts ?? summary.overlayMounts),
471
+ feedback: parseFeedbackEvents(record.feedback),
472
+ feedbackCursor: optionalString(record.feedbackCursor) ?? lastFeedbackCursor(parseFeedbackEvents(record.feedback)),
473
+ selection: defaultSelection(document.pages[0]?.id ?? null),
474
+ viewport: { x: 120, y: 96, zoom: 1 },
475
+ pendingMutation: false
476
+ }, canvasSessionId);
477
+ return session;
478
+ }
479
+ requireSessionForMessage(message, record) {
480
+ const payload = record ?? (isRecord(message.payload) ? message.payload : {});
481
+ const session = resolveSessionForMessage(this.sessions, message, payload);
482
+ const clientId = requireString(message.clientId, "clientId");
483
+ const leaseId = optionalString(message.leaseId);
484
+ if (session.ownerClientId !== clientId || (leaseId && session.leaseId !== leaseId)) {
485
+ throw new Error("Canvas session ownership mismatch.");
486
+ }
487
+ return session;
488
+ }
489
+ mergeEditorState(session, viewport, selection) {
490
+ if (viewport) {
491
+ session.viewport = {
492
+ x: typeof viewport.x === "number" ? viewport.x : session.viewport.x,
493
+ y: typeof viewport.y === "number" ? viewport.y : session.viewport.y,
494
+ zoom: typeof viewport.zoom === "number" && Number.isFinite(viewport.zoom) ? viewport.zoom : session.viewport.zoom
495
+ };
496
+ }
497
+ if (selection) {
498
+ session.selection = {
499
+ pageId: typeof selection.pageId === "string" ? selection.pageId : session.selection.pageId,
500
+ nodeId: typeof selection.nodeId === "string" || selection.nodeId === null ? selection.nodeId : session.selection.nodeId,
501
+ targetId: typeof selection.targetId === "string" || selection.targetId === null ? selection.targetId : session.selection.targetId,
502
+ updatedAt: new Date().toISOString()
503
+ };
504
+ }
505
+ }
506
+ getPageStateByTabId(tabId) {
507
+ const session = this.sessions.getByTabId(tabId);
508
+ if (!session || !session.designTabTargetId) {
509
+ return null;
510
+ }
511
+ return buildPageState(session, tabId);
512
+ }
513
+ async waitForPagePort(tabId, timeoutMs) {
514
+ const existing = this.firstConnectedPagePort(tabId);
515
+ if (existing) {
516
+ return existing;
517
+ }
518
+ const startedAt = Date.now();
519
+ while (Date.now() - startedAt < timeoutMs) {
520
+ await delay(50);
521
+ const next = this.firstConnectedPagePort(tabId);
522
+ if (next) {
523
+ return next;
524
+ }
525
+ }
526
+ throw new Error("Canvas page port unavailable.");
527
+ }
528
+ firstConnectedPagePort(tabId) {
529
+ const ports = this.pagePorts.get(tabId);
530
+ if (!ports || ports.size === 0) {
531
+ return null;
532
+ }
533
+ return ports.values().next().value ?? null;
534
+ }
535
+ sendResponse(message, payload) {
536
+ const response = {
537
+ type: "canvas_response",
538
+ requestId: message.requestId,
539
+ clientId: message.clientId,
540
+ canvasSessionId: message.canvasSessionId,
541
+ payload
542
+ };
543
+ this.sendEnvelope(response);
544
+ }
545
+ sendError(message, error) {
546
+ this.sendEnvelope({
547
+ type: "canvas_error",
548
+ requestId: message.requestId,
549
+ clientId: message.clientId,
550
+ canvasSessionId: message.canvasSessionId,
551
+ error
552
+ });
553
+ }
554
+ sendEvent(event) {
555
+ this.sendEnvelope(event);
556
+ }
557
+ postCanvasState(port, state, type, extra = {}) {
558
+ try {
559
+ port.postMessage({
560
+ type,
561
+ state,
562
+ ...extra
563
+ });
564
+ }
565
+ catch {
566
+ // ignore disconnected page ports
567
+ }
568
+ }
569
+ broadcastCanvasState(tabId, type, extra = {}) {
570
+ const ports = this.pagePorts.get(tabId);
571
+ if (!ports || ports.size === 0) {
572
+ return;
573
+ }
574
+ const state = this.getPageStateByTabId(tabId);
575
+ for (const port of ports) {
576
+ this.postCanvasState(port, state, type, extra);
577
+ }
578
+ }
579
+ broadcastIfDesignTab(session) {
580
+ if (!session.designTabTargetId) {
581
+ return;
582
+ }
583
+ this.broadcastCanvasState(parseTargetId(session.designTabTargetId), "canvas-page:update");
584
+ }
585
+ async closeRuntimeSession(session, reason) {
586
+ const released = [...session.targets.values()];
587
+ for (const target of released) {
588
+ this.broadcastCanvasState(target.tabId, "canvas-page:closed", { reason });
589
+ await this.tabs.closeTab(target.tabId).catch(() => undefined);
590
+ this.pagePorts.delete(target.tabId);
591
+ }
592
+ this.sessions.delete(session.id);
593
+ this.sendEvent({
594
+ type: "canvas_event",
595
+ clientId: session.ownerClientId,
596
+ canvasSessionId: session.id,
597
+ event: "canvas_session_expired",
598
+ payload: { reason }
599
+ });
600
+ }
601
+ async createTab(url, previewMode) {
602
+ return await new Promise((resolve, reject) => {
603
+ chrome.tabs.create({ url, active: previewMode === "focused", pinned: previewMode === "pinned" }, (tab) => {
604
+ const lastError = chrome.runtime.lastError;
605
+ if (lastError) {
606
+ reject(new Error(lastError.message));
607
+ return;
608
+ }
609
+ if (!tab || typeof tab.id !== "number") {
610
+ reject(new Error("Canvas tab creation failed"));
611
+ return;
612
+ }
613
+ resolve(tab);
614
+ });
615
+ });
616
+ }
617
+ }
618
+ function buildPageState(session, tabId) {
619
+ return {
620
+ tabId,
621
+ targetId: formatTargetId(tabId),
622
+ canvasSessionId: session.id,
623
+ documentId: session.document.documentId,
624
+ documentRevision: session.documentRevision,
625
+ title: session.document.title,
626
+ document: session.document,
627
+ html: session.html,
628
+ previewMode: session.previewMode,
629
+ previewState: session.previewState,
630
+ updatedAt: new Date().toISOString(),
631
+ summary: session.summary,
632
+ targets: session.previewTargets,
633
+ overlayMounts: session.overlayMounts,
634
+ feedback: session.feedback,
635
+ feedbackCursor: session.feedbackCursor,
636
+ selection: session.selection,
637
+ viewport: session.viewport,
638
+ pendingMutation: session.pendingMutation
639
+ };
640
+ }
641
+ function defaultSelection(pageId) {
642
+ return {
643
+ pageId,
644
+ nodeId: null,
645
+ targetId: null,
646
+ updatedAt: new Date().toISOString()
647
+ };
648
+ }
649
+ function normalizeViewport(value) {
650
+ if (!isRecord(value)) {
651
+ return null;
652
+ }
653
+ return {
654
+ x: typeof value.x === "number" ? value.x : undefined,
655
+ y: typeof value.y === "number" ? value.y : undefined,
656
+ zoom: typeof value.zoom === "number" ? value.zoom : undefined
657
+ };
658
+ }
659
+ function normalizeSelection(value) {
660
+ if (!isRecord(value)) {
661
+ return null;
662
+ }
663
+ return {
664
+ pageId: typeof value.pageId === "string" || value.pageId === null ? value.pageId : undefined,
665
+ nodeId: typeof value.nodeId === "string" || value.nodeId === null ? value.nodeId : undefined,
666
+ targetId: typeof value.targetId === "string" || value.targetId === null ? value.targetId : undefined
667
+ };
668
+ }
669
+ function requireCanvasDocument(value) {
670
+ const document = requireRecord(value, "document");
671
+ const pagesValue = Array.isArray(document.pages) ? document.pages : [];
672
+ return {
673
+ documentId: requireString(document.documentId, "documentId"),
674
+ title: optionalString(document.title) ?? "OpenDevBrowser Canvas",
675
+ updatedAt: optionalString(document.updatedAt) ?? undefined,
676
+ bindings: Array.isArray(document.bindings)
677
+ ? document.bindings.flatMap((bindingValue) => {
678
+ const binding = isRecord(bindingValue) ? bindingValue : null;
679
+ if (!binding || typeof binding.id !== "string" || typeof binding.nodeId !== "string") {
680
+ return [];
681
+ }
682
+ return [{
683
+ id: binding.id,
684
+ nodeId: binding.nodeId,
685
+ kind: optionalString(binding.kind) ?? "component",
686
+ componentName: optionalString(binding.componentName) ?? undefined,
687
+ metadata: isRecord(binding.metadata) ? binding.metadata : {}
688
+ }];
689
+ })
690
+ : [],
691
+ assets: Array.isArray(document.assets)
692
+ ? document.assets.flatMap((assetValue) => {
693
+ const asset = isRecord(assetValue) ? assetValue : null;
694
+ if (!asset || typeof asset.id !== "string") {
695
+ return [];
696
+ }
697
+ return [{
698
+ id: asset.id,
699
+ sourceType: optionalString(asset.sourceType) ?? undefined,
700
+ kind: optionalString(asset.kind) ?? undefined,
701
+ repoPath: optionalString(asset.repoPath),
702
+ url: optionalString(asset.url),
703
+ mime: optionalString(asset.mime) ?? undefined,
704
+ metadata: isRecord(asset.metadata) ? asset.metadata : {}
705
+ }];
706
+ })
707
+ : [],
708
+ componentInventory: Array.isArray(document.componentInventory)
709
+ ? document.componentInventory.filter(isRecord)
710
+ : [],
711
+ pages: pagesValue.map((pageValue) => {
712
+ const page = requireRecord(pageValue, "page");
713
+ const nodesValue = Array.isArray(page.nodes) ? page.nodes : [];
714
+ return {
715
+ id: requireString(page.id, "page.id"),
716
+ name: optionalString(page.name) ?? requireString(page.id, "page.id"),
717
+ path: optionalString(page.path) ?? "/",
718
+ rootNodeId: optionalString(page.rootNodeId) ?? null,
719
+ prototypeIds: Array.isArray(page.prototypeIds) ? page.prototypeIds.filter((entry) => typeof entry === "string") : [],
720
+ nodes: nodesValue.map((nodeValue) => {
721
+ const node = requireRecord(nodeValue, "node");
722
+ return {
723
+ id: requireString(node.id, "node.id"),
724
+ kind: optionalString(node.kind) ?? "frame",
725
+ name: optionalString(node.name) ?? "node",
726
+ pageId: optionalString(node.pageId) ?? undefined,
727
+ parentId: optionalString(node.parentId),
728
+ childIds: Array.isArray(node.childIds) ? node.childIds.filter((entry) => typeof entry === "string") : [],
729
+ rect: normalizeRect(node.rect),
730
+ props: isRecord(node.props) ? node.props : {},
731
+ style: isRecord(node.style) ? node.style : {},
732
+ bindingRefs: isRecord(node.bindingRefs) ? node.bindingRefs : {},
733
+ metadata: isRecord(node.metadata) ? node.metadata : {}
734
+ };
735
+ }),
736
+ metadata: isRecord(page.metadata) ? page.metadata : {}
737
+ };
738
+ })
739
+ };
740
+ }
741
+ function requireRenderedHtml(record) {
742
+ return requireString(record.html, "html");
743
+ }
744
+ function normalizeRect(value) {
745
+ if (!isRecord(value)) {
746
+ return { x: 0, y: 0, width: 320, height: 180 };
747
+ }
748
+ return {
749
+ x: typeof value.x === "number" ? value.x : 0,
750
+ y: typeof value.y === "number" ? value.y : 0,
751
+ width: typeof value.width === "number" ? value.width : 320,
752
+ height: typeof value.height === "number" ? value.height : 180
753
+ };
754
+ }
755
+ function parseOverlayMounts(value) {
756
+ if (!Array.isArray(value)) {
757
+ return [];
758
+ }
759
+ return value.flatMap((entry) => {
760
+ if (!isRecord(entry)) {
761
+ return [];
762
+ }
763
+ const mountId = optionalString(entry.mountId);
764
+ const targetId = optionalString(entry.targetId);
765
+ const mountedAt = optionalString(entry.mountedAt);
766
+ return mountId && targetId && mountedAt
767
+ ? [{ mountId, targetId, mountedAt }]
768
+ : [];
769
+ });
770
+ }
771
+ function parseFeedbackEvents(value) {
772
+ if (!Array.isArray(value)) {
773
+ return [];
774
+ }
775
+ const events = [];
776
+ for (const entry of value) {
777
+ if (!isRecord(entry)) {
778
+ continue;
779
+ }
780
+ if (entry.eventType === "feedback.item" && isRecord(entry.item)) {
781
+ const item = entry.item;
782
+ const id = optionalString(item.id);
783
+ const cursor = optionalString(item.cursor);
784
+ const documentId = optionalString(item.documentId);
785
+ if (!id || !cursor || !documentId) {
786
+ continue;
787
+ }
788
+ events.push({
789
+ eventType: "feedback.item",
790
+ item: {
791
+ id,
792
+ cursor,
793
+ category: optionalString(item.category) ?? "validation",
794
+ class: optionalString(item.class) ?? "feedback",
795
+ severity: optionalString(item.severity) ?? "info",
796
+ message: optionalString(item.message) ?? "",
797
+ documentId,
798
+ documentRevision: typeof item.documentRevision === "number" ? item.documentRevision : 0,
799
+ pageId: optionalString(item.pageId),
800
+ prototypeId: optionalString(item.prototypeId),
801
+ targetId: optionalString(item.targetId),
802
+ evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs.filter((ref) => typeof ref === "string") : [],
803
+ details: isRecord(item.details) ? item.details : {}
804
+ }
805
+ });
806
+ continue;
807
+ }
808
+ if (entry.eventType === "feedback.heartbeat") {
809
+ events.push({
810
+ eventType: "feedback.heartbeat",
811
+ cursor: optionalString(entry.cursor),
812
+ ts: optionalString(entry.ts) ?? new Date().toISOString(),
813
+ activeTargetIds: Array.isArray(entry.activeTargetIds) ? entry.activeTargetIds.filter((id) => typeof id === "string") : []
814
+ });
815
+ continue;
816
+ }
817
+ if (entry.eventType === "feedback.complete") {
818
+ const reason = optionalString(entry.reason);
819
+ if (!reason) {
820
+ continue;
821
+ }
822
+ events.push({
823
+ eventType: "feedback.complete",
824
+ cursor: optionalString(entry.cursor),
825
+ ts: optionalString(entry.ts) ?? new Date().toISOString(),
826
+ reason: reason
827
+ });
828
+ continue;
829
+ }
830
+ }
831
+ return events;
832
+ }
833
+ function lastFeedbackCursor(events) {
834
+ for (let index = events.length - 1; index >= 0; index -= 1) {
835
+ const entry = events[index];
836
+ if (entry?.eventType === "feedback.item") {
837
+ return entry.item.cursor;
838
+ }
839
+ if (entry?.eventType === "feedback.heartbeat" || entry?.eventType === "feedback.complete") {
840
+ return entry.cursor;
841
+ }
842
+ }
843
+ return null;
844
+ }
845
+ function dedupeOverlayMounts(mounts) {
846
+ const byId = new Map();
847
+ for (const mount of mounts) {
848
+ byId.set(mount.mountId, mount);
849
+ }
850
+ return [...byId.values()];
851
+ }
852
+ function requireRecord(value, label) {
853
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
854
+ throw new Error(`Invalid ${label}`);
855
+ }
856
+ return value;
857
+ }
858
+ function resolveCanvasSessionId(message, payload) {
859
+ return optionalString(message.canvasSessionId)
860
+ ?? optionalString(payload.canvasSessionId)
861
+ ?? optionalString(isRecord(payload.summary) ? payload.summary.canvasSessionId : undefined)
862
+ ?? createCoordinatorId();
863
+ }
864
+ function resolveSessionForMessage(sessions, message, payload) {
865
+ const directId = optionalString(message.canvasSessionId)
866
+ ?? optionalString(payload.canvasSessionId)
867
+ ?? optionalString(isRecord(payload.summary) ? payload.summary.canvasSessionId : undefined);
868
+ if (directId) {
869
+ return sessions.requireSession(directId);
870
+ }
871
+ const targetId = optionalString(payload.targetId);
872
+ if (targetId) {
873
+ const session = sessions.getByTabId(parseTargetId(targetId));
874
+ if (session) {
875
+ return session;
876
+ }
877
+ }
878
+ throw missingCanvasSession();
879
+ }
880
+ function missingCanvasSession() {
881
+ return new Error("Missing canvasSessionId");
882
+ }
883
+ function isRecord(value) {
884
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
885
+ }
886
+ function requireString(value, label) {
887
+ if (typeof value !== "string" || value.trim().length === 0) {
888
+ throw new Error(`Missing ${label}`);
889
+ }
890
+ return value;
891
+ }
892
+ function optionalString(value) {
893
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
894
+ }
895
+ function optionalNumber(value) {
896
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
897
+ }
898
+ function requireEnum(value, label, allowed) {
899
+ if (typeof value !== "string" || !allowed.includes(value)) {
900
+ throw new Error(`Invalid ${label}`);
901
+ }
902
+ return value;
903
+ }
904
+ function normalizePreviewState(value) {
905
+ return value === "focused" || value === "pinned" || value === "background" || value === "degraded"
906
+ ? value
907
+ : null;
908
+ }
909
+ function parseTargetId(targetId) {
910
+ const raw = targetId.startsWith("tab-") ? targetId.slice(4) : targetId;
911
+ const tabId = Number(raw);
912
+ if (!Number.isInteger(tabId) || tabId <= 0) {
913
+ throw new Error(`Invalid targetId: ${targetId}`);
914
+ }
915
+ return tabId;
916
+ }
917
+ function formatTargetId(tabId) {
918
+ if (!Number.isInteger(tabId)) {
919
+ throw new Error("Tab id unavailable");
920
+ }
921
+ return `tab-${tabId}`;
922
+ }
923
+ function requireTabId(tab) {
924
+ const tabId = tab.id;
925
+ if (typeof tabId !== "number" || !Number.isInteger(tabId)) {
926
+ throw new Error("Canvas tab creation failed");
927
+ }
928
+ return tabId;
929
+ }
930
+ async function insertCss(tabId, css) {
931
+ await new Promise((resolve, reject) => {
932
+ chrome.scripting.insertCSS({ target: { tabId }, css }, () => {
933
+ const lastError = chrome.runtime.lastError;
934
+ if (lastError) {
935
+ reject(new Error(lastError.message));
936
+ return;
937
+ }
938
+ resolve();
939
+ });
940
+ });
941
+ }
942
+ async function executeInTab(tabId, func, args) {
943
+ return await new Promise((resolve, reject) => {
944
+ chrome.scripting.executeScript({ target: { tabId }, func: func, args }, (results) => {
945
+ const lastError = chrome.runtime.lastError;
946
+ if (lastError) {
947
+ reject(new Error(lastError.message));
948
+ return;
949
+ }
950
+ const [first] = results ?? [];
951
+ resolve((first?.result ?? null));
952
+ });
953
+ });
954
+ }
955
+ function mountOverlayScript(input) {
956
+ const styleId = "opendevbrowser-canvas-style";
957
+ if (!document.getElementById(styleId)) {
958
+ const style = document.createElement("style");
959
+ style.id = styleId;
960
+ style.textContent = input.cssText;
961
+ document.head.append(style);
962
+ }
963
+ document.getElementById(input.mountId)?.remove();
964
+ const root = document.createElement("div");
965
+ root.id = input.mountId;
966
+ root.className = "opendevbrowser-canvas-overlay";
967
+ const heading = document.createElement("strong");
968
+ heading.textContent = input.title;
969
+ const detail = document.createElement("div");
970
+ detail.textContent = input.selection.nodeId ? `Selected ${input.selection.nodeId}` : input.prototypeId;
971
+ root.append(heading, detail);
972
+ document.body.append(root);
973
+ if (input.selection.nodeId) {
974
+ const element = document.querySelector(`[data-node-id="${input.selection.nodeId}"]`);
975
+ if (element instanceof HTMLElement) {
976
+ element.classList.add("opendevbrowser-canvas-highlight");
977
+ }
978
+ }
979
+ return { previewState: "overlay_mounted" };
980
+ }
981
+ function unmountOverlayScript(mountId) {
982
+ document.getElementById(mountId)?.remove();
983
+ document.querySelectorAll(".opendevbrowser-canvas-highlight").forEach((element) => {
984
+ element.classList.remove("opendevbrowser-canvas-highlight");
985
+ });
986
+ return true;
987
+ }
988
+ function selectOverlayScript(input) {
989
+ document.querySelectorAll(".opendevbrowser-canvas-highlight").forEach((element) => {
990
+ element.classList.remove("opendevbrowser-canvas-highlight");
991
+ });
992
+ const selector = typeof input.selectionHint.selector === "string"
993
+ ? input.selectionHint.selector
994
+ : (input.nodeId ? `[data-node-id="${input.nodeId}"]` : null);
995
+ const element = selector ? document.querySelector(selector) : null;
996
+ if (!(element instanceof HTMLElement)) {
997
+ return { matched: false };
998
+ }
999
+ element.classList.add("opendevbrowser-canvas-highlight");
1000
+ return {
1001
+ matched: true,
1002
+ selector,
1003
+ tagName: element.tagName.toLowerCase(),
1004
+ text: element.innerText.slice(0, 160),
1005
+ id: element.id || null,
1006
+ className: element.className || null
1007
+ };
1008
+ }
1009
+ function syncOverlayScript(input) {
1010
+ let root = document.getElementById(input.mountId);
1011
+ if (!(root instanceof HTMLElement)) {
1012
+ root = document.createElement("div");
1013
+ root.id = input.mountId;
1014
+ root.className = "opendevbrowser-canvas-overlay";
1015
+ const heading = document.createElement("strong");
1016
+ heading.textContent = input.title;
1017
+ const detail = document.createElement("div");
1018
+ detail.textContent = input.selection.nodeId ? `Selected ${input.selection.nodeId}` : "Canvas overlay synced";
1019
+ root.append(heading, detail);
1020
+ document.body.append(root);
1021
+ }
1022
+ const strong = root.querySelector("strong");
1023
+ if (strong) {
1024
+ strong.textContent = input.title;
1025
+ }
1026
+ const detail = root.querySelector("div");
1027
+ if (detail) {
1028
+ detail.textContent = input.selection.nodeId ? `Selected ${input.selection.nodeId}` : "Canvas overlay synced";
1029
+ }
1030
+ document.querySelectorAll(".opendevbrowser-canvas-highlight").forEach((element) => {
1031
+ element.classList.remove("opendevbrowser-canvas-highlight");
1032
+ });
1033
+ if (input.selection.nodeId) {
1034
+ const element = document.querySelector(`[data-node-id="${input.selection.nodeId}"]`);
1035
+ if (element instanceof HTMLElement) {
1036
+ element.classList.add("opendevbrowser-canvas-highlight");
1037
+ }
1038
+ }
1039
+ return { overlayState: "mounted" };
1040
+ }
1041
+ function normalizeCanvasError(error) {
1042
+ if (error instanceof Error) {
1043
+ const message = error.message;
1044
+ const restricted = message.includes("Cannot access") || message.includes("chrome://") || message.includes("restricted");
1045
+ return {
1046
+ code: restricted ? "restricted_url" : "execution_failed",
1047
+ message,
1048
+ retryable: false
1049
+ };
1050
+ }
1051
+ return {
1052
+ code: "execution_failed",
1053
+ message: "Canvas request failed",
1054
+ retryable: false
1055
+ };
1056
+ }
1057
+ function delay(ms) {
1058
+ return new Promise((resolve) => {
1059
+ setTimeout(resolve, ms);
1060
+ });
1061
+ }