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
package/src/server.js
CHANGED
|
@@ -1,2646 +1,354 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Kiro Mobile Bridge Server
|
|
4
|
+
* @version 1.0.10
|
|
4
5
|
*
|
|
5
|
-
* A
|
|
6
|
+
* A mobile web interface for monitoring Kiro IDE agent sessions from your phone over LAN.
|
|
6
7
|
* Captures snapshots of the chat interface via CDP and lets you send messages remotely.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import express from 'express';
|
|
10
11
|
import { createServer } from 'http';
|
|
11
|
-
import http from 'http';
|
|
12
12
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
13
|
-
import { networkInterfaces } from 'os';
|
|
14
13
|
import { fileURLToPath } from 'url';
|
|
15
14
|
import { dirname, join } from 'path';
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
// Services
|
|
17
|
+
import { fetchCDPTargets, connectToCDP } from './services/cdp.js';
|
|
18
|
+
import { captureMetadata, captureCSS, captureSnapshot, captureEditor } from './services/snapshot.js';
|
|
19
|
+
|
|
20
|
+
// Utils
|
|
21
|
+
import { generateId, computeHash } from './utils/hash.js';
|
|
22
|
+
import { getLocalIP } from './utils/network.js';
|
|
23
|
+
import {
|
|
24
|
+
CDP_PORTS,
|
|
25
|
+
DISCOVERY_INTERVAL_ACTIVE,
|
|
26
|
+
DISCOVERY_INTERVAL_STABLE,
|
|
27
|
+
SNAPSHOT_INTERVAL_ACTIVE,
|
|
28
|
+
SNAPSHOT_INTERVAL_IDLE,
|
|
29
|
+
SNAPSHOT_IDLE_THRESHOLD
|
|
30
|
+
} from './utils/constants.js';
|
|
31
|
+
|
|
32
|
+
// Routes
|
|
33
|
+
import { createApiRouter } from './routes/api.js';
|
|
17
34
|
|
|
18
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
36
|
const __dirname = dirname(__filename);
|
|
20
37
|
|
|
38
|
+
// =============================================================================
|
|
21
39
|
// Configuration
|
|
22
|
-
|
|
23
|
-
const CDP_PORTS = [9000, 9001, 9002, 9003, 9222, 9229];
|
|
40
|
+
// =============================================================================
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
const cascades = new Map(); // cascadeId -> { id, cdp, metadata, snapshot, css, snapshotHash, sidebar, editor }
|
|
27
|
-
const mainWindowCDP = { connection: null, id: null }; // Separate CDP connection for main VS Code window
|
|
42
|
+
const PORT = process.env.PORT || 3000;
|
|
28
43
|
|
|
29
44
|
// =============================================================================
|
|
30
|
-
//
|
|
45
|
+
// State Management
|
|
31
46
|
// =============================================================================
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
resolve(JSON.parse(data));
|
|
49
|
-
} catch (e) {
|
|
50
|
-
reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
req.on('error', (err) => {
|
|
56
|
-
reject(new Error(`Failed to fetch ${url}: ${err.message}`));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
req.on('timeout', () => {
|
|
60
|
-
req.destroy();
|
|
61
|
-
reject(new Error(`Timeout fetching ${url}`));
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Create a CDP connection to a target
|
|
68
|
-
* @param {string} wsUrl - WebSocket debugger URL
|
|
69
|
-
* @returns {Promise<CDPConnection>} - CDP connection object
|
|
70
|
-
*
|
|
71
|
-
* @typedef {Object} CDPConnection
|
|
72
|
-
* @property {WebSocket} ws - The WebSocket connection
|
|
73
|
-
* @property {function(string, object): Promise<any>} call - Send CDP command
|
|
74
|
-
* @property {Array<{id: number, name: string, origin: string}>} contexts - Runtime execution contexts
|
|
75
|
-
* @property {number|null} rootContextId - Main context ID for evaluation
|
|
76
|
-
* @property {function(): void} close - Close the connection
|
|
77
|
-
*/
|
|
78
|
-
function connectToCDP(wsUrl) {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
const ws = new WebSocket(wsUrl);
|
|
81
|
-
let idCounter = 1;
|
|
82
|
-
const pendingCalls = new Map(); // id -> { resolve, reject }
|
|
83
|
-
const contexts = [];
|
|
84
|
-
let rootContextId = null;
|
|
85
|
-
let isConnected = false;
|
|
86
|
-
|
|
87
|
-
// Handle incoming messages
|
|
88
|
-
ws.on('message', (rawMsg) => {
|
|
89
|
-
try {
|
|
90
|
-
const msg = JSON.parse(rawMsg.toString());
|
|
91
|
-
|
|
92
|
-
// Handle CDP events
|
|
93
|
-
if (msg.method === 'Runtime.executionContextCreated') {
|
|
94
|
-
const ctx = msg.params.context;
|
|
95
|
-
contexts.push(ctx);
|
|
96
|
-
|
|
97
|
-
// Track the main/root context (usually the first one or one with specific origin)
|
|
98
|
-
// The root context typically has origin matching the page or is the first created
|
|
99
|
-
if (rootContextId === null || ctx.auxData?.isDefault) {
|
|
100
|
-
rootContextId = ctx.id;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (msg.method === 'Runtime.executionContextDestroyed') {
|
|
107
|
-
const ctxId = msg.params.executionContextId;
|
|
108
|
-
const idx = contexts.findIndex(c => c.id === ctxId);
|
|
109
|
-
if (idx !== -1) {
|
|
110
|
-
contexts.splice(idx, 1);
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
if (rootContextId === ctxId) {
|
|
114
|
-
rootContextId = contexts.length > 0 ? contexts[0].id : null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (msg.method === 'Runtime.executionContextsCleared') {
|
|
119
|
-
contexts.length = 0;
|
|
120
|
-
rootContextId = null;
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Handle responses to our calls
|
|
125
|
-
if (msg.id !== undefined && pendingCalls.has(msg.id)) {
|
|
126
|
-
const { resolve: res, reject: rej } = pendingCalls.get(msg.id);
|
|
127
|
-
pendingCalls.delete(msg.id);
|
|
128
|
-
|
|
129
|
-
if (msg.error) {
|
|
130
|
-
rej(new Error(`CDP Error: ${msg.error.message} (code: ${msg.error.code})`));
|
|
131
|
-
} else {
|
|
132
|
-
res(msg.result);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} catch (e) {
|
|
136
|
-
console.error('[CDP] Failed to parse message:', e.message);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
ws.on('open', async () => {
|
|
141
|
-
isConnected = true;
|
|
142
|
-
console.log(`[CDP] Connected to ${wsUrl}`);
|
|
143
|
-
|
|
144
|
-
// Create the CDP connection object
|
|
145
|
-
const cdp = {
|
|
146
|
-
ws,
|
|
147
|
-
contexts,
|
|
148
|
-
get rootContextId() { return rootContextId; },
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Send a CDP command and wait for response
|
|
152
|
-
* @param {string} method - CDP method name
|
|
153
|
-
* @param {object} params - Method parameters
|
|
154
|
-
* @returns {Promise<any>} - CDP response result
|
|
155
|
-
*/
|
|
156
|
-
call(method, params = {}) {
|
|
157
|
-
return new Promise((res, rej) => {
|
|
158
|
-
if (!isConnected) {
|
|
159
|
-
rej(new Error('CDP connection is closed'));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const id = idCounter++;
|
|
164
|
-
pendingCalls.set(id, { resolve: res, reject: rej });
|
|
165
|
-
|
|
166
|
-
const message = JSON.stringify({ id, method, params });
|
|
167
|
-
ws.send(message);
|
|
168
|
-
|
|
169
|
-
// Timeout for calls (10 seconds)
|
|
170
|
-
setTimeout(() => {
|
|
171
|
-
if (pendingCalls.has(id)) {
|
|
172
|
-
pendingCalls.delete(id);
|
|
173
|
-
rej(new Error(`CDP call timeout: ${method}`));
|
|
174
|
-
}
|
|
175
|
-
}, 10000);
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Close the CDP connection
|
|
181
|
-
*/
|
|
182
|
-
close() {
|
|
183
|
-
isConnected = false;
|
|
184
|
-
// Reject all pending calls
|
|
185
|
-
for (const [id, { reject }] of pendingCalls) {
|
|
186
|
-
reject(new Error('CDP connection closed'));
|
|
187
|
-
}
|
|
188
|
-
pendingCalls.clear();
|
|
189
|
-
ws.terminate();
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
// Enable Runtime to receive execution context events
|
|
195
|
-
await cdp.call('Runtime.enable', {});
|
|
196
|
-
|
|
197
|
-
// Wait a bit for contexts to be discovered
|
|
198
|
-
await new Promise(r => setTimeout(r, 300));
|
|
199
|
-
|
|
200
|
-
console.log(`[CDP] Runtime enabled, found ${contexts.length} context(s)`);
|
|
201
|
-
resolve(cdp);
|
|
202
|
-
} catch (err) {
|
|
203
|
-
cdp.close();
|
|
204
|
-
reject(err);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
ws.on('error', (err) => {
|
|
209
|
-
console.error(`[CDP] WebSocket error: ${err.message}`);
|
|
210
|
-
isConnected = false;
|
|
211
|
-
reject(err);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
ws.on('close', () => {
|
|
215
|
-
console.log('[CDP] Connection closed');
|
|
216
|
-
isConnected = false;
|
|
217
|
-
// Reject all pending calls
|
|
218
|
-
for (const [id, { reject }] of pendingCalls) {
|
|
219
|
-
reject(new Error('CDP connection closed'));
|
|
220
|
-
}
|
|
221
|
-
pendingCalls.clear();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Generate a unique ID for a cascade based on WebSocket URL
|
|
228
|
-
* @param {string} wsUrl - WebSocket debugger URL
|
|
229
|
-
* @returns {string} - Hash ID
|
|
230
|
-
*/
|
|
231
|
-
function generateCascadeId(wsUrl) {
|
|
232
|
-
return crypto.createHash('md5').update(wsUrl).digest('hex').substring(0, 8);
|
|
233
|
-
}
|
|
48
|
+
const cascades = new Map(); // cascadeId -> { id, cdp, metadata, snapshot, css, snapshotHash, editor, editorHash }
|
|
49
|
+
const mainWindowCDP = { connection: null, id: null };
|
|
50
|
+
|
|
51
|
+
const pollingState = {
|
|
52
|
+
lastCascadeCount: 0,
|
|
53
|
+
lastMainWindowConnected: false,
|
|
54
|
+
discoveryInterval: null,
|
|
55
|
+
discoveryIntervalMs: DISCOVERY_INTERVAL_ACTIVE,
|
|
56
|
+
stableCount: 0,
|
|
57
|
+
snapshotInterval: null,
|
|
58
|
+
snapshotIntervalMs: SNAPSHOT_INTERVAL_ACTIVE,
|
|
59
|
+
lastSnapshotChange: Date.now(),
|
|
60
|
+
idleThreshold: SNAPSHOT_IDLE_THRESHOLD
|
|
61
|
+
};
|
|
234
62
|
|
|
235
63
|
// =============================================================================
|
|
236
|
-
//
|
|
64
|
+
// Discovery Service
|
|
237
65
|
// =============================================================================
|
|
238
66
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
*/
|
|
244
|
-
function computeHash(content) {
|
|
245
|
-
return crypto.createHash('md5').update(content).digest('hex');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Extract chat metadata (title, active state) from the page via CDP
|
|
250
|
-
* @param {CDPConnection} cdp - CDP connection
|
|
251
|
-
* @returns {Promise<{chatTitle: string, isActive: boolean}>}
|
|
252
|
-
*/
|
|
253
|
-
async function captureMetadata(cdp) {
|
|
254
|
-
if (!cdp.rootContextId) {
|
|
255
|
-
return { chatTitle: '', isActive: false };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const script = `
|
|
259
|
-
(function() {
|
|
260
|
-
// Try to find chat title from various possible elements
|
|
261
|
-
let chatTitle = '';
|
|
262
|
-
let isActive = false;
|
|
263
|
-
|
|
264
|
-
// Look for chat title in common locations
|
|
265
|
-
// Kiro might have title in header, tab, or specific element
|
|
266
|
-
const titleSelectors = [
|
|
267
|
-
'.chat-title',
|
|
268
|
-
'.conversation-title',
|
|
269
|
-
'[data-testid="chat-title"]',
|
|
270
|
-
'.chat-header h1',
|
|
271
|
-
'.chat-header h2',
|
|
272
|
-
'.chat-header .title'
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
for (const selector of titleSelectors) {
|
|
276
|
-
const el = document.querySelector(selector);
|
|
277
|
-
if (el && el.textContent) {
|
|
278
|
-
chatTitle = el.textContent.trim();
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Check if chat is active (has recent activity or is focused)
|
|
284
|
-
// Look for typing indicators, loading states, or recent messages
|
|
285
|
-
const activeIndicators = [
|
|
286
|
-
'.typing-indicator',
|
|
287
|
-
'.loading-indicator',
|
|
288
|
-
'[data-loading="true"]',
|
|
289
|
-
'.chat-loading'
|
|
290
|
-
];
|
|
291
|
-
|
|
292
|
-
for (const selector of activeIndicators) {
|
|
293
|
-
if (document.querySelector(selector)) {
|
|
294
|
-
isActive = true;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Also check if document is focused
|
|
300
|
-
isActive = isActive || document.hasFocus();
|
|
301
|
-
|
|
302
|
-
return { chatTitle, isActive };
|
|
303
|
-
})()
|
|
304
|
-
`;
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
308
|
-
expression: script,
|
|
309
|
-
contextId: cdp.rootContextId,
|
|
310
|
-
returnByValue: true
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (result.result && result.result.value) {
|
|
314
|
-
return result.result.value;
|
|
315
|
-
}
|
|
316
|
-
} catch (err) {
|
|
317
|
-
console.error('[Snapshot] Failed to capture metadata:', err.message);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return { chatTitle: '', isActive: false };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Capture CSS styles from the page (run once per connection)
|
|
325
|
-
* Gathers all stylesheets and CSS variables, returns CSS string
|
|
326
|
-
* @param {CDPConnection} cdp - CDP connection
|
|
327
|
-
* @returns {Promise<string>} - Combined CSS string
|
|
328
|
-
*/
|
|
329
|
-
async function captureCSS(cdp) {
|
|
330
|
-
if (!cdp.rootContextId) {
|
|
331
|
-
return '';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const script = `
|
|
335
|
-
(function() {
|
|
336
|
-
let css = '';
|
|
337
|
-
|
|
338
|
-
// VS Code webviews use nested iframes - look for #active-frame
|
|
339
|
-
let targetDoc = document;
|
|
340
|
-
const activeFrame = document.getElementById('active-frame');
|
|
341
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
342
|
-
targetDoc = activeFrame.contentDocument;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// First, capture all CSS custom properties (variables) from :root/html/body
|
|
346
|
-
// These are needed because VS Code styles use var(--vscode-*) extensively
|
|
347
|
-
const rootEl = targetDoc.documentElement;
|
|
348
|
-
const bodyEl = targetDoc.body;
|
|
349
|
-
const rootStyles = window.getComputedStyle(rootEl);
|
|
350
|
-
const bodyStyles = window.getComputedStyle(bodyEl);
|
|
351
|
-
|
|
352
|
-
let cssVars = ':root {\\n';
|
|
353
|
-
|
|
354
|
-
// Get all CSS properties and filter for custom properties (start with --)
|
|
355
|
-
const allProps = [];
|
|
356
|
-
for (let i = 0; i < rootStyles.length; i++) {
|
|
357
|
-
allProps.push(rootStyles[i]);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Also check for VS Code specific variables by iterating stylesheets
|
|
361
|
-
for (const sheet of targetDoc.styleSheets) {
|
|
362
|
-
try {
|
|
363
|
-
if (sheet.cssRules) {
|
|
364
|
-
for (const rule of sheet.cssRules) {
|
|
365
|
-
if (rule.style) {
|
|
366
|
-
for (let i = 0; i < rule.style.length; i++) {
|
|
367
|
-
const prop = rule.style[i];
|
|
368
|
-
if (prop.startsWith('--') && !allProps.includes(prop)) {
|
|
369
|
-
allProps.push(prop);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
} catch (e) {}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Get computed values for all custom properties
|
|
379
|
-
for (const prop of allProps) {
|
|
380
|
-
if (prop.startsWith('--')) {
|
|
381
|
-
const value = rootStyles.getPropertyValue(prop).trim();
|
|
382
|
-
if (value) {
|
|
383
|
-
cssVars += ' ' + prop + ': ' + value + ';\\n';
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
cssVars += '}\\n\\n';
|
|
388
|
-
|
|
389
|
-
css += cssVars;
|
|
390
|
-
|
|
391
|
-
// Gather all stylesheets from target document
|
|
392
|
-
for (const sheet of targetDoc.styleSheets) {
|
|
393
|
-
try {
|
|
394
|
-
if (sheet.cssRules) {
|
|
395
|
-
for (const rule of sheet.cssRules) {
|
|
396
|
-
css += rule.cssText + '\\n';
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
} catch (e) {
|
|
400
|
-
// Cross-origin stylesheets will throw
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Also gather inline styles from <style> tags
|
|
405
|
-
const styleTags = targetDoc.querySelectorAll('style');
|
|
406
|
-
for (const tag of styleTags) {
|
|
407
|
-
css += tag.textContent + '\\n';
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return css;
|
|
411
|
-
})()
|
|
412
|
-
`;
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
416
|
-
expression: script,
|
|
417
|
-
contextId: cdp.rootContextId,
|
|
418
|
-
returnByValue: true
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (result.result && result.result.value) {
|
|
422
|
-
return result.result.value;
|
|
423
|
-
}
|
|
424
|
-
} catch (err) {
|
|
425
|
-
console.error('[Snapshot] Failed to capture CSS:', err.message);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return '';
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Capture HTML snapshot of the chat interface
|
|
433
|
-
* @param {CDPConnection} cdp - CDP connection
|
|
434
|
-
* @returns {Promise<{html: string, bodyBg: string, bodyColor: string} | null>}
|
|
435
|
-
*/
|
|
436
|
-
async function captureSnapshot(cdp) {
|
|
437
|
-
if (!cdp.rootContextId) {
|
|
438
|
-
console.log('[Snapshot] No rootContextId available');
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const script = `
|
|
443
|
-
(function() {
|
|
444
|
-
const debug = {
|
|
445
|
-
hasActiveFrame: false,
|
|
446
|
-
activeFrameAccessible: false,
|
|
447
|
-
bodyExists: false,
|
|
448
|
-
selectorsChecked: [],
|
|
449
|
-
foundElement: null,
|
|
450
|
-
htmlLength: 0
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// VS Code webviews use nested iframes - look for #active-frame
|
|
454
|
-
let targetDoc = document;
|
|
455
|
-
let targetBody = document.body;
|
|
456
|
-
|
|
457
|
-
debug.bodyExists = !!targetBody;
|
|
458
|
-
|
|
459
|
-
const activeFrame = document.getElementById('active-frame');
|
|
460
|
-
debug.hasActiveFrame = !!activeFrame;
|
|
461
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
462
|
-
debug.activeFrameAccessible = true;
|
|
463
|
-
targetDoc = activeFrame.contentDocument;
|
|
464
|
-
targetBody = targetDoc.body;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (!targetBody) {
|
|
468
|
-
return { html: '<div style="padding:20px;color:#888;">No content found</div>', bodyBg: '', bodyColor: '', debug };
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Get body styles
|
|
472
|
-
const bodyStyles = window.getComputedStyle(targetBody);
|
|
473
|
-
const bodyBg = bodyStyles.backgroundColor || '';
|
|
474
|
-
const bodyColor = bodyStyles.color || '';
|
|
475
|
-
|
|
476
|
-
// FIXED: Capture the ENTIRE body to include React Portals (like History panel)
|
|
477
|
-
// React Portals render outside #root as siblings, so we must capture body
|
|
478
|
-
// to get overlays, drawers, modals, and other portal-based UI elements
|
|
479
|
-
let chatElement = targetBody;
|
|
480
|
-
debug.foundElement = 'body (captures all portals)';
|
|
481
|
-
|
|
482
|
-
// Log what we're capturing for debugging
|
|
483
|
-
const rootEl = targetDoc.querySelector('#root');
|
|
484
|
-
const bodyChildren = targetBody.children.length;
|
|
485
|
-
debug.selectorsChecked.push({
|
|
486
|
-
selector: 'body',
|
|
487
|
-
found: true,
|
|
488
|
-
htmlLength: targetBody.innerHTML.length,
|
|
489
|
-
bodyChildren: bodyChildren,
|
|
490
|
-
hasRoot: !!rootEl,
|
|
491
|
-
rootLength: rootEl ? rootEl.innerHTML.length : 0
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
debug.htmlLength = chatElement.innerHTML.length;
|
|
495
|
-
|
|
496
|
-
// Scroll chat container to bottom to show latest messages
|
|
497
|
-
const scrollContainers = targetDoc.querySelectorAll('[class*="scroll"], [style*="overflow"]');
|
|
498
|
-
for (const container of scrollContainers) {
|
|
499
|
-
if (container.scrollHeight > container.clientHeight) {
|
|
500
|
-
container.scrollTop = container.scrollHeight;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// CRITICAL: Clone FIRST to avoid modifying the original DOM (which crashes Kiro)
|
|
505
|
-
const clone = chatElement.cloneNode(true);
|
|
506
|
-
|
|
507
|
-
// Now safely remove tooltips, popovers, and overlays from the CLONE only
|
|
508
|
-
const elementsToRemove = [
|
|
509
|
-
'[role="tooltip"]',
|
|
510
|
-
'[data-tooltip]',
|
|
511
|
-
'[class*="tooltip"]:not(button):not([role="button"])',
|
|
512
|
-
'[class*="Tooltip"]:not(button):not([role="button"])',
|
|
513
|
-
'[class*="popover"]:not(button):not([role="button"])',
|
|
514
|
-
'[class*="Popover"]:not(button):not([role="button"])',
|
|
515
|
-
'[class*="dropdown-menu"]',
|
|
516
|
-
'[class*="dropdownMenu"]',
|
|
517
|
-
'[class*="DropdownMenu"]',
|
|
518
|
-
'[class*="dropdown-content"]',
|
|
519
|
-
'[class*="dropdownContent"]',
|
|
520
|
-
'[class*="menu"]:not([role="menubar"]):not([class*="menubar"]):not(button):not([role="button"])',
|
|
521
|
-
'[class*="overlay"]:not(button):not([role="button"])',
|
|
522
|
-
'[class*="Overlay"]:not(button):not([role="button"])',
|
|
523
|
-
'[class*="modal"]',
|
|
524
|
-
'[class*="Modal"]',
|
|
525
|
-
'[style*="position: fixed"]:not(button):not([role="button"]):not([class*="input"]):not([class*="chat"])',
|
|
526
|
-
'[style*="position:fixed"]:not(button):not([role="button"]):not([class*="input"]):not([class*="chat"])'
|
|
527
|
-
];
|
|
528
|
-
|
|
529
|
-
elementsToRemove.forEach(selector => {
|
|
530
|
-
try {
|
|
531
|
-
clone.querySelectorAll(selector).forEach(el => {
|
|
532
|
-
const isMainContent = el.closest('#root > div:first-child');
|
|
533
|
-
const isTooltip = el.matches('[role="tooltip"], [class*="tooltip"], [class*="Tooltip"]');
|
|
534
|
-
const isImportantUI = el.matches('[class*="model"], [class*="Model"], [class*="context"], [class*="Context"], [class*="input"], [class*="Input"], [class*="selector"], [class*="Selector"], button, [role="button"]');
|
|
535
|
-
|
|
536
|
-
if (isTooltip || (!isMainContent && !isImportantUI)) {
|
|
537
|
-
el.remove();
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
} catch(e) {}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Fix SVG currentColor in the CLONE only (not original DOM)
|
|
544
|
-
const cloneSvgs = clone.querySelectorAll('svg');
|
|
545
|
-
for (const svg of cloneSvgs) {
|
|
546
|
-
try {
|
|
547
|
-
// Get computed color from the ORIGINAL element for accurate color
|
|
548
|
-
const originalSvg = chatElement.querySelector('svg[class="' + (svg.getAttribute('class') || '') + '"]');
|
|
549
|
-
const computedColor = originalSvg
|
|
550
|
-
? (window.getComputedStyle(originalSvg).color || window.getComputedStyle(originalSvg.parentElement).color)
|
|
551
|
-
: '#cccccc';
|
|
552
|
-
if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)') {
|
|
553
|
-
svg.querySelectorAll('[fill="currentColor"]').forEach(el => el.setAttribute('fill', computedColor));
|
|
554
|
-
svg.querySelectorAll('[stroke="currentColor"]').forEach(el => el.setAttribute('stroke', computedColor));
|
|
555
|
-
if (svg.getAttribute('fill') === 'currentColor') svg.setAttribute('fill', computedColor);
|
|
556
|
-
if (svg.getAttribute('stroke') === 'currentColor') svg.setAttribute('stroke', computedColor);
|
|
557
|
-
if (!svg.getAttribute('fill') && !svg.getAttribute('stroke')) svg.style.color = computedColor;
|
|
558
|
-
}
|
|
559
|
-
} catch(e) {}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// REMOVE PLACEHOLDER TEXT from Lexical editor
|
|
563
|
-
// The placeholder is a sibling div to the contenteditable that overlays it
|
|
564
|
-
// We need to find and remove it to prevent text overlap on mobile
|
|
565
|
-
try {
|
|
566
|
-
// Method 1: Find placeholder by checking siblings of contenteditable
|
|
567
|
-
const editables = clone.querySelectorAll('[contenteditable="true"], [data-lexical-editor="true"]');
|
|
568
|
-
editables.forEach(editable => {
|
|
569
|
-
const parent = editable.parentElement;
|
|
570
|
-
if (parent) {
|
|
571
|
-
// Check all siblings
|
|
572
|
-
Array.from(parent.children).forEach(sibling => {
|
|
573
|
-
if (sibling === editable) return;
|
|
574
|
-
// Check if this sibling looks like a placeholder
|
|
575
|
-
const text = (sibling.textContent || '').toLowerCase();
|
|
576
|
-
if (text.includes('ask') || text.includes('question') || text.includes('task') ||
|
|
577
|
-
text.includes('describe') || text.includes('type') || text.includes('message')) {
|
|
578
|
-
sibling.remove();
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Method 2: Find by common placeholder class patterns
|
|
585
|
-
const placeholderSelectors = [
|
|
586
|
-
'[class*="placeholder"]',
|
|
587
|
-
'[class*="Placeholder"]',
|
|
588
|
-
'[data-placeholder]'
|
|
589
|
-
];
|
|
590
|
-
placeholderSelectors.forEach(sel => {
|
|
591
|
-
clone.querySelectorAll(sel).forEach(el => {
|
|
592
|
-
// Don't remove the actual input
|
|
593
|
-
if (el.matches('[contenteditable], [data-lexical-editor], textarea, input')) return;
|
|
594
|
-
// Don't remove if it contains an input
|
|
595
|
-
if (el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) return;
|
|
596
|
-
el.remove();
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
// Method 3: Find any element with placeholder-like text that's positioned absolutely
|
|
601
|
-
clone.querySelectorAll('*').forEach(el => {
|
|
602
|
-
if (el.matches('[contenteditable], [data-lexical-editor], textarea, input, button, svg')) return;
|
|
603
|
-
const text = (el.textContent || '').toLowerCase().trim();
|
|
604
|
-
const style = el.getAttribute('style') || '';
|
|
605
|
-
// If it has placeholder text and is positioned (likely overlay)
|
|
606
|
-
if ((text.includes('ask a question') || text.includes('describe a task') ||
|
|
607
|
-
text === 'ask a question or describe a task...' || text === 'ask a question or describe a task') &&
|
|
608
|
-
(style.includes('position') || el.children.length === 0)) {
|
|
609
|
-
el.remove();
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
} catch(e) {}
|
|
613
|
-
|
|
614
|
-
return {
|
|
615
|
-
html: clone.outerHTML,
|
|
616
|
-
bodyBg,
|
|
617
|
-
bodyColor,
|
|
618
|
-
debug
|
|
619
|
-
};
|
|
620
|
-
})()
|
|
621
|
-
`;
|
|
622
|
-
|
|
623
|
-
try {
|
|
624
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
625
|
-
expression: script,
|
|
626
|
-
contextId: cdp.rootContextId,
|
|
627
|
-
returnByValue: true
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
if (result.result && result.result.value) {
|
|
631
|
-
return result.result.value;
|
|
632
|
-
}
|
|
633
|
-
} catch (err) {
|
|
634
|
-
console.error('[Snapshot] Failed to capture HTML:', err.message);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Capture Sidebar panel HTML snapshot (File Explorer + Kiro panels)
|
|
642
|
-
* @param {CDPConnection} cdp - CDP connection
|
|
643
|
-
* @returns {Promise<{html: string, files: Array, kiroPanels: Array} | null>}
|
|
644
|
-
*/
|
|
645
|
-
async function captureSidebar(cdp) {
|
|
646
|
-
if (!cdp.rootContextId) return null;
|
|
647
|
-
|
|
648
|
-
const script = `
|
|
649
|
-
(function() {
|
|
650
|
-
let targetDoc = document;
|
|
651
|
-
const activeFrame = document.getElementById('active-frame');
|
|
652
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
653
|
-
targetDoc = activeFrame.contentDocument;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const result = {
|
|
657
|
-
html: '',
|
|
658
|
-
files: [],
|
|
659
|
-
kiroPanels: [],
|
|
660
|
-
hasContent: false
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
// Look for file explorer / sidebar
|
|
664
|
-
const sidebarSelectors = [
|
|
665
|
-
'.sidebar',
|
|
666
|
-
'.explorer-viewlet',
|
|
667
|
-
'[class*="sidebar"]',
|
|
668
|
-
'.activitybar + .part',
|
|
669
|
-
'.monaco-workbench .part.sidebar',
|
|
670
|
-
'[data-testid="sidebar"]',
|
|
671
|
-
'.composite.viewlet'
|
|
672
|
-
];
|
|
673
|
-
|
|
674
|
-
let sidebarElement = null;
|
|
675
|
-
for (const selector of sidebarSelectors) {
|
|
676
|
-
const el = targetDoc.querySelector(selector);
|
|
677
|
-
if (el && el.innerHTML.length > 50) {
|
|
678
|
-
sidebarElement = el;
|
|
679
|
-
break;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Extract file tree structure
|
|
684
|
-
const fileTreeSelectors = [
|
|
685
|
-
'.monaco-list-row',
|
|
686
|
-
'.explorer-item',
|
|
687
|
-
'[class*="tree-row"]',
|
|
688
|
-
'.file-icon-themable-tree .monaco-list-row'
|
|
689
|
-
];
|
|
690
|
-
|
|
691
|
-
for (const selector of fileTreeSelectors) {
|
|
692
|
-
const items = targetDoc.querySelectorAll(selector);
|
|
693
|
-
if (items.length > 0) {
|
|
694
|
-
items.forEach(item => {
|
|
695
|
-
const label = item.querySelector('.label-name, .monaco-icon-label-container, [class*="label"]');
|
|
696
|
-
const icon = item.querySelector('.file-icon, .folder-icon, [class*="icon"]');
|
|
697
|
-
const isFolder = item.classList.contains('folder') ||
|
|
698
|
-
item.querySelector('.folder-icon') !== null ||
|
|
699
|
-
item.getAttribute('aria-expanded') !== null;
|
|
700
|
-
const isExpanded = item.getAttribute('aria-expanded') === 'true';
|
|
701
|
-
const depth = parseInt(item.style.paddingLeft || item.style.textIndent || '0') / 8 || 0;
|
|
702
|
-
|
|
703
|
-
if (label && label.textContent) {
|
|
704
|
-
result.files.push({
|
|
705
|
-
name: label.textContent.trim(),
|
|
706
|
-
isFolder,
|
|
707
|
-
isExpanded,
|
|
708
|
-
depth: Math.floor(depth)
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Look for Kiro-specific panels (specs, hooks, steering)
|
|
717
|
-
const kiroPanelSelectors = [
|
|
718
|
-
'[class*="kiro"]',
|
|
719
|
-
'[data-testid*="kiro"]',
|
|
720
|
-
'.specs-panel',
|
|
721
|
-
'.hooks-panel',
|
|
722
|
-
'.steering-panel'
|
|
723
|
-
];
|
|
724
|
-
|
|
725
|
-
for (const selector of kiroPanelSelectors) {
|
|
726
|
-
const panels = targetDoc.querySelectorAll(selector);
|
|
727
|
-
panels.forEach(panel => {
|
|
728
|
-
const title = panel.querySelector('h2, h3, .title, [class*="title"]');
|
|
729
|
-
if (title && title.textContent) {
|
|
730
|
-
result.kiroPanels.push({
|
|
731
|
-
title: title.textContent.trim(),
|
|
732
|
-
html: panel.outerHTML.substring(0, 5000) // Limit size
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (sidebarElement) {
|
|
739
|
-
result.html = sidebarElement.outerHTML;
|
|
740
|
-
result.hasContent = true;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
result.hasContent = result.hasContent || result.files.length > 0 || result.kiroPanels.length > 0;
|
|
744
|
-
|
|
745
|
-
return result;
|
|
746
|
-
})()
|
|
747
|
-
`;
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
751
|
-
expression: script,
|
|
752
|
-
contextId: cdp.rootContextId,
|
|
753
|
-
returnByValue: true
|
|
754
|
-
});
|
|
755
|
-
if (result.result && result.result.value) return result.result.value;
|
|
756
|
-
} catch (err) {
|
|
757
|
-
console.error('[Sidebar] Failed to capture:', err.message);
|
|
758
|
-
}
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Capture Editor panel HTML snapshot (currently open file)
|
|
764
|
-
* @param {CDPConnection} cdp - CDP connection
|
|
765
|
-
* @returns {Promise<{html: string, fileName: string, language: string, content: string} | null>}
|
|
766
|
-
*/
|
|
767
|
-
async function captureEditor(cdp) {
|
|
768
|
-
if (!cdp.rootContextId) return null;
|
|
769
|
-
|
|
770
|
-
const script = `
|
|
771
|
-
(function() {
|
|
772
|
-
let targetDoc = document;
|
|
773
|
-
const activeFrame = document.getElementById('active-frame');
|
|
774
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
775
|
-
targetDoc = activeFrame.contentDocument;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const result = {
|
|
779
|
-
html: '',
|
|
780
|
-
fileName: '',
|
|
781
|
-
language: '',
|
|
782
|
-
content: '',
|
|
783
|
-
lineCount: 0,
|
|
784
|
-
hasContent: false
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
// Get active tab / file name - try multiple selectors
|
|
788
|
-
const tabSelectors = [
|
|
789
|
-
'.tab.active .label-name',
|
|
790
|
-
'.tab.active .monaco-icon-label-container .label-name',
|
|
791
|
-
'.tab.selected .monaco-icon-label',
|
|
792
|
-
'[class*="tab"][class*="active"] .label-name',
|
|
793
|
-
'.editor-group-container .tab.active',
|
|
794
|
-
'.tabs-container .tab.active',
|
|
795
|
-
'.tab.active',
|
|
796
|
-
'[role="tab"][aria-selected="true"]'
|
|
797
|
-
];
|
|
798
|
-
|
|
799
|
-
for (const selector of tabSelectors) {
|
|
800
|
-
try {
|
|
801
|
-
const tab = targetDoc.querySelector(selector);
|
|
802
|
-
if (tab && tab.textContent) {
|
|
803
|
-
result.fileName = tab.textContent.trim().split('\\n')[0].trim();
|
|
804
|
-
if (result.fileName) break;
|
|
805
|
-
}
|
|
806
|
-
} catch(e) {}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Try to get content from Monaco editor's internal model (best approach)
|
|
810
|
-
try {
|
|
811
|
-
// Look for Monaco editor instance
|
|
812
|
-
const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
|
|
813
|
-
for (const editorEl of monacoEditors) {
|
|
814
|
-
// Try to access the editor instance through VS Code's API
|
|
815
|
-
const editorInstance = editorEl.__vscode_editor__ ||
|
|
816
|
-
editorEl._editor ||
|
|
817
|
-
(window.monaco && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
|
|
818
|
-
|
|
819
|
-
if (editorInstance && editorInstance.getModel) {
|
|
820
|
-
const model = editorInstance.getModel();
|
|
821
|
-
if (model) {
|
|
822
|
-
result.content = model.getValue();
|
|
823
|
-
result.lineCount = model.getLineCount();
|
|
824
|
-
result.language = model.getLanguageId ? model.getLanguageId() : (model.getModeId ? model.getModeId() : '');
|
|
825
|
-
result.hasContent = true;
|
|
826
|
-
break;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
} catch(e) {
|
|
831
|
-
console.log('Monaco API access failed:', e);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Fallback: Try to get content from textarea (some editors use this)
|
|
835
|
-
if (!result.content) {
|
|
836
|
-
try {
|
|
837
|
-
const textareas = targetDoc.querySelectorAll('textarea.inputarea, textarea[class*="input"]');
|
|
838
|
-
for (const ta of textareas) {
|
|
839
|
-
if (ta.value && ta.value.length > 10) {
|
|
840
|
-
result.content = ta.value;
|
|
841
|
-
result.hasContent = true;
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
} catch(e) {}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Fallback: Extract from visible view-lines (limited to what's rendered)
|
|
849
|
-
if (!result.content) {
|
|
850
|
-
const editorSelectors = [
|
|
851
|
-
'.monaco-editor .view-lines',
|
|
852
|
-
'.monaco-editor',
|
|
853
|
-
'.lines-content'
|
|
854
|
-
];
|
|
855
|
-
|
|
856
|
-
let viewLinesElement = null;
|
|
857
|
-
let editorElement = null;
|
|
858
|
-
for (const selector of editorSelectors) {
|
|
859
|
-
try {
|
|
860
|
-
const el = targetDoc.querySelector(selector);
|
|
861
|
-
if (el) {
|
|
862
|
-
viewLinesElement = el.querySelector('.view-lines') || el;
|
|
863
|
-
editorElement = el.closest('.monaco-editor') || el;
|
|
864
|
-
break;
|
|
865
|
-
}
|
|
866
|
-
} catch(e) {}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (viewLinesElement) {
|
|
870
|
-
const lines = viewLinesElement.querySelectorAll('.view-line');
|
|
871
|
-
|
|
872
|
-
// Try to get line numbers from the line number gutter
|
|
873
|
-
const lineNumberElements = editorElement ?
|
|
874
|
-
editorElement.querySelectorAll('.line-numbers, .margin-view-overlays .line-numbers') : [];
|
|
875
|
-
|
|
876
|
-
if (lines.length > 0) {
|
|
877
|
-
// Create a map of line number to content
|
|
878
|
-
const lineMap = new Map();
|
|
879
|
-
let minLineNum = Infinity;
|
|
880
|
-
let maxLineNum = 0;
|
|
881
|
-
|
|
882
|
-
// Try to match lines with their line numbers from the gutter
|
|
883
|
-
const lineNumMap = new Map();
|
|
884
|
-
lineNumberElements.forEach(ln => {
|
|
885
|
-
const top = parseFloat(ln.style.top) || 0;
|
|
886
|
-
const num = parseInt(ln.textContent, 10);
|
|
887
|
-
if (!isNaN(num)) {
|
|
888
|
-
lineNumMap.set(Math.round(top), num);
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
lines.forEach(line => {
|
|
893
|
-
const top = parseFloat(line.style.top) || 0;
|
|
894
|
-
const roundedTop = Math.round(top);
|
|
895
|
-
|
|
896
|
-
// Try to get line number from gutter, or calculate from position
|
|
897
|
-
let lineNum = lineNumMap.get(roundedTop);
|
|
898
|
-
if (!lineNum) {
|
|
899
|
-
// Fallback: calculate from top position (19px line height)
|
|
900
|
-
const lineHeight = 19;
|
|
901
|
-
lineNum = Math.round(top / lineHeight) + 1;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const text = line.textContent || '';
|
|
905
|
-
lineMap.set(lineNum, text);
|
|
906
|
-
minLineNum = Math.min(minLineNum, lineNum);
|
|
907
|
-
maxLineNum = Math.max(maxLineNum, lineNum);
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// Build content from line map, starting from minLineNum
|
|
911
|
-
let codeContent = '';
|
|
912
|
-
const startLine = Math.max(1, minLineNum);
|
|
913
|
-
for (let i = startLine; i <= Math.min(maxLineNum, startLine + 500); i++) {
|
|
914
|
-
codeContent += (lineMap.get(i) || '') + '\\n';
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
result.content = codeContent;
|
|
918
|
-
result.lineCount = maxLineNum;
|
|
919
|
-
result.startLine = startLine;
|
|
920
|
-
result.hasContent = codeContent.trim().length > 0;
|
|
921
|
-
|
|
922
|
-
// Mark as partial if we don't start from line 1
|
|
923
|
-
if (startLine > 1) {
|
|
924
|
-
result.isPartial = true;
|
|
925
|
-
result.note = 'Showing lines ' + startLine + '-' + maxLineNum + '. Scroll in Kiro to see other parts.';
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Get language from editor element if not already set
|
|
932
|
-
if (!result.language) {
|
|
933
|
-
try {
|
|
934
|
-
const monacoEditor = targetDoc.querySelector('.monaco-editor');
|
|
935
|
-
if (monacoEditor) {
|
|
936
|
-
const modeId = monacoEditor.getAttribute('data-mode-id');
|
|
937
|
-
if (modeId) result.language = modeId;
|
|
938
|
-
|
|
939
|
-
const langMatch = monacoEditor.className.match(/\\b(typescript|javascript|python|java|html|css|json|markdown|yaml|xml|sql|go|rust|c|cpp|csharp)\\b/i);
|
|
940
|
-
if (langMatch) result.language = langMatch[1].toLowerCase();
|
|
941
|
-
}
|
|
942
|
-
} catch(e) {}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Fallback: detect language from filename
|
|
946
|
-
if (!result.language && result.fileName) {
|
|
947
|
-
const ext = result.fileName.split('.').pop()?.toLowerCase();
|
|
948
|
-
const extMap = {
|
|
949
|
-
'ts': 'typescript', 'tsx': 'typescript',
|
|
950
|
-
'js': 'javascript', 'jsx': 'javascript',
|
|
951
|
-
'py': 'python', 'java': 'java',
|
|
952
|
-
'html': 'html', 'css': 'css',
|
|
953
|
-
'json': 'json', 'md': 'markdown',
|
|
954
|
-
'yaml': 'yaml', 'yml': 'yaml',
|
|
955
|
-
'xml': 'xml', 'sql': 'sql',
|
|
956
|
-
'go': 'go', 'rs': 'rust',
|
|
957
|
-
'c': 'c', 'cpp': 'cpp', 'h': 'c',
|
|
958
|
-
'cs': 'csharp', 'rb': 'ruby',
|
|
959
|
-
'php': 'php', 'sh': 'bash'
|
|
960
|
-
};
|
|
961
|
-
result.language = extMap[ext] || ext || '';
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// Add note about partial content
|
|
965
|
-
if (result.hasContent && result.lineCount < 50) {
|
|
966
|
-
result.isPartial = true;
|
|
967
|
-
result.note = 'Showing visible lines only. Scroll in Kiro to see more.';
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return result;
|
|
971
|
-
})()
|
|
972
|
-
`;
|
|
973
|
-
|
|
974
|
-
try {
|
|
975
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
976
|
-
expression: script,
|
|
977
|
-
contextId: cdp.rootContextId,
|
|
978
|
-
returnByValue: true
|
|
979
|
-
});
|
|
980
|
-
if (result.result && result.result.value) return result.result.value;
|
|
981
|
-
} catch (err) {
|
|
982
|
-
console.error('[Editor] Failed to capture:', err.message);
|
|
983
|
-
}
|
|
984
|
-
return null;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* Alternative: Read file content directly from filesystem
|
|
989
|
-
* This is more reliable than trying to scrape Monaco editor
|
|
990
|
-
*/
|
|
991
|
-
async function readFileContent(filePath, workspaceRoot) {
|
|
992
|
-
const fs = await import('fs/promises');
|
|
993
|
-
const path = await import('path');
|
|
994
|
-
|
|
995
|
-
try {
|
|
996
|
-
// Try to resolve the file path
|
|
997
|
-
let fullPath = filePath;
|
|
998
|
-
if (!path.isAbsolute(filePath) && workspaceRoot) {
|
|
999
|
-
fullPath = path.join(workspaceRoot, filePath);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
1003
|
-
return content;
|
|
1004
|
-
} catch (err) {
|
|
1005
|
-
console.error('[ReadFile] Failed to read:', err.message);
|
|
1006
|
-
return null;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Poll all cascades for snapshot changes
|
|
1012
|
-
* Captures snapshots, compares hashes, and broadcasts updates on change
|
|
1013
|
-
*/
|
|
1014
|
-
async function pollSnapshots() {
|
|
1015
|
-
for (const [cascadeId, cascade] of cascades) {
|
|
1016
|
-
try {
|
|
1017
|
-
const cdp = cascade.cdp;
|
|
1018
|
-
|
|
1019
|
-
// Capture CSS once if not already captured
|
|
1020
|
-
if (cascade.css === null) {
|
|
1021
|
-
cascade.css = await captureCSS(cdp);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Capture metadata
|
|
1025
|
-
const metadata = await captureMetadata(cdp);
|
|
1026
|
-
cascade.metadata.chatTitle = metadata.chatTitle || cascade.metadata.chatTitle;
|
|
1027
|
-
cascade.metadata.isActive = metadata.isActive;
|
|
1028
|
-
|
|
1029
|
-
// Capture HTML snapshot (chat) from Kiro Agent webview
|
|
1030
|
-
const snapshot = await captureSnapshot(cdp);
|
|
1031
|
-
|
|
1032
|
-
if (snapshot) {
|
|
1033
|
-
const newHash = computeHash(snapshot.html);
|
|
1034
|
-
if (newHash !== cascade.snapshotHash) {
|
|
1035
|
-
cascade.snapshot = snapshot;
|
|
1036
|
-
cascade.snapshotHash = newHash;
|
|
1037
|
-
broadcastSnapshotUpdate(cascadeId, 'chat');
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Use main window CDP for sidebar/editor
|
|
1042
|
-
const mainCDP = mainWindowCDP.connection;
|
|
1043
|
-
if (mainCDP && mainCDP.rootContextId) {
|
|
1044
|
-
// Capture Sidebar snapshot from main window
|
|
1045
|
-
const sidebar = await captureSidebar(mainCDP);
|
|
1046
|
-
if (sidebar && sidebar.hasContent) {
|
|
1047
|
-
const sideHash = computeHash(JSON.stringify(sidebar.files) + sidebar.html);
|
|
1048
|
-
if (sideHash !== cascade.sidebarHash) {
|
|
1049
|
-
cascade.sidebar = sidebar;
|
|
1050
|
-
cascade.sidebarHash = sideHash;
|
|
1051
|
-
broadcastSnapshotUpdate(cascadeId, 'sidebar');
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Capture Editor snapshot from main window
|
|
1056
|
-
const editor = await captureEditor(mainCDP);
|
|
1057
|
-
if (editor && editor.hasContent) {
|
|
1058
|
-
const editorHash = computeHash(editor.content + editor.fileName);
|
|
1059
|
-
if (editorHash !== cascade.editorHash) {
|
|
1060
|
-
cascade.editor = editor;
|
|
1061
|
-
cascade.editorHash = editorHash;
|
|
1062
|
-
broadcastSnapshotUpdate(cascadeId, 'editor');
|
|
1063
|
-
}
|
|
1064
|
-
} else if (cascade.editor && cascade.editor.hasContent) {
|
|
1065
|
-
// Clear stale editor data when no file is open
|
|
1066
|
-
cascade.editor = { hasContent: false, fileName: '', content: '' };
|
|
1067
|
-
cascade.editorHash = '';
|
|
1068
|
-
broadcastSnapshotUpdate(cascadeId, 'editor');
|
|
1069
|
-
}
|
|
1070
|
-
} else if (!mainCDP) {
|
|
1071
|
-
// Main window not connected yet - this is normal during startup
|
|
1072
|
-
}
|
|
1073
|
-
} catch (err) {
|
|
1074
|
-
console.error(`[Snapshot] Error polling cascade ${cascadeId}:`, err.message);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* Broadcast a snapshot update notification to all connected WebSocket clients
|
|
1081
|
-
* @param {string} cascadeId - ID of the cascade that was updated
|
|
1082
|
-
* @param {string} panel - Panel type that was updated (chat, sidebar, editor)
|
|
1083
|
-
*/
|
|
1084
|
-
function broadcastSnapshotUpdate(cascadeId, panel = 'chat') {
|
|
1085
|
-
const message = JSON.stringify({
|
|
1086
|
-
type: 'snapshot_update',
|
|
1087
|
-
cascadeId,
|
|
1088
|
-
panel
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
for (const client of wss.clients) {
|
|
1092
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1093
|
-
client.send(message);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// =============================================================================
|
|
1099
|
-
// Message Injection (Task 6)
|
|
1100
|
-
// =============================================================================
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
* CDP script to inject a message into the chat input and send it.
|
|
1104
|
-
*
|
|
1105
|
-
* This script:
|
|
1106
|
-
* 1. Finds the chat input element (contenteditable div or textarea)
|
|
1107
|
-
* 2. Inserts the message text into the input
|
|
1108
|
-
* 3. Triggers the send button click or dispatches Enter key event
|
|
1109
|
-
*
|
|
1110
|
-
* @param {string} messageText - The message to inject (will be escaped)
|
|
1111
|
-
* @returns {string} - JavaScript expression to evaluate in the page context
|
|
1112
|
-
*/
|
|
1113
|
-
function createInjectMessageScript(messageText) {
|
|
1114
|
-
// Escape the message for safe inclusion in JavaScript string
|
|
1115
|
-
const escapedMessage = messageText
|
|
1116
|
-
.replace(/\\/g, '\\\\')
|
|
1117
|
-
.replace(/'/g, "\\'")
|
|
1118
|
-
.replace(/\n/g, '\\n')
|
|
1119
|
-
.replace(/\r/g, '\\r');
|
|
1120
|
-
|
|
1121
|
-
return `(async () => {
|
|
1122
|
-
const text = '${escapedMessage}';
|
|
1123
|
-
|
|
1124
|
-
// VS Code webviews use nested iframes - look for #active-frame
|
|
1125
|
-
let targetDoc = document;
|
|
1126
|
-
const activeFrame = document.getElementById('active-frame');
|
|
1127
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
1128
|
-
targetDoc = activeFrame.contentDocument;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// 6.1 Find input element (contenteditable or textarea)
|
|
1132
|
-
// UPDATED: Support both Lexical and ProseMirror/TipTap editors
|
|
1133
|
-
let editor = null;
|
|
1134
|
-
|
|
1135
|
-
// Try ProseMirror/TipTap editor first (confirmed via Playwriter debugging)
|
|
1136
|
-
let editors = [...targetDoc.querySelectorAll('.tiptap.ProseMirror[contenteditable="true"]')]
|
|
1137
|
-
.filter(el => el.offsetParent !== null);
|
|
1138
|
-
editor = editors.at(-1);
|
|
1139
|
-
|
|
1140
|
-
// Fallback: Try Kiro's Lexical editor
|
|
1141
|
-
if (!editor) {
|
|
1142
|
-
editors = [...targetDoc.querySelectorAll('[data-lexical-editor="true"][contenteditable="true"][role="textbox"]')]
|
|
1143
|
-
.filter(el => el.offsetParent !== null);
|
|
1144
|
-
editor = editors.at(-1);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// Fallback: try any contenteditable in the cascade area
|
|
1148
|
-
if (!editor) {
|
|
1149
|
-
editors = [...targetDoc.querySelectorAll('#cascade [contenteditable="true"]')]
|
|
1150
|
-
.filter(el => el.offsetParent !== null);
|
|
1151
|
-
editor = editors.at(-1);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// Fallback: try any contenteditable
|
|
1155
|
-
if (!editor) {
|
|
1156
|
-
editors = [...targetDoc.querySelectorAll('[contenteditable="true"]')]
|
|
1157
|
-
.filter(el => el.offsetParent !== null);
|
|
1158
|
-
editor = editors.at(-1);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Fallback: try textarea
|
|
1162
|
-
if (!editor) {
|
|
1163
|
-
const textareas = [...targetDoc.querySelectorAll('textarea')]
|
|
1164
|
-
.filter(el => el.offsetParent !== null);
|
|
1165
|
-
editor = textareas.at(-1);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
if (!editor) {
|
|
1169
|
-
return { ok: false, error: 'editor_not_found', message: 'Could not find chat input element' };
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
const isTextarea = editor.tagName.toLowerCase() === 'textarea';
|
|
1173
|
-
const isProseMirror = editor.classList.contains('ProseMirror') || editor.classList.contains('tiptap');
|
|
1174
|
-
|
|
1175
|
-
// 6.2 Insert text into input element
|
|
1176
|
-
editor.focus();
|
|
1177
|
-
|
|
1178
|
-
if (isTextarea) {
|
|
1179
|
-
// For textarea, set value directly and dispatch input event
|
|
1180
|
-
editor.value = text;
|
|
1181
|
-
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1182
|
-
editor.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1183
|
-
} else if (isProseMirror) {
|
|
1184
|
-
// For ProseMirror/TipTap, we need to use their specific API or simulate typing
|
|
1185
|
-
// First clear existing content
|
|
1186
|
-
editor.innerHTML = '';
|
|
1187
|
-
|
|
1188
|
-
// Create a paragraph with the text (ProseMirror structure)
|
|
1189
|
-
const p = targetDoc.createElement('p');
|
|
1190
|
-
p.textContent = text;
|
|
1191
|
-
editor.appendChild(p);
|
|
1192
|
-
|
|
1193
|
-
// Dispatch input event to trigger ProseMirror's update
|
|
1194
|
-
editor.dispatchEvent(new InputEvent('input', {
|
|
1195
|
-
bubbles: true,
|
|
1196
|
-
inputType: 'insertText',
|
|
1197
|
-
data: text
|
|
1198
|
-
}));
|
|
1199
|
-
} else {
|
|
1200
|
-
// For contenteditable, use execCommand or fallback to direct manipulation
|
|
1201
|
-
// First, select all and delete existing content
|
|
1202
|
-
targetDoc.execCommand?.('selectAll', false, null);
|
|
1203
|
-
targetDoc.execCommand?.('delete', false, null);
|
|
1204
|
-
|
|
1205
|
-
// Try insertText command
|
|
1206
|
-
let inserted = false;
|
|
1207
|
-
try {
|
|
1208
|
-
inserted = !!targetDoc.execCommand?.('insertText', false, text);
|
|
1209
|
-
} catch (e) {
|
|
1210
|
-
inserted = false;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// Fallback: set textContent and dispatch events
|
|
1214
|
-
if (!inserted) {
|
|
1215
|
-
editor.textContent = text;
|
|
1216
|
-
editor.dispatchEvent(new InputEvent('beforeinput', {
|
|
1217
|
-
bubbles: true,
|
|
1218
|
-
inputType: 'insertText',
|
|
1219
|
-
data: text
|
|
1220
|
-
}));
|
|
1221
|
-
editor.dispatchEvent(new InputEvent('input', {
|
|
1222
|
-
bubbles: true,
|
|
1223
|
-
inputType: 'insertText',
|
|
1224
|
-
data: text
|
|
1225
|
-
}));
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Wait for React/framework to process the input
|
|
1230
|
-
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
1231
|
-
|
|
1232
|
-
// 6.3 Trigger send button click or Enter key
|
|
1233
|
-
// Try to find the send button (arrow-right icon button)
|
|
1234
|
-
const submitButton = targetDoc.querySelector('svg.lucide-arrow-right')?.closest('button');
|
|
1235
|
-
|
|
1236
|
-
if (submitButton && !submitButton.disabled) {
|
|
1237
|
-
submitButton.click();
|
|
1238
|
-
return { ok: true, method: 'click_submit', inputType: isTextarea ? 'textarea' : 'contenteditable' };
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// Fallback: try other common send button patterns
|
|
1242
|
-
const altSubmitButtons = [
|
|
1243
|
-
targetDoc.querySelector('[data-tooltip-id*="send"]')?.closest('button'),
|
|
1244
|
-
targetDoc.querySelector('button[type="submit"]'),
|
|
1245
|
-
targetDoc.querySelector('button[aria-label*="send" i]'),
|
|
1246
|
-
targetDoc.querySelector('button[aria-label*="submit" i]')
|
|
1247
|
-
].filter(btn => btn && !btn.disabled && btn.offsetParent !== null);
|
|
1248
|
-
|
|
1249
|
-
if (altSubmitButtons.length > 0) {
|
|
1250
|
-
altSubmitButtons[0].click();
|
|
1251
|
-
return { ok: true, method: 'click_alt_submit', inputType: isTextarea ? 'textarea' : 'contenteditable' };
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Last resort: dispatch Enter key event
|
|
1255
|
-
editor.dispatchEvent(new KeyboardEvent('keydown', {
|
|
1256
|
-
bubbles: true,
|
|
1257
|
-
key: 'Enter',
|
|
1258
|
-
code: 'Enter',
|
|
1259
|
-
keyCode: 13,
|
|
1260
|
-
which: 13
|
|
1261
|
-
}));
|
|
1262
|
-
editor.dispatchEvent(new KeyboardEvent('keypress', {
|
|
1263
|
-
bubbles: true,
|
|
1264
|
-
key: 'Enter',
|
|
1265
|
-
code: 'Enter',
|
|
1266
|
-
keyCode: 13,
|
|
1267
|
-
which: 13
|
|
1268
|
-
}));
|
|
1269
|
-
editor.dispatchEvent(new KeyboardEvent('keyup', {
|
|
1270
|
-
bubbles: true,
|
|
1271
|
-
key: 'Enter',
|
|
1272
|
-
code: 'Enter',
|
|
1273
|
-
keyCode: 13,
|
|
1274
|
-
which: 13
|
|
1275
|
-
}));
|
|
1276
|
-
|
|
1277
|
-
return {
|
|
1278
|
-
ok: true,
|
|
1279
|
-
method: 'enter_key',
|
|
1280
|
-
inputType: isTextarea ? 'textarea' : 'contenteditable',
|
|
1281
|
-
submitButtonFound: !!submitButton,
|
|
1282
|
-
submitButtonDisabled: submitButton?.disabled ?? null
|
|
1283
|
-
};
|
|
1284
|
-
})()`;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/**
|
|
1288
|
-
* Inject a message into the chat via CDP
|
|
1289
|
-
*
|
|
1290
|
-
* @param {CDPConnection} cdp - CDP connection object
|
|
1291
|
-
* @param {string} message - Message text to inject
|
|
1292
|
-
* @returns {Promise<{success: boolean, method?: string, error?: string}>}
|
|
1293
|
-
*/
|
|
1294
|
-
async function injectMessage(cdp, message) {
|
|
1295
|
-
if (!cdp.rootContextId) {
|
|
1296
|
-
return { success: false, error: 'No execution context available' };
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
const script = createInjectMessageScript(message);
|
|
1300
|
-
|
|
1301
|
-
try {
|
|
1302
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
1303
|
-
expression: script,
|
|
1304
|
-
contextId: cdp.rootContextId,
|
|
1305
|
-
returnByValue: true,
|
|
1306
|
-
awaitPromise: true
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (result.exceptionDetails) {
|
|
1310
|
-
const errorMsg = result.exceptionDetails.exception?.description ||
|
|
1311
|
-
result.exceptionDetails.text ||
|
|
1312
|
-
'Unknown error';
|
|
1313
|
-
console.error('[Inject] Script exception:', errorMsg);
|
|
1314
|
-
return { success: false, error: errorMsg };
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const value = result.result?.value;
|
|
1318
|
-
if (!value) {
|
|
1319
|
-
return { success: false, error: 'No result from injection script' };
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (value.ok) {
|
|
1323
|
-
console.log(`[Inject] Message sent via ${value.method} (${value.inputType})`);
|
|
1324
|
-
return {
|
|
1325
|
-
success: true,
|
|
1326
|
-
method: value.method,
|
|
1327
|
-
inputType: value.inputType
|
|
1328
|
-
};
|
|
1329
|
-
} else {
|
|
1330
|
-
console.error('[Inject] Failed:', value.error, value.message);
|
|
1331
|
-
return { success: false, error: value.message || value.error };
|
|
1332
|
-
}
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
console.error('[Inject] CDP call failed:', err.message);
|
|
1335
|
-
return { success: false, error: err.message };
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// =============================================================================
|
|
1340
|
-
// Discovery Loop (Task 3)
|
|
1341
|
-
// =============================================================================
|
|
1342
|
-
|
|
1343
|
-
/**
|
|
1344
|
-
* Discover CDP targets across all configured ports
|
|
1345
|
-
* Scans ports 9000-9003, connects to:
|
|
1346
|
-
* 1. Kiro Agent webview (for chat)
|
|
1347
|
-
* 2. Main VS Code window (for sidebar, editor)
|
|
1348
|
-
*/
|
|
1349
|
-
async function discoverTargets() {
|
|
1350
|
-
console.log('[Discovery] Scanning for CDP targets...');
|
|
1351
|
-
|
|
1352
|
-
// Track which cascade IDs we find in this scan
|
|
1353
|
-
const foundCascadeIds = new Set();
|
|
1354
|
-
let foundMainWindow = false;
|
|
1355
|
-
|
|
1356
|
-
// 3.1 Scan all CDP ports for targets
|
|
1357
|
-
for (const port of CDP_PORTS) {
|
|
1358
|
-
try {
|
|
1359
|
-
const targets = await fetchCDPTargets(port);
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
// Find the main VS Code window (type: page, url starts with vscode-file://)
|
|
1364
|
-
const mainWindowTarget = targets.find(target => {
|
|
1365
|
-
const url = (target.url || '').toLowerCase();
|
|
1366
|
-
return target.type === 'page' &&
|
|
1367
|
-
(url.startsWith('vscode-file://') || url.includes('workbench')) &&
|
|
1368
|
-
target.webSocketDebuggerUrl;
|
|
1369
|
-
});
|
|
1370
|
-
|
|
1371
|
-
// Connect to main window for sidebar/editor
|
|
1372
|
-
if (mainWindowTarget && !mainWindowCDP.connection) {
|
|
1373
|
-
console.log(`[Discovery] Found main VS Code window: ${mainWindowTarget.title}`);
|
|
1374
|
-
try {
|
|
1375
|
-
const cdp = await connectToCDP(mainWindowTarget.webSocketDebuggerUrl);
|
|
1376
|
-
mainWindowCDP.connection = cdp;
|
|
1377
|
-
mainWindowCDP.id = generateCascadeId(mainWindowTarget.webSocketDebuggerUrl);
|
|
1378
|
-
foundMainWindow = true;
|
|
1379
|
-
console.log(`[Discovery] Connected to main window: ${mainWindowCDP.id}`);
|
|
1380
|
-
|
|
1381
|
-
// Set up disconnect handler
|
|
1382
|
-
cdp.ws.on('close', () => {
|
|
1383
|
-
console.log(`[Discovery] Main window disconnected`);
|
|
1384
|
-
mainWindowCDP.connection = null;
|
|
1385
|
-
mainWindowCDP.id = null;
|
|
1386
|
-
});
|
|
1387
|
-
} catch (err) {
|
|
1388
|
-
console.error(`[Discovery] Failed to connect to main window: ${err.message}`);
|
|
1389
|
-
}
|
|
1390
|
-
} else if (mainWindowTarget) {
|
|
1391
|
-
foundMainWindow = true;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// Find Kiro Agent webview (for chat)
|
|
1395
|
-
const kiroAgentTargets = targets.filter(target => {
|
|
1396
|
-
const url = (target.url || '').toLowerCase();
|
|
1397
|
-
return (url.includes('kiroagent') || url.includes('vscode-webview')) &&
|
|
1398
|
-
target.webSocketDebuggerUrl &&
|
|
1399
|
-
target.type !== 'page';
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
|
-
for (const target of kiroAgentTargets) {
|
|
1403
|
-
const wsUrl = target.webSocketDebuggerUrl;
|
|
1404
|
-
const cascadeId = generateCascadeId(wsUrl);
|
|
1405
|
-
foundCascadeIds.add(cascadeId);
|
|
1406
|
-
|
|
1407
|
-
// 3.3 Connect to new targets, reuse existing connections
|
|
1408
|
-
if (!cascades.has(cascadeId)) {
|
|
1409
|
-
console.log(`[Discovery] Found new Kiro Agent: ${target.title} (${cascadeId})`);
|
|
1410
|
-
|
|
1411
|
-
try {
|
|
1412
|
-
const cdp = await connectToCDP(wsUrl);
|
|
1413
|
-
|
|
1414
|
-
// Create cascade object
|
|
1415
|
-
const cascade = {
|
|
1416
|
-
id: cascadeId,
|
|
1417
|
-
cdp,
|
|
1418
|
-
metadata: {
|
|
1419
|
-
windowTitle: target.title || 'Unknown',
|
|
1420
|
-
chatTitle: '',
|
|
1421
|
-
isActive: true
|
|
1422
|
-
},
|
|
1423
|
-
snapshot: null,
|
|
1424
|
-
css: null,
|
|
1425
|
-
snapshotHash: null,
|
|
1426
|
-
// Panel snapshots (populated from main window)
|
|
1427
|
-
sidebar: null,
|
|
1428
|
-
sidebarHash: null,
|
|
1429
|
-
editor: null,
|
|
1430
|
-
editorHash: null
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
cascades.set(cascadeId, cascade);
|
|
1434
|
-
console.log(`[Discovery] Connected to cascade: ${cascadeId}`);
|
|
1435
|
-
|
|
1436
|
-
// Set up disconnect handler
|
|
1437
|
-
cdp.ws.on('close', () => {
|
|
1438
|
-
console.log(`[Discovery] Cascade disconnected: ${cascadeId}`);
|
|
1439
|
-
cascades.delete(cascadeId);
|
|
1440
|
-
broadcastCascadeList();
|
|
1441
|
-
});
|
|
1442
|
-
|
|
1443
|
-
// Broadcast updated cascade list to all clients
|
|
1444
|
-
broadcastCascadeList();
|
|
1445
|
-
|
|
1446
|
-
} catch (err) {
|
|
1447
|
-
console.error(`[Discovery] Failed to connect to ${cascadeId}: ${err.message}`);
|
|
1448
|
-
}
|
|
1449
|
-
} else {
|
|
1450
|
-
// Update metadata for existing connection
|
|
1451
|
-
const cascade = cascades.get(cascadeId);
|
|
1452
|
-
cascade.metadata.windowTitle = target.title || cascade.metadata.windowTitle;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
} catch (err) {
|
|
1456
|
-
// Port not available or no CDP server - silent unless debugging
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// 3.4 Clean up disconnected targets
|
|
1461
|
-
for (const [cascadeId, cascade] of cascades) {
|
|
1462
|
-
if (!foundCascadeIds.has(cascadeId)) {
|
|
1463
|
-
console.log(`[Discovery] Target no longer available: ${cascadeId}`);
|
|
1464
|
-
try {
|
|
1465
|
-
cascade.cdp.close();
|
|
1466
|
-
} catch (e) {}
|
|
1467
|
-
cascades.delete(cascadeId);
|
|
1468
|
-
broadcastCascadeList();
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
console.log(`[Discovery] Active cascades: ${cascades.size}${foundMainWindow ? ' (main window connected)' : ''}`);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
/**
|
|
1476
|
-
* Broadcast the current cascade list to all connected WebSocket clients
|
|
1477
|
-
*/
|
|
1478
|
-
function broadcastCascadeList() {
|
|
1479
|
-
const cascadeList = Array.from(cascades.values()).map(c => ({
|
|
1480
|
-
id: c.id,
|
|
1481
|
-
title: c.metadata.chatTitle || c.metadata.windowTitle,
|
|
1482
|
-
window: c.metadata.windowTitle,
|
|
1483
|
-
active: c.metadata.isActive
|
|
1484
|
-
}));
|
|
1485
|
-
|
|
1486
|
-
const message = JSON.stringify({
|
|
1487
|
-
type: 'cascade_list',
|
|
1488
|
-
cascades: cascadeList
|
|
1489
|
-
});
|
|
1490
|
-
|
|
1491
|
-
for (const client of wss.clients) {
|
|
1492
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1493
|
-
client.send(message);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Get local IP address for display
|
|
1499
|
-
function getLocalIP() {
|
|
1500
|
-
const interfaces = networkInterfaces();
|
|
1501
|
-
for (const name of Object.keys(interfaces)) {
|
|
1502
|
-
for (const iface of interfaces[name]) {
|
|
1503
|
-
// Skip internal and non-IPv4 addresses
|
|
1504
|
-
if (iface.family === 'IPv4' && !iface.internal) {
|
|
1505
|
-
return iface.address;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
return 'localhost';
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// Express app setup
|
|
1513
|
-
const app = express();
|
|
1514
|
-
app.use(express.json());
|
|
1515
|
-
|
|
1516
|
-
// Serve static files from public directory
|
|
1517
|
-
app.use(express.static(join(__dirname, 'public')));
|
|
1518
|
-
|
|
1519
|
-
// =============================================================================
|
|
1520
|
-
// REST API Endpoints (Task 5)
|
|
1521
|
-
// =============================================================================
|
|
1522
|
-
|
|
1523
|
-
/**
|
|
1524
|
-
* GET /cascades - List active chat sessions
|
|
1525
|
-
* Returns array of { id, title, window, active }
|
|
1526
|
-
*/
|
|
1527
|
-
app.get('/cascades', (req, res) => {
|
|
1528
|
-
const cascadeList = Array.from(cascades.values()).map(c => ({
|
|
1529
|
-
id: c.id,
|
|
1530
|
-
title: c.metadata?.chatTitle || c.metadata?.windowTitle || 'Unknown',
|
|
1531
|
-
window: c.metadata?.windowTitle || 'Unknown',
|
|
1532
|
-
active: c.metadata?.isActive || false
|
|
1533
|
-
}));
|
|
1534
|
-
res.json(cascadeList);
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1537
|
-
/**
|
|
1538
|
-
* GET /snapshot/:id - Get HTML snapshot for a specific cascade
|
|
1539
|
-
* Returns snapshot object { html, bodyBg, bodyColor } or 404
|
|
1540
|
-
*/
|
|
1541
|
-
app.get('/snapshot/:id', (req, res) => {
|
|
1542
|
-
const cascade = cascades.get(req.params.id);
|
|
1543
|
-
if (!cascade) {
|
|
1544
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1545
|
-
}
|
|
1546
|
-
if (!cascade.snapshot) {
|
|
1547
|
-
return res.status(404).json({ error: 'No snapshot available' });
|
|
1548
|
-
}
|
|
1549
|
-
res.json(cascade.snapshot);
|
|
1550
|
-
});
|
|
1551
|
-
|
|
1552
|
-
/**
|
|
1553
|
-
* GET /snapshot - Get snapshot of first active cascade (convenience endpoint)
|
|
1554
|
-
* Returns snapshot object or 404 if no cascades available
|
|
1555
|
-
*/
|
|
1556
|
-
app.get('/snapshot', (req, res) => {
|
|
1557
|
-
const firstCascade = cascades.values().next().value;
|
|
1558
|
-
if (!firstCascade) {
|
|
1559
|
-
return res.status(404).json({ error: 'No cascades available' });
|
|
1560
|
-
}
|
|
1561
|
-
if (!firstCascade.snapshot) {
|
|
1562
|
-
return res.status(404).json({ error: 'No snapshot available' });
|
|
1563
|
-
}
|
|
1564
|
-
res.json(firstCascade.snapshot);
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
/**
|
|
1568
|
-
* GET /debug/:id - Debug endpoint to discover DOM structure
|
|
1569
|
-
* Returns list of potential chat elements
|
|
1570
|
-
*/
|
|
1571
|
-
app.get('/debug/:id', async (req, res) => {
|
|
1572
|
-
const cascade = cascades.get(req.params.id);
|
|
1573
|
-
if (!cascade) {
|
|
1574
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Instead of looking at DOM, let's list ALL CDP targets
|
|
1578
|
-
const results = [];
|
|
1579
|
-
|
|
1580
|
-
for (const port of CDP_PORTS) {
|
|
1581
|
-
try {
|
|
1582
|
-
const targets = await fetchCDPTargets(port);
|
|
1583
|
-
targets.forEach(t => {
|
|
1584
|
-
results.push({
|
|
1585
|
-
port,
|
|
1586
|
-
type: t.type,
|
|
1587
|
-
title: t.title,
|
|
1588
|
-
url: t.url?.substring(0, 100),
|
|
1589
|
-
hasWsUrl: !!t.webSocketDebuggerUrl
|
|
1590
|
-
});
|
|
1591
|
-
});
|
|
1592
|
-
} catch (e) {
|
|
1593
|
-
// ignore
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
res.json(results);
|
|
1598
|
-
});
|
|
1599
|
-
|
|
1600
|
-
/**
|
|
1601
|
-
* GET /dom/:id - Debug endpoint to see actual DOM content
|
|
1602
|
-
*/
|
|
1603
|
-
app.get('/dom/:id', async (req, res) => {
|
|
1604
|
-
const cascade = cascades.get(req.params.id);
|
|
1605
|
-
if (!cascade) {
|
|
1606
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
const script = `
|
|
1610
|
-
(function() {
|
|
1611
|
-
// Check for nested iframe (VS Code webview pattern)
|
|
1612
|
-
const activeFrame = document.getElementById('active-frame');
|
|
1613
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
1614
|
-
const innerBody = activeFrame.contentDocument.body;
|
|
1615
|
-
return {
|
|
1616
|
-
type: 'nested-iframe',
|
|
1617
|
-
url: window.location.href,
|
|
1618
|
-
innerURL: activeFrame.src,
|
|
1619
|
-
innerBodyHTML: innerBody ? innerBody.innerHTML.substring(0, 5000) : 'no inner body',
|
|
1620
|
-
innerBodyChildCount: innerBody ? innerBody.children.length : 0,
|
|
1621
|
-
innerDivs: innerBody ? Array.from(innerBody.querySelectorAll('div')).slice(0, 30).map(d => ({
|
|
1622
|
-
id: d.id,
|
|
1623
|
-
className: d.className?.substring?.(0, 100) || '',
|
|
1624
|
-
childCount: d.children.length
|
|
1625
|
-
})) : []
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
return {
|
|
1630
|
-
type: 'direct',
|
|
1631
|
-
url: window.location.href,
|
|
1632
|
-
title: document.title,
|
|
1633
|
-
bodyHTML: document.body ? document.body.innerHTML.substring(0, 5000) : 'no body',
|
|
1634
|
-
bodyChildCount: document.body ? document.body.children.length : 0,
|
|
1635
|
-
hasActiveFrame: !!activeFrame,
|
|
1636
|
-
activeFrameSrc: activeFrame?.src
|
|
1637
|
-
};
|
|
1638
|
-
})()
|
|
1639
|
-
`;
|
|
1640
|
-
|
|
1641
|
-
try {
|
|
1642
|
-
const result = await cascade.cdp.call('Runtime.evaluate', {
|
|
1643
|
-
expression: script,
|
|
1644
|
-
contextId: cascade.cdp.rootContextId,
|
|
1645
|
-
returnByValue: true
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
|
-
res.json(result.result?.value || { error: 'no result' });
|
|
1649
|
-
} catch (err) {
|
|
1650
|
-
res.status(500).json({ error: err.message });
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
|
|
1654
|
-
/**
|
|
1655
|
-
* GET /styles/:id - Get CSS for a specific cascade
|
|
1656
|
-
* Returns CSS string or 404
|
|
1657
|
-
*/
|
|
1658
|
-
app.get('/styles/:id', (req, res) => {
|
|
1659
|
-
const cascade = cascades.get(req.params.id);
|
|
1660
|
-
if (!cascade) {
|
|
1661
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1662
|
-
}
|
|
1663
|
-
if (!cascade.css) {
|
|
1664
|
-
return res.status(404).json({ error: 'No styles available' });
|
|
1665
|
-
}
|
|
1666
|
-
res.type('text/css').send(cascade.css);
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
/**
|
|
1670
|
-
* GET /sidebar/:id - Get Sidebar snapshot for a specific cascade
|
|
1671
|
-
* Returns sidebar object { html, files, kiroPanels, hasContent } or 404
|
|
1672
|
-
*/
|
|
1673
|
-
app.get('/sidebar/:id', (req, res) => {
|
|
1674
|
-
const cascade = cascades.get(req.params.id);
|
|
1675
|
-
if (!cascade) {
|
|
1676
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1677
|
-
}
|
|
1678
|
-
if (!cascade.sidebar || !cascade.sidebar.hasContent) {
|
|
1679
|
-
return res.status(404).json({ error: 'No sidebar content available' });
|
|
1680
|
-
}
|
|
1681
|
-
res.json(cascade.sidebar);
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* GET /editor/:id - Get Editor snapshot for a specific cascade
|
|
1686
|
-
* Returns editor object { html, fileName, language, content, lineCount, hasContent } or 404
|
|
1687
|
-
*/
|
|
1688
|
-
app.get('/editor/:id', (req, res) => {
|
|
1689
|
-
const cascade = cascades.get(req.params.id);
|
|
1690
|
-
if (!cascade) {
|
|
1691
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1692
|
-
}
|
|
1693
|
-
if (!cascade.editor || !cascade.editor.hasContent) {
|
|
1694
|
-
return res.status(404).json({ error: 'No editor content available' });
|
|
1695
|
-
}
|
|
1696
|
-
res.json(cascade.editor);
|
|
1697
|
-
});
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* POST /readFile/:id - Read a file directly from the filesystem
|
|
1701
|
-
* Body: { filePath: string }
|
|
1702
|
-
* Returns { content, fileName, language, lineCount, hasContent: true }
|
|
1703
|
-
* This bypasses Monaco's virtual scrolling limitation by reading the file directly
|
|
1704
|
-
*/
|
|
1705
|
-
app.post('/readFile/:id', async (req, res) => {
|
|
1706
|
-
const cascade = cascades.get(req.params.id);
|
|
1707
|
-
if (!cascade) {
|
|
1708
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
const { filePath } = req.body;
|
|
1712
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
1713
|
-
return res.status(400).json({ error: 'filePath is required' });
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
console.log(`[ReadFile] Reading file: ${filePath}`);
|
|
1717
|
-
|
|
1718
|
-
try {
|
|
1719
|
-
const fs = await import('fs/promises');
|
|
1720
|
-
const path = await import('path');
|
|
1721
|
-
|
|
1722
|
-
// Helper function to recursively find a file by name
|
|
1723
|
-
async function findFileRecursive(dir, fileName, maxDepth = 4, currentDepth = 0) {
|
|
1724
|
-
if (currentDepth > maxDepth) return null;
|
|
1725
|
-
|
|
1726
|
-
try {
|
|
1727
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1728
|
-
|
|
1729
|
-
// First check if file exists directly in this directory
|
|
1730
|
-
for (const entry of entries) {
|
|
1731
|
-
if (entry.isFile() && entry.name === fileName) {
|
|
1732
|
-
return path.join(dir, entry.name);
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Then search subdirectories (skip node_modules, .git, etc. but allow .kiro)
|
|
1737
|
-
for (const entry of entries) {
|
|
1738
|
-
if (entry.isDirectory() &&
|
|
1739
|
-
(!entry.name.startsWith('.') || entry.name === '.kiro') &&
|
|
1740
|
-
entry.name !== 'node_modules' &&
|
|
1741
|
-
entry.name !== 'dist' &&
|
|
1742
|
-
entry.name !== 'build' &&
|
|
1743
|
-
entry.name !== '.next') {
|
|
1744
|
-
const found = await findFileRecursive(
|
|
1745
|
-
path.join(dir, entry.name),
|
|
1746
|
-
fileName,
|
|
1747
|
-
maxDepth,
|
|
1748
|
-
currentDepth + 1
|
|
1749
|
-
);
|
|
1750
|
-
if (found) return found;
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
} catch (e) {
|
|
1754
|
-
// Directory not accessible
|
|
1755
|
-
}
|
|
1756
|
-
return null;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// Try to get workspace root from VS Code via CDP
|
|
1760
|
-
let workspaceRoot = null;
|
|
1761
|
-
if (mainWindowCDP.connection && mainWindowCDP.connection.rootContextId) {
|
|
1762
|
-
try {
|
|
1763
|
-
const wsScript = `
|
|
1764
|
-
(function() {
|
|
1765
|
-
// Try to get workspace folder from VS Code API
|
|
1766
|
-
if (typeof acquireVsCodeApi !== 'undefined') {
|
|
1767
|
-
return null; // Can't access workspace from webview
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// Try to find workspace path from window title or breadcrumbs
|
|
1771
|
-
let targetDoc = document;
|
|
1772
|
-
const activeFrame = document.getElementById('active-frame');
|
|
1773
|
-
if (activeFrame && activeFrame.contentDocument) {
|
|
1774
|
-
targetDoc = activeFrame.contentDocument;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// Look for breadcrumb path
|
|
1778
|
-
const breadcrumb = targetDoc.querySelector('.monaco-breadcrumbs, .breadcrumbs-control');
|
|
1779
|
-
if (breadcrumb) {
|
|
1780
|
-
const parts = breadcrumb.textContent.split(/[/\\\\]/);
|
|
1781
|
-
if (parts.length > 1) {
|
|
1782
|
-
return parts.slice(0, -1).join('/');
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
// Try to get from window title
|
|
1787
|
-
const title = document.title || '';
|
|
1788
|
-
const match = title.match(/- ([A-Za-z]:[^-]+|\/[^-]+)/);
|
|
1789
|
-
if (match) {
|
|
1790
|
-
return match[1].trim();
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
return null;
|
|
1794
|
-
})()
|
|
1795
|
-
`;
|
|
1796
|
-
|
|
1797
|
-
const wsResult = await mainWindowCDP.connection.call('Runtime.evaluate', {
|
|
1798
|
-
expression: wsScript,
|
|
1799
|
-
contextId: mainWindowCDP.connection.rootContextId,
|
|
1800
|
-
returnByValue: true
|
|
1801
|
-
});
|
|
1802
|
-
|
|
1803
|
-
if (wsResult.result && wsResult.result.value) {
|
|
1804
|
-
workspaceRoot = wsResult.result.value;
|
|
1805
|
-
}
|
|
1806
|
-
} catch (e) {
|
|
1807
|
-
console.log('[ReadFile] Could not get workspace root from CDP:', e.message);
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// Try multiple possible paths
|
|
1812
|
-
const possiblePaths = [];
|
|
1813
|
-
const fileName = path.basename(filePath);
|
|
1814
|
-
|
|
1815
|
-
// If path is absolute, use it directly
|
|
1816
|
-
if (path.isAbsolute(filePath)) {
|
|
1817
|
-
possiblePaths.push(filePath);
|
|
1818
|
-
} else {
|
|
1819
|
-
// Try relative to workspace root if we have it
|
|
1820
|
-
if (workspaceRoot) {
|
|
1821
|
-
possiblePaths.push(path.join(workspaceRoot, filePath));
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// Try relative to common workspace locations
|
|
1825
|
-
const commonRoots = [
|
|
1826
|
-
process.cwd(),
|
|
1827
|
-
path.dirname(__dirname), // Parent of kiro-mobile-bridge
|
|
1828
|
-
path.join(path.dirname(__dirname), '..'), // Two levels up
|
|
1829
|
-
];
|
|
1830
|
-
|
|
1831
|
-
for (const root of commonRoots) {
|
|
1832
|
-
possiblePaths.push(path.join(root, filePath));
|
|
1833
|
-
// Also try in public subdirectory (common for web projects)
|
|
1834
|
-
possiblePaths.push(path.join(root, 'public', filePath));
|
|
1835
|
-
possiblePaths.push(path.join(root, 'src', filePath));
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
// Try each path until we find the file
|
|
1840
|
-
let content = null;
|
|
1841
|
-
let foundPath = null;
|
|
1842
|
-
|
|
1843
|
-
for (const tryPath of possiblePaths) {
|
|
1844
|
-
try {
|
|
1845
|
-
content = await fs.readFile(tryPath, 'utf-8');
|
|
1846
|
-
foundPath = tryPath;
|
|
1847
|
-
console.log(`[ReadFile] Found file at: ${tryPath}`);
|
|
1848
|
-
break;
|
|
1849
|
-
} catch (e) {
|
|
1850
|
-
// File not found at this path, try next
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
// If still not found, do a recursive search from workspace roots
|
|
1855
|
-
if (!content) {
|
|
1856
|
-
console.log(`[ReadFile] Direct paths failed, searching recursively for: ${fileName}`);
|
|
1857
|
-
const searchRoots = [
|
|
1858
|
-
process.cwd(),
|
|
1859
|
-
path.dirname(__dirname),
|
|
1860
|
-
];
|
|
1861
|
-
|
|
1862
|
-
for (const root of searchRoots) {
|
|
1863
|
-
foundPath = await findFileRecursive(root, fileName);
|
|
1864
|
-
if (foundPath) {
|
|
1865
|
-
try {
|
|
1866
|
-
content = await fs.readFile(foundPath, 'utf-8');
|
|
1867
|
-
console.log(`[ReadFile] Found file via recursive search: ${foundPath}`);
|
|
1868
|
-
break;
|
|
1869
|
-
} catch (e) {
|
|
1870
|
-
foundPath = null;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
if (!content) {
|
|
1877
|
-
console.log(`[ReadFile] File not found. Tried paths:`, possiblePaths);
|
|
1878
|
-
return res.status(404).json({ error: 'File not found', triedPaths: possiblePaths, searchedFor: fileName });
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// Detect language from file extension
|
|
1882
|
-
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
1883
|
-
const extMap = {
|
|
1884
|
-
'ts': 'typescript', 'tsx': 'typescript',
|
|
1885
|
-
'js': 'javascript', 'jsx': 'javascript',
|
|
1886
|
-
'py': 'python', 'java': 'java',
|
|
1887
|
-
'html': 'html', 'css': 'css',
|
|
1888
|
-
'json': 'json', 'md': 'markdown',
|
|
1889
|
-
'yaml': 'yaml', 'yml': 'yaml',
|
|
1890
|
-
'xml': 'xml', 'sql': 'sql',
|
|
1891
|
-
'go': 'go', 'rs': 'rust',
|
|
1892
|
-
'c': 'c', 'cpp': 'cpp', 'h': 'c',
|
|
1893
|
-
'cs': 'csharp', 'rb': 'ruby',
|
|
1894
|
-
'php': 'php', 'sh': 'bash',
|
|
1895
|
-
'vue': 'vue', 'svelte': 'svelte',
|
|
1896
|
-
'cob': 'cobol', 'cbl': 'cobol'
|
|
1897
|
-
};
|
|
1898
|
-
|
|
1899
|
-
const language = extMap[ext] || ext || '';
|
|
1900
|
-
const lines = content.split('\n');
|
|
1901
|
-
|
|
1902
|
-
res.json({
|
|
1903
|
-
content,
|
|
1904
|
-
fileName: path.basename(filePath),
|
|
1905
|
-
fullPath: foundPath,
|
|
1906
|
-
language,
|
|
1907
|
-
lineCount: lines.length,
|
|
1908
|
-
hasContent: true,
|
|
1909
|
-
startLine: 1,
|
|
1910
|
-
isPartial: false
|
|
1911
|
-
});
|
|
1912
|
-
|
|
1913
|
-
} catch (err) {
|
|
1914
|
-
console.error(`[ReadFile] Error:`, err.message);
|
|
1915
|
-
res.status(500).json({ error: err.message });
|
|
1916
|
-
}
|
|
1917
|
-
});
|
|
1918
|
-
|
|
1919
|
-
/**
|
|
1920
|
-
* GET /files/:id - List all code files in the workspace
|
|
1921
|
-
* Returns { files: [{ name, path, language }] }
|
|
1922
|
-
* Scans workspace directory for code files (filtered by extension)
|
|
1923
|
-
*/
|
|
1924
|
-
app.get('/files/:id', async (req, res) => {
|
|
1925
|
-
const cascade = cascades.get(req.params.id);
|
|
1926
|
-
if (!cascade) {
|
|
1927
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
try {
|
|
1931
|
-
const fs = await import('fs/promises');
|
|
1932
|
-
const path = await import('path');
|
|
1933
|
-
|
|
1934
|
-
// Code file extensions to include
|
|
1935
|
-
const codeExtensions = new Set([
|
|
1936
|
-
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
1937
|
-
'.py', '.java', '.go', '.rs', '.rb', '.php',
|
|
1938
|
-
'.html', '.css', '.scss', '.sass', '.less',
|
|
1939
|
-
'.json', '.yaml', '.yml', '.xml', '.toml',
|
|
1940
|
-
'.md', '.mdx', '.txt',
|
|
1941
|
-
'.sql', '.graphql', '.gql',
|
|
1942
|
-
'.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
|
|
1943
|
-
'.c', '.cpp', '.h', '.hpp', '.cs',
|
|
1944
|
-
'.vue', '.svelte', '.astro',
|
|
1945
|
-
'.env', '.gitignore', '.dockerignore',
|
|
1946
|
-
'.cob', '.cbl'
|
|
1947
|
-
]);
|
|
1948
|
-
|
|
1949
|
-
// Extension to language mapping
|
|
1950
|
-
const extToLang = {
|
|
1951
|
-
'.ts': 'typescript', '.tsx': 'typescript',
|
|
1952
|
-
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
1953
|
-
'.py': 'python', '.java': 'java', '.go': 'go', '.rs': 'rust',
|
|
1954
|
-
'.rb': 'ruby', '.php': 'php',
|
|
1955
|
-
'.html': 'html', '.css': 'css', '.scss': 'scss', '.sass': 'sass', '.less': 'less',
|
|
1956
|
-
'.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.xml': 'xml', '.toml': 'toml',
|
|
1957
|
-
'.md': 'markdown', '.mdx': 'markdown',
|
|
1958
|
-
'.sql': 'sql', '.graphql': 'graphql', '.gql': 'graphql',
|
|
1959
|
-
'.sh': 'bash', '.bash': 'bash', '.zsh': 'zsh', '.ps1': 'powershell', '.bat': 'batch', '.cmd': 'batch',
|
|
1960
|
-
'.c': 'c', '.cpp': 'cpp', '.h': 'c', '.hpp': 'cpp', '.cs': 'csharp',
|
|
1961
|
-
'.vue': 'vue', '.svelte': 'svelte', '.astro': 'astro',
|
|
1962
|
-
'.cob': 'cobol', '.cbl': 'cobol'
|
|
1963
|
-
};
|
|
1964
|
-
|
|
1965
|
-
// Try to get workspace root from Kiro/VS Code window title
|
|
1966
|
-
let workspaceRoot = null;
|
|
1967
|
-
|
|
1968
|
-
// Method 1: Try to extract from main window CDP (VS Code window title contains workspace path)
|
|
1969
|
-
if (mainWindowCDP.connection && mainWindowCDP.connection.rootContextId) {
|
|
1970
|
-
try {
|
|
1971
|
-
const titleScript = `document.title`;
|
|
1972
|
-
const titleResult = await mainWindowCDP.connection.call('Runtime.evaluate', {
|
|
1973
|
-
expression: titleScript,
|
|
1974
|
-
contextId: mainWindowCDP.connection.rootContextId,
|
|
1975
|
-
returnByValue: true
|
|
1976
|
-
});
|
|
1977
|
-
|
|
1978
|
-
const windowTitle = titleResult.result?.value || '';
|
|
1979
|
-
// VS Code/Kiro title format: "filename - foldername - Kiro" or "foldername - Kiro"
|
|
1980
|
-
// Extract the workspace folder name from title
|
|
1981
|
-
const titleParts = windowTitle.split(' - ');
|
|
1982
|
-
if (titleParts.length >= 2) {
|
|
1983
|
-
// The folder name is usually the second-to-last part before "Kiro"
|
|
1984
|
-
const folderName = titleParts[titleParts.length - 2].trim();
|
|
1985
|
-
|
|
1986
|
-
// Try to find this folder in common locations
|
|
1987
|
-
const possibleRoots = [
|
|
1988
|
-
process.cwd(),
|
|
1989
|
-
path.join(process.env.HOME || process.env.USERPROFILE || '', folderName),
|
|
1990
|
-
path.join(process.env.HOME || process.env.USERPROFILE || '', 'projects', folderName),
|
|
1991
|
-
path.join(process.env.HOME || process.env.USERPROFILE || '', 'dev', folderName),
|
|
1992
|
-
path.join(process.env.HOME || process.env.USERPROFILE || '', 'code', folderName),
|
|
1993
|
-
path.join(process.env.HOME || process.env.USERPROFILE || '', 'workspace', folderName),
|
|
1994
|
-
// Windows common paths
|
|
1995
|
-
path.join('C:', 'Users', process.env.USERNAME || '', folderName),
|
|
1996
|
-
path.join('C:', 'gab', folderName),
|
|
1997
|
-
path.join('C:', 'dev', folderName),
|
|
1998
|
-
path.join('C:', 'projects', folderName),
|
|
1999
|
-
];
|
|
2000
|
-
|
|
2001
|
-
for (const root of possibleRoots) {
|
|
2002
|
-
try {
|
|
2003
|
-
const stat = await fs.stat(root);
|
|
2004
|
-
if (stat.isDirectory()) {
|
|
2005
|
-
// Verify it looks like a project (has package.json, .git, or src folder)
|
|
2006
|
-
const hasPackageJson = await fs.access(path.join(root, 'package.json')).then(() => true).catch(() => false);
|
|
2007
|
-
const hasGit = await fs.access(path.join(root, '.git')).then(() => true).catch(() => false);
|
|
2008
|
-
const hasSrc = await fs.access(path.join(root, 'src')).then(() => true).catch(() => false);
|
|
2009
|
-
|
|
2010
|
-
if (hasPackageJson || hasGit || hasSrc) {
|
|
2011
|
-
workspaceRoot = root;
|
|
2012
|
-
break;
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
} catch (e) {
|
|
2016
|
-
// Path doesn't exist, try next
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
} catch (e) {
|
|
2021
|
-
// CDP call failed, continue with fallback
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// Method 2: Fallback to current working directory
|
|
2026
|
-
if (!workspaceRoot) {
|
|
2027
|
-
workspaceRoot = process.cwd();
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
const files = [];
|
|
2031
|
-
|
|
2032
|
-
// Recursive function to collect files
|
|
2033
|
-
async function collectFiles(dir, relativePath = '', depth = 0) {
|
|
2034
|
-
if (depth > 5) return; // Max depth
|
|
2035
|
-
|
|
2036
|
-
try {
|
|
2037
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2038
|
-
|
|
2039
|
-
for (const entry of entries) {
|
|
2040
|
-
// Skip hidden files/folders EXCEPT .kiro and .github, and common non-code directories
|
|
2041
|
-
if ((entry.name.startsWith('.') && entry.name !== '.kiro' && entry.name !== '.github') ||
|
|
2042
|
-
entry.name === 'node_modules' ||
|
|
2043
|
-
entry.name === 'dist' ||
|
|
2044
|
-
entry.name === 'build' ||
|
|
2045
|
-
entry.name === '.next' ||
|
|
2046
|
-
entry.name === '__pycache__' ||
|
|
2047
|
-
entry.name === 'venv' ||
|
|
2048
|
-
entry.name === 'coverage') {
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
const entryPath = path.join(dir, entry.name);
|
|
2053
|
-
const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
2054
|
-
|
|
2055
|
-
if (entry.isFile()) {
|
|
2056
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
2057
|
-
if (codeExtensions.has(ext) || entry.name === 'Dockerfile' || entry.name === 'Makefile') {
|
|
2058
|
-
files.push({
|
|
2059
|
-
name: entry.name,
|
|
2060
|
-
path: entryRelative,
|
|
2061
|
-
language: extToLang[ext] || ext.slice(1) || 'text'
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
} else if (entry.isDirectory()) {
|
|
2065
|
-
await collectFiles(entryPath, entryRelative, depth + 1);
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
} catch (e) {
|
|
2069
|
-
// Directory not accessible
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
await collectFiles(workspaceRoot);
|
|
2074
|
-
|
|
2075
|
-
// Sort files: by path for easier browsing
|
|
2076
|
-
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2077
|
-
|
|
2078
|
-
console.log(`[Files] Found ${files.length} files in ${workspaceRoot}`);
|
|
2079
|
-
res.json({ files, workspaceRoot });
|
|
2080
|
-
|
|
2081
|
-
} catch (err) {
|
|
2082
|
-
console.error(`[Files] Error:`, err.message);
|
|
2083
|
-
res.status(500).json({ error: err.message });
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
/**
|
|
2088
|
-
* POST /openFile/:id - Open a file in the Kiro editor
|
|
2089
|
-
* Body: { filePath: string }
|
|
2090
|
-
* Uses VS Code command to open the file
|
|
2091
|
-
*/
|
|
2092
|
-
app.post('/openFile/:id', async (req, res) => {
|
|
2093
|
-
const cascade = cascades.get(req.params.id);
|
|
2094
|
-
if (!cascade) {
|
|
2095
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
const { filePath } = req.body;
|
|
2099
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
2100
|
-
return res.status(400).json({ error: 'filePath is required' });
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// Use main window CDP to execute VS Code command
|
|
2104
|
-
if (!mainWindowCDP.connection || !mainWindowCDP.connection.rootContextId) {
|
|
2105
|
-
return res.status(503).json({ error: 'Main window CDP connection not available' });
|
|
2106
|
-
}
|
|
67
|
+
async function discoverTargets() {
|
|
68
|
+
const foundCascadeIds = new Set();
|
|
69
|
+
let foundMainWindow = false;
|
|
70
|
+
let stateChanged = false;
|
|
2107
71
|
|
|
2108
|
-
|
|
72
|
+
const portResults = await Promise.allSettled(
|
|
73
|
+
CDP_PORTS.map(port => fetchCDPTargets(port).then(targets => ({ port, targets })))
|
|
74
|
+
);
|
|
2109
75
|
|
|
2110
|
-
|
|
2111
|
-
|
|
76
|
+
for (const result of portResults) {
|
|
77
|
+
if (result.status !== 'fulfilled') continue;
|
|
78
|
+
const { port, targets } = result.value;
|
|
2112
79
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
(
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
80
|
+
try {
|
|
81
|
+
// Find main VS Code window
|
|
82
|
+
const mainWindowTarget = targets.find(target => {
|
|
83
|
+
const url = (target.url || '').toLowerCase();
|
|
84
|
+
return target.type === 'page' &&
|
|
85
|
+
(url.startsWith('vscode-file://') || url.includes('workbench')) &&
|
|
86
|
+
target.webSocketDebuggerUrl;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (mainWindowTarget && !mainWindowCDP.connection) {
|
|
90
|
+
console.log(`[Discovery] Found main VS Code window: ${mainWindowTarget.title}`);
|
|
91
|
+
try {
|
|
92
|
+
const cdp = await connectToCDP(mainWindowTarget.webSocketDebuggerUrl);
|
|
93
|
+
mainWindowCDP.connection = cdp;
|
|
94
|
+
mainWindowCDP.id = generateId(mainWindowTarget.webSocketDebuggerUrl);
|
|
95
|
+
foundMainWindow = true;
|
|
96
|
+
stateChanged = true;
|
|
97
|
+
|
|
98
|
+
cdp.ws.on('close', () => {
|
|
99
|
+
console.log(`[Discovery] Main window disconnected`);
|
|
100
|
+
mainWindowCDP.connection = null;
|
|
101
|
+
mainWindowCDP.id = null;
|
|
102
|
+
adjustDiscoveryInterval(true);
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error(`[Discovery] Failed to connect to main window: ${err.message}`);
|
|
2130
106
|
}
|
|
107
|
+
} else if (mainWindowTarget) {
|
|
108
|
+
foundMainWindow = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find Kiro Agent webviews
|
|
112
|
+
const kiroAgentTargets = targets.filter(target => {
|
|
113
|
+
const url = (target.url || '').toLowerCase();
|
|
114
|
+
return (url.includes('kiroagent') || url.includes('vscode-webview')) &&
|
|
115
|
+
target.webSocketDebuggerUrl && target.type !== 'page';
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
for (const target of kiroAgentTargets) {
|
|
119
|
+
const wsUrl = target.webSocketDebuggerUrl;
|
|
120
|
+
const cascadeId = generateId(wsUrl);
|
|
121
|
+
foundCascadeIds.add(cascadeId);
|
|
2131
122
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
for (const link of fileLinks) {
|
|
2135
|
-
const text = link.textContent || '';
|
|
2136
|
-
const href = link.getAttribute('href') || '';
|
|
2137
|
-
const dataPath = link.getAttribute('data-path') || '';
|
|
123
|
+
if (!cascades.has(cascadeId)) {
|
|
124
|
+
stateChanged = true;
|
|
2138
125
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
126
|
+
try {
|
|
127
|
+
const cdp = await connectToCDP(wsUrl);
|
|
128
|
+
|
|
129
|
+
cascades.set(cascadeId, {
|
|
130
|
+
id: cascadeId,
|
|
131
|
+
cdp,
|
|
132
|
+
metadata: { windowTitle: target.title || 'Unknown', chatTitle: '', isActive: true },
|
|
133
|
+
snapshot: null,
|
|
134
|
+
css: null,
|
|
135
|
+
snapshotHash: null,
|
|
136
|
+
editor: null,
|
|
137
|
+
editorHash: null
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
cdp.ws.on('close', () => {
|
|
141
|
+
console.log(`[Discovery] Cascade disconnected: ${cascadeId}`);
|
|
142
|
+
cascades.delete(cascadeId);
|
|
143
|
+
broadcastCascadeList();
|
|
144
|
+
adjustDiscoveryInterval(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
broadcastCascadeList();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`[Discovery] Failed to connect to ${cascadeId}: ${err.message}`);
|
|
2142
150
|
}
|
|
151
|
+
} else {
|
|
152
|
+
cascades.get(cascadeId).metadata.windowTitle = target.title || cascades.get(cascadeId).metadata.windowTitle;
|
|
2143
153
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
})()
|
|
2149
|
-
`;
|
|
2150
|
-
|
|
2151
|
-
const evalResult = await cdp.call('Runtime.evaluate', {
|
|
2152
|
-
expression: script,
|
|
2153
|
-
contextId: cdp.rootContextId,
|
|
2154
|
-
returnByValue: true,
|
|
2155
|
-
awaitPromise: false
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
if (evalResult.result && evalResult.result.value) {
|
|
2159
|
-
res.json(evalResult.result.value);
|
|
2160
|
-
} else {
|
|
2161
|
-
res.json({ success: false, error: 'Script execution returned no result' });
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Log port scanning errors for debugging
|
|
157
|
+
console.debug(`[Discovery] Error scanning port ${port}: ${err.message}`);
|
|
2162
158
|
}
|
|
2163
|
-
} catch (err) {
|
|
2164
|
-
console.error(`[OpenFile] Error:`, err.message);
|
|
2165
|
-
res.status(500).json({ success: false, error: err.message });
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2168
|
-
|
|
2169
|
-
/**
|
|
2170
|
-
* POST /send/:id - Send message to a cascade
|
|
2171
|
-
* Body: { message: string }
|
|
2172
|
-
* Injects the message into the chat via CDP (Task 6)
|
|
2173
|
-
*/
|
|
2174
|
-
app.post('/send/:id', async (req, res) => {
|
|
2175
|
-
const cascade = cascades.get(req.params.id);
|
|
2176
|
-
if (!cascade) {
|
|
2177
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
const { message } = req.body;
|
|
2181
|
-
if (!message || typeof message !== 'string') {
|
|
2182
|
-
return res.status(400).json({ error: 'Message is required' });
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
if (!cascade.cdp) {
|
|
2186
|
-
return res.status(503).json({ error: 'CDP connection not available' });
|
|
2187
159
|
}
|
|
2188
160
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
res.status(500).json({
|
|
2202
|
-
success: false,
|
|
2203
|
-
error: result.error || 'Message injection failed'
|
|
2204
|
-
});
|
|
161
|
+
// Clean up disconnected targets
|
|
162
|
+
for (const [cascadeId, cascade] of cascades) {
|
|
163
|
+
if (!foundCascadeIds.has(cascadeId)) {
|
|
164
|
+
console.log(`[Discovery] Target no longer available: ${cascadeId}`);
|
|
165
|
+
stateChanged = true;
|
|
166
|
+
try {
|
|
167
|
+
cascade.cdp.close();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.debug(`[Discovery] Error closing cascade ${cascadeId}: ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
cascades.delete(cascadeId);
|
|
172
|
+
broadcastCascadeList();
|
|
2205
173
|
}
|
|
2206
|
-
} catch (err) {
|
|
2207
|
-
console.error(`[Send] Error injecting message:`, err.message);
|
|
2208
|
-
res.status(500).json({
|
|
2209
|
-
success: false,
|
|
2210
|
-
error: err.message
|
|
2211
|
-
});
|
|
2212
174
|
}
|
|
2213
|
-
});
|
|
2214
|
-
|
|
2215
|
-
/**
|
|
2216
|
-
* POST /click/:id - Click an element in the Kiro UI
|
|
2217
|
-
* Body: { tag, text, ariaLabel, title, role, className, id, relativeX, relativeY }
|
|
2218
|
-
* Finds and clicks the matching element via CDP
|
|
2219
|
-
*/
|
|
2220
|
-
app.post('/click/:id', async (req, res) => {
|
|
2221
|
-
const cascade = cascades.get(req.params.id);
|
|
2222
|
-
if (!cascade) {
|
|
2223
|
-
return res.status(404).json({ error: 'Cascade not found' });
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
if (!cascade.cdp || !cascade.cdp.rootContextId) {
|
|
2227
|
-
return res.status(503).json({ error: 'CDP connection not available' });
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
const clickInfo = req.body;
|
|
2231
|
-
console.log(`[Click] Attempting click:`, clickInfo.text?.substring(0, 30) || clickInfo.ariaLabel || clickInfo.tag);
|
|
2232
175
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
console.log(`[Click] Closing tab: "${clickInfo.parentTabLabel}"`);
|
|
2236
|
-
}
|
|
176
|
+
const mainWindowChanged = foundMainWindow !== pollingState.lastMainWindowConnected;
|
|
177
|
+
const cascadeCountChanged = cascades.size !== pollingState.lastCascadeCount;
|
|
2237
178
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
179
|
+
if (stateChanged || mainWindowChanged || cascadeCountChanged) {
|
|
180
|
+
console.log(`[Discovery] Active cascades: ${cascades.size}${foundMainWindow ? ' (main window connected)' : ''}`);
|
|
181
|
+
pollingState.lastCascadeCount = cascades.size;
|
|
182
|
+
pollingState.lastMainWindowConnected = foundMainWindow;
|
|
183
|
+
adjustDiscoveryInterval(true);
|
|
184
|
+
} else {
|
|
185
|
+
adjustDiscoveryInterval(false);
|
|
2244
186
|
}
|
|
2245
|
-
}
|
|
187
|
+
}
|
|
2246
188
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
targetDoc = activeFrame.contentDocument;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
const info = ${JSON.stringify(clickInfo)};
|
|
2261
|
-
let element = null;
|
|
2262
|
-
let matchMethod = '';
|
|
2263
|
-
let isTabClick = info.isTab || info.role === 'tab';
|
|
2264
|
-
let isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
|
|
2265
|
-
let isToggle = info.isToggle || info.role === 'switch';
|
|
2266
|
-
let isDropdown = info.isDropdown || info.ariaHaspopup;
|
|
2267
|
-
let isSendButton = info.isSendButton || (info.ariaLabel && info.ariaLabel.toLowerCase().includes('send'));
|
|
2268
|
-
|
|
2269
|
-
// Handle send button clicks
|
|
2270
|
-
if (isSendButton && !element) {
|
|
2271
|
-
// Try to find the send button (arrow-right icon button)
|
|
2272
|
-
const sendSelectors = [
|
|
2273
|
-
'svg.lucide-arrow-right',
|
|
2274
|
-
'svg[class*="arrow-right"]',
|
|
2275
|
-
'[data-tooltip-id*="send"]',
|
|
2276
|
-
'button[type="submit"]',
|
|
2277
|
-
'button[aria-label*="send" i]',
|
|
2278
|
-
'button[aria-label*="submit" i]',
|
|
2279
|
-
'[class*="send-button"]',
|
|
2280
|
-
'[class*="sendButton"]',
|
|
2281
|
-
'[class*="submit-button"]',
|
|
2282
|
-
'[class*="submitButton"]'
|
|
2283
|
-
];
|
|
2284
|
-
|
|
2285
|
-
for (const sel of sendSelectors) {
|
|
2286
|
-
try {
|
|
2287
|
-
const el = targetDoc.querySelector(sel);
|
|
2288
|
-
if (el) {
|
|
2289
|
-
element = el.closest('button') || el;
|
|
2290
|
-
if (element && !element.disabled) {
|
|
2291
|
-
matchMethod = 'send-button-' + sel.split('[')[0];
|
|
2292
|
-
break;
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
} catch(e) {}
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
// Handle toggle/switch clicks
|
|
2300
|
-
if (isToggle && !element) {
|
|
2301
|
-
// Find by toggle ID first
|
|
2302
|
-
if (info.toggleId) {
|
|
2303
|
-
element = targetDoc.getElementById(info.toggleId);
|
|
2304
|
-
if (element) matchMethod = 'toggle-id';
|
|
2305
|
-
}
|
|
2306
|
-
// Find by label text
|
|
2307
|
-
if (!element && info.text) {
|
|
2308
|
-
const toggles = targetDoc.querySelectorAll('.kiro-toggle-switch, [role="switch"]');
|
|
2309
|
-
for (const t of toggles) {
|
|
2310
|
-
const label = t.querySelector('label') || t.closest('.kiro-toggle-switch')?.querySelector('label');
|
|
2311
|
-
if (label && label.textContent.trim().toLowerCase().includes(info.text.toLowerCase())) {
|
|
2312
|
-
element = t.querySelector('input') || t;
|
|
2313
|
-
matchMethod = 'toggle-label';
|
|
2314
|
-
break;
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
// Fallback: find any toggle switch
|
|
2319
|
-
if (!element) {
|
|
2320
|
-
element = targetDoc.querySelector('#autonomy-mode-toggle-switch, .kiro-toggle-switch input, [role="switch"]');
|
|
2321
|
-
if (element) matchMethod = 'toggle-fallback';
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Snapshot Polling
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
async function pollSnapshots() {
|
|
194
|
+
let anyChanges = false;
|
|
195
|
+
|
|
196
|
+
for (const [cascadeId, cascade] of cascades) {
|
|
197
|
+
try {
|
|
198
|
+
const cdp = cascade.cdp;
|
|
2324
199
|
|
|
2325
|
-
//
|
|
2326
|
-
if (
|
|
2327
|
-
|
|
2328
|
-
if (info.text) {
|
|
2329
|
-
const dropdowns = targetDoc.querySelectorAll('.kiro-dropdown-trigger, [aria-haspopup="true"], [aria-haspopup="listbox"]');
|
|
2330
|
-
for (const d of dropdowns) {
|
|
2331
|
-
if (d.textContent.trim().toLowerCase().includes(info.text.toLowerCase())) {
|
|
2332
|
-
element = d;
|
|
2333
|
-
matchMethod = 'dropdown-text';
|
|
2334
|
-
break;
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
// Fallback: find any dropdown trigger
|
|
2339
|
-
if (!element) {
|
|
2340
|
-
element = targetDoc.querySelector('.kiro-dropdown-trigger, [aria-haspopup="true"]');
|
|
2341
|
-
if (element) matchMethod = 'dropdown-fallback';
|
|
2342
|
-
}
|
|
200
|
+
// Capture CSS once
|
|
201
|
+
if (cascade.css === null) {
|
|
202
|
+
cascade.css = await captureCSS(cdp);
|
|
2343
203
|
}
|
|
2344
204
|
|
|
2345
|
-
//
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
// FIXED: If parentTabLabel is provided, find the close button in that specific tab
|
|
2350
|
-
if (info.parentTabLabel) {
|
|
2351
|
-
const searchLabel = info.parentTabLabel.trim().toLowerCase();
|
|
2352
|
-
|
|
2353
|
-
for (const btn of closeButtons) {
|
|
2354
|
-
const parentTab = btn.closest('[role="tab"]');
|
|
2355
|
-
if (parentTab) {
|
|
2356
|
-
const labelEl = parentTab.querySelector('.kiro-tabs-item-label, [class*="label"]');
|
|
2357
|
-
const tabLabel = labelEl ? labelEl.textContent.trim().toLowerCase() : parentTab.textContent.trim().toLowerCase();
|
|
2358
|
-
|
|
2359
|
-
// Match the tab by its label
|
|
2360
|
-
if (tabLabel.includes(searchLabel) || searchLabel.includes(tabLabel)) {
|
|
2361
|
-
element = btn;
|
|
2362
|
-
matchMethod = 'close-button-by-tab-label';
|
|
2363
|
-
break;
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
} else {
|
|
2368
|
-
// Fallback: Original logic - find close button in selected tab
|
|
2369
|
-
for (const btn of closeButtons) {
|
|
2370
|
-
const parentTab = btn.closest('[role="tab"]');
|
|
2371
|
-
if (parentTab && parentTab.getAttribute('aria-selected') === 'true') {
|
|
2372
|
-
element = btn;
|
|
2373
|
-
matchMethod = 'close-button-selected-tab';
|
|
2374
|
-
break;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
// If still not found, use first close button as last resort
|
|
2380
|
-
if (!element && closeButtons.length > 0) {
|
|
2381
|
-
element = closeButtons[0];
|
|
2382
|
-
matchMethod = 'close-button-first';
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
205
|
+
// Capture metadata
|
|
206
|
+
const metadata = await captureMetadata(cdp);
|
|
207
|
+
cascade.metadata.chatTitle = metadata.chatTitle || cascade.metadata.chatTitle;
|
|
208
|
+
cascade.metadata.isActive = metadata.isActive;
|
|
2385
209
|
|
|
2386
|
-
//
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
'code',
|
|
2396
|
-
'span',
|
|
2397
|
-
'[class*="file"]',
|
|
2398
|
-
'[class*="link"]',
|
|
2399
|
-
'[class*="path"]'
|
|
2400
|
-
];
|
|
2401
|
-
|
|
2402
|
-
for (const selector of fileSelectors) {
|
|
2403
|
-
const candidates = targetDoc.querySelectorAll(selector);
|
|
2404
|
-
for (const el of candidates) {
|
|
2405
|
-
const text = (el.textContent || '').trim();
|
|
2406
|
-
const dataPath = el.getAttribute('data-path') || '';
|
|
2407
|
-
const href = el.getAttribute('href') || '';
|
|
2408
|
-
|
|
2409
|
-
if (text.includes(filePath) || text.includes(fileName) ||
|
|
2410
|
-
dataPath.includes(filePath) || dataPath.includes(fileName) ||
|
|
2411
|
-
href.includes(filePath) || href.includes(fileName)) {
|
|
2412
|
-
element = el;
|
|
2413
|
-
matchMethod = 'file-link-' + selector.split('[')[0];
|
|
2414
|
-
break;
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
if (element) break;
|
|
210
|
+
// Capture chat snapshot
|
|
211
|
+
const snapshot = await captureSnapshot(cdp);
|
|
212
|
+
if (snapshot) {
|
|
213
|
+
const newHash = computeHash(snapshot.html);
|
|
214
|
+
if (newHash !== cascade.snapshotHash) {
|
|
215
|
+
cascade.snapshot = snapshot;
|
|
216
|
+
cascade.snapshotHash = newHash;
|
|
217
|
+
broadcastSnapshotUpdate(cascadeId, 'chat');
|
|
218
|
+
anyChanges = true;
|
|
2418
219
|
}
|
|
2419
220
|
}
|
|
2420
221
|
|
|
2421
|
-
//
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
break;
|
|
222
|
+
// Capture editor from main window
|
|
223
|
+
// Store rootContextId locally to avoid race conditions during async operations
|
|
224
|
+
const mainCDP = mainWindowCDP.connection;
|
|
225
|
+
const contextId = mainCDP?.rootContextId;
|
|
226
|
+
if (mainCDP && contextId) {
|
|
227
|
+
const editor = await captureEditor(mainCDP);
|
|
228
|
+
if (editor?.hasContent) {
|
|
229
|
+
const editorHash = computeHash(editor.content + editor.fileName);
|
|
230
|
+
if (editorHash !== cascade.editorHash) {
|
|
231
|
+
cascade.editor = editor;
|
|
232
|
+
cascade.editorHash = editorHash;
|
|
233
|
+
broadcastSnapshotUpdate(cascadeId, 'editor');
|
|
234
|
+
anyChanges = true;
|
|
2435
235
|
}
|
|
236
|
+
} else if (cascade.editor?.hasContent) {
|
|
237
|
+
cascade.editor = { hasContent: false, fileName: '', content: '' };
|
|
238
|
+
cascade.editorHash = '';
|
|
239
|
+
broadcastSnapshotUpdate(cascadeId, 'editor');
|
|
240
|
+
anyChanges = true;
|
|
2436
241
|
}
|
|
2437
242
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
const label = (c.getAttribute('aria-label') || '').toLowerCase();
|
|
2446
|
-
if (!label.includes('close') && !label.includes('delete') && !label.includes('remove')) {
|
|
2447
|
-
element = c;
|
|
2448
|
-
matchMethod = 'aria-label';
|
|
2449
|
-
break;
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
} catch(e) {}
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
// 2. Try by title
|
|
2456
|
-
if (info.title && !element) {
|
|
2457
|
-
try {
|
|
2458
|
-
element = targetDoc.querySelector('[title="' + info.title.replace(/"/g, '\\\\"') + '"]');
|
|
2459
|
-
if (element) matchMethod = 'title';
|
|
2460
|
-
} catch(e) {}
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
// 3. Try by text content - search all clickable elements
|
|
2464
|
-
if (info.text && info.text.trim() && !element) {
|
|
2465
|
-
const searchText = info.text.trim();
|
|
2466
|
-
// Include divs and spans that might be clickable (like history items)
|
|
2467
|
-
const allElements = targetDoc.querySelectorAll('button, [role="button"], [role="tab"], [role="menuitem"], [role="switch"], [role="listitem"], [role="option"], a, [tabindex="0"], [class*="button"], [class*="btn"], [style*="cursor: pointer"], [style*="cursor:pointer"], div[class*="cursor-pointer"], div[class*="hover"], [onclick]');
|
|
2468
|
-
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`[Snapshot] Error polling cascade ${cascadeId}:`, err.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
adjustSnapshotInterval(anyChanges);
|
|
249
|
+
}
|
|
2469
250
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
if (!isCloseButton) {
|
|
2474
|
-
const ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
2475
|
-
if (ariaLabel.includes('close') || ariaLabel.includes('delete')) continue;
|
|
2476
|
-
if (el.classList.contains('kiro-tabs-item-close')) continue;
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
const elText = (el.textContent || '').trim();
|
|
2480
|
-
|
|
2481
|
-
// FIXED: More strict matching to prevent false positives
|
|
2482
|
-
// - Exact match: always OK
|
|
2483
|
-
// - searchText.includes(elText): ONLY if elText is meaningful (>= 10 chars)
|
|
2484
|
-
// - elText.includes(searchText): always OK (element contains what we're looking for)
|
|
2485
|
-
const isExactMatch = elText === searchText;
|
|
2486
|
-
const elementContainsSearch = searchText.length > 0 && elText.includes(searchText);
|
|
2487
|
-
const searchContainsElement = elText.length >= 10 && searchText.includes(elText); // Require minimum length
|
|
2488
|
-
|
|
2489
|
-
if (isExactMatch || elementContainsSearch || searchContainsElement) {
|
|
251
|
+
// =============================================================================
|
|
252
|
+
// Adaptive Polling
|
|
253
|
+
// =============================================================================
|
|
2490
254
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
element = el;
|
|
2508
|
-
matchMethod = 'aria-label-partial';
|
|
2509
|
-
break;
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
// 5. Try by role
|
|
2515
|
-
if (info.role && !element) {
|
|
2516
|
-
const candidates = targetDoc.querySelectorAll('[role="' + info.role + '"]');
|
|
2517
|
-
if (info.text) {
|
|
2518
|
-
for (const c of candidates) {
|
|
2519
|
-
if ((c.textContent || '').includes(info.text.substring(0, 15))) {
|
|
2520
|
-
element = c;
|
|
2521
|
-
matchMethod = 'role+text';
|
|
2522
|
-
break;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
// 6. FALLBACK: Search ALL elements by text content (for history items, etc.)
|
|
2529
|
-
if (info.text && info.text.trim() && !element) {
|
|
2530
|
-
const searchText = info.text.trim();
|
|
2531
|
-
// Search all elements that might be clickable based on their content
|
|
2532
|
-
const allElements = targetDoc.querySelectorAll('*');
|
|
2533
|
-
|
|
2534
|
-
for (const el of allElements) {
|
|
2535
|
-
// Skip script, style, and other non-visible elements
|
|
2536
|
-
if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(el.tagName)) continue;
|
|
2537
|
-
|
|
2538
|
-
const elText = (el.textContent || '').trim();
|
|
2539
|
-
|
|
2540
|
-
// Look for elements where the text matches closely
|
|
2541
|
-
if (elText === searchText || (elText.includes(searchText) && elText.length < searchText.length * 2)) {
|
|
2542
|
-
// Check if this element or its parent is clickable
|
|
2543
|
-
const style = window.getComputedStyle(el);
|
|
2544
|
-
const parentStyle = el.parentElement ? window.getComputedStyle(el.parentElement) : null;
|
|
2545
|
-
const isClickable = style.cursor === 'pointer' ||
|
|
2546
|
-
(parentStyle && parentStyle.cursor === 'pointer') ||
|
|
2547
|
-
el.onclick ||
|
|
2548
|
-
el.parentElement?.onclick ||
|
|
2549
|
-
el.closest('[role="button"]') ||
|
|
2550
|
-
el.closest('[tabindex]');
|
|
2551
|
-
|
|
2552
|
-
if (isClickable) {
|
|
2553
|
-
// Click the clickable parent if the element itself isn't clickable
|
|
2554
|
-
element = style.cursor === 'pointer' ? el : (el.closest('[style*="cursor: pointer"], [style*="cursor:pointer"]') || el.parentElement || el);
|
|
2555
|
-
matchMethod = 'fallback-text-search';
|
|
255
|
+
function adjustDiscoveryInterval(hasChanges) {
|
|
256
|
+
if (hasChanges) {
|
|
257
|
+
pollingState.stableCount = 0;
|
|
258
|
+
if (pollingState.discoveryIntervalMs !== DISCOVERY_INTERVAL_ACTIVE) {
|
|
259
|
+
pollingState.discoveryIntervalMs = DISCOVERY_INTERVAL_ACTIVE;
|
|
260
|
+
restartDiscoveryInterval();
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
pollingState.stableCount++;
|
|
264
|
+
if (pollingState.stableCount >= 3 && pollingState.discoveryIntervalMs !== DISCOVERY_INTERVAL_STABLE) {
|
|
265
|
+
pollingState.discoveryIntervalMs = DISCOVERY_INTERVAL_STABLE;
|
|
266
|
+
restartDiscoveryInterval();
|
|
267
|
+
console.log('[Discovery] Stable state, slowing to 30s interval');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
2556
271
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
try {
|
|
2570
|
-
element.click();
|
|
2571
|
-
return {
|
|
2572
|
-
found: true,
|
|
2573
|
-
clicked: true,
|
|
2574
|
-
matchMethod,
|
|
2575
|
-
tag: element.tagName,
|
|
2576
|
-
isTab: isTabClick,
|
|
2577
|
-
isCloseButton: isCloseButton
|
|
2578
|
-
};
|
|
2579
|
-
} catch (clickError) {
|
|
2580
|
-
// Fallback: try dispatching click event
|
|
2581
|
-
try {
|
|
2582
|
-
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
2583
|
-
return {
|
|
2584
|
-
found: true,
|
|
2585
|
-
clicked: true,
|
|
2586
|
-
matchMethod: matchMethod + '-dispatch',
|
|
2587
|
-
tag: element.tagName,
|
|
2588
|
-
isTab: isTabClick,
|
|
2589
|
-
isCloseButton: isCloseButton
|
|
2590
|
-
};
|
|
2591
|
-
} catch (e) {
|
|
2592
|
-
return { found: true, clicked: false, error: 'Click failed: ' + e.message, matchMethod };
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
})()
|
|
2596
|
-
`;
|
|
2597
|
-
|
|
2598
|
-
try {
|
|
2599
|
-
// Execute the find-and-click script
|
|
2600
|
-
const result = await cdp.call('Runtime.evaluate', {
|
|
2601
|
-
expression: findScript,
|
|
2602
|
-
contextId: cdp.rootContextId,
|
|
2603
|
-
returnByValue: true
|
|
2604
|
-
});
|
|
2605
|
-
|
|
2606
|
-
const elementInfo = result.result?.value;
|
|
2607
|
-
if (!elementInfo || !elementInfo.found) {
|
|
2608
|
-
console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text);
|
|
2609
|
-
return { success: false, error: 'Element not found' };
|
|
272
|
+
function restartDiscoveryInterval() {
|
|
273
|
+
if (pollingState.discoveryInterval) clearInterval(pollingState.discoveryInterval);
|
|
274
|
+
pollingState.discoveryInterval = setInterval(discoverTargets, pollingState.discoveryIntervalMs);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function adjustSnapshotInterval(hasChanges) {
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
if (hasChanges) {
|
|
280
|
+
pollingState.lastSnapshotChange = now;
|
|
281
|
+
if (pollingState.snapshotIntervalMs !== SNAPSHOT_INTERVAL_ACTIVE) {
|
|
282
|
+
pollingState.snapshotIntervalMs = SNAPSHOT_INTERVAL_ACTIVE;
|
|
283
|
+
restartSnapshotInterval();
|
|
2610
284
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
console.log('[Click] Found but click failed:', elementInfo.error);
|
|
2617
|
-
return { success: false, error: elementInfo.error || 'Click failed' };
|
|
285
|
+
} else {
|
|
286
|
+
const idleTime = now - pollingState.lastSnapshotChange;
|
|
287
|
+
if (idleTime > pollingState.idleThreshold && pollingState.snapshotIntervalMs !== SNAPSHOT_INTERVAL_IDLE) {
|
|
288
|
+
pollingState.snapshotIntervalMs = SNAPSHOT_INTERVAL_IDLE;
|
|
289
|
+
restartSnapshotInterval();
|
|
2618
290
|
}
|
|
2619
|
-
|
|
2620
|
-
} catch (err) {
|
|
2621
|
-
console.error('[Click] CDP error:', err.message);
|
|
2622
|
-
return { success: false, error: err.message };
|
|
2623
291
|
}
|
|
2624
292
|
}
|
|
2625
293
|
|
|
2626
|
-
|
|
2627
|
-
|
|
294
|
+
function restartSnapshotInterval() {
|
|
295
|
+
if (pollingState.snapshotInterval) clearInterval(pollingState.snapshotInterval);
|
|
296
|
+
pollingState.snapshotInterval = setInterval(pollSnapshots, pollingState.snapshotIntervalMs);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// WebSocket Broadcasting
|
|
301
|
+
// =============================================================================
|
|
302
|
+
|
|
303
|
+
let wss; // Will be set after server creation
|
|
304
|
+
|
|
305
|
+
function broadcastSnapshotUpdate(cascadeId, panel = 'chat') {
|
|
306
|
+
if (!wss) return;
|
|
307
|
+
const message = JSON.stringify({ type: 'snapshot_update', cascadeId, panel });
|
|
308
|
+
for (const client of wss.clients) {
|
|
309
|
+
if (client.readyState === WebSocket.OPEN) client.send(message);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function broadcastCascadeList() {
|
|
314
|
+
if (!wss) return;
|
|
315
|
+
const cascadeList = Array.from(cascades.values()).map(c => ({
|
|
316
|
+
id: c.id,
|
|
317
|
+
title: c.metadata?.chatTitle || c.metadata?.windowTitle || 'Unknown',
|
|
318
|
+
window: c.metadata?.windowTitle || 'Unknown',
|
|
319
|
+
active: c.metadata?.isActive || false
|
|
320
|
+
}));
|
|
321
|
+
|
|
322
|
+
const message = JSON.stringify({ type: 'cascade_list', cascades: cascadeList });
|
|
323
|
+
for (const client of wss.clients) {
|
|
324
|
+
if (client.readyState === WebSocket.OPEN) client.send(message);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// Express App Setup
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
const app = express();
|
|
333
|
+
app.use(express.json({ limit: '1mb' })); // Limit request body size
|
|
334
|
+
app.use(express.static(join(__dirname, 'public')));
|
|
335
|
+
|
|
336
|
+
// Mount API routes
|
|
337
|
+
app.use('/', createApiRouter(cascades, mainWindowCDP));
|
|
2628
338
|
|
|
2629
339
|
// =============================================================================
|
|
2630
|
-
//
|
|
340
|
+
// Server Startup
|
|
2631
341
|
// =============================================================================
|
|
2632
342
|
|
|
2633
|
-
const
|
|
343
|
+
const httpServer = createServer(app);
|
|
2634
344
|
|
|
2635
|
-
|
|
2636
|
-
// 7.2 Send cascade list on client connect
|
|
2637
|
-
// 7.3 Broadcast snapshot updates when content changes (handled by broadcastSnapshotUpdate)
|
|
345
|
+
wss = new WebSocketServer({ server: httpServer });
|
|
2638
346
|
|
|
2639
347
|
wss.on('connection', (ws, req) => {
|
|
2640
348
|
const clientIP = req.socket.remoteAddress || 'unknown';
|
|
2641
349
|
console.log(`[WebSocket] Client connected from ${clientIP}`);
|
|
2642
350
|
|
|
2643
|
-
//
|
|
351
|
+
// Send cascade list on connect
|
|
2644
352
|
const cascadeList = Array.from(cascades.values()).map(c => ({
|
|
2645
353
|
id: c.id,
|
|
2646
354
|
title: c.metadata?.chatTitle || c.metadata?.windowTitle || 'Unknown',
|
|
@@ -2648,39 +356,25 @@ wss.on('connection', (ws, req) => {
|
|
|
2648
356
|
active: c.metadata?.isActive || false
|
|
2649
357
|
}));
|
|
2650
358
|
|
|
2651
|
-
ws.send(JSON.stringify({
|
|
2652
|
-
type: 'cascade_list',
|
|
2653
|
-
cascades: cascadeList
|
|
2654
|
-
}));
|
|
2655
|
-
|
|
2656
|
-
// Handle client disconnect
|
|
2657
|
-
ws.on('close', () => {
|
|
2658
|
-
console.log(`[WebSocket] Client disconnected from ${clientIP}`);
|
|
2659
|
-
});
|
|
359
|
+
ws.send(JSON.stringify({ type: 'cascade_list', cascades: cascadeList }));
|
|
2660
360
|
|
|
2661
|
-
|
|
2662
|
-
ws.on('error', (err) => {
|
|
2663
|
-
console.error(`[WebSocket] Error from ${clientIP}:`, err.message);
|
|
2664
|
-
});
|
|
361
|
+
ws.on('close', () => console.log(`[WebSocket] Client disconnected from ${clientIP}`));
|
|
362
|
+
ws.on('error', (err) => console.error(`[WebSocket] Error from ${clientIP}:`, err.message));
|
|
2665
363
|
});
|
|
2666
364
|
|
|
2667
|
-
// Start server
|
|
2668
365
|
httpServer.listen(PORT, () => {
|
|
2669
366
|
const localIP = getLocalIP();
|
|
2670
367
|
console.log('');
|
|
2671
|
-
console.log('
|
|
368
|
+
console.log('Kiro Mobile Bridge');
|
|
2672
369
|
console.log('─────────────────────');
|
|
2673
370
|
console.log(`Local: http://localhost:${PORT}`);
|
|
2674
371
|
console.log(`Network: http://${localIP}:${PORT}`);
|
|
2675
372
|
console.log('');
|
|
2676
373
|
console.log('Open the Network URL on your phone to monitor Kiro.');
|
|
2677
374
|
console.log('');
|
|
2678
|
-
console.log('');
|
|
2679
375
|
|
|
2680
|
-
//
|
|
376
|
+
// Start discovery and polling
|
|
2681
377
|
discoverTargets();
|
|
2682
|
-
setInterval(discoverTargets,
|
|
2683
|
-
|
|
2684
|
-
// 4.5 Run snapshot polling every 1 second for faster updates
|
|
2685
|
-
setInterval(pollSnapshots, 1000);
|
|
378
|
+
pollingState.discoveryInterval = setInterval(discoverTargets, pollingState.discoveryIntervalMs);
|
|
379
|
+
pollingState.snapshotInterval = setInterval(pollSnapshots, pollingState.snapshotIntervalMs);
|
|
2686
380
|
});
|