playwriter 0.0.103 → 0.1.0

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 (74) hide show
  1. package/dist/bippy.js +1 -1
  2. package/dist/cdp-relay.d.ts.map +1 -1
  3. package/dist/cdp-relay.js +5 -0
  4. package/dist/cdp-relay.js.map +1 -1
  5. package/dist/cli-help.test.d.ts +2 -0
  6. package/dist/cli-help.test.d.ts.map +1 -0
  7. package/dist/cli-help.test.js +31 -0
  8. package/dist/cli-help.test.js.map +1 -0
  9. package/dist/cli.js +13 -5
  10. package/dist/cli.js.map +1 -1
  11. package/dist/executor.d.ts +2 -1
  12. package/dist/executor.d.ts.map +1 -1
  13. package/dist/executor.js +32 -22
  14. package/dist/executor.js.map +1 -1
  15. package/dist/extension/background.js +516 -22
  16. package/dist/extension/manifest.json +4 -2
  17. package/dist/extension-connection.test.d.ts.map +1 -1
  18. package/dist/extension-connection.test.js +79 -0
  19. package/dist/extension-connection.test.js.map +1 -1
  20. package/dist/ghost-cursor-client.js +170 -83
  21. package/dist/{recording-ghost-cursor.d.ts → ghost-cursor-controller.d.ts} +15 -10
  22. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  23. package/dist/ghost-cursor-controller.js +98 -0
  24. package/dist/ghost-cursor-controller.js.map +1 -0
  25. package/dist/ghost-cursor.d.ts.map +1 -1
  26. package/dist/ghost-cursor.js +42 -26
  27. package/dist/ghost-cursor.js.map +1 -1
  28. package/dist/on-mouse-action.test.js +25 -0
  29. package/dist/on-mouse-action.test.js.map +1 -1
  30. package/dist/popup-relocation.test.d.ts +7 -0
  31. package/dist/popup-relocation.test.d.ts.map +1 -0
  32. package/dist/popup-relocation.test.js +139 -0
  33. package/dist/popup-relocation.test.js.map +1 -0
  34. package/dist/prompt.md +13 -12
  35. package/dist/readability.js +1 -1
  36. package/dist/relay-core.test.d.ts.map +1 -1
  37. package/dist/relay-core.test.js +101 -1
  38. package/dist/relay-core.test.js.map +1 -1
  39. package/dist/relay-state.d.ts +1 -0
  40. package/dist/relay-state.d.ts.map +1 -1
  41. package/dist/relay-state.js.map +1 -1
  42. package/dist/screen-recording.d.ts +2 -2
  43. package/dist/screen-recording.d.ts.map +1 -1
  44. package/dist/screen-recording.js +0 -3
  45. package/dist/screen-recording.js.map +1 -1
  46. package/dist/selector-generator.js +1 -1
  47. package/package.json +6 -6
  48. package/src/aria-snapshots/github-interactive.txt +5 -3
  49. package/src/aria-snapshots/github-raw.txt +8 -5
  50. package/src/aria-snapshots/hackernews-interactive.txt +241 -242
  51. package/src/aria-snapshots/hackernews-raw.txt +267 -268
  52. package/src/aria-snapshots/prosemirror-interactive.txt +3 -1
  53. package/src/aria-snapshots/prosemirror-raw.txt +4 -1
  54. package/src/assets/aria-labels-hacker-news.png +0 -0
  55. package/src/assets/aria-labels-old-reddit.png +0 -0
  56. package/src/cdp-relay.ts +5 -0
  57. package/src/cli-help.test.ts +41 -0
  58. package/src/cli.ts +14 -9
  59. package/src/executor.ts +33 -22
  60. package/src/extension-connection.test.ts +88 -0
  61. package/src/ghost-cursor-client.ts +221 -96
  62. package/src/{recording-ghost-cursor.ts → ghost-cursor-controller.ts} +50 -34
  63. package/src/ghost-cursor.ts +54 -41
  64. package/src/on-mouse-action.test.ts +30 -0
  65. package/src/popup-relocation.test.ts +163 -0
  66. package/src/relay-core.test.ts +117 -0
  67. package/src/relay-state.ts +1 -1
  68. package/src/screen-recording.ts +3 -6
  69. package/src/skill.md +13 -12
  70. package/src/snapshots/shadcn-ui-accessibility-full.md +174 -181
  71. package/src/snapshots/shadcn-ui-accessibility-interactive.md +6 -14
  72. package/dist/recording-ghost-cursor.d.ts.map +0 -1
  73. package/dist/recording-ghost-cursor.js +0 -79
  74. package/dist/recording-ghost-cursor.js.map +0 -1
@@ -27,6 +27,337 @@ var createStoreImpl = (createState) => {
27
27
  };
28
28
  var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
29
29
  //#endregion
