kiro-mobile-bridge 1.0.7 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -24
- package/package.json +1 -1
- package/src/public/index.html +1414 -1628
- package/src/routes/api.js +539 -0
- package/src/server.js +287 -2593
- package/src/services/cdp.js +210 -0
- package/src/services/click.js +533 -0
- package/src/services/message.js +214 -0
- package/src/services/snapshot.js +370 -0
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +34 -0
- package/src/utils/network.js +64 -0
- package/src/utils/security.js +160 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message injection service - sends messages to Kiro chat via CDP
|
|
3
|
+
*/
|
|
4
|
+
import { escapeForJavaScript, validateMessage } from '../utils/security.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create script to inject message into chat input
|
|
8
|
+
* @param {string} messageText - Message to inject
|
|
9
|
+
* @returns {string} - JavaScript expression
|
|
10
|
+
*/
|
|
11
|
+
function createInjectScript(messageText) {
|
|
12
|
+
// Use proper escaping to prevent XSS and injection attacks
|
|
13
|
+
const escaped = escapeForJavaScript(messageText);
|
|
14
|
+
|
|
15
|
+
return `(async () => {
|
|
16
|
+
const text = '${escaped}';
|
|
17
|
+
let targetDoc = document;
|
|
18
|
+
const activeFrame = document.getElementById('active-frame');
|
|
19
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
20
|
+
|
|
21
|
+
// Find the CHAT INPUT editor specifically (not any contenteditable in the page)
|
|
22
|
+
// Look for the input area at the bottom of the chat, not message bubbles
|
|
23
|
+
let editor = null;
|
|
24
|
+
|
|
25
|
+
// Strategy 1: Find by common chat input container patterns
|
|
26
|
+
const inputContainerSelectors = [
|
|
27
|
+
'[class*="chat-input"]',
|
|
28
|
+
'[class*="message-input"]',
|
|
29
|
+
'[class*="composer"]',
|
|
30
|
+
'[class*="input-area"]',
|
|
31
|
+
'[class*="InputArea"]',
|
|
32
|
+
'form[class*="chat"]',
|
|
33
|
+
'[data-testid*="input"]'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const containerSel of inputContainerSelectors) {
|
|
37
|
+
const container = targetDoc.querySelector(containerSel);
|
|
38
|
+
if (container) {
|
|
39
|
+
const editorInContainer = container.querySelector('.tiptap.ProseMirror[contenteditable="true"], [data-lexical-editor="true"][contenteditable="true"], [contenteditable="true"], textarea');
|
|
40
|
+
if (editorInContainer && editorInContainer.offsetParent !== null) {
|
|
41
|
+
editor = editorInContainer;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Strategy 2: Find TipTap/ProseMirror editors that are visible and near a submit button
|
|
48
|
+
if (!editor) {
|
|
49
|
+
const allEditors = [...targetDoc.querySelectorAll('.tiptap.ProseMirror[contenteditable="true"]')].filter(el => el.offsetParent !== null);
|
|
50
|
+
for (const ed of allEditors) {
|
|
51
|
+
// Check if there's a submit button nearby (sibling or in same parent form)
|
|
52
|
+
const parent = ed.closest('form') || ed.parentElement?.parentElement?.parentElement;
|
|
53
|
+
if (parent) {
|
|
54
|
+
const hasSubmit = parent.querySelector('button[data-variant="submit"], button[type="submit"], svg.lucide-arrow-right');
|
|
55
|
+
if (hasSubmit) { editor = ed; break; }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Fallback to last visible TipTap editor
|
|
59
|
+
if (!editor && allEditors.length > 0) editor = allEditors.at(-1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strategy 3: Find Lexical editors
|
|
63
|
+
if (!editor) {
|
|
64
|
+
const lexicalEditors = [...targetDoc.querySelectorAll('[data-lexical-editor="true"][contenteditable="true"]')].filter(el => el.offsetParent !== null);
|
|
65
|
+
for (const ed of lexicalEditors) {
|
|
66
|
+
const parent = ed.closest('form') || ed.parentElement?.parentElement?.parentElement;
|
|
67
|
+
if (parent) {
|
|
68
|
+
const hasSubmit = parent.querySelector('button[data-variant="submit"], button[type="submit"]');
|
|
69
|
+
if (hasSubmit) { editor = ed; break; }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!editor && lexicalEditors.length > 0) editor = lexicalEditors.at(-1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strategy 4: Generic contenteditable (last resort)
|
|
76
|
+
if (!editor) {
|
|
77
|
+
const editables = [...targetDoc.querySelectorAll('[contenteditable="true"]')].filter(el => el.offsetParent !== null);
|
|
78
|
+
editor = editables.at(-1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Strategy 5: Textarea fallback
|
|
82
|
+
if (!editor) {
|
|
83
|
+
const textareas = [...targetDoc.querySelectorAll('textarea')].filter(el => el.offsetParent !== null);
|
|
84
|
+
editor = textareas.at(-1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!editor) return { ok: false, error: 'editor_not_found' };
|
|
88
|
+
|
|
89
|
+
const isTextarea = editor.tagName.toLowerCase() === 'textarea';
|
|
90
|
+
const isProseMirror = editor.classList.contains('ProseMirror') || editor.classList.contains('tiptap');
|
|
91
|
+
const isLexical = editor.hasAttribute('data-lexical-editor');
|
|
92
|
+
|
|
93
|
+
// Focus the editor first
|
|
94
|
+
editor.focus();
|
|
95
|
+
await new Promise(r => setTimeout(r, 50));
|
|
96
|
+
|
|
97
|
+
if (isTextarea) {
|
|
98
|
+
// Textarea: simple value assignment
|
|
99
|
+
editor.value = text;
|
|
100
|
+
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
|
101
|
+
} else if (isProseMirror) {
|
|
102
|
+
// ProseMirror/TipTap: Clear and insert via DOM manipulation
|
|
103
|
+
// This works because TipTap syncs DOM changes to its state
|
|
104
|
+
editor.innerHTML = '';
|
|
105
|
+
const p = targetDoc.createElement('p');
|
|
106
|
+
p.textContent = text;
|
|
107
|
+
editor.appendChild(p);
|
|
108
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
109
|
+
} else if (isLexical) {
|
|
110
|
+
// Lexical: Must use execCommand to properly update internal state
|
|
111
|
+
// Lexical listens to beforeinput/input events from execCommand
|
|
112
|
+
|
|
113
|
+
// First, select all content
|
|
114
|
+
const selection = targetDoc.getSelection();
|
|
115
|
+
const range = targetDoc.createRange();
|
|
116
|
+
range.selectNodeContents(editor);
|
|
117
|
+
selection.removeAllRanges();
|
|
118
|
+
selection.addRange(range);
|
|
119
|
+
|
|
120
|
+
// Delete existing content
|
|
121
|
+
targetDoc.execCommand('delete', false, null);
|
|
122
|
+
|
|
123
|
+
// Wait for Lexical to process the deletion
|
|
124
|
+
await new Promise(r => setTimeout(r, 30));
|
|
125
|
+
|
|
126
|
+
// Insert new text using execCommand (Lexical intercepts this)
|
|
127
|
+
const inserted = targetDoc.execCommand('insertText', false, text);
|
|
128
|
+
|
|
129
|
+
if (!inserted) {
|
|
130
|
+
// Fallback: Try setting textContent and dispatching events
|
|
131
|
+
editor.textContent = text;
|
|
132
|
+
editor.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: text }));
|
|
133
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Generic contenteditable
|
|
137
|
+
const selection = targetDoc.getSelection();
|
|
138
|
+
const range = targetDoc.createRange();
|
|
139
|
+
range.selectNodeContents(editor);
|
|
140
|
+
selection.removeAllRanges();
|
|
141
|
+
selection.addRange(range);
|
|
142
|
+
|
|
143
|
+
targetDoc.execCommand('delete', false, null);
|
|
144
|
+
|
|
145
|
+
let inserted = false;
|
|
146
|
+
try { inserted = !!targetDoc.execCommand('insertText', false, text); } catch (e) {}
|
|
147
|
+
|
|
148
|
+
if (!inserted) {
|
|
149
|
+
editor.textContent = text;
|
|
150
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Wait for editor state to sync
|
|
155
|
+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
156
|
+
await new Promise(r => setTimeout(r, 100));
|
|
157
|
+
|
|
158
|
+
// Find and click submit button
|
|
159
|
+
const submitButton = targetDoc.querySelector('button[data-variant="submit"]:not([disabled])') ||
|
|
160
|
+
targetDoc.querySelector('svg.lucide-arrow-right')?.closest('button:not([disabled])') ||
|
|
161
|
+
targetDoc.querySelector('button[type="submit"]:not([disabled])') ||
|
|
162
|
+
targetDoc.querySelector('button[aria-label*="send" i]:not([disabled])');
|
|
163
|
+
|
|
164
|
+
if (submitButton) {
|
|
165
|
+
submitButton.click();
|
|
166
|
+
return { ok: true, method: 'click_submit' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fallback: Enter key
|
|
170
|
+
editor.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13 }));
|
|
171
|
+
return { ok: true, method: 'enter_key' };
|
|
172
|
+
})()`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Inject a message into the chat via CDP
|
|
177
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
178
|
+
* @param {string} message - Message text
|
|
179
|
+
* @returns {Promise<{success: boolean, method?: string, error?: string}>}
|
|
180
|
+
*/
|
|
181
|
+
export async function injectMessage(cdp, message) {
|
|
182
|
+
// Validate message before processing
|
|
183
|
+
const validation = validateMessage(message);
|
|
184
|
+
if (!validation.valid) {
|
|
185
|
+
return { success: false, error: validation.error };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!cdp.rootContextId) {
|
|
189
|
+
return { success: false, error: 'No execution context available' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
194
|
+
expression: createInjectScript(message),
|
|
195
|
+
contextId: cdp.rootContextId,
|
|
196
|
+
returnByValue: true,
|
|
197
|
+
awaitPromise: true
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (result.exceptionDetails) {
|
|
201
|
+
return { success: false, error: result.exceptionDetails.exception?.description || 'Unknown error' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const value = result.result?.value;
|
|
205
|
+
if (value?.ok) {
|
|
206
|
+
console.log(`[Inject] Message sent via ${value.method}`);
|
|
207
|
+
return { success: true, method: value.method };
|
|
208
|
+
}
|
|
209
|
+
return { success: false, error: value?.error || 'Injection failed' };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('[Inject] CDP call failed:', err.message);
|
|
212
|
+
return { success: false, error: err.message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot capture service - captures DOM snapshots from Kiro via CDP
|
|
3
|
+
*/
|
|
4
|
+
import { getLanguageFromExtension } from '../utils/constants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Capture chat metadata (title, active state)
|
|
8
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
9
|
+
* @returns {Promise<{chatTitle: string, isActive: boolean}>}
|
|
10
|
+
*/
|
|
11
|
+
export async function captureMetadata(cdp) {
|
|
12
|
+
if (!cdp.rootContextId) return { chatTitle: '', isActive: false };
|
|
13
|
+
|
|
14
|
+
const script = `(function() {
|
|
15
|
+
let chatTitle = '';
|
|
16
|
+
let isActive = false;
|
|
17
|
+
|
|
18
|
+
const titleSelectors = ['.chat-title', '.conversation-title', '[data-testid="chat-title"]', '.chat-header h1', '.chat-header h2'];
|
|
19
|
+
for (const selector of titleSelectors) {
|
|
20
|
+
const el = document.querySelector(selector);
|
|
21
|
+
if (el && el.textContent) { chatTitle = el.textContent.trim(); break; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const activeIndicators = ['.typing-indicator', '.loading-indicator', '[data-loading="true"]'];
|
|
25
|
+
for (const selector of activeIndicators) {
|
|
26
|
+
if (document.querySelector(selector)) { isActive = true; break; }
|
|
27
|
+
}
|
|
28
|
+
isActive = isActive || document.hasFocus();
|
|
29
|
+
|
|
30
|
+
return { chatTitle, isActive };
|
|
31
|
+
})()`;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
35
|
+
expression: script,
|
|
36
|
+
contextId: cdp.rootContextId,
|
|
37
|
+
returnByValue: true
|
|
38
|
+
});
|
|
39
|
+
return result.result?.value || { chatTitle: '', isActive: false };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[Snapshot] Failed to capture metadata:', err.message);
|
|
42
|
+
return { chatTitle: '', isActive: false };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Capture CSS styles from the page (run once per connection)
|
|
48
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
49
|
+
* @returns {Promise<string>} - Combined CSS string
|
|
50
|
+
*/
|
|
51
|
+
export async function captureCSS(cdp) {
|
|
52
|
+
if (!cdp.rootContextId) return '';
|
|
53
|
+
|
|
54
|
+
const script = `(function() {
|
|
55
|
+
let css = '';
|
|
56
|
+
let targetDoc = document;
|
|
57
|
+
const activeFrame = document.getElementById('active-frame');
|
|
58
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
59
|
+
|
|
60
|
+
const rootStyles = window.getComputedStyle(targetDoc.documentElement);
|
|
61
|
+
const allProps = [];
|
|
62
|
+
for (let i = 0; i < rootStyles.length; i++) allProps.push(rootStyles[i]);
|
|
63
|
+
|
|
64
|
+
// Safely iterate stylesheets with null check
|
|
65
|
+
const styleSheets = targetDoc.styleSheets || [];
|
|
66
|
+
for (const sheet of styleSheets) {
|
|
67
|
+
try {
|
|
68
|
+
if (sheet.cssRules) {
|
|
69
|
+
for (const rule of sheet.cssRules) {
|
|
70
|
+
if (rule.style) {
|
|
71
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
72
|
+
const prop = rule.style[i];
|
|
73
|
+
if (prop.startsWith('--') && !allProps.includes(prop)) allProps.push(prop);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// CORS restriction on external stylesheets, skip
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let cssVars = ':root {\\n';
|
|
84
|
+
for (const prop of allProps) {
|
|
85
|
+
if (prop.startsWith('--')) {
|
|
86
|
+
const value = rootStyles.getPropertyValue(prop).trim();
|
|
87
|
+
if (value) cssVars += ' ' + prop + ': ' + value + ';\\n';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
cssVars += '}\\n\\n';
|
|
91
|
+
css += cssVars;
|
|
92
|
+
|
|
93
|
+
for (const sheet of styleSheets) {
|
|
94
|
+
try {
|
|
95
|
+
if (sheet.cssRules) {
|
|
96
|
+
for (const rule of sheet.cssRules) css += rule.cssText + '\\n';
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// CORS restriction, skip
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const styleTags = targetDoc.querySelectorAll('style');
|
|
104
|
+
for (const tag of styleTags) css += tag.textContent + '\\n';
|
|
105
|
+
|
|
106
|
+
return css;
|
|
107
|
+
})()`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
111
|
+
expression: script,
|
|
112
|
+
contextId: cdp.rootContextId,
|
|
113
|
+
returnByValue: true
|
|
114
|
+
});
|
|
115
|
+
return result.result?.value || '';
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error('[Snapshot] Failed to capture CSS:', err.message);
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Capture HTML snapshot of the chat interface
|
|
125
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
126
|
+
* @returns {Promise<{html: string, bodyBg: string, bodyColor: string} | null>}
|
|
127
|
+
*/
|
|
128
|
+
export async function captureSnapshot(cdp) {
|
|
129
|
+
if (!cdp.rootContextId) {
|
|
130
|
+
console.log('[Snapshot] No rootContextId available');
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const script = `(function() {
|
|
135
|
+
let targetDoc = document;
|
|
136
|
+
let targetBody = document.body;
|
|
137
|
+
|
|
138
|
+
const activeFrame = document.getElementById('active-frame');
|
|
139
|
+
if (activeFrame && activeFrame.contentDocument) {
|
|
140
|
+
targetDoc = activeFrame.contentDocument;
|
|
141
|
+
targetBody = targetDoc.body;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!targetBody) return { html: '<div style="padding:20px;color:#888;">No content found</div>', bodyBg: '', bodyColor: '' };
|
|
145
|
+
|
|
146
|
+
const bodyStyles = window.getComputedStyle(targetBody);
|
|
147
|
+
const bodyBg = bodyStyles.backgroundColor || '';
|
|
148
|
+
const bodyColor = bodyStyles.color || '';
|
|
149
|
+
|
|
150
|
+
const scrollContainers = targetDoc.querySelectorAll('[class*="scroll"], [style*="overflow"]');
|
|
151
|
+
|
|
152
|
+
for (const container of scrollContainers) {
|
|
153
|
+
if (container.scrollHeight > container.clientHeight) {
|
|
154
|
+
const isHistoryPanel = container.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
|
|
155
|
+
container.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
|
|
156
|
+
|
|
157
|
+
if (isHistoryPanel) {
|
|
158
|
+
container.scrollTop = 0;
|
|
159
|
+
} else {
|
|
160
|
+
container.scrollTop = container.scrollHeight;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const clone = targetBody.cloneNode(true);
|
|
166
|
+
|
|
167
|
+
// Capture any portal/overlay content that might be outside the body
|
|
168
|
+
// Radix UI and similar libraries render dropdowns in portals at document root
|
|
169
|
+
const portals = targetDoc.querySelectorAll('[data-radix-portal], [data-radix-popper-content-wrapper], [class*="portal"], [class*="Portal"]');
|
|
170
|
+
portals.forEach(portal => {
|
|
171
|
+
try {
|
|
172
|
+
const portalClone = portal.cloneNode(true);
|
|
173
|
+
clone.appendChild(portalClone);
|
|
174
|
+
} catch(e) {
|
|
175
|
+
// Clone failed, skip
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Also check for any floating/overlay elements at document level
|
|
180
|
+
const floatingSelectors = [
|
|
181
|
+
'body > [role="listbox"]',
|
|
182
|
+
'body > [role="menu"]',
|
|
183
|
+
'body > [data-state="open"]',
|
|
184
|
+
'body > [class*="dropdown"]',
|
|
185
|
+
'body > div[style*="position: absolute"]',
|
|
186
|
+
'body > div[style*="position: fixed"]'
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
floatingSelectors.forEach(sel => {
|
|
190
|
+
try {
|
|
191
|
+
targetDoc.querySelectorAll(sel).forEach(el => {
|
|
192
|
+
const elClone = el.cloneNode(true);
|
|
193
|
+
clone.appendChild(elClone);
|
|
194
|
+
});
|
|
195
|
+
} catch(e) {
|
|
196
|
+
// Selector failed, skip
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Remove ONLY tooltips from clone - preserve everything else
|
|
201
|
+
try {
|
|
202
|
+
clone.querySelectorAll('[role="tooltip"]').forEach(el => el.remove());
|
|
203
|
+
} catch(e) {
|
|
204
|
+
// Removal failed, continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Fix SVG currentColor
|
|
208
|
+
clone.querySelectorAll('svg').forEach(svg => {
|
|
209
|
+
try {
|
|
210
|
+
const computedColor = '#cccccc';
|
|
211
|
+
svg.querySelectorAll('[fill="currentColor"]').forEach(el => el.setAttribute('fill', computedColor));
|
|
212
|
+
svg.querySelectorAll('[stroke="currentColor"]').forEach(el => el.setAttribute('stroke', computedColor));
|
|
213
|
+
} catch(e) {
|
|
214
|
+
// SVG fix failed, continue
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Remove placeholder text
|
|
219
|
+
try {
|
|
220
|
+
clone.querySelectorAll('[contenteditable="true"], [data-lexical-editor="true"]').forEach(editable => {
|
|
221
|
+
const parent = editable.parentElement;
|
|
222
|
+
if (parent) {
|
|
223
|
+
Array.from(parent.children).forEach(sibling => {
|
|
224
|
+
if (sibling === editable) return;
|
|
225
|
+
const text = (sibling.textContent || '').toLowerCase();
|
|
226
|
+
if (text.includes('ask') || text.includes('question') || text.includes('task') || text.includes('describe')) {
|
|
227
|
+
sibling.remove();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
clone.querySelectorAll('[class*="placeholder"], [class*="Placeholder"], [data-placeholder]').forEach(el => {
|
|
234
|
+
if (!el.matches('[contenteditable], [data-lexical-editor], textarea, input')) {
|
|
235
|
+
if (!el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) el.remove();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
} catch(e) {
|
|
239
|
+
// Placeholder removal failed, continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { html: clone.outerHTML, bodyBg, bodyColor };
|
|
243
|
+
})()`;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
247
|
+
expression: script,
|
|
248
|
+
contextId: cdp.rootContextId,
|
|
249
|
+
returnByValue: true
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return result.result?.value || null;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('[Snapshot] Failed to capture HTML:', err.message);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Capture Editor panel snapshot (currently open file)
|
|
261
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
262
|
+
* @returns {Promise<{fileName: string, language: string, content: string, lineCount: number, hasContent: boolean} | null>}
|
|
263
|
+
*/
|
|
264
|
+
export async function captureEditor(cdp) {
|
|
265
|
+
if (!cdp.rootContextId) return null;
|
|
266
|
+
|
|
267
|
+
const script = `(function() {
|
|
268
|
+
let targetDoc = document;
|
|
269
|
+
const activeFrame = document.getElementById('active-frame');
|
|
270
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
271
|
+
|
|
272
|
+
const result = { html: '', fileName: '', language: '', content: '', lineCount: 0, hasContent: false };
|
|
273
|
+
|
|
274
|
+
// Get active tab / file name
|
|
275
|
+
const tabSelectors = ['.tab.active .label-name', '.tab.active', '[role="tab"][aria-selected="true"]'];
|
|
276
|
+
for (const selector of tabSelectors) {
|
|
277
|
+
try {
|
|
278
|
+
const tab = targetDoc.querySelector(selector);
|
|
279
|
+
if (tab && tab.textContent) {
|
|
280
|
+
result.fileName = tab.textContent.trim().split('\\n')[0].trim();
|
|
281
|
+
if (result.fileName) break;
|
|
282
|
+
}
|
|
283
|
+
} catch(e) {
|
|
284
|
+
// Selector failed, continue
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Try Monaco API
|
|
289
|
+
try {
|
|
290
|
+
const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
|
|
291
|
+
for (const editorEl of monacoEditors) {
|
|
292
|
+
const editorInstance = editorEl.__vscode_editor__ || editorEl._editor ||
|
|
293
|
+
(window.monaco && window.monaco.editor && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
|
|
294
|
+
if (editorInstance && editorInstance.getModel) {
|
|
295
|
+
const model = editorInstance.getModel();
|
|
296
|
+
if (model) {
|
|
297
|
+
result.content = model.getValue();
|
|
298
|
+
result.lineCount = model.getLineCount();
|
|
299
|
+
result.language = model.getLanguageId ? model.getLanguageId() : '';
|
|
300
|
+
result.hasContent = true;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch(e) {
|
|
306
|
+
// Monaco API not available, continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Fallback: Extract from view-lines
|
|
310
|
+
if (!result.content) {
|
|
311
|
+
const viewLines = targetDoc.querySelector('.monaco-editor .view-lines');
|
|
312
|
+
if (viewLines) {
|
|
313
|
+
const lines = viewLines.querySelectorAll('.view-line');
|
|
314
|
+
if (lines.length > 0) {
|
|
315
|
+
let codeContent = '';
|
|
316
|
+
let minLineNum = Infinity, maxLineNum = 0;
|
|
317
|
+
const lineMap = new Map();
|
|
318
|
+
|
|
319
|
+
lines.forEach(line => {
|
|
320
|
+
const top = parseFloat(line.style.top) || 0;
|
|
321
|
+
const lineNum = Math.round(top / 19) + 1;
|
|
322
|
+
lineMap.set(lineNum, line.textContent || '');
|
|
323
|
+
minLineNum = Math.min(minLineNum, lineNum);
|
|
324
|
+
maxLineNum = Math.max(maxLineNum, lineNum);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
for (let i = minLineNum; i <= Math.min(maxLineNum, minLineNum + 500); i++) {
|
|
328
|
+
codeContent += (lineMap.get(i) || '') + '\\n';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
result.content = codeContent;
|
|
332
|
+
result.lineCount = maxLineNum;
|
|
333
|
+
result.startLine = minLineNum;
|
|
334
|
+
result.hasContent = codeContent.trim().length > 0;
|
|
335
|
+
if (minLineNum > 1) {
|
|
336
|
+
result.isPartial = true;
|
|
337
|
+
result.note = 'Showing lines ' + minLineNum + '-' + maxLineNum + '. Scroll in Kiro to see other parts.';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Detect language from filename
|
|
344
|
+
if (!result.language && result.fileName) {
|
|
345
|
+
const ext = result.fileName.split('.').pop()?.toLowerCase();
|
|
346
|
+
const extMap = {
|
|
347
|
+
'ts': 'typescript', 'tsx': 'typescript', 'js': 'javascript', 'jsx': 'javascript',
|
|
348
|
+
'py': 'python', 'java': 'java', 'html': 'html', 'css': 'css', 'json': 'json',
|
|
349
|
+
'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'go': 'go', 'rs': 'rust',
|
|
350
|
+
'c': 'c', 'cpp': 'cpp', 'h': 'c', 'cs': 'csharp', 'rb': 'ruby', 'php': 'php',
|
|
351
|
+
'sql': 'sql', 'sh': 'bash', 'vue': 'vue', 'svelte': 'svelte'
|
|
352
|
+
};
|
|
353
|
+
result.language = extMap[ext] || ext || '';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
})()`;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
361
|
+
expression: script,
|
|
362
|
+
contextId: cdp.rootContextId,
|
|
363
|
+
returnByValue: true
|
|
364
|
+
});
|
|
365
|
+
return result.result?.value || null;
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.error('[Editor] Failed to capture:', err.message);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants used across the application
|
|
3
|
+
* Centralizes configuration to eliminate duplication and improve maintainability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CDP ports to scan for Kiro instances
|
|
8
|
+
* @type {number[]}
|
|
9
|
+
*/
|
|
10
|
+
export const CDP_PORTS = [9000, 9001, 9002, 9003, 9222, 9229];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Model names for AI model detection and matching
|
|
14
|
+
* Order matters: check specific names (opus, sonnet, haiku) BEFORE generic (claude)
|
|
15
|
+
* @type {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export const MODEL_NAMES = ['auto', 'opus', 'sonnet', 'haiku', 'gpt', 'claude', 'gemini', 'llama'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Code file extensions for workspace file filtering
|
|
21
|
+
* @type {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
export const CODE_EXTENSIONS = new Set([
|
|
24
|
+
'.ts', '.tsx', '.js', '.jsx', '.py', '.java', '.go', '.rs',
|
|
25
|
+
'.html', '.css', '.scss', '.json', '.yaml', '.yml', '.md',
|
|
26
|
+
'.sql', '.sh', '.c', '.cpp', '.h', '.cs', '.vue', '.svelte', '.rb', '.php'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* File extension to language mapping for syntax highlighting
|
|
31
|
+
* @type {Object<string, string>}
|
|
32
|
+
*/
|
|
33
|
+
export const EXTENSION_TO_LANGUAGE = {
|
|
34
|
+
'.ts': 'typescript',
|
|
35
|
+
'.tsx': 'typescript',
|
|
36
|
+
'.js': 'javascript',
|
|
37
|
+
'.jsx': 'javascript',
|
|
38
|
+
'.py': 'python',
|
|
39
|
+
'.html': 'html',
|
|
40
|
+
'.css': 'css',
|
|
41
|
+
'.scss': 'scss',
|
|
42
|
+
'.json': 'json',
|
|
43
|
+
'.md': 'markdown',
|
|
44
|
+
'.yaml': 'yaml',
|
|
45
|
+
'.yml': 'yaml',
|
|
46
|
+
'.go': 'go',
|
|
47
|
+
'.rs': 'rust',
|
|
48
|
+
'.java': 'java',
|
|
49
|
+
'.c': 'c',
|
|
50
|
+
'.cpp': 'cpp',
|
|
51
|
+
'.h': 'c',
|
|
52
|
+
'.cs': 'csharp',
|
|
53
|
+
'.rb': 'ruby',
|
|
54
|
+
'.php': 'php',
|
|
55
|
+
'.sql': 'sql',
|
|
56
|
+
'.sh': 'bash',
|
|
57
|
+
'.vue': 'vue',
|
|
58
|
+
'.svelte': 'svelte'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get language from file extension
|
|
63
|
+
* @param {string} filename - File name or path
|
|
64
|
+
* @returns {string} - Language identifier or extension without dot
|
|
65
|
+
*/
|
|
66
|
+
export function getLanguageFromExtension(filename) {
|
|
67
|
+
const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
|
|
68
|
+
return EXTENSION_TO_LANGUAGE[ext] || ext.slice(1) || 'text';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a file has a code extension
|
|
73
|
+
* @param {string} filename - File name or path
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isCodeFile(filename) {
|
|
77
|
+
const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
|
|
78
|
+
return CODE_EXTENSIONS.has(ext);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* CDP call timeout in milliseconds
|
|
83
|
+
* @type {number}
|
|
84
|
+
*/
|
|
85
|
+
export const CDP_CALL_TIMEOUT = 10000;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* HTTP request timeout in milliseconds
|
|
89
|
+
* @type {number}
|
|
90
|
+
*/
|
|
91
|
+
export const HTTP_TIMEOUT = 2000;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Discovery polling intervals
|
|
95
|
+
*/
|
|
96
|
+
export const DISCOVERY_INTERVAL_ACTIVE = 10000; // 10 seconds when changes detected
|
|
97
|
+
export const DISCOVERY_INTERVAL_STABLE = 30000; // 30 seconds when stable
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Snapshot polling intervals
|
|
101
|
+
*/
|
|
102
|
+
export const SNAPSHOT_INTERVAL_ACTIVE = 1000; // 1 second when active
|
|
103
|
+
export const SNAPSHOT_INTERVAL_IDLE = 3000; // 3 seconds when idle
|
|
104
|
+
export const SNAPSHOT_IDLE_THRESHOLD = 10000; // 10 seconds before considered idle
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Maximum depth for recursive file search
|
|
108
|
+
* @type {number}
|
|
109
|
+
*/
|
|
110
|
+
export const MAX_FILE_SEARCH_DEPTH = 4;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Maximum depth for workspace file collection
|
|
114
|
+
* @type {number}
|
|
115
|
+
*/
|
|
116
|
+
export const MAX_WORKSPACE_DEPTH = 5;
|