kiro-mobile-bridge 1.0.7 → 1.0.10
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/README.md +16 -24
- package/package.json +1 -1
- package/src/public/index.html +1414 -1628
- package/src/routes/api.js +539 -0
- package/src/server.js +287 -2593
- package/src/services/cdp.js +210 -0
- package/src/services/click.js +533 -0
- package/src/services/message.js +214 -0
- package/src/services/snapshot.js +370 -0
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +34 -0
- package/src/utils/network.js +64 -0
- package/src/utils/security.js +160 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click service - handles UI element clicks via CDP
|
|
3
|
+
*/
|
|
4
|
+
import { MODEL_NAMES } from '../utils/constants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the click script for CDP evaluation
|
|
8
|
+
* This is separated for maintainability and testing
|
|
9
|
+
* @param {object} clickInfo - Element identification info (already sanitized)
|
|
10
|
+
* @returns {string} - JavaScript expression
|
|
11
|
+
*/
|
|
12
|
+
function buildClickScript(clickInfo) {
|
|
13
|
+
// Escape the clickInfo for safe inclusion in the script
|
|
14
|
+
const safeClickInfo = JSON.stringify(clickInfo);
|
|
15
|
+
const modelNamesJson = JSON.stringify(MODEL_NAMES);
|
|
16
|
+
|
|
17
|
+
return `(function() {
|
|
18
|
+
let targetDoc = document;
|
|
19
|
+
const activeFrame = document.getElementById('active-frame');
|
|
20
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
21
|
+
|
|
22
|
+
const info = ${safeClickInfo};
|
|
23
|
+
const modelNames = ${modelNamesJson};
|
|
24
|
+
let element = null;
|
|
25
|
+
let matchMethod = '';
|
|
26
|
+
|
|
27
|
+
// Helper functions
|
|
28
|
+
const isVisible = (el) => el && el.offsetParent !== null;
|
|
29
|
+
const findModelName = (text) => {
|
|
30
|
+
const lowerText = text.toLowerCase();
|
|
31
|
+
for (const m of modelNames) {
|
|
32
|
+
if (lowerText.includes(m)) return m;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Determine element type
|
|
38
|
+
const isTabClick = info.isTab || info.role === 'tab';
|
|
39
|
+
const isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
|
|
40
|
+
const isToggle = info.isToggle || info.role === 'switch';
|
|
41
|
+
const isModelSelector = info.isModelSelector;
|
|
42
|
+
const isModelOption = info.isModelOption;
|
|
43
|
+
|
|
44
|
+
// =========================================================================
|
|
45
|
+
// Model Selector Button Click
|
|
46
|
+
// =========================================================================
|
|
47
|
+
if (isModelSelector && !element) {
|
|
48
|
+
// Strategy 1: Find Kiro's specific dropdown trigger
|
|
49
|
+
const kiroDropdownTrigger = targetDoc.querySelector('button.kiro-dropdown-trigger[aria-haspopup="true"]');
|
|
50
|
+
if (kiroDropdownTrigger && isVisible(kiroDropdownTrigger)) {
|
|
51
|
+
const triggerText = (kiroDropdownTrigger.textContent || '').toLowerCase();
|
|
52
|
+
if (modelNames.some(m => triggerText.includes(m))) {
|
|
53
|
+
element = kiroDropdownTrigger;
|
|
54
|
+
matchMethod = 'kiro-dropdown-trigger';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Strategy 2: Find by aria-haspopup with model text
|
|
59
|
+
if (!element) {
|
|
60
|
+
const hasPopupButtons = targetDoc.querySelectorAll('button[aria-haspopup="true"], button[aria-haspopup="listbox"], button[aria-haspopup="menu"]');
|
|
61
|
+
for (const btn of hasPopupButtons) {
|
|
62
|
+
if (!isVisible(btn)) continue;
|
|
63
|
+
const btnText = (btn.textContent || '').toLowerCase();
|
|
64
|
+
if (modelNames.some(m => btnText.includes(m))) {
|
|
65
|
+
element = btn;
|
|
66
|
+
matchMethod = 'aria-haspopup-model';
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Strategy 3: Find by class patterns
|
|
73
|
+
if (!element) {
|
|
74
|
+
const modelSelectors = [
|
|
75
|
+
'.kiro-dropdown-trigger',
|
|
76
|
+
'[class*="model-selector"]', '[class*="modelSelector"]',
|
|
77
|
+
'[class*="model-dropdown"]', '[class*="modelDropdown"]',
|
|
78
|
+
'button[class*="dropdown-trigger"]'
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const sel of modelSelectors) {
|
|
82
|
+
try {
|
|
83
|
+
const candidates = targetDoc.querySelectorAll(sel);
|
|
84
|
+
for (const c of candidates) {
|
|
85
|
+
if (isVisible(c)) {
|
|
86
|
+
element = c;
|
|
87
|
+
matchMethod = 'model-selector-class';
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (element) break;
|
|
92
|
+
} catch(e) {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =========================================================================
|
|
98
|
+
// Model Option Selection (from dropdown menu)
|
|
99
|
+
// =========================================================================
|
|
100
|
+
if (isModelOption && !element) {
|
|
101
|
+
const searchText = (info.text || '').trim().toLowerCase();
|
|
102
|
+
const searchModelName = findModelName(searchText);
|
|
103
|
+
|
|
104
|
+
// Check if dropdown is currently open
|
|
105
|
+
const openDropdown = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"][class*="open"], [role="listbox"], [role="menu"]');
|
|
106
|
+
const isDropdownOpen = openDropdown && isVisible(openDropdown);
|
|
107
|
+
|
|
108
|
+
// If dropdown is NOT open, we need to open it first
|
|
109
|
+
if (!isDropdownOpen) {
|
|
110
|
+
const dropdownTrigger = targetDoc.querySelector('.kiro-dropdown-trigger[aria-haspopup="true"], button[aria-haspopup="true"], button[aria-haspopup="listbox"]');
|
|
111
|
+
if (dropdownTrigger && isVisible(dropdownTrigger)) {
|
|
112
|
+
const triggerText = (dropdownTrigger.textContent || '').toLowerCase();
|
|
113
|
+
if (modelNames.some(m => triggerText.includes(m))) {
|
|
114
|
+
dropdownTrigger.click();
|
|
115
|
+
return { found: true, clicked: true, needsRetry: true, matchMethod: 'dropdown-opened-for-option' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Strategy 1: Find Kiro's specific dropdown items
|
|
121
|
+
const kiroDropdownItems = targetDoc.querySelectorAll('.kiro-dropdown-item, .kiro-dropdown-menu > div');
|
|
122
|
+
for (const item of kiroDropdownItems) {
|
|
123
|
+
if (!isVisible(item)) continue;
|
|
124
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
125
|
+
const itemModelName = findModelName(itemText);
|
|
126
|
+
|
|
127
|
+
if (searchModelName && itemModelName && searchModelName === itemModelName) {
|
|
128
|
+
const searchVersion = searchText.match(/\\d+\\.?\\d*/)?.[0];
|
|
129
|
+
const itemVersion = itemText.match(/\\d+\\.?\\d*/)?.[0];
|
|
130
|
+
|
|
131
|
+
if (!searchVersion || !itemVersion || searchVersion === itemVersion) {
|
|
132
|
+
element = item;
|
|
133
|
+
matchMethod = 'kiro-dropdown-item';
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Strategy 2: Find by role selectors
|
|
140
|
+
if (!element) {
|
|
141
|
+
const optionSelectors = ['[role="option"]', '[role="menuitem"]', '[class*="dropdown-item"]', '[class*="menu-item"]'];
|
|
142
|
+
|
|
143
|
+
for (const sel of optionSelectors) {
|
|
144
|
+
try {
|
|
145
|
+
const options = targetDoc.querySelectorAll(sel);
|
|
146
|
+
for (const opt of options) {
|
|
147
|
+
if (!isVisible(opt)) continue;
|
|
148
|
+
const optText = (opt.textContent || '').trim().toLowerCase();
|
|
149
|
+
if (searchText && (optText.includes(searchText) || searchText.includes(optText.substring(0, 20)))) {
|
|
150
|
+
element = opt;
|
|
151
|
+
matchMethod = 'model-option-role';
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (element) break;
|
|
156
|
+
} catch(e) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strategy 3: Find any clickable element in dropdown with matching model name
|
|
161
|
+
if (!element && searchText && searchModelName) {
|
|
162
|
+
const dropdownMenu = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"], [role="listbox"], [role="menu"]');
|
|
163
|
+
if (dropdownMenu) {
|
|
164
|
+
const allItems = dropdownMenu.querySelectorAll('*');
|
|
165
|
+
for (const item of allItems) {
|
|
166
|
+
if (item.children.length > 5) continue;
|
|
167
|
+
if (!isVisible(item)) continue;
|
|
168
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
169
|
+
const itemModelName = findModelName(itemText);
|
|
170
|
+
|
|
171
|
+
if (itemModelName && searchModelName === itemModelName) {
|
|
172
|
+
element = item;
|
|
173
|
+
matchMethod = 'dropdown-menu-item';
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// Send Button
|
|
183
|
+
// =========================================================================
|
|
184
|
+
if (info.isSendButton && !element) {
|
|
185
|
+
const sendSelectors = ['button[data-variant="submit"]', 'svg.lucide-arrow-right', 'button[type="submit"]', 'button[aria-label*="send" i]'];
|
|
186
|
+
for (const sel of sendSelectors) {
|
|
187
|
+
try {
|
|
188
|
+
const el = targetDoc.querySelector(sel);
|
|
189
|
+
if (el) {
|
|
190
|
+
element = el.closest('button') || el;
|
|
191
|
+
if (element && !element.disabled) { matchMethod = 'send-button'; break; }
|
|
192
|
+
}
|
|
193
|
+
} catch(e) {}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// Toggle/Switch
|
|
199
|
+
// =========================================================================
|
|
200
|
+
if (isToggle && !element) {
|
|
201
|
+
if (info.toggleId) {
|
|
202
|
+
element = targetDoc.getElementById(info.toggleId);
|
|
203
|
+
if (element) matchMethod = 'toggle-id';
|
|
204
|
+
}
|
|
205
|
+
if (!element && info.text) {
|
|
206
|
+
const toggles = targetDoc.querySelectorAll('.kiro-toggle-switch, [role="switch"]');
|
|
207
|
+
for (const t of toggles) {
|
|
208
|
+
const label = t.querySelector('label');
|
|
209
|
+
if (label && label.textContent.trim().toLowerCase().includes(info.text.toLowerCase())) {
|
|
210
|
+
element = t.querySelector('input') || t;
|
|
211
|
+
matchMethod = 'toggle-label';
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// Notification Banner Buttons
|
|
220
|
+
// =========================================================================
|
|
221
|
+
if (info.isNotificationButton && !element) {
|
|
222
|
+
const searchText = (info.text || '').trim().toLowerCase();
|
|
223
|
+
const searchAriaLabel = (info.ariaLabel || '').trim().toLowerCase();
|
|
224
|
+
|
|
225
|
+
const snackbarButtons = targetDoc.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button, .kiro-snackbar-header button');
|
|
226
|
+
|
|
227
|
+
// Strategy 1: Find by text
|
|
228
|
+
for (const btn of snackbarButtons) {
|
|
229
|
+
if (!isVisible(btn)) continue;
|
|
230
|
+
const btnText = (btn.textContent || '').trim().toLowerCase();
|
|
231
|
+
if (searchText && btnText && (btnText.includes(searchText) || searchText.includes(btnText))) {
|
|
232
|
+
element = btn;
|
|
233
|
+
matchMethod = 'snackbar-button-text';
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Strategy 2: Find X/close/dismiss icon button
|
|
239
|
+
if (!element && (searchText === 'close' || searchText === 'dismiss' || searchText === 'x' ||
|
|
240
|
+
searchText === 'icon-button' || searchText === '' || !searchText ||
|
|
241
|
+
searchAriaLabel.includes('close') || searchAriaLabel.includes('dismiss'))) {
|
|
242
|
+
for (const btn of snackbarButtons) {
|
|
243
|
+
if (!isVisible(btn)) continue;
|
|
244
|
+
const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
245
|
+
const isExpandBtn = btnAriaLabel.includes('expand') || btn.classList.contains('kiro-snackbar-expand');
|
|
246
|
+
|
|
247
|
+
if (isExpandBtn) continue;
|
|
248
|
+
|
|
249
|
+
if (btnAriaLabel.includes('close') || btnAriaLabel.includes('dismiss') ||
|
|
250
|
+
btn.querySelector('.codicon-close, .codicon-x, [class*="close"]')) {
|
|
251
|
+
element = btn;
|
|
252
|
+
matchMethod = 'snackbar-dismiss-button';
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isIconBtn = btn.classList.contains('kiro-icon-button');
|
|
257
|
+
const btnText = (btn.textContent || '').trim();
|
|
258
|
+
if (isIconBtn && !btnText && !isExpandBtn) {
|
|
259
|
+
element = btn;
|
|
260
|
+
matchMethod = 'snackbar-icon-dismiss';
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Strategy 3: Find by aria-label
|
|
267
|
+
if (!element && searchAriaLabel && !searchAriaLabel.includes('expand')) {
|
|
268
|
+
for (const btn of snackbarButtons) {
|
|
269
|
+
if (!isVisible(btn)) continue;
|
|
270
|
+
const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
271
|
+
if (btnAriaLabel.includes('expand')) continue;
|
|
272
|
+
if (btnAriaLabel && (btnAriaLabel.includes(searchAriaLabel) || searchAriaLabel.includes(btnAriaLabel))) {
|
|
273
|
+
element = btn;
|
|
274
|
+
matchMethod = 'snackbar-button-aria';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Strategy 4: Find in agent-outcome notifications
|
|
281
|
+
if (!element) {
|
|
282
|
+
const outcomeButtons = targetDoc.querySelectorAll('.agent-outcome button, .agent-outcome-notification button, [class*="outcome"] button');
|
|
283
|
+
for (const btn of outcomeButtons) {
|
|
284
|
+
if (!isVisible(btn)) continue;
|
|
285
|
+
const btnText = (btn.textContent || '').trim().toLowerCase();
|
|
286
|
+
if (searchText && (btnText.includes(searchText) || searchText.includes(btnText))) {
|
|
287
|
+
element = btn;
|
|
288
|
+
matchMethod = 'outcome-button';
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Strategy 5: Find expand/collapse arrow (only if explicitly requested)
|
|
295
|
+
if (!element && (searchText.includes('expand') || searchAriaLabel.includes('expand'))) {
|
|
296
|
+
const expandArrow = targetDoc.querySelector('.kiro-snackbar [class*="expand"], .kiro-snackbar [class*="arrow"], .kiro-snackbar [class*="chevron"], .kiro-snackbar button[aria-label*="expand" i]');
|
|
297
|
+
if (expandArrow && isVisible(expandArrow)) {
|
|
298
|
+
element = expandArrow;
|
|
299
|
+
matchMethod = 'snackbar-expand';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =========================================================================
|
|
305
|
+
// Close Button
|
|
306
|
+
// =========================================================================
|
|
307
|
+
if (isCloseButton && !element) {
|
|
308
|
+
const closeButtons = targetDoc.querySelectorAll('[aria-label="close"], .kiro-tabs-item-close, [class*="close"]');
|
|
309
|
+
if (info.parentTabLabel) {
|
|
310
|
+
const searchLabel = info.parentTabLabel.trim().toLowerCase();
|
|
311
|
+
for (const btn of closeButtons) {
|
|
312
|
+
const parentTab = btn.closest('[role="tab"]');
|
|
313
|
+
if (parentTab) {
|
|
314
|
+
const labelEl = parentTab.querySelector('.kiro-tabs-item-label, [class*="label"]');
|
|
315
|
+
const tabLabel = labelEl ? labelEl.textContent.trim().toLowerCase() : '';
|
|
316
|
+
if (tabLabel.includes(searchLabel) || searchLabel.includes(tabLabel)) {
|
|
317
|
+
element = btn;
|
|
318
|
+
matchMethod = 'close-button-by-tab';
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!element && closeButtons.length > 0) {
|
|
325
|
+
for (const btn of closeButtons) {
|
|
326
|
+
const parentTab = btn.closest('[role="tab"]');
|
|
327
|
+
if (parentTab && parentTab.getAttribute('aria-selected') === 'true') {
|
|
328
|
+
element = btn;
|
|
329
|
+
matchMethod = 'close-button-selected';
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// =========================================================================
|
|
337
|
+
// Tab Click
|
|
338
|
+
// =========================================================================
|
|
339
|
+
if (isTabClick && !element) {
|
|
340
|
+
const allTabs = targetDoc.querySelectorAll('[role="tab"]');
|
|
341
|
+
const searchText = (info.tabLabel || info.text || '').trim().toLowerCase();
|
|
342
|
+
for (const tab of allTabs) {
|
|
343
|
+
const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
|
|
344
|
+
const tabText = labelEl ? labelEl.textContent.trim().toLowerCase() : tab.textContent.trim().toLowerCase();
|
|
345
|
+
if (searchText && (tabText.includes(searchText) || searchText.includes(tabText))) {
|
|
346
|
+
element = tab;
|
|
347
|
+
matchMethod = 'tab-label';
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// =========================================================================
|
|
354
|
+
// File Link
|
|
355
|
+
// =========================================================================
|
|
356
|
+
if (info.isFileLink && info.filePath && !element) {
|
|
357
|
+
const fileName = info.filePath.split('/').pop().split('\\\\').pop();
|
|
358
|
+
const fileSelectors = ['a[href*="' + fileName + '"]', '[data-path*="' + fileName + '"]', 'code', 'span', '[class*="file"]'];
|
|
359
|
+
for (const selector of fileSelectors) {
|
|
360
|
+
try {
|
|
361
|
+
const candidates = targetDoc.querySelectorAll(selector);
|
|
362
|
+
for (const el of candidates) {
|
|
363
|
+
const text = (el.textContent || '').trim();
|
|
364
|
+
if (text.includes(info.filePath) || text.includes(fileName)) {
|
|
365
|
+
element = el;
|
|
366
|
+
matchMethod = 'file-link';
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (element) break;
|
|
371
|
+
} catch(e) {}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =========================================================================
|
|
376
|
+
// Aria Label Match
|
|
377
|
+
// =========================================================================
|
|
378
|
+
if (info.ariaLabel && !element && !isCloseButton) {
|
|
379
|
+
try {
|
|
380
|
+
const escapedLabel = info.ariaLabel.replace(/"/g, '\\\\"');
|
|
381
|
+
const candidates = targetDoc.querySelectorAll('[aria-label="' + escapedLabel + '"]');
|
|
382
|
+
for (const c of candidates) {
|
|
383
|
+
const label = (c.getAttribute('aria-label') || '').toLowerCase();
|
|
384
|
+
if (!label.includes('close')) { element = c; matchMethod = 'aria-label'; break; }
|
|
385
|
+
}
|
|
386
|
+
} catch(e) {}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =========================================================================
|
|
390
|
+
// History/Session List Items
|
|
391
|
+
// =========================================================================
|
|
392
|
+
if (info.isHistoryItem && !element) {
|
|
393
|
+
const searchText = (info.text || '').trim().toLowerCase();
|
|
394
|
+
const datePattern = /\\d{1,2}\\/\\d{1,2}\\/\\d{4}|\\d{1,2}:\\d{2}:\\d{2}/;
|
|
395
|
+
const allDivs = targetDoc.querySelectorAll('div, li, article');
|
|
396
|
+
const historyItems = [];
|
|
397
|
+
|
|
398
|
+
for (const item of allDivs) {
|
|
399
|
+
if (item.children.length > 15) continue;
|
|
400
|
+
const text = item.textContent || '';
|
|
401
|
+
if (datePattern.test(text) && text.length > 20 && text.length < 500) {
|
|
402
|
+
historyItems.push(item);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const item of historyItems) {
|
|
407
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
408
|
+
if (searchText && (itemText.includes(searchText) || searchText.includes(itemText.substring(0, 50)))) {
|
|
409
|
+
element = item;
|
|
410
|
+
matchMethod = 'history-item-date';
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!element) {
|
|
416
|
+
const historySelectors = ['[role="listitem"]', '[role="option"]', '[class*="history"] > *', '[class*="session"] > *'];
|
|
417
|
+
for (const selector of historySelectors) {
|
|
418
|
+
try {
|
|
419
|
+
const items = targetDoc.querySelectorAll(selector);
|
|
420
|
+
for (const item of items) {
|
|
421
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
422
|
+
if (searchText && (itemText.includes(searchText) || searchText.includes(itemText.substring(0, 50)))) {
|
|
423
|
+
element = item;
|
|
424
|
+
matchMethod = 'history-item-selector';
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (element) break;
|
|
429
|
+
} catch(e) {}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// =========================================================================
|
|
435
|
+
// Text Content Match (fallback)
|
|
436
|
+
// =========================================================================
|
|
437
|
+
if (info.text && info.text.trim() && !element) {
|
|
438
|
+
const searchText = info.text.trim();
|
|
439
|
+
const allElements = targetDoc.querySelectorAll('button, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="listitem"], a, [tabindex="0"], [class*="cursor-pointer"]');
|
|
440
|
+
for (const el of allElements) {
|
|
441
|
+
if (!isCloseButton) {
|
|
442
|
+
const ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
443
|
+
if (ariaLabel.includes('close')) continue;
|
|
444
|
+
}
|
|
445
|
+
const elText = (el.textContent || '').trim();
|
|
446
|
+
if (elText === searchText || elText.includes(searchText) || (elText.length >= 10 && searchText.includes(elText))) {
|
|
447
|
+
element = el;
|
|
448
|
+
matchMethod = 'text-content';
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =========================================================================
|
|
455
|
+
// Execute Click
|
|
456
|
+
// =========================================================================
|
|
457
|
+
if (!element) return { found: false, error: 'Element not found' };
|
|
458
|
+
|
|
459
|
+
const elementInfo = {
|
|
460
|
+
tag: element.tagName,
|
|
461
|
+
className: element.className?.substring?.(0, 100) || '',
|
|
462
|
+
role: element.getAttribute('role'),
|
|
463
|
+
ariaHaspopup: element.getAttribute('aria-haspopup'),
|
|
464
|
+
ariaExpanded: element.getAttribute('aria-expanded'),
|
|
465
|
+
dataState: element.getAttribute('data-state'),
|
|
466
|
+
textContent: (element.textContent || '').substring(0, 50)
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
element.click();
|
|
471
|
+
return { found: true, clicked: true, matchMethod, elementInfo };
|
|
472
|
+
} catch (e) {
|
|
473
|
+
try {
|
|
474
|
+
const rect = element.getBoundingClientRect();
|
|
475
|
+
const centerX = rect.left + rect.width / 2;
|
|
476
|
+
const centerY = rect.top + rect.height / 2;
|
|
477
|
+
|
|
478
|
+
const mouseOpts = {
|
|
479
|
+
bubbles: true,
|
|
480
|
+
cancelable: true,
|
|
481
|
+
view: window,
|
|
482
|
+
clientX: centerX,
|
|
483
|
+
clientY: centerY
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
element.dispatchEvent(new MouseEvent('mousedown', mouseOpts));
|
|
487
|
+
element.dispatchEvent(new MouseEvent('mouseup', mouseOpts));
|
|
488
|
+
element.dispatchEvent(new MouseEvent('click', mouseOpts));
|
|
489
|
+
|
|
490
|
+
return { found: true, clicked: true, matchMethod: matchMethod + '-dispatch', elementInfo };
|
|
491
|
+
} catch (e2) {
|
|
492
|
+
return { found: true, clicked: false, error: 'Click failed: ' + e2.message };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
})()`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Click an element in the Kiro UI via CDP
|
|
500
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
501
|
+
* @param {object} clickInfo - Element identification info (should be sanitized)
|
|
502
|
+
* @returns {Promise<{success: boolean, matchMethod?: string, error?: string}>}
|
|
503
|
+
*/
|
|
504
|
+
export async function clickElement(cdp, clickInfo) {
|
|
505
|
+
const script = buildClickScript(clickInfo);
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
509
|
+
expression: script,
|
|
510
|
+
contextId: cdp.rootContextId,
|
|
511
|
+
returnByValue: true
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const elementInfo = result.result?.value;
|
|
515
|
+
|
|
516
|
+
if (!elementInfo?.found) {
|
|
517
|
+
console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text || 'unknown');
|
|
518
|
+
return { success: false, error: 'Element not found' };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (elementInfo.clicked) {
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
matchMethod: elementInfo.matchMethod,
|
|
525
|
+
needsRetry: elementInfo.needsRetry
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return { success: false, error: elementInfo.error || 'Click failed' };
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.error('[Click] CDP error:', err.message);
|
|
531
|
+
return { success: false, error: err.message };
|
|
532
|
+
}
|
|
533
|
+
}
|