kiro-mobile-bridge 1.0.6 → 1.0.8

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.
@@ -9,234 +9,213 @@
9
9
  <title>Kiro Mobile Bridge</title>
10
10
  <link rel="stylesheet" href="https://unpkg.com/@vscode/codicons@0.0.35/dist/codicon.css">
11
11
  <style>
12
+ /* =============================================================================
13
+ Base Styles
14
+ ============================================================================= */
12
15
  *, *::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; }
16
+ html, body {
17
+ height: 100%;
18
+ overflow: hidden;
19
+ background: #1e1e1e;
20
+ font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, sans-serif;
21
+ font-size: 13px;
22
+ color: #fff;
23
+ }
14
24
  .app { display: flex; flex-direction: column; height: 100%; height: 100dvh; }
15
25
 
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; }
26
+ /* =============================================================================
27
+ Navigation Bar
28
+ ============================================================================= */
29
+ .nav-bar {
30
+ display: flex;
31
+ align-items: center;
32
+ background: #1e1e1e;
33
+ border-bottom: 1px solid #3c3c3c;
34
+ padding: 0 8px;
35
+ flex-shrink: 0;
36
+ }
18
37
  .status { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 10px 8px; }
19
38
  .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #666; }
20
39
  .status-dot.connected { background: #4caf50; }
21
40
  .status-dot.disconnected { background: #f44336; }
22
- .status-text { color: #888; display: none; }
23
41
  .nav-tabs { display: flex; align-items: center; flex: 1; overflow-x: auto; -webkit-overflow-scrolling: touch; }
24
42
  .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; }
43
+ .nav-tab {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 6px;
47
+ padding: 10px 16px;
48
+ color: #888;
49
+ font-size: 12px;
50
+ font-weight: 500;
51
+ cursor: pointer;
52
+ border-bottom: 2px solid transparent;
53
+ transition: all 0.2s ease;
54
+ white-space: nowrap;
55
+ user-select: none;
56
+ }
27
57
  .nav-tab:hover { color: #ccc; background: rgba(255,255,255,0.05); }
28
58
  .nav-tab.active { color: #fff; border-bottom-color: #7138cc; }
29
59
  .nav-tab .codicon { font-size: 14px; }
30
60
 
31
- /* Panel Container */
61
+ /* =============================================================================
62
+ Panel Container
63
+ ============================================================================= */
32
64
  .panel-container { flex: 1; overflow: hidden; position: relative; }
33
65
  .panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; display: none; }
34
66
  .panel.active { display: flex; flex-direction: column; }
35
67
 
36
- /* Chat Panel */
68
+ /* =============================================================================
69
+ Chat Panel
70
+ ============================================================================= */
37
71
  .chat-container { flex: 1; overflow: hidden; position: relative; }
38
72
  .chat-content { width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }
39
73
  #chatContent, #chatContent * { font-family: inherit; }
40
74
  #chatContent code, #chatContent pre { font-family: Menlo, Monaco, "Courier New", monospace !important; }
41
75
  #chatContent .codicon { font-family: codicon !important; font-size: 16px; line-height: 1; }
42
76
 
43
- /* Editor Panel */
77
+ /* =============================================================================
78
+ Editor Panel
79
+ ============================================================================= */
44
80
  .editor-container { flex: 1; overflow: hidden; background: #1e1e1e; display: flex; flex-direction: column; position: relative; }
45
81
  .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; }
82
+ .editor-filename { font-size: 13px; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; }
47
83
  .editor-search-btn { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
48
84
  .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; }
85
+ .editor-explorer-btn { display: flex; align-items: center; gap: 6px; color: #a78bfa; font-size: 14px; cursor: pointer; padding: 4px 10px; border-radius: 6px; background: rgba(113, 56, 204, 0.15); border: 1px solid rgba(113, 56, 204, 0.3); }
86
+ .editor-explorer-btn:hover { background: rgba(113, 56, 204, 0.25); border-color: rgba(113, 56, 204, 0.5); }
87
+ .editor-explorer-text { font-size: 11px; font-weight: 600; color: #a78bfa; letter-spacing: 0.5px; }
88
+ .editor-explorer-indicator { font-size: 10px; color: #888; margin-left: 2px; transition: transform 0.2s ease; }
52
89
  .editor-search-bar { display: none; align-items: center; gap: 8px; padding: 6px 12px; background: #252526; border-bottom: 1px solid #3c3c3c; }
53
90
  .editor-search-bar.open { display: flex; }
54
91
  .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
92
  .editor-search-bar input:focus { border-color: #007acc; }
56
93
  .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); }
94
+ .editor-search-nav, .editor-search-close { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
95
+ .editor-search-nav:hover, .editor-search-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
61
96
  .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', 'JetBrains Mono', 'SF Mono', Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; font-size: 14px; line-height: 1.6; color: #d4d4d4; background: #1e1e1e; }
63
- .editor-line { display: flex; min-height: 1.6em; }
97
+ .editor-code { margin: 0; padding: 8px 0; font-family: Consolas, 'Courier New', monospace; font-size: 12px; line-height: 1.5; color: #d4d4d4; background: #1e1e1e; }
98
+ .editor-line { display: flex; min-height: 18px; }
64
99
  .editor-line:hover { background: rgba(255,255,255,0.04); }
65
- .editor-line-num { width: 50px; min-width: 50px; text-align: right; padding-right: 16px; color: #858585; user-select: none; flex-shrink: 0; font-size: 14px; font-family: inherit; }
66
- .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; overflow-x: auto; tab-size: 2; font-family: inherit; }
100
+ .editor-line-num { width: 50px; min-width: 50px; text-align: right; padding-right: 16px; color: #858585; user-select: none; flex-shrink: 0; font-size: 12px; }
101
+ .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; overflow-x: auto; tab-size: 2; }
67
102
  .search-highlight { background: #ffd500; color: #000; border-radius: 2px; padding: 0 1px; }
68
103
  .search-highlight.current { background: #ff6b00; color: #fff; outline: 2px solid #ff6b00; }
69
104
 
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; }
105
+ /* =============================================================================
106
+ Tasks Panel
107
+ ============================================================================= */
108
+ .tasks-container { flex: 1; overflow: hidden; background: #0d0d0d; display: flex; flex-direction: column; }
109
+ .tasks-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: #0d0d0d; flex-shrink: 0; }
110
+ .tasks-dropdown { flex: 1; padding: 10px 14px; background: #1a1a1a; border: none; border-radius: 10px; color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; outline: none; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }
111
+ .tasks-dropdown option { background: #1a1a1a; color: #fff; }
112
+ .tasks-content { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0 16px 24px; }
113
+ .tasks-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #444; text-align: center; padding: 32px; }
114
+ .tasks-empty .codicon { font-size: 40px; margin-bottom: 16px; }
115
+ .task-md { font-size: 14px; }
116
+
117
+ /* Task Groups */
118
+ .task-group { background: #141414; border-radius: 16px; margin-bottom: 6px; overflow: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
119
+ .task-group.expanded { background: #181818; box-shadow: 0 0 0 1px rgba(60, 60, 60, 0.5), 0 8px 32px rgba(0,0,0,0.4); }
120
+ .task-group.expanded.completed { box-shadow: 0 0 0 1px rgba(113, 56, 204, 0.4), 0 8px 32px rgba(0,0,0,0.4); }
121
+ .task-group-header { display: flex; align-items: center; gap: 12px; padding: 14px 16px; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
122
+ .task-group-indicator { width: 24px; height: 24px; min-width: 24px; border-radius: 50%; background: #2a2a2a; display: flex; align-items: center; justify-content: center; }
123
+ .task-group-indicator svg { display: none; width: 14px; height: 14px; color: #fff; }
124
+ .task-group.completed .task-group-indicator { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); }
125
+ .task-group.completed .task-group-indicator svg { display: block; }
126
+ .task-group-number { color: #666; font-size: 14px; font-weight: 700; width: 24px; text-align: center; }
127
+ .task-group.expanded .task-group-number { color: #ccc; }
128
+ .task-group.completed .task-group-number { color: #a855f7; }
129
+ .task-group-title { font-size: 14px; font-weight: 500; color: #e5e5e5; flex: 1; line-height: 1.4; }
130
+ .task-group.completed .task-group-title { color: #888; }
131
+ .task-group-meta { display: flex; align-items: center; gap: 8px; }
132
+ .task-group-count { font-size: 12px; font-weight: 600; color: #444; padding: 4px 10px; background: #1f1f1f; border-radius: 16px; }
133
+ .task-group.completed .task-group-count { color: #a855f7; background: rgba(124, 58, 237, 0.15); }
134
+ .task-group-arrow { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: #444; transition: transform 0.3s ease; }
135
+ .task-group-arrow svg { width: 12px; height: 12px; }
136
+ .task-group.expanded .task-group-arrow { transform: rotate(90deg); color: #888; }
137
+ .task-group-body { max-height: 0; overflow: hidden; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
138
+ .task-group.expanded .task-group-body { max-height: 2000px; }
139
+ .task-group-body-inner { padding: 0 16px 12px 52px; }
140
+
141
+ /* Task Items */
142
+ .task-item { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
143
+ .task-item:last-child { border-bottom: none; }
144
+ .task-checkbox { width: 18px; height: 18px; min-width: 18px; border: 2px solid #444; border-radius: 5px; display: flex; align-items: center; justify-content: center; background: transparent; }
145
+ .task-checkbox.checked { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); border-color: transparent; }
146
+ .task-checkbox.checked svg { display: block; }
147
+ .task-checkbox svg { display: none; width: 10px; height: 10px; color: #fff; }
148
+ .task-text { flex: 1; font-size: 13px; line-height: 1.4; color: #999; padding-top: 1px; }
149
+ .task-text.completed { color: #555; }
150
+ .task-item.indent-2 { padding-left: 20px; }
151
+ .task-item.indent-3 { padding-left: 40px; }
152
+
153
+ /* =============================================================================
154
+ File Tree Dropdown (under Explorer button)
155
+ ============================================================================= */
156
+ .file-tree-dropdown {
157
+ position: absolute;
158
+ top: 100%;
159
+ left: 0;
160
+ width: 280px;
161
+ max-height: 400px;
162
+ background: #1e1e1e;
163
+ border: 1px solid #3c3c3c;
164
+ border-radius: 6px;
165
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
166
+ display: none;
167
+ flex-direction: column;
168
+ z-index: 100;
169
+ margin-top: 4px;
170
+ }
171
+ .file-tree-dropdown.open { display: flex; }
77
172
  .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); }
173
+ .file-tree-folder-header { display: flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: pointer; user-select: none; }
174
+ .file-tree-folder-header:hover { background: rgba(255,255,255,0.08); }
80
175
  .file-tree-folder-icon { color: #888; font-size: 14px; transition: transform 0.15s; }
81
176
  .file-tree-folder-icon.expanded { transform: rotate(90deg); }
82
177
  .file-tree-folder-name { font-size: 13px; color: #c5c5c5; }
83
178
  .file-tree-folder-contents { padding-left: 16px; display: none; }
84
179
  .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); }
180
+ .file-tree-file { display: flex; align-items: center; gap: 6px; padding: 6px 10px 6px 26px; cursor: pointer; }
181
+ .file-tree-file:hover { background: rgba(255,255,255,0.08); }
87
182
  .file-tree-file:active { background: #094771; }
88
183
  .file-tree-file-icon { color: #888; font-size: 14px; }
89
184
  .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 - Kiro Dark Theme */
94
- .token-keyword { color: #c586c0; font-weight: normal; }
95
- .token-string { color: #ce9178; }
96
- .token-number { color: #b5cea8; }
97
- .token-comment { color: #6a9955; font-style: italic; }
98
- .token-function { color: #dcdcaa; }
99
- .token-type { color: #4ec9b0; }
100
- .token-operator { color: #d4d4d4; }
101
- .token-property { color: #9cdcfe; }
102
- .token-variable { color: #9cdcfe; }
103
- .token-constant { color: #4fc1ff; }
104
- .token-class { color: #4ec9b0; }
105
- .token-punctuation { color: #d4d4d4; }
106
- .token-tag { color: #569cd6; }
107
- .token-attr-name { color: #9cdcfe; }
108
- .token-attr-value { color: #ce9178; }
109
- /* Kiro theme token classes */
110
- .tok-kw { color: #c586c0; } /* Keywords - purple/pink */
111
- .tok-str { color: #ce9178; } /* Strings - orange */
112
- .tok-num { color: #b5cea8; } /* Numbers - light green */
113
- .tok-cmt { color: #6a9955; font-style: italic; } /* Comments - green italic */
114
- .tok-fn { color: #dcdcaa; } /* Functions - yellow */
115
- .tok-type { color: #4ec9b0; } /* Types - teal */
116
- /* Syntax Highlighting */
117
- .token-keyword { color: #569cd6; font-weight: 500; }
118
- .token-string { color: #ce9178; }
119
- .token-number { color: #b5cea8; }
120
- .token-comment { color: #6a9955; font-style: italic; }
121
- .token-function { color: #dcdcaa; }
122
- .token-type { color: #4ec9b0; }
123
- .token-operator { color: #d4d4d4; }
124
-
125
- /* Loading & Toast */
185
+ .file-tree-empty, .file-tree-loading { padding: 20px; text-align: center; color: #666; font-size: 13px; }
186
+ .file-tree-loading { color: #888; display: flex; flex-direction: column; align-items: center; gap: 8px; }
187
+ .editor-explorer-wrapper { position: relative; }
188
+
189
+ /* =============================================================================
190
+ Syntax Highlighting (Kiro Dark Theme)
191
+ ============================================================================= */
192
+ .tok-kw { color: #c586c0; }
193
+ .tok-str { color: #ce9178; }
194
+ .tok-num { color: #b5cea8; }
195
+ .tok-cmt { color: #6a9955; font-style: italic; }
196
+ .tok-fn { color: #dcdcaa; }
197
+ .tok-type { color: #4ec9b0; }
198
+
199
+ /* =============================================================================
200
+ Loading & Toast
201
+ ============================================================================= */
126
202
  .loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #888; font-size: 14px; flex-direction: column; gap: 12px; }
127
203
  .loading-spinner { width: 24px; height: 24px; border: 2px solid #3c3c3c; border-top-color: #7138cc; border-radius: 50%; animation: spin 1s linear infinite; }
128
204
  @keyframes spin { to { transform: rotate(360deg); } }
129
-
130
205
  .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; }
131
206
  .toast.visible { opacity: 1; }
132
207
  .toast.success { background: #4caf50; }
133
208
 
134
- /* Empty State */
209
+ /* =============================================================================
210
+ Empty State
211
+ ============================================================================= */
135
212
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #888; text-align: center; padding: 20px; }
136
213
  .empty-state .codicon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
137
214
  .empty-state p { font-size: 14px; max-width: 280px; }
138
-
139
- /* Task List Styling - Match Kiro IDE */
140
- [class*="task"], [class*="Task"] {
141
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
142
- }
143
-
144
- /* Task section headers (CURRENT TASKS, TASKS IN QUEUE) */
145
- [class*="task"] h2, [class*="Task"] h2,
146
- [class*="task"] h3, [class*="Task"] h3,
147
- [class*="task-header"], [class*="TaskHeader"],
148
- [class*="section-title"], [class*="SectionTitle"] {
149
- font-size: 13px !important;
150
- font-weight: 700 !important;
151
- letter-spacing: 0.5px !important;
152
- text-transform: uppercase !important;
153
- color: #ffffff !important;
154
- margin: 0 0 12px 0 !important;
155
- padding: 0 !important;
156
- line-height: 1.4 !important;
157
- }
158
-
159
- /* Task list containers */
160
- [class*="task-list"], [class*="TaskList"],
161
- [class*="task-section"], [class*="TaskSection"] {
162
- margin-bottom: 24px !important;
163
- padding: 0 !important;
164
- }
165
-
166
- /* Empty state messages (No active tasks, No tasks in queue) */
167
- [class*="task"] p:empty, [class*="Task"] p:empty,
168
- [class*="no-task"], [class*="NoTask"],
169
- [class*="empty-task"], [class*="EmptyTask"],
170
- [class*="task-empty"], [class*="TaskEmpty"] {
171
- font-size: 13px !important;
172
- font-style: italic !important;
173
- color: #888888 !important;
174
- margin: 0 0 16px 0 !important;
175
- padding: 0 !important;
176
- line-height: 1.6 !important;
177
- }
178
-
179
- /* Catch all paragraphs under task sections that look like empty states */
180
- [class*="task"] p, [class*="Task"] p {
181
- font-size: 13px !important;
182
- line-height: 1.6 !important;
183
- }
184
-
185
- /* Specific targeting for common patterns */
186
- div[class*="current"] h2, div[class*="Current"] h2,
187
- div[class*="queue"] h2, div[class*="Queue"] h2 {
188
- font-size: 13px !important;
189
- font-weight: 700 !important;
190
- letter-spacing: 0.5px !important;
191
- text-transform: uppercase !important;
192
- color: #ffffff !important;
193
- }
194
-
195
- /* Horizontal divider between sections */
196
- [class*="task-divider"], [class*="TaskDivider"],
197
- [class*="task"] hr, [class*="Task"] hr {
198
- border: none !important;
199
- border-top: 1px solid #3c3c3c !important;
200
- margin: 20px 0 !important;
201
- }
202
-
203
- /* Individual task items (when they exist) */
204
- [class*="task-item"], [class*="TaskItem"] {
205
- padding: 8px 12px !important;
206
- margin: 4px 0 !important;
207
- background: rgba(255, 255, 255, 0.03) !important;
208
- border-radius: 4px !important;
209
- border-left: 2px solid #7138cc !important;
210
- font-size: 13px !important;
211
- color: #cccccc !important;
212
- line-height: 1.5 !important;
213
- }
214
-
215
- [class*="task-item"]:hover, [class*="TaskItem"]:hover {
216
- background: rgba(255, 255, 255, 0.05) !important;
217
- }
218
-
219
- /* Task item with close button */
220
- [class*="task-item"] button, [class*="TaskItem"] button {
221
- margin-left: 8px !important;
222
- padding: 2px 6px !important;
223
- font-size: 11px !important;
224
- background: rgba(255, 255, 255, 0.1) !important;
225
- border: 1px solid #4a464f !important;
226
- border-radius: 3px !important;
227
- color: #888 !important;
228
- cursor: pointer !important;
229
- }
230
-
231
- [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
232
- background: rgba(255, 255, 255, 0.15) !important;
233
- color: #fff !important;
234
- }
235
215
  </style>
236
216
  </head>
237
217
  <body>
238
218
  <div class="app">
239
- <!-- Navigation Bar (tabs | title + indicator) -->
240
219
  <nav class="nav-bar">
241
220
  <div class="nav-tabs" id="navTabs">
242
221
  <div class="nav-tab active" data-panel="chat">
@@ -245,17 +224,18 @@
245
224
  </div>
246
225
  <div class="nav-tab" data-panel="editor">
247
226
  <span class="codicon codicon-code"></span>
248
- <span>Editor</span>
227
+ <span>Code</span>
228
+ </div>
229
+ <div class="nav-tab" data-panel="tasks">
230
+ <span class="codicon codicon-checklist"></span>
231
+ <span>Tasks</span>
249
232
  </div>
250
233
  </div>
251
- <span class="header-title">Kiro Mobile</span>
252
234
  <div class="status">
253
235
  <span class="status-dot" id="statusDot"></span>
254
- <span class="status-text" id="statusText">Connecting...</span>
255
236
  </div>
256
237
  </nav>
257
238
 
258
- <!-- Panel Container -->
259
239
  <div class="panel-container">
260
240
  <!-- Chat Panel -->
261
241
  <div class="panel active" id="chatPanel">
@@ -268,251 +248,214 @@
268
248
  <!-- Editor Panel -->
269
249
  <div class="panel" id="editorPanel">
270
250
  <div class="editor-container">
271
- <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading editor...</span></div>
251
+ <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading code...</span></div>
272
252
  <div class="editor-header" id="editorHeader" style="display: none;">
253
+ <div class="editor-explorer-wrapper">
254
+ <div class="editor-explorer-btn" id="editorExplorerBtn" title="Browse and select files">
255
+ <span class="codicon codicon-folder" style="color: #a78bfa;"></span>
256
+ <span class="editor-explorer-text">EXPLORER</span>
257
+ <span class="codicon codicon-chevron-down editor-explorer-indicator" id="explorerChevron"></span>
258
+ </div>
259
+ <div class="file-tree-dropdown" id="fileTreeDropdown">
260
+ <div class="file-tree" id="fileTree">
261
+ <div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>
262
+ </div>
263
+ </div>
264
+ </div>
273
265
  <span class="codicon codicon-file-code" style="color: #888;"></span>
274
266
  <span class="editor-filename" id="editorFilename">No file open</span>
275
- <span class="editor-search-btn codicon codicon-search" id="editorSearchBtn" title="Find in file"></span>
276
- <div class="editor-explorer-btn" id="editorExplorerBtn" title="Open Explorer">
277
- <span class="codicon codicon-folder" style="color: #dcb67a;"></span>
278
- <span class="editor-explorer-text">EXPLORER</span>
279
- </div>
267
+ <div style="flex: 1;"></div>
268
+ <span class="editor-search-btn codicon codicon-search" id="editorSearchBtn" title="Find"></span>
280
269
  </div>
281
270
  <div class="editor-search-bar" id="editorSearchBar">
282
- <input type="text" id="editorSearchInput" placeholder="Find in file..." autocomplete="off" />
271
+ <input type="text" id="editorSearchInput" placeholder="Find..." autocomplete="off" />
283
272
  <span class="editor-search-count" id="editorSearchCount"></span>
284
- <span class="editor-search-nav codicon codicon-arrow-up" id="editorSearchPrev" title="Previous"></span>
285
- <span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext" title="Next"></span>
286
- <span class="editor-search-close codicon codicon-close" id="editorSearchClose" title="Close"></span>
287
- </div>
288
- <div class="file-tree-panel" id="fileTreePanel">
289
- <div class="file-tree-header">
290
- <span class="codicon codicon-folder-opened" style="color: #dcb67a;"></span>
291
- <span class="file-tree-header-title">Explorer</span>
292
- <span class="file-tree-close codicon codicon-close" id="fileTreeClose"></span>
293
- </div>
294
- <div class="file-tree" id="fileTree">
295
- <div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>
296
- </div>
273
+ <span class="editor-search-nav codicon codicon-arrow-up" id="editorSearchPrev"></span>
274
+ <span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext"></span>
275
+ <span class="editor-search-close codicon codicon-close" id="editorSearchClose"></span>
297
276
  </div>
298
277
  <div class="editor-content" id="editorContent" style="display: none;"></div>
299
278
  </div>
300
279
  </div>
280
+
281
+ <!-- Tasks Panel -->
282
+ <div class="panel" id="tasksPanel">
283
+ <div class="tasks-container">
284
+ <div class="loading" id="tasksLoading"><div class="loading-spinner"></div><span>Loading tasks...</span></div>
285
+ <div class="tasks-header" id="tasksHeader" style="display: none;">
286
+ <span class="codicon codicon-checklist" style="color: #7138cc; font-size: 16px;"></span>
287
+ <select class="tasks-dropdown" id="tasksDropdown">
288
+ <option value="">Select a spec...</option>
289
+ </select>
290
+ </div>
291
+ <div class="tasks-content" id="tasksContent" style="display: none;"></div>
292
+ </div>
293
+ </div>
301
294
  </div>
302
295
 
303
296
  <div class="toast" id="toast"></div>
304
297
  </div>
305
298
 
306
299
  <script>
300
+ // =============================================================================
307
301
  // State
302
+ // =============================================================================
308
303
  let ws = null, reconnectAttempts = 0, reconnectTimeout = null;
309
304
  let cascades = [], selectedCascadeId = null, currentStyles = null;
310
- let isTypingInKiroInput = false;
311
- let activePanel = 'chat';
312
- let lastSuccessfulSnapshot = null;
313
- let navigationPending = false;
314
- let workspaceFiles = []; // Cached file list
315
- let fileTreeOpen = false;
316
- let expandedFolders = new Set(); // Track expanded folders
317
- let updateDebounceTimer = null; // Debounce rapid snapshot updates
318
- let isRendering = false; // Prevent concurrent renders
305
+ let isTypingInKiroInput = false, activePanel = 'chat';
306
+ let lastSuccessfulSnapshot = null, navigationPending = false;
307
+ let workspaceFiles = [], fileTreeOpen = false, expandedFolders = new Set();
308
+ let updateDebounceTimer = null, isRendering = false;
309
+ let tasksData = [], selectedTaskIndex = -1, tasksPollingTimer = null, lastTasksHash = '';
310
+ let searchMatches = [], currentMatchIndex = -1;
319
311
 
312
+ // =============================================================================
320
313
  // DOM Elements
321
- const statusDot = document.getElementById('statusDot');
322
- const statusText = document.getElementById('statusText');
323
- const navTabs = document.getElementById('navTabs');
324
- const toast = document.getElementById('toast');
325
- const fileTreePanel = document.getElementById('fileTreePanel');
326
- const fileTree = document.getElementById('fileTree');
327
- const fileTreeClose = document.getElementById('fileTreeClose');
328
-
329
- // Panel elements
314
+ // =============================================================================
315
+ const $ = id => document.getElementById(id);
316
+ const statusDot = $('statusDot');
317
+ const navTabs = $('navTabs');
318
+ const toast = $('toast');
319
+ const fileTreeDropdown = $('fileTreeDropdown');
320
+ const fileTree = $('fileTree');
321
+ const explorerChevron = $('explorerChevron');
322
+
330
323
  const panels = {
331
- chat: {
332
- panel: document.getElementById('chatPanel'),
333
- loading: document.getElementById('chatLoading'),
334
- content: document.getElementById('chatContent'),
335
- container: document.getElementById('chatContainer')
336
- },
337
- editor: {
338
- panel: document.getElementById('editorPanel'),
339
- loading: document.getElementById('editorLoading'),
340
- content: document.getElementById('editorContent'),
341
- header: document.getElementById('editorHeader'),
342
- filename: document.getElementById('editorFilename')
343
- }
324
+ chat: { panel: $('chatPanel'), loading: $('chatLoading'), content: $('chatContent'), container: $('chatContainer') },
325
+ editor: { panel: $('editorPanel'), loading: $('editorLoading'), content: $('editorContent'), header: $('editorHeader'), filename: $('editorFilename') },
326
+ tasks: { panel: $('tasksPanel'), loading: $('tasksLoading'), content: $('tasksContent'), header: $('tasksHeader'), dropdown: $('tasksDropdown') }
344
327
  };
345
328
 
346
- // Search state
347
- let searchMatches = [];
348
- let currentMatchIndex = -1;
349
- let originalEditorContent = '';
350
-
351
- // Search DOM elements
352
- const editorSearchBtn = document.getElementById('editorSearchBtn');
353
- const editorSearchBar = document.getElementById('editorSearchBar');
354
- const editorSearchInput = document.getElementById('editorSearchInput');
355
- const editorSearchCount = document.getElementById('editorSearchCount');
356
- const editorSearchPrev = document.getElementById('editorSearchPrev');
357
- const editorSearchNext = document.getElementById('editorSearchNext');
358
- const editorSearchClose = document.getElementById('editorSearchClose');
359
-
360
- // Status management
329
+ // =============================================================================
330
+ // Utilities
331
+ // =============================================================================
361
332
  function setStatus(status) {
362
333
  statusDot.className = 'status-dot ' + status;
363
- statusText.textContent = status === 'connected' ? 'Connected' : status === 'disconnected' ? 'Disconnected' : 'Connecting...';
364
334
  }
365
-
335
+
366
336
  function showToast(message, duration = 3000, isSuccess = false) {
367
337
  toast.textContent = message;
368
338
  toast.className = 'toast visible' + (isSuccess ? ' success' : '');
369
- setTimeout(() => { toast.classList.remove('visible'); }, duration);
339
+ setTimeout(() => toast.classList.remove('visible'), duration);
370
340
  }
371
-
341
+
342
+ function escapeHtml(text) {
343
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
344
+ }
345
+
346
+ function showLoading(panelName, msg = 'Loading...') {
347
+ const p = panels[panelName];
348
+ if (p.loading) { p.loading.querySelector('span').textContent = msg; p.loading.style.display = 'flex'; }
349
+ if (p.content) p.content.style.display = 'none';
350
+ if (p.header) p.header.style.display = 'none';
351
+ }
352
+
353
+ function hideLoading(panelName) {
354
+ const p = panels[panelName];
355
+ if (p.loading) p.loading.style.display = 'none';
356
+ if (p.content) p.content.style.display = 'block';
357
+ if (p.header) p.header.style.display = 'flex';
358
+ }
359
+
360
+ function showEmptyState(panelName, icon, message) {
361
+ const p = panels[panelName];
362
+ p.content.innerHTML = `<div class="empty-state"><span class="codicon ${icon}"></span><p>${message}</p></div>`;
363
+ hideLoading(panelName);
364
+ }
365
+
366
+ // =============================================================================
372
367
  // Navigation
368
+ // =============================================================================
373
369
  navTabs.addEventListener('click', (e) => {
374
370
  const tab = e.target.closest('.nav-tab');
375
- if (!tab) return;
371
+ if (!tab || tab.dataset.panel === activePanel) return;
376
372
 
377
- const panelName = tab.dataset.panel;
378
- if (panelName === activePanel) return;
373
+ if (activePanel === 'tasks') stopTasksPolling();
379
374
 
380
- // Update tabs
381
375
  navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
382
376
  tab.classList.add('active');
383
377
 
384
- // Update panels
385
- Object.keys(panels).forEach(key => {
386
- panels[key].panel.classList.toggle('active', key === panelName);
387
- });
388
-
389
- activePanel = panelName;
378
+ Object.keys(panels).forEach(key => panels[key].panel.classList.toggle('active', key === tab.dataset.panel));
379
+ activePanel = tab.dataset.panel;
390
380
 
391
- // Fetch content for the new panel
392
- if (selectedCascadeId) {
393
- fetchPanelContent(panelName, selectedCascadeId);
394
- }
381
+ if (selectedCascadeId) fetchPanelContent(activePanel, selectedCascadeId);
395
382
  });
396
-
397
- // WebSocket connection
383
+
384
+ // =============================================================================
385
+ // WebSocket Connection
386
+ // =============================================================================
398
387
  function connect() {
399
388
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
400
389
  ws = new WebSocket(`${protocol}//${window.location.host}`);
401
390
 
402
- ws.onopen = () => {
403
- setStatus('connected');
404
- reconnectAttempts = 0;
405
- };
406
-
407
- ws.onmessage = (e) => {
408
- try {
409
- handleMessage(JSON.parse(e.data));
410
- } catch (err) {
411
- console.error('Failed to parse message:', err);
412
- }
413
- };
414
-
415
- ws.onclose = () => {
416
- setStatus('disconnected');
417
- scheduleReconnect();
418
- };
419
-
391
+ ws.onopen = () => { setStatus('connected'); reconnectAttempts = 0; };
392
+ ws.onmessage = (e) => { try { handleMessage(JSON.parse(e.data)); } catch (err) {} };
393
+ ws.onclose = () => { setStatus('disconnected'); scheduleReconnect(); };
420
394
  ws.onerror = () => {};
421
395
  }
422
-
396
+
423
397
  function scheduleReconnect() {
424
398
  if (reconnectTimeout) clearTimeout(reconnectTimeout);
425
399
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
426
400
  reconnectAttempts++;
427
401
  reconnectTimeout = setTimeout(connect, delay);
428
402
  }
429
-
403
+
430
404
  function handleMessage(data) {
431
405
  if (data.type === 'cascade_list') {
432
406
  cascades = data.cascades || [];
433
- if (!selectedCascadeId && cascades.length > 0) {
434
- selectCascade(cascades[0].id);
435
- }
407
+ if (!selectedCascadeId && cascades.length > 0) selectCascade(cascades[0].id);
436
408
  } else if (data.type === 'snapshot_update' && data.cascadeId === selectedCascadeId) {
437
- // Only fetch if user is not typing AND the updated panel is the active one
438
- // AND we're not currently rendering (prevent flickering from rapid updates)
439
409
  const panel = data.panel || 'chat';
440
410
  if (!isTypingInKiroInput && panel === activePanel && !isRendering) {
441
- // Debounce rapid updates - wait 500ms before fetching
442
411
  if (updateDebounceTimer) clearTimeout(updateDebounceTimer);
443
- updateDebounceTimer = setTimeout(() => {
444
- fetchPanelContent(panel, data.cascadeId, true); // true = isUpdate (don't scroll)
445
- }, 500);
412
+ updateDebounceTimer = setTimeout(() => fetchPanelContent(panel, data.cascadeId, true), 500);
446
413
  }
447
414
  }
448
415
  }
449
-
416
+
450
417
  function selectCascade(cascadeId) {
451
418
  selectedCascadeId = cascadeId;
452
419
  currentStyles = null;
453
- workspaceFiles = []; // Clear file cache when switching cascades
420
+ workspaceFiles = [];
421
+ lastTasksHash = '';
422
+ stopTasksPolling();
454
423
 
455
424
  if (cascadeId) {
456
- fetchStyles(cascadeId).then(() => {
457
- fetchPanelContent(activePanel, cascadeId);
458
- });
425
+ fetchStyles(cascadeId).then(() => fetchPanelContent(activePanel, cascadeId));
459
426
  } else {
460
427
  showLoading('chat', 'Waiting for Kiro...');
461
428
  }
462
429
  }
463
-
430
+
431
+ // =============================================================================
432
+ // Data Fetching
433
+ // =============================================================================
464
434
  async function fetchStyles(cascadeId) {
465
435
  try {
466
436
  const r = await fetch(`/styles/${cascadeId}`);
467
437
  if (r.ok) currentStyles = await r.text();
468
- } catch (e) {
469
- console.error('Failed to fetch styles:', e);
470
- }
471
- }
472
-
473
- function showLoading(panelName, msg = 'Loading...') {
474
- const p = panels[panelName];
475
- if (p.loading) {
476
- p.loading.querySelector('span').textContent = msg;
477
- p.loading.style.display = 'flex';
478
- }
479
- if (p.content) p.content.style.display = 'none';
480
- if (p.header) p.header.style.display = 'none';
481
- }
482
-
483
- function hideLoading(panelName) {
484
- const p = panels[panelName];
485
- if (p.loading) p.loading.style.display = 'none';
486
- if (p.content) p.content.style.display = 'block';
487
- if (p.header) p.header.style.display = 'flex';
438
+ } catch (e) {}
488
439
  }
489
-
490
- // Fetch panel content
440
+
491
441
  async function fetchPanelContent(panelName, cascadeId, isUpdate = false) {
492
442
  switch (panelName) {
493
- case 'chat':
494
- await fetchChatSnapshot(cascadeId, isUpdate);
495
- break;
496
- case 'editor':
497
- await fetchEditorSnapshot(cascadeId, isUpdate);
498
- break;
443
+ case 'chat': await fetchChatSnapshot(cascadeId, isUpdate); break;
444
+ case 'editor': await fetchEditorSnapshot(cascadeId, isUpdate); break;
445
+ case 'tasks': await fetchTasksContent(cascadeId, isUpdate); break;
499
446
  }
500
447
  }
501
-
502
- // Chat snapshot
448
+
503
449
  async function fetchChatSnapshot(cascadeId, isUpdate = false) {
504
450
  try {
505
451
  const r = await fetch(`/snapshot/${cascadeId}`);
506
452
  if (!r.ok) {
507
- if (navigationPending) {
508
- setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500);
509
- return;
510
- }
453
+ if (navigationPending) { setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500); return; }
511
454
  if (r.status === 404) showLoading('chat', 'Waiting for snapshot...');
512
455
  return;
513
456
  }
514
457
  const snapshot = await r.json();
515
- if (snapshot && snapshot.html && snapshot.html.length > 100) {
458
+ if (snapshot?.html?.length > 100) {
516
459
  lastSuccessfulSnapshot = snapshot;
517
460
  navigationPending = false;
518
461
  renderChatSnapshot(snapshot, isUpdate);
@@ -525,68 +468,7 @@
525
468
  if (!navigationPending) showToast('Failed to load chat');
526
469
  }
527
470
  }
528
-
529
- function renderChatSnapshot(snapshot, isUpdate = false) {
530
- // Prevent concurrent renders
531
- if (isRendering) return;
532
- isRendering = true;
533
-
534
- // Save scroll position of inner scrollable element before update
535
- let innerScrollable = panels.chat.content.querySelector('.overflow-y-auto, [class*="scroll"]');
536
- const hadContent = innerScrollable && innerScrollable.scrollHeight > 100;
537
- const scrollTop = innerScrollable ? innerScrollable.scrollTop : 0;
538
- const scrollHeight = innerScrollable ? innerScrollable.scrollHeight : 0;
539
- const clientHeight = innerScrollable ? innerScrollable.clientHeight : 0;
540
- const wasAtBottom = !hadContent || (scrollHeight - scrollTop - clientHeight < 100);
541
-
542
- let html = getBaseStyles();
543
- if (currentStyles) html += `<style>${currentStyles}</style>`;
544
- html += snapshot.html || '';
545
-
546
- panels.chat.content.innerHTML = html;
547
-
548
- // Set background color - use solid dark background instead of transparent
549
- if (snapshot.bodyBg) {
550
- panels.chat.container.style.background = snapshot.bodyBg;
551
- } else {
552
- // Fallback to solid dark background matching Kiro IDE
553
- panels.chat.container.style.background = '#1e1e1e';
554
- }
555
-
556
- hideLoading('chat');
557
-
558
- // Remove placeholder text that overlaps with input
559
- removePlaceholderText();
560
-
561
- // Find the new inner scrollable element and scroll it
562
- requestAnimationFrame(() => {
563
- requestAnimationFrame(() => {
564
- // Find all scrollable elements and scroll them
565
- const scrollables = panels.chat.content.querySelectorAll('.overflow-y-auto, [class*="scroll"]');
566
- scrollables.forEach(el => {
567
- if (el.scrollHeight > el.clientHeight + 10) {
568
- if (!isUpdate || wasAtBottom) {
569
- el.scrollTop = el.scrollHeight;
570
- } else if (hadContent) {
571
- el.scrollTop = scrollTop;
572
- }
573
- }
574
- });
575
-
576
- // Also scroll the main container if needed
577
- if (!isUpdate || wasAtBottom) {
578
- panels.chat.content.scrollTop = panels.chat.content.scrollHeight;
579
- }
580
-
581
- // Release rendering lock
582
- isRendering = false;
583
- });
584
- });
585
-
586
- makeInteractive();
587
- }
588
-
589
- // Editor snapshot
471
+
590
472
  async function fetchEditorSnapshot(cascadeId, isUpdate = false) {
591
473
  try {
592
474
  const r = await fetch(`/editor/${cascadeId}`);
@@ -594,320 +476,192 @@
594
476
  showEmptyState('editor', 'codicon-file-code', 'No file is currently open. Open a file in Kiro to view it here.');
595
477
  return;
596
478
  }
597
- const data = await r.json();
598
- renderEditorSnapshot(data, isUpdate);
479
+ renderEditorSnapshot(await r.json(), isUpdate);
599
480
  } catch (e) {
600
481
  showEmptyState('editor', 'codicon-file-code', 'Failed to load editor');
601
482
  }
602
483
  }
603
-
604
- function renderEditorSnapshot(data, isUpdate = false) {
605
- const scrollTop = panels.editor.content.scrollTop;
484
+
485
+ async function fetchTasksContent(cascadeId, isUpdate = false) {
486
+ // Only show loading if we don't have cached data
487
+ const hasCachedData = tasksData.length > 0 && lastTasksHash;
488
+ if (!isUpdate && !hasCachedData) showLoading('tasks', 'Loading tasks...');
606
489
 
607
- // Update header
608
- panels.editor.filename.textContent = data.fileName || 'Untitled';
490
+ try {
491
+ const r = await fetch(`/tasks/${cascadeId}`);
492
+ if (!r.ok) { showTasksEmptyState('No tasks found.'); stopTasksPolling(); return; }
493
+
494
+ const data = await r.json();
495
+ const newTasks = data.tasks || [];
496
+ const newHash = JSON.stringify(newTasks);
497
+
498
+ if (newTasks.length === 0) { showTasksEmptyState('No task files found.'); stopTasksPolling(); return; }
499
+
500
+ if (newHash !== lastTasksHash) {
501
+ lastTasksHash = newHash;
502
+ tasksData = newTasks;
503
+ populateTasksDropdown();
504
+ if (selectedTaskIndex < 0 || selectedTaskIndex >= tasksData.length) selectedTaskIndex = 0;
505
+ renderTaskContent(tasksData[selectedTaskIndex]);
506
+ }
507
+ // Always ensure UI is visible after successful fetch
508
+ panels.tasks.header.style.display = 'flex';
509
+ panels.tasks.content.style.display = 'block';
510
+ hideLoading('tasks');
511
+ startTasksPolling(cascadeId);
512
+ } catch (e) {
513
+ if (!isUpdate) showTasksEmptyState('Failed to load tasks');
514
+ stopTasksPolling();
515
+ }
516
+ }
517
+
518
+ // =============================================================================
519
+ // Tasks Panel
520
+ // =============================================================================
521
+ function startTasksPolling(cascadeId) {
522
+ if (tasksPollingTimer) return;
523
+ tasksPollingTimer = setInterval(() => {
524
+ if (activePanel === 'tasks' && selectedCascadeId) fetchTasksContent(selectedCascadeId, true);
525
+ }, 3000);
526
+ }
527
+
528
+ function stopTasksPolling() {
529
+ if (tasksPollingTimer) { clearInterval(tasksPollingTimer); tasksPollingTimer = null; }
530
+ }
531
+
532
+ function showTasksEmptyState(message) {
533
+ panels.tasks.header.style.display = 'none';
534
+ panels.tasks.content.innerHTML = `<div class="tasks-empty"><span class="codicon codicon-checklist"></span><p>${message}</p></div>`;
535
+ panels.tasks.content.style.display = 'flex';
536
+ hideLoading('tasks');
537
+ }
538
+
539
+ function populateTasksDropdown() {
540
+ const dropdown = panels.tasks.dropdown;
541
+ dropdown.innerHTML = '';
542
+ tasksData.forEach((task, index) => {
543
+ const option = document.createElement('option');
544
+ option.value = index;
545
+ option.textContent = task.name;
546
+ if (index === selectedTaskIndex) option.selected = true;
547
+ dropdown.appendChild(option);
548
+ });
609
549
 
610
- // Show header
611
- panels.editor.header.style.display = 'flex';
550
+ dropdown.onchange = async (e) => {
551
+ selectedTaskIndex = parseInt(e.target.value, 10);
552
+ if (tasksData[selectedTaskIndex]) {
553
+ renderTaskContent(tasksData[selectedTaskIndex]);
554
+ await openSpecInKiro(tasksData[selectedTaskIndex].name);
555
+ }
556
+ };
557
+ }
558
+
559
+ async function openSpecInKiro(specName) {
560
+ if (!selectedCascadeId || !specName) return;
561
+ showToast(`Opening ${specName}...`, 1500);
562
+ try {
563
+ await fetch(`/open-spec/${selectedCascadeId}`, {
564
+ method: 'POST',
565
+ headers: { 'Content-Type': 'application/json' },
566
+ body: JSON.stringify({ specName })
567
+ });
568
+ } catch (e) {}
569
+ }
570
+
571
+ function renderTaskContent(task) {
572
+ if (!task?.content) { panels.tasks.content.innerHTML = '<div class="tasks-empty"><p>No content</p></div>'; return; }
573
+ panels.tasks.content.innerHTML = `<div class="task-md">${parseTaskMarkdown(task.content)}</div>`;
574
+
575
+ panels.tasks.content.querySelectorAll('.task-group-header').forEach(header => {
576
+ const clickables = header.querySelectorAll('.task-group-title, .task-group-indicator, .task-group-arrow, .task-group-count');
577
+ clickables.forEach(el => {
578
+ if (el) {
579
+ el.style.cursor = 'pointer';
580
+ el.onclick = (e) => { e.stopPropagation(); header.closest('.task-group').classList.toggle('expanded'); };
581
+ }
582
+ });
583
+ });
584
+ }
585
+
586
+ function parseTaskMarkdown(markdown) {
587
+ const lines = markdown.split('\n');
588
+ let html = '';
589
+ let currentGroupNumber = '', currentGroupTitle = '', currentGroupTasks = [];
612
590
 
613
- // Store original content for search
614
- originalEditorContent = data.content || '';
591
+ const arrowIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg>`;
592
+ const checkIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg>`;
615
593
 
616
- // Reset search when new file loaded
617
- closeEditorSearch();
594
+ const flushGroup = () => {
595
+ if (currentGroupTitle && currentGroupTasks.length > 0) {
596
+ const completed = currentGroupTasks.filter(t => t.checked).length;
597
+ const total = currentGroupTasks.length;
598
+ const isComplete = completed === total;
599
+ const hasIncomplete = completed < total;
600
+
601
+ html += `<div class="task-group${hasIncomplete ? ' expanded' : ''}${isComplete ? ' completed' : ''}" data-task-number="${currentGroupNumber}">`;
602
+ html += `<div class="task-group-header"><div class="task-group-indicator">${checkIcon}</div>`;
603
+ html += `<span class="task-group-number">${currentGroupNumber}</span>`;
604
+ html += `<span class="task-group-title">${escapeHtml(currentGroupTitle)}</span>`;
605
+ html += `<div class="task-group-meta"><span class="task-group-count">${completed}/${total}</span>`;
606
+ html += `<span class="task-group-arrow">${arrowIcon}</span></div></div>`;
607
+ html += `<div class="task-group-body"><div class="task-group-body-inner">`;
608
+
609
+ currentGroupTasks.slice(1).forEach(task => {
610
+ const indent = task.indent > 1 ? ` indent-${Math.min(task.indent, 3)}` : '';
611
+ html += `<div class="task-item${indent}"><span class="task-checkbox${task.checked ? ' checked' : ''}">${checkIcon}</span>`;
612
+ html += `<span class="task-text${task.checked ? ' completed' : ''}">${escapeHtml(task.text)}</span></div>`;
613
+ });
614
+
615
+ html += `</div></div></div>`;
616
+ }
617
+ currentGroupNumber = ''; currentGroupTitle = ''; currentGroupTasks = [];
618
+ };
618
619
 
619
- // Prefer text content over raw HTML for better mobile display
620
- if (data.content && data.content.trim().length > 0) {
621
- const lines = data.content.split('\n');
622
- let html = '';
620
+ for (const line of lines) {
621
+ const trimmed = line.trim();
622
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.startsWith('- [')) continue;
623
623
 
624
- // Add note if partial content
625
- if (data.isPartial || data.note) {
626
- html += `<div style="padding: 8px 12px; background: #2d2d30; color: #888; font-size: 11px; border-bottom: 1px solid #3c3c3c;">
627
- <span class="codicon codicon-info" style="margin-right: 6px;"></span>
628
- ${data.note || 'Showing visible lines only. Scroll in Kiro to see more.'}
629
- </div>`;
624
+ const mainMatch = trimmed.match(/^- \[([ xX])\] (\d+)\.?\s+(.*)$/);
625
+ if (mainMatch) {
626
+ flushGroup();
627
+ currentGroupNumber = mainMatch[2];
628
+ currentGroupTitle = mainMatch[3];
629
+ currentGroupTasks.push({ text: mainMatch[3], checked: mainMatch[1].toLowerCase() === 'x', indent: 0 });
630
+ continue;
630
631
  }
631
632
 
632
- html += '<pre class="editor-code">';
633
-
634
- // Use startLine from server if available, otherwise start from 1
635
- const startLineNum = data.startLine || 1;
636
-
637
- // Filter out empty lines at the start if there are too many
638
- let startIdx = 0;
639
- while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) {
640
- startIdx++;
633
+ const subMatch = trimmed.match(/^- \[([ xX])\] (\d+\.\d+)\s+(.*)$/);
634
+ if (subMatch && currentGroupTitle) {
635
+ currentGroupTasks.push({ text: subMatch[3], checked: subMatch[1].toLowerCase() === 'x', indent: 1 });
636
+ continue;
641
637
  }
642
638
 
643
- const displayLines = lines.slice(startIdx);
644
- displayLines.forEach((line, idx) => {
645
- const lineNum = startLineNum + startIdx + idx;
646
- // Convert tabs to spaces first, then highlight
647
- const lineWithSpaces = line.replace(/\t/g, ' ');
648
- const highlighted = highlightSyntax(lineWithSpaces, data.language);
649
- html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${highlighted || ' '}</span></div>`;
650
- });
651
- html += '</pre>';
652
-
653
- // Add line count info
654
- const endLine = startLineNum + startIdx + displayLines.length - 1;
655
- html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
656
- Lines ${startLineNum}-${endLine}${data.lineCount ? ` of ${data.lineCount}` : ''}
657
- </div>`;
658
-
659
- panels.editor.content.innerHTML = html;
660
- panels.editor.content.style.display = 'block';
661
- console.log('[Editor] Rendered', displayLines.length, 'lines of code starting from line', startLineNum);
662
- } else if (data.html && data.html.length > 100) {
663
- // Fallback to raw HTML if no text content
664
- let html = getBaseStyles();
665
- if (currentStyles) html += `<style>${currentStyles}</style>`;
666
- html += data.html;
667
- panels.editor.content.innerHTML = html;
668
- panels.editor.content.style.display = 'block';
669
- console.log('[Editor] Rendered raw HTML');
670
- } else {
671
- showEmptyState('editor', 'codicon-file-code', 'No editor content available. Open a file in Kiro to view it here.');
672
- return;
639
+ const taskMatch = line.match(/^(\s*)- \[([ xX])\] (.*)$/);
640
+ if (taskMatch) {
641
+ const indent = Math.floor(taskMatch[1].length / 2);
642
+ const isChecked = taskMatch[2].toLowerCase() === 'x';
643
+ const text = taskMatch[3];
644
+
645
+ const numMatch = text.match(/^(\d+)\.?\s+(.*)$/);
646
+ if (numMatch && indent === 0) {
647
+ flushGroup();
648
+ currentGroupNumber = numMatch[1];
649
+ currentGroupTitle = numMatch[2];
650
+ currentGroupTasks.push({ text: numMatch[2], checked: isChecked, indent: 0 });
651
+ } else if (currentGroupTitle) {
652
+ currentGroupTasks.push({ text, checked: isChecked, indent: Math.max(1, indent) });
653
+ }
654
+ }
673
655
  }
674
- hideLoading('editor');
675
656
 
676
- // Preserve scroll on updates
677
- if (isUpdate) {
678
- panels.editor.content.scrollTop = scrollTop;
679
- }
657
+ flushGroup();
658
+ return html;
680
659
  }
681
660
 
682
- // Syntax highlighting - simple and safe approach
683
- function highlightSyntax(line, language) {
684
- // First escape HTML to prevent XSS
685
- const escaped = escapeHtml(line);
686
-
687
- // Skip highlighting for very long lines or if no language
688
- if (escaped.length > 1000 || !language) {
689
- return escaped;
690
- }
691
-
692
- // Language detection
693
- const isJS = ['javascript', 'typescript', 'jsx', 'tsx', 'js', 'ts'].includes(language);
694
- const isPython = language === 'python' || language === 'py';
695
- const isJSON = language === 'json';
696
- const isYAML = language === 'yaml' || language === 'yml';
697
- const isShell = ['bash', 'sh', 'shell', 'zsh'].includes(language);
698
- const isHTML = language === 'html' || language === 'xml';
699
- const isCSS = language === 'css' || language === 'scss' || language === 'sass' || language === 'less';
700
- const isMarkdown = language === 'markdown' || language === 'md';
701
-
702
- // For HTML files, use a simpler approach - just colorize without spans that could break
703
- if (isHTML) {
704
- let result = escaped;
705
- // Comments
706
- result = result.replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="tok-cmt">$1</span>');
707
- // Strings in attributes
708
- result = result.replace(/=(&quot;[^&]*?&quot;)/g, '=<span class="tok-str">$1</span>');
709
- result = result.replace(/=(&#39;[^&]*?&#39;)/g, '=<span class="tok-str">$1</span>');
710
- return result;
711
- }
712
-
713
- // For CSS files
714
- if (isCSS) {
715
- let result = escaped;
716
- // Comments
717
- result = result.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="tok-cmt">$1</span>');
718
- // Strings
719
- result = result.replace(/(&quot;[^&]*?&quot;)/g, '<span class="tok-str">$1</span>');
720
- result = result.replace(/(&#39;[^&]*?&#39;)/g, '<span class="tok-str">$1</span>');
721
- // Numbers with units
722
- result = result.replace(/:\s*([0-9]+(?:\.[0-9]+)?(?:px|em|rem|%|vh|vw|s|ms)?)/g, ': <span class="tok-num">$1</span>');
723
- // Hex colors
724
- result = result.replace(/(#[0-9a-fA-F]{3,8})\b/g, '<span class="tok-num">$1</span>');
725
- return result;
726
- }
727
-
728
- // Build result by processing character by character for other languages
729
- let result = '';
730
- let i = 0;
731
-
732
- while (i < escaped.length) {
733
- // Check for comments
734
- if (isJS && escaped.slice(i, i + 2) === '//') {
735
- result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>';
736
- break;
737
- }
738
- if ((isPython || isShell || isYAML) && escaped[i] === '#') {
739
- result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>';
740
- break;
741
- }
742
-
743
- // Check for strings (escaped quotes: &quot; or &#39;)
744
- if (escaped.slice(i, i + 6) === '&quot;') {
745
- const endIdx = escaped.indexOf('&quot;', i + 6);
746
- if (endIdx !== -1) {
747
- const str = escaped.slice(i, endIdx + 6);
748
- result += '<span class="tok-str">' + str + '</span>';
749
- i = endIdx + 6;
750
- continue;
751
- }
752
- }
753
- if (escaped.slice(i, i + 5) === '&#39;') {
754
- const endIdx = escaped.indexOf('&#39;', i + 5);
755
- if (endIdx !== -1) {
756
- const str = escaped.slice(i, endIdx + 5);
757
- result += '<span class="tok-str">' + str + '</span>';
758
- i = endIdx + 5;
759
- continue;
760
- }
761
- }
762
-
763
- // Check for numbers
764
- if (/[0-9]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z_]/.test(escaped[i - 1]))) {
765
- let numEnd = i;
766
- while (numEnd < escaped.length && /[0-9.]/.test(escaped[numEnd])) {
767
- numEnd++;
768
- }
769
- if (numEnd > i) {
770
- result += '<span class="tok-num">' + escaped.slice(i, numEnd) + '</span>';
771
- i = numEnd;
772
- continue;
773
- }
774
- }
775
-
776
- // Check for keywords (word boundary)
777
- if (/[a-zA-Z_]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z0-9_]/.test(escaped[i - 1]))) {
778
- let wordEnd = i;
779
- while (wordEnd < escaped.length && /[a-zA-Z0-9_]/.test(escaped[wordEnd])) {
780
- wordEnd++;
781
- }
782
- const word = escaped.slice(i, wordEnd);
783
-
784
- // Define keywords per language
785
- let keywords = [];
786
- if (isJS) {
787
- keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'class', 'extends', 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'switch', 'case', 'break', 'continue', 'default', 'static', 'get', 'set'];
788
- } else if (isPython) {
789
- keywords = ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import', 'from', 'as', 'try', 'except', 'raise', 'with', 'async', 'await', 'None', 'True', 'False', 'self', 'and', 'or', 'not', 'in', 'is', 'lambda', 'pass', 'break', 'continue'];
790
- } else if (isJSON) {
791
- keywords = ['true', 'false', 'null'];
792
- } else if (isYAML) {
793
- keywords = ['true', 'false', 'null', 'yes', 'no'];
794
- } else if (!isMarkdown) {
795
- keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'true', 'false', 'null', 'class', 'public', 'private', 'static', 'void', 'int', 'string', 'bool'];
796
- }
797
-
798
- if (keywords.includes(word)) {
799
- result += '<span class="tok-kw">' + word + '</span>';
800
- } else {
801
- result += word;
802
- }
803
- i = wordEnd;
804
- continue;
805
- }
806
-
807
- // Default: just add the character
808
- result += escaped[i];
809
- i++;
810
- }
811
-
812
- return result;
813
- }
814
-
815
- // Helper functions
816
- function escapeHtml(text) {
817
- return text
818
- .replace(/&/g, '&amp;')
819
- .replace(/</g, '&lt;')
820
- .replace(/>/g, '&gt;')
821
- .replace(/"/g, '&quot;')
822
- .replace(/'/g, '&#39;');
823
- }
824
-
825
- function showEmptyState(panelName, icon, message) {
826
- const p = panels[panelName];
827
- p.content.innerHTML = `
828
- <div class="empty-state">
829
- <span class="codicon ${icon}"></span>
830
- <p>${message}</p>
831
- </div>
832
- `;
833
- hideLoading(panelName);
834
- }
835
-
836
- function toggleSection(header) {
837
- header.classList.toggle('collapsed');
838
- const content = header.nextElementSibling;
839
- if (content) content.classList.toggle('collapsed');
840
- }
841
-
842
- // Remove placeholder text that overlaps with input field
843
- function removePlaceholderText() {
844
- const content = panels.chat.content;
845
-
846
- // Method 1: Find elements by class name containing "placeholder"
847
- content.querySelectorAll('[class*="placeholder"], [class*="Placeholder"]').forEach(el => {
848
- // Don't remove actual input elements
849
- if (el.matches('[contenteditable], textarea, input, [data-lexical-editor]')) return;
850
- if (el.querySelector('[contenteditable], textarea, input, [data-lexical-editor]')) return;
851
-
852
- // Hide it
853
- el.style.display = 'none';
854
- el.style.visibility = 'hidden';
855
- el.style.opacity = '0';
856
- });
857
-
858
- // Method 2: Find elements with data-placeholder attribute
859
- content.querySelectorAll('[data-placeholder]').forEach(el => {
860
- // Remove the data-placeholder attribute to prevent CSS ::before content
861
- if (!el.matches('[contenteditable], [data-lexical-editor]')) {
862
- el.style.display = 'none';
863
- }
864
- });
865
-
866
- // Method 3: Find any element containing placeholder-like text and hide it
867
- const placeholderTexts = ['ask a question', 'describe a task', 'type a message', 'enter a message'];
868
-
869
- const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false);
870
- const nodesToHide = [];
871
-
872
- while (walker.nextNode()) {
873
- const text = (walker.currentNode.textContent || '').toLowerCase();
874
- for (const placeholder of placeholderTexts) {
875
- if (text.includes(placeholder)) {
876
- const parent = walker.currentNode.parentElement;
877
- if (parent && !parent.matches('[contenteditable], textarea, input, [data-lexical-editor]')) {
878
- nodesToHide.push(parent);
879
- }
880
- break;
881
- }
882
- }
883
- }
884
-
885
- nodesToHide.forEach(el => {
886
- el.style.display = 'none';
887
- el.style.visibility = 'hidden';
888
- });
889
-
890
- // Method 4: Find absolutely positioned elements inside input containers and hide them
891
- // These are often placeholder overlays
892
- const inputContainers = content.querySelectorAll('[class*="input"], [class*="composer"], [class*="editor"]');
893
- inputContainers.forEach(container => {
894
- container.querySelectorAll('*').forEach(el => {
895
- const style = window.getComputedStyle(el);
896
- // If it's absolutely positioned and not the input itself, it might be a placeholder
897
- if (style.position === 'absolute' &&
898
- !el.matches('[contenteditable], textarea, input, [data-lexical-editor], button, svg')) {
899
- const text = (el.textContent || '').toLowerCase();
900
- if (text.includes('ask') || text.includes('task') || text.includes('question') ||
901
- text.includes('describe') || text.includes('message') || text.includes('type')) {
902
- el.style.display = 'none';
903
- el.style.visibility = 'hidden';
904
- }
905
- }
906
- });
907
- });
908
- }
909
-
910
- // Base styles for chat panel
661
+
662
+ // =============================================================================
663
+ // Chat Rendering
664
+ // =============================================================================
911
665
  function getBaseStyles() {
912
666
  return `<style>
913
667
  :root, body, #root, .app {
@@ -944,38 +698,30 @@
944
698
  background-color: #1e1e1e !important;
945
699
  }
946
700
 
947
- /* ========== HIDE PLACEHOLDER TEXT IN INPUT AREA ========== */
948
- /* Placeholder removal is handled via JavaScript in removePlaceholderText() */
949
701
  /* Keep input elements visible and interactive */
950
- [contenteditable="true"],
951
- [data-lexical-editor="true"],
952
- .ProseMirror,
953
- textarea {
702
+ [contenteditable="true"], [data-lexical-editor="true"], .ProseMirror, textarea {
954
703
  display: block !important;
955
704
  visibility: visible !important;
956
705
  opacity: 1 !important;
957
706
  pointer-events: auto !important;
958
707
  }
959
708
 
960
- /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
961
- [role="tooltip"],
962
- [data-tooltip],
709
+ /* Hide tooltips, popovers, and overlay elements */
710
+ [role="tooltip"], [data-tooltip],
963
711
  [class*="tooltip"]:not(button):not([role="button"]),
964
712
  [class*="Tooltip"]:not(button):not([role="button"]),
965
713
  [class*="popover"]:not(button):not([role="button"]),
966
714
  [class*="Popover"]:not(button):not([role="button"]),
967
715
  [class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
968
716
  [class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
969
- [class*="modal"],
970
- [class*="Modal"] {
717
+ [class*="modal"], [class*="Modal"] {
971
718
  display: none !important;
972
719
  visibility: hidden !important;
973
720
  opacity: 0 !important;
974
721
  pointer-events: none !important;
975
722
  }
976
723
 
977
- /* ========== MODEL SELECTOR DROPDOWN ========== */
978
- /* Dropdown menu panel - needs solid background and proper z-index */
724
+ /* Model selector dropdown */
979
725
  [class*="dropdown-menu"], [class*="dropdownMenu"], [class*="DropdownMenu"],
980
726
  [class*="dropdown-content"], [class*="dropdownContent"], [class*="DropdownContent"],
981
727
  [class*="model-list"], [class*="modelList"], [class*="ModelList"],
@@ -1007,31 +753,12 @@
1007
753
  color: #cccccc !important;
1008
754
  }
1009
755
 
1010
- /* Hide model descriptions in dropdown - only show model name */
1011
- [class*="model-description"], [class*="modelDescription"], [class*="ModelDescription"],
1012
- [class*="model-info"], [class*="modelInfo"], [class*="ModelInfo"],
1013
- [class*="dropdown-item"] > span:not(:first-child),
1014
- [class*="model-option"] > span:not(:first-child),
1015
- [role="option"] > div:last-child,
1016
- [role="option"] > span:last-child:not(:first-child),
1017
- [role="menuitem"] > div:last-child,
1018
- [class*="credit"], [class*="Credit"] {
1019
- display: none !important;
1020
- }
1021
-
1022
- /* Simplify model dropdown items - just show the name */
1023
- [role="option"], [role="menuitem"] {
1024
- flex-direction: row !important;
1025
- align-items: center !important;
1026
- gap: 8px !important;
1027
- }
1028
-
1029
756
  [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
1030
757
  [role="option"]:hover, [role="menuitem"]:hover {
1031
758
  background: rgba(255, 255, 255, 0.1) !important;
1032
759
  }
1033
760
 
1034
- /* Model selector button - NO border, match Kiro style */
761
+ /* Model selector button */
1035
762
  [class*="model-selector"], [class*="modelSelector"], [class*="ModelSelector"],
1036
763
  [class*="model-dropdown"], [class*="modelDropdown"], [class*="ModelDropdown"],
1037
764
  button[class*="dropdown"], [role="button"][class*="dropdown"],
@@ -1056,45 +783,28 @@
1056
783
  background: rgba(255, 255, 255, 0.1) !important;
1057
784
  }
1058
785
 
1059
- /* Model selector chevron/arrow icon */
1060
- [class*="model-selector"] svg, [class*="modelSelector"] svg,
1061
- [class*="model-dropdown"] svg, button[class*="dropdown"] svg {
1062
- width: 12px !important;
1063
- height: 12px !important;
1064
- flex-shrink: 0 !important;
1065
- }
1066
-
1067
- /* Model name text inside selector */
1068
- [class*="model-selector"] span, [class*="modelSelector"] span,
1069
- [class*="model-name"], [class*="modelName"] {
1070
- overflow: hidden !important;
1071
- text-overflow: ellipsis !important;
1072
- white-space: nowrap !important;
1073
- }
1074
-
1075
- /* Reset positioning for captured content to prevent layout issues */
786
+ /* Reset positioning */
1076
787
  #root, .app, [class*="cascade"], [class*="Cascade"] {
1077
788
  position: relative !important;
1078
789
  }
1079
790
 
1080
- /* Ensure proper stacking and no fixed positioning breaks layout */
1081
- [style*="position: fixed"],
1082
- [style*="position:fixed"] {
791
+ [style*="position: fixed"], [style*="position:fixed"] {
1083
792
  position: absolute !important;
1084
793
  }
1085
794
 
1086
795
  * { font-family: inherit; }
1087
796
  code, pre, .monaco-editor { font-family: Menlo, Monaco, "Courier New", monospace !important; }
1088
- /* Icon sizes - keep them small like in IDE */
797
+
798
+ /* Icon sizes */
1089
799
  svg[viewBox="0 0 24 24"] { width: 16px !important; height: 16px !important; }
1090
800
  svg[viewBox="0 0 16 16"] { width: 14px !important; height: 14px !important; }
1091
801
  svg { color: inherit; max-width: 20px !important; max-height: 20px !important; }
1092
802
  svg path, svg rect { fill: currentColor; }
1093
803
  svg[fill="none"] path { fill: none; }
1094
- /* Preserve stroke-based SVG circles (like context window progress ring) */
1095
804
  svg circle[stroke] { fill: none !important; }
1096
805
  svg circle:not([stroke]) { fill: currentColor; }
1097
- /* Context window progress circle styling */
806
+
807
+ /* Context window progress circle */
1098
808
  svg[class*="context"], svg[class*="progress"], [class*="context"] svg {
1099
809
  overflow: visible !important;
1100
810
  }
@@ -1102,7 +812,6 @@
1102
812
  fill: none !important;
1103
813
  stroke-linecap: round !important;
1104
814
  }
1105
- /* Context window indicator - circular progress ring */
1106
815
  [class*="context-window"], [class*="contextWindow"], [class*="ContextWindow"],
1107
816
  [class*="context-indicator"], [class*="contextIndicator"], [class*="ContextIndicator"],
1108
817
  [class*="token-usage"], [class*="tokenUsage"], [class*="TokenUsage"] {
@@ -1116,7 +825,6 @@
1116
825
  position: relative !important;
1117
826
  flex-shrink: 0 !important;
1118
827
  }
1119
- /* Create gray background circle using box-shadow on the container */
1120
828
  [class*="context-window"]::before, [class*="contextWindow"]::before, [class*="ContextWindow"]::before,
1121
829
  [class*="context-indicator"]::before, [class*="contextIndicator"]::before {
1122
830
  content: '' !important;
@@ -1145,15 +853,12 @@
1145
853
  stroke-linecap: round !important;
1146
854
  stroke: #4ade80 !important;
1147
855
  }
1148
- /* Ensure circles are visible */
1149
- [class*="context"] svg circle,
1150
- [class*="Context"] svg circle {
856
+ [class*="context"] svg circle, [class*="Context"] svg circle {
1151
857
  display: block !important;
1152
858
  visibility: visible !important;
1153
859
  }
1154
860
 
1155
- /* ========== CHAT INPUT TOOLBAR AREA ========== */
1156
- /* The toolbar/footer area containing model selector, context, autopilot, input */
861
+ /* Chat input toolbar area */
1157
862
  [class*="chat-input"], [class*="chatInput"], [class*="ChatInput"],
1158
863
  [class*="input-area"], [class*="inputArea"], [class*="InputArea"],
1159
864
  [class*="message-input"], [class*="messageInput"], [class*="MessageInput"],
@@ -1167,7 +872,7 @@
1167
872
  border-top: 1px solid var(--color-border-primary, #4a464f) !important;
1168
873
  }
1169
874
 
1170
- /* Input toolbar row (contains # button, model selector, context, autopilot) */
875
+ /* Input toolbar row */
1171
876
  [class*="input-toolbar"], [class*="inputToolbar"], [class*="InputToolbar"],
1172
877
  [class*="chat-toolbar"], [class*="chatToolbar"], [class*="ChatToolbar"],
1173
878
  [class*="composer-toolbar"], [class*="composerToolbar"] {
@@ -1178,7 +883,7 @@
1178
883
  flex-wrap: wrap !important;
1179
884
  }
1180
885
 
1181
- /* Context button (# symbol) - NO border, match Kiro style */
886
+ /* Context button (# symbol) */
1182
887
  [class*="context-button"], [class*="contextButton"], [class*="ContextButton"],
1183
888
  button[aria-label*="context"], button[aria-label*="Context"],
1184
889
  [class*="hash-button"], [class*="hashButton"] {
@@ -1203,30 +908,15 @@
1203
908
  background: rgba(255, 255, 255, 0.1) !important;
1204
909
  }
1205
910
 
1206
- /* ========== MESSAGE TEXT FLOW - INLINE ELEMENTS ========== */
1207
- /* Fix text flow so file links and code stay inline with text */
1208
- [class*="message"] p,
1209
- [class*="Message"] p,
1210
- [class*="chat"] p,
1211
- [class*="Chat"] p {
911
+ /* Message text flow - inline elements */
912
+ [class*="message"] p, [class*="Message"] p, [class*="chat"] p, [class*="Chat"] p {
1212
913
  display: block !important;
1213
914
  }
1214
915
 
1215
- /* Make file links, code, and inline elements stay inline */
1216
- [class*="message"] a,
1217
- [class*="Message"] a,
1218
- [class*="message"] code,
1219
- [class*="Message"] code,
1220
- [class*="message"] span,
1221
- [class*="Message"] span,
1222
- [class*="chat"] a,
1223
- [class*="Chat"] a,
1224
- [class*="chat"] code,
1225
- [class*="Chat"] code,
1226
- [class*="file-link"],
1227
- [class*="FileLink"],
1228
- [class*="inline"],
1229
- code:not(pre code) {
916
+ [class*="message"] a, [class*="Message"] a, [class*="message"] code, [class*="Message"] code,
917
+ [class*="message"] span, [class*="Message"] span, [class*="chat"] a, [class*="Chat"] a,
918
+ [class*="chat"] code, [class*="Chat"] code, [class*="file-link"], [class*="FileLink"],
919
+ [class*="inline"], code:not(pre code) {
1230
920
  display: inline !important;
1231
921
  vertical-align: baseline !important;
1232
922
  }
@@ -1240,17 +930,10 @@
1240
930
  font-size: 12px !important;
1241
931
  }
1242
932
 
1243
- /* File badges/chips should be inline */
1244
- [class*="file-badge"],
1245
- [class*="FileBadge"],
1246
- [class*="file-chip"],
1247
- [class*="FileChip"],
1248
- [class*="deleted"],
1249
- [class*="Deleted"],
1250
- [class*="created"],
1251
- [class*="Created"],
1252
- [class*="modified"],
1253
- [class*="Modified"] {
933
+ /* File badges/chips */
934
+ [class*="file-badge"], [class*="FileBadge"], [class*="file-chip"], [class*="FileChip"],
935
+ [class*="deleted"], [class*="Deleted"], [class*="created"], [class*="Created"],
936
+ [class*="modified"], [class*="Modified"] {
1254
937
  display: inline-flex !important;
1255
938
  align-items: center !important;
1256
939
  gap: 4px !important;
@@ -1262,19 +945,10 @@
1262
945
  vertical-align: middle !important;
1263
946
  }
1264
947
 
1265
- /* Bullet lists should have proper inline content */
1266
- ul li, ol li {
1267
- display: list-item !important;
1268
- }
1269
-
1270
- ul li > *, ol li > * {
1271
- display: inline !important;
1272
- }
1273
-
1274
- ul li > p, ol li > p {
1275
- display: inline !important;
1276
- margin: 0 !important;
1277
- }
948
+ /* Bullet lists */
949
+ ul li, ol li { display: list-item !important; }
950
+ ul li > *, ol li > * { display: inline !important; }
951
+ ul li > p, ol li > p { display: inline !important; margin: 0 !important; }
1278
952
 
1279
953
  [role="switch"], [class*="toggle"] {
1280
954
  overflow: visible !important;
@@ -1282,117 +956,7 @@
1282
956
  flex-shrink: 0 !important;
1283
957
  }
1284
958
 
1285
- /* ========== TASK LIST STYLING - EXACT KIRO IDE MATCH ========== */
1286
-
1287
- /* Force solid background for task containers */
1288
- [class*="task"], [class*="Task"],
1289
- div:has(> h1:contains("CURRENT TASKS")),
1290
- div:has(> h2:contains("CURRENT TASKS")),
1291
- div:has(> h3:contains("CURRENT TASKS")),
1292
- div:has(> h1:contains("TASKS IN QUEUE")),
1293
- div:has(> h2:contains("TASKS IN QUEUE")),
1294
- div:has(> h3:contains("TASKS IN QUEUE")) {
1295
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
1296
- background: #1e1e1e !important;
1297
- background-color: #1e1e1e !important;
1298
- }
1299
-
1300
- /* Task section headers - BOLD WHITE UPPERCASE */
1301
- h1:contains("CURRENT TASKS"),
1302
- h2:contains("CURRENT TASKS"),
1303
- h3:contains("CURRENT TASKS"),
1304
- h1:contains("TASKS IN QUEUE"),
1305
- h2:contains("TASKS IN QUEUE"),
1306
- h3:contains("TASKS IN QUEUE"),
1307
- [class*="task"] h1, [class*="Task"] h1,
1308
- [class*="task"] h2, [class*="Task"] h2,
1309
- [class*="task"] h3, [class*="Task"] h3,
1310
- [class*="task-header"], [class*="TaskHeader"],
1311
- [class*="section-title"], [class*="SectionTitle"],
1312
- div[class*="current"] h1, div[class*="Current"] h1,
1313
- div[class*="current"] h2, div[class*="Current"] h2,
1314
- div[class*="current"] h3, div[class*="Current"] h3,
1315
- div[class*="queue"] h1, div[class*="Queue"] h1,
1316
- div[class*="queue"] h2, div[class*="Queue"] h2,
1317
- div[class*="queue"] h3, div[class*="Queue"] h3 {
1318
- font-size: 13px !important;
1319
- font-weight: 700 !important;
1320
- letter-spacing: 0.5px !important;
1321
- text-transform: uppercase !important;
1322
- color: #ffffff !important;
1323
- margin: 0 0 12px 0 !important;
1324
- padding: 0 !important;
1325
- line-height: 1.4 !important;
1326
- background: transparent !important;
1327
- }
1328
-
1329
- /* Empty state messages - ITALIC GRAY */
1330
- p:contains("No active tasks"),
1331
- p:contains("No tasks in queue"),
1332
- [class*="task"] p:not([class*="task-item"]),
1333
- [class*="Task"] p:not([class*="TaskItem"]),
1334
- [class*="no-task"], [class*="NoTask"],
1335
- [class*="empty-task"], [class*="EmptyTask"],
1336
- [class*="task-empty"], [class*="TaskEmpty"] {
1337
- font-size: 13px !important;
1338
- font-style: italic !important;
1339
- color: #888888 !important;
1340
- margin: 0 0 16px 0 !important;
1341
- padding: 0 !important;
1342
- line-height: 1.6 !important;
1343
- background: transparent !important;
1344
- }
1345
-
1346
- /* Task list containers */
1347
- [class*="task-list"], [class*="TaskList"],
1348
- [class*="task-section"], [class*="TaskSection"] {
1349
- margin-bottom: 24px !important;
1350
- padding: 0 !important;
1351
- background: #1e1e1e !important;
1352
- }
1353
-
1354
- /* Horizontal divider between sections */
1355
- [class*="task-divider"], [class*="TaskDivider"],
1356
- [class*="task"] hr, [class*="Task"] hr {
1357
- border: none !important;
1358
- border-top: 1px solid #3c3c3c !important;
1359
- margin: 20px 0 !important;
1360
- background: transparent !important;
1361
- }
1362
-
1363
- /* Individual task items (when they exist) */
1364
- [class*="task-item"], [class*="TaskItem"] {
1365
- padding: 8px 12px !important;
1366
- margin: 4px 0 !important;
1367
- background: rgba(255, 255, 255, 0.03) !important;
1368
- border-radius: 4px !important;
1369
- border-left: 2px solid #7138cc !important;
1370
- font-size: 13px !important;
1371
- color: #cccccc !important;
1372
- line-height: 1.5 !important;
1373
- }
1374
-
1375
- [class*="task-item"]:hover, [class*="TaskItem"]:hover {
1376
- background: rgba(255, 255, 255, 0.05) !important;
1377
- }
1378
-
1379
- /* Task item with close button */
1380
- [class*="task-item"] button, [class*="TaskItem"] button {
1381
- margin-left: 8px !important;
1382
- padding: 2px 6px !important;
1383
- font-size: 11px !important;
1384
- background: rgba(255, 255, 255, 0.1) !important;
1385
- border: 1px solid #4a464f !important;
1386
- border-radius: 3px !important;
1387
- color: #888 !important;
1388
- cursor: pointer !important;
1389
- }
1390
-
1391
- [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
1392
- background: rgba(255, 255, 255, 0.15) !important;
1393
- color: #fff !important;
1394
- }
1395
-
959
+ /* Toggle switch styling */
1396
960
  .kiro-toggle-switch {
1397
961
  display: flex !important;
1398
962
  align-items: center !important;
@@ -1431,193 +995,586 @@
1431
995
  }
1432
996
  </style>`;
1433
997
  }
1434
-
1435
- // Make chat elements interactive
1436
- function makeInteractive() {
1437
- const content = panels.chat.content;
998
+
999
+ function renderChatSnapshot(snapshot, isUpdate = false) {
1000
+ if (isRendering) return;
1001
+ isRendering = true;
1438
1002
 
1439
- // Input fields - support both Lexical and ProseMirror/TipTap editors
1440
- const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1441
- for (const selector of inputSelectors) {
1442
- content.querySelectorAll(selector).forEach(el => {
1443
- el.style.cursor = 'text';
1444
- el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1445
- el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1446
- el.addEventListener('keydown', async (e) => {
1447
- if (e.key === 'Enter' && !e.shiftKey) {
1448
- e.preventDefault();
1449
- const text = el.textContent || el.value || '';
1450
- if (text.trim()) {
1451
- await sendToKiro(text.trim());
1452
- el.textContent = '';
1453
- el.innerHTML = '';
1454
- isTypingInKiroInput = false;
1455
- }
1456
- }
1457
- });
1458
- });
1459
- }
1003
+ let innerScrollable = panels.chat.content.querySelector('.overflow-y-auto, [class*="scroll"]');
1004
+ const hadContent = innerScrollable && innerScrollable.scrollHeight > 100;
1005
+ const scrollTop = innerScrollable ? innerScrollable.scrollTop : 0;
1006
+ const scrollHeight = innerScrollable ? innerScrollable.scrollHeight : 0;
1007
+ const clientHeight = innerScrollable ? innerScrollable.clientHeight : 0;
1008
+ const wasAtBottom = !hadContent || (scrollHeight - scrollTop - clientHeight < 100);
1460
1009
 
1461
- // Toggle switches
1462
- content.querySelectorAll('.kiro-toggle-switch, [role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1463
- const input = toggle.tagName === 'INPUT' ? toggle : toggle.querySelector('input[type="checkbox"]');
1464
- const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1465
-
1466
- wrapper.style.cursor = 'pointer';
1467
- wrapper.onclick = async (e) => {
1468
- e.preventDefault();
1469
- e.stopPropagation();
1470
-
1471
- const label = wrapper.querySelector('label');
1472
- const clickInfo = {
1473
- tag: 'div',
1474
- text: label ? label.textContent.trim() : 'toggle',
1475
- role: 'switch',
1476
- isToggle: true,
1477
- toggleId: input ? input.id : '',
1478
- checked: input ? input.checked : false
1479
- };
1480
- await sendClickToKiro(clickInfo);
1481
- return false;
1482
- };
1483
- });
1010
+ let html = getBaseStyles();
1011
+ if (currentStyles) html += `<style>${currentStyles}</style>`;
1012
+ html += snapshot.html || '';
1484
1013
 
1485
- // Send button - find and make it work
1486
- // CONFIRMED via Playwriter: The send button is <button data-variant="submit"> with codicon-arrow-up icon
1014
+ panels.chat.content.innerHTML = html;
1015
+ panels.chat.container.style.background = snapshot.bodyBg || '#1e1e1e';
1016
+ hideLoading('chat');
1487
1017
 
1488
- // Helper function to attach send handler to a button
1489
- const attachSendHandler = (btn) => {
1490
- if (!btn || btn.dataset.sendHandlerAttached) return;
1491
- btn.dataset.sendHandlerAttached = 'true';
1492
-
1493
- // Remove disabled so button is always clickable and highlighted
1494
- btn.removeAttribute('disabled');
1495
- btn.style.cursor = 'pointer';
1496
-
1497
- btn.onclick = async (e) => {
1498
- e.preventDefault();
1499
- e.stopPropagation();
1500
-
1501
- // Find the input field and get its text
1502
- const inputSelectors = ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea'];
1503
- let inputText = '';
1504
- let inputEl = null;
1505
-
1506
- for (const inputSel of inputSelectors) {
1507
- const input = content.querySelector(inputSel);
1508
- if (input) {
1509
- inputEl = input;
1510
- inputText = input.textContent || input.innerText || input.value || '';
1511
- if (inputText.trim()) break;
1512
- }
1513
- }
1018
+ removePlaceholderText();
1019
+ fixContextWindowCircles();
1020
+
1021
+ requestAnimationFrame(() => {
1022
+ requestAnimationFrame(() => {
1023
+ // Only detect history panel by class names - NOT by date patterns
1024
+ // Date patterns are too aggressive and match regular chat timestamps
1025
+ const hasHistoryClass = panels.chat.content.querySelector('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"], [class*="conversation-list"], [class*="ConversationList"]');
1514
1026
 
1515
- if (inputText.trim()) {
1516
- await sendToKiro(inputText.trim());
1517
- // Clear the input after sending
1518
- if (inputEl) {
1519
- if (inputEl.textContent !== undefined) {
1520
- inputEl.textContent = '';
1521
- inputEl.innerHTML = '';
1522
- } else if (inputEl.value !== undefined) {
1523
- inputEl.value = '';
1027
+ panels.chat.content.querySelectorAll('.overflow-y-auto, [class*="scroll"]').forEach(el => {
1028
+ const isHistoryContainer = el.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
1029
+ el.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
1030
+
1031
+ if (el.scrollHeight > el.clientHeight + 10) {
1032
+ if (isHistoryContainer) {
1033
+ el.scrollTop = 0;
1034
+ } else if (!isUpdate || wasAtBottom) {
1035
+ el.scrollTop = el.scrollHeight;
1036
+ } else if (hadContent) {
1037
+ el.scrollTop = scrollTop;
1524
1038
  }
1525
1039
  }
1526
- showToast('Sent!', 1500, true);
1527
- } else {
1528
- showToast('Type a message first', 1500);
1040
+ });
1041
+
1042
+ // Only scroll main content to top if it's clearly a history panel
1043
+ if (hasHistoryClass) {
1044
+ panels.chat.content.scrollTop = 0;
1045
+ } else if (!isUpdate || wasAtBottom) {
1046
+ panels.chat.content.scrollTop = panels.chat.content.scrollHeight;
1529
1047
  }
1530
- return false;
1531
- };
1532
- };
1533
-
1534
- // PRIMARY METHOD: Find the submit button by data-variant="submit" (CONFIRMED via Playwriter)
1535
- const submitBtn = content.querySelector('button[data-variant="submit"]');
1536
- if (submitBtn) {
1537
- attachSendHandler(submitBtn);
1538
- }
1048
+ isRendering = false;
1049
+ });
1050
+ });
1539
1051
 
1540
- // FALLBACK Method 1: Find buttons with codicon-arrow-up (Kiro's send icon)
1541
- content.querySelectorAll('button').forEach(btn => {
1542
- if (btn.dataset.sendHandlerAttached) return;
1543
- const hasArrowUp = btn.querySelector('.codicon-arrow-up, .codicon-send, [class*="arrow-up"]');
1544
- if (hasArrowUp) {
1545
- attachSendHandler(btn);
1052
+ makeInteractive();
1053
+ }
1054
+
1055
+ function removePlaceholderText() {
1056
+ const content = panels.chat.content;
1057
+ content.querySelectorAll('[class*="placeholder"], [class*="Placeholder"]').forEach(el => {
1058
+ if (!el.matches('[contenteditable], textarea, input, [data-lexical-editor]') &&
1059
+ !el.querySelector('[contenteditable], textarea, input, [data-lexical-editor]')) {
1060
+ el.style.display = 'none';
1546
1061
  }
1547
1062
  });
1548
-
1549
- // FALLBACK Method 2: Find SVGs that look like arrows
1550
- content.querySelectorAll('svg').forEach(svg => {
1551
- const svgClass = (svg.getAttribute('class') || '').toLowerCase();
1552
- const svgParent = svg.closest('button');
1553
-
1554
- if (svgClass.includes('arrow') || svgClass.includes('send') || svgClass.includes('lucide')) {
1555
- if (svgParent) {
1556
- attachSendHandler(svgParent);
1063
+ }
1064
+
1065
+ function fixContextWindowCircles() {
1066
+ panels.chat.content.querySelectorAll('svg').forEach(svg => {
1067
+ const circles = svg.querySelectorAll('circle');
1068
+ if (circles.length >= 1) {
1069
+ let progressCircle = null;
1070
+ circles.forEach(circle => {
1071
+ const dashArray = circle.getAttribute('stroke-dasharray') || circle.style.strokeDasharray;
1072
+ if (dashArray && dashArray !== 'none') progressCircle = circle;
1073
+ });
1074
+
1075
+ if (progressCircle && !svg.querySelector('circle[data-track="true"]')) {
1076
+ const cx = progressCircle.getAttribute('cx') || '8';
1077
+ const cy = progressCircle.getAttribute('cy') || '8';
1078
+ const r = progressCircle.getAttribute('r') || '6';
1079
+ const strokeWidth = progressCircle.getAttribute('stroke-width') || '2';
1080
+
1081
+ const trackCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1082
+ trackCircle.setAttribute('cx', cx);
1083
+ trackCircle.setAttribute('cy', cy);
1084
+ trackCircle.setAttribute('r', r);
1085
+ trackCircle.setAttribute('stroke', '#3c3c3c');
1086
+ trackCircle.setAttribute('stroke-width', strokeWidth);
1087
+ trackCircle.setAttribute('fill', 'none');
1088
+ trackCircle.setAttribute('data-track', 'true');
1089
+
1090
+ svg.insertBefore(trackCircle, svg.firstChild);
1091
+ progressCircle.setAttribute('stroke', '#4ade80');
1557
1092
  }
1558
1093
  }
1559
1094
  });
1095
+ }
1096
+
1097
+ // =============================================================================
1098
+ // Editor Rendering
1099
+ // =============================================================================
1100
+ function renderEditorSnapshot(data, isUpdate = false) {
1101
+ const scrollTop = panels.editor.content.scrollTop;
1102
+ panels.editor.filename.textContent = data.fileName || 'Untitled';
1103
+ panels.editor.header.style.display = 'flex';
1104
+ closeEditorSearch();
1560
1105
 
1561
- // FALLBACK Method 3: Find buttons by various selectors
1562
- const sendButtonSelectors = [
1563
- 'button[type="submit"]',
1564
- 'button[aria-label*="send" i]',
1565
- 'button[aria-label*="submit" i]',
1566
- '[class*="send-button"]',
1567
- '[class*="sendButton"]',
1568
- '[class*="submit-button"]',
1569
- '[class*="submitButton"]'
1570
- ];
1571
-
1572
- for (const selector of sendButtonSelectors) {
1573
- try {
1574
- content.querySelectorAll(selector).forEach(el => {
1575
- const btn = el.closest('button') || el;
1576
- if (btn.tagName === 'BUTTON') {
1577
- attachSendHandler(btn);
1578
- }
1579
- });
1580
- } catch(e) {}
1581
- }
1106
+ if (data.content?.trim().length > 0) {
1107
+ const lines = data.content.split('\n');
1108
+ let html = '';
1109
+
1110
+ if (data.isPartial || data.note) {
1111
+ html += `<div style="padding: 8px 12px; background: #2d2d30; color: #888; font-size: 11px; border-bottom: 1px solid #3c3c3c;">
1112
+ <span class="codicon codicon-info" style="margin-right: 6px;"></span>${data.note || 'Showing visible lines only.'}
1113
+ </div>`;
1114
+ }
1115
+
1116
+ html += '<pre class="editor-code">';
1117
+ const startLineNum = data.startLine || 1;
1118
+ let startIdx = 0;
1119
+ while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) startIdx++;
1120
+
1121
+ lines.slice(startIdx).forEach((line, idx) => {
1122
+ const lineNum = startLineNum + startIdx + idx;
1123
+ const highlighted = highlightSyntax(line.replace(/\t/g, ' '), data.language);
1124
+ html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${highlighted || ' '}</span></div>`;
1125
+ });
1126
+ html += '</pre>';
1127
+
1128
+ const endLine = startLineNum + startIdx + lines.slice(startIdx).length - 1;
1129
+ html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
1130
+ Lines ${startLineNum}-${endLine}${data.lineCount ? ` of ${data.lineCount}` : ''}
1131
+ </div>`;
1132
+
1133
+ panels.editor.content.innerHTML = html;
1134
+ panels.editor.content.style.display = 'block';
1135
+ } else {
1136
+ showEmptyState('editor', 'codicon-file-code', 'No editor content available.');
1137
+ return;
1138
+ }
1582
1139
 
1583
- // Method 3: Find ALL buttons and attach handler to ones that look like send buttons
1584
- // This is more reliable than using getBoundingClientRect which doesn't work before layout
1585
- content.querySelectorAll('button').forEach(btn => {
1586
- if (btn.dataset.sendHandlerAttached) return;
1140
+ hideLoading('editor');
1141
+ if (isUpdate) panels.editor.content.scrollTop = scrollTop;
1142
+ }
1143
+
1144
+ function highlightSyntax(line, language) {
1145
+ const escaped = escapeHtml(line);
1146
+ if (escaped.length > 1000 || !language) return escaped;
1147
+
1148
+ const isJS = ['javascript', 'typescript', 'jsx', 'tsx', 'js', 'ts'].includes(language);
1149
+ const isPython = language === 'python' || language === 'py';
1150
+
1151
+ let result = '';
1152
+ let i = 0;
1153
+
1154
+ while (i < escaped.length) {
1155
+ // Comments
1156
+ if (isJS && escaped.slice(i, i + 2) === '//') { result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>'; break; }
1157
+ if ((isPython) && escaped[i] === '#') { result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>'; break; }
1158
+
1159
+ // Strings
1160
+ if (escaped.slice(i, i + 6) === '&quot;') {
1161
+ const endIdx = escaped.indexOf('&quot;', i + 6);
1162
+ if (endIdx !== -1) { result += '<span class="tok-str">' + escaped.slice(i, endIdx + 6) + '</span>'; i = endIdx + 6; continue; }
1163
+ }
1164
+ if (escaped.slice(i, i + 5) === '&#39;') {
1165
+ const endIdx = escaped.indexOf('&#39;', i + 5);
1166
+ if (endIdx !== -1) { result += '<span class="tok-str">' + escaped.slice(i, endIdx + 5) + '</span>'; i = endIdx + 5; continue; }
1167
+ }
1587
1168
 
1588
- const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
1589
- const className = (btn.className || '').toLowerCase();
1590
- const innerHTML = btn.innerHTML.toLowerCase();
1169
+ // Numbers
1170
+ if (/[0-9]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z_]/.test(escaped[i - 1]))) {
1171
+ let numEnd = i;
1172
+ while (numEnd < escaped.length && /[0-9.]/.test(escaped[numEnd])) numEnd++;
1173
+ if (numEnd > i) { result += '<span class="tok-num">' + escaped.slice(i, numEnd) + '</span>'; i = numEnd; continue; }
1174
+ }
1591
1175
 
1592
- // Skip buttons that are clearly NOT send buttons
1593
- if (ariaLabel.includes('context') || ariaLabel.includes('model') ||
1594
- ariaLabel.includes('close') || ariaLabel.includes('menu') ||
1595
- className.includes('context') || className.includes('model') ||
1596
- className.includes('dropdown') || className.includes('toggle') ||
1597
- className.includes('close') || className.includes('menu')) {
1598
- return;
1176
+ // Keywords
1177
+ if (/[a-zA-Z_]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z0-9_]/.test(escaped[i - 1]))) {
1178
+ let wordEnd = i;
1179
+ while (wordEnd < escaped.length && /[a-zA-Z0-9_]/.test(escaped[wordEnd])) wordEnd++;
1180
+ const word = escaped.slice(i, wordEnd);
1181
+
1182
+ let keywords = [];
1183
+ if (isJS) keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'class', 'extends', 'true', 'false', 'null', 'undefined'];
1184
+ else if (isPython) keywords = ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import', 'from', 'as', 'try', 'except', 'raise', 'with', 'async', 'await', 'None', 'True', 'False', 'self'];
1185
+
1186
+ if (keywords.includes(word)) result += '<span class="tok-kw">' + word + '</span>';
1187
+ else result += word;
1188
+ i = wordEnd;
1189
+ continue;
1599
1190
  }
1600
1191
 
1601
- // Check if button contains an arrow SVG (common for send buttons)
1602
- const hasSvg = btn.querySelector('svg');
1603
- const hasArrowPath = btn.querySelector('path, line, polyline');
1192
+ result += escaped[i];
1193
+ i++;
1194
+ }
1195
+
1196
+ return result;
1197
+ }
1198
+
1199
+ // =============================================================================
1200
+ // Editor Search
1201
+ // =============================================================================
1202
+ function openEditorSearch() {
1203
+ $('editorSearchBar').classList.add('open');
1204
+ $('editorSearchInput').focus();
1205
+ $('editorSearchInput').select();
1206
+ }
1207
+
1208
+ function closeEditorSearch() {
1209
+ $('editorSearchBar').classList.remove('open');
1210
+ $('editorSearchInput').value = '';
1211
+ searchMatches = [];
1212
+ currentMatchIndex = -1;
1213
+ $('editorSearchCount').textContent = '';
1214
+ panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]').forEach(el => {
1215
+ el.innerHTML = el.dataset.originalHtml;
1216
+ delete el.dataset.originalHtml;
1217
+ });
1218
+ }
1219
+
1220
+ function performSearch(query) {
1221
+ panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]').forEach(el => {
1222
+ el.innerHTML = el.dataset.originalHtml;
1223
+ delete el.dataset.originalHtml;
1224
+ });
1225
+ searchMatches = [];
1226
+ currentMatchIndex = -1;
1227
+
1228
+ if (!query || query.length < 1) { $('editorSearchCount').textContent = ''; return; }
1229
+
1230
+ const queryLower = query.toLowerCase();
1231
+ panels.editor.content.querySelectorAll('.editor-line-code').forEach((lineEl, lineIndex) => {
1232
+ const text = lineEl.textContent;
1233
+ const textLower = text.toLowerCase();
1234
+ let startIndex = 0, matchIndex;
1604
1235
 
1605
- // If button has an SVG with arrow-like elements, it's likely a send button
1606
- if (hasSvg && hasArrowPath) {
1607
- attachSendHandler(btn);
1236
+ while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
1237
+ searchMatches.push({ lineIndex, lineEl, startIndex: matchIndex, endIndex: matchIndex + query.length });
1238
+ startIndex = matchIndex + 1;
1608
1239
  }
1609
1240
  });
1610
1241
 
1242
+ if (searchMatches.length > 0) {
1243
+ highlightSearchMatches(query);
1244
+ currentMatchIndex = 0;
1245
+ scrollToSearchMatch(0);
1246
+ updateSearchCount();
1247
+ } else {
1248
+ $('editorSearchCount').textContent = 'No results';
1249
+ }
1250
+ }
1251
+
1252
+ function highlightSearchMatches(query) {
1253
+ const queryLower = query.toLowerCase();
1254
+ panels.editor.content.querySelectorAll('.editor-line-code').forEach(lineEl => {
1255
+ const text = lineEl.textContent;
1256
+ if (text.toLowerCase().includes(queryLower)) {
1257
+ if (!lineEl.dataset.originalHtml) lineEl.dataset.originalHtml = lineEl.innerHTML;
1258
+
1259
+ const matches = [];
1260
+ let idx = 0;
1261
+ while ((idx = text.toLowerCase().indexOf(queryLower, idx)) !== -1) {
1262
+ matches.push({ start: idx, end: idx + query.length });
1263
+ idx++;
1264
+ }
1265
+
1266
+ if (matches.length > 0) {
1267
+ let result = '', lastEnd = 0;
1268
+ matches.forEach(match => {
1269
+ result += escapeHtml(text.slice(lastEnd, match.start));
1270
+ result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
1271
+ lastEnd = match.end;
1272
+ });
1273
+ result += escapeHtml(text.slice(lastEnd));
1274
+ lineEl.innerHTML = result;
1275
+ }
1276
+ }
1277
+ });
1278
+ }
1279
+
1280
+ function scrollToSearchMatch(index) {
1281
+ const highlights = panels.editor.content.querySelectorAll('.search-highlight');
1282
+ highlights.forEach(el => el.classList.remove('current'));
1283
+
1284
+ if (highlights[index]) {
1285
+ highlights[index].classList.add('current');
1286
+ const lineEl = highlights[index].closest('.editor-line');
1287
+ if (lineEl) {
1288
+ const container = panels.editor.content;
1289
+ const containerRect = container.getBoundingClientRect();
1290
+ const lineRect = lineEl.getBoundingClientRect();
1291
+ const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
1292
+ container.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' });
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ function updateSearchCount() {
1298
+ $('editorSearchCount').textContent = searchMatches.length > 0 ? `${currentMatchIndex + 1}/${searchMatches.length}` : 'No results';
1299
+ }
1300
+
1301
+ function goToNextMatch() {
1302
+ if (searchMatches.length === 0) return;
1303
+ currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
1304
+ scrollToSearchMatch(currentMatchIndex);
1305
+ updateSearchCount();
1306
+ }
1307
+
1308
+ function goToPrevMatch() {
1309
+ if (searchMatches.length === 0) return;
1310
+ currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
1311
+ scrollToSearchMatch(currentMatchIndex);
1312
+ updateSearchCount();
1313
+ }
1314
+
1315
+ // Setup search
1316
+ $('editorSearchBtn').onclick = openEditorSearch;
1317
+ $('editorSearchClose').onclick = closeEditorSearch;
1318
+ $('editorSearchNext').onclick = goToNextMatch;
1319
+ $('editorSearchPrev').onclick = goToPrevMatch;
1320
+ $('editorSearchInput').oninput = (e) => performSearch(e.target.value);
1321
+ $('editorSearchInput').onkeydown = (e) => {
1322
+ if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? goToPrevMatch() : goToNextMatch(); }
1323
+ else if (e.key === 'Escape') closeEditorSearch();
1324
+ };
1325
+ document.addEventListener('keydown', (e) => {
1326
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') { e.preventDefault(); openEditorSearch(); }
1327
+ });
1328
+
1329
+ // =============================================================================
1330
+ // File Tree
1331
+ // =============================================================================
1332
+ function toggleFileTree() {
1333
+ fileTreeOpen = !fileTreeOpen;
1334
+ fileTreeDropdown.classList.toggle('open', fileTreeOpen);
1335
+ explorerChevron.style.transform = fileTreeOpen ? 'rotate(180deg)' : '';
1336
+ if (fileTreeOpen && workspaceFiles.length === 0) fetchWorkspaceFiles();
1337
+ else if (fileTreeOpen) renderFileTree(workspaceFiles);
1338
+ }
1339
+
1340
+ function closeFileTree() {
1341
+ fileTreeOpen = false;
1342
+ fileTreeDropdown.classList.remove('open');
1343
+ explorerChevron.style.transform = '';
1344
+ }
1345
+
1346
+ async function fetchWorkspaceFiles() {
1347
+ if (!selectedCascadeId) return;
1348
+ fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1349
+
1350
+ try {
1351
+ const r = await fetch(`/files/${selectedCascadeId}`);
1352
+ if (!r.ok) throw new Error('Failed to fetch files');
1353
+ const data = await r.json();
1354
+ workspaceFiles = data.files || [];
1355
+ renderFileTree(workspaceFiles);
1356
+ } catch (e) {
1357
+ fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1358
+ }
1359
+ }
1360
+
1361
+ function buildFileTree(files) {
1362
+ const root = { folders: {}, files: [] };
1363
+ files.forEach(file => {
1364
+ const parts = file.path.split(/[/\\]/);
1365
+ let current = root;
1366
+ for (let i = 0; i < parts.length - 1; i++) {
1367
+ const folderName = parts[i];
1368
+ if (!current.folders[folderName]) current.folders[folderName] = { folders: {}, files: [] };
1369
+ current = current.folders[folderName];
1370
+ }
1371
+ current.files.push({ name: parts[parts.length - 1], path: file.path });
1372
+ });
1373
+ return root;
1374
+ }
1375
+
1376
+ function getFileIcon(name) {
1377
+ const ext = name.split('.').pop()?.toLowerCase();
1378
+ const iconMap = {
1379
+ 'js': 'codicon-file-code', 'jsx': 'codicon-file-code', 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1380
+ 'html': 'codicon-file-code', 'css': 'codicon-file-code', 'json': 'codicon-json', 'md': 'codicon-markdown',
1381
+ 'py': 'codicon-file-code', 'java': 'codicon-file-code', 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
1382
+ 'sql': 'codicon-database', 'txt': 'codicon-file'
1383
+ };
1384
+ return iconMap[ext] || 'codicon-file';
1385
+ }
1386
+
1387
+ function renderTreeNode(node, path = '') {
1388
+ let html = '';
1389
+ const folderNames = Object.keys(node.folders).sort();
1390
+ const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
1391
+
1392
+ folderNames.forEach(folderName => {
1393
+ const folderPath = path ? `${path}/${folderName}` : folderName;
1394
+ const isExpanded = expandedFolders.has(folderPath);
1395
+ html += `<div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
1396
+ <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
1397
+ <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
1398
+ <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #dcb67a;"></span>
1399
+ <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
1400
+ </div>
1401
+ <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">${renderTreeNode(node.folders[folderName], folderPath)}</div>
1402
+ </div>`;
1403
+ });
1404
+
1405
+ sortedFiles.forEach(file => {
1406
+ html += `<div class="file-tree-file" data-path="${escapeHtml(file.path)}">
1407
+ <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
1408
+ <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
1409
+ </div>`;
1410
+ });
1411
+
1412
+ return html;
1413
+ }
1414
+
1415
+ function renderFileTree(files) {
1416
+ if (files.length === 0) { fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>'; return; }
1417
+
1418
+ const tree = buildFileTree(files);
1419
+ fileTree.innerHTML = renderTreeNode(tree);
1420
+
1421
+ fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
1422
+ header.onclick = (e) => {
1423
+ e.stopPropagation();
1424
+ const folderPath = header.dataset.folder;
1425
+ const folder = header.closest('.file-tree-folder');
1426
+ const contents = folder.querySelector('.file-tree-folder-contents');
1427
+ const chevron = header.querySelector('.file-tree-folder-icon');
1428
+ const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
1429
+
1430
+ if (expandedFolders.has(folderPath)) {
1431
+ expandedFolders.delete(folderPath);
1432
+ contents.classList.remove('expanded');
1433
+ chevron.classList.remove('expanded');
1434
+ folderIcon.classList.remove('codicon-folder-opened');
1435
+ folderIcon.classList.add('codicon-folder');
1436
+ } else {
1437
+ expandedFolders.add(folderPath);
1438
+ contents.classList.add('expanded');
1439
+ chevron.classList.add('expanded');
1440
+ folderIcon.classList.remove('codicon-folder');
1441
+ folderIcon.classList.add('codicon-folder-opened');
1442
+ }
1443
+ };
1444
+ });
1445
+
1446
+ fileTree.querySelectorAll('.file-tree-file').forEach(item => {
1447
+ item.onclick = () => { closeFileTree(); openFileInEditor(item.dataset.path); };
1448
+ });
1449
+ }
1450
+
1451
+ // =============================================================================
1452
+ // Chat Interactivity
1453
+ // =============================================================================
1454
+ function makeInteractive() {
1455
+ const content = panels.chat.content;
1456
+
1457
+ // Input fields
1458
+ const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1459
+ inputSelectors.forEach(selector => {
1460
+ content.querySelectorAll(selector).forEach(el => {
1461
+ el.style.cursor = 'text';
1462
+ el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1463
+ el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1464
+ el.addEventListener('keydown', async (e) => {
1465
+ if (e.key === 'Enter' && !e.shiftKey) {
1466
+ e.preventDefault();
1467
+ const text = el.textContent || el.value || '';
1468
+ if (text.trim()) {
1469
+ await sendToKiro(text.trim());
1470
+ el.textContent = ''; el.innerHTML = '';
1471
+ isTypingInKiroInput = false;
1472
+ }
1473
+ }
1474
+ });
1475
+ });
1476
+ });
1477
+
1478
+ // Send button
1479
+ const attachSendHandler = (btn) => {
1480
+ if (!btn || btn.dataset.sendHandlerAttached) return;
1481
+ btn.dataset.sendHandlerAttached = 'true';
1482
+ btn.removeAttribute('disabled');
1483
+ btn.style.cursor = 'pointer';
1484
+
1485
+ btn.onclick = async (e) => {
1486
+ e.preventDefault(); e.stopPropagation();
1487
+ let inputText = '', inputEl = null;
1488
+
1489
+ // Find the ACTUAL chat input (last visible editor, not old messages)
1490
+ // Look for input containers first, then fall back to last visible editor
1491
+ const inputContainerSelectors = [
1492
+ '[class*="chat-input"]',
1493
+ '[class*="message-input"]',
1494
+ '[class*="composer"]',
1495
+ '[class*="input-area"]',
1496
+ '[class*="InputArea"]',
1497
+ 'form[class*="chat"]'
1498
+ ];
1499
+
1500
+ // Strategy 1: Find editor inside input container
1501
+ for (const containerSel of inputContainerSelectors) {
1502
+ const container = content.querySelector(containerSel);
1503
+ if (container) {
1504
+ const editorInContainer = container.querySelector('.tiptap, .ProseMirror, [data-lexical-editor="true"], [contenteditable="true"], textarea');
1505
+ if (editorInContainer && editorInContainer.offsetParent !== null) {
1506
+ inputEl = editorInContainer;
1507
+ inputText = editorInContainer.textContent || editorInContainer.innerText || editorInContainer.value || '';
1508
+ break;
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ // Strategy 2: Find editor near the submit button itself
1514
+ if (!inputEl) {
1515
+ const btnParent = btn.closest('form') || btn.parentElement?.parentElement?.parentElement;
1516
+ if (btnParent) {
1517
+ const nearbyEditor = btnParent.querySelector('.tiptap, .ProseMirror, [data-lexical-editor="true"], [contenteditable="true"], textarea');
1518
+ if (nearbyEditor && nearbyEditor.offsetParent !== null) {
1519
+ inputEl = nearbyEditor;
1520
+ inputText = nearbyEditor.textContent || nearbyEditor.innerText || nearbyEditor.value || '';
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ // Strategy 3: Get the LAST visible editor (input is typically at bottom)
1526
+ if (!inputEl) {
1527
+ for (const sel of ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea']) {
1528
+ const allEditors = [...content.querySelectorAll(sel)].filter(el => el.offsetParent !== null);
1529
+ if (allEditors.length > 0) {
1530
+ inputEl = allEditors.at(-1); // Get LAST one, not first
1531
+ inputText = inputEl.textContent || inputEl.innerText || inputEl.value || '';
1532
+ if (inputText.trim()) break;
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ if (inputText.trim()) {
1538
+ await sendToKiro(inputText.trim());
1539
+ if (inputEl) { inputEl.textContent = ''; inputEl.innerHTML = ''; }
1540
+ showToast('Sent!', 1500, true);
1541
+ } else {
1542
+ showToast('Type a message first', 1500);
1543
+ }
1544
+ return false;
1545
+ };
1546
+ };
1547
+
1548
+ const submitBtn = content.querySelector('button[data-variant="submit"]');
1549
+ if (submitBtn) attachSendHandler(submitBtn);
1550
+
1551
+ content.querySelectorAll('button').forEach(btn => {
1552
+ if (btn.dataset.sendHandlerAttached) return;
1553
+ if (btn.querySelector('.codicon-arrow-up, .codicon-send')) attachSendHandler(btn);
1554
+ });
1555
+
1556
+ // Toggle switches
1557
+ content.querySelectorAll('[role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1558
+ const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1559
+ wrapper.style.cursor = 'pointer';
1560
+ wrapper.onclick = async (e) => {
1561
+ e.preventDefault(); e.stopPropagation();
1562
+ const label = wrapper.querySelector('label');
1563
+ await sendClickToKiro({ tag: 'div', text: label ? label.textContent.trim() : 'toggle', role: 'switch', isToggle: true });
1564
+ return false;
1565
+ };
1566
+ });
1567
+
1611
1568
  // Tabs
1612
1569
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1613
1570
  const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
1614
-
1615
1571
  if (closeBtn) {
1616
1572
  closeBtn.style.cursor = 'pointer';
1617
1573
  closeBtn.onclick = async (e) => {
1618
- e.preventDefault();
1619
- e.stopPropagation();
1620
- await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true });
1574
+ e.preventDefault(); e.stopPropagation();
1575
+ const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1576
+ const tabLabel = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1577
+ await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true, parentTabLabel: tabLabel });
1621
1578
  return false;
1622
1579
  };
1623
1580
  }
@@ -1625,129 +1582,167 @@
1625
1582
  tab.style.cursor = 'pointer';
1626
1583
  tab.onclick = async (e) => {
1627
1584
  if (e.target.closest('[aria-label="close"], [class*="close"], button')) return;
1628
- e.preventDefault();
1629
- e.stopPropagation();
1630
-
1585
+ e.preventDefault(); e.stopPropagation();
1631
1586
  const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1632
1587
  const labelText = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1633
-
1634
1588
  await sendClickToKiro({ tag: 'div', text: labelText, role: 'tab', isTab: true, tabLabel: labelText });
1635
1589
  return false;
1636
1590
  };
1637
1591
  });
1638
1592
 
1639
- // File links - detect file paths in chat and make them clickable to open in Editor
1640
- 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;
1641
- const filePathPattern = /^[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+$/;
1642
-
1643
- // Look for elements that might contain file paths
1593
+ // File links
1594
+ 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)$/i;
1644
1595
  content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
1645
1596
  if (el.onclick) return;
1646
1597
  const text = (el.textContent || '').trim();
1647
1598
  const dataPath = el.getAttribute('data-path') || '';
1648
1599
  const href = el.getAttribute('href') || '';
1649
1600
 
1650
- // Check if this looks like a file path
1651
- const isFilePath = (
1652
- fileExtensions.test(text) ||
1653
- fileExtensions.test(dataPath) ||
1654
- fileExtensions.test(href) ||
1655
- (filePathPattern.test(text) && text.includes('/'))
1656
- );
1657
-
1658
- if (isFilePath) {
1601
+ if (fileExtensions.test(text) || fileExtensions.test(dataPath) || fileExtensions.test(href)) {
1659
1602
  const filePath = dataPath || text || href;
1660
1603
  el.style.cursor = 'pointer';
1661
- el.style.textDecoration = 'underline';
1662
1604
  el.style.color = '#7eb6ff';
1663
1605
  el.title = `Open ${filePath} in Editor`;
1664
-
1665
- el.onclick = async (e) => {
1666
- e.preventDefault();
1667
- e.stopPropagation();
1668
- await openFileInEditor(filePath);
1669
- return false;
1670
- };
1606
+ el.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); await openFileInEditor(filePath); return false; };
1671
1607
  }
1672
1608
  });
1673
1609
 
1674
1610
  // Buttons and clickable elements
1675
1611
  const clickableSelectors = ['button', '[role="button"]', '[role="menuitem"]', '[role="option"]', '[role="checkbox"]', 'a[href]', '[tabindex="0"]'];
1676
1612
  const allClickables = new Set();
1677
- clickableSelectors.forEach(sel => {
1678
- try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {}
1679
- });
1613
+ clickableSelectors.forEach(sel => { try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {} });
1680
1614
 
1681
- // Also add dropdown menu items
1682
- content.querySelectorAll('[class*="dropdown-item"], [class*="dropdownItem"], [class*="model-option"], [class*="modelOption"]').forEach(el => {
1683
- allClickables.add(el);
1615
+ allClickables.forEach(el => {
1616
+ if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1617
+ if (el.closest('[role="tab"]') || el.closest('.kiro-toggle-switch') || el.onclick) return;
1618
+
1619
+ el.style.cursor = 'pointer';
1620
+ el.onclick = async (e) => {
1621
+ e.preventDefault(); e.stopPropagation();
1622
+ const clickInfo = {
1623
+ tag: el.tagName?.toLowerCase() || '',
1624
+ text: (el.textContent || '').trim().substring(0, 100),
1625
+ ariaLabel: el.getAttribute('aria-label') || '',
1626
+ role: el.getAttribute('role') || '',
1627
+ className: el.className || ''
1628
+ };
1629
+ await sendClickToKiro(clickInfo);
1630
+ return false;
1631
+ };
1684
1632
  });
1685
1633
 
1686
- // ADDED: Support for History panel items and other cursor-pointer elements
1687
- // History items are divs with cursor-pointer class containing session info
1688
- content.querySelectorAll('[class*="cursor-pointer"], [class*="hover\\:"], [class*="group"]').forEach(el => {
1689
- // Check if this looks like a history/session item (contains date pattern)
1634
+ // Chat history items - detect and make clickable
1635
+ // Kiro history items typically have: icon, title text, and date
1636
+ // We need to find these items even if they don't have semantic roles
1637
+
1638
+ // First, find all potential history item containers
1639
+ const potentialHistoryItems = [];
1640
+
1641
+ // Look for elements that contain date patterns (history items have dates)
1642
+ const datePattern = /\d{1,2}\/\d{1,2}\/\d{4}|\d{1,2}:\d{2}:\d{2}\s*(AM|PM)?/i;
1643
+
1644
+ content.querySelectorAll('div, li, article, section').forEach(el => {
1645
+ // Skip if already has handler
1646
+ if (el.onclick) return;
1647
+ // Skip inputs
1648
+ if (el.matches('[contenteditable], textarea, input')) return;
1649
+ // Skip very large containers
1650
+ if (el.children.length > 15) return;
1651
+ // Skip if inside tab or toggle
1652
+ if (el.closest('[role="tab"]') || el.closest('.kiro-toggle-switch')) return;
1653
+
1690
1654
  const text = el.textContent || '';
1691
- const hasDate = /\d{1,2}\/\d{1,2}\/\d{4}/.test(text);
1692
- const hasTime = /\d{1,2}:\d{2}/.test(text);
1693
- if (hasDate || hasTime || el.classList.contains('cursor-pointer')) {
1694
- allClickables.add(el);
1655
+ const hasDate = datePattern.test(text);
1656
+ const hasIcon = el.querySelector('svg, [class*="icon"], [class*="Icon"], .codicon');
1657
+ const textLength = text.trim().length;
1658
+
1659
+ // History items typically have: date + some text (20-500 chars) + possibly an icon
1660
+ if (hasDate && textLength > 20 && textLength < 500) {
1661
+ // Check if this looks like a list item (has siblings with similar structure)
1662
+ const parent = el.parentElement;
1663
+ const siblings = parent ? [...parent.children].filter(c => c.tagName === el.tagName) : [];
1664
+ const isInList = siblings.length > 1;
1665
+
1666
+ // Also check for cursor pointer style or clickable class
1667
+ const computedStyle = window.getComputedStyle(el);
1668
+ const hasPointer = computedStyle.cursor === 'pointer';
1669
+ const hasClickableClass = el.className && (
1670
+ el.className.includes('item') ||
1671
+ el.className.includes('Item') ||
1672
+ el.className.includes('row') ||
1673
+ el.className.includes('Row') ||
1674
+ el.className.includes('entry') ||
1675
+ el.className.includes('Entry')
1676
+ );
1677
+
1678
+ if (isInList || hasPointer || hasClickableClass || hasIcon) {
1679
+ potentialHistoryItems.push(el);
1680
+ }
1695
1681
  }
1696
1682
  });
1697
1683
 
1698
- // Also look for any div that has inline cursor:pointer style
1699
- content.querySelectorAll('div, span').forEach(el => {
1700
- const style = el.getAttribute('style') || '';
1701
- if (style.includes('cursor') && style.includes('pointer')) {
1702
- allClickables.add(el);
1703
- }
1684
+ // Also add standard selectors
1685
+ const historySelectors = [
1686
+ '[role="listitem"]',
1687
+ '[role="option"]',
1688
+ '[class*="history-item"]',
1689
+ '[class*="historyItem"]',
1690
+ '[class*="session-item"]',
1691
+ '[class*="sessionItem"]',
1692
+ '[class*="chat-item"]',
1693
+ '[class*="chatItem"]'
1694
+ ];
1695
+
1696
+ historySelectors.forEach(selector => {
1697
+ try {
1698
+ content.querySelectorAll(selector).forEach(el => {
1699
+ if (!el.onclick && !potentialHistoryItems.includes(el)) {
1700
+ potentialHistoryItems.push(el);
1701
+ }
1702
+ });
1703
+ } catch(e) {}
1704
1704
  });
1705
1705
 
1706
- allClickables.forEach(el => {
1707
- if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1708
- if (el.closest('[role="tab"]')) return;
1709
- if (el.closest('.kiro-toggle-switch')) return;
1710
- if (el.onclick) return;
1711
-
1706
+ // Attach click handlers to all potential history items
1707
+ potentialHistoryItems.forEach(el => {
1712
1708
  el.style.cursor = 'pointer';
1713
1709
  el.onclick = async (e) => {
1714
1710
  e.preventDefault();
1715
1711
  e.stopPropagation();
1716
- const clickInfo = getElementClickInfo(el);
1717
1712
 
1718
- // Check if this is a model selection item
1719
- if (el.matches('[role="option"], [role="menuitem"], [class*="model"], [class*="Model"]') ||
1720
- el.closest('[role="listbox"], [role="menu"], [class*="dropdown-menu"], [class*="dropdownMenu"]')) {
1721
- clickInfo.isModelSelection = true;
1722
- clickInfo.modelName = el.textContent?.trim().split('\n')[0] || '';
1723
- }
1713
+ // Extract the title (first meaningful text, not the date)
1714
+ let itemText = '';
1715
+ const textNodes = [];
1716
+ el.querySelectorAll('span, p, div').forEach(child => {
1717
+ const t = child.textContent?.trim();
1718
+ if (t && t.length > 5 && !datePattern.test(t)) {
1719
+ textNodes.push(t);
1720
+ }
1721
+ });
1722
+ itemText = textNodes[0] || el.textContent?.trim().split('\n')[0] || '';
1723
+ itemText = itemText.substring(0, 100);
1724
+
1725
+ const clickInfo = {
1726
+ tag: el.tagName?.toLowerCase() || 'div',
1727
+ text: itemText,
1728
+ ariaLabel: el.getAttribute('aria-label') || '',
1729
+ role: el.getAttribute('role') || 'listitem',
1730
+ className: el.className || '',
1731
+ isHistoryItem: true
1732
+ };
1724
1733
 
1725
1734
  await sendClickToKiro(clickInfo);
1735
+
1736
+ // Refresh snapshot after clicking history item
1737
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
1726
1738
  return false;
1727
1739
  };
1728
1740
  });
1729
1741
  }
1730
-
1731
- function getElementClickInfo(el) {
1732
- const info = {
1733
- tag: el.tagName?.toLowerCase() || '',
1734
- text: (el.textContent || '').trim().substring(0, 100),
1735
- ariaLabel: el.getAttribute('aria-label') || '',
1736
- title: el.getAttribute('title') || '',
1737
- role: el.getAttribute('role') || '',
1738
- className: el.className || '',
1739
- isTab: el.getAttribute('role') === 'tab',
1740
- dataTestId: el.getAttribute('data-testid') || ''
1741
- };
1742
-
1743
- if (!info.ariaLabel) {
1744
- const parent = el.closest('[aria-label]');
1745
- if (parent) info.ariaLabel = parent.getAttribute('aria-label') || '';
1746
- }
1747
-
1748
- return info;
1749
- }
1750
-
1742
+
1743
+ // =============================================================================
1744
+ // Kiro Communication
1745
+ // =============================================================================
1751
1746
  async function sendToKiro(message) {
1752
1747
  if (!message || !selectedCascadeId) return;
1753
1748
  try {
@@ -1763,21 +1758,15 @@
1763
1758
  showToast('Failed to send');
1764
1759
  }
1765
1760
  }
1766
-
1761
+
1767
1762
  async function sendClickToKiro(clickInfo) {
1768
1763
  if (!selectedCascadeId) return;
1769
1764
 
1770
- const isNavigation =
1771
- clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1765
+ const isNavigation = clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1772
1766
  clickInfo.ariaLabel?.toLowerCase().includes('close') ||
1773
1767
  clickInfo.text?.toLowerCase().includes('back') ||
1774
1768
  clickInfo.role === 'tab';
1775
1769
 
1776
- const isModelSelection = clickInfo.isModelSelection ||
1777
- clickInfo.role === 'option' ||
1778
- clickInfo.role === 'menuitem' ||
1779
- clickInfo.className?.includes('model');
1780
-
1781
1770
  if (isNavigation) navigationPending = true;
1782
1771
 
1783
1772
  try {
@@ -1787,39 +1776,30 @@
1787
1776
  body: JSON.stringify(clickInfo)
1788
1777
  });
1789
1778
 
1790
- if (isNavigation || isModelSelection) {
1791
- // Refresh snapshot after navigation or model selection
1792
- setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1793
- }
1779
+ if (isNavigation) setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1794
1780
  } catch (e) {
1795
1781
  navigationPending = false;
1796
1782
  }
1797
1783
  }
1798
-
1799
- // Open file in Editor tab
1784
+
1800
1785
  async function openFileInEditor(filePath) {
1801
1786
  if (!selectedCascadeId || !filePath) return;
1802
1787
 
1803
1788
  showToast(`Opening ${filePath}...`, 1500);
1804
1789
 
1805
- // Switch to Editor tab immediately
1790
+ // Switch to Editor tab
1806
1791
  const editorTab = document.querySelector('[data-panel="editor"]');
1807
1792
  if (editorTab) {
1808
1793
  navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1809
- editorTab.classList.add('active');
1810
-
1811
- Object.values(panels).forEach(p => {
1812
- if (p.panel) p.panel.classList.remove('active');
1813
- });
1794
+ editorTab.classList.add('active');
1795
+ Object.values(panels).forEach(p => { if (p.panel) p.panel.classList.remove('active'); });
1814
1796
  panels.editor.panel.classList.add('active');
1815
1797
  activePanel = 'editor';
1816
-
1817
- // Show loading state
1818
1798
  showLoading('editor', 'Loading file...');
1819
1799
  }
1820
1800
 
1821
1801
  try {
1822
- // First, try to read the file directly from filesystem (bypasses Monaco limitation)
1802
+ // Try direct file read first
1823
1803
  const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
1824
1804
  method: 'POST',
1825
1805
  headers: { 'Content-Type': 'application/json' },
@@ -1831,472 +1811,61 @@
1831
1811
  if (data.content) {
1832
1812
  renderEditorSnapshot(data);
1833
1813
  showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1834
- console.log('[Editor] Loaded file directly:', data.fileName, data.lineCount, 'lines');
1835
1814
  return;
1836
1815
  }
1837
- } else {
1838
- const errorData = await readResult.json().catch(() => ({}));
1839
- console.log('[Editor] Direct file read failed:', errorData.error || readResult.status);
1840
1816
  }
1841
1817
 
1842
- // Fallback: Try to click on the file link in Kiro to open it in Monaco
1843
- console.log('[Editor] Falling back to Monaco capture...');
1844
- const clickResult = await fetch(`/click/${selectedCascadeId}`, {
1818
+ // Fallback: Click file link in Kiro
1819
+ await fetch(`/click/${selectedCascadeId}`, {
1845
1820
  method: 'POST',
1846
1821
  headers: { 'Content-Type': 'application/json' },
1847
- body: JSON.stringify({
1848
- tag: 'a',
1849
- text: filePath,
1850
- isFileLink: true,
1851
- filePath: filePath
1852
- })
1822
+ body: JSON.stringify({ tag: 'a', text: filePath, isFileLink: true, filePath })
1853
1823
  });
1854
1824
 
1855
- const result = await clickResult.json();
1856
- console.log('Click result:', result);
1857
-
1858
- // Wait for the file to open in Kiro
1859
1825
  await new Promise(resolve => setTimeout(resolve, 800));
1860
1826
 
1861
- // Fetch the editor content from Monaco
1862
1827
  const fetchEditor = async () => {
1863
1828
  try {
1864
1829
  const r = await fetch(`/editor/${selectedCascadeId}`);
1865
1830
  if (r.ok) {
1866
1831
  const data = await r.json();
1867
- if (data.content || data.html) {
1868
- renderEditorSnapshot(data);
1869
- showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1870
- return true;
1871
- }
1832
+ if (data.content || data.html) { renderEditorSnapshot(data); showToast(`Opened ${data.fileName || filePath}`, 1500, true); return true; }
1872
1833
  }
1873
1834
  } catch (e) {}
1874
1835
  return false;
1875
1836
  };
1876
1837
 
1877
- // Try fetching a few times with delays
1878
1838
  if (!await fetchEditor()) {
1879
1839
  await new Promise(resolve => setTimeout(resolve, 500));
1880
1840
  if (!await fetchEditor()) {
1881
- await new Promise(resolve => setTimeout(resolve, 500));
1882
- if (!await fetchEditor()) {
1883
- showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}. File may not exist or path is incorrect.`);
1884
- }
1841
+ showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}.`);
1885
1842
  }
1886
1843
  }
1887
-
1888
1844
  } catch (e) {
1889
1845
  showToast('Failed to open file');
1890
- console.error('Open file error:', e);
1891
- showEmptyState('editor', 'codicon-file-code', 'Failed to load file. Check console for details.');
1892
- }
1893
- }
1894
-
1895
- // ==================== File Tree ====================
1896
-
1897
- // Toggle file tree panel
1898
- function toggleFileTree() {
1899
- fileTreeOpen = !fileTreeOpen;
1900
- fileTreePanel.classList.toggle('open', fileTreeOpen);
1901
-
1902
- if (fileTreeOpen) {
1903
- // Load files if not cached
1904
- if (workspaceFiles.length === 0) {
1905
- fetchWorkspaceFiles();
1906
- } else {
1907
- renderFileTree(workspaceFiles);
1908
- }
1909
- }
1910
- }
1911
-
1912
- // Close file tree
1913
- function closeFileTree() {
1914
- fileTreeOpen = false;
1915
- fileTreePanel.classList.remove('open');
1916
- }
1917
-
1918
- // Fetch workspace files from server
1919
- async function fetchWorkspaceFiles() {
1920
- if (!selectedCascadeId) return;
1921
-
1922
- fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1923
-
1924
- try {
1925
- const r = await fetch(`/files/${selectedCascadeId}`);
1926
- if (!r.ok) throw new Error('Failed to fetch files');
1927
-
1928
- const data = await r.json();
1929
- workspaceFiles = data.files || [];
1930
- console.log(`[FileTree] Loaded ${workspaceFiles.length} files`);
1931
- renderFileTree(workspaceFiles);
1932
- } catch (e) {
1933
- console.error('Failed to fetch files:', e);
1934
- fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1935
- }
1936
- }
1937
-
1938
- // Build tree structure from flat file list
1939
- function buildFileTree(files) {
1940
- const root = { folders: {}, files: [] };
1941
-
1942
- files.forEach(file => {
1943
- const parts = file.path.split(/[/\\]/);
1944
- let current = root;
1945
-
1946
- // Navigate/create folder structure
1947
- for (let i = 0; i < parts.length - 1; i++) {
1948
- const folderName = parts[i];
1949
- if (!current.folders[folderName]) {
1950
- current.folders[folderName] = { folders: {}, files: [] };
1951
- }
1952
- current = current.folders[folderName];
1953
- }
1954
-
1955
- // Add file to current folder
1956
- current.files.push({ name: parts[parts.length - 1], path: file.path });
1957
- });
1958
-
1959
- return root;
1960
- }
1961
-
1962
- // Get file icon based on extension
1963
- function getFileIcon(name) {
1964
- const ext = name.split('.').pop()?.toLowerCase();
1965
- const iconMap = {
1966
- 'js': 'codicon-file-code', 'jsx': 'codicon-file-code',
1967
- 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1968
- 'html': 'codicon-file-code', 'css': 'codicon-file-code',
1969
- 'json': 'codicon-json', 'md': 'codicon-markdown',
1970
- 'py': 'codicon-file-code', 'java': 'codicon-file-code',
1971
- 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
1972
- 'cob': 'codicon-file-code', 'cbl': 'codicon-file-code',
1973
- 'sql': 'codicon-database', 'txt': 'codicon-file'
1974
- };
1975
- return iconMap[ext] || 'codicon-file';
1976
- }
1977
-
1978
- // Render tree node recursively
1979
- function renderTreeNode(node, path = '', depth = 0) {
1980
- let html = '';
1981
-
1982
- // Sort folders first, then files
1983
- const folderNames = Object.keys(node.folders).sort();
1984
- const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
1985
-
1986
- // Render folders
1987
- folderNames.forEach(folderName => {
1988
- const folderPath = path ? `${path}/${folderName}` : folderName;
1989
- const isExpanded = expandedFolders.has(folderPath);
1990
-
1991
- html += `
1992
- <div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
1993
- <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
1994
- <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
1995
- <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #dcb67a;"></span>
1996
- <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
1997
- </div>
1998
- <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">
1999
- ${renderTreeNode(node.folders[folderName], folderPath, depth + 1)}
2000
- </div>
2001
- </div>
2002
- `;
2003
- });
2004
-
2005
- // Render files
2006
- sortedFiles.forEach(file => {
2007
- html += `
2008
- <div class="file-tree-file" data-path="${escapeHtml(file.path)}">
2009
- <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
2010
- <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
2011
- </div>
2012
- `;
2013
- });
2014
-
2015
- return html;
2016
- }
2017
-
2018
- // Render file tree
2019
- function renderFileTree(files) {
2020
- if (files.length === 0) {
2021
- fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>';
2022
- return;
2023
- }
2024
-
2025
- // Build and render tree structure
2026
- const tree = buildFileTree(files);
2027
- fileTree.innerHTML = renderTreeNode(tree);
2028
-
2029
- // Add click handlers for folders
2030
- fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
2031
- header.onclick = (e) => {
2032
- e.stopPropagation();
2033
- const folderPath = header.dataset.folder;
2034
- const folder = header.closest('.file-tree-folder');
2035
- const contents = folder.querySelector('.file-tree-folder-contents');
2036
- const chevron = header.querySelector('.file-tree-folder-icon');
2037
- const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
2038
-
2039
- if (expandedFolders.has(folderPath)) {
2040
- expandedFolders.delete(folderPath);
2041
- contents.classList.remove('expanded');
2042
- chevron.classList.remove('expanded');
2043
- folderIcon.classList.remove('codicon-folder-opened');
2044
- folderIcon.classList.add('codicon-folder');
2045
- } else {
2046
- expandedFolders.add(folderPath);
2047
- contents.classList.add('expanded');
2048
- chevron.classList.add('expanded');
2049
- folderIcon.classList.remove('codicon-folder');
2050
- folderIcon.classList.add('codicon-folder-opened');
2051
- }
2052
- };
2053
- });
2054
-
2055
- // Add click handlers for files
2056
- fileTree.querySelectorAll('.file-tree-file').forEach(item => {
2057
- item.onclick = () => {
2058
- const path = item.dataset.path;
2059
- closeFileTree();
2060
- openFileInEditor(path);
2061
- };
2062
- });
2063
- }
2064
-
2065
- // Setup file tree event listeners
2066
- function setupFileTree() {
2067
- const explorerBtn = document.getElementById('editorExplorerBtn');
2068
-
2069
- // Explorer button click toggles file tree
2070
- explorerBtn.onclick = (e) => {
2071
- e.stopPropagation();
2072
- toggleFileTree();
2073
- };
2074
-
2075
- // Close button
2076
- fileTreeClose.onclick = (e) => {
2077
- e.stopPropagation();
2078
- closeFileTree();
2079
- };
2080
-
2081
- // Prevent clicks inside panel from closing it
2082
- fileTreePanel.onclick = (e) => {
2083
- e.stopPropagation();
2084
- };
2085
-
2086
- // Close on escape key
2087
- document.addEventListener('keydown', (e) => {
2088
- if (e.key === 'Escape' && fileTreeOpen) {
2089
- closeFileTree();
2090
- }
2091
- });
2092
- }
2093
-
2094
- // Initialize file tree
2095
- setupFileTree();
2096
-
2097
- // ==================== Editor Search ====================
2098
-
2099
- function openEditorSearch() {
2100
- editorSearchBar.classList.add('open');
2101
- editorSearchInput.focus();
2102
- editorSearchInput.select();
2103
- }
2104
-
2105
- function closeEditorSearch() {
2106
- editorSearchBar.classList.remove('open');
2107
- editorSearchInput.value = '';
2108
- searchMatches = [];
2109
- currentMatchIndex = -1;
2110
- editorSearchCount.textContent = '';
2111
- clearSearchHighlights();
2112
- }
2113
-
2114
- function clearSearchHighlights() {
2115
- // Restore original HTML for lines that were modified
2116
- const modifiedLines = panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]');
2117
- modifiedLines.forEach(el => {
2118
- el.innerHTML = el.dataset.originalHtml;
2119
- delete el.dataset.originalHtml;
2120
- });
2121
- }
2122
-
2123
- function performSearch(query) {
2124
- clearSearchHighlights();
2125
- searchMatches = [];
2126
- currentMatchIndex = -1;
2127
-
2128
- if (!query || query.length < 1) {
2129
- editorSearchCount.textContent = '';
2130
- return;
2131
- }
2132
-
2133
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
2134
- const queryLower = query.toLowerCase();
2135
-
2136
- codeElements.forEach((lineEl, lineIndex) => {
2137
- const text = lineEl.textContent;
2138
- const textLower = text.toLowerCase();
2139
- let startIndex = 0;
2140
- let matchIndex;
2141
-
2142
- while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
2143
- searchMatches.push({
2144
- lineIndex,
2145
- lineEl,
2146
- startIndex: matchIndex,
2147
- endIndex: matchIndex + query.length
2148
- });
2149
- startIndex = matchIndex + 1;
2150
- }
2151
- });
2152
-
2153
- if (searchMatches.length > 0) {
2154
- highlightMatches(query);
2155
- currentMatchIndex = 0;
2156
- scrollToMatch(0);
2157
- updateSearchCount();
2158
- } else {
2159
- editorSearchCount.textContent = 'No results';
2160
- }
2161
- }
2162
-
2163
- function highlightMatches(query) {
2164
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
2165
- const queryLower = query.toLowerCase();
2166
-
2167
- codeElements.forEach(lineEl => {
2168
- const text = lineEl.textContent;
2169
- const textLower = text.toLowerCase();
2170
-
2171
- if (textLower.includes(queryLower)) {
2172
- // Store original HTML to restore later
2173
- if (!lineEl.dataset.originalHtml) {
2174
- lineEl.dataset.originalHtml = lineEl.innerHTML;
2175
- }
2176
-
2177
- // Work with text content, find all match positions
2178
- const matches = [];
2179
- let idx = 0;
2180
- while ((idx = textLower.indexOf(queryLower, idx)) !== -1) {
2181
- matches.push({ start: idx, end: idx + query.length });
2182
- idx++;
2183
- }
2184
-
2185
- if (matches.length > 0) {
2186
- // Rebuild the line with highlights
2187
- let result = '';
2188
- let lastEnd = 0;
2189
-
2190
- matches.forEach(match => {
2191
- // Add text before match (escaped)
2192
- result += escapeHtml(text.slice(lastEnd, match.start));
2193
- // Add highlighted match
2194
- result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
2195
- lastEnd = match.end;
2196
- });
2197
-
2198
- // Add remaining text
2199
- result += escapeHtml(text.slice(lastEnd));
2200
-
2201
- lineEl.innerHTML = result;
2202
- }
2203
- }
2204
- });
2205
- }
2206
-
2207
- function scrollToMatch(index) {
2208
- const highlights = panels.editor.content.querySelectorAll('.search-highlight');
2209
-
2210
- // Remove current class from all
2211
- highlights.forEach(el => el.classList.remove('current'));
2212
-
2213
- if (highlights[index]) {
2214
- highlights[index].classList.add('current');
2215
-
2216
- // Get the scroll container and the highlight element
2217
- const container = panels.editor.content;
2218
- const element = highlights[index];
2219
-
2220
- // Get the line element (parent of the highlight)
2221
- const lineEl = element.closest('.editor-line');
2222
- if (lineEl) {
2223
- // Calculate scroll position to center the line
2224
- const containerRect = container.getBoundingClientRect();
2225
- const lineRect = lineEl.getBoundingClientRect();
2226
- const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
2227
-
2228
- container.scrollTo({
2229
- top: Math.max(0, scrollTop),
2230
- behavior: 'smooth'
2231
- });
2232
- }
2233
- }
2234
- }
2235
-
2236
- function updateSearchCount() {
2237
- if (searchMatches.length > 0) {
2238
- editorSearchCount.textContent = `${currentMatchIndex + 1}/${searchMatches.length}`;
2239
- } else {
2240
- editorSearchCount.textContent = 'No results';
1846
+ showEmptyState('editor', 'codicon-file-code', 'Failed to load file.');
2241
1847
  }
2242
1848
  }
2243
1849
 
2244
- function goToNextMatch() {
2245
- if (searchMatches.length === 0) return;
2246
- currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
2247
- scrollToMatch(currentMatchIndex);
2248
- updateSearchCount();
2249
- }
1850
+ // =============================================================================
1851
+ // Event Listeners Setup
1852
+ // =============================================================================
1853
+ $('editorExplorerBtn').onclick = (e) => { e.stopPropagation(); toggleFileTree(); };
1854
+ fileTreeDropdown.onclick = (e) => { e.stopPropagation(); };
2250
1855
 
2251
- function goToPrevMatch() {
2252
- if (searchMatches.length === 0) return;
2253
- currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
2254
- scrollToMatch(currentMatchIndex);
2255
- updateSearchCount();
2256
- }
1856
+ // Close dropdown when clicking outside
1857
+ document.addEventListener('click', (e) => {
1858
+ if (fileTreeOpen && !e.target.closest('.editor-explorer-wrapper')) closeFileTree();
1859
+ });
2257
1860
 
2258
- // Setup editor search
2259
- function setupEditorSearch() {
2260
- editorSearchBtn.onclick = () => openEditorSearch();
2261
- editorSearchClose.onclick = () => closeEditorSearch();
2262
- editorSearchNext.onclick = () => goToNextMatch();
2263
- editorSearchPrev.onclick = () => goToPrevMatch();
2264
-
2265
- editorSearchInput.oninput = (e) => {
2266
- performSearch(e.target.value);
2267
- };
2268
-
2269
- editorSearchInput.onkeydown = (e) => {
2270
- if (e.key === 'Enter') {
2271
- e.preventDefault();
2272
- if (e.shiftKey) {
2273
- goToPrevMatch();
2274
- } else {
2275
- goToNextMatch();
2276
- }
2277
- } else if (e.key === 'Escape') {
2278
- closeEditorSearch();
2279
- }
2280
- };
2281
-
2282
- // Ctrl+F / Cmd+F to open search
2283
- document.addEventListener('keydown', (e) => {
2284
- if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') {
2285
- e.preventDefault();
2286
- openEditorSearch();
2287
- }
2288
- });
2289
- }
1861
+ document.addEventListener('keydown', (e) => {
1862
+ if (e.key === 'Escape' && fileTreeOpen) closeFileTree();
1863
+ });
2290
1864
 
2291
- setupEditorSearch();
2292
-
2293
- // Visibility change handler
2294
1865
  document.addEventListener('visibilitychange', () => {
2295
- if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
2296
- connect();
2297
- }
1866
+ if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) connect();
2298
1867
  });
2299
-
1868
+
2300
1869
  // Initialize
2301
1870
  connect();
2302
1871
  </script>