kiro-mobile-bridge 1.0.7 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,234 +9,218 @@
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); }
80
- .file-tree-folder-icon { color: #888; font-size: 14px; transition: transform 0.15s; }
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); }
175
+ .file-tree-folder-icon { color: #a78bfa; 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
215
 
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
- }
216
+ /* =============================================================================
217
+ Mobile Bridge Customizations - Hide Follow and Revert buttons in snackbar
218
+ ============================================================================= */
219
+ .kiro-snackbar-actions .kiro-button[data-variant="primary"] { display: none !important; }
235
220
  </style>
236
221
  </head>
237
222
  <body>
238
223
  <div class="app">
239
- <!-- Navigation Bar (tabs | title + indicator) -->
240
224
  <nav class="nav-bar">
241
225
  <div class="nav-tabs" id="navTabs">
242
226
  <div class="nav-tab active" data-panel="chat">
@@ -245,17 +229,18 @@
245
229
  </div>
246
230
  <div class="nav-tab" data-panel="editor">
247
231
  <span class="codicon codicon-code"></span>
248
- <span>Editor</span>
232
+ <span>Code</span>
233
+ </div>
234
+ <div class="nav-tab" data-panel="tasks">
235
+ <span class="codicon codicon-checklist"></span>
236
+ <span>Tasks</span>
249
237
  </div>
250
238
  </div>
251
- <span class="header-title">Kiro Mobile</span>
252
239
  <div class="status">
253
240
  <span class="status-dot" id="statusDot"></span>
254
- <span class="status-text" id="statusText">Connecting...</span>
255
241
  </div>
256
242
  </nav>
257
243
 
258
- <!-- Panel Container -->
259
244
  <div class="panel-container">
260
245
  <!-- Chat Panel -->
261
246
  <div class="panel active" id="chatPanel">
@@ -268,251 +253,214 @@
268
253
  <!-- Editor Panel -->
269
254
  <div class="panel" id="editorPanel">
270
255
  <div class="editor-container">
271
- <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading editor...</span></div>
256
+ <div class="loading" id="editorLoading"><div class="loading-spinner"></div><span>Loading code...</span></div>
272
257
  <div class="editor-header" id="editorHeader" style="display: none;">
258
+ <div class="editor-explorer-wrapper">
259
+ <div class="editor-explorer-btn" id="editorExplorerBtn" title="Browse and select files">
260
+ <span class="codicon codicon-folder" style="color: #a78bfa;"></span>
261
+ <span class="editor-explorer-text">EXPLORER</span>
262
+ <span class="codicon codicon-chevron-down editor-explorer-indicator" id="explorerChevron"></span>
263
+ </div>
264
+ <div class="file-tree-dropdown" id="fileTreeDropdown">
265
+ <div class="file-tree" id="fileTree">
266
+ <div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>
267
+ </div>
268
+ </div>
269
+ </div>
273
270
  <span class="codicon codicon-file-code" style="color: #888;"></span>
274
271
  <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>
272
+ <div style="flex: 1;"></div>
273
+ <span class="editor-search-btn codicon codicon-search" id="editorSearchBtn" title="Find"></span>
280
274
  </div>
281
275
  <div class="editor-search-bar" id="editorSearchBar">
282
- <input type="text" id="editorSearchInput" placeholder="Find in file..." autocomplete="off" />
276
+ <input type="text" id="editorSearchInput" placeholder="Find..." autocomplete="off" />
283
277
  <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>
278
+ <span class="editor-search-nav codicon codicon-arrow-up" id="editorSearchPrev"></span>
279
+ <span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext"></span>
280
+ <span class="editor-search-close codicon codicon-close" id="editorSearchClose"></span>
297
281
  </div>
298
282
  <div class="editor-content" id="editorContent" style="display: none;"></div>
299
283
  </div>
300
284
  </div>
285
+
286
+ <!-- Tasks Panel -->
287
+ <div class="panel" id="tasksPanel">
288
+ <div class="tasks-container">
289
+ <div class="loading" id="tasksLoading"><div class="loading-spinner"></div><span>Loading tasks...</span></div>
290
+ <div class="tasks-header" id="tasksHeader" style="display: none;">
291
+ <span class="codicon codicon-checklist" style="color: #7138cc; font-size: 16px;"></span>
292
+ <select class="tasks-dropdown" id="tasksDropdown">
293
+ <option value="">Select a spec...</option>
294
+ </select>
295
+ </div>
296
+ <div class="tasks-content" id="tasksContent" style="display: none;"></div>
297
+ </div>
298
+ </div>
301
299
  </div>
302
300
 
303
301
  <div class="toast" id="toast"></div>
304
302
  </div>
305
303
 
306
304
  <script>
305
+ // =============================================================================
307
306
  // State
307
+ // =============================================================================
308
308
  let ws = null, reconnectAttempts = 0, reconnectTimeout = null;
309
309
  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
310
+ let isTypingInKiroInput = false, activePanel = 'chat';
311
+ let lastSuccessfulSnapshot = null, navigationPending = false;
312
+ let workspaceFiles = [], fileTreeOpen = false, expandedFolders = new Set();
313
+ let updateDebounceTimer = null, isRendering = false;
314
+ let tasksData = [], selectedTaskIndex = -1, tasksPollingTimer = null, lastTasksHash = '';
315
+ let searchMatches = [], currentMatchIndex = -1;
319
316
 
317
+ // =============================================================================
320
318
  // 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
319
+ // =============================================================================
320
+ const $ = id => document.getElementById(id);
321
+ const statusDot = $('statusDot');
322
+ const navTabs = $('navTabs');
323
+ const toast = $('toast');
324
+ const fileTreeDropdown = $('fileTreeDropdown');
325
+ const fileTree = $('fileTree');
326
+ const explorerChevron = $('explorerChevron');
327
+
330
328
  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
- }
329
+ chat: { panel: $('chatPanel'), loading: $('chatLoading'), content: $('chatContent'), container: $('chatContainer') },
330
+ editor: { panel: $('editorPanel'), loading: $('editorLoading'), content: $('editorContent'), header: $('editorHeader'), filename: $('editorFilename') },
331
+ tasks: { panel: $('tasksPanel'), loading: $('tasksLoading'), content: $('tasksContent'), header: $('tasksHeader'), dropdown: $('tasksDropdown') }
344
332
  };
345
333
 
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
334
+ // =============================================================================
335
+ // Utilities
336
+ // =============================================================================
361
337
  function setStatus(status) {
362
338
  statusDot.className = 'status-dot ' + status;
363
- statusText.textContent = status === 'connected' ? 'Connected' : status === 'disconnected' ? 'Disconnected' : 'Connecting...';
364
339
  }
365
-
340
+
366
341
  function showToast(message, duration = 3000, isSuccess = false) {
367
342
  toast.textContent = message;
368
343
  toast.className = 'toast visible' + (isSuccess ? ' success' : '');
369
- setTimeout(() => { toast.classList.remove('visible'); }, duration);
344
+ setTimeout(() => toast.classList.remove('visible'), duration);
370
345
  }
371
-
346
+
347
+ function escapeHtml(text) {
348
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
349
+ }
350
+
351
+ function showLoading(panelName, msg = 'Loading...') {
352
+ const p = panels[panelName];
353
+ if (p.loading) { p.loading.querySelector('span').textContent = msg; p.loading.style.display = 'flex'; }
354
+ if (p.content) p.content.style.display = 'none';
355
+ if (p.header) p.header.style.display = 'none';
356
+ }
357
+
358
+ function hideLoading(panelName) {
359
+ const p = panels[panelName];
360
+ if (p.loading) p.loading.style.display = 'none';
361
+ if (p.content) p.content.style.display = 'block';
362
+ if (p.header) p.header.style.display = 'flex';
363
+ }
364
+
365
+ function showEmptyState(panelName, icon, message) {
366
+ const p = panels[panelName];
367
+ p.content.innerHTML = `<div class="empty-state"><span class="codicon ${icon}"></span><p>${message}</p></div>`;
368
+ hideLoading(panelName);
369
+ }
370
+
371
+ // =============================================================================
372
372
  // Navigation
373
+ // =============================================================================
373
374
  navTabs.addEventListener('click', (e) => {
374
375
  const tab = e.target.closest('.nav-tab');
375
- if (!tab) return;
376
+ if (!tab || tab.dataset.panel === activePanel) return;
376
377
 
377
- const panelName = tab.dataset.panel;
378
- if (panelName === activePanel) return;
378
+ if (activePanel === 'tasks') stopTasksPolling();
379
379
 
380
- // Update tabs
381
380
  navTabs.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
382
381
  tab.classList.add('active');
383
382
 
384
- // Update panels
385
- Object.keys(panels).forEach(key => {
386
- panels[key].panel.classList.toggle('active', key === panelName);
387
- });
388
-
389
- activePanel = panelName;
383
+ Object.keys(panels).forEach(key => panels[key].panel.classList.toggle('active', key === tab.dataset.panel));
384
+ activePanel = tab.dataset.panel;
390
385
 
391
- // Fetch content for the new panel
392
- if (selectedCascadeId) {
393
- fetchPanelContent(panelName, selectedCascadeId);
394
- }
386
+ if (selectedCascadeId) fetchPanelContent(activePanel, selectedCascadeId);
395
387
  });
396
-
397
- // WebSocket connection
388
+
389
+ // =============================================================================
390
+ // WebSocket Connection
391
+ // =============================================================================
398
392
  function connect() {
399
393
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
400
394
  ws = new WebSocket(`${protocol}//${window.location.host}`);
401
395
 
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
-
396
+ ws.onopen = () => { setStatus('connected'); reconnectAttempts = 0; };
397
+ ws.onmessage = (e) => { try { handleMessage(JSON.parse(e.data)); } catch (err) {} };
398
+ ws.onclose = () => { setStatus('disconnected'); scheduleReconnect(); };
420
399
  ws.onerror = () => {};
421
400
  }
422
-
401
+
423
402
  function scheduleReconnect() {
424
403
  if (reconnectTimeout) clearTimeout(reconnectTimeout);
425
404
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
426
405
  reconnectAttempts++;
427
406
  reconnectTimeout = setTimeout(connect, delay);
428
407
  }
429
-
408
+
430
409
  function handleMessage(data) {
431
410
  if (data.type === 'cascade_list') {
432
411
  cascades = data.cascades || [];
433
- if (!selectedCascadeId && cascades.length > 0) {
434
- selectCascade(cascades[0].id);
435
- }
412
+ if (!selectedCascadeId && cascades.length > 0) selectCascade(cascades[0].id);
436
413
  } 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
414
  const panel = data.panel || 'chat';
440
415
  if (!isTypingInKiroInput && panel === activePanel && !isRendering) {
441
- // Debounce rapid updates - wait 500ms before fetching
442
416
  if (updateDebounceTimer) clearTimeout(updateDebounceTimer);
443
- updateDebounceTimer = setTimeout(() => {
444
- fetchPanelContent(panel, data.cascadeId, true); // true = isUpdate (don't scroll)
445
- }, 500);
417
+ updateDebounceTimer = setTimeout(() => fetchPanelContent(panel, data.cascadeId, true), 500);
446
418
  }
447
419
  }
448
420
  }
