playwriter 0.0.89 → 0.0.103

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 (137) hide show
  1. package/dist/aria-snapshot.d.ts +7 -3
  2. package/dist/aria-snapshot.d.ts.map +1 -1
  3. package/dist/aria-snapshot.js +76 -18
  4. package/dist/aria-snapshot.js.map +1 -1
  5. package/dist/aria-snapshot.test.js +94 -0
  6. package/dist/aria-snapshot.test.js.map +1 -1
  7. package/dist/bippy.js +1 -1
  8. package/dist/browser-config.d.ts +12 -1
  9. package/dist/browser-config.d.ts.map +1 -1
  10. package/dist/browser-config.js +101 -38
  11. package/dist/browser-config.js.map +1 -1
  12. package/dist/browser-launch.d.ts +17 -0
  13. package/dist/browser-launch.d.ts.map +1 -0
  14. package/dist/browser-launch.js +44 -0
  15. package/dist/browser-launch.js.map +1 -0
  16. package/dist/cdp-relay.d.ts.map +1 -1
  17. package/dist/cdp-relay.js +34 -10
  18. package/dist/cdp-relay.js.map +1 -1
  19. package/dist/channel-owner-inspect.test.d.ts +2 -0
  20. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  21. package/dist/channel-owner-inspect.test.js +75 -0
  22. package/dist/channel-owner-inspect.test.js.map +1 -0
  23. package/dist/chrome-discovery.d.ts +65 -0
  24. package/dist/chrome-discovery.d.ts.map +1 -0
  25. package/dist/chrome-discovery.js +165 -0
  26. package/dist/chrome-discovery.js.map +1 -0
  27. package/dist/chrome-discovery.test.d.ts +2 -0
  28. package/dist/chrome-discovery.test.d.ts.map +1 -0
  29. package/dist/chrome-discovery.test.js +41 -0
  30. package/dist/chrome-discovery.test.js.map +1 -0
  31. package/dist/cli.js +357 -53
  32. package/dist/cli.js.map +1 -1
  33. package/dist/executor.d.ts +44 -11
  34. package/dist/executor.d.ts.map +1 -1
  35. package/dist/executor.js +121 -55
  36. package/dist/executor.js.map +1 -1
  37. package/dist/executor.unit.test.js +29 -1
  38. package/dist/executor.unit.test.js.map +1 -1
  39. package/dist/extension/background.js +1704 -0
  40. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  41. package/dist/extension/icons/icon-black-128.png +0 -0
  42. package/dist/extension/icons/icon-black-16.png +0 -0
  43. package/dist/extension/icons/icon-black-32.png +0 -0
  44. package/dist/extension/icons/icon-black-48.png +0 -0
  45. package/dist/extension/icons/icon-gray-128.png +0 -0
  46. package/dist/extension/icons/icon-gray-16.png +0 -0
  47. package/dist/extension/icons/icon-gray-32.png +0 -0
  48. package/dist/extension/icons/icon-gray-48.png +0 -0
  49. package/dist/extension/icons/icon-green-128.png +0 -0
  50. package/dist/extension/icons/icon-green-16.png +0 -0
  51. package/dist/extension/icons/icon-green-32.png +0 -0
  52. package/dist/extension/icons/icon-green-48.png +0 -0
  53. package/dist/extension/icons/playwriter-icon-black.png +0 -0
  54. package/dist/extension/icons/playwriter-icon-gray-disabled.png +0 -0
  55. package/dist/extension/icons/playwriter-icon-gray-disabled.svg +1 -0
  56. package/dist/extension/manifest.json +40 -0
  57. package/dist/extension/offscreen.js +214 -0
  58. package/dist/extension/src/offscreen.html +9 -0
  59. package/dist/extension/src/prism-bash.min.js +1 -0
  60. package/dist/extension/src/prism.min.js +1 -0
  61. package/dist/extension/src/welcome.html +399 -0
  62. package/dist/kitty-graphics.d.ts +22 -0
  63. package/dist/kitty-graphics.d.ts.map +1 -0
  64. package/dist/kitty-graphics.js +71 -0
  65. package/dist/kitty-graphics.js.map +1 -0
  66. package/dist/kitty-graphics.test.d.ts +2 -0
  67. package/dist/kitty-graphics.test.d.ts.map +1 -0
  68. package/dist/kitty-graphics.test.js +125 -0
  69. package/dist/kitty-graphics.test.js.map +1 -0
  70. package/dist/mcp.d.ts.map +1 -1
  71. package/dist/mcp.js +76 -12
  72. package/dist/mcp.js.map +1 -1
  73. package/dist/package-paths.d.ts +3 -0
  74. package/dist/package-paths.d.ts.map +1 -0
  75. package/dist/package-paths.js +29 -0
  76. package/dist/package-paths.js.map +1 -0
  77. package/dist/prompt.md +4 -2
  78. package/dist/readability.js +1 -1
  79. package/dist/relay-core.test.js +14 -4
  80. package/dist/relay-core.test.js.map +1 -1
  81. package/dist/relay-navigation.test.d.ts.map +1 -1
  82. package/dist/relay-navigation.test.js +95 -1
  83. package/dist/relay-navigation.test.js.map +1 -1
  84. package/dist/scoped-fs.d.ts +5 -4
  85. package/dist/scoped-fs.d.ts.map +1 -1
  86. package/dist/scoped-fs.js +11 -9
  87. package/dist/scoped-fs.js.map +1 -1
  88. package/dist/scoped-fs.test.d.ts +5 -0
  89. package/dist/scoped-fs.test.d.ts.map +1 -0
  90. package/dist/scoped-fs.test.js +70 -0
  91. package/dist/scoped-fs.test.js.map +1 -0
  92. package/dist/screen-recording.d.ts +3 -6
  93. package/dist/screen-recording.d.ts.map +1 -1
  94. package/dist/screen-recording.js +7 -21
  95. package/dist/screen-recording.js.map +1 -1
  96. package/dist/selector-generator.js +1 -1
  97. package/dist/snapshot-tools.test.js +3 -4
  98. package/dist/snapshot-tools.test.js.map +1 -1
  99. package/dist/test-utils.d.ts +3 -1
  100. package/dist/test-utils.d.ts.map +1 -1
  101. package/dist/test-utils.js +24 -12
  102. package/dist/test-utils.js.map +1 -1
  103. package/package.json +6 -5
  104. package/src/aria-snapshot.test.ts +110 -0
  105. package/src/aria-snapshot.ts +86 -19
  106. package/src/aria-snapshots/contenteditable-interactive.txt +7 -0
  107. package/src/aria-snapshots/contenteditable-raw.txt +14 -0
  108. package/src/aria-snapshots/github-interactive.txt +0 -2
  109. package/src/aria-snapshots/github-raw.txt +0 -4
  110. package/src/aria-snapshots/hackernews-interactive.txt +240 -237
  111. package/src/aria-snapshots/hackernews-raw.txt +262 -258
  112. package/src/aria-snapshots/prosemirror-interactive.txt +63 -0
  113. package/src/aria-snapshots/prosemirror-raw.txt +141 -0
  114. package/src/assets/aria-labels-hacker-news.png +0 -0
  115. package/src/browser-config.ts +143 -37
  116. package/src/browser-launch.ts +75 -0
  117. package/src/cdp-relay.ts +46 -11
  118. package/src/channel-owner-inspect.test.ts +94 -0
  119. package/src/chrome-discovery.test.ts +50 -0
  120. package/src/chrome-discovery.ts +211 -0
  121. package/src/cli.ts +432 -59
  122. package/src/executor.ts +171 -72
  123. package/src/executor.unit.test.ts +32 -1
  124. package/src/kitty-graphics.test.ts +139 -0
  125. package/src/kitty-graphics.ts +79 -0
  126. package/src/mcp.ts +82 -12
  127. package/src/package-paths.ts +36 -0
  128. package/src/relay-core.test.ts +14 -4
  129. package/src/relay-navigation.test.ts +106 -1
  130. package/src/scoped-fs.test.ts +77 -0
  131. package/src/scoped-fs.ts +12 -9
  132. package/src/screen-recording.ts +7 -22
  133. package/src/skill.md +32 -2
  134. package/src/snapshot-tools.test.ts +3 -4
  135. package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
  136. package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
  137. package/src/test-utils.ts +25 -11
