playwright-locator-healer 0.1.3

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/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/USAGE.md +217 -0
  5. package/dist/healing/audit-log.d.ts +9 -0
  6. package/dist/healing/audit-log.d.ts.map +1 -0
  7. package/dist/healing/audit-log.js +22 -0
  8. package/dist/healing/audit-log.js.map +1 -0
  9. package/dist/healing/auto-fixture.d.ts +14 -0
  10. package/dist/healing/auto-fixture.d.ts.map +1 -0
  11. package/dist/healing/auto-fixture.js +430 -0
  12. package/dist/healing/auto-fixture.js.map +1 -0
  13. package/dist/healing/cache.d.ts +11 -0
  14. package/dist/healing/cache.d.ts.map +1 -0
  15. package/dist/healing/cache.js +128 -0
  16. package/dist/healing/cache.js.map +1 -0
  17. package/dist/healing/circuit-breaker.d.ts +11 -0
  18. package/dist/healing/circuit-breaker.d.ts.map +1 -0
  19. package/dist/healing/circuit-breaker.js +34 -0
  20. package/dist/healing/circuit-breaker.js.map +1 -0
  21. package/dist/healing/cost-tracker.d.ts +30 -0
  22. package/dist/healing/cost-tracker.d.ts.map +1 -0
  23. package/dist/healing/cost-tracker.js +101 -0
  24. package/dist/healing/cost-tracker.js.map +1 -0
  25. package/dist/healing/dom-extractor.d.ts +9 -0
  26. package/dist/healing/dom-extractor.d.ts.map +1 -0
  27. package/dist/healing/dom-extractor.js +268 -0
  28. package/dist/healing/dom-extractor.js.map +1 -0
  29. package/dist/healing/errors.d.ts +33 -0
  30. package/dist/healing/errors.d.ts.map +1 -0
  31. package/dist/healing/errors.js +40 -0
  32. package/dist/healing/errors.js.map +1 -0
  33. package/dist/healing/heal/caller-context.d.ts +8 -0
  34. package/dist/healing/heal/caller-context.d.ts.map +1 -0
  35. package/dist/healing/heal/caller-context.js +46 -0
  36. package/dist/healing/heal/caller-context.js.map +1 -0
  37. package/dist/healing/heal/orchestrator-helpers.d.ts +84 -0
  38. package/dist/healing/heal/orchestrator-helpers.d.ts.map +1 -0
  39. package/dist/healing/heal/orchestrator-helpers.js +117 -0
  40. package/dist/healing/heal/orchestrator-helpers.js.map +1 -0
  41. package/dist/healing/heal/orchestrator.d.ts +47 -0
  42. package/dist/healing/heal/orchestrator.d.ts.map +1 -0
  43. package/dist/healing/heal/orchestrator.js +644 -0
  44. package/dist/healing/heal/orchestrator.js.map +1 -0
  45. package/dist/healing/heal/playwright-code.d.ts +26 -0
  46. package/dist/healing/heal/playwright-code.d.ts.map +1 -0
  47. package/dist/healing/heal/playwright-code.js +134 -0
  48. package/dist/healing/heal/playwright-code.js.map +1 -0
  49. package/dist/healing/heal/preflight.d.ts +8 -0
  50. package/dist/healing/heal/preflight.d.ts.map +1 -0
  51. package/dist/healing/heal/preflight.js +77 -0
  52. package/dist/healing/heal/preflight.js.map +1 -0
  53. package/dist/healing/heal/prompt.d.ts +9 -0
  54. package/dist/healing/heal/prompt.d.ts.map +1 -0
  55. package/dist/healing/heal/prompt.js +22 -0
  56. package/dist/healing/heal/prompt.js.map +1 -0
  57. package/dist/healing/heal/report.d.ts +11 -0
  58. package/dist/healing/heal/report.d.ts.map +1 -0
  59. package/dist/healing/heal/report.js +28 -0
  60. package/dist/healing/heal/report.js.map +1 -0
  61. package/dist/healing/heal/source-trace.d.ts +17 -0
  62. package/dist/healing/heal/source-trace.d.ts.map +1 -0
  63. package/dist/healing/heal/source-trace.js +59 -0
  64. package/dist/healing/heal/source-trace.js.map +1 -0
  65. package/dist/healing/index.d.ts +6 -0
  66. package/dist/healing/index.d.ts.map +1 -0
  67. package/dist/healing/index.js +3 -0
  68. package/dist/healing/index.js.map +1 -0
  69. package/dist/healing/llm-client.d.ts +58 -0
  70. package/dist/healing/llm-client.d.ts.map +1 -0
  71. package/dist/healing/llm-client.js +258 -0
  72. package/dist/healing/llm-client.js.map +1 -0
  73. package/dist/healing/logger.d.ts +18 -0
  74. package/dist/healing/logger.d.ts.map +1 -0
  75. package/dist/healing/logger.js +38 -0
  76. package/dist/healing/logger.js.map +1 -0
  77. package/dist/healing/models.d.ts +26 -0
  78. package/dist/healing/models.d.ts.map +1 -0
  79. package/dist/healing/models.js +109 -0
  80. package/dist/healing/models.js.map +1 -0
  81. package/dist/healing/overlay/bridge.d.ts +46 -0
  82. package/dist/healing/overlay/bridge.d.ts.map +1 -0
  83. package/dist/healing/overlay/bridge.js +2 -0
  84. package/dist/healing/overlay/bridge.js.map +1 -0
  85. package/dist/healing/overlay/dock.css.d.ts +2 -0
  86. package/dist/healing/overlay/dock.css.d.ts.map +1 -0
  87. package/dist/healing/overlay/dock.css.js +448 -0
  88. package/dist/healing/overlay/dock.css.js.map +1 -0
  89. package/dist/healing/overlay/dock.html.d.ts +46 -0
  90. package/dist/healing/overlay/dock.html.d.ts.map +1 -0
  91. package/dist/healing/overlay/dock.html.js +248 -0
  92. package/dist/healing/overlay/dock.html.js.map +1 -0
  93. package/dist/healing/overlay/drag.d.ts +17 -0
  94. package/dist/healing/overlay/drag.d.ts.map +1 -0
  95. package/dist/healing/overlay/drag.js +68 -0
  96. package/dist/healing/overlay/drag.js.map +1 -0
  97. package/dist/healing/overlay/inject.d.ts +41 -0
  98. package/dist/healing/overlay/inject.d.ts.map +1 -0
  99. package/dist/healing/overlay/inject.js +277 -0
  100. package/dist/healing/overlay/inject.js.map +1 -0
  101. package/dist/healing/overlay/keybinds.d.ts +33 -0
  102. package/dist/healing/overlay/keybinds.d.ts.map +1 -0
  103. package/dist/healing/overlay/keybinds.js +105 -0
  104. package/dist/healing/overlay/keybinds.js.map +1 -0
  105. package/dist/healing/prompts/heal-v21.d.ts +8 -0
  106. package/dist/healing/prompts/heal-v21.d.ts.map +1 -0
  107. package/dist/healing/prompts/heal-v21.js +204 -0
  108. package/dist/healing/prompts/heal-v21.js.map +1 -0
  109. package/dist/healing/retry-policy.d.ts +37 -0
  110. package/dist/healing/retry-policy.d.ts.map +1 -0
  111. package/dist/healing/retry-policy.js +46 -0
  112. package/dist/healing/retry-policy.js.map +1 -0
  113. package/dist/healing/session-cost.d.ts +44 -0
  114. package/dist/healing/session-cost.d.ts.map +1 -0
  115. package/dist/healing/session-cost.js +95 -0
  116. package/dist/healing/session-cost.js.map +1 -0
  117. package/dist/healing/test-fixture.d.ts +9 -0
  118. package/dist/healing/test-fixture.d.ts.map +1 -0
  119. package/dist/healing/test-fixture.js +37 -0
  120. package/dist/healing/test-fixture.js.map +1 -0
  121. package/dist/healing/types.d.ts +399 -0
  122. package/dist/healing/types.d.ts.map +1 -0
  123. package/dist/healing/types.js +162 -0
  124. package/dist/healing/types.js.map +1 -0
  125. package/dist/healing/validator.d.ts +64 -0
  126. package/dist/healing/validator.d.ts.map +1 -0
  127. package/dist/healing/validator.js +286 -0
  128. package/dist/healing/validator.js.map +1 -0
  129. package/dist/scripts/heal-report.d.ts +23 -0
  130. package/dist/scripts/heal-report.d.ts.map +1 -0
  131. package/dist/scripts/heal-report.js +106 -0
  132. package/dist/scripts/heal-report.js.map +1 -0
  133. package/dist/scripts/init.d.ts +3 -0
  134. package/dist/scripts/init.d.ts.map +1 -0
  135. package/dist/scripts/init.js +132 -0
  136. package/dist/scripts/init.js.map +1 -0
  137. package/package.json +84 -0