449
-
421
+
450
422
  function selectCascade(cascadeId) {
451
423
  selectedCascadeId = cascadeId;
452
424
  currentStyles = null;
453
- workspaceFiles = []; // Clear file cache when switching cascades
425
+ workspaceFiles = [];
426
+ lastTasksHash = '';
427
+ stopTasksPolling();
454
428
 
455
429
  if (cascadeId) {
456
- fetchStyles(cascadeId).then(() => {
457
- fetchPanelContent(activePanel, cascadeId);
458
- });
430
+ fetchStyles(cascadeId).then(() => fetchPanelContent(activePanel, cascadeId));
459
431
  } else {
460
432
  showLoading('chat', 'Waiting for Kiro...');
461
433
  }
462
434
  }
463
-
435
+
436
+ // =============================================================================
437
+ // Data Fetching
438
+ // =============================================================================
464
439
  async function fetchStyles(cascadeId) {
465
440
  try {
466
441
  const r = await fetch(`/styles/${cascadeId}`);
467
442
  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';
443
+ } catch (e) {}
488
444
  }
489
-
490
- // Fetch panel content
445
+
491
446
  async function fetchPanelContent(panelName, cascadeId, isUpdate = false) {
492
447
  switch (panelName) {
493
- case 'chat':
494
- await fetchChatSnapshot(cascadeId, isUpdate);
495
- break;
496
- case 'editor':
497
- await fetchEditorSnapshot(cascadeId, isUpdate);
498
- break;
448
+ case 'chat': await fetchChatSnapshot(cascadeId, isUpdate); break;
449
+ case 'editor': await fetchEditorSnapshot(cascadeId, isUpdate); break;
450
+ case 'tasks': await fetchTasksContent(cascadeId, isUpdate); break;
499
451
  }
500
452
  }
501
-
502
- // Chat snapshot
453
+
503
454
  async function fetchChatSnapshot(cascadeId, isUpdate = false) {
504
455
  try {
505
456
  const r = await fetch(`/snapshot/${cascadeId}`);
506
457
  if (!r.ok) {
507
- if (navigationPending) {
508
- setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500);
509
- return;
510
- }
458
+ if (navigationPending) { setTimeout(() => fetchChatSnapshot(cascadeId, isUpdate), 500); return; }
511
459
  if (r.status === 404) showLoading('chat', 'Waiting for snapshot...');
512
460
  return;
513
461
  }
514
462
  const snapshot = await r.json();
515
- if (snapshot && snapshot.html && snapshot.html.length > 100) {
463
+ if (snapshot?.html?.length > 100) {
516
464
  lastSuccessfulSnapshot = snapshot;
517
465
  navigationPending = false;
518
466
  renderChatSnapshot(snapshot, isUpdate);
@@ -525,68 +473,7 @@
525
473
  if (!navigationPending) showToast('Failed to load chat');
526
474
  }
527
475
  }
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
476
+
590
477
  async function fetchEditorSnapshot(cascadeId, isUpdate = false) {
591
478
  try {
592
479
  const r = await fetch(`/editor/${cascadeId}`);
@@ -594,320 +481,192 @@
594
481
  showEmptyState('editor', 'codicon-file-code', 'No file is currently open. Open a file in Kiro to view it here.');
595
482
  return;
596
483
  }
597
- const data = await r.json();
598
- renderEditorSnapshot(data, isUpdate);
484
+ renderEditorSnapshot(await r.json(), isUpdate);
599
485
  } catch (e) {
600
486
  showEmptyState('editor', 'codicon-file-code', 'Failed to load editor');
601
487
  }
602
488
  }
603
-
604
- function renderEditorSnapshot(data, isUpdate = false) {
605
- const scrollTop = panels.editor.content.scrollTop;
489
+
490
+ async function fetchTasksContent(cascadeId, isUpdate = false) {
491
+ // Only show loading if we don't have cached data
492
+ const hasCachedData = tasksData.length > 0 && lastTasksHash;
493
+ if (!isUpdate && !hasCachedData) showLoading('tasks', 'Loading tasks...');
606
494
 
607
- // Update header
608
- panels.editor.filename.textContent = data.fileName || 'Untitled';
495
+ try {
496
+ const r = await fetch(`/tasks/${cascadeId}`);
497
+ if (!r.ok) { showTasksEmptyState('No tasks found.'); stopTasksPolling(); return; }
498
+
499
+ const data = await r.json();
500
+ const newTasks = data.tasks || [];
501
+ const newHash = JSON.stringify(newTasks);
502
+
503
+ if (newTasks.length === 0) { showTasksEmptyState('No task files found.'); stopTasksPolling(); return; }
504
+
505
+ if (newHash !== lastTasksHash) {
506
+ lastTasksHash = newHash;
507
+ tasksData = newTasks;
508
+ populateTasksDropdown();
509
+ if (selectedTaskIndex < 0 || selectedTaskIndex >= tasksData.length) selectedTaskIndex = 0;
510
+ renderTaskContent(tasksData[selectedTaskIndex]);
511
+ }
512
+ // Always ensure UI is visible after successful fetch
513
+ panels.tasks.header.style.display = 'flex';
514
+ panels.tasks.content.style.display = 'block';
515
+ hideLoading('tasks');
516
+ startTasksPolling(cascadeId);
517
+ } catch (e) {
518
+ if (!isUpdate) showTasksEmptyState('Failed to load tasks');
519
+ stopTasksPolling();
520
+ }
521
+ }
522
+
523
+ // =============================================================================
524
+ // Tasks Panel
525
+ // =============================================================================
526
+ function startTasksPolling(cascadeId) {
527
+ if (tasksPollingTimer) return;
528
+ tasksPollingTimer = setInterval(() => {
529
+ if (activePanel === 'tasks' && selectedCascadeId) fetchTasksContent(selectedCascadeId, true);
530
+ }, 3000);
531
+ }
532
+
533
+ function stopTasksPolling() {
534
+ if (tasksPollingTimer) { clearInterval(tasksPollingTimer); tasksPollingTimer = null; }
535
+ }
536
+
537
+ function showTasksEmptyState(message) {
538
+ panels.tasks.header.style.display = 'none';
539
+ panels.tasks.content.innerHTML = `<div class="tasks-empty"><span class="codicon codicon-checklist"></span><p>${message}</p></div>`;
540
+ panels.tasks.content.style.display = 'flex';
541
+ hideLoading('tasks');
542
+ }
543
+
544
+ function populateTasksDropdown() {
545
+ const dropdown = panels.tasks.dropdown;
546
+ dropdown.innerHTML = '';
547
+ tasksData.forEach((task, index) => {
548
+ const option = document.createElement('option');
549
+ option.value = index;
550
+ option.textContent = task.name;
551
+ if (index === selectedTaskIndex) option.selected = true;
552
+ dropdown.appendChild(option);
553
+ });
609
554
 
610
- // Show header
611
- panels.editor.header.style.display = 'flex';
555
+ dropdown.onchange = async (e) => {
556
+ selectedTaskIndex = parseInt(e.target.value, 10);
557
+ if (tasksData[selectedTaskIndex]) {
558
+ renderTaskContent(tasksData[selectedTaskIndex]);
559
+ await openSpecInKiro(tasksData[selectedTaskIndex].name);
560
+ }
561
+ };
562
+ }
563
+
564
+ async function openSpecInKiro(specName) {
565
+ if (!selectedCascadeId || !specName) return;
566
+ showToast(`Opening ${specName}...`, 1500);
567
+ try {
568
+ await fetch(`/open-spec/${selectedCascadeId}`, {
569
+ method: 'POST',
570
+ headers: { 'Content-Type': 'application/json' },
571
+ body: JSON.stringify({ specName })
572
+ });
573
+ } catch (e) {}
574
+ }
575
+
576
+ function renderTaskContent(task) {
577
+ if (!task?.content) { panels.tasks.content.innerHTML = '<div class="tasks-empty"><p>No content</p></div>'; return; }
578
+ panels.tasks.content.innerHTML = `<div class="task-md">${parseTaskMarkdown(task.content)}</div>`;
579
+
580
+ panels.tasks.content.querySelectorAll('.task-group-header').forEach(header => {
581
+ const clickables = header.querySelectorAll('.task-group-title, .task-group-indicator, .task-group-arrow, .task-group-count');
582
+ clickables.forEach(el => {
583
+ if (el) {
584
+ el.style.cursor = 'pointer';
585
+ el.onclick = (e) => { e.stopPropagation(); header.closest('.task-group').classList.toggle('expanded'); };
586
+ }
587
+ });
588
+ });
589
+ }
590
+
591
+ function parseTaskMarkdown(markdown) {
592
+ const lines = markdown.split('\n');
593
+ let html = '';
594
+ let currentGroupNumber = '', currentGroupTitle = '', currentGroupTasks = [];
612
595
 
613
- // Store original content for search
614
- originalEditorContent = data.content || '';
596
+ const arrowIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg>`;
597
+ const checkIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg>`;
615
598
 
616
- // Reset search when new file loaded
617
- closeEditorSearch();
599
+ const flushGroup = () => {
600
+ if (currentGroupTitle && currentGroupTasks.length > 0) {
601
+ const completed = currentGroupTasks.filter(t => t.checked).length;
602
+ const total = currentGroupTasks.length;
603
+ const isComplete = completed === total;
604
+ const hasIncomplete = completed < total;
605
+
606
+ html += `<div class="task-group${hasIncomplete ? ' expanded' : ''}${isComplete ? ' completed' : ''}" data-task-number="${currentGroupNumber}">`;
607
+ html += `<div class="task-group-header"><div class="task-group-indicator">${checkIcon}</div>`;
608
+ html += `<span class="task-group-number">${currentGroupNumber}</span>`;
609
+ html += `<span class="task-group-title">${escapeHtml(currentGroupTitle)}</span>`;
610
+ html += `<div class="task-group-meta"><span class="task-group-count">${completed}/${total}</span>`;
611
+ html += `<span class="task-group-arrow">${arrowIcon}</span></div></div>`;
612
+ html += `<div class="task-group-body"><div class="task-group-body-inner">`;
613
+
614
+ currentGroupTasks.slice(1).forEach(task => {
615
+ const indent = task.indent > 1 ? ` indent-${Math.min(task.indent, 3)}` : '';
616
+ html += `<div class="task-item${indent}"><span class="task-checkbox${task.checked ? ' checked' : ''}">${checkIcon}</span>`;
617
+ html += `<span class="task-text${task.checked ? ' completed' : ''}">${escapeHtml(task.text)}</span></div>`;
618
+ });
619
+
620
+ html += `</div></div></div>`;
621
+ }
622
+ currentGroupNumber = ''; currentGroupTitle = ''; currentGroupTasks = [];
623
+ };
618
624
 
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 = '';
625
+ for (const line of lines) {
626
+ const trimmed = line.trim();
627
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.startsWith('- [')) continue;
623
628
 
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>`;
629
+ const mainMatch = trimmed.match(/^- \[([ xX])\] (\d+)\.?\s+(.*)$/);
630
+ if (mainMatch) {
631
+ flushGroup();
632
+ currentGroupNumber = mainMatch[2];
633
+ currentGroupTitle = mainMatch[3];
634
+ currentGroupTasks.push({ text: mainMatch[3], checked: mainMatch[1].toLowerCase() === 'x', indent: 0 });
635
+ continue;
630
636
  }
631
637
 
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++;
638
+ const subMatch = trimmed.match(/^- \[([ xX])\] (\d+\.\d+)\s+(.*)$/);
639
+ if (subMatch && currentGroupTitle) {
640
+ currentGroupTasks.push({ text: subMatch[3], checked: subMatch[1].toLowerCase() === 'x', indent: 1 });
641
+ continue;
641
642
  }
642
643
 
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;
644
+ const taskMatch = line.match(/^(\s*)- \[([ xX])\] (.*)$/);
645
+ if (taskMatch) {
646
+ const indent = Math.floor(taskMatch[1].length / 2);
647
+ const isChecked = taskMatch[2].toLowerCase() === 'x';
648
+ const text = taskMatch[3];
649
+
650
+ const numMatch = text.match(/^(\d+)\.?\s+(.*)$/);
651
+ if (numMatch && indent === 0) {
652
+ flushGroup();
653
+ currentGroupNumber = numMatch[1];
654
+ currentGroupTitle = numMatch[2];
655
+ currentGroupTasks.push({ text: numMatch[2], checked: isChecked, indent: 0 });
656
+ } else if (currentGroupTitle) {
657
+ currentGroupTasks.push({ text, checked: isChecked, indent: Math.max(1, indent) });
658
+ }
659
+ }
673
660
  }
674
- hideLoading('editor');
675
661
 
676
- // Preserve scroll on updates
677
- if (isUpdate) {
678
- panels.editor.content.scrollTop = scrollTop;
679
- }
662
+ flushGroup();
663
+ return html;
680
664
  }
681
665
 
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
666
+
667
+ // =============================================================================
668
+ // Chat Rendering
669
+ // =============================================================================
911
670
  function getBaseStyles() {
912
671
  return `<style>
913
672
  :root, body, #root, .app {
@@ -944,38 +703,30 @@
944
703
  background-color: #1e1e1e !important;
945
704
  }
946
705
 
947
- /* ========== HIDE PLACEHOLDER TEXT IN INPUT AREA ========== */
948
- /* Placeholder removal is handled via JavaScript in removePlaceholderText() */
949
706
  /* Keep input elements visible and interactive */
950
- [contenteditable="true"],
951
- [data-lexical-editor="true"],
952
- .ProseMirror,
953
- textarea {
707
+ [contenteditable="true"], [data-lexical-editor="true"], .ProseMirror, textarea {
954
708
  display: block !important;
955
709
  visibility: visible !important;
956
710
  opacity: 1 !important;
957
711
  pointer-events: auto !important;
958
712
  }
959
713
 
960
- /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
961
- [role="tooltip"],
962
- [data-tooltip],
963
- [class*="tooltip"]:not(button):not([role="button"]),
964
- [class*="Tooltip"]:not(button):not([role="button"]),
965
- [class*="popover"]:not(button):not([role="button"]),
966
- [class*="Popover"]:not(button):not([role="button"]),
967
- [class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
968
- [class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
969
- [class*="modal"],
970
- [class*="Modal"] {
714
+ /* Hide tooltips, popovers, and overlay elements */
715
+ /* IMPORTANT: Do NOT hide model-related elements - use negative lookahead patterns */
716
+ [role="tooltip"], [data-tooltip],
717
+ [class*="tooltip"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]),
718
+ [class*="Tooltip"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]),
719
+ [class*="popover"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]):not([role="listbox"]):not([role="menu"]),
720
+ [class*="Popover"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]):not([role="listbox"]):not([role="menu"]),
721
+ [class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]):not([class*="model"]):not([class*="Model"]),
722
+ [class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]):not([class*="model"]):not([class*="Model"]) {
971
723
  display: none !important;
972
724
  visibility: hidden !important;
973
725
  opacity: 0 !important;
974
726
  pointer-events: none !important;
975
727
  }