@@ -0,0 +1,1704 @@
1
+ //#region ../node_modules/.pnpm/zustand@5.0.8_@types+react@19.2.7_react@19.2.3/node_modules/zustand/esm/vanilla.mjs
2
+ var createStoreImpl = (createState) => {
3
+ let state;
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ const setState = (partial, replace) => {
6
+ const nextState = typeof partial === "function" ? partial(state) : partial;
7
+ if (!Object.is(nextState, state)) {
8
+ const previousState = state;
9
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
10
+ listeners.forEach((listener) => listener(state, previousState));
11
+ }
12
+ };
13
+ const getState = () => state;
14
+ const getInitialState = () => initialState;
15
+ const subscribe = (listener) => {
16
+ listeners.add(listener);
17
+ return () => listeners.delete(listener);
18
+ };
19
+ const api = {
20
+ setState,
21
+ getState,
22
+ getInitialState,
23
+ subscribe
24
+ };
25
+ const initialState = state = createState(setState, getState, api);
26
+ return api;
27
+ };
28
+ var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
29
+ //#endregion
30
+ //#region ../playwriter/src/ghost-browser.ts
31
+ /**
32
+ * Handles ghost-browser commands in the extension.
33
+ * Calls the appropriate chrome.* API and returns the result.
34
+ *
35
+ * @param params - Command parameters (namespace, method, args)
36
+ * @param chromeApi - The chrome object (passed to avoid global dependency)
37
+ * @returns Result object with success/error status
38
+ */
39
+ async function handleGhostBrowserCommand(params, chromeApi) {
40
+ const { namespace, method, args } = params;
41
+ try {
42
+ const api = chromeApi[namespace];
43
+ if (!api) return {
44
+ success: false,
45
+ error: `chrome.${namespace} not available (not running in Ghost Browser?)`
46
+ };
47
+ const fn = api[method];
48
+ if (typeof fn !== "function") {
49
+ if (method in api) return {
50
+ success: true,
51
+ result: api[method]
52
+ };
53
+ return {
54
+ success: false,
55
+ error: `chrome.${namespace}.${method} is not a function or property`
56
+ };
57
+ }
58
+ return {
59
+ success: true,
60
+ result: await new Promise((resolve, reject) => {
61
+ fn.call(api, ...args, (result) => {
62
+ if (chromeApi.runtime.lastError) reject(new Error(chromeApi.runtime.lastError.message));
63
+ else resolve(result);
64
+ });
65
+ })
66
+ };
67
+ } catch (error) {
68
+ return {
69
+ success: false,
70
+ error: error.message
71
+ };
72
+ }
73
+ }
74
+ //#endregion
75
+ //#region src/recording.ts
76
+ var activeRecordings = /* @__PURE__ */ new Map();
77
+ var offscreenDocumentCreating = null;
78
+ /**
79
+ * Get the active recordings map (for cleanup on tab disconnect).
80
+ */
81
+ function getActiveRecordings() {
82
+ return activeRecordings;
83
+ }
84
+ async function ensureOffscreenDocument() {
85
+ if ((await chrome.runtime.getContexts({
86
+ contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
87
+ documentUrls: [chrome.runtime.getURL("src/offscreen.html")]
88
+ })).length > 0) return;
89
+ if (offscreenDocumentCreating) return offscreenDocumentCreating;
90
+ offscreenDocumentCreating = chrome.offscreen.createDocument({
91
+ url: "src/offscreen.html",
92
+ reasons: [chrome.offscreen.Reason.USER_MEDIA],
93
+ justification: "Screen recording via chrome.tabCapture"
94
+ });
95
+ try {
96
+ await offscreenDocumentCreating;
97
+ } finally {
98
+ offscreenDocumentCreating = null;
99
+ }
100
+ }
101
+ function resolveTabIdFromSessionId(sessionId) {
102
+ if (!sessionId) {
103
+ for (const [tabId, tab] of store.getState().tabs) if (tab.state === "connected") return tabId;
104
+ return;
105
+ }
106
+ return getTabBySessionId(sessionId)?.tabId;
107
+ }
108
+ function updateTabRecordingState(tabId, isRecording) {
109
+ store.setState((state) => {
110
+ const newTabs = new Map(state.tabs);
111
+ const existing = newTabs.get(tabId);
112
+ if (existing) newTabs.set(tabId, {
113
+ ...existing,
114
+ isRecording
115
+ });
116
+ return { tabs: newTabs };
117
+ });
118
+ }
119
+ async function handleStartRecording(params) {
120
+ const tabId = resolveTabIdFromSessionId(params.sessionId);
121
+ if (!tabId) return {
122
+ success: false,
123
+ error: "No connected tab found for recording. Click the Playwriter extension icon on the tab you want to record."
124
+ };
125
+ if (activeRecordings.has(tabId)) return {
126
+ success: false,
127
+ error: "Recording already in progress for this tab"
128
+ };
129
+ const tabInfo = store.getState().tabs.get(tabId);
130
+ if (!tabInfo || tabInfo.state !== "connected") return {
131
+ success: false,
132
+ error: "Tab is not connected"
133
+ };
134
+ logger.debug("Starting recording for tab:", tabId, "params:", params);
135
+ try {
136
+ await ensureOffscreenDocument();
137
+ const streamId = await new Promise((resolve, reject) => {
138
+ chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, (id) => {
139
+ if (chrome.runtime.lastError) {
140
+ const errorMsg = chrome.runtime.lastError.message || "Unknown error";
141
+ if (errorMsg.includes("Extension has not been invoked") || errorMsg.includes("activeTab")) reject(/* @__PURE__ */ new Error(`${errorMsg}. Click the Playwriter extension icon on this tab to enable recording.`));
142
+ else reject(new Error(errorMsg));
143
+ } else if (!id) reject(/* @__PURE__ */ new Error("Failed to get media stream ID"));
144
+ else resolve(id);
145
+ });
146
+ });
147
+ logger.debug("Got stream ID for tab:", tabId, "streamId:", streamId.substring(0, 20) + "...");
148
+ const result = await chrome.runtime.sendMessage({
149
+ action: "startRecording",
150
+ tabId,
151
+ streamId,
152
+ frameRate: params.frameRate ?? 30,
153
+ videoBitsPerSecond: params.videoBitsPerSecond ?? 25e5,
154
+ audioBitsPerSecond: params.audioBitsPerSecond ?? 128e3,
155
+ audio: params.audio ?? false
156
+ });
157
+ if (!result.success) return {
158
+ success: false,
159
+ error: result.error || "Failed to start recording in offscreen document"
160
+ };
161
+ const startedAt = result.startedAt || Date.now();
162
+ activeRecordings.set(tabId, {
163
+ tabId,
164
+ startedAt
165
+ });
166
+ updateTabRecordingState(tabId, true);
167
+ logger.debug("Recording started for tab:", tabId, "mimeType:", result.mimeType);
168
+ return {
169
+ success: true,
170
+ tabId,
171
+ startedAt
172
+ };
173
+ } catch (error) {
174
+ const errorMessage = error instanceof Error ? error.message : String(error);
175
+ logger.error("Failed to start recording:", error);
176
+ return {
177
+ success: false,
178
+ error: errorMessage
179
+ };
180
+ }
181
+ }
182
+ async function handleStopRecording(params) {
183
+ const tabId = resolveTabIdFromSessionId(params.sessionId);
184
+ if (!tabId) return {
185
+ success: false,
186
+ error: "No connected tab found"
187
+ };
188
+ const recording = activeRecordings.get(tabId);
189
+ if (!recording) return {
190
+ success: false,
191
+ error: "No active recording for this tab"
192
+ };
193
+ logger.debug("Stopping recording for tab:", tabId);
194
+ try {
195
+ const result = await chrome.runtime.sendMessage({
196
+ action: "stopRecording",
197
+ tabId
198
+ });
199
+ if (!result.success) return {
200
+ success: false,
201
+ error: result.error || "Failed to stop recording in offscreen document"
202
+ };
203
+ const duration = result.duration || Date.now() - recording.startedAt;
204
+ activeRecordings.delete(tabId);
205
+ updateTabRecordingState(tabId, false);
206
+ logger.debug("Recording stopped for tab:", tabId, "duration:", duration);
207
+ return {
208
+ success: true,
209
+ tabId,
210
+ duration
211
+ };
212
+ } catch (error) {
213
+ const errorMessage = error instanceof Error ? error.message : String(error);
214
+ logger.error("Failed to stop recording:", error);
215
+ return {
216
+ success: false,
217
+ error: errorMessage
218
+ };
219
+ }
220
+ }
221
+ async function handleIsRecording(params) {
222
+ const tabId = resolveTabIdFromSessionId(params.sessionId);
223
+ if (!tabId) return { isRecording: false };
224
+ const recording = activeRecordings.get(tabId);
225
+ if (!recording) return {
226
+ isRecording: false,
227
+ tabId
228
+ };
229
+ try {
230
+ return {
231
+ isRecording: (await chrome.runtime.sendMessage({
232
+ action: "isRecording",
233
+ tabId
234
+ })).isRecording,
235
+ tabId,
236
+ startedAt: recording.startedAt
237
+ };
238
+ } catch {
239
+ return {
240
+ isRecording: false,
241
+ tabId
242
+ };
243
+ }
244
+ }
245
+ async function handleCancelRecording(params) {
246
+ const tabId = resolveTabIdFromSessionId(params.sessionId);
247
+ if (!tabId) return {
248
+ success: false,
249
+ error: "No connected tab found"
250
+ };
251
+ if (!activeRecordings.get(tabId)) return { success: true };
252
+ logger.debug("Cancelling recording for tab:", tabId);
253
+ try {
254
+ await chrome.runtime.sendMessage({
255
+ action: "cancelRecording",
256
+ tabId
257
+ });
258
+ activeRecordings.delete(tabId);
259
+ updateTabRecordingState(tabId, false);
260
+ if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
261
+ method: "recordingCancelled",
262
+ params: { tabId }
263
+ });
264
+ return { success: true };
265
+ } catch (error) {
266
+ const errorMessage = error instanceof Error ? error.message : String(error);
267
+ logger.error("Failed to cancel recording:", error);
268
+ return {
269
+ success: false,
270
+ error: errorMessage
271
+ };
272
+ }
273
+ }
274
+ /**
275
+ * Clean up recordings when tab is disconnected.
276
+ */
277
+ async function cleanupRecordingForTab(tabId) {
278
+ if (activeRecordings.get(tabId)) {
279
+ logger.debug("Cleaning up recording for disconnected tab:", tabId);
280
+ try {
281
+ await chrome.runtime.sendMessage({
282
+ action: "cancelRecording",
283
+ tabId
284
+ });
285
+ } catch (e) {
286
+ logger.debug("Error cleaning up recording:", e);
287
+ }
288
+ activeRecordings.delete(tabId);
289
+ }
290
+ }
291
+ //#endregion
292
+ //#region src/background.ts
293
+ var RELAY_HOST = "127.0.0.1";
294
+ var RELAY_PORT = 19988;
295
+ function sleep(ms) {
296
+ return new Promise((resolve) => setTimeout(resolve, ms));
297
+ }
298
+ async function detectBrowserName() {
299
+ if (chrome.ghostPublicAPI) return "Ghost";
300
+ const brands = navigator.userAgentData?.brands;
301
+ if (brands && brands.length > 0) {
302
+ const brandNames = brands.map((brand) => {
303
+ return brand.brand.trim().toLowerCase();
304
+ });
305
+ if (brandNames.some((brand) => brand === "brave")) return "Brave";
306
+ if (brandNames.some((brand) => brand === "microsoft edge")) return "Edge";
307
+ if (brandNames.some((brand) => brand === "opera")) return "Opera";
308
+ if (brandNames.some((brand) => brand === "vivaldi")) return "Vivaldi";
309
+ if (brandNames.some((brand) => brand === "google chrome")) return "Chrome";
310
+ if (brandNames.some((brand) => brand === "chromium")) return "Chromium";
311
+ }
312
+ const ua = navigator.userAgent.toLowerCase();
313
+ if (ua.includes("edg/")) return "Edge";
314
+ if (ua.includes("opr/")) return "Opera";
315
+ if (ua.includes("vivaldi")) return "Vivaldi";
316
+ if (ua.includes("brave")) return "Brave";
317
+ if (ua.includes("chrome")) return "Chrome";
318
+ return "Chromium";
319
+ }
320
+ var identityPromise = null;
321
+ var tabSessionScope = (() => {
322
+ const values = new Uint32Array(2);
323
+ crypto.getRandomValues(values);
324
+ return Array.from(values).map((value) => {
325
+ return value.toString(36);
326
+ }).join("");
327
+ })();
328
+ async function getExtensionIdentity() {
329
+ if (identityPromise) return identityPromise;
330
+ identityPromise = (async () => {
331
+ const browser = await detectBrowserName();
332
+ try {
333
+ const info = await chrome.identity.getProfileUserInfo({ accountStatus: "ANY" });
334
+ return {
335
+ browser,
336
+ email: info.email || "",
337
+ id: info.id || ""
338
+ };
339
+ } catch {
340
+ return {
341
+ browser,
342
+ email: "",
343
+ id: ""
344
+ };
345
+ }
346
+ })();
347
+ return identityPromise;
348
+ }
349
+ var TAB_GROUP_COLOR = "green";
350
+ var TAB_GROUP_TITLE = "playwriter";
351
+ var childSessions = /* @__PURE__ */ new Map();
352
+ var nextSessionId = 1;
353
+ var tabGroupQueue = Promise.resolve();
354
+ var autoAttachParams = null;
355
+ var recordingChunkBuffer = [];
356
+ /**
357
+ * Flush buffered recording chunks to the WebSocket.
358
+ * Called when WebSocket becomes ready.
359
+ */
360
+ function flushRecordingChunkBuffer(ws) {
361
+ if (recordingChunkBuffer.length === 0) return;
362
+ logger.debug(`Flushing ${recordingChunkBuffer.length} buffered recording chunks`);
363
+ while (recordingChunkBuffer.length > 0) {
364
+ const { tabId, data, final } = recordingChunkBuffer.shift();
365
+ ws.send(JSON.stringify({
366
+ method: "recordingData",
367
+ params: {
368
+ tabId,
369
+ final
370
+ }
371
+ }));
372
+ if (data && !final) {
373
+ const buffer = new Uint8Array(data);
374
+ ws.send(buffer);
375
+ }
376
+ }
377
+ }
378
+ var ConnectionManager = class {
379
+ ws = null;
380
+ connectionPromise = null;
381
+ preserveTabsOnDetach = false;
382
+ async ensureConnection() {
383
+ if (this.ws?.readyState === WebSocket.OPEN) return;
384
+ if (store.getState().connectionState === "extension-replaced") throw new Error("Another Playwriter extension is already connected");
385
+ if (this.connectionPromise) return this.connectionPromise;
386
+ const GLOBAL_TIMEOUT_MS = 15e3;
387
+ this.connectionPromise = Promise.race([this.connect(), new Promise((_, reject) => {
388
+ setTimeout(() => {
389
+ reject(/* @__PURE__ */ new Error("Connection timeout (global)"));
390
+ }, GLOBAL_TIMEOUT_MS);
391
+ })]);
392
+ try {
393
+ await this.connectionPromise;
394
+ } finally {
395
+ this.connectionPromise = null;
396
+ }
397
+ }
398
+ async connect() {
399
+ logger.debug(`Waiting for server at http://${RELAY_HOST}:${RELAY_PORT}...`);
400
+ const maxAttempts = 5;
401
+ for (let attempt = 0; attempt < maxAttempts; attempt++) try {
402
+ await fetch(`http://${RELAY_HOST}:${RELAY_PORT}`, {
403
+ method: "HEAD",
404
+ signal: AbortSignal.timeout(2e3)
405
+ });
406
+ logger.debug("Server is available");
407
+ break;
408
+ } catch {
409
+ if (attempt === maxAttempts - 1) throw new Error("Server not available");
410
+ logger.debug(`Server not available, retrying... (attempt ${attempt + 1}/${maxAttempts})`);
411
+ await sleep(1e3);
412
+ }
413
+ const identity = await getExtensionIdentity();
414
+ const relayUrl = new URL(`ws://${RELAY_HOST}:${RELAY_PORT}/extension`);
415
+ if (identity.browser) relayUrl.searchParams.set("browser", identity.browser);
416
+ if (identity.email) relayUrl.searchParams.set("email", identity.email);
417
+ if (identity.id) relayUrl.searchParams.set("id", identity.id);
418
+ relayUrl.searchParams.set("v", "0.0.103");
419
+ logger.debug("Creating WebSocket connection to:", relayUrl);
420
+ const socket = new WebSocket(relayUrl.toString());
421
+ await new Promise((resolve, reject) => {
422
+ let settled = false;
423
+ const timeout = setTimeout(() => {
424
+ if (settled) return;
425
+ settled = true;
426
+ logger.debug("WebSocket connection TIMEOUT after 5 seconds");
427
+ try {
428
+ socket.close();
429
+ } catch {}
430
+ reject(/* @__PURE__ */ new Error("Connection timeout"));
431
+ }, 5e3);
432
+ socket.onopen = () => {
433
+ if (settled) return;
434
+ settled = true;
435
+ logger.debug("WebSocket connected");
436
+ clearTimeout(timeout);
437
+ flushRecordingChunkBuffer(socket);
438
+ resolve();
439
+ };
440
+ socket.onerror = (error) => {
441
+ logger.debug("WebSocket error during connection:", error);
442
+ if (settled) return;
443
+ settled = true;
444
+ clearTimeout(timeout);
445
+ reject(/* @__PURE__ */ new Error("WebSocket connection failed"));
446
+ };
447
+ socket.onclose = (event) => {
448
+ logger.debug("WebSocket closed during connection:", {
449
+ code: event.code,
450
+ reason: event.reason
451
+ });
452
+ if (settled) return;
453
+ settled = true;
454
+ clearTimeout(timeout);
455
+ if (event.code === 4002 || event.reason === "Extension Already In Use") reject(/* @__PURE__ */ new Error("Extension Already In Use"));
456
+ else reject(/* @__PURE__ */ new Error(`WebSocket closed: ${event.reason || event.code}`));
457
+ };
458
+ });
459
+ this.ws = socket;
460
+ this.ws.onmessage = async (event) => {
461
+ let message;
462
+ try {
463
+ message = JSON.parse(event.data);
464
+ } catch (error) {
465
+ logger.debug("Error parsing message:", error);
466
+ sendMessage({ error: {
467
+ code: -32700,
468
+ message: `Error parsing message: ${error.message}`
469
+ } });
470
+ return;
471
+ }
472
+ if (message.method === "ping") {
473
+ sendMessage({ method: "pong" });
474
+ return;
475
+ }
476
+ if (message.method === "createInitialTab") {
477
+ try {
478
+ logger.debug("Creating initial tab for Playwright client");
479
+ const tab = await chrome.tabs.create({
480
+ url: "about:blank",
481
+ active: false
482
+ });
483
+ if (tab.id) {
484
+ setTabConnecting(tab.id);
485
+ const { targetInfo, sessionId } = await attachTab(tab.id, { skipAttachedEvent: true });
486
+ logger.debug("Initial tab created and connected:", tab.id, "sessionId:", sessionId);
487
+ sendMessage({
488
+ id: message.id,
489
+ result: {
490
+ success: true,
491
+ tabId: tab.id,
492
+ sessionId,
493
+ targetInfo
494
+ }
495
+ });
496
+ } else throw new Error("Failed to create tab - no tab ID returned");
497
+ } catch (error) {
498
+ logger.debug("Failed to create initial tab:", error);
499
+ sendMessage({
500
+ id: message.id,
501
+ error: error.message
502
+ });
503
+ }
504
+ return;
505
+ }
506
+ if (message.method === "startRecording") {
507
+ try {
508
+ const result = await handleStartRecording(message.params);
509
+ sendMessage({
510
+ id: message.id,
511
+ result
512
+ });
513
+ } catch (error) {
514
+ logger.error("Failed to start recording:", error);
515
+ sendMessage({
516
+ id: message.id,
517
+ result: {
518
+ success: false,
519
+ error: error.message
520
+ }
521
+ });
522
+ }
523
+ return;
524
+ }
525
+ if (message.method === "stopRecording") {
526
+ try {
527
+ const result = await handleStopRecording(message.params);
528
+ sendMessage({
529
+ id: message.id,
530
+ result
531
+ });
532
+ } catch (error) {
533
+ logger.error("Failed to stop recording:", error);
534
+ sendMessage({
535
+ id: message.id,
536
+ result: {
537
+ success: false,
538
+ error: error.message
539
+ }
540
+ });
541
+ }
542
+ return;
543
+ }
544
+ if (message.method === "isRecording") {
545
+ try {
546
+ const result = await handleIsRecording(message.params);
547
+ sendMessage({
548
+ id: message.id,
549
+ result
550
+ });
551
+ } catch (error) {
552
+ logger.error("Failed to check recording status:", error);
553
+ sendMessage({
554
+ id: message.id,
555
+ result: { isRecording: false }
556
+ });
557
+ }
558
+ return;
559
+ }
560
+ if (message.method === "cancelRecording") {
561
+ try {
562
+ const result = await handleCancelRecording(message.params);
563
+ sendMessage({
564
+ id: message.id,
565
+ result
566
+ });
567
+ } catch (error) {
568
+ logger.error("Failed to cancel recording:", error);
569
+ sendMessage({
570
+ id: message.id,
571
+ result: {
572
+ success: false,
573
+ error: error.message
574
+ }
575
+ });
576
+ }
577
+ return;
578
+ }
579
+ if (message.method === "ghost-browser") {
580
+ const params = message.params;
581
+ const result = await handleGhostBrowserCommand(params, chrome);
582
+ if (!result.success) logger.error("Ghost Browser API error:", result.error);
583
+ if (result.success && params.namespace === "ghostPublicAPI" && params.method === "openTab") {
584
+ const tabId = result.result;
585
+ if (tabId) {
586
+ logger.debug("Auto-connecting Ghost Browser tab:", tabId);
587
+ setTabConnecting(tabId);
588
+ await sleep(100);
589
+ await attachTab(tabId);
590
+ }
591
+ }
592
+ sendMessage({
593
+ id: message.id,
594
+ result
595
+ });
596
+ return;
597
+ }
598
+ const response = { id: message.id };
599
+ try {
600
+ response.result = await handleCommand(message);
601
+ } catch (error) {
602
+ logger.debug("Error handling command:", error);
603
+ response.error = error.message;
604
+ }
605
+ sendMessage(response);
606
+ };
607
+ this.ws.onclose = (event) => {
608
+ this.handleClose(event.reason, event.code);
609
+ };
610
+ this.ws.onerror = (event) => {
611
+ logger.debug("WebSocket error:", event);
612
+ };
613
+ chrome.debugger.onEvent.addListener(onDebuggerEvent);
614
+ chrome.debugger.onDetach.addListener(onDebuggerDetach);
615
+ logger.debug("Connection established");
616
+ }
617
+ handleClose(reason, code) {
618
+ try {
619
+ const mem = performance.memory;
620
+ if (mem) {
621
+ const formatMB = (b) => (b / 1024 / 1024).toFixed(2) + "MB";
622
+ logger.warn(`DISCONNECT MEMORY: used=${formatMB(mem.usedJSHeapSize)} total=${formatMB(mem.totalJSHeapSize)} limit=${formatMB(mem.jsHeapSizeLimit)}`);
623
+ }
624
+ } catch {}
625
+ logger.warn(`DISCONNECT: WS closed code=${code} reason=${reason || "none"} stack=${getCallStack()}`);
626
+ chrome.debugger.onEvent.removeListener(onDebuggerEvent);
627
+ chrome.debugger.onDetach.removeListener(onDebuggerDetach);
628
+ const isExtensionReplaced = reason === "Extension Replaced" || code === 4001;
629
+ const isExtensionInUse = reason === "Extension Already In Use" || code === 4002;
630
+ this.preserveTabsOnDetach = !(isExtensionReplaced || isExtensionInUse);
631
+ const { tabs } = store.getState();
632
+ for (const [tabId] of tabs) chrome.debugger.detach({ tabId }).catch((err) => {
633
+ logger.debug("Error detaching from tab:", tabId, err.message);
634
+ });
635
+ childSessions.clear();
636
+ this.ws = null;
637
+ if (isExtensionReplaced) {
638
+ logger.debug("Disconnected: another Playwriter extension connected (this one was idle)");
639
+ store.setState({
640
+ tabs: /* @__PURE__ */ new Map(),
641
+ connectionState: "extension-replaced",
642
+ errorText: "Another Playwriter extension took over the connection"
643
+ });
644
+ return;
645
+ }
646
+ if (isExtensionInUse) {
647
+ logger.debug("Rejected: another Playwriter extension is actively in use");
648
+ store.setState({
649
+ tabs: /* @__PURE__ */ new Map(),
650
+ connectionState: "extension-replaced",
651
+ errorText: "Another Playwriter extension is actively in use"
652
+ });
653
+ return;
654
+ }
655
+ store.setState((state) => {
656
+ const newTabs = new Map(state.tabs);
657
+ for (const [tabId, tab] of newTabs) newTabs.set(tabId, {
658
+ ...tab,
659
+ state: "connecting"
660
+ });
661
+ return {
662
+ tabs: newTabs,
663
+ connectionState: "idle",
664
+ errorText: void 0
665
+ };
666
+ });
667
+ }
668
+ async maintainLoop() {
669
+ while (true) {
670
+ if (this.ws?.readyState === WebSocket.OPEN) {
671
+ await sleep(1e3);
672
+ continue;
673
+ }
674
+ if (store.getState().connectionState === "extension-replaced") {
675
+ try {
676
+ const data = await (await fetch(`http://${RELAY_HOST}:${RELAY_PORT}/extension/status`, {
677
+ method: "GET",
678
+ signal: AbortSignal.timeout(2e3)
679
+ })).json();
680
+ if (!data.connected || data.activeTargets === 0) {
681
+ store.setState({
682
+ connectionState: "idle",
683
+ errorText: void 0
684
+ });
685
+ logger.debug("Extension slot is free (connected:", data.connected, "activeTargets:", data.activeTargets, "), cleared error state");
686
+ } else logger.debug("Extension slot still taken (activeTargets:", data.activeTargets, "), will retry...");
687
+ } catch {
688
+ logger.debug("Server not available, will retry...");
689
+ }
690
+ await sleep(3e3);
691
+ continue;
692
+ }
693
+ const currentTabs = store.getState().tabs;
694
+ if (Array.from(currentTabs.values()).some((t) => t.state === "connected")) store.setState((state) => {
695
+ const newTabs = new Map(state.tabs);
696
+ for (const [tabId, tab] of newTabs) if (tab.state === "connected") newTabs.set(tabId, {
697
+ ...tab,
698
+ state: "connecting"
699
+ });
700
+ return { tabs: newTabs };
701
+ });
702
+ try {
703
+ await this.ensureConnection();
704
+ store.setState({ connectionState: "connected" });
705
+ const tabsToReattach = Array.from(store.getState().tabs.entries()).filter(([_, tab]) => tab.state === "connecting").map(([tabId]) => tabId);
706
+ for (const tabId of tabsToReattach) {
707
+ const currentTab = store.getState().tabs.get(tabId);
708
+ if (!currentTab || currentTab.state !== "connecting") {
709
+ logger.debug("Skipping reattach, tab state changed:", tabId, currentTab?.state);
710
+ continue;
711
+ }
712
+ try {
713
+ await chrome.tabs.get(tabId);
714
+ await attachTab(tabId);
715
+ logger.debug("Successfully re-attached tab:", tabId);
716
+ } catch (error) {
717
+ logger.debug("Failed to re-attach tab:", tabId, error.message);
718
+ store.setState((state) => {
719
+ const newTabs = new Map(state.tabs);
720
+ newTabs.delete(tabId);
721
+ return { tabs: newTabs };
722
+ });
723
+ }
724
+ }
725
+ this.preserveTabsOnDetach = false;
726
+ } catch (error) {
727
+ logger.debug("Connection attempt failed:", error.message);
728
+ if (error.message === "Extension Already In Use") store.setState({
729
+ connectionState: "extension-replaced",
730
+ errorText: "Another Playwriter extension is actively in use"
731
+ });
732
+ else store.setState({ connectionState: "idle" });
733
+ }
734
+ await sleep(3e3);
735
+ }
736
+ }
737
+ };
738
+ var connectionManager = new ConnectionManager();
739
+ var store = createStore(() => ({
740
+ tabs: /* @__PURE__ */ new Map(),
741
+ connectionState: "idle",
742
+ currentTabId: void 0,
743
+ errorText: void 0
744
+ }));
745
+ globalThis.toggleExtensionForActiveTab = toggleExtensionForActiveTab;
746
+ globalThis.disconnectEverything = disconnectEverything;
747
+ globalThis.getExtensionState = () => store.getState();
748
+ var MAX_LOG_STRING_LENGTH = 2e3;
749
+ function truncateLogString(value) {
750
+ if (value.length <= MAX_LOG_STRING_LENGTH) return value;
751
+ return `${value.slice(0, MAX_LOG_STRING_LENGTH)}…[truncated ${value.length - MAX_LOG_STRING_LENGTH} chars]`;
752
+ }
753
+ function safeSerialize(arg) {
754
+ if (arg === void 0) return "undefined";
755
+ if (arg === null) return "null";
756
+ if (typeof arg === "function") return `[Function: ${arg.name || "anonymous"}]`;
757
+ if (typeof arg === "symbol") return String(arg);
758
+ if (typeof arg === "string") return truncateLogString(arg);
759
+ if (arg instanceof Error) return truncateLogString(arg.stack || arg.message || String(arg));
760
+ if (typeof arg === "object") try {
761
+ const seen = /* @__PURE__ */ new WeakSet();
762
+ return truncateLogString(JSON.stringify(arg, (key, value) => {
763
+ if (typeof value === "object" && value !== null) {
764
+ if (seen.has(value)) return "[Circular]";
765
+ seen.add(value);
766
+ if (value instanceof Map) return {
767
+ dataType: "Map",
768
+ value: Array.from(value.entries())
769
+ };
770
+ if (value instanceof Set) return {
771
+ dataType: "Set",
772
+ value: Array.from(value.values())
773
+ };
774
+ }
775
+ return value;
776
+ }));
777
+ } catch {
778
+ return truncateLogString(String(arg));
779
+ }
780
+ return truncateLogString(String(arg));
781
+ }
782
+ function sendLog(level, args) {
783
+ sendMessage({
784
+ method: "log",
785
+ params: {
786
+ level,
787
+ args: args.map(safeSerialize)
788
+ }
789
+ });
790
+ }
791
+ var logger = {
792
+ log: (...args) => {
793
+ console.log(...args);
794
+ sendLog("log", args);
795
+ },
796
+ debug: (...args) => {
797
+ console.debug(...args);
798
+ sendLog("debug", args);
799
+ },
800
+ info: (...args) => {
801
+ console.info(...args);
802
+ sendLog("info", args);
803
+ },
804
+ warn: (...args) => {
805
+ console.warn(...args);
806
+ sendLog("warn", args);
807
+ },
808
+ error: (...args) => {
809
+ console.error(...args);
810
+ sendLog("error", args);
811
+ }
812
+ };
813
+ function getCallStack() {
814
+ return ((/* @__PURE__ */ new Error()).stack || "").split("\n").slice(2, 6).join(" <- ").replace(/\s+/g, " ");
815
+ }
816
+ self.addEventListener("error", (event) => {
817
+ const stack = event.error?.stack || `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`;
818
+ logger.error("Uncaught error:", stack);
819
+ });
820
+ self.addEventListener("unhandledrejection", (event) => {
821
+ const reason = event.reason;
822
+ const stack = reason?.stack || String(reason);
823
+ logger.error("Unhandled promise rejection:", stack);
824
+ });
825
+ var messageCount = 0;
826
+ function sendMessage(message) {
827
+ if (connectionManager.ws?.readyState === WebSocket.OPEN) try {
828
+ connectionManager.ws.send(JSON.stringify(message));
829
+ if (++messageCount % 100 === 0) checkMemory();
830
+ } catch (error) {
831
+ console.debug("ERROR sending message:", error, "message type:", message.method || "response");
832
+ }
833
+ }
834
+ async function syncTabGroup() {
835
+ try {
836
+ const { connectionState } = store.getState();
837
+ const isRelayConnected = connectionState === "connected";
838
+ const connectedTabIds = Array.from(store.getState().tabs.entries()).filter(([_, info]) => info.state === "connected" || info.state === "connecting" && isRelayConnected).map(([tabId]) => tabId);
839
+ const existingGroups = await chrome.tabGroups.query({ title: TAB_GROUP_TITLE });
840
+ if (connectedTabIds.length === 0) {
841
+ for (const group of existingGroups) {
842
+ const tabIdsToUngroup = (await chrome.tabs.query({ groupId: group.id })).map((t) => t.id).filter((id) => id !== void 0);
843
+ if (tabIdsToUngroup.length > 0) await chrome.tabs.ungroup(tabIdsToUngroup);
844
+ logger.debug("Cleared playwriter group:", group.id);
845
+ }
846
+ return;
847
+ }
848
+ let groupId = existingGroups[0]?.id;
849
+ if (existingGroups.length > 1) {
850
+ const [keep, ...duplicates] = existingGroups;
851
+ groupId = keep.id;
852
+ for (const group of duplicates) {
853
+ const tabIdsToUngroup = (await chrome.tabs.query({ groupId: group.id })).map((t) => t.id).filter((id) => id !== void 0);
854
+ if (tabIdsToUngroup.length > 0) await chrome.tabs.ungroup(tabIdsToUngroup);
855
+ logger.debug("Removed duplicate playwriter group:", group.id);
856
+ }
857
+ }
858
+ const tabsInGroup = (await chrome.tabs.query({})).filter((t) => t.groupId === groupId && t.id !== void 0);
859
+ const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id));
860
+ const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id));
861
+ const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id));
862
+ if (tabsToRemove.length > 0) try {
863
+ await chrome.tabs.ungroup(tabsToRemove);
864
+ logger.debug("Removed tabs from group:", tabsToRemove);
865
+ } catch (e) {
866
+ logger.debug("Failed to ungroup tabs:", tabsToRemove, e.message);
867
+ }
868
+ if (tabsToAdd.length > 0) if (groupId === void 0) {
869
+ const newGroupId = await chrome.tabs.group({ tabIds: tabsToAdd });
870
+ await chrome.tabGroups.update(newGroupId, {
871
+ title: TAB_GROUP_TITLE,
872
+ color: TAB_GROUP_COLOR
873
+ });
874
+ logger.debug("Created tab group:", newGroupId, "with tabs:", tabsToAdd);
875
+ } else {
876
+ await chrome.tabs.group({
877
+ tabIds: tabsToAdd,
878
+ groupId
879
+ });
880
+ await chrome.tabGroups.update(groupId, {
881
+ title: TAB_GROUP_TITLE,
882
+ color: TAB_GROUP_COLOR
883
+ });
884
+ logger.debug("Added tabs to existing group:", tabsToAdd);
885
+ }
886
+ else if (groupId !== void 0) await chrome.tabGroups.update(groupId, {
887
+ title: TAB_GROUP_TITLE,
888
+ color: TAB_GROUP_COLOR
889
+ });
890
+ } catch (error) {
891
+ logger.debug("Failed to sync tab group:", error.message);
892
+ }
893
+ }
894
+ function getTabBySessionId(sessionId) {
895
+ for (const [tabId, tab] of store.getState().tabs) if (tab.sessionId === sessionId) return {
896
+ tabId,
897
+ tab
898
+ };
899
+ }
900
+ function getTabByTargetId(targetId) {
901
+ for (const [tabId, tab] of store.getState().tabs) if (tab.targetId === targetId) return {
902
+ tabId,
903
+ tab
904
+ };
905
+ }
906
+ function emitChildDetachesForTab(tabId) {
907
+ Array.from(childSessions.entries()).filter(([_, parentTab]) => parentTab.tabId === tabId).forEach(([childSessionId, parentTab]) => {
908
+ sendMessage({
909
+ method: "forwardCDPEvent",
910
+ params: {
911
+ method: "Target.detachedFromTarget",
912
+ params: parentTab.targetId ? {
913
+ sessionId: childSessionId,
914
+ targetId: parentTab.targetId
915
+ } : { sessionId: childSessionId }
916
+ }
917
+ });
918
+ logger.debug("Cleaning up child session:", childSessionId, "for tab:", tabId);
919
+ childSessions.delete(childSessionId);
920
+ });
921
+ }
922
+ function getTabForCommand(msg) {
923
+ const sessionId = msg.params.sessionId;
924
+ if (sessionId) {
925
+ const found = getTabBySessionId(sessionId);
926
+ if (found) return found;
927
+ const child = childSessions.get(sessionId);
928
+ if (child) {
929
+ const tab = store.getState().tabs.get(child.tabId);
930
+ if (tab) return {
931
+ tabId: child.tabId,
932
+ tab
933
+ };
934
+ }
935
+ }
936
+ const paramsSessionId = msg.params.params && "sessionId" in msg.params.params && typeof msg.params.params.sessionId === "string" ? msg.params.params.sessionId : void 0;
937
+ if (paramsSessionId) {
938
+ const found = getTabBySessionId(paramsSessionId);
939
+ if (found) return found;
940
+ const child = childSessions.get(paramsSessionId);
941
+ if (child) {
942
+ const tab = store.getState().tabs.get(child.tabId);
943
+ if (tab) return {
944
+ tabId: child.tabId,
945
+ tab
946
+ };
947
+ }
948
+ }
949
+ const targetId = msg.params.params && "targetId" in msg.params.params && typeof msg.params.params.targetId === "string" ? msg.params.params.targetId : void 0;
950
+ if (targetId) return getTabByTargetId(targetId);
951
+ }
952
+ async function handleCommand(msg) {
953
+ if (msg.method !== "forwardCDPCommand") return;
954
+ const resolved = getTabForCommand(msg);
955
+ let targetTabId = resolved?.tabId;
956
+ let targetTab = resolved?.tab;
957
+ const debuggee = targetTabId ? { tabId: targetTabId } : void 0;
958
+ if (msg.params.method === "Target.setAutoAttach" && !msg.params.sessionId) {
959
+ const params = msg.params.params;
960
+ if (!params) return {};
961
+ autoAttachParams = params;
962
+ const connectedTabIds = Array.from(store.getState().tabs.entries()).filter(([_, info]) => info.state === "connected").map(([tabId]) => tabId);
963
+ await Promise.all(connectedTabIds.map(async (tabId) => {
964
+ try {
965
+ await chrome.debugger.sendCommand({ tabId }, "Target.setAutoAttach", params);
966
+ } catch (error) {
967
+ logger.debug("Failed to set auto-attach for tab:", tabId, error);
968
+ }
969
+ }));
970
+ return {};
971
+ }
972
+ switch (msg.params.method) {
973
+ case "Runtime.enable": {
974
+ if (!debuggee) throw new Error(`No debuggee found for Runtime.enable (sessionId: ${msg.params.sessionId})`);
975
+ const runtimeSession = {
976
+ ...debuggee,
977
+ sessionId: msg.params.sessionId !== targetTab?.sessionId ? msg.params.sessionId : void 0
978
+ };
979
+ try {
980
+ await chrome.debugger.sendCommand(runtimeSession, "Runtime.disable");
981
+ await sleep(50);
982
+ } catch (e) {
983
+ logger.debug("Error disabling Runtime (ignoring):", e);
984
+ }
985
+ return await chrome.debugger.sendCommand(runtimeSession, "Runtime.enable", msg.params.params);
986
+ }
987
+ case "Target.createTarget": {
988
+ const url = msg.params.params?.url || "about:blank";
989
+ logger.debug("Creating new tab with URL:", url);
990
+ const tab = await chrome.tabs.create({
991
+ url,
992
+ active: false
993
+ });
994
+ if (!tab.id) throw new Error("Failed to create tab");
995
+ setTabConnecting(tab.id);
996
+ logger.debug("Created tab:", tab.id, "waiting for it to load...");
997
+ await sleep(100);
998
+ const { targetInfo } = await attachTab(tab.id);
999
+ return { targetId: targetInfo.targetId };
1000
+ }
1001
+ case "Target.closeTarget":
1002
+ if (!targetTabId) {
1003
+ logger.log(`Target not found: ${msg.params.params?.targetId}`);
1004
+ return { success: false };
1005
+ }
1006
+ await chrome.tabs.remove(targetTabId);
1007
+ return { success: true };
1008
+ }
1009
+ if (!debuggee || !targetTab) {
1010
+ if (msg.params.method === "Target.detachFromTarget") return {};
1011
+ throw new Error(`No tab found for method ${msg.params.method} sessionId: ${msg.params.sessionId} params: ${JSON.stringify(msg.params.params || null)}`);
1012
+ }
1013
+ logger.debug("CDP command:", msg.params.method, "for tab:", targetTabId);
1014
+ const debuggerSession = {
1015
+ ...debuggee,
1016
+ sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : void 0
1017
+ };
1018
+ return await chrome.debugger.sendCommand(debuggerSession, msg.params.method, msg.params.params);
1019
+ }
1020
+ function onDebuggerEvent(source, method, params) {
1021
+ const tab = source.tabId ? store.getState().tabs.get(source.tabId) : void 0;
1022
+ if (!tab) return;
1023
+ logger.debug("Forwarding CDP event:", method, "from tab:", source.tabId);
1024
+ if (method === "Target.attachedToTarget" && params?.sessionId) {
1025
+ const targetUrl = params.targetInfo?.url;
1026
+ if (isRestrictedUrl(targetUrl)) {
1027
+ logger.debug("Ignoring restricted child target:", targetUrl, "sessionId:", params.sessionId, "for tab:", source.tabId);
1028
+ if (source.tabId) chrome.debugger.sendCommand({ tabId: source.tabId }, "Target.detachFromTarget", { sessionId: params.sessionId }).catch((e) => {
1029
+ logger.debug("Failed to detach restricted child target (expected):", e);
1030
+ });
1031
+ return;
1032
+ }
1033
+ logger.debug("Child target attached:", params.sessionId, "for tab:", source.tabId);
1034
+ const targetId = params.targetInfo?.targetId;
1035
+ childSessions.set(params.sessionId, {
1036
+ tabId: source.tabId,
1037
+ targetId
1038
+ });
1039
+ }
1040
+ if (method === "Target.detachedFromTarget" && params?.sessionId) {
1041
+ const mainTab = getTabBySessionId(params.sessionId);
1042
+ if (mainTab) {
1043
+ logger.debug("Main tab detached via CDP event:", mainTab.tabId, "sessionId:", params.sessionId);
1044
+ store.setState((state) => {
1045
+ const newTabs = new Map(state.tabs);
1046
+ newTabs.delete(mainTab.tabId);
1047
+ return { tabs: newTabs };
1048
+ });
1049
+ emitChildDetachesForTab(mainTab.tabId);
1050
+ } else {
1051
+ logger.debug("Child target detached:", params.sessionId);
1052
+ childSessions.delete(params.sessionId);
1053
+ }
1054
+ }
1055
+ sendMessage({
1056
+ method: "forwardCDPEvent",
1057
+ params: {
1058
+ sessionId: source.sessionId || tab.sessionId,
1059
+ method,
1060
+ params
1061
+ }
1062
+ });
1063
+ }
1064
+ function onDebuggerDetach(source, reason) {
1065
+ const tabId = source.tabId;
1066
+ if (!tabId || !store.getState().tabs.has(tabId)) {
1067
+ logger.debug("Ignoring debugger detach event for untracked tab:", tabId);
1068
+ return;
1069
+ }
1070
+ if (connectionManager.preserveTabsOnDetach) {
1071
+ logger.debug("Ignoring debugger detach during relay reconnect:", tabId, reason);
1072
+ return;
1073
+ }
1074
+ logger.warn(`DISCONNECT: onDebuggerDetach tabId=${tabId} reason=${reason}`);
1075
+ const tab = store.getState().tabs.get(tabId);
1076
+ if (tab) sendMessage({
1077
+ method: "forwardCDPEvent",
1078
+ params: {
1079
+ method: "Target.detachedFromTarget",
1080
+ params: {
1081
+ sessionId: tab.sessionId,
1082
+ targetId: tab.targetId
1083
+ }
1084
+ }
1085
+ });
1086
+ emitChildDetachesForTab(tabId);
1087
+ store.setState((state) => {
1088
+ const newTabs = new Map(state.tabs);
1089
+ newTabs.delete(tabId);
1090
+ return { tabs: newTabs };
1091
+ });
1092
+ if (reason === chrome.debugger.DetachReason.CANCELED_BY_USER) store.setState({
1093
+ connectionState: "idle",
1094
+ errorText: void 0
1095
+ });
1096
+ }
1097
+ async function removeRestrictedIframes(tabId) {
1098
+ try {
1099
+ const totalRemoved = (await chrome.scripting.executeScript({
1100
+ target: {
1101
+ tabId,
1102
+ allFrames: true
1103
+ },
1104
+ func: (ownExtIds) => {
1105
+ const roots = [document];
1106
+ document.querySelectorAll("*").forEach((el) => {
1107
+ const shadow = el.shadowRoot;
1108
+ if (shadow) roots.push(shadow);
1109
+ });
1110
+ let removed = 0;
1111
+ for (const root of roots) root.querySelectorAll("iframe").forEach((iframe) => {
1112
+ const src = iframe.src || iframe.getAttribute("src") || "";
1113
+ if (!src.startsWith("chrome-extension://")) return;
1114
+ const extId = src.replace("chrome-extension://", "").split("/")[0];
1115
+ if (ownExtIds.includes(extId)) return;
1116
+ iframe.remove();
1117
+ removed++;
1118
+ });
1119
+ return removed;
1120
+ },
1121
+ args: [OUR_EXTENSION_IDS]
1122
+ })).reduce((sum, r) => sum + (r.result ?? 0), 0);
1123
+ if (totalRemoved > 0) logger.debug(`Removed ${totalRemoved} restricted chrome-extension:// iframe(s) from tab:`, tabId);
1124
+ return totalRemoved;
1125
+ } catch (e) {
1126
+ logger.debug("Could not remove restricted iframes (expected on some pages):", e.message);
1127
+ return 0;
1128
+ }
1129
+ }
1130
+ async function attachTab(tabId, { skipAttachedEvent = false } = {}) {
1131
+ const debuggee = { tabId };
1132
+ let debuggerAttached = false;
1133
+ try {
1134
+ logger.debug("Attaching debugger to tab:", tabId);
1135
+ const maxAttachAttempts = 3;
1136
+ for (let attempt = 1; attempt <= maxAttachAttempts; attempt++) try {
1137
+ await chrome.debugger.attach(debuggee, "1.3");
1138
+ break;
1139
+ } catch (attachError) {
1140
+ const msg = attachError.message ?? "";
1141
+ if (!(msg.includes("chrome-extension://") || msg.includes("different extension")) || attempt === maxAttachAttempts) throw attachError;
1142
+ logger.debug(`Debugger attach blocked by chrome-extension:// iframe (attempt ${attempt}/${maxAttachAttempts}), removing and retrying:`, tabId);
1143
+ await removeRestrictedIframes(tabId);
1144
+ await sleep(50);
1145
+ }
1146
+ debuggerAttached = true;
1147
+ logger.debug("Debugger attached successfully to tab:", tabId);
1148
+ await chrome.debugger.sendCommand(debuggee, "Page.enable");
1149
+ if (autoAttachParams) try {
1150
+ await chrome.debugger.sendCommand(debuggee, "Target.setAutoAttach", autoAttachParams);
1151
+ } catch (error) {
1152
+ logger.debug("Failed to apply auto-attach for tab:", tabId, error);
1153
+ }
1154
+ const contextMenuScript = `
1155
+ document.addEventListener('contextmenu', (e) => {
1156
+ window.__playwriter_lastRightClicked = e.target;
1157
+ }, true);
1158
+ `;
1159
+ await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: contextMenuScript });
1160
+ await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: contextMenuScript });
1161
+ const targetInfo = (await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")).targetInfo;
1162
+ if (!targetInfo.url || targetInfo.url === "" || targetInfo.url === ":") logger.error("WARNING: Target.attachedToTarget will be sent with empty URL! tabId:", tabId, "targetInfo:", JSON.stringify(targetInfo));
1163
+ const attachOrder = nextSessionId;
1164
+ const sessionId = `pw-tab-${tabSessionScope}-${nextSessionId++}`;
1165
+ store.setState((state) => {
1166
+ const newTabs = new Map(state.tabs);
1167
+ newTabs.set(tabId, {
1168
+ sessionId,
1169
+ targetId: targetInfo.targetId,
1170
+ state: "connected",
1171
+ attachOrder
1172
+ });
1173
+ return {
1174
+ tabs: newTabs,
1175
+ connectionState: "connected",
1176
+ errorText: void 0
1177
+ };
1178
+ });
1179
+ if (!skipAttachedEvent) sendMessage({
1180
+ method: "forwardCDPEvent",
1181
+ params: {
1182
+ method: "Target.attachedToTarget",
1183
+ params: {
1184
+ sessionId,
1185
+ targetInfo: {
1186
+ ...targetInfo,
1187
+ attached: true
1188
+ },
1189
+ waitingForDebugger: false
1190
+ }
1191
+ }
1192
+ });
1193
+ logger.debug("Tab attached successfully:", tabId, "sessionId:", sessionId, "targetId:", targetInfo.targetId, "url:", targetInfo.url, "skipAttachedEvent:", skipAttachedEvent);
1194
+ return {
1195
+ targetInfo,
1196
+ sessionId
1197
+ };
1198
+ } catch (error) {
1199
+ if (debuggerAttached) {
1200
+ logger.debug("Cleaning up debugger after partial attach failure:", tabId);
1201
+ chrome.debugger.detach(debuggee).catch(() => {});
1202
+ }
1203
+ throw error;
1204
+ }
1205
+ }
1206
+ function detachTab(tabId, shouldDetachDebugger) {
1207
+ const tab = store.getState().tabs.get(tabId);
1208
+ if (!tab) {
1209
+ logger.debug("detachTab: tab not found in map:", tabId);
1210
+ return;
1211
+ }
1212
+ cleanupRecordingForTab(tabId);
1213
+ logger.warn(`DISCONNECT: detachTab tabId=${tabId} shouldDetach=${shouldDetachDebugger} stack=${getCallStack()}`);
1214
+ if (tab.sessionId && tab.targetId) sendMessage({
1215
+ method: "forwardCDPEvent",
1216
+ params: {
1217
+ method: "Target.detachedFromTarget",
1218
+ params: {
1219
+ sessionId: tab.sessionId,
1220
+ targetId: tab.targetId
1221
+ }
1222
+ }
1223
+ });
1224
+ store.setState((state) => {
1225
+ const newTabs = new Map(state.tabs);
1226
+ newTabs.delete(tabId);
1227
+ return { tabs: newTabs };
1228
+ });
1229
+ emitChildDetachesForTab(tabId);
1230
+ if (shouldDetachDebugger) chrome.debugger.detach({ tabId }).catch((err) => {
1231
+ logger.debug("Error detaching debugger from tab:", tabId, err.message);
1232
+ });
1233
+ }
1234
+ async function connectTab(tabId) {
1235
+ try {
1236
+ logger.debug(`Starting connection to tab ${tabId}`);
1237
+ setTabConnecting(tabId);
1238
+ await connectionManager.ensureConnection();
1239
+ await attachTab(tabId);
1240
+ logger.debug(`Successfully connected to tab ${tabId}`);
1241
+ } catch (error) {
1242
+ logger.debug(`Failed to connect to tab ${tabId}:`, error);
1243
+ const isExtensionInUse = error.message === "Extension Already In Use" || error.message === "Another Playwriter extension is already connected";
1244
+ const isWsError = error.message === "Server not available" || error.message === "Connection timeout" || error.message.startsWith("WebSocket");
1245
+ if (isExtensionInUse) {
1246
+ logger.debug(`Another extension is in use, entering polling mode`);
1247
+ store.setState((state) => {
1248
+ const newTabs = new Map(state.tabs);
1249
+ newTabs.delete(tabId);
1250
+ return {
1251
+ tabs: newTabs,
1252
+ connectionState: "extension-replaced",
1253
+ errorText: "Another Playwriter extension is actively in use"
1254
+ };
1255
+ });
1256
+ } else if (isWsError) logger.debug(`WS connection failed, keeping tab ${tabId} in connecting state for retry`);
1257
+ else store.setState((state) => {
1258
+ const newTabs = new Map(state.tabs);
1259
+ newTabs.set(tabId, {
1260
+ state: "error",
1261
+ errorText: `Error: ${error.message}`
1262
+ });
1263
+ return { tabs: newTabs };
1264
+ });
1265
+ }
1266
+ }
1267
+ function setTabConnecting(tabId) {
1268
+ store.setState((state) => {
1269
+ const newTabs = new Map(state.tabs);
1270
+ const existing = newTabs.get(tabId);
1271
+ newTabs.set(tabId, {
1272
+ ...existing,
1273
+ state: "connecting"
1274
+ });
1275
+ return { tabs: newTabs };
1276
+ });
1277
+ }
1278
+ async function disconnectTab(tabId) {
1279
+ logger.debug(`Disconnecting tab ${tabId}`);
1280
+ const { tabs } = store.getState();
1281
+ if (!tabs.has(tabId)) {
1282
+ logger.debug("Tab not in tabs map, ignoring disconnect");
1283
+ return;
1284
+ }
1285
+ detachTab(tabId, true);
1286
+ }
1287
+ async function toggleExtensionForActiveTab() {
1288
+ const tab = (await chrome.tabs.query({
1289
+ active: true,
1290
+ currentWindow: true
1291
+ }))[0];
1292
+ if (!tab?.id) throw new Error("No active tab found");
1293
+ await onActionClicked(tab);
1294
+ await new Promise((resolve) => {
1295
+ const check = () => {
1296
+ if (store.getState().tabs.get(tab.id)?.state === "connecting") {
1297
+ setTimeout(check, 100);
1298
+ return;
1299
+ }
1300
+ resolve();
1301
+ };
1302
+ check();
1303
+ });
1304
+ const state = store.getState();
1305
+ return {
1306
+ isConnected: state.tabs.has(tab.id) && state.tabs.get(tab.id)?.state === "connected",
1307
+ state
1308
+ };
1309
+ }
1310
+ async function disconnectEverything() {
1311
+ tabGroupQueue = tabGroupQueue.then(async () => {
1312
+ const { tabs } = store.getState();
1313
+ for (const tabId of tabs.keys()) await disconnectTab(tabId);
1314
+ });
1315
+ await tabGroupQueue;
1316
+ }
1317
+ async function resetDebugger() {
1318
+ let targets = await chrome.debugger.getTargets();
1319
+ targets = targets.filter((x) => x.tabId && x.attached);
1320
+ logger.log(`found ${targets.length} existing debugger targets. detaching them before background script starts`);
1321
+ for (const target of targets) await chrome.debugger.detach({ tabId: target.tabId });
1322
+ }
1323
+ var OUR_EXTENSION_IDS = ["jfeammnjpkecdekppnclgkkffahnhfhe", "pebbngnfojnignonigcnkdilknapkgid"];
1324
+ function isRestrictedUrl(url) {
1325
+ if (!url) return false;
1326
+ if (url.startsWith("chrome-extension://")) {
1327
+ const extensionId = url.replace("chrome-extension://", "").split("/")[0];
1328
+ return !OUR_EXTENSION_IDS.includes(extensionId);
1329
+ }
1330
+ return [
1331
+ "chrome://",
1332
+ "devtools://",
1333
+ "edge://",
1334
+ "https://chrome.google.com/",
1335
+ "https://chromewebstore.google.com/"
1336
+ ].some((prefix) => url.startsWith(prefix));
1337
+ }
1338
+ var icons = {
1339
+ connected: {
1340
+ path: {
1341
+ "16": "/icons/icon-green-16.png",
1342
+ "32": "/icons/icon-green-32.png",
1343
+ "48": "/icons/icon-green-48.png",
1344
+ "128": "/icons/icon-green-128.png"
1345
+ },
1346
+ title: "Connected - Click to disconnect",
1347
+ badgeText: "",
1348
+ badgeColor: [
1349
+ 64,
1350
+ 64,
1351
+ 64,
1352
+ 255
1353
+ ]
1354
+ },
1355
+ connecting: {
1356
+ path: {
1357
+ "16": "/icons/icon-gray-16.png",
1358
+ "32": "/icons/icon-gray-32.png",
1359
+ "48": "/icons/icon-gray-48.png",
1360
+ "128": "/icons/icon-gray-128.png"
1361
+ },
1362
+ title: "Waiting for MCP WS server...",
1363
+ badgeText: "...",
1364
+ badgeColor: [
1365
+ 64,
1366
+ 64,
1367
+ 64,
1368
+ 255
1369
+ ]
1370
+ },
1371
+ idle: {
1372
+ path: {
1373
+ "16": "/icons/icon-black-16.png",
1374
+ "32": "/icons/icon-black-32.png",
1375
+ "48": "/icons/icon-black-48.png",
1376
+ "128": "/icons/icon-black-128.png"
1377
+ },
1378
+ title: "Click to attach debugger",
1379
+ badgeText: "",
1380
+ badgeColor: [
1381
+ 64,
1382
+ 64,
1383
+ 64,
1384
+ 255
1385
+ ]
1386
+ },
1387
+ restricted: {
1388
+ path: {
1389
+ "16": "/icons/icon-gray-16.png",
1390
+ "32": "/icons/icon-gray-32.png",
1391
+ "48": "/icons/icon-gray-48.png",
1392
+ "128": "/icons/icon-gray-128.png"
1393
+ },
1394
+ title: "Cannot attach to this page",
1395
+ badgeText: "",
1396
+ badgeColor: [
1397
+ 64,
1398
+ 64,
1399
+ 64,
1400
+ 255
1401
+ ]
1402
+ },
1403
+ extensionReplaced: {
1404
+ path: {
1405
+ "16": "/icons/icon-gray-16.png",
1406
+ "32": "/icons/icon-gray-32.png",
1407
+ "48": "/icons/icon-gray-48.png",
1408
+ "128": "/icons/icon-gray-128.png"
1409
+ },
1410
+ title: "Another Playwriter extension connected - Click to retry",
1411
+ badgeText: "!",
1412
+ badgeColor: [
1413
+ 220,
1414
+ 38,
1415
+ 38,
1416
+ 255
1417
+ ]
1418
+ },
1419
+ tabError: {
1420
+ path: {
1421
+ "16": "/icons/icon-gray-16.png",
1422
+ "32": "/icons/icon-gray-32.png",
1423
+ "48": "/icons/icon-gray-48.png",
1424
+ "128": "/icons/icon-gray-128.png"
1425
+ },
1426
+ title: "Error",
1427
+ badgeText: "!",
1428
+ badgeColor: [
1429
+ 220,
1430
+ 38,
1431
+ 38,
1432
+ 255
1433
+ ]
1434
+ }
1435
+ };
1436
+ async function updateIcons() {
1437
+ const { connectionState, tabs, errorText } = store.getState();
1438
+ const connectedCount = Array.from(tabs.values()).filter((t) => t.state === "connected").length;
1439
+ const allTabs = await chrome.tabs.query({});
1440
+ const tabUrlMap = new Map(allTabs.map((tab) => [tab.id, tab.url]));
1441
+ const allTabIds = [void 0, ...allTabs.map((tab) => tab.id).filter((id) => id !== void 0)];
1442
+ for (const tabId of allTabIds) {
1443
+ const tabInfo = tabId !== void 0 ? tabs.get(tabId) : void 0;
1444
+ const tabUrl = tabId !== void 0 ? tabUrlMap.get(tabId) : void 0;
1445
+ const iconConfig = (() => {
1446
+ if (connectionState === "extension-replaced") return icons.extensionReplaced;
1447
+ if (tabId !== void 0 && isRestrictedUrl(tabUrl)) return icons.restricted;
1448
+ if (tabInfo?.state === "error") return icons.tabError;
1449
+ if (tabInfo?.state === "connecting") return icons.connecting;
1450
+ if (tabInfo?.state === "connected") return icons.connected;
1451
+ return icons.idle;
1452
+ })();
1453
+ const title = (() => {
1454
+ if (connectionState === "extension-replaced" && errorText) return errorText;
1455
+ if (tabInfo?.errorText) return tabInfo.errorText;
1456
+ return iconConfig.title;
1457
+ })();
1458
+ const badgeText = (() => {
1459
+ if (iconConfig === icons.connected || iconConfig === icons.idle || iconConfig === icons.restricted) return connectedCount > 0 ? String(connectedCount) : "";
1460
+ return iconConfig.badgeText;
1461
+ })();
1462
+ chrome.action.setIcon({
1463
+ tabId,
1464
+ path: iconConfig.path
1465
+ });
1466
+ chrome.action.setTitle({
1467
+ tabId,
1468
+ title
1469
+ });
1470
+ if (iconConfig.badgeColor) chrome.action.setBadgeBackgroundColor({
1471
+ tabId,
1472
+ color: iconConfig.badgeColor
1473
+ });
1474
+ chrome.action.setBadgeText({
1475
+ tabId,
1476
+ text: badgeText
1477
+ });
1478
+ }
1479
+ }
1480
+ async function onTabRemoved(tabId) {
1481
+ const { tabs } = store.getState();
1482
+ if (!tabs.has(tabId)) return;
1483
+ logger.debug(`Connected tab ${tabId} was closed, disconnecting`);
1484
+ await disconnectTab(tabId);
1485
+ }
1486
+ async function onTabActivated(activeInfo) {
1487
+ store.setState({ currentTabId: activeInfo.tabId });
1488
+ }
1489
+ async function onActionClicked(tab) {
1490
+ if (!tab.id) {
1491
+ logger.debug("No tab ID available");
1492
+ return;
1493
+ }
1494
+ if (isRestrictedUrl(tab.url)) {
1495
+ logger.debug("Cannot attach to restricted URL:", tab.url);
1496
+ return;
1497
+ }
1498
+ const { tabs, connectionState } = store.getState();
1499
+ const tabInfo = tabs.get(tab.id);
1500
+ if (connectionState === "extension-replaced") {
1501
+ logger.debug("Clearing extension-replaced state, attempting to reconnect");
1502
+ store.setState({
1503
+ connectionState: "idle",
1504
+ errorText: void 0
1505
+ });
1506
+ await connectTab(tab.id);
1507
+ return;
1508
+ }
1509
+ if (tabInfo?.state === "error") {
1510
+ logger.debug("Tab has error - disconnecting to clear state");
1511
+ await disconnectTab(tab.id);
1512
+ return;
1513
+ }
1514
+ if (tabInfo?.state === "connecting") {
1515
+ logger.debug("Tab is already connecting, ignoring click");
1516
+ return;
1517
+ }
1518
+ if (tabInfo?.state === "connected") await disconnectTab(tab.id);
1519
+ else await connectTab(tab.id);
1520
+ }
1521
+ resetDebugger();
1522
+ connectionManager.maintainLoop();
1523
+ chrome.contextMenus.remove("playwriter-pin-element").catch(() => {}).finally(() => {
1524
+ chrome.contextMenus?.create({
1525
+ id: "playwriter-pin-element",
1526
+ title: "Copy Playwriter Element Reference",
1527
+ contexts: ["all"],
1528
+ visible: false
1529
+ });
1530
+ });
1531
+ function updateContextMenuVisibility() {
1532
+ const { currentTabId, tabs } = store.getState();
1533
+ const isConnected = currentTabId !== void 0 && tabs.get(currentTabId)?.state === "connected";
1534
+ chrome.contextMenus?.update("playwriter-pin-element", { visible: isConnected });
1535
+ }
1536
+ chrome.runtime.onInstalled.addListener((details) => {});
1537
+ function serializeTabs(tabs) {
1538
+ return JSON.stringify(Array.from(tabs.entries()));
1539
+ }
1540
+ store.subscribe((state, prevState) => {
1541
+ logger.log(state);
1542
+ updateIcons();
1543
+ updateContextMenuVisibility();
1544
+ if (serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)) tabGroupQueue = tabGroupQueue.then(syncTabGroup).catch((e) => {
1545
+ logger.debug("syncTabGroup error:", e);
1546
+ });
1547
+ });
1548
+ logger.debug(`Using relay host: ${RELAY_HOST}, port: ${RELAY_PORT}`);
1549
+ var lastMemoryUsage = 0;
1550
+ var lastMemoryCheck = Date.now();
1551
+ var MEMORY_WARNING_THRESHOLD = 50 * 1024 * 1024;
1552
+ var MEMORY_CRITICAL_THRESHOLD = 100 * 1024 * 1024;
1553
+ var MEMORY_GROWTH_THRESHOLD = 10 * 1024 * 1024;
1554
+ function checkMemory() {
1555
+ try {
1556
+ const memory = performance.memory;
1557
+ if (!memory) return;
1558
+ const used = memory.usedJSHeapSize;
1559
+ const total = memory.totalJSHeapSize;
1560
+ const limit = memory.jsHeapSizeLimit;
1561
+ const now = Date.now();
1562
+ const timeDelta = now - lastMemoryCheck;
1563
+ const memoryDelta = used - lastMemoryUsage;
1564
+ const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2) + "MB";
1565
+ const growthRate = timeDelta > 0 ? memoryDelta / timeDelta * 1e3 : 0;
1566
+ if (used > MEMORY_CRITICAL_THRESHOLD) logger.error(`MEMORY CRITICAL: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`);
1567
+ else if (used > MEMORY_WARNING_THRESHOLD) logger.warn(`MEMORY WARNING: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`);
1568
+ else if (memoryDelta > MEMORY_GROWTH_THRESHOLD && timeDelta < 6e4) logger.warn(`MEMORY SPIKE: grew ${formatMB(memoryDelta)} in ${(timeDelta / 1e3).toFixed(1)}s (used=${formatMB(used)})`);
1569
+ lastMemoryUsage = used;
1570
+ lastMemoryCheck = now;
1571
+ } catch (e) {}
1572
+ }
1573
+ setInterval(checkMemory, 5e3);
1574
+ checkMemory();
1575
+ chrome.tabs.onRemoved.addListener(onTabRemoved);
1576
+ chrome.tabs.onActivated.addListener(onTabActivated);
1577
+ chrome.action.onClicked.addListener(onActionClicked);
1578
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1579
+ updateIcons();
1580
+ if (changeInfo.groupId !== void 0) tabGroupQueue = tabGroupQueue.then(async () => {
1581
+ const groupId = (await chrome.tabGroups.query({ title: TAB_GROUP_TITLE }))[0]?.id;
1582
+ if (groupId === void 0) return;
1583
+ const { tabs } = store.getState();
1584
+ if (changeInfo.groupId === groupId) {
1585
+ if (!tabs.has(tabId) && !isRestrictedUrl(tab.url)) {
1586
+ logger.debug("Tab manually added to playwriter group:", tabId);
1587
+ await connectTab(tabId);
1588
+ }
1589
+ } else if (tabs.has(tabId)) {
1590
+ if (tabs.get(tabId)?.state === "connecting") {
1591
+ logger.debug("Tab removed from group while connecting, ignoring:", tabId);
1592
+ return;
1593
+ }
1594
+ logger.debug("Tab manually removed from playwriter group:", tabId);
1595
+ await disconnectTab(tabId);
1596
+ }
1597
+ }).catch((e) => {
1598
+ logger.debug("onTabUpdated handler error:", e);
1599
+ });
1600
+ });
1601
+ chrome.contextMenus?.onClicked.addListener(async (info, tab) => {
1602
+ if (info.menuItemId !== "playwriter-pin-element" || !tab?.id) return;
1603
+ const tabInfo = store.getState().tabs.get(tab.id);
1604
+ if (!tabInfo || tabInfo.state !== "connected") {
1605
+ logger.debug("Tab not connected, ignoring");
1606
+ return;
1607
+ }
1608
+ const debuggee = { tabId: tab.id };
1609
+ const count = (tabInfo.pinnedCount || 0) + 1;
1610
+ store.setState((state) => {
1611
+ const newTabs = new Map(state.tabs);
1612
+ const existing = newTabs.get(tab.id);
1613
+ if (existing) newTabs.set(tab.id, {
1614
+ ...existing,
1615
+ pinnedCount: count
1616
+ });
1617
+ return { tabs: newTabs };
1618
+ });
1619
+ const name = `playwriterPinnedElem${count}`;
1620
+ const connectedTabs = Array.from(store.getState().tabs.entries()).filter(([_, t]) => t.state === "connected").sort((a, b) => (a[1].attachOrder ?? 0) - (b[1].attachOrder ?? 0));
1621
+ const pageIndex = connectedTabs.findIndex(([id]) => id === tab.id);
1622
+ const hasMultiplePages = connectedTabs.length > 1;
1623
+ try {
1624
+ const result = await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
1625
+ expression: `
1626
+ if (window.__playwriter_lastRightClicked) {
1627
+ window.${name} = window.__playwriter_lastRightClicked;
1628
+ '${name}';
1629
+ } else {
1630
+ throw new Error('No element was right-clicked');
1631
+ }
1632
+ `,
1633
+ returnByValue: true
1634
+ });
1635
+ if (result.exceptionDetails) {
1636
+ logger.error("Failed to pin element:", result.exceptionDetails.text);
1637
+ return;
1638
+ }
1639
+ const clipboardText = hasMultiplePages ? `globalThis.${name} (page ${pageIndex}, ${tab.url || "unknown url"})` : `globalThis.${name}`;
1640
+ await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
1641
+ expression: `
1642
+ (() => {
1643
+ const el = window.${name};
1644
+ if (!el) return;
1645
+ const orig = el.getAttribute('style') || '';
1646
+ el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
1647
+ setTimeout(() => el.setAttribute('style', orig), 300);
1648
+ return navigator.clipboard.writeText(${JSON.stringify(clipboardText)});
1649
+ })()
1650
+ `,
1651
+ awaitPromise: true
1652
+ });
1653
+ logger.debug("Pinned element as:", name);
1654
+ } catch (error) {
1655
+ logger.error("Failed to pin element:", error.message);
1656
+ }
1657
+ });
1658
+ updateIcons();
1659
+ chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
1660
+ if (message.action === "recordingChunk") {
1661
+ const { tabId, data, final } = message;
1662
+ if (connectionManager.ws?.readyState === WebSocket.OPEN) {
1663
+ sendMessage({
1664
+ method: "recordingData",
1665
+ params: {
1666
+ tabId,
1667
+ final
1668
+ }
1669
+ });
1670
+ if (data && !final) {
1671
+ const buffer = new Uint8Array(data);
1672
+ connectionManager.ws.send(buffer);
1673
+ }
1674
+ } else {
1675
+ logger.debug(`Buffering recording chunk for tab ${tabId} (WebSocket not ready)`);
1676
+ recordingChunkBuffer.push({
1677
+ tabId,
1678
+ data,
1679
+ final
1680
+ });
1681
+ }
1682
+ return false;
1683
+ }
1684
+ if (message.action === "recordingCancelled") {
1685
+ const { tabId } = message;
1686
+ getActiveRecordings().delete(tabId);
1687
+ store.setState((state) => {
1688
+ const newTabs = new Map(state.tabs);
1689
+ const existing = newTabs.get(tabId);
1690
+ if (existing) newTabs.set(tabId, {
1691
+ ...existing,
1692
+ isRecording: false
1693
+ });
1694
+ return { tabs: newTabs };
1695
+ });
1696
+ if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
1697
+ method: "recordingCancelled",
1698
+ params: { tabId }
1699
+ });
1700
+ return false;
1701
+ }
1702
+ return false;
1703
+ });
1704
+ //#endregion