@open-gitagent/voice 1.0.0

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/dist/ui.html ADDED
@@ -0,0 +1,3859 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gitagent: {{AGENT_NAME}}</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap');
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body {
11
+ background: #0d1117; color: #e6edf3;
12
+ font-family: 'Inter', 'IBM Plex Mono', monospace;
13
+ display: flex; flex-direction: column;
14
+ height: 100vh; overflow: hidden;
15
+ }
16
+ .header {
17
+ display: flex; align-items: center; justify-content: space-between;
18
+ padding: 0 20px; height: 40px; min-height: 40px;
19
+ background: #0e1117; border-bottom: 1px solid #21262d;
20
+ }
21
+ .header-left { display: flex; align-items: center; gap: 10px; }
22
+ .header-logo {
23
+ flex-shrink: 0; width: 22px; height: 22px;
24
+ display: grid; grid-template-columns: repeat(10,2.2px); grid-template-rows: repeat(10,2.2px);
25
+ image-rendering: pixelated;
26
+ filter: drop-shadow(0 0 4px rgba(255,79,99,0.9)) drop-shadow(0 0 12px rgba(255,79,99,0.5));
27
+ }
28
+ .header-logo .px { width: 2.2px; height: 2.2px; }
29
+ .header-logo .px-o { background: #12070b; }
30
+ .header-logo .px-f { background: #ff4f63; }
31
+ .header-logo .px-e { background: transparent; }
32
+ .header-left h1 { font-size: 15px; font-weight: 600; font-family: 'IBM Plex Mono', monospace; }
33
+ .header-left h1 span { color: #58a6ff; }
34
+ .header-right { display: flex; align-items: center; gap: 16px; }
35
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #484f58; display: inline-block; }
36
+ .status-dot.connected { background: #3fb950; }
37
+ .status-dot.error { background: #f85149; }
38
+ .status-text { font-size: 11px; color: #8b949e; margin-left: 6px; }
39
+ .view-tabs { display: flex; gap: 2px; }
40
+ .view-tab {
41
+ font-family: inherit; font-size: 11px; padding: 4px 14px;
42
+ background: transparent; border: 1px solid transparent; border-radius: 4px;
43
+ color: #8b949e; cursor: pointer; transition: all 0.15s;
44
+ }
45
+ .view-tab:hover { color: #b1bac4; background: #161b22; }
46
+ .view-tab.active { border-color: #58a6ff; color: #58a6ff; background: rgba(88,166,255,0.08); }
47
+ .main { display: flex; flex: 1; overflow: hidden; min-height: 0; }
48
+
49
+ /* CHAT SIDEBAR */
50
+ .chat-sidebar {
51
+ width: 220px; min-width: 220px; background: #0a0d12; border-right: 1px solid #21262d;
52
+ display: flex; flex-direction: column; overflow: hidden;
53
+ transition: width 0.25s cubic-bezier(0.4,0,0.2,1), min-width 0.25s cubic-bezier(0.4,0,0.2,1), opacity 0.2s ease;
54
+ }
55
+ .chat-sidebar.collapsed {
56
+ width: 0; min-width: 0; opacity: 0; pointer-events: none; border-right: none;
57
+ }
58
+ .chat-sidebar-collapse {
59
+ background: none; border: none; color: #484f58; cursor: pointer; padding: 2px;
60
+ display: flex; align-items: center; transition: color 0.15s;
61
+ }
62
+ .chat-sidebar-collapse:hover { color: #b1bac4; }
63
+ .chat-sidebar-collapse svg { width: 16px; height: 16px; }
64
+ .chat-sidebar-edge-tab {
65
+ position: absolute; left: 0; top: 50%; transform: translateY(-50%);
66
+ width: 16px; height: 48px; background: #161b22; border: 1px solid #21262d;
67
+ border-left: none; border-radius: 0 6px 6px 0; cursor: pointer;
68
+ display: none; align-items: center; justify-content: center; color: #484f58;
69
+ z-index: 10; transition: color 0.15s, background 0.15s;
70
+ }
71
+ .chat-sidebar-edge-tab:hover { color: #b1bac4; background: #1c2129; }
72
+ .chat-sidebar-edge-tab svg { width: 12px; height: 12px; }
73
+ .chat-sidebar-edge-tab.visible { display: flex; }
74
+ .chat-sidebar-toggle {
75
+ background: none; border: 1px solid #21262d; border-radius: 4px; color: #8b949e;
76
+ cursor: pointer; font-size: 11px; font-family: 'IBM Plex Mono', monospace;
77
+ padding: 3px 8px; display: flex; align-items: center; gap: 5px; transition: all 0.15s;
78
+ }
79
+ .chat-sidebar-toggle:hover { border-color: #30363d; color: #b1bac4; }
80
+ .chat-sidebar-toggle svg { width: 14px; height: 14px; }
81
+ .chat-sidebar-header {
82
+ padding: 10px 12px; display: flex; align-items: center; justify-content: space-between;
83
+ border-bottom: 1px solid #21262d;
84
+ }
85
+ .chat-sidebar-header span {
86
+ font-size: 11px; color: #8b949e; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
87
+ }
88
+ .new-chat-btn {
89
+ background: #238636; border: none; border-radius: 4px; color: #fff;
90
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 600;
91
+ padding: 4px 10px; cursor: pointer; transition: background 0.15s;
92
+ }
93
+ .new-chat-btn:hover { background: #2ea043; }
94
+ .chat-list { flex: 1; overflow-y: auto; padding: 4px 0; }
95
+ .chat-item {
96
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 12px;
97
+ font-family: 'IBM Plex Mono', monospace; color: #8b949e; cursor: pointer;
98
+ transition: background 0.1s; border-left: 2px solid transparent;
99
+ }
100
+ .chat-item:hover { background: #161b22; color: #b1bac4; }
101
+ .chat-item.active { background: rgba(88,166,255,0.08); color: #e6edf3; border-left-color: #58a6ff; }
102
+ .chat-item .chat-icon { width: 14px; height: 14px; flex-shrink: 0; opacity: 0.5; }
103
+ .chat-item .chat-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
104
+ .chat-item .chat-time { font-size: 10px; color: #484f58; flex-shrink: 0; }
105
+ .chat-item .chat-delete {
106
+ width: 18px; height: 18px; border: none; background: transparent; color: #484f58;
107
+ cursor: pointer; border-radius: 3px; display: flex; align-items: center; justify-content: center;
108
+ opacity: 0; transition: opacity 0.1s; font-size: 13px;
109
+ }
110
+ .chat-item:hover .chat-delete { opacity: 1; }
111
+ .chat-item .chat-delete:hover { background: #21262d; color: #f85149; }
112
+ .branch-indicator {
113
+ padding: 8px 12px; border-top: 1px solid #21262d; font-size: 10px;
114
+ font-family: 'IBM Plex Mono', monospace; color: #484f58; display: flex; align-items: center; gap: 6px;
115
+ }
116
+ .branch-indicator svg { width: 12px; height: 12px; }
117
+ .branch-indicator .branch-name { color: #8b949e; }
118
+
119
+ /* CHAT VIEW */
120
+ .chat-view { display: flex; flex: 1; overflow: hidden; min-height: 0; }
121
+ .chat-view.hidden { display: none; }
122
+ .panel-cam {
123
+ width: 280px; min-width: 280px; border-right: 1px solid #21262d;
124
+ display: flex; flex-direction: column; padding: 16px; gap: 14px; background: #0e1117;
125
+ }
126
+ .camera-container {
127
+ position: relative; background: #161b22; border: 1px solid #21262d;
128
+ border-radius: 6px; overflow: hidden; aspect-ratio: 4/3;
129
+ }
130
+ .camera-container video { width: 100%; height: 100%; object-fit: cover; display: block; }
131
+ .camera-container .camera-off {
132
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #484f58; font-size: 12px;
133
+ }
134
+ .camera-container canvas { display: none; }
135
+ .controls { display: flex; gap: 8px; flex-wrap: wrap; }
136
+ .ctrl-btn {
137
+ flex: 1; padding: 10px; border-radius: 6px; background: #161b22; border: 1px solid #21262d;
138
+ color: #8b949e; font-family: 'IBM Plex Mono', monospace; font-size: 11px;
139
+ cursor: pointer; transition: all 0.15s;
140
+ display: flex; align-items: center; justify-content: center; gap: 6px;
141
+ }
142
+ .ctrl-btn:hover { border-color: #30363d; color: #b1bac4; }
143
+ .ctrl-btn.active { border-color: #58a6ff; color: #58a6ff; background: rgba(88,166,255,0.08); }
144
+ .ctrl-btn svg { width: 14px; height: 14px; }
145
+ /* Agent Vitals */
146
+ .agent-vitals {
147
+ flex: 1; display: flex; flex-direction: column; gap: 0;
148
+ background: #0a0e14; border: 1px solid #21262d; border-radius: 8px; overflow: hidden;
149
+ font-family: 'IBM Plex Mono', monospace; position: relative;
150
+ }
151
+ .vitals-header {
152
+ display: flex; align-items: center; justify-content: space-between;
153
+ padding: 8px 12px; border-bottom: 1px solid #161b22;
154
+ }
155
+ .vitals-title { font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; color: #3fb950; font-weight: 600; }
156
+ .vitals-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #3fb950; animation: vitalPulse 1.5s ease-in-out infinite; }
157
+ @keyframes vitalPulse { 0%,100% { opacity: 1; box-shadow: 0 0 4px #3fb950; } 50% { opacity: 0.4; box-shadow: none; } }
158
+ .vitals-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #161b22; flex: 1; }
159
+ .vital-cell {
160
+ background: #0a0e14; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px;
161
+ position: relative; overflow: hidden;
162
+ }
163
+ .vital-label { font-size: 8px; text-transform: uppercase; letter-spacing: 1px; color: #484f58; }
164
+ .vital-value { font-size: 16px; font-weight: 700; line-height: 1; }
165
+ .vital-unit { font-size: 8px; color: #484f58; font-weight: 400; }
166
+ .vital-bar { height: 2px; border-radius: 1px; background: #161b22; margin-top: 4px; }
167
+ .vital-bar-fill { height: 100%; border-radius: 1px; transition: width 0.8s ease; }
168
+ .vital-cell.cpu .vital-value { color: #58a6ff; }
169
+ .vital-cell.cpu .vital-bar-fill { background: #58a6ff; }
170
+ .vital-cell.mem .vital-value { color: #f0883e; }
171
+ .vital-cell.mem .vital-bar-fill { background: #f0883e; }
172
+ .vital-cell.tokens .vital-value { color: #d2a8ff; }
173
+ .vital-cell.tokens .vital-bar-fill { background: #d2a8ff; }
174
+ .vital-cell.uptime .vital-value { color: #3fb950; }
175
+ .vitals-wave {
176
+ height: 32px; padding: 0 10px; border-top: 1px solid #161b22;
177
+ display: flex; align-items: center; gap: 8px;
178
+ }
179
+ .vitals-wave canvas { width: 100%; height: 24px; }
180
+ .vitals-wave-label { font-size: 8px; color: #f85149; text-transform: uppercase; letter-spacing: 1px; flex-shrink: 0; }
181
+ .vital-cell.wide { grid-column: 1 / -1; }
182
+
183
+ .panel-right { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; background: #0d1117; overflow: hidden; position: relative; }
184
+ .conversation {
185
+ flex: 1; overflow-y: auto; padding: 16px 20px; font-size: 12px; line-height: 1.8;
186
+ font-family: 'IBM Plex Mono', monospace;
187
+ }
188
+ .conversation:empty::before { content: "Conversation will appear here..."; color: #484f58; }
189
+ .conv-msg { margin-bottom: 3px; animation: fade-in 0.2s ease-out; }
190
+ .conv-msg.user { color: #b1bac4; }
191
+ .conv-msg.user .label { color: #58a6ff; font-weight: 600; }
192
+ .conv-msg.telegram { border-left: 2px solid #2AABEE; padding-left: 8px; }
193
+ .conv-msg.telegram .label.tg { color: #2AABEE; }
194
+ .conv-msg.assistant { color: #e6edf3; }
195
+ .conv-msg.assistant .label { color: #3fb950; font-weight: 600; }
196
+ .conv-msg.tool { color: #d29922; }
197
+ .conv-msg.agent-working { display: flex; align-items: flex-start; gap: 10px; min-height: 32px; }
198
+ .agent-working-spinner {
199
+ width: 18px; height: 18px; flex-shrink: 0;
200
+ border: 2px solid rgba(255,79,99,0.2); border-top-color: #ff4f63;
201
+ border-radius: 50%; animation: agentSpin 0.8s linear infinite;
202
+ }
203
+ @keyframes agentSpin { to { transform: rotate(360deg); } }
204
+ .agent-working-text { font-size: 11px; word-break: break-word; }
205
+ .agent-working-verb { color: #8b949e; font-style: italic; animation: verbPulse 2s ease-in-out infinite; }
206
+ .agent-working-name { color: #ff4f63; font-weight: 600; }
207
+ .agent-working-sep { color: #e6edf3; font-weight: 600; }
208
+ .agent-working-query { color: #8b949e; }
209
+ .conv-msg.memory-saving { display: flex; align-items: center; gap: 8px; height: 28px; overflow: hidden; }
210
+ .memory-saving-spinner {
211
+ width: 14px; height: 14px; flex-shrink: 0;
212
+ border: 2px solid rgba(163,113,247,0.2); border-top-color: #a371f7;
213
+ border-radius: 50%; animation: agentSpin 1.2s linear infinite;
214
+ }
215
+ .memory-saving-text { font-size: 11px; color: #a371f7; font-style: italic; animation: verbPulse 2s ease-in-out infinite; }
216
+ .memory-saving-detail { font-size: 10px; color: #8b949e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
217
+ .conv-msg.tool-call { color: #d2a8ff; font-size: 11px; }
218
+ .conv-msg.tool-call .label { color: #bc8cff; font-weight: 600; }
219
+ .conv-msg.tool-result { color: #7ee787; font-size: 11px; }
220
+ .conv-msg.tool-result.error { color: #f85149; }
221
+ .conv-msg.tool-result .label { font-weight: 600; }
222
+ .conv-msg.schedule-header {
223
+ background: rgba(88,166,255,0.06);
224
+ border-left: 3px solid #58a6ff;
225
+ padding: 8px 12px;
226
+ margin: 8px 0 2px 0;
227
+ border-radius: 0 6px 6px 0;
228
+ font-size: 12px;
229
+ color: #58a6ff;
230
+ }
231
+ .conv-msg.schedule-header .schedule-label { font-weight: 600; }
232
+ .conv-msg.schedule-header .schedule-prompt-preview { color: #8b949e; margin-left: 8px; font-style: italic; }
233
+ .conv-msg.schedule-done {
234
+ background: rgba(63,185,80,0.06);
235
+ border-left: 3px solid #3fb950;
236
+ padding: 6px 12px;
237
+ margin: 2px 0 8px 0;
238
+ border-radius: 0 6px 6px 0;
239
+ font-size: 12px;
240
+ color: #8b949e;
241
+ }
242
+ .conv-msg.schedule-done.error { border-left-color: #f85149; }
243
+ .tool-activity { margin-bottom: 3px; overflow: hidden; height: 32px; display: flex; align-items: center; gap: 8px; }
244
+ .tool-activity .tool-sprite {
245
+ flex-shrink: 0; width: 24px; height: 24px;
246
+ display: grid; grid-template-columns: repeat(10,2.4px); grid-template-rows: repeat(10,2.4px);
247
+ image-rendering: pixelated;
248
+ animation: spriteFloat 1.8s ease-in-out infinite, spriteGlow 1.4s ease-in-out infinite;
249
+ }
250
+ .tool-activity .tool-sprite .px { width: 2.4px; height: 2.4px; }
251
+ .tool-activity .tool-sprite .px-o { background: #12070b; }
252
+ .tool-activity .tool-sprite .px-f { background: #ff4f63; animation: spriteColor 2.8s ease-in-out infinite; }
253
+ .tool-activity .tool-sprite .px-e { background: transparent; }
254
+ .tool-activity .tool-verb { color: #8b949e; font-size: 11px; font-style: italic; white-space: nowrap; flex-shrink: 0; animation: verbPulse 2s ease-in-out infinite; }
255
+ .tool-activity .tool-activity-body { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; line-height: 32px; }
256
+ .tool-activity .tool-activity-body .tool-call,
257
+ .tool-activity .tool-activity-body .tool-result { display: inline; animation: toolFadeIn 0.2s ease-out; }
258
+ .tool-activity .tool-call,
259
+ .tool-activity .tool-result { color: #d2a8ff; font-size: 11px; }
260
+ .tool-activity .tool-call .label { color: #bc8cff; font-weight: 600; }
261
+ .tool-activity .tool-call .skill-label { color: #f0883e; }
262
+ .tool-activity .tool-call .skill-name { color: #ffa657; font-weight: 500; }
263
+ .tool-activity .tool-call .tool-args { display: inline; margin: 0; font-size: 11px; }
264
+ .tool-activity .tool-result { color: #7ee787; }
265
+ .tool-activity .tool-result.error { color: #f85149; }
266
+ .tool-activity .tool-result .label { font-weight: 600; }
267
+ .tool-activity .tool-result.thinking-fallback .label { color: #8b949e; font-style: italic; font-weight: 400; animation: verbPulse 2s ease-in-out infinite; }
268
+ .tool-activity .tool-result .tool-content { display: inline; margin: 0; padding: 0; background: none; font-size: 11px; max-height: none; overflow: hidden; }
269
+ .tool-activity-summary { cursor: pointer; color: #8b949e; font-size: 11px; line-height: 32px; }
270
+ .tool-activity-summary:hover { color: #c9d1d9; }
271
+ .tool-summary-toggle { user-select: none; }
272
+ .tool-activity.expanded { height: auto; }
273
+ .tool-activity.expanded .tool-activity-body { overflow: visible; white-space: normal; }
274
+ @keyframes toolFadeIn {
275
+ from { opacity: 0; transform: translateY(3px); }
276
+ to { opacity: 1; transform: translateY(0); }
277
+ }
278
+ @keyframes toolSummaryIn {
279
+ from { opacity: 0; }
280
+ to { opacity: 1; }
281
+ }
282
+ @keyframes spriteFloat {
283
+ 0%, 100% { transform: translateY(0) scale(1); }
284
+ 50% { transform: translateY(-3px) scale(1.05); }
285
+ }
286
+ @keyframes spriteGlow {
287
+ 0%, 100% { filter: drop-shadow(0 0 2px rgba(255,79,99,0.6)) drop-shadow(0 0 5px rgba(255,79,99,0.3)); }
288
+ 50% { filter: drop-shadow(0 0 5px rgba(255,79,99,1)) drop-shadow(0 0 10px rgba(255,79,99,0.6)); }
289
+ }
290
+ @keyframes spriteColor {
291
+ 0%, 100% { background: #ff4f63; }
292
+ 33% { background: #ff6b7a; }
293
+ 66% { background: #ff2040; }
294
+ }
295
+ @keyframes verbPulse {
296
+ 0%, 100% { opacity: 0.6; }
297
+ 50% { opacity: 1; }
298
+ }
299
+ .conv-msg.thinking { color: #6e7681; font-size: 11px; font-style: italic; }
300
+ .conv-msg .tool-args { color: #8b949e; font-size: 10px; margin-top: 2px; word-break: break-all; max-height: 60px; overflow: hidden; }
301
+ .conv-msg .tool-content { color: #8b949e; font-size: 10px; margin-top: 2px; white-space: pre-wrap; max-height: 80px; overflow-y: auto; background: #161b22; border-radius: 4px; padding: 4px 8px; }
302
+ .conv-msg.system { color: #8b949e; font-style: italic; }
303
+ .input-bar { display: flex; padding: 12px 16px; border-top: 1px solid #21262d; gap: 8px; }
304
+ .input-bar input {
305
+ flex: 1; background: #161b22; border: 1px solid #21262d; border-radius: 6px;
306
+ padding: 10px 14px; color: #e6edf3; font-family: 'IBM Plex Mono', monospace; font-size: 13px; outline: none;
307
+ }
308
+ .input-bar input:focus { border-color: #58a6ff; }
309
+ .input-bar input::placeholder { color: #484f58; }
310
+ .input-bar button {
311
+ background: #238636; border: none; border-radius: 6px; color: #fff;
312
+ font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 600; padding: 10px 18px; cursor: pointer;
313
+ }
314
+ .input-bar button:hover { background: #2ea043; }
315
+ .attach-btn {
316
+ background: transparent; border: 1px solid #21262d; border-radius: 6px; color: #8b949e;
317
+ font-size: 16px; padding: 6px 10px; cursor: pointer; display: flex; align-items: center;
318
+ transition: border-color 0.15s, color 0.15s;
319
+ }
320
+ .attach-btn:hover { border-color: #58a6ff; color: #58a6ff; }
321
+ .attach-btn svg { width: 18px; height: 18px; }
322
+ .file-preview-bar {
323
+ display: none; padding: 6px 16px; border-top: 1px solid #21262d; gap: 6px; flex-wrap: wrap;
324
+ background: #0d1117;
325
+ }
326
+ .file-preview-bar.active { display: flex; }
327
+ .file-chip {
328
+ display: flex; align-items: center; gap: 6px; background: #161b22; border: 1px solid #21262d;
329
+ border-radius: 4px; padding: 4px 8px; font-size: 11px; color: #8b949e;
330
+ font-family: 'IBM Plex Mono', monospace;
331
+ }
332
+ .file-chip img { max-height: 32px; max-width: 48px; border-radius: 2px; }
333
+ .file-chip .remove-file {
334
+ background: none; border: none; color: #484f58; cursor: pointer; font-size: 14px;
335
+ padding: 0 2px; line-height: 1;
336
+ }
337
+ .file-chip .remove-file:hover { color: #f85149; }
338
+ .drop-overlay {
339
+ display: none; position: absolute; inset: 0; background: rgba(88,166,255,0.08);
340
+ border: 2px dashed #58a6ff; border-radius: 8px; z-index: 100;
341
+ align-items: center; justify-content: center; font-size: 14px; color: #58a6ff;
342
+ font-family: 'IBM Plex Mono', monospace; pointer-events: none;
343
+ }
344
+ .drop-overlay.active { display: flex; }
345
+
346
+ /* Audit mode toggle in header */
347
+ .audit-toggle {
348
+ display: flex; align-items: center; gap: 6px; cursor: pointer;
349
+ font-size: 11px; color: #8b949e; font-family: 'IBM Plex Mono', monospace;
350
+ padding: 3px 10px; border-radius: 4px; border: 1px solid #21262d;
351
+ background: transparent; transition: all 0.2s ease; user-select: none;
352
+ }
353
+ .audit-toggle:hover { border-color: #30363d; color: #b1bac4; }
354
+ .audit-toggle.active { border-color: #3fb950; color: #3fb950; background: rgba(63,185,80,0.08); }
355
+ .audit-toggle .audit-dot {
356
+ width: 6px; height: 6px; border-radius: 50%; background: #484f58;
357
+ transition: background 0.2s, box-shadow 0.2s;
358
+ }
359
+ .audit-toggle.active .audit-dot {
360
+ background: #3fb950;
361
+ box-shadow: 0 0 6px rgba(63,185,80,0.6);
362
+ }
363
+ @keyframes audit-dot-pulse {
364
+ 0%,100% { box-shadow: 0 0 6px rgba(63,185,80,0.6); }
365
+ 50% { box-shadow: 0 0 12px rgba(63,185,80,0.9); }
366
+ }
367
+ .audit-toggle.active.recording .audit-dot {
368
+ animation: audit-dot-pulse 1.5s ease-in-out infinite;
369
+ }
370
+
371
+ /* Files panel (right side of chat) — contains tree + inline diff viewer */
372
+ .files-panel {
373
+ width: 40vw; min-width: 200px; background: #0e1117; border-left: 1px solid #21262d;
374
+ display: flex; flex-direction: column; overflow: hidden; position: relative;
375
+ transition: opacity 0.25s ease;
376
+ }
377
+ .files-panel.collapsed { min-width: 0; }
378
+ .files-panel.collapsed {
379
+ width: 0 !important; min-width: 0 !important; opacity: 0; pointer-events: none; border-left: none;
380
+ }
381
+ /* Resize handles */
382
+ .resize-handle-x {
383
+ position: absolute; left: -3px; top: 0; bottom: 0; width: 6px;
384
+ cursor: col-resize; z-index: 20;
385
+ }
386
+ .resize-handle-x:hover, .resize-handle-x.active { background: rgba(88,166,255,0.3); }
387
+ .resize-handle-y {
388
+ height: 6px; cursor: row-resize; flex-shrink: 0; position: relative;
389
+ }
390
+ .resize-handle-y:hover, .resize-handle-y.active { background: rgba(88,166,255,0.3); }
391
+ .resize-handle-y::after {
392
+ content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
393
+ width: 24px; height: 2px; background: #30363d; border-radius: 1px;
394
+ }
395
+ .files-panel-header {
396
+ padding: 10px 16px; font-size: 11px; color: #8b949e; font-weight: 600;
397
+ text-transform: uppercase; letter-spacing: 0.5px;
398
+ display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #21262d;
399
+ flex-shrink: 0;
400
+ }
401
+ .files-panel-header .fp-title { display: flex; align-items: center; gap: 8px; }
402
+ .files-panel-header .fp-count {
403
+ font-size: 9px; background: rgba(63,185,80,0.15); color: #3fb950;
404
+ padding: 1px 6px; border-radius: 8px; font-weight: 500; min-width: 18px; text-align: center;
405
+ transition: opacity 0.3s, transform 0.3s;
406
+ }
407
+ .files-panel-header .fp-count.hidden { opacity: 0; transform: scale(0.5); pointer-events: none; }
408
+ .files-panel-header button {
409
+ background: none; border: none; color: #8b949e; cursor: pointer;
410
+ font-size: 14px; padding: 2px 6px; border-radius: 3px; font-family: inherit;
411
+ }
412
+ .files-panel-header button:hover { color: #58a6ff; background: rgba(88,166,255,0.08); }
413
+
414
+ /* File tree area */
415
+ .files-panel .file-tree { flex: 1; overflow-y: auto; padding: 4px 0; min-height: 80px; }
416
+
417
+ /* File tree change animations */
418
+ @keyframes file-pulse {
419
+ 0% { background: transparent; box-shadow: none; }
420
+ 15% { background: rgba(63,185,80,0.18); box-shadow: inset 0 0 12px rgba(63,185,80,0.15), 0 0 8px rgba(63,185,80,0.1); }
421
+ 100% { background: transparent; box-shadow: none; }
422
+ }
423
+ @keyframes file-slide-in {
424
+ 0% { opacity: 0; transform: translateX(12px); }
425
+ 100% { opacity: 1; transform: translateX(0); }
426
+ }
427
+ .ft-item.changed, summary.changed { animation: file-pulse 2s ease-out; }
428
+ .ft-item.new-file, summary.new-file { animation: file-slide-in 0.3s ease-out, file-pulse 2s ease-out 0.3s; }
429
+ .ft-item { position: relative; }
430
+ .ft-badge {
431
+ margin-left: auto; font-size: 9px; font-weight: 600; padding: 1px 5px;
432
+ border-radius: 3px; letter-spacing: 0.3px; flex-shrink: 0;
433
+ }
434
+ .ft-badge.pop { animation: badge-pop 0.35s cubic-bezier(0.34,1.56,0.64,1); }
435
+ @keyframes badge-pop { 0% { opacity: 0; transform: scale(0.3); } 100% { opacity: 1; transform: scale(1); } }
436
+ .ft-badge.edited { background: rgba(63,185,80,0.15); color: #3fb950; }
437
+ .ft-badge.edited.pop { box-shadow: 0 0 8px rgba(63,185,80,0.4); }
438
+ .ft-badge.new { background: rgba(88,166,255,0.15); color: #58a6ff; }
439
+ .ft-badge.new.pop { box-shadow: 0 0 8px rgba(88,166,255,0.4); }
440
+ .ft-item .ft-gutter {
441
+ position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
442
+ border-radius: 0 2px 2px 0;
443
+ }
444
+ .ft-gutter.edited { background: #3fb950; }
445
+ .ft-gutter.new { background: #58a6ff; }
446
+
447
+ /* Inline diff viewer (inside files-panel, below tree) */
448
+ .diff-viewer {
449
+ background: #0a0d12;
450
+ display: flex; flex-direction: column; overflow: hidden;
451
+ height: 0; opacity: 0; flex-shrink: 0;
452
+ transition: height 0.35s cubic-bezier(0.4,0,0.2,1), opacity 0.25s ease;
453
+ }
454
+ .diff-viewer.open {
455
+ height: 280px; opacity: 1;
456
+ }
457
+ .diff-viewer-header {
458
+ display: flex; align-items: center; justify-content: space-between;
459
+ padding: 6px 12px; border-bottom: 1px solid #161b22; flex-shrink: 0;
460
+ }
461
+ .diff-viewer-header .dv-left { display: flex; align-items: center; gap: 8px; overflow: hidden; }
462
+ .diff-viewer-header .dv-path {
463
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #b1bac4;
464
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
465
+ }
466
+ .diff-viewer-header .dv-status {
467
+ font-size: 8px; font-weight: 700; padding: 2px 6px; border-radius: 3px;
468
+ flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.5px;
469
+ }
470
+ .dv-status.edited { background: rgba(63,185,80,0.15); color: #3fb950; }
471
+ .dv-status.viewing { background: rgba(88,166,255,0.1); color: #58a6ff; }
472
+ .diff-viewer-header button {
473
+ background: none; border: none; color: #484f58; cursor: pointer;
474
+ font-size: 14px; padding: 2px 6px; border-radius: 3px;
475
+ }
476
+ .diff-viewer-header button:hover { color: #e6edf3; background: #161b22; }
477
+ /* Auto-close countdown bar */
478
+ .dv-countdown {
479
+ height: 2px; background: #3fb950; transition: width 2s linear;
480
+ flex-shrink: 0;
481
+ }
482
+ .dv-countdown.done { width: 0 !important; }
483
+ .diff-viewer-content {
484
+ flex: 1; overflow: auto; padding: 0; margin: 0; position: relative;
485
+ }
486
+ .dv-md-toggle { display: flex; align-items: center; }
487
+ .dv-md-toggle.active { color: #58a6ff !important; }
488
+ .dv-markdown {
489
+ padding: 16px 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
490
+ font-size: 13px; line-height: 1.6; color: #e6edf3;
491
+ }
492
+ .dv-markdown.hidden, .dv-image.hidden,
493
+ .dv-html.hidden, .dv-pdf.hidden, .dv-video.hidden, .dv-audio.hidden, .dv-binary.hidden { display: none; }
494
+ .dv-html, .dv-pdf {
495
+ flex: 1; width: 100%; height: 100%; border: 0; background: #0a0d12;
496
+ }
497
+ .dv-video, .dv-audio {
498
+ flex: 1; display: flex; align-items: center; justify-content: center;
499
+ padding: 16px; background: #0a0d12; max-width: 100%;
500
+ }
501
+ .dv-video video, .dv-video { max-width: 100%; max-height: 100%; }
502
+ .dv-audio audio { width: 100%; max-width: 480px; }
503
+ .dv-binary {
504
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
505
+ padding: 24px; background: #0a0d12; gap: 14px; text-align: center; color: #c9d1d9;
506
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
507
+ }
508
+ .dv-binary .bin-icon { font-size: 32px; opacity: 0.6; }
509
+ .dv-binary .bin-name { font-size: 14px; font-weight: 600; word-break: break-all; max-width: 100%; }
510
+ .dv-binary .bin-meta { font-size: 11px; color: #8b949e; font-family: 'IBM Plex Mono', monospace; }
511
+ .dv-binary .bin-hint { font-size: 11px; color: #6e7681; max-width: 320px; }
512
+ .dv-binary a.bin-download {
513
+ display: inline-block; padding: 6px 14px; border-radius: 6px;
514
+ background: #238636; color: #fff; text-decoration: none; font-size: 12px; font-weight: 600;
515
+ border: 1px solid #2ea043;
516
+ }
517
+ .dv-binary a.bin-download:hover { background: #2ea043; }
518
+ .dv-dl-btn {
519
+ background: transparent; border: 1px solid #30363d; color: #8b949e;
520
+ padding: 2px 8px; border-radius: 4px; font-size: 11px; cursor: pointer;
521
+ margin-right: 4px;
522
+ }
523
+ .dv-dl-btn:hover { border-color: #58a6ff; color: #58a6ff; }
524
+ .dv-dl-btn.hidden { display: none; }
525
+ .dv-image {
526
+ display: flex; align-items: center; justify-content: center; padding: 20px;
527
+ flex: 1; background: #0a0d12; overflow: auto;
528
+ }
529
+ .dv-image img {
530
+ max-width: 100%; max-height: 100%; object-fit: contain;
531
+ border-radius: 6px; border: 1px solid #21262d;
532
+ background: repeating-conic-gradient(#1c2129 0% 25%, #161b22 0% 50%) 0 0 / 16px 16px;
533
+ }
534
+ .dv-image .img-meta {
535
+ position: absolute; bottom: 8px; right: 12px; font-size: 10px; color: #484f58;
536
+ font-family: 'IBM Plex Mono', monospace;
537
+ }
538
+ .dv-markdown h1 { font-size: 1.6em; font-weight: 600; border-bottom: 1px solid #21262d; padding-bottom: 6px; margin: 16px 0 8px; color: #f0f6fc; }
539
+ .dv-markdown h2 { font-size: 1.3em; font-weight: 600; border-bottom: 1px solid #21262d; padding-bottom: 4px; margin: 14px 0 6px; color: #f0f6fc; }
540
+ .dv-markdown h3 { font-size: 1.1em; font-weight: 600; margin: 12px 0 4px; color: #f0f6fc; }
541
+ .dv-markdown h4, .dv-markdown h5, .dv-markdown h6 { font-size: 1em; font-weight: 600; margin: 10px 0 4px; color: #e6edf3; }
542
+ .dv-markdown p { margin: 0 0 10px; }
543
+ .dv-markdown ul, .dv-markdown ol { padding-left: 24px; margin: 0 0 10px; }
544
+ .dv-markdown li { margin: 2px 0; }
545
+ .dv-markdown code {
546
+ background: rgba(110,118,129,0.15); padding: 2px 5px; border-radius: 3px;
547
+ font-family: 'JetBrains Mono', 'IBM Plex Mono', monospace; font-size: 0.9em;
548
+ }
549
+ .dv-markdown pre {
550
+ background: #161b22; border: 1px solid #21262d; border-radius: 6px;
551
+ padding: 12px 16px; overflow-x: auto; margin: 0 0 12px;
552
+ }
553
+ .dv-markdown pre code { background: none; padding: 0; font-size: 12px; }
554
+ .dv-markdown blockquote {
555
+ border-left: 3px solid #30363d; padding: 4px 16px; margin: 0 0 10px; color: #8b949e;
556
+ }
557
+ .dv-markdown a { color: #58a6ff; text-decoration: none; }
558
+ .dv-markdown a:hover { text-decoration: underline; }
559
+ .dv-markdown hr { border: none; border-top: 1px solid #21262d; margin: 16px 0; }
560
+ .dv-markdown strong { color: #f0f6fc; }
561
+ .dv-markdown table { border-collapse: collapse; margin: 0 0 12px; width: 100%; }
562
+ .dv-markdown th, .dv-markdown td { border: 1px solid #21262d; padding: 6px 12px; text-align: left; }
563
+ .dv-markdown th { background: #161b22; font-weight: 600; }
564
+ .dv-markdown img { max-width: 100%; border-radius: 6px; }
565
+ .diff-viewer-content pre {
566
+ margin: 0; padding: 8px 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', monospace;
567
+ font-size: 11px; line-height: 1.55; color: #e6edf3; white-space: pre;
568
+ counter-reset: line;
569
+ }
570
+ .diff-viewer-content .dv-line {
571
+ display: block; padding: 0 10px 0 52px; position: relative; min-height: 1.55em;
572
+ }
573
+ .diff-viewer-content .dv-line::before {
574
+ content: counter(line); counter-increment: line;
575
+ position: absolute; left: 0; width: 36px; text-align: right; padding-right: 8px;
576
+ color: #484f58; font-size: 10px; user-select: none;
577
+ }
578
+ .diff-viewer-content .dv-line:hover { background: rgba(88,166,255,0.04); }
579
+ .diff-viewer-content .dv-line .dv-gutter {
580
+ position: absolute; left: 40px; top: 2px; bottom: 2px; width: 3px;
581
+ border-radius: 2px;
582
+ }
583
+ .dv-gutter.g-added { background: #3fb950; box-shadow: 0 0 6px rgba(63,185,80,0.5); }
584
+ .dv-gutter.g-modified { background: #d29922; box-shadow: 0 0 6px rgba(210,153,34,0.5); }
585
+ @keyframes dv-line-flash {
586
+ 0% { background: rgba(63,185,80,0.3); box-shadow: inset 0 0 20px rgba(63,185,80,0.15); }
587
+ 40% { background: rgba(63,185,80,0.12); box-shadow: inset 0 0 8px rgba(63,185,80,0.06); }
588
+ 100% { background: transparent; box-shadow: none; }
589
+ }
590
+ .dv-line.line-changed { animation: dv-line-flash 2s ease-out forwards; border-left: 2px solid rgba(210,153,34,0.4); padding-left: 50px; }
591
+ .dv-line.line-added { animation: dv-line-flash 2s ease-out forwards; border-left: 2px solid rgba(63,185,80,0.4); padding-left: 50px; }
592
+ @keyframes dv-line-enter {
593
+ 0% { opacity: 0; transform: translateX(6px); }
594
+ 100% { opacity: 1; transform: translateX(0); }
595
+ }
596
+ .dv-line.line-enter { animation: dv-line-enter 0.15s ease-out forwards; }
597
+
598
+ /* FILES VIEW (disabled)
599
+ .files-view { display: flex; flex: 1; overflow: hidden; }
600
+ .files-view.hidden { display: none; }
601
+ .activity-bar {
602
+ width: 48px; min-width: 48px; background: #0a0d12; border-right: 1px solid #21262d;
603
+ display: flex; flex-direction: column; align-items: center; padding: 8px 0; gap: 4px;
604
+ }
605
+ .activity-btn {
606
+ width: 36px; height: 36px; border-radius: 6px; border: none; background: transparent;
607
+ color: #8b949e; cursor: pointer; display: flex; align-items: center; justify-content: center;
608
+ transition: all 0.15s; position: relative;
609
+ }
610
+ .activity-btn:hover { color: #e6edf3; background: #161b22; }
611
+ .activity-btn.active { color: #e6edf3; }
612
+ .activity-btn.active::before {
613
+ content: ''; position: absolute; left: 0; top: 6px; bottom: 6px;
614
+ width: 2px; background: #58a6ff; border-radius: 0 2px 2px 0;
615
+ }
616
+ .activity-btn svg { width: 20px; height: 20px; }
617
+ .file-sidebar {
618
+ width: 260px; min-width: 260px; background: #0e1117; border-right: 1px solid #21262d;
619
+ display: flex; flex-direction: column; overflow: hidden;
620
+ }
621
+ .file-sidebar-header {
622
+ padding: 10px 16px; font-size: 11px; color: #8b949e; font-weight: 600;
623
+ text-transform: uppercase; letter-spacing: 0.5px;
624
+ display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #21262d;
625
+ }
626
+ .file-sidebar-header button {
627
+ background: none; border: none; color: #8b949e; cursor: pointer;
628
+ font-size: 11px; padding: 2px 6px; border-radius: 3px; font-family: inherit;
629
+ }
630
+ .file-sidebar-header button:hover { color: #58a6ff; background: rgba(88,166,255,0.08); }
631
+ .file-tree { flex: 1; overflow-y: auto; padding: 4px 0; }
632
+ /* File tree — native <details>/<summary> based */
633
+ .ft-dir { border: none; margin: 0; padding: 0; }
634
+ .ft-dir > summary {
635
+ display: flex; align-items: center; gap: 4px; padding: 3px 8px; font-size: 13px;
636
+ cursor: pointer; color: #b1bac4; white-space: nowrap; overflow: hidden;
637
+ text-overflow: ellipsis; font-family: 'IBM Plex Mono', monospace;
638
+ list-style: none; transition: background 0.1s; user-select: none;
639
+ }
640
+ .ft-dir > summary::-webkit-details-marker { display: none; }
641
+ .ft-dir > summary::marker { display: none; content: ''; }
642
+ .ft-dir > summary:hover { background: #161b22; }
643
+ .ft-dir > .ft-children { margin: 0; padding: 0; }
644
+ .ft-item {
645
+ display: flex; align-items: center; gap: 4px; padding: 3px 8px; font-size: 13px;
646
+ cursor: pointer; color: #b1bac4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
647
+ transition: background 0.1s; font-family: 'IBM Plex Mono', monospace;
648
+ }
649
+ .ft-item:hover { background: #161b22; }
650
+ .ft-item.active-viewer { background: rgba(88,166,255,0.08); color: #e6edf3; }
651
+ .ft-icon { width: 16px; height: 16px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
652
+ .ft-chevron { width: 14px; height: 14px; flex-shrink: 0; color: #484f58; display: flex; align-items: center; justify-content: center; transition: transform 0.15s; }
653
+ .ft-dir[open] > summary .ft-chevron { transform: rotate(90deg); }
654
+ .ft-name { overflow: hidden; text-overflow: ellipsis; margin-left: 2px; }
655
+ .ft-icon.folder { color: #d29922; }
656
+ .ft-icon.ts { color: #58a6ff; }
657
+ .ft-icon.js { color: #f0db4f; }
658
+ .ft-icon.json { color: #d4883b; }
659
+ .ft-icon.css { color: #d291ff; }
660
+ .ft-icon.html { color: #f06529; }
661
+ .ft-icon.md { color: #8b949e; }
662
+ .ft-icon.yaml { color: #cb4a32; }
663
+ .ft-icon.py { color: #3572A5; }
664
+ .ft-icon.sh { color: #89e051; }
665
+ .ft-icon.default { color: #6e7681; }
666
+ .editor-area { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #0d1117; }
667
+ .editor-tabs {
668
+ display: flex; background: #0a0d12; border-bottom: 1px solid #21262d; overflow-x: auto; min-height: 35px;
669
+ }
670
+ .editor-tabs:empty { display: none; }
671
+ .ed-tab {
672
+ display: flex; align-items: center; gap: 6px; padding: 0 14px; font-size: 12px;
673
+ font-family: 'IBM Plex Mono', monospace; cursor: pointer; border-right: 1px solid #21262d;
674
+ color: #8b949e; background: #0a0d12; flex-shrink: 0; transition: all 0.1s; height: 35px;
675
+ }
676
+ .ed-tab:hover { color: #b1bac4; }
677
+ .ed-tab.active { background: #0d1117; color: #e6edf3; border-top: 2px solid #58a6ff; }
678
+ .ed-tab:not(.active) { border-top: 2px solid transparent; }
679
+ .ed-tab .tab-icon { width: 14px; height: 14px; flex-shrink: 0; display: flex; align-items: center; }
680
+ .ed-tab .tab-modified { width: 6px; height: 6px; border-radius: 50%; background: #58a6ff; flex-shrink: 0; }
681
+ .ed-tab .tab-close {
682
+ width: 18px; height: 18px; border: none; background: transparent; color: #8b949e;
683
+ cursor: pointer; border-radius: 3px; display: flex; align-items: center; justify-content: center;
684
+ opacity: 0; transition: opacity 0.1s; font-size: 14px; margin-left: 4px;
685
+ }
686
+ .ed-tab:hover .tab-close { opacity: 1; }
687
+ .ed-tab .tab-close:hover { background: #21262d; color: #e6edf3; }
688
+ .editor-container { flex: 1; overflow: hidden; }
689
+ .editor-empty {
690
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
691
+ color: #484f58; gap: 12px;
692
+ }
693
+ .editor-empty svg { width: 48px; height: 48px; opacity: 0.2; }
694
+ .editor-empty p { font-size: 13px; }
695
+ .editor-empty .shortcuts { display: flex; gap: 20px; font-size: 11px; color: #6e7681; margin-top: 4px; }
696
+ .editor-empty .shortcuts kbd {
697
+ background: #161b22; padding: 2px 6px; border-radius: 3px;
698
+ font-family: 'IBM Plex Mono', monospace; font-size: 10px; border: 1px solid #21262d; margin-right: 4px;
699
+ }
700
+ .status-bar {
701
+ height: 24px; min-height: 24px; background: #1a3a5c; border-top: 1px solid #21262d;
702
+ display: flex; align-items: center; padding: 0 12px; font-size: 11px; color: #8b949e; gap: 16px;
703
+ font-family: 'IBM Plex Mono', monospace;
704
+ }
705
+ .status-bar .sb-right { margin-left: auto; display: flex; gap: 16px; }
706
+ FILES VIEW (disabled) */
707
+ /* Scrollbar — dark theme */
708
+ * { scrollbar-width: thin; scrollbar-color: rgba(139,148,158,0.25) transparent; }
709
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
710
+ ::-webkit-scrollbar-track { background: transparent; }
711
+ ::-webkit-scrollbar-thumb { background: rgba(139,148,158,0.2); border-radius: 3px; }
712
+ ::-webkit-scrollbar-thumb:hover { background: rgba(139,148,158,0.35); }
713
+ ::-webkit-scrollbar-corner { background: transparent; }
714
+ /* INTEGRATIONS VIEW */
715
+ .integrations-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
716
+ .integrations-view.hidden { display: none; }
717
+ .integrations-header {
718
+ display: flex; align-items: center; justify-content: space-between;
719
+ padding: 16px 20px; border-bottom: 1px solid #21262d;
720
+ }
721
+ .integrations-header h2 { font-size: 15px; font-weight: 600; font-family: 'IBM Plex Mono', monospace; color: #e6edf3; }
722
+ .integrations-header button {
723
+ background: #161b22; border: 1px solid #21262d; border-radius: 6px; color: #8b949e;
724
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; padding: 6px 14px; cursor: pointer;
725
+ }
726
+ .integrations-header button:hover { border-color: #30363d; color: #b1bac4; }
727
+ .integrations-grid {
728
+ flex: 1; overflow-y: auto; padding: 20px;
729
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px;
730
+ align-content: start;
731
+ }
732
+ .integrations-empty {
733
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
734
+ color: #484f58; gap: 8px; font-size: 13px; font-family: 'IBM Plex Mono', monospace;
735
+ }
736
+ .toolkit-card {
737
+ background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px;
738
+ display: flex; flex-direction: column; gap: 10px; transition: border-color 0.15s;
739
+ }
740
+ .toolkit-card:hover { border-color: #30363d; }
741
+ .toolkit-card .tk-top { display: flex; align-items: center; gap: 10px; }
742
+ .toolkit-card .tk-logo {
743
+ width: 36px; height: 36px; border-radius: 8px; object-fit: contain; background: #0d1117;
744
+ flex-shrink: 0;
745
+ }
746
+ .toolkit-card .tk-logo-placeholder {
747
+ width: 36px; height: 36px; border-radius: 8px; background: #21262d;
748
+ display: flex; align-items: center; justify-content: center; font-size: 16px; color: #484f58;
749
+ flex-shrink: 0;
750
+ }
751
+ .toolkit-card .tk-name { font-size: 13px; font-weight: 600; color: #e6edf3; }
752
+ .toolkit-card .tk-desc {
753
+ font-size: 11px; color: #8b949e; line-height: 1.5;
754
+ overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
755
+ }
756
+ .toolkit-card .tk-actions { margin-top: auto; }
757
+ .tk-btn {
758
+ width: 100%; padding: 8px; border-radius: 6px; font-family: 'IBM Plex Mono', monospace;
759
+ font-size: 11px; font-weight: 600; cursor: pointer; border: none; transition: all 0.15s;
760
+ }
761
+ .tk-btn.connect { background: #238636; color: #fff; }
762
+ .tk-btn.connect:hover { background: #2ea043; }
763
+ .tk-btn.connected { background: rgba(63,185,80,0.12); color: #3fb950; border: 1px solid #23612c; }
764
+ .tk-btn.connected:hover { background: rgba(248,81,73,0.12); color: #f85149; border-color: #da3633; }
765
+
766
+ /* Skills Marketplace view */
767
+ .skills-view.hidden { display: none !important; }
768
+
769
+ /* SkillFlows view */
770
+ .flows-view { display: flex; flex: 1; overflow: hidden; }
771
+ .flows-view.hidden { display: none !important; }
772
+ .flows-skills-panel {
773
+ width: 220px; min-width: 220px; border-right: 1px solid #21262d;
774
+ padding: 12px; overflow-y: auto; background: #010409;
775
+ }
776
+ .flows-skill-tree { padding: 4px 0; }
777
+ .flows-skill-dir { border: none; margin: 0; padding: 0; }
778
+ .flows-skill-dir > summary {
779
+ display: flex; align-items: center; gap: 4px; padding: 2px 4px;
780
+ font-family: 'JetBrains Mono','IBM Plex Mono','Fira Code',monospace;
781
+ font-size: 13px; color: #b1bac4; user-select: none; cursor: pointer;
782
+ list-style: none;
783
+ }
784
+ .flows-skill-dir > summary::-webkit-details-marker { display: none; }
785
+ .flows-skill-dir > summary::marker { display: none; content: ''; }
786
+ .flows-skill-dir > summary:hover { background: #161b22; }
787
+ .flows-skill-dir[open] > summary .ft-chevron { transform: rotate(90deg); }
788
+ .flows-skill-dir > summary .ft-chevron { transition: transform 0.15s; }
789
+ .flows-skill-item {
790
+ display: flex; align-items: center; gap: 4px; padding: 2px 4px 2px 24px;
791
+ font-family: 'JetBrains Mono','IBM Plex Mono','Fira Code',monospace;
792
+ font-size: 13px; color: #c9d1d9; cursor: default;
793
+ }
794
+ .flows-skill-item:hover { background: #161b22; }
795
+ .flows-skill-item .skill-add {
796
+ display: none; margin-left: auto; width: 20px; height: 20px;
797
+ border: none; border-radius: 50%; background: transparent;
798
+ color: #3fb950; font-size: 14px; cursor: pointer;
799
+ line-height: 20px; text-align: center; padding: 0; flex-shrink: 0;
800
+ }
801
+ .flows-skill-item:hover .skill-add { display: flex; align-items: center; justify-content: center; }
802
+ .flows-skill-item .skill-add:hover { background: #238636; color: #fff; }
803
+ .flows-step-card {
804
+ min-width: 220px; max-width: 220px; background: #161b22;
805
+ border: 1px solid #30363d; border-radius: 8px; padding: 10px;
806
+ display: flex; flex-direction: column; gap: 6px; flex-shrink: 0;
807
+ }
808
+ .flows-step-card .step-header {
809
+ display: flex; justify-content: space-between; align-items: center; cursor: grab;
810
+ }
811
+ .flows-step-card .step-skill {
812
+ font-size: 12px; font-weight: 600; color: #58a6ff;
813
+ }
814
+ .flows-step-card .step-num {
815
+ font-size: 10px; color: #484f58;
816
+ }
817
+ .flows-step-card .step-remove {
818
+ background: none; border: none; color: #f85149; cursor: pointer;
819
+ font-size: 14px; padding: 0 4px; line-height: 1;
820
+ }
821
+ .flows-step-card textarea {
822
+ width: 100%; min-height: 80px; background: #0d1117; border: 1px solid #21262d;
823
+ border-radius: 4px; color: #c9d1d9; font-size: 12px; padding: 6px;
824
+ resize: vertical; outline: none; font-family: inherit;
825
+ }
826
+ .flows-step-card textarea:focus { border-color: #58a6ff; }
827
+ .flows-gate-card {
828
+ min-width: 220px; max-width: 220px; background: #161b22;
829
+ border: 1px solid #f0883e; border-radius: 8px; padding: 10px;
830
+ display: flex; flex-direction: column; gap: 6px; flex-shrink: 0;
831
+ }
832
+ .flows-gate-card .gate-header {
833
+ display: flex; justify-content: space-between; align-items: center; cursor: grab;
834
+ }
835
+ .flows-gate-card .gate-label {
836
+ display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: #f0883e;
837
+ }
838
+ .flows-gate-card .gate-label svg { width: 14px; height: 14px; }
839
+ .flows-gate-card .gate-remove {
840
+ background: none; border: none; color: #f85149; cursor: pointer;
841
+ font-size: 14px; padding: 0 4px; line-height: 1;
842
+ }
843
+ .flows-gate-card .gate-info {
844
+ font-size: 11px; color: #8b949e; line-height: 1.4;
845
+ }
846
+ .flows-gate-card .gate-channels {
847
+ display: flex; gap: 6px; flex-wrap: wrap;
848
+ }
849
+ .flows-gate-card .gate-channel {
850
+ font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 500;
851
+ }
852
+ .flows-gate-card .gate-channel.tg { background: rgba(42,171,238,0.12); color: #2AABEE; }
853
+ .flows-gate-card .gate-channel.wa { background: rgba(37,211,102,0.12); color: #25D366; }
854
+ .flows-gate-card select {
855
+ width: 100%; padding: 4px 6px; background: #0d1117; border: 1px solid #21262d;
856
+ border-radius: 4px; color: #c9d1d9; font-size: 11px; outline: none;
857
+ }
858
+ .flows-skill-item.gate-item { color: #f0883e; }
859
+ .flows-arrow {
860
+ display: flex; align-items: center; color: #484f58; font-size: 20px;
861
+ padding: 0 4px; flex-shrink: 0;
862
+ }
863
+ .flows-step-card.dragging, .flows-gate-card.dragging { opacity: 0.35; border-style: dashed; }
864
+ .flows-drop-indicator {
865
+ width: 3px; align-self: stretch; min-height: 80px; margin: 0 2px;
866
+ background: #58a6ff; border-radius: 2px; flex-shrink: 0; position: relative;
867
+ }
868
+ .flows-drop-indicator::after {
869
+ content: '\2295'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
870
+ color: #58a6ff; font-size: 16px; background: #0d1117; border-radius: 50%;
871
+ width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
872
+ }
873
+ .flows-saved-item {
874
+ display: flex; justify-content: space-between; align-items: center;
875
+ padding: 8px 12px; background: #161b22; border: 1px solid #21262d;
876
+ border-radius: 6px; cursor: pointer; transition: border-color 0.15s;
877
+ }
878
+ .flows-saved-item:hover { border-color: #58a6ff; }
879
+ .flows-saved-item .flow-name { color: #c9d1d9; font-size: 13px; font-weight: 500; }
880
+ .flows-saved-item .flow-trigger { color: #58a6ff; font-size: 11px; font-family: monospace; }
881
+ .flows-saved-item .flow-desc { color: #8b949e; font-size: 11px; }
882
+ .flows-saved-item .flow-delete {
883
+ background: none; border: none; color: #f85149; cursor: pointer;
884
+ font-size: 13px; padding: 2px 6px; opacity: 0.6;
885
+ }
886
+ .flows-saved-item .flow-delete:hover { opacity: 1; }
887
+
888
+ /* Scheduler view */
889
+ /* Logs view */
890
+ .logs-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
891
+ .logs-view.hidden { display: none; }
892
+ .logs-toolbar {
893
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
894
+ padding: 8px 12px; background: #161b22; border-bottom: 1px solid #21262d; flex-shrink: 0;
895
+ }
896
+ .logs-filters { display: flex; gap: 6px; align-items: center; flex: 1; min-width: 0; }
897
+ .logs-filters select, .logs-filters input {
898
+ padding: 5px 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 4px;
899
+ color: #c9d1d9; font-size: 11px; font-family: inherit; outline: none;
900
+ }
901
+ .logs-filters select { min-width: 100px; }
902
+ .logs-filters input { flex: 1; min-width: 120px; }
903
+ .logs-filters select:focus, .logs-filters input:focus { border-color: #58a6ff; }
904
+ .logs-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
905
+ .logs-actions label { font-size: 11px; color: #8b949e; display: flex; align-items: center; gap: 4px; cursor: pointer; }
906
+ .logs-actions button {
907
+ padding: 4px 10px; background: #21262d; border: 1px solid #30363d; border-radius: 4px;
908
+ color: #c9d1d9; font-size: 11px; cursor: pointer; font-family: inherit;
909
+ }
910
+ .logs-actions button:hover { background: #30363d; }
911
+ .logs-output {
912
+ flex: 1; overflow-y: auto; padding: 0; font-family: 'IBM Plex Mono', monospace;
913
+ font-size: 12px; line-height: 1.6; background: #0d1117;
914
+ }
915
+ .log-entry { display: flex; gap: 8px; padding: 1px 10px; border-bottom: 1px solid #161b2200; }
916
+ .log-entry:hover { background: #161b22; }
917
+ .log-ts { color: #484f58; min-width: 80px; flex-shrink: 0; white-space: nowrap; }
918
+ .log-src {
919
+ display: inline-block; min-width: 72px; text-align: center; font-size: 10px; font-weight: 600;
920
+ padding: 0 6px; border-radius: 3px; flex-shrink: 0; line-height: 1.6;
921
+ }
922
+ .log-src-voice { color: #58a6ff; background: rgba(88,166,255,0.1); }
923
+ .log-src-telegram { color: #2188ff; background: rgba(33,136,255,0.1); }
924
+ .log-src-whatsapp { color: #3fb950; background: rgba(63,185,80,0.1); }
925
+ .log-src-triggers { color: #d29922; background: rgba(210,153,34,0.1); }
926
+ .log-src-sdk { color: #bc8cff; background: rgba(188,140,255,0.1); }
927
+ .log-src-scheduler { color: #f0883e; background: rgba(240,136,62,0.1); }
928
+ .log-src-system { color: #8b949e; background: rgba(139,148,158,0.1); }
929
+ .log-msg { flex: 1; white-space: pre-wrap; word-break: break-all; }
930
+ .log-msg.log-error { color: #f85149; }
931
+ .log-msg.log-warn { color: #d29922; }
932
+ .log-msg.log-info { color: #c9d1d9; }
933
+
934
+ .scheduler-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
935
+ .scheduler-view.hidden { display: none; }
936
+ .scheduler-content { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 16px; }
937
+ .scheduler-form { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
938
+ .scheduler-form label { font-size: 11px; color: #8b949e; margin-bottom: 2px; display: block; }
939
+ .scheduler-form input, .scheduler-form textarea, .scheduler-form select {
940
+ width: 100%; padding: 8px 10px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
941
+ color: #c9d1d9; font-size: 13px; outline: none; box-sizing: border-box; font-family: inherit;
942
+ }
943
+ .scheduler-form input:focus, .scheduler-form textarea:focus { border-color: #58a6ff; }
944
+ .scheduler-form textarea { resize: vertical; min-height: 60px; }
945
+ .schedule-card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; }
946
+ .schedule-card.disabled { opacity: 0.5; }
947
+ .schedule-card-header { display: flex; justify-content: space-between; align-items: center; }
948
+ .schedule-card-id { font-weight: 600; color: #e6edf3; font-size: 14px; }
949
+ .schedule-cron { background: rgba(88,166,255,0.1); color: #58a6ff; padding: 2px 8px; border-radius: 4px; font-family: monospace; font-size: 11px; }
950
+ .schedule-prompt { color: #c9d1d9; font-size: 13px; margin: 8px 0; white-space: pre-wrap; word-break: break-word; max-height: 60px; overflow: hidden; }
951
+ .schedule-meta { font-size: 11px; color: #8b949e; display: flex; gap: 12px; align-items: center; }
952
+ .schedule-meta .status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin-right: 4px; }
953
+ .schedule-meta .status-dot.success { background: #3fb950; }
954
+ .schedule-meta .status-dot.error { background: #f85149; }
955
+ .schedule-actions { display: flex; gap: 6px; margin-top: 8px; }
956
+ .schedule-actions button {
957
+ padding: 4px 10px; border-radius: 4px; border: 1px solid #30363d; background: #21262d;
958
+ color: #c9d1d9; font-size: 11px; cursor: pointer;
959
+ }
960
+ .schedule-actions button:hover { border-color: #58a6ff; color: #58a6ff; }
961
+ .schedule-actions button.danger:hover { border-color: #f85149; color: #f85149; }
962
+ .schedule-toggle { position: relative; width: 36px; height: 20px; cursor: pointer; }
963
+ .schedule-toggle input { display: none; }
964
+ .schedule-toggle .slider { position: absolute; inset: 0; background: #30363d; border-radius: 10px; transition: 0.2s; }
965
+ .schedule-toggle .slider::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; bottom: 3px; background: #8b949e; border-radius: 50%; transition: 0.2s; }
966
+ .schedule-toggle input:checked + .slider { background: #238636; }
967
+ .schedule-toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
968
+
969
+ /* Communication view */
970
+ .comms-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
971
+ .comms-view.hidden { display: none; }
972
+ .settings-view { display: flex; flex: 1; overflow-y: auto; padding: 16px; }
973
+ .settings-view.hidden { display: none !important; }
974
+ .comms-header {
975
+ display: flex; flex-direction: column; gap: 4px;
976
+ padding: 16px 20px; border-bottom: 1px solid #21262d;
977
+ }
978
+ .comms-header h2 { font-size: 15px; font-weight: 600; font-family: 'IBM Plex Mono', monospace; color: #e6edf3; margin: 0; }
979
+ .comms-subtitle { font-size: 12px; color: #484f58; }
980
+ .comms-content {
981
+ flex: 1; overflow-y: auto; padding: 20px;
982
+ display: grid;
983
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
984
+ gap: 16px;
985
+ align-content: start;
986
+ }
987
+ .comms-card {
988
+ background: #161b22; border: 1px solid #21262d; border-radius: 12px; padding: 24px;
989
+ display: flex; flex-direction: column; gap: 16px;
990
+ transition: border-color 0.2s ease;
991
+ }
992
+ .comms-card:hover { border-color: #30363d; }
993
+ .comms-card.telegram { border-top: 2px solid #2AABEE; }
994
+ .comms-card.whatsapp { border-top: 2px solid #25D366; }
995
+ .comms-card.phone { border-top: 2px solid #f44336; }
996
+ .comms-card-top { display: flex; align-items: flex-start; gap: 14px; }
997
+ .comms-card-icon {
998
+ width: 48px; height: 48px; border-radius: 12px; background: #0d1117;
999
+ display: flex; align-items: center; justify-content: center; flex-shrink: 0;
1000
+ color: #58a6ff;
1001
+ }
1002
+ .comms-card-icon svg { width: 28px; height: 28px; }
1003
+ .comms-card-icon.telegram { color: #2AABEE; background: rgba(42,171,238,0.08); }
1004
+ .comms-card-icon.whatsapp { color: #25D366; background: rgba(37,211,102,0.08); }
1005
+ .comms-card-icon.phone { color: #f44336; background: rgba(244,67,54,0.08); }
1006
+ .phone-url-box {
1007
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px;
1008
+ background: #0d1117; border: 1px solid #21262d; border-radius: 8px; font-size: 12px;
1009
+ }
1010
+ .phone-url-box code {
1011
+ flex: 1; color: #e6edf3; font-family: 'IBM Plex Mono', monospace; word-break: break-all;
1012
+ font-size: 11px;
1013
+ }
1014
+ .phone-url-box .copy-btn {
1015
+ background: none; border: 1px solid #30363d; color: #8b949e; border-radius: 6px;
1016
+ padding: 4px 10px; cursor: pointer; font-size: 11px; white-space: nowrap;
1017
+ }
1018
+ .phone-url-box .copy-btn:hover { color: #e6edf3; border-color: #58a6ff; }
1019
+ .phone-url-box .copy-btn.copied { color: #3fb950; border-color: #3fb950; }
1020
+ .conv-msg.whatsapp { border-left: 2px solid #25D366; padding-left: 8px; }
1021
+ .conv-msg.whatsapp .label.wa { color: #25D366; }
1022
+ .comms-status.scanning { background: rgba(210,153,34,0.12); color: #d29922; }
1023
+ .wa-qr-container { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 10px; }
1024
+ .wa-qr-container canvas { border-radius: 8px; background: #fff; padding: 8px; }
1025
+ .wa-qr-hint { font-size: 10px; color: #8b949e; text-align: center; }
1026
+ .wa-clear-auth { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #8b949e; }
1027
+ .wa-clear-auth input { accent-color: #f85149; }
1028
+ .comms-card-info { display: flex; flex-direction: column; gap: 3px; flex: 1; }
1029
+ .comms-card-name { font-size: 14px; font-weight: 600; color: #e6edf3; }
1030
+ .comms-card-desc { font-size: 11px; color: #8b949e; line-height: 1.5; }
1031
+ .comms-status {
1032
+ font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px;
1033
+ flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.5px;
1034
+ }
1035
+ .comms-status.connected { background: rgba(63,185,80,0.12); color: #3fb950; }
1036
+ .comms-status.disconnected { background: rgba(139,148,158,0.1); color: #484f58; }
1037
+ .comms-status.error { background: rgba(248,81,73,0.12); color: #f85149; }
1038
+ .comms-card-config { display: flex; flex-direction: column; gap: 12px; }
1039
+ .comms-field { display: flex; flex-direction: column; gap: 5px; }
1040
+ .comms-field label {
1041
+ font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase;
1042
+ letter-spacing: 0.3px;
1043
+ }
1044
+ .comms-field input {
1045
+ background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 8px 12px;
1046
+ color: #e6edf3; font-family: 'IBM Plex Mono', monospace; font-size: 12px;
1047
+ outline: none; transition: border-color 0.15s;
1048
+ }
1049
+ .comms-field input:focus { border-color: #58a6ff; }
1050
+ .comms-field input::placeholder { color: #30363d; }
1051
+ .comms-hint { font-size: 10px; color: #484f58; }
1052
+ .comms-hint a { color: #58a6ff; text-decoration: none; }
1053
+ .comms-hint a:hover { text-decoration: underline; }
1054
+ .comms-card-actions { display: flex; gap: 8px; }
1055
+ .comms-card.is-connected .comms-card-config { display: none; }
1056
+ .comms-bot-info {
1057
+ display: flex; align-items: center; gap: 10px; padding: 10px 14px;
1058
+ background: rgba(63,185,80,0.06); border: 1px solid #23612c; border-radius: 8px;
1059
+ }
1060
+ .comms-bot-info .bot-name { font-size: 13px; font-weight: 600; color: #3fb950; }
1061
+ .comms-bot-info .bot-username { font-size: 11px; color: #8b949e; }
1062
+
1063
+ @keyframes fade-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
1064
+ @media (max-width: 700px) {
1065
+ /* ── Header ── */
1066
+ .header { padding: 0 8px; height: 36px; min-height: 36px; }
1067
+ .header-left { gap: 6px; flex: 1; min-width: 0; }
1068
+ .header-left h1 { display: none; }
1069
+ .header-right { gap: 8px; }
1070
+ .audit-toggle span:last-child { display: none; }
1071
+ .audit-toggle { padding: 4px 6px; }
1072
+
1073
+ /* ── Tabs — scrollable strip ── */
1074
+ .view-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; flex-shrink: 1; min-width: 0; scrollbar-width: none; }
1075
+ .view-tabs::-webkit-scrollbar { display: none; }
1076
+ .view-tab { padding: 3px 10px; font-size: 10px; white-space: nowrap; flex-shrink: 0; }
1077
+
1078
+ /* ── Chat layout — stack vertically ── */
1079
+ .chat-view { flex-direction: column; }
1080
+ .panel-cam { width: 100%; min-width: 0; border-right: none; border-bottom: 1px solid #21262d; padding: 10px; gap: 10px; }
1081
+ .camera-container { aspect-ratio: 16/9; }
1082
+
1083
+ /* ── Controls — larger touch targets ── */
1084
+ .controls { gap: 6px; }
1085
+ .ctrl-btn { min-height: 44px; padding: 10px 12px; font-size: 12px; }
1086
+ .speaker-toggle { min-height: 44px; font-size: 12px; }
1087
+
1088
+ /* ── Vitals — compact ── */
1089
+ .agent-vitals { font-size: 10px; }
1090
+ .vital-value { font-size: 14px; }
1091
+ .vital-label { font-size: 7px; }
1092
+ .vitals-wave { height: 28px; }
1093
+
1094
+ /* ── Conversation ── */
1095
+ .conversation { padding: 10px 12px; font-size: 12px; }
1096
+
1097
+ /* ── Input bar — full width, tappable ── */
1098
+ .input-bar { padding: 8px 10px; gap: 6px; }
1099
+ .input-bar input { min-height: 44px; font-size: 14px; padding: 10px 12px; }
1100
+ .input-bar button { min-height: 44px; padding: 10px 14px; }
1101
+
1102
+ /* ── Chat sidebar — overlay when expanded ── */
1103
+ .chat-sidebar { position: absolute; left: 0; top: 0; bottom: 0; z-index: 60; width: 260px; min-width: 260px; }
1104
+ .chat-sidebar.collapsed { width: 0; min-width: 0; }
1105
+
1106
+ /* ── File sidebar / Files panel ── */
1107
+ .activity-bar { display: none; }
1108
+ .file-sidebar { display: none; }
1109
+ .files-panel { position: absolute; right: 0; top: 0; bottom: 0; z-index: 50; width: 280px; min-width: 280px; }
1110
+
1111
+ /* ── SkillFlows — stack panels ── */
1112
+ .flows-view { flex-direction: column; }
1113
+ .flows-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; border-bottom: 1px solid #21262d; }
1114
+
1115
+ /* ── Scheduler — stack panels ── */
1116
+ .scheduler-view { flex-direction: column; }
1117
+ .scheduler-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; }
1118
+
1119
+ /* ── Comms — stack panels ── */
1120
+ .comms-view { flex-direction: column; overflow-y: auto; }
1121
+ .comms-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; }
1122
+
1123
+ /* ── Logs — stack toolbar ── */
1124
+ .logs-toolbar { flex-direction: column; gap: 6px; }
1125
+ .logs-filters { flex-wrap: wrap; }
1126
+
1127
+ /* ── Settings — already responsive via max-width ── */
1128
+ .settings-view { padding: 12px; }
1129
+ }
1130
+ </style>
1131
+ </head>
1132
+ <body>
1133
+ <div class="header">
1134
+ <div class="header-left">
1135
+ <div class="header-logo" id="headerLogo"></div>
1136
+ <h1>Gitagent: {{AGENT_NAME}}</h1>
1137
+ <button class="chat-sidebar-toggle" id="chatSidebarToggle" onclick="toggleChatSidebar()" title="Toggle chat list">
1138
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
1139
+ </button>
1140
+ <div class="view-tabs">
1141
+ <button class="view-tab active" id="tabChat" onclick="switchView('chat')">Chat</button>
1142
+ <!-- <button class="view-tab" id="tabFiles" onclick="switchView('files')">Files</button> -->
1143
+ <button class="view-tab" id="tabSkills" onclick="switchView('skills')">Skills</button>
1144
+ <button class="view-tab" id="tabIntegrations" onclick="switchView('integrations')">Integrations</button>
1145
+ <button class="view-tab" id="tabComms" onclick="switchView('comms')">Communication</button>
1146
+ <button class="view-tab" id="tabFlows" onclick="switchView('flows')">SkillFlows</button>
1147
+ <button class="view-tab" id="tabScheduler" onclick="switchView('scheduler')">Scheduler</button>
1148
+ <button class="view-tab" id="tabLogs" onclick="switchView('logs')">Logs</button>
1149
+ <button class="view-tab" id="tabSettings" onclick="switchView('settings')">Settings</button>
1150
+ </div>
1151
+ </div>
1152
+ <div class="header-right">
1153
+ <button class="audit-toggle active recording" id="auditToggle" onclick="toggleAuditMode()" title="File System: watch agent file changes live">
1154
+ <span class="audit-dot"></span>
1155
+ <span>File System</span>
1156
+ </button>
1157
+ <span class="status-dot" id="statusDot"></span>
1158
+ <span class="status-text" id="statusText">Disconnected</span>
1159
+ </div>
1160
+ </div>
1161
+ <div class="main" style="position:relative;">
1162
+ <button class="chat-sidebar-edge-tab visible" id="chatEdgeTab" onclick="toggleChatSidebar()" title="Show chats">
1163
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
1164
+ </button>
1165
+ <div class="chat-sidebar collapsed" id="chatSidebar">
1166
+ <div class="chat-sidebar-header">
1167
+ <button class="chat-sidebar-collapse" onclick="toggleChatSidebar()" title="Collapse chats">
1168
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
1169
+ </button>
1170
+ <span>Chats</span>
1171
+ <button class="new-chat-btn" onclick="newChat()">+ New</button>
1172
+ </div>
1173
+ <div class="chat-list" id="chatList"></div>
1174
+ <div class="branch-indicator" id="branchIndicator">
1175
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
1176
+ <span class="branch-name" id="branchName">main</span>
1177
+ </div>
1178
+ </div>
1179
+ <div class="chat-view" id="chatView">
1180
+ <div id="voiceWarning" style="display:none;padding:8px 14px;margin:8px 8px 0;background:rgba(210,153,34,0.12);border:1px solid rgba(210,153,34,0.3);border-radius:6px;color:#d29922;font-size:12px;">
1181
+ Voice mode unavailable — no API key set. Use the <a href="#" onclick="switchView('settings');return false;" style="color:#58a6ff;text-decoration:underline;">Settings</a> tab to add your key. Text chat works normally.
1182
+ </div>
1183
+ <div class="panel-cam">
1184
+ <div class="camera-container">
1185
+ <div class="camera-off" id="cameraOff">Camera off</div>
1186
+ <video id="cameraVideo" autoplay playsinline muted style="display:none;"></video>
1187
+ <canvas id="cameraCanvas"></canvas>
1188
+ </div>
1189
+ <div class="controls">
1190
+ <button class="ctrl-btn" id="cameraBtn" onclick="toggleCamera()">
1191
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.934a.5.5 0 0 0-.777-.416L16 11"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>
1192
+ Camera
1193
+ </button>
1194
+ <button class="ctrl-btn" id="flipCamBtn" onclick="flipCamera()" title="Switch front/back camera" style="flex:0;padding:10px;">
1195
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
1196
+ </button>
1197
+ <button class="ctrl-btn" id="screenBtn" onclick="toggleScreen()">
1198
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
1199
+ Screen
1200
+ </button>
1201
+ <button class="ctrl-btn" id="micBtn" onclick="toggleMic()">
1202
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
1203
+ Mic
1204
+ </button>
1205
+ <button class="ctrl-btn active" id="muteBtn" onclick="toggleMute()">
1206
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
1207
+ Speaker
1208
+ </button>
1209
+ </div>
1210
+ <div class="agent-vitals" id="agentVitals">
1211
+ <div class="vitals-header">
1212
+ <span class="vitals-title">Agent Vitals</span>
1213
+ <span class="vitals-status-dot" id="vitalsDot"></span>
1214
+ </div>
1215
+ <div class="vitals-grid">
1216
+ <div class="vital-cell cpu">
1217
+ <span class="vital-label">CPU</span>
1218
+ <span class="vital-value" id="vitalCpu">0<span class="vital-unit">%</span></span>
1219
+ <div class="vital-bar"><div class="vital-bar-fill" id="vitalCpuBar" style="width:0%"></div></div>
1220
+ </div>
1221
+ <div class="vital-cell mem">
1222
+ <span class="vital-label">Memory</span>
1223
+ <span class="vital-value" id="vitalMem">0<span class="vital-unit">MB</span></span>
1224
+ <div class="vital-bar"><div class="vital-bar-fill" id="vitalMemBar" style="width:0%"></div></div>
1225
+ </div>
1226
+ <div class="vital-cell tokens">
1227
+ <span class="vital-label">Tokens</span>
1228
+ <span class="vital-value" id="vitalTokens">0<span class="vital-unit">tok</span></span>
1229
+ <div class="vital-bar"><div class="vital-bar-fill" id="vitalTokensBar" style="width:0%"></div></div>
1230
+ </div>
1231
+ <div class="vital-cell uptime">
1232
+ <span class="vital-label">Uptime</span>
1233
+ <span class="vital-value" id="vitalUptime">00:00</span>
1234
+ </div>
1235
+ <div class="vital-cell wide">
1236
+ <div class="vitals-wave">
1237
+ <span class="vitals-wave-label">Pulse</span>
1238
+ <canvas id="vitalsWaveCanvas"></canvas>
1239
+ </div>
1240
+ </div>
1241
+ </div>
1242
+ </div>
1243
+ </div>
1244
+ <div class="panel-right">
1245
+ <div class="conversation" id="conversation"></div>
1246
+ <div class="file-preview-bar" id="filePreviewBar"></div>
1247
+ <div class="input-bar">
1248
+ <button class="attach-btn" onclick="document.getElementById('fileInput').click()" title="Attach files">
1249
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
1250
+ </button>
1251
+ <input type="file" id="fileInput" multiple style="display:none" onchange="handleFileSelect(this.files)" />
1252
+ <input type="text" id="textInput" placeholder="Type a message or drop files..." autocomplete="off" />
1253
+ <button onclick="sendText()">Send</button>
1254
+ </div>
1255
+ <div class="drop-overlay" id="dropOverlay">Drop files here</div>
1256
+ </div>
1257
+ <div class="files-panel" id="filesPanel">
1258
+ <div class="resize-handle-x" id="fpResizeX"></div>
1259
+ <div class="files-panel-header">
1260
+ <span class="fp-title">Files <span class="fp-count hidden" id="fpCount">0</span></span>
1261
+ <button onclick="toggleAuditMode()">&times;</button>
1262
+ </div>
1263
+ <div class="file-tree" id="explorerTree"></div>
1264
+ <div class="resize-handle-y" id="dvResizeY"></div>
1265
+ <div class="diff-viewer" id="diffViewer">
1266
+ <div class="diff-viewer-header">
1267
+ <div class="dv-left">
1268
+ <span class="dv-path" id="dvPath"></span>
1269
+ <span class="dv-status viewing" id="dvStatus">VIEWING</span>
1270
+ </div>
1271
+ <button class="dv-md-toggle hidden" id="dvMdToggle" onclick="toggleMdView()" title="Toggle markdown preview">
1272
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h20v18H2z"/><path d="M7 15V9l2.5 3L12 9v6"/><path d="M17 9v6l-2-3"/></svg>
1273
+ </button>
1274
+ <button class="dv-dl-btn hidden" id="dvDownload" onclick="downloadCurrentFile()" title="Download">Download</button>
1275
+ <button onclick="closeDiffViewer()" title="Close">&times;</button>
1276
+ </div>
1277
+ <div class="dv-countdown" id="dvCountdown" style="width:100%"></div>
1278
+ <div class="diff-viewer-content">
1279
+ <pre id="dvPre"></pre>
1280
+ <div class="dv-markdown hidden" id="dvMarkdown"></div>
1281
+ <div class="dv-image hidden" id="dvImage"></div>
1282
+ <iframe id="dvHtml" class="dv-html hidden" sandbox="allow-scripts allow-forms allow-popups allow-modals" referrerpolicy="no-referrer"></iframe>
1283
+ <iframe id="dvPdf" class="dv-pdf hidden"></iframe>
1284
+ <div class="dv-video hidden" id="dvVideo"></div>
1285
+ <div class="dv-audio hidden" id="dvAudio"></div>
1286
+ <div class="dv-binary hidden" id="dvBinary"></div>
1287
+ </div>
1288
+ </div>
1289
+ </div>
1290
+ </div>
1291
+ <!-- FILES VIEW (disabled)
1292
+ <div class="files-view hidden" id="filesView">
1293
+ <div class="activity-bar">
1294
+ <button class="activity-btn active" title="Explorer">
1295
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
1296
+ </button>
1297
+ <button class="activity-btn" title="Search">
1298
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
1299
+ </button>
1300
+ </div>
1301
+ <div class="file-sidebar">
1302
+ <div class="file-sidebar-header"><span>Explorer</span><button onclick="loadFileTree()">Refresh</button></div>
1303
+ <div class="file-tree" id="fileTree"><div style="padding:16px;color:#484f58;font-size:12px;">Loading...</div></div>
1304
+ </div>
1305
+ <div class="editor-area">
1306
+ <div class="editor-tabs" id="editorTabs"></div>
1307
+ <div class="editor-container" id="editorContainer">
1308
+ <div class="editor-empty" id="editorEmpty">
1309
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
1310
+ <p>Select a file to start editing</p>
1311
+ <div class="shortcuts"><span><kbd>Cmd+S</kbd> Save</span><span><kbd>Cmd+W</kbd> Close tab</span></div>
1312
+ </div>
1313
+ </div>
1314
+ <div class="status-bar">
1315
+ <div id="sbBranch"></div>
1316
+ <div class="sb-right"><div id="sbLang"></div><div>UTF-8</div></div>
1317
+ </div>
1318
+ </div>
1319
+ </div>
1320
+ -->
1321
+ <div class="integrations-view hidden" id="integrationsView">
1322
+ <div class="integrations-header">
1323
+ <h2>Integrations</h2>
1324
+ <button onclick="loadToolkits()">Refresh</button>
1325
+ </div>
1326
+ <div class="integrations-grid" id="integrationsGrid">
1327
+ <div class="integrations-empty" id="integrationsEmpty">Loading...</div>
1328
+ </div>
1329
+ </div>
1330
+ <div class="comms-view hidden" id="commsView">
1331
+ <div class="comms-header">
1332
+ <h2>Communication</h2>
1333
+ <span class="comms-subtitle">Connect messaging channels so your agent can send and receive messages</span>
1334
+ </div>
1335
+ <div class="comms-content">
1336
+ <div class="comms-card telegram" id="telegramCard">
1337
+ <div class="comms-card-top">
1338
+ <div class="comms-card-icon">
1339
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.28-.02-.12.03-2.02 1.28-5.69 3.77-.54.37-1.03.55-1.47.54-.48-.01-1.41-.27-2.1-.5-.85-.28-1.52-.43-1.46-.91.03-.25.38-.51 1.05-.78 4.12-1.79 6.87-2.97 8.26-3.54 3.93-1.62 4.75-1.9 5.28-1.91.12 0 .37.03.54.17.14.12.18.28.2.47-.01.06.01.24 0 .37z"/></svg>
1340
+ </div>
1341
+ <div class="comms-card-info">
1342
+ <span class="comms-card-name">Telegram</span>
1343
+ <span class="comms-card-desc">Connect a Telegram bot to chat with your agent from anywhere</span>
1344
+ </div>
1345
+ <span class="comms-status" id="tgStatus"></span>
1346
+ </div>
1347
+ <div class="comms-card-config" id="tgConfig">
1348
+ <div class="comms-field">
1349
+ <label>Bot Token</label>
1350
+ <input type="password" id="tgToken" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v..." spellcheck="false" autocomplete="off" />
1351
+ <span class="comms-hint">Get one from <a href="https://t.me/BotFather" target="_blank">@BotFather</a> on Telegram</span>
1352
+ </div>
1353
+ <div class="comms-field">
1354
+ <label>Allowed Users</label>
1355
+ <input type="text" id="tgAllowedUsers" placeholder="@username1, @username2" spellcheck="false" autocomplete="off" />
1356
+ <span class="comms-hint">Comma-separated usernames. Use <code>*</code> to allow everyone. Empty = block all.</span>
1357
+ </div>
1358
+ </div>
1359
+ <div class="comms-card-security" id="tgSecurity" style="display:none;">
1360
+ <div class="comms-field">
1361
+ <label>Allowed Users</label>
1362
+ <div style="display:flex;gap:8px;align-items:center;">
1363
+ <input type="text" id="tgAllowedUsersLive" placeholder="@username1, @username2" spellcheck="false" autocomplete="off" style="flex:1;" />
1364
+ <button class="tk-btn connect" onclick="saveTgAllowedUsers()" style="padding:6px 14px;font-size:12px;">Save</button>
1365
+ </div>
1366
+ <span class="comms-hint">Comma-separated usernames. Use <code>*</code> to allow everyone. Empty = block all.</span>
1367
+ </div>
1368
+ </div>
1369
+ <div class="comms-card-actions">
1370
+ <button class="tk-btn connect" id="tgConnectBtn" onclick="toggleTelegram()">Connect</button>
1371
+ </div>
1372
+ </div>
1373
+
1374
+ <!-- WhatsApp Card -->
1375
+ <div class="comms-card whatsapp" id="whatsappCard">
1376
+ <div class="comms-card-top">
1377
+ <div class="comms-card-icon whatsapp">
1378
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
1379
+ </div>
1380
+ <div class="comms-card-info">
1381
+ <span class="comms-card-name">WhatsApp</span>
1382
+ <span class="comms-card-desc">Send messages to yourself on WhatsApp and get agent responses</span>
1383
+ </div>
1384
+ <span class="comms-status" id="waStatus"></span>
1385
+ </div>
1386
+ <div class="comms-card-config" id="waConfig">
1387
+ <div class="wa-qr-container" id="waQrContainer" style="display:none;">
1388
+ <canvas id="waQrCanvas" width="256" height="256"></canvas>
1389
+ <span class="wa-qr-hint">Scan with WhatsApp &rarr; Settings &rarr; Linked Devices</span>
1390
+ </div>
1391
+ </div>
1392
+ <div class="comms-card-actions">
1393
+ <label class="wa-clear-auth" id="waClearLabel" style="display:none;">
1394
+ <input type="checkbox" id="waClearAuth" /> Clear session on disconnect
1395
+ </label>
1396
+ <button class="tk-btn connect" id="waConnectBtn" onclick="toggleWhatsApp()">Connect</button>
1397
+ </div>
1398
+ </div>
1399
+
1400
+ <!-- Phone Number / Twilio Card -->
1401
+ <div class="comms-card phone" id="phoneCard">
1402
+ <div class="comms-card-top">
1403
+ <div class="comms-card-icon phone">
1404
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/></svg>
1405
+ </div>
1406
+ <div class="comms-card-info">
1407
+ <span class="comms-card-name">Phone Number</span>
1408
+ <span class="comms-card-desc">Receive SMS via Twilio webhook — set this URL in your Twilio phone number config</span>
1409
+ </div>
1410
+ </div>
1411
+ <div class="comms-card-config">
1412
+ <div class="phone-url-box">
1413
+ <code id="phoneWebhookUrl"></code>
1414
+ <button class="copy-btn" onclick="copyWebhookUrl(this)">Copy</button>
1415
+ </div>
1416
+ <span style="font-size:10px; color:#484f58; margin-top:4px; display:block;">
1417
+ Paste this URL in Twilio &rarr; Phone Numbers &rarr; your number &rarr; Messaging &rarr; "A message comes in" webhook
1418
+ </span>
1419
+ </div>
1420
+ </div>
1421
+ </div>
1422
+ </div>
1423
+ <div class="flows-view hidden" id="flowsView">
1424
+ <div class="flows-skills-panel">
1425
+ <input id="flowSkillsSearch" type="text" placeholder="Search skills..." oninput="filterFlowSkills()" style="width:100%;padding:6px 8px;margin-bottom:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:12px;outline:none;box-sizing:border-box;">
1426
+ <div id="flowSkillsList" class="flows-skill-tree"></div>
1427
+ </div>
1428
+ <div style="flex:1;display:flex;flex-direction:column;overflow-y:auto;padding:16px;">
1429
+ <div style="display:flex;gap:12px;margin-bottom:16px;align-items:flex-end;flex-shrink:0;">
1430
+ <div style="flex:1;">
1431
+ <label style="display:block;font-size:11px;color:#8b949e;margin-bottom:4px;">Flow Name (kebab-case)</label>
1432
+ <input id="flowNameInput" type="text" placeholder="my-flow-name" pattern="[a-z0-9]+(-[a-z0-9]+)*" style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:14px;outline:none;">
1433
+ </div>
1434
+ <div style="flex:2;">
1435
+ <label style="display:block;font-size:11px;color:#8b949e;margin-bottom:4px;">Description</label>
1436
+ <input id="flowDescInput" type="text" placeholder="What this flow does..." style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:14px;outline:none;">
1437
+ </div>
1438
+ <button onclick="saveFlow()" style="padding:8px 16px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;white-space:nowrap;">Save Flow</button>
1439
+ <button onclick="clearFlow()" style="padding:8px 16px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;cursor:pointer;font-size:13px;white-space:nowrap;">Clear</button>
1440
+ </div>
1441
+ <div style="flex-shrink:0;overflow-x:auto;overflow-y:hidden;margin-bottom:16px;">
1442
+ <div id="flowStepsContainer" style="display:flex;align-items:flex-start;gap:0;min-height:160px;padding:8px 0;">
1443
+ <div style="display:flex;align-items:center;justify-content:center;width:100%;color:#484f58;font-size:14px;font-style:italic;" id="flowEmptyMsg">Click a skill from the panel to add steps</div>
1444
+ </div>
1445
+ </div>
1446
+ <div style="flex-shrink:0;border-top:1px solid #21262d;padding-top:12px;">
1447
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
1448
+ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Saved Flows</h3>
1449
+ <button onclick="clearFlow();document.getElementById('flowNameInput').focus();" style="padding:4px 12px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:12px;white-space:nowrap;">+ New Workflow</button>
1450
+ </div>
1451
+ <div id="savedFlowsList" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;"></div>
1452
+ </div>
1453
+ </div>
1454
+ </div>
1455
+ <div class="scheduler-view hidden" id="schedulerView">
1456
+ <div class="scheduler-content">
1457
+ <div class="scheduler-form">
1458
+ <div style="display:flex;gap:12px;">
1459
+ <div style="flex:1;">
1460
+ <label>Job ID (kebab-case)</label>
1461
+ <input id="scheduleId" type="text" placeholder="daily-standup">
1462
+ </div>
1463
+ <div style="flex:1;">
1464
+ <label>Mode</label>
1465
+ <div style="display:flex;gap:6px;">
1466
+ <button id="scheduleModeRepeat" onclick="setScheduleMode('repeat')" class="schedule-mode-btn active" style="flex:1;padding:8px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:12px;">Repeat</button>
1467
+ <button id="scheduleModeOnce" onclick="setScheduleMode('once')" class="schedule-mode-btn" style="flex:1;padding:8px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#8b949e;cursor:pointer;font-size:12px;">Run Once</button>
1468
+ </div>
1469
+ </div>
1470
+ </div>
1471
+ <div id="scheduleCronRow">
1472
+ <label>Cron Expression</label>
1473
+ <div style="display:flex;gap:6px;">
1474
+ <input id="scheduleCron" type="text" placeholder="0 9 * * 1-5" style="flex:1;">
1475
+ <select id="scheduleCronPreset" onchange="applySchedulePreset()" style="width:auto;min-width:120px;">
1476
+ <option value="">Presets...</option>
1477
+ <option value="0 9 * * *">Daily 9am</option>
1478
+ <option value="0 9 * * 1-5">Weekdays 9am</option>
1479
+ <option value="0 */6 * * *">Every 6 hours</option>
1480
+ <option value="*/30 * * * *">Every 30 min</option>
1481
+ <option value="0 9 * * 1">Weekly Monday</option>
1482
+ <option value="*/1 * * * *">Every minute (test)</option>
1483
+ </select>
1484
+ </div>
1485
+ <div style="font-size:10px;color:#484f58;margin-top:4px;">Standard 5-field cron: min hour day month weekday</div>
1486
+ </div>
1487
+ <div id="scheduleRunAtRow" style="display:none;">
1488
+ <label>Run At (date &amp; time)</label>
1489
+ <input id="scheduleRunAt" type="datetime-local" style="max-width:280px;">
1490
+ <div style="font-size:10px;color:#484f58;margin-top:4px;">Job will run once at this time and then auto-disable</div>
1491
+ </div>
1492
+ <div>
1493
+ <label>Prompt</label>
1494
+ <textarea id="schedulePrompt" rows="3" placeholder="Summarize git commits from the last 24 hours"></textarea>
1495
+ </div>
1496
+ <div style="display:flex;gap:8px;">
1497
+ <button onclick="saveScheduleJob()" style="padding:8px 16px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;">Save Schedule</button>
1498
+ <button onclick="clearScheduleForm()" style="padding:8px 16px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;cursor:pointer;font-size:13px;">Clear</button>
1499
+ </div>
1500
+ </div>
1501
+ <div id="schedulesList"></div>
1502
+ </div>
1503
+ </div>
1504
+ <div class="logs-view hidden" id="logsView">
1505
+ <div class="logs-toolbar">
1506
+ <div class="logs-filters">
1507
+ <select id="logSourceFilter" onchange="applyLogFilters()">
1508
+ <option value="">All Sources</option>
1509
+ <option value="voice">Voice</option>
1510
+ <option value="telegram">Telegram</option>
1511
+ <option value="whatsapp">WhatsApp</option>
1512
+ <option value="triggers">Triggers</option>
1513
+ <option value="sdk">SDK</option>
1514
+ <option value="scheduler">Scheduler</option>
1515
+ <option value="system">System</option>
1516
+ </select>
1517
+ <select id="logLevelFilter" onchange="applyLogFilters()">
1518
+ <option value="">All Levels</option>
1519
+ <option value="info">Info</option>
1520
+ <option value="warn">Warn</option>
1521
+ <option value="error">Error</option>
1522
+ </select>
1523
+ <input id="logSearchInput" type="text" placeholder="Search logs..." oninput="applyLogFilters()">
1524
+ </div>
1525
+ <div class="logs-actions">
1526
+ <label class="logs-autoscroll-label"><input type="checkbox" id="logAutoScroll" checked> Auto-scroll</label>
1527
+ <button onclick="clearLogView()">Clear</button>
1528
+ </div>
1529
+ </div>
1530
+ <div class="logs-output" id="logsOutput"></div>
1531
+ </div>
1532
+ <div class="settings-view hidden" id="settingsView">
1533
+ <div style="max-width:560px;width:100%;margin:0 auto;">
1534
+ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
1535
+ <div id="settingsStatus" style="display:none;padding:10px 14px;border-radius:6px;margin-bottom:16px;font-size:13px;"></div>
1536
+ <div style="display:flex;flex-direction:column;gap:14px;">
1537
+ <div>
1538
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Agent Model</label>
1539
+ <select id="settingsModel" style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;">
1540
+ <option value="">Loading...</option>
1541
+ </select>
1542
+ </div>
1543
+ <div>
1544
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Custom Model <span style="color:#484f58;">(provider:model-id)</span></label>
1545
+ <input id="settingsCustomModel" type="text" placeholder="e.g. ollama:llama3 or ollama:llama3@http://localhost:11434/v1" style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1546
+ </div>
1547
+ <div>
1548
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Custom Base URL <span style="color:#484f58;">(for OpenAI-compatible endpoints — Ollama, LM Studio, vLLM, etc.)</span></label>
1549
+ <input id="settingsBaseUrl" type="text" placeholder="e.g. http://localhost:11434/v1" style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1550
+ </div>
1551
+ <hr style="border:none;border-top:1px solid #21262d;margin:4px 0;">
1552
+ <div>
1553
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">OpenAI API Key <span style="color:#484f58;">(voice)</span></label>
1554
+ <input id="settingsOpenaiKey" type="password" placeholder="sk-..." style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1555
+ </div>
1556
+ <div>
1557
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Anthropic API Key <span style="color:#484f58;">(agent brain)</span></label>
1558
+ <input id="settingsAnthropicKey" type="password" placeholder="sk-ant-..." style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1559
+ </div>
1560
+ <div>
1561
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Gemini API Key <span style="color:#484f58;">(optional)</span></label>
1562
+ <input id="settingsGeminiKey" type="password" placeholder="AI..." style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1563
+ </div>
1564
+ <div>
1565
+ <label style="display:block;color:#8b949e;font-size:12px;margin-bottom:4px;">Composio API Key <span style="color:#484f58;">(optional — Gmail, Calendar, Slack)</span></label>
1566
+ <input id="settingsComposioKey" type="password" placeholder="ak_..." style="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box;">
1567
+ </div>
1568
+ <hr style="border:none;border-top:1px solid #21262d;margin:4px 0;">
1569
+ <div style="display:flex;gap:8px;align-items:center;">
1570
+ <button onclick="saveSettings()" style="padding:8px 20px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;">Save</button>
1571
+ <span id="settingsSaving" style="display:none;color:#8b949e;font-size:12px;">Saving...</span>
1572
+ </div>
1573
+ <p style="color:#484f58;font-size:11px;margin:0;">Saves to .env and agent.yaml in your agent directory. Changes take effect on the next query.</p>
1574
+ </div>
1575
+ </div>
1576
+ </div>
1577
+ <div class="skills-view hidden" id="skillsView" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
1578
+ <iframe id="skillsFrame" src="" style="flex:1;border:none;background:#0d1117;width:100%;"></iframe>
1579
+ </div>
1580
+ </div>
1581
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
1582
+ <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
1583
+ <!-- <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script> -->
1584
+ <script>
1585
+ var hasComposio={{HAS_COMPOSIO}};
1586
+ if(!hasComposio){document.getElementById('tabIntegrations').style.display='none';}
1587
+ let currentView='chat',filesLoaded=false,integrationsLoaded=false,commsLoaded=false,skillsLoaded=false,flowsLoaded=false,schedulerLoaded=false,logsLoaded=false,settingsLoaded=false;
1588
+ function switchView(v){
1589
+ currentView=v;
1590
+ document.getElementById('chatView').classList.toggle('hidden',v!=='chat');
1591
+ document.getElementById('integrationsView').classList.toggle('hidden',v!=='integrations');
1592
+ document.getElementById('commsView').classList.toggle('hidden',v!=='comms');
1593
+ document.getElementById('skillsView').classList.toggle('hidden',v!=='skills');
1594
+ document.getElementById('flowsView').classList.toggle('hidden',v!=='flows');
1595
+ document.getElementById('schedulerView').classList.toggle('hidden',v!=='scheduler');
1596
+ document.getElementById('logsView').classList.toggle('hidden',v!=='logs');
1597
+ document.getElementById('settingsView').classList.toggle('hidden',v!=='settings');
1598
+ document.getElementById('tabChat').classList.toggle('active',v==='chat');
1599
+ document.getElementById('tabIntegrations').classList.toggle('active',v==='integrations');
1600
+ document.getElementById('tabComms').classList.toggle('active',v==='comms');
1601
+ document.getElementById('tabSkills').classList.toggle('active',v==='skills');
1602
+ document.getElementById('tabFlows').classList.toggle('active',v==='flows');
1603
+ document.getElementById('tabScheduler').classList.toggle('active',v==='scheduler');
1604
+ document.getElementById('tabLogs').classList.toggle('active',v==='logs');
1605
+ document.getElementById('tabSettings').classList.toggle('active',v==='settings');
1606
+ if(v==='integrations'&&!integrationsLoaded){loadToolkits();integrationsLoaded=true;}
1607
+ if(v==='comms'&&!commsLoaded){loadTelegramStatus();loadWhatsAppStatus();loadPhoneWebhookUrl();commsLoaded=true;}
1608
+ if(v==='skills'&&!skillsLoaded){document.getElementById('skillsFrame').src='/api/skills-mp/proxy?path=/';skillsLoaded=true;}
1609
+ if(v==='flows'){loadFlowSkills();loadSavedFlows();flowsLoaded=true;}
1610
+ if(v==='scheduler'&&!schedulerLoaded){loadSchedules();schedulerLoaded=true;}
1611
+ if(v==='logs'&&!logsLoaded){loadLogs();logsLoaded=true;}
1612
+ if(v==='settings'&&!settingsLoaded){loadSettings();settingsLoaded=true;}
1613
+ }
1614
+ // Skills MP install handler
1615
+ window.addEventListener('message',function(e){
1616
+ if(!e.data||e.data.type!=='install_skill')return;
1617
+ var source=e.data.source;
1618
+ if(!source)return;
1619
+ fetch('/api/skills-mp/install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:source})})
1620
+ .then(function(r){return r.json();})
1621
+ .then(function(d){
1622
+ if(d.ok){
1623
+ alert('Installed');
1624
+ // Notify the iframe that install succeeded
1625
+ var frame=document.getElementById('skillsFrame');
1626
+ if(frame&&frame.contentWindow)frame.contentWindow.postMessage({type:'install_success',source:source},'*');
1627
+ // Refresh SkillFlows so newly installed skill appears immediately
1628
+ loadFlowSkills();
1629
+ } else {
1630
+ alert('Install failed: '+(d.error||'Unknown error'));
1631
+ }
1632
+ })
1633
+ .catch(function(err){alert('Install failed: '+err.message);});
1634
+ });
1635
+
1636
+ // ── SkillFlows builder ─────────────────────────────────────────────
1637
+ var flowSteps = [];
1638
+ var flowSkillsCache = [];
1639
+ var flowCommsStatus = { telegram: false, whatsapp: false };
1640
+
1641
+ function refreshFlowCommsStatus() {
1642
+ Promise.all([
1643
+ fetch('/api/telegram/status').then(function(r){return r.json();}).catch(function(){return {};}),
1644
+ fetch('/api/whatsapp/status').then(function(r){return r.json();}).catch(function(){return {};})
1645
+ ]).then(function(results) {
1646
+ flowCommsStatus.telegram = !!(results[0] && results[0].connected);
1647
+ flowCommsStatus.whatsapp = !!(results[1] && results[1].connected);
1648
+ updateGateVisibility();
1649
+ });
1650
+ }
1651
+
1652
+ function updateGateVisibility() {
1653
+ var gateEl = document.getElementById('flowGateItem');
1654
+ if (gateEl) gateEl.style.display = (flowCommsStatus.telegram || flowCommsStatus.whatsapp) ? 'flex' : 'none';
1655
+ }
1656
+
1657
+ function loadFlowSkills() {
1658
+ fetch('/api/skills/list').then(function(r){return r.json();}).then(function(d){
1659
+ flowSkillsCache = d.skills || [];
1660
+ document.getElementById('flowSkillsSearch').value = '';
1661
+ renderFlowSkillsList(flowSkillsCache);
1662
+ }).catch(function(err){ console.error('loadFlowSkills error:', err); });
1663
+ refreshFlowCommsStatus();
1664
+ }
1665
+
1666
+ function filterFlowSkills() {
1667
+ var query = (document.getElementById('flowSkillsSearch').value || '').toLowerCase();
1668
+ if (!query) { renderFlowSkillsList(flowSkillsCache); return; }
1669
+ var filtered = flowSkillsCache.filter(function(s){
1670
+ return s.name.toLowerCase().indexOf(query) !== -1 ||
1671
+ (s.description || '').toLowerCase().indexOf(query) !== -1;
1672
+ });
1673
+ renderFlowSkillsList(filtered);
1674
+ }
1675
+
1676
+ function renderFlowSkillsList(skills) {
1677
+ var el = document.getElementById('flowSkillsList');
1678
+ el.innerHTML = '';
1679
+ // Root folder row (collapsible)
1680
+ var details = document.createElement('details');
1681
+ details.className = 'flows-skill-dir';
1682
+ details.open = true;
1683
+ var summary = document.createElement('summary');
1684
+ summary.innerHTML = '<span class="ft-chevron">' + ICONS.chevronRight + '</span>' +
1685
+ '<span class="ft-icon folder">' + ICONS.folder + '</span>' +
1686
+ '<span class="ft-name">skills</span>';
1687
+ details.appendChild(summary);
1688
+ // Children container
1689
+ var children = document.createElement('div');
1690
+ if (skills.length === 0) {
1691
+ children.innerHTML = '<div style="color:#484f58;font-size:12px;font-style:italic;padding-left:24px;">' +
1692
+ (flowSkillsCache.length === 0 ? 'No skills installed' : 'No matching skills') + '</div>';
1693
+ } else {
1694
+ skills.forEach(function(s){
1695
+ var row = document.createElement('div');
1696
+ row.className = 'flows-skill-item';
1697
+ row.title = s.description || s.name;
1698
+ row.innerHTML = '<span class="ft-icon" style="color:#58a6ff;">' + ICONS.fileCode + '</span>' +
1699
+ '<span class="ft-name">' + escHtml(s.name) + '</span>' +
1700
+ '<button class="skill-add" onclick="event.stopPropagation();addFlowStep(\'' + escHtml(s.name).replace(/'/g, "\\'") + '\')">+</button>';
1701
+ children.appendChild(row);
1702
+ });
1703
+ }
1704
+ details.appendChild(children);
1705
+ // Approval Gate item (only visible when comms are connected)
1706
+ var gateRow = document.createElement('div');
1707
+ gateRow.className = 'flows-skill-item gate-item';
1708
+ gateRow.id = 'flowGateItem';
1709
+ gateRow.title = 'Pause flow and ask for user approval via Telegram or WhatsApp';
1710
+ gateRow.style.display = (flowCommsStatus.telegram || flowCommsStatus.whatsapp) ? 'flex' : 'none';
1711
+ gateRow.innerHTML = '<span class="ft-icon" style="color:#f0883e;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg></span>' +
1712
+ '<span class="ft-name">approval gate</span>' +
1713
+ '<button class="skill-add" onclick="event.stopPropagation();addApprovalGate()">+</button>';
1714
+ details.appendChild(gateRow);
1715
+ el.appendChild(details);
1716
+ }
1717
+
1718
+ function addFlowStep(skillName) {
1719
+ flowSteps.push({ skill: skillName, prompt: '' });
1720
+ renderFlowSteps();
1721
+ }
1722
+
1723
+ function addApprovalGate() {
1724
+ var channel = flowCommsStatus.telegram ? 'telegram' : 'whatsapp';
1725
+ flowSteps.push({ skill: '__approval_gate__', prompt: '', channel: channel });
1726
+ renderFlowSteps();
1727
+ }
1728
+
1729
+ function removeFlowStep(index) {
1730
+ flowSteps.splice(index, 1);
1731
+ renderFlowSteps();
1732
+ }
1733
+
1734
+ var flowDragIdx = null;
1735
+
1736
+ function flowDragStart(e) {
1737
+ flowDragIdx = parseInt(e.currentTarget.dataset.idx);
1738
+ e.currentTarget.classList.add('dragging');
1739
+ e.dataTransfer.effectAllowed = 'move';
1740
+ }
1741
+
1742
+ function flowDragEnd(e) {
1743
+ flowDragIdx = null;
1744
+ e.currentTarget.classList.remove('dragging');
1745
+ document.querySelectorAll('.flows-drop-indicator').forEach(function(el){ el.remove(); });
1746
+ }
1747
+
1748
+ function flowDragOver(e) {
1749
+ if (flowDragIdx === null) return;
1750
+ e.preventDefault();
1751
+ e.dataTransfer.dropEffect = 'move';
1752
+ var container = document.getElementById('flowStepsContainer');
1753
+ container.querySelectorAll('.flows-drop-indicator').forEach(function(el){ el.remove(); });
1754
+ var card = e.target.closest('.flows-step-card, .flows-gate-card, .flows-arrow');
1755
+ if (!card) return;
1756
+ var rect = card.getBoundingClientRect();
1757
+ var midX = rect.left + rect.width / 2;
1758
+ var indicator = document.createElement('div');
1759
+ indicator.className = 'flows-drop-indicator';
1760
+ if (e.clientX < midX) {
1761
+ card.parentNode.insertBefore(indicator, card);
1762
+ } else {
1763
+ card.parentNode.insertBefore(indicator, card.nextSibling);
1764
+ }
1765
+ }
1766
+
1767
+ function flowDrop(e) {
1768
+ if (flowDragIdx === null) return;
1769
+ e.preventDefault();
1770
+ var container = document.getElementById('flowStepsContainer');
1771
+ var indicator = container.querySelector('.flows-drop-indicator');
1772
+ if (!indicator) return;
1773
+ var allChildren = Array.from(container.children);
1774
+ var idxInDom = allChildren.indexOf(indicator);
1775
+ var cardsBefore = 0;
1776
+ for (var j = 0; j < idxInDom; j++) {
1777
+ if (allChildren[j].classList.contains('flows-step-card') || allChildren[j].classList.contains('flows-gate-card')) cardsBefore++;
1778
+ }
1779
+ var targetIdx = cardsBefore;
1780
+ if (targetIdx !== flowDragIdx) {
1781
+ var moved = flowSteps.splice(flowDragIdx, 1)[0];
1782
+ var insertAt = targetIdx > flowDragIdx ? targetIdx - 1 : targetIdx;
1783
+ flowSteps.splice(insertAt, 0, moved);
1784
+ }
1785
+ flowDragIdx = null;
1786
+ container.querySelectorAll('.flows-drop-indicator').forEach(function(el){ el.remove(); });
1787
+ renderFlowSteps();
1788
+ }
1789
+
1790
+ function renderFlowSteps() {
1791
+ var container = document.getElementById('flowStepsContainer');
1792
+ var emptyMsg = document.getElementById('flowEmptyMsg');
1793
+ if (flowSteps.length === 0) {
1794
+ container.innerHTML = '';
1795
+ var msg = document.createElement('div');
1796
+ msg.id = 'flowEmptyMsg';
1797
+ msg.style.cssText = 'display:flex;align-items:center;justify-content:center;width:100%;color:#484f58;font-size:14px;font-style:italic;';
1798
+ msg.textContent = 'Click a skill from the panel to add steps';
1799
+ container.appendChild(msg);
1800
+ return;
1801
+ }
1802
+ container.innerHTML = '';
1803
+ flowSteps.forEach(function(step, i) {
1804
+ if (i > 0) {
1805
+ var arrow = document.createElement('div');
1806
+ arrow.className = 'flows-arrow';
1807
+ arrow.innerHTML = '&#8594;';
1808
+ container.appendChild(arrow);
1809
+ }
1810
+ if (step.skill === '__approval_gate__') {
1811
+ var gate = document.createElement('div');
1812
+ gate.className = 'flows-gate-card';
1813
+ var channels = [];
1814
+ if (flowCommsStatus.telegram) channels.push('<span class="gate-channel tg">Telegram</span>');
1815
+ if (flowCommsStatus.whatsapp) channels.push('<span class="gate-channel wa">WhatsApp</span>');
1816
+ gate.innerHTML =
1817
+ '<div class="gate-header">' +
1818
+ '<span class="gate-label"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg> Approval Gate</span>' +
1819
+ '<button class="gate-remove" data-idx="' + i + '" style="background:none;border:none;color:#f85149;cursor:pointer;font-size:14px;padding:0 4px;line-height:1;">&times;</button>' +
1820
+ '</div>' +
1821
+ '<div class="gate-info">Flow pauses here and asks for your approval via:</div>' +
1822
+ '<div class="gate-channels">' + channels.join('') + '</div>' +
1823
+ '<select data-idx="' + i + '" class="gate-channel-select">' +
1824
+ (flowCommsStatus.telegram ? '<option value="telegram"' + (step.channel === 'telegram' ? ' selected' : '') + '>Telegram</option>' : '') +
1825
+ (flowCommsStatus.whatsapp ? '<option value="whatsapp"' + (step.channel === 'whatsapp' ? ' selected' : '') + '>WhatsApp</option>' : '') +
1826
+ '</select>' +
1827
+ '<textarea placeholder="Custom approval message (optional)" data-idx="' + i + '" style="min-height:40px;">' +
1828
+ (step.prompt || '') +
1829
+ '</textarea>';
1830
+ gate.draggable = true;
1831
+ gate.dataset.idx = i;
1832
+ gate.ondragstart = flowDragStart;
1833
+ gate.ondragend = flowDragEnd;
1834
+ container.appendChild(gate);
1835
+ } else {
1836
+ var card = document.createElement('div');
1837
+ card.className = 'flows-step-card';
1838
+ card.innerHTML =
1839
+ '<div class="step-header">' +
1840
+ '<span class="step-num">Step ' + (i+1) + '</span>' +
1841
+ '<span class="step-skill">' + step.skill + '</span>' +
1842
+ '<button class="step-remove" data-idx="' + i + '">&times;</button>' +
1843
+ '</div>' +
1844
+ '<textarea placeholder="Prompt for this step... Use {input} for user input" data-idx="' + i + '">' +
1845
+ (step.prompt || '') +
1846
+ '</textarea>';
1847
+ card.draggable = true;
1848
+ card.dataset.idx = i;
1849
+ card.ondragstart = flowDragStart;
1850
+ card.ondragend = flowDragEnd;
1851
+ container.appendChild(card);
1852
+ }
1853
+ });
1854
+ container.ondragover = flowDragOver;
1855
+ container.ondrop = flowDrop;
1856
+ // Wire up events
1857
+ container.querySelectorAll('.step-remove, .gate-remove').forEach(function(btn){
1858
+ btn.onclick = function(){ removeFlowStep(parseInt(this.dataset.idx)); };
1859
+ });
1860
+ container.querySelectorAll('textarea').forEach(function(ta){
1861
+ ta.oninput = function(){ flowSteps[parseInt(this.dataset.idx)].prompt = this.value; };
1862
+ });
1863
+ container.querySelectorAll('.gate-channel-select').forEach(function(sel){
1864
+ sel.onchange = function(){ flowSteps[parseInt(this.dataset.idx)].channel = this.value; };
1865
+ });
1866
+ }
1867
+
1868
+ function saveFlow() {
1869
+ var name = document.getElementById('flowNameInput').value.trim();
1870
+ var desc = document.getElementById('flowDescInput').value.trim();
1871
+ if (!name) { alert('Please enter a flow name'); return; }
1872
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) { alert('Flow name must be kebab-case (e.g. my-flow)'); return; }
1873
+ if (flowSteps.length === 0) { alert('Add at least one step'); return; }
1874
+ for (var i = 0; i < flowSteps.length; i++) {
1875
+ if (flowSteps[i].skill === '__approval_gate__') continue;
1876
+ if (!flowSteps[i].prompt.trim()) { alert('Step ' + (i+1) + ' needs a prompt'); return; }
1877
+ }
1878
+ fetch('/api/flows/save', {
1879
+ method: 'POST',
1880
+ headers: { 'Content-Type': 'application/json' },
1881
+ body: JSON.stringify({ name: name, description: desc, steps: flowSteps })
1882
+ }).then(function(r){ return r.json(); }).then(function(d) {
1883
+ if (d.ok) { loadSavedFlows(); clearFlow(); } else { alert('Save failed: ' + (d.error || 'Unknown error')); }
1884
+ }).catch(function(err){ alert('Save failed: ' + err.message); });
1885
+ }
1886
+
1887
+ function loadSavedFlows() {
1888
+ fetch('/api/flows/list').then(function(r){return r.json();}).then(function(d){
1889
+ var list = document.getElementById('savedFlowsList');
1890
+ var flows = d.flows || [];
1891
+ if (flows.length === 0) {
1892
+ list.innerHTML = '<div style="color:#484f58;font-size:12px;font-style:italic;">No flows saved yet</div>';
1893
+ return;
1894
+ }
1895
+ list.innerHTML = '';
1896
+ flows.forEach(function(f) {
1897
+ var item = document.createElement('div');
1898
+ item.className = 'flows-saved-item';
1899
+ item.innerHTML =
1900
+ '<div style="flex:1;" onclick="loadFlowForEdit(\'' + f.name + '\')">' +
1901
+ '<div class="flow-name">' + f.name + ' <span class="flow-trigger">@' + f.name + '</span></div>' +
1902
+ '<div class="flow-desc">' + (f.description || '') + ' &middot; ' + (f.steps ? f.steps.length : 0) + ' steps</div>' +
1903
+ '</div>' +
1904
+ '<button class="flow-delete" data-name="' + f.name + '" title="Delete flow">&times;</button>';
1905
+ list.appendChild(item);
1906
+ });
1907
+ list.querySelectorAll('.flow-delete').forEach(function(btn) {
1908
+ btn.onclick = function(e) {
1909
+ e.stopPropagation();
1910
+ if (!confirm('Delete flow "' + this.dataset.name + '"?')) return;
1911
+ fetch('/api/flows/delete', {
1912
+ method: 'DELETE',
1913
+ headers: { 'Content-Type': 'application/json' },
1914
+ body: JSON.stringify({ name: this.dataset.name })
1915
+ }).then(function(){ loadSavedFlows(); });
1916
+ };
1917
+ });
1918
+ }).catch(function(err){ console.error('loadSavedFlows error:', err); });
1919
+ }
1920
+
1921
+ function loadFlowForEdit(name) {
1922
+ fetch('/api/flows/list').then(function(r){return r.json();}).then(function(d) {
1923
+ var flow = (d.flows || []).find(function(f){ return f.name === name; });
1924
+ if (!flow) return;
1925
+ document.getElementById('flowNameInput').value = flow.name;
1926
+ document.getElementById('flowDescInput').value = flow.description || '';
1927
+ flowSteps = (flow.steps || []).map(function(s){ var o = { skill: s.skill, prompt: s.prompt }; if (s.channel) o.channel = s.channel; return o; });
1928
+ renderFlowSteps();
1929
+ });
1930
+ }
1931
+
1932
+ function clearFlow() {
1933
+ document.getElementById('flowNameInput').value = '';
1934
+ document.getElementById('flowDescInput').value = '';
1935
+ flowSteps = [];
1936
+ renderFlowSteps();
1937
+ }
1938
+
1939
+ /* FILES/MONACO DISABLED
1940
+ let monacoEditor=null,monacoReady=false;
1941
+ require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs'}});
1942
+ require(['vs/editor/editor.main'],function(){monacoReady=true;monaco.editor.defineTheme('gitagent-dark',{base:'vs-dark',inherit:true,rules:[],colors:{'editor.background':'#0d1117','editor.lineHighlightBackground':'#161b2280','editorLineNumber.foreground':'#484f58','editorLineNumber.activeForeground':'#8b949e','editor.selectionBackground':'#58a6ff30','editorCursor.foreground':'#58a6ff','editorWidget.background':'#161b22','editorWidget.border':'#21262d','minimap.background':'#0d1117'}});});
1943
+
1944
+ function getLanguage(f){var e=f.split('.').pop().toLowerCase(),m={ts:'typescript',tsx:'typescript',js:'javascript',jsx:'javascript',json:'json',html:'html',css:'css',scss:'scss',less:'less',md:'markdown',py:'python',sh:'shell',bash:'shell',yaml:'yaml',yml:'yaml',xml:'xml',sql:'sql',rs:'rust',go:'go',java:'java',c:'c',cpp:'cpp',h:'c',rb:'ruby',php:'php',swift:'swift',kt:'kotlin',toml:'ini',env:'ini',gitignore:'ini'};return m[e]||'plaintext';}
1945
+
1946
+ function ensureMonacoEditor(){if(monacoEditor||!monacoReady)return;var c=document.getElementById('editorContainer');document.getElementById('editorEmpty').style.display='none';var d=document.createElement('div');d.id='monacoMount';d.style.cssText='width:100%;height:100%;';c.appendChild(d);monacoEditor=monaco.editor.create(d,{value:'',language:'plaintext',theme:'gitagent-dark',fontSize:13,fontFamily:"'JetBrains Mono','IBM Plex Mono','Fira Code',monospace",fontLigatures:true,minimap:{enabled:true,scale:1},lineNumbers:'on',renderLineHighlight:'all',scrollBeyondLastLine:false,padding:{top:8},bracketPairColorization:{enabled:true},smoothScrolling:true,cursorBlinking:'smooth',cursorSmoothCaretAnimation:'on',wordWrap:'on',tabSize:2,automaticLayout:true});monacoEditor.onDidChangeModelContent(function(){if(activeTabPath&&openTabs[activeTabPath]){openTabs[activeTabPath].modified=true;renderTabs();}});}
1947
+
1948
+ var ICONS={chevronRight:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"/></svg>',chevronDown:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"/></svg>',folder:'<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>',folderOpen:'<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M5 19h14a2 2 0 001.84-1.22L23 12H5.24a2 2 0 00-1.84 1.22L1 19h2a2 2 0 002-2V7a2 2 0 012-2h4l2 2h6a2 2 0 012 2v1"/></svg>',file:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>',fileCode:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><path d="M10 12l-2 2 2 2"/><path d="M14 12l2 2-2 2"/></svg>'};
1949
+
1950
+ function getFileIconClass(n){var e=n.split('.').pop().toLowerCase();if(['ts','tsx'].includes(e))return'ts';if(['js','jsx','mjs'].includes(e))return'js';if(e==='json')return'json';if(['css','scss','less'].includes(e))return'css';if(['html','htm'].includes(e))return'html';if(['md','txt','rst'].includes(e))return'md';if(['yaml','yml'].includes(e))return'yaml';if(e==='py')return'py';if(['sh','bash','zsh'].includes(e))return'sh';return'default';}
1951
+ function getFileIconSvg(n){var c=getFileIconClass(n);if(['ts','js','css','html','py','sh'].includes(c))return ICONS.fileCode;return ICONS.file;}
1952
+
1953
+ var activeTabPath=null;
1954
+ async function loadFileTree(){var t=document.getElementById('fileTree');t.innerHTML='<div style="padding:16px;color:#484f58;font-size:12px;">Loading...</div>';try{var r=await fetch('/api/files?path=.');var d=await r.json();t.innerHTML='';renderTree(d.entries,t,0);}catch(e){t.innerHTML='<div style="padding:16px;color:#f85149;font-size:12px;">Failed to load files</div>';}}
1955
+
1956
+ function renderTree(entries,parent,depth){for(var i=0;i<entries.length;i++){var entry=entries[i];if(entry.type==='directory'){var group=document.createElement('div');group.className='ft-group';var item=document.createElement('div');item.className='ft-item dir';item.style.paddingLeft=(depth*12+8)+'px';item.innerHTML='<span class="ft-chevron">'+ICONS.chevronDown+'</span><span class="ft-icon folder">'+ICONS.folderOpen+'</span><span class="ft-name">'+escHtml(entry.name)+'</span>';item.onclick=(function(g,it){return function(e){e.stopPropagation();var c=g.classList.toggle('collapsed');it.querySelector('.ft-chevron').innerHTML=c?ICONS.chevronRight:ICONS.chevronDown;it.querySelector('.ft-icon').innerHTML=c?ICONS.folder:ICONS.folderOpen;};})(group,item);var ch=document.createElement('div');ch.className='ft-children';if(entry.children)renderTree(entry.children,ch,depth+1);group.appendChild(item);group.appendChild(ch);parent.appendChild(group);}else{var fi=document.createElement('div');fi.className='ft-item'+(activeTabPath===entry.path?' active':'');fi.style.paddingLeft=(depth*12+22)+'px';fi.innerHTML='<span class="ft-icon '+getFileIconClass(entry.name)+'">'+getFileIconSvg(entry.name)+'</span><span class="ft-name">'+escHtml(entry.name)+'</span>';fi.onclick=(function(p,n){return function(){openFile(p,n);};})(entry.path,entry.name);fi.dataset.path=entry.path;parent.appendChild(fi);}}}
1957
+
1958
+ var openTabs={};
1959
+ function renderTabs(){var t=document.getElementById('editorTabs');t.innerHTML='';for(var p in openTabs){var tab=openTabs[p];var d=document.createElement('div');d.className='ed-tab'+(p===activeTabPath?' active':'');d.innerHTML='<span class="tab-icon ft-icon '+getFileIconClass(tab.name)+'">'+getFileIconSvg(tab.name)+'</span><span>'+escHtml(tab.name)+'</span>'+(tab.modified?'<span class="tab-modified"></span>':'')+'<button class="tab-close">&times;</button>';(function(path){d.querySelector('.tab-close').onclick=function(e){e.stopPropagation();closeTab(path);};d.onclick=function(){switchTab(path);};})(p);t.appendChild(d);}}
1960
+
1961
+ function switchTab(p){if(!openTabs[p])return;if(activeTabPath&&openTabs[activeTabPath]&&monacoEditor)openTabs[activeTabPath].content=monacoEditor.getValue();activeTabPath=p;var tab=openTabs[p];if(monacoEditor){monaco.editor.setModelLanguage(monacoEditor.getModel(),tab.language);monacoEditor.setValue(tab.content);}document.getElementById('sbLang').textContent=tab.language;renderTabs();document.querySelectorAll('.ft-item').forEach(function(el){el.classList.toggle('active',el.dataset.path===activeTabPath);});}
1962
+
1963
+ function closeTab(p){delete openTabs[p];var rem=Object.keys(openTabs);if(p===activeTabPath){if(rem.length>0)switchTab(rem[rem.length-1]);else{activeTabPath=null;if(monacoEditor)monacoEditor.setValue('');document.getElementById('editorEmpty').style.display='';var m=document.getElementById('monacoMount');if(m)m.style.display='none';document.getElementById('sbLang').textContent='';}}renderTabs();document.querySelectorAll('.ft-item').forEach(function(el){el.classList.toggle('active',el.dataset.path===activeTabPath);});}
1964
+
1965
+ async function openFile(fp,fn){if(openTabs[fp]){switchTab(fp);return;}ensureMonacoEditor();var m=document.getElementById('monacoMount');if(m)m.style.display='';document.getElementById('editorEmpty').style.display='none';var lang=getLanguage(fn);openTabs[fp]={name:fn,content:'Loading...',modified:false,language:lang};activeTabPath=fp;renderTabs();try{var r=await fetch('/api/file?path='+encodeURIComponent(fp));var d=await r.json();openTabs[fp].content=d.error?'// Error: '+d.error:d.content;}catch(e){openTabs[fp].content='// Failed to load file';}openTabs[fp].modified=false;if(monacoEditor&&activeTabPath===fp){monaco.editor.setModelLanguage(monacoEditor.getModel(),lang);monacoEditor.setValue(openTabs[fp].content);}document.getElementById('sbLang').textContent=lang;renderTabs();document.querySelectorAll('.ft-item').forEach(function(el){el.classList.toggle('active',el.dataset.path===activeTabPath);});}
1966
+
1967
+ async function saveCurrentFile(){if(!activeTabPath||!openTabs[activeTabPath])return;if(monacoEditor)openTabs[activeTabPath].content=monacoEditor.getValue();try{var r=await fetch('/api/file',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:activeTabPath,content:openTabs[activeTabPath].content})});var d=await r.json();if(d.ok){openTabs[activeTabPath].modified=false;renderTabs();}}catch(e){}}
1968
+
1969
+ document.addEventListener('keydown',function(e){if(currentView!=='files')return;if((e.ctrlKey||e.metaKey)&&e.key==='s'){e.preventDefault();saveCurrentFile();}if((e.ctrlKey||e.metaKey)&&e.key==='w'){e.preventDefault();if(activeTabPath)closeTab(activeTabPath);}});
1970
+ FILES/MONACO DISABLED */
1971
+
1972
+ // CHAT / VOICE
1973
+ var ws=null,audioCtx=null,micStream=null,micProcessor=null,micActive=false,cameraStream=null,cameraActive=false,cameraInterval=null,screenStream=null,screenActive=false,screenInterval=null;
1974
+ var statusDot=document.getElementById('statusDot'),statusText=document.getElementById('statusText'),micBtn=document.getElementById('micBtn'),cameraBtn=document.getElementById('cameraBtn'),screenBtn=document.getElementById('screenBtn'),cameraVideo=document.getElementById('cameraVideo'),cameraOff=document.getElementById('cameraOff'),cameraCanvas=document.getElementById('cameraCanvas'),conv=document.getElementById('conversation'),textInput=document.getElementById('textInput');
1975
+
1976
+ function setStatus(t,s){statusText.textContent=t;statusDot.className='status-dot '+(s||'');}
1977
+ function appendConv(h,c){var d=document.createElement('div');d.className='conv-msg '+(c||'');d.innerHTML=h;conv.appendChild(d);conv.scrollTop=conv.scrollHeight;}
1978
+ function connectWS(){if(ws)return;setStatus('Connecting...','');ws=new WebSocket((window.location.protocol==='https:'?'wss://':'ws://')+window.location.host);ws.onopen=function(){setStatus('Connected','connected');if(auditMode)loadExplorerTree();};ws.onmessage=function(e){try{handleServerMessage(JSON.parse(e.data));}catch(x){}};ws.onerror=function(){setStatus('Connection error','error');};ws.onclose=function(){ws=null;setStatus('Disconnected','');stopMic();stopCamera();stopScreen();};}
1979
+ function sendMsg(m){if(ws&&ws.readyState===WebSocket.OPEN)ws.send(JSON.stringify(m));}
1980
+
1981
+ // Agent Vitals
1982
+ var vitalsTokenCount=0, vitalsMaxTokens=200000, vitalsServerUptime=0;
1983
+ var waveData=[], waveCanvas=document.getElementById('vitalsWaveCanvas'), waveCtx=waveCanvas?waveCanvas.getContext('2d'):null;
1984
+
1985
+ function formatUptime(s){
1986
+ var h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sec=s%60;
1987
+ return (h>0?(h+':'):'')+String(m).padStart(2,'0')+':'+String(sec).padStart(2,'0');
1988
+ }
1989
+ function updateUptime(){
1990
+ // Interpolate locally between API polls for smooth display,
1991
+ // but base value always comes from the server snapshot
1992
+ vitalsServerUptime++;
1993
+ var el=document.getElementById('vitalUptime');
1994
+ if(el) el.innerHTML=formatUptime(vitalsServerUptime);
1995
+ }
1996
+ setInterval(updateUptime,1000);
1997
+
1998
+ function updateVitalTokens(count){
1999
+ vitalsTokenCount=count;
2000
+ var el=document.getElementById('vitalTokens');
2001
+ var bar=document.getElementById('vitalTokensBar');
2002
+ if(el){
2003
+ var display=count>=1000?(Math.round(count/100)/10)+'k':count;
2004
+ el.innerHTML=display+'<span class="vital-unit">tok</span>';
2005
+ }
2006
+ if(bar) bar.style.width=Math.min(100,Math.round(count/vitalsMaxTokens*100))+'%';
2007
+ }
2008
+
2009
+ function pollSystemVitals(){
2010
+ fetch('/api/vitals').then(function(r){return r.json();}).then(function(d){
2011
+ var cpuEl=document.getElementById('vitalCpu'), cpuBar=document.getElementById('vitalCpuBar');
2012
+ var memEl=document.getElementById('vitalMem'), memBar=document.getElementById('vitalMemBar');
2013
+ if(cpuEl&&d.cpu!==undefined){cpuEl.innerHTML=Math.round(d.cpu)+'<span class="vital-unit">%</span>';if(cpuBar)cpuBar.style.width=Math.round(d.cpu)+'%';}
2014
+ if(memEl&&d.mem!==undefined){
2015
+ var mb=Math.round(d.mem);
2016
+ var display=mb>=1024?(Math.round(mb/102.4)/10)+'GB':mb+'MB';
2017
+ var unit=mb>=1024?'':'MB';
2018
+ memEl.innerHTML=display.replace(/[A-Z]+$/,'')+'<span class="vital-unit">'+display.replace(/^[\d.]+/,'')+'</span>';
2019
+ if(memBar)memBar.style.width=Math.min(100,Math.round(mb/4096*100))+'%';
2020
+ }
2021
+ if(d.tokens!==undefined) updateVitalTokens(d.tokens);
2022
+ // Sync uptime from server snapshot so UI matches API
2023
+ if(d.uptime!==undefined) vitalsServerUptime=d.uptime;
2024
+ // Push to wave
2025
+ waveData.push(d.cpu||0);
2026
+ if(waveData.length>60) waveData.shift();
2027
+ }).catch(function(){});
2028
+ }
2029
+ setInterval(pollSystemVitals,2000);
2030
+ pollSystemVitals();
2031
+
2032
+ function drawWave(){
2033
+ if(!waveCanvas||!waveCtx) return;
2034
+ var w=waveCanvas.width=waveCanvas.offsetWidth*2, h=waveCanvas.height=waveCanvas.offsetHeight*2;
2035
+ waveCtx.clearRect(0,0,w,h);
2036
+ if(waveData.length<2){requestAnimationFrame(drawWave);return;}
2037
+ waveCtx.strokeStyle='#f85149'; waveCtx.lineWidth=1.5; waveCtx.shadowColor='#f85149'; waveCtx.shadowBlur=4;
2038
+ waveCtx.beginPath();
2039
+ var step=w/(waveData.length-1);
2040
+ for(var i=0;i<waveData.length;i++){
2041
+ var val=waveData[i]/100;
2042
+ var y=h-val*(h-4)-2;
2043
+ if(i===0)waveCtx.moveTo(0,y);else{
2044
+ var px=(i-1)*step,py=h-(waveData[i-1]/100)*(h-4)-2;
2045
+ var cx=(px+i*step)/2;
2046
+ waveCtx.bezierCurveTo(cx,py,cx,y,i*step,y);
2047
+ }
2048
+ }
2049
+ waveCtx.stroke();
2050
+ // Glow line
2051
+ waveCtx.shadowBlur=0; waveCtx.strokeStyle='rgba(248,81,73,0.15)'; waveCtx.lineWidth=6;
2052
+ waveCtx.beginPath();
2053
+ for(var i=0;i<waveData.length;i++){
2054
+ var val=waveData[i]/100;var y=h-val*(h-4)-2;
2055
+ if(i===0)waveCtx.moveTo(0,y);else{
2056
+ var px=(i-1)*step,py=h-(waveData[i-1]/100)*(h-4)-2;
2057
+ var cx=(px+i*step)/2;
2058
+ waveCtx.bezierCurveTo(cx,py,cx,y,i*step,y);
2059
+ }
2060
+ }
2061
+ waveCtx.stroke();
2062
+ requestAnimationFrame(drawWave);
2063
+ }
2064
+ requestAnimationFrame(drawWave);
2065
+
2066
+ var assistantText='',thinkingEl=null,toolActivityEl=null,toolCount=0,isReplay=false,toolVerb='';
2067
+ var toolVerbs=['susurrating','skulking','slithering','shuffling','skimming','scudding','swishing','sparkling','twinkling','glowing','shimmering','murmuring','lilting','drifting','billowing','meandering','hovering','skirling','thrumming','wending','gadding','loitering','brooding','looming','haunting','seething','flickering','glimmering','whispering','rustling','eddying','trembling','quivering','wavering','gliding','cascading','rippling','tumbling','stirring','wandering'];
2068
+ var spriteMap=[
2069
+ [0,0,1,1,1,1,1,1,0,0],[0,1,2,2,2,2,2,2,1,0],[1,2,2,2,2,2,2,2,2,1],
2070
+ [1,2,1,2,2,2,2,1,2,1],[1,2,1,2,2,2,2,1,2,1],[1,2,2,2,2,2,2,2,2,1],
2071
+ [1,2,2,2,2,2,2,2,2,1],[0,1,2,2,2,2,2,2,1,0],[1,2,2,1,2,2,1,2,2,1],[0,1,1,0,1,1,0,1,1,0]
2072
+ ];
2073
+ function buildSprite(){
2074
+ var s=document.createElement('div');s.className='tool-sprite';
2075
+ spriteMap.flat().forEach(function(v){var p=document.createElement('div');p.className='px '+(v===1?'px-o':v===2?'px-f':'px-e');s.appendChild(p);});
2076
+ return s;
2077
+ }
2078
+ (function(){var el=document.getElementById('headerLogo');if(el)spriteMap.flat().forEach(function(v){var p=document.createElement('div');p.className='px '+(v===1?'px-o':v===2?'px-f':'px-e');el.appendChild(p);});})();
2079
+ function getToolActivity(){
2080
+ if(!toolActivityEl){
2081
+ toolActivityEl=document.createElement('div');
2082
+ toolActivityEl.className='conv-msg tool-activity';
2083
+ toolVerb=toolVerbs[Math.floor(Math.random()*toolVerbs.length)];
2084
+ var sprite=buildSprite();
2085
+ var verb=document.createElement('span');verb.className='tool-verb';verb.textContent='< '+toolVerb+'... >';
2086
+ var body=document.createElement('div');body.className='tool-activity-body';
2087
+ var sum=document.createElement('div');sum.className='tool-activity-summary';sum.style.display='none';
2088
+ toolActivityEl.appendChild(sprite);
2089
+ toolActivityEl.appendChild(verb);
2090
+ toolActivityEl.appendChild(body);
2091
+ toolActivityEl.appendChild(sum);
2092
+ conv.appendChild(toolActivityEl);
2093
+ toolCount=0;
2094
+ }
2095
+ return toolActivityEl;
2096
+ }
2097
+ function collapseToolActivity(){
2098
+ if(toolActivityEl&&toolCount>0){
2099
+ var el=toolActivityEl;
2100
+ var body=el.querySelector('.tool-activity-body');
2101
+ var sprite=el.querySelector('.tool-sprite');
2102
+ var verb=el.querySelector('.tool-verb');
2103
+ var sum=el.querySelector('.tool-activity-summary');
2104
+ var tc=toolCount;
2105
+ body.style.display='none';
2106
+ if(sprite)sprite.style.display='none';
2107
+ if(verb)verb.style.display='none';
2108
+ sum.style.display='';
2109
+ sum.innerHTML='<span class="tool-summary-toggle">▸ Used '+tc+' tool'+(tc!==1?'s':'')+'</span>';
2110
+ if(!isReplay) sum.style.animation='toolSummaryIn 0.2s ease-out';
2111
+ el.style.height='auto';
2112
+ sum.onclick=function(){
2113
+ var isHidden=body.style.display==='none';
2114
+ body.style.display=isHidden?'':'none';
2115
+ el.classList.toggle('expanded',isHidden);
2116
+ sum.querySelector('.tool-summary-toggle').textContent=(isHidden?'▾':'▸')+' Used '+tc+' tool'+(tc!==1?'s':'');
2117
+ };
2118
+ }
2119
+ toolActivityEl=null;
2120
+ toolCount=0;
2121
+ toolVerb='';
2122
+ }
2123
+ function handleServerMessage(m){switch(m.type){
2124
+ case'audio_delta':playAudioDelta(m.audio);break;
2125
+ case'interrupt':flushAudio();break;
2126
+ case'transcript':
2127
+ if(m.role==='assistant'&&m.text&&m.text.indexOf('Voice mode unavailable')===0){
2128
+ document.getElementById('voiceWarning').style.display='block';
2129
+ var _mic=document.getElementById('micBtn');if(_mic)_mic.style.display='none';
2130
+ var _cam=document.getElementById('cameraBtn');if(_cam)_cam.style.display='none';
2131
+ var _scr=document.getElementById('screenBtn');if(_scr)_scr.style.display='none';
2132
+ var _spk=document.querySelector('.speaker-toggle');if(_spk)_spk.style.display='none';
2133
+ break;
2134
+ }
2135
+ if(m.role==='user'&&!m.partial){
2136
+ var isTg=m.text&&m.text.startsWith('[Telegram]');
2137
+ var isWa=m.text&&m.text.startsWith('[WhatsApp]');
2138
+ var label=isTg?'<span class="label tg">Telegram: </span>':isWa?'<span class="label wa">WhatsApp: </span>':'<span class="label">You: </span>';
2139
+ var cleanText=isTg?m.text.replace(/^\[Telegram\]\s*/,''):isWa?m.text.replace(/^\[WhatsApp\]:\s*/,''):m.text;
2140
+ appendConv(label+escHtml(cleanText),isTg?'user telegram':isWa?'user whatsapp':'user');
2141
+ }
2142
+ else if(m.role==='assistant'){if(m.partial)assistantText+=m.text;else{collapseToolActivity();if(assistantText){appendConv('<span class="label">{{AGENT_NAME}}: </span>'+escHtml(assistantText),'assistant');assistantText='';}else appendConv('<span class="label">{{AGENT_NAME}}: </span>'+escHtml(m.text),'assistant');}}
2143
+ break;
2144
+ case'agent_working':
2145
+ var awVerb=toolVerbs[Math.floor(Math.random()*toolVerbs.length)];
2146
+ var awEl=document.createElement('div');awEl.className='conv-msg agent-working';
2147
+ var awSpinner=document.createElement('div');awSpinner.className='agent-working-spinner';awEl.appendChild(awSpinner);
2148
+ var awText=document.createElement('span');awText.className='agent-working-text';
2149
+ awText.innerHTML='<span class="agent-working-name">gitagent</span> <span class="agent-working-verb">&lt; '+escHtml(awVerb)+'... &gt;</span> <span class="agent-working-sep">:</span> <span class="agent-working-query">'+escHtml(m.query)+'</span>';
2150
+ awEl.appendChild(awText);
2151
+ conv.appendChild(awEl);conv.scrollTop=conv.scrollHeight;
2152
+ setStatus('Agent working...','connected');break;
2153
+ case'agent_done':
2154
+ thinkingEl=null;
2155
+ collapseToolActivity();
2156
+ conv.querySelectorAll('.conv-msg.agent-working').forEach(function(el){el.remove();});
2157
+ setStatus('Connected','connected');
2158
+ if(filesLoaded)loadFileTree();
2159
+ refreshFileTree();break;
2160
+ case'log_entry':
2161
+ appendLogEntry(m.entry);
2162
+ break;
2163
+ case'memory_saving':
2164
+ if(m.status==='start'){
2165
+ var msEl=document.createElement('div');msEl.className='conv-msg memory-saving';msEl.id='memory-saving-indicator';
2166
+ var msSpinner=document.createElement('div');msSpinner.className='memory-saving-spinner';msEl.appendChild(msSpinner);
2167
+ var msText=document.createElement('span');msText.className='memory-saving-text';msText.textContent='< remembering... >';msEl.appendChild(msText);
2168
+ if(m.text){var msDetail=document.createElement('span');msDetail.className='memory-saving-detail';msDetail.textContent=m.text;msEl.appendChild(msDetail);}
2169
+ conv.appendChild(msEl);conv.scrollTop=conv.scrollHeight;
2170
+ } else {
2171
+ var existing=document.getElementById('memory-saving-indicator');
2172
+ if(existing)existing.remove();
2173
+ }
2174
+ break;
2175
+ case'tool_call':
2176
+ thinkingEl=null;
2177
+ toolCount++;
2178
+ if(isReplay){
2179
+ var ta=getToolActivity();
2180
+ var cur=ta.querySelector('.tool-activity-body');
2181
+ cur.innerHTML='<span class="tool-call">'+friendlyToolCall(m.toolName,m.args)+'</span>';
2182
+ } else {
2183
+ var ta=getToolActivity();
2184
+ var cur=ta.querySelector('.tool-activity-body');
2185
+ cur.innerHTML='<span class="tool-call">'+friendlyToolCall(m.toolName,m.args)+'</span>';
2186
+ cur.style.display='';
2187
+ conv.scrollTop=conv.scrollHeight;
2188
+ }
2189
+ // In audit mode, track which file the agent is about to write
2190
+ if(auditMode){
2191
+ var writePath=detectFileWritePath(m.toolName,m.args);
2192
+ if(writePath)lastToolWritePath=writePath;
2193
+ }
2194
+ break;
2195
+ case'tool_result':
2196
+ var ta2=getToolActivity();
2197
+ var cur2=ta2.querySelector('.tool-activity-body');
2198
+ if(m.isError){
2199
+ cur2.innerHTML='<span class="tool-result thinking-fallback"><span class="label">thinking...</span></span>';
2200
+ } else {
2201
+ cur2.innerHTML='<span class="tool-result">'+friendlyToolResult(m.toolName,m.content,false)+'</span>';
2202
+ }
2203
+ if(!isReplay)conv.scrollTop=conv.scrollHeight;
2204
+ // In audit mode, auto-open the file that was just written
2205
+ if(auditMode&&lastToolWritePath&&!m.isError){
2206
+ var autoPath=lastToolWritePath;
2207
+ lastToolWritePath=null;
2208
+ auditAutoOpenFile(autoPath);
2209
+ }
2210
+ refreshFileTree();break;
2211
+ case'agent_thinking':
2212
+ if(!thinkingEl){thinkingEl=document.createElement('div');thinkingEl.className='conv-msg thinking';thinkingEl.innerHTML='<span class="label">Thinking: </span><span class="thinking-text"></span>';conv.appendChild(thinkingEl);}
2213
+ thinkingEl.querySelector('.thinking-text').textContent+=m.text;
2214
+ conv.scrollTop=conv.scrollHeight;
2215
+ break;
2216
+ case'files_changed':refreshFileTree();if(currentView==='flows'){loadFlowSkills();}break;
2217
+ case'error':appendConv('Error: '+escHtml(m.message),'system');break;
2218
+ case'schedule_start':
2219
+ var schEl=document.createElement('div');
2220
+ schEl.className='conv-msg schedule-header';
2221
+ schEl.innerHTML='<span class="schedule-label">\u25b6 Ran schedule "'+escHtml(m.id)+'"</span>'
2222
+ +'<span class="schedule-prompt-preview">'+escHtml((m.prompt||'').slice(0,80))+'</span>';
2223
+ conv.appendChild(schEl);
2224
+ conv.scrollTop=conv.scrollHeight;
2225
+ break;
2226
+ case'schedule_result':
2227
+ var sdEl=document.createElement('div');
2228
+ sdEl.className='conv-msg schedule-done'+(m.success?'':' error');
2229
+ sdEl.textContent=(m.success?'\u2713':'\u2717')+' Schedule "'+m.id+'" completed'+(m.success?'':' with error');
2230
+ conv.appendChild(sdEl);
2231
+ conv.scrollTop=conv.scrollHeight;
2232
+ if(currentView==='scheduler')loadSchedules();
2233
+ break;
2234
+ }}
2235
+ function escHtml(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML;}
2236
+
2237
+ // Parse composio tool names like "composio_googlecalendar_GOOGLECALENDAR_EVENTS_LIST" into friendly text
2238
+ function parseComposioName(name){
2239
+ var raw=name.replace(/^composio_/,'');
2240
+ // Split on first underscore to get service, then action
2241
+ var parts=raw.split('_');
2242
+ var service=parts[0].toLowerCase();
2243
+ // The action is everything after the service prefix (which repeats)
2244
+ // e.g. "googlecalendar_GOOGLECALENDAR_EVENTS_LIST" → action = "EVENTS_LIST"
2245
+ var actionRaw=raw.replace(new RegExp('^[^_]+_'+service+'_?','i'),'');
2246
+ if(!actionRaw) actionRaw=parts.slice(1).join('_');
2247
+ var action=actionRaw.toLowerCase().replace(/_/g,' ');
2248
+ // Friendly service names
2249
+ var services={google:'Google',googlecalendar:'Google Calendar',gmail:'Gmail',
2250
+ github:'GitHub',slack:'Slack',notion:'Notion',drive:'Google Drive',
2251
+ googledrive:'Google Drive',sheets:'Google Sheets',googlesheets:'Google Sheets',
2252
+ outlook:'Outlook',discord:'Discord',trello:'Trello',linear:'Linear',
2253
+ jira:'Jira',asana:'Asana',hubspot:'HubSpot',salesforce:'Salesforce',
2254
+ spotify:'Spotify',twitter:'Twitter',youtube:'YouTube'};
2255
+ var friendlyService=services[service]||service.charAt(0).toUpperCase()+service.slice(1);
2256
+ // Friendly action verbs
2257
+ var actionMap={
2258
+ 'events list':'checking events','find event':'finding event','create event':'creating event',
2259
+ 'delete event':'deleting event','update event':'updating event','quick add':'adding event',
2260
+ 'send email':'sending email','create email draft':'drafting email',
2261
+ 'list emails':'checking emails','get email':'reading email','fetch emails':'checking emails',
2262
+ 'list messages':'checking messages','send message':'sending message',
2263
+ 'get message':'reading message','search messages':'searching messages',
2264
+ 'list labels':'checking labels','get profile':'checking profile',
2265
+ 'list repos':'checking repos','create issue':'creating issue',
2266
+ 'list issues':'checking issues','get repo':'checking repo',
2267
+ 'create pr':'creating pull request','list prs':'checking pull requests',
2268
+ 'search contacts':'searching contacts','list contacts':'checking contacts',
2269
+ 'find contact':'finding contact','get contact':'looking up contact',
2270
+ };
2271
+ var friendlyAction=actionMap[action]||action;
2272
+ return {service:friendlyService,action:friendlyAction};
2273
+ }
2274
+
2275
+ // Summarize composio args into a short human string
2276
+ function summarizeArgs(args){
2277
+ var a=args||{};
2278
+ var parts=[];
2279
+ // Calendar
2280
+ if(a.calendarId&&a.calendarId!=='primary') parts.push(a.calendarId);
2281
+ if(a.summary) parts.push('"'+a.summary+'"');
2282
+ if(a.query||a.q) parts.push('"'+(a.query||a.q)+'"');
2283
+ // Email
2284
+ if(a.to||a.recipient) parts.push('to '+(a.to||a.recipient));
2285
+ if(a.subject) parts.push('"'+a.subject+'"');
2286
+ // General
2287
+ if(a.name&&!a.phone) parts.push(a.name);
2288
+ if(a.message&&!a.to) parts.push('"'+a.message.slice(0,60)+'"');
2289
+ if(a.owner&&a.repo) parts.push(a.owner+'/'+a.repo);
2290
+ if(a.channel) parts.push('#'+a.channel);
2291
+ return parts.length?parts.join(', '):'';
2292
+ }
2293
+
2294
+ // Parse composio JSON result into something readable
2295
+ function summarizeResult(name,content){
2296
+ try{
2297
+ var d=JSON.parse(content);
2298
+ var data=d.data||d;
2299
+ // Calendar events
2300
+ if(data.items&&Array.isArray(data.items)){
2301
+ if(data.items.length===0) return 'No events found';
2302
+ var evts=data.items.slice(0,5).map(function(e){
2303
+ var when='';
2304
+ if(e.start){
2305
+ var dt=e.start.dateTime||e.start.date||'';
2306
+ if(dt){try{var dd=new Date(dt);when=' ('+dd.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'})+', '+dd.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'})+')';}catch(x){}}
2307
+ }
2308
+ return (e.summary||'Untitled')+when;
2309
+ });
2310
+ var more=data.items.length>5?' and '+(data.items.length-5)+' more':'';
2311
+ return 'Found '+data.items.length+' event'+(data.items.length!==1?'s':'')+':\n'+evts.join('\n')+more;
2312
+ }
2313
+ // Email list
2314
+ if(data.messages&&Array.isArray(data.messages)){
2315
+ if(data.messages.length===0) return 'No messages found';
2316
+ return 'Found '+data.messages.length+' message'+(data.messages.length!==1?'s':'');
2317
+ }
2318
+ // Single email/event
2319
+ if(data.summary&&data.start) return 'Event: '+data.summary;
2320
+ if(data.subject) return (data.from?'From '+data.from+': ':'')+data.subject;
2321
+ // Contact
2322
+ if(data.emailAddress||data.email) return (data.displayName||data.name||'Contact')+' — '+(data.emailAddress||data.email);
2323
+ if(Array.isArray(data)&&data.length>0&&data[0].emailAddress) return data.map(function(c){return (c.displayName||c.name||'?')+' <'+(c.emailAddress||c.email)+'>';}).slice(0,5).join('\n');
2324
+ // GitHub
2325
+ if(data.full_name&&data.html_url) return data.full_name;
2326
+ if(data.title&&data.number) return '#'+data.number+' '+data.title;
2327
+ // Generic success
2328
+ if(d.successful===true){
2329
+ if(data.id||data.htmlLink) return 'Done';
2330
+ if(typeof data==='string') return data.slice(0,200);
2331
+ }
2332
+ if(d.successful===false) return 'Failed: '+(d.error||'unknown error');
2333
+ }catch(e){}
2334
+ return null; // couldn't parse, use fallback
2335
+ }
2336
+
2337
+ function friendlyToolCall(name,args){
2338
+ var a=args||{};
2339
+ switch(name){
2340
+ case'send_whatsapp_message':
2341
+ return '<span class="label">Sending WhatsApp to '+escHtml(a.to||'?')+':</span> '+escHtml(a.message||'');
2342
+ case'save_whatsapp_contact':
2343
+ return '<span class="label">Saving contact:</span> '+escHtml(a.name||'?')+' ('+escHtml(a.phone||'?')+')';
2344
+ case'list_whatsapp_contacts':
2345
+ return '<span class="label">Looking up contacts...</span>';
2346
+ case'create_trigger':
2347
+ return '<span class="label">Creating trigger:</span> when '+escHtml(a.from||'anyone')+' says "'+escHtml(a.pattern||'')+'" &rarr; reply "'+escHtml(a.reply||'')+'"';
2348
+ case'list_triggers':
2349
+ return '<span class="label">Checking triggers...</span>';
2350
+ case'delete_trigger':
2351
+ return '<span class="label">Deleting trigger</span> '+escHtml(a.id||'');
2352
+ case'toggle_trigger':
2353
+ return '<span class="label">'+(a.enabled?'Enabling':'Disabling')+' trigger</span> '+escHtml(a.id||'');
2354
+ default:
2355
+ if(name.startsWith('composio_')){
2356
+ var p=parseComposioName(name);
2357
+ var detail=summarizeArgs(a);
2358
+ return '<span class="label">'+escHtml(p.service)+':</span> '+escHtml(p.action)+(detail?' &mdash; '+escHtml(detail):'');
2359
+ }
2360
+ // Detect skill usage from file reads/globs targeting skills/ directory
2361
+ var skillPath=a.file_path||a.path||a.command||'';
2362
+ var skillMatch=skillPath.match(/skills\/([^\/]+)/);
2363
+ if(skillMatch){
2364
+ var skillName=skillMatch[1];
2365
+ return '<span class="label skill-label">Using skill:</span> <span class="skill-name">'+escHtml('skills/'+skillName)+'</span>';
2366
+ }
2367
+ var argsStr=JSON.stringify(a);
2368
+ var preview=argsStr.length>120?argsStr.slice(0,120)+'...':argsStr;
2369
+ return '<span class="label">'+escHtml(name)+'</span><span class="tool-args">'+escHtml(preview)+'</span>';
2370
+ }
2371
+ }
2372
+
2373
+ function friendlyToolResult(name,content,isError){
2374
+ if(isError) return '<span class="label" style="color:#f85149">Failed: </span>'+escHtml(content.slice(0,200));
2375
+ switch(name){
2376
+ case'send_whatsapp_message':
2377
+ case'save_whatsapp_contact':
2378
+ return '<span class="label" style="color:#3fb950">&#10003;</span> '+escHtml(content);
2379
+ case'list_whatsapp_contacts':
2380
+ if(content.startsWith('No saved')) return '<span class="label">No contacts saved yet</span>';
2381
+ return '<span class="label">Contacts:</span><div class="tool-content">'+escHtml(content)+'</div>';
2382
+ case'create_trigger':
2383
+ return '<span class="label" style="color:#3fb950">&#10003; Trigger set up</span>';
2384
+ case'list_triggers':
2385
+ if(content.startsWith('No triggers')) return '<span class="label">No triggers set up</span>';
2386
+ return '<span class="label">Triggers:</span><div class="tool-content">'+escHtml(content)+'</div>';
2387
+ case'delete_trigger':
2388
+ case'toggle_trigger':
2389
+ return '<span class="label" style="color:#3fb950">&#10003;</span> '+escHtml(content);
2390
+ default:
2391
+ if(name.startsWith('composio_')){
2392
+ var friendly=summarizeResult(name,content);
2393
+ if(friendly) return '<span class="label" style="color:#3fb950">&#10003;</span> '+escHtml(friendly).replace(/\n/g,'<br>');
2394
+ // Fallback: just show success/truncated
2395
+ var preview=content.length>200?content.slice(0,200)+'...':content;
2396
+ return '<span class="label" style="color:#3fb950">&#10003;</span> Done';
2397
+ }
2398
+ var preview=content.length>300?content.slice(0,300)+'...':content;
2399
+ return '<span class="label">'+escHtml(name)+':</span><div class="tool-content">'+escHtml(preview)+'</div>';
2400
+ }
2401
+ }
2402
+
2403
+ var audioQueue=[],isPlaying=false,currentSource=null,nextPlayTime=0,audioGain=null,speakerMuted=false;
2404
+ function flushAudio(){if(currentSource){try{currentSource.stop();}catch(e){}}currentSource=null;audioQueue=[];nextPlayTime=0;isPlaying=false;}
2405
+ function playAudioDelta(b){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24000});var bin=atob(b),bytes=new Uint8Array(bin.length);for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);var i16=new Int16Array(bytes.buffer),f32=new Float32Array(i16.length);for(var i=0;i<i16.length;i++)f32[i]=i16[i]/32768;var buf=audioCtx.createBuffer(1,f32.length,24000);buf.getChannelData(0).set(f32);audioQueue.push(buf);if(!isPlaying)playNext();}
2406
+ function ensureGainNode(){if(!audioGain&&audioCtx){audioGain=audioCtx.createGain();audioGain.gain.value=speakerMuted?0:1;audioGain.connect(audioCtx.destination);}}
2407
+ function playNext(){if(audioQueue.length===0){isPlaying=false;return;}isPlaying=true;var buf=audioQueue.shift(),src=audioCtx.createBufferSource();src.buffer=buf;ensureGainNode();src.connect(audioGain);var now=audioCtx.currentTime,st=Math.max(now,nextPlayTime);src.start(st);nextPlayTime=st+buf.duration;src.onended=function(){if(audioQueue.length>0)playNext();else isPlaying=false;};currentSource=src;}
2408
+ function toggleMute(){speakerMuted=!speakerMuted;var btn=document.getElementById('muteBtn');if(speakerMuted){btn.classList.remove('active');btn.querySelector('svg').innerHTML='<line x1="2" x2="22" y1="2" y2="22"/><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>';if(audioGain)audioGain.gain.value=0;flushAudio();}else{btn.classList.add('active');btn.querySelector('svg').innerHTML='<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>';if(audioGain)audioGain.gain.value=1;}}
2409
+
2410
+ async function toggleMic(){if(micActive)stopMic();else await startMic();}
2411
+ async function startMic(){connectWS();if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){appendConv('Microphone requires HTTPS or localhost. Access via https:// or http://localhost:'+location.port,'system');return;}try{micStream=await navigator.mediaDevices.getUserMedia({audio:true});}catch(e){if(e.name==='NotAllowedError')appendConv('Microphone permission denied — check browser settings','system');else if(e.name==='NotFoundError')appendConv('No microphone found','system');else appendConv('Microphone error: '+e.message,'system');return;}if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24000});var src=audioCtx.createMediaStreamSource(micStream);micProcessor=audioCtx.createScriptProcessor(4096,1,1);src.connect(micProcessor);micProcessor.connect(audioCtx.destination);micProcessor.onaudioprocess=function(e){if(!micActive||!ws||ws.readyState!==WebSocket.OPEN)return;var inp=e.inputBuffer.getChannelData(0),i16=new Int16Array(inp.length);for(var i=0;i<inp.length;i++)i16[i]=Math.max(-32768,Math.min(32767,Math.floor(inp[i]*32768)));sendMsg({type:'audio',audio:btoa(String.fromCharCode.apply(null,new Uint8Array(i16.buffer)))});};micActive=true;micBtn.classList.add('active');}
2412
+ function stopMic(){micActive=false;micBtn.classList.remove('active');if(micProcessor){micProcessor.disconnect();micProcessor=null;}if(micStream){micStream.getTracks().forEach(function(t){t.stop();});micStream=null;}}
2413
+
2414
+ var cameraFacing='user';
2415
+ async function toggleCamera(){if(cameraActive)stopCamera();else await startCamera();}
2416
+ async function flipCamera(){if(!cameraActive)return;cameraFacing=cameraFacing==='user'?'environment':'user';stopCamera();await startCamera();}
2417
+ async function startCamera(){connectWS();if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){appendConv('Camera requires HTTPS or localhost. Access via https:// or http://localhost:'+location.port,'system');return;}try{cameraStream=await navigator.mediaDevices.getUserMedia({video:{width:640,height:480,facingMode:cameraFacing}});}catch(e){if(e.name==='NotAllowedError')appendConv('Camera permission denied — check browser settings','system');else if(e.name==='NotFoundError')appendConv('No camera found','system');else appendConv('Camera error: '+e.message,'system');return;}cameraVideo.srcObject=cameraStream;cameraVideo.style.display='block';cameraOff.style.display='none';cameraCanvas.width=640;cameraCanvas.height=480;var ctx=cameraCanvas.getContext('2d');cameraInterval=setInterval(function(){if(!cameraActive||!ws||ws.readyState!==WebSocket.OPEN)return;ctx.drawImage(cameraVideo,0,0,640,480);var u=cameraCanvas.toDataURL('image/jpeg',0.7);sendMsg({type:'video_frame',frame:u.split(',')[1],mimeType:'image/jpeg'});},1000);cameraActive=true;cameraBtn.classList.add('active');}
2418
+ function stopCamera(){cameraActive=false;cameraBtn.classList.remove('active');if(cameraInterval){clearInterval(cameraInterval);cameraInterval=null;}if(cameraStream){cameraStream.getTracks().forEach(function(t){t.stop();});cameraStream=null;}cameraVideo.style.display='none';cameraOff.style.display='';}
2419
+
2420
+ async function toggleScreen(){if(screenActive)stopScreen();else await startScreen();}
2421
+ async function startScreen(){connectWS();try{screenStream=await navigator.mediaDevices.getDisplayMedia({video:{cursor:'always'}});}catch(e){appendConv('Screen sharing cancelled','system');return;}var video=document.createElement('video');video.srcObject=screenStream;video.muted=true;video.playsInline=true;video.play();var canvas=document.createElement('canvas');var ctx=canvas.getContext('2d');screenStream.getVideoTracks()[0].onended=function(){stopScreen();};screenInterval=setInterval(function(){if(!screenActive||!ws||ws.readyState!==WebSocket.OPEN)return;var vw=video.videoWidth||1920,vh=video.videoHeight||1080;var scale=Math.min(960/vw,540/vh,1);canvas.width=Math.round(vw*scale);canvas.height=Math.round(vh*scale);ctx.drawImage(video,0,0,canvas.width,canvas.height);var u=canvas.toDataURL('image/jpeg',0.6);sendMsg({type:'video_frame',frame:u.split(',')[1],mimeType:'image/jpeg',source:'screen'});},2000);screenActive=true;screenBtn.classList.add('active');cameraVideo.srcObject=screenStream;cameraVideo.style.display='block';cameraOff.style.display='none';}
2422
+ function stopScreen(){screenActive=false;screenBtn.classList.remove('active');if(screenInterval){clearInterval(screenInterval);screenInterval=null;}if(screenStream){screenStream.getTracks().forEach(function(t){t.stop();});screenStream=null;}if(!cameraActive){cameraVideo.style.display='none';cameraOff.style.display='';cameraVideo.srcObject=cameraStream;}else{cameraVideo.srcObject=cameraStream;}}
2423
+
2424
+ // FILE ATTACHMENTS
2425
+ var pendingFiles=[];
2426
+ var MAX_FILE_SIZE=10*1024*1024; // 10MB
2427
+
2428
+ function handleFileSelect(fileList){
2429
+ for(var i=0;i<fileList.length;i++){
2430
+ var f=fileList[i];
2431
+ if(f.size>MAX_FILE_SIZE){appendConv('File too large (max 10MB): '+escHtml(f.name),'system');continue;}
2432
+ pendingFiles.push(f);
2433
+ }
2434
+ renderFilePreview();
2435
+ document.getElementById('fileInput').value='';
2436
+ }
2437
+
2438
+ function renderFilePreview(){
2439
+ var bar=document.getElementById('filePreviewBar');
2440
+ bar.innerHTML='';
2441
+ if(pendingFiles.length===0){bar.classList.remove('active');return;}
2442
+ bar.classList.add('active');
2443
+ pendingFiles.forEach(function(f,idx){
2444
+ var chip=document.createElement('div');
2445
+ chip.className='file-chip';
2446
+ var preview='';
2447
+ if(f.type.startsWith('image/')&&f._dataUrl){
2448
+ preview='<img src="'+f._dataUrl+'" alt="preview" />';
2449
+ }
2450
+ chip.innerHTML=preview+'<span>'+escHtml(f.name)+'</span><button class="remove-file" onclick="removeFile('+idx+')">&times;</button>';
2451
+ bar.appendChild(chip);
2452
+ });
2453
+ // Generate image previews
2454
+ pendingFiles.forEach(function(f,idx){
2455
+ if(f.type.startsWith('image/')&&!f._dataUrl){
2456
+ var reader=new FileReader();
2457
+ reader.onload=function(e){f._dataUrl=e.target.result;renderFilePreview();};
2458
+ reader.readAsDataURL(f);
2459
+ }
2460
+ });
2461
+ }
2462
+
2463
+ function removeFile(idx){pendingFiles.splice(idx,1);renderFilePreview();}
2464
+
2465
+ function readFileAsBase64(file){
2466
+ return new Promise(function(resolve){
2467
+ var reader=new FileReader();
2468
+ reader.onload=function(e){
2469
+ var b64=e.target.result.split(',')[1]||'';
2470
+ resolve(b64);
2471
+ };
2472
+ reader.readAsDataURL(file);
2473
+ });
2474
+ }
2475
+
2476
+ async function sendText(){
2477
+ var t=textInput.value.trim();
2478
+ var hasFiles=pendingFiles.length>0;
2479
+ if(!t&&!hasFiles)return;
2480
+ connectWS();
2481
+
2482
+ if(hasFiles){
2483
+ var filesToSend=pendingFiles.slice();
2484
+ pendingFiles=[];
2485
+ renderFilePreview();
2486
+
2487
+ for(var i=0;i<filesToSend.length;i++){
2488
+ var f=filesToSend[i];
2489
+ var b64=await readFileAsBase64(f);
2490
+ var displayText=t||(filesToSend.length===1?'':'');
2491
+ // Show in conversation
2492
+ if(f.type.startsWith('image/')&&f._dataUrl){
2493
+ appendConv('<span class="label">You: </span>'+(t?escHtml(t)+' ':'')+'<br><img src="'+f._dataUrl+'" style="max-width:200px;max-height:150px;border-radius:4px;margin-top:4px;" />','user');
2494
+ } else {
2495
+ appendConv('<span class="label">You: </span>'+(t?escHtml(t)+' ':'')+'[Attached: '+escHtml(f.name)+']','user');
2496
+ }
2497
+ sendMsg({type:'file',name:f.name,mimeType:f.type||'application/octet-stream',data:b64,text:i===0?t:''});
2498
+ t=''; // Only send text with first file
2499
+ }
2500
+ } else {
2501
+ appendConv('<span class="label">You: </span>'+escHtml(t),'user');
2502
+ sendMsg({type:'text',text:t});
2503
+ }
2504
+ textInput.value='';
2505
+ }
2506
+
2507
+ textInput.addEventListener('keydown',function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendText();}});
2508
+
2509
+ // DRAG AND DROP
2510
+ var panelRight=document.querySelector('.panel-right');
2511
+ var dropOverlay=document.getElementById('dropOverlay');
2512
+ var dragCounter=0;
2513
+
2514
+ panelRight.addEventListener('dragenter',function(e){e.preventDefault();dragCounter++;dropOverlay.classList.add('active');});
2515
+ panelRight.addEventListener('dragleave',function(e){e.preventDefault();dragCounter--;if(dragCounter<=0){dragCounter=0;dropOverlay.classList.remove('active');}});
2516
+ panelRight.addEventListener('dragover',function(e){e.preventDefault();});
2517
+ panelRight.addEventListener('drop',function(e){
2518
+ e.preventDefault();dragCounter=0;dropOverlay.classList.remove('active');
2519
+ if(e.dataTransfer.files.length>0)handleFileSelect(e.dataTransfer.files);
2520
+ });
2521
+
2522
+ // CHAT SIDEBAR TOGGLE
2523
+ function toggleChatSidebar(){
2524
+ var sb=document.getElementById('chatSidebar');
2525
+ var tab=document.getElementById('chatEdgeTab');
2526
+ var isCollapsed=sb.classList.toggle('collapsed');
2527
+ tab.classList.toggle('visible',isCollapsed);
2528
+ }
2529
+
2530
+ // RESIZE HANDLES
2531
+ (function(){
2532
+ // Files panel horizontal resize
2533
+ var fpHandle=document.getElementById('fpResizeX');
2534
+ var fp=document.getElementById('filesPanel');
2535
+ fpHandle.addEventListener('mousedown',function(e){
2536
+ e.preventDefault();
2537
+ fpHandle.classList.add('active');
2538
+ var startX=e.clientX, startW=fp.offsetWidth;
2539
+ function onMove(e){
2540
+ var w=startW-(e.clientX-startX);
2541
+ if(w<200)w=200;
2542
+ if(w>window.innerWidth*0.6)w=window.innerWidth*0.6;
2543
+ fp.style.width=w+'px';
2544
+ fp.style.transition='none';
2545
+ }
2546
+ function onUp(){
2547
+ fpHandle.classList.remove('active');
2548
+ fp.style.transition='';
2549
+ document.removeEventListener('mousemove',onMove);
2550
+ document.removeEventListener('mouseup',onUp);
2551
+ }
2552
+ document.addEventListener('mousemove',onMove);
2553
+ document.addEventListener('mouseup',onUp);
2554
+ });
2555
+
2556
+ // Diff viewer vertical resize
2557
+ var dvHandle=document.getElementById('dvResizeY');
2558
+ var dv=document.getElementById('diffViewer');
2559
+ dvHandle.addEventListener('mousedown',function(e){
2560
+ e.preventDefault();
2561
+ dvHandle.classList.add('active');
2562
+ var startY=e.clientY, startH=dv.offsetHeight;
2563
+ function onMove(e){
2564
+ var h=startH-(e.clientY-startY);
2565
+ if(h<80)h=80;
2566
+ var maxH=fp.offsetHeight-120;
2567
+ if(h>maxH)h=maxH;
2568
+ dv.style.height=h+'px';
2569
+ dv.style.transition='none';
2570
+ }
2571
+ function onUp(){
2572
+ dvHandle.classList.remove('active');
2573
+ dv.style.transition='';
2574
+ document.removeEventListener('mousemove',onMove);
2575
+ document.removeEventListener('mouseup',onUp);
2576
+ }
2577
+ document.addEventListener('mousemove',onMove);
2578
+ document.addEventListener('mouseup',onUp);
2579
+ });
2580
+ })();
2581
+
2582
+ // CHAT BRANCHES
2583
+ var currentBranch='',chatBranches=[];
2584
+
2585
+ async function loadChatBranches(){
2586
+ try{
2587
+ var r=await fetch('/api/chat/list');
2588
+ var d=await r.json();
2589
+ if(d.error)return;
2590
+ chatBranches=d.chats||[];
2591
+ currentBranch=d.current||'';
2592
+ renderChatList();
2593
+ document.getElementById('branchName').textContent=currentBranch||'main';
2594
+ }catch(e){}
2595
+ }
2596
+
2597
+ function renderChatList(){
2598
+ var list=document.getElementById('chatList');
2599
+ list.innerHTML='';
2600
+ chatBranches.forEach(function(chat){
2601
+ var item=document.createElement('div');
2602
+ item.className='chat-item'+(chat.branch===currentBranch?' active':'');
2603
+ var icon='<svg class="chat-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
2604
+ var name=chat.name||chat.branch.replace('chat/','');
2605
+ var time=chat.time?'<span class="chat-time">'+escHtml(chat.time)+'</span>':'';
2606
+ var del='<button class="chat-delete" onclick="event.stopPropagation();deleteChat(\''+escHtml(chat.branch)+'\')" title="Delete">&times;</button>';
2607
+ item.innerHTML=icon+'<span class="chat-name">'+escHtml(name)+'</span>'+time+del;
2608
+ (function(branch){item.onclick=function(){switchChat(branch);};})(chat.branch);
2609
+ list.appendChild(item);
2610
+ });
2611
+ }
2612
+
2613
+ async function newChat(){
2614
+ try{
2615
+ var r=await fetch('/api/chat/new',{method:'POST'});
2616
+ var d=await r.json();
2617
+ if(d.error){appendConv('Error: '+escHtml(d.error),'system');return;}
2618
+ currentBranch=d.branch;
2619
+ document.getElementById('branchName').textContent=d.branch;
2620
+ chatBranches.unshift({branch:d.branch,name:d.branch.replace('chat/',''),time:'just now'});
2621
+ renderChatList();
2622
+ // Clear conversation
2623
+ conv.innerHTML='';
2624
+ assistantText='';thinkingEl=null;
2625
+ // Reconnect WS for fresh session
2626
+ if(ws){ws.close();ws=null;}
2627
+ setTimeout(function(){connectWS();loadChatBranches();},300);
2628
+ }catch(e){appendConv('Failed to create new chat','system');}
2629
+ }
2630
+
2631
+ async function restoreChatHistory(branch){
2632
+ try{
2633
+ var r=await fetch('/api/chat/history?branch='+encodeURIComponent(branch));
2634
+ var d=await r.json();
2635
+ isReplay=true;
2636
+ if(d.messages)d.messages.forEach(function(m){handleServerMessage(m);});
2637
+ collapseToolActivity();
2638
+ isReplay=false;
2639
+ }catch(e){}
2640
+ }
2641
+
2642
+ async function switchChat(branch){
2643
+ if(branch===currentBranch)return;
2644
+ try{
2645
+ var r=await fetch('/api/chat/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({branch:branch})});
2646
+ var d=await r.json();
2647
+ if(d.error){appendConv('Error: '+escHtml(d.error),'system');return;}
2648
+ currentBranch=branch;
2649
+ document.getElementById('branchName').textContent=branch;
2650
+ renderChatList();
2651
+ conv.innerHTML='';
2652
+ assistantText='';thinkingEl=null;
2653
+ await restoreChatHistory(branch);
2654
+ if(ws){ws.close();ws=null;}
2655
+ setTimeout(function(){connectWS();loadChatBranches();},300);
2656
+ }catch(e){appendConv('Failed to switch chat','system');}
2657
+ }
2658
+
2659
+ async function deleteChat(branch){
2660
+ if(branch===currentBranch){appendConv('Cannot delete the active chat','system');return;}
2661
+ try{
2662
+ await fetch('/api/chat/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({branch:branch})});
2663
+ loadChatBranches();
2664
+ }catch(e){}
2665
+ }
2666
+
2667
+ // Load chat list on startup and restore history for current branch
2668
+ loadChatBranches().then(function(){
2669
+ if(currentBranch)restoreChatHistory(currentBranch);
2670
+ });
2671
+
2672
+ // INTEGRATIONS
2673
+ var toolkitConnections={};
2674
+ function showGridMessage(msg){
2675
+ var grid=document.getElementById('integrationsGrid');
2676
+ grid.innerHTML='<div class="integrations-empty">'+escHtml(msg)+'</div>';
2677
+ }
2678
+ async function loadToolkits(){
2679
+ var grid=document.getElementById('integrationsGrid');
2680
+ showGridMessage('Loading...');
2681
+ try{
2682
+ var tr=await fetch('/api/composio/toolkits');
2683
+ if(tr.status===501){showGridMessage('Composio not configured. Set COMPOSIO_API_KEY to enable integrations.');return;}
2684
+ var toolkits=await tr.json();
2685
+ if(toolkits.error){showGridMessage(toolkits.error);return;}
2686
+ // Also fetch connections for status
2687
+ var cr=await fetch('/api/composio/connections');
2688
+ var conns=cr.ok?await cr.json():[];
2689
+ toolkitConnections={};
2690
+ if(Array.isArray(conns))conns.forEach(function(c){toolkitConnections[c.toolkitSlug]=c.id;});
2691
+ if(!Array.isArray(toolkits)||toolkits.length===0){showGridMessage('No toolkits available.');return;}
2692
+ grid.innerHTML='';
2693
+ toolkits.forEach(function(tk){
2694
+ var isConn=tk.connected||!!toolkitConnections[tk.slug];
2695
+ var connId=toolkitConnections[tk.slug]||'';
2696
+ var card=document.createElement('div');card.className='toolkit-card';
2697
+ var logoHtml=tk.logo?'<img class="tk-logo" src="'+escHtml(tk.logo)+'" alt="" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tk-logo-placeholder" style="display:none">'+escHtml(tk.name.charAt(0))+'</div>':'<div class="tk-logo-placeholder">'+escHtml(tk.name.charAt(0))+'</div>';
2698
+ var btnClass=isConn?'tk-btn connected':'tk-btn connect';
2699
+ var btnLabel=isConn?'Connected':'Connect';
2700
+ card.innerHTML='<div class="tk-top">'+logoHtml+'<span class="tk-name">'+escHtml(tk.name)+'</span></div><div class="tk-desc">'+escHtml(tk.description)+'</div><div class="tk-actions"><button class="'+btnClass+'" data-slug="'+escHtml(tk.slug)+'" data-conn="'+escHtml(connId)+'" onclick="toggleToolkit(this)">'+btnLabel+'</button></div>';
2701
+ grid.appendChild(card);
2702
+ });
2703
+ }catch(e){showGridMessage('Failed to load integrations.');}
2704
+ }
2705
+
2706
+ async function toggleToolkit(btn){
2707
+ var slug=btn.dataset.slug;var connId=btn.dataset.conn;
2708
+ if(connId){
2709
+ // Disconnect
2710
+ btn.textContent='Disconnecting...';btn.disabled=true;
2711
+ try{await fetch('/api/composio/connections/'+encodeURIComponent(connId),{method:'DELETE'});
2712
+ }catch(e){}
2713
+ loadToolkits();
2714
+ }else{
2715
+ // Connect via OAuth
2716
+ btn.textContent='Connecting...';btn.disabled=true;
2717
+ try{
2718
+ var r=await fetch('/api/composio/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({toolkit:slug,redirectUrl:window.location.origin+'/api/composio/callback'})});
2719
+ var d=await r.json();
2720
+ if(d.redirectUrl){window.open(d.redirectUrl,'composio_auth','width=600,height=700');}
2721
+ else{loadToolkits();}
2722
+ }catch(e){btn.textContent='Connect';btn.disabled=false;}
2723
+ }
2724
+ }
2725
+
2726
+ // Listen for OAuth completion from popup
2727
+ window.addEventListener('message',function(e){
2728
+ if(e.data&&e.data.type==='composio_auth_complete'){loadToolkits();}
2729
+ });
2730
+ // Also refresh when window regains focus (in case popup completed)
2731
+ window.addEventListener('focus',function(){if(currentView==='integrations'&&integrationsLoaded)loadToolkits();});
2732
+
2733
+ // ── SCHEDULER ──────────────────────────────────────────────────────
2734
+ var _schedules=[],_scheduleMode='repeat';
2735
+ function cronToHuman(expr){
2736
+ var map={'0 9 * * *':'Daily at 9:00 AM','0 9 * * 1-5':'Weekdays at 9:00 AM','0 */6 * * *':'Every 6 hours',
2737
+ '*/30 * * * *':'Every 30 minutes','0 9 * * 1':'Weekly on Monday at 9:00 AM','*/1 * * * *':'Every minute'};
2738
+ return map[expr]||expr;
2739
+ }
2740
+ function setScheduleMode(mode){
2741
+ _scheduleMode=mode;
2742
+ var rBtn=document.getElementById('scheduleModeRepeat'),oBtn=document.getElementById('scheduleModeOnce');
2743
+ var cronRow=document.getElementById('scheduleCronRow'),runAtRow=document.getElementById('scheduleRunAtRow');
2744
+ if(mode==='once'){
2745
+ oBtn.style.background='#238636';oBtn.style.borderColor='#2ea043';oBtn.style.color='#fff';
2746
+ rBtn.style.background='#21262d';rBtn.style.borderColor='#30363d';rBtn.style.color='#8b949e';
2747
+ cronRow.style.display='none';runAtRow.style.display='';
2748
+ }else{
2749
+ rBtn.style.background='#238636';rBtn.style.borderColor='#2ea043';rBtn.style.color='#fff';
2750
+ oBtn.style.background='#21262d';oBtn.style.borderColor='#30363d';oBtn.style.color='#8b949e';
2751
+ cronRow.style.display='';runAtRow.style.display='none';
2752
+ }
2753
+ }
2754
+ async function loadSchedules(){
2755
+ try{
2756
+ var r=await fetch('/api/schedules/list');var d=await r.json();
2757
+ _schedules=d.schedules||[];
2758
+ renderScheduleCards();
2759
+ }catch(e){console.error('Failed to load schedules',e);}
2760
+ }
2761
+ function renderScheduleCards(){
2762
+ var el=document.getElementById('schedulesList');
2763
+ if(!_schedules.length){el.innerHTML='<div style="color:#484f58;font-size:13px;text-align:center;padding:20px;">No scheduled jobs yet. Create one above.</div>';return;}
2764
+ el.innerHTML=_schedules.map(function(s){
2765
+ var statusDot=s.lastResult?'<span class="status-dot '+(s.lastResult==='success'?'success':'error')+'"></span>'+s.lastResult:'';
2766
+ var lastRun=s.lastRunAt?new Date(s.lastRunAt).toLocaleString():'Never';
2767
+ var isOnce=s.mode==='once';
2768
+ var timingBadge=isOnce&&s.runAt
2769
+ ?'<span class="schedule-cron" title="Run once at '+new Date(s.runAt).toLocaleString()+'">'+new Date(s.runAt).toLocaleString()+'</span>'
2770
+ :'<span class="schedule-cron" title="'+cronToHuman(s.cron)+'">'+s.cron+'</span>';
2771
+ var modeBadge=isOnce?'<span style="background:rgba(210,153,34,0.15);color:#d29922;padding:2px 6px;border-radius:4px;font-size:10px;margin-left:6px;">once</span>'
2772
+ :'<span style="background:rgba(63,185,80,0.15);color:#3fb950;padding:2px 6px;border-radius:4px;font-size:10px;margin-left:6px;">repeat</span>';
2773
+ var timingDesc=isOnce&&s.runAt?'Runs once at '+new Date(s.runAt).toLocaleString():cronToHuman(s.cron);
2774
+ return '<div class="schedule-card'+(s.enabled?'':' disabled')+'" data-id="'+s.id+'">'
2775
+ +'<div class="schedule-card-header">'
2776
+ +'<span class="schedule-card-id">'+s.id+modeBadge+'</span>'
2777
+ +'<div style="display:flex;align-items:center;gap:8px;">'
2778
+ +timingBadge
2779
+ +'<label class="schedule-toggle"><input type="checkbox" '+(s.enabled?'checked':'')+' onchange="toggleScheduleJob(\''+s.id+'\',this.checked)"><span class="slider"></span></label>'
2780
+ +'</div>'
2781
+ +'</div>'
2782
+ +'<div class="schedule-prompt">'+s.prompt.replace(/</g,'&lt;')+'</div>'
2783
+ +'<div class="schedule-meta">'
2784
+ +'<span>'+timingDesc+'</span>'
2785
+ +'<span>Last run: '+lastRun+'</span>'
2786
+ +'<span>'+statusDot+'</span>'
2787
+ +'</div>'
2788
+ +'<div class="schedule-actions">'
2789
+ +'<button onclick="editScheduleJob(\''+s.id+'\')">Edit</button>'
2790
+ +'<button onclick="runScheduleNow(\''+s.id+'\')">Run Now</button>'
2791
+ +(isOnce&&!s.enabled&&s.lastRunAt?'<button onclick="resetScheduleJob(\''+s.id+'\')" style="border-color:#d29922;color:#d29922;">Reset</button>':'')
2792
+ +'<button class="danger" onclick="deleteScheduleJob(\''+s.id+'\')">Delete</button>'
2793
+ +'</div>'
2794
+ +'</div>';
2795
+ }).join('');
2796
+ }
2797
+ async function saveScheduleJob(){
2798
+ var id=document.getElementById('scheduleId').value.trim();
2799
+ var prompt=document.getElementById('schedulePrompt').value.trim();
2800
+ if(!id||!prompt){alert('Please fill in Job ID and Prompt.');return;}
2801
+ var payload={id:id,prompt:prompt,mode:_scheduleMode,enabled:true};
2802
+ if(_scheduleMode==='once'){
2803
+ var runAt=document.getElementById('scheduleRunAt').value;
2804
+ if(runAt){
2805
+ payload.runAt=new Date(runAt).toISOString();
2806
+ }else{
2807
+ var cronExpr=document.getElementById('scheduleCron').value.trim();
2808
+ if(!cronExpr){alert('Please set a Run At time or switch to Repeat mode.');return;}
2809
+ payload.cron=cronExpr;
2810
+ }
2811
+ }else{
2812
+ var cronExpr=document.getElementById('scheduleCron').value.trim();
2813
+ if(!cronExpr){alert('Please enter a cron expression.');return;}
2814
+ payload.cron=cronExpr;
2815
+ }
2816
+ try{
2817
+ var r=await fetch('/api/schedules/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
2818
+ var d=await r.json();
2819
+ if(!r.ok){alert(d.error||'Failed to save');return;}
2820
+ clearScheduleForm();loadSchedules();
2821
+ }catch(e){alert('Error: '+e.message);}
2822
+ }
2823
+ function clearScheduleForm(){
2824
+ document.getElementById('scheduleId').value='';
2825
+ document.getElementById('scheduleCron').value='';
2826
+ document.getElementById('schedulePrompt').value='';
2827
+ document.getElementById('scheduleRunAt').value='';
2828
+ document.getElementById('scheduleCronPreset').selectedIndex=0;
2829
+ setScheduleMode('repeat');
2830
+ }
2831
+ function applySchedulePreset(){
2832
+ var val=document.getElementById('scheduleCronPreset').value;
2833
+ if(val)document.getElementById('scheduleCron').value=val;
2834
+ }
2835
+ async function toggleScheduleJob(id,enabled){
2836
+ try{
2837
+ await fetch('/api/schedules/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,enabled:enabled})});
2838
+ loadSchedules();
2839
+ }catch(e){console.error(e);}
2840
+ }
2841
+ async function deleteScheduleJob(id){
2842
+ if(!confirm('Delete schedule "'+id+'"?'))return;
2843
+ try{
2844
+ await fetch('/api/schedules/delete',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id})});
2845
+ loadSchedules();
2846
+ }catch(e){alert('Error: '+e.message);}
2847
+ }
2848
+ async function runScheduleNow(id){
2849
+ try{
2850
+ var r=await fetch('/api/schedules/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id})});
2851
+ var d=await r.json();
2852
+ if(r.ok){
2853
+ // Find the card and show running indicator
2854
+ var card=document.querySelector('.schedule-card[data-id="'+id+'"]');
2855
+ if(card){var meta=card.querySelector('.schedule-meta');if(meta)meta.innerHTML='<span style="color:#58a6ff;">Running...</span>';}
2856
+ }else{alert(d.error||'Failed to trigger');}
2857
+ }catch(e){alert('Error: '+e.message);}
2858
+ }
2859
+ function editScheduleJob(id){
2860
+ var s=_schedules.find(function(x){return x.id===id;});
2861
+ if(!s)return;
2862
+ document.getElementById('scheduleId').value=s.id;
2863
+ document.getElementById('scheduleCron').value=s.cron||'';
2864
+ document.getElementById('schedulePrompt').value=s.prompt;
2865
+ setScheduleMode(s.mode||'repeat');
2866
+ if(s.runAt){
2867
+ var d=new Date(s.runAt);
2868
+ var local=new Date(d.getTime()-d.getTimezoneOffset()*60000).toISOString().slice(0,16);
2869
+ document.getElementById('scheduleRunAt').value=local;
2870
+ }else{
2871
+ document.getElementById('scheduleRunAt').value='';
2872
+ }
2873
+ document.getElementById('schedulerView').querySelector('.scheduler-content').scrollTop=0;
2874
+ }
2875
+ async function resetScheduleJob(id){
2876
+ var s=_schedules.find(function(x){return x.id===id;});
2877
+ if(!s)return;
2878
+ if(s.runAt){
2879
+ // For runAt schedules, prompt for a new time
2880
+ var newTime=prompt('Enter new run time (YYYY-MM-DD HH:MM) or leave empty to re-use '+new Date(s.runAt).toLocaleString()+':');
2881
+ var runAt=s.runAt;
2882
+ if(newTime&&newTime.trim()){
2883
+ var parsed=new Date(newTime.trim());
2884
+ if(isNaN(parsed.getTime())){alert('Invalid date format.');return;}
2885
+ runAt=parsed.toISOString();
2886
+ }
2887
+ try{
2888
+ var r=await fetch('/api/schedules/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:s.id,prompt:s.prompt,mode:'once',runAt:runAt,enabled:true})});
2889
+ var d=await r.json();if(!r.ok){alert(d.error||'Failed to reset');return;}
2890
+ loadSchedules();
2891
+ }catch(e){alert('Error: '+e.message);}
2892
+ }else{
2893
+ // For cron-based once schedules, just re-enable
2894
+ try{
2895
+ await fetch('/api/schedules/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,enabled:true})});
2896
+ loadSchedules();
2897
+ }catch(e){alert('Error: '+e.message);}
2898
+ }
2899
+ }
2900
+
2901
+ // ── SETTINGS ────────────────────────────────────────────────────────
2902
+ var _knownModels=[
2903
+ {value:'anthropic:claude-sonnet-4-6',label:'Claude Sonnet 4.6'},
2904
+ {value:'anthropic:claude-opus-4-6',label:'Claude Opus 4.6'},
2905
+ {value:'anthropic:claude-haiku-4-5-20251001',label:'Claude Haiku 4.5'},
2906
+ {value:'openai:gpt-4o',label:'GPT-4o'},
2907
+ {value:'openai:gpt-4o-mini',label:'GPT-4o Mini'},
2908
+ {value:'google:gemini-2.0-flash-001',label:'Gemini 2.0 Flash'},
2909
+ {value:'groq:llama-3.3-70b-versatile',label:'Groq Llama 3.3 70B'},
2910
+ {value:'deepseek:deepseek-chat',label:'DeepSeek Chat'},
2911
+ {value:'',label:'Custom (enter below)'}
2912
+ ];
2913
+ // ── Logs viewer ─────────────────────────────────────────────────────
2914
+ var logEntries=[];
2915
+ var logMaxId=0;
2916
+ var LOG_CLIENT_CAP=5000;
2917
+
2918
+ function loadLogs(){
2919
+ fetch('/api/logs').then(function(r){return r.json();}).then(function(d){
2920
+ if(d.entries&&d.entries.length){
2921
+ logEntries=d.entries;
2922
+ logMaxId=d.entries[d.entries.length-1].id;
2923
+ renderAllLogs();
2924
+ }
2925
+ }).catch(function(){});
2926
+ }
2927
+
2928
+ function appendLogEntry(entry){
2929
+ logEntries.push(entry);
2930
+ if(entry.id>logMaxId)logMaxId=entry.id;
2931
+ if(logEntries.length>LOG_CLIENT_CAP)logEntries=logEntries.slice(logEntries.length-LOG_CLIENT_CAP);
2932
+ if(matchesLogFilter(entry)){
2933
+ var el=renderLogEntry(entry);
2934
+ var out=document.getElementById('logsOutput');
2935
+ out.appendChild(el);
2936
+ if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2937
+ }
2938
+ }
2939
+
2940
+ function renderLogEntry(entry){
2941
+ var row=document.createElement('div');row.className='log-entry';
2942
+ var ts=document.createElement('span');ts.className='log-ts';
2943
+ var d=new Date(entry.ts);
2944
+ ts.textContent=d.toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'})+'.'+String(d.getMilliseconds()).padStart(3,'0');
2945
+ var src=document.createElement('span');
2946
+ src.className='log-src log-src-'+(entry.source||'system');
2947
+ src.textContent=entry.source||'system';
2948
+ var msg=document.createElement('span');
2949
+ msg.className='log-msg log-'+entry.level;
2950
+ msg.textContent=entry.message;
2951
+ row.appendChild(ts);row.appendChild(src);row.appendChild(msg);
2952
+ return row;
2953
+ }
2954
+
2955
+ function matchesLogFilter(entry){
2956
+ var sf=document.getElementById('logSourceFilter').value;
2957
+ var lf=document.getElementById('logLevelFilter').value;
2958
+ var q=(document.getElementById('logSearchInput').value||'').toLowerCase();
2959
+ if(sf&&entry.source!==sf)return false;
2960
+ if(lf&&entry.level!==lf)return false;
2961
+ if(q&&entry.message.toLowerCase().indexOf(q)===-1)return false;
2962
+ return true;
2963
+ }
2964
+
2965
+ function applyLogFilters(){
2966
+ var out=document.getElementById('logsOutput');out.innerHTML='';
2967
+ for(var i=0;i<logEntries.length;i++){
2968
+ if(matchesLogFilter(logEntries[i])){
2969
+ out.appendChild(renderLogEntry(logEntries[i]));
2970
+ }
2971
+ }
2972
+ if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2973
+ }
2974
+
2975
+ function clearLogView(){
2976
+ logEntries=[];
2977
+ document.getElementById('logsOutput').innerHTML='';
2978
+ }
2979
+
2980
+ function renderAllLogs(){
2981
+ var out=document.getElementById('logsOutput');out.innerHTML='';
2982
+ for(var i=0;i<logEntries.length;i++){
2983
+ if(matchesLogFilter(logEntries[i])){
2984
+ out.appendChild(renderLogEntry(logEntries[i]));
2985
+ }
2986
+ }
2987
+ if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2988
+ }
2989
+
2990
+ function loadSettings(){
2991
+ fetch('/api/settings').then(function(r){return r.json();}).then(function(d){
2992
+ var sel=document.getElementById('settingsModel');
2993
+ sel.innerHTML='';
2994
+ _knownModels.forEach(function(m){
2995
+ var opt=document.createElement('option');opt.value=m.value;opt.textContent=m.label;
2996
+ sel.appendChild(opt);
2997
+ });
2998
+ // Set current model
2999
+ if(d.model){
3000
+ var found=_knownModels.some(function(m){return m.value===d.model;});
3001
+ if(found){sel.value=d.model;}
3002
+ else{sel.value='';document.getElementById('settingsCustomModel').value=d.model;}
3003
+ }
3004
+ // Mask keys — show placeholder if set
3005
+ document.getElementById('settingsOpenaiKey').placeholder=d.keys.OPENAI_API_KEY?'•••••••• (set)':'sk-...';
3006
+ document.getElementById('settingsAnthropicKey').placeholder=d.keys.ANTHROPIC_API_KEY?'•••••••• (set)':'sk-ant-...';
3007
+ document.getElementById('settingsGeminiKey').placeholder=d.keys.GEMINI_API_KEY?'•••••••• (set)':'AI...';
3008
+ document.getElementById('settingsComposioKey').placeholder=d.keys.COMPOSIO_API_KEY?'•••••••• (set)':'ak_...';
3009
+ // Base URL
3010
+ if(d.baseUrl)document.getElementById('settingsBaseUrl').value=d.baseUrl;
3011
+ }).catch(function(e){showSettingsStatus('Failed to load settings: '+e.message,'error');});
3012
+ }
3013
+ function showSettingsStatus(msg,type){
3014
+ var el=document.getElementById('settingsStatus');
3015
+ el.style.display='block';
3016
+ el.textContent=msg;
3017
+ el.style.background=type==='error'?'rgba(248,81,73,0.15)':'rgba(63,185,80,0.15)';
3018
+ el.style.color=type==='error'?'#f85149':'#3fb950';
3019
+ el.style.border='1px solid '+(type==='error'?'#f8514966':'#3fb95066');
3020
+ if(type!=='error')setTimeout(function(){el.style.display='none';},4000);
3021
+ }
3022
+ function saveSettings(){
3023
+ var sel=document.getElementById('settingsModel');
3024
+ var model=sel.value||document.getElementById('settingsCustomModel').value;
3025
+ var baseUrl=document.getElementById('settingsBaseUrl').value.trim();
3026
+ var payload={model:model,baseUrl:baseUrl,keys:{}};
3027
+ var openai=document.getElementById('settingsOpenaiKey').value;
3028
+ var anthropic=document.getElementById('settingsAnthropicKey').value;
3029
+ var gemini=document.getElementById('settingsGeminiKey').value;
3030
+ var composio=document.getElementById('settingsComposioKey').value;
3031
+ if(openai)payload.keys.OPENAI_API_KEY=openai;
3032
+ if(anthropic)payload.keys.ANTHROPIC_API_KEY=anthropic;
3033
+ if(gemini)payload.keys.GEMINI_API_KEY=gemini;
3034
+ if(composio)payload.keys.COMPOSIO_API_KEY=composio;
3035
+ document.getElementById('settingsSaving').style.display='inline';
3036
+ fetch('/api/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
3037
+ .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
3038
+ .then(function(res){
3039
+ document.getElementById('settingsSaving').style.display='none';
3040
+ if(res.ok){
3041
+ // Clear password fields immediately (don't leave secrets in the form)
3042
+ document.getElementById('settingsOpenaiKey').value='';
3043
+ document.getElementById('settingsAnthropicKey').value='';
3044
+ document.getElementById('settingsGeminiKey').value='';
3045
+ document.getElementById('settingsComposioKey').value='';
3046
+ // Voice adapter is created per-WebSocket connection from process.env at connect time.
3047
+ // If keys changed, reload so the next connection picks up the new keys and voice works
3048
+ // without a manual restart.
3049
+ var hasKeyChange=!!(payload.keys && Object.keys(payload.keys).length);
3050
+ if(hasKeyChange){
3051
+ showSettingsStatus('Settings saved — reloading to apply new keys…','success');
3052
+ setTimeout(function(){window.location.reload();},900);
3053
+ }else{
3054
+ showSettingsStatus('Settings saved.','success');
3055
+ settingsLoaded=false;loadSettings();settingsLoaded=true;
3056
+ }
3057
+ }else{
3058
+ showSettingsStatus(res.data.error||'Failed to save','error');
3059
+ }
3060
+ }).catch(function(e){
3061
+ document.getElementById('settingsSaving').style.display='none';
3062
+ showSettingsStatus('Error: '+e.message,'error');
3063
+ });
3064
+ }
3065
+
3066
+ // ── TELEGRAM / COMMUNICATION ────────────────────────────────────────
3067
+ async function loadTelegramStatus(){
3068
+ var card=document.getElementById('telegramCard');
3069
+ var statusEl=document.getElementById('tgStatus');
3070
+ var btn=document.getElementById('tgConnectBtn');
3071
+ var tokenInput=document.getElementById('tgToken');
3072
+ var configDiv=document.getElementById('tgConfig');
3073
+ try{
3074
+ var r=await fetch('/api/telegram/status');
3075
+ var d=await r.json();
3076
+ var allowedUsersInput=document.getElementById('tgAllowedUsers');
3077
+ var securityDiv=document.getElementById('tgSecurity');
3078
+ var liveInput=document.getElementById('tgAllowedUsersLive');
3079
+ var usersStr=(d.allowedUsers||[]).map(function(u){return '@'+u;}).join(', ');
3080
+ if(d.connected){
3081
+ card.classList.add('is-connected');
3082
+ statusEl.className='comms-status connected';
3083
+ statusEl.textContent='Connected';
3084
+ btn.className='tk-btn connected';
3085
+ btn.textContent='Disconnect';
3086
+ var existing=card.querySelector('.comms-bot-info');
3087
+ if(existing)existing.remove();
3088
+ if(d.botName){
3089
+ var info=document.createElement('div');
3090
+ info.className='comms-bot-info';
3091
+ info.innerHTML='<span class="bot-name">'+escHtml(d.botName)+'</span><span class="bot-username">@'+escHtml(d.botUsername||'')+'</span>';
3092
+ configDiv.parentNode.insertBefore(info,configDiv);
3093
+ }
3094
+ // Show live security editor when connected
3095
+ securityDiv.style.display='';
3096
+ liveInput.value=usersStr;
3097
+ }else{
3098
+ card.classList.remove('is-connected');
3099
+ statusEl.className='comms-status disconnected';
3100
+ statusEl.textContent='Not connected';
3101
+ btn.className='tk-btn connect';
3102
+ btn.textContent='Connect';
3103
+ var existing2=card.querySelector('.comms-bot-info');
3104
+ if(existing2)existing2.remove();
3105
+ if(d.hasToken)tokenInput.placeholder='Token saved (enter new to change)';
3106
+ securityDiv.style.display='none';
3107
+ if(usersStr)allowedUsersInput.value=usersStr;
3108
+ }
3109
+ }catch(e){
3110
+ statusEl.className='comms-status error';
3111
+ statusEl.textContent='Error';
3112
+ }
3113
+ }
3114
+
3115
+ async function toggleTelegram(){
3116
+ var card=document.getElementById('telegramCard');
3117
+ var btn=document.getElementById('tgConnectBtn');
3118
+ var tokenInput=document.getElementById('tgToken');
3119
+
3120
+ if(card.classList.contains('is-connected')){
3121
+ btn.disabled=true;btn.textContent='Disconnecting...';
3122
+ try{
3123
+ await fetch('/api/telegram/disconnect',{method:'POST'});
3124
+ }catch(e){}
3125
+ btn.disabled=false;
3126
+ loadTelegramStatus();
3127
+ return;
3128
+ }
3129
+
3130
+ var token=tokenInput.value.trim();
3131
+ var allowedUsers=document.getElementById('tgAllowedUsers').value.trim();
3132
+ if(!token&&!tokenInput.placeholder.includes('saved')){
3133
+ tokenInput.style.borderColor='#f85149';
3134
+ tokenInput.focus();
3135
+ return;
3136
+ }
3137
+ btn.disabled=true;btn.textContent='Connecting...';
3138
+ tokenInput.style.borderColor='';
3139
+ try{
3140
+ var r=await fetch('/api/telegram/connect',{
3141
+ method:'POST',
3142
+ headers:{'Content-Type':'application/json'},
3143
+ body:JSON.stringify({token:token||undefined,allowedUsers:allowedUsers||undefined})
3144
+ });
3145
+ var d=await r.json();
3146
+ if(d.error){
3147
+ document.getElementById('tgStatus').className='comms-status error';
3148
+ document.getElementById('tgStatus').textContent=d.error;
3149
+ btn.disabled=false;btn.textContent='Connect';
3150
+ return;
3151
+ }
3152
+ }catch(e){
3153
+ btn.disabled=false;btn.textContent='Connect';
3154
+ return;
3155
+ }
3156
+ btn.disabled=false;
3157
+ tokenInput.value='';
3158
+ loadTelegramStatus();
3159
+ }
3160
+
3161
+ async function saveTgAllowedUsers(){
3162
+ var input=document.getElementById('tgAllowedUsersLive');
3163
+ var users=input.value.trim();
3164
+ try{
3165
+ var r=await fetch('/api/telegram/allowed-users',{
3166
+ method:'POST',
3167
+ headers:{'Content-Type':'application/json'},
3168
+ body:JSON.stringify({users:users})
3169
+ });
3170
+ var d=await r.json();
3171
+ if(d.ok){
3172
+ input.style.borderColor='#3fb950';
3173
+ setTimeout(function(){input.style.borderColor='';},1500);
3174
+ }
3175
+ }catch(e){}
3176
+ }
3177
+
3178
+ // ── WhatsApp ────────────────────────────────────────────────────────
3179
+ var waQrPollTimer=null;
3180
+
3181
+ function renderQR(canvas,text){
3182
+ var qr=qrcode(0,'L');
3183
+ qr.addData(text);
3184
+ qr.make();
3185
+ var count=qr.getModuleCount();
3186
+ var ctx=canvas.getContext('2d');
3187
+ var size=canvas.width;
3188
+ ctx.fillStyle='#fff';ctx.fillRect(0,0,size,size);
3189
+ var cellSize=Math.floor(size/count);
3190
+ var offset=Math.floor((size-cellSize*count)/2);
3191
+ ctx.fillStyle='#000';
3192
+ for(var r=0;r<count;r++)for(var c=0;c<count;c++){
3193
+ if(qr.isDark(r,c))ctx.fillRect(offset+c*cellSize,offset+r*cellSize,cellSize,cellSize);
3194
+ }
3195
+ }
3196
+
3197
+ async function loadWhatsAppStatus(){
3198
+ var card=document.getElementById('whatsappCard');
3199
+ var statusEl=document.getElementById('waStatus');
3200
+ var btn=document.getElementById('waConnectBtn');
3201
+ var qrContainer=document.getElementById('waQrContainer');
3202
+ var clearLabel=document.getElementById('waClearLabel');
3203
+ try{
3204
+ var r=await fetch('/api/whatsapp/status');
3205
+ var d=await r.json();
3206
+ if(d.connected){
3207
+ card.classList.add('is-connected');
3208
+ statusEl.className='comms-status connected';
3209
+ statusEl.textContent='Connected';
3210
+ btn.className='tk-btn connected';
3211
+ btn.textContent='Disconnect';
3212
+ clearLabel.style.display='';
3213
+ qrContainer.style.display='none';
3214
+ if(waQrPollTimer){clearInterval(waQrPollTimer);waQrPollTimer=null;}
3215
+ // Show phone number
3216
+ var existing=card.querySelector('.comms-bot-info');
3217
+ if(existing)existing.remove();
3218
+ if(d.phoneNumber){
3219
+ var info=document.createElement('div');
3220
+ info.className='comms-bot-info';
3221
+ info.innerHTML='<span class="bot-name">'+escHtml(d.phoneNumber)+'</span><span class="bot-username">WhatsApp linked</span>';
3222
+ document.getElementById('waConfig').parentNode.insertBefore(info,document.getElementById('waConfig'));
3223
+ }
3224
+ }else if(d.qrCode){
3225
+ card.classList.remove('is-connected');
3226
+ statusEl.className='comms-status scanning';
3227
+ statusEl.textContent='Scanning';
3228
+ btn.className='tk-btn connect';
3229
+ btn.textContent='Cancel';
3230
+ clearLabel.style.display='none';
3231
+ qrContainer.style.display='';
3232
+ renderQR(document.getElementById('waQrCanvas'),d.qrCode);
3233
+ var existing2=card.querySelector('.comms-bot-info');
3234
+ if(existing2)existing2.remove();
3235
+ }else{
3236
+ card.classList.remove('is-connected');
3237
+ statusEl.className='comms-status disconnected';
3238
+ statusEl.textContent='Not connected';
3239
+ btn.className='tk-btn connect';
3240
+ btn.textContent='Connect';
3241
+ clearLabel.style.display='none';
3242
+ qrContainer.style.display='none';
3243
+ var existing3=card.querySelector('.comms-bot-info');
3244
+ if(existing3)existing3.remove();
3245
+ }
3246
+ }catch(e){
3247
+ statusEl.className='comms-status error';
3248
+ statusEl.textContent='Error';
3249
+ }
3250
+ }
3251
+
3252
+ async function toggleWhatsApp(){
3253
+ var card=document.getElementById('whatsappCard');
3254
+ var btn=document.getElementById('waConnectBtn');
3255
+
3256
+ if(card.classList.contains('is-connected')){
3257
+ // Disconnect
3258
+ btn.disabled=true;btn.textContent='Disconnecting...';
3259
+ var clearAuth=document.getElementById('waClearAuth').checked;
3260
+ try{
3261
+ await fetch('/api/whatsapp/disconnect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({clearAuth:clearAuth})});
3262
+ }catch(e){}
3263
+ btn.disabled=false;
3264
+ document.getElementById('waClearAuth').checked=false;
3265
+ if(waQrPollTimer){clearInterval(waQrPollTimer);waQrPollTimer=null;}
3266
+ loadWhatsAppStatus();
3267
+ return;
3268
+ }
3269
+
3270
+ // If currently scanning (cancel)
3271
+ if(document.getElementById('waStatus').textContent==='Scanning'){
3272
+ btn.disabled=true;btn.textContent='Canceling...';
3273
+ try{await fetch('/api/whatsapp/disconnect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});}catch(e){}
3274
+ btn.disabled=false;
3275
+ if(waQrPollTimer){clearInterval(waQrPollTimer);waQrPollTimer=null;}
3276
+ loadWhatsAppStatus();
3277
+ return;
3278
+ }
3279
+
3280
+ // Connect
3281
+ btn.disabled=true;btn.textContent='Connecting...';
3282
+ try{
3283
+ var r=await fetch('/api/whatsapp/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
3284
+ var d=await r.json();
3285
+ if(d.error){
3286
+ document.getElementById('waStatus').className='comms-status error';
3287
+ document.getElementById('waStatus').textContent=d.error;
3288
+ btn.disabled=false;btn.textContent='Connect';
3289
+ return;
3290
+ }
3291
+ }catch(e){btn.disabled=false;btn.textContent='Connect';return;}
3292
+ btn.disabled=false;
3293
+ // Poll for QR code / connection
3294
+ waQrPollTimer=setInterval(function(){loadWhatsAppStatus();},2000);
3295
+ setTimeout(function(){loadWhatsAppStatus();},500);
3296
+ }
3297
+
3298
+ // ── PHONE / TWILIO ──────────────────────────────────────────────────
3299
+ function loadPhoneWebhookUrl(){
3300
+ var el=document.getElementById('phoneWebhookUrl');
3301
+ if(!el) return;
3302
+ var base=window.location.origin;
3303
+ el.textContent=base+'/api/phone/webhook';
3304
+ }
3305
+ function copyWebhookUrl(btn){
3306
+ var url=document.getElementById('phoneWebhookUrl').textContent;
3307
+ navigator.clipboard.writeText(url).then(function(){
3308
+ btn.textContent='Copied!';
3309
+ btn.classList.add('copied');
3310
+ setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);
3311
+ });
3312
+ }
3313
+
3314
+ // ── AUDIT MODE & FILE EXPLORER ──────────────────────────────────────
3315
+ var ICONS={chevronRight:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"/></svg>',chevronDown:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"/></svg>',folder:'<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>',folderOpen:'<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M5 19h14a2 2 0 001.84-1.22L23 12H5.24a2 2 0 00-1.84 1.22L1 19h2a2 2 0 002-2V7a2 2 0 012-2h4l2 2h6a2 2 0 012 2v1"/></svg>',file:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>',fileCode:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><path d="M10 12l-2 2 2 2"/><path d="M14 12l2 2-2 2"/></svg>'};
3316
+
3317
+ function getFileIconClass(n){var e=n.split('.').pop().toLowerCase();if(['ts','tsx'].includes(e))return'ts';if(['js','jsx','mjs'].includes(e))return'js';if(e==='json')return'json';if(['css','scss','less'].includes(e))return'css';if(['html','htm'].includes(e))return'html';if(['md','txt','rst'].includes(e))return'md';if(['yaml','yml'].includes(e))return'yaml';if(e==='py')return'py';if(['sh','bash','zsh'].includes(e))return'sh';return'default';}
3318
+ function getFileIconSvg(n){var c=getFileIconClass(n);if(['ts','js','css','html','py','sh'].includes(c))return ICONS.fileCode;return ICONS.file;}
3319
+
3320
+ var auditMode=true;
3321
+ var auditBaselineMtimes=null; // snapshot taken when audit mode turns on — never triggers EDITED
3322
+ var fileTreeMtimes={};
3323
+ var diffOpenPath=null;
3324
+ var diffOpenContent='';
3325
+ var refreshDebounceTimer=null;
3326
+ var lastToolWritePath=null;
3327
+ var auditEditedPaths=new Set(); // files confirmed edited since audit started
3328
+ var fileContentCache={}; // path -> content for diff
3329
+ var autoCloseTimer=null;
3330
+
3331
+ function toggleAuditMode(){
3332
+ auditMode=!auditMode;
3333
+ var toggle=document.getElementById('auditToggle');
3334
+ var panel=document.getElementById('filesPanel');
3335
+ toggle.classList.toggle('active',auditMode);
3336
+ panel.classList.toggle('collapsed',!auditMode);
3337
+ if(auditMode){
3338
+ toggle.classList.add('recording');
3339
+ auditEditedPaths.clear();
3340
+ auditBaselineMtimes=null; // will be set on first tree load
3341
+ fileTreeMtimes={};
3342
+ fileContentCache={};
3343
+ closeDiffViewer();
3344
+ loadExplorerTree();
3345
+ }else{
3346
+ toggle.classList.remove('recording');
3347
+ closeDiffViewer();
3348
+ }
3349
+ }
3350
+
3351
+ // Detect file paths from tool_call args for auto-open
3352
+ function detectFileWritePath(toolName,args){
3353
+ if(!args)return null;
3354
+ var tn=toolName.toLowerCase();
3355
+ var isWrite=tn==='write'||tn==='edit'||tn==='write_file'||tn==='edit_file'
3356
+ ||tn==='create_file'||tn==='save_file'||tn==='writefile'||tn==='editfile'
3357
+ ||tn.indexOf('write')!==-1||tn.indexOf('edit')!==-1;
3358
+ if(!isWrite)return null;
3359
+ var path=args.file_path||args.path||args.filepath||args.filename||args.file||null;
3360
+ if(!path)return null;
3361
+ if(typeof path==='string'&&path.startsWith('/')){
3362
+ var parts=path.split('/');
3363
+ for(var i=parts.length-1;i>=0;i--){
3364
+ if(parts[i]==='memory'||parts[i]==='skills'||parts[i]==='src'||parts[i]==='workspace'){
3365
+ return parts.slice(i).join('/');
3366
+ }
3367
+ }
3368
+ return parts[parts.length-1];
3369
+ }
3370
+ return path;
3371
+ }
3372
+
3373
+ // Called after a successful write tool — auto-open inline diff
3374
+ function auditAutoOpenFile(path){
3375
+ auditEditedPaths.add(path);
3376
+ setTimeout(function(){
3377
+ loadExplorerTree();
3378
+ openDiffViewer(path,true,2500);
3379
+ },400);
3380
+ }
3381
+
3382
+ function collectMtimes(entries,out){
3383
+ for(var i=0;i<entries.length;i++){
3384
+ var e=entries[i];
3385
+ if(e.type==='file'&&e.mtime)out[e.path]=e.mtime;
3386
+ if(e.children)collectMtimes(e.children,out);
3387
+ }
3388
+ }
3389
+
3390
+ // Compare mtimes, but only if we have a previous baseline
3391
+ function findChangedSinceLastRefresh(oldMtimes,newMtimes){
3392
+ var changed=[];
3393
+ // If oldMtimes is empty, this is baseline — nothing is "changed"
3394
+ if(Object.keys(oldMtimes).length===0)return changed;
3395
+ for(var p in newMtimes){
3396
+ if(!oldMtimes[p]||newMtimes[p]>oldMtimes[p])changed.push(p);
3397
+ }
3398
+ return changed;
3399
+ }
3400
+
3401
+ function updateChangeCount(){
3402
+ var countEl=document.getElementById('fpCount');
3403
+ var n=auditEditedPaths.size;
3404
+ countEl.textContent=n;
3405
+ countEl.classList.toggle('hidden',n===0);
3406
+ }
3407
+
3408
+ async function loadExplorerTree(){
3409
+ var t=document.getElementById('explorerTree');
3410
+ try{
3411
+ var r=await fetch('/api/files?path=.');
3412
+ var d=await r.json();
3413
+ var newMtimes={};
3414
+ collectMtimes(d.entries,newMtimes);
3415
+
3416
+ // First load sets the baseline — nothing should be marked changed
3417
+ var justChanged=findChangedSinceLastRefresh(fileTreeMtimes,newMtimes);
3418
+
3419
+ // Identify truly new files (not in previous baseline at all)
3420
+ var justNew=[];
3421
+ if(Object.keys(fileTreeMtimes).length>0){
3422
+ for(var j=0;j<justChanged.length;j++){
3423
+ if(!fileTreeMtimes[justChanged[j]])justNew.push(justChanged[j]);
3424
+ }
3425
+ }
3426
+
3427
+ // Only add to auditEditedPaths if these are real changes (not baseline)
3428
+ justChanged.forEach(function(p){auditEditedPaths.add(p);});
3429
+ fileTreeMtimes=newMtimes;
3430
+ updateChangeCount();
3431
+ t.innerHTML='';
3432
+ renderExplorerTree(d.entries,t,0,justChanged,justNew);
3433
+
3434
+ // Auto-expand parents of just-changed files
3435
+ if(justChanged.length>0){
3436
+ justChanged.forEach(function(cp){
3437
+ var parts=cp.split('/');
3438
+ for(var i=1;i<parts.length;i++){
3439
+ var parentPath=parts.slice(0,i).join('/');
3440
+ t.querySelectorAll('.ft-dir').forEach(function(det){
3441
+ if(det.dataset.path===parentPath)det.open=true;
3442
+ });
3443
+ }
3444
+ });
3445
+ // Scroll the first changed item into view in the tree
3446
+ var firstItem=t.querySelector('.ft-item.changed,.ft-item.new-file,summary.changed,summary.new-file');
3447
+ if(firstItem)firstItem.scrollIntoView({behavior:'smooth',block:'nearest'});
3448
+
3449
+ // Auto-open the changed file in the diff viewer
3450
+ if(diffOpenPath&&justChanged.indexOf(diffOpenPath)!==-1){
3451
+ // Currently-open file was updated — refresh it and scroll to diff
3452
+ openDiffViewer(diffOpenPath,true);
3453
+ }else{
3454
+ // Open the first changed file automatically
3455
+ openDiffViewer(justChanged[0],true);
3456
+ }
3457
+ }else if(diffOpenPath&&justChanged&&justChanged.indexOf(diffOpenPath)!==-1){
3458
+ // No new changes detected but the open file was in the changed set
3459
+ openDiffViewer(diffOpenPath,true);
3460
+ }
3461
+ }catch(e){
3462
+ t.innerHTML='<div style="padding:16px;color:#f85149;font-size:12px;">Failed to load</div>';
3463
+ }
3464
+ }
3465
+
3466
+ function renderExplorerTree(entries,parent,depth,justChanged,justNew){
3467
+ for(var i=0;i<entries.length;i++){
3468
+ var entry=entries[i];
3469
+ if(entry.type==='directory'){
3470
+ var dirHasChanges=justChanged&&justChanged.some(function(p){return p.startsWith(entry.path+'/');});
3471
+ var dirHasNew=justNew&&justNew.some(function(p){return p.startsWith(entry.path+'/');});
3472
+ var autoExpand=entry.name==='memory'||dirHasChanges||dirHasNew;
3473
+ var det=document.createElement('details');
3474
+ det.className='ft-dir';
3475
+ det.dataset.path=entry.path;
3476
+ if(autoExpand)det.open=true;
3477
+ var sum=document.createElement('summary');
3478
+ if(dirHasNew)sum.classList.add('new-file');
3479
+ else if(dirHasChanges)sum.classList.add('changed');
3480
+ sum.style.paddingLeft=(depth*12+8)+'px';
3481
+ var dirBadge='';
3482
+ if(dirHasNew)dirBadge='<span class="ft-badge new pop">NEW</span>';
3483
+ else if(dirHasChanges)dirBadge='<span class="ft-badge edited pop">EDITED</span>';
3484
+ sum.innerHTML='<span class="ft-chevron">'+ICONS.chevronRight+'</span><span class="ft-icon folder">'+ICONS.folder+'</span><span class="ft-name">'+escHtml(entry.name)+'</span>'+dirBadge;
3485
+ det.appendChild(sum);
3486
+ var ch=document.createElement('div');
3487
+ ch.className='ft-children';
3488
+ if(entry.children)renderExplorerTree(entry.children,ch,depth+1,justChanged,justNew);
3489
+ det.appendChild(ch);
3490
+ parent.appendChild(det);
3491
+ }else{
3492
+ var fi=document.createElement('div');
3493
+ fi.className='ft-item';
3494
+ var isJustChanged=justChanged&&justChanged.indexOf(entry.path)!==-1;
3495
+ var isJustNew=justNew&&justNew.indexOf(entry.path)!==-1;
3496
+ var wasEdited=auditEditedPaths.has(entry.path);
3497
+ if(isJustNew)fi.classList.add('new-file');
3498
+ else if(isJustChanged)fi.classList.add('changed');
3499
+ if(diffOpenPath===entry.path)fi.classList.add('active-viewer');
3500
+ fi.style.paddingLeft=(depth*12+22)+'px';
3501
+ var badgeHtml='';
3502
+ var gutterHtml='';
3503
+ if(isJustNew){
3504
+ badgeHtml='<span class="ft-badge new pop">NEW</span>';
3505
+ gutterHtml='<span class="ft-gutter edited"></span>';
3506
+ }else if(isJustChanged){
3507
+ badgeHtml='<span class="ft-badge edited pop">EDITED</span>';
3508
+ gutterHtml='<span class="ft-gutter edited"></span>';
3509
+ }else if(wasEdited){
3510
+ badgeHtml='<span class="ft-badge edited" style="opacity:0.45">EDITED</span>';
3511
+ gutterHtml='<span class="ft-gutter edited" style="opacity:0.3"></span>';
3512
+ }
3513
+ fi.innerHTML=gutterHtml+'<span class="ft-icon '+getFileIconClass(entry.name)+'">'+getFileIconSvg(entry.name)+'</span><span class="ft-name">'+escHtml(entry.name)+'</span>'+badgeHtml;
3514
+ fi.onclick=(function(p){return function(){openDiffViewer(p,false);};})(entry.path);
3515
+ fi.dataset.path=entry.path;
3516
+ parent.appendChild(fi);
3517
+ }
3518
+ }
3519
+ }
3520
+
3521
+ // ── Inline diff viewer (inside files panel) ──────────────────────────
3522
+ function previewUrlFor(path){
3523
+ return '/preview/'+path.split('/').map(encodeURIComponent).join('/');
3524
+ }
3525
+ function rawUrlFor(path,download){
3526
+ return '/api/file/raw?path='+encodeURIComponent(path)+(download?'&download=1':'');
3527
+ }
3528
+ function fmtBytes(n){
3529
+ if(!Number.isFinite(n))return '';
3530
+ if(n<1024)return n+' B';
3531
+ if(n<1024*1024)return (n/1024).toFixed(1)+' KB';
3532
+ if(n<1024*1024*1024)return (n/1024/1024).toFixed(1)+' MB';
3533
+ return (n/1024/1024/1024).toFixed(2)+' GB';
3534
+ }
3535
+
3536
+ async function openDiffViewer(path,isAutoEdit,autoCloseMs){
3537
+ if(autoCloseTimer){clearTimeout(autoCloseTimer);autoCloseTimer=null;}
3538
+ var viewer=document.getElementById('diffViewer');
3539
+ var pathEl=document.getElementById('dvPath');
3540
+ var statusEl=document.getElementById('dvStatus');
3541
+ var countdown=document.getElementById('dvCountdown');
3542
+ var pre=document.getElementById('dvPre');
3543
+ var mdToggle=document.getElementById('dvMdToggle');
3544
+ var mdDiv=document.getElementById('dvMarkdown');
3545
+ var imgDiv=document.getElementById('dvImage');
3546
+ var htmlEl=document.getElementById('dvHtml');
3547
+ var pdfEl=document.getElementById('dvPdf');
3548
+ var videoEl=document.getElementById('dvVideo');
3549
+ var audioEl=document.getElementById('dvAudio');
3550
+ var binEl=document.getElementById('dvBinary');
3551
+ var dlBtn=document.getElementById('dvDownload');
3552
+
3553
+ // Highlight active file in tree
3554
+ document.querySelectorAll('#explorerTree .ft-item').forEach(function(el){
3555
+ el.classList.toggle('active-viewer',el.dataset.path===path);
3556
+ });
3557
+
3558
+ var isReopen=(diffOpenPath===path&&viewer.classList.contains('open'));
3559
+ diffOpenPath=path;
3560
+ pathEl.textContent=path.split('/').pop();
3561
+ pathEl.title=path;
3562
+
3563
+ // Reset every panel — strict additive show below.
3564
+ mdToggle.classList.add('hidden');
3565
+ mdToggle.classList.remove('active');
3566
+ pre.style.display='';
3567
+ mdDiv.classList.add('hidden');
3568
+ imgDiv.classList.add('hidden'); imgDiv.innerHTML='';
3569
+ htmlEl.classList.add('hidden'); htmlEl.removeAttribute('src');
3570
+ pdfEl.classList.add('hidden'); pdfEl.removeAttribute('src');
3571
+ videoEl.classList.add('hidden'); videoEl.innerHTML='';
3572
+ audioEl.classList.add('hidden'); audioEl.innerHTML='';
3573
+ binEl.classList.add('hidden'); binEl.innerHTML='';
3574
+ dvMdActive=false;
3575
+ dlBtn.classList.add('hidden');
3576
+
3577
+ if(isAutoEdit){
3578
+ statusEl.className='dv-status edited';
3579
+ statusEl.textContent='EDITED';
3580
+ }else{
3581
+ statusEl.className='dv-status viewing';
3582
+ statusEl.textContent='VIEWING';
3583
+ }
3584
+
3585
+ // Open the viewer panel
3586
+ viewer.classList.add('open');
3587
+ countdown.style.transition='none';
3588
+ countdown.style.width='100%';
3589
+ countdown.classList.remove('done');
3590
+
3591
+ if(!isReopen){
3592
+ pre.innerHTML='<span class="dv-line" style="color:#484f58;">Loading...</span>';
3593
+ }
3594
+
3595
+ // Fetch metadata first so we know how to render before pulling bytes.
3596
+ var meta=null;
3597
+ try{
3598
+ var mr=await fetch('/api/file/meta?path='+encodeURIComponent(path));
3599
+ if(mr.ok)meta=await mr.json();
3600
+ }catch(_){/* fall through to extension-based guess */}
3601
+ if(!meta||!meta.kind){
3602
+ var ext=(path.split('.').pop()||'').toLowerCase();
3603
+ var guess='text';
3604
+ if(/^(png|jpg|jpeg|gif|webp|svg|bmp|ico|avif)$/.test(ext))guess='image';
3605
+ else if(ext==='md'||ext==='markdown')guess='markdown';
3606
+ else if(ext==='html'||ext==='htm')guess='html';
3607
+ else if(ext==='pdf')guess='pdf';
3608
+ else if(/^(mp4|webm|mov|m4v)$/.test(ext))guess='video';
3609
+ else if(/^(mp3|wav|ogg|m4a|aac|flac)$/.test(ext))guess='audio';
3610
+ meta={kind:guess,name:path.split('/').pop(),size:0,mtime:0};
3611
+ }
3612
+
3613
+ var nonText=(meta.kind!=='text'&&meta.kind!=='markdown');
3614
+
3615
+ // HTML — render in sandboxed iframe via path-based /preview route so relative URLs resolve.
3616
+ if(meta.kind==='html'){
3617
+ pre.style.display='none';
3618
+ pre.innerHTML='';
3619
+ htmlEl.src=previewUrlFor(path)+'?t='+Date.now();
3620
+ htmlEl.classList.remove('hidden');
3621
+ dlBtn.classList.remove('hidden');
3622
+ countdown.style.width='0';
3623
+ return;
3624
+ }
3625
+
3626
+ // PDF — browsers render natively in an iframe given the right Content-Type.
3627
+ if(meta.kind==='pdf'){
3628
+ pre.style.display='none';
3629
+ pre.innerHTML='';
3630
+ pdfEl.src=rawUrlFor(path)+'&t='+Date.now();
3631
+ pdfEl.classList.remove('hidden');
3632
+ dlBtn.classList.remove('hidden');
3633
+ countdown.style.width='0';
3634
+ return;
3635
+ }
3636
+
3637
+ // Video / Audio — native elements, server supports Range so they stream.
3638
+ if(meta.kind==='video'){
3639
+ pre.style.display='none'; pre.innerHTML='';
3640
+ var v=document.createElement('video');
3641
+ v.controls=true; v.src=rawUrlFor(path); v.preload='metadata';
3642
+ videoEl.appendChild(v);
3643
+ videoEl.classList.remove('hidden');
3644
+ dlBtn.classList.remove('hidden');
3645
+ countdown.style.width='0';
3646
+ return;
3647
+ }
3648
+ if(meta.kind==='audio'){
3649
+ pre.style.display='none'; pre.innerHTML='';
3650
+ var a=document.createElement('audio');
3651
+ a.controls=true; a.src=rawUrlFor(path); a.preload='metadata';
3652
+ audioEl.appendChild(a);
3653
+ audioEl.classList.remove('hidden');
3654
+ dlBtn.classList.remove('hidden');
3655
+ countdown.style.width='0';
3656
+ return;
3657
+ }
3658
+
3659
+ // Image — keep existing direct render (now keyed off meta).
3660
+ if(meta.kind==='image'){
3661
+ pre.style.display='none'; pre.innerHTML='';
3662
+ imgDiv.classList.remove('hidden');
3663
+ var img=document.createElement('img');
3664
+ img.src=rawUrlFor(path)+'&t='+Date.now();
3665
+ img.alt=path.split('/').pop();
3666
+ img.onload=function(){
3667
+ var m=document.createElement('span');
3668
+ m.className='img-meta';
3669
+ m.textContent=img.naturalWidth+'×'+img.naturalHeight;
3670
+ imgDiv.appendChild(m);
3671
+ };
3672
+ imgDiv.appendChild(img);
3673
+ dlBtn.classList.remove('hidden');
3674
+ countdown.style.width='0';
3675
+ return;
3676
+ }
3677
+
3678
+ // Binary / unviewable — placeholder card + Download. On manual click (not auto-edit), trigger download immediately.
3679
+ if(meta.kind==='binary'){
3680
+ pre.style.display='none'; pre.innerHTML='';
3681
+ var when=meta.mtime?new Date(meta.mtime).toLocaleString():'';
3682
+ binEl.innerHTML=
3683
+ '<div class="bin-icon">📄</div>'+
3684
+ '<div class="bin-name"></div>'+
3685
+ '<div class="bin-meta"></div>'+
3686
+ '<a class="bin-download" download href="'+rawUrlFor(path,true)+'">Download</a>'+
3687
+ '<div class="bin-hint">This file type can’t be previewed in the browser. Download it to open with the right app.</div>';
3688
+ binEl.querySelector('.bin-name').textContent=meta.name||path.split('/').pop();
3689
+ binEl.querySelector('.bin-meta').textContent=[fmtBytes(meta.size),when].filter(Boolean).join(' · ');
3690
+ binEl.classList.remove('hidden');
3691
+ dlBtn.classList.remove('hidden');
3692
+ countdown.style.width='0';
3693
+ if(!isAutoEdit&&!isReopen){
3694
+ // Auto-trigger download when the user manually clicked an unviewable file.
3695
+ // Skip on reopen so re-clicking the same row doesn't repeatedly download.
3696
+ var anchor=binEl.querySelector('a.bin-download');
3697
+ if(anchor)anchor.click();
3698
+ }
3699
+ return;
3700
+ }
3701
+
3702
+ // Markdown vs plain text — existing diff/highlight branch.
3703
+ var isMd=(meta.kind==='markdown');
3704
+ mdToggle.classList.toggle('hidden',!isMd);
3705
+ if(isMd){
3706
+ dvMdActive=true;
3707
+ mdToggle.classList.add('active');
3708
+ pre.style.display='none';
3709
+ mdDiv.classList.remove('hidden');
3710
+ }
3711
+
3712
+ try{
3713
+ var r=await fetch('/api/file?path='+encodeURIComponent(path));
3714
+ var d=await r.json();
3715
+ if(d.error){pre.innerHTML='<span class="dv-line" style="color:#f85149;">'+escHtml(d.error)+'</span>';return;}
3716
+
3717
+ var oldContent=fileContentCache[path]||'';
3718
+ var newContent=d.content||'';
3719
+ var isNewFile=!fileContentCache.hasOwnProperty(path);
3720
+ fileContentCache[path]=newContent;
3721
+ diffOpenContent=newContent;
3722
+
3723
+ var oldLines=oldContent?oldContent.split('\n'):[];
3724
+ var newLines=newContent.split('\n');
3725
+ var hasDiff=(oldContent.length>0&&oldContent!==newContent)||isNewFile;
3726
+
3727
+ pre.innerHTML='';
3728
+ var maxStagger=Math.min(newLines.length,200);
3729
+ for(var i=0;i<newLines.length;i++){
3730
+ var span=document.createElement('span');
3731
+ span.className='dv-line';
3732
+ span.textContent=newLines[i];
3733
+
3734
+ var isChangedLine=hasDiff&&!isNewFile&&(i>=oldLines.length||newLines[i]!==oldLines[i]);
3735
+ var isAddedLine=hasDiff&&(isNewFile||i>=oldLines.length);
3736
+
3737
+ if(isAddedLine){
3738
+ span.classList.add('line-added');
3739
+ var gm=document.createElement('span');
3740
+ gm.className='dv-gutter g-added';
3741
+ span.appendChild(gm);
3742
+ }else if(isChangedLine){
3743
+ span.classList.add('line-changed');
3744
+ var gm2=document.createElement('span');
3745
+ gm2.className='dv-gutter g-modified';
3746
+ span.appendChild(gm2);
3747
+ }
3748
+ // Stagger entry for first open (no previous content)
3749
+ if(!hasDiff&&!isReopen&&i<maxStagger){
3750
+ span.classList.add('line-enter');
3751
+ span.style.animationDelay=(i*6)+'ms';
3752
+ span.style.opacity='0';
3753
+ }
3754
+ pre.appendChild(span);
3755
+ pre.appendChild(document.createTextNode('\n'));
3756
+ }
3757
+
3758
+ // Render markdown preview if .md file
3759
+ if(isMd&&typeof marked!=='undefined'&&newContent){
3760
+ mdDiv.innerHTML=marked.parse(newContent);
3761
+ }
3762
+
3763
+ // Scroll to first changed line
3764
+ if(hasDiff){
3765
+ var firstChanged=pre.querySelector('.line-changed,.line-added');
3766
+ if(firstChanged){
3767
+ setTimeout(function(){
3768
+ firstChanged.scrollIntoView({behavior:'smooth',block:'center'});
3769
+ },80);
3770
+ }
3771
+ }
3772
+
3773
+ // Auto-close countdown for auto-opened edits
3774
+ if(isAutoEdit){
3775
+ var closeDelay=autoCloseMs||5000;
3776
+ var barDuration=(closeDelay-500)/1000; // bar animation slightly shorter
3777
+ // Kick off the countdown bar after a brief paint delay
3778
+ requestAnimationFrame(function(){
3779
+ requestAnimationFrame(function(){
3780
+ countdown.style.transition='width '+barDuration+'s linear';
3781
+ countdown.classList.add('done');
3782
+ });
3783
+ });
3784
+ autoCloseTimer=setTimeout(function(){
3785
+ autoCloseTimer=null;
3786
+ // Only auto-close if still showing this same auto-edit
3787
+ if(diffOpenPath===path)closeDiffViewer();
3788
+ },closeDelay);
3789
+ }else{
3790
+ // Manual open — hide the countdown bar
3791
+ countdown.style.width='0';
3792
+ }
3793
+ }catch(e){
3794
+ pre.innerHTML='<span class="dv-line" style="color:#f85149;">Failed to load</span>';
3795
+ }
3796
+ }
3797
+
3798
+ var dvMdActive=false;
3799
+ function toggleMdView(){
3800
+ var pre=document.getElementById('dvPre');
3801
+ var md=document.getElementById('dvMarkdown');
3802
+ var btn=document.getElementById('dvMdToggle');
3803
+ dvMdActive=!dvMdActive;
3804
+ btn.classList.toggle('active',dvMdActive);
3805
+ if(dvMdActive){
3806
+ pre.style.display='none';
3807
+ md.classList.remove('hidden');
3808
+ if(typeof marked!=='undefined'&&diffOpenContent){
3809
+ md.innerHTML=marked.parse(diffOpenContent);
3810
+ }
3811
+ }else{
3812
+ pre.style.display='';
3813
+ md.classList.add('hidden');
3814
+ }
3815
+ }
3816
+
3817
+ function closeDiffViewer(){
3818
+ if(autoCloseTimer){clearTimeout(autoCloseTimer);autoCloseTimer=null;}
3819
+ document.getElementById('diffViewer').classList.remove('open');
3820
+ document.querySelectorAll('#explorerTree .ft-item.active-viewer').forEach(function(el){
3821
+ el.classList.remove('active-viewer');
3822
+ });
3823
+ // Stop any media / iframe loads to free resources.
3824
+ ['dvHtml','dvPdf'].forEach(function(id){
3825
+ var el=document.getElementById(id);
3826
+ if(el){el.removeAttribute('src');el.classList.add('hidden');}
3827
+ });
3828
+ ['dvVideo','dvAudio','dvBinary','dvImage'].forEach(function(id){
3829
+ var el=document.getElementById(id);
3830
+ if(el){el.innerHTML='';el.classList.add('hidden');}
3831
+ });
3832
+ var dlBtn=document.getElementById('dvDownload');
3833
+ if(dlBtn)dlBtn.classList.add('hidden');
3834
+ diffOpenPath=null;
3835
+ diffOpenContent='';
3836
+ }
3837
+
3838
+ function downloadCurrentFile(){
3839
+ if(!diffOpenPath)return;
3840
+ var a=document.createElement('a');
3841
+ a.href='/api/file/raw?path='+encodeURIComponent(diffOpenPath)+'&download=1';
3842
+ a.download=diffOpenPath.split('/').pop()||'file';
3843
+ document.body.appendChild(a);
3844
+ a.click();
3845
+ document.body.removeChild(a);
3846
+ }
3847
+
3848
+ function refreshFileTree(){
3849
+ if(!auditMode)return;
3850
+ if(refreshDebounceTimer)clearTimeout(refreshDebounceTimer);
3851
+ refreshDebounceTimer=setTimeout(function(){loadExplorerTree();},500);
3852
+ }
3853
+
3854
+ document.addEventListener('keydown',function(e){if(e.key==='Escape'&&diffOpenPath)closeDiffViewer();});
3855
+
3856
+ connectWS();
3857
+ </script>
3858
+ </body>
3859
+ </html>