976
728
 
977
- /* ========== MODEL SELECTOR DROPDOWN ========== */
978
- /* Dropdown menu panel - needs solid background and proper z-index */
729
+ /* Model selector dropdown */
979
730
  [class*="dropdown-menu"], [class*="dropdownMenu"], [class*="DropdownMenu"],
980
731
  [class*="dropdown-content"], [class*="dropdownContent"], [class*="DropdownContent"],
981
732
  [class*="model-list"], [class*="modelList"], [class*="ModelList"],
@@ -996,6 +747,7 @@
996
747
  /* Dropdown menu items */
997
748
  [class*="dropdown-item"], [class*="dropdownItem"], [class*="DropdownItem"],
998
749
  [class*="model-option"], [class*="modelOption"], [class*="ModelOption"],
750
+ .kiro-dropdown-item,
999
751
  [role="option"], [role="menuitem"] {
1000
752
  display: flex !important;
1001
753
  visibility: visible !important;
@@ -1007,31 +759,35 @@
1007
759
  color: #cccccc !important;
1008
760
  }
1009
761
 
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;
762
+ [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
763
+ .kiro-dropdown-item:hover,
764
+ [role="option"]:hover, [role="menuitem"]:hover {
765
+ background: rgba(255, 255, 255, 0.1) !important;
1020
766
  }
1021
767
 
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;
768
+ /* Hide model descriptions in dropdown - keep only name and credit */
769
+ .kiro-dropdown-item [class*="description"],
770
+ .kiro-dropdown-item [class*="Description"],
771
+ .kiro-dropdown-item [class*="subtitle"],
772
+ .kiro-dropdown-item [class*="Subtitle"],
773
+ .kiro-dropdown-item > div:last-child:not(:first-child),
774
+ .kiro-dropdown-menu [class*="description"],
775
+ .kiro-dropdown-menu [class*="Description"],
776
+ [class*="dropdown-item"] [class*="description"],
777
+ [class*="dropdown-item"] [class*="Description"],
778
+ [role="option"] [class*="description"],
779
+ [role="option"] [class*="Description"] {
780
+ display: none !important;
1027
781
  }
1028
782
 
1029
- [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
1030
- [role="option"]:hover, [role="menuitem"]:hover {
1031
- background: rgba(255, 255, 255, 0.1) !important;
783
+ /* Compact model dropdown items */
784
+ .kiro-dropdown-item,
785
+ .kiro-dropdown-menu > div {
786
+ padding: 8px 12px !important;
787
+ min-height: auto !important;
1032
788
  }
1033
789
 
1034
- /* Model selector button - NO border, match Kiro style */
790
+ /* Model selector button */
1035
791
  [class*="model-selector"], [class*="modelSelector"], [class*="ModelSelector"],
1036
792
  [class*="model-dropdown"], [class*="modelDropdown"], [class*="ModelDropdown"],
1037
793
  button[class*="dropdown"], [role="button"][class*="dropdown"],
@@ -1056,61 +812,28 @@
1056
812
  background: rgba(255, 255, 255, 0.1) !important;
1057
813
  }
1058
814
 
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 */
815
+ /* Reset positioning */
1092
816
  #root, .app, [class*="cascade"], [class*="Cascade"] {
1093
817
  position: relative !important;
1094
818
  }
1095
819
 
1096
- /* Ensure proper stacking and no fixed positioning breaks layout */
1097
- [style*="position: fixed"],
1098
- [style*="position:fixed"] {
820
+ [style*="position: fixed"], [style*="position:fixed"] {
1099
821
  position: absolute !important;
1100
822
  }
1101
823
 
1102
824
  * { font-family: inherit; }
1103
825
  code, pre, .monaco-editor { font-family: Menlo, Monaco, "Courier New", monospace !important; }
1104
- /* Icon sizes - keep them small like in IDE */
826
+
827
+ /* Icon sizes */
1105
828
  svg[viewBox="0 0 24 24"] { width: 16px !important; height: 16px !important; }
1106
829
  svg[viewBox="0 0 16 16"] { width: 14px !important; height: 14px !important; }
1107
830
  svg { color: inherit; max-width: 20px !important; max-height: 20px !important; }
1108
831
  svg path, svg rect { fill: currentColor; }
1109
832
  svg[fill="none"] path { fill: none; }
1110
- /* Preserve stroke-based SVG circles (like context window progress ring) */
1111
833
  svg circle[stroke] { fill: none !important; }
1112
834
  svg circle:not([stroke]) { fill: currentColor; }
1113
- /* Context window progress circle styling */
835
+
836
+ /* Context window progress circle */
1114
837
  svg[class*="context"], svg[class*="progress"], [class*="context"] svg {
1115
838
  overflow: visible !important;
1116
839
  }
@@ -1118,7 +841,6 @@
1118
841
  fill: none !important;
1119
842
  stroke-linecap: round !important;
1120
843
  }
1121
- /* Context window indicator - circular progress ring */
1122
844
  [class*="context-window"], [class*="contextWindow"], [class*="ContextWindow"],
1123
845
  [class*="context-indicator"], [class*="contextIndicator"], [class*="ContextIndicator"],
1124
846
  [class*="token-usage"], [class*="tokenUsage"], [class*="TokenUsage"] {
@@ -1132,7 +854,6 @@
1132
854
  position: relative !important;
1133
855
  flex-shrink: 0 !important;
1134
856
  }
1135
- /* Create gray background circle using box-shadow on the container */
1136
857
  [class*="context-window"]::before, [class*="contextWindow"]::before, [class*="ContextWindow"]::before,
1137
858
  [class*="context-indicator"]::before, [class*="contextIndicator"]::before {
1138
859
  content: '' !important;
@@ -1161,15 +882,12 @@
1161
882
  stroke-linecap: round !important;
1162
883
  stroke: #4ade80 !important;
1163
884
  }
1164
- /* Ensure circles are visible */
1165
- [class*="context"] svg circle,
1166
- [class*="Context"] svg circle {
885
+ [class*="context"] svg circle, [class*="Context"] svg circle {
1167
886
  display: block !important;
1168
887
  visibility: visible !important;
1169
888
  }
1170
889
 
1171
- /* ========== CHAT INPUT TOOLBAR AREA ========== */
1172
- /* The toolbar/footer area containing model selector, context, autopilot, input */
890
+ /* Chat input toolbar area */
1173
891
  [class*="chat-input"], [class*="chatInput"], [class*="ChatInput"],
1174
892
  [class*="input-area"], [class*="inputArea"], [class*="InputArea"],
1175
893
  [class*="message-input"], [class*="messageInput"], [class*="MessageInput"],
@@ -1183,7 +901,7 @@
1183
901
  border-top: 1px solid var(--color-border-primary, #4a464f) !important;
1184
902
  }
1185
903
 
1186
- /* Input toolbar row (contains # button, model selector, context, autopilot) */
904
+ /* Input toolbar row */
1187
905
  [class*="input-toolbar"], [class*="inputToolbar"], [class*="InputToolbar"],
1188
906
  [class*="chat-toolbar"], [class*="chatToolbar"], [class*="ChatToolbar"],
1189
907
  [class*="composer-toolbar"], [class*="composerToolbar"] {
@@ -1194,7 +912,7 @@
1194
912
  flex-wrap: wrap !important;
1195
913
  }
1196
914
 
1197
- /* Context button (# symbol) - NO border, match Kiro style */
915
+ /* Context button (# symbol) */
1198
916
  [class*="context-button"], [class*="contextButton"], [class*="ContextButton"],
1199
917
  button[aria-label*="context"], button[aria-label*="Context"],
1200
918
  [class*="hash-button"], [class*="hashButton"] {
@@ -1219,30 +937,15 @@
1219
937
  background: rgba(255, 255, 255, 0.1) !important;
1220
938
  }
1221
939
 
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 {
940
+ /* Message text flow - inline elements */
941
+ [class*="message"] p, [class*="Message"] p, [class*="chat"] p, [class*="Chat"] p {
1228
942
  display: block !important;
1229
943
  }
1230
944
 
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) {
945
+ [class*="message"] a, [class*="Message"] a, [class*="message"] code, [class*="Message"] code,
946
+ [class*="message"] span, [class*="Message"] span, [class*="chat"] a, [class*="Chat"] a,
947
+ [class*="chat"] code, [class*="Chat"] code, [class*="file-link"], [class*="FileLink"],
948
+ [class*="inline"], code:not(pre code) {
1246
949
  display: inline !important;
1247
950
  vertical-align: baseline !important;
1248
951
  }
@@ -1256,17 +959,10 @@
1256
959
  font-size: 12px !important;
1257
960
  }
1258
961
 
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"] {
962
+ /* File badges/chips */
963
+ [class*="file-badge"], [class*="FileBadge"], [class*="file-chip"], [class*="FileChip"],
964
+ [class*="deleted"], [class*="Deleted"], [class*="created"], [class*="Created"],
965
+ [class*="modified"], [class*="Modified"] {
1270
966
  display: inline-flex !important;
1271
967
  align-items: center !important;
1272
968
  gap: 4px !important;
@@ -1278,19 +974,10 @@
1278
974
  vertical-align: middle !important;
1279
975
  }
1280
976
 
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
- }
977
+ /* Bullet lists */
978
+ ul li, ol li { display: list-item !important; }
979
+ ul li > *, ol li > * { display: inline !important; }
980
+ ul li > p, ol li > p { display: inline !important; margin: 0 !important; }
1294
981
 
1295
982
  [role="switch"], [class*="toggle"] {
1296
983
  overflow: visible !important;
@@ -1298,117 +985,7 @@
1298
985
  flex-shrink: 0 !important;
1299
986
  }
1300
987
 
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
-
988
+ /* Toggle switch styling */
1412
989
  .kiro-toggle-switch {
1413
990
  display: flex !important;
1414
991
  align-items: center !important;
@@ -1447,207 +1024,654 @@
1447
1024
  }
1448
1025
  </style>`;
1449
1026
  }
1450
-
1451
- // Make chat elements interactive
1452
- function makeInteractive() {
1453
- const content = panels.chat.content;
1027
+
1028
+ function renderChatSnapshot(snapshot, isUpdate = false) {
1029
+ if (isRendering) return;
1030
+ isRendering = true;
1454
1031
 
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
- }
1032
+ let innerScrollable = panels.chat.content.querySelector('.overflow-y-auto, [class*="scroll"]');
1033
+ const hadContent = innerScrollable && innerScrollable.scrollHeight > 100;
1034
+ const scrollTop = innerScrollable ? innerScrollable.scrollTop : 0;
1035
+ const scrollHeight = innerScrollable ? innerScrollable.scrollHeight : 0;
1036
+ const clientHeight = innerScrollable ? innerScrollable.clientHeight : 0;
1037
+ const wasAtBottom = !hadContent || (scrollHeight - scrollTop - clientHeight < 100);
1476
1038
 
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
- });
1039
+ let html = getBaseStyles();
1040
+ if (currentStyles) html += `<style>${currentStyles}</style>`;
1041
+ html += snapshot.html || '';
1042
+
1043
+ panels.chat.content.innerHTML = html;
1044
+ panels.chat.container.style.background = snapshot.bodyBg || '#1e1e1e';
1045
+ hideLoading('chat');
1500
1046
 
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
1047
+ removePlaceholderText();
1048
+ fixContextWindowCircles();
1049
+ hideRevertButton();
1503
1050
 
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
- }
1051
+ requestAnimationFrame(() => {
1052
+ requestAnimationFrame(() => {
1053
+ // Only detect history panel by class names - NOT by date patterns
1054
+ // Date patterns are too aggressive and match regular chat timestamps
1055
+ const hasHistoryClass = panels.chat.content.querySelector('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"], [class*="conversation-list"], [class*="ConversationList"]');
1530
1056
 
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 = '';
1057
+ panels.chat.content.querySelectorAll('.overflow-y-auto, [class*="scroll"]').forEach(el => {
1058
+ const isHistoryContainer = el.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
1059
+ el.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
1060
+
1061
+ if (el.scrollHeight > el.clientHeight + 10) {
1062
+ if (isHistoryContainer) {
1063
+ el.scrollTop = 0;
1064
+ } else if (!isUpdate || wasAtBottom) {
1065
+ el.scrollTop = el.scrollHeight;
1066
+ } else if (hadContent) {
1067
+ el.scrollTop = scrollTop;
1540
1068
  }
1541
1069
  }
1542
- showToast('Sent!', 1500, true);
1543
- } else {
1544
- showToast('Type a message first', 1500);
1070
+ });
1071
+
1072
+ // Only scroll main content to top if it's clearly a history panel
1073
+ if (hasHistoryClass) {
1074
+ panels.chat.content.scrollTop = 0;
1075
+ } else if (!isUpdate || wasAtBottom) {
1076
+ panels.chat.content.scrollTop = panels.chat.content.scrollHeight;
1545
1077
  }
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
- }
1078
+ isRendering = false;
1079
+ });
1080
+ });
1555
1081
 
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);
1082
+ makeInteractive();
1083
+ }
1084
+
1085
+ function hideRevertButton() {
1086
+ // Hide Revert button by finding buttons with "Revert" text in snackbar
1087
+ panels.chat.content.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button').forEach(btn => {
1088
+ const text = (btn.textContent || '').trim().toLowerCase();
1089
+ if (text === 'revert') {
1090
+ btn.style.display = 'none';
1562
1091
  }
1563
1092
  });
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);
1573
- }
1093
+ }
1094
+
1095
+ function removePlaceholderText() {
1096
+ const content = panels.chat.content;
1097
+ content.querySelectorAll('[class*="placeholder"], [class*="Placeholder"]').forEach(el => {
1098
+ if (!el.matches('[contenteditable], textarea, input, [data-lexical-editor]') &&
1099
+ !el.querySelector('[contenteditable], textarea, input, [data-lexical-editor]')) {
1100
+ el.style.display = 'none';
1574
1101
  }
1575
1102
  });
1576
-
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
- }
1103
+ }
1104
+
1105
+ function fixContextWindowCircles() {
1106
+ panels.chat.content.querySelectorAll('svg').forEach(svg => {
1107
+ const circles = svg.querySelectorAll('circle');
1108
+ if (circles.length >= 1) {
1109
+ let progressCircle = null;
1110
+ circles.forEach(circle => {
1111
+ const dashArray = circle.getAttribute('stroke-dasharray') || circle.style.strokeDasharray;
1112
+ if (dashArray && dashArray !== 'none') progressCircle = circle;
1595
1113
  });
1596
- } catch(e) {}
1114
+
1115
+ if (progressCircle && !svg.querySelector('circle[data-track="true"]')) {
1116
+ const cx = progressCircle.getAttribute('cx') || '8';
1117
+ const cy = progressCircle.getAttribute('cy') || '8';
1118
+ const r = progressCircle.getAttribute('r') || '6';
1119
+ const strokeWidth = progressCircle.getAttribute('stroke-width') || '2';
1120
+
1121
+ const trackCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1122
+ trackCircle.setAttribute('cx', cx);
1123
+ trackCircle.setAttribute('cy', cy);
1124
+ trackCircle.setAttribute('r', r);
1125
+ trackCircle.setAttribute('stroke', '#3c3c3c');
1126
+ trackCircle.setAttribute('stroke-width', strokeWidth);
1127
+ trackCircle.setAttribute('fill', 'none');
1128
+ trackCircle.setAttribute('data-track', 'true');
1129
+
1130
+ svg.insertBefore(trackCircle, svg.firstChild);
1131
+ progressCircle.setAttribute('stroke', '#4ade80');
1132
+ }
1133
+ }
1134
+ });
1135
+ }
1136
+
1137
+ // =============================================================================
1138
+ // Editor Rendering
1139
+ // =============================================================================
1140
+ function renderEditorSnapshot(data, isUpdate = false) {
1141
+ const scrollTop = panels.editor.content.scrollTop;
1142
+ panels.editor.filename.textContent = data.fileName || 'Untitled';
1143
+ panels.editor.header.style.display = 'flex';
1144
+ closeEditorSearch();
1145
+
1146
+ if (data.content?.trim().length > 0) {
1147
+ const lines = data.content.split('\n');
1148
+ let html = '';
1149
+
1150
+ if (data.isPartial || data.note) {
1151
+ html += `<div style="padding: 8px 12px; background: #2d2d30; color: #888; font-size: 11px; border-bottom: 1px solid #3c3c3c;">
1152
+ <span class="codicon codicon-info" style="margin-right: 6px;"></span>${data.note || 'Showing visible lines only.'}
1153
+ </div>`;
1154
+ }
1155
+
1156
+ html += '<pre class="editor-code">';
1157
+ const startLineNum = data.startLine || 1;
1158
+ let startIdx = 0;
1159
+ while (startIdx < lines.length && lines[startIdx].trim() === '' && startIdx < 3) startIdx++;
1160
+
1161
+ lines.slice(startIdx).forEach((line, idx) => {
1162
+ const lineNum = startLineNum + startIdx + idx;
1163
+ const highlighted = highlightSyntax(line.replace(/\t/g, ' '), data.language);
1164
+ html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${highlighted || ' '}</span></div>`;
1165
+ });
1166
+ html += '</pre>';
1167
+
1168
+ const endLine = startLineNum + startIdx + lines.slice(startIdx).length - 1;
1169
+ html += `<div style="padding: 6px 12px; background: #252526; color: #666; font-size: 11px; border-top: 1px solid #3c3c3c;">
1170
+ Lines ${startLineNum}-${endLine}${data.lineCount ? ` of ${data.lineCount}` : ''}
1171
+ </div>`;
1172
+
1173
+ panels.editor.content.innerHTML = html;
1174
+ panels.editor.content.style.display = 'block';
1175
+ } else {
1176
+ showEmptyState('editor', 'codicon-file-code', 'No editor content available.');
1177
+ return;
1597
1178
  }
1598
1179
 
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;
1180
+ hideLoading('editor');
1181
+ if (isUpdate) panels.editor.content.scrollTop = scrollTop;
1182
+ }
1183
+
1184
+ function highlightSyntax(line, language) {
1185
+ const escaped = escapeHtml(line);
1186
+ if (escaped.length > 1000 || !language) return escaped;
1187
+
1188
+ const isJS = ['javascript', 'typescript', 'jsx', 'tsx', 'js', 'ts'].includes(language);
1189
+ const isPython = language === 'python' || language === 'py';
1190
+
1191
+ let result = '';
1192
+ let i = 0;
1193
+
1194
+ while (i < escaped.length) {
1195
+ // Comments
1196
+ if (isJS && escaped.slice(i, i + 2) === '//') { result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>'; break; }
1197
+ if ((isPython) && escaped[i] === '#') { result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>'; break; }
1603
1198
 
1604
- const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
1605
- const className = (btn.className || '').toLowerCase();
1606
- const innerHTML = btn.innerHTML.toLowerCase();
1199
+ // Strings
1200
+ if (escaped.slice(i, i + 6) === '&quot;') {
1201
+ const endIdx = escaped.indexOf('&quot;', i + 6);
1202
+ if (endIdx !== -1) { result += '<span class="tok-str">' + escaped.slice(i, endIdx + 6) + '</span>'; i = endIdx + 6; continue; }
1203
+ }
1204
+ if (escaped.slice(i, i + 5) === '&#39;') {
1205
+ const endIdx = escaped.indexOf('&#39;', i + 5);
1206
+ if (endIdx !== -1) { result += '<span class="tok-str">' + escaped.slice(i, endIdx + 5) + '</span>'; i = endIdx + 5; continue; }
1207
+ }
1607
1208
 
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;
1209
+ // Numbers
1210
+ if (/[0-9]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z_]/.test(escaped[i - 1]))) {
1211
+ let numEnd = i;
1212
+ while (numEnd < escaped.length && /[0-9.]/.test(escaped[numEnd])) numEnd++;
1213
+ if (numEnd > i) { result += '<span class="tok-num">' + escaped.slice(i, numEnd) + '</span>'; i = numEnd; continue; }
1615
1214
  }
1616
1215
 
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');
1216
+ // Keywords
1217
+ if (/[a-zA-Z_]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z0-9_]/.test(escaped[i - 1]))) {
1218
+ let wordEnd = i;
1219
+ while (wordEnd < escaped.length && /[a-zA-Z0-9_]/.test(escaped[wordEnd])) wordEnd++;
1220
+ const word = escaped.slice(i, wordEnd);
1221
+
1222
+ let keywords = [];
1223
+ 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'];
1224
+ 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'];
1225
+
1226
+ if (keywords.includes(word)) result += '<span class="tok-kw">' + word + '</span>';
1227
+ else result += word;
1228
+ i = wordEnd;
1229
+ continue;
1230
+ }
1620
1231
 
1621
- // If button has an SVG with arrow-like elements, it's likely a send button
1622
- if (hasSvg && hasArrowPath) {
1623
- attachSendHandler(btn);
1232
+ result += escaped[i];
1233
+ i++;
1234
+ }
1235
+
1236
+ return result;
1237
+ }
1238
+
1239
+ // =============================================================================
1240
+ // Editor Search
1241
+ // =============================================================================
1242
+ function openEditorSearch() {
1243
+ $('editorSearchBar').classList.add('open');
1244
+ $('editorSearchInput').focus();
1245
+ $('editorSearchInput').select();
1246
+ }
1247
+
1248
+ function closeEditorSearch() {
1249
+ $('editorSearchBar').classList.remove('open');
1250
+ $('editorSearchInput').value = '';
1251
+ searchMatches = [];
1252
+ currentMatchIndex = -1;
1253
+ $('editorSearchCount').textContent = '';
1254
+ panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]').forEach(el => {
1255
+ el.innerHTML = el.dataset.originalHtml;
1256
+ delete el.dataset.originalHtml;
1257
+ });
1258
+ }
1259
+
1260
+ function performSearch(query) {
1261
+ panels.editor.content.querySelectorAll('.editor-line-code[data-original-html]').forEach(el => {
1262
+ el.innerHTML = el.dataset.originalHtml;
1263
+ delete el.dataset.originalHtml;
1264
+ });
1265
+ searchMatches = [];
1266
+ currentMatchIndex = -1;
1267
+
1268
+ if (!query || query.length < 1) { $('editorSearchCount').textContent = ''; return; }
1269
+
1270
+ const queryLower = query.toLowerCase();
1271
+ panels.editor.content.querySelectorAll('.editor-line-code').forEach((lineEl, lineIndex) => {
1272
+ const text = lineEl.textContent;
1273
+ const textLower = text.toLowerCase();
1274
+ let startIndex = 0, matchIndex;
1275
+
1276
+ while ((matchIndex = textLower.indexOf(queryLower, startIndex)) !== -1) {
1277
+ searchMatches.push({ lineIndex, lineEl, startIndex: matchIndex, endIndex: matchIndex + query.length });
1278
+ startIndex = matchIndex + 1;
1279
+ }
1280
+ });
1281
+
1282
+ if (searchMatches.length > 0) {
1283
+ highlightSearchMatches(query);
1284
+ currentMatchIndex = 0;
1285
+ scrollToSearchMatch(0);
1286
+ updateSearchCount();
1287
+ } else {
1288
+ $('editorSearchCount').textContent = 'No results';
1289
+ }
1290
+ }
1291
+
1292
+ function highlightSearchMatches(query) {
1293
+ const queryLower = query.toLowerCase();
1294
+ panels.editor.content.querySelectorAll('.editor-line-code').forEach(lineEl => {
1295
+ const text = lineEl.textContent;
1296
+ if (text.toLowerCase().includes(queryLower)) {
1297
+ if (!lineEl.dataset.originalHtml) lineEl.dataset.originalHtml = lineEl.innerHTML;
1298
+
1299
+ const matches = [];
1300
+ let idx = 0;
1301
+ while ((idx = text.toLowerCase().indexOf(queryLower, idx)) !== -1) {
1302
+ matches.push({ start: idx, end: idx + query.length });
1303
+ idx++;
1304
+ }
1305
+
1306
+ if (matches.length > 0) {
1307
+ let result = '', lastEnd = 0;
1308
+ matches.forEach(match => {
1309
+ result += escapeHtml(text.slice(lastEnd, match.start));
1310
+ result += `<span class="search-highlight">${escapeHtml(text.slice(match.start, match.end))}</span>`;
1311
+ lastEnd = match.end;
1312
+ });
1313
+ result += escapeHtml(text.slice(lastEnd));
1314
+ lineEl.innerHTML = result;
1315
+ }
1316
+ }
1317
+ });
1318
+ }
1319
+
1320
+ function scrollToSearchMatch(index) {
1321
+ const highlights = panels.editor.content.querySelectorAll('.search-highlight');
1322
+ highlights.forEach(el => el.classList.remove('current'));
1323
+
1324
+ if (highlights[index]) {
1325
+ highlights[index].classList.add('current');
1326
+ const lineEl = highlights[index].closest('.editor-line');
1327
+ if (lineEl) {
1328
+ const container = panels.editor.content;
1329
+ const containerRect = container.getBoundingClientRect();
1330
+ const lineRect = lineEl.getBoundingClientRect();
1331
+ const scrollTop = container.scrollTop + (lineRect.top - containerRect.top) - (containerRect.height / 2) + (lineRect.height / 2);
1332
+ container.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' });
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ function updateSearchCount() {
1338
+ $('editorSearchCount').textContent = searchMatches.length > 0 ? `${currentMatchIndex + 1}/${searchMatches.length}` : 'No results';
1339
+ }
1340
+
1341
+ function goToNextMatch() {
1342
+ if (searchMatches.length === 0) return;
1343
+ currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
1344
+ scrollToSearchMatch(currentMatchIndex);
1345
+ updateSearchCount();
1346
+ }
1347
+
1348
+ function goToPrevMatch() {
1349
+ if (searchMatches.length === 0) return;
1350
+ currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
1351
+ scrollToSearchMatch(currentMatchIndex);
1352
+ updateSearchCount();
1353
+ }
1354
+
1355
+ // Setup search
1356
+ $('editorSearchBtn').onclick = openEditorSearch;
1357
+ $('editorSearchClose').onclick = closeEditorSearch;
1358
+ $('editorSearchNext').onclick = goToNextMatch;
1359
+ $('editorSearchPrev').onclick = goToPrevMatch;
1360
+ $('editorSearchInput').oninput = (e) => performSearch(e.target.value);
1361
+ $('editorSearchInput').onkeydown = (e) => {
1362
+ if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? goToPrevMatch() : goToNextMatch(); }
1363
+ else if (e.key === 'Escape') closeEditorSearch();
1364
+ };
1365
+ document.addEventListener('keydown', (e) => {
1366
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f' && activePanel === 'editor') { e.preventDefault(); openEditorSearch(); }
1367
+ });
1368
+
1369
+ // =============================================================================
1370
+ // File Tree
1371
+ // =============================================================================
1372
+ function toggleFileTree() {
1373
+ fileTreeOpen = !fileTreeOpen;
1374
+ fileTreeDropdown.classList.toggle('open', fileTreeOpen);
1375
+ explorerChevron.style.transform = fileTreeOpen ? 'rotate(180deg)' : '';
1376
+ if (fileTreeOpen && workspaceFiles.length === 0) fetchWorkspaceFiles();
1377
+ else if (fileTreeOpen) renderFileTree(workspaceFiles);
1378
+ }
1379
+
1380
+ function closeFileTree() {
1381
+ fileTreeOpen = false;
1382
+ fileTreeDropdown.classList.remove('open');
1383
+ explorerChevron.style.transform = '';
1384
+ }
1385
+
1386
+ async function fetchWorkspaceFiles() {
1387
+ if (!selectedCascadeId) return;
1388
+ fileTree.innerHTML = '<div class="file-tree-loading"><div class="loading-spinner"></div>Loading files...</div>';
1389
+
1390
+ try {
1391
+ const r = await fetch(`/files/${selectedCascadeId}`);
1392
+ if (!r.ok) throw new Error('Failed to fetch files');
1393
+ const data = await r.json();
1394
+ workspaceFiles = data.files || [];
1395
+ renderFileTree(workspaceFiles);
1396
+ } catch (e) {
1397
+ fileTree.innerHTML = '<div class="file-tree-empty">Failed to load files</div>';
1398
+ }
1399
+ }
1400
+
1401
+ function buildFileTree(files) {
1402
+ const root = { folders: {}, files: [] };
1403
+ files.forEach(file => {
1404
+ const parts = file.path.split(/[/\\]/);
1405
+ let current = root;
1406
+ for (let i = 0; i < parts.length - 1; i++) {
1407
+ const folderName = parts[i];
1408
+ if (!current.folders[folderName]) current.folders[folderName] = { folders: {}, files: [] };
1409
+ current = current.folders[folderName];
1624
1410
  }
1411
+ current.files.push({ name: parts[parts.length - 1], path: file.path });
1412
+ });
1413
+ return root;
1414
+ }
1415
+
1416
+ function getFileIcon(name) {
1417
+ const ext = name.split('.').pop()?.toLowerCase();
1418
+ const iconMap = {
1419
+ 'js': 'codicon-file-code', 'jsx': 'codicon-file-code', 'ts': 'codicon-file-code', 'tsx': 'codicon-file-code',
1420
+ 'html': 'codicon-file-code', 'css': 'codicon-file-code', 'json': 'codicon-json', 'md': 'codicon-markdown',
1421
+ 'py': 'codicon-file-code', 'java': 'codicon-file-code', 'go': 'codicon-file-code', 'rs': 'codicon-file-code',
1422
+ 'sql': 'codicon-database', 'txt': 'codicon-file'
1423
+ };
1424
+ return iconMap[ext] || 'codicon-file';
1425
+ }
1426
+
1427
+ function renderTreeNode(node, path = '') {
1428
+ let html = '';
1429
+ const folderNames = Object.keys(node.folders).sort();
1430
+ const sortedFiles = [...node.files].sort((a, b) => a.name.localeCompare(b.name));
1431
+
1432
+ folderNames.forEach(folderName => {
1433
+ const folderPath = path ? `${path}/${folderName}` : folderName;
1434
+ const isExpanded = expandedFolders.has(folderPath);
1435
+ html += `<div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
1436
+ <div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
1437
+ <span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
1438
+ <span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #a78bfa;"></span>
1439
+ <span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
1440
+ </div>
1441
+ <div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">${renderTreeNode(node.folders[folderName], folderPath)}</div>
1442
+ </div>`;
1443
+ });
1444
+
1445
+ sortedFiles.forEach(file => {
1446
+ html += `<div class="file-tree-file" data-path="${escapeHtml(file.path)}">
1447
+ <span class="file-tree-file-icon codicon ${getFileIcon(file.name)}"></span>
1448
+ <span class="file-tree-file-name">${escapeHtml(file.name)}</span>
1449
+ </div>`;
1450
+ });
1451
+
1452
+ return html;
1453
+ }
1454
+
1455
+ function renderFileTree(files) {
1456
+ if (files.length === 0) { fileTree.innerHTML = '<div class="file-tree-empty">No files found</div>'; return; }
1457
+
1458
+ const tree = buildFileTree(files);
1459
+ fileTree.innerHTML = renderTreeNode(tree);
1460
+
1461
+ fileTree.querySelectorAll('.file-tree-folder-header').forEach(header => {
1462
+ header.onclick = (e) => {
1463
+ e.stopPropagation();
1464
+ const folderPath = header.dataset.folder;
1465
+ const folder = header.closest('.file-tree-folder');
1466
+ const contents = folder.querySelector('.file-tree-folder-contents');
1467
+ const chevron = header.querySelector('.file-tree-folder-icon');
1468
+ const folderIcon = header.querySelector('.codicon-folder, .codicon-folder-opened');
1469
+
1470
+ if (expandedFolders.has(folderPath)) {
1471
+ expandedFolders.delete(folderPath);
1472
+ contents.classList.remove('expanded');
1473
+ chevron.classList.remove('expanded');
1474
+ folderIcon.classList.remove('codicon-folder-opened');
1475
+ folderIcon.classList.add('codicon-folder');
1476
+ } else {
1477
+ expandedFolders.add(folderPath);
1478
+ contents.classList.add('expanded');
1479
+ chevron.classList.add('expanded');
1480
+ folderIcon.classList.remove('codicon-folder');
1481
+ folderIcon.classList.add('codicon-folder-opened');
1482
+ }
1483
+ };
1484
+ });
1485
+
1486
+ fileTree.querySelectorAll('.file-tree-file').forEach(item => {
1487
+ item.onclick = () => { closeFileTree(); openFileInEditor(item.dataset.path); };
1488
+ });
1489
+ }
1490
+
1491
+ // =============================================================================
1492
+ // Chat Interactivity
1493
+ // =============================================================================
1494
+ function makeInteractive() {
1495
+ const content = panels.chat.content;
1496
+
1497
+ // Input fields
1498
+ const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1499
+ inputSelectors.forEach(selector => {
1500
+ content.querySelectorAll(selector).forEach(el => {
1501
+ el.style.cursor = 'text';
1502
+ el.addEventListener('focus', () => { isTypingInKiroInput = true; });
1503
+ el.addEventListener('blur', () => { setTimeout(() => { isTypingInKiroInput = false; }, 500); });
1504
+ el.addEventListener('keydown', async (e) => {
1505
+ if (e.key === 'Enter' && !e.shiftKey) {
1506
+ e.preventDefault();
1507
+ const text = el.textContent || el.value || '';
1508
+ if (text.trim()) {
1509
+ await sendToKiro(text.trim());
1510
+ el.textContent = ''; el.innerHTML = '';
1511
+ isTypingInKiroInput = false;
1512
+ }
1513
+ }
1514
+ });
1515
+ });
1516
+ });
1517
+
1518
+ // Send button
1519
+ const attachSendHandler = (btn) => {
1520
+ if (!btn || btn.dataset.sendHandlerAttached) return;
1521
+ btn.dataset.sendHandlerAttached = 'true';
1522
+ btn.removeAttribute('disabled');
1523
+ btn.style.cursor = 'pointer';
1524
+
1525
+ btn.onclick = async (e) => {
1526
+ e.preventDefault(); e.stopPropagation();
1527
+ let inputText = '', inputEl = null;
1528
+
1529
+ // Find the ACTUAL chat input (last visible editor, not old messages)
1530
+ // Look for input containers first, then fall back to last visible editor
1531
+ const inputContainerSelectors = [
1532
+ '[class*="chat-input"]',
1533
+ '[class*="message-input"]',
1534
+ '[class*="composer"]',
1535
+ '[class*="input-area"]',
1536
+ '[class*="InputArea"]',
1537
+ 'form[class*="chat"]'
1538
+ ];
1539
+
1540
+ // Strategy 1: Find editor inside input container
1541
+ for (const containerSel of inputContainerSelectors) {
1542
+ const container = content.querySelector(containerSel);
1543
+ if (container) {
1544
+ const editorInContainer = container.querySelector('.tiptap, .ProseMirror, [data-lexical-editor="true"], [contenteditable="true"], textarea');
1545
+ if (editorInContainer && editorInContainer.offsetParent !== null) {
1546
+ inputEl = editorInContainer;
1547
+ inputText = editorInContainer.textContent || editorInContainer.innerText || editorInContainer.value || '';
1548
+ break;
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ // Strategy 2: Find editor near the submit button itself
1554
+ if (!inputEl) {
1555
+ const btnParent = btn.closest('form') || btn.parentElement?.parentElement?.parentElement;
1556
+ if (btnParent) {
1557
+ const nearbyEditor = btnParent.querySelector('.tiptap, .ProseMirror, [data-lexical-editor="true"], [contenteditable="true"], textarea');
1558
+ if (nearbyEditor && nearbyEditor.offsetParent !== null) {
1559
+ inputEl = nearbyEditor;
1560
+ inputText = nearbyEditor.textContent || nearbyEditor.innerText || nearbyEditor.value || '';
1561
+ }
1562
+ }
1563
+ }
1564
+
1565
+ // Strategy 3: Get the LAST visible editor (input is typically at bottom)
1566
+ if (!inputEl) {
1567
+ for (const sel of ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea']) {
1568
+ const allEditors = [...content.querySelectorAll(sel)].filter(el => el.offsetParent !== null);
1569
+ if (allEditors.length > 0) {
1570
+ inputEl = allEditors.at(-1); // Get LAST one, not first
1571
+ inputText = inputEl.textContent || inputEl.innerText || inputEl.value || '';
1572
+ if (inputText.trim()) break;
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ if (inputText.trim()) {
1578
+ await sendToKiro(inputText.trim());
1579
+ if (inputEl) { inputEl.textContent = ''; inputEl.innerHTML = ''; }
1580
+ showToast('Sent!', 1500, true);
1581
+ } else {
1582
+ showToast('Type a message first', 1500);
1583
+ }
1584
+ return false;
1585
+ };
1586
+ };
1587
+
1588
+ const submitBtn = content.querySelector('button[data-variant="submit"]');
1589
+ if (submitBtn) attachSendHandler(submitBtn);
1590
+
1591
+ content.querySelectorAll('button').forEach(btn => {
1592
+ if (btn.dataset.sendHandlerAttached) return;
1593
+ if (btn.querySelector('.codicon-arrow-up, .codicon-send')) attachSendHandler(btn);
1594
+ });
1595
+
1596
+ // Toggle switches
1597
+ content.querySelectorAll('[role="switch"], input[type="checkbox"][role="switch"]').forEach(toggle => {
1598
+ const wrapper = toggle.closest('.kiro-toggle-switch') || toggle;
1599
+ wrapper.style.cursor = 'pointer';
1600
+ wrapper.onclick = async (e) => {
1601
+ e.preventDefault(); e.stopPropagation();
1602
+ const label = wrapper.querySelector('label');
1603
+ await sendClickToKiro({ tag: 'div', text: label ? label.textContent.trim() : 'toggle', role: 'switch', isToggle: true });
1604
+ // Refresh snapshot after toggle to show updated state
1605
+ // Use longer delay (800ms) to allow server polling to capture the change
1606
+ // Then refresh again at 1500ms to ensure we have the latest state
1607
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 800);
1608
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 1500);
1609
+ return false;
1610
+ };
1611
+ });
1612
+
1613
+ // Notification banner buttons (View all, X) in snackbar and agent-outcome
1614
+ // Note: Revert and Follow buttons are hidden
1615
+ const notificationSelectors = [
1616
+ '.kiro-snackbar button',
1617
+ '.kiro-snackbar-actions button',
1618
+ '.kiro-snackbar-header button',
1619
+ '.agent-outcome-notification button',
1620
+ '.agent-outcome button',
1621
+ '[class*="notification"] button',
1622
+ '[class*="outcome"] button'
1623
+ ];
1624
+ notificationSelectors.forEach(sel => {
1625
+ try {
1626
+ content.querySelectorAll(sel).forEach(btn => {
1627
+ const btnText = (btn.textContent || '').trim().toLowerCase();
1628
+ // Skip Follow and Revert buttons (hidden via CSS/JS)
1629
+ if (btnText.includes('follow') || btnText === 'revert') return;
1630
+
1631
+ btn.style.cursor = 'pointer';
1632
+ btn.onclick = async (e) => {
1633
+ e.preventDefault(); e.stopPropagation();
1634
+
1635
+ // Identify the button - for icon buttons use aria-label or detect close icon
1636
+ let buttonText = (btn.textContent || '').trim().substring(0, 50);
1637
+ const ariaLabel = btn.getAttribute('aria-label') || '';
1638
+ const isIconButton = btn.classList.contains('kiro-icon-button');
1639
+ const hasCloseIcon = btn.querySelector('.codicon-close, .codicon-x, [class*="close"]');
1640
+
1641
+ // For X/close icon buttons with no text
1642
+ if (!buttonText && (isIconButton || hasCloseIcon)) {
1643
+ buttonText = ariaLabel || 'dismiss';
1644
+ }
1645
+
1646
+ const clickInfo = {
1647
+ tag: 'button',
1648
+ text: buttonText,
1649
+ ariaLabel: ariaLabel,
1650
+ role: btn.getAttribute('role') || 'button',
1651
+ className: btn.className || '',
1652
+ isNotificationButton: true,
1653
+ isIconButton: isIconButton || !buttonText
1654
+ };
1655
+ console.log('[Click] Notification button:', clickInfo.text || clickInfo.ariaLabel || 'icon-button');
1656
+ await sendClickToKiro(clickInfo);
1657
+ // Refresh after clicking notification buttons
1658
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
1659
+ return false;
1660
+ };
1661
+ });
1662
+ } catch(e) {}
1625
1663
  });
1626
1664
 
1627
1665
  // Tabs
1628
1666
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1629
1667
  const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
1630
-
1631
1668
  if (closeBtn) {
1632
1669
  closeBtn.style.cursor = 'pointer';
1633
1670
  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
1671
+ e.preventDefault(); e.stopPropagation();
1638
1672
  const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1639
1673
  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
- });
1674
+ await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true, parentTabLabel: tabLabel });
1651
1675
  return false;
1652
1676
  };
1653
1677
  }
@@ -1655,131 +1679,305 @@
1655
1679
  tab.style.cursor = 'pointer';
1656
1680
  tab.onclick = async (e) => {
1657
1681
  if (e.target.closest('[aria-label="close"], [class*="close"], button')) return;
1658
- e.preventDefault();
1659
- e.stopPropagation();
1660
-
1682
+ e.preventDefault(); e.stopPropagation();
1661
1683
  const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1662
1684
  const labelText = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1663
-
1664
1685
  await sendClickToKiro({ tag: 'div', text: labelText, role: 'tab', isTab: true, tabLabel: labelText });
1665
1686
  return false;
1666
1687
  };
1667
1688
  });
1668
1689
 
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]+$/;
1690
+ // Model selector buttons and dropdown options
1691
+ const modelNames = ['claude', 'sonnet', 'opus', 'haiku', 'auto', 'gpt', 'model', 'gemini', 'llama'];
1692
+ const modelSelectorSelectors = [
1693
+ '[class*="model-selector"]', '[class*="modelSelector"]', '[class*="ModelSelector"]',
1694
+ '[class*="model-dropdown"]', '[class*="modelDropdown"]', '[class*="ModelDropdown"]',
1695
+ '[aria-label*="model" i]', '[aria-label*="Model"]',
1696
+ '[class*="select-model"]', '[class*="selectModel"]',
1697
+ '[role="combobox"]'
1698
+ ];
1699
+
1700
+ // Handle model selector buttons
1701
+ modelSelectorSelectors.forEach(sel => {
1702
+ try {
1703
+ content.querySelectorAll(sel).forEach(el => {
1704
+ if (el.onclick) return;
1705
+ el.style.cursor = 'pointer';
1706
+ el.onclick = async (e) => {
1707
+ e.preventDefault(); e.stopPropagation();
1708
+ const clickInfo = {
1709
+ tag: el.tagName?.toLowerCase() || 'button',
1710
+ text: (el.textContent || '').trim().substring(0, 100),
1711
+ ariaLabel: el.getAttribute('aria-label') || '',
1712
+ role: el.getAttribute('role') || 'button',
1713
+ className: el.className || '',
1714
+ isModelSelector: true
1715
+ };
1716
+ await sendClickToKiro(clickInfo);
1717
+ // Refresh snapshot after clicking to show dropdown
1718
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1719
+ return false;
1720
+ };
1721
+ });
1722
+ } catch(e) {}
1723
+ });
1724
+
1725
+ // Handle dropdown menu items (model options) - including Kiro-specific classes
1726
+ const dropdownItemSelectors = [
1727
+ '.kiro-dropdown-item',
1728
+ '.kiro-dropdown-menu > div',
1729
+ '[role="option"]', '[role="menuitem"]', '[role="listitem"]',
1730
+ '[class*="dropdown-item"]', '[class*="dropdownItem"]', '[class*="DropdownItem"]',
1731
+ '[class*="menu-item"]', '[class*="menuItem"]', '[class*="MenuItem"]'
1732
+ ];
1672
1733
 
1673
- // Look for elements that might contain file paths
1734
+ dropdownItemSelectors.forEach(sel => {
1735
+ try {
1736
+ content.querySelectorAll(sel).forEach(el => {
1737
+ if (el.onclick) return;
1738
+ // Skip if it's inside a tab
1739
+ if (el.closest('[role="tab"]')) return;
1740
+
1741
+ const elText = (el.textContent || '').toLowerCase();
1742
+ const isModelOption = modelNames.some(m => elText.includes(m));
1743
+
1744
+ // Only attach handler if it looks like a model option
1745
+ if (!isModelOption) return;
1746
+
1747
+ el.style.cursor = 'pointer';
1748
+ el.onclick = async (e) => {
1749
+ e.preventDefault(); e.stopPropagation();
1750
+
1751
+ // Extract just the model name for cleaner matching
1752
+ let modelText = (el.textContent || '').trim();
1753
+ // Try to get just the first line (model name)
1754
+ const firstLine = modelText.split('\n')[0].trim();
1755
+ if (firstLine.length > 5 && firstLine.length < 50) {
1756
+ modelText = firstLine;
1757
+ }
1758
+
1759
+ const clickInfo = {
1760
+ tag: el.tagName?.toLowerCase() || 'div',
1761
+ text: modelText.substring(0, 50),
1762
+ ariaLabel: el.getAttribute('aria-label') || '',
1763
+ role: el.getAttribute('role') || 'option',
1764
+ className: el.className || '',
1765
+ isModelOption: true,
1766
+ isModelSelector: false
1767
+ };
1768
+ await sendClickToKiro(clickInfo);
1769
+ // Refresh snapshot after selecting option
1770
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
1771
+ return false;
1772
+ };
1773
+ });
1774
+ } catch(e) {}
1775
+ });
1776
+
1777
+ // Also detect model-related buttons by text content
1778
+ content.querySelectorAll('button, [role="button"]').forEach(btn => {
1779
+ if (btn.onclick) return;
1780
+ const btnText = (btn.textContent || '').toLowerCase();
1781
+ const isModelRelated = modelNames.some(m => btnText.includes(m));
1782
+
1783
+ if (isModelRelated) {
1784
+ btn.style.cursor = 'pointer';
1785
+ btn.onclick = async (e) => {
1786
+ e.preventDefault(); e.stopPropagation();
1787
+ const clickInfo = {
1788
+ tag: 'button',
1789
+ text: (btn.textContent || '').trim().substring(0, 100),
1790
+ ariaLabel: btn.getAttribute('aria-label') || '',
1791
+ role: btn.getAttribute('role') || 'button',
1792
+ className: btn.className || '',
1793
+ isModelSelector: true
1794
+ };
1795
+ await sendClickToKiro(clickInfo);
1796
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
1797
+ return false;
1798
+ };
1799
+ }
1800
+ });
1801
+
1802
+ // File links
1803
+ 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
1804
  content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
1675
1805
  if (el.onclick) return;
1676
1806
  const text = (el.textContent || '').trim();
1677
1807
  const dataPath = el.getAttribute('data-path') || '';
1678
1808
  const href = el.getAttribute('href') || '';
1679
1809
 
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) {
1810
+ if (fileExtensions.test(text) || fileExtensions.test(dataPath) || fileExtensions.test(href)) {
1689
1811
  const filePath = dataPath || text || href;
1690
1812
  el.style.cursor = 'pointer';
1691
- el.style.textDecoration = 'underline';
1692
1813
  el.style.color = '#7eb6ff';
1693
1814
  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
- };
1815
+ el.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); await openFileInEditor(filePath); return false; };
1701
1816
  }
1702
1817
  });
1703
1818
 
1704
1819
  // Buttons and clickable elements
1705
1820
  const clickableSelectors = ['button', '[role="button"]', '[role="menuitem"]', '[role="option"]', '[role="checkbox"]', 'a[href]', '[tabindex="0"]'];
1706
1821
  const allClickables = new Set();
1707
- clickableSelectors.forEach(sel => {
1708
- try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {}
1709
- });
1822
+ clickableSelectors.forEach(sel => { try { content.querySelectorAll(sel).forEach(el => allClickables.add(el)); } catch(e) {} });
1710
1823
 
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);
1824
+ allClickables.forEach(el => {
1825
+ if (el.matches && el.matches('[contenteditable], textarea, input:not([type="checkbox"])')) return;
1826
+ if (el.closest('[role="tab"]') || el.closest('.kiro-toggle-switch') || el.onclick) return;
1827
+
1828
+ el.style.cursor = 'pointer';
1829
+ el.onclick = async (e) => {
1830
+ e.preventDefault(); e.stopPropagation();
1831
+ const clickInfo = {
1832
+ tag: el.tagName?.toLowerCase() || '',
1833
+ text: (el.textContent || '').trim().substring(0, 100),
1834
+ ariaLabel: el.getAttribute('aria-label') || '',
1835
+ role: el.getAttribute('role') || '',
1836
+ className: el.className || ''
1837
+ };
1838
+ await sendClickToKiro(clickInfo);
1839
+ return false;
1840
+ };
1714
1841
  });
1715
1842
 
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)
1843
+ // Chat history items - detect and make clickable
1844
+ // Kiro history items typically have: icon, title text, and date
1845
+ // We need to find these items even if they don't have semantic roles
1846
+
1847
+ // First, find all potential history item containers
1848
+ const potentialHistoryItems = [];
1849
+
1850
+ // Look for elements that contain date patterns (history items have dates)
1851
+ const datePattern = /\d{1,2}\/\d{1,2}\/\d{4}|\d{1,2}:\d{2}:\d{2}\s*(AM|PM)?/i;
1852
+
1853
+ content.querySelectorAll('div, li, article, section').forEach(el => {
1854
+ // Skip if already has handler
1855
+ if (el.onclick) return;
1856
+ // Skip inputs
1857
+ if (el.matches('[contenteditable], textarea, input')) return;
1858
+ // Skip very large containers
1859
+ if (el.children.length > 15) return;
1860
+ // Skip if inside tab or toggle
1861
+ if (el.closest('[role="tab"]') || el.closest('.kiro-toggle-switch')) return;
1862
+
1720
1863
  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);
1864
+ const hasDate = datePattern.test(text);
1865
+ const hasIcon = el.querySelector('svg, [class*="icon"], [class*="Icon"], .codicon');
1866
+ const textLength = text.trim().length;
1867
+
1868
+ // History items typically have: date + some text (20-500 chars) + possibly an icon
1869
+ if (hasDate && textLength > 20 && textLength < 500) {
1870
+ // Check if this looks like a list item (has siblings with similar structure)
1871
+ const parent = el.parentElement;
1872
+ const siblings = parent ? [...parent.children].filter(c => c.tagName === el.tagName) : [];
1873
+ const isInList = siblings.length > 1;
1874
+
1875
+ // Also check for cursor pointer style or clickable class
1876
+ const computedStyle = window.getComputedStyle(el);
1877
+ const hasPointer = computedStyle.cursor === 'pointer';
1878
+ const hasClickableClass = el.className && (
1879
+ el.className.includes('item') ||
1880
+ el.className.includes('Item') ||
1881
+ el.className.includes('row') ||
1882
+ el.className.includes('Row') ||
1883
+ el.className.includes('entry') ||
1884
+ el.className.includes('Entry')
1885
+ );
1886
+
1887
+ if (isInList || hasPointer || hasClickableClass || hasIcon) {
1888
+ potentialHistoryItems.push(el);
1889
+ }
1725
1890
  }
1726
1891
  });
1727
1892
 
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
- }
1893
+ // Also add standard selectors
1894
+ const historySelectors = [
1895
+ '[role="listitem"]',
1896
+ '[role="option"]',
1897
+ '[class*="history-item"]',
1898
+ '[class*="historyItem"]',
1899
+ '[class*="session-item"]',
1900
+ '[class*="sessionItem"]',
1901
+ '[class*="chat-item"]',
1902
+ '[class*="chatItem"]'
1903
+ ];
1904
+
1905
+ historySelectors.forEach(selector => {
1906
+ try {
1907
+ content.querySelectorAll(selector).forEach(el => {
1908
+ if (!el.onclick && !potentialHistoryItems.includes(el)) {
1909
+ potentialHistoryItems.push(el);
1910
+ }
1911
+ });
1912
+ } catch(e) {}
1734
1913
  });
1735
1914
 
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
-
1915
+ // Attach click handlers to all potential history items
1916
+ potentialHistoryItems.forEach(el => {
1742
1917
  el.style.cursor = 'pointer';
1743
1918
  el.onclick = async (e) => {
1744
1919
  e.preventDefault();
1745
1920
  e.stopPropagation();
1746
- const clickInfo = getElementClickInfo(el);
1747
1921
 
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
- }
1922
+ // Extract the title (first meaningful text, not the date)
1923
+ let itemText = '';
1924
+ const textNodes = [];
1925
+ el.querySelectorAll('span, p, div').forEach(child => {
1926
+ const t = child.textContent?.trim();
1927
+ if (t && t.length > 5 && !datePattern.test(t)) {
1928
+ textNodes.push(t);
1929
+ }
1930
+ });
1931
+ itemText = textNodes[0] || el.textContent?.trim().split('\n')[0] || '';
1932
+ itemText = itemText.substring(0, 100);
1933
+
1934
+ const clickInfo = {
1935
+ tag: el.tagName?.toLowerCase() || 'div',
1936
+ text: itemText,
1937
+ ariaLabel: el.getAttribute('aria-label') || '',
1938
+ role: el.getAttribute('role') || 'listitem',
1939
+ className: el.className || '',
1940
+ isHistoryItem: true
1941
+ };
1754
1942
 
1755
1943
  await sendClickToKiro(clickInfo);
1944
+
1945
+ // Refresh snapshot after clicking history item
1946
+ setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
1756
1947
  return false;
1757
1948
  };
1758
1949
  });
1759
1950
  }
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
- };
1951
+
1952
+ // =============================================================================
1953
+ // Kiro Communication
1954
+ // =============================================================================
1955
+
1956
+ // Rate limiting for message sending
1957
+ let lastMessageTime = 0;
1958
+ const MESSAGE_RATE_LIMIT_MS = 1000; // Minimum 1 second between messages
1959
+ let pendingMessage = false;
1960
+
1961
+ async function sendToKiro(message) {
1962
+ if (!message || !selectedCascadeId) return;
1772
1963
 
1773
- if (!info.ariaLabel) {
1774
- const parent = el.closest('[aria-label]');
1775
- if (parent) info.ariaLabel = parent.getAttribute('aria-label') || '';
1964
+ // Rate limiting check
1965
+ const now = Date.now();
1966
+ if (now - lastMessageTime < MESSAGE_RATE_LIMIT_MS) {
1967
+ if (!pendingMessage) {
1968
+ showToast('Please wait before sending another message', 1500);
1969
+ }
1970
+ return;
1776
1971
  }
1777
1972
 
1778
- return info;
1779
- }
1780
-
1781
- async function sendToKiro(message) {
1782
- if (!message || !selectedCascadeId) return;
1973
+ if (pendingMessage) {
1974
+ showToast('Message already being sent', 1500);
1975
+ return;
1976
+ }
1977
+
1978
+ pendingMessage = true;
1979
+ lastMessageTime = now;
1980
+
1783
1981
  try {
1784
1982
  const r = await fetch(`/send/${selectedCascadeId}`, {
1785
1983
  method: 'POST',
@@ -1791,65 +1989,64 @@
1791
1989
  else showToast(result.error || 'Failed to send');
1792
1990
  } catch (e) {
1793
1991
  showToast('Failed to send');
1992
+ } finally {
1993
+ pendingMessage = false;
1794
1994
  }
1795
1995
  }
1796
-
1797
- async function sendClickToKiro(clickInfo) {
1996
+
1997
+ async function sendClickToKiro(clickInfo, retryCount = 0) {
1798
1998
  if (!selectedCascadeId) return;
1799
1999
 
1800
- const isNavigation =
1801
- clickInfo.ariaLabel?.toLowerCase().includes('back') ||
2000
+ const isNavigation = clickInfo.ariaLabel?.toLowerCase().includes('back') ||
1802
2001
  clickInfo.ariaLabel?.toLowerCase().includes('close') ||
1803
2002
  clickInfo.text?.toLowerCase().includes('back') ||
1804
2003
  clickInfo.role === 'tab';
1805
2004
 
1806
- const isModelSelection = clickInfo.isModelSelection ||
1807
- clickInfo.role === 'option' ||
1808
- clickInfo.role === 'menuitem' ||
1809
- clickInfo.className?.includes('model');
1810
-
1811
2005
  if (isNavigation) navigationPending = true;
1812
2006
 
1813
2007
  try {
1814
- await fetch(`/click/${selectedCascadeId}`, {
2008
+ const response = await fetch(`/click/${selectedCascadeId}`, {
1815
2009
  method: 'POST',
1816
2010
  headers: { 'Content-Type': 'application/json' },
1817
2011
  body: JSON.stringify(clickInfo)
1818
2012
  });
1819
2013
 
1820
- if (isNavigation || isModelSelection) {
1821
- // Refresh snapshot after navigation or model selection
1822
- setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
2014
+ const result = await response.json();
2015
+
2016
+ // Handle retry for model option clicks (dropdown was opened, need to click option)
2017
+ if (result.needsRetry && clickInfo.isModelOption && retryCount < 2) {
2018
+ console.log('[Click] Dropdown opened, retrying option click in 350ms...');
2019
+ await new Promise(resolve => setTimeout(resolve, 350));
2020
+ return sendClickToKiro(clickInfo, retryCount + 1);
1823
2021
  }
2022
+
2023
+ if (isNavigation) setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
2024
+
2025
+ return result;
1824
2026
  } catch (e) {
1825
2027
  navigationPending = false;
2028
+ return { success: false, error: e.message };
1826
2029
  }
1827
2030
  }
1828
-
1829
- // Open file in Editor tab
2031
+
1830
2032
  async function openFileInEditor(filePath) {
1831
2033
  if (!selectedCascadeId || !filePath) return;
1832
2034
 
1833
2035
  showToast(`Opening ${filePath}...`, 1500);
1834
2036
 
1835
- // Switch to Editor tab immediately
2037
+ // Switch to Editor tab
1836
2038
  const editorTab = document.querySelector('[data-panel="editor"]');
1837
2039
  if (editorTab) {
1838
2040
  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
- });
2041
+ editorTab.classList.add('active');
2042
+ Object.values(panels).forEach(p => { if (p.panel) p.panel.classList.remove('active'); });
1844
2043
  panels.editor.panel.classList.add('active');
1845
2044
  activePanel = 'editor';
1846
-
1847
- // Show loading state
1848
2045
  showLoading('editor', 'Loading file...');
1849
2046
  }
1850
2047
 
1851
2048
  try {
1852
- // First, try to read the file directly from filesystem (bypasses Monaco limitation)
2049
+ // Try direct file read first
1853
2050
  const readResult = await fetch(`/readFile/${selectedCascadeId}`, {
1854
2051
  method: 'POST',
1855
2052
  headers: { 'Content-Type': 'application/json' },
@@ -1861,472 +2058,61 @@
1861
2058
  if (data.content) {
1862
2059
  renderEditorSnapshot(data);
1863
2060
  showToast(`Opened ${data.fileName || filePath}`, 1500, true);
1864
- console.log('[Editor] Loaded file directly:', data.fileName, data.lineCount, 'lines');
1865
2061
  return;
1866
2062
  }
1867
- } else {
1868
- const errorData = await readResult.json().catch(() => ({}));
1869
- console.log('[Editor] Direct file read failed:', errorData.error || readResult.status);
1870
2063
  }
1871
2064
 
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}`, {
2065
+ // Fallback: Click file link in Kiro
2066
+ await fetch(`/click/${selectedCascadeId}`, {
1875
2067
  method: 'POST',
1876
2068
  headers: { 'Content-Type': 'application/json' },
1877
- body: JSON.stringify({
1878
- tag: 'a',
1879
- text: filePath,
1880
- isFileLink: true,
1881
- filePath: filePath
1882
- })
2069
+ body: JSON.stringify({ tag: 'a', text: filePath, isFileLink: true, filePath })
1883
2070
  });
1884
2071
 
1885
- const result = await clickResult.json();
1886
- console.log('Click result:', result);
1887
-
1888
- // Wait for the file to open in Kiro
1889
2072
  await new Promise(resolve => setTimeout(resolve, 800));
1890
2073
 
1891
- // Fetch the editor content from Monaco
1892
2074
  const fetchEditor = async () => {
1893
2075
  try {
1894
2076
  const r = await fetch(`/editor/${selectedCascadeId}`);
1895
2077
  if (r.ok) {
1896
2078
  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
- }
2079
+ if (data.content || data.html) { renderEditorSnapshot(data); showToast(`Opened ${data.fileName || filePath}`, 1500, true); return true; }
1902
2080
  }
1903
2081
  } catch (e) {}
1904
2082
  return false;
1905
2083
  };
1906
2084
 
1907
- // Try fetching a few times with delays
1908
2085
  if (!await fetchEditor()) {
1909
2086
  await new Promise(resolve => setTimeout(resolve, 500));
1910
2087
  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
- }
2088
+ showEmptyState('editor', 'codicon-file-code', `Could not load ${filePath}.`);
1915
2089
  }
1916
2090
  }
1917
-
1918
2091
  } catch (e) {
1919
2092
  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';
2093
+ showEmptyState('editor', 'codicon-file-code', 'Failed to load file.');
2271
2094
  }
2272
2095
  }
2273
2096
 
2274
- function goToNextMatch() {
2275
- if (searchMatches.length === 0) return;
2276
- currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
2277
- scrollToMatch(currentMatchIndex);
2278
- updateSearchCount();
2279
- }
2097
+ // =============================================================================
2098
+ // Event Listeners Setup
2099
+ // =============================================================================
2100
+ $('editorExplorerBtn').onclick = (e) => { e.stopPropagation(); toggleFileTree(); };
2101
+ fileTreeDropdown.onclick = (e) => { e.stopPropagation(); };
2280
2102
 
2281
- function goToPrevMatch() {
2282
- if (searchMatches.length === 0) return;
2283
- currentMatchIndex = (currentMatchIndex - 1 + searchMatches.length) % searchMatches.length;
2284
- scrollToMatch(currentMatchIndex);
2285
- updateSearchCount();
2286
- }
2103
+ // Close dropdown when clicking outside
2104
+ document.addEventListener('click', (e) => {
2105
+ if (fileTreeOpen && !e.target.closest('.editor-explorer-wrapper')) closeFileTree();
2106
+ });
2287
2107
 
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
- }
2108
+ document.addEventListener('keydown', (e) => {
2109
+ if (e.key === 'Escape' && fileTreeOpen) closeFileTree();
2110
+ });
2320
2111
 
2321
- setupEditorSearch();
2322
-
2323
- // Visibility change handler
2324
2112
  document.addEventListener('visibilitychange', () => {
2325
- if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
2326
- connect();
2327
- }
2113
+ if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) connect();
2328
2114
  });
2329
-
2115
+
2330
2116
  // Initialize
2331
2117
  connect();
2332
2118
  </script>