30
+ //#region src/toolbar/toolbar.ts
31
+ function initPlaywriterToolbar() {
32
+ if (window.__playwriterToolbarInstalled) return;
33
+ window.__playwriterToolbarInstalled = true;
34
+ try {
35
+ if (window !== window.top) return;
36
+ } catch {
37
+ return;
38
+ }
39
+ let pinModeActive = false;
40
+ let pinCount = 0;
41
+ let toastTimer = null;
42
+ let overlayEl = null;
43
+ let pinBtn;
44
+ const host = document.createElement("div");
45
+ host.setAttribute("data-playwriter-toolbar", "1");
46
+ host.style.cssText = "position:fixed;top:12px;right:12px;z-index:2147483647;pointer-events:none;font-size:0;line-height:0;";
47
+ const shadow = host.attachShadow({ mode: "closed" });
48
+ const styleEl = document.createElement("style");
49
+ styleEl.textContent = `
50
+ *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
51
+ .toolbar {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 2px;
55
+ padding: 3px;
56
+ background: #fff;
57
+ border-radius: 10px;
58
+ pointer-events: all;
59
+ user-select: none;
60
+ box-shadow: 0px 0px 0.5px rgba(0,0,0,0.18), 0px 3px 8px rgba(0,0,0,0.1), 0px 1px 3px rgba(0,0,0,0.1);
61
+ }
62
+ .divider {
63
+ width: 1px;
64
+ height: 12px;
65
+ background: rgba(0, 0, 0, 0.08);
66
+ margin: 0 1px;
67
+ flex-shrink: 0;
68
+ }
69
+ .btn {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ width: 26px;
74
+ height: 26px;
75
+ border: none;
76
+ border-radius: 7px;
77
+ background: transparent;
78
+ color: #000;
79
+ cursor: pointer;
80
+ transition: background 0.1s;
81
+ padding: 0;
82
+ flex-shrink: 0;
83
+ outline: none;
84
+ }
85
+ .btn:hover {
86
+ background: rgba(0, 0, 0, 0.04);
87
+ }
88
+ .btn.active {
89
+ background: #0d99ff;
90
+ color: #fff;
91
+ }
92
+ .btn.active:hover {
93
+ background: #0d99ff;
94
+ filter: brightness(1.05);
95
+ }
96
+ /* When active, the logo inner cursor path needs to match the blue bg
97
+ so it appears as a "cutout" through the white outer shape */
98
+ .btn.active .logo-inner { fill: #0d99ff; }
99
+ .toast {
100
+ position: fixed;
101
+ background: #0f172a;
102
+ border-radius: 8px;
103
+ padding: 9px 18px;
104
+ color: rgba(255, 255, 255, 0.85);
105
+ font-size: 11px;
106
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
107
+ pointer-events: none;
108
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
109
+ white-space: nowrap;
110
+ z-index: 1;
111
+ --toast-transform: translateX(-50%);
112
+ animation: toast-in 0.15s ease;
113
+ }
114
+ @keyframes toast-in {
115
+ from { opacity: 0; transform: var(--toast-transform) translateY(4px); }
116
+ to { opacity: 1; transform: var(--toast-transform); }
117
+ }
118
+ `;
119
+ const toolbarEl = document.createElement("div");
120
+ toolbarEl.className = "toolbar";
121
+ toolbarEl.setAttribute("role", "toolbar");
122
+ toolbarEl.setAttribute("aria-label", "Playwriter tools");
123
+ shadow.appendChild(styleEl);
124
+ shadow.appendChild(toolbarEl);
125
+ function showToast(msg, anchorRect) {
126
+ shadow.querySelectorAll(".toast").forEach((el) => {
127
+ el.remove();
128
+ });
129
+ if (toastTimer !== null) clearTimeout(toastTimer);
130
+ const toastEl = document.createElement("div");
131
+ toastEl.className = "toast";
132
+ toastEl.textContent = msg;
133
+ if (anchorRect) {
134
+ const GAP = 8;
135
+ const centerX = anchorRect.left + anchorRect.width / 2;
136
+ const belowY = anchorRect.bottom + GAP;
137
+ const fitsBelow = belowY + 36 < window.innerHeight;
138
+ const top = fitsBelow ? belowY : anchorRect.top - GAP;
139
+ const transformOrigin = fitsBelow ? "top center" : "bottom center";
140
+ toastEl.style.left = Math.max(8, Math.min(centerX, window.innerWidth - 8)) + "px";
141
+ toastEl.style.top = top + "px";
142
+ const baseTransform = fitsBelow ? "translateX(-50%)" : "translateX(-50%) translateY(-100%)";
143
+ toastEl.style.setProperty("--toast-transform", baseTransform);
144
+ toastEl.style.transform = baseTransform;
145
+ toastEl.style.transformOrigin = transformOrigin;
146
+ } else {
147
+ toastEl.style.bottom = "20px";
148
+ toastEl.style.left = "50%";
149
+ toastEl.style.transform = "translateX(-50%)";
150
+ }
151
+ shadow.appendChild(toastEl);
152
+ toastTimer = window.setTimeout(() => {
153
+ toastEl.remove();
154
+ }, 1900);
155
+ }
156
+ function getOverlay() {
157
+ if (!overlayEl) {
158
+ const EDGE = "color-mix(in oklch, oklch(0.62 0.18 255) 80%, transparent)";
159
+ const FILL = "color-mix(in oklch, oklch(0.62 0.18 255) 8%, transparent)";
160
+ const container = document.createElement("div");
161
+ container.setAttribute("data-playwriter-overlay", "1");
162
+ container.style.cssText = [
163
+ "position:fixed",
164
+ "pointer-events:none",
165
+ "z-index:2147483646",
166
+ `background:${FILL}`,
167
+ "display:none"
168
+ ].join(";");
169
+ const edgeTop = document.createElement("div");
170
+ edgeTop.style.cssText = `position:absolute;top:0;left:0;width:100%;height:1px;background:${EDGE};`;
171
+ const edgeRight = document.createElement("div");
172
+ edgeRight.style.cssText = `position:absolute;top:0;right:0;width:1px;height:100%;background:${EDGE};`;
173
+ const edgeBottom = document.createElement("div");
174
+ edgeBottom.style.cssText = `position:absolute;bottom:0;left:0;width:100%;height:1px;background:${EDGE};`;
175
+ const edgeLeft = document.createElement("div");
176
+ edgeLeft.style.cssText = `position:absolute;top:0;left:0;width:1px;height:100%;background:${EDGE};`;
177
+ container.appendChild(edgeTop);
178
+ container.appendChild(edgeRight);
179
+ container.appendChild(edgeBottom);
180
+ container.appendChild(edgeLeft);
181
+ document.documentElement.appendChild(container);
182
+ overlayEl = container;
183
+ }
184
+ return overlayEl;
185
+ }
186
+ function positionOverlay(target) {
187
+ const rect = target.getBoundingClientRect();
188
+ if (!rect.width && !rect.height) return;
189
+ const overlay = getOverlay();
190
+ overlay.style.display = "block";
191
+ overlay.style.top = rect.top + "px";
192
+ overlay.style.left = rect.left + "px";
193
+ overlay.style.width = rect.width + "px";
194
+ overlay.style.height = rect.height + "px";
195
+ }
196
+ function hideOverlay() {
197
+ if (overlayEl) overlayEl.style.display = "none";
198
+ }
199
+ function removeOverlay() {
200
+ if (overlayEl) {
201
+ overlayEl.remove();
202
+ overlayEl = null;
203
+ }
204
+ }
205
+ function getTargetAt(x, y) {
206
+ return document.elementsFromPoint(x, y).find((el) => !el.hasAttribute("data-playwriter-overlay") && !el.hasAttribute("data-playwriter-toolbar") && el !== document.documentElement && el !== document.body) ?? null;
207
+ }
208
+ function isOverToolbar(e) {
209
+ return e.composedPath().some((node) => node === host);
210
+ }
211
+ function flashElement(el) {
212
+ const s = el.style;
213
+ if (!s) return;
214
+ const prevOutline = s.outline;
215
+ const prevOffset = s.outlineOffset;
216
+ s.outline = "1px solid #22c55e";
217
+ s.outlineOffset = "2px";
218
+ window.setTimeout(() => {
219
+ s.outline = prevOutline;
220
+ s.outlineOffset = prevOffset;
221
+ }, 350);
222
+ }
223
+ function copyText(text) {
224
+ navigator.clipboard.writeText(text).catch(() => {
225
+ try {
226
+ const ta = document.createElement("textarea");
227
+ ta.value = text;
228
+ ta.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none;";
229
+ document.body.appendChild(ta);
230
+ ta.focus();
231
+ ta.select();
232
+ document.execCommand("copy");
233
+ ta.remove();
234
+ } catch {}
235
+ });
236
+ }
237
+ function allocatePinName() {
238
+ const shared = window.__playwriterPinCount;
239
+ if (typeof shared === "number" && shared > pinCount) pinCount = shared;
240
+ pinCount++;
241
+ window.__playwriterPinCount = pinCount;
242
+ return `playwriterPinnedElem${pinCount}`;
243
+ }
244
+ function onMouseMove(e) {
245
+ if (isOverToolbar(e)) {
246
+ hideOverlay();
247
+ return;
248
+ }
249
+ const target = getTargetAt(e.clientX, e.clientY);
250
+ if (target) positionOverlay(target);
251
+ else hideOverlay();
252
+ }
253
+ function describeElement(el, n) {
254
+ const tag = el.tagName ? el.tagName.toLowerCase() : "";
255
+ const id = el.id || "";
256
+ const cls = typeof el.className === "string" ? el.className : "";
257
+ const role = el.getAttribute("role") || "";
258
+ const aria = el.getAttribute("aria-label") || "";
259
+ const nameAttr = el.getAttribute("name") || "";
260
+ const href = el.getAttribute("href") || "";
261
+ const typeAttr = el.getAttribute("type") || "";
262
+ const text = (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 300);
263
+ const r = el.getBoundingClientRect();
264
+ const rect = `x=${Math.round(r.x)} y=${Math.round(r.y)} w=${Math.round(r.width)} h=${Math.round(r.height)}`;
265
+ const visible = r.width > 0 && r.height > 0;
266
+ return [
267
+ `Pinned #${n} (globalThis.playwriterPinnedElem${n})`,
268
+ `URL: ${location.href}`,
269
+ `Tag: ${tag}`,
270
+ !!id && `ID: ${id}`,
271
+ !!cls && `Classes: ${cls.slice(0, 200)}`,
272
+ !!role && `Role: ${role}`,
273
+ !!aria && `Aria-label: ${aria}`,
274
+ !!nameAttr && `Name: ${nameAttr}`,
275
+ !!href && `Href: ${href.slice(0, 200)}`,
276
+ !!typeAttr && `Type: ${typeAttr}`,
277
+ !!text && `Text: ${text}`,
278
+ `Rect: ${rect}`,
279
+ `Visible: ${visible}`
280
+ ].filter((line) => typeof line === "string").join("\n");
281
+ }
282
+ function buildInspectionCode(n, url, summary) {
283
+ return `state.page=context.pages().find(x=>x.url()===${JSON.stringify(url).replace(/'/g, "\\u0027")})||context.pages()[0]; console.log(${JSON.stringify(summary).replace(/'/g, "\\u0027")}+"\\n\\nouterHTML:\\n"+await state.page.evaluate(n=>globalThis["playwriterPinnedElem"+n]?.outerHTML,${n}))`;
284
+ }
285
+ function onClick(e) {
286
+ if (isOverToolbar(e)) return;
287
+ e.preventDefault();
288
+ e.stopImmediatePropagation();
289
+ const target = getTargetAt(e.clientX, e.clientY);
290
+ if (!target) return;
291
+ const name = allocatePinName();
292
+ const n = pinCount;
293
+ window[name] = target;
294
+ flashElement(target);
295
+ const url = location.href;
296
+ copyText("see the element I pinned in the playwriter tab `playwriter -e '" + buildInspectionCode(n, url, describeElement(target, n)) + "'`");
297
+ showToast(`Copied pin #${n}`, target.getBoundingClientRect());
298
+ setPinMode(false);
299
+ }
300
+ function onKeyDown(e) {
301
+ if (e.key === "Escape") setPinMode(false);
302
+ }
303
+ function setPinMode(on) {
304
+ pinModeActive = on;
305
+ pinBtn.classList.toggle("active", on);
306
+ if (on) {
307
+ document.documentElement.style.cursor = "crosshair";
308
+ getOverlay();
309
+ document.addEventListener("mousemove", onMouseMove, {
310
+ capture: true,
311
+ passive: true
312
+ });
313
+ document.addEventListener("click", onClick, true);
314
+ document.addEventListener("keydown", onKeyDown, true);
315
+ } else {
316
+ document.documentElement.style.cursor = "";
317
+ hideOverlay();
318
+ document.removeEventListener("mousemove", onMouseMove, true);
319
+ document.removeEventListener("click", onClick, true);
320
+ document.removeEventListener("keydown", onKeyDown, true);
321
+ }
322
+ }
323
+ const CLIPBOARD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 424 424" aria-hidden="true"><path d="M 0 212 C 0 112.063 0 62.095 31.037 31.037 C 62.116 0 112.063 0 212 0 C 311.937 0 361.905 0 392.942 31.037 C 424 62.116 424 112.063 424 212 C 424 311.937 424 361.905 392.942 392.942 C 361.926 424 311.937 424 212 424 C 112.063 424 62.095 424 31.037 392.942 C 0 361.926 0 311.937 0 212" fill="currentColor"/><path class="logo-inner" d="M 225.732 260.521 L 277.905 312.673 C 283.311 318.1 286.003 320.793 289.014 322.043 C 293.042 323.718 297.557 323.718 301.585 322.043 C 304.596 320.793 307.309 318.1 312.694 312.694 C 318.1 307.288 320.793 304.596 322.043 301.585 C 323.722 297.563 323.722 293.036 322.043 289.014 C 320.793 286.003 318.1 283.29 312.694 277.905 L 260.521 225.732 L 276.442 209.789 C 292.766 193.465 300.907 185.325 298.999 176.548 C 297.07 167.792 286.237 163.785 264.591 155.814 L 192.384 129.208 C 149.2 113.308 127.618 105.358 116.488 116.488 C 105.358 127.618 113.308 149.2 129.208 192.384 L 155.814 264.591 C 163.785 286.237 167.792 297.07 176.548 298.999 C 185.303 300.928 193.465 292.766 209.789 276.442 Z" fill="white"/></svg>`;
324
+ const CLOSE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
325
+ pinBtn = document.createElement("button");
326
+ pinBtn.className = "btn";
327
+ pinBtn.setAttribute("aria-label", "Pin element — click any element to copy inspection code for a playwriter -e call");
328
+ pinBtn.setAttribute("title", "Pin element (click to copy inspection code)");
329
+ pinBtn.innerHTML = CLIPBOARD_SVG;
330
+ pinBtn.addEventListener("click", (e) => {
331
+ e.stopPropagation();
332
+ setPinMode(!pinModeActive);
333
+ });
334
+ const dividerEl = document.createElement("div");
335
+ dividerEl.className = "divider";
336
+ dividerEl.setAttribute("aria-hidden", "true");
337
+ const closeBtn = document.createElement("button");
338
+ closeBtn.className = "btn";
339
+ closeBtn.setAttribute("aria-label", "Close Playwriter toolbar");
340
+ closeBtn.setAttribute("title", "Close toolbar");
341
+ closeBtn.innerHTML = CLOSE_SVG;
342
+ closeBtn.addEventListener("click", (e) => {
343
+ e.stopPropagation();
344
+ setPinMode(false);
345
+ host.style.display = "none";
346
+ });
347
+ toolbarEl.appendChild(pinBtn);
348
+ toolbarEl.appendChild(dividerEl);
349
+ toolbarEl.appendChild(closeBtn);
350
+ document.documentElement.appendChild(host);
351
+ window.__playwriterToolbarDestroy = function() {
352
+ setPinMode(false);
353
+ removeOverlay();
354
+ host.remove();
355
+ delete window.__playwriterToolbarInstalled;
356
+ delete window.__playwriterToolbarDestroy;
357
+ delete window.__playwriterPinCount;
358
+ };
359
+ }
360
+ //#endregion
30
361
  //#region ../playwriter/src/ghost-browser.ts
