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.
- package/README.md +16 -24
- package/package.json +1 -1
- package/src/public/index.html +1414 -1628
- package/src/routes/api.js +539 -0
- package/src/server.js +287 -2593
- package/src/services/cdp.js +210 -0
- package/src/services/click.js +533 -0
- package/src/services/message.js +214 -0
- package/src/services/snapshot.js +370 -0
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +34 -0
- package/src/utils/network.js +64 -0
- package/src/utils/security.js +160 -0
package/src/public/index.html
CHANGED
|
@@ -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 {
|
|
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
|
-
/*
|
|
17
|
-
|
|
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 {
|
|
26
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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;
|
|
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: #
|
|
50
|
-
.editor-explorer-btn:hover { background:
|
|
51
|
-
.editor-explorer-text { font-size: 11px; font-weight: 600; color: #
|
|
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;
|
|
66
|
-
.editor-line-code { flex: 1; padding-right: 12px; white-space: pre; overflow-x: auto; tab-size: 2;
|
|
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
|
-
/*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.
|
|
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:
|
|
79
|
-
.file-tree-folder-header:hover { background: rgba(255,255,255,0.
|
|
80
|
-
.file-tree-folder-icon { color: #
|
|
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:
|
|
86
|
-
.file-tree-file:hover { background: rgba(255,255,255,0.
|
|
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 {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
140
|
-
|
|
141
|
-
|
|
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>
|
|
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
|
|
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
|
-
<
|
|
276
|
-
<
|
|
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
|
|
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"
|
|
285
|
-
<span class="editor-search-nav codicon codicon-arrow-down" id="editorSearchNext"
|
|
286
|
-
<span class="editor-search-close codicon codicon-close" id="editorSearchClose"
|
|
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
|
|
312
|
-
let
|
|
313
|
-
let
|
|
314
|
-
let
|
|
315
|
-
let
|
|
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
|
-
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
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(() =>
|
|
344
|
+
setTimeout(() => toast.classList.remove('visible'), duration);
|
|
370
345
|
}
|
|
371
|
-
|
|
346
|
+
|
|
347
|
+
function escapeHtml(text) {
|
|
348
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
392
|
-
if (selectedCascadeId) {
|
|
393
|
-
fetchPanelContent(panelName, selectedCascadeId);
|
|
394
|
-
}
|
|
386
|
+
if (selectedCascadeId) fetchPanelContent(activePanel, selectedCascadeId);
|
|
395
387
|
});
|
|
396
|
-
|
|
397
|
-
//
|
|
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
|
-
|
|
404
|
-
|
|
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 = [];
|
|
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
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
605
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
614
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
panels.editor.content.scrollTop = scrollTop;
|
|
679
|
-
}
|
|
662
|
+
flushGroup();
|
|
663
|
+
return html;
|
|
680
664
|
}
|
|
681
665
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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(/(<!--[\s\S]*?-->)/g, '<span class="tok-cmt">$1</span>');
|
|
707
|
-
// Strings in attributes
|
|
708
|
-
result = result.replace(/=("[^&]*?")/g, '=<span class="tok-str">$1</span>');
|
|
709
|
-
result = result.replace(/=('[^&]*?')/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(/("[^&]*?")/g, '<span class="tok-str">$1</span>');
|
|
720
|
-
result = result.replace(/('[^&]*?')/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: " or ')
|
|
744
|
-
if (escaped.slice(i, i + 6) === '"') {
|
|
745
|
-
const endIdx = escaped.indexOf('"', 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) === ''') {
|
|
754
|
-
const endIdx = escaped.indexOf(''', 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, '&')
|
|
819
|
-
.replace(/</g, '<')
|
|
820
|
-
.replace(/>/g, '>')
|
|
821
|
-
.replace(/"/g, '"')
|
|
822
|
-
.replace(/'/g, ''');
|
|
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
|
-
/*
|
|
961
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
[
|
|
1013
|
-
|
|
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
|
-
/*
|
|
1023
|
-
[
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
|
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)
|
|
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
|
-
/*
|
|
1223
|
-
|
|
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
|
-
|
|
1232
|
-
[class*="message"] a,
|
|
1233
|
-
[class*="
|
|
1234
|
-
[class*="
|
|
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
|
|
1260
|
-
[class*="file-badge"],
|
|
1261
|
-
[class*="
|
|
1262
|
-
[class*="
|
|
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
|
|
1282
|
-
ul li, ol li {
|
|
1283
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1027
|
+
|
|
1028
|
+
function renderChatSnapshot(snapshot, isUpdate = false) {
|
|
1029
|
+
if (isRendering) return;
|
|
1030
|
+
isRendering = true;
|
|
1454
1031
|
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1502
|
-
|
|
1047
|
+
removePlaceholderText();
|
|
1048
|
+
fixContextWindowCircles();
|
|
1049
|
+
hideRevertButton();
|
|
1503
1050
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
} else if (
|
|
1539
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1199
|
+
// Strings
|
|
1200
|
+
if (escaped.slice(i, i + 6) === '"') {
|
|
1201
|
+
const endIdx = escaped.indexOf('"', 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) === ''') {
|
|
1205
|
+
const endIdx = escaped.indexOf(''', 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
|
-
//
|
|
1609
|
-
if (
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
//
|
|
1670
|
-
const
|
|
1671
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
-
//
|
|
1717
|
-
//
|
|
1718
|
-
|
|
1719
|
-
|
|
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 =
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
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
|
-
//
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
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
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
1873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
}
|
|
2097
|
+
// =============================================================================
|
|
2098
|
+
// Event Listeners Setup
|
|
2099
|
+
// =============================================================================
|
|
2100
|
+
$('editorExplorerBtn').onclick = (e) => { e.stopPropagation(); toggleFileTree(); };
|
|
2101
|
+
fileTreeDropdown.onclick = (e) => { e.stopPropagation(); };
|
|
2280
2102
|
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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>
|