chrometools-mcp 2.5.0 → 3.1.2

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 (48) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +406 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/extension/background.js +643 -0
  12. package/extension/content.js +715 -0
  13. package/extension/icons/create-icons.js +164 -0
  14. package/extension/icons/icon128.png +0 -0
  15. package/extension/icons/icon16.png +0 -0
  16. package/extension/icons/icon48.png +0 -0
  17. package/extension/manifest.json +58 -0
  18. package/extension/popup/popup.css +437 -0
  19. package/extension/popup/popup.html +102 -0
  20. package/extension/popup/popup.js +415 -0
  21. package/extension/recorder-overlay.css +93 -0
  22. package/index.js +3347 -2901
  23. package/models/BaseInputModel.js +93 -0
  24. package/models/CheckboxGroupModel.js +199 -0
  25. package/models/CheckboxModel.js +103 -0
  26. package/models/ColorInputModel.js +53 -0
  27. package/models/DateInputModel.js +67 -0
  28. package/models/RadioGroupModel.js +126 -0
  29. package/models/RangeInputModel.js +60 -0
  30. package/models/SelectModel.js +97 -0
  31. package/models/TextInputModel.js +34 -0
  32. package/models/TextareaModel.js +59 -0
  33. package/models/TimeInputModel.js +49 -0
  34. package/models/index.js +122 -0
  35. package/package.json +3 -2
  36. package/pom/apom-converter.js +267 -0
  37. package/pom/apom-tree-converter.js +515 -0
  38. package/pom/element-id-generator.js +175 -0
  39. package/recorder/page-object-generator.js +16 -0
  40. package/recorder/scenario-executor.js +80 -2
  41. package/server/tool-definitions.js +839 -713
  42. package/server/tool-groups.js +1 -1
  43. package/server/tool-schemas.js +367 -326
  44. package/server/websocket-bridge.js +447 -0
  45. package/utils/selector-resolver.js +186 -0
  46. package/utils/ui-framework-detector.js +392 -0
  47. package/RELEASE_NOTES_v2.5.0.md +0 -109
  48. package/npm_publish_output.txt +0 -0
@@ -0,0 +1,715 @@
1
+ /**
2
+ * ChromeTools MCP Extension - Content Script
3
+ *
4
+ * Injected into every page to:
5
+ * - Record user actions (click, type, select, etc.)
6
+ * - Show recording overlay
7
+ * - Generate selectors for elements
8
+ * - Detect and mask secrets
9
+ */
10
+
11
+ (function() {
12
+ 'use strict';
13
+
14
+ // Prevent multiple injections
15
+ if (window.__chrometoolsContentScript) {
16
+ return;
17
+ }
18
+ window.__chrometoolsContentScript = true;
19
+
20
+ // ============================================
21
+ // State
22
+ // ============================================
23
+
24
+ let isRecording = false;
25
+ let isPaused = false;
26
+ let overlay = null;
27
+ let actionCount = 0;
28
+
29
+ // Debounce timers
30
+ const inputDebounceTimers = new Map();
31
+ const lastInputValues = new Map(); // Value when element was last recorded
32
+ const inputStartValues = new Map(); // Value when user started typing (before any input)
33
+ let scrollDebounceTimer = null;
34
+ let lastHoverTarget = null;
35
+
36
+ // ============================================
37
+ // Selector Generator
38
+ // ============================================
39
+
40
+ function generateSelector(element) {
41
+ if (!element || element === document.body || element === document.documentElement) {
42
+ return { primary: 'body', fallbacks: [] };
43
+ }
44
+
45
+ const selectors = [];
46
+
47
+ // 1. ID selector (highest priority)
48
+ if (element.id && !isGeneratedId(element.id)) {
49
+ selectors.push(`#${CSS.escape(element.id)}`);
50
+ }
51
+
52
+ // 2. data-testid, data-cy, data-test attributes
53
+ const testAttrs = ['data-testid', 'data-cy', 'data-test', 'data-qa'];
54
+ for (const attr of testAttrs) {
55
+ const value = element.getAttribute(attr);
56
+ if (value) {
57
+ selectors.push(`[${attr}="${CSS.escape(value)}"]`);
58
+ }
59
+ }
60
+
61
+ // 3. name attribute (for form elements)
62
+ if (element.name) {
63
+ const tag = element.tagName.toLowerCase();
64
+ selectors.push(`${tag}[name="${CSS.escape(element.name)}"]`);
65
+ }
66
+
67
+ // 4. aria-label
68
+ const ariaLabel = element.getAttribute('aria-label');
69
+ if (ariaLabel) {
70
+ selectors.push(`[aria-label="${CSS.escape(ariaLabel)}"]`);
71
+ }
72
+
73
+ // 5. Type + placeholder for inputs
74
+ if (element.tagName === 'INPUT') {
75
+ const type = element.type || 'text';
76
+ const placeholder = element.placeholder;
77
+ if (placeholder) {
78
+ selectors.push(`input[type="${type}"][placeholder="${CSS.escape(placeholder)}"]`);
79
+ }
80
+ }
81
+
82
+ // 6. Button/link text
83
+ if (['BUTTON', 'A'].includes(element.tagName)) {
84
+ const text = element.textContent?.trim();
85
+ if (text && text.length < 50) {
86
+ // Use XPath for text matching (will be converted on server side)
87
+ selectors.push(`//${element.tagName.toLowerCase()}[contains(text(),"${text.substring(0, 30)}")]`);
88
+ }
89
+ }
90
+
91
+ // 7. CSS class combination (avoid dynamic classes)
92
+ const stableClasses = getStableClasses(element);
93
+ if (stableClasses.length > 0) {
94
+ const tag = element.tagName.toLowerCase();
95
+ selectors.push(`${tag}.${stableClasses.join('.')}`);
96
+ }
97
+
98
+ // 8. Fallback: nth-child path
99
+ const nthPath = getNthChildPath(element);
100
+ if (nthPath) {
101
+ selectors.push(nthPath);
102
+ }
103
+
104
+ // Validate selectors
105
+ const validSelectors = selectors.filter(sel => {
106
+ if (sel.startsWith('//')) return true; // XPath
107
+ try {
108
+ const matches = document.querySelectorAll(sel);
109
+ return matches.length === 1 || (matches.length > 0 && matches[0] === element);
110
+ } catch {
111
+ return false;
112
+ }
113
+ });
114
+
115
+ return {
116
+ primary: validSelectors[0] || nthPath || 'body',
117
+ fallbacks: validSelectors.slice(1, 4),
118
+ elementInfo: {
119
+ tag: element.tagName.toLowerCase(),
120
+ id: element.id || null,
121
+ classes: Array.from(element.classList),
122
+ text: element.textContent?.trim().substring(0, 50) || null
123
+ }
124
+ };
125
+ }
126
+
127
+ function isGeneratedId(id) {
128
+ // Common patterns for auto-generated IDs
129
+ const patterns = [
130
+ /^:r[0-9a-z]+:$/i, // React 18
131
+ /^[a-f0-9]{8,}$/i, // UUID-like
132
+ /^ember\d+$/i, // Ember
133
+ /^ng-\d+$/i, // Angular
134
+ /^uid-\d+$/i, // Generic
135
+ /^\d+$/, // Pure numbers
136
+ ];
137
+ return patterns.some(p => p.test(id));
138
+ }
139
+
140
+ function getStableClasses(element) {
141
+ const unstablePatterns = [
142
+ /^css-[a-z0-9]+$/i, // CSS modules
143
+ /^sc-[a-z]+$/i, // styled-components
144
+ /^_[a-z0-9]+_[a-z0-9]+$/i, // CSS modules variant
145
+ /^[a-z]+-[a-f0-9]{5,}$/i, // Hash-based
146
+ /^jsx-\d+$/i, // styled-jsx
147
+ /^emotion-\d+$/i, // Emotion
148
+ ];
149
+
150
+ return Array.from(element.classList).filter(cls => {
151
+ if (cls.length < 2) return false;
152
+ return !unstablePatterns.some(p => p.test(cls));
153
+ }).slice(0, 3);
154
+ }
155
+
156
+ function getNthChildPath(element, maxDepth = 5) {
157
+ const path = [];
158
+ let current = element;
159
+ let depth = 0;
160
+
161
+ while (current && current !== document.body && depth < maxDepth) {
162
+ const parent = current.parentElement;
163
+ if (!parent) break;
164
+
165
+ const siblings = Array.from(parent.children).filter(
166
+ el => el.tagName === current.tagName
167
+ );
168
+
169
+ if (siblings.length === 1) {
170
+ path.unshift(current.tagName.toLowerCase());
171
+ } else {
172
+ const index = siblings.indexOf(current) + 1;
173
+ path.unshift(`${current.tagName.toLowerCase()}:nth-of-type(${index})`);
174
+ }
175
+
176
+ current = parent;
177
+ depth++;
178
+ }
179
+
180
+ return path.length > 0 ? path.join(' > ') : null;
181
+ }
182
+
183
+ // ============================================
184
+ // Secret Detection
185
+ // ============================================
186
+
187
+ function isSecretField(element) {
188
+ const type = (element.type || '').toLowerCase();
189
+ const name = (element.name || '').toLowerCase();
190
+ const id = (element.id || '').toLowerCase();
191
+ const placeholder = (element.placeholder || '').toLowerCase();
192
+ const ariaLabel = (element.getAttribute('aria-label') || '').toLowerCase();
193
+ const autocomplete = (element.autocomplete || '').toLowerCase();
194
+
195
+ // Password fields
196
+ if (type === 'password') {
197
+ return { isSecret: true, fieldType: 'password' };
198
+ }
199
+
200
+ // Secret keywords
201
+ const secretKeywords = [
202
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
203
+ 'api-key', 'auth', 'credential', 'private', 'ssn', 'credit', 'cvv',
204
+ 'cvc', 'pin', 'otp', 'totp', '2fa', 'mfa'
205
+ ];
206
+
207
+ const allText = `${name} ${id} ${placeholder} ${ariaLabel} ${autocomplete}`;
208
+
209
+ for (const keyword of secretKeywords) {
210
+ if (allText.includes(keyword)) {
211
+ return { isSecret: true, fieldType: keyword };
212
+ }
213
+ }
214
+
215
+ return { isSecret: false, fieldType: null };
216
+ }
217
+
218
+ function generateParamName(fieldType, element) {
219
+ const name = element.name || element.id || fieldType || 'secret';
220
+ const cleanName = name
221
+ .replace(/[^a-zA-Z0-9]/g, '_')
222
+ .replace(/_+/g, '_')
223
+ .replace(/^_|_$/g, '')
224
+ .toLowerCase();
225
+
226
+ return cleanName || 'secret';
227
+ }
228
+
229
+ // ============================================
230
+ // Overlay UI
231
+ // ============================================
232
+
233
+ function createOverlay() {
234
+ if (overlay) return;
235
+
236
+ overlay = document.createElement('div');
237
+ overlay.id = 'chrometools-recorder-overlay';
238
+ overlay.innerHTML = `
239
+ <div class="recorder-dot"></div>
240
+ <span class="recorder-text">Recording <span class="action-count">${actionCount}</span></span>
241
+ `;
242
+
243
+ document.body.appendChild(overlay);
244
+
245
+ // Make draggable
246
+ makeDraggable(overlay);
247
+
248
+ // Add recording indicator to body
249
+ document.body.classList.add('chrometools-recording');
250
+ }
251
+
252
+ function removeOverlay() {
253
+ if (overlay) {
254
+ overlay.remove();
255
+ overlay = null;
256
+ }
257
+ document.body.classList.remove('chrometools-recording');
258
+ }
259
+
260
+ function updateOverlay() {
261
+ if (!overlay) return;
262
+ const countEl = overlay.querySelector('.action-count');
263
+ if (countEl) {
264
+ countEl.textContent = actionCount;
265
+ }
266
+
267
+ if (isPaused) {
268
+ overlay.classList.add('paused');
269
+ overlay.querySelector('.recorder-text').textContent = `Paused (${actionCount})`;
270
+ } else {
271
+ overlay.classList.remove('paused');
272
+ overlay.querySelector('.recorder-text').innerHTML = `Recording <span class="action-count">${actionCount}</span>`;
273
+ }
274
+ }
275
+
276
+ function makeDraggable(element) {
277
+ let isDragging = false;
278
+ let offsetX, offsetY;
279
+
280
+ element.addEventListener('mousedown', (e) => {
281
+ isDragging = true;
282
+ offsetX = e.clientX - element.offsetLeft;
283
+ offsetY = e.clientY - element.offsetTop;
284
+ element.style.cursor = 'grabbing';
285
+ });
286
+
287
+ document.addEventListener('mousemove', (e) => {
288
+ if (!isDragging) return;
289
+ e.preventDefault();
290
+
291
+ const x = e.clientX - offsetX;
292
+ const y = e.clientY - offsetY;
293
+
294
+ element.style.left = `${x}px`;
295
+ element.style.top = `${y}px`;
296
+ element.style.right = 'auto';
297
+ });
298
+
299
+ document.addEventListener('mouseup', () => {
300
+ isDragging = false;
301
+ if (element) {
302
+ element.style.cursor = 'move';
303
+ }
304
+ });
305
+ }
306
+
307
+ // ============================================
308
+ // Event Handlers
309
+ // ============================================
310
+
311
+ function handleClick(e) {
312
+ if (!isRecording || isPaused) return;
313
+ if (e.target.closest('#chrometools-recorder-overlay')) return;
314
+
315
+ const target = findClickableTarget(e.target);
316
+ if (!isElementVisible(target)) return;
317
+
318
+ const selector = generateSelector(target);
319
+
320
+ sendAction({
321
+ type: 'click',
322
+ selector,
323
+ timestamp: Date.now(),
324
+ data: {
325
+ text: target.textContent?.trim().substring(0, 50) || '',
326
+ href: target.href || null,
327
+ tagName: target.tagName.toLowerCase()
328
+ }
329
+ });
330
+
331
+ highlightElement(target);
332
+ }
333
+
334
+ function handleInput(e) {
335
+ if (!isRecording || isPaused) return;
336
+ if (e.target.closest('#chrometools-recorder-overlay')) return;
337
+
338
+ const element = e.target;
339
+ const timerId = inputDebounceTimers.get(element);
340
+ if (timerId) clearTimeout(timerId);
341
+
342
+ // Capture the START value when user begins typing in this element
343
+ // This value stays the same until we actually record the action
344
+ if (!inputStartValues.has(element)) {
345
+ inputStartValues.set(element, lastInputValues.get(element) || '');
346
+ }
347
+
348
+ // NO debounce timer - only record on blur/Enter/tab switch
349
+ // This prevents multiple recordings when user types slowly with pauses
350
+ // The value will be recorded when:
351
+ // 1. User clicks elsewhere (blur)
352
+ // 2. User presses Enter or Tab
353
+ // 3. Recording stops
354
+ }
355
+
356
+ function flushAllPendingInputs() {
357
+ // Flush all elements that have pending input values
358
+ for (const element of inputStartValues.keys()) {
359
+ flushInputValue(element);
360
+ }
361
+ }
362
+
363
+ function flushInputValue(element) {
364
+ // Clear any pending timer
365
+ const timerId = inputDebounceTimers.get(element);
366
+ if (timerId) {
367
+ clearTimeout(timerId);
368
+ inputDebounceTimers.delete(element);
369
+ }
370
+
371
+ // Get the value from when user started typing
372
+ const startValue = inputStartValues.get(element);
373
+ if (startValue === undefined) return; // No input session in progress
374
+
375
+ const finalValue = element.value;
376
+
377
+ // Only record if value actually changed
378
+ if (finalValue === startValue) {
379
+ inputStartValues.delete(element);
380
+ return;
381
+ }
382
+
383
+ if (!isElementVisible(element)) {
384
+ inputStartValues.delete(element);
385
+ return;
386
+ }
387
+
388
+ const selector = generateSelector(element);
389
+ const secretInfo = isSecretField(element);
390
+
391
+ let recordedValue = finalValue;
392
+ let paramName = null;
393
+
394
+ if (secretInfo.isSecret) {
395
+ paramName = generateParamName(secretInfo.fieldType, element);
396
+ recordedValue = `{{${paramName}}}`;
397
+
398
+ // Register secret with background
399
+ chrome.runtime.sendMessage({
400
+ type: 'REGISTER_SECRET',
401
+ paramName,
402
+ value: finalValue
403
+ });
404
+ }
405
+
406
+ sendAction({
407
+ type: 'type',
408
+ selector,
409
+ timestamp: Date.now(),
410
+ data: {
411
+ text: recordedValue,
412
+ isSecret: secretInfo.isSecret,
413
+ paramName,
414
+ clearFirst: startValue === ''
415
+ }
416
+ });
417
+
418
+ // Update stored value and clear the start value
419
+ lastInputValues.set(element, finalValue);
420
+ inputStartValues.delete(element);
421
+ highlightElement(element);
422
+ }
423
+
424
+ function handleBlur(e) {
425
+ if (!isRecording || isPaused) return;
426
+ if (e.target.closest('#chrometools-recorder-overlay')) return;
427
+
428
+ const element = e.target;
429
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
430
+ flushInputValue(element);
431
+ }
432
+ }
433
+
434
+ function handleChange(e) {
435
+ if (!isRecording || isPaused) return;
436
+ if (e.target.closest('#chrometools-recorder-overlay')) return;
437
+
438
+ const element = e.target;
439
+
440
+ if (element.tagName === 'SELECT') {
441
+ if (!isElementVisible(element)) return;
442
+
443
+ const selector = generateSelector(element);
444
+
445
+ sendAction({
446
+ type: 'select',
447
+ selector,
448
+ timestamp: Date.now(),
449
+ data: {
450
+ value: element.value,
451
+ text: element.options[element.selectedIndex]?.text
452
+ }
453
+ });
454
+
455
+ highlightElement(element);
456
+ } else if (element.type === 'file') {
457
+ if (!isElementVisible(element)) return;
458
+
459
+ const selector = generateSelector(element);
460
+
461
+ sendAction({
462
+ type: 'upload',
463
+ selector,
464
+ timestamp: Date.now(),
465
+ data: {
466
+ fileName: element.files[0]?.name,
467
+ filePath: '{{filePath}}'
468
+ }
469
+ });
470
+ } else if (element.type === 'checkbox' || element.type === 'radio') {
471
+ if (!isElementVisible(element)) return;
472
+
473
+ const selector = generateSelector(element);
474
+
475
+ sendAction({
476
+ type: 'click',
477
+ selector,
478
+ timestamp: Date.now(),
479
+ data: {
480
+ checked: element.checked,
481
+ inputType: element.type,
482
+ value: element.value
483
+ }
484
+ });
485
+
486
+ highlightElement(element);
487
+ }
488
+ }
489
+
490
+ function handleKeyDown(e) {
491
+ if (!isRecording || isPaused) return;
492
+ if (e.target.closest('#chrometools-recorder-overlay')) return;
493
+
494
+ const element = e.target;
495
+
496
+ // Flush input before Enter/Tab (these typically submit or move to next field)
497
+ if (e.key === 'Enter' || e.key === 'Tab') {
498
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
499
+ flushInputValue(element);
500
+ }
501
+ }
502
+
503
+ const specialKeys = ['Enter', 'Escape', 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace', 'Delete'];
504
+
505
+ if (specialKeys.includes(e.key) || e.ctrlKey || e.metaKey) {
506
+ const modifiers = [];
507
+ if (e.ctrlKey) modifiers.push('Control');
508
+ if (e.shiftKey) modifiers.push('Shift');
509
+ if (e.altKey) modifiers.push('Alt');
510
+ if (e.metaKey) modifiers.push('Meta');
511
+
512
+ // Skip pure modifier keys
513
+ if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) return;
514
+
515
+ sendAction({
516
+ type: 'keypress',
517
+ selector: null,
518
+ timestamp: Date.now(),
519
+ data: {
520
+ key: e.key,
521
+ modifiers
522
+ }
523
+ });
524
+ }
525
+ }
526
+
527
+ function handleScroll(e) {
528
+ if (!isRecording || isPaused) return;
529
+
530
+ if (scrollDebounceTimer) clearTimeout(scrollDebounceTimer);
531
+
532
+ scrollDebounceTimer = setTimeout(() => {
533
+ const target = e.target === document ? document.documentElement : e.target;
534
+ if (target.nodeType !== 1) return;
535
+
536
+ const selector = generateSelector(target);
537
+
538
+ sendAction({
539
+ type: 'scroll',
540
+ selector,
541
+ timestamp: Date.now(),
542
+ data: {
543
+ scrollTop: target.scrollTop,
544
+ scrollLeft: target.scrollLeft
545
+ }
546
+ });
547
+ }, 1000);
548
+ }
549
+
550
+ // ============================================
551
+ // Helpers
552
+ // ============================================
553
+
554
+ function findClickableTarget(element) {
555
+ let current = element;
556
+ const maxDepth = 5;
557
+ let depth = 0;
558
+
559
+ while (current && current !== document.body && depth < maxDepth) {
560
+ const isInteractive =
561
+ current.tagName === 'A' ||
562
+ current.tagName === 'BUTTON' ||
563
+ current.getAttribute('role') === 'button' ||
564
+ current.getAttribute('role') === 'link' ||
565
+ current.hasAttribute('onclick') ||
566
+ current.onclick !== null ||
567
+ getComputedStyle(current).cursor === 'pointer';
568
+
569
+ if (isInteractive) {
570
+ return current;
571
+ }
572
+
573
+ current = current.parentElement;
574
+ depth++;
575
+ }
576
+
577
+ return element;
578
+ }
579
+
580
+ function isElementVisible(element) {
581
+ if (!element) return false;
582
+ if (element.offsetWidth === 0 && element.offsetHeight === 0) return false;
583
+
584
+ const style = getComputedStyle(element);
585
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
586
+ return false;
587
+ }
588
+
589
+ return true;
590
+ }
591
+
592
+ function highlightElement(element) {
593
+ element.classList.add('chrometools-highlight');
594
+ setTimeout(() => {
595
+ element.classList.remove('chrometools-highlight');
596
+ }, 500);
597
+ }
598
+
599
+ function sendAction(action) {
600
+ actionCount++;
601
+ updateOverlay();
602
+
603
+ chrome.runtime.sendMessage({
604
+ type: 'ACTION',
605
+ action
606
+ });
607
+ }
608
+
609
+ // ============================================
610
+ // Event Listener Management
611
+ // ============================================
612
+
613
+ function attachEventListeners() {
614
+ document.addEventListener('click', handleClick, true);
615
+ document.addEventListener('input', handleInput, true);
616
+ document.addEventListener('change', handleChange, true);
617
+ document.addEventListener('keydown', handleKeyDown, true);
618
+ document.addEventListener('blur', handleBlur, true);
619
+ document.addEventListener('scroll', handleScroll, true);
620
+ }
621
+
622
+ function detachEventListeners() {
623
+ document.removeEventListener('click', handleClick, true);
624
+ document.removeEventListener('input', handleInput, true);
625
+ document.removeEventListener('change', handleChange, true);
626
+ document.removeEventListener('keydown', handleKeyDown, true);
627
+ document.removeEventListener('blur', handleBlur, true);
628
+ document.removeEventListener('scroll', handleScroll, true);
629
+ }
630
+
631
+ // ============================================
632
+ // Message Handling
633
+ // ============================================
634
+
635
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
636
+ switch (message.type) {
637
+ case 'RECORDING_STARTED':
638
+ isRecording = true;
639
+ isPaused = false;
640
+ // Use provided actionCount if available (e.g., when switching tabs during recording)
641
+ actionCount = message.actionCount || actionCount || 0;
642
+ createOverlay();
643
+ attachEventListeners();
644
+ sendResponse({ success: true });
645
+ break;
646
+
647
+ case 'RECORDING_STOPPED':
648
+ // Flush any pending input values before stopping
649
+ flushAllPendingInputs();
650
+ isRecording = false;
651
+ isPaused = false;
652
+ removeOverlay();
653
+ detachEventListeners();
654
+ sendResponse({ success: true });
655
+ break;
656
+
657
+ case 'RECORDING_PAUSED':
658
+ isPaused = true;
659
+ updateOverlay();
660
+ sendResponse({ success: true });
661
+ break;
662
+
663
+ case 'RECORDING_RESUMED':
664
+ isPaused = false;
665
+ updateOverlay();
666
+ sendResponse({ success: true });
667
+ break;
668
+
669
+ case 'GET_RECORDING_STATE':
670
+ sendResponse({
671
+ isRecording,
672
+ isPaused,
673
+ actionCount
674
+ });
675
+ break;
676
+
677
+ default:
678
+ sendResponse({ error: 'Unknown message type' });
679
+ }
680
+ return false;
681
+ });
682
+
683
+ // ============================================
684
+ // Initialization
685
+ // ============================================
686
+
687
+ async function initialize() {
688
+ // Check if recording is already in progress
689
+ try {
690
+ const response = await chrome.runtime.sendMessage({ type: 'GET_RECORDING_STATE' });
691
+ if (response && response.isRecording) {
692
+ isRecording = true;
693
+ isPaused = response.isPaused;
694
+ actionCount = response.actionCount || 0;
695
+
696
+ // Wait for DOM to be ready
697
+ if (document.readyState === 'loading') {
698
+ document.addEventListener('DOMContentLoaded', () => {
699
+ createOverlay();
700
+ attachEventListeners();
701
+ });
702
+ } else {
703
+ createOverlay();
704
+ attachEventListeners();
705
+ }
706
+ }
707
+ } catch (error) {
708
+ // Extension context might not be ready, that's ok
709
+ console.log('[ChromeTools] Content script initialized (no active recording)');
710
+ }
711
+ }
712
+
713
+ initialize();
714
+
715
+ })();