31
362
  /**
32
363
  * Handles ghost-browser commands in the extension.
@@ -72,6 +403,9 @@ async function handleGhostBrowserCommand(params, chromeApi) {
72
403
  }
73
404
  }
74
405
  //#endregion
406
+ //#region ../playwriter/dist/ghost-cursor-client.js?raw
407
+ var ghost_cursor_client_default = "(() => {\n var __defProp = Object.defineProperty;\n var __getOwnPropNames = Object.getOwnPropertyNames;\n var __getOwnPropDesc = Object.getOwnPropertyDescriptor;\n var __hasOwnProp = Object.prototype.hasOwnProperty;\n function __accessProp(key) {\n return this[key];\n }\n var __toCommonJS = (from) => {\n var entry = (__moduleCache ??= new WeakMap).get(from), desc;\n if (entry)\n return entry;\n entry = __defProp({}, \"__esModule\", { value: true });\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (var key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(entry, key))\n __defProp(entry, key, {\n get: __accessProp.bind(from, key),\n enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable\n });\n }\n __moduleCache.set(from, entry);\n return entry;\n };\n var __moduleCache;\n\n // src/ghost-cursor-client.ts\n var exports_ghost_cursor_client = {};\n\n // src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts\n var SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL = \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjE4IiBoZWlnaHQ9Ijk1OCIgdmlld0JveD0iMCAwIDYxOCA5NTgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfMzg0XzI3KSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTI3LjA2MiAzNy4wMzMxTDU0MC42OTYgNDUxLjU1NUM1OTIuNjUzIDUwMy42NiA1NTUuNzk0IDU5Mi41NzQgNDgyLjIyNiA1OTIuNTc0TDQyMS44MzEgNTkyLjU2OUw0ODEuODIxIDczNS4wNTRDNDkyLjMzMSA3NjAuMDIxIDQ5Mi40NzkgNzg3LjY1MiA0ODIuMjY1IDgxMi43NjdDNDcyLjAwMiA4MzcuOTMyIDQ1Mi41NjEgODU3LjU3IDQyNy40OTYgODY4LjA4QzQxNC44NjQgODczLjM1OSA0MDEuNjQgODc2LjAyNCAzODguMTIxIDg3Ni4wMjRDMzQ3LjExNyA4NzYuMDI0IDMxMC4zNTggODUxLjYgMjk0LjQ3IDgxMy44MDRMMjMxLjQyIDY2My45MThMMTkwLjM2OCA3MDAuMzM3QzEzNy4wMjkgNzQ3LjUwOCA1MyA3MDkuNjYzIDUzIDYzOC40MTNWNjcuNjc0NEM1MyAyOC45OTAzIDk5LjcyNjggOS42NDgyOCAxMjcuMDYyIDM3LjAzMzFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEwMi4zMTYgOTkuNjUyQzEwMi4zMTYgOTMuMTg4MiAxMTAuMTYyIDg5LjkzMTYgMTE0LjcwMSA5NC41MjA0TDUwNC44OTcgNDg1LjU1NUM1MjYuMTY0IDUwNi44NzEgNTExLjA2NSA1NDMuMjM2IDQ4MC45NjcgNTQzLjIzNkwzNDcuNTQ2IDU0My4xNjFMNDM2LjM0MiA3NTQuMTQzQzQ0Ny41NDIgNzgwLjc4OCA0MzUuMDA5IDgxMS40MjkgNDA4LjQxNCA4MjIuNTgxQzM4MS43MiA4MzMuNzgxIDM1MS4xMjggODIxLjI5OCAzMzkuOTc3IDc5NC43MDJMMjUwLjI5MyA1ODEuMzUyTDE1OC41MTcgNjYyLjY0NEMxMzcuOTkxIDY4MC44MDEgMTA2LjMxOSA2NjguMTQ1IDEwMi42NjQgNjQyLjMyM0wxMDIuMzE2IDYzNy4zMzFWOTkuNjUyWiIgZmlsbD0iYmxhY2siLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kXzM4NF8yNyIgeD0iMC4zNCIgeT0iMC43OTkyMTkiIHdpZHRoPSI2MTcuMzIiIGhlaWdodD0iOTU3LjE0NCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSIyOS4yNiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyNi4zMyIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd18zODRfMjciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfMzg0XzI3IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPg==\";\n\n // src/ghost-cursor-client.ts\n var isTopFrame = (() => {\n try {\n return window === window.top;\n } catch {\n return false;\n }\n })();\n var CURSOR_ID = \"__playwriter_ghost_cursor__\";\n var SCREENSTUDIO_POINTER_ASPECT_RATIO = 618 / 958;\n var SCREENSTUDIO_HOTSPOT_X_RATIO = 0.14;\n var SCREENSTUDIO_HOTSPOT_Y_RATIO = 0.06;\n var MINIMAL_TRIANGLE_HOTSPOT_X_RATIO = 0.07;\n var MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO = 0.06;\n var MOVE_EASING = \"cubic-bezier(0.65, 0, 0.35, 1)\";\n var PRESS_EASING = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n var PRESS_DURATION_MS = 140;\n var IDLE_HIDE_DELAY_MS = 5000;\n var IDLE_FADE_OUT_MS = 600;\n var DEFAULT_OPTIONS = {\n style: \"minimal\",\n color: \"#111827\",\n size: 22,\n zIndex: 2147483647,\n easing: MOVE_EASING,\n minDurationMs: 220,\n maxDurationMs: 1500,\n speedPxPerMs: 1.2\n };\n var runtime = {\n outerElement: null,\n innerElement: null,\n options: DEFAULT_OPTIONS,\n x: 0,\n y: 0,\n scale: 1,\n hasPosition: false,\n enabled: false,\n idleHidden: false\n };\n var idleHideTimer = null;\n function clamp(options) {\n const { value, min, max } = options;\n return Math.min(max, Math.max(min, value));\n }\n function mergeOptions(options) {\n if (!options) {\n return DEFAULT_OPTIONS;\n }\n return {\n style: options.style ?? DEFAULT_OPTIONS.style,\n color: options.color ?? DEFAULT_OPTIONS.color,\n size: options.size ?? DEFAULT_OPTIONS.size,\n zIndex: options.zIndex ?? DEFAULT_OPTIONS.zIndex,\n easing: options.easing ?? DEFAULT_OPTIONS.easing,\n minDurationMs: options.minDurationMs ?? DEFAULT_OPTIONS.minDurationMs,\n maxDurationMs: options.maxDurationMs ?? DEFAULT_OPTIONS.maxDurationMs,\n speedPxPerMs: options.speedPxPerMs ?? DEFAULT_OPTIONS.speedPxPerMs\n };\n }\n function getCursorDimensions() {\n if (runtime.options.style === \"screenstudio\") {\n const height = runtime.options.size;\n const width = Math.max(10, Math.round(height * SCREENSTUDIO_POINTER_ASPECT_RATIO));\n return { width, height };\n }\n if (runtime.options.style === \"minimal\") {\n const size = Math.max(12, runtime.options.size);\n return { width: size, height: size };\n }\n return { width: runtime.options.size, height: runtime.options.size };\n }\n function getHotspotOffsetPx() {\n const dimensions = getCursorDimensions();\n if (runtime.options.style === \"screenstudio\") {\n return {\n x: Math.round(dimensions.width * SCREENSTUDIO_HOTSPOT_X_RATIO),\n y: Math.round(dimensions.height * SCREENSTUDIO_HOTSPOT_Y_RATIO)\n };\n }\n if (runtime.options.style === \"minimal\") {\n return {\n x: Math.round(dimensions.width * MINIMAL_TRIANGLE_HOTSPOT_X_RATIO),\n y: Math.round(dimensions.height * MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO)\n };\n }\n return {\n x: Math.round(dimensions.width / 2),\n y: Math.round(dimensions.height / 2)\n };\n }\n function getBaseOpacity() {\n if (runtime.options.style === \"screenstudio\") {\n return \"0.95\";\n }\n if (runtime.options.style === \"minimal\") {\n return \"1\";\n }\n return \"0.72\";\n }\n function applyTranslate() {\n if (!runtime.outerElement) {\n return;\n }\n const hotspot = getHotspotOffsetPx();\n runtime.outerElement.style.transform = `translate3d(${runtime.x - hotspot.x}px, ${runtime.y - hotspot.y}px, 0)`;\n }\n function applyScale() {\n if (!runtime.innerElement) {\n return;\n }\n runtime.innerElement.style.transform = `scale(${runtime.scale})`;\n }\n function computeDurationMs(options) {\n if (!runtime.hasPosition) {\n return 0;\n }\n const dx = options.targetX - runtime.x;\n const dy = options.targetY - runtime.y;\n const distance = Math.hypot(dx, dy);\n const rawDurationMs = distance / runtime.options.speedPxPerMs;\n return clamp({\n value: rawDurationMs,\n min: runtime.options.minDurationMs,\n max: runtime.options.maxDurationMs\n });\n }\n function createCursorElement() {\n const outer = document.createElement(\"div\");\n outer.id = CURSOR_ID;\n outer.setAttribute(\"aria-hidden\", \"true\");\n outer.style.position = \"fixed\";\n outer.style.left = \"0\";\n outer.style.top = \"0\";\n outer.style.pointerEvents = \"none\";\n outer.style.zIndex = `${runtime.options.zIndex}`;\n outer.style.transitionProperty = \"transform\";\n outer.style.transitionTimingFunction = runtime.options.easing;\n outer.style.transitionDuration = \"0ms\";\n outer.style.willChange = \"transform\";\n const inner = document.createElement(\"div\");\n inner.style.transitionProperty = \"transform, opacity\";\n inner.style.transitionTimingFunction = PRESS_EASING;\n inner.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n inner.style.opacity = getBaseOpacity();\n outer.appendChild(inner);\n runtime.outerElement = outer;\n runtime.innerElement = inner;\n applyRuntimeVisualOptions();\n return outer;\n }\n function ensureCursorElement() {\n const existing = document.getElementById(CURSOR_ID);\n if (existing) {\n runtime.outerElement = existing;\n runtime.innerElement = existing.firstElementChild || null;\n return existing;\n }\n const outer = createCursorElement();\n const root = document.documentElement || document.body;\n root.appendChild(outer);\n return outer;\n }\n function applyRuntimeVisualOptions() {\n if (!runtime.innerElement) {\n return;\n }\n const dimensions = getCursorDimensions();\n runtime.innerElement.style.width = `${dimensions.width}px`;\n runtime.innerElement.style.height = `${dimensions.height}px`;\n if (runtime.outerElement) {\n runtime.outerElement.style.zIndex = `${runtime.options.zIndex}`;\n runtime.outerElement.style.transitionTimingFunction = runtime.options.easing;\n }\n const hotspot = getHotspotOffsetPx();\n runtime.innerElement.style.transformOrigin = `${hotspot.x}px ${hotspot.y}px`;\n if (runtime.options.style === \"screenstudio\") {\n runtime.innerElement.style.borderRadius = \"0\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = \"transparent\";\n runtime.innerElement.style.backgroundImage = `url(\"${SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL}\")`;\n runtime.innerElement.style.backgroundRepeat = \"no-repeat\";\n runtime.innerElement.style.backgroundPosition = \"left top\";\n runtime.innerElement.style.backgroundSize = \"contain\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.filter = \"none\";\n runtime.innerElement.style.boxShadow = \"none\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n return;\n }\n if (runtime.options.style === \"minimal\") {\n const triangleSvg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"-1 -1 26 26\"><path fill=\"white\" stroke=\"${runtime.options.color}\" stroke-width=\"1.5\" stroke-linejoin=\"round\" d=\"m23.284 19.124l-6.866-6.895a.4.4 0 0 1-.118-.296a.43.43 0 0 1 .163-.282l4.439-3.077a1.48 1.48 0 0 0 .621-1.48a1.48 1.48 0 0 0-1.036-1.198L1.623.302a1.14 1.14 0 0 0-1.11.282A1.13 1.13 0 0 0 .29 1.649L5.928 20.44a1.48 1.48 0 0 0 1.183 1.035a1.48 1.48 0 0 0 1.48-.621l3.078-4.44a.37.37 0 0 1 .31-.118a.43.43 0 0 1 .296.104l6.91 6.91a1.48 1.48 0 0 0 2.087 0l2.086-2.086a1.48 1.48 0 0 0-.074-2.101\"/></svg>`;\n const triangleDataUrl = `url(\"data:image/svg+xml,${encodeURIComponent(triangleSvg)}\")`;\n runtime.innerElement.style.borderRadius = \"0\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = \"transparent\";\n runtime.innerElement.style.backgroundImage = triangleDataUrl;\n runtime.innerElement.style.backgroundRepeat = \"no-repeat\";\n runtime.innerElement.style.backgroundSize = \"contain\";\n runtime.innerElement.style.backgroundPosition = \"left top\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.boxShadow = \"none\";\n runtime.innerElement.style.filter = \"drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n return;\n }\n runtime.innerElement.style.borderRadius = \"999px\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = runtime.options.color;\n runtime.innerElement.style.backgroundImage = \"none\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.filter = \"none\";\n runtime.innerElement.style.boxShadow = \"0 2px 10px rgba(0, 0, 0, 0.18), inset 0 0 0 2px rgba(255, 255, 255, 0.55)\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n function clearIdleHideTimer() {\n if (idleHideTimer !== null) {\n clearTimeout(idleHideTimer);\n idleHideTimer = null;\n }\n }\n function scheduleIdleHide() {\n clearIdleHideTimer();\n idleHideTimer = setTimeout(() => {\n idleHideTimer = null;\n if (!runtime.enabled || !runtime.innerElement) {\n return;\n }\n runtime.idleHidden = true;\n runtime.innerElement.style.transitionDuration = `${IDLE_FADE_OUT_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = \"0\";\n }, IDLE_HIDE_DELAY_MS);\n }\n function wakeFromIdle(options) {\n runtime.x = options.x;\n runtime.y = options.y;\n runtime.hasPosition = true;\n if (runtime.innerElement) {\n runtime.innerElement.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n }\n function moveCursor(options) {\n if (!runtime.enabled) {\n return;\n }\n ensureCursorElement();\n const durationMs = computeDurationMs({ targetX: options.x, targetY: options.y });\n if (runtime.outerElement) {\n runtime.outerElement.style.transitionDuration = `${Math.round(durationMs)}ms`;\n runtime.outerElement.style.transitionTimingFunction = runtime.options.easing;\n }\n runtime.x = options.x;\n runtime.y = options.y;\n runtime.hasPosition = true;\n applyTranslate();\n }\n function setPressed(options) {\n if (!runtime.enabled || !runtime.innerElement) {\n return;\n }\n runtime.scale = options.pressed ? runtime.options.style === \"dot\" ? 0.92 : 0.95 : 1;\n runtime.innerElement.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = options.pressed ? \"1\" : getBaseOpacity();\n applyScale();\n }\n function enable(options) {\n runtime.options = mergeOptions(options);\n runtime.enabled = true;\n ensureCursorElement();\n applyRuntimeVisualOptions();\n if (!runtime.hasPosition) {\n runtime.x = Math.round(window.innerWidth / 2);\n runtime.y = Math.round(window.innerHeight / 2);\n runtime.scale = 1;\n runtime.hasPosition = true;\n }\n runtime.idleHidden = false;\n if (runtime.innerElement) {\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n applyTranslate();\n applyScale();\n scheduleIdleHide();\n }\n function disable() {\n runtime.enabled = false;\n runtime.scale = 1;\n runtime.hasPosition = false;\n runtime.idleHidden = false;\n clearIdleHideTimer();\n if (runtime.outerElement) {\n runtime.outerElement.remove();\n runtime.outerElement = null;\n runtime.innerElement = null;\n }\n }\n function applyMouseAction(action) {\n if (!runtime.enabled) {\n return;\n }\n if (runtime.idleHidden) {\n runtime.idleHidden = false;\n wakeFromIdle({ x: action.x, y: action.y });\n }\n if (action.type === \"move\" || action.type === \"wheel\") {\n moveCursor({ x: action.x, y: action.y });\n } else if (action.type === \"down\") {\n moveCursor({ x: action.x, y: action.y });\n setPressed({ pressed: true });\n } else if (action.type === \"up\") {\n moveCursor({ x: action.x, y: action.y });\n setPressed({ pressed: false });\n }\n scheduleIdleHide();\n }\n var api = {\n enable,\n disable,\n applyMouseAction,\n isEnabled: () => {\n return runtime.enabled;\n }\n };\n if (isTopFrame) {\n globalThis.__playwriterGhostCursor = api;\n try {\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", () => {\n try {\n api.enable();\n } catch {}\n }, { once: true });\n } else {\n api.enable();\n }\n } catch {}\n }\n})();\n";
408
+ //#endregion
75
409
  //#region src/recording.ts
76
410
  var activeRecordings = /* @__PURE__ */ new Map();
