kiro-mobile-bridge 1.0.1 → 1.0.5

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