@@ -0,0 +1,277 @@
1
+ import { DOCK_CSS } from './dock.css.js';
2
+ import { renderDock } from './dock.html.js';
3
+ import { BINDING_NAME } from './bridge.js';
4
+ import { mountDrag } from './drag.js';
5
+ import { mountKeybinds } from './keybinds.js';
6
+ const pageOverlayState = new WeakMap();
7
+ /**
8
+ * Inject the heal overlay (shadow host + dock UI + drag + keybinds) into
9
+ * the given Playwright page. Idempotent per page: subsequent calls reuse
10
+ * the same message queue. The returned handle owns the lifecycle.
11
+ *
12
+ * @param page - Playwright `Page` to inject into.
13
+ * @returns Handle the orchestrator uses to drive the overlay.
14
+ */
15
+ export async function injectOverlay(page) {
16
+ let state = pageOverlayState.get(page);
17
+ if (!state) {
18
+ state = { queue: [], waiters: [], abortHandler: null };
19
+ pageOverlayState.set(page, state);
20
+ const s = state;
21
+ await page.exposeBinding(BINDING_NAME, (_src, msg) => {
22
+ // Out-of-band abort: if a handler is registered, give it first refusal.
23
+ if (s.abortHandler && s.abortHandler(msg))
24
+ return;
25
+ const w = s.waiters.shift();
26
+ if (w)
27
+ w(msg);
28
+ else
29
+ s.queue.push(msg);
30
+ });
31
+ }
32
+ const { queue, waiters } = state;
33
+ await page.evaluate(({ css, html }) => {
34
+ const host = document.createElement('div');
35
+ host.id = '__heal_host__';
36
+ document.documentElement.appendChild(host);
37
+ const root = host.attachShadow({ mode: 'open' });
38
+ window.__healOverlay_root = root;
39
+ const style = document.createElement('style');
40
+ style.textContent = css;
41
+ root.appendChild(style);
42
+ const container = document.createElement('div');
43
+ container.id = '__heal_container__';
44
+ container.innerHTML = html;
45
+ root.appendChild(container);
46
+ const send = (msg) => window.__healOverlay_send(msg);
47
+ let picking = false;
48
+ let lastEl = null;
49
+ let lastOutline = '';
50
+ let lastOffset = '';
51
+ let lastBoxShadow = '';
52
+ const hl = (el) => {
53
+ if (lastEl) {
54
+ lastEl.style.outline = lastOutline;
55
+ lastEl.style.outlineOffset = lastOffset;
56
+ lastEl.style.boxShadow = lastBoxShadow;
57
+ }
58
+ lastEl = el instanceof HTMLElement ? el : null;
59
+ if (lastEl) {
60
+ lastOutline = lastEl.style.outline;
61
+ lastOffset = lastEl.style.outlineOffset;
62
+ lastBoxShadow = lastEl.style.boxShadow;
63
+ lastEl.style.outline = '2px solid #c7ff00';
64
+ lastEl.style.outlineOffset = '2px';
65
+ lastEl.style.boxShadow = '0 0 0 6px rgba(199,255,0,0.18)';
66
+ }
67
+ };
68
+ const xpathFor = (el) => {
69
+ const segs = [];
70
+ for (let n = el; n && n.nodeType === 1; n = n.parentElement) {
71
+ let i = 1;
72
+ for (let s = n.previousElementSibling; s; s = s.previousElementSibling)
73
+ if (s.tagName === n.tagName)
74
+ i++;
75
+ segs.unshift(`${n.tagName.toLowerCase()}[${i}]`);
76
+ }
77
+ return '/' + segs.join('/');
78
+ };
79
+ const isTooBroad = (el) => {
80
+ if (!el)
81
+ return true;
82
+ const tag = el.tagName.toLowerCase();
83
+ return tag === 'html' || tag === 'body';
84
+ };
85
+ document.addEventListener('mousemove', (e) => {
86
+ if (!picking)
87
+ return;
88
+ const el = e.target;
89
+ if (!el || host.contains(el) || isTooBroad(el)) {
90
+ hl(null);
91
+ return;
92
+ }
93
+ hl(el);
94
+ }, true);
95
+ const summarise = (el) => {
96
+ const tag = el.tagName.toLowerCase();
97
+ const attrs = [];
98
+ for (const a of Array.from(el.attributes)) {
99
+ if (a.name === 'style')
100
+ continue;
101
+ const v = a.value.length > 60 ? a.value.slice(0, 60) + '…' : a.value;
102
+ attrs.push(`${a.name}="${v}"`);
103
+ }
104
+ const text = (el.textContent ?? '').trim().replace(/\s+/g, ' ').slice(0, 80);
105
+ const open = `<${tag}${attrs.length ? ' ' + attrs.join(' ') : ''}>`;
106
+ return text ? `${open}${text}${text.length === 80 ? '…' : ''}</${tag}>` : `${open}…</${tag}>`;
107
+ };
108
+ document.addEventListener('click', (e) => {
109
+ if (!picking)
110
+ return;
111
+ // P2-6: composedPath() pierces open shadow DOM so we capture the
112
+ // deepest non-overlay element instead of the shadow host.
113
+ // P2-7: when the path's deepest element has a closed shadowRoot (or
114
+ // we cannot reach past the host), surface a clear shadow-unsupported
115
+ // marker so the orchestrator can throw a typed HealingError.
116
+ const path = (e.composedPath ? e.composedPath() : [e.target]);
117
+ let target = null;
118
+ for (const node of path) {
119
+ const el = node;
120
+ if (!el || el.nodeType !== 1)
121
+ continue;
122
+ if (host.contains(el))
123
+ continue;
124
+ if (isTooBroad(el))
125
+ continue;
126
+ target = el;
127
+ break;
128
+ }
129
+ if (!target) {
130
+ // composedPath() returned no usable element — most commonly because
131
+ // we hit a closed shadow root (open shadow elements come through in
132
+ // composedPath; closed ones cannot be pierced from outside).
133
+ const host0 = path[0];
134
+ if (host0) {
135
+ e.preventDefault();
136
+ e.stopPropagation();
137
+ send({ type: 'pick-error', reason: 'shadow-unsupported', xpath: xpathFor(host0) });
138
+ }
139
+ return;
140
+ }
141
+ e.preventDefault();
142
+ e.stopPropagation();
143
+ send({ type: 'pick', html: summarise(target), xpath: xpathFor(target) });
144
+ }, true);
145
+ // Document-level keydown shortcuts mounted from ./keybinds.ts after this
146
+ // shadow-host evaluate block returns. The keybinds module looks up
147
+ // `__heal_host__`, the shadow root, and the exposed `__healOverlay_send`
148
+ // binding (set by page.exposeBinding above).
149
+ // Drag logic lives in overlay/drag.ts — wired via window.__healOverlay_drag.
150
+ const wireButtons = () => {
151
+ const buttons = root.querySelectorAll('button[data-act]');
152
+ buttons.forEach((btn) => {
153
+ btn.addEventListener('click', (ev) => {
154
+ ev.preventDefault();
155
+ ev.stopPropagation();
156
+ const act = btn.dataset.act;
157
+ if (act === 'intent') {
158
+ const ta = root.getElementById('heal-intent');
159
+ send({ type: 'intent', text: ta?.value ?? '' });
160
+ }
161
+ else if (act === 'accept') {
162
+ send({ type: 'accept' });
163
+ }
164
+ else if (act === 'reject') {
165
+ send({ type: 'reject' });
166
+ }
167
+ else if (act === 'edit') {
168
+ send({ type: 'edit' });
169
+ }
170
+ else if (act === 'cancel') {
171
+ send({ type: 'cancel' });
172
+ }
173
+ else if (act === 'broaden') {
174
+ send({ type: 'broaden' });
175
+ }
176
+ else if (act === 'force-llm') {
177
+ send({ type: 'force-llm' });
178
+ }
179
+ else if (act === 'cancel-llm') {
180
+ send({ type: 'cancel-llm' });
181
+ }
182
+ else if (act === 'pick-preview-continue') {
183
+ send({ type: 'pick-preview-continue' });
184
+ }
185
+ else if (act === 'pick-preview-repick') {
186
+ send({ type: 'pick-preview-repick' });
187
+ }
188
+ });
189
+ });
190
+ };
191
+ // Elapsed timer: poll a single span#heal-elapsed inside the dock.
192
+ // We start an interval on first setState; it no-ops if the span is absent.
193
+ let elapsedTimer = null;
194
+ const tickElapsed = () => {
195
+ const span = root.getElementById('heal-elapsed');
196
+ if (!span)
197
+ return;
198
+ const startedStr = span.dataset.started || '';
199
+ const started = Number(startedStr);
200
+ if (!started || !Number.isFinite(started))
201
+ return;
202
+ const secs = Math.max(0, Math.floor((Date.now() - started) / 1000));
203
+ span.textContent = secs + 's';
204
+ };
205
+ window.__healOverlay_api = {
206
+ setState(stateHtml) {
207
+ container.innerHTML = stateHtml;
208
+ window.__healOverlay_drag?.wireDrag?.();
209
+ wireButtons();
210
+ window.__healOverlay_drag?.applyDockTransform?.();
211
+ if (elapsedTimer === null) {
212
+ elapsedTimer = window.setInterval(tickElapsed, 250);
213
+ }
214
+ tickElapsed();
215
+ const ta = root.getElementById('heal-intent');
216
+ if (ta) {
217
+ ta.focus();
218
+ ta.addEventListener('keydown', (e) => {
219
+ if (e.key === 'Enter' && !e.shiftKey) {
220
+ e.preventDefault();
221
+ send({ type: 'intent', text: ta.value });
222
+ }
223
+ });
224
+ }
225
+ },
226
+ pickOn() { picking = true; document.body.style.cursor = 'crosshair'; },
227
+ pickOff() { picking = false; document.body.style.cursor = ''; hl(null); },
228
+ highlight(xp) {
229
+ const it = document.evaluate(xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
230
+ if (it.singleNodeValue)
231
+ hl(it.singleNodeValue);
232
+ },
233
+ destroy() {
234
+ if (elapsedTimer !== null) {
235
+ clearInterval(elapsedTimer);
236
+ elapsedTimer = null;
237
+ }
238
+ host.remove();
239
+ },
240
+ };
241
+ }, { css: DOCK_CSS, html: '' });
242
+ await mountDrag(page);
243
+ await mountKeybinds(page);
244
+ let cancelFn = null;
245
+ return {
246
+ async setState(state) {
247
+ const html = renderDock(state);
248
+ // Sync stage to keybinds module so keyboard routing stays correct.
249
+ await page.evaluate(([h, stage]) => {
250
+ window.__healOverlay_api.setState(h);
251
+ window.__healOverlay_setStage?.(stage);
252
+ }, [html, state.stage]);
253
+ },
254
+ async enablePickMode() { await page.evaluate(() => window.__healOverlay_api.pickOn()); },
255
+ async disablePickMode() { await page.evaluate(() => window.__healOverlay_api.pickOff()); },
256
+ async highlight(xpath) { await page.evaluate((x) => window.__healOverlay_api.highlight(x), xpath); },
257
+ waitForMessage() {
258
+ return new Promise((resolve) => {
259
+ const m = queue.shift();
260
+ if (m)
261
+ resolve(m);
262
+ else
263
+ waiters.push(resolve);
264
+ });
265
+ },
266
+ async destroy() { await page.evaluate(() => window.__healOverlay_api.destroy()); },
267
+ setCancelInFlight(fn) { cancelFn = fn; },
268
+ cancelInFlight() { if (cancelFn) {
269
+ try {
270
+ cancelFn();
271
+ }
272
+ catch { /* ignore */ }
273
+ } },
274
+ setAbortHandler(fn) { state.abortHandler = fn; },
275
+ };
276
+ }
277
+ //# sourceMappingURL=inject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject.js","sourceRoot":"","sources":["../../../healing/overlay/inject.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * healing/overlay/keybinds.ts
3
+ *
4
+ * Document-level keydown handler for the heal overlay. Mounted via
5
+ * `mountKeybinds(page)` after the shadow host is in place. Routes
6
+ * single-letter shortcuts to overlay actions, but suppresses them when
7
+ * the keystroke originates from an input/textarea/contentEditable so the
8
+ * developer can type freely into the intent textarea.
9
+ *
10
+ * Shortcuts (only active OUTSIDE text fields):
11
+ * A → accept R → reject E → edit B → broaden
12
+ * L → force-llm P → cancel-llm
13
+ *
14
+ * pick-preview stage shortcuts (never conflicting with text fields — no
15
+ * textarea is present in this stage):
16
+ * Enter → pick-preview-continue
17
+ * r / R → pick-preview-repick
18
+ * Escape → cancel
19
+ *
20
+ * Escape always cancels, even from inside a text field.
21
+ * Enter inside the `#heal-intent` textarea submits the intent.
22
+ */
23
+ import type { Page } from '@playwright/test';
24
+ /**
25
+ * Install the global keydown listener that drives the overlay's keyboard
26
+ * shortcuts. Idempotent in practice — relies on `injectOverlay` running
27
+ * once per page lifecycle.
28
+ *
29
+ * @param page - Playwright `Page` the overlay is mounted on.
30
+ * @returns Resolves once the listener is attached.
31
+ */
32
+ export declare function mountKeybinds(page: Page): Promise<void>;
33
+ //# sourceMappingURL=keybinds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keybinds.d.ts","sourceRoot":"","sources":["../../../healing/overlay/keybinds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgG7D"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Install the global keydown listener that drives the overlay's keyboard
3
+ * shortcuts. Idempotent in practice — relies on `injectOverlay` running
4
+ * once per page lifecycle.
5
+ *
6
+ * @param page - Playwright `Page` the overlay is mounted on.
7
+ * @returns Resolves once the listener is attached.
8
+ */
9
+ export async function mountKeybinds(page) {
10
+ await page.evaluate(() => {
11
+ const host = document.getElementById('__heal_host__');
12
+ if (!host)
13
+ return;
14
+ const root = window.__healOverlay_root;
15
+ if (!root)
16
+ return;
17
+ const send = (msg) => window.__healOverlay_send(msg);
18
+ // Current stage is tracked in a closure variable, updated by the orchestrator
19
+ // via a lightweight state-sync hook on the __healOverlay_api.setState path.
20
+ // This allows keybinds to branch by stage without polling the DOM.
21
+ let currentStage = 'pick';
22
+ const origSetState = window.__healOverlay_api?.setState;
23
+ // Hook into setState to track the current stage. We patch it here (after
24
+ // mountDrag / mountKeybinds run) — setState is called before any
25
+ // interactive stage, so the first call syncs the state.
26
+ const patchSetState = () => {
27
+ const api = window.__healOverlay_api;
28
+ if (!api || api.__keybinds_patched)
29
+ return;
30
+ const original = api.setState.bind(api);
31
+ api.setState = (html) => {
32
+ // Extract stage from the rendered HTML: look for data-stage attribute
33
+ // or the dock div class / stage indicator embedded by renderDock.
34
+ // We use a tiny sentinel comment injected at render time.
35
+ const m = html.match(/data-stage="([^"]+)"/);
36
+ if (m)
37
+ currentStage = m[1];
38
+ return original(html);
39
+ };
40
+ api.__keybinds_patched = true;
41
+ };
42
+ document.addEventListener('keydown', (e) => {
43
+ // Lazy-patch setState on first keydown (api guaranteed to exist by then).
44
+ patchSetState();
45
+ // ── pick-preview stage: dedicated shortcuts, no textarea conflict ──
46
+ if (currentStage === 'pick-preview') {
47
+ if (e.key === 'Escape')
48
+ return send({ type: 'cancel' });
49
+ if (e.key === 'Enter') {
50
+ e.preventDefault();
51
+ return send({ type: 'pick-preview-continue' });
52
+ }
53
+ if (e.key === 'r' || e.key === 'R') {
54
+ e.preventDefault();
55
+ return send({ type: 'pick-preview-repick' });
56
+ }
57
+ // Swallow all other keys in this stage to prevent accidental actions.
58
+ return;
59
+ }
60
+ // If the keystroke originated from any text input (textarea / input /
61
+ // contentEditable), suppress letter shortcuts entirely. Otherwise
62
+ // letters typed into the intent box would trigger A/R/E/B/L/P actions.
63
+ const path = e.composedPath();
64
+ const typingInField = path.some((n) => {
65
+ if (!(n instanceof HTMLElement))
66
+ return false;
67
+ const tag = n.tagName;
68
+ return tag === 'TEXTAREA' || tag === 'INPUT' || n.isContentEditable;
69
+ });
70
+ // Escape always cancels, even from inside the textarea.
71
+ if (e.key === 'Escape')
72
+ return send({ type: 'cancel' });
73
+ if (typingInField) {
74
+ // Enter inside our intent textarea submits intent.
75
+ const intent = root.getElementById('heal-intent');
76
+ if (intent && e.target === host && e.key === 'Enter' && !e.shiftKey) {
77
+ e.preventDefault();
78
+ return send({ type: 'intent', text: intent.value });
79
+ }
80
+ return; // let the field receive the keystroke normally
81
+ }
82
+ // Outside any input field — overlay shortcuts active.
83
+ if (e.key === 'a' || e.key === 'A')
84
+ return send({ type: 'accept' });
85
+ if (e.key === 'r' || e.key === 'R')
86
+ return send({ type: 'reject' });
87
+ if (e.key === 'e' || e.key === 'E')
88
+ return send({ type: 'edit' });
89
+ if (e.key === 'b' || e.key === 'B')
90
+ return send({ type: 'broaden' });
91
+ if (e.key === 'l' || e.key === 'L')
92
+ return send({ type: 'force-llm' });
93
+ if (e.key === 'p' || e.key === 'P')
94
+ return send({ type: 'cancel-llm' });
95
+ }, true);
96
+ // Expose a direct stage-sync function so the orchestrator (running in
97
+ // Node.js context) can set the stage label via page.evaluate without
98
+ // depending on HTML parsing. Called by injectOverlay.setState wrapper.
99
+ window.__healOverlay_setStage = (stage) => { currentStage = stage; };
100
+ // intentionally swallowed: origSetState ref may be undefined if keybinds
101
+ // mount before api — patchSetState handles the deferred case
102
+ void origSetState;
103
+ });
104
+ }
105
+ //# sourceMappingURL=keybinds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keybinds.js","sourceRoot":"","sources":["../../../healing/overlay/keybinds.ts"],"names":[],"mappings":"AAwBA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAU;IAC5C,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;QACvB,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,IAAI,GAAI,MAAyD,CAAC,kBAAkB,CAAC;QAC3F,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,IAAI,GAAG,CAAC,GAAY,EAAE,EAAE,CAC3B,MAAkE,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAE9F,8EAA8E;QAC9E,4EAA4E;QAC5E,mEAAmE;QACnE,IAAI,YAAY,GAAW,MAAM,CAAC;QAClC,MAAM,YAAY,GAAI,MAAc,CAAC,iBAAiB,EAAE,QAAQ,CAAC;QAEjE,yEAAyE;QACzE,iEAAiE;QACjE,wDAAwD;QACxD,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,MAAM,GAAG,GAAI,MAAc,CAAC,iBAAiB,CAAC;YAC9C,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,kBAAkB;gBAAE,OAAO;YAC3C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxC,GAAG,CAAC,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE;gBAC9B,sEAAsE;gBACtE,kEAAkE;gBAClE,0DAA0D;gBAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;gBAC7C,IAAI,CAAC;oBAAE,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC3B,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC,CAAC;YACF,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAChC,CAAC,CAAC;QAEF,QAAQ,CAAC,gBAAgB,CACvB,SAAS,EACT,CAAC,CAAC,EAAE,EAAE;YACJ,0EAA0E;YAC1E,aAAa,EAAE,CAAC;YAEhB,sEAAsE;YACtE,IAAI,YAAY,KAAK,cAAc,EAAE,CAAC;gBACpC,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;oBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBACxD,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;oBACtB,CAAC,CAAC,cAAc,EAAE,CAAC;oBACnB,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;gBACjD,CAAC;gBACD,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;oBACnC,CAAC,CAAC,cAAc,EAAE,CAAC;oBACnB,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,CAAC,CAAC;gBAC/C,CAAC;gBACD,sEAAsE;gBACtE,OAAO;YACT,CAAC;YAED,sEAAsE;YACtE,kEAAkE;YAClE,uEAAuE;YACvE,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC;YAC9B,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;gBACpC,IAAI,CAAC,CAAC,CAAC,YAAY,WAAW,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAC9C,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC;gBACtB,OAAO,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,iBAAiB,CAAC;YACtE,CAAC,CAAC,CAAC;YAEH,wDAAwD;YACxD,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAExD,IAAI,aAAa,EAAE,CAAC;gBAClB,mDAAmD;gBACnD,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,aAAa,CAA+B,CAAC;gBAChF,IAAI,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;oBACpE,CAAC,CAAC,cAAc,EAAE,CAAC;oBACnB,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;gBACtD,CAAC;gBACD,OAAO,CAAC,+CAA+C;YACzD,CAAC;YAED,sDAAsD;YACtD,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YACpE,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YACpE,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAClE,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;YACrE,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;YACvE,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QAC1E,CAAC,EACD,IAAI,CACL,CAAC;QAEF,sEAAsE;QACtE,qEAAqE;QACrE,uEAAuE;QACtE,MAAc,CAAC,sBAAsB,GAAG,CAAC,KAAa,EAAE,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtF,yEAAyE;QACzE,6DAA6D;QAC7D,KAAK,YAAY,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Senior-SDET persona + strict tier rubric used for every
3
+ * `requestHealedLocator` call. Mutating this string invalidates OpenAI's
4
+ * prompt-cache hash and momentarily increases per-call cost until the new
5
+ * prefix re-warms.
6
+ */
7
+ export declare const HEAL_V21_SYSTEM_PROMPT = "# WHO YOU ARE\nYou are a senior SDET architect with 8+ years writing Playwright and\nSelenium tests at production scale. You have been burned by every UI\nframework footgun \u2014 React reconciliation ids, Emotion / styled-\ncomponents hashed class names, MUI `css-1k4n5`, Vue `data-v-{hash}`,\nAngular `_ngcontent-c12`, Ember UUIDs, React 18 `:r0:` useId,\nSalesforce LWC scoped classes. You instinctively distrust anything that\nlooks generated and instinctively trust ARIA roles, accessible names,\nand test ids. You write the locator a careful human would write by\nhand \u2014 not the locator a tool would auto-export.\n\n# WHAT YOU DO (scope of work)\nONE thing only: take the developer's picked element + their intent\ndescription, and produce exactly 3 ranked Playwright locators in JSON.\nThat is all. You do not refactor tests. You do not suggest test design\nchanges. You do not explain your reasoning at length. You do not output\nanything that is not strict JSON matching the required schema.\n\n# WHAT IS IN SCOPE\n- Generating ARIA-role, test-id, label, text, attribute-CSS, xpath, or\n Playwright filter-chain locators that uniquely match the picked\n element on the current page.\n- Drilling DOWN when the picked element is a container \u2014 your locator\n must resolve to the inner element matching the user's intent verb.\n- Drilling UP when the picked element is a non-interactive descendant\n (a `<span>` / `<svg>` / `<i>` inside a `<button>` / `<a>`) AND the\n developer's intent describes the interactive ancestor \u2014 target the\n clickable ancestor.\n\n# WHAT IS OUT OF SCOPE (do not do these)\n- Inventing role names, attribute values, or text that does not appear\n in the picked element or candidate list. NO HALLUCINATION.\n- Proposing any locator with auto-generated tokens. If a token has the\n shape `<prefix>-<digits/hex>` with 5+ alphanumeric chars after a\n hyphen or underscore, it is auto-generated. Reject it.\n- Producing positional xpath (`//div[3]/section[2]/button[1]`) or deep\n descendant CSS chains (`body > div > main > div > section > button`)\n before round 3.\n- Producing `page.evaluate(...)` or JavaScript expressions unless round\n is exactly 4.\n- Returning more or fewer than 3 suggestions.\n- Returning prose, commentary, markdown fences, or anything outside the\n JSON object.\n\n# HOW YOU THINK (silent 3-step internal monologue \u2014 do NOT emit)\n\n Step 1. UNDERSTAND\n Restate the developer's intent in one sentence. What element are\n they acting on? What is the verb (click, fill, expect)? If the\n intent words and the picked element disagree (intent says \"submit\"\n but pick is a Cancel button), THE PICK WINS. Note the disagreement\n in your rationale.\n\n Step 2. ANCHOR\n Walk the picked element's properties in this priority order. Stop\n at the FIRST strong stable anchor:\n a. dataTestId present and not auto-generated \u2192 use it.\n b. accessibleName non-empty AND (explicit role OR implicit role\n from the table below) \u2192 use role + name.\n c. associated <label> text for form fields \u2192 use label.\n d. unique visible text (siblingsSameTag === 1 AND no other\n candidate has the same accessibleName) \u2192 use text.\n e. stable id (does not match auto-gen pattern) \u2192 use #id.\n f. stable attribute (name, type, data-action / data-event /\n data-name \u2014 NOT data-v / data-reactid / data-emotion).\n If NONE hold, you are in fragile territory: announce in rationale\n and fall back to tier 4\u20136.\n\n Step 3. SCOPE\n Ask: does my chosen anchor uniquely identify THE picked element on\n the current page? Check siblingsSameTag and candidate count.\n \u2192 If YES, emit the bare semantic locator (tier 1 or 2).\n \u2192 If NO, wrap it in a filter-chain (tier 3). Pick a parent\n landmark visible in parentRole (form, navigation, dialog,\n region, main, listitem, table). Add `filter({hasText: 'X'})`\n using a phrase that comes from the intent or visible sibling\n text \u2014 never invented.\n\n# WHAT YOU KNOW (skill)\n\n## Implicit ARIA roles (use when no explicit role attribute)\n <button> \u2192 button\n <a href> \u2192 link\n <input type=text> \u2192 textbox\n <input type=submit> \u2192 button\n <input type=checkbox> \u2192 checkbox\n <input type=radio> \u2192 radio\n <select> \u2192 combobox\n <textarea> \u2192 textbox\n <h1>..<h6> \u2192 heading\n <nav> \u2192 navigation\n <main> \u2192 main\n <header> \u2192 banner\n <footer> \u2192 contentinfo\n <aside> \u2192 complementary\n <ul>/<ol> \u2192 list\n <li> \u2192 listitem\n <table> \u2192 table\n <tr> \u2192 row\n <td> \u2192 cell\n <form> \u2192 form\n\n## Accessible name computation (use this order)\n 1. aria-labelledby (referenced element's text)\n 2. aria-label attribute\n 3. <label for=id> for inputs (or wrapping <label>)\n 4. element's visible textContent\n 5. placeholder (inputs only, weak fallback)\n 6. title attribute (weakest)\n\n## Edge-case recognition (apply these rules without being asked)\n - Pick is <span>/<svg>/<i> inside a <button>/<a> \u2192\n target the interactive ancestor. Use parentRole + nearest\n accessibleName.\n - Pick is a container <section>/<div>/<form> and intent verb is\n click/fill/select/expect-visible \u2192 drill down to the inner\n interactive element matching the intent noun.\n - accessibleName empty (icon-only button) \u2192 look for aria-label or\n title. If neither, the element is a11y-broken; fall to data-*\n or stable id.\n - Multiple elements share the same accessibleName (5 \"Edit\" buttons\n in a table) \u2192 tier 3 filter-chain by row text from neighborText.\n - Form input has no <label for=> but has placeholder \u2192 use\n role=textbox + accessibleName from associatedLabel, OR scope to\n enclosing form.\n - Generic visible text (\"Submit\", \"Save\", \"Close\") appears in many\n components on the page \u2192 NEVER bare text=; always scope via\n filter-chain.\n\n# OUTPUT CONTRACT\nReturn strict JSON: { \"suggestions\": [3 entries] }. Each entry:\n - selectorType: \"role\" | \"text\" | \"label\" | \"testid\" | \"css\" |\n \"xpath\" | \"filter-chain\" | \"js\"\n - selector: the bare selector value (forms below)\n - tier: integer 1-7 matching the tier table\n - rationale: ONE sentence stating the tier and what disambiguates.\n\n## Selector forms by type\n role \u2192 `button[name=\"Submit\"]` (NO `role=` prefix in the value)\n text \u2192 `Sign in` (visible text only)\n label \u2192 `Email address` (label text only)\n testid \u2192 `submit-btn` (the testid value only)\n css \u2192 `#stable-id`, `[name=\"email\"]`, `[data-action=\"save\"]`\n xpath \u2192 `//button[@data-x=\"y\"]` (attribute-anchored only)\n filter-chain \u2192 JSON: {\"root\":\"role=...\",\"filters\":[{\"hasText\":\"...\"}],\"target\":\"role=...\"}\n js \u2192 JS expression returning ONE Element (round 4 only, never cached)\n\n## Tier rubric (the tier you emit is a CONSEQUENCE of ANCHOR + SCOPE)\n 1 = clean semantic (role+name / label / text) \u2014 anchor unique\n 2 = testid \u2014 dataTestId present and clean\n 3 = filter-chain \u2014 semantic anchor exists but multi-matches \u2192 scope\n 4 = attribute CSS / stable id\n 5 = class chain CSS (brittle \u2014 last CSS resort)\n 6 = attr-anchored xpath\n 7 = js evaluate (round 4 escape hatch, never cached)\n\n## Round semantics\n Round 1: best single-shot. Allowed tiers: 1-4.\n Round 2: previous attempts failed \u2014 read previousAttempts, avoid\n repeats. Allowed tiers: 1-5. Strongly prefer chained scope.\n Round 3: allow tier 6 (attribute-anchored xpath).\n Round 4: allow tier 7 (js evaluate).\n\n## Quoting (multi-word names break without quotes)\n CORRECT: role=region[name=\"Login form\"]\n CORRECT: button[name=\"Sign in\"]\n WRONG: role=region[name=Login form] \u2190 will not parse\n\n# WORKED EXAMPLE (shows ANCHOR + SCOPE producing tier 1)\nPicked: <button class=\"x_a82\">Sign in</button> inside <form aria-label=\"Login form\">\nsiblingsSameTag: 1, accessibleName: \"Sign in\", parentRole: \"form\"\nIntent: \"sign-in button in the login form\"\nRound: 1\nPrevious: []\nSilent reasoning: UNDERSTAND \u2192 click the sign-in button.\n ANCHOR \u2192 no testid, but accessibleName \"Sign in\" +\n implicit role button.\n SCOPE \u2192 siblingsSameTag=1, only one Sign in on page \u2192\n tier 1.\nOutput:\n{\"suggestions\":[\n {\"selectorType\":\"role\",\"selector\":\"button[name=\\\"Sign in\\\"]\",\"rationale\":\"Tier 1: unique role+accessibleName.\",\"tier\":1},\n {\"selectorType\":\"filter-chain\",\"selector\":\"{\\\"root\\\":\\\"role=region[name=\\\\\\\"Login form\\\\\\\"]\\\",\\\"filters\\\":[],\\\"target\\\":\\\"role=button[name=\\\\\\\"Sign in\\\\\\\"]\\\"}\",\"rationale\":\"Tier 3: scoped via login-form landmark for extra robustness.\",\"tier\":3},\n {\"selectorType\":\"text\",\"selector\":\"Sign in\",\"rationale\":\"Tier 1 text variant.\",\"tier\":1}\n]}\n\n# REMEMBER\nThink silently. Output strict JSON only. No markdown fences. No\ncommentary outside JSON. No invented values.";
8
+ //# sourceMappingURL=heal-v21.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heal-v21.d.ts","sourceRoot":"","sources":["../../../healing/prompts/heal-v21.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,s6SA4LU,CAAC"}