77
411
  var offscreenDocumentCreating = null;
@@ -295,6 +629,13 @@ var RELAY_PORT = 19988;
295
629
  function sleep(ms) {
296
630
  return new Promise((resolve) => setTimeout(resolve, ms));
297
631
  }
632
+ function createInstallId() {
633
+ const values = new Uint32Array(2);
634
+ crypto.getRandomValues(values);
635
+ return Array.from(values).map((value) => {
636
+ return value.toString(36);
637
+ }).join("");
638
+ }
298
639
  async function detectBrowserName() {
299
640
  if (chrome.ghostPublicAPI) return "Ghost";
300
641
  const brands = navigator.userAgentData?.brands;
@@ -318,6 +659,7 @@ async function detectBrowserName() {
318
659
  return "Chromium";
319
660
  }
320
661
  var identityPromise = null;
662
+ var installIdPromise = null;
321
663
  var tabSessionScope = (() => {
322
664
  const values = new Uint32Array(2);
323
665
  crypto.getRandomValues(values);
@@ -325,22 +667,42 @@ var tabSessionScope = (() => {
325
667
  return value.toString(36);
326
668
  }).join("");
327
669
  })();
670
+ async function getInstallId() {
671
+ if (installIdPromise) return installIdPromise;
672
+ installIdPromise = (async () => {
673
+ const existing = await chrome.storage.local.get("playwriterInstallId");
674
+ const storedInstallId = typeof existing.playwriterInstallId === "string" ? existing.playwriterInstallId : "";
675
+ if (storedInstallId) return storedInstallId;
676
+ const installId = createInstallId();
677
+ await chrome.storage.local.set({ playwriterInstallId: installId });
678
+ return installId;
679
+ })().catch((error) => {
680
+ installIdPromise = null;
681
+ throw error;
682
+ });
683
+ return installIdPromise;
684
+ }
328
685
  async function getExtensionIdentity() {
329
686
  if (identityPromise) return identityPromise;
330
687
  identityPromise = (async () => {
331
688
  const browser = await detectBrowserName();
689
+ const installId = await getInstallId().catch(() => {
690
+ return tabSessionScope;
691
+ });
332
692
  try {
333
693
  const info = await chrome.identity.getProfileUserInfo({ accountStatus: "ANY" });
334
694
  return {
335
695
  browser,
336
696
  email: info.email || "",
337
- id: info.id || ""
697
+ id: info.id || "",
698
+ installId
338
699
  };
339
700
  } catch {
340
701
  return {
341
702
  browser,
342
703
  email: "",
343
- id: ""
704
+ id: "",
705
+ installId
344
706
  };
345
707
  }
346
708
  })();
@@ -415,7 +777,8 @@ var ConnectionManager = class {
415
777
  if (identity.browser) relayUrl.searchParams.set("browser", identity.browser);
416
778
  if (identity.email) relayUrl.searchParams.set("email", identity.email);
417
779
  if (identity.id) relayUrl.searchParams.set("id", identity.id);
418
- relayUrl.searchParams.set("v", "0.0.103");
780
+ if (identity.installId) relayUrl.searchParams.set("installId", identity.installId);
781
+ relayUrl.searchParams.set("v", "0.1.0");
419
782
  logger.debug("Creating WebSocket connection to:", relayUrl);
420
783
  const socket = new WebSocket(relayUrl.toString());
421
784
  await new Promise((resolve, reject) => {
@@ -677,7 +1040,7 @@ var ConnectionManager = class {
677
1040
  method: "GET",
678
1041
  signal: AbortSignal.timeout(2e3)
679
1042
  })).json();
680
- if (!data.connected || data.activeTargets === 0) {
1043
+ if (!data.connected) {
681
1044
  store.setState({
682
1045
  connectionState: "idle",
683
1046
  errorText: void 0
@@ -1158,6 +1521,12 @@ async function attachTab(tabId, { skipAttachedEvent = false } = {}) {
1158
1521
  `;
1159
1522
  await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: contextMenuScript });
1160
1523
  await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: contextMenuScript });
1524
+ try {
1525
+ await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: ghost_cursor_client_default });
1526
+ await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: ghost_cursor_client_default });
1527
+ } catch (err) {
1528
+ logger.debug("Could not inject ghost cursor (restricted page):", err.message);
1529
+ }
1161
1530
  const targetInfo = (await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")).targetInfo;
1162
1531
  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
1532
  const attachOrder = nextSessionId;
@@ -1191,6 +1560,16 @@ async function attachTab(tabId, { skipAttachedEvent = false } = {}) {
1191
1560
  }
1192
1561
  });
1193
1562
  logger.debug("Tab attached successfully:", tabId, "sessionId:", sessionId, "targetId:", targetInfo.targetId, "url:", targetInfo.url, "skipAttachedEvent:", skipAttachedEvent);
1563
+ chrome.scripting.executeScript({
1564
+ target: {
1565
+ tabId,
1566
+ allFrames: false
1567
+ },
1568
+ world: "MAIN",
1569
+ func: initPlaywriterToolbar
1570
+ }).catch((err) => {
1571
+ logger.debug("Could not inject toolbar (restricted page):", err.message);
1572
+ });
1194
1573
  return {
1195
1574
  targetInfo,
1196
1575
  sessionId
@@ -1210,6 +1589,20 @@ function detachTab(tabId, shouldDetachDebugger) {
1210
1589
  return;
1211
1590
  }
1212
1591
  cleanupRecordingForTab(tabId);
1592
+ chrome.scripting.executeScript({
1593
+ target: { tabId },
1594
+ world: "MAIN",
1595
+ func: () => {
1596
+ window.__playwriterToolbarDestroy?.();
1597
+ }
1598
+ }).catch(() => {});
1599
+ chrome.scripting.executeScript({
1600
+ target: { tabId },
1601
+ world: "MAIN",
1602
+ func: () => {
1603
+ globalThis.__playwriterGhostCursor?.disable?.();
1604
+ }
1605
+ }).catch(() => {});
1213
1606
  logger.warn(`DISCONNECT: detachTab tabId=${tabId} shouldDetach=${shouldDetachDebugger} stack=${getCallStack()}`);
1214
1607
  if (tab.sessionId && tab.targetId) sendMessage({
1215
1608
  method: "forwardCDPEvent",
@@ -1254,14 +1647,31 @@ async function connectTab(tabId) {
1254
1647
  };
1255
1648
  });
1256
1649
  } 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}`
1650
+ else {
1651
+ let tabStillExists = true;
1652
+ try {
1653
+ await chrome.tabs.get(tabId);
1654
+ } catch {
1655
+ tabStillExists = false;
1656
+ }
1657
+ if (!tabStillExists) {
1658
+ logger.debug(`Tab ${tabId} was closed during connect, dropping error state`);
1659
+ store.setState((state) => {
1660
+ const newTabs = new Map(state.tabs);
1661
+ newTabs.delete(tabId);
1662
+ return { tabs: newTabs };
1663
+ });
1664
+ return;
1665
+ }
1666
+ store.setState((state) => {
1667
+ const newTabs = new Map(state.tabs);
1668
+ newTabs.set(tabId, {
1669
+ state: "error",
1670
+ errorText: `Error: ${error.message}`
1671
+ });
1672
+ return { tabs: newTabs };
1262
1673
  });
1263
- return { tabs: newTabs };
1264
- });
1674
+ }
1265
1675
  }
1266
1676
  }
1267
1677
  function setTabConnecting(tabId) {
@@ -1478,6 +1888,7 @@ async function updateIcons() {
1478
1888
  }
1479
1889
  }
1480
1890
  async function onTabRemoved(tabId) {
1891
+ popupSourceTabMap.delete(tabId);
1481
1892
  const { tabs } = store.getState();
1482
1893
  if (!tabs.has(tabId)) return;
1483
1894
  logger.debug(`Connected tab ${tabId} was closed, disconnecting`);
@@ -1598,6 +2009,75 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1598
2009
  logger.debug("onTabUpdated handler error:", e);
1599
2010
  });
1600
2011
  });
2012
+ var popupSourceTabMap = /* @__PURE__ */ new Map();
2013
+ chrome.webNavigation.onCreatedNavigationTarget.addListener((details) => {
2014
+ popupSourceTabMap.set(details.tabId, details.sourceTabId);
2015
+ setTimeout(() => {
2016
+ popupSourceTabMap.delete(details.tabId);
2017
+ }, 1e4);
2018
+ });
2019
+ chrome.windows.onCreated.addListener(async (popupWindow) => {
2020
+ if (popupWindow.type !== "popup" || popupWindow.id === void 0) return;
2021
+ try {
2022
+ let popupTabs = [];
2023
+ for (let attempt = 0; attempt < 5; attempt++) {
2024
+ popupTabs = await chrome.tabs.query({ windowId: popupWindow.id });
2025
+ if (popupTabs.length > 0) break;
2026
+ await sleep(20);
2027
+ }
2028
+ const tabIds = popupTabs.map((t) => t.id).filter((id) => {
2029
+ return id !== void 0;
2030
+ });
2031
+ if (tabIds.length === 0) {
2032
+ logger.debug(`Popup window ${popupWindow.id} has no tabs after retry, skipping`);
2033
+ return;
2034
+ }
2035
+ const { tabs: connectedTabs } = store.getState();
2036
+ let sourceTabId;
2037
+ for (const tabId of tabIds) {
2038
+ const candidate = popupSourceTabMap.get(tabId);
2039
+ if (candidate !== void 0 && connectedTabs.has(candidate)) {
2040
+ sourceTabId = candidate;
2041
+ break;
2042
+ }
2043
+ }
2044
+ for (const tabId of tabIds) popupSourceTabMap.delete(tabId);
2045
+ if (sourceTabId === void 0) {
2046
+ logger.debug(`Popup window ${popupWindow.id} not opened by a Playwriter-connected tab, leaving alone (tabs=${JSON.stringify(tabIds)})`);
2047
+ return;
2048
+ }
2049
+ let destinationWindowId;
2050
+ try {
2051
+ const sourceTab = await chrome.tabs.get(sourceTabId);
2052
+ if (sourceTab.windowId === void 0) {
2053
+ const focused = await chrome.windows.getLastFocused({ populate: false });
2054
+ if (focused.id === void 0 || focused.id === popupWindow.id) return;
2055
+ destinationWindowId = focused.id;
2056
+ } else destinationWindowId = sourceTab.windowId;
2057
+ } catch (e) {
2058
+ logger.debug(`Source tab ${sourceTabId} no longer exists, skipping relocation:`, e);
2059
+ return;
2060
+ }
2061
+ logger.debug(`Relocating ${tabIds.length} popup tab(s) from window ${popupWindow.id} into source window ${destinationWindowId} (sourceTabId=${sourceTabId})`);
2062
+ await chrome.tabs.move(tabIds, {
2063
+ windowId: destinationWindowId,
2064
+ index: -1
2065
+ });
2066
+ try {
2067
+ await chrome.windows.remove(popupWindow.id);
2068
+ } catch {}
2069
+ for (const tabId of tabIds) {
2070
+ if (connectedTabs.has(tabId)) continue;
2071
+ try {
2072
+ await connectTab(tabId);
2073
+ } catch (e) {
2074
+ logger.warn(`Failed to auto-connect relocated popup tab ${tabId}:`, e);
2075
+ }
2076
+ }
2077
+ } catch (e) {
2078
+ logger.warn("Failed to relocate popup window:", e);
2079
+ }
2080
+ });
1601
2081
  chrome.contextMenus?.onClicked.addListener(async (info, tab) => {
1602
2082
  if (info.menuItemId !== "playwriter-pin-element" || !tab?.id) return;
1603
2083
  const tabInfo = store.getState().tabs.get(tab.id);
@@ -1606,21 +2086,19 @@ chrome.contextMenus?.onClicked.addListener(async (info, tab) => {
1606
2086
  return;
1607
2087
  }
1608
2088
  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
2089
  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
2090
  const pageIndex = connectedTabs.findIndex(([id]) => id === tab.id);
1622
2091
  const hasMultiplePages = connectedTabs.length > 1;
1623
2092
  try {
2093
+ const name = `playwriterPinnedElem${(await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
2094
+ expression: `
2095
+ (function() {
2096
+ window.__playwriterPinCount = (window.__playwriterPinCount || 0) + 1;
2097
+ return window.__playwriterPinCount;
2098
+ })()
2099
+ `,
2100
+ returnByValue: true
2101
+ })).result?.value ?? 1}`;
1624
2102
  const result = await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
1625
2103
  expression: `
1626
2104
  if (window.__playwriter_lastRightClicked) {
@@ -1701,4 +2179,20 @@ chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
1701
2179
  }
1702
2180
  return false;
1703
2181
  });
2182
+ chrome.webNavigation.onDOMContentLoaded.addListener((details) => {
2183
+ if (details.frameId !== 0) return;
2184
+ const { tabs } = store.getState();
2185
+ const tabInfo = tabs.get(details.tabId);
2186
+ if (!tabInfo || tabInfo.state !== "connected") return;
2187
+ chrome.scripting.executeScript({
2188
+ target: {
2189
+ tabId: details.tabId,
2190
+ allFrames: false
2191
+ },
2192
+ world: "MAIN",
2193
+ func: initPlaywriterToolbar
2194
+ }).catch((err) => {
2195
+ logger.debug("Could not re-inject toolbar after navigation:", err.message);
2196
+ });
2197
+ });
1704
2198
  //#endregion