kiro-mobile-bridge 1.0.0 → 1.0.4

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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -221
  3. package/package.json +40 -40
  4. package/src/public/index.html +1960 -1940
  5. package/src/server.js +2488 -2672
@@ -1,1940 +1,1960 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <meta name="theme-color" content="#1e1e1e">
7
- <meta name="apple-mobile-web-app-capable" content="yes">
8
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
- <title>Kiro Mobile Bridge</title>
10
- <link rel="stylesheet" href="https://unpkg.com/@vscode/codicons@0.0.35/dist/codicon.css">
11
- <style>
12
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
- html, body { height: 100%; overflow: hidden; background: #1e1e1e; font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif; font-size: 13px; color: #fff; }
14
- .app { display: flex; flex-direction: column; height: 100%; height: 100dvh; }
15
-
16
- /* Navigation Bar (combined header + tabs) */
17
- .nav-bar { display: flex; align-items: center; background: #1e1e1e; border-bottom: 1px solid #3c3c3c; padding: 0 8px; flex-shrink: 0; }
18
- .status { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 10px 8px; }
19
- .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #666; }
20
- .status-dot.connected { background: #4caf50; }
21
- .status-dot.disconnected { background: #f44336; }
22
- .status-text { color: #888; display: none; }
23
- .nav-tabs { display: flex; align-items: center; flex: 1; overflow-x: auto; -webkit-overflow-scrolling: touch; }
24
- .nav-tabs::-webkit-scrollbar { display: none; }
25
- .nav-tab { display: flex; align-items: center; gap: 6px; padding: 10px 16px; color: #888; font-size: 12px; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; white-space: nowrap; user-select: none; }
26
- .header-title { font-size: 13px; font-weight: 600; color: #888; padding: 10px 12px; white-space: nowrap; }
27
- .nav-tab:hover { color: #ccc; background: rgba(255,255,255,0.05); }
28
- .nav-tab.active { color: #fff; border-bottom-color: #7138cc; }
29
- .nav-tab .codicon { font-size: 14px; }
30
-
31
- /* Panel Container */
32
- .panel-container { flex: 1; overflow: hidden; position: relative; }
33
- .panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; display: none; }
34
- .panel.active { display: flex; flex-direction: column; }
35
-
36
- /* Chat Panel */
37
- .chat-container { flex: 1; overflow: hidden; position: relative; }
38
- .chat-content { width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }
39
- #chatContent, #chatContent * { font-family: inherit; }
40
- #chatContent code, #chatContent pre { font-family: Menlo, Monaco, "Courier New", monospace !important; }
41
- #chatContent .codicon { font-family: codicon !important; font-size: 16px; line-height: 1; }
42
-
43
- /* Editor Panel */
44
- .editor-container { flex: 1; overflow: hidden; background: #1e1e1e; display: flex; flex-direction: column; position: relative; }
45
- .editor-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; flex-shrink: 0; user-select: none; }
46
- .editor-filename { font-size: 13px; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
47
- .editor-search-btn { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
48
- .editor-search-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
49
- .editor-explorer-btn { display: flex; align-items: center; gap: 6px; color: #888; font-size: 14px; cursor: pointer; padding: 4px 8px; border-radius: 4px; background: #3c3c3c; }
50
- .editor-explorer-btn:hover { background: #4a4a4a; }
51
- .editor-explorer-text { font-size: 11px; font-weight: 600; color: #888; letter-spacing: 0.5px; }
52
- .editor-search-bar { display: none; align-items: center; gap: 8px; padding: 6px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; }
53
- .editor-search-bar.open { display: flex; }
54
- .editor-search-bar input { flex: 1; padding: 6px 10px; background: #1e1e1e; border: 1px solid #3c3c3c; border-radius: 4px; color: #fff; font-size: 13px; outline: none; }
55
- .editor-search-bar input:focus { border-color: #007acc; }
56
- .editor-search-count { font-size: 12px; color: #888; min-width: 50px; text-align: center; }
57
- .editor-search-nav { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
58
- .editor-search-nav:hover { color: #fff; background: rgba(255,255,255,0.1); }
59
- .editor-search-close { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
60
- .editor-search-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
61
- .editor-content { flex: 1; overflow: auto; -webkit-overflow-scrolling: touch; background: #1e1e1e; }
62
- .editor-code { margin: 0; padding: 8px 0; font-family: 'Cascadia Code', 'Fira Code', Menlo, Monaco, "Courier New", monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre; overflow-x: auto; background: #1e1e1e; }
63
- .editor-line { display: flex; min-height: 1.6em; }
64
- .editor-line:hover { background: rgba(255,255,255,0.04); }
65
- .editor-line-num { width: 45px; min-width: 45px; text-align: right; padding-right: 12px; color: #6e7681; user-select: none; flex-shrink: 0; font-size: 12px; }
66
- .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; tab-size: 2; }
67
- .search-highlight { background: #ffd500; color: #000; border-radius: 2px; padding: 0 1px; }
68
- .search-highlight.current { background: #ff6b00; color: #fff; outline: 2px solid #ff6b00; }
69
-
70
- /* File Tree Panel */
71
- .file-tree-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #252526; display: flex; flex-direction: column; z-index: 100; display: none; }
72
- .file-tree-panel.open { display: flex; }
73
- .file-tree-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; }
74
- .file-tree-header-title { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
75
- .file-tree-close { margin-left: auto; color: #888; cursor: pointer; font-size: 16px; }
76
- .file-tree-close:hover { color: #fff; }
77
- .file-tree { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 4px 0; }
78
- .file-tree-folder-header { display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; user-select: none; }
79
- .file-tree-folder-header:hover { background: rgba(255,255,255,0.05); }
80
- .file-tree-folder-icon { color: #888; font-size: 14px; transition: transform 0.15s; }
81
- .file-tree-folder-icon.expanded { transform: rotate(90deg); }
82
- .file-tree-folder-name { font-size: 13px; color: #c5c5c5; }
83
- .file-tree-folder-contents { padding-left: 16px; display: none; }
84
- .file-tree-folder-contents.expanded { display: block; }
85
- .file-tree-file { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 24px; cursor: pointer; }
86
- .file-tree-file:hover { background: rgba(255,255,255,0.05); }
87
- .file-tree-file:active { background: #094771; }
88
- .file-tree-file-icon { color: #888; font-size: 14px; }
89
- .file-tree-file-name { font-size: 13px; color: #c5c5c5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
90
- .file-tree-empty { padding: 20px; text-align: center; color: #666; font-size: 13px; }
91
- .file-tree-loading { padding: 20px; text-align: center; color: #888; display: flex; flex-direction: column; align-items: center; gap: 8px; }
92
-
93
- /* Syntax Highlighting */
94
- .tok-kw { color: #569cd6; }
95
- .tok-str { color: #ce9178; }
96
- .tok-num { color: #b5cea8; }
97
- .tok-cmt { color: #6a9955; }
98
- .tok-fn { color: #dcdcaa; }
99
- .tok-type { color: #4ec9b0; }
100
-
101
- /* Syntax Highlighting */
102
- .token-keyword { color: #569cd6; font-weight: 500; }
103
- .token-string { color: #ce9178; }
104
- .token-number { color: #b5cea8; }
105
- .token-comment { color: #6a9955; font-style: italic; }
106
- .token-function { color: #dcdcaa; }
107
- .token-type { color: #4ec9b0; }
108
- .token-operator { color: #d4d4d4; }
109
-
110
- /* Loading & Toast */
111
- .loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #888; font-size: 14px; flex-direction: column; gap: 12px; }
112
- .loading-spinner { width: 24px; height: 24px; border: 2px solid #3c3c3c; border-top-color: #7138cc; border-radius: 50%; animation: spin 1s linear infinite; }
113
- @keyframes spin { to { transform: rotate(360deg); } }
114
-
115
- .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #f44336; color: #fff; padding: 10px 16px; border-radius: 6px; font-size: 13px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; z-index: 1000; }
116
- .toast.visible { opacity: 1; }
117
- .toast.success { background: #4caf50; }
118
-
119
- /* Empty State */
120
- .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #888; text-align: center; padding: 20px; }
121
- .empty-state .codicon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
122
- .empty-state p { font-size: 14px; max-width: 280px; }
123
-
124
- /* Task List Styling - Match Kiro IDE */
125
- [class*="task"], [class*="Task"] {
126
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
127
- }
128
-
129
- /* Task section headers (CURRENT TASKS, TASKS IN QUEUE) */
130
- [class*="task"] h2, [class*="Task"] h2,
131
- [class*="task"] h3, [class*="Task"] h3,
132
- [class*="task-header"], [class*="TaskHeader"],
133
- [class*="section-title"], [class*="SectionTitle"] {
134
- font-size: 13px !important;
135
- font-weight: 700 !important;
136
- letter-spacing: 0.5px !important;
137
- text-transform: uppercase !important;
138
- color: #ffffff !important;
139
- margin: 0 0 12px 0 !important;
140
- padding: 0 !important;
141
- line-height: 1.4 !important;
142
- }
143
-
144
- /* Task list containers */
145
- [class*="task-list"], [class*="TaskList"],
146
- [class*="task-section"], [class*="TaskSection"] {
147
- margin-bottom: 24px !important;
148
- padding: 0 !important;
149
- }
150
-
151
- /* Empty state messages (No active tasks, No tasks in queue) */
152
- [class*="task"] p:empty, [class*="Task"] p:empty,
153
- [class*="no-task"], [class*="NoTask"],
154
- [class*="empty-task"], [class*="EmptyTask"],
155
- [class*="task-empty"], [class*="TaskEmpty"] {
156
- font-size: 13px !important;
157
- font-style: italic !important;
158
- color: #888888 !important;
159
- margin: 0 0 16px 0 !important;
160
- padding: 0 !important;
161
- line-height: 1.6 !important;
162
- }
163
-
164
- /* Catch all paragraphs under task sections that look like empty states */
165
- [class*="task"] p, [class*="Task"] p {
166
- font-size: 13px !important;
167
- line-height: 1.6 !important;
168
- }
169
-
170
- /* Specific targeting for common patterns */
171
- div[class*="current"] h2, div[class*="Current"] h2,
172
- div[class*="queue"] h2, div[class*="Queue"] h2 {
173
- font-size: 13px !important;
174
- font-weight: 700 !important;
175
- letter-spacing: 0.5px !important;
176
- text-transform: uppercase !important;
177
- color: #ffffff !important;
178
- }
179
-
180
- /* Horizontal divider between sections */
181
- [class*="task-divider"], [class*="TaskDivider"],
182
- [class*="task"] hr, [class*="Task"] hr {
183
- border: none !important;
184
- border-top: 1px solid #3c3c3c !important;
185
- margin: 20px 0 !important;
186
- }
187
-
188
- /* Individual task items (when they exist) */
189
- [class*="task-item"], [class*="TaskItem"] {
190
- padding: 8px 12px !important;
191
- margin: 4px 0 !important;
192
- background: rgba(255, 255, 255, 0.03) !important;
193
- border-radius: 4px !important;
194
- border-left: 2px solid #7138cc !important;
195
- font-size: 13px !important;
196
- color: #cccccc !important;
197
- line-height: 1.5 !important;
198
- }
199
-
200
- [class*="task-item"]:hover, [class*="TaskItem"]:hover {
201
- background: rgba(255, 255, 255, 0.05) !important;
202
- }
203
-
204
- /* Task item with close button */
205
- [class*="task-item"] button, [class*="TaskItem"] button {
206
- margin-left: 8px !important;
207
- padding: 2px 6px !important;
208
- font-size: 11px !important;
209
- background: rgba(255, 255, 255, 0.1) !important;
210
- border: 1px solid #4a464f !important;
211
- border-radius: 3px !important;
212
- color: #888 !important;
213
- cursor: pointer !important;
214
- }
215
-
216
- [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
217
- background: rgba(255, 255, 255, 0.15) !important;
218
- color: #fff !important;
219
- }
220
- </style>
221
- </head>
222
- <body>
223
- <div class="app">
224
- <!-- Navigation Bar (tabs | title + indicator) -->
225
- <nav class="nav-bar">
226
- <div class="nav-tabs" id="navTabs">
227
- <div class="nav-tab active" data-panel="chat">
228
- <span class="codicon codicon-comment-discussion"></span>
229
- <span>Chat</span>
230
- </div>
231
- <div class="nav-tab" data-panel="editor">
232
- <span class="codicon codicon-code"></span>
233
- <span>Editor</span>
234
- </div>
235
- </div>
236
- <span class="header-title">Kiro Mobile</span>
237
- <div class="status">
238
- <span class="status-dot" id="statusDot"></span>
239
- <span class="status-text" id="statusText">Connecting...</span>
240
- </div>
241
- </nav>
242
-
243
- <!-- Panel Container -->
244
- <div class="panel-container">
245
- <!-- Chat Panel -->
246
- <div class="panel active" id="chatPanel">
247
- <div class="chat-container" id="chatContainer">
248
- <div class="loading" id="chatLoading"><div class="loading-spinner"></div><span>Waiting for Kiro...</span></div>
249
- <div class="chat-content" id="chatContent" style="display: none;"></div>
250
- </div>
251
- </div>
252
-
253
- <!-- Editor Panel -->
254
- <div class="panel" id="editorPanel">
255
- <div class="editor-container">
256
- <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading editor...</span></div>
257
- <div class="editor-header" id="editorHeader" style="display: none;">
258
- <span class="codicon codicon-file-code" style="color: #888;"></span>
259
- <span class="editor-filename" id="editorFilename">No file open</span>
260
- <span class="editor-search-btn codicon codicon-search" id="editorSearchBtn" title="Find in file"></span>
261
- <div class="editor-explorer-btn" id="editorExplorerBtn" title="Open Explorer">
262
- <span class="codicon codicon-folder" style="color: #dcb67a;"></span>
263
- <span class="editor-explorer-text">EXPLORER</span>
264
- </div>
265
- </div>
266
- <div class="editor-search-bar" id="editorSearchBar">
267
- <input type="text" id="editorSearchInput" placeholder="Find in file..." autocomplete="off" />
268
- <span class="editor-search-count" id="editorSearchCount"></span>
269
- <span class="editor-search-nav codicon codicon-arrow-up" id="editorSearchPrev" title="Previous"></span>
270
- <span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext" title="Next"></span>
271
- <span class="editor-search-close codicon codicon-close" id="editorSearchClose" title="Close"></span>
272
- </div>
273
- <div class="file-tree-panel" id="fileTreePanel">
274
- <div class="file-tree-header">
275
- <span class="codicon codicon-folder-opened" style="color: #dcb67a;"></span>
276
- <span class="file-tree-header-title">Explorer</span>
277
- <span class="file-tree-close codicon codicon-close" id="fileTreeClose"></span>
278
- </div>
279
- <div class="file-tree" id="fileTree">
280
- <div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>
281
- </div>
282
- </div>
283
- <div class="editor-content" id="editorContent" style="display: none;"></div>
284
- </div>
285
- </div>
286
- </div>
287
-
288
- <div class="toast" id="toast"></div>
289
- </div>
290
-
291
- <script>
292
- // State
293
- let ws = null, reconnectAttempts = 0, reconnectTimeout = null;
294
- let cascades = [], selectedCascadeId = null, currentStyles = null;
295
- let isTypingInKiroInput = false;
296
- let activePanel = 'chat';
297
- let lastSuccessfulSnapshot = null;
298
- let navigationPending = false;
299
- let workspaceFiles = []; // Cached file list
300
- let fileTreeOpen = false;
301
- let expandedFolders = new Set(); // Track expanded folders
302
- let updateDebounceTimer = null; // Debounce rapid snapshot updates
303
- let isRendering = false; // Prevent concurrent renders
304
-
305
- // DOM Elements
306
- const statusDot = document.getElementById('statusDot');
307
- const statusText = document.getElementById('statusText');
308
- const navTabs = document.getElementById('navTabs');
309
- const toast = document.getElementById('toast');
310
- const fileTreePanel = document.getElementById('fileTreePanel');
311
- const fileTree = document.getElementById('fileTree');
312
- const fileTreeClose = document.getElementById('fileTreeClose');
313
-
314
- // Panel elements
315
- const panels = {
316
- chat: {
317
- panel: document.getElementById('chatPanel'),
318
- loading: document.getElementById('chatLoading'),
319
- content: document.getElementById('chatContent'),
320
- container: document.getElementById('chatContainer')
321
- },
322
- editor: {
323
- panel: document.getElementById('editorPanel'),
324
- loading: document.getElementById('editorLoading'),
325
- content: document.getElementById('editorContent'),
326
- header: document.getElementById('editorHeader'),
327
- filename: document.getElementById('editorFilename')
328
- }
329
- };
330
-
331
- // Search state
332
- let searchMatches = [];
333
- let currentMatchIndex = -1;
334
- let originalEditorContent = '';
335
-
336
- // Search DOM elements
337
- const editorSearchBtn = document.getElementById('editorSearchBtn');
338
- const editorSearchBar = document.getElementById('editorSearchBar');
339
- const editorSearchInput = document.getElementById('editorSearchInput');
340
- const editorSearchCount = document.getElementById('editorSearchCount');
341
- const editorSearchPrev = document.getElementById('editorSearchPrev');
342
- const editorSearchNext = document.getElementById('editorSearchNext');
343
- const editorSearchClose = document.getElementById('editorSearchClose');
344
-
345
- // Status management
346
- function setStatus(status) {
347
- statusDot.className = 'status-dot ' + status;
348
- statusText.textContent = status === 'connected' ? 'Connected' : status === 'disconnected' ? 'Disconnected' : 'Connecting...';
349
- }
350
-
351
- function showToast(message, duration = 3000, isSuccess = false) {
352
- toast.textContent = message;
353
- toast.className = 'toast visible' + (isSuccess ? ' success' : '');
354
- setTimeout(() => { toast.classList.remove('visible'); }, duration);
355
- }
356
-
357
- // Navigation
358
- navTabs.addEventListener('click', (e) => {
359
- const tab = e.target.closest('.nav-tab');
360
- if (!tab) return;
361
-
362
- const panelName = tab.dataset.panel;
363
- if (panelName === activePanel) return;
364
-
365
- // Update tabs
366
- navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
367
- tab.classList.add('active');
368
-
369
- // Update panels
370
- Object.keys(panels).forEach(key => {
371
- panels[key].panel.classList.toggle('active', key === panelName);
372
- });
373
-
374
- activePanel = panelName;
375
-
376
- // Fetch content for the new panel
377
- if (selectedCascadeId) {
378
- fetchPanelContent(panelName, selectedCascadeId);
379
- }
380
- });
381
-
382
- // WebSocket connection
383
- function connect() {
384
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
385
- ws = new WebSocket(`${protocol}//${window.location.host}`);
386
-
387
- ws.onopen = () => {
388
- setStatus('connected');
389
- reconnectAttempts = 0;
390
- };
391
-
392
- ws.onmessage = (e) => {
393
- try {
394
- handleMessage(JSON.parse(e.data));
395
- } catch (err) {
396
- console.error('Failed to parse message:', err);
397
- }
398
- };
399
-
400
- ws.onclose = () => {
401
- setStatus('disconnected');
402
- scheduleReconnect();
403
- };
404
-
405
- ws.onerror = () => {};
406
- }
407
-
408
- function scheduleReconnect() {
409
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
410
- const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
411
- reconnectAttempts++;
412
- reconnectTimeout = setTimeout(connect, delay);
413
- }
414
-
415
- function handleMessage(data) {
416
- if (data.type === 'cascade_list') {
417
- cascades = data.cascades || [];
418
- if (!selectedCascadeId && cascades.length > 0) {
419
- selectCascade(cascades[0].id);
420
- }
421
- } else if (data.type === 'snapshot_update' && data.cascadeId === selectedCascadeId) {
422
- // Only fetch if user is not typing AND the updated panel is the active one
423
- // AND we're not currently rendering (prevent flickering from rapid updates)
424
- const panel = data.panel || 'chat';
425
- if (!isTypingInKiroInput && panel === activePanel && !isRendering) {
426
- // Debounce rapid updates - wait 500ms before fetching
427
- if (updateDebounceTimer) clearTimeout(updateDebounceTimer);
428
- updateDebounceTimer = setTimeout(() => {
429
- fetchPanelContent(panel, data.cascadeId, true); // true = isUpdate (don't scroll)
430
- }, 500);
431
- }
432
- }
433
- }
434
-
435
- function selectCascade(cascadeId) {
436
- selectedCascadeId = cascadeId;
437
- currentStyles = null;
438
-
439
- if (cascadeId) {
440
- fetchStyles(cascadeId).then(() => {
441
- fetchPanelContent(activePanel, cascadeId);
442
- });
443
- } else {
444
- showLoading('chat', 'Waiting for Kiro...');
445
- }
446
- }
447
-
448
- async function fetchStyles(cascadeId) {
449
- try {
450
- const r = await fetch(`/styles/${cascadeId}`);
451
- if (r.ok) currentStyles = await r.text();
452
- } catch (e) {
453
- console.error('Failed to fetch styles:', e);
454
- }
455
- }
456
-
457
- function showLoading(panelName, msg = 'Loading...') {
458
- const p = panels[panelName];
459
- if (p.loading) {
460
- p.loading.querySelector('span').textContent = msg;
461
- p.loading.style.display = 'flex';
462
- }
463
- if (p.content) p.content.style.display = 'none';
464
- if (p.header) p.header.style.display = 'none';
465
- }
466
-
467
- function hideLoading(panelName) {
468
- const p = panels[panelName];
469
- if (p.loading) p.loading.style.display = 'none';
470
- if (p.content) p.content.style.display = 'block';
471
- if (p.header) p.header.style.display = 'flex';
472
- }
473
-
474
- // Fetch panel content
475
- async function fetchPanelContent(panelName, cascadeId, isUpdate = false) {
476
- switch (panelName) {
477
- case 'chat':
478
- await fetchChatSnapshot(cascadeId, isUpdate);
479
- break;
480
- case 'editor':
481
- await fetchEditorSnapshot(cascadeId, isUpdate);
482
- break;
483
- }
484
- }
485
-
486
- // Chat snapshot
487
- async function fetchChatSnapshot(cascadeId, isUpdate = false) {
488
- try {
489
- const r = await fetch(`/snapshot/${cascadeId}`);
490
- if (!r.ok) {
491
- if (navigationPending) {
492
- setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500);
493
- return;
494
- }
495
- if (r.status === 404) showLoading('chat', 'Waiting for snapshot...');
496
- return;
497
- }
498
- const snapshot = await r.json();
499
- if (snapshot && snapshot.html && snapshot.html.length > 100) {
500
- lastSuccessfulSnapshot = snapshot;
501
- navigationPending = false;
502
- renderChatSnapshot(snapshot, isUpdate);
503
- } else if (lastSuccessfulSnapshot && navigationPending) {
504
- setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 300);
505
- } else {
506
- renderChatSnapshot(snapshot);
507
- }
508
- } catch (e) {
509
- if (!navigationPending) showToast('Failed to load chat');
510
- }
511
- }
512
-
513
- function renderChatSnapshot(snapshot, isUpdate = false) {
514
- // Prevent concurrent renders
515
- if (isRendering) return;
516
- isRendering = true;
517
-
518
- // Save scroll position of inner scrollable element before update
519
- let innerScrollable = panels.chat.content.querySelector('.overflow-y-auto, [class*="scroll"]');
520
- const hadContent = innerScrollable && innerScrollable.scrollHeight > 100;
521
- const scrollTop = innerScrollable ? innerScrollable.scrollTop : 0;
522
- const scrollHeight = innerScrollable ? innerScrollable.scrollHeight : 0;
523
- const clientHeight = innerScrollable ? innerScrollable.clientHeight : 0;
524
- const wasAtBottom = !hadContent || (scrollHeight - scrollTop - clientHeight < 100);
525
-
526
- let html = getBaseStyles();
527
- if (currentStyles) html += `<style>${currentStyles}</style>`;
528
- html += snapshot.html || '';
529
-
530
- panels.chat.content.innerHTML = html;
531
-
532
- // Set background color - use solid dark background instead of transparent
533
- if (snapshot.bodyBg) {
534
- panels.chat.container.style.background = snapshot.bodyBg;
535
- } else {
536
- // Fallback to solid dark background matching Kiro IDE
537
- panels.chat.container.style.background = '#1e1e1e';
538
- }
539
-
540
- hideLoading('chat');
541
-
542
- // Find the new inner scrollable element and scroll it
543
- requestAnimationFrame(() => {
544
- requestAnimationFrame(() => {
545
- // Find all scrollable elements and scroll them
546
- const scrollables = panels.chat.content.querySelectorAll('.overflow-y-auto, [class*="scroll"]');
547
- scrollables.forEach(el => {
548
- if (el.scrollHeight > el.clientHeight + 10) {
549
- if (!isUpdate || wasAtBottom) {
550
- el.scrollTop = el.scrollHeight;
551
- } else if (hadContent) {
552
- el.scrollTop = scrollTop;
553
- }
554
- }
555
- });
556
-
557
- // Also scroll the main container if needed
558
- if (!isUpdate || wasAtBottom) {
559
- panels.chat.content.scrollTop = panels.chat.content.scrollHeight;
560
- }
561
-
562
- // Release rendering lock
563
- isRendering = false;
564
- });
565
- });
566
-
567
- makeInteractive();
568
- }
569
-
570
- // Editor snapshot
571
- async function fetchEditorSnapshot(cascadeId, isUpdate = false) {
572
- try {
573
- const r = await fetch(`/editor/${cascadeId}`);
574
- if (!r.ok) {
575
- showEmptyState('editor', 'codicon-file-code', 'No file is currently open. Open a file in Kiro to view it here.');
576
- return;
577
- }
578
- const data = await r.json();
579
- renderEditorSnapshot(data, isUpdate);
580
- } catch (e) {
581
- showEmptyState('editor', 'codicon-file-code', 'Failed to load editor');
582
- }
583
- }
584
-
585
- function renderEditorSnapshot(data, isUpdate = false) {
586
- const scrollTop = panels.editor.content.scrollTop;
587
-
588
- // Update header
589
- panels.editor.filename.textContent = data.fileName || 'Untitled';
590
-
591
- // Show header
592
- panels.editor.header.style.display = 'flex';
593
-
594
- // Store original content for search
595
- originalEditorContent = data.content || '';
596
-
597
- // Reset search when new file loaded
598
- closeEditorSearch();
599
-
600
- // Prefer text content over raw HTML for better mobile display
601
- if (data.content && data.content.trim().length > 0) {
602
- const lines = data.content.split('\n');
603
- let html = '';
604
-
605
- // Add note if partial content
606
- if (data.isPartial || data.note) {
607
- html += `<div style="padding: 8px 12px; background: #2d2d30; color: #888; font-size: 11px; border-bottom: 1px solid #3c3c3c;">
608
- <span class="codicon codicon-info" style="margin-right: 6px;"></span>
609
- ${data.note || 'Showing visible lines only. Scroll in Kiro to see more.'}
610
- </div>`;
611
- }
612
-
613
- html += '<pre class="editor-code">';
614
-
615
- // Use startLine from server if available, otherwise start from 1
616
- const startLineNum = data.startLine || 1;
617
-
618
- // Filter out empty lines at the start if there are too many
619
- let startIdx = 0;
620
- while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) {
621
- startIdx++;
622
- }
623
-
624
- const displayLines = lines.slice(startIdx);
625
- displayLines.forEach((line, idx) => {
626
- const lineNum = startLineNum + startIdx + idx;
627
- const highlighted = highlightSyntax(line, data.language);
628
- // Preserve whitespace - convert tabs and spaces
629
- let preservedLine = highlighted
630
- .replace(/\t/g, ' ')
631
- .replace(/^ +/, match => '&nbsp;'.repeat(match.length));
632
- html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${preservedLine || '&nbsp;'}</span></div>`;
633
- });
634
- html += '</pre>';
635
-
636
- // Add line count info
637
- const endLine = startLineNum + startIdx + displayLines.length - 1;
638
- html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
639
- Lines ${startLineNum}-${endLine}${data.lineCount ? ` of ${data.lineCount}` : ''}
640
- </div>`;
641
-
642
- panels.editor.content.innerHTML = html;
643
- panels.editor.content.style.display = 'block';
644
- console.log('[Editor] Rendered', displayLines.length, 'lines of code starting from line', startLineNum);
645
- } else if (data.html && data.html.length > 100) {
646
- // Fallback to raw HTML if no text content
647
- let html = getBaseStyles();
648
- if (currentStyles) html += `<style>${currentStyles}</style>`;
649
- html += data.html;
650
- panels.editor.content.innerHTML = html;
651
- panels.editor.content.style.display = 'block';
652
- console.log('[Editor] Rendered raw HTML');
653
- } else {
654
- showEmptyState('editor', 'codicon-file-code', 'No editor content available. Open a file in Kiro to view it here.');
655
- return;
656
- }
657
- hideLoading('editor');
658
-
659
- // Preserve scroll on updates
660
- if (isUpdate) {
661
- panels.editor.content.scrollTop = scrollTop;
662
- }
663
- }
664
-
665
- // Syntax highlighting (basic)
666
- function highlightSyntax(line, language) {
667
- let escaped = escapeHtml(line);
668
-
669
- // Skip syntax highlighting for CSS (too complex with # colors)
670
- if (language === 'css' || language === 'html') {
671
- return escaped;
672
- }
673
-
674
- // Comments - be careful not to match CSS color codes
675
- // Only match // comments and # comments that start at beginning or after whitespace
676
- escaped = escaped.replace(/(\/\/.*$)/gm, '<span class="token-comment">$1</span>');
677
- escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
678
- // Python/shell comments - only if # is at start of line or after whitespace, not in middle of word
679
- if (language === 'python' || language === 'bash' || language === 'sh') {
680
- escaped = escaped.replace(/(^|\s)(#.*)$/gm, '$1<span class="token-comment">$2</span>');
681
- }
682
-
683
- // Strings - match quoted strings
684
- escaped = escaped.replace(/(&quot;[^&]*?&quot;)/g, '<span class="token-string">$1</span>');
685
- escaped = escaped.replace(/(&#39;[^&]*?&#39;)/g, '<span class="token-string">$1</span>');
686
-
687
- // Keywords
688
- const keywords = ['const', 'let', 'var', 'function', 'class', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'super', 'extends', 'implements', 'interface', 'type', 'enum', 'public', 'private', 'protected', 'static', 'readonly', 'def', 'self', 'None', 'True', 'False'];
689
- keywords.forEach(kw => {
690
- const regex = new RegExp(`\\b(${kw})\\b`, 'g');
691
- escaped = escaped.replace(regex, '<span class="token-keyword">$1</span>');
692
- });
693
-
694
- // Numbers - but not inside other tokens
695
- escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
696
-
697
- return escaped;
698
- }
699
-
700
- // Helper functions
701
- function escapeHtml(text) {
702
- const div = document.createElement('div');
703
- div.textContent = text;
704
- return div.innerHTML;
705
- }
706
-
707
- function showEmptyState(panelName, icon, message) {
708
- const p = panels[panelName];
709
- p.content.innerHTML = `
710
- <div class="empty-state">
711
- <span class="codicon ${icon}"></span>
712
- <p>${message}</p>
713
- </div>
714
- `;
715
- hideLoading(panelName);
716
- }
717
-
718
- function toggleSection(header) {
719
- header.classList.toggle('collapsed');
720
- const content = header.nextElementSibling;
721
- if (content) content.classList.toggle('collapsed');
722
- }
723
-
724
- // Base styles for chat panel
725
- function getBaseStyles() {
726
- return `<style>
727
- :root, body, #root, .app {
728
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
729
- font-size: 13px;
730
- --vscode-editor-background: #211d25;
731
- --vscode-editor-foreground: #ffffff;
732
- --vscode-foreground: #ffffff;
733
- --vscode-background: #19161d;
734
- --color-text-primary: #ffffff;
735
- --color-text-secondary: rgba(255, 255, 255, 0.8);
736
- --color-text-tertiary: rgba(255, 255, 255, 0.6);
737
- --color-border-primary: #4a464f;
738
- --vscode-toggleSwitch-onForeground: #ffffff;
739
- --vscode-toggleSwitch-onBackground: #8141e6;
740
- --vscode-toggleSwitch-offForeground: #ffffff;
741
- --vscode-toggleSwitch-offBackground: #28242e;
742
- --vscode-chatTab-foreground: #c1bec6;
743
- --vscode-chatTab-background: transparent;
744
- --vscode-button-tertiaryForeground: #c1bec6;
745
- --vscode-button-tertiaryBackground: transparent;
746
- --vscode-button-tertiaryHoverBackground: rgba(255, 255, 255, 0.1);
747
- --vscode-button-submitBackground: #7138cc;
748
- --vscode-button-submitHoverBackground: #8e47ff;
749
- --vscode-input-border: #4a464f;
750
- --vscode-chatMessage-background: rgba(255, 255, 255, 0.1);
751
- --vscode-agentInProgressIcon-foreground: #7138cc;
752
- --vscode-agentAcceptedIcon-foreground: rgba(128, 255, 181, 0.75);
753
- }
754
-
755
- /* Force solid background for all content */
756
- body, #root, .app, [class*="chat"], [class*="Chat"], [class*="cascade"], [class*="Cascade"] {
757
- background: #1e1e1e !important;
758
- background-color: #1e1e1e !important;
759
- }
760
-
761
- /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
762
- [role="tooltip"],
763
- [data-tooltip],
764
- [class*="tooltip"]:not(button):not([role="button"]),
765
- [class*="Tooltip"]:not(button):not([role="button"]),
766
- [class*="popover"]:not(button):not([role="button"]),
767
- [class*="Popover"]:not(button):not([role="button"]),
768
- [class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
769
- [class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
770
- [class*="modal"],
771
- [class*="Modal"] {
772
- display: none !important;
773
- visibility: hidden !important;
774
- opacity: 0 !important;
775
- pointer-events: none !important;
776
- }
777
-
778
- /* ========== MODEL SELECTOR DROPDOWN ========== */
779
- /* Dropdown menu panel - needs solid background and proper z-index */
780
- [class*="dropdown-menu"], [class*="dropdownMenu"], [class*="DropdownMenu"],
781
- [class*="dropdown-content"], [class*="dropdownContent"], [class*="DropdownContent"],
782
- [class*="model-list"], [class*="modelList"], [class*="ModelList"],
783
- [role="listbox"], [role="menu"] {
784
- display: block !important;
785
- visibility: visible !important;
786
- opacity: 1 !important;
787
- pointer-events: auto !important;
788
- background: #2d2d30 !important;
789
- background-color: #2d2d30 !important;
790
- border: 1px solid #4a464f !important;
791
- border-radius: 6px !important;
792
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
793
- z-index: 9999 !important;
794
- position: absolute !important;
795
- }
796
-
797
- /* Dropdown menu items */
798
- [class*="dropdown-item"], [class*="dropdownItem"], [class*="DropdownItem"],
799
- [class*="model-option"], [class*="modelOption"], [class*="ModelOption"],
800
- [role="option"], [role="menuitem"] {
801
- display: flex !important;
802
- visibility: visible !important;
803
- opacity: 1 !important;
804
- pointer-events: auto !important;
805
- padding: 8px 12px !important;
806
- cursor: pointer !important;
807
- background: transparent !important;
808
- color: #cccccc !important;
809
- }
810
-
811
- [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
812
- [role="option"]:hover, [role="menuitem"]:hover {
813
- background: rgba(255, 255, 255, 0.1) !important;
814
- }
815
-
816
- /* Model selector button - NO border, match Kiro style */
817
- [class*="model-selector"], [class*="modelSelector"], [class*="ModelSelector"],
818
- [class*="model-dropdown"], [class*="modelDropdown"], [class*="ModelDropdown"],
819
- button[class*="dropdown"], [role="button"][class*="dropdown"],
820
- [class*="select-model"], [class*="selectModel"] {
821
- display: inline-flex !important;
822
- visibility: visible !important;
823
- opacity: 1 !important;
824
- pointer-events: auto !important;
825
- align-items: center !important;
826
- gap: 4px !important;
827
- padding: 4px 8px !important;
828
- background: transparent !important;
829
- border: none !important;
830
- border-radius: 4px !important;
831
- color: var(--vscode-foreground, #c1bec6) !important;
832
- font-size: 12px !important;
833
- cursor: pointer !important;
834
- }
835
-
836
- [class*="model-selector"]:hover, [class*="modelSelector"]:hover,
837
- button[class*="dropdown"]:hover {
838
- background: rgba(255, 255, 255, 0.1) !important;
839
- }
840
-
841
- /* Model selector chevron/arrow icon */
842
- [class*="model-selector"] svg, [class*="modelSelector"] svg,
843
- [class*="model-dropdown"] svg, button[class*="dropdown"] svg {
844
- width: 12px !important;
845
- height: 12px !important;
846
- flex-shrink: 0 !important;
847
- }
848
-
849
- /* Model name text inside selector */
850
- [class*="model-selector"] span, [class*="modelSelector"] span,
851
- [class*="model-name"], [class*="modelName"] {
852
- overflow: hidden !important;
853
- text-overflow: ellipsis !important;
854
- white-space: nowrap !important;
855
- }
856
-
857
- /* Reset positioning for captured content to prevent layout issues */
858
- #root, .app, [class*="cascade"], [class*="Cascade"] {
859
- position: relative !important;
860
- }
861
-
862
- /* Ensure proper stacking and no fixed positioning breaks layout */
863
- [style*="position: fixed"],
864
- [style*="position:fixed"] {
865
- position: absolute !important;
866
- }
867
-
868
- * { font-family: inherit; }
869
- code, pre, .monaco-editor { font-family: Menlo, Monaco, "Courier New", monospace !important; }
870
- /* Icon sizes - keep them small like in IDE */
871
- svg[viewBox="0 0 24 24"] { width: 16px !important; height: 16px !important; }
872
- svg[viewBox="0 0 16 16"] { width: 14px !important; height: 14px !important; }
873
- svg { color: inherit; max-width: 20px !important; max-height: 20px !important; }
874
- svg path, svg rect { fill: currentColor; }
875
- svg[fill="none"] path { fill: none; }
876
- /* Preserve stroke-based SVG circles (like context window progress ring) */
877
- svg circle[stroke] { fill: none !important; }
878
- svg circle:not([stroke]) { fill: currentColor; }
879
- /* Context window progress circle styling */
880
- svg[class*="context"], svg[class*="progress"], [class*="context"] svg {
881
- overflow: visible !important;
882
- }
883
- svg circle[stroke-dasharray] {
884
- fill: none !important;
885
- stroke-linecap: round !important;
886
- }
887
- /* Context window indicator - circular progress ring */
888
- [class*="context-window"], [class*="contextWindow"], [class*="ContextWindow"],
889
- [class*="context-indicator"], [class*="contextIndicator"], [class*="ContextIndicator"],
890
- [class*="token-usage"], [class*="tokenUsage"], [class*="TokenUsage"] {
891
- display: inline-flex !important;
892
- visibility: visible !important;
893
- opacity: 1 !important;
894
- align-items: center !important;
895
- justify-content: center !important;
896
- width: 20px !important;
897
- height: 20px !important;
898
- position: relative !important;
899
- flex-shrink: 0 !important;
900
- }
901
- /* Create gray background circle using box-shadow on the container */
902
- [class*="context-window"]::before, [class*="contextWindow"]::before, [class*="ContextWindow"]::before,
903
- [class*="context-indicator"]::before, [class*="contextIndicator"]::before {
904
- content: '' !important;
905
- position: absolute !important;
906
- top: 2px !important;
907
- left: 2px !important;
908
- width: 16px !important;
909
- height: 16px !important;
910
- border-radius: 50% !important;
911
- border: 2px solid #4a464f !important;
912
- box-sizing: border-box !important;
913
- z-index: 0 !important;
914
- }
915
- [class*="context-window"] svg, [class*="contextWindow"] svg, [class*="ContextWindow"] svg,
916
- [class*="context-indicator"] svg, [class*="contextIndicator"] svg {
917
- width: 16px !important;
918
- height: 16px !important;
919
- transform: rotate(-90deg) !important;
920
- position: relative !important;
921
- z-index: 1 !important;
922
- }
923
- [class*="context-window"] svg circle, [class*="contextWindow"] svg circle, [class*="ContextWindow"] svg circle,
924
- [class*="context-indicator"] svg circle, [class*="contextIndicator"] svg circle {
925
- fill: none !important;
926
- stroke-width: 2 !important;
927
- stroke-linecap: round !important;
928
- stroke: #4ade80 !important;
929
- }
930
- /* Ensure circles are visible */
931
- [class*="context"] svg circle,
932
- [class*="Context"] svg circle {
933
- display: block !important;
934
- visibility: visible !important;
935
- }
936
-
937
- /* ========== CHAT INPUT TOOLBAR AREA ========== */
938
- /* The toolbar/footer area containing model selector, context, autopilot, input */
939
- [class*="chat-input"], [class*="chatInput"], [class*="ChatInput"],
940
- [class*="input-area"], [class*="inputArea"], [class*="InputArea"],
941
- [class*="message-input"], [class*="messageInput"], [class*="MessageInput"],
942
- [class*="composer"], [class*="Composer"] {
943
- display: flex !important;
944
- visibility: visible !important;
945
- flex-direction: column !important;
946
- gap: 8px !important;
947
- padding: 8px 12px !important;
948
- background: var(--vscode-editor-background, #1e1e1e) !important;
949
- border-top: 1px solid var(--color-border-primary, #4a464f) !important;
950
- }
951
-
952
- /* Input toolbar row (contains # button, model selector, context, autopilot) */
953
- [class*="input-toolbar"], [class*="inputToolbar"], [class*="InputToolbar"],
954
- [class*="chat-toolbar"], [class*="chatToolbar"], [class*="ChatToolbar"],
955
- [class*="composer-toolbar"], [class*="composerToolbar"] {
956
- display: flex !important;
957
- visibility: visible !important;
958
- align-items: center !important;
959
- gap: 8px !important;
960
- flex-wrap: wrap !important;
961
- }
962
-
963
- /* Context button (# symbol) - NO border, match Kiro style */
964
- [class*="context-button"], [class*="contextButton"], [class*="ContextButton"],
965
- button[aria-label*="context"], button[aria-label*="Context"],
966
- [class*="hash-button"], [class*="hashButton"] {
967
- display: inline-flex !important;
968
- visibility: visible !important;
969
- align-items: center !important;
970
- justify-content: center !important;
971
- width: 28px !important;
972
- height: 28px !important;
973
- padding: 4px !important;
974
- background: transparent !important;
975
- border: none !important;
976
- border-radius: 4px !important;
977
- color: var(--vscode-foreground, #c1bec6) !important;
978
- font-size: 16px !important;
979
- font-weight: 600 !important;
980
- cursor: pointer !important;
981
- }
982
-
983
- [class*="context-button"]:hover, [class*="contextButton"]:hover,
984
- button[aria-label*="context"]:hover {
985
- background: rgba(255, 255, 255, 0.1) !important;
986
- }
987
-
988
- /* ========== MESSAGE TEXT FLOW - INLINE ELEMENTS ========== */
989
- /* Fix text flow so file links and code stay inline with text */
990
- [class*="message"] p,
991
- [class*="Message"] p,
992
- [class*="chat"] p,
993
- [class*="Chat"] p {
994
- display: block !important;
995
- }
996
-
997
- /* Make file links, code, and inline elements stay inline */
998
- [class*="message"] a,
999
- [class*="Message"] a,
1000
- [class*="message"] code,
1001
- [class*="Message"] code,
1002
- [class*="message"] span,
1003
- [class*="Message"] span,
1004
- [class*="chat"] a,
1005
- [class*="Chat"] a,
1006
- [class*="chat"] code,
1007
- [class*="Chat"] code,
1008
- [class*="file-link"],
1009
- [class*="FileLink"],
1010
- [class*="inline"],
1011
- code:not(pre code) {
1012
- display: inline !important;
1013
- vertical-align: baseline !important;
1014
- }
1015
-
1016
- /* Inline code styling */
1017
- code:not(pre code) {
1018
- background: rgba(255, 255, 255, 0.1) !important;
1019
- padding: 2px 6px !important;
1020
- border-radius: 3px !important;
1021
- font-family: Menlo, Monaco, "Courier New", monospace !important;
1022
- font-size: 12px !important;
1023
- }
1024
-
1025
- /* File badges/chips should be inline */
1026
- [class*="file-badge"],
1027
- [class*="FileBadge"],
1028
- [class*="file-chip"],
1029
- [class*="FileChip"],
1030
- [class*="deleted"],
1031
- [class*="Deleted"],
1032
- [class*="created"],
1033
- [class*="Created"],
1034
- [class*="modified"],
1035
- [class*="Modified"] {
1036
- display: inline-flex !important;
1037
- align-items: center !important;
1038
- gap: 4px !important;
1039
- padding: 2px 8px !important;
1040
- border-radius: 4px !important;
1041
- background: rgba(255, 255, 255, 0.08) !important;
1042
- margin: 2px 4px !important;
1043
- font-size: 12px !important;
1044
- vertical-align: middle !important;
1045
- }
1046
-
1047
- /* Bullet lists should have proper inline content */
1048
- ul li, ol li {
1049
- display: list-item !important;
1050
- }
1051
-
1052
- ul li > *, ol li > * {
1053
- display: inline !important;
1054
- }
1055
-
1056
- ul li > p, ol li > p {
1057
- display: inline !important;
1058
- margin: 0 !important;
1059
- }
1060
-
1061
- [role="switch"], [class*="toggle"] {
1062
- overflow: visible !important;
1063
- min-width: 36px !important;
1064
- flex-shrink: 0 !important;
1065
- }
1066
-
1067
- /* ========== TASK LIST STYLING - EXACT KIRO IDE MATCH ========== */
1068
-
1069
- /* Force solid background for task containers */
1070
- [class*="task"], [class*="Task"],
1071
- div:has(> h1:contains("CURRENT TASKS")),
1072
- div:has(> h2:contains("CURRENT TASKS")),
1073
- div:has(> h3:contains("CURRENT TASKS")),
1074
- div:has(> h1:contains("TASKS IN QUEUE")),
1075
- div:has(> h2:contains("TASKS IN QUEUE")),
1076
- div:has(> h3:contains("TASKS IN QUEUE")) {
1077
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
1078
- background: #1e1e1e !important;
1079
- background-color: #1e1e1e !important;
1080
- }
1081
-
1082
- /* Task section headers - BOLD WHITE UPPERCASE */
1083
- h1:contains("CURRENT TASKS"),
1084
- h2:contains("CURRENT TASKS"),
1085
- h3:contains("CURRENT TASKS"),
1086
- h1:contains("TASKS IN QUEUE"),
1087
- h2:contains("TASKS IN QUEUE"),
1088
- h3:contains("TASKS IN QUEUE"),
1089
- [class*="task"] h1, [class*="Task"] h1,
1090
- [class*="task"] h2, [class*="Task"] h2,
1091
- [class*="task"] h3, [class*="Task"] h3,
1092
- [class*="task-header"], [class*="TaskHeader"],
1093
- [class*="section-title"], [class*="SectionTitle"],
1094
- div[class*="current"] h1, div[class*="Current"] h1,
1095
- div[class*="current"] h2, div[class*="Current"] h2,
1096
- div[class*="current"] h3, div[class*="Current"] h3,
1097
- div[class*="queue"] h1, div[class*="Queue"] h1,
1098
- div[class*="queue"] h2, div[class*="Queue"] h2,
1099
- div[class*="queue"] h3, div[class*="Queue"] h3 {
1100
- font-size: 13px !important;
1101
- font-weight: 700 !important;
1102
- letter-spacing: 0.5px !important;
1103
- text-transform: uppercase !important;
1104
- color: #ffffff !important;
1105
- margin: 0 0 12px 0 !important;
1106
- padding: 0 !important;
1107
- line-height: 1.4 !important;
1108
- background: transparent !important;
1109
- }
1110
-
1111
- /* Empty state messages - ITALIC GRAY */
1112
- p:contains("No active tasks"),
1113
- p:contains("No tasks in queue"),
1114
- [class*="task"] p:not([class*="task-item"]),
1115
- [class*="Task"] p:not([class*="TaskItem"]),
1116
- [class*="no-task"], [class*="NoTask"],
1117
- [class*="empty-task"], [class*="EmptyTask"],
1118
- [class*="task-empty"], [class*="TaskEmpty"] {
1119
- font-size: 13px !important;
1120
- font-style: italic !important;
1121
- color: #888888 !important;
1122
- margin: 0 0 16px 0 !important;
1123
- padding: 0 !important;
1124
- line-height: 1.6 !important;
1125
- background: transparent !important;
1126
- }
1127
-
1128
- /* Task list containers */
1129
- [class*="task-list"], [class*="TaskList"],
1130
- [class*="task-section"], [class*="TaskSection"] {
1131
- margin-bottom: 24px !important;
1132
- padding: 0 !important;
1133
- background: #1e1e1e !important;
1134
- }
1135
-
1136
- /* Horizontal divider between sections */
1137
- [class*="task-divider"], [class*="TaskDivider"],
1138
- [class*="task"] hr, [class*="Task"] hr {
1139
- border: none !important;
1140
- border-top: 1px solid #3c3c3c !important;
1141
- margin: 20px 0 !important;
1142
- background: transparent !important;
1143
- }
1144
-
1145
- /* Individual task items (when they exist) */
1146
- [class*="task-item"], [class*="TaskItem"] {
1147
- padding: 8px 12px !important;
1148
- margin: 4px 0 !important;
1149
- background: rgba(255, 255, 255, 0.03) !important;
1150
- border-radius: 4px !important;
1151
- border-left: 2px solid #7138cc !important;
1152
- font-size: 13px !important;
1153
- color: #cccccc !important;
1154
- line-height: 1.5 !important;
1155
- }
1156
-
1157
- [class*="task-item"]:hover, [class*="TaskItem"]:hover {
1158
- background: rgba(255, 255, 255, 0.05) !important;
1159
- }
1160
-
1161
- /* Task item with close button */
1162
- [class*="task-item"] button, [class*="TaskItem"] button {
1163
- margin-left: 8px !important;
1164
- padding: 2px 6px !important;
1165
- font-size: 11px !important;
1166
- background: rgba(255, 255, 255, 0.1) !important;
1167
- border: 1px solid #4a464f !important;
1168
- border-radius: 3px !important;
1169
- color: #888 !important;
1170
- cursor: pointer !important;
1171
- }
1172
-
1173
- [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
1174
- background: rgba(255, 255, 255, 0.15) !important;
1175
- color: #fff !important;
1176
- }
1177
-
1178
- .kiro-toggle-switch {
1179
- display: flex !important;
1180
- align-items: center !important;
1181
- gap: 4px !important;
1182
- padding: 4px !important;
1183
- border-radius: 6px !important;
1184
- border: none !important;
1185
- }
1186
- .kiro-toggle-switch > input[type="checkbox"] {
1187
- appearance: none !important;
1188
- width: 36px !important;
1189
- height: 18px !important;
1190
- border-radius: 9px !important;
1191
- background-color: var(--vscode-toggleSwitch-offBackground) !important;
1192
- border: 1px solid var(--vscode-input-border) !important;
1193
- cursor: pointer !important;
1194
- position: relative !important;
1195
- }
1196
- .kiro-toggle-switch > input[type="checkbox"]::after {
1197
- content: "" !important;
1198
- position: absolute !important;
1199
- top: 50% !important;
1200
- left: 2px !important;
1201
- transform: translateY(-50%) !important;
1202
- width: 14px !important;
1203
- height: 14px !important;
1204
- border-radius: 50% !important;
1205
- background-color: #fff !important;
1206
- transition: left 0.2s !important;
1207
- }
1208
- .kiro-toggle-switch > input[type="checkbox"]:checked {
1209
- background-color: var(--vscode-toggleSwitch-onBackground) !important;
1210
- }
1211
- .kiro-toggle-switch > input[type="checkbox"]:checked::after {
1212
- left: 18px !important;
1213
- }
1214
- </style>`;
1215
- }
1216
-
1217
- // Make chat elements interactive
1218
- function makeInteractive() {
1219
- const content = panels.chat.content;
1220
-
1221
- // Input fields
1222
- const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror'];
1223
- for (const selector of inputSelectors) {
1224
- content.querySelectorAll(selector).forEach(el => {
1225
- el.style.cursor = 'text';
1226
- el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1227
- el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1228
- el.addEventListener('keydown', async (e) => {
1229
- if (e.key === 'Enter' && !e.shiftKey) {
1230
- e.preventDefault();
1231
- const text = el.textContent || el.value || '';
1232
- if (text.trim()) {
1233
- await sendToKiro(text.trim());
1234
- el.textContent = '';
1235
- el.innerHTML = '';
1236
- isTypingInKiroInput = false;
1237
- }
1238
- }
1239
- });
1240
- });
1241
- }
1242
-
1243
- // Toggle switches
1244
- content.querySelectorAll('.kiro-toggle-switch, [role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1245
- const input = toggle.tagName === 'INPUT' ? toggle : toggle.querySelector('input[type="checkbox"]');
1246
- const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1247
-
1248
- wrapper.style.cursor = 'pointer';
1249
- wrapper.onclick = async (e) => {
1250
- e.preventDefault();
1251
- e.stopPropagation();
1252
-
1253
- const label = wrapper.querySelector('label');
1254
- const clickInfo = {
1255
- tag: 'div',
1256
- text: label ? label.textContent.trim() : 'toggle',
1257
- role: 'switch',
1258
- isToggle: true,
1259
- toggleId: input ? input.id : '',
1260
- checked: input ? input.checked : false
1261
- };
1262
- await sendClickToKiro(clickInfo);
1263
- return false;
1264
- };
1265
- });
1266
-
1267
- // Tabs
1268
- content.querySelectorAll('[role="tab"]').forEach(tab => {
1269
- const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
1270
-
1271
- if (closeBtn) {
1272
- closeBtn.style.cursor = 'pointer';
1273
- closeBtn.onclick = async (e) => {
1274
- e.preventDefault();
1275
- e.stopPropagation();
1276
- await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true });
1277
- return false;
1278
- };
1279
- }
1280
-
1281
- tab.style.cursor = 'pointer';
1282
- tab.onclick = async (e) => {
1283
- if (e.target.closest('[aria-label="close"], [class*="close"], button')) return;
1284
- e.preventDefault();
1285
- e.stopPropagation();
1286
-
1287
- const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1288
- const labelText = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1289
-
1290
- await sendClickToKiro({ tag: 'div', text: labelText, role: 'tab', isTab: true, tabLabel: labelText });
1291
- return false;
1292
- };
1293
- });
1294
-
1295
- // File links - detect file paths in chat and make them clickable to open in Editor
1296
- const fileExtensions = /\.(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|astro|cob|cbl)$/i;
1297
- const filePathPattern = /^[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+$/;
1298
-
1299
- // Look for elements that might contain file paths
1300
- content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
1301
- if (el.onclick) return;
1302
- const text = (el.textContent || '').trim();
1303
- const dataPath = el.getAttribute('data-path') || '';
1304
- const href = el.getAttribute('href') || '';
1305
-
1306
- // Check if this looks like a file path
1307
- const isFilePath = (
1308
- fileExtensions.test(text) ||
1309
- fileExtensions.test(dataPath) ||
1310
- fileExtensions.test(href) ||
1311
- (filePathPattern.test(text) && text.includes('/'))
1312
- );
1313
-
1314
- if (isFilePath) {
1315
- const filePath = dataPath || text || href;
1316
- el.style.cursor = 'pointer';
1317
- el.style.textDecoration = 'underline';
1318
- el.style.color = '#7eb6ff';
1319
- el.title = `Open ${filePath} in Editor`;
1320
-
1321
- el.onclick = async (e) => {
1322
- e.preventDefault();
1323
- e.stopPropagation();
1324
- await openFileInEditor(filePath);
1325
- return false;
1326
- };
1327
- }
1328
- });
1329
-
1330
- // Buttons and clickable elements
1331
- const clickableSelectors = ['button', '[role="button"]', '[role="menuitem"]', '[role="option"]', '[role="checkbox"]', 'a[href]', '[tabindex="0"]'];
1332
- const allClickables = new Set();
1333
- clickableSelectors.forEach(sel => {
1334
- try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {}
1335
- });
1336
-
1337
- // Also add dropdown menu items
1338
- content.querySelectorAll('[class*="dropdown-item"], [class*="dropdownItem"], [class*="model-option"], [class*="modelOption"]').forEach(el => {
1339
- allClickables.add(el);
1340
- });
1341
-
1342
- allClickables.forEach(el => {
1343
- if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1344
- if (el.closest('[role="tab"]')) return;
1345
- if (el.closest('.kiro-toggle-switch')) return;
1346
- if (el.onclick) return;
1347
-
1348
- el.style.cursor = 'pointer';
1349
- el.onclick = async (e) => {
1350
- e.preventDefault();
1351
- e.stopPropagation();
1352
- const clickInfo = getElementClickInfo(el);
1353
-
1354
- // Check if this is a model selection item
1355
- if (el.matches('[role="option"], [role="menuitem"], [class*="model"], [class*="Model"]') ||
1356
- el.closest('[role="listbox"], [role="menu"], [class*="dropdown-menu"], [class*="dropdownMenu"]')) {
1357
- clickInfo.isModelSelection = true;
1358
- clickInfo.modelName = el.textContent?.trim().split('\n')[0] || '';
1359
- }
1360
-
1361
- await sendClickToKiro(clickInfo);
1362
- return false;
1363
- };
1364
- });
1365
- }
1366
-
1367
- function getElementClickInfo(el) {
1368
- const info = {
1369
- tag: el.tagName?.toLowerCase() || '',
1370
- text: (el.textContent || '').trim().substring(0, 100),
1371
- ariaLabel: el.getAttribute('aria-label') || '',
1372
- title: el.getAttribute('title') || '',
1373
- role: el.getAttribute('role') || '',
1374
- className: el.className || '',
1375
- isTab: el.getAttribute('role') === 'tab',
1376
- dataTestId: el.getAttribute('data-testid') || ''
1377
- };
1378
-
1379
- if (!info.ariaLabel) {
1380
- const parent = el.closest('[aria-label]');
1381
- if (parent) info.ariaLabel = parent.getAttribute('aria-label') || '';
1382
- }
1383
-
1384
- return info;
1385
- }
1386
-
1387
- async function sendToKiro(message) {
1388
- if (!message || !selectedCascadeId) return;
1389
- try {
1390
- const r = await fetch(`/send/${selectedCascadeId}`, {
1391
- method: 'POST',
1392
- headers: { 'Content-Type': 'application/json' },
1393
- body: JSON.stringify({ message })
1394
- });
1395
- const result = await r.json();
1396
- if (result.success) showToast('Sent!', 1500, true);
1397
- else showToast(result.error || 'Failed to send');
1398
- } catch (e) {
1399
- showToast('Failed to send');
1400
- }
1401
- }
1402
-
1403
- async function sendClickToKiro(clickInfo) {
1404
- if (!selectedCascadeId) return;
1405
-
1406
- const isNavigation =
1407
- clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1408
- clickInfo.ariaLabel?.toLowerCase().includes('close') ||
1409
- clickInfo.text?.toLowerCase().includes('back') ||
1410
- clickInfo.role === 'tab';
1411
-
1412
- const isModelSelection = clickInfo.isModelSelection ||
1413
- clickInfo.role === 'option' ||
1414
- clickInfo.role === 'menuitem' ||
1415
- clickInfo.className?.includes('model');
1416
-
1417
- if (isNavigation) navigationPending = true;
1418
-
1419
- try {
1420
- await fetch(`/click/${selectedCascadeId}`, {
1421
- method: 'POST',
1422
- headers: { 'Content-Type': 'application/json' },
1423
- body: JSON.stringify(clickInfo)
1424
- });
1425
-
1426
- if (isNavigation || isModelSelection) {
1427
- // Refresh snapshot after navigation or model selection
1428
- setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1429
- }
1430
- } catch (e) {
1431
- navigationPending = false;
1432
- }
1433
- }
1434
-
1435
- // Open file in Editor tab
1436
- async function openFileInEditor(filePath) {
1437
- if (!selectedCascadeId || !filePath) return;
1438
-
1439
- showToast(`Opening ${filePath}...`, 1500);
1440
-
1441
- // Switch to Editor tab immediately
1442
- const editorTab = document.querySelector('[data-panel="editor"]');
1443
- if (editorTab) {
1444
- navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1445
- editorTab.classList.add('active');
1446
-
1447
- Object.values(panels).forEach(p => {
1448
- if (p.panel) p.panel.classList.remove('active');
1449
- });
1450
- panels.editor.panel.classList.add('active');
1451
- activePanel = 'editor';
1452
-
1453
- // Show loading state
1454
- showLoading('editor', 'Loading file...');
1455
- }
1456
-
1457
- try {
1458
- // First, try to read the file directly from filesystem (bypasses Monaco limitation)
1459
- const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
1460
- method: 'POST',
1461
- headers: { 'Content-Type': 'application/json' },
1462
- body: JSON.stringify({ filePath })
1463
- });
1464
-
1465
- if (readResult.ok) {
1466
- const data = await readResult.json();
1467
- if (data.content) {
1468
- renderEditorSnapshot(data);
1469
- showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1470
- console.log('[Editor] Loaded file directly:', data.fileName, data.lineCount, 'lines');
1471
- return;
1472
- }
1473
- } else {
1474
- const errorData = await readResult.json().catch(() => ({}));
1475
- console.log('[Editor] Direct file read failed:', errorData.error || readResult.status);
1476
- }
1477
-
1478
- // Fallback: Try to click on the file link in Kiro to open it in Monaco
1479
- console.log('[Editor] Falling back to Monaco capture...');
1480
- const clickResult = await fetch(`/click/${selectedCascadeId}`, {
1481
- method: 'POST',
1482
- headers: { 'Content-Type': 'application/json' },
1483
- body: JSON.stringify({
1484
- tag: 'a',
1485
- text: filePath,
1486
- isFileLink: true,
1487
- filePath: filePath
1488
- })
1489
- });
1490
-
1491
- const result = await clickResult.json();
1492
- console.log('Click result:', result);
1493
-
1494
- // Wait for the file to open in Kiro
1495
- await new Promise(resolve => setTimeout(resolve, 800));
1496
-
1497
- // Fetch the editor content from Monaco
1498
- const fetchEditor = async () => {
1499
- try {
1500
- const r = await fetch(`/editor/${selectedCascadeId}`);
1501
- if (r.ok) {
1502
- const data = await r.json();
1503
- if (data.content || data.html) {
1504
- renderEditorSnapshot(data);
1505
- showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1506
- return true;
1507
- }
1508
- }
1509
- } catch (e) {}
1510
- return false;
1511
- };
1512
-
1513
- // Try fetching a few times with delays
1514
- if (!await fetchEditor()) {
1515
- await new Promise(resolve => setTimeout(resolve, 500));
1516
- if (!await fetchEditor()) {
1517
- await new Promise(resolve => setTimeout(resolve, 500));
1518
- if (!await fetchEditor()) {
1519
- showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}. File may not exist or path is incorrect.`);
1520
- }
1521
- }
1522
- }
1523
-
1524
- } catch (e) {
1525
- showToast('Failed to open file');
1526
- console.error('Open file error:', e);
1527
- showEmptyState('editor', 'codicon-file-code', 'Failed to load file. Check console for details.');
1528
- }
1529
- }
1530
-
1531
- // ==================== File Tree ====================
1532
-
1533
- // Toggle file tree panel
1534
- function toggleFileTree() {
1535
- fileTreeOpen = !fileTreeOpen;
1536
- fileTreePanel.classList.toggle('open', fileTreeOpen);
1537
-
1538
- if (fileTreeOpen) {
1539
- // Load files if not cached
1540
- if (workspaceFiles.length === 0) {
1541
- fetchWorkspaceFiles();
1542
- } else {
1543
- renderFileTree(workspaceFiles);
1544
- }
1545
- }
1546
- }
1547
-
1548
- // Close file tree
1549
- function closeFileTree() {
1550
- fileTreeOpen = false;
1551
- fileTreePanel.classList.remove('open');
1552
- }
1553
-
1554
- // Fetch workspace files from server
1555
- async function fetchWorkspaceFiles() {
1556
- if (!selectedCascadeId) return;
1557
-
1558
- fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1559
-
1560
- try {
1561
- const r = await fetch(`/files/${selectedCascadeId}`);
1562
- if (!r.ok) throw new Error('Failed to fetch files');
1563
-
1564
- const data = await r.json();
1565
- workspaceFiles = data.files || [];
1566
- console.log(`[FileTree] Loaded ${workspaceFiles.length} files`);
1567
- renderFileTree(workspaceFiles);
1568
- } catch (e) {
1569
- console.error('Failed to fetch files:', e);
1570
- fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1571
- }
1572
- }
1573
-
1574
- // Build tree structure from flat file list
1575
- function buildFileTree(files) {
1576
- const root = { folders: {}, files: [] };
1577
-
1578
- files.forEach(file => {
1579
- const parts = file.path.split(/[/\\]/);
1580
- let current = root;
1581
-
1582
- // Navigate/create folder structure
1583
- for (let i = 0; i < parts.length - 1; i++) {
1584
- const folderName = parts[i];
1585
- if (!current.folders[folderName]) {
1586
- current.folders[folderName] = { folders: {}, files: [] };
1587
- }
1588
- current = current.folders[folderName];
1589
- }
1590
-
1591
- // Add file to current folder
1592
- current.files.push({ name: parts[parts.length - 1], path: file.path });
1593
- });
1594
-
1595
- return root;
1596
- }
1597
-
1598
- // Get file icon based on extension
1599
- function getFileIcon(name) {
1600
- const ext = name.split('.').pop()?.toLowerCase();
1601
- const iconMap = {
1602
- 'js': 'codicon-file-code', 'jsx': 'codicon-file-code',
1603
- 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1604
- 'html': 'codicon-file-code', 'css': 'codicon-file-code',
1605
- 'json': 'codicon-json', 'md': 'codicon-markdown',
1606
- 'py': 'codicon-file-code', 'java': 'codicon-file-code',
1607
- 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
1608
- 'cob': 'codicon-file-code', 'cbl': 'codicon-file-code',
1609
- 'sql': 'codicon-database', 'txt': 'codicon-file'
1610
- };
1611
- return iconMap[ext] || 'codicon-file';
1612
- }
1613
-
1614
- // Render tree node recursively
1615
- function renderTreeNode(node, path = '', depth = 0) {
1616
- let html = '';
1617
-
1618
- // Sort folders first, then files
1619
- const folderNames = Object.keys(node.folders).sort();
1620
- const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
1621
-
1622
- // Render folders
1623
- folderNames.forEach(folderName => {
1624
- const folderPath = path ? `${path}/${folderName}` : folderName;
1625
- const isExpanded = expandedFolders.has(folderPath);
1626
-
1627
- html += `
1628
- <div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
1629
- <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
1630
- <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
1631
- <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #dcb67a;"></span>
1632
- <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
1633
- </div>
1634
- <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">
1635
- ${renderTreeNode(node.folders[folderName], folderPath, depth + 1)}
1636
- </div>
1637
- </div>
1638
- `;
1639
- });
1640
-
1641
- // Render files
1642
- sortedFiles.forEach(file => {
1643
- html += `
1644
- <div class="file-tree-file" data-path="${escapeHtml(file.path)}">
1645
- <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
1646
- <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
1647
- </div>
1648
- `;
1649
- });
1650
-
1651
- return html;
1652
- }
1653
-
1654
- // Render file tree
1655
- function renderFileTree(files) {
1656
- if (files.length === 0) {
1657
- fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>';
1658
- return;
1659
- }
1660
-
1661
- // Build and render tree structure
1662
- const tree = buildFileTree(files);
1663
- fileTree.innerHTML = renderTreeNode(tree);
1664
-
1665
- // Add click handlers for folders
1666
- fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
1667
- header.onclick = (e) => {
1668
- e.stopPropagation();
1669
- const folderPath = header.dataset.folder;
1670
- const folder = header.closest('.file-tree-folder');
1671
- const contents = folder.querySelector('.file-tree-folder-contents');
1672
- const chevron = header.querySelector('.file-tree-folder-icon');
1673
- const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
1674
-
1675
- if (expandedFolders.has(folderPath)) {
1676
- expandedFolders.delete(folderPath);
1677
- contents.classList.remove('expanded');
1678
- chevron.classList.remove('expanded');
1679
- folderIcon.classList.remove('codicon-folder-opened');
1680
- folderIcon.classList.add('codicon-folder');
1681
- } else {
1682
- expandedFolders.add(folderPath);
1683
- contents.classList.add('expanded');
1684
- chevron.classList.add('expanded');
1685
- folderIcon.classList.remove('codicon-folder');
1686
- folderIcon.classList.add('codicon-folder-opened');
1687
- }
1688
- };
1689
- });
1690
-
1691
- // Add click handlers for files
1692
- fileTree.querySelectorAll('.file-tree-file').forEach(item => {
1693
- item.onclick = () => {
1694
- const path = item.dataset.path;
1695
- closeFileTree();
1696
- openFileInEditor(path);
1697
- };
1698
- });
1699
- }
1700
-
1701
- // Setup file tree event listeners
1702
- function setupFileTree() {
1703
- const explorerBtn = document.getElementById('editorExplorerBtn');
1704
-
1705
- // Explorer button click toggles file tree
1706
- explorerBtn.onclick = (e) => {
1707
- e.stopPropagation();
1708
- toggleFileTree();
1709
- };
1710
-
1711
- // Close button
1712
- fileTreeClose.onclick = (e) => {
1713
- e.stopPropagation();
1714
- closeFileTree();
1715
- };
1716
-
1717
- // Prevent clicks inside panel from closing it
1718
- fileTreePanel.onclick = (e) => {
1719
- e.stopPropagation();
1720
- };
1721
-
1722
- // Close on escape key
1723
- document.addEventListener('keydown', (e) => {
1724
- if (e.key === 'Escape' && fileTreeOpen) {
1725
- closeFileTree();
1726
- }
1727
- });
1728
- }
1729
-
1730
- // Initialize file tree
1731
- setupFileTree();
1732
-
1733
- // ==================== Editor Search ====================
1734
-
1735
- function openEditorSearch() {
1736
- editorSearchBar.classList.add('open');
1737
- editorSearchInput.focus();
1738
- editorSearchInput.select();
1739
- }
1740
-
1741
- function closeEditorSearch() {
1742
- editorSearchBar.classList.remove('open');
1743
- editorSearchInput.value = '';
1744
- searchMatches = [];
1745
- currentMatchIndex = -1;
1746
- editorSearchCount.textContent = '';
1747
- clearSearchHighlights();
1748
- }
1749
-
1750
- function clearSearchHighlights() {
1751
- // Restore original HTML for lines that were modified
1752
- const modifiedLines = panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]');
1753
- modifiedLines.forEach(el => {
1754
- el.innerHTML = el.dataset.originalHtml;
1755
- delete el.dataset.originalHtml;
1756
- });
1757
- }
1758
-
1759
- function performSearch(query) {
1760
- clearSearchHighlights();
1761
- searchMatches = [];
1762
- currentMatchIndex = -1;
1763
-
1764
- if (!query || query.length < 1) {
1765
- editorSearchCount.textContent = '';
1766
- return;
1767
- }
1768
-
1769
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
1770
- const queryLower = query.toLowerCase();
1771
-
1772
- codeElements.forEach((lineEl, lineIndex) => {
1773
- const text = lineEl.textContent;
1774
- const textLower = text.toLowerCase();
1775
- let startIndex = 0;
1776
- let matchIndex;
1777
-
1778
- while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
1779
- searchMatches.push({
1780
- lineIndex,
1781
- lineEl,
1782
- startIndex: matchIndex,
1783
- endIndex: matchIndex + query.length
1784
- });
1785
- startIndex = matchIndex + 1;
1786
- }
1787
- });
1788
-
1789
- if (searchMatches.length > 0) {
1790
- highlightMatches(query);
1791
- currentMatchIndex = 0;
1792
- scrollToMatch(0);
1793
- updateSearchCount();
1794
- } else {
1795
- editorSearchCount.textContent = 'No results';
1796
- }
1797
- }
1798
-
1799
- function highlightMatches(query) {
1800
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
1801
- const queryLower = query.toLowerCase();
1802
-
1803
- codeElements.forEach(lineEl => {
1804
- const text = lineEl.textContent;
1805
- const textLower = text.toLowerCase();
1806
-
1807
- if (textLower.includes(queryLower)) {
1808
- // Store original HTML to restore later
1809
- if (!lineEl.dataset.originalHtml) {
1810
- lineEl.dataset.originalHtml = lineEl.innerHTML;
1811
- }
1812
-
1813
- // Work with text content, find all match positions
1814
- const matches = [];
1815
- let idx = 0;
1816
- while ((idx = textLower.indexOf(queryLower, idx)) !== -1) {
1817
- matches.push({ start: idx, end: idx + query.length });
1818
- idx++;
1819
- }
1820
-
1821
- if (matches.length > 0) {
1822
- // Rebuild the line with highlights
1823
- let result = '';
1824
- let lastEnd = 0;
1825
-
1826
- matches.forEach(match => {
1827
- // Add text before match (escaped)
1828
- result += escapeHtml(text.slice(lastEnd, match.start));
1829
- // Add highlighted match
1830
- result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
1831
- lastEnd = match.end;
1832
- });
1833
-
1834
- // Add remaining text
1835
- result += escapeHtml(text.slice(lastEnd));
1836
-
1837
- lineEl.innerHTML = result;
1838
- }
1839
- }
1840
- });
1841
- }
1842
-
1843
- function scrollToMatch(index) {
1844
- const highlights = panels.editor.content.querySelectorAll('.search-highlight');
1845
-
1846
- // Remove current class from all
1847
- highlights.forEach(el => el.classList.remove('current'));
1848
-
1849
- if (highlights[index]) {
1850
- highlights[index].classList.add('current');
1851
-
1852
- // Get the scroll container and the highlight element
1853
- const container = panels.editor.content;
1854
- const element = highlights[index];
1855
-
1856
- // Get the line element (parent of the highlight)
1857
- const lineEl = element.closest('.editor-line');
1858
- if (lineEl) {
1859
- // Calculate scroll position to center the line
1860
- const containerRect = container.getBoundingClientRect();
1861
- const lineRect = lineEl.getBoundingClientRect();
1862
- const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
1863
-
1864
- container.scrollTo({
1865
- top: Math.max(0, scrollTop),
1866
- behavior: 'smooth'
1867
- });
1868
- }
1869
- }
1870
- }
1871
-
1872
- function updateSearchCount() {
1873
- if (searchMatches.length > 0) {
1874
- editorSearchCount.textContent = `${currentMatchIndex + 1}/${searchMatches.length}`;
1875
- } else {
1876
- editorSearchCount.textContent = 'No results';
1877
- }
1878
- }
1879
-
1880
- function goToNextMatch() {
1881
- if (searchMatches.length === 0) return;
1882
- currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
1883
- scrollToMatch(currentMatchIndex);
1884
- updateSearchCount();
1885
- }
1886
-
1887
- function goToPrevMatch() {
1888
- if (searchMatches.length === 0) return;
1889
- currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
1890
- scrollToMatch(currentMatchIndex);
1891
- updateSearchCount();
1892
- }
1893
-
1894
- // Setup editor search
1895
- function setupEditorSearch() {
1896
- editorSearchBtn.onclick = () => openEditorSearch();
1897
- editorSearchClose.onclick = () => closeEditorSearch();
1898
- editorSearchNext.onclick = () => goToNextMatch();
1899
- editorSearchPrev.onclick = () => goToPrevMatch();
1900
-
1901
- editorSearchInput.oninput = (e) => {
1902
- performSearch(e.target.value);
1903
- };
1904
-
1905
- editorSearchInput.onkeydown = (e) => {
1906
- if (e.key === 'Enter') {
1907
- e.preventDefault();
1908
- if (e.shiftKey) {
1909
- goToPrevMatch();
1910
- } else {
1911
- goToNextMatch();
1912
- }
1913
- } else if (e.key === 'Escape') {
1914
- closeEditorSearch();
1915
- }
1916
- };
1917
-
1918
- // Ctrl+F / Cmd+F to open search
1919
- document.addEventListener('keydown', (e) => {
1920
- if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') {
1921
- e.preventDefault();
1922
- openEditorSearch();
1923
- }
1924
- });
1925
- }
1926
-
1927
- setupEditorSearch();
1928
-
1929
- // Visibility change handler
1930
- document.addEventListener('visibilitychange', () => {
1931
- if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
1932
- connect();
1933
- }
1934
- });
1935
-
1936
- // Initialize
1937
- connect();
1938
- </script>
1939
- </body>
1940
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <meta name="theme-color" content="#1e1e1e">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
+ <title>Kiro Mobile Bridge</title>
10
+ <link rel="stylesheet" href="https://unpkg.com/@vscode/codicons@0.0.35/dist/codicon.css">
11
+ <style>
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+ html, body { height: 100%; overflow: hidden; background: #1e1e1e; font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif; font-size: 13px; color: #fff; }
14
+ .app { display: flex; flex-direction: column; height: 100%; height: 100dvh; }
15
+
16
+ /* Navigation Bar (combined header + tabs) */
17
+ .nav-bar { display: flex; align-items: center; background: #1e1e1e; border-bottom: 1px solid #3c3c3c; padding: 0 8px; flex-shrink: 0; }
18
+ .status { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 10px 8px; }
19
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #666; }
20
+ .status-dot.connected { background: #4caf50; }
21
+ .status-dot.disconnected { background: #f44336; }
22
+ .status-text { color: #888; display: none; }
23
+ .nav-tabs { display: flex; align-items: center; flex: 1; overflow-x: auto; -webkit-overflow-scrolling: touch; }
24
+ .nav-tabs::-webkit-scrollbar { display: none; }
25
+ .nav-tab { display: flex; align-items: center; gap: 6px; padding: 10px 16px; color: #888; font-size: 12px; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; white-space: nowrap; user-select: none; }
26
+ .header-title { font-size: 13px; font-weight: 600; color: #888; padding: 10px 12px; white-space: nowrap; }
27
+ .nav-tab:hover { color: #ccc; background: rgba(255,255,255,0.05); }
28
+ .nav-tab.active { color: #fff; border-bottom-color: #7138cc; }
29
+ .nav-tab .codicon { font-size: 14px; }
30
+
31
+ /* Panel Container */
32
+ .panel-container { flex: 1; overflow: hidden; position: relative; }
33
+ .panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; display: none; }
34
+ .panel.active { display: flex; flex-direction: column; }
35
+
36
+ /* Chat Panel */
37
+ .chat-container { flex: 1; overflow: hidden; position: relative; }
38
+ .chat-content { width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }
39
+ #chatContent, #chatContent * { font-family: inherit; }
40
+ #chatContent code, #chatContent pre { font-family: Menlo, Monaco, "Courier New", monospace !important; }
41
+ #chatContent .codicon { font-family: codicon !important; font-size: 16px; line-height: 1; }
42
+
43
+ /* Editor Panel */
44
+ .editor-container { flex: 1; overflow: hidden; background: #1e1e1e; display: flex; flex-direction: column; position: relative; }
45
+ .editor-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; flex-shrink: 0; user-select: none; }
46
+ .editor-filename { font-size: 13px; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
47
+ .editor-search-btn { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
48
+ .editor-search-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
49
+ .editor-explorer-btn { display: flex; align-items: center; gap: 6px; color: #888; font-size: 14px; cursor: pointer; padding: 4px 8px; border-radius: 4px; background: #3c3c3c; }
50
+ .editor-explorer-btn:hover { background: #4a4a4a; }
51
+ .editor-explorer-text { font-size: 11px; font-weight: 600; color: #888; letter-spacing: 0.5px; }
52
+ .editor-search-bar { display: none; align-items: center; gap: 8px; padding: 6px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; }
53
+ .editor-search-bar.open { display: flex; }
54
+ .editor-search-bar input { flex: 1; padding: 6px 10px; background: #1e1e1e; border: 1px solid #3c3c3c; border-radius: 4px; color: #fff; font-size: 13px; outline: none; }
55
+ .editor-search-bar input:focus { border-color: #007acc; }
56
+ .editor-search-count { font-size: 12px; color: #888; min-width: 50px; text-align: center; }
57
+ .editor-search-nav { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
58
+ .editor-search-nav:hover { color: #fff; background: rgba(255,255,255,0.1); }
59
+ .editor-search-close { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
60
+ .editor-search-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
61
+ .editor-content { flex: 1; overflow: auto; -webkit-overflow-scrolling: touch; background: #1e1e1e; }
62
+ .editor-code { margin: 0; padding: 8px 0; font-family: 'Cascadia Code', 'Fira Code', Menlo, Monaco, "Courier New", monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre; overflow-x: auto; background: #1e1e1e; }
63
+ .editor-line { display: flex; min-height: 1.6em; }
64
+ .editor-line:hover { background: rgba(255,255,255,0.04); }
65
+ .editor-line-num { width: 45px; min-width: 45px; text-align: right; padding-right: 12px; color: #6e7681; user-select: none; flex-shrink: 0; font-size: 12px; }
66
+ .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; tab-size: 2; }
67
+ .search-highlight { background: #ffd500; color: #000; border-radius: 2px; padding: 0 1px; }
68
+ .search-highlight.current { background: #ff6b00; color: #fff; outline: 2px solid #ff6b00; }
69
+
70
+ /* File Tree Panel */
71
+ .file-tree-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #252526; display: flex; flex-direction: column; z-index: 100; display: none; }
72
+ .file-tree-panel.open { display: flex; }
73
+ .file-tree-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; }
74
+ .file-tree-header-title { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
75
+ .file-tree-close { margin-left: auto; color: #888; cursor: pointer; font-size: 16px; }
76
+ .file-tree-close:hover { color: #fff; }
77
+ .file-tree { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 4px 0; }
78
+ .file-tree-folder-header { display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; user-select: none; }
79
+ .file-tree-folder-header:hover { background: rgba(255,255,255,0.05); }
80
+ .file-tree-folder-icon { color: #888; font-size: 14px; transition: transform 0.15s; }
81
+ .file-tree-folder-icon.expanded { transform: rotate(90deg); }
82
+ .file-tree-folder-name { font-size: 13px; color: #c5c5c5; }
83
+ .file-tree-folder-contents { padding-left: 16px; display: none; }
84
+ .file-tree-folder-contents.expanded { display: block; }
85
+ .file-tree-file { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 24px; cursor: pointer; }
86
+ .file-tree-file:hover { background: rgba(255,255,255,0.05); }
87
+ .file-tree-file:active { background: #094771; }
88
+ .file-tree-file-icon { color: #888; font-size: 14px; }
89
+ .file-tree-file-name { font-size: 13px; color: #c5c5c5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
90
+ .file-tree-empty { padding: 20px; text-align: center; color: #666; font-size: 13px; }
91
+ .file-tree-loading { padding: 20px; text-align: center; color: #888; display: flex; flex-direction: column; align-items: center; gap: 8px; }
92
+
93
+ /* Syntax Highlighting */
94
+ .tok-kw { color: #569cd6; }
95
+ .tok-str { color: #ce9178; }
96
+ .tok-num { color: #b5cea8; }
97
+ .tok-cmt { color: #6a9955; }
98
+ .tok-fn { color: #dcdcaa; }
99
+ .tok-type { color: #4ec9b0; }
100
+
101
+ /* Syntax Highlighting */
102
+ .token-keyword { color: #569cd6; font-weight: 500; }
103
+ .token-string { color: #ce9178; }
104
+ .token-number { color: #b5cea8; }
105
+ .token-comment { color: #6a9955; font-style: italic; }
106
+ .token-function { color: #dcdcaa; }
107
+ .token-type { color: #4ec9b0; }
108
+ .token-operator { color: #d4d4d4; }
109
+
110
+ /* Loading & Toast */
111
+ .loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #888; font-size: 14px; flex-direction: column; gap: 12px; }
112
+ .loading-spinner { width: 24px; height: 24px; border: 2px solid #3c3c3c; border-top-color: #7138cc; border-radius: 50%; animation: spin 1s linear infinite; }
113
+ @keyframes spin { to { transform: rotate(360deg); } }
114
+
115
+ .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #f44336; color: #fff; padding: 10px 16px; border-radius: 6px; font-size: 13px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; z-index: 1000; }
116
+ .toast.visible { opacity: 1; }
117
+ .toast.success { background: #4caf50; }
118
+
119
+ /* Empty State */
120
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #888; text-align: center; padding: 20px; }
121
+ .empty-state .codicon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
122
+ .empty-state p { font-size: 14px; max-width: 280px; }
123
+
124
+ /* Task List Styling - Match Kiro IDE */
125
+ [class*="task"], [class*="Task"] {
126
+ font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
127
+ }
128
+
129
+ /* Task section headers (CURRENT TASKS, TASKS IN QUEUE) */
130
+ [class*="task"] h2, [class*="Task"] h2,
131
+ [class*="task"] h3, [class*="Task"] h3,
132
+ [class*="task-header"], [class*="TaskHeader"],
133
+ [class*="section-title"], [class*="SectionTitle"] {
134
+ font-size: 13px !important;
135
+ font-weight: 700 !important;
136
+ letter-spacing: 0.5px !important;
137
+ text-transform: uppercase !important;
138
+ color: #ffffff !important;
139
+ margin: 0 0 12px 0 !important;
140
+ padding: 0 !important;
141
+ line-height: 1.4 !important;
142
+ }
143
+
144
+ /* Task list containers */
145
+ [class*="task-list"], [class*="TaskList"],
146
+ [class*="task-section"], [class*="TaskSection"] {
147
+ margin-bottom: 24px !important;
148
+ padding: 0 !important;
149
+ }
150
+
151
+ /* Empty state messages (No active tasks, No tasks in queue) */
152
+ [class*="task"] p:empty, [class*="Task"] p:empty,
153
+ [class*="no-task"], [class*="NoTask"],
154
+ [class*="empty-task"], [class*="EmptyTask"],
155
+ [class*="task-empty"], [class*="TaskEmpty"] {
156
+ font-size: 13px !important;
157
+ font-style: italic !important;
158
+ color: #888888 !important;
159
+ margin: 0 0 16px 0 !important;
160
+ padding: 0 !important;
161
+ line-height: 1.6 !important;
162
+ }
163
+
164
+ /* Catch all paragraphs under task sections that look like empty states */
165
+ [class*="task"] p, [class*="Task"] p {
166
+ font-size: 13px !important;
167
+ line-height: 1.6 !important;
168
+ }
169
+
170
+ /* Specific targeting for common patterns */
171
+ div[class*="current"] h2, div[class*="Current"] h2,
172
+ div[class*="queue"] h2, div[class*="Queue"] h2 {
173
+ font-size: 13px !important;
174
+ font-weight: 700 !important;
175
+ letter-spacing: 0.5px !important;
176
+ text-transform: uppercase !important;
177
+ color: #ffffff !important;
178
+ }
179
+
180
+ /* Horizontal divider between sections */
181
+ [class*="task-divider"], [class*="TaskDivider"],
182
+ [class*="task"] hr, [class*="Task"] hr {
183
+ border: none !important;
184
+ border-top: 1px solid #3c3c3c !important;
185
+ margin: 20px 0 !important;
186
+ }
187
+
188
+ /* Individual task items (when they exist) */
189
+ [class*="task-item"], [class*="TaskItem"] {
190
+ padding: 8px 12px !important;
191
+ margin: 4px 0 !important;
192
+ background: rgba(255, 255, 255, 0.03) !important;
193
+ border-radius: 4px !important;
194
+ border-left: 2px solid #7138cc !important;
195
+ font-size: 13px !important;
196
+ color: #cccccc !important;
197
+ line-height: 1.5 !important;
198
+ }
199
+
200
+ [class*="task-item"]:hover, [class*="TaskItem"]:hover {
201
+ background: rgba(255, 255, 255, 0.05) !important;
202
+ }
203
+
204
+ /* Task item with close button */
205
+ [class*="task-item"] button, [class*="TaskItem"] button {
206
+ margin-left: 8px !important;
207
+ padding: 2px 6px !important;
208
+ font-size: 11px !important;
209
+ background: rgba(255, 255, 255, 0.1) !important;
210
+ border: 1px solid #4a464f !important;
211
+ border-radius: 3px !important;
212
+ color: #888 !important;
213
+ cursor: pointer !important;
214
+ }
215
+
216
+ [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
217
+ background: rgba(255, 255, 255, 0.15) !important;
218
+ color: #fff !important;
219
+ }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ <div class="app">
224
+ <!-- Navigation Bar (tabs | title + indicator) -->
225
+ <nav class="nav-bar">
226
+ <div class="nav-tabs" id="navTabs">
227
+ <div class="nav-tab active" data-panel="chat">
228
+ <span class="codicon codicon-comment-discussion"></span>
229
+ <span>Chat</span>
230
+ </div>
231
+ <div class="nav-tab" data-panel="editor">
232
+ <span class="codicon codicon-code"></span>
233
+ <span>Editor</span>
234
+ </div>
235
+ </div>
236
+ <span class="header-title">Kiro Mobile</span>
237
+ <div class="status">
238
+ <span class="status-dot" id="statusDot"></span>
239
+ <span class="status-text" id="statusText">Connecting...</span>
240
+ </div>
241
+ </nav>
242
+
243
+ <!-- Panel Container -->
244
+ <div class="panel-container">
245
+ <!-- Chat Panel -->
246
+ <div class="panel active" id="chatPanel">
247
+ <div class="chat-container" id="chatContainer">
248
+ <div class="loading" id="chatLoading"><div class="loading-spinner"></div><span>Waiting for Kiro...</span></div>
249
+ <div class="chat-content" id="chatContent" style="display: none;"></div>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- Editor Panel -->
254
+ <div class="panel" id="editorPanel">
255
+ <div class="editor-container">
256
+ <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading editor...</span></div>
257
+ <div class="editor-header" id="editorHeader" style="display: none;">
258
+ <span class="codicon codicon-file-code" style="color: #888;"></span>
259
+ <span class="editor-filename" id="editorFilename">No file open</span>
260
+ <span class="editor-search-btn codicon codicon-search" id="editorSearchBtn" title="Find in file"></span>
261
+ <div class="editor-explorer-btn" id="editorExplorerBtn" title="Open Explorer">
262
+ <span class="codicon codicon-folder" style="color: #dcb67a;"></span>
263
+ <span class="editor-explorer-text">EXPLORER</span>
264
+ </div>
265
+ </div>
266
+ <div class="editor-search-bar" id="editorSearchBar">
267
+ <input type="text" id="editorSearchInput" placeholder="Find in file..." autocomplete="off" />
268
+ <span class="editor-search-count" id="editorSearchCount"></span>
269
+ <span class="editor-search-nav codicon codicon-arrow-up" id="editorSearchPrev" title="Previous"></span>
270
+ <span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext" title="Next"></span>
271
+ <span class="editor-search-close codicon codicon-close" id="editorSearchClose" title="Close"></span>
272
+ </div>
273
+ <div class="file-tree-panel" id="fileTreePanel">
274
+ <div class="file-tree-header">
275
+ <span class="codicon codicon-folder-opened" style="color: #dcb67a;"></span>
276
+ <span class="file-tree-header-title">Explorer</span>
277
+ <span class="file-tree-close codicon codicon-close" id="fileTreeClose"></span>
278
+ </div>
279
+ <div class="file-tree" id="fileTree">
280
+ <div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>
281
+ </div>
282
+ </div>
283
+ <div class="editor-content" id="editorContent" style="display: none;"></div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <div class="toast" id="toast"></div>
289
+ </div>
290
+
291
+ <script>
292
+ // State
293
+ let ws = null, reconnectAttempts = 0, reconnectTimeout = null;
294
+ let cascades = [], selectedCascadeId = null, currentStyles = null;
295
+ let isTypingInKiroInput = false;
296
+ let activePanel = 'chat';
297
+ let lastSuccessfulSnapshot = null;
298
+ let navigationPending = false;
299
+ let workspaceFiles = []; // Cached file list
300
+ let fileTreeOpen = false;
301
+ let expandedFolders = new Set(); // Track expanded folders
302
+ let updateDebounceTimer = null; // Debounce rapid snapshot updates
303
+ let isRendering = false; // Prevent concurrent renders
304
+
305
+ // DOM Elements
306
+ const statusDot = document.getElementById('statusDot');
307
+ const statusText = document.getElementById('statusText');
308
+ const navTabs = document.getElementById('navTabs');
309
+ const toast = document.getElementById('toast');
310
+ const fileTreePanel = document.getElementById('fileTreePanel');
311
+ const fileTree = document.getElementById('fileTree');
312
+ const fileTreeClose = document.getElementById('fileTreeClose');
313
+
314
+ // Panel elements
315
+ const panels = {
316
+ chat: {
317
+ panel: document.getElementById('chatPanel'),
318
+ loading: document.getElementById('chatLoading'),
319
+ content: document.getElementById('chatContent'),
320
+ container: document.getElementById('chatContainer')
321
+ },
322
+ editor: {
323
+ panel: document.getElementById('editorPanel'),
324
+ loading: document.getElementById('editorLoading'),
325
+ content: document.getElementById('editorContent'),
326
+ header: document.getElementById('editorHeader'),
327
+ filename: document.getElementById('editorFilename')
328
+ }
329
+ };
330
+
331
+ // Search state
332
+ let searchMatches = [];
333
+ let currentMatchIndex = -1;
334
+ let originalEditorContent = '';
335
+
336
+ // Search DOM elements
337
+ const editorSearchBtn = document.getElementById('editorSearchBtn');
338
+ const editorSearchBar = document.getElementById('editorSearchBar');
339
+ const editorSearchInput = document.getElementById('editorSearchInput');
340
+ const editorSearchCount = document.getElementById('editorSearchCount');
341
+ const editorSearchPrev = document.getElementById('editorSearchPrev');
342
+ const editorSearchNext = document.getElementById('editorSearchNext');
343
+ const editorSearchClose = document.getElementById('editorSearchClose');
344
+
345
+ // Status management
346
+ function setStatus(status) {
347
+ statusDot.className = 'status-dot ' + status;
348
+ statusText.textContent = status === 'connected' ? 'Connected' : status === 'disconnected' ? 'Disconnected' : 'Connecting...';
349
+ }
350
+
351
+ function showToast(message, duration = 3000, isSuccess = false) {
352
+ toast.textContent = message;
353
+ toast.className = 'toast visible' + (isSuccess ? ' success' : '');
354
+ setTimeout(() => { toast.classList.remove('visible'); }, duration);
355
+ }
356
+
357
+ // Navigation
358
+ navTabs.addEventListener('click', (e) => {
359
+ const tab = e.target.closest('.nav-tab');
360
+ if (!tab) return;
361
+
362
+ const panelName = tab.dataset.panel;
363
+ if (panelName === activePanel) return;
364
+
365
+ // Update tabs
366
+ navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
367
+ tab.classList.add('active');
368
+
369
+ // Update panels
370
+ Object.keys(panels).forEach(key => {
371
+ panels[key].panel.classList.toggle('active', key === panelName);
372
+ });
373
+
374
+ activePanel = panelName;
375
+
376
+ // Fetch content for the new panel
377
+ if (selectedCascadeId) {
378
+ fetchPanelContent(panelName, selectedCascadeId);
379
+ }
380
+ });
381
+
382
+ // WebSocket connection
383
+ function connect() {
384
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
385
+ ws = new WebSocket(`${protocol}//${window.location.host}`);
386
+
387
+ ws.onopen = () => {
388
+ setStatus('connected');
389
+ reconnectAttempts = 0;
390
+ };
391
+
392
+ ws.onmessage = (e) => {
393
+ try {
394
+ handleMessage(JSON.parse(e.data));
395
+ } catch (err) {
396
+ console.error('Failed to parse message:', err);
397
+ }
398
+ };
399
+
400
+ ws.onclose = () => {
401
+ setStatus('disconnected');
402
+ scheduleReconnect();
403
+ };
404
+
405
+ ws.onerror = () => {};
406
+ }
407
+
408
+ function scheduleReconnect() {
409
+ if (reconnectTimeout) clearTimeout(reconnectTimeout);
410
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
411
+ reconnectAttempts++;
412
+ reconnectTimeout = setTimeout(connect, delay);
413
+ }
414
+
415
+ function handleMessage(data) {
416
+ if (data.type === 'cascade_list') {
417
+ cascades = data.cascades || [];
418
+ if (!selectedCascadeId && cascades.length > 0) {
419
+ selectCascade(cascades[0].id);
420
+ }
421
+ } else if (data.type === 'snapshot_update' && data.cascadeId === selectedCascadeId) {
422
+ // Only fetch if user is not typing AND the updated panel is the active one
423
+ // AND we're not currently rendering (prevent flickering from rapid updates)
424
+ const panel = data.panel || 'chat';
425
+ if (!isTypingInKiroInput && panel === activePanel && !isRendering) {
426
+ // Debounce rapid updates - wait 500ms before fetching
427
+ if (updateDebounceTimer) clearTimeout(updateDebounceTimer);
428
+ updateDebounceTimer = setTimeout(() => {
429
+ fetchPanelContent(panel, data.cascadeId, true); // true = isUpdate (don't scroll)
430
+ }, 500);
431
+ }
432
+ }
433
+ }
434
+
435
+ function selectCascade(cascadeId) {
436
+ selectedCascadeId = cascadeId;
437
+ currentStyles = null;
438
+
439
+ if (cascadeId) {
440
+ fetchStyles(cascadeId).then(() => {
441
+ fetchPanelContent(activePanel, cascadeId);
442
+ });
443
+ } else {
444
+ showLoading('chat', 'Waiting for Kiro...');
445
+ }
446
+ }
447
+
448
+ async function fetchStyles(cascadeId) {
449
+ try {
450
+ const r = await fetch(`/styles/${cascadeId}`);
451
+ if (r.ok) currentStyles = await r.text();
452
+ } catch (e) {
453
+ console.error('Failed to fetch styles:', e);
454
+ }
455
+ }
456
+
457
+ function showLoading(panelName, msg = 'Loading...') {
458
+ const p = panels[panelName];
459
+ if (p.loading) {
460
+ p.loading.querySelector('span').textContent = msg;
461
+ p.loading.style.display = 'flex';
462
+ }
463
+ if (p.content) p.content.style.display = 'none';
464
+ if (p.header) p.header.style.display = 'none';
465
+ }
466
+
467
+ function hideLoading(panelName) {
468
+ const p = panels[panelName];
469
+ if (p.loading) p.loading.style.display = 'none';
470
+ if (p.content) p.content.style.display = 'block';
471
+ if (p.header) p.header.style.display = 'flex';
472
+ }
473
+
474
+ // Fetch panel content
475
+ async function fetchPanelContent(panelName, cascadeId, isUpdate = false) {
476
+ switch (panelName) {
477
+ case 'chat':
478
+ await fetchChatSnapshot(cascadeId, isUpdate);
479
+ break;
480
+ case 'editor':
481
+ await fetchEditorSnapshot(cascadeId, isUpdate);
482
+ break;
483
+ }
484
+ }
485
+
486
+ // Chat snapshot
487
+ async function fetchChatSnapshot(cascadeId, isUpdate = false) {
488
+ try {
489
+ const r = await fetch(`/snapshot/${cascadeId}`);
490
+ if (!r.ok) {
491
+ if (navigationPending) {
492
+ setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500);
493
+ return;
494
+ }
495
+ if (r.status === 404) showLoading('chat', 'Waiting for snapshot...');
496
+ return;
497
+ }
498
+ const snapshot = await r.json();
499
+ if (snapshot && snapshot.html && snapshot.html.length > 100) {
500
+ lastSuccessfulSnapshot = snapshot;
501
+ navigationPending = false;
502
+ renderChatSnapshot(snapshot, isUpdate);
503
+ } else if (lastSuccessfulSnapshot && navigationPending) {
504
+ setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 300);
505
+ } else {
506
+ renderChatSnapshot(snapshot);
507
+ }
508
+ } catch (e) {
509
+ if (!navigationPending) showToast('Failed to load chat');
510
+ }
511
+ }
512
+
513
+ function renderChatSnapshot(snapshot, isUpdate = false) {
514
+ // Prevent concurrent renders
515
+ if (isRendering) return;
516
+ isRendering = true;
517
+
518
+ // Save scroll position of inner scrollable element before update
519
+ let innerScrollable = panels.chat.content.querySelector('.overflow-y-auto, [class*="scroll"]');
520
+ const hadContent = innerScrollable && innerScrollable.scrollHeight > 100;
521
+ const scrollTop = innerScrollable ? innerScrollable.scrollTop : 0;
522
+ const scrollHeight = innerScrollable ? innerScrollable.scrollHeight : 0;
523
+ const clientHeight = innerScrollable ? innerScrollable.clientHeight : 0;
524
+ const wasAtBottom = !hadContent || (scrollHeight - scrollTop - clientHeight < 100);
525
+
526
+ let html = getBaseStyles();
527
+ if (currentStyles) html += `<style>${currentStyles}</style>`;
528
+ html += snapshot.html || '';
529
+
530
+ panels.chat.content.innerHTML = html;
531
+
532
+ // Set background color - use solid dark background instead of transparent
533
+ if (snapshot.bodyBg) {
534
+ panels.chat.container.style.background = snapshot.bodyBg;
535
+ } else {
536
+ // Fallback to solid dark background matching Kiro IDE
537
+ panels.chat.container.style.background = '#1e1e1e';
538
+ }
539
+
540
+ hideLoading('chat');
541
+
542
+ // Find the new inner scrollable element and scroll it
543
+ requestAnimationFrame(() => {
544
+ requestAnimationFrame(() => {
545
+ // Find all scrollable elements and scroll them
546
+ const scrollables = panels.chat.content.querySelectorAll('.overflow-y-auto, [class*="scroll"]');
547
+ scrollables.forEach(el => {
548
+ if (el.scrollHeight > el.clientHeight + 10) {
549
+ if (!isUpdate || wasAtBottom) {
550
+ el.scrollTop = el.scrollHeight;
551
+ } else if (hadContent) {
552
+ el.scrollTop = scrollTop;
553
+ }
554
+ }
555
+ });
556
+
557
+ // Also scroll the main container if needed
558
+ if (!isUpdate || wasAtBottom) {
559
+ panels.chat.content.scrollTop = panels.chat.content.scrollHeight;
560
+ }
561
+
562
+ // Release rendering lock
563
+ isRendering = false;
564
+ });
565
+ });
566
+
567
+ makeInteractive();
568
+ }
569
+
570
+ // Editor snapshot
571
+ async function fetchEditorSnapshot(cascadeId, isUpdate = false) {
572
+ try {
573
+ const r = await fetch(`/editor/${cascadeId}`);
574
+ if (!r.ok) {
575
+ showEmptyState('editor', 'codicon-file-code', 'No file is currently open. Open a file in Kiro to view it here.');
576
+ return;
577
+ }
578
+ const data = await r.json();
579
+ renderEditorSnapshot(data, isUpdate);
580
+ } catch (e) {
581
+ showEmptyState('editor', 'codicon-file-code', 'Failed to load editor');
582
+ }
583
+ }
584
+
585
+ function renderEditorSnapshot(data, isUpdate = false) {
586
+ const scrollTop = panels.editor.content.scrollTop;
587
+
588
+ // Update header
589
+ panels.editor.filename.textContent = data.fileName || 'Untitled';
590
+
591
+ // Show header
592
+ panels.editor.header.style.display = 'flex';
593
+
594
+ // Store original content for search
595
+ originalEditorContent = data.content || '';
596
+
597
+ // Reset search when new file loaded
598
+ closeEditorSearch();
599
+
600
+ // Prefer text content over raw HTML for better mobile display
601
+ if (data.content && data.content.trim().length > 0) {
602
+ const lines = data.content.split('\n');
603
+ let html = '';
604
+
605
+ // Add note if partial content
606
+ if (data.isPartial || data.note) {
607
+ html += `<div style="padding: 8px 12px; background: #2d2d30; color: #888; font-size: 11px; border-bottom: 1px solid #3c3c3c;">
608
+ <span class="codicon codicon-info" style="margin-right: 6px;"></span>
609
+ ${data.note || 'Showing visible lines only. Scroll in Kiro to see more.'}
610
+ </div>`;
611
+ }
612
+
613
+ html += '<pre class="editor-code">';
614
+
615
+ // Use startLine from server if available, otherwise start from 1
616
+ const startLineNum = data.startLine || 1;
617
+
618
+ // Filter out empty lines at the start if there are too many
619
+ let startIdx = 0;
620
+ while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) {
621
+ startIdx++;
622
+ }
623
+
624
+ const displayLines = lines.slice(startIdx);
625
+ displayLines.forEach((line, idx) => {
626
+ const lineNum = startLineNum + startIdx + idx;
627
+ const highlighted = highlightSyntax(line, data.language);
628
+ // Preserve whitespace - convert tabs and spaces
629
+ let preservedLine = highlighted
630
+ .replace(/\t/g, ' ')
631
+ .replace(/^ +/, match => '&nbsp;'.repeat(match.length));
632
+ html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${preservedLine || '&nbsp;'}</span></div>`;
633
+ });
634
+ html += '</pre>';
635
+
636
+ // Add line count info
637
+ const endLine = startLineNum + startIdx + displayLines.length - 1;
638
+ html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
639
+ Lines ${startLineNum}-${endLine}${data.lineCount ? ` of ${data.lineCount}` : ''}
640
+ </div>`;
641
+
642
+ panels.editor.content.innerHTML = html;
643
+ panels.editor.content.style.display = 'block';
644
+ console.log('[Editor] Rendered', displayLines.length, 'lines of code starting from line', startLineNum);
645
+ } else if (data.html && data.html.length > 100) {
646
+ // Fallback to raw HTML if no text content
647
+ let html = getBaseStyles();
648
+ if (currentStyles) html += `<style>${currentStyles}</style>`;
649
+ html += data.html;
650
+ panels.editor.content.innerHTML = html;
651
+ panels.editor.content.style.display = 'block';
652
+ console.log('[Editor] Rendered raw HTML');
653
+ } else {
654
+ showEmptyState('editor', 'codicon-file-code', 'No editor content available. Open a file in Kiro to view it here.');
655
+ return;
656
+ }
657
+ hideLoading('editor');
658
+
659
+ // Preserve scroll on updates
660
+ if (isUpdate) {
661
+ panels.editor.content.scrollTop = scrollTop;
662
+ }
663
+ }
664
+
665
+ // Syntax highlighting (basic)
666
+ function highlightSyntax(line, language) {
667
+ let escaped = escapeHtml(line);
668
+
669
+ // Skip syntax highlighting for CSS (too complex with # colors)
670
+ if (language === 'css' || language === 'html') {
671
+ return escaped;
672
+ }
673
+
674
+ // Comments - be careful not to match CSS color codes
675
+ // Only match // comments and # comments that start at beginning or after whitespace
676
+ escaped = escaped.replace(/(\/\/.*$)/gm, '<span class="token-comment">$1</span>');
677
+ escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
678
+ // Python/shell comments - only if # is at start of line or after whitespace, not in middle of word
679
+ if (language === 'python' || language === 'bash' || language === 'sh') {
680
+ escaped = escaped.replace(/(^|\s)(#.*)$/gm, '$1<span class="token-comment">$2</span>');
681
+ }
682
+
683
+ // Strings - match quoted strings
684
+ escaped = escaped.replace(/(&quot;[^&]*?&quot;)/g, '<span class="token-string">$1</span>');
685
+ escaped = escaped.replace(/(&#39;[^&]*?&#39;)/g, '<span class="token-string">$1</span>');
686
+
687
+ // Keywords
688
+ const keywords = ['const', 'let', 'var', 'function', 'class', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'super', 'extends', 'implements', 'interface', 'type', 'enum', 'public', 'private', 'protected', 'static', 'readonly', 'def', 'self', 'None', 'True', 'False'];
689
+ keywords.forEach(kw => {
690
+ const regex = new RegExp(`\\b(${kw})\\b`, 'g');
691
+ escaped = escaped.replace(regex, '<span class="token-keyword">$1</span>');
692
+ });
693
+
694
+ // Numbers - but not inside other tokens
695
+ escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
696
+
697
+ return escaped;
698
+ }
699
+
700
+ // Helper functions
701
+ function escapeHtml(text) {
702
+ const div = document.createElement('div');
703
+ div.textContent = text;
704
+ return div.innerHTML;
705
+ }
706
+
707
+ function showEmptyState(panelName, icon, message) {
708
+ const p = panels[panelName];
709
+ p.content.innerHTML = `
710
+ <div class="empty-state">
711
+ <span class="codicon ${icon}"></span>
712
+ <p>${message}</p>
713
+ </div>
714
+ `;
715
+ hideLoading(panelName);
716
+ }
717
+
718
+ function toggleSection(header) {
719
+ header.classList.toggle('collapsed');
720
+ const content = header.nextElementSibling;
721
+ if (content) content.classList.toggle('collapsed');
722
+ }
723
+
724
+ // Base styles for chat panel
725
+ function getBaseStyles() {
726
+ return `<style>
727
+ :root, body, #root, .app {
728
+ font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
729
+ font-size: 13px;
730
+ --vscode-editor-background: #211d25;
731
+ --vscode-editor-foreground: #ffffff;
732
+ --vscode-foreground: #ffffff;
733
+ --vscode-background: #19161d;
734
+ --color-text-primary: #ffffff;
735
+ --color-text-secondary: rgba(255, 255, 255, 0.8);
736
+ --color-text-tertiary: rgba(255, 255, 255, 0.6);
737
+ --color-border-primary: #4a464f;
738
+ --vscode-toggleSwitch-onForeground: #ffffff;
739
+ --vscode-toggleSwitch-onBackground: #8141e6;
740
+ --vscode-toggleSwitch-offForeground: #ffffff;
741
+ --vscode-toggleSwitch-offBackground: #28242e;
742
+ --vscode-chatTab-foreground: #c1bec6;
743
+ --vscode-chatTab-background: transparent;
744
+ --vscode-button-tertiaryForeground: #c1bec6;
745
+ --vscode-button-tertiaryBackground: transparent;
746
+ --vscode-button-tertiaryHoverBackground: rgba(255, 255, 255, 0.1);
747
+ --vscode-button-submitBackground: #7138cc;
748
+ --vscode-button-submitHoverBackground: #8e47ff;
749
+ --vscode-input-border: #4a464f;
750
+ --vscode-chatMessage-background: rgba(255, 255, 255, 0.1);
751
+ --vscode-agentInProgressIcon-foreground: #7138cc;
752
+ --vscode-agentAcceptedIcon-foreground: rgba(128, 255, 181, 0.75);
753
+ }
754
+
755
+ /* Force solid background for all content */
756
+ body, #root, .app, [class*="chat"], [class*="Chat"], [class*="cascade"], [class*="Cascade"] {
757
+ background: #1e1e1e !important;
758
+ background-color: #1e1e1e !important;
759
+ }
760
+
761
+ /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
762
+ [role="tooltip"],
763
+ [data-tooltip],
764
+ [class*="tooltip"]:not(button):not([role="button"]),
765
+ [class*="Tooltip"]:not(button):not([role="button"]),
766
+ [class*="popover"]:not(button):not([role="button"]),
767
+ [class*="Popover"]:not(button):not([role="button"]),
768
+ [class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
769
+ [class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
770
+ [class*="modal"],
771
+ [class*="Modal"] {
772
+ display: none !important;
773
+ visibility: hidden !important;
774
+ opacity: 0 !important;
775
+ pointer-events: none !important;
776
+ }
777
+
778
+ /* ========== MODEL SELECTOR DROPDOWN ========== */
779
+ /* Dropdown menu panel - needs solid background and proper z-index */
780
+ [class*="dropdown-menu"], [class*="dropdownMenu"], [class*="DropdownMenu"],
781
+ [class*="dropdown-content"], [class*="dropdownContent"], [class*="DropdownContent"],
782
+ [class*="model-list"], [class*="modelList"], [class*="ModelList"],
783
+ [role="listbox"], [role="menu"] {
784
+ display: block !important;
785
+ visibility: visible !important;
786
+ opacity: 1 !important;
787
+ pointer-events: auto !important;
788
+ background: #2d2d30 !important;
789
+ background-color: #2d2d30 !important;
790
+ border: 1px solid #4a464f !important;
791
+ border-radius: 6px !important;
792
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
793
+ z-index: 9999 !important;
794
+ position: absolute !important;
795
+ }
796
+
797
+ /* Dropdown menu items */
798
+ [class*="dropdown-item"], [class*="dropdownItem"], [class*="DropdownItem"],
799
+ [class*="model-option"], [class*="modelOption"], [class*="ModelOption"],
800
+ [role="option"], [role="menuitem"] {
801
+ display: flex !important;
802
+ visibility: visible !important;
803
+ opacity: 1 !important;
804
+ pointer-events: auto !important;
805
+ padding: 8px 12px !important;
806
+ cursor: pointer !important;
807
+ background: transparent !important;
808
+ color: #cccccc !important;
809
+ }
810
+
811
+ [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
812
+ [role="option"]:hover, [role="menuitem"]:hover {
813
+ background: rgba(255, 255, 255, 0.1) !important;
814
+ }
815
+
816
+ /* Model selector button - NO border, match Kiro style */
817
+ [class*="model-selector"], [class*="modelSelector"], [class*="ModelSelector"],
818
+ [class*="model-dropdown"], [class*="modelDropdown"], [class*="ModelDropdown"],
819
+ button[class*="dropdown"], [role="button"][class*="dropdown"],
820
+ [class*="select-model"], [class*="selectModel"] {
821
+ display: inline-flex !important;
822
+ visibility: visible !important;
823
+ opacity: 1 !important;
824
+ pointer-events: auto !important;
825
+ align-items: center !important;
826
+ gap: 4px !important;
827
+ padding: 4px 8px !important;
828
+ background: transparent !important;
829
+ border: none !important;
830
+ border-radius: 4px !important;
831
+ color: var(--vscode-foreground, #c1bec6) !important;
832
+ font-size: 12px !important;
833
+ cursor: pointer !important;
834
+ }
835
+
836
+ [class*="model-selector"]:hover, [class*="modelSelector"]:hover,
837
+ button[class*="dropdown"]:hover {
838
+ background: rgba(255, 255, 255, 0.1) !important;
839
+ }
840
+
841
+ /* Model selector chevron/arrow icon */
842
+ [class*="model-selector"] svg, [class*="modelSelector"] svg,
843
+ [class*="model-dropdown"] svg, button[class*="dropdown"] svg {
844
+ width: 12px !important;
845
+ height: 12px !important;
846
+ flex-shrink: 0 !important;
847
+ }
848
+
849
+ /* Model name text inside selector */
850
+ [class*="model-selector"] span, [class*="modelSelector"] span,
851
+ [class*="model-name"], [class*="modelName"] {
852
+ overflow: hidden !important;
853
+ text-overflow: ellipsis !important;
854
+ white-space: nowrap !important;
855
+ }
856
+
857
+ /* Reset positioning for captured content to prevent layout issues */
858
+ #root, .app, [class*="cascade"], [class*="Cascade"] {
859
+ position: relative !important;
860
+ }
861
+
862
+ /* Ensure proper stacking and no fixed positioning breaks layout */
863
+ [style*="position: fixed"],
864
+ [style*="position:fixed"] {
865
+ position: absolute !important;
866
+ }
867
+
868
+ * { font-family: inherit; }
869
+ code, pre, .monaco-editor { font-family: Menlo, Monaco, "Courier New", monospace !important; }
870
+ /* Icon sizes - keep them small like in IDE */
871
+ svg[viewBox="0 0 24 24"] { width: 16px !important; height: 16px !important; }
872
+ svg[viewBox="0 0 16 16"] { width: 14px !important; height: 14px !important; }
873
+ svg { color: inherit; max-width: 20px !important; max-height: 20px !important; }
874
+ svg path, svg rect { fill: currentColor; }
875
+ svg[fill="none"] path { fill: none; }
876
+ /* Preserve stroke-based SVG circles (like context window progress ring) */
877
+ svg circle[stroke] { fill: none !important; }
878
+ svg circle:not([stroke]) { fill: currentColor; }
879
+ /* Context window progress circle styling */
880
+ svg[class*="context"], svg[class*="progress"], [class*="context"] svg {
881
+ overflow: visible !important;
882
+ }
883
+ svg circle[stroke-dasharray] {
884
+ fill: none !important;
885
+ stroke-linecap: round !important;
886
+ }
887
+ /* Context window indicator - circular progress ring */
888
+ [class*="context-window"], [class*="contextWindow"], [class*="ContextWindow"],
889
+ [class*="context-indicator"], [class*="contextIndicator"], [class*="ContextIndicator"],
890
+ [class*="token-usage"], [class*="tokenUsage"], [class*="TokenUsage"] {
891
+ display: inline-flex !important;
892
+ visibility: visible !important;
893
+ opacity: 1 !important;
894
+ align-items: center !important;
895
+ justify-content: center !important;
896
+ width: 20px !important;
897
+ height: 20px !important;
898
+ position: relative !important;
899
+ flex-shrink: 0 !important;
900
+ }
901
+ /* Create gray background circle using box-shadow on the container */
902
+ [class*="context-window"]::before, [class*="contextWindow"]::before, [class*="ContextWindow"]::before,
903
+ [class*="context-indicator"]::before, [class*="contextIndicator"]::before {
904
+ content: '' !important;
905
+ position: absolute !important;
906
+ top: 2px !important;
907
+ left: 2px !important;
908
+ width: 16px !important;
909
+ height: 16px !important;
910
+ border-radius: 50% !important;
911
+ border: 2px solid #4a464f !important;
912
+ box-sizing: border-box !important;
913
+ z-index: 0 !important;
914
+ }
915
+ [class*="context-window"] svg, [class*="contextWindow"] svg, [class*="ContextWindow"] svg,
916
+ [class*="context-indicator"] svg, [class*="contextIndicator"] svg {
917
+ width: 16px !important;
918
+ height: 16px !important;
919
+ transform: rotate(-90deg) !important;
920
+ position: relative !important;
921
+ z-index: 1 !important;
922
+ }
923
+ [class*="context-window"] svg circle, [class*="contextWindow"] svg circle, [class*="ContextWindow"] svg circle,
924
+ [class*="context-indicator"] svg circle, [class*="contextIndicator"] svg circle {
925
+ fill: none !important;
926
+ stroke-width: 2 !important;
927
+ stroke-linecap: round !important;
928
+ stroke: #4ade80 !important;
929
+ }
930
+ /* Ensure circles are visible */
931
+ [class*="context"] svg circle,
932
+ [class*="Context"] svg circle {
933
+ display: block !important;
934
+ visibility: visible !important;
935
+ }
936
+
937
+ /* ========== CHAT INPUT TOOLBAR AREA ========== */
938
+ /* The toolbar/footer area containing model selector, context, autopilot, input */
939
+ [class*="chat-input"], [class*="chatInput"], [class*="ChatInput"],
940
+ [class*="input-area"], [class*="inputArea"], [class*="InputArea"],
941
+ [class*="message-input"], [class*="messageInput"], [class*="MessageInput"],
942
+ [class*="composer"], [class*="Composer"] {
943
+ display: flex !important;
944
+ visibility: visible !important;
945
+ flex-direction: column !important;
946
+ gap: 8px !important;
947
+ padding: 8px 12px !important;
948
+ background: var(--vscode-editor-background, #1e1e1e) !important;
949
+ border-top: 1px solid var(--color-border-primary, #4a464f) !important;
950
+ }
951
+
952
+ /* Input toolbar row (contains # button, model selector, context, autopilot) */
953
+ [class*="input-toolbar"], [class*="inputToolbar"], [class*="InputToolbar"],
954
+ [class*="chat-toolbar"], [class*="chatToolbar"], [class*="ChatToolbar"],
955
+ [class*="composer-toolbar"], [class*="composerToolbar"] {
956
+ display: flex !important;
957
+ visibility: visible !important;
958
+ align-items: center !important;
959
+ gap: 8px !important;
960
+ flex-wrap: wrap !important;
961
+ }
962
+
963
+ /* Context button (# symbol) - NO border, match Kiro style */
964
+ [class*="context-button"], [class*="contextButton"], [class*="ContextButton"],
965
+ button[aria-label*="context"], button[aria-label*="Context"],
966
+ [class*="hash-button"], [class*="hashButton"] {
967
+ display: inline-flex !important;
968
+ visibility: visible !important;
969
+ align-items: center !important;
970
+ justify-content: center !important;
971
+ width: 28px !important;
972
+ height: 28px !important;
973
+ padding: 4px !important;
974
+ background: transparent !important;
975
+ border: none !important;
976
+ border-radius: 4px !important;
977
+ color: var(--vscode-foreground, #c1bec6) !important;
978
+ font-size: 16px !important;
979
+ font-weight: 600 !important;
980
+ cursor: pointer !important;
981
+ }
982
+
983
+ [class*="context-button"]:hover, [class*="contextButton"]:hover,
984
+ button[aria-label*="context"]:hover {
985
+ background: rgba(255, 255, 255, 0.1) !important;
986
+ }
987
+
988
+ /* ========== MESSAGE TEXT FLOW - INLINE ELEMENTS ========== */
989
+ /* Fix text flow so file links and code stay inline with text */
990
+ [class*="message"] p,
991
+ [class*="Message"] p,
992
+ [class*="chat"] p,
993
+ [class*="Chat"] p {
994
+ display: block !important;
995
+ }
996
+
997
+ /* Make file links, code, and inline elements stay inline */
998
+ [class*="message"] a,
999
+ [class*="Message"] a,
1000
+ [class*="message"] code,
1001
+ [class*="Message"] code,
1002
+ [class*="message"] span,
1003
+ [class*="Message"] span,
1004
+ [class*="chat"] a,
1005
+ [class*="Chat"] a,
1006
+ [class*="chat"] code,
1007
+ [class*="Chat"] code,
1008
+ [class*="file-link"],
1009
+ [class*="FileLink"],
1010
+ [class*="inline"],
1011
+ code:not(pre code) {
1012
+ display: inline !important;
1013
+ vertical-align: baseline !important;
1014
+ }
1015
+
1016
+ /* Inline code styling */
1017
+ code:not(pre code) {
1018
+ background: rgba(255, 255, 255, 0.1) !important;
1019
+ padding: 2px 6px !important;
1020
+ border-radius: 3px !important;
1021
+ font-family: Menlo, Monaco, "Courier New", monospace !important;
1022
+ font-size: 12px !important;
1023
+ }
1024
+
1025
+ /* File badges/chips should be inline */
1026
+ [class*="file-badge"],
1027
+ [class*="FileBadge"],
1028
+ [class*="file-chip"],
1029
+ [class*="FileChip"],
1030
+ [class*="deleted"],
1031
+ [class*="Deleted"],
1032
+ [class*="created"],
1033
+ [class*="Created"],
1034
+ [class*="modified"],
1035
+ [class*="Modified"] {
1036
+ display: inline-flex !important;
1037
+ align-items: center !important;
1038
+ gap: 4px !important;
1039
+ padding: 2px 8px !important;
1040
+ border-radius: 4px !important;
1041
+ background: rgba(255, 255, 255, 0.08) !important;
1042
+ margin: 2px 4px !important;
1043
+ font-size: 12px !important;
1044
+ vertical-align: middle !important;
1045
+ }
1046
+
1047
+ /* Bullet lists should have proper inline content */
1048
+ ul li, ol li {
1049
+ display: list-item !important;
1050
+ }
1051
+
1052
+ ul li > *, ol li > * {
1053
+ display: inline !important;
1054
+ }
1055
+
1056
+ ul li > p, ol li > p {
1057
+ display: inline !important;
1058
+ margin: 0 !important;
1059
+ }
1060
+
1061
+ [role="switch"], [class*="toggle"] {
1062
+ overflow: visible !important;
1063
+ min-width: 36px !important;
1064
+ flex-shrink: 0 !important;
1065
+ }
1066
+
1067
+ /* ========== TASK LIST STYLING - EXACT KIRO IDE MATCH ========== */
1068
+
1069
+ /* Force solid background for task containers */
1070
+ [class*="task"], [class*="Task"],
1071
+ div:has(> h1:contains("CURRENT TASKS")),
1072
+ div:has(> h2:contains("CURRENT TASKS")),
1073
+ div:has(> h3:contains("CURRENT TASKS")),
1074
+ div:has(> h1:contains("TASKS IN QUEUE")),
1075
+ div:has(> h2:contains("TASKS IN QUEUE")),
1076
+ div:has(> h3:contains("TASKS IN QUEUE")) {
1077
+ font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
1078
+ background: #1e1e1e !important;
1079
+ background-color: #1e1e1e !important;
1080
+ }
1081
+
1082
+ /* Task section headers - BOLD WHITE UPPERCASE */
1083
+ h1:contains("CURRENT TASKS"),
1084
+ h2:contains("CURRENT TASKS"),
1085
+ h3:contains("CURRENT TASKS"),
1086
+ h1:contains("TASKS IN QUEUE"),
1087
+ h2:contains("TASKS IN QUEUE"),
1088
+ h3:contains("TASKS IN QUEUE"),
1089
+ [class*="task"] h1, [class*="Task"] h1,
1090
+ [class*="task"] h2, [class*="Task"] h2,
1091
+ [class*="task"] h3, [class*="Task"] h3,
1092
+ [class*="task-header"], [class*="TaskHeader"],
1093
+ [class*="section-title"], [class*="SectionTitle"],
1094
+ div[class*="current"] h1, div[class*="Current"] h1,
1095
+ div[class*="current"] h2, div[class*="Current"] h2,
1096
+ div[class*="current"] h3, div[class*="Current"] h3,
1097
+ div[class*="queue"] h1, div[class*="Queue"] h1,
1098
+ div[class*="queue"] h2, div[class*="Queue"] h2,
1099
+ div[class*="queue"] h3, div[class*="Queue"] h3 {
1100
+ font-size: 13px !important;
1101
+ font-weight: 700 !important;
1102
+ letter-spacing: 0.5px !important;
1103
+ text-transform: uppercase !important;
1104
+ color: #ffffff !important;
1105
+ margin: 0 0 12px 0 !important;
1106
+ padding: 0 !important;
1107
+ line-height: 1.4 !important;
1108
+ background: transparent !important;
1109
+ }
1110
+
1111
+ /* Empty state messages - ITALIC GRAY */
1112
+ p:contains("No active tasks"),
1113
+ p:contains("No tasks in queue"),
1114
+ [class*="task"] p:not([class*="task-item"]),
1115
+ [class*="Task"] p:not([class*="TaskItem"]),
1116
+ [class*="no-task"], [class*="NoTask"],
1117
+ [class*="empty-task"], [class*="EmptyTask"],
1118
+ [class*="task-empty"], [class*="TaskEmpty"] {
1119
+ font-size: 13px !important;
1120
+ font-style: italic !important;
1121
+ color: #888888 !important;
1122
+ margin: 0 0 16px 0 !important;
1123
+ padding: 0 !important;
1124
+ line-height: 1.6 !important;
1125
+ background: transparent !important;
1126
+ }
1127
+
1128
+ /* Task list containers */
1129
+ [class*="task-list"], [class*="TaskList"],
1130
+ [class*="task-section"], [class*="TaskSection"] {
1131
+ margin-bottom: 24px !important;
1132
+ padding: 0 !important;
1133
+ background: #1e1e1e !important;
1134
+ }
1135
+
1136
+ /* Horizontal divider between sections */
1137
+ [class*="task-divider"], [class*="TaskDivider"],
1138
+ [class*="task"] hr, [class*="Task"] hr {
1139
+ border: none !important;
1140
+ border-top: 1px solid #3c3c3c !important;
1141
+ margin: 20px 0 !important;
1142
+ background: transparent !important;
1143
+ }
1144
+
1145
+ /* Individual task items (when they exist) */
1146
+ [class*="task-item"], [class*="TaskItem"] {
1147
+ padding: 8px 12px !important;
1148
+ margin: 4px 0 !important;
1149
+ background: rgba(255, 255, 255, 0.03) !important;
1150
+ border-radius: 4px !important;
1151
+ border-left: 2px solid #7138cc !important;
1152
+ font-size: 13px !important;
1153
+ color: #cccccc !important;
1154
+ line-height: 1.5 !important;
1155
+ }
1156
+
1157
+ [class*="task-item"]:hover, [class*="TaskItem"]:hover {
1158
+ background: rgba(255, 255, 255, 0.05) !important;
1159
+ }
1160
+
1161
+ /* Task item with close button */
1162
+ [class*="task-item"] button, [class*="TaskItem"] button {
1163
+ margin-left: 8px !important;
1164
+ padding: 2px 6px !important;
1165
+ font-size: 11px !important;
1166
+ background: rgba(255, 255, 255, 0.1) !important;
1167
+ border: 1px solid #4a464f !important;
1168
+ border-radius: 3px !important;
1169
+ color: #888 !important;
1170
+ cursor: pointer !important;
1171
+ }
1172
+
1173
+ [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
1174
+ background: rgba(255, 255, 255, 0.15) !important;
1175
+ color: #fff !important;
1176
+ }
1177
+
1178
+ .kiro-toggle-switch {
1179
+ display: flex !important;
1180
+ align-items: center !important;
1181
+ gap: 4px !important;
1182
+ padding: 4px !important;
1183
+ border-radius: 6px !important;
1184
+ border: none !important;
1185
+ }
1186
+ .kiro-toggle-switch > input[type="checkbox"] {
1187
+ appearance: none !important;
1188
+ width: 36px !important;
1189
+ height: 18px !important;
1190
+ border-radius: 9px !important;
1191
+ background-color: var(--vscode-toggleSwitch-offBackground) !important;
1192
+ border: 1px solid var(--vscode-input-border) !important;
1193
+ cursor: pointer !important;
1194
+ position: relative !important;
1195
+ }
1196
+ .kiro-toggle-switch > input[type="checkbox"]::after {
1197
+ content: "" !important;
1198
+ position: absolute !important;
1199
+ top: 50% !important;
1200
+ left: 2px !important;
1201
+ transform: translateY(-50%) !important;
1202
+ width: 14px !important;
1203
+ height: 14px !important;
1204
+ border-radius: 50% !important;
1205
+ background-color: #fff !important;
1206
+ transition: left 0.2s !important;
1207
+ }
1208
+ .kiro-toggle-switch > input[type="checkbox"]:checked {
1209
+ background-color: var(--vscode-toggleSwitch-onBackground) !important;
1210
+ }
1211
+ .kiro-toggle-switch > input[type="checkbox"]:checked::after {
1212
+ left: 18px !important;
1213
+ }
1214
+ </style>`;
1215
+ }
1216
+
1217
+ // Make chat elements interactive
1218
+ function makeInteractive() {
1219
+ const content = panels.chat.content;
1220
+
1221
+ // Input fields
1222
+ const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror'];
1223
+ for (const selector of inputSelectors) {
1224
+ content.querySelectorAll(selector).forEach(el => {
1225
+ el.style.cursor = 'text';
1226
+ el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1227
+ el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1228
+ el.addEventListener('keydown', async (e) => {
1229
+ if (e.key === 'Enter' && !e.shiftKey) {
1230
+ e.preventDefault();
1231
+ const text = el.textContent || el.value || '';
1232
+ if (text.trim()) {
1233
+ await sendToKiro(text.trim());
1234
+ el.textContent = '';
1235
+ el.innerHTML = '';
1236
+ isTypingInKiroInput = false;
1237
+ }
1238
+ }
1239
+ });
1240
+ });
1241
+ }
1242
+
1243
+ // Toggle switches
1244
+ content.querySelectorAll('.kiro-toggle-switch, [role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1245
+ const input = toggle.tagName === 'INPUT' ? toggle : toggle.querySelector('input[type="checkbox"]');
1246
+ const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1247
+
1248
+ wrapper.style.cursor = 'pointer';
1249
+ wrapper.onclick = async (e) => {
1250
+ e.preventDefault();
1251
+ e.stopPropagation();
1252
+
1253
+ const label = wrapper.querySelector('label');
1254
+ const clickInfo = {
1255
+ tag: 'div',
1256
+ text: label ? label.textContent.trim() : 'toggle',
1257
+ role: 'switch',
1258
+ isToggle: true,
1259
+ toggleId: input ? input.id : '',
1260
+ checked: input ? input.checked : false
1261
+ };
1262
+ await sendClickToKiro(clickInfo);
1263
+ return false;
1264
+ };
1265
+ });
1266
+
1267
+ // Tabs
1268
+ content.querySelectorAll('[role="tab"]').forEach(tab => {
1269
+ const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
1270
+
1271
+ if (closeBtn) {
1272
+ closeBtn.style.cursor = 'pointer';
1273
+ closeBtn.onclick = async (e) => {
1274
+ e.preventDefault();
1275
+ e.stopPropagation();
1276
+ await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true });
1277
+ return false;
1278
+ };
1279
+ }
1280
+
1281
+ tab.style.cursor = 'pointer';
1282
+ tab.onclick = async (e) => {
1283
+ if (e.target.closest('[aria-label="close"], [class*="close"], button')) return;
1284
+ e.preventDefault();
1285
+ e.stopPropagation();
1286
+
1287
+ const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1288
+ const labelText = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1289
+
1290
+ await sendClickToKiro({ tag: 'div', text: labelText, role: 'tab', isTab: true, tabLabel: labelText });
1291
+ return false;
1292
+ };
1293
+ });
1294
+
1295
+ // File links - detect file paths in chat and make them clickable to open in Editor
1296
+ const fileExtensions = /\.(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|astro|cob|cbl)$/i;
1297
+ const filePathPattern = /^[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+$/;
1298
+
1299
+ // Look for elements that might contain file paths
1300
+ content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
1301
+ if (el.onclick) return;
1302
+ const text = (el.textContent || '').trim();
1303
+ const dataPath = el.getAttribute('data-path') || '';
1304
+ const href = el.getAttribute('href') || '';
1305
+
1306
+ // Check if this looks like a file path
1307
+ const isFilePath = (
1308
+ fileExtensions.test(text) ||
1309
+ fileExtensions.test(dataPath) ||
1310
+ fileExtensions.test(href) ||
1311
+ (filePathPattern.test(text) && text.includes('/'))
1312
+ );
1313
+
1314
+ if (isFilePath) {
1315
+ const filePath = dataPath || text || href;
1316
+ el.style.cursor = 'pointer';
1317
+ el.style.textDecoration = 'underline';
1318
+ el.style.color = '#7eb6ff';
1319
+ el.title = `Open ${filePath} in Editor`;
1320
+
1321
+ el.onclick = async (e) => {
1322
+ e.preventDefault();
1323
+ e.stopPropagation();
1324
+ await openFileInEditor(filePath);
1325
+ return false;
1326
+ };
1327
+ }
1328
+ });
1329
+
1330
+ // Buttons and clickable elements
1331
+ const clickableSelectors = ['button', '[role="button"]', '[role="menuitem"]', '[role="option"]', '[role="checkbox"]', 'a[href]', '[tabindex="0"]'];
1332
+ const allClickables = new Set();
1333
+ clickableSelectors.forEach(sel => {
1334
+ try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {}
1335
+ });
1336
+
1337
+ // Also add dropdown menu items
1338
+ content.querySelectorAll('[class*="dropdown-item"], [class*="dropdownItem"], [class*="model-option"], [class*="modelOption"]').forEach(el => {
1339
+ allClickables.add(el);
1340
+ });
1341
+
1342
+ // ADDED: Support for History panel items and other cursor-pointer elements
1343
+ // History items are divs with cursor-pointer class containing session info
1344
+ content.querySelectorAll('[class*="cursor-pointer"], [class*="hover\\:"], [class*="group"]').forEach(el => {
1345
+ // Check if this looks like a history/session item (contains date pattern)
1346
+ const text = el.textContent || '';
1347
+ const hasDate = /\d{1,2}\/\d{1,2}\/\d{4}/.test(text);
1348
+ const hasTime = /\d{1,2}:\d{2}/.test(text);
1349
+ if (hasDate || hasTime || el.classList.contains('cursor-pointer')) {
1350
+ allClickables.add(el);
1351
+ }
1352
+ });
1353
+
1354
+ // Also look for any div that has inline cursor:pointer style
1355
+ content.querySelectorAll('div, span').forEach(el => {
1356
+ const style = el.getAttribute('style') || '';
1357
+ if (style.includes('cursor') && style.includes('pointer')) {
1358
+ allClickables.add(el);
1359
+ }
1360
+ });
1361
+
1362
+ allClickables.forEach(el => {
1363
+ if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1364
+ if (el.closest('[role="tab"]')) return;
1365
+ if (el.closest('.kiro-toggle-switch')) return;
1366
+ if (el.onclick) return;
1367
+
1368
+ el.style.cursor = 'pointer';
1369
+ el.onclick = async (e) => {
1370
+ e.preventDefault();
1371
+ e.stopPropagation();
1372
+ const clickInfo = getElementClickInfo(el);
1373
+
1374
+ // Check if this is a model selection item
1375
+ if (el.matches('[role="option"], [role="menuitem"], [class*="model"], [class*="Model"]') ||
1376
+ el.closest('[role="listbox"], [role="menu"], [class*="dropdown-menu"], [class*="dropdownMenu"]')) {
1377
+ clickInfo.isModelSelection = true;
1378
+ clickInfo.modelName = el.textContent?.trim().split('\n')[0] || '';
1379
+ }
1380
+
1381
+ await sendClickToKiro(clickInfo);
1382
+ return false;
1383
+ };
1384
+ });
1385
+ }
1386
+
1387
+ function getElementClickInfo(el) {
1388
+ const info = {
1389
+ tag: el.tagName?.toLowerCase() || '',
1390
+ text: (el.textContent || '').trim().substring(0, 100),
1391
+ ariaLabel: el.getAttribute('aria-label') || '',
1392
+ title: el.getAttribute('title') || '',
1393
+ role: el.getAttribute('role') || '',
1394
+ className: el.className || '',
1395
+ isTab: el.getAttribute('role') === 'tab',
1396
+ dataTestId: el.getAttribute('data-testid') || ''
1397
+ };
1398
+
1399
+ if (!info.ariaLabel) {
1400
+ const parent = el.closest('[aria-label]');
1401
+ if (parent) info.ariaLabel = parent.getAttribute('aria-label') || '';
1402
+ }
1403
+
1404
+ return info;
1405
+ }
1406
+
1407
+ async function sendToKiro(message) {
1408
+ if (!message || !selectedCascadeId) return;
1409
+ try {
1410
+ const r = await fetch(`/send/${selectedCascadeId}`, {
1411
+ method: 'POST',
1412
+ headers: { 'Content-Type': 'application/json' },
1413
+ body: JSON.stringify({ message })
1414
+ });
1415
+ const result = await r.json();
1416
+ if (result.success) showToast('Sent!', 1500, true);
1417
+ else showToast(result.error || 'Failed to send');
1418
+ } catch (e) {
1419
+ showToast('Failed to send');
1420
+ }
1421
+ }
1422
+
1423
+ async function sendClickToKiro(clickInfo) {
1424
+ if (!selectedCascadeId) return;
1425
+
1426
+ const isNavigation =
1427
+ clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1428
+ clickInfo.ariaLabel?.toLowerCase().includes('close') ||
1429
+ clickInfo.text?.toLowerCase().includes('back') ||
1430
+ clickInfo.role === 'tab';
1431
+
1432
+ const isModelSelection = clickInfo.isModelSelection ||
1433
+ clickInfo.role === 'option' ||
1434
+ clickInfo.role === 'menuitem' ||
1435
+ clickInfo.className?.includes('model');
1436
+
1437
+ if (isNavigation) navigationPending = true;
1438
+
1439
+ try {
1440
+ await fetch(`/click/${selectedCascadeId}`, {
1441
+ method: 'POST',
1442
+ headers: { 'Content-Type': 'application/json' },
1443
+ body: JSON.stringify(clickInfo)
1444
+ });
1445
+
1446
+ if (isNavigation || isModelSelection) {
1447
+ // Refresh snapshot after navigation or model selection
1448
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1449
+ }
1450
+ } catch (e) {
1451
+ navigationPending = false;
1452
+ }
1453
+ }
1454
+
1455
+ // Open file in Editor tab
1456
+ async function openFileInEditor(filePath) {
1457
+ if (!selectedCascadeId || !filePath) return;
1458
+
1459
+ showToast(`Opening ${filePath}...`, 1500);
1460
+
1461
+ // Switch to Editor tab immediately
1462
+ const editorTab = document.querySelector('[data-panel="editor"]');
1463
+ if (editorTab) {
1464
+ navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1465
+ editorTab.classList.add('active');
1466
+
1467
+ Object.values(panels).forEach(p => {
1468
+ if (p.panel) p.panel.classList.remove('active');
1469
+ });
1470
+ panels.editor.panel.classList.add('active');
1471
+ activePanel = 'editor';
1472
+
1473
+ // Show loading state
1474
+ showLoading('editor', 'Loading file...');
1475
+ }
1476
+
1477
+ try {
1478
+ // First, try to read the file directly from filesystem (bypasses Monaco limitation)
1479
+ const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
1480
+ method: 'POST',
1481
+ headers: { 'Content-Type': 'application/json' },
1482
+ body: JSON.stringify({ filePath })
1483
+ });
1484
+
1485
+ if (readResult.ok) {
1486
+ const data = await readResult.json();
1487
+ if (data.content) {
1488
+ renderEditorSnapshot(data);
1489
+ showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1490
+ console.log('[Editor] Loaded file directly:', data.fileName, data.lineCount, 'lines');
1491
+ return;
1492
+ }
1493
+ } else {
1494
+ const errorData = await readResult.json().catch(() => ({}));
1495
+ console.log('[Editor] Direct file read failed:', errorData.error || readResult.status);
1496
+ }
1497
+
1498
+ // Fallback: Try to click on the file link in Kiro to open it in Monaco
1499
+ console.log('[Editor] Falling back to Monaco capture...');
1500
+ const clickResult = await fetch(`/click/${selectedCascadeId}`, {
1501
+ method: 'POST',
1502
+ headers: { 'Content-Type': 'application/json' },
1503
+ body: JSON.stringify({
1504
+ tag: 'a',
1505
+ text: filePath,
1506
+ isFileLink: true,
1507
+ filePath: filePath
1508
+ })
1509
+ });
1510
+
1511
+ const result = await clickResult.json();
1512
+ console.log('Click result:', result);
1513
+
1514
+ // Wait for the file to open in Kiro
1515
+ await new Promise(resolve => setTimeout(resolve, 800));
1516
+
1517
+ // Fetch the editor content from Monaco
1518
+ const fetchEditor = async () => {
1519
+ try {
1520
+ const r = await fetch(`/editor/${selectedCascadeId}`);
1521
+ if (r.ok) {
1522
+ const data = await r.json();
1523
+ if (data.content || data.html) {
1524
+ renderEditorSnapshot(data);
1525
+ showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1526
+ return true;
1527
+ }
1528
+ }
1529
+ } catch (e) {}
1530
+ return false;
1531
+ };
1532
+
1533
+ // Try fetching a few times with delays
1534
+ if (!await fetchEditor()) {
1535
+ await new Promise(resolve => setTimeout(resolve, 500));
1536
+ if (!await fetchEditor()) {
1537
+ await new Promise(resolve => setTimeout(resolve, 500));
1538
+ if (!await fetchEditor()) {
1539
+ showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}. File may not exist or path is incorrect.`);
1540
+ }
1541
+ }
1542
+ }
1543
+
1544
+ } catch (e) {
1545
+ showToast('Failed to open file');
1546
+ console.error('Open file error:', e);
1547
+ showEmptyState('editor', 'codicon-file-code', 'Failed to load file. Check console for details.');
1548
+ }
1549
+ }
1550
+
1551
+ // ==================== File Tree ====================
1552
+
1553
+ // Toggle file tree panel
1554
+ function toggleFileTree() {
1555
+ fileTreeOpen = !fileTreeOpen;
1556
+ fileTreePanel.classList.toggle('open', fileTreeOpen);
1557
+
1558
+ if (fileTreeOpen) {
1559
+ // Load files if not cached
1560
+ if (workspaceFiles.length === 0) {
1561
+ fetchWorkspaceFiles();
1562
+ } else {
1563
+ renderFileTree(workspaceFiles);
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ // Close file tree
1569
+ function closeFileTree() {
1570
+ fileTreeOpen = false;
1571
+ fileTreePanel.classList.remove('open');
1572
+ }
1573
+
1574
+ // Fetch workspace files from server
1575
+ async function fetchWorkspaceFiles() {
1576
+ if (!selectedCascadeId) return;
1577
+
1578
+ fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1579
+
1580
+ try {
1581
+ const r = await fetch(`/files/${selectedCascadeId}`);
1582
+ if (!r.ok) throw new Error('Failed to fetch files');
1583
+
1584
+ const data = await r.json();
1585
+ workspaceFiles = data.files || [];
1586
+ console.log(`[FileTree] Loaded ${workspaceFiles.length} files`);
1587
+ renderFileTree(workspaceFiles);
1588
+ } catch (e) {
1589
+ console.error('Failed to fetch files:', e);
1590
+ fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1591
+ }
1592
+ }
1593
+
1594
+ // Build tree structure from flat file list
1595
+ function buildFileTree(files) {
1596
+ const root = { folders: {}, files: [] };
1597
+
1598
+ files.forEach(file => {
1599
+ const parts = file.path.split(/[/\\]/);
1600
+ let current = root;
1601
+
1602
+ // Navigate/create folder structure
1603
+ for (let i = 0; i < parts.length - 1; i++) {
1604
+ const folderName = parts[i];
1605
+ if (!current.folders[folderName]) {
1606
+ current.folders[folderName] = { folders: {}, files: [] };
1607
+ }
1608
+ current = current.folders[folderName];
1609
+ }
1610
+
1611
+ // Add file to current folder
1612
+ current.files.push({ name: parts[parts.length - 1], path: file.path });
1613
+ });
1614
+
1615
+ return root;
1616
+ }
1617
+
1618
+ // Get file icon based on extension
1619
+ function getFileIcon(name) {
1620
+ const ext = name.split('.').pop()?.toLowerCase();
1621
+ const iconMap = {
1622
+ 'js': 'codicon-file-code', 'jsx': 'codicon-file-code',
1623
+ 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1624
+ 'html': 'codicon-file-code', 'css': 'codicon-file-code',
1625
+ 'json': 'codicon-json', 'md': 'codicon-markdown',
1626
+ 'py': 'codicon-file-code', 'java': 'codicon-file-code',
1627
+ 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
1628
+ 'cob': 'codicon-file-code', 'cbl': 'codicon-file-code',
1629
+ 'sql': 'codicon-database', 'txt': 'codicon-file'
1630
+ };
1631
+ return iconMap[ext] || 'codicon-file';
1632
+ }
1633
+
1634
+ // Render tree node recursively
1635
+ function renderTreeNode(node, path = '', depth = 0) {
1636
+ let html = '';
1637
+
1638
+ // Sort folders first, then files
1639
+ const folderNames = Object.keys(node.folders).sort();
1640
+ const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
1641
+
1642
+ // Render folders
1643
+ folderNames.forEach(folderName => {
1644
+ const folderPath = path ? `${path}/${folderName}` : folderName;
1645
+ const isExpanded = expandedFolders.has(folderPath);
1646
+
1647
+ html += `
1648
+ <div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
1649
+ <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
1650
+ <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
1651
+ <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #dcb67a;"></span>
1652
+ <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
1653
+ </div>
1654
+ <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">
1655
+ ${renderTreeNode(node.folders[folderName], folderPath, depth + 1)}
1656
+ </div>
1657
+ </div>
1658
+ `;
1659
+ });
1660
+
1661
+ // Render files
1662
+ sortedFiles.forEach(file => {
1663
+ html += `
1664
+ <div class="file-tree-file" data-path="${escapeHtml(file.path)}">
1665
+ <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
1666
+ <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
1667
+ </div>
1668
+ `;
1669
+ });
1670
+
1671
+ return html;
1672
+ }
1673
+
1674
+ // Render file tree
1675
+ function renderFileTree(files) {
1676
+ if (files.length === 0) {
1677
+ fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>';
1678
+ return;
1679
+ }
1680
+
1681
+ // Build and render tree structure
1682
+ const tree = buildFileTree(files);
1683
+ fileTree.innerHTML = renderTreeNode(tree);
1684
+
1685
+ // Add click handlers for folders
1686
+ fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
1687
+ header.onclick = (e) => {
1688
+ e.stopPropagation();
1689
+ const folderPath = header.dataset.folder;
1690
+ const folder = header.closest('.file-tree-folder');
1691
+ const contents = folder.querySelector('.file-tree-folder-contents');
1692
+ const chevron = header.querySelector('.file-tree-folder-icon');
1693
+ const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
1694
+
1695
+ if (expandedFolders.has(folderPath)) {
1696
+ expandedFolders.delete(folderPath);
1697
+ contents.classList.remove('expanded');
1698
+ chevron.classList.remove('expanded');
1699
+ folderIcon.classList.remove('codicon-folder-opened');
1700
+ folderIcon.classList.add('codicon-folder');
1701
+ } else {
1702
+ expandedFolders.add(folderPath);
1703
+ contents.classList.add('expanded');
1704
+ chevron.classList.add('expanded');
1705
+ folderIcon.classList.remove('codicon-folder');
1706
+ folderIcon.classList.add('codicon-folder-opened');
1707
+ }
1708
+ };
1709
+ });
1710
+
1711
+ // Add click handlers for files
1712
+ fileTree.querySelectorAll('.file-tree-file').forEach(item => {
1713
+ item.onclick = () => {
1714
+ const path = item.dataset.path;
1715
+ closeFileTree();
1716
+ openFileInEditor(path);
1717
+ };
1718
+ });
1719
+ }
1720
+
1721
+ // Setup file tree event listeners
1722
+ function setupFileTree() {
1723
+ const explorerBtn = document.getElementById('editorExplorerBtn');
1724
+
1725
+ // Explorer button click toggles file tree
1726
+ explorerBtn.onclick = (e) => {
1727
+ e.stopPropagation();
1728
+ toggleFileTree();
1729
+ };
1730
+
1731
+ // Close button
1732
+ fileTreeClose.onclick = (e) => {
1733
+ e.stopPropagation();
1734
+ closeFileTree();
1735
+ };
1736
+
1737
+ // Prevent clicks inside panel from closing it
1738
+ fileTreePanel.onclick = (e) => {
1739
+ e.stopPropagation();
1740
+ };
1741
+
1742
+ // Close on escape key
1743
+ document.addEventListener('keydown', (e) => {
1744
+ if (e.key === 'Escape' && fileTreeOpen) {
1745
+ closeFileTree();
1746
+ }
1747
+ });
1748
+ }
1749
+
1750
+ // Initialize file tree
1751
+ setupFileTree();
1752
+
1753
+ // ==================== Editor Search ====================
1754
+
1755
+ function openEditorSearch() {
1756
+ editorSearchBar.classList.add('open');
1757
+ editorSearchInput.focus();
1758
+ editorSearchInput.select();
1759
+ }
1760
+
1761
+ function closeEditorSearch() {
1762
+ editorSearchBar.classList.remove('open');
1763
+ editorSearchInput.value = '';
1764
+ searchMatches = [];
1765
+ currentMatchIndex = -1;
1766
+ editorSearchCount.textContent = '';
1767
+ clearSearchHighlights();
1768
+ }
1769
+
1770
+ function clearSearchHighlights() {
1771
+ // Restore original HTML for lines that were modified
1772
+ const modifiedLines = panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]');
1773
+ modifiedLines.forEach(el => {
1774
+ el.innerHTML = el.dataset.originalHtml;
1775
+ delete el.dataset.originalHtml;
1776
+ });
1777
+ }
1778
+
1779
+ function performSearch(query) {
1780
+ clearSearchHighlights();
1781
+ searchMatches = [];
1782
+ currentMatchIndex = -1;
1783
+
1784
+ if (!query || query.length < 1) {
1785
+ editorSearchCount.textContent = '';
1786
+ return;
1787
+ }
1788
+
1789
+ const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
1790
+ const queryLower = query.toLowerCase();
1791
+
1792
+ codeElements.forEach((lineEl, lineIndex) => {
1793
+ const text = lineEl.textContent;
1794
+ const textLower = text.toLowerCase();
1795
+ let startIndex = 0;
1796
+ let matchIndex;
1797
+
1798
+ while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
1799
+ searchMatches.push({
1800
+ lineIndex,
1801
+ lineEl,
1802
+ startIndex: matchIndex,
1803
+ endIndex: matchIndex + query.length
1804
+ });
1805
+ startIndex = matchIndex + 1;
1806
+ }
1807
+ });
1808
+
1809
+ if (searchMatches.length > 0) {
1810
+ highlightMatches(query);
1811
+ currentMatchIndex = 0;
1812
+ scrollToMatch(0);
1813
+ updateSearchCount();
1814
+ } else {
1815
+ editorSearchCount.textContent = 'No results';
1816
+ }
1817
+ }
1818
+
1819
+ function highlightMatches(query) {
1820
+ const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
1821
+ const queryLower = query.toLowerCase();
1822
+
1823
+ codeElements.forEach(lineEl => {
1824
+ const text = lineEl.textContent;
1825
+ const textLower = text.toLowerCase();
1826
+
1827
+ if (textLower.includes(queryLower)) {
1828
+ // Store original HTML to restore later
1829
+ if (!lineEl.dataset.originalHtml) {
1830
+ lineEl.dataset.originalHtml = lineEl.innerHTML;
1831
+ }
1832
+
1833
+ // Work with text content, find all match positions
1834
+ const matches = [];
1835
+ let idx = 0;
1836
+ while ((idx = textLower.indexOf(queryLower, idx)) !== -1) {
1837
+ matches.push({ start: idx, end: idx + query.length });
1838
+ idx++;
1839
+ }
1840
+
1841
+ if (matches.length > 0) {
1842
+ // Rebuild the line with highlights
1843
+ let result = '';
1844
+ let lastEnd = 0;
1845
+
1846
+ matches.forEach(match => {
1847
+ // Add text before match (escaped)
1848
+ result += escapeHtml(text.slice(lastEnd, match.start));
1849
+ // Add highlighted match
1850
+ result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
1851
+ lastEnd = match.end;
1852
+ });
1853
+
1854
+ // Add remaining text
1855
+ result += escapeHtml(text.slice(lastEnd));
1856
+
1857
+ lineEl.innerHTML = result;
1858
+ }
1859
+ }
1860
+ });
1861
+ }
1862
+
1863
+ function scrollToMatch(index) {
1864
+ const highlights = panels.editor.content.querySelectorAll('.search-highlight');
1865
+
1866
+ // Remove current class from all
1867
+ highlights.forEach(el => el.classList.remove('current'));
1868
+
1869
+ if (highlights[index]) {
1870
+ highlights[index].classList.add('current');
1871
+
1872
+ // Get the scroll container and the highlight element
1873
+ const container = panels.editor.content;
1874
+ const element = highlights[index];
1875
+
1876
+ // Get the line element (parent of the highlight)
1877
+ const lineEl = element.closest('.editor-line');
1878
+ if (lineEl) {
1879
+ // Calculate scroll position to center the line
1880
+ const containerRect = container.getBoundingClientRect();
1881
+ const lineRect = lineEl.getBoundingClientRect();
1882
+ const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
1883
+
1884
+ container.scrollTo({
1885
+ top: Math.max(0, scrollTop),
1886
+ behavior: 'smooth'
1887
+ });
1888
+ }
1889
+ }
1890
+ }
1891
+
1892
+ function updateSearchCount() {
1893
+ if (searchMatches.length > 0) {
1894
+ editorSearchCount.textContent = `${currentMatchIndex + 1}/${searchMatches.length}`;
1895
+ } else {
1896
+ editorSearchCount.textContent = 'No results';
1897
+ }
1898
+ }
1899
+
1900
+ function goToNextMatch() {
1901
+ if (searchMatches.length === 0) return;
1902
+ currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
1903
+ scrollToMatch(currentMatchIndex);
1904
+ updateSearchCount();
1905
+ }
1906
+
1907
+ function goToPrevMatch() {
1908
+ if (searchMatches.length === 0) return;
1909
+ currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
1910
+ scrollToMatch(currentMatchIndex);
1911
+ updateSearchCount();
1912
+ }
1913
+
1914
+ // Setup editor search
1915
+ function setupEditorSearch() {
1916
+ editorSearchBtn.onclick = () => openEditorSearch();
1917
+ editorSearchClose.onclick = () => closeEditorSearch();
1918
+ editorSearchNext.onclick = () => goToNextMatch();
1919
+ editorSearchPrev.onclick = () => goToPrevMatch();
1920
+
1921
+ editorSearchInput.oninput = (e) => {
1922
+ performSearch(e.target.value);
1923
+ };
1924
+
1925
+ editorSearchInput.onkeydown = (e) => {
1926
+ if (e.key === 'Enter') {
1927
+ e.preventDefault();
1928
+ if (e.shiftKey) {
1929
+ goToPrevMatch();
1930
+ } else {
1931
+ goToNextMatch();
1932
+ }
1933
+ } else if (e.key === 'Escape') {
1934
+ closeEditorSearch();
1935
+ }
1936
+ };
1937
+
1938
+ // Ctrl+F / Cmd+F to open search
1939
+ document.addEventListener('keydown', (e) => {
1940
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') {
1941
+ e.preventDefault();
1942
+ openEditorSearch();
1943
+ }
1944
+ });
1945
+ }
1946
+
1947
+ setupEditorSearch();
1948
+
1949
+ // Visibility change handler
1950
+ document.addEventListener('visibilitychange', () => {
1951
+ if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
1952
+ connect();
1953
+ }
1954
+ });
1955
+
1956
+ // Initialize
1957
+ connect();
1958
+ </script>
1959
+ </body>
1960
+ </html>