kiro-mobile-bridge 1.0.7 → 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
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; }
63
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: 12px; 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,61 +783,28 @@
1056
783
  background: rgba(255, 255, 255, 0.1) !important;
1057
784
  }
1058
785
 
1059
- /* Hide notification bars and change acceptance UI */
1060
- [class*="notification"], [class*="Notification"],
1061
- [class*="toast"], [class*="Toast"],
1062
- [class*="banner"], [class*="Banner"],
1063
- [class*="change-accepted"], [class*="changeAccepted"],
1064
- [class*="revert"], [class*="Revert"],
1065
- [class*="view-all"], [class*="viewAll"],
1066
- [class*="status-bar-notification"],
1067
- div[role="status"], div[role="alert"] {
1068
- display: none !important;
1069
- visibility: hidden !important;
1070
- opacity: 0 !important;
1071
- height: 0 !important;
1072
- overflow: hidden !important;
1073
- }
1074
-
1075
- /* Model selector chevron/arrow icon */
1076
- [class*="model-selector"] svg, [class*="modelSelector"] svg,
1077
- [class*="model-dropdown"] svg, button[class*="dropdown"] svg {
1078
- width: 12px !important;
1079
- height: 12px !important;
1080
- flex-shrink: 0 !important;
1081
- }
1082
-
1083
- /* Model name text inside selector */
1084
- [class*="model-selector"] span, [class*="modelSelector"] span,
1085
- [class*="model-name"], [class*="modelName"] {
1086
- overflow: hidden !important;
1087
- text-overflow: ellipsis !important;
1088
- white-space: nowrap !important;
1089
- }
1090
-
1091
- /* Reset positioning for captured content to prevent layout issues */
786
+ /* Reset positioning */
1092
787
  #root, .app, [class*="cascade"], [class*="Cascade"] {
1093
788
  position: relative !important;
1094
789
  }
1095
790
 
1096
- /* Ensure proper stacking and no fixed positioning breaks layout */
1097
- [style*="position: fixed"],
1098
- [style*="position:fixed"] {
791
+ [style*="position: fixed"], [style*="position:fixed"] {
1099
792
  position: absolute !important;
1100
793
  }
1101
794
 
1102
795
  * { font-family: inherit; }
1103
796
  code, pre, .monaco-editor { font-family: Menlo, Monaco, "Courier New", monospace !important; }
1104
- /* Icon sizes - keep them small like in IDE */
797
+
798
+ /* Icon sizes */
1105
799
  svg[viewBox="0 0 24 24"] { width: 16px !important; height: 16px !important; }
1106
800
  svg[viewBox="0 0 16 16"] { width: 14px !important; height: 14px !important; }
1107
801
  svg { color: inherit; max-width: 20px !important; max-height: 20px !important; }
1108
802
  svg path, svg rect { fill: currentColor; }
1109
803
  svg[fill="none"] path { fill: none; }
1110
- /* Preserve stroke-based SVG circles (like context window progress ring) */
1111
804
  svg circle[stroke] { fill: none !important; }
1112
805
  svg circle:not([stroke]) { fill: currentColor; }
1113
- /* Context window progress circle styling */
806
+
807
+ /* Context window progress circle */
1114
808
  svg[class*="context"], svg[class*="progress"], [class*="context"] svg {
1115
809
  overflow: visible !important;
1116
810
  }
@@ -1118,7 +812,6 @@
1118
812
  fill: none !important;
1119
813
  stroke-linecap: round !important;
1120
814
  }
1121
- /* Context window indicator - circular progress ring */
1122
815
  [class*="context-window"], [class*="contextWindow"], [class*="ContextWindow"],
1123
816
  [class*="context-indicator"], [class*="contextIndicator"], [class*="ContextIndicator"],
1124
817
  [class*="token-usage"], [class*="tokenUsage"], [class*="TokenUsage"] {
@@ -1132,7 +825,6 @@
1132
825
  position: relative !important;
1133
826
  flex-shrink: 0 !important;
1134
827
  }
1135
- /* Create gray background circle using box-shadow on the container */
1136
828
  [class*="context-window"]::before, [class*="contextWindow"]::before, [class*="ContextWindow"]::before,
1137
829
  [class*="context-indicator"]::before, [class*="contextIndicator"]::before {
1138
830
  content: '' !important;
@@ -1161,15 +853,12 @@
1161
853
  stroke-linecap: round !important;
1162
854
  stroke: #4ade80 !important;
1163
855
  }
1164
- /* Ensure circles are visible */
1165
- [class*="context"] svg circle,
1166
- [class*="Context"] svg circle {
856
+ [class*="context"] svg circle, [class*="Context"] svg circle {
1167
857
  display: block !important;
1168
858
  visibility: visible !important;
1169
859
  }
1170
860
 
1171
- /* ========== CHAT INPUT TOOLBAR AREA ========== */
1172
- /* The toolbar/footer area containing model selector, context, autopilot, input */
861
+ /* Chat input toolbar area */
1173
862
  [class*="chat-input"], [class*="chatInput"], [class*="ChatInput"],
1174
863
  [class*="input-area"], [class*="inputArea"], [class*="InputArea"],
1175
864
  [class*="message-input"], [class*="messageInput"], [class*="MessageInput"],
@@ -1183,7 +872,7 @@
1183
872
  border-top: 1px solid var(--color-border-primary, #4a464f) !important;
1184
873
  }
1185
874
 
1186
- /* Input toolbar row (contains # button, model selector, context, autopilot) */
875
+ /* Input toolbar row */
1187
876
  [class*="input-toolbar"], [class*="inputToolbar"], [class*="InputToolbar"],
1188
877
  [class*="chat-toolbar"], [class*="chatToolbar"], [class*="ChatToolbar"],
1189
878
  [class*="composer-toolbar"], [class*="composerToolbar"] {
@@ -1194,7 +883,7 @@
1194
883
  flex-wrap: wrap !important;
1195
884
  }
1196
885
 
1197
- /* Context button (# symbol) - NO border, match Kiro style */
886
+ /* Context button (# symbol) */
1198
887
  [class*="context-button"], [class*="contextButton"], [class*="ContextButton"],
1199
888
  button[aria-label*="context"], button[aria-label*="Context"],
1200
889
  [class*="hash-button"], [class*="hashButton"] {
@@ -1219,30 +908,15 @@
1219
908
  background: rgba(255, 255, 255, 0.1) !important;
1220
909
  }
1221
910
 
1222
- /* ========== MESSAGE TEXT FLOW - INLINE ELEMENTS ========== */
1223
- /* Fix text flow so file links and code stay inline with text */
1224
- [class*="message"] p,
1225
- [class*="Message"] p,
1226
- [class*="chat"] p,
1227
- [class*="Chat"] p {
911
+ /* Message text flow - inline elements */
912
+ [class*="message"] p, [class*="Message"] p, [class*="chat"] p, [class*="Chat"] p {
1228
913
  display: block !important;
1229
914
  }
1230
915
 
1231
- /* Make file links, code, and inline elements stay inline */
1232
- [class*="message"] a,
1233
- [class*="Message"] a,
1234
- [class*="message"] code,
1235
- [class*="Message"] code,
1236
- [class*="message"] span,
1237
- [class*="Message"] span,
1238
- [class*="chat"] a,
1239
- [class*="Chat"] a,
1240
- [class*="chat"] code,
1241
- [class*="Chat"] code,
1242
- [class*="file-link"],
1243
- [class*="FileLink"],
1244
- [class*="inline"],
1245
- 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) {
1246
920
  display: inline !important;
1247
921
  vertical-align: baseline !important;
1248
922
  }
@@ -1256,17 +930,10 @@
1256
930
  font-size: 12px !important;
1257
931
  }
1258
932
 
1259
- /* File badges/chips should be inline */
1260
- [class*="file-badge"],
1261
- [class*="FileBadge"],
1262
- [class*="file-chip"],
1263
- [class*="FileChip"],
1264
- [class*="deleted"],
1265
- [class*="Deleted"],
1266
- [class*="created"],
1267
- [class*="Created"],
1268
- [class*="modified"],
1269
- [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"] {
1270
937
  display: inline-flex !important;
1271
938
  align-items: center !important;
1272
939
  gap: 4px !important;
@@ -1278,19 +945,10 @@
1278
945
  vertical-align: middle !important;
1279
946
  }
1280
947
 
1281
- /* Bullet lists should have proper inline content */
1282
- ul li, ol li {
1283
- display: list-item !important;
1284
- }
1285
-
1286
- ul li > *, ol li > * {
1287
- display: inline !important;
1288
- }
1289
-
1290
- ul li > p, ol li > p {
1291
- display: inline !important;
1292
- margin: 0 !important;
1293
- }
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; }
1294
952
 
1295
953
  [role="switch"], [class*="toggle"] {
1296
954
  overflow: visible !important;
@@ -1298,117 +956,7 @@
1298
956
  flex-shrink: 0 !important;
1299
957
  }
1300
958
 
1301
- /* ========== TASK LIST STYLING - EXACT KIRO IDE MATCH ========== */
1302
-
1303
- /* Force solid background for task containers */
1304
- [class*="task"], [class*="Task"],
1305
- div:has(> h1:contains("CURRENT TASKS")),
1306
- div:has(> h2:contains("CURRENT TASKS")),
1307
- div:has(> h3:contains("CURRENT TASKS")),
1308
- div:has(> h1:contains("TASKS IN QUEUE")),
1309
- div:has(> h2:contains("TASKS IN QUEUE")),
1310
- div:has(> h3:contains("TASKS IN QUEUE")) {
1311
- font-family: "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Ubuntu, "Droid Sans", sans-serif !important;
1312
- background: #1e1e1e !important;
1313
- background-color: #1e1e1e !important;
1314
- }
1315
-
1316
- /* Task section headers - BOLD WHITE UPPERCASE */
1317
- h1:contains("CURRENT TASKS"),
1318
- h2:contains("CURRENT TASKS"),
1319
- h3:contains("CURRENT TASKS"),
1320
- h1:contains("TASKS IN QUEUE"),
1321
- h2:contains("TASKS IN QUEUE"),
1322
- h3:contains("TASKS IN QUEUE"),
1323
- [class*="task"] h1, [class*="Task"] h1,
1324
- [class*="task"] h2, [class*="Task"] h2,
1325
- [class*="task"] h3, [class*="Task"] h3,
1326
- [class*="task-header"], [class*="TaskHeader"],
1327
- [class*="section-title"], [class*="SectionTitle"],
1328
- div[class*="current"] h1, div[class*="Current"] h1,
1329
- div[class*="current"] h2, div[class*="Current"] h2,
1330
- div[class*="current"] h3, div[class*="Current"] h3,
1331
- div[class*="queue"] h1, div[class*="Queue"] h1,
1332
- div[class*="queue"] h2, div[class*="Queue"] h2,
1333
- div[class*="queue"] h3, div[class*="Queue"] h3 {
1334
- font-size: 13px !important;
1335
- font-weight: 700 !important;
1336
- letter-spacing: 0.5px !important;
1337
- text-transform: uppercase !important;
1338
- color: #ffffff !important;
1339
- margin: 0 0 12px 0 !important;
1340
- padding: 0 !important;
1341
- line-height: 1.4 !important;
1342
- background: transparent !important;
1343
- }
1344
-
1345
- /* Empty state messages - ITALIC GRAY */
1346
- p:contains("No active tasks"),
1347
- p:contains("No tasks in queue"),
1348
- [class*="task"] p:not([class*="task-item"]),
1349
- [class*="Task"] p:not([class*="TaskItem"]),
1350
- [class*="no-task"], [class*="NoTask"],
1351
- [class*="empty-task"], [class*="EmptyTask"],
1352
- [class*="task-empty"], [class*="TaskEmpty"] {
1353
- font-size: 13px !important;
1354
- font-style: italic !important;
1355
- color: #888888 !important;
1356
- margin: 0 0 16px 0 !important;
1357
- padding: 0 !important;
1358
- line-height: 1.6 !important;
1359
- background: transparent !important;
1360
- }
1361
-
1362
- /* Task list containers */
1363
- [class*="task-list"], [class*="TaskList"],
1364
- [class*="task-section"], [class*="TaskSection"] {
1365
- margin-bottom: 24px !important;
1366
- padding: 0 !important;
1367
- background: #1e1e1e !important;
1368
- }
1369
-
1370
- /* Horizontal divider between sections */
1371
- [class*="task-divider"], [class*="TaskDivider"],
1372
- [class*="task"] hr, [class*="Task"] hr {
1373
- border: none !important;
1374
- border-top: 1px solid #3c3c3c !important;
1375
- margin: 20px 0 !important;
1376
- background: transparent !important;
1377
- }
1378
-
1379
- /* Individual task items (when they exist) */
1380
- [class*="task-item"], [class*="TaskItem"] {
1381
- padding: 8px 12px !important;
1382
- margin: 4px 0 !important;
1383
- background: rgba(255, 255, 255, 0.03) !important;
1384
- border-radius: 4px !important;
1385
- border-left: 2px solid #7138cc !important;
1386
- font-size: 13px !important;
1387
- color: #cccccc !important;
1388
- line-height: 1.5 !important;
1389
- }
1390
-
1391
- [class*="task-item"]:hover, [class*="TaskItem"]:hover {
1392
- background: rgba(255, 255, 255, 0.05) !important;
1393
- }
1394
-
1395
- /* Task item with close button */
1396
- [class*="task-item"] button, [class*="TaskItem"] button {
1397
- margin-left: 8px !important;
1398
- padding: 2px 6px !important;
1399
- font-size: 11px !important;
1400
- background: rgba(255, 255, 255, 0.1) !important;
1401
- border: 1px solid #4a464f !important;
1402
- border-radius: 3px !important;
1403
- color: #888 !important;
1404
- cursor: pointer !important;
1405
- }
1406
-
1407
- [class*="task-item"] button:hover, [class*="TaskItem"] button:hover {
1408
- background: rgba(255, 255, 255, 0.15) !important;
1409
- color: #fff !important;
1410
- }
1411
-
959
+ /* Toggle switch styling */
1412
960
  .kiro-toggle-switch {
1413
961
  display: flex !important;
1414
962
  align-items: center !important;
@@ -1447,207 +995,586 @@
1447
995
  }
1448
996
  </style>`;
1449
997
  }
1450
-
1451
- // Make chat elements interactive
1452
- function makeInteractive() {
1453
- const content = panels.chat.content;
998
+
999
+ function renderChatSnapshot(snapshot, isUpdate = false) {
1000
+ if (isRendering) return;
1001
+ isRendering = true;
1454
1002
 
1455
- // Input fields - support both Lexical and ProseMirror/TipTap editors
1456
- const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1457
- for (const selector of inputSelectors) {
1458
- content.querySelectorAll(selector).forEach(el => {
1459
- el.style.cursor = 'text';
1460
- el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1461
- el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1462
- el.addEventListener('keydown', async (e) => {
1463
- if (e.key === 'Enter' && !e.shiftKey) {
1464
- e.preventDefault();
1465
- const text = el.textContent || el.value || '';
1466
- if (text.trim()) {
1467
- await sendToKiro(text.trim());
1468
- el.textContent = '';
1469
- el.innerHTML = '';
1470
- isTypingInKiroInput = false;
1471
- }
1472
- }
1473
- });
1474
- });
1475
- }
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);
1476
1009
 
1477
- // Toggle switches
1478
- content.querySelectorAll('.kiro-toggle-switch, [role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1479
- const input = toggle.tagName === 'INPUT' ? toggle : toggle.querySelector('input[type="checkbox"]');
1480
- const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1481
-
1482
- wrapper.style.cursor = 'pointer';
1483
- wrapper.onclick = async (e) => {
1484
- e.preventDefault();
1485
- e.stopPropagation();
1486
-
1487
- const label = wrapper.querySelector('label');
1488
- const clickInfo = {
1489
- tag: 'div',
1490
- text: label ? label.textContent.trim() : 'toggle',
1491
- role: 'switch',
1492
- isToggle: true,
1493
- toggleId: input ? input.id : '',
1494
- checked: input ? input.checked : false
1495
- };
1496
- await sendClickToKiro(clickInfo);
1497
- return false;
1498
- };
1499
- });
1010
+ let html = getBaseStyles();
1011
+ if (currentStyles) html += `<style>${currentStyles}</style>`;
1012
+ html += snapshot.html || '';
1500
1013
 
1501
- // Send button - find and make it work
1502
- // 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');
1503
1017
 
1504
- // Helper function to attach send handler to a button
1505
- const attachSendHandler = (btn) => {
1506
- if (!btn || btn.dataset.sendHandlerAttached) return;
1507
- btn.dataset.sendHandlerAttached = 'true';
1508
-
1509
- // Remove disabled so button is always clickable and highlighted
1510
- btn.removeAttribute('disabled');
1511
- btn.style.cursor = 'pointer';
1512
-
1513
- btn.onclick = async (e) => {
1514
- e.preventDefault();
1515
- e.stopPropagation();
1516
-
1517
- // Find the input field and get its text
1518
- const inputSelectors = ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea'];
1519
- let inputText = '';
1520
- let inputEl = null;
1521
-
1522
- for (const inputSel of inputSelectors) {
1523
- const input = content.querySelector(inputSel);
1524
- if (input) {
1525
- inputEl = input;
1526
- inputText = input.textContent || input.innerText || input.value || '';
1527
- if (inputText.trim()) break;
1528
- }
1529
- }
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"]');
1530
1026
 
1531
- if (inputText.trim()) {
1532
- await sendToKiro(inputText.trim());
1533
- // Clear the input after sending
1534
- if (inputEl) {
1535
- if (inputEl.textContent !== undefined) {
1536
- inputEl.textContent = '';
1537
- inputEl.innerHTML = '';
1538
- } else if (inputEl.value !== undefined) {
1539
- 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;
1540
1038
  }
1541
1039
  }
1542
- showToast('Sent!', 1500, true);
1543
- } else {
1544
- 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;
1545
1047
  }
1546
- return false;
1547
- };
1548
- };
1549
-
1550
- // PRIMARY METHOD: Find the submit button by data-variant="submit" (CONFIRMED via Playwriter)
1551
- const submitBtn = content.querySelector('button[data-variant="submit"]');
1552
- if (submitBtn) {
1553
- attachSendHandler(submitBtn);
1554
- }
1048
+ isRendering = false;
1049
+ });
1050
+ });
1555
1051
 
1556
- // FALLBACK Method 1: Find buttons with codicon-arrow-up (Kiro's send icon)
1557
- content.querySelectorAll('button').forEach(btn => {
1558
- if (btn.dataset.sendHandlerAttached) return;
1559
- const hasArrowUp = btn.querySelector('.codicon-arrow-up, .codicon-send, [class*="arrow-up"]');
1560
- if (hasArrowUp) {
1561
- 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';
1562
1061
  }
1563
1062
  });
1564
-
1565
- // FALLBACK Method 2: Find SVGs that look like arrows
1566
- content.querySelectorAll('svg').forEach(svg => {
1567
- const svgClass = (svg.getAttribute('class') || '').toLowerCase();
1568
- const svgParent = svg.closest('button');
1569
-
1570
- if (svgClass.includes('arrow') || svgClass.includes('send') || svgClass.includes('lucide')) {
1571
- if (svgParent) {
1572
- 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');
1573
1092
  }
1574
1093
  }
1575
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();
1576
1105
 
1577
- // FALLBACK Method 3: Find buttons by various selectors
1578
- const sendButtonSelectors = [
1579
- 'button[type="submit"]',
1580
- 'button[aria-label*="send" i]',
1581
- 'button[aria-label*="submit" i]',
1582
- '[class*="send-button"]',
1583
- '[class*="sendButton"]',
1584
- '[class*="submit-button"]',
1585
- '[class*="submitButton"]'
1586
- ];
1587
-
1588
- for (const selector of sendButtonSelectors) {
1589
- try {
1590
- content.querySelectorAll(selector).forEach(el => {
1591
- const btn = el.closest('button') || el;
1592
- if (btn.tagName === 'BUTTON') {
1593
- attachSendHandler(btn);
1594
- }
1595
- });
1596
- } catch(e) {}
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;
1597
1138
  }
1598
1139
 
1599
- // Method 3: Find ALL buttons and attach handler to ones that look like send buttons
1600
- // This is more reliable than using getBoundingClientRect which doesn't work before layout
1601
- content.querySelectorAll('button').forEach(btn => {
1602
- 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; }
1603
1158
 
1604
- const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
1605
- const className = (btn.className || '').toLowerCase();
1606
- const innerHTML = btn.innerHTML.toLowerCase();
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
+ }
1607
1168
 
1608
- // Skip buttons that are clearly NOT send buttons
1609
- if (ariaLabel.includes('context') || ariaLabel.includes('model') ||
1610
- ariaLabel.includes('close') || ariaLabel.includes('menu') ||
1611
- className.includes('context') || className.includes('model') ||
1612
- className.includes('dropdown') || className.includes('toggle') ||
1613
- className.includes('close') || className.includes('menu')) {
1614
- return;
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
+ }
1175
+
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;
1615
1190
  }
1616
1191
 
1617
- // Check if button contains an arrow SVG (common for send buttons)
1618
- const hasSvg = btn.querySelector('svg');
1619
- 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;
1620
1235
 
1621
- // If button has an SVG with arrow-like elements, it's likely a send button
1622
- if (hasSvg && hasArrowPath) {
1623
- 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;
1239
+ }
1240
+ });
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' });
1624
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
+ };
1625
1566
  });
1626
1567
 
1627
1568
  // Tabs
1628
1569
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1629
1570
  const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
1630
-
1631
1571
  if (closeBtn) {
1632
1572
  closeBtn.style.cursor = 'pointer';
1633
1573
  closeBtn.onclick = async (e) => {
1634
- e.preventDefault();
1635
- e.stopPropagation();
1636
-
1637
- // FIXED: Include tab label to identify which tab's close button to click
1574
+ e.preventDefault(); e.stopPropagation();
1638
1575
  const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1639
1576
  const tabLabel = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1640
-
1641
- console.log('[Tab Close] Closing tab:', tabLabel);
1642
-
1643
- await sendClickToKiro({
1644
- tag: 'button',
1645
- text: 'close',
1646
- ariaLabel: 'close',
1647
- role: 'button',
1648
- isCloseButton: true,
1649
- parentTabLabel: tabLabel // NEW: Identify which tab this close button belongs to
1650
- });
1577
+ await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true, parentTabLabel: tabLabel });
1651
1578
  return false;
1652
1579
  };
1653
1580
  }
@@ -1655,129 +1582,167 @@
1655
1582
  tab.style.cursor = 'pointer';
1656
1583
  tab.onclick = async (e) => {
1657
1584
  if (e.target.closest('[aria-label="close"], [class*="close"], button')) return;
1658
- e.preventDefault();
1659
- e.stopPropagation();
1660
-
1585
+ e.preventDefault(); e.stopPropagation();
1661
1586
  const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1662
1587
  const labelText = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1663
-
1664
1588
  await sendClickToKiro({ tag: 'div', text: labelText, role: 'tab', isTab: true, tabLabel: labelText });
1665
1589
  return false;
1666
1590
  };
1667
1591
  });
1668
1592
 
1669
- // File links - detect file paths in chat and make them clickable to open in Editor
1670
- 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;
1671
- const filePathPattern = /^[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+$/;
1672
-
1673
- // 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;
1674
1595
  content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
1675
1596
  if (el.onclick) return;
1676
1597
  const text = (el.textContent || '').trim();
1677
1598
  const dataPath = el.getAttribute('data-path') || '';
1678
1599
  const href = el.getAttribute('href') || '';
1679
1600
 
1680
- // Check if this looks like a file path
1681
- const isFilePath = (
1682
- fileExtensions.test(text) ||
1683
- fileExtensions.test(dataPath) ||
1684
- fileExtensions.test(href) ||
1685
- (filePathPattern.test(text) && text.includes('/'))
1686
- );
1687
-
1688
- if (isFilePath) {
1601
+ if (fileExtensions.test(text) || fileExtensions.test(dataPath) || fileExtensions.test(href)) {
1689
1602
  const filePath = dataPath || text || href;
1690
1603
  el.style.cursor = 'pointer';
1691
- el.style.textDecoration = 'underline';
1692
1604
  el.style.color = '#7eb6ff';
1693
1605
  el.title = `Open ${filePath} in Editor`;
1694
-
1695
- el.onclick = async (e) => {
1696
- e.preventDefault();
1697
- e.stopPropagation();
1698
- await openFileInEditor(filePath);
1699
- return false;
1700
- };
1606
+ el.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); await openFileInEditor(filePath); return false; };
1701
1607
  }
1702
1608
  });
1703
1609
 
1704
1610
  // Buttons and clickable elements
1705
1611
  const clickableSelectors = ['button', '[role="button"]', '[role="menuitem"]', '[role="option"]', '[role="checkbox"]', 'a[href]', '[tabindex="0"]'];
1706
1612
  const allClickables = new Set();
1707
- clickableSelectors.forEach(sel => {
1708
- try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {}
1709
- });
1613
+ clickableSelectors.forEach(sel => { try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {} });
1710
1614
 
1711
- // Also add dropdown menu items
1712
- content.querySelectorAll('[class*="dropdown-item"], [class*="dropdownItem"], [class*="model-option"], [class*="modelOption"]').forEach(el => {
1713
- 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
+ };
1714
1632
  });
1715
1633
 
1716
- // ADDED: Support for History panel items and other cursor-pointer elements
1717
- // History items are divs with cursor-pointer class containing session info
1718
- content.querySelectorAll('[class*="cursor-pointer"], [class*="hover\\:"], [class*="group"]').forEach(el => {
1719
- // 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
+
1720
1654
  const text = el.textContent || '';
1721
- const hasDate = /\d{1,2}\/\d{1,2}\/\d{4}/.test(text);
1722
- const hasTime = /\d{1,2}:\d{2}/.test(text);
1723
- if (hasDate || hasTime || el.classList.contains('cursor-pointer')) {
1724
- 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
+ }
1725
1681
  }
1726
1682
  });
1727
1683
 
1728
- // Also look for any div that has inline cursor:pointer style
1729
- content.querySelectorAll('div, span').forEach(el => {
1730
- const style = el.getAttribute('style') || '';
1731
- if (style.includes('cursor') && style.includes('pointer')) {
1732
- allClickables.add(el);
1733
- }
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) {}
1734
1704
  });
1735
1705
 
1736
- allClickables.forEach(el => {
1737
- if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1738
- if (el.closest('[role="tab"]')) return;
1739
- if (el.closest('.kiro-toggle-switch')) return;
1740
- if (el.onclick) return;
1741
-
1706
+ // Attach click handlers to all potential history items
1707
+ potentialHistoryItems.forEach(el => {
1742
1708
  el.style.cursor = 'pointer';
1743
1709
  el.onclick = async (e) => {
1744
1710
  e.preventDefault();
1745
1711
  e.stopPropagation();
1746
- const clickInfo = getElementClickInfo(el);
1747
1712
 
1748
- // Check if this is a model selection item
1749
- if (el.matches('[role="option"], [role="menuitem"], [class*="model"], [class*="Model"]') ||
1750
- el.closest('[role="listbox"], [role="menu"], [class*="dropdown-menu"], [class*="dropdownMenu"]')) {
1751
- clickInfo.isModelSelection = true;
1752
- clickInfo.modelName = el.textContent?.trim().split('\n')[0] || '';
1753
- }
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
+ };
1754
1733
 
1755
1734
  await sendClickToKiro(clickInfo);
1735
+
1736
+ // Refresh snapshot after clicking history item
1737
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
1756
1738
  return false;
1757
1739
  };
1758
1740
  });
1759
1741
  }
1760
-
1761
- function getElementClickInfo(el) {
1762
- const info = {
1763
- tag: el.tagName?.toLowerCase() || '',
1764
- text: (el.textContent || '').trim().substring(0, 100),
1765
- ariaLabel: el.getAttribute('aria-label') || '',
1766
- title: el.getAttribute('title') || '',
1767
- role: el.getAttribute('role') || '',
1768
- className: el.className || '',
1769
- isTab: el.getAttribute('role') === 'tab',
1770
- dataTestId: el.getAttribute('data-testid') || ''
1771
- };
1772
-
1773
- if (!info.ariaLabel) {
1774
- const parent = el.closest('[aria-label]');
1775
- if (parent) info.ariaLabel = parent.getAttribute('aria-label') || '';
1776
- }
1777
-
1778
- return info;
1779
- }
1780
-
1742
+
1743
+ // =============================================================================
1744
+ // Kiro Communication
1745
+ // =============================================================================
1781
1746
  async function sendToKiro(message) {
1782
1747
  if (!message || !selectedCascadeId) return;
1783
1748
  try {
@@ -1793,21 +1758,15 @@
1793
1758
  showToast('Failed to send');
1794
1759
  }
1795
1760
  }
1796
-
1761
+
1797
1762
  async function sendClickToKiro(clickInfo) {
1798
1763
  if (!selectedCascadeId) return;
1799
1764
 
1800
- const isNavigation =
1801
- clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1765
+ const isNavigation = clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1802
1766
  clickInfo.ariaLabel?.toLowerCase().includes('close') ||
1803
1767
  clickInfo.text?.toLowerCase().includes('back') ||
1804
1768
  clickInfo.role === 'tab';
1805
1769
 
1806
- const isModelSelection = clickInfo.isModelSelection ||
1807
- clickInfo.role === 'option' ||
1808
- clickInfo.role === 'menuitem' ||
1809
- clickInfo.className?.includes('model');
1810
-
1811
1770
  if (isNavigation) navigationPending = true;
1812
1771
 
1813
1772
  try {
@@ -1817,39 +1776,30 @@
1817
1776
  body: JSON.stringify(clickInfo)
1818
1777
  });
1819
1778
 
1820
- if (isNavigation || isModelSelection) {
1821
- // Refresh snapshot after navigation or model selection
1822
- setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1823
- }
1779
+ if (isNavigation) setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1824
1780
  } catch (e) {
1825
1781
  navigationPending = false;
1826
1782
  }
1827
1783
  }
1828
-
1829
- // Open file in Editor tab
1784
+
1830
1785
  async function openFileInEditor(filePath) {
1831
1786
  if (!selectedCascadeId || !filePath) return;
1832
1787
 
1833
1788
  showToast(`Opening ${filePath}...`, 1500);
1834
1789
 
1835
- // Switch to Editor tab immediately
1790
+ // Switch to Editor tab
1836
1791
  const editorTab = document.querySelector('[data-panel="editor"]');
1837
1792
  if (editorTab) {
1838
1793
  navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1839
- editorTab.classList.add('active');
1840
-
1841
- Object.values(panels).forEach(p => {
1842
- if (p.panel) p.panel.classList.remove('active');
1843
- });
1794
+ editorTab.classList.add('active');
1795
+ Object.values(panels).forEach(p => { if (p.panel) p.panel.classList.remove('active'); });
1844
1796
  panels.editor.panel.classList.add('active');
1845
1797
  activePanel = 'editor';
1846
-
1847
- // Show loading state
1848
1798
  showLoading('editor', 'Loading file...');
1849
1799
  }
1850
1800
 
1851
1801
  try {
1852
- // First, try to read the file directly from filesystem (bypasses Monaco limitation)
1802
+ // Try direct file read first
1853
1803
  const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
1854
1804
  method: 'POST',
1855
1805
  headers: { 'Content-Type': 'application/json' },
@@ -1861,472 +1811,61 @@
1861
1811
  if (data.content) {
1862
1812
  renderEditorSnapshot(data);
1863
1813
  showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1864
- console.log('[Editor] Loaded file directly:', data.fileName, data.lineCount, 'lines');
1865
1814
  return;
1866
1815
  }
1867
- } else {
1868
- const errorData = await readResult.json().catch(() => ({}));
1869
- console.log('[Editor] Direct file read failed:', errorData.error || readResult.status);
1870
1816
  }
1871
1817
 
1872
- // Fallback: Try to click on the file link in Kiro to open it in Monaco
1873
- console.log('[Editor] Falling back to Monaco capture...');
1874
- const clickResult = await fetch(`/click/${selectedCascadeId}`, {
1818
+ // Fallback: Click file link in Kiro
1819
+ await fetch(`/click/${selectedCascadeId}`, {
1875
1820
  method: 'POST',
1876
1821
  headers: { 'Content-Type': 'application/json' },
1877
- body: JSON.stringify({
1878
- tag: 'a',
1879
- text: filePath,
1880
- isFileLink: true,
1881
- filePath: filePath
1882
- })
1822
+ body: JSON.stringify({ tag: 'a', text: filePath, isFileLink: true, filePath })
1883
1823
  });
1884
1824
 
1885
- const result = await clickResult.json();
1886
- console.log('Click result:', result);
1887
-
1888
- // Wait for the file to open in Kiro
1889
1825
  await new Promise(resolve => setTimeout(resolve, 800));
1890
1826
 
1891
- // Fetch the editor content from Monaco
1892
1827
  const fetchEditor = async () => {
1893
1828
  try {
1894
1829
  const r = await fetch(`/editor/${selectedCascadeId}`);
1895
1830
  if (r.ok) {
1896
1831
  const data = await r.json();
1897
- if (data.content || data.html) {
1898
- renderEditorSnapshot(data);
1899
- showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1900
- return true;
1901
- }
1832
+ if (data.content || data.html) { renderEditorSnapshot(data); showToast(`Opened ${data.fileName || filePath}`, 1500, true); return true; }
1902
1833
  }
1903
1834
  } catch (e) {}
1904
1835
  return false;
1905
1836
  };
1906
1837
 
1907
- // Try fetching a few times with delays
1908
1838
  if (!await fetchEditor()) {
1909
1839
  await new Promise(resolve => setTimeout(resolve, 500));
1910
1840
  if (!await fetchEditor()) {
1911
- await new Promise(resolve => setTimeout(resolve, 500));
1912
- if (!await fetchEditor()) {
1913
- showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}. File may not exist or path is incorrect.`);
1914
- }
1841
+ showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}.`);
1915
1842
  }
1916
1843
  }
1917
-
1918
1844
  } catch (e) {
1919
1845
  showToast('Failed to open file');
1920
- console.error('Open file error:', e);
1921
- showEmptyState('editor', 'codicon-file-code', 'Failed to load file. Check console for details.');
1922
- }
1923
- }
1924
-
1925
- // ==================== File Tree ====================
1926
-
1927
- // Toggle file tree panel
1928
- function toggleFileTree() {
1929
- fileTreeOpen = !fileTreeOpen;
1930
- fileTreePanel.classList.toggle('open', fileTreeOpen);
1931
-
1932
- if (fileTreeOpen) {
1933
- // Load files if not cached
1934
- if (workspaceFiles.length === 0) {
1935
- fetchWorkspaceFiles();
1936
- } else {
1937
- renderFileTree(workspaceFiles);
1938
- }
1939
- }
1940
- }
1941
-
1942
- // Close file tree
1943
- function closeFileTree() {
1944
- fileTreeOpen = false;
1945
- fileTreePanel.classList.remove('open');
1946
- }
1947
-
1948
- // Fetch workspace files from server
1949
- async function fetchWorkspaceFiles() {
1950
- if (!selectedCascadeId) return;
1951
-
1952
- fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1953
-
1954
- try {
1955
- const r = await fetch(`/files/${selectedCascadeId}`);
1956
- if (!r.ok) throw new Error('Failed to fetch files');
1957
-
1958
- const data = await r.json();
1959
- workspaceFiles = data.files || [];
1960
- console.log(`[FileTree] Loaded ${workspaceFiles.length} files`);
1961
- renderFileTree(workspaceFiles);
1962
- } catch (e) {
1963
- console.error('Failed to fetch files:', e);
1964
- fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1965
- }
1966
- }
1967
-
1968
- // Build tree structure from flat file list
1969
- function buildFileTree(files) {
1970
- const root = { folders: {}, files: [] };
1971
-
1972
- files.forEach(file => {
1973
- const parts = file.path.split(/[/\\]/);
1974
- let current = root;
1975
-
1976
- // Navigate/create folder structure
1977
- for (let i = 0; i < parts.length - 1; i++) {
1978
- const folderName = parts[i];
1979
- if (!current.folders[folderName]) {
1980
- current.folders[folderName] = { folders: {}, files: [] };
1981
- }
1982
- current = current.folders[folderName];
1983
- }
1984
-
1985
- // Add file to current folder
1986
- current.files.push({ name: parts[parts.length - 1], path: file.path });
1987
- });
1988
-
1989
- return root;
1990
- }
1991
-
1992
- // Get file icon based on extension
1993
- function getFileIcon(name) {
1994
- const ext = name.split('.').pop()?.toLowerCase();
1995
- const iconMap = {
1996
- 'js': 'codicon-file-code', 'jsx': 'codicon-file-code',
1997
- 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1998
- 'html': 'codicon-file-code', 'css': 'codicon-file-code',
1999
- 'json': 'codicon-json', 'md': 'codicon-markdown',
2000
- 'py': 'codicon-file-code', 'java': 'codicon-file-code',
2001
- 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
2002
- 'cob': 'codicon-file-code', 'cbl': 'codicon-file-code',
2003
- 'sql': 'codicon-database', 'txt': 'codicon-file'
2004
- };
2005
- return iconMap[ext] || 'codicon-file';
2006
- }
2007
-
2008
- // Render tree node recursively
2009
- function renderTreeNode(node, path = '', depth = 0) {
2010
- let html = '';
2011
-
2012
- // Sort folders first, then files
2013
- const folderNames = Object.keys(node.folders).sort();
2014
- const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
2015
-
2016
- // Render folders
2017
- folderNames.forEach(folderName => {
2018
- const folderPath = path ? `${path}/${folderName}` : folderName;
2019
- const isExpanded = expandedFolders.has(folderPath);
2020
-
2021
- html += `
2022
- <div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
2023
- <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
2024
- <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
2025
- <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #dcb67a;"></span>
2026
- <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
2027
- </div>
2028
- <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">
2029
- ${renderTreeNode(node.folders[folderName], folderPath, depth + 1)}
2030
- </div>
2031
- </div>
2032
- `;
2033
- });
2034
-
2035
- // Render files
2036
- sortedFiles.forEach(file => {
2037
- html += `
2038
- <div class="file-tree-file" data-path="${escapeHtml(file.path)}">
2039
- <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
2040
- <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
2041
- </div>
2042
- `;
2043
- });
2044
-
2045
- return html;
2046
- }
2047
-
2048
- // Render file tree
2049
- function renderFileTree(files) {
2050
- if (files.length === 0) {
2051
- fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>';
2052
- return;
2053
- }
2054
-
2055
- // Build and render tree structure
2056
- const tree = buildFileTree(files);
2057
- fileTree.innerHTML = renderTreeNode(tree);
2058
-
2059
- // Add click handlers for folders
2060
- fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
2061
- header.onclick = (e) => {
2062
- e.stopPropagation();
2063
- const folderPath = header.dataset.folder;
2064
- const folder = header.closest('.file-tree-folder');
2065
- const contents = folder.querySelector('.file-tree-folder-contents');
2066
- const chevron = header.querySelector('.file-tree-folder-icon');
2067
- const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
2068
-
2069
- if (expandedFolders.has(folderPath)) {
2070
- expandedFolders.delete(folderPath);
2071
- contents.classList.remove('expanded');
2072
- chevron.classList.remove('expanded');
2073
- folderIcon.classList.remove('codicon-folder-opened');
2074
- folderIcon.classList.add('codicon-folder');
2075
- } else {
2076
- expandedFolders.add(folderPath);
2077
- contents.classList.add('expanded');
2078
- chevron.classList.add('expanded');
2079
- folderIcon.classList.remove('codicon-folder');
2080
- folderIcon.classList.add('codicon-folder-opened');
2081
- }
2082
- };
2083
- });
2084
-
2085
- // Add click handlers for files
2086
- fileTree.querySelectorAll('.file-tree-file').forEach(item => {
2087
- item.onclick = () => {
2088
- const path = item.dataset.path;
2089
- closeFileTree();
2090
- openFileInEditor(path);
2091
- };
2092
- });
2093
- }
2094
-
2095
- // Setup file tree event listeners
2096
- function setupFileTree() {
2097
- const explorerBtn = document.getElementById('editorExplorerBtn');
2098
-
2099
- // Explorer button click toggles file tree
2100
- explorerBtn.onclick = (e) => {
2101
- e.stopPropagation();
2102
- toggleFileTree();
2103
- };
2104
-
2105
- // Close button
2106
- fileTreeClose.onclick = (e) => {
2107
- e.stopPropagation();
2108
- closeFileTree();
2109
- };
2110
-
2111
- // Prevent clicks inside panel from closing it
2112
- fileTreePanel.onclick = (e) => {
2113
- e.stopPropagation();
2114
- };
2115
-
2116
- // Close on escape key
2117
- document.addEventListener('keydown', (e) => {
2118
- if (e.key === 'Escape' && fileTreeOpen) {
2119
- closeFileTree();
2120
- }
2121
- });
2122
- }
2123
-
2124
- // Initialize file tree
2125
- setupFileTree();
2126
-
2127
- // ==================== Editor Search ====================
2128
-
2129
- function openEditorSearch() {
2130
- editorSearchBar.classList.add('open');
2131
- editorSearchInput.focus();
2132
- editorSearchInput.select();
2133
- }
2134
-
2135
- function closeEditorSearch() {
2136
- editorSearchBar.classList.remove('open');
2137
- editorSearchInput.value = '';
2138
- searchMatches = [];
2139
- currentMatchIndex = -1;
2140
- editorSearchCount.textContent = '';
2141
- clearSearchHighlights();
2142
- }
2143
-
2144
- function clearSearchHighlights() {
2145
- // Restore original HTML for lines that were modified
2146
- const modifiedLines = panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]');
2147
- modifiedLines.forEach(el => {
2148
- el.innerHTML = el.dataset.originalHtml;
2149
- delete el.dataset.originalHtml;
2150
- });
2151
- }
2152
-
2153
- function performSearch(query) {
2154
- clearSearchHighlights();
2155
- searchMatches = [];
2156
- currentMatchIndex = -1;
2157
-
2158
- if (!query || query.length < 1) {
2159
- editorSearchCount.textContent = '';
2160
- return;
2161
- }
2162
-
2163
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
2164
- const queryLower = query.toLowerCase();
2165
-
2166
- codeElements.forEach((lineEl, lineIndex) => {
2167
- const text = lineEl.textContent;
2168
- const textLower = text.toLowerCase();
2169
- let startIndex = 0;
2170
- let matchIndex;
2171
-
2172
- while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
2173
- searchMatches.push({
2174
- lineIndex,
2175
- lineEl,
2176
- startIndex: matchIndex,
2177
- endIndex: matchIndex + query.length
2178
- });
2179
- startIndex = matchIndex + 1;
2180
- }
2181
- });
2182
-
2183
- if (searchMatches.length > 0) {
2184
- highlightMatches(query);
2185
- currentMatchIndex = 0;
2186
- scrollToMatch(0);
2187
- updateSearchCount();
2188
- } else {
2189
- editorSearchCount.textContent = 'No results';
2190
- }
2191
- }
2192
-
2193
- function highlightMatches(query) {
2194
- const codeElements = panels.editor.content.querySelectorAll('.editor-line-code');
2195
- const queryLower = query.toLowerCase();
2196
-
2197
- codeElements.forEach(lineEl => {
2198
- const text = lineEl.textContent;
2199
- const textLower = text.toLowerCase();
2200
-
2201
- if (textLower.includes(queryLower)) {
2202
- // Store original HTML to restore later
2203
- if (!lineEl.dataset.originalHtml) {
2204
- lineEl.dataset.originalHtml = lineEl.innerHTML;
2205
- }
2206
-
2207
- // Work with text content, find all match positions
2208
- const matches = [];
2209
- let idx = 0;
2210
- while ((idx = textLower.indexOf(queryLower, idx)) !== -1) {
2211
- matches.push({ start: idx, end: idx + query.length });
2212
- idx++;
2213
- }
2214
-
2215
- if (matches.length > 0) {
2216
- // Rebuild the line with highlights
2217
- let result = '';
2218
- let lastEnd = 0;
2219
-
2220
- matches.forEach(match => {
2221
- // Add text before match (escaped)
2222
- result += escapeHtml(text.slice(lastEnd, match.start));
2223
- // Add highlighted match
2224
- result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
2225
- lastEnd = match.end;
2226
- });
2227
-
2228
- // Add remaining text
2229
- result += escapeHtml(text.slice(lastEnd));
2230
-
2231
- lineEl.innerHTML = result;
2232
- }
2233
- }
2234
- });
2235
- }
2236
-
2237
- function scrollToMatch(index) {
2238
- const highlights = panels.editor.content.querySelectorAll('.search-highlight');
2239
-
2240
- // Remove current class from all
2241
- highlights.forEach(el => el.classList.remove('current'));
2242
-
2243
- if (highlights[index]) {
2244
- highlights[index].classList.add('current');
2245
-
2246
- // Get the scroll container and the highlight element
2247
- const container = panels.editor.content;
2248
- const element = highlights[index];
2249
-
2250
- // Get the line element (parent of the highlight)
2251
- const lineEl = element.closest('.editor-line');
2252
- if (lineEl) {
2253
- // Calculate scroll position to center the line
2254
- const containerRect = container.getBoundingClientRect();
2255
- const lineRect = lineEl.getBoundingClientRect();
2256
- const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
2257
-
2258
- container.scrollTo({
2259
- top: Math.max(0, scrollTop),
2260
- behavior: 'smooth'
2261
- });
2262
- }
2263
- }
2264
- }
2265
-
2266
- function updateSearchCount() {
2267
- if (searchMatches.length > 0) {
2268
- editorSearchCount.textContent = `${currentMatchIndex + 1}/${searchMatches.length}`;
2269
- } else {
2270
- editorSearchCount.textContent = 'No results';
1846
+ showEmptyState('editor', 'codicon-file-code', 'Failed to load file.');
2271
1847
  }
2272
1848
  }
2273
1849
 
2274
- function goToNextMatch() {
2275
- if (searchMatches.length === 0) return;
2276
- currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
2277
- scrollToMatch(currentMatchIndex);
2278
- updateSearchCount();
2279
- }
1850
+ // =============================================================================
1851
+ // Event Listeners Setup
1852
+ // =============================================================================
1853
+ $('editorExplorerBtn').onclick = (e) => { e.stopPropagation(); toggleFileTree(); };
1854
+ fileTreeDropdown.onclick = (e) => { e.stopPropagation(); };
2280
1855
 
2281
- function goToPrevMatch() {
2282
- if (searchMatches.length === 0) return;
2283
- currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
2284
- scrollToMatch(currentMatchIndex);
2285
- updateSearchCount();
2286
- }
1856
+ // Close dropdown when clicking outside
1857
+ document.addEventListener('click', (e) => {
1858
+ if (fileTreeOpen && !e.target.closest('.editor-explorer-wrapper')) closeFileTree();
1859
+ });
2287
1860
 
2288
- // Setup editor search
2289
- function setupEditorSearch() {
2290
- editorSearchBtn.onclick = () => openEditorSearch();
2291
- editorSearchClose.onclick = () => closeEditorSearch();
2292
- editorSearchNext.onclick = () => goToNextMatch();
2293
- editorSearchPrev.onclick = () => goToPrevMatch();
2294
-
2295
- editorSearchInput.oninput = (e) => {
2296
- performSearch(e.target.value);
2297
- };
2298
-
2299
- editorSearchInput.onkeydown = (e) => {
2300
- if (e.key === 'Enter') {
2301
- e.preventDefault();
2302
- if (e.shiftKey) {
2303
- goToPrevMatch();
2304
- } else {
2305
- goToNextMatch();
2306
- }
2307
- } else if (e.key === 'Escape') {
2308
- closeEditorSearch();
2309
- }
2310
- };
2311
-
2312
- // Ctrl+F / Cmd+F to open search
2313
- document.addEventListener('keydown', (e) => {
2314
- if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') {
2315
- e.preventDefault();
2316
- openEditorSearch();
2317
- }
2318
- });
2319
- }
1861
+ document.addEventListener('keydown', (e) => {
1862
+ if (e.key === 'Escape' && fileTreeOpen) closeFileTree();
1863
+ });
2320
1864
 
2321
- setupEditorSearch();
2322
-
2323
- // Visibility change handler
2324
1865
  document.addEventListener('visibilitychange', () => {
2325
- if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
2326
- connect();
2327
- }
1866
+ if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) connect();
2328
1867
  });
2329
-
1868
+
2330
1869
  // Initialize
2331
1870
  connect();
2332
1871
  </script>