chrometools-mcp 2.4.2 → 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.
- package/CHANGELOG.md +540 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +494 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/element-finder-utils.js +138 -28
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/figma-tools.js +120 -0
- package/index.js +3347 -2518
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -656
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +367 -296
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ChromeTools MCP</title>
|
|
7
|
+
<link rel="stylesheet" href="popup.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="popup-container">
|
|
11
|
+
<!-- Header -->
|
|
12
|
+
<header class="header">
|
|
13
|
+
<div class="logo">
|
|
14
|
+
<span class="logo-icon">CT</span>
|
|
15
|
+
<span class="logo-text">ChromeTools</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="connection-status" id="connection-status">
|
|
18
|
+
<span class="status-dot"></span>
|
|
19
|
+
<span class="status-text">Disconnected</span>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<!-- Tabs -->
|
|
24
|
+
<nav class="tabs">
|
|
25
|
+
<button class="tab active" data-tab="recorder">Recorder</button>
|
|
26
|
+
<button class="tab" data-tab="tabs">Tabs</button>
|
|
27
|
+
</nav>
|
|
28
|
+
|
|
29
|
+
<!-- Recorder Tab -->
|
|
30
|
+
<section class="tab-content active" id="tab-recorder">
|
|
31
|
+
<!-- Recording Status -->
|
|
32
|
+
<div class="recording-status" id="recording-status">
|
|
33
|
+
<span class="recording-dot"></span>
|
|
34
|
+
<span class="recording-text">Not recording</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Metadata Form (shown when not recording) -->
|
|
38
|
+
<div class="metadata-form" id="metadata-form">
|
|
39
|
+
<div class="form-group">
|
|
40
|
+
<label for="scenario-name">Scenario Name *</label>
|
|
41
|
+
<input type="text" id="scenario-name" placeholder="e.g., login-flow" required>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<label for="scenario-desc">Description</label>
|
|
45
|
+
<input type="text" id="scenario-desc" placeholder="Optional description">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label for="scenario-tags">Tags</label>
|
|
49
|
+
<input type="text" id="scenario-tags" placeholder="auth, critical, smoke">
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Action Count (shown when recording) -->
|
|
54
|
+
<div class="action-count-display hidden" id="action-count-display">
|
|
55
|
+
<span class="count-number" id="action-count">0</span>
|
|
56
|
+
<span class="count-label">actions recorded</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Controls -->
|
|
60
|
+
<div class="controls">
|
|
61
|
+
<button class="btn btn-primary" id="btn-start">
|
|
62
|
+
<span class="btn-icon">●</span> Start Recording
|
|
63
|
+
</button>
|
|
64
|
+
<button class="btn btn-secondary hidden" id="btn-pause">
|
|
65
|
+
<span class="btn-icon">❚❚</span> Pause
|
|
66
|
+
</button>
|
|
67
|
+
<button class="btn btn-danger hidden" id="btn-stop">
|
|
68
|
+
<span class="btn-icon">■</span> Stop & Save
|
|
69
|
+
</button>
|
|
70
|
+
<button class="btn btn-ghost hidden" id="btn-clear">
|
|
71
|
+
Clear
|
|
72
|
+
</button>
|
|
73
|
+
<button class="btn btn-reset hidden" id="btn-reset">
|
|
74
|
+
✖ Reset
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Actions Preview -->
|
|
79
|
+
<div class="actions-preview hidden" id="actions-preview">
|
|
80
|
+
<h4>Recent Actions</h4>
|
|
81
|
+
<ul class="actions-list" id="actions-list">
|
|
82
|
+
<!-- Actions will be populated here -->
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<!-- Tabs Tab -->
|
|
88
|
+
<section class="tab-content" id="tab-tabs">
|
|
89
|
+
<div class="tabs-list" id="tabs-list">
|
|
90
|
+
<div class="tabs-loading">Loading tabs...</div>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<!-- Footer -->
|
|
95
|
+
<footer class="footer">
|
|
96
|
+
<span class="version">v1.0.0</span>
|
|
97
|
+
</footer>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script src="popup.js"></script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChromeTools MCP Extension - Popup Script
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// DOM Elements
|
|
6
|
+
const elements = {
|
|
7
|
+
// Status
|
|
8
|
+
connectionStatus: document.getElementById('connection-status'),
|
|
9
|
+
recordingStatus: document.getElementById('recording-status'),
|
|
10
|
+
|
|
11
|
+
// Tabs navigation
|
|
12
|
+
tabButtons: document.querySelectorAll('.tab'),
|
|
13
|
+
tabContents: document.querySelectorAll('.tab-content'),
|
|
14
|
+
|
|
15
|
+
// Recorder
|
|
16
|
+
metadataForm: document.getElementById('metadata-form'),
|
|
17
|
+
actionCountDisplay: document.getElementById('action-count-display'),
|
|
18
|
+
actionCount: document.getElementById('action-count'),
|
|
19
|
+
actionsPreview: document.getElementById('actions-preview'),
|
|
20
|
+
actionsList: document.getElementById('actions-list'),
|
|
21
|
+
|
|
22
|
+
// Form fields
|
|
23
|
+
scenarioName: document.getElementById('scenario-name'),
|
|
24
|
+
scenarioDesc: document.getElementById('scenario-desc'),
|
|
25
|
+
scenarioTags: document.getElementById('scenario-tags'),
|
|
26
|
+
|
|
27
|
+
// Buttons
|
|
28
|
+
btnStart: document.getElementById('btn-start'),
|
|
29
|
+
btnPause: document.getElementById('btn-pause'),
|
|
30
|
+
btnStop: document.getElementById('btn-stop'),
|
|
31
|
+
btnClear: document.getElementById('btn-clear'),
|
|
32
|
+
btnReset: document.getElementById('btn-reset'),
|
|
33
|
+
|
|
34
|
+
// Tabs list
|
|
35
|
+
tabsList: document.getElementById('tabs-list')
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
let currentState = {
|
|
40
|
+
isRecording: false,
|
|
41
|
+
isPaused: false,
|
|
42
|
+
actions: [],
|
|
43
|
+
isConnected: false,
|
|
44
|
+
scenarioName: '',
|
|
45
|
+
scenarioDescription: '',
|
|
46
|
+
scenarioTags: []
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// Tab Navigation
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
elements.tabButtons.forEach(button => {
|
|
54
|
+
button.addEventListener('click', () => {
|
|
55
|
+
const tabName = button.dataset.tab;
|
|
56
|
+
|
|
57
|
+
// Update buttons
|
|
58
|
+
elements.tabButtons.forEach(b => b.classList.remove('active'));
|
|
59
|
+
button.classList.add('active');
|
|
60
|
+
|
|
61
|
+
// Update content
|
|
62
|
+
elements.tabContents.forEach(content => {
|
|
63
|
+
content.classList.remove('active');
|
|
64
|
+
if (content.id === `tab-${tabName}`) {
|
|
65
|
+
content.classList.add('active');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Load tabs list if switching to tabs tab
|
|
70
|
+
if (tabName === 'tabs') {
|
|
71
|
+
loadTabsList();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// State Management
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
async function loadState() {
|
|
81
|
+
try {
|
|
82
|
+
const response = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
|
|
83
|
+
currentState = {
|
|
84
|
+
isRecording: response.isRecording || false,
|
|
85
|
+
isPaused: response.isPaused || false,
|
|
86
|
+
actions: response.actions || [],
|
|
87
|
+
isConnected: response.isConnected || false,
|
|
88
|
+
scenarioName: response.scenarioName || '',
|
|
89
|
+
scenarioDescription: response.scenarioDescription || '',
|
|
90
|
+
scenarioTags: response.scenarioTags || []
|
|
91
|
+
};
|
|
92
|
+
updateUI();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to load state:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updateUI() {
|
|
99
|
+
// Connection status
|
|
100
|
+
if (currentState.isConnected) {
|
|
101
|
+
elements.connectionStatus.classList.add('connected');
|
|
102
|
+
elements.connectionStatus.querySelector('.status-text').textContent = 'Connected';
|
|
103
|
+
} else {
|
|
104
|
+
elements.connectionStatus.classList.remove('connected');
|
|
105
|
+
elements.connectionStatus.querySelector('.status-text').textContent = 'Disconnected';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Recording status
|
|
109
|
+
elements.recordingStatus.classList.remove('recording', 'paused');
|
|
110
|
+
if (currentState.isRecording) {
|
|
111
|
+
if (currentState.isPaused) {
|
|
112
|
+
elements.recordingStatus.classList.add('paused');
|
|
113
|
+
elements.recordingStatus.querySelector('.recording-text').textContent = 'Paused';
|
|
114
|
+
} else {
|
|
115
|
+
elements.recordingStatus.classList.add('recording');
|
|
116
|
+
elements.recordingStatus.querySelector('.recording-text').textContent = 'Recording...';
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
elements.recordingStatus.querySelector('.recording-text').textContent = 'Not recording';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Form vs Action Count
|
|
123
|
+
if (currentState.isRecording) {
|
|
124
|
+
elements.metadataForm.classList.add('hidden');
|
|
125
|
+
elements.actionCountDisplay.classList.remove('hidden');
|
|
126
|
+
elements.actionCount.textContent = currentState.actions.length;
|
|
127
|
+
|
|
128
|
+
// Show actions preview
|
|
129
|
+
if (currentState.actions.length > 0) {
|
|
130
|
+
elements.actionsPreview.classList.remove('hidden');
|
|
131
|
+
updateActionsList();
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
elements.metadataForm.classList.remove('hidden');
|
|
135
|
+
elements.actionCountDisplay.classList.add('hidden');
|
|
136
|
+
elements.actionsPreview.classList.add('hidden');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Buttons
|
|
140
|
+
if (currentState.isRecording) {
|
|
141
|
+
elements.btnStart.classList.add('hidden');
|
|
142
|
+
elements.btnPause.classList.remove('hidden');
|
|
143
|
+
elements.btnStop.classList.remove('hidden');
|
|
144
|
+
elements.btnClear.classList.remove('hidden');
|
|
145
|
+
elements.btnReset.classList.remove('hidden');
|
|
146
|
+
|
|
147
|
+
// Update pause button text
|
|
148
|
+
if (currentState.isPaused) {
|
|
149
|
+
elements.btnPause.innerHTML = '<span class="btn-icon">►</span> Resume';
|
|
150
|
+
} else {
|
|
151
|
+
elements.btnPause.innerHTML = '<span class="btn-icon">❚❚</span> Pause';
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
elements.btnStart.classList.remove('hidden');
|
|
155
|
+
elements.btnPause.classList.add('hidden');
|
|
156
|
+
elements.btnStop.classList.add('hidden');
|
|
157
|
+
elements.btnClear.classList.add('hidden');
|
|
158
|
+
elements.btnReset.classList.add('hidden');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Disable start if not connected or no name
|
|
162
|
+
elements.btnStart.disabled = !currentState.isConnected;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function updateActionsList() {
|
|
166
|
+
const recentActions = currentState.actions.slice(-5).reverse();
|
|
167
|
+
|
|
168
|
+
elements.actionsList.innerHTML = recentActions.map(action => {
|
|
169
|
+
let details = '';
|
|
170
|
+
switch (action.type) {
|
|
171
|
+
case 'click':
|
|
172
|
+
details = action.data?.text || action.selector?.primary || '';
|
|
173
|
+
break;
|
|
174
|
+
case 'type':
|
|
175
|
+
details = action.data?.isSecret ? '***' : (action.data?.text || '').substring(0, 20);
|
|
176
|
+
break;
|
|
177
|
+
case 'select':
|
|
178
|
+
details = action.data?.text || action.data?.value || '';
|
|
179
|
+
break;
|
|
180
|
+
case 'keypress':
|
|
181
|
+
details = (action.data?.modifiers || []).concat(action.data?.key || '').join('+');
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
details = action.selector?.primary || '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `
|
|
188
|
+
<li>
|
|
189
|
+
<span class="action-type">${action.type}</span>
|
|
190
|
+
<span class="action-details">${escapeHtml(details)}</span>
|
|
191
|
+
</li>
|
|
192
|
+
`;
|
|
193
|
+
}).join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function escapeHtml(text) {
|
|
197
|
+
const div = document.createElement('div');
|
|
198
|
+
div.textContent = text;
|
|
199
|
+
return div.innerHTML;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================
|
|
203
|
+
// Button Handlers
|
|
204
|
+
// ============================================
|
|
205
|
+
|
|
206
|
+
elements.btnStart.addEventListener('click', async () => {
|
|
207
|
+
const name = elements.scenarioName.value.trim();
|
|
208
|
+
const description = elements.scenarioDesc.value.trim();
|
|
209
|
+
const tags = elements.scenarioTags.value.split(',').map(t => t.trim()).filter(Boolean);
|
|
210
|
+
|
|
211
|
+
if (!name) {
|
|
212
|
+
elements.scenarioName.focus();
|
|
213
|
+
elements.scenarioName.style.borderColor = '#ef4444';
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
elements.scenarioName.style.borderColor = '';
|
|
216
|
+
}, 2000);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await chrome.runtime.sendMessage({
|
|
222
|
+
type: 'START_RECORDING',
|
|
223
|
+
options: {
|
|
224
|
+
name,
|
|
225
|
+
description,
|
|
226
|
+
tags
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Save metadata in state for later use
|
|
231
|
+
currentState.isRecording = true;
|
|
232
|
+
currentState.isPaused = false;
|
|
233
|
+
currentState.actions = [];
|
|
234
|
+
currentState.scenarioName = name;
|
|
235
|
+
currentState.scenarioDescription = description;
|
|
236
|
+
currentState.scenarioTags = tags;
|
|
237
|
+
updateUI();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Failed to start recording:', error);
|
|
240
|
+
alert('Failed to start recording: ' + error.message);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
elements.btnPause.addEventListener('click', async () => {
|
|
245
|
+
try {
|
|
246
|
+
const response = await chrome.runtime.sendMessage({ type: 'PAUSE_RECORDING' });
|
|
247
|
+
currentState.isPaused = response.isPaused;
|
|
248
|
+
updateUI();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Failed to pause recording:', error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
elements.btnStop.addEventListener('click', async () => {
|
|
255
|
+
// Use saved scenario name from state (form is hidden during recording)
|
|
256
|
+
const name = currentState.scenarioName || elements.scenarioName.value.trim();
|
|
257
|
+
|
|
258
|
+
if (!name) {
|
|
259
|
+
alert('Please enter a scenario name');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// First, stop recording to get actions and secrets
|
|
265
|
+
const stopResult = await chrome.runtime.sendMessage({ type: 'STOP_RECORDING' });
|
|
266
|
+
|
|
267
|
+
if (!stopResult.success) {
|
|
268
|
+
throw new Error('Failed to stop recording');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Then save the scenario with collected data
|
|
272
|
+
await chrome.runtime.sendMessage({
|
|
273
|
+
type: 'SAVE_SCENARIO',
|
|
274
|
+
scenario: {
|
|
275
|
+
name,
|
|
276
|
+
description: currentState.scenarioDescription || elements.scenarioDesc.value.trim(),
|
|
277
|
+
tags: currentState.scenarioTags.length > 0 ? currentState.scenarioTags : elements.scenarioTags.value.split(',').map(t => t.trim()).filter(Boolean),
|
|
278
|
+
actions: stopResult.actions || [],
|
|
279
|
+
secrets: stopResult.secrets || {},
|
|
280
|
+
metadata: stopResult.metadata || {}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Reset form and state
|
|
285
|
+
elements.scenarioName.value = '';
|
|
286
|
+
elements.scenarioDesc.value = '';
|
|
287
|
+
elements.scenarioTags.value = '';
|
|
288
|
+
|
|
289
|
+
currentState.isRecording = false;
|
|
290
|
+
currentState.isPaused = false;
|
|
291
|
+
currentState.actions = [];
|
|
292
|
+
currentState.scenarioName = '';
|
|
293
|
+
currentState.scenarioDescription = '';
|
|
294
|
+
currentState.scenarioTags = [];
|
|
295
|
+
updateUI();
|
|
296
|
+
|
|
297
|
+
// Show success message
|
|
298
|
+
alert('Scenario saved successfully!');
|
|
299
|
+
|
|
300
|
+
// Force reload state from background to ensure sync
|
|
301
|
+
await loadState();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('Failed to save scenario:', error);
|
|
304
|
+
alert('Failed to save scenario: ' + error.message);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
elements.btnClear.addEventListener('click', async () => {
|
|
309
|
+
if (!confirm('Clear all recorded actions?')) return;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
await chrome.runtime.sendMessage({ type: 'CLEAR_ACTIONS' });
|
|
313
|
+
currentState.actions = [];
|
|
314
|
+
updateUI();
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('Failed to clear actions:', error);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
elements.btnReset.addEventListener('click', async () => {
|
|
321
|
+
if (!confirm('Force reset recording? All recorded actions will be lost.')) return;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await chrome.runtime.sendMessage({ type: 'FORCE_RESET' });
|
|
325
|
+
|
|
326
|
+
// Reset local state
|
|
327
|
+
currentState.isRecording = false;
|
|
328
|
+
currentState.isPaused = false;
|
|
329
|
+
currentState.actions = [];
|
|
330
|
+
currentState.scenarioName = '';
|
|
331
|
+
currentState.scenarioDescription = '';
|
|
332
|
+
currentState.scenarioTags = [];
|
|
333
|
+
|
|
334
|
+
// Reset form
|
|
335
|
+
elements.scenarioName.value = '';
|
|
336
|
+
elements.scenarioDesc.value = '';
|
|
337
|
+
elements.scenarioTags.value = '';
|
|
338
|
+
|
|
339
|
+
updateUI();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('Failed to reset:', error);
|
|
342
|
+
alert('Failed to reset: ' + error.message);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ============================================
|
|
347
|
+
// Tabs List
|
|
348
|
+
// ============================================
|
|
349
|
+
|
|
350
|
+
async function loadTabsList() {
|
|
351
|
+
elements.tabsList.innerHTML = '<div class="tabs-loading">Loading tabs...</div>';
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const tabs = await chrome.tabs.query({});
|
|
355
|
+
|
|
356
|
+
if (tabs.length === 0) {
|
|
357
|
+
elements.tabsList.innerHTML = '<div class="tabs-loading">No tabs open</div>';
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
elements.tabsList.innerHTML = tabs.map(tab => `
|
|
362
|
+
<div class="tab-item ${tab.active ? 'active' : ''}" data-tab-id="${tab.id}">
|
|
363
|
+
<img class="tab-favicon" src="${tab.favIconUrl || ''}" alt="" onerror="this.style.display='none'">
|
|
364
|
+
<div class="tab-info">
|
|
365
|
+
<div class="tab-title">${escapeHtml(tab.title || 'Untitled')}</div>
|
|
366
|
+
<div class="tab-url">${escapeHtml(tab.url || '')}</div>
|
|
367
|
+
</div>
|
|
368
|
+
${tab.active ? '<span class="tab-active-badge">Active</span>' : ''}
|
|
369
|
+
</div>
|
|
370
|
+
`).join('');
|
|
371
|
+
|
|
372
|
+
// Add click handlers
|
|
373
|
+
elements.tabsList.querySelectorAll('.tab-item').forEach(item => {
|
|
374
|
+
item.addEventListener('click', () => {
|
|
375
|
+
const tabId = parseInt(item.dataset.tabId);
|
|
376
|
+
chrome.tabs.update(tabId, { active: true });
|
|
377
|
+
window.close();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Failed to load tabs:', error);
|
|
383
|
+
elements.tabsList.innerHTML = '<div class="tabs-loading">Failed to load tabs</div>';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================
|
|
388
|
+
// Message Listener
|
|
389
|
+
// ============================================
|
|
390
|
+
|
|
391
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
392
|
+
switch (message.type) {
|
|
393
|
+
case 'ACTION_RECORDED':
|
|
394
|
+
currentState.actions.push({}); // Just increment count
|
|
395
|
+
elements.actionCount.textContent = message.actionCount || currentState.actions.length;
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'SCENARIO_SAVED':
|
|
399
|
+
if (message.success) {
|
|
400
|
+
// Already handled in btnStop click
|
|
401
|
+
} else {
|
|
402
|
+
alert('Failed to save scenario: ' + (message.error || 'Unknown error'));
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ============================================
|
|
409
|
+
// Initialization
|
|
410
|
+
// ============================================
|
|
411
|
+
|
|
412
|
+
loadState();
|
|
413
|
+
|
|
414
|
+
// Refresh state periodically
|
|
415
|
+
setInterval(loadState, 2000);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* ChromeTools MCP - Recorder Overlay Styles */
|
|
2
|
+
|
|
3
|
+
#chrometools-recorder-overlay {
|
|
4
|
+
position: fixed;
|
|
5
|
+
top: 10px;
|
|
6
|
+
right: 10px;
|
|
7
|
+
padding: 8px 16px;
|
|
8
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
9
|
+
color: white;
|
|
10
|
+
border-radius: 20px;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
12
|
+
font-size: 12px;
|
|
13
|
+
font-weight: 500;
|
|
14
|
+
z-index: 2147483647;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
19
|
+
cursor: move;
|
|
20
|
+
user-select: none;
|
|
21
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#chrometools-recorder-overlay:hover {
|
|
25
|
+
transform: scale(1.02);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#chrometools-recorder-overlay.minimized {
|
|
29
|
+
width: 40px;
|
|
30
|
+
height: 40px;
|
|
31
|
+
padding: 0;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#chrometools-recorder-overlay.minimized .recorder-text {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#chrometools-recorder-overlay .recorder-dot {
|
|
41
|
+
width: 8px;
|
|
42
|
+
height: 8px;
|
|
43
|
+
background: white;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
animation: chrometools-pulse 1.5s ease-in-out infinite;
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@keyframes chrometools-pulse {
|
|
50
|
+
0%, 100% {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
transform: scale(1);
|
|
53
|
+
}
|
|
54
|
+
50% {
|
|
55
|
+
opacity: 0.5;
|
|
56
|
+
transform: scale(1.1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#chrometools-recorder-overlay .recorder-text {
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#chrometools-recorder-overlay .action-count {
|
|
65
|
+
background: rgba(255, 255, 255, 0.2);
|
|
66
|
+
padding: 2px 8px;
|
|
67
|
+
border-radius: 10px;
|
|
68
|
+
font-size: 11px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Highlight for recorded elements */
|
|
72
|
+
.chrometools-highlight {
|
|
73
|
+
outline: 2px solid #10b981 !important;
|
|
74
|
+
outline-offset: 2px !important;
|
|
75
|
+
background-color: rgba(16, 185, 129, 0.1) !important;
|
|
76
|
+
transition: all 0.2s ease !important;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Recording indicator on body */
|
|
80
|
+
body.chrometools-recording {
|
|
81
|
+
outline: 3px solid rgba(239, 68, 68, 0.5) !important;
|
|
82
|
+
outline-offset: -3px !important;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Paused state */
|
|
86
|
+
#chrometools-recorder-overlay.paused {
|
|
87
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#chrometools-recorder-overlay.paused .recorder-dot {
|
|
91
|
+
animation: none;
|
|
92
|
+
background: rgba(255, 255, 255, 0.6);
|
|
93
|
+
}
|