kiro-mobile-bridge 1.0.13 → 1.0.15
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 +3 -8
- package/package.json +1 -1
- package/src/public/index.html +52 -14
- package/src/routes/api.js +104 -4
- package/src/services/cdp.js +15 -1
- package/src/services/snapshot.js +123 -18
- package/src/utils/network.js +63 -16
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight mobile interface that lets you monitor and control Kiro IDE agent sessions from your phone over LAN, with a live preview of chat, tasks, and code via Chrome DevTools Protocol.
|
|
4
4
|
|
|
5
|
-
<img width="1829" height="1065" alt="Untitled design (
|
|
5
|
+
<img width="1829" height="1065" alt="Untitled design (4)" src="https://github.com/user-attachments/assets/d548c43b-4501-4d66-aed7-ad021a44f9cb" />
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
@@ -24,14 +24,9 @@ A lightweight mobile interface that lets you monitor and control Kiro IDE agent
|
|
|
24
24
|
|
|
25
25
|
Start Kiro with the remote debugging port enabled:
|
|
26
26
|
|
|
27
|
-
**Run Kiro with debugging port:**
|
|
27
|
+
**Run Kiro with debugging port on terminal:**
|
|
28
28
|
```bash
|
|
29
|
-
|
|
30
|
-
"%LOCALAPPDATA%\Programs\Kiro\Kiro.exe" --remote-debugging-port=9000
|
|
31
|
-
# macOS
|
|
32
|
-
/Applications/Kiro.app/Contents/MacOS/Kiro --remote-debugging-port=9000
|
|
33
|
-
# Linux (installed)
|
|
34
|
-
/opt/Kiro/kiro --remote-debugging-port=9000
|
|
29
|
+
kiro --remote-debugging-port=9000
|
|
35
30
|
```
|
|
36
31
|
|
|
37
32
|
### 2. Run with npx (Recommended)
|
package/package.json
CHANGED
package/src/public/index.html
CHANGED
|
@@ -214,9 +214,10 @@
|
|
|
214
214
|
.empty-state p { font-size: 14px; max-width: 280px; }
|
|
215
215
|
|
|
216
216
|
/* =============================================================================
|
|
217
|
-
Mobile Bridge Customizations - Hide Follow
|
|
217
|
+
Mobile Bridge Customizations - Hide Follow button in snackbar (keep "Looks good to me")
|
|
218
218
|
============================================================================= */
|
|
219
|
-
|
|
219
|
+
/* Hide only the Follow button by targeting buttons containing "Follow" text - done via JS */
|
|
220
|
+
/* Note: "Looks good to me" and other action buttons should remain visible */
|
|
220
221
|
</style>
|
|
221
222
|
</head>
|
|
222
223
|
<body>
|
|
@@ -1083,10 +1084,11 @@
|
|
|
1083
1084
|
}
|
|
1084
1085
|
|
|
1085
1086
|
function hideRevertButton() {
|
|
1086
|
-
// Hide Revert
|
|
1087
|
+
// Hide Revert and Follow buttons by finding buttons with specific text in snackbar
|
|
1088
|
+
// Note: "Looks good to me" and other action buttons should remain visible
|
|
1087
1089
|
panels.chat.content.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button').forEach(btn => {
|
|
1088
1090
|
const text = (btn.textContent || '').trim().toLowerCase();
|
|
1089
|
-
if (text === 'revert') {
|
|
1091
|
+
if (text === 'revert' || text === 'follow') {
|
|
1090
1092
|
btn.style.display = 'none';
|
|
1091
1093
|
}
|
|
1092
1094
|
});
|
|
@@ -1147,15 +1149,19 @@
|
|
|
1147
1149
|
const lines = data.content.split('\n');
|
|
1148
1150
|
let html = '';
|
|
1149
1151
|
|
|
1152
|
+
// Show info banner for partial content
|
|
1150
1153
|
if (data.isPartial || data.note) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1154
|
+
const noteText = data.note || 'Showing visible lines only. Use Explorer to open full file.';
|
|
1155
|
+
html += `<div style="padding: 10px 12px; background: linear-gradient(135deg, #2d2d30 0%, #252526 100%); color: #a78bfa; font-size: 12px; border-bottom: 1px solid #3c3c3c; display: flex; align-items: center; gap: 8px;">
|
|
1156
|
+
<span class="codicon codicon-warning" style="color: #f0b429;"></span>
|
|
1157
|
+
<span>${noteText}</span>
|
|
1153
1158
|
</div>`;
|
|
1154
1159
|
}
|
|
1155
1160
|
|
|
1156
1161
|
html += '<pre class="editor-code">';
|
|
1157
1162
|
const startLineNum = data.startLine || 1;
|
|
1158
1163
|
let startIdx = 0;
|
|
1164
|
+
// Skip leading empty lines (max 3)
|
|
1159
1165
|
while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) startIdx++;
|
|
1160
1166
|
|
|
1161
1167
|
lines.slice(startIdx).forEach((line, idx) => {
|
|
@@ -1165,9 +1171,17 @@
|
|
|
1165
1171
|
});
|
|
1166
1172
|
html += '</pre>';
|
|
1167
1173
|
|
|
1174
|
+
// Footer with line info
|
|
1168
1175
|
const endLine = startLineNum + startIdx + lines.slice(startIdx).length - 1;
|
|
1176
|
+
const lineInfo = data.lineCount
|
|
1177
|
+
? `Lines ${startLineNum}-${endLine} of ${data.lineCount}`
|
|
1178
|
+
: `Lines ${startLineNum}-${endLine}`;
|
|
1179
|
+
const fullFileIndicator = !data.isPartial && data.lineCount && endLine >= data.lineCount
|
|
1180
|
+
? ' <span style="color: #4caf50;">✓ Complete file</span>'
|
|
1181
|
+
: '';
|
|
1182
|
+
|
|
1169
1183
|
html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
|
|
1170
|
-
|
|
1184
|
+
${lineInfo}${fullFileIndicator}
|
|
1171
1185
|
</div>`;
|
|
1172
1186
|
|
|
1173
1187
|
panels.editor.content.innerHTML = html;
|
|
@@ -2032,7 +2046,21 @@
|
|
|
2032
2046
|
async function openFileInEditor(filePath) {
|
|
2033
2047
|
if (!selectedCascadeId || !filePath) return;
|
|
2034
2048
|
|
|
2035
|
-
|
|
2049
|
+
// Clean the file path - remove extra whitespace, quotes, backticks, and common prefixes
|
|
2050
|
+
let cleanPath = filePath.trim()
|
|
2051
|
+
.replace(/^['"`]+|['"`]+$/g, '') // Remove surrounding quotes
|
|
2052
|
+
.replace(/^\s*#\s*/, '') // Remove markdown header prefix
|
|
2053
|
+
.replace(/^\s*[-*]\s*/, '') // Remove list markers
|
|
2054
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
2055
|
+
.trim();
|
|
2056
|
+
|
|
2057
|
+
// Extract just the file path if it contains extra text (e.g., "file.ts - some description")
|
|
2058
|
+
const pathMatch = cleanPath.match(/^([^\s]+\.(ts|tsx|js|jsx|py|java|html|css|json|md|yaml|yml|xml|sql|go|rs|c|cpp|h|cs|rb|php|sh|vue|svelte))/i);
|
|
2059
|
+
if (pathMatch) {
|
|
2060
|
+
cleanPath = pathMatch[1];
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
showToast(`Opening ${cleanPath}...`, 1500);
|
|
2036
2064
|
|
|
2037
2065
|
// Switch to Editor tab
|
|
2038
2066
|
const editorTab = document.querySelector('[data-panel="editor"]');
|
|
@@ -2050,25 +2078,30 @@
|
|
|
2050
2078
|
const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
|
|
2051
2079
|
method: 'POST',
|
|
2052
2080
|
headers: { 'Content-Type': 'application/json' },
|
|
2053
|
-
body: JSON.stringify({ filePath })
|
|
2081
|
+
body: JSON.stringify({ filePath: cleanPath })
|
|
2054
2082
|
});
|
|
2055
2083
|
|
|
2056
2084
|
if (readResult.ok) {
|
|
2057
2085
|
const data = await readResult.json();
|
|
2058
2086
|
if (data.content) {
|
|
2059
2087
|
renderEditorSnapshot(data);
|
|
2060
|
-
showToast(`Opened ${data.fileName ||
|
|
2088
|
+
showToast(`Opened ${data.fileName || cleanPath}`, 1500, true);
|
|
2061
2089
|
return;
|
|
2062
2090
|
}
|
|
2091
|
+
} else {
|
|
2092
|
+
// Log the error for debugging
|
|
2093
|
+
const errorData = await readResult.json().catch(() => ({}));
|
|
2094
|
+
console.log('[OpenFile] Direct read failed:', errorData.error || readResult.status);
|
|
2063
2095
|
}
|
|
2064
2096
|
|
|
2065
|
-
// Fallback: Click file link in Kiro
|
|
2097
|
+
// Fallback: Click file link in Kiro to open it there
|
|
2066
2098
|
await fetch(`/click/${selectedCascadeId}`, {
|
|
2067
2099
|
method: 'POST',
|
|
2068
2100
|
headers: { 'Content-Type': 'application/json' },
|
|
2069
|
-
body: JSON.stringify({ tag: 'a', text:
|
|
2101
|
+
body: JSON.stringify({ tag: 'a', text: cleanPath, isFileLink: true, filePath: cleanPath })
|
|
2070
2102
|
});
|
|
2071
2103
|
|
|
2104
|
+
// Wait for Kiro to open the file
|
|
2072
2105
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
2073
2106
|
|
|
2074
2107
|
const fetchEditor = async () => {
|
|
@@ -2076,7 +2109,11 @@
|
|
|
2076
2109
|
const r = await fetch(`/editor/${selectedCascadeId}`);
|
|
2077
2110
|
if (r.ok) {
|
|
2078
2111
|
const data = await r.json();
|
|
2079
|
-
if (data.content || data.html) {
|
|
2112
|
+
if (data.content || data.html) {
|
|
2113
|
+
renderEditorSnapshot(data);
|
|
2114
|
+
showToast(`Opened ${data.fileName || cleanPath}`, 1500, true);
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2080
2117
|
}
|
|
2081
2118
|
} catch (e) {}
|
|
2082
2119
|
return false;
|
|
@@ -2085,10 +2122,11 @@
|
|
|
2085
2122
|
if (!await fetchEditor()) {
|
|
2086
2123
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2087
2124
|
if (!await fetchEditor()) {
|
|
2088
|
-
showEmptyState('editor', 'codicon-file-code', `Could not load ${
|
|
2125
|
+
showEmptyState('editor', 'codicon-file-code', `Could not load ${cleanPath}. Try opening it in Kiro first.`);
|
|
2089
2126
|
}
|
|
2090
2127
|
}
|
|
2091
2128
|
} catch (e) {
|
|
2129
|
+
console.error('[OpenFile] Error:', e);
|
|
2092
2130
|
showToast('Failed to open file');
|
|
2093
2131
|
showEmptyState('editor', 'codicon-file-code', 'Failed to load file.');
|
|
2094
2132
|
}
|
package/src/routes/api.js
CHANGED
|
@@ -215,6 +215,7 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
215
215
|
try {
|
|
216
216
|
// Get workspace root
|
|
217
217
|
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
|
|
218
|
+
console.log(`[ReadFile] Workspace root: ${workspaceRoot}`);
|
|
218
219
|
|
|
219
220
|
// SECURITY: Validate path is within workspace root
|
|
220
221
|
const pathValidation = validatePathWithinRoot(sanitizedPath, workspaceRoot);
|
|
@@ -222,19 +223,46 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
222
223
|
let content = null;
|
|
223
224
|
let foundPath = null;
|
|
224
225
|
|
|
226
|
+
// Try 1: Direct path validation
|
|
225
227
|
if (pathValidation.valid) {
|
|
226
|
-
// Try the validated path first
|
|
227
228
|
try {
|
|
228
229
|
content = await fs.readFile(pathValidation.resolvedPath, 'utf-8');
|
|
229
230
|
foundPath = pathValidation.resolvedPath;
|
|
231
|
+
console.log(`[ReadFile] Found at validated path: ${foundPath}`);
|
|
230
232
|
} catch (e) {
|
|
231
|
-
// File doesn't exist at validated path, try
|
|
233
|
+
// File doesn't exist at validated path, try other methods
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
//
|
|
237
|
+
// Try 2: Common path variations
|
|
238
|
+
if (!content) {
|
|
239
|
+
const pathVariations = [
|
|
240
|
+
sanitizedPath,
|
|
241
|
+
sanitizedPath.replace(/^\.\//, ''), // Remove leading ./
|
|
242
|
+
sanitizedPath.replace(/^\//, ''), // Remove leading /
|
|
243
|
+
`src/${sanitizedPath}`, // Try in src/
|
|
244
|
+
`src/${sanitizedPath.replace(/^src\//, '')}`,
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
for (const variation of pathVariations) {
|
|
248
|
+
const varValidation = validatePathWithinRoot(variation, workspaceRoot);
|
|
249
|
+
if (varValidation.valid) {
|
|
250
|
+
try {
|
|
251
|
+
content = await fs.readFile(varValidation.resolvedPath, 'utf-8');
|
|
252
|
+
foundPath = varValidation.resolvedPath;
|
|
253
|
+
console.log(`[ReadFile] Found at variation: ${foundPath}`);
|
|
254
|
+
break;
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// Continue to next variation
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Try 3: Search by filename within workspace
|
|
236
263
|
if (!content) {
|
|
237
264
|
const fileName = path.basename(sanitizedPath);
|
|
265
|
+
console.log(`[ReadFile] Searching for filename: ${fileName}`);
|
|
238
266
|
foundPath = await findFileRecursive(workspaceRoot, fileName, MAX_FILE_SEARCH_DEPTH);
|
|
239
267
|
|
|
240
268
|
if (foundPath) {
|
|
@@ -242,6 +270,7 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
242
270
|
const foundValidation = validatePathWithinRoot(foundPath, workspaceRoot);
|
|
243
271
|
if (foundValidation.valid) {
|
|
244
272
|
content = await fs.readFile(foundPath, 'utf-8');
|
|
273
|
+
console.log(`[ReadFile] Found via search: ${foundPath}`);
|
|
245
274
|
} else {
|
|
246
275
|
console.warn(`[ReadFile] Found file outside workspace: ${foundPath}`);
|
|
247
276
|
foundPath = null;
|
|
@@ -249,7 +278,28 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
|
|
281
|
+
// Try 4: Search with partial path matching
|
|
282
|
+
if (!content && sanitizedPath.includes('/')) {
|
|
283
|
+
const pathParts = sanitizedPath.split('/');
|
|
284
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
285
|
+
const parentDir = pathParts[pathParts.length - 2];
|
|
286
|
+
|
|
287
|
+
if (parentDir && fileName) {
|
|
288
|
+
console.log(`[ReadFile] Searching for ${parentDir}/${fileName}`);
|
|
289
|
+
foundPath = await findFileWithParent(workspaceRoot, parentDir, fileName, MAX_FILE_SEARCH_DEPTH);
|
|
290
|
+
|
|
291
|
+
if (foundPath) {
|
|
292
|
+
const foundValidation = validatePathWithinRoot(foundPath, workspaceRoot);
|
|
293
|
+
if (foundValidation.valid) {
|
|
294
|
+
content = await fs.readFile(foundPath, 'utf-8');
|
|
295
|
+
console.log(`[ReadFile] Found via parent search: ${foundPath}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
252
301
|
if (!content) {
|
|
302
|
+
console.log(`[ReadFile] File not found: ${sanitizedPath}`);
|
|
253
303
|
return res.status(404).json({ error: 'File not found within workspace' });
|
|
254
304
|
}
|
|
255
305
|
|
|
@@ -257,7 +307,7 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
257
307
|
|
|
258
308
|
res.json({
|
|
259
309
|
content,
|
|
260
|
-
fileName: path.basename(sanitizedPath),
|
|
310
|
+
fileName: path.basename(foundPath || sanitizedPath),
|
|
261
311
|
fullPath: foundPath,
|
|
262
312
|
language,
|
|
263
313
|
lineCount: content.split('\n').length,
|
|
@@ -489,6 +539,56 @@ async function findFileRecursive(dir, fileName, maxDepth = MAX_FILE_SEARCH_DEPTH
|
|
|
489
539
|
return null;
|
|
490
540
|
}
|
|
491
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Find file with matching parent directory name
|
|
544
|
+
* Useful when path is like "services/snapshot.js" and we need to find the right one
|
|
545
|
+
* @param {string} dir - Directory to search
|
|
546
|
+
* @param {string} parentDirName - Expected parent directory name
|
|
547
|
+
* @param {string} fileName - File name to find
|
|
548
|
+
* @param {number} maxDepth - Maximum search depth
|
|
549
|
+
* @param {number} currentDepth - Current depth (internal)
|
|
550
|
+
* @returns {Promise<string|null>}
|
|
551
|
+
*/
|
|
552
|
+
async function findFileWithParent(dir, parentDirName, fileName, maxDepth = MAX_FILE_SEARCH_DEPTH, currentDepth = 0) {
|
|
553
|
+
if (currentDepth > maxDepth) return null;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
557
|
+
|
|
558
|
+
// Check if current directory matches parent name and contains the file
|
|
559
|
+
const currentDirName = path.basename(dir);
|
|
560
|
+
if (currentDirName === parentDirName) {
|
|
561
|
+
for (const entry of entries) {
|
|
562
|
+
if (entry.isFile() && entry.name === fileName) {
|
|
563
|
+
return path.join(dir, entry.name);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Recurse into directories
|
|
569
|
+
for (const entry of entries) {
|
|
570
|
+
if (entry.isDirectory() &&
|
|
571
|
+
(!entry.name.startsWith('.') || entry.name === '.kiro') &&
|
|
572
|
+
entry.name !== 'node_modules' &&
|
|
573
|
+
entry.name !== 'dist' &&
|
|
574
|
+
entry.name !== 'build' &&
|
|
575
|
+
entry.name !== '.git') {
|
|
576
|
+
const found = await findFileWithParent(
|
|
577
|
+
path.join(dir, entry.name),
|
|
578
|
+
parentDirName,
|
|
579
|
+
fileName,
|
|
580
|
+
maxDepth,
|
|
581
|
+
currentDepth + 1
|
|
582
|
+
);
|
|
583
|
+
if (found) return found;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
// Directory not accessible, skip
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
492
592
|
/**
|
|
493
593
|
* Collect workspace files for file tree
|
|
494
594
|
* @param {string} workspaceRoot - Workspace root directory
|
package/src/services/cdp.js
CHANGED
|
@@ -7,13 +7,27 @@ import { CDP_CALL_TIMEOUT, HTTP_TIMEOUT } from '../utils/constants.js';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Fetch JSON from a CDP endpoint
|
|
10
|
+
* Tries both 127.0.0.1 and localhost for cross-platform compatibility
|
|
10
11
|
* @param {number} port - The port to fetch from
|
|
11
12
|
* @param {string} path - The path to fetch (default: /json/list)
|
|
12
13
|
* @returns {Promise<any>} - Parsed JSON response
|
|
13
14
|
*/
|
|
14
15
|
export function fetchCDPTargets(port, path = '/json/list') {
|
|
16
|
+
// Try 127.0.0.1 first, then localhost as fallback
|
|
17
|
+
return fetchFromHost('127.0.0.1', port, path)
|
|
18
|
+
.catch(() => fetchFromHost('localhost', port, path));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch JSON from a specific host
|
|
23
|
+
* @param {string} host - The host to connect to
|
|
24
|
+
* @param {number} port - The port to fetch from
|
|
25
|
+
* @param {string} path - The path to fetch
|
|
26
|
+
* @returns {Promise<any>} - Parsed JSON response
|
|
27
|
+
*/
|
|
28
|
+
function fetchFromHost(host, port, path) {
|
|
15
29
|
return new Promise((resolve, reject) => {
|
|
16
|
-
const url = `http
|
|
30
|
+
const url = `http://${host}:${port}${path}`;
|
|
17
31
|
|
|
18
32
|
const req = http.get(url, { timeout: HTTP_TIMEOUT }, (res) => {
|
|
19
33
|
let data = '';
|
package/src/services/snapshot.js
CHANGED
|
@@ -285,28 +285,96 @@ export async function captureEditor(cdp) {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
// Try
|
|
288
|
+
// Method 1: Try VS Code/Kiro API (acquireVsCodeApi or global vscode)
|
|
289
289
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
290
|
+
// Check for VS Code webview API
|
|
291
|
+
if (typeof acquireVsCodeApi === 'function') {
|
|
292
|
+
const vscode = acquireVsCodeApi();
|
|
293
|
+
if (vscode && vscode.getState) {
|
|
294
|
+
const state = vscode.getState();
|
|
295
|
+
if (state && state.content) {
|
|
296
|
+
result.content = state.content;
|
|
297
|
+
result.lineCount = state.content.split('\\n').length;
|
|
298
|
+
result.hasContent = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch(e) {
|
|
303
|
+
// VS Code API not available
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Method 2: Try Monaco API with multiple access patterns
|
|
307
|
+
if (!result.content) {
|
|
308
|
+
try {
|
|
309
|
+
const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
|
|
310
|
+
for (const editorEl of monacoEditors) {
|
|
311
|
+
// Try various ways to access the editor instance
|
|
312
|
+
let editorInstance = null;
|
|
313
|
+
|
|
314
|
+
// Direct property access
|
|
315
|
+
editorInstance = editorEl.__vscode_editor__ || editorEl._editor;
|
|
316
|
+
|
|
317
|
+
// Try global monaco
|
|
318
|
+
if (!editorInstance && window.monaco && window.monaco.editor) {
|
|
319
|
+
const editors = window.monaco.editor.getEditors ? window.monaco.editor.getEditors() : [];
|
|
320
|
+
if (editors.length > 0) editorInstance = editors[0];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Try to find editor via data attributes or parent elements
|
|
324
|
+
if (!editorInstance) {
|
|
325
|
+
const editorContainer = editorEl.closest('[data-uri]') || editorEl.closest('.editor-instance');
|
|
326
|
+
if (editorContainer && editorContainer._editor) {
|
|
327
|
+
editorInstance = editorContainer._editor;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (editorInstance && editorInstance.getModel) {
|
|
332
|
+
const model = editorInstance.getModel();
|
|
333
|
+
if (model) {
|
|
334
|
+
result.content = model.getValue();
|
|
335
|
+
result.lineCount = model.getLineCount();
|
|
336
|
+
result.language = model.getLanguageId ? model.getLanguageId() : '';
|
|
337
|
+
result.hasContent = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch(e) {
|
|
343
|
+
// Monaco API not available, continue
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Method 3: Try to get content from Monaco's internal model store
|
|
348
|
+
if (!result.content) {
|
|
349
|
+
try {
|
|
350
|
+
if (window.monaco && window.monaco.editor) {
|
|
351
|
+
const models = window.monaco.editor.getModels ? window.monaco.editor.getModels() : [];
|
|
352
|
+
// Find the model that matches the active file
|
|
353
|
+
for (const model of models) {
|
|
354
|
+
const uri = model.uri ? model.uri.toString() : '';
|
|
355
|
+
if (result.fileName && uri.includes(result.fileName)) {
|
|
356
|
+
result.content = model.getValue();
|
|
357
|
+
result.lineCount = model.getLineCount();
|
|
358
|
+
result.language = model.getLanguageId ? model.getLanguageId() : '';
|
|
359
|
+
result.hasContent = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// If no match by filename, use the first model with content
|
|
364
|
+
if (!result.content && models.length > 0) {
|
|
365
|
+
const model = models[0];
|
|
297
366
|
result.content = model.getValue();
|
|
298
367
|
result.lineCount = model.getLineCount();
|
|
299
368
|
result.language = model.getLanguageId ? model.getLanguageId() : '';
|
|
300
369
|
result.hasContent = true;
|
|
301
|
-
break;
|
|
302
370
|
}
|
|
303
371
|
}
|
|
372
|
+
} catch(e) {
|
|
373
|
+
// Model store not accessible
|
|
304
374
|
}
|
|
305
|
-
} catch(e) {
|
|
306
|
-
// Monaco API not available, continue
|
|
307
375
|
}
|
|
308
376
|
|
|
309
|
-
//
|
|
377
|
+
// Method 4: Fallback - Extract from view-lines (visible lines only)
|
|
310
378
|
if (!result.content) {
|
|
311
379
|
const viewLines = targetDoc.querySelector('.monaco-editor .view-lines');
|
|
312
380
|
if (viewLines) {
|
|
@@ -316,25 +384,62 @@ export async function captureEditor(cdp) {
|
|
|
316
384
|
let minLineNum = Infinity, maxLineNum = 0;
|
|
317
385
|
const lineMap = new Map();
|
|
318
386
|
|
|
387
|
+
// Get line height from first line for accurate line number calculation
|
|
388
|
+
let lineHeight = 19;
|
|
389
|
+
if (lines[0]) {
|
|
390
|
+
const firstLineTop = parseFloat(lines[0].style.top) || 0;
|
|
391
|
+
const secondLine = lines[1];
|
|
392
|
+
if (secondLine) {
|
|
393
|
+
const secondLineTop = parseFloat(secondLine.style.top) || 0;
|
|
394
|
+
if (secondLineTop > firstLineTop) {
|
|
395
|
+
lineHeight = secondLineTop - firstLineTop;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
319
400
|
lines.forEach(line => {
|
|
320
401
|
const top = parseFloat(line.style.top) || 0;
|
|
321
|
-
const lineNum = Math.round(top /
|
|
402
|
+
const lineNum = Math.round(top / lineHeight) + 1;
|
|
322
403
|
lineMap.set(lineNum, line.textContent || '');
|
|
323
404
|
minLineNum = Math.min(minLineNum, lineNum);
|
|
324
405
|
maxLineNum = Math.max(maxLineNum, lineNum);
|
|
325
406
|
});
|
|
326
407
|
|
|
327
|
-
|
|
408
|
+
// Get total line count from scrollbar or line numbers if available
|
|
409
|
+
let totalLines = maxLineNum;
|
|
410
|
+
const lineNumbersContainer = targetDoc.querySelector('.monaco-editor .line-numbers');
|
|
411
|
+
if (lineNumbersContainer) {
|
|
412
|
+
const lastLineNum = lineNumbersContainer.querySelector('.line-numbers:last-child');
|
|
413
|
+
if (lastLineNum) {
|
|
414
|
+
const num = parseInt(lastLineNum.textContent, 10);
|
|
415
|
+
if (!isNaN(num) && num > totalLines) totalLines = num;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Also try to get total from editor's scrollable height
|
|
420
|
+
const scrollableElement = targetDoc.querySelector('.monaco-editor .monaco-scrollable-element');
|
|
421
|
+
if (scrollableElement) {
|
|
422
|
+
const scrollHeight = scrollableElement.scrollHeight;
|
|
423
|
+
const estimatedLines = Math.ceil(scrollHeight / lineHeight);
|
|
424
|
+
if (estimatedLines > totalLines) totalLines = estimatedLines;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (let i = minLineNum; i <= maxLineNum; i++) {
|
|
328
428
|
codeContent += (lineMap.get(i) || '') + '\\n';
|
|
329
429
|
}
|
|
330
430
|
|
|
331
431
|
result.content = codeContent;
|
|
332
|
-
result.lineCount =
|
|
432
|
+
result.lineCount = totalLines;
|
|
333
433
|
result.startLine = minLineNum;
|
|
434
|
+
result.endLine = maxLineNum;
|
|
334
435
|
result.hasContent = codeContent.trim().length > 0;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
436
|
+
|
|
437
|
+
// Always mark as partial when using view-lines fallback
|
|
438
|
+
result.isPartial = true;
|
|
439
|
+
if (minLineNum > 1 || maxLineNum < totalLines) {
|
|
440
|
+
result.note = 'Showing lines ' + minLineNum + '-' + maxLineNum + ' of ' + totalLines + '. Use Explorer to open full file.';
|
|
441
|
+
} else {
|
|
442
|
+
result.note = 'Visible lines only. Use Explorer to open full file.';
|
|
338
443
|
}
|
|
339
444
|
}
|
|
340
445
|
}
|
package/src/utils/network.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Network utilities
|
|
2
|
+
* Network utilities for cross-platform local IP detection
|
|
3
|
+
*
|
|
4
|
+
* Supports: Windows, macOS, Linux
|
|
5
|
+
*
|
|
6
|
+
* Node.js os.networkInterfaces() family property:
|
|
7
|
+
* - Node < 18.0.0: returns string ('IPv4' or 'IPv6')
|
|
8
|
+
* - Node 18.0.0 - 18.3.x: returns number (4 or 6)
|
|
9
|
+
* - Node >= 18.4.0: returns string ('IPv4' or 'IPv6')
|
|
3
10
|
*/
|
|
4
11
|
import { networkInterfaces } from 'os';
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Check if interface is IPv4
|
|
8
15
|
* Handles both string ('IPv4') and number (4) family values
|
|
16
|
+
* for compatibility across Node.js versions
|
|
9
17
|
* @param {object} iface - Network interface object
|
|
10
18
|
* @returns {boolean}
|
|
11
19
|
*/
|
|
@@ -13,22 +21,66 @@ function isIPv4(iface) {
|
|
|
13
21
|
return iface.family === 'IPv4' || iface.family === 4;
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Check if interface name is a virtual/container interface to skip
|
|
26
|
+
* @param {string} name - Interface name
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isVirtualInterface(name) {
|
|
30
|
+
const lowerName = name.toLowerCase();
|
|
31
|
+
const virtualPatterns = [
|
|
32
|
+
'vethernet', // Windows WSL/Hyper-V
|
|
33
|
+
'docker', // Docker
|
|
34
|
+
'vmware', // VMware
|
|
35
|
+
'virtualbox', // VirtualBox
|
|
36
|
+
'vbox', // VirtualBox alternate
|
|
37
|
+
'virbr', // Linux libvirt bridge
|
|
38
|
+
'br-', // Docker bridge
|
|
39
|
+
'veth', // Virtual ethernet
|
|
40
|
+
'tailscale', // Tailscale VPN
|
|
41
|
+
'tun', // VPN tunnel
|
|
42
|
+
'tap', // VPN tap
|
|
43
|
+
'utun', // macOS VPN
|
|
44
|
+
'awdl', // Apple Wireless Direct Link
|
|
45
|
+
'llw', // Apple Low Latency WLAN
|
|
46
|
+
'bridge', // Bridge interfaces
|
|
47
|
+
'ham', // Hamachi VPN
|
|
48
|
+
'zt', // ZeroTier
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return virtualPatterns.some(pattern => lowerName.includes(pattern));
|
|
52
|
+
}
|
|
53
|
+
|
|
16
54
|
/**
|
|
17
55
|
* Get local IP address for LAN access
|
|
18
|
-
* Returns the first non-internal IPv4 address found
|
|
56
|
+
* Returns the first non-internal IPv4 address found, prioritizing
|
|
57
|
+
* physical network interfaces over virtual ones.
|
|
19
58
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
59
|
+
* Platform-specific interface names:
|
|
60
|
+
* - Windows: 'Ethernet', 'Wi-Fi', 'Ethernet 2', 'Local Area Connection'
|
|
61
|
+
* - macOS: 'en0' (Wi-Fi), 'en1' (Ethernet), 'en2', etc.
|
|
62
|
+
* - Linux: 'eth0', 'enp0s3', 'ens33', 'wlan0', 'wlp2s0'
|
|
22
63
|
*
|
|
23
64
|
* @returns {string} - Local IP or 'localhost' if no suitable interface found
|
|
24
65
|
*/
|
|
25
66
|
export function getLocalIP() {
|
|
26
67
|
const interfaces = networkInterfaces();
|
|
27
68
|
|
|
28
|
-
//
|
|
29
|
-
|
|
69
|
+
// Priority 1: Common physical interface names across platforms
|
|
70
|
+
// Order matters - check most common first
|
|
71
|
+
const priorityInterfaces = [
|
|
72
|
+
// Windows
|
|
73
|
+
'Ethernet', 'Wi-Fi', 'Ethernet 2', 'Local Area Connection',
|
|
74
|
+
// macOS
|
|
75
|
+
'en0', 'en1', 'en2', 'en3', 'en4', 'en5',
|
|
76
|
+
// Linux (traditional)
|
|
77
|
+
'eth0', 'eth1', 'wlan0', 'wlan1',
|
|
78
|
+
// Linux (systemd predictable names)
|
|
79
|
+
'enp0s3', 'enp0s25', 'enp0s31f6', 'ens33', 'ens160', 'ens192',
|
|
80
|
+
'wlp2s0', 'wlp3s0', 'wlp0s20f3',
|
|
81
|
+
];
|
|
30
82
|
|
|
31
|
-
// First
|
|
83
|
+
// First pass: try priority interfaces
|
|
32
84
|
for (const name of priorityInterfaces) {
|
|
33
85
|
const ifaces = interfaces[name];
|
|
34
86
|
if (ifaces) {
|
|
@@ -40,15 +92,10 @@ export function getLocalIP() {
|
|
|
40
92
|
}
|
|
41
93
|
}
|
|
42
94
|
|
|
43
|
-
//
|
|
95
|
+
// Second pass: any non-virtual interface
|
|
44
96
|
for (const name of Object.keys(interfaces)) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
name.toLowerCase().includes('docker') ||
|
|
48
|
-
name.toLowerCase().includes('vmware') ||
|
|
49
|
-
name.toLowerCase().includes('virtualbox')) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
97
|
+
if (isVirtualInterface(name)) continue;
|
|
98
|
+
|
|
52
99
|
for (const iface of interfaces[name]) {
|
|
53
100
|
if (isIPv4(iface) && !iface.internal) {
|
|
54
101
|
return iface.address;
|
|
@@ -56,7 +103,7 @@ export function getLocalIP() {
|
|
|
56
103
|
}
|
|
57
104
|
}
|
|
58
105
|
|
|
59
|
-
// Last resort: any non-internal IPv4
|
|
106
|
+
// Last resort: any non-internal IPv4 (including virtual)
|
|
60
107
|
for (const name of Object.keys(interfaces)) {
|
|
61
108
|
for (const iface of interfaces[name]) {
|
|
62
109
|
if (isIPv4(iface) && !iface.internal) {
|