neohive 6.0.2 → 6.0.3

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/dashboard.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Neohive</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%230d1117'/><path d='M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z' fill='%2358a6ff'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>">
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%230d1117'/><path d='M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z' fill='%23f59e0b'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>">
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@@ -12,64 +12,69 @@
12
12
  * { margin: 0; padding: 0; box-sizing: border-box; }
13
13
 
14
14
  :root {
15
- --bg: #080b12;
16
- --surface: #0f1318;
17
- --surface-2: #161c24;
18
- --surface-3: #1e2530;
19
- --border: rgba(255, 255, 255, 0.06);
20
- --border-light: rgba(255, 255, 255, 0.1);
21
- --text: #f0f4f8;
22
- --text-dim: #94a3b8;
23
- --text-muted: #5a6578;
24
- --accent: #6c8aff;
25
- --accent-dim: rgba(108, 138, 255, 0.12);
26
- --accent-glow: rgba(108, 138, 255, 0.25);
27
- --green: #34d399;
28
- --green-dim: rgba(52, 211, 153, 0.12);
29
- --red: #f87171;
30
- --red-dim: rgba(248, 113, 113, 0.12);
31
- --orange: #fbbf24;
32
- --orange-dim: rgba(251, 191, 36, 0.12);
33
- --purple: #c084fc;
34
- --purple-dim: rgba(192, 132, 252, 0.12);
35
- --yellow: #fbbf24;
36
- --sidebar-w: 280px;
37
- --header-h: 56px;
38
- --glow: 0 0 20px rgba(108, 138, 255, 0.08);
39
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
40
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
41
- --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
42
- --gradient-accent: linear-gradient(135deg, #6c8aff, #a78bfa);
43
- --gradient-surface: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, transparent 100%);
15
+ --bg: #09090b;
16
+ --surface: #111113;
17
+ --surface-2: #1a1a1f;
18
+ --surface-3: #232329;
19
+ --border: rgba(255, 255, 255, 0.07);
20
+ --border-light: rgba(255, 255, 255, 0.12);
21
+ --text: #ececf1;
22
+ --text-dim: #8e8ea0;
23
+ --text-muted: #56566a;
24
+ --accent: #f59e0b;
25
+ --accent-dim: rgba(245, 158, 11, 0.12);
26
+ --accent-glow: rgba(245, 158, 11, 0.2);
27
+ --green: #22c55e;
28
+ --green-dim: rgba(34, 197, 94, 0.12);
29
+ --red: #ef4444;
30
+ --red-dim: rgba(239, 68, 68, 0.12);
31
+ --orange: #f97316;
32
+ --orange-dim: rgba(249, 115, 22, 0.12);
33
+ --purple: #a78bfa;
34
+ --purple-dim: rgba(167, 139, 250, 0.12);
35
+ --yellow: #eab308;
36
+ --text-xs: 12px;
37
+ --text-sm: 13px;
38
+ --text-base: 14px;
39
+ --text-lg: 16px;
40
+ --text-xl: 24px;
41
+ --sidebar-w: 220px;
42
+ --header-h: 48px;
43
+ --glow: 0 0 24px rgba(245, 158, 11, 0.06);
44
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
45
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
46
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6);
47
+ --gradient-accent: linear-gradient(135deg, #f59e0b, #f97316);
48
+ --gradient-surface: linear-gradient(180deg, rgba(255,255,255,0.02) 0%, transparent 100%);
44
49
  }
45
50
 
46
51
  [data-theme="light"] {
47
- --bg: #f0f2f5;
52
+ --bg: #fafaf9;
48
53
  --surface: #ffffff;
49
- --surface-2: #f7f8fa;
50
- --surface-3: #eef0f4;
54
+ --surface-2: #f5f5f4;
55
+ --surface-3: #e7e5e4;
51
56
  --border: rgba(0, 0, 0, 0.08);
52
57
  --border-light: rgba(0, 0, 0, 0.12);
53
- --text: #111827;
54
- --text-dim: #4b5563;
55
- --text-muted: #9ca3af;
56
- --accent: #4f6eff;
57
- --accent-dim: rgba(79, 110, 255, 0.08);
58
- --accent-glow: rgba(79, 110, 255, 0.15);
59
- --green: #059669;
60
- --green-dim: rgba(5, 150, 105, 0.08);
58
+ --text: #0c0a09;
59
+ --text-dim: #57534e;
60
+ --text-muted: #a8a29e;
61
+ --accent: #d97706;
62
+ --accent-dim: rgba(217, 119, 6, 0.08);
63
+ --accent-glow: rgba(217, 119, 6, 0.12);
64
+ --green: #16a34a;
65
+ --green-dim: rgba(22, 163, 74, 0.08);
61
66
  --red: #dc2626;
62
67
  --red-dim: rgba(220, 38, 38, 0.08);
63
- --orange: #d97706;
64
- --orange-dim: rgba(217, 119, 6, 0.08);
68
+ --orange: #ea580c;
69
+ --orange-dim: rgba(234, 88, 12, 0.08);
65
70
  --purple: #7c3aed;
66
71
  --purple-dim: rgba(124, 58, 237, 0.08);
67
- --yellow: #d97706;
68
- --glow: 0 0 20px rgba(79, 110, 255, 0.05);
69
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
70
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
71
- --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12);
72
- --gradient-accent: linear-gradient(135deg, #4f6eff, #7c3aed);
72
+ --yellow: #ca8a04;
73
+ --glow: 0 0 20px rgba(217, 119, 6, 0.05);
74
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
75
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
76
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1);
77
+ --gradient-accent: linear-gradient(135deg, #d97706, #ea580c);
73
78
  --gradient-surface: linear-gradient(180deg, rgba(0,0,0,0.01) 0%, transparent 100%);
74
79
  }
75
80
 
@@ -112,12 +117,10 @@
112
117
 
113
118
  /* ===== HEADER ===== */
114
119
  .header {
115
- background: rgba(15, 19, 24, 0.8);
116
- backdrop-filter: blur(16px) saturate(180%);
117
- -webkit-backdrop-filter: blur(16px) saturate(180%);
120
+ background: var(--bg);
118
121
  border-bottom: 1px solid var(--border);
119
- padding: 0 20px;
120
- height: var(--header-h);
122
+ padding: 0 16px;
123
+ height: 48px;
121
124
  display: flex;
122
125
  align-items: center;
123
126
  justify-content: space-between;
@@ -129,64 +132,53 @@
129
132
  }
130
133
 
131
134
  [data-theme="light"] .header {
132
- background: rgba(255, 255, 255, 0.85);
135
+ background: var(--bg);
133
136
  }
134
137
 
135
138
  .header-left {
136
139
  display: flex;
137
140
  align-items: center;
138
- gap: 16px;
141
+ gap: 12px;
139
142
  }
140
143
 
141
144
  .logo {
142
- font-size: 18px;
143
- font-weight: 800;
144
- background: var(--gradient-accent);
145
- -webkit-background-clip: text;
146
- -webkit-text-fill-color: transparent;
147
- background-clip: text;
145
+ font-size: 15px;
146
+ font-weight: 700;
147
+ color: var(--text);
148
148
  white-space: nowrap;
149
149
  letter-spacing: -0.3px;
150
150
  }
151
151
 
152
152
  .header-stats {
153
- display: flex;
154
- gap: 16px;
155
- align-items: center;
153
+ display: none;
156
154
  }
157
155
 
158
156
  .h-stat {
159
- display: flex;
160
- align-items: center;
161
- gap: 5px;
162
- font-size: 12px;
163
- color: var(--text-dim);
157
+ display: none;
164
158
  }
165
159
 
166
160
  .h-stat-val {
167
- font-weight: 700;
168
- color: var(--accent);
169
- font-size: 13px;
161
+ display: none;
170
162
  }
171
163
 
172
164
  .header-actions {
173
165
  display: flex;
174
166
  align-items: center;
175
- gap: 8px;
167
+ gap: 4px;
176
168
  }
177
169
 
178
170
  .connection {
179
171
  display: flex;
180
172
  align-items: center;
181
173
  gap: 5px;
182
- font-size: 11px;
174
+ font-size: 12px;
183
175
  color: var(--text-dim);
184
- margin-right: 8px;
176
+ margin-right: 4px;
185
177
  }
186
178
 
187
179
  .conn-dot {
188
- width: 6px;
189
- height: 6px;
180
+ width: 7px;
181
+ height: 7px;
190
182
  border-radius: 50%;
191
183
  background: var(--green);
192
184
  box-shadow: 0 0 6px var(--green);
@@ -205,7 +197,7 @@
205
197
  padding: 6px 12px;
206
198
  border-radius: 8px;
207
199
  cursor: pointer;
208
- font-size: 12px;
200
+ font-size: 13px;
209
201
  font-weight: 500;
210
202
  transition: all 0.2s ease;
211
203
  white-space: nowrap;
@@ -246,7 +238,13 @@
246
238
  display: flex;
247
239
  flex-direction: column;
248
240
  overflow: hidden;
249
- transition: transform 0.25s ease;
241
+ transition: width 0.25s ease, min-width 0.25s ease, margin-left 0.25s ease;
242
+ }
243
+ .sidebar.collapsed {
244
+ width: 0;
245
+ min-width: 0;
246
+ border-right: none;
247
+ overflow: hidden;
250
248
  }
251
249
 
252
250
  .sidebar-scroll {
@@ -260,7 +258,7 @@
260
258
  }
261
259
 
262
260
  .sidebar-title {
263
- font-size: 10px;
261
+ font-size: 11px;
264
262
  font-weight: 700;
265
263
  color: var(--text-muted);
266
264
  text-transform: uppercase;
@@ -296,7 +294,7 @@
296
294
  border: 1px solid var(--border);
297
295
  border-radius: 6px;
298
296
  padding: 4px 8px;
299
- font-size: 11px;
297
+ font-size: 12px;
300
298
  color: var(--text);
301
299
  outline: none;
302
300
  min-width: 0;
@@ -307,7 +305,7 @@
307
305
  border: 1px solid var(--border);
308
306
  border-radius: 6px;
309
307
  padding: 4px 4px;
310
- font-size: 10px;
308
+ font-size: 11px;
311
309
  color: var(--text);
312
310
  outline: none;
313
311
  cursor: pointer;
@@ -325,7 +323,7 @@
325
323
  padding: 4px 6px;
326
324
  cursor: pointer;
327
325
  border-radius: 6px;
328
- font-size: 10px;
326
+ font-size: 11px;
329
327
  font-weight: 700;
330
328
  text-transform: uppercase;
331
329
  letter-spacing: 0.5px;
@@ -335,14 +333,14 @@
335
333
  .role-group-header:hover { background: var(--surface-2); }
336
334
  .role-group-header .role-group-arrow {
337
335
  transition: transform 0.2s;
338
- font-size: 8px;
336
+ font-size: 9px;
339
337
  }
340
338
  .role-group-header .role-group-arrow.collapsed { transform: rotate(-90deg); }
341
339
  .role-group-header .role-group-count {
342
340
  background: var(--surface-2);
343
341
  padding: 1px 6px;
344
342
  border-radius: 8px;
345
- font-size: 9px;
343
+ font-size: 10px;
346
344
  color: var(--text-dim);
347
345
  }
348
346
  .role-group-body.collapsed { display: none; }
@@ -352,20 +350,20 @@
352
350
  background: var(--surface-2);
353
351
  background-image: var(--gradient-surface);
354
352
  border: 1px solid var(--border);
355
- border-radius: 10px;
356
- padding: 10px 12px;
357
- margin-bottom: 6px;
353
+ border-radius: 8px;
354
+ padding: 6px 10px;
355
+ margin-bottom: 4px;
358
356
  transition: all 0.25s ease;
357
+ cursor: pointer;
359
358
  }
360
359
 
361
360
  .agent-card:hover { border-color: var(--border-light); box-shadow: var(--shadow-sm); }
362
361
 
363
- .agent-card.sleeping {
364
- border-color: var(--orange);
362
+ .agent-card.sleeping, .agent-card.idle {
365
363
  border-left: 3px solid var(--orange);
366
364
  }
367
365
 
368
- .agent-card.dead {
366
+ .agent-card.dead, .agent-card.offline {
369
367
  opacity: 0.5;
370
368
  }
371
369
 
@@ -373,142 +371,50 @@
373
371
  display: flex;
374
372
  align-items: center;
375
373
  gap: 8px;
376
- margin-bottom: 6px;
377
374
  }
378
375
 
379
376
  .agent-avatar {
380
- width: 28px;
381
- height: 28px;
377
+ width: 26px;
378
+ height: 26px;
382
379
  border-radius: 50%;
383
380
  display: flex;
384
381
  align-items: center;
385
382
  justify-content: center;
386
383
  font-weight: 700;
387
- font-size: 12px;
384
+ font-size: 11px;
388
385
  color: #fff;
389
386
  flex-shrink: 0;
390
387
  }
391
388
 
392
389
  .agent-info { flex: 1; min-width: 0; }
393
390
 
394
- .agent-name {
395
- font-weight: 600;
396
- font-size: 13px;
397
- display: flex;
398
- align-items: center;
399
- gap: 6px;
391
+ .agent-status-dot {
392
+ width: 8px;
393
+ height: 8px;
394
+ border-radius: 50%;
395
+ flex-shrink: 0;
400
396
  }
397
+ .agent-status-dot.active, .agent-status-dot.working { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
398
+ .agent-status-dot.listening { background: #3b82f6; box-shadow: 0 0 6px #3b82f6; animation: pulse 2s infinite; }
399
+ .agent-status-dot.sleeping, .agent-status-dot.idle { background: #f59e0b; animation: pulse 2s infinite; }
400
+ .agent-status-dot.dead, .agent-status-dot.offline { background: #6b7280; }
401
401
 
402
- .agent-badge {
403
- font-size: 9px;
404
- padding: 1px 5px;
405
- border-radius: 6px;
402
+ .agent-name {
406
403
  font-weight: 600;
407
- text-transform: uppercase;
408
- letter-spacing: 0.3px;
409
- }
410
-
411
- .agent-badge.active { background: var(--green-dim); color: var(--green); }
412
- .agent-badge.sleeping { background: var(--orange-dim); color: var(--orange); }
413
- .agent-badge.dead { background: var(--red-dim); color: var(--red); }
414
-
415
- .agent-status-intent {
416
- font-size: 11px;
417
- color: var(--text-dim);
418
- padding: 3px 0;
404
+ font-size: 13px;
419
405
  white-space: nowrap;
420
406
  overflow: hidden;
421
407
  text-overflow: ellipsis;
422
- font-style: italic;
423
- }
424
-
425
- .agent-status-intent::before {
426
- content: '\1F4AD';
427
- margin-right: 4px;
428
- font-style: normal;
429
- }
430
-
431
- .listen-badge {
432
- font-size: 9px;
433
- padding: 2px 6px;
434
- border-radius: 6px;
435
- font-weight: 700;
436
- text-transform: uppercase;
437
- letter-spacing: 0.3px;
438
- display: inline-flex;
439
- align-items: center;
440
- gap: 4px;
441
- margin-top: 5px;
442
- }
443
-
444
- .listen-badge.listening {
445
- background: var(--green-dim);
446
- color: var(--green);
447
- border: 1px solid rgba(63, 185, 80, 0.3);
448
- }
449
-
450
- .listen-badge.busy {
451
- background: var(--orange-dim);
452
- color: var(--yellow);
453
- border: 1px solid rgba(227, 179, 65, 0.3);
454
- }
455
-
456
- .listen-badge.not-listening {
457
- background: var(--red-dim);
458
- color: var(--red);
459
- border: 1px solid rgba(248, 81, 73, 0.3);
460
- animation: pulseAlert 1.5s infinite;
461
- }
462
-
463
- .listen-badge.offline {
464
- background: var(--surface-3);
465
- color: var(--text-muted);
466
- border: 1px solid var(--border);
467
- }
468
-
469
- .listen-dot {
470
- width: 5px;
471
- height: 5px;
472
- border-radius: 50%;
473
- display: inline-block;
474
- }
475
-
476
- .listen-dot.on { background: var(--green); box-shadow: 0 0 8px var(--green); }
477
- .listen-dot.off { background: var(--red); }
478
-
479
- @keyframes pulseAlert {
480
- 0%, 100% { opacity: 1; }
481
- 50% { opacity: 0.6; }
482
408
  }
483
409
 
484
- .agent-meta {
485
- font-size: 10px;
410
+ .agent-subtitle {
411
+ font-size: 12px;
486
412
  color: var(--text-muted);
487
- display: flex;
488
- gap: 8px;
489
- }
490
-
491
- .agent-activity {
492
- font-size: 10px;
493
- color: var(--text-dim);
494
- margin-top: 4px;
495
- display: flex;
496
- align-items: center;
497
- gap: 4px;
498
- }
499
-
500
- .agent-activity-icon {
501
- display: inline-block;
502
- width: 6px;
503
- height: 6px;
504
- border-radius: 50%;
505
- flex-shrink: 0;
413
+ white-space: nowrap;
414
+ overflow: hidden;
415
+ text-overflow: ellipsis;
506
416
  }
507
417
 
508
- .agent-activity-icon.active { background: var(--green); box-shadow: 0 0 4px var(--green); }
509
- .agent-activity-icon.sleeping { background: var(--orange); animation: pulse 2s infinite; }
510
- .agent-activity-icon.dead { background: var(--red); }
511
-
512
418
  .nudge-btn {
513
419
  background: var(--orange-dim);
514
420
  color: var(--orange);
@@ -908,15 +814,15 @@
908
814
  flex-wrap: wrap;
909
815
  }
910
816
 
911
- .msg-from { font-weight: 600; font-size: 13px; }
912
- .msg-arrow { color: var(--text-muted); font-size: 11px; }
913
- .msg-to { font-size: 12px; color: var(--text-dim); }
914
- .msg-time { font-size: 10px; color: var(--text-muted); }
817
+ .msg-from { font-weight: 600; font-size: 14px; }
818
+ .msg-arrow { color: var(--text-muted); font-size: 12px; }
819
+ .msg-to { font-size: 13px; color: var(--text-dim); }
820
+ .msg-time { font-size: 11px; color: var(--text-muted); }
915
821
 
916
822
  .badge-channel {
917
823
  background: var(--purple-dim);
918
824
  color: var(--purple);
919
- font-size: 9px;
825
+ font-size: 10px;
920
826
  padding: 1px 5px;
921
827
  border-radius: 6px;
922
828
  font-weight: 600;
@@ -937,7 +843,7 @@
937
843
  .channel-filter-bar::-webkit-scrollbar { display: none; }
938
844
 
939
845
  .channel-tab {
940
- font-size: 11px;
846
+ font-size: 12px;
941
847
  padding: 3px 10px;
942
848
  border-radius: 12px;
943
849
  border: 1px solid var(--border);
@@ -963,7 +869,7 @@
963
869
  }
964
870
 
965
871
  .badge {
966
- font-size: 9px;
872
+ font-size: 10px;
967
873
  padding: 2px 6px;
968
874
  border-radius: 6px;
969
875
  font-weight: 600;
@@ -975,16 +881,16 @@
975
881
 
976
882
  /* ===== MARKDOWN CONTENT ===== */
977
883
  .msg-content {
978
- font-size: 13px;
884
+ font-size: 14px;
979
885
  line-height: 1.6;
980
886
  word-break: break-word;
981
887
  color: var(--text);
982
888
  }
983
889
 
984
- .msg-content h1 { font-size: 20px; font-weight: 700; margin: 12px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
985
- .msg-content h2 { font-size: 17px; font-weight: 700; margin: 10px 0 5px; padding-bottom: 3px; border-bottom: 1px solid var(--border); }
986
- .msg-content h3 { font-size: 15px; font-weight: 600; margin: 8px 0 4px; }
987
- .msg-content h4 { font-size: 14px; font-weight: 600; margin: 6px 0 3px; }
890
+ .msg-content h1 { font-size: 22px; font-weight: 700; margin: 12px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
891
+ .msg-content h2 { font-size: 18px; font-weight: 700; margin: 10px 0 5px; padding-bottom: 3px; border-bottom: 1px solid var(--border); }
892
+ .msg-content h3 { font-size: 16px; font-weight: 600; margin: 8px 0 4px; }
893
+ .msg-content h4 { font-size: 15px; font-weight: 600; margin: 6px 0 3px; }
988
894
 
989
895
  .msg-content p { margin: 4px 0; }
990
896
 
@@ -1200,8 +1106,8 @@
1200
1106
  }
1201
1107
 
1202
1108
  .empty-icon { font-size: 40px; opacity: 0.2; }
1203
- .empty-text { font-size: 15px; }
1204
- .empty-sub { font-size: 12px; color: var(--text-muted); }
1109
+ .empty-text { font-size: 16px; }
1110
+ .empty-sub { font-size: 13px; color: var(--text-muted); }
1205
1111
 
1206
1112
  /* ===== MESSAGE FLASH ===== */
1207
1113
  .message-new { animation: flashIn 0.8s ease-out; }
@@ -1489,28 +1395,272 @@
1489
1395
  30% { opacity: 1; transform: translateY(-3px); }
1490
1396
  }
1491
1397
 
1492
- /* ===== VIEW TABS ===== */
1493
- .view-tabs {
1398
+ /* ===== UNIFIED NAV SIDEBAR ===== */
1399
+ .nav-sidebar {
1400
+ width: 220px;
1401
+ min-width: 220px;
1402
+ background: var(--surface);
1403
+ border-right: 1px solid var(--border);
1404
+ display: flex;
1405
+ flex-direction: column;
1406
+ overflow-y: auto;
1407
+ overflow-x: hidden;
1408
+ flex-shrink: 0;
1409
+ transition: width 0.25s ease, min-width 0.25s ease;
1410
+ }
1411
+ .nav-sidebar.collapsed {
1412
+ width: 56px;
1413
+ min-width: 56px;
1414
+ }
1415
+ .nav-sidebar-header {
1416
+ display: flex;
1417
+ align-items: center;
1418
+ justify-content: space-between;
1419
+ padding: 16px 16px 12px;
1420
+ flex-shrink: 0;
1421
+ }
1422
+ .nav-sidebar-logo {
1423
+ font-size: 13px;
1424
+ font-weight: 700;
1425
+ color: var(--text-dim);
1426
+ white-space: nowrap;
1427
+ overflow: hidden;
1428
+ letter-spacing: 0.02em;
1429
+ }
1430
+ .nav-sidebar.collapsed .nav-sidebar-logo { display: none; }
1431
+ .nav-sidebar-toggle {
1432
+ background: none;
1433
+ border: none;
1434
+ color: var(--text-muted);
1435
+ font-size: 14px;
1436
+ cursor: pointer;
1437
+ padding: 6px 8px;
1438
+ border-radius: 6px;
1439
+ transition: all 0.15s;
1440
+ font-family: inherit;
1441
+ flex-shrink: 0;
1442
+ }
1443
+ .nav-sidebar-toggle:hover { background: rgba(255,255,255,0.06); color: var(--text); }
1444
+ .nav-sidebar.collapsed .nav-sidebar-header { justify-content: center; padding: 14px 8px 10px; }
1445
+ .nav-sidebar.collapsed .nav-sidebar-toggle { margin: 0; }
1446
+ .nav-sidebar-section { padding: 4px 6px; }
1447
+ .nav-section-main { display: flex; flex-direction: column; gap: 1px; }
1448
+ .nav-sidebar-divider { height: 1px; background: var(--border); margin: 10px 14px; flex-shrink: 0; }
1449
+ .nav-sidebar-label {
1450
+ font-size: 10px;
1451
+ font-weight: 600;
1452
+ color: var(--text-muted);
1453
+ letter-spacing: 0.08em;
1454
+ padding: 8px 14px 6px;
1455
+ white-space: nowrap;
1456
+ overflow: hidden;
1457
+ text-transform: uppercase;
1458
+ }
1459
+ .nav-sidebar.collapsed .nav-sidebar-label { display: none; }
1460
+ .nav-sidebar.collapsed .nav-sidebar-project { display: none; }
1461
+ .nav-sidebar.collapsed .nav-sidebar-agents #agents-list { display: none; }
1462
+ .nav-sidebar-spacer { flex: 1; }
1463
+ .nav-sidebar-agents { overflow-y: auto; min-height: 0; }
1464
+
1465
+ /* Nav items */
1466
+ .nav-item {
1467
+ display: flex;
1468
+ align-items: center;
1469
+ gap: 10px;
1470
+ padding: 8px 12px;
1471
+ cursor: pointer;
1472
+ border-radius: 8px;
1473
+ transition: all 0.15s;
1474
+ white-space: nowrap;
1475
+ overflow: hidden;
1476
+ margin: 1px 6px;
1477
+ position: relative;
1478
+ }
1479
+ .nav-item:hover { background: rgba(255,255,255,0.04); }
1480
+ .nav-item.active { background: var(--accent-dim); }
1481
+ .nav-item .nav-icon { line-height: 0; flex-shrink: 0; width: 20px; min-width: 20px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); }
1482
+ .nav-item .nav-icon svg { flex-shrink: 0; width: 16px; height: 16px; }
1483
+ .nav-item.active .nav-icon { color: var(--accent); }
1484
+ .nav-item:hover .nav-icon { color: var(--text); }
1485
+ .nav-item .nav-text { font-size: 13px; font-weight: 500; color: var(--text-dim); }
1486
+ .nav-item.active .nav-text { color: var(--accent); font-weight: 600; }
1487
+ .nav-item:hover .nav-text { color: var(--text); }
1488
+ .nav-sidebar.collapsed .nav-item { justify-content: center; padding: 10px 4px; margin: 0; }
1489
+ .nav-sidebar.collapsed .nav-item .nav-text { display: none; }
1490
+
1491
+ /* ===== AGENT STATUS BAR ===== */
1492
+ .agent-bar {
1494
1493
  display: flex;
1494
+ align-items: center;
1495
+ gap: 6px;
1496
+ padding: 6px 12px;
1495
1497
  background: var(--surface);
1496
1498
  border-bottom: 1px solid var(--border);
1499
+ overflow-x: auto;
1500
+ flex-shrink: 0;
1501
+ min-height: 36px;
1502
+ }
1503
+ .agent-bar::-webkit-scrollbar { height: 0; }
1504
+ .agent-pill {
1505
+ display: flex;
1506
+ align-items: center;
1507
+ gap: 5px;
1508
+ padding: 3px 10px 3px 6px;
1509
+ border-radius: 20px;
1510
+ background: var(--surface-2);
1511
+ border: 1px solid var(--border);
1512
+ font-size: 13px;
1513
+ white-space: nowrap;
1514
+ cursor: pointer;
1515
+ transition: all 0.15s;
1516
+ flex-shrink: 0;
1497
1517
  }
1518
+ .agent-pill:hover { border-color: var(--accent); }
1519
+ .agent-pill .status-dot {
1520
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
1521
+ }
1522
+ .agent-pill.online .status-dot, .agent-pill.working .status-dot { background: #22c55e; }
1523
+ .agent-pill.listening .status-dot { background: #3b82f6; animation: pulse 2s infinite; }
1524
+ .agent-pill.idle .status-dot { background: #f59e0b; }
1525
+ .agent-pill.offline .status-dot { background: #6b7280; opacity: 0.6; }
1526
+ .agent-pill .agent-name { font-weight: 600; color: var(--text); }
1527
+ .agent-pill .agent-activity { color: var(--text-muted); font-size: 12px; }
1528
+ .agent-pill.offline .agent-name { color: var(--text-muted); opacity: 0.6; }
1498
1529
 
1499
- .view-tab {
1500
- flex: 1;
1501
- text-align: center;
1502
- padding: 10px 8px;
1503
- font-size: 12px;
1504
- font-weight: 600;
1530
+ /* ===== OVERVIEW PAGE ===== */
1531
+ .overview-area { display: none; flex-direction: column; gap: 24px; padding: 28px 32px; overflow-y: auto; }
1532
+ .overview-area.visible { display: flex; }
1533
+
1534
+ .overview-welcome {
1535
+ margin-bottom: 4px;
1536
+ }
1537
+ .overview-welcome h2 {
1538
+ font-size: 22px;
1539
+ font-weight: 700;
1540
+ color: var(--text);
1541
+ margin-bottom: 4px;
1542
+ letter-spacing: -0.3px;
1543
+ }
1544
+ .overview-welcome p {
1545
+ font-size: 13px;
1505
1546
  color: var(--text-muted);
1506
- cursor: pointer;
1507
- border-bottom: 2px solid transparent;
1508
- transition: all 0.2s ease;
1509
- letter-spacing: 0.02em;
1510
1547
  }
1511
1548
 
1512
- .view-tab:hover { color: var(--text); background: rgba(255,255,255,0.02); }
1513
- .view-tab.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--accent-dim); }
1549
+ .overview-metrics {
1550
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;
1551
+ }
1552
+ .metric-card {
1553
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
1554
+ padding: 20px 22px; display: flex; flex-direction: column; gap: 6px;
1555
+ position: relative; overflow: hidden;
1556
+ transition: border-color 0.2s, box-shadow 0.2s;
1557
+ }
1558
+ .metric-card:hover { border-color: var(--border-light); box-shadow: var(--shadow-sm); }
1559
+ .metric-card .metric-icon {
1560
+ width: 36px; height: 36px; border-radius: 10px;
1561
+ display: flex; align-items: center; justify-content: center;
1562
+ margin-bottom: 4px; flex-shrink: 0;
1563
+ }
1564
+ .metric-card .metric-label { font-size: 12px; color: var(--text-muted); font-weight: 500; letter-spacing: 0.02em; }
1565
+ .metric-card .metric-value { font-size: 32px; font-weight: 800; color: var(--text); line-height: 1.1; letter-spacing: -0.5px; }
1566
+ .metric-card .metric-sub { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
1567
+
1568
+ .overview-grid {
1569
+ display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
1570
+ }
1571
+ .overview-panel {
1572
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px;
1573
+ transition: border-color 0.2s;
1574
+ }
1575
+ .overview-panel:hover { border-color: var(--border-light); }
1576
+ .overview-panel h3 {
1577
+ font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 14px;
1578
+ display: flex; align-items: center; justify-content: space-between;
1579
+ }
1580
+ .overview-panel h3 a { font-size: 12px; font-weight: 500; color: var(--text-muted); cursor: pointer; text-decoration: none; transition: color 0.15s; }
1581
+ .overview-panel h3 a:hover { color: var(--accent); }
1582
+ .overview-agent-row {
1583
+ display: flex; align-items: center; gap: 10px; padding: 8px 0;
1584
+ border-bottom: 1px solid var(--border);
1585
+ }
1586
+ .overview-agent-row:last-child { border-bottom: none; }
1587
+ .overview-agent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
1588
+ .overview-agent-name { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }
1589
+ .overview-agent-status { font-size: 12px; color: var(--text-muted); }
1590
+ .overview-msg-row {
1591
+ display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; align-items: baseline;
1592
+ }
1593
+ .overview-msg-row:last-child { border-bottom: none; }
1594
+ .overview-msg-from { font-weight: 600; color: var(--text); min-width: 80px; font-size: 13px; }
1595
+ .overview-msg-text { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; font-size: 13px; }
1596
+ .overview-msg-time { color: var(--text-muted); font-size: 11px; flex-shrink: 0; }
1597
+ .overview-full-width { grid-column: 1 / -1; }
1598
+ .overview-task-summary { display: flex; gap: 10px; flex-wrap: wrap; }
1599
+ .overview-task-badge {
1600
+ padding: 6px 14px; border-radius: 8px; font-size: 13px; font-weight: 600;
1601
+ }
1602
+ .overview-task-badge.pending { background: rgba(234, 179, 8, 0.1); color: var(--yellow); }
1603
+ .overview-task-badge.active { background: rgba(245, 158, 11, 0.1); color: var(--accent); }
1604
+ .overview-task-badge.done { background: rgba(63, 185, 80, 0.1); color: var(--green); }
1605
+ .overview-launch-btn {
1606
+ display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px;
1607
+ background: var(--gradient-accent); color: #fff; border: none; border-radius: 10px;
1608
+ font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; margin-top: 12px;
1609
+ transition: all 0.2s; box-shadow: 0 2px 8px var(--accent-glow);
1610
+ }
1611
+ .overview-launch-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
1612
+
1613
+ /* ===== TOAST NOTIFICATIONS ===== */
1614
+ .toast-container {
1615
+ position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 9999;
1616
+ display: flex; flex-direction: column; gap: 8px; pointer-events: none; align-items: center;
1617
+ }
1618
+ .toast {
1619
+ background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
1620
+ padding: 12px 20px; font-size: 13px; color: var(--text); box-shadow: var(--shadow-lg);
1621
+ pointer-events: auto; display: flex; align-items: center; gap: 10px;
1622
+ animation: toast-in 0.3s ease-out;
1623
+ max-width: 420px;
1624
+ }
1625
+ .toast.toast-error { background: var(--red-dim); border-color: var(--red); color: var(--red); }
1626
+ .toast.toast-success { background: var(--green-dim); border-color: var(--green); }
1627
+ .toast.leaving { animation: toast-out 0.3s ease-in forwards; }
1628
+ .toast-icon { font-size: 16px; flex-shrink: 0; }
1629
+ .toast-text { flex: 1; }
1630
+ @keyframes toast-in { from { opacity: 0; transform: translateY(-12px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
1631
+ @keyframes toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(12px); } }
1632
+
1633
+ /* ===== USER MESSAGES ===== */
1634
+ .message.user-msg { background: rgba(245, 158, 11, 0.04); border-left: 2px solid var(--accent); }
1635
+
1636
+ /* Hide old view-tabs (replaced by nav sidebar) */
1637
+ .view-tabs { display: none !important; }
1638
+
1639
+ /* ===== SETTINGS MENU ===== */
1640
+ .header-settings-btn {
1641
+ background: none; border: none; color: var(--text-muted); cursor: pointer;
1642
+ padding: 6px; border-radius: 6px; display: flex; align-items: center;
1643
+ transition: all 0.15s;
1644
+ }
1645
+ .header-settings-btn:hover { background: rgba(255,255,255,0.06); color: var(--text); }
1646
+ .settings-item {
1647
+ padding: 8px 14px; font-size: 12px; color: var(--text-dim); cursor: pointer;
1648
+ display: flex; align-items: center; gap: 8px; transition: background 0.1s;
1649
+ }
1650
+ .settings-item:hover { background: rgba(255,255,255,0.04); color: var(--text); }
1651
+ .settings-item.settings-danger { color: var(--red); }
1652
+ .settings-item.settings-danger:hover { background: var(--red-dim); }
1653
+
1654
+ /* (metric-card styles consolidated in overview section above) */
1655
+
1656
+ /* ===== BUTTON SYSTEM ===== */
1657
+ .btn-primary { background: var(--gradient-accent); color: #fff; border: none; }
1658
+ .btn-secondary { background: transparent; color: var(--text-dim); border: 1px solid var(--border); }
1659
+ .btn-secondary:hover { border-color: var(--accent); color: var(--text); }
1660
+ .btn-ghost { background: none; border: none; color: var(--text-muted); }
1661
+ .btn-ghost:hover { color: var(--text); }
1662
+ .btn-danger { background: transparent; color: var(--red); border: 1px solid var(--red-dim); }
1663
+ .btn-danger:hover { background: var(--red-dim); }
1514
1664
 
1515
1665
  /* ===== LAN BADGE ===== */
1516
1666
  .lan-badge {
@@ -2093,7 +2243,7 @@
2093
2243
  @media (max-width: 768px) {
2094
2244
  :root {
2095
2245
  --sidebar-w: 280px;
2096
- --header-h: 50px;
2246
+ --header-h: 48px;
2097
2247
  }
2098
2248
 
2099
2249
  .mobile-toggle { display: block; }
@@ -2201,12 +2351,23 @@
2201
2351
 
2202
2352
  /* Scroll-to-bottom button: bigger touch target */
2203
2353
  .scroll-bottom { width: 44px; height: 44px; font-size: 18px; bottom: 80px; }
2354
+
2355
+ /* Icon rail: hide on mobile */
2356
+ .nav-sidebar { display: none; }
2357
+
2358
+ /* Agent bar: smaller on mobile */
2359
+ .agent-bar { padding: 4px 8px; min-height: 30px; }
2360
+ .agent-pill { font-size: 10px; padding: 2px 8px 2px 5px; }
2361
+
2362
+ /* Overview: single column on mobile */
2363
+ .overview-grid { grid-template-columns: 1fr; }
2364
+ .overview-metrics { grid-template-columns: 1fr; }
2204
2365
  }
2205
2366
 
2206
2367
  /* ===== MOBILE: PHONE (480px) ===== */
2207
2368
  @media (max-width: 480px) {
2208
2369
  :root {
2209
- --header-h: 46px;
2370
+ --header-h: 44px;
2210
2371
  }
2211
2372
 
2212
2373
  .header { padding: 0 8px; }
@@ -2581,11 +2742,47 @@
2581
2742
  .launch-area {
2582
2743
  flex: 1;
2583
2744
  overflow-y: auto;
2584
- padding: 20px;
2745
+ padding: 28px 32px;
2585
2746
  display: none;
2586
2747
  }
2587
-
2588
2748
  .launch-area.visible { display: block; }
2749
+ .launch-section {
2750
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
2751
+ padding: 20px; margin-bottom: 20px;
2752
+ }
2753
+ .launch-section h3 { font-size: 16px; font-weight: 700; margin-bottom: 16px; color: var(--text); }
2754
+ .launch-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
2755
+ .launch-form-field { display: flex; flex-direction: column; gap: 4px; }
2756
+ .launch-form-field label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
2757
+ .launch-form-field input, .launch-form-field select {
2758
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
2759
+ padding: 8px 12px; font-size: 13px; color: var(--text); outline: none; transition: border-color 0.15s;
2760
+ }
2761
+ .launch-form-field input:focus, .launch-form-field select:focus { border-color: var(--accent); }
2762
+ .launch-form-field input.invalid { border-color: var(--red); }
2763
+ .launch-form-field .field-hint { font-size: 11px; color: var(--text-muted); min-height: 16px; }
2764
+ .launch-form-field .field-hint.error { color: var(--red); }
2765
+ .launch-form-field .field-hint.success { color: var(--green); }
2766
+ .launch-prompt-output {
2767
+ background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
2768
+ padding: 12px; font-size: 12px; font-family: 'SFMono-Regular', Consolas, monospace;
2769
+ color: var(--text); line-height: 1.5; max-height: 200px; overflow-y: auto;
2770
+ white-space: pre-wrap; cursor: pointer; position: relative; transition: border-color 0.15s;
2771
+ }
2772
+ .launch-prompt-output:hover { border-color: var(--accent); }
2773
+ .template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
2774
+ .template-card {
2775
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px;
2776
+ padding: 16px; cursor: pointer; transition: all 0.15s;
2777
+ }
2778
+ .template-card:hover { border-color: var(--accent); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
2779
+ .template-card-name { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
2780
+ .template-card-desc { font-size: 12px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.4; }
2781
+ .template-card-agents { display: flex; flex-wrap: wrap; gap: 4px; }
2782
+ .template-agent-chip {
2783
+ font-size: 11px; padding: 2px 8px; border-radius: 6px;
2784
+ background: var(--accent-dim); color: var(--accent); font-weight: 500;
2785
+ }
2589
2786
 
2590
2787
  /* ===== RULES VIEW ===== */
2591
2788
  .rules-area {
@@ -2697,7 +2894,7 @@
2697
2894
  .docs-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
2698
2895
  .docs-badge-beta { background: rgba(210, 153, 34, 0.15); color: #d29922; }
2699
2896
  .docs-badge-new { background: rgba(63, 185, 80, 0.15); color: #3fb950; }
2700
- .docs-badge-stable { background: rgba(88, 166, 255, 0.15); color: #58a6ff; }
2897
+ .docs-badge-stable { background: rgba(245, 158, 11, 0.15); color: var(--accent); }
2701
2898
  .docs-phase-diagram { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 10px 0; }
2702
2899
  .docs-phase-box { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 14px; font-size: 12px; font-weight: 600; color: var(--text); }
2703
2900
  .docs-phase-arrow { color: var(--text-muted); font-size: 14px; }
@@ -3125,10 +3322,7 @@
3125
3322
  color: var(--text-muted);
3126
3323
  }
3127
3324
 
3128
- /* ===== v3.0: PLUGINS SECTION ===== */
3129
- /* Plugins removed in v3.4.3 */
3130
-
3131
- /* ===== v3.0: AVATAR PICKER ===== */
3325
+ /* ===== AVATAR PICKER ===== */
3132
3326
  .avatar-option {
3133
3327
  width: 44px;
3134
3328
  height: 44px;
@@ -3329,49 +3523,36 @@
3329
3523
  <div class="header">
3330
3524
  <div class="header-left">
3331
3525
  <button class="mobile-toggle" onclick="toggleSidebar()" aria-label="Menu">&#9776;</button>
3332
- <img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%230d1117'/><path d='M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z' fill='%2358a6ff'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>" alt="" style="height:28px;margin-right:4px;vertical-align:middle">
3526
+ <svg width="18" height="18" viewBox="0 0 32 32" style="flex-shrink:0"><defs><linearGradient id="hg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f59e0b"/><stop offset="100%" stop-color="#f97316"/></linearGradient></defs><path d="M16 2L27.6 9v14L16 30 4.4 23V9z" fill="url(#hg)" opacity="0.9"/><path d="M16 7L23 11v8l-7 4-7-4v-8z" fill="#09090b" opacity="0.6"/><circle cx="16" cy="15.5" r="3" fill="#fff" opacity="0.9"/></svg>
3333
3527
  <div class="logo">Neohive</div>
3334
- <span id="mobile-project-name" style="display:none;font-size:11px;color:var(--orange);font-weight:600;background:var(--orange-dim);padding:2px 8px;border-radius:10px" onclick="toggleSidebar()"></span>
3335
- <select id="mobile-project-select" class="mobile-project-select" onchange="mobileSelectProject(this.value)" style="display:none">
3336
- <option value="">Select project...</option>
3337
- </select>
3338
- <div class="header-stats">
3339
- <div class="h-stat"><span class="h-stat-val" id="stat-messages">0</span> msgs</div>
3340
- <div class="h-stat"><span class="h-stat-val" id="stat-agents">0</span> agents</div>
3341
- <div class="h-stat"><span class="h-stat-val" id="stat-sleeping" style="color:var(--orange)">0</span> sleeping</div>
3342
- <div class="h-stat"><span class="h-stat-val" id="stat-threads">0</span> threads</div>
3343
- <div class="h-stat"><span class="h-stat-val" id="stat-clis">0</span> CLI types</div>
3344
- <div class="h-stat" id="managed-badge" style="display:none;background:rgba(88,166,255,0.15);border-radius:8px;padding:2px 8px">
3345
- <span class="h-stat-val" style="color:#58a6ff;font-size:11px" id="managed-phase">MANAGED</span>
3346
- <span style="color:#8b949e;font-size:10px" id="managed-floor-info"></span>
3347
- </div>
3348
- </div>
3528
+ <!-- Hidden stat elements for JS compatibility -->
3529
+ <span id="stat-messages" style="display:none">0</span>
3530
+ <span id="stat-agents" style="display:none">0</span>
3531
+ <span id="stat-sleeping" style="display:none">0</span>
3532
+ <span id="stat-threads" style="display:none">0</span>
3533
+ <span id="stat-clis" style="display:none">0</span>
3534
+ <div id="managed-badge" style="display:none"><span id="managed-phase"></span><span id="managed-floor-info"></span></div>
3349
3535
  </div>
3350
3536
  <div class="header-actions">
3537
+ <span class="conn-dot" title="Connected"></span>
3538
+ <!-- Settings dropdown -->
3351
3539
  <div style="position:relative;display:inline-block">
3352
- <button class="notif-toggle" id="notif-bell" onclick="toggleNotifPanel()" title="Notifications" style="position:relative">&#x1f514;<span id="notif-badge" style="display:none;position:absolute;top:-4px;right:-4px;background:var(--red);color:#fff;font-size:8px;font-weight:700;min-width:14px;height:14px;border-radius:7px;text-align:center;line-height:14px;padding:0 3px">0</span></button>
3353
- <div id="notif-panel" style="display:none;position:absolute;right:0;top:100%;margin-top:8px;background:var(--surface);border:1px solid var(--border-light);border-radius:12px;width:320px;max-height:400px;overflow-y:auto;z-index:300;box-shadow:var(--shadow-lg)">
3354
- <div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center">Notifications <button onclick="clearNotifications()" style="background:none;border:none;color:var(--text-muted);font-size:11px;cursor:pointer">Clear</button></div>
3355
- <div id="notif-list" style="padding:4px 0"><div style="padding:16px;text-align:center;color:var(--text-muted);font-size:12px">No notifications yet</div></div>
3540
+ <button class="header-settings-btn" onclick="toggleSettingsMenu()" title="Settings">
3541
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M2.9 2.9l1.4 1.4M11.7 11.7l1.4 1.4M13.1 2.9l-1.4 1.4M4.3 11.7l-1.4 1.4"/></svg>
3542
+ </button>
3543
+ <div id="settings-menu" style="display:none;position:absolute;right:0;top:100%;margin-top:6px;background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;z-index:300;min-width:180px;box-shadow:var(--shadow-lg)">
3544
+ <div class="settings-item" onclick="toggleTheme();toggleSettingsMenu()"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="4"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg> Theme</div>
3545
+ <div class="settings-item" id="settings-notif-item" onclick="toggleCombinedNotifications();"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6a4 4 0 018 0v3l2 2H2l2-2z"/><path d="M6 13a2 2 0 004 0"/></svg> <span id="settings-notif-label">Notifications</span></div>
3546
+ <div style="height:1px;background:var(--border);margin:4px 8px"></div>
3547
+ <div class="settings-item" onclick="exportShareableHTML();toggleSettingsMenu()">Export HTML</div>
3548
+ <div class="settings-item" onclick="exportJSON();toggleSettingsMenu()">Export JSON</div>
3549
+ <div class="settings-item" onclick="enterReplay();toggleSettingsMenu()">Replay</div>
3550
+ <div class="settings-item" onclick="openPhoneAccess();toggleSettingsMenu()">Phone Access</div>
3551
+ <div class="settings-item" onclick="switchView('docs');toggleSettingsMenu()">Help &amp; Docs</div>
3552
+ <div style="height:1px;background:var(--border);margin:4px 8px"></div>
3553
+ <div class="settings-item settings-danger" onclick="doReset();toggleSettingsMenu()">Reset Data</div>
3356
3554
  </div>
3357
3555
  </div>
3358
- <button class="notif-toggle" id="notif-toggle" onclick="toggleNotifications()" title="Browser notifications">&#x1f514;</button>
3359
- <button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light theme">&#x1f319;</button>
3360
- <span onclick="toggleShortcutsOverlay()" style="font-size:10px;color:var(--text-muted);cursor:pointer;padding:0 4px" title="Keyboard shortcuts">? keys</span>
3361
- <button class="sound-toggle" id="sound-toggle" onclick="toggleSound()" title="Toggle notification sound">&#x1f508;</button>
3362
- <button class="phone-btn" id="phone-btn" onclick="openPhoneAccess()" title="Phone access (LAN)">&#x1F4F1;</button>
3363
- <div class="connection"><span class="conn-dot"></span><span id="conn-label">Live</span><span class="conn-detail" id="conn-detail"></span></div>
3364
- <button class="btn" onclick="enterReplay()" id="replay-header-btn" title="Replay conversation">Replay</button>
3365
- <div style="position:relative;display:inline-block">
3366
- <button class="btn btn-primary" onclick="toggleExportMenu()">Export &#x25BE;</button>
3367
- <div id="export-menu" style="display:none;position:absolute;right:0;top:100%;margin-top:4px;background:var(--surface-2);border:1px solid var(--border);border-radius:6px;overflow:hidden;z-index:200;min-width:160px">
3368
- <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportShareableHTML();toggleExportMenu()">HTML (shareable)</div>
3369
- <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportConversation();toggleExportMenu()">Markdown (.md)</div>
3370
- <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportJSON();toggleExportMenu()">JSON (.json)</div>
3371
- <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s;border-top:1px solid var(--border)" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportReplay();toggleExportMenu()">Animated Replay (.html)</div>
3372
- </div>
3373
- </div>
3374
- <button class="btn btn-danger" onclick="doReset()">Reset</button>
3375
3556
  </div>
3376
3557
  </div>
3377
3558
 
@@ -3406,85 +3587,76 @@
3406
3587
 
3407
3588
  <!-- APP LAYOUT -->
3408
3589
  <div class="app">
3409
- <div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
3410
-
3411
- <!-- SIDEBAR -->
3412
- <div class="sidebar" id="sidebar">
3413
- <!-- Project Switcher -->
3414
- <div class="project-switcher">
3415
- <select class="project-select" id="project-select" onchange="switchProject()">
3416
- <option value="">Default (local)</option>
3417
- </select>
3418
- <input class="project-input" id="project-path-input" placeholder="Enter project folder path..."
3419
- onkeydown="if(event.key==='Enter'){addProject();}">
3420
- <div class="project-actions">
3421
- <button class="btn btn-primary" onclick="showAddProject()">+ Add</button>
3422
- <button class="btn" onclick="discoverProjects()">Discover</button>
3423
- <button class="btn btn-danger" onclick="removeProject()" id="remove-project-btn" style="display:none">Remove</button>
3424
- </div>
3425
- <div id="discover-results" style="display:none"></div>
3590
+ <!-- UNIFIED NAV SIDEBAR -->
3591
+ <nav class="nav-sidebar expanded" id="nav-sidebar">
3592
+ <div class="nav-sidebar-header">
3593
+ <span class="nav-sidebar-logo"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="var(--accent)" stroke-width="1.5" style="vertical-align:-2px;margin-right:6px"><path d="M8 1.5L14 5v6l-6 3.5L2 11V5z"/></svg>Neohive</span>
3594
+ <button class="nav-sidebar-toggle" onclick="toggleNavSidebar()" title="Collapse sidebar">&#x25C0;</button>
3426
3595
  </div>
3427
- <div class="sidebar-scroll">
3428
- <!-- Agents Section -->
3429
- <div class="sidebar-section">
3430
- <div class="sidebar-title">
3431
- <span>Agents</span>
3432
- <span class="alert-count" id="alert-count" style="display:none">0</span>
3433
- </div>
3434
- <div class="agent-filter-bar" id="agent-filter-bar">
3435
- <input type="text" id="agent-search" placeholder="Search..." oninput="filterAgents()">
3436
- <select id="agent-role-filter" onchange="filterAgents()">
3437
- <option value="">Role</option>
3438
- <option value="lead">Lead</option>
3439
- <option value="backend">Backend</option>
3440
- <option value="frontend">Frontend</option>
3441
- <option value="quality">Quality</option>
3442
- <option value="monitor">Monitor</option>
3443
- <option value="advisor">Advisor</option>
3444
- </select>
3445
- <select id="agent-status-filter" onchange="filterAgents()">
3446
- <option value="">Status</option>
3447
- <option value="active">Online</option>
3448
- <option value="dead">Offline</option>
3449
- <option value="sleeping">Idle</option>
3450
- </select>
3451
- </div>
3452
- <div id="agents-list"></div>
3596
+ <!-- Navigation -->
3597
+ <div class="nav-sidebar-section nav-section-main">
3598
+ <div class="nav-item active" data-view="overview" onclick="switchView('overview')">
3599
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1.5L14 5v6l-6 3.5L2 11V5z"/></svg></span><span class="nav-text">Overview</span>
3453
3600
  </div>
3454
-
3455
- <!-- Agent Stats Section -->
3456
- <div class="sidebar-section">
3457
- <div class="sidebar-title"><span>Stats</span></div>
3458
- <div id="agent-stats"></div>
3601
+ <div class="nav-item" data-view="messages" onclick="switchView('messages')">
3602
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3h12v8H6l-3 2.5V11H2z" stroke-linejoin="round"/></svg></span><span class="nav-text">Messages</span>
3459
3603
  </div>
3460
-
3461
- <!-- Activity Heatmap -->
3462
- <div class="sidebar-section">
3463
- <div class="sidebar-title"><span>Activity</span></div>
3464
- <div id="activity-heatmap"></div>
3604
+ <div class="nav-item" data-view="tasks" onclick="switchView('tasks')">
3605
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><path d="M5 8l2 2 4-4"/></svg></span><span class="nav-text">Tasks</span>
3465
3606
  </div>
3466
-
3467
- <!-- Bookmarks Section -->
3468
- <div class="sidebar-section">
3469
- <div class="sidebar-title">
3470
- <span>Bookmarks</span>
3471
- <span class="alert-count" id="bookmark-count" style="display:none">0</span>
3472
- </div>
3473
- <div id="bookmarks-list"></div>
3474
- <div class="filter-clear" id="bookmark-clear" onclick="clearBookmarkFilter()" style="display:none">&#x2715; Clear filter</div>
3607
+ <div class="nav-item" data-view="workflows" onclick="switchView('workflows')">
3608
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 8a6 6 0 0111 0M14 8a6 6 0 01-11 0"/><path d="M13 5v3h-3M3 11v-3h3"/></svg></span><span class="nav-text">Workflows</span>
3475
3609
  </div>
3476
-
3477
- <!-- Threads Section -->
3478
- <div class="sidebar-section">
3479
- <div class="sidebar-title"><span>Threads</span></div>
3480
- <div id="threads-list"></div>
3481
- <div class="filter-clear" id="filter-clear" onclick="clearThreadFilter()">&#x2715; Clear filter</div>
3610
+ <div class="nav-item" data-view="stats" onclick="switchView('stats')">
3611
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 14V8M8 14V3M13 14V6"/></svg></span><span class="nav-text">Analytics</span>
3612
+ </div>
3613
+ <div class="nav-item" data-view="launch" onclick="switchView('launch')">
3614
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v12M2 8h12"/></svg></span><span class="nav-text">Launch</span>
3482
3615
  </div>
3483
3616
  </div>
3484
- </div>
3617
+ <div class="nav-sidebar-divider"></div>
3618
+ <!-- Agents -->
3619
+ <div class="nav-sidebar-section nav-sidebar-agents">
3620
+ <div class="nav-sidebar-label">AGENTS</div>
3621
+ <div id="agents-list"></div>
3622
+ </div>
3623
+ <div class="nav-sidebar-divider"></div>
3624
+ <!-- Project -->
3625
+ <div class="nav-sidebar-section nav-sidebar-project">
3626
+ <div class="nav-sidebar-label" style="display:flex;align-items:center;justify-content:space-between;gap:6px">
3627
+ <span>PROJECT</span>
3628
+ <span style="display:flex;align-items:center;gap:2px">
3629
+ <button type="button" id="remove-project-btn" onclick="removeProject()" title="Remove selected project from this list (does not delete .neohive data on disk)" aria-label="Remove project"
3630
+ style="display:none;background:none;border:none;color:var(--text-muted);cursor:pointer;padding:2px 4px;line-height:1;border-radius:4px;opacity:0.85"
3631
+ onmouseenter="this.style.color='#f87171'" onmouseleave="this.style.color='var(--text-muted)'">
3632
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.25" aria-hidden="true"><path d="M2 4h12"/><path d="M5 4V2.5h6V4"/><path d="M6 7v5M10 7v5"/><path d="M3 4v9a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4"/></svg>
3633
+ </button>
3634
+ <button type="button" onclick="showAddProject()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Add project">+</button>
3635
+ </span>
3636
+ </div>
3637
+ <select class="project-select" id="project-select" onchange="switchProject()" style="width:100%;margin-top:4px;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px">
3638
+ <option value="">Default (local)</option>
3639
+ </select>
3640
+ <button type="button" id="remove-project-text-btn" class="btn" onclick="removeProject()" style="display:none;width:100%;margin-top:6px;padding:5px 8px;font-size:11px;background:var(--surface-2);border:1px solid var(--border);color:var(--text-muted)" title="Removes this folder from the dashboard only">Remove project from list</button>
3641
+ <input class="project-input" id="project-path-input" placeholder="Repo root (not …/.neohive)" title="Use the folder you open in Cursor — not the .neohive subfolder" onkeydown="if(event.key==='Enter')addProject();if(event.key==='Escape')this.classList.remove('visible');" style="margin-top:4px">
3642
+ </div>
3643
+ <div class="nav-sidebar-spacer"></div>
3644
+ <!-- Bottom nav -->
3645
+ <div class="nav-sidebar-section">
3646
+ <div class="nav-item" data-view="plan" onclick="switchView('plan')">
3647
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 2l10 6-10 6z"/></svg></span><span class="nav-text">Plan</span>
3648
+ </div>
3649
+ <div class="nav-item" data-view="rules" onclick="switchView('rules')">
3650
+ <span class="nav-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1L2 4v4c0 3.5 2.5 6.5 6 7.5 3.5-1 6-4 6-7.5V4z"/></svg></span><span class="nav-text">Rules</span>
3651
+ </div>
3652
+ </div>
3653
+ </nav>
3485
3654
 
3486
3655
  <!-- MAIN -->
3487
3656
  <div class="main" style="position:relative">
3657
+ <!-- Agent Status Bar -->
3658
+ <div class="agent-bar" id="agent-bar"></div>
3659
+
3488
3660
  <div class="replay-bar" id="replay-bar">
3489
3661
  <div class="replay-controls">
3490
3662
  <button class="replay-btn" onclick="replayPlayPause()" id="replay-play">&#x25B6;</button>
@@ -3525,6 +3697,7 @@
3525
3697
  <div id="pinned-list"></div>
3526
3698
  </div>
3527
3699
  <div class="messages-area" id="messages"></div>
3700
+ <div class="overview-area visible" id="overview-area"></div>
3528
3701
  <div class="tasks-area" id="tasks-area"></div>
3529
3702
  <div class="workspaces-area" id="workspaces-area"></div>
3530
3703
  <div class="workflows-area" id="workflows-area"></div>
@@ -3569,6 +3742,7 @@
3569
3742
  <div class="profile-popup-bio" id="pp-bio"></div>
3570
3743
  <div class="profile-popup-stats" id="pp-stats"></div>
3571
3744
  <button class="btn btn-primary" style="width:100%;margin-top:10px" id="pp-edit-btn" onclick="openProfileEditor()">Edit Profile</button>
3745
+ <button class="btn btn-danger" style="width:100%;margin-top:6px" id="pp-delete-btn" onclick="confirmDeleteAgent()">Delete Agent</button>
3572
3746
  </div>
3573
3747
 
3574
3748
  <!-- Character Designer Panel -->
@@ -3579,8 +3753,6 @@
3579
3753
  <button class="cd-close" onclick="closeProfileEditor()">&times;</button>
3580
3754
  </div>
3581
3755
  <div class="cd-body">
3582
- <!-- 3D Preview -->
3583
- <div class="cd-preview" id="cd-preview-container"></div>
3584
3756
 
3585
3757
  <!-- Info Section: Avatar + Name/Role/Bio -->
3586
3758
  <div class="cd-info-section">
@@ -3831,8 +4003,8 @@
3831
4003
  </div>
3832
4004
  <div class="cd-color-row">
3833
4005
  <label>Shirt color</label>
3834
- <input type="color" id="pe-shirt-color" value="#58a6ff" onchange="cdOnChange()">
3835
- <span class="cd-color-hex" id="cd-shirt-hex">#58a6ff</span>
4006
+ <input type="color" id="pe-shirt-color" value="#f59e0b" onchange="cdOnChange()">
4007
+ <span class="cd-color-hex" id="cd-shirt-hex">#f59e0b</span>
3836
4008
  </div>
3837
4009
  <div class="cd-color-row">
3838
4010
  <label>Pants color</label>
@@ -3967,6 +4139,9 @@ function lttFetch(url, opts) {
3967
4139
 
3968
4140
  var activeThread = null;
3969
4141
  var activeChannel = null; // null = all channels
4142
+ // Safe element getter — returns dummy div for removed elements to prevent null crashes
4143
+ function $id(id) { return document.getElementById(id) || document.createElement('div'); }
4144
+
3970
4145
  var activeProject = ''; // empty = default/local
3971
4146
  var cachedHistory = [];
3972
4147
  var cachedAgents = {};
@@ -3980,8 +4155,8 @@ window.addEventListener('resize', function() { isMobile = window.innerWidth <= 7
3980
4155
 
3981
4156
  // Agent color palette
3982
4157
  var COLORS = [
3983
- '#58a6ff','#3fb950','#d29922','#f85149','#bc8cff',
3984
- '#f778ba','#79c0ff','#7ee787','#e3b341','#ffa198'
4158
+ '#f59e0b','#f97316','#3fb950','#d29922','#f85149',
4159
+ '#f778ba','#7ee787','#e3b341','#ffa198','#14b8a6'
3985
4160
  ];
3986
4161
  var COLORS_LIGHT = [
3987
4162
  '#0969da','#1a7f37','#9a6700','#cf222e','#8250df',
@@ -4197,7 +4372,7 @@ function renderTable(rows) {
4197
4372
 
4198
4373
  // ==================== AGENT MONITORING ====================
4199
4374
 
4200
- var PROVIDER_COLORS = { claude: '#d97706', anthropic: '#d97706', openai: '#10b981', gemini: '#3b82f6', google: '#3b82f6' };
4375
+ var PROVIDER_COLORS = { claude: '#d97706', anthropic: '#d97706', openai: '#10b981', gemini: '#3b82f6', google: '#3b82f6', cursor: '#6366f1' };
4201
4376
 
4202
4377
  function getProviderBadge(provider) {
4203
4378
  if (!provider) return '';
@@ -4212,6 +4387,7 @@ var agentFilterStatus = '';
4212
4387
  var collapsedRoleGroups = {};
4213
4388
 
4214
4389
  function filterAgents() {
4390
+ if (!document.getElementById('agent-search')) return;
4215
4391
  agentFilterSearch = (document.getElementById('agent-search').value || '').toLowerCase();
4216
4392
  agentFilterRole = document.getElementById('agent-role-filter').value;
4217
4393
  agentFilterStatus = document.getElementById('agent-status-filter').value;
@@ -4223,11 +4399,13 @@ function toggleRoleGroup(role) {
4223
4399
  renderAgents(cachedAgents);
4224
4400
  }
4225
4401
 
4402
+ var _lastAgentsHash = '';
4226
4403
  function renderAgents(agents) {
4404
+ updateAgentBar(agents);
4227
4405
  var el = document.getElementById('agents-list');
4228
4406
  var keys = Object.keys(agents);
4229
4407
  if (!keys.length) {
4230
- el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No agents registered</div>';
4408
+ if (_lastAgentsHash !== 'empty') { el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No agents registered</div>'; _lastAgentsHash = 'empty'; }
4231
4409
  return;
4232
4410
  }
4233
4411
 
@@ -4236,7 +4414,7 @@ function renderAgents(agents) {
4236
4414
  for (var f = 0; f < keys.length; f++) {
4237
4415
  var fname = keys[f];
4238
4416
  var finfo = agents[fname];
4239
- var fstate = finfo.status || (finfo.alive ? 'active' : 'dead');
4417
+ var fstate = finfo.status || (finfo.alive ? 'working' : 'offline');
4240
4418
  if (agentFilterSearch && fname.toLowerCase().indexOf(agentFilterSearch) === -1 &&
4241
4419
  (!finfo.display_name || finfo.display_name.toLowerCase().indexOf(agentFilterSearch) === -1)) continue;
4242
4420
  if (agentFilterRole && finfo.role !== agentFilterRole) continue;
@@ -4244,8 +4422,17 @@ function renderAgents(agents) {
4244
4422
  filtered.push(fname);
4245
4423
  }
4246
4424
 
4425
+ var collapsedSig = Object.keys(collapsedRoleGroups).filter(function(k) { return collapsedRoleGroups[k]; }).sort().join(',');
4426
+ var structuralPart = keys.map(function(k) { var a = agents[k]; return k + ':' + (a.alive ? '1' : '0') + ':' + (a.is_listening ? 'L' : 'W') + ':' + (a.role || '') + ':' + (a.status || ''); }).join('|');
4427
+ var useGrouping = filtered.length > 6;
4428
+ var layoutHash = structuralPart + '|F:' + agentFilterSearch + ':' + agentFilterRole + ':' + agentFilterStatus + '|C:' + collapsedSig + '|G:' + (useGrouping ? '1' : '0');
4429
+
4247
4430
  if (!filtered.length) {
4248
4431
  el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No matching agents</div>';
4432
+ _lastAgentsHash = layoutHash;
4433
+ var alertEl0 = document.getElementById('alert-count');
4434
+ if (alertEl0) alertEl0.style.display = 'none';
4435
+ updateInjectTargets(keys);
4249
4436
  return;
4250
4437
  }
4251
4438
 
@@ -4261,9 +4448,6 @@ function renderAgents(agents) {
4261
4448
  roleGroups[grole].push(gname);
4262
4449
  }
4263
4450
 
4264
- // Only use role grouping if more than 6 agents (otherwise flat list)
4265
- var useGrouping = filtered.length > 6;
4266
-
4267
4451
  var sleepCount = 0;
4268
4452
  var html = '';
4269
4453
 
@@ -4272,90 +4456,44 @@ function renderAgents(agents) {
4272
4456
  for (var i = 0; i < agentNames.length; i++) {
4273
4457
  var name = agentNames[i];
4274
4458
  var info = agents[name];
4275
- var color = getColor(name);
4276
- // Use server-provided status if available, fallback to client-side detection
4277
- var state = info.status || (info.alive ? 'active' : 'dead');
4278
- if (state === 'sleeping') sleepCount++;
4279
-
4280
- var activityText = '';
4281
- if (state === 'dead') {
4282
- activityText = 'Process not running';
4283
- } else if (info.idle_seconds != null) {
4284
- if (info.idle_seconds < 5) activityText = 'Active just now';
4285
- else if (info.idle_seconds < 60) activityText = 'Active ' + info.idle_seconds + 's ago';
4286
- else activityText = 'Idle ' + Math.floor(info.idle_seconds / 60) + 'm ' + (info.idle_seconds % 60) + 's';
4287
- } else if (info.last_message) {
4288
- activityText = 'Last msg ' + timeAgo(info.last_message);
4289
- } else {
4290
- activityText = 'Registered ' + timeAgo(info.registered_at);
4291
- }
4292
-
4293
- var stateLabel = state.charAt(0).toUpperCase() + state.slice(1);
4294
- var cardClass = 'agent-card' + (state !== 'active' ? ' ' + state : '');
4295
-
4296
- var nudgeHtml = '';
4297
- if (state === 'sleeping' || (state === 'active' && !info.is_listening && info.idle_seconds > 30)) {
4298
- nudgeHtml = '<button class="nudge-btn" onclick="sendNudge(\'' + escapeHtml(name) + '\')">Send Nudge</button>';
4299
- }
4300
- var removeHtml = '';
4301
- var respawnHtml = '';
4302
- if (state === 'dead') {
4303
- removeHtml = '<button class="remove-agent-btn" onclick="event.stopPropagation();removeAgent(\'' + escapeHtml(name) + '\')" title="Remove this agent">Remove</button>';
4304
- respawnHtml = '<button class="respawn-btn" onclick="event.stopPropagation();respawnAgent(\'' + escapeHtml(name) + '\')" title="Generate resume prompt for this agent">&#x1F504; Respawn</button>';
4305
- }
4306
-
4307
- // Listening status — simplified: skip for dead (already shown via badge)
4308
- var listenHtml = '';
4309
- if (state !== 'dead') {
4310
- if (info.is_listening) {
4311
- var sinceTxt = info.listening_since ? ' since ' + formatTime(info.listening_since) : '';
4312
- listenHtml = '<div class="listen-badge listening" title="Agent is waiting for messages — can receive dashboard injections"><span class="listen-dot on"></span>Listening' + sinceTxt + '</div>';
4313
- } else if (state === 'active') {
4314
- listenHtml = '<div class="listen-badge busy" title="Agent is working, not currently listening for messages"><span class="listen-dot off"></span>Busy</div>';
4315
- } else {
4316
- listenHtml = '<div class="listen-badge not-listening" title="Agent is not listening. Send a nudge or type \'listen\' in the terminal."><span class="listen-dot off"></span>Not Listening</div>';
4459
+ var color = getColor(name);
4460
+ var state = !info.alive ? 'offline' : info.is_listening ? 'listening' : (info.status === 'idle' || info.status === 'sleeping') ? 'idle' : 'working';
4461
+ if (state === 'idle') sleepCount++;
4462
+
4463
+ // Compact activity text
4464
+ var activityText = '';
4465
+ if (state === 'offline' || state === 'dead') {
4466
+ activityText = 'offline';
4467
+ } else if (info.idle_seconds != null) {
4468
+ if (info.idle_seconds < 5) activityText = 'just now';
4469
+ else if (info.idle_seconds < 60) activityText = info.idle_seconds + 's ago';
4470
+ else activityText = Math.floor(info.idle_seconds / 60) + 'm ago';
4471
+ } else if (info.last_activity) {
4472
+ activityText = timeAgo(info.last_activity);
4317
4473
  }
4318
- }
4319
4474
 
4320
- var avatarHtml = info.avatar
4321
- ? '<img class="agent-avatar-img" src="' + escapeHtml(info.avatar) + '" alt="' + escapeHtml(name) + '" onerror="this.style.display=\'none\'">'
4322
- : '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>';
4323
- var displayName = info.display_name || name;
4324
- var roleHtml = '';
4325
- if (info.role) {
4326
- var roleBg = info.role === 'quality' ? 'background:rgba(34,197,94,0.15);color:var(--green)' :
4327
- info.role === 'monitor' ? 'background:rgba(239,68,68,0.15);color:var(--red,#ef4444)' :
4328
- info.role === 'advisor' ? 'background:rgba(168,85,247,0.15);color:var(--purple,#a855f7)' :
4329
- info.role === 'lead' ? 'background:rgba(59,130,246,0.15);color:var(--accent)' : '';
4330
- roleHtml = '<span class="role-badge" style="' + roleBg + '">' + escapeHtml(info.role) + '</span>';
4331
- }
4332
- var branchHtml = info.branch && info.branch !== 'main' ? ' <span style="font-size:9px;color:var(--text-muted)">[' + escapeHtml(info.branch) + ']</span>' : '';
4333
-
4334
- cardHtml += '<div class="' + cardClass + '">' +
4335
- '<div class="agent-top" onclick="showProfilePopup(\'' + escapeHtml(name) + '\', event)" style="cursor:pointer">' +
4336
- avatarHtml +
4337
- '<div class="agent-info">' +
4338
- '<div class="agent-name" style="color:' + color + '">' +
4339
- escapeHtml(displayName) +
4340
- roleHtml +
4341
- ' <span class="agent-badge ' + state + '">' + stateLabel + '</span>' +
4342
- branchHtml +
4343
- '</div>' +
4344
- '<div class="agent-meta"><span>' + getProviderBadge(info.provider) + 'PID ' + info.pid + '</span>' +
4345
- (info.last_activity ? '<span style="margin-left:8px" title="Last heartbeat: ' + new Date(info.last_activity).toLocaleTimeString() + '">&#x1f493; ' + timeAgo(info.last_activity) + '</span>' : '') +
4475
+ var cardClass = 'agent-card' + (state !== 'working' ? ' ' + state : '');
4476
+ var avatarHtml = info.avatar
4477
+ ? '<img class="agent-avatar-img" src="' + escapeHtml(info.avatar) + '" alt="' + escapeHtml(name) + '" onerror="this.style.display=\'none\'">'
4478
+ : '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>';
4479
+ var displayName = info.display_name || name;
4480
+
4481
+ // Subtitle: role + activity
4482
+ var subtitleParts = [];
4483
+ if (info.role) subtitleParts.push(info.role);
4484
+ if (activityText) subtitleParts.push(activityText);
4485
+ var subtitleText = subtitleParts.join(' \u00B7 ');
4486
+
4487
+ cardHtml += '<div class="' + cardClass + '" data-agent="' + escapeHtml(name) + '" onclick="showProfilePopup(\'' + escapeHtml(name) + '\', event)">' +
4488
+ '<div class="agent-top">' +
4489
+ avatarHtml +
4490
+ '<div class="agent-info">' +
4491
+ '<div class="agent-name" style="color:' + color + '">' + escapeHtml(displayName) + '</div>' +
4492
+ '<div class="agent-subtitle">' + escapeHtml(subtitleText) + '</div>' +
4346
4493
  '</div>' +
4494
+ '<span class="agent-status-dot ' + state + '" title="' + state + '"></span>' +
4347
4495
  '</div>' +
4348
- '</div>' +
4349
- '<div class="agent-activity">' +
4350
- '<span class="agent-activity-icon ' + state + '"></span>' +
4351
- activityText +
4352
- '</div>' +
4353
- (info.current_status ? '<div class="agent-status-intent" title="' + escapeHtml(info.current_status) + '">' + escapeHtml(info.current_status) + '</div>' : '') +
4354
- listenHtml +
4355
- nudgeHtml +
4356
- respawnHtml +
4357
- removeHtml +
4358
- '</div>';
4496
+ '</div>';
4359
4497
  }
4360
4498
  return cardHtml;
4361
4499
  }
@@ -4382,10 +4520,47 @@ function renderAgents(agents) {
4382
4520
  html += renderAgentCards(filtered);
4383
4521
  }
4384
4522
 
4385
- el.innerHTML = html;
4523
+ // DOM patching: when layout is unchanged, update cards in place (activity text, dots); else full rebuild
4524
+ var existingCards = el.querySelectorAll('[data-agent]');
4525
+ var canPatch = !useGrouping && existingCards.length === filtered.length && existingCards.length > 0;
4526
+ var didPatch = false;
4527
+ if (canPatch && layoutHash === _lastAgentsHash) {
4528
+ didPatch = true;
4529
+ for (var pi = 0; pi < filtered.length; pi++) {
4530
+ var pName = filtered[pi];
4531
+ var pCard = el.querySelector('[data-agent="' + pName + '"]');
4532
+ if (!pCard) { didPatch = false; break; }
4533
+ var pInfo = agents[pName];
4534
+ var pState = !pInfo.alive ? 'offline' : pInfo.is_listening ? 'listening' : (pInfo.status === 'idle' || pInfo.status === 'sleeping') ? 'idle' : 'working';
4535
+ var dot = pCard.querySelector('.agent-status-dot');
4536
+ if (dot) { dot.className = 'agent-status-dot ' + pState; dot.title = pState; }
4537
+ var sub = pCard.querySelector('.agent-subtitle');
4538
+ if (sub) {
4539
+ var parts = [];
4540
+ if (pInfo.role) parts.push(pInfo.role);
4541
+ if (pState === 'offline') parts.push('offline');
4542
+ else if (pInfo.idle_seconds != null) {
4543
+ if (pInfo.idle_seconds < 5) parts.push('just now');
4544
+ else if (pInfo.idle_seconds < 60) parts.push(pInfo.idle_seconds + 's ago');
4545
+ else parts.push(Math.floor(pInfo.idle_seconds / 60) + 'm ago');
4546
+ } else if (pInfo.last_activity) {
4547
+ parts.push(timeAgo(pInfo.last_activity));
4548
+ }
4549
+ var newSub = parts.join(' \u00B7 ');
4550
+ if (sub.textContent !== newSub) sub.textContent = newSub;
4551
+ }
4552
+ var newCardClass = 'agent-card' + (pState !== 'working' ? ' ' + pState : '');
4553
+ if (pCard.className !== newCardClass) pCard.className = newCardClass;
4554
+ }
4555
+ }
4556
+ if (!didPatch) {
4557
+ el.innerHTML = html;
4558
+ _lastAgentsHash = layoutHash;
4559
+ }
4386
4560
 
4387
4561
  // Update alert badge
4388
4562
  var alertEl = document.getElementById('alert-count');
4563
+ if (!alertEl) return;
4389
4564
  if (sleepCount > 0) {
4390
4565
  alertEl.textContent = sleepCount;
4391
4566
  alertEl.style.display = '';
@@ -4464,12 +4639,12 @@ function showRespawnModal(agentName, prompt) {
4464
4639
 
4465
4640
  overlay.innerHTML =
4466
4641
  '<div class="respawn-modal">' +
4467
- '<h3>&#x1F504; Respawn ' + escapeHtml(agentName) + '</h3>' +
4642
+ '<h3>Respawn ' + escapeHtml(agentName) + '</h3>' +
4468
4643
  '<p style="color:var(--text-secondary);font-size:12px;margin:0 0 8px">Copy this prompt and paste it into a fresh CLI terminal to respawn the agent:</p>' +
4469
4644
  '<div class="respawn-modal-prompt" id="respawn-prompt-text">' + escapeHtml(prompt) + '</div>' +
4470
4645
  '<div class="respawn-modal-actions">' +
4471
4646
  '<button class="respawn-close-btn" onclick="dismissRespawnModal()">Close</button>' +
4472
- '<button class="respawn-copy-btn" onclick="copyRespawnPrompt()">&#x1F4CB; Copy to Clipboard</button>' +
4647
+ '<button class="respawn-copy-btn" onclick="copyRespawnPrompt()">Copy to Clipboard</button>' +
4473
4648
  '</div>' +
4474
4649
  '</div>';
4475
4650
 
@@ -4489,7 +4664,7 @@ function copyRespawnPrompt() {
4489
4664
  var btn = document.querySelector('.respawn-copy-btn');
4490
4665
  if (btn) {
4491
4666
  btn.textContent = '\u2705 Copied!';
4492
- setTimeout(function() { btn.innerHTML = '&#x1F4CB; Copy to Clipboard'; }, 2000);
4667
+ setTimeout(function() { btn.innerHTML = 'Copy to Clipboard'; }, 2000);
4493
4668
  }
4494
4669
  }).catch(function() {
4495
4670
  // Fallback: select all text
@@ -4568,6 +4743,7 @@ function doInject() {
4568
4743
  // ==================== THREADS ====================
4569
4744
 
4570
4745
  function renderThreads(messages) {
4746
+ if (!document.getElementById('threads-list')) return;
4571
4747
  var threads = {};
4572
4748
  for (var i = 0; i < messages.length; i++) {
4573
4749
  var m = messages[i];
@@ -4616,17 +4792,17 @@ function renderMessages(messages) {
4616
4792
  ? '<div class="empty-sub">Select a project above to see agent conversations</div>'
4617
4793
  : '<div class="empty-sub">Waiting for messages in: ' + activeProject.split(/[/\\]/).pop() + '</div>';
4618
4794
  el.innerHTML = '<div class="empty-state">' +
4619
- '<div class="empty-icon">&#x1f4ac;</div>' +
4795
+ '<div class="empty-icon" style="font-size:40px;opacity:0.2">--</div>' +
4620
4796
  '<div class="empty-text">No messages yet</div>' +
4621
4797
  mobileHint +
4622
4798
  '</div>';
4623
4799
  } else {
4624
4800
  el.innerHTML = '<div class="empty-state">' +
4625
- '<div class="empty-icon">&#x1f680;</div>' +
4626
- '<div class="empty-text">Neohive v5.0</div>' +
4801
+ '<div class="empty-icon" style="font-size:40px;opacity:0.2">--</div>' +
4802
+ '<div class="empty-text">Neohive v6.0</div>' +
4627
4803
  '<div class="empty-sub">Autonomous AI agent teams — one command, zero babysitting</div>' +
4628
4804
  '<div class="onboard-steps">' +
4629
- '<div class="onboard-step" style="margin-bottom:12px"><span class="onboard-num" style="background:var(--accent)">&#x26A1;</span><span style="font-weight:600">Quickest start — one command:</span></div>' +
4805
+ '<div class="onboard-step" style="margin-bottom:12px"><span class="onboard-num" style="background:var(--accent)"></span><span style="font-weight:600">Quickest start — one command:</span></div>' +
4630
4806
  '<div class="copy-block" onclick="copyText(this)" data-text="npx neohive run &quot;build a REST API with auth&quot; --agents 4"><span class="copy-hint">click to copy</span>npx neohive run "build a REST API with auth" --agents 4</div>' +
4631
4807
  '<div style="font-size:11px;color:var(--text-muted);margin:8px 0 16px;text-align:center">Spawns 4 agents, auto-assigns roles, creates autonomous workflow. Walk away, come back to finished work.</div>' +
4632
4808
  '<div style="border-top:1px solid var(--border);padding-top:12px;margin-top:4px"></div>' +
@@ -4710,10 +4886,12 @@ function renderMessages(messages) {
4710
4886
 
4711
4887
  for (var i = windowStart; i < filtered.length; i++) {
4712
4888
  var m = filtered[i];
4713
- var color = getColor(m.from);
4714
- var fromDisplay = (cachedAgents[m.from] && cachedAgents[m.from].display_name) || m.from;
4715
- var toDisplay = (cachedAgents[m.to] && cachedAgents[m.to].display_name) || m.to;
4889
+ var color = m.from === '__user__' ? 'var(--accent)' : getColor(m.from);
4890
+ var fromDisplay = m.from === '__user__' ? 'You' : (cachedAgents[m.from] && cachedAgents[m.from].display_name) || m.from;
4891
+ var toDisplay = m.to === '__user__' ? 'You' : (cachedAgents[m.to] && cachedAgents[m.to].display_name) || m.to;
4716
4892
  var newClass = (isNew && i >= lastMessageCount) ? ' message-new' : '';
4893
+ var isUserMsg = m.from === '__user__';
4894
+ var isReplyToUser = m.to === '__user__';
4717
4895
  var isSystem = m.system === true;
4718
4896
 
4719
4897
  // Date separator
@@ -4758,7 +4936,7 @@ function renderMessages(messages) {
4758
4936
  buildMsgActions(m.id) +
4759
4937
  msgAvatarHtml +
4760
4938
  '<div class="msg-body">' +
4761
- '<div class="handoff-banner">&#x1f91d; Handoff: ' + escapeHtml(fromDisplay) + ' &rarr; ' + escapeHtml(toDisplay) + '</div>' +
4939
+ '<div class="handoff-banner">Handoff: ' + escapeHtml(fromDisplay) + ' &rarr; ' + escapeHtml(toDisplay) + '</div>' +
4762
4940
  '<div class="msg-header">' +
4763
4941
  '<span class="msg-from" style="color:' + color + '" title="@' + escapeHtml(m.from) + '">' + escapeHtml(fromDisplay) + '</span>' +
4764
4942
  '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
@@ -4785,7 +4963,7 @@ function renderMessages(messages) {
4785
4963
  '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
4786
4964
  '<div class="msg-badges">' + badges + '</div>' +
4787
4965
  '</div>' +
4788
- '<div class="file-meta"><span class="file-icon">&#x1f4c4;</span>' + escapeHtml(fileName) + '<span class="file-size">' + fileSize + '</span></div>' +
4966
+ '<div class="file-meta"><span class="file-icon" style="opacity:0.5">[ ]</span>' + escapeHtml(fileName) + '<span class="file-size">' + fileSize + '</span></div>' +
4789
4967
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4790
4968
  buildReactionsHtml(m.id) +
4791
4969
  buildReadReceipts(m.id) +
@@ -4793,7 +4971,9 @@ function renderMessages(messages) {
4793
4971
  lastFrom = m.from;
4794
4972
  lastTo = m.to;
4795
4973
  } else {
4796
- html += '<div class="message' + newClass + groupClass + '">' +
4974
+ var userMsgClass = isUserMsg ? ' user-msg' : '';
4975
+ var replyBadge = isReplyToUser ? '<span class="badge" style="background:var(--accent-dim);color:var(--accent)">Reply to you</span>' : '';
4976
+ html += '<div class="message' + newClass + groupClass + userMsgClass + '">' +
4797
4977
  buildMsgActions(m.id) +
4798
4978
  msgAvatarHtml +
4799
4979
  '<div class="msg-body">' +
@@ -4802,7 +4982,7 @@ function renderMessages(messages) {
4802
4982
  '<span class="msg-arrow">&rarr;</span>' +
4803
4983
  '<span class="msg-to">' + escapeHtml(toDisplay) + '</span>' +
4804
4984
  '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
4805
- '<div class="msg-badges">' + badges + '</div>' +
4985
+ '<div class="msg-badges">' + replyBadge + badges + '</div>' +
4806
4986
  '</div>' +
4807
4987
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4808
4988
  buildReactionsHtml(m.id) +
@@ -4972,13 +5152,17 @@ function renderChannelBar(messages) {
4972
5152
  }
4973
5153
 
4974
5154
  function toggleSidebar() {
4975
- document.getElementById('sidebar').classList.toggle('open');
4976
- document.getElementById('sidebar-overlay').classList.toggle('open');
5155
+ var sb = document.getElementById('sidebar') || document.getElementById('nav-sidebar');
5156
+ if (sb) sb.classList.toggle('open');
5157
+ var ov = document.getElementById('sidebar-overlay');
5158
+ if (ov) ov.classList.toggle('open');
4977
5159
  }
4978
5160
 
4979
5161
  function closeSidebar() {
4980
- document.getElementById('sidebar').classList.remove('open');
4981
- document.getElementById('sidebar-overlay').classList.remove('open');
5162
+ var sb = document.getElementById('sidebar') || document.getElementById('nav-sidebar');
5163
+ if (sb) sb.classList.remove('open');
5164
+ var ov = document.getElementById('sidebar-overlay');
5165
+ if (ov) ov.classList.remove('open');
4982
5166
  }
4983
5167
 
4984
5168
  var newWhileScrolled = 0;
@@ -5136,7 +5320,7 @@ function deepSearch() {
5136
5320
  lastMessageCount = 0;
5137
5321
  var el = document.getElementById('messages');
5138
5322
  if (!messages.length) {
5139
- el.innerHTML = '<div class="empty-state"><div class="empty-icon">&#x1f50d;</div><div class="empty-text">No results for "' + escapeHtml(query) + '"</div><div class="empty-sub">Searched full history including channels</div></div>';
5323
+ el.innerHTML = '<div class="empty-state"><div class="empty-icon" style="font-size:40px;opacity:0.2">--</div><div class="empty-text">No results for "' + escapeHtml(query) + '"</div><div class="empty-sub">Searched full history including channels</div></div>';
5140
5324
  return;
5141
5325
  }
5142
5326
  // Render search results directly (don't overwrite cachedHistory)
@@ -5261,14 +5445,14 @@ function buildMsgActions(msgId) {
5261
5445
 
5262
5446
  var msg = cachedHistory.find(function(m) { return m.id === msgId; });
5263
5447
  var isDeletable = msg && (msg.from === 'dashboard' || msg.from === 'Dashboard' || msg.from === 'system' || msg.from === '__system__');
5264
- var deleteBtn = isDeletable ? '<button class="msg-action-btn" onclick="deleteMessage(\'' + msgId + '\')" title="Delete" style="color:var(--red,#e74c3c)">&#x1f5d1;&#xFE0F;</button>' : '';
5448
+ var deleteBtn = isDeletable ? '<button class="msg-action-btn" onclick="deleteMessage(\'' + msgId + '\')" title="Delete" style="color:var(--red,#e74c3c)">Del</button>' : '';
5265
5449
 
5266
5450
  return '<div class="msg-actions">' +
5267
- '<button class="msg-action-btn" onclick="toggleReactPicker(\'' + msgId + '\')" title="React">&#x1f600;</button>' +
5451
+ '<button class="msg-action-btn" onclick="toggleReactPicker(\'' + msgId + '\')" title="React">+</button>' +
5268
5452
  '<button class="msg-action-btn' + pinClass + '" onclick="togglePin(\'' + msgId + '\')" title="Pin">\ud83d\udccc</button>' +
5269
5453
  '<button class="msg-action-btn' + starClass + '" onclick="toggleBookmark(\'' + msgId + '\')" title="Bookmark">' + starChar + '</button>' +
5270
- '<button class="msg-action-btn" onclick="copyMessage(\'' + msgId + '\', this)" title="Copy">&#x1f4cb;</button>' +
5271
- '<button class="msg-action-btn" onclick="openEditMessage(\'' + msgId + '\')" title="Edit">&#x270F;&#xFE0F;</button>' +
5454
+ '<button class="msg-action-btn" onclick="copyMessage(\'' + msgId + '\', this)" title="Copy">Copy</button>' +
5455
+ '<button class="msg-action-btn" onclick="openEditMessage(\'' + msgId + '\')" title="Edit">Edit</button>' +
5272
5456
  deleteBtn +
5273
5457
  '<div class="react-picker" id="react-' + msgId + '">' +
5274
5458
  REACTION_EMOJIS.map(function(e) { return '<button class="react-emoji" onclick="addReaction(\'' + msgId + '\',\'' + e + '\')">' + e + '</button>'; }).join('') +
@@ -5281,7 +5465,7 @@ function copyMessage(msgId, btn) {
5281
5465
  if (!msg) return;
5282
5466
  navigator.clipboard.writeText(msg.content).then(function() {
5283
5467
  var orig = btn.innerHTML;
5284
- btn.innerHTML = '&#x2705;';
5468
+ btn.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 8l3 3 7-7"/></svg>';
5285
5469
  btn.title = 'Copied!';
5286
5470
  setTimeout(function() { btn.innerHTML = orig; btn.title = 'Copy'; }, 1500);
5287
5471
  });
@@ -5381,11 +5565,11 @@ function applyTheme(theme) {
5381
5565
  currentTheme = theme;
5382
5566
  if (theme === 'light') {
5383
5567
  document.documentElement.setAttribute('data-theme', 'light');
5384
- document.getElementById('theme-toggle').innerHTML = '&#x2600;&#xfe0f;';
5385
5568
  } else {
5386
5569
  document.documentElement.removeAttribute('data-theme');
5387
- document.getElementById('theme-toggle').innerHTML = '&#x1f319;';
5388
5570
  }
5571
+ var themeBtn = document.getElementById('theme-toggle');
5572
+ if (themeBtn) themeBtn.innerHTML = theme === 'light' ? 'Light' : 'Dark';
5389
5573
  localStorage.setItem('ltt-theme', theme);
5390
5574
  }
5391
5575
 
@@ -5504,6 +5688,7 @@ function copyText(el) {
5504
5688
  // ==================== AGENT STATS ====================
5505
5689
 
5506
5690
  function renderAgentStats() {
5691
+ if (!document.getElementById('agent-stats')) return;
5507
5692
  var el = document.getElementById('agent-stats');
5508
5693
  if (!cachedHistory.length) {
5509
5694
  el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No data yet</div>';
@@ -5575,6 +5760,7 @@ function fetchActivity() {
5575
5760
 
5576
5761
  function renderActivityHeatmap(data) {
5577
5762
  var el = document.getElementById('activity-heatmap');
5763
+ if (!el) return;
5578
5764
  if (!data.agents || !Object.keys(data.agents).length) {
5579
5765
  el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No activity data</div>';
5580
5766
  return;
@@ -5593,7 +5779,7 @@ function renderActivityHeatmap(data) {
5593
5779
  html += '<div class="activity-agent">' +
5594
5780
  '<div class="activity-label">' +
5595
5781
  '<span class="activity-name" style="color:' + color + '">' + escapeHtml(name) + '</span>' +
5596
- '<span class="activity-pct">' + pct + '%</span>' +
5782
+ '<span class="activity-pct">' + pct + '<span style="font-size:0.75em;margin-left:1px">%</span></span>' +
5597
5783
  '</div>' +
5598
5784
  '<div class="activity-bar">' +
5599
5785
  '<div class="activity-fill" style="width:' + pct + '%;background:' + color + '"></div>' +
@@ -5632,13 +5818,14 @@ function toggleBookmark(msgId) {
5632
5818
  }
5633
5819
 
5634
5820
  function renderBookmarksSidebar() {
5821
+ if (!document.getElementById('bookmarks-list')) return;
5635
5822
  var el = document.getElementById('bookmarks-list');
5636
5823
  var countEl = document.getElementById('bookmark-count');
5637
5824
  var clearEl = document.getElementById('bookmark-clear');
5638
5825
  var ids = Object.keys(bookmarks);
5639
5826
 
5640
5827
  if (!ids.length) {
5641
- el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No bookmarks yet<br><span style="font-size:10px;opacity:0.7">Hover a message and click &#x2606; to bookmark</span></div>';
5828
+ el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No bookmarks yet<br><span style="font-size:10px;opacity:0.7">Hover a message and click star to bookmark</span></div>';
5642
5829
  countEl.style.display = 'none';
5643
5830
  clearEl.style.display = 'none';
5644
5831
  return;
@@ -5710,19 +5897,28 @@ function exportJSON() {
5710
5897
 
5711
5898
  // ==================== VIEW SWITCHING ====================
5712
5899
 
5713
- var activeView = 'office';
5900
+ var activeView = localStorage.getItem('neohive_activeView') || 'overview';
5714
5901
 
5715
5902
  function switchView(view) {
5716
5903
  activeView = view;
5717
- document.getElementById('tab-messages').classList.toggle('active', view === 'messages');
5718
- document.getElementById('tab-tasks').classList.toggle('active', view === 'tasks');
5719
- document.getElementById('tab-workspaces').classList.toggle('active', view === 'workspaces');
5720
- document.getElementById('tab-workflows').classList.toggle('active', view === 'workflows');
5721
- document.getElementById('tab-plan').classList.toggle('active', view === 'plan');
5722
- document.getElementById('tab-launch').classList.toggle('active', view === 'launch');
5723
- document.getElementById('tab-rules').classList.toggle('active', view === 'rules');
5724
- document.getElementById('tab-stats').classList.toggle('active', view === 'stats');
5725
- document.getElementById('tab-docs').classList.toggle('active', view === 'docs');
5904
+ try { localStorage.setItem('neohive_activeView', view); } catch {}
5905
+ // Reset render hashes so switching tabs forces a fresh render
5906
+ _lastTasksHash = '';
5907
+ _lastAgentsHash = '';
5908
+ if (renderOverview._lastHash) renderOverview._lastHash = '';
5909
+ // Update nav sidebar active state
5910
+ var navItems = document.querySelectorAll('.nav-item');
5911
+ for (var i = 0; i < navItems.length; i++) {
5912
+ navItems[i].classList.toggle('active', navItems[i].getAttribute('data-view') === view);
5913
+ }
5914
+ // Update old tab active states (kept for compatibility)
5915
+ var tabIds = ['messages','tasks','workspaces','workflows','plan','launch','rules','stats','docs'];
5916
+ for (var t = 0; t < tabIds.length; t++) {
5917
+ var el = document.getElementById('tab-' + tabIds[t]);
5918
+ if (el) el.classList.toggle('active', view === tabIds[t]);
5919
+ }
5920
+ // Toggle view areas
5921
+ document.getElementById('overview-area').classList.toggle('visible', view === 'overview');
5726
5922
  document.getElementById('messages').style.display = view === 'messages' ? 'flex' : 'none';
5727
5923
  document.getElementById('tasks-area').classList.toggle('visible', view === 'tasks');
5728
5924
  document.getElementById('workspaces-area').classList.toggle('visible', view === 'workspaces');
@@ -5735,6 +5931,9 @@ function switchView(view) {
5735
5931
  document.getElementById('docs-area').classList.toggle('visible', view === 'docs');
5736
5932
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
5737
5933
  document.getElementById('channel-filter-bar').style.display = view === 'messages' && activeChannel !== undefined ? '' : 'none';
5934
+ var composeBar = document.querySelector('.msg-input-bar');
5935
+ if (composeBar) composeBar.style.display = (view === 'messages') ? 'flex' : 'none';
5936
+ if (view === 'overview') renderOverview();
5738
5937
  if (view === 'messages') renderChannelBar(cachedHistory);
5739
5938
  if (view === 'tasks') fetchTasks();
5740
5939
  if (view === 'workspaces') fetchWorkspaces();
@@ -5744,7 +5943,6 @@ function switchView(view) {
5744
5943
  if (view === 'launch') renderLaunchPanel();
5745
5944
  if (view === 'rules') fetchRules();
5746
5945
  if (view === 'stats') fetchStats();
5747
- // Auto-close sidebar on mobile after view switch
5748
5946
  if (isMobile) closeSidebar();
5749
5947
  }
5750
5948
 
@@ -5754,7 +5952,10 @@ var cachedTasks = [];
5754
5952
 
5755
5953
  function fetchTasks() {
5756
5954
  var pq = projectParam();
5757
- document.getElementById('tasks-area').innerHTML = '<div class="loading-spinner">Loading tasks...</div>';
5955
+ var tasksArea = document.getElementById('tasks-area');
5956
+ if (tasksArea && !tasksArea.querySelector('.kanban') && !tasksArea.querySelector('.tasks-empty')) {
5957
+ tasksArea.innerHTML = '<div class="loading-spinner">Loading tasks...</div>';
5958
+ }
5758
5959
  lttFetch('/api/tasks' + pq).then(function(r) { return r.json(); }).then(function(tasks) {
5759
5960
  cachedTasks = Array.isArray(tasks) ? tasks : [];
5760
5961
  renderTasks();
@@ -5764,9 +5965,13 @@ function fetchTasks() {
5764
5965
  });
5765
5966
  }
5766
5967
 
5968
+ var _lastTasksHash = '';
5767
5969
  function renderTasks() {
5768
5970
  var el = document.getElementById('tasks-area');
5971
+ var savedScroll = el.scrollTop;
5769
5972
  if (!cachedTasks.length) {
5973
+ _lastTasksHash = '';
5974
+
5770
5975
  el.innerHTML = '<div class="kanban">' +
5771
5976
  '<div class="kanban-col">' +
5772
5977
  '<div class="kanban-title pending">Pending<span class="kanban-count">0</span></div>' +
@@ -5789,6 +5994,11 @@ function renderTasks() {
5789
5994
  return;
5790
5995
  }
5791
5996
 
5997
+ // Data hash to skip unnecessary DOM rebuilds
5998
+ var tasksHash = cachedTasks.map(function(t) { return t.id + ':' + t.status + ':' + (t.assignee || ''); }).join('|');
5999
+ if (tasksHash === _lastTasksHash) return;
6000
+ _lastTasksHash = tasksHash;
6001
+
5792
6002
  var groups = { pending: [], in_progress: [], in_review: [], done: [], blocked: [] };
5793
6003
  for (var i = 0; i < cachedTasks.length; i++) {
5794
6004
  var t = cachedTasks[i];
@@ -5817,7 +6027,10 @@ function renderTasks() {
5817
6027
  html += '</div>';
5818
6028
  }
5819
6029
  html += '</div>';
5820
- el.innerHTML = html;
6030
+ if (el.innerHTML !== html) {
6031
+ el.innerHTML = html;
6032
+ el.scrollTop = savedScroll;
6033
+ }
5821
6034
  }
5822
6035
 
5823
6036
  function buildTaskCard(t) {
@@ -5846,7 +6059,7 @@ function buildTaskCard(t) {
5846
6059
  notesHtml = '<div style="font-size:10px;color:var(--text-muted);margin-top:4px;font-style:italic">' + escapeHtml(lastNote.by) + ': ' + escapeHtml((lastNote.text || '').substring(0, 80)) + '</div>';
5847
6060
  }
5848
6061
 
5849
- return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)">' +
6062
+ return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)" onclick="showTaskDetail(\'' + t.id + '\')">' +
5850
6063
  '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + (badgesHtml ? ' ' + badgesHtml : '') + '</div>' +
5851
6064
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
5852
6065
  notesHtml +
@@ -5868,6 +6081,152 @@ function updateTaskStatus(taskId, newStatus) {
5868
6081
  }).catch(function(e) { console.error('Task update failed:', e); });
5869
6082
  }
5870
6083
 
6084
+ // ==================== TASK DETAIL VIEW ====================
6085
+
6086
+ function showTaskDetail(taskId) {
6087
+ var task = cachedTasks.find(function(t) { return t.id === taskId; });
6088
+ if (!task) return;
6089
+ var el = document.getElementById('task-detail-content');
6090
+ if (!el) return;
6091
+ var statusColors = { pending: 'var(--yellow)', in_progress: 'var(--accent)', in_review: 'var(--purple)', done: 'var(--green)', blocked: 'var(--red)' };
6092
+ var statusColor = statusColors[task.status] || 'var(--text-muted)';
6093
+
6094
+ var html = '';
6095
+ html += '<div style="margin-bottom:16px"><div style="font-size:18px;font-weight:700;color:var(--text);margin-bottom:6px">' + escapeHtml(task.title || 'Untitled') + '</div>';
6096
+ html += '<span style="font-size:11px;padding:3px 8px;border-radius:6px;font-weight:600;background:' + statusColor + '20;color:' + statusColor + '">' + escapeHtml(task.status || 'pending') + '</span></div>';
6097
+
6098
+ if (task.description) {
6099
+ html += '<div style="font-size:13px;color:var(--text-dim);line-height:1.6;margin-bottom:16px;padding:12px;background:var(--surface-2);border-radius:8px">' + escapeHtml(task.description) + '</div>';
6100
+ }
6101
+
6102
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;font-size:12px">';
6103
+ html += '<div style="color:var(--text-muted)">Assignee <div style="color:var(--text);font-weight:600;margin-top:2px">' + escapeHtml(task.assignee || 'Unassigned') + '</div></div>';
6104
+ html += '<div style="color:var(--text-muted)">Created by <div style="color:var(--text);font-weight:600;margin-top:2px">' + escapeHtml(task.created_by || '?') + '</div></div>';
6105
+ html += '<div style="color:var(--text-muted)">Created <div style="color:var(--text);margin-top:2px">' + (task.created_at ? new Date(task.created_at).toLocaleString() : '?') + '</div></div>';
6106
+ html += '<div style="color:var(--text-muted)">Updated <div style="color:var(--text);margin-top:2px">' + (task.updated_at ? new Date(task.updated_at).toLocaleString() : '?') + '</div></div>';
6107
+ html += '</div>';
6108
+
6109
+ // Notes
6110
+ if (task.notes && task.notes.length) {
6111
+ html += '<div style="margin-bottom:16px"><div style="font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:6px">Notes (' + task.notes.length + ')</div>';
6112
+ for (var n = 0; n < task.notes.length; n++) {
6113
+ var note = task.notes[n];
6114
+ html += '<div style="font-size:12px;padding:6px 8px;background:var(--surface-2);border-radius:6px;margin-bottom:4px"><span style="font-weight:600;color:var(--text)">' + escapeHtml(note.by || '?') + '</span> <span style="color:var(--text-muted);font-size:10px">' + (note.at ? timeAgo(note.at) : '') + '</span><div style="color:var(--text-dim);margin-top:2px">' + escapeHtml(note.text || '') + '</div></div>';
6115
+ }
6116
+ html += '</div>';
6117
+ }
6118
+
6119
+ // Action buttons
6120
+ var canEdit = task.status === 'pending';
6121
+ var canDelete = task.status !== 'in_progress';
6122
+ html += '<div style="border-top:1px solid var(--border);padding-top:12px;display:flex;justify-content:flex-end;gap:8px">';
6123
+ if (canEdit) {
6124
+ html += '<button class="btn btn-primary" onclick="editTask(\'' + task.id + '\')" style="font-size:12px">Edit</button>';
6125
+ } else {
6126
+ html += '<button class="btn" disabled style="font-size:12px;opacity:0.3;cursor:not-allowed" title="Only pending tasks can be edited">Edit</button>';
6127
+ }
6128
+ if (canDelete) {
6129
+ html += '<button class="btn btn-danger" id="task-delete-btn" onclick="confirmDeleteTask(\'' + task.id + '\')" style="font-size:12px">Delete Task</button>';
6130
+ } else {
6131
+ html += '<button class="btn btn-danger" disabled style="font-size:12px;opacity:0.4;cursor:not-allowed" title="Cannot delete active task">Delete Task</button>';
6132
+ }
6133
+ html += '</div>';
6134
+
6135
+ el.innerHTML = html;
6136
+ var taskOverlay = document.getElementById('task-detail-overlay');
6137
+ if (taskOverlay) taskOverlay.classList.add('open');
6138
+ }
6139
+
6140
+ function closeTaskDetail() {
6141
+ var taskOv = document.getElementById('task-detail-overlay');
6142
+ if (taskOv) taskOv.classList.remove('open');
6143
+ var btn = document.getElementById('task-delete-btn');
6144
+ if (btn) { btn.textContent = 'Delete Task'; btn._confirming = false; }
6145
+ }
6146
+
6147
+ function editTask(taskId) {
6148
+ var task = cachedTasks.find(function(t) { return t.id === taskId; });
6149
+ if (!task) return;
6150
+ var el = document.getElementById('task-detail-content');
6151
+ if (!el) return;
6152
+ var html = '';
6153
+ html += '<div style="font-size:16px;font-weight:700;color:var(--text);margin-bottom:16px">Edit Task</div>';
6154
+ html += '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">Title</label>';
6155
+ html += '<input type="text" id="task-edit-title" value="' + escapeHtml(task.title || '') + '" style="width:100%;background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);outline:none"></div>';
6156
+ html += '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">Description</label>';
6157
+ html += '<textarea id="task-edit-desc" style="width:100%;min-height:80px;background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);outline:none;resize:vertical;font-family:inherit">' + escapeHtml(task.description || '') + '</textarea></div>';
6158
+ html += '<div style="margin-bottom:16px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">Assignee</label>';
6159
+ html += '<input type="text" id="task-edit-assignee" value="' + escapeHtml(task.assignee || '') + '" placeholder="Agent name" style="width:100%;background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);outline:none"></div>';
6160
+ html += '<div style="display:flex;gap:8px;justify-content:flex-end">';
6161
+ html += '<button class="btn" onclick="showTaskDetail(\'' + taskId + '\')" style="font-size:12px">Cancel</button>';
6162
+ html += '<button class="btn btn-primary" onclick="saveTaskEdit(\'' + taskId + '\')" style="font-size:12px">Save Changes</button>';
6163
+ html += '</div>';
6164
+ el.innerHTML = html;
6165
+ }
6166
+
6167
+ function saveTaskEdit(taskId) {
6168
+ var title = document.getElementById('task-edit-title').value.trim();
6169
+ var desc = document.getElementById('task-edit-desc').value.trim();
6170
+ var assignee = document.getElementById('task-edit-assignee').value.trim();
6171
+ if (!title) { showToast('!', 'Title is required'); return; }
6172
+ lttFetch('/api/tasks' + projectParam(), {
6173
+ method: 'PUT',
6174
+ headers: { 'Content-Type': 'application/json' },
6175
+ body: JSON.stringify({ task_id: taskId, title: title, description: desc, assignee: assignee || null })
6176
+ }).then(function(r) { return r.json(); }).then(function(data) {
6177
+ if (data.error) {
6178
+ showToast('!', data.error);
6179
+ } else {
6180
+ showToast('&#x2713;', 'Task updated');
6181
+ closeTaskDetail();
6182
+ fetchTasks();
6183
+ }
6184
+ }).catch(function() {
6185
+ showToast('!', 'Save failed');
6186
+ });
6187
+ }
6188
+
6189
+ var _taskDeleteTimer = null;
6190
+ function confirmDeleteTask(taskId) {
6191
+ var btn = document.getElementById('task-delete-btn');
6192
+ if (!btn) return;
6193
+ if (!btn._confirming) {
6194
+ btn._confirming = true;
6195
+ btn.textContent = 'Confirm Delete';
6196
+ btn.style.background = 'var(--red-dim)';
6197
+ _taskDeleteTimer = setTimeout(function() {
6198
+ btn.textContent = 'Delete Task';
6199
+ btn.style.background = '';
6200
+ btn._confirming = false;
6201
+ }, 3000);
6202
+ } else {
6203
+ if (_taskDeleteTimer) clearTimeout(_taskDeleteTimer);
6204
+ btn.textContent = 'Deleting...';
6205
+ btn.disabled = true;
6206
+ lttFetch('/api/tasks' + projectParam(), {
6207
+ method: 'DELETE',
6208
+ headers: { 'Content-Type': 'application/json' },
6209
+ body: JSON.stringify({ task_id: taskId })
6210
+ }).then(function(r) { return r.json(); }).then(function(data) {
6211
+ if (data.error) {
6212
+ showToast('!', 'Delete failed: ' + data.error);
6213
+ btn.textContent = 'Delete Task';
6214
+ btn.disabled = false;
6215
+ btn._confirming = false;
6216
+ } else {
6217
+ showToast('&#x2713;', 'Task deleted');
6218
+ closeTaskDetail();
6219
+ fetchTasks();
6220
+ }
6221
+ }).catch(function() {
6222
+ showToast('!', 'Delete failed');
6223
+ btn.textContent = 'Delete Task';
6224
+ btn.disabled = false;
6225
+ btn._confirming = false;
6226
+ });
6227
+ }
6228
+ }
6229
+
5871
6230
  // ==================== KANBAN DRAG-AND-DROP ====================
5872
6231
 
5873
6232
  var draggedTaskId = null;
@@ -5912,7 +6271,7 @@ function onKanbanDrop(e) {
5912
6271
 
5913
6272
  // ==================== STATS VIEW ====================
5914
6273
 
5915
- var AGENT_COLORS = ['#58a6ff', '#f78166', '#7ee787', '#d2a8ff', '#ffa657', '#ff7b72', '#79c0ff', '#56d364'];
6274
+ var AGENT_COLORS = ['#f59e0b', '#f97316', '#7ee787', '#d29922', '#ffa657', '#14b8a6', '#f778ba', '#56d364'];
5916
6275
 
5917
6276
  function fetchStats() {
5918
6277
  var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
@@ -6203,13 +6562,13 @@ function deleteRule(ruleId) {
6203
6562
  // ==================== EXPORT MENU ====================
6204
6563
 
6205
6564
  function toggleExportMenu() {
6206
- var menu = document.getElementById('export-menu');
6565
+ var menu = document.getElementById('export-menu') || document.createElement('div');
6207
6566
  menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
6208
6567
  }
6209
6568
 
6210
6569
  // Close menu on outside click
6211
6570
  document.addEventListener('click', function(e) {
6212
- var menu = document.getElementById('export-menu');
6571
+ var menu = document.getElementById('export-menu') || document.createElement('div');
6213
6572
  if (menu && menu.style.display !== 'none' && !e.target.closest('#export-menu') && !e.target.closest('.btn-primary')) {
6214
6573
  menu.style.display = 'none';
6215
6574
  }
@@ -6281,23 +6640,75 @@ function showProfilePopup(agentName, event) {
6281
6640
 
6282
6641
  document.addEventListener('click', function() {
6283
6642
  document.getElementById('profile-popup').classList.remove('open');
6643
+ // Reset delete button if popup closes
6644
+ var delBtn = document.getElementById('pp-delete-btn');
6645
+ if (delBtn) { delBtn.textContent = 'Delete Agent'; delBtn._confirming = false; }
6284
6646
  });
6285
6647
 
6648
+ var _deleteConfirmTimer = null;
6649
+ function confirmDeleteAgent() {
6650
+ var btn = document.getElementById('pp-delete-btn');
6651
+ if (!editingAgent) return;
6652
+ if (!btn._confirming) {
6653
+ // First click: change to confirm state
6654
+ btn._confirming = true;
6655
+ btn.textContent = 'Confirm Delete';
6656
+ btn.style.background = 'var(--red-dim)';
6657
+ // Reset after 3s if not confirmed
6658
+ _deleteConfirmTimer = setTimeout(function() {
6659
+ btn.textContent = 'Delete Agent';
6660
+ btn.style.background = '';
6661
+ btn._confirming = false;
6662
+ }, 3000);
6663
+ } else {
6664
+ // Second click: actually delete
6665
+ if (_deleteConfirmTimer) clearTimeout(_deleteConfirmTimer);
6666
+ btn.textContent = 'Deleting...';
6667
+ btn.disabled = true;
6668
+ var agentName = editingAgent;
6669
+ var pq = projectParam();
6670
+ lttFetch('/api/agents' + pq, {
6671
+ method: 'DELETE',
6672
+ headers: { 'Content-Type': 'application/json' },
6673
+ body: JSON.stringify({ name: agentName })
6674
+ }).then(function(r) { return r.json(); }).then(function(data) {
6675
+ if (data.error) {
6676
+ showToast('!', 'Delete failed: ' + data.error);
6677
+ btn.textContent = 'Delete Agent';
6678
+ btn.disabled = false;
6679
+ btn._confirming = false;
6680
+ } else {
6681
+ showToast('&#x2713;', agentName + ' removed');
6682
+ document.getElementById('profile-popup').classList.remove('open');
6683
+ btn.textContent = 'Delete Agent';
6684
+ btn.disabled = false;
6685
+ btn._confirming = false;
6686
+ poll(); // Refresh agents
6687
+ }
6688
+ }).catch(function() {
6689
+ showToast('!', 'Delete failed');
6690
+ btn.textContent = 'Delete Agent';
6691
+ btn.disabled = false;
6692
+ btn._confirming = false;
6693
+ });
6694
+ }
6695
+ }
6696
+
6286
6697
  // ==================== v3.0: PROFILE EDITOR ====================
6287
6698
 
6288
6699
  var BUILT_IN_AVATARS = [
6289
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2358a6ff'/%3E%3Ccircle cx='22' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='4' fill='%23fff'/%3E%3Crect x='20' y='38' width='24' height='4' rx='2' fill='%23fff'/%3E%3Crect x='14' y='12' width='6' height='10' rx='3' fill='%2358a6ff' stroke='%23fff' stroke-width='1.5'/%3E%3Crect x='44' y='12' width='6' height='10' rx='3' fill='%2358a6ff' stroke='%23fff' stroke-width='1.5'/%3E%3C/svg%3E",
6700
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23f59e0b'/%3E%3Ccircle cx='22' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='4' fill='%23fff'/%3E%3Crect x='20' y='38' width='24' height='4' rx='2' fill='%23fff'/%3E%3Crect x='14' y='12' width='6' height='10' rx='3' fill='%23f59e0b' stroke='%23fff' stroke-width='1.5'/%3E%3Crect x='44' y='12' width='6' height='10' rx='3' fill='%23f59e0b' stroke='%23fff' stroke-width='1.5'/%3E%3C/svg%3E",
6290
6701
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%233fb950'/%3E%3Ccircle cx='22' cy='26' r='5' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='5' fill='%23fff'/%3E%3Ccircle cx='22' cy='26' r='2' fill='%23333'/%3E%3Ccircle cx='42' cy='26' r='2' fill='%23333'/%3E%3Cpath d='M20 38 Q32 46 44 38' stroke='%23fff' fill='none' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E",
6291
6702
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23d29922'/%3E%3Crect x='16' y='22' width='12' height='8' rx='2' fill='%23fff'/%3E%3Crect x='36' y='22' width='12' height='8' rx='2' fill='%23fff'/%3E%3Ccircle cx='22' cy='26' r='2' fill='%23333'/%3E%3Ccircle cx='42' cy='26' r='2' fill='%23333'/%3E%3Cpath d='M24 40 H40' stroke='%23fff' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E",
6292
6703
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23f85149'/%3E%3Ccircle cx='22' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='22' cy='26' r='2' fill='%23333'/%3E%3Ccircle cx='42' cy='26' r='2' fill='%23333'/%3E%3Cpath d='M22 40 Q32 34 42 40' stroke='%23fff' fill='none' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E",
6293
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23bc8cff'/%3E%3Ccircle cx='22' cy='28' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='28' r='4' fill='%23fff'/%3E%3Cpath d='M16 18 L22 24' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M48 18 L42 24' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3Cellipse cx='32' cy='42' rx='8' ry='4' fill='%23fff'/%3E%3C/svg%3E",
6704
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23fb923c'/%3E%3Ccircle cx='22' cy='28' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='28' r='4' fill='%23fff'/%3E%3Cpath d='M16 18 L22 24' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M48 18 L42 24' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3Cellipse cx='32' cy='42' rx='8' ry='4' fill='%23fff'/%3E%3C/svg%3E",
6294
6705
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23f778ba'/%3E%3Ccircle cx='24' cy='26' r='6' fill='%23fff'/%3E%3Ccircle cx='40' cy='26' r='6' fill='%23fff'/%3E%3Ccircle cx='24' cy='26' r='3' fill='%23333'/%3E%3Ccircle cx='40' cy='26' r='3' fill='%23333'/%3E%3Cpath d='M26 40 Q32 46 38 40' stroke='%23fff' fill='none' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E",
6295
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2379c0ff'/%3E%3Crect x='17' y='23' width='10' height='6' rx='3' fill='%23fff'/%3E%3Crect x='37' y='23' width='10' height='6' rx='3' fill='%23fff'/%3E%3Cpath d='M22 38 L32 44 L42 38' stroke='%23fff' fill='none' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E",
6706
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23fbbf24'/%3E%3Crect x='17' y='23' width='10' height='6' rx='3' fill='%23fff'/%3E%3Crect x='37' y='23' width='10' height='6' rx='3' fill='%23fff'/%3E%3Cpath d='M22 38 L32 44 L42 38' stroke='%23fff' fill='none' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E",
6296
6707
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%237ee787'/%3E%3Ccircle cx='22' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='4' fill='%23fff'/%3E%3Ccircle cx='23' cy='25' r='2' fill='%23333'/%3E%3Ccircle cx='43' cy='25' r='2' fill='%23333'/%3E%3Cpath d='M20 38 Q32 48 44 38' stroke='%23fff' fill='none' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E",
6297
6708
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23e3b341'/%3E%3Cpath d='M18 22 L26 30 L18 30Z' fill='%23fff'/%3E%3Cpath d='M46 22 L38 30 L46 30Z' fill='%23fff'/%3E%3Crect x='24' y='38' width='16' height='6' rx='3' fill='%23fff'/%3E%3C/svg%3E",
6298
6709
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23ffa198'/%3E%3Ccircle cx='22' cy='26' r='5' fill='%23fff'/%3E%3Ccircle cx='42' cy='26' r='5' fill='%23fff'/%3E%3Ccircle cx='22' cy='27' r='2.5' fill='%23333'/%3E%3Ccircle cx='42' cy='27' r='2.5' fill='%23333'/%3E%3Cellipse cx='32' cy='42' rx='6' ry='3' fill='%23fff'/%3E%3C/svg%3E",
6299
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230969da'/%3E%3Crect x='16' y='20' width='14' height='10' rx='2' fill='%23fff'/%3E%3Crect x='34' y='20' width='14' height='10' rx='2' fill='%23fff'/%3E%3Ccircle cx='23' cy='25' r='2' fill='%230969da'/%3E%3Ccircle cx='41' cy='25' r='2' fill='%230969da'/%3E%3Crect x='26' y='38' width='12' height='4' rx='2' fill='%23fff'/%3E%3C/svg%3E",
6300
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238250df'/%3E%3Ccircle cx='24' cy='24' r='5' fill='%23fff'/%3E%3Ccircle cx='40' cy='24' r='5' fill='%23fff'/%3E%3Ccircle cx='24' cy='24' r='2' fill='%238250df'/%3E%3Ccircle cx='40' cy='24' r='2' fill='%238250df'/%3E%3Cpath d='M20 38 Q32 50 44 38' stroke='%23fff' fill='none' stroke-width='3' stroke-linecap='round'/%3E%3Ccircle cx='32' cy='10' r='4' fill='%23fff'/%3E%3C/svg%3E",
6710
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23d97706'/%3E%3Crect x='16' y='20' width='14' height='10' rx='2' fill='%23fff'/%3E%3Crect x='34' y='20' width='14' height='10' rx='2' fill='%23fff'/%3E%3Ccircle cx='23' cy='25' r='2' fill='%23d97706'/%3E%3Ccircle cx='41' cy='25' r='2' fill='%23d97706'/%3E%3Crect x='26' y='38' width='12' height='4' rx='2' fill='%23fff'/%3E%3C/svg%3E",
6711
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%23b45309'/%3E%3Ccircle cx='24' cy='24' r='5' fill='%23fff'/%3E%3Ccircle cx='40' cy='24' r='5' fill='%23fff'/%3E%3Ccircle cx='24' cy='24' r='2' fill='%23b45309'/%3E%3Ccircle cx='40' cy='24' r='2' fill='%23b45309'/%3E%3Cpath d='M20 38 Q32 50 44 38' stroke='%23fff' fill='none' stroke-width='3' stroke-linecap='round'/%3E%3Ccircle cx='32' cy='10' r='4' fill='%23fff'/%3E%3C/svg%3E",
6301
6712
  ];
6302
6713
 
6303
6714
  var editingAgent = '';
@@ -6412,7 +6823,7 @@ function cdPickBodyType(el) {
6412
6823
  function cdRandomize() {
6413
6824
  var skinColors = ['#FFD5B8', '#F5D0A9', '#D4A574', '#C68642', '#8D5524', '#6B3E26', '#FFDFC4', '#F0C8A0', '#E0AC69', '#503335'];
6414
6825
  var hairColors = ['#4A3728', '#1a1a1a', '#8B4513', '#D4A574', '#333', '#8B0000', '#F5DEB3', '#FFD700', '#A52A2A', '#2C1608'];
6415
- var shirtColors = ['#58a6ff', '#f97316', '#a855f7', '#22c55e', '#ef4444', '#eab308', '#06b6d4', '#ec4899', '#6366f1', '#14b8a6'];
6826
+ var shirtColors = ['#f59e0b', '#f97316', '#a855f7', '#22c55e', '#ef4444', '#eab308', '#06b6d4', '#ec4899', '#6366f1', '#14b8a6'];
6416
6827
  var pantsColors = ['#2d3748', '#3d2b1f', '#1e1b2e', '#1a2e1a', '#2e1a1a', '#2e2a1a', '#1a2e2e', '#2e1a28', '#1a1a3e', '#1e2e2e'];
6417
6828
  var shoeColors = ['#1a1a2e', '#2a1a0a', '#0a0a0a', '#3a2a1a', '#1a0a0a'];
6418
6829
  var hairStyles = ['none', 'short', 'spiky', 'long', 'ponytail', 'bob', 'curly', 'afro', 'bun', 'braids', 'mohawk', 'wavy'];
@@ -6482,7 +6893,7 @@ function openProfileEditor() {
6482
6893
 
6483
6894
  // Set color pickers
6484
6895
  document.getElementById('pe-head-color').value = defApp.head_color || '#FFD5B8';
6485
- document.getElementById('pe-shirt-color').value = defApp.shirt_color || '#58a6ff';
6896
+ document.getElementById('pe-shirt-color').value = defApp.shirt_color || '#f59e0b';
6486
6897
  document.getElementById('pe-hair-color').value = defApp.hair_color || '#4A3728';
6487
6898
  document.getElementById('pe-pants-color').value = defApp.pants_color || '#2d3748';
6488
6899
  document.getElementById('pe-shoe-color').value = defApp.shoe_color || '#1a1a2e';
@@ -6658,74 +7069,159 @@ function renderWorkspaces(data) {
6658
7069
 
6659
7070
  function fetchWorkflows() {
6660
7071
  var pq = projectParam();
6661
- document.getElementById('workflows-area').innerHTML = '<div class="loading-spinner">Loading workflows...</div>';
7072
+ var wfArea = document.getElementById('workflows-area');
7073
+ // Only show loading spinner on first load (no existing workflow elements)
7074
+ if (wfArea && !wfArea.querySelector('[data-wf-id]') && !wfArea.querySelector('.tasks-empty')) {
7075
+ wfArea.innerHTML = '<div class="loading-spinner">Loading workflows...</div>';
7076
+ }
6662
7077
  lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }).then(function(data) {
6663
7078
  renderWorkflows(Array.isArray(data) ? data : []);
6664
7079
  }).catch(function() {});
6665
7080
  }
6666
7081
 
7082
+ function buildWorkflowHTML(wf) {
7083
+ var doneCount = wf.steps.filter(function(s) { return s.status === 'done'; }).length;
7084
+ var pct = Math.round((doneCount / wf.steps.length) * 100);
7085
+ var statusColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
7086
+ var barColor = statusColor;
7087
+
7088
+ var stepsHtml = '';
7089
+ for (var j = 0; j < wf.steps.length; j++) {
7090
+ var s = wf.steps[j];
7091
+ var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
7092
+ if (j > 0) stepsHtml += '<span class="step-arrow">&rarr;</span>';
7093
+ var stepTimeHtml = '';
7094
+ if (s.status === 'done' && s.started_at && s.completed_at) {
7095
+ var dur = Math.round((new Date(s.completed_at) - new Date(s.started_at)) / 60000);
7096
+ stepTimeHtml = '<div class="step-time" style="font-size:9px;color:var(--text-muted);margin-top:2px">' + (dur > 0 ? dur + 'm' : '<1m') + '</div>';
7097
+ } else if (s.status === 'in_progress' && s.started_at) {
7098
+ var elapsed = Math.round((Date.now() - new Date(s.started_at)) / 60000);
7099
+ stepTimeHtml = '<div class="step-time" style="font-size:9px;color:var(--accent);margin-top:2px">' + elapsed + 'm elapsed</div>';
7100
+ }
7101
+ stepsHtml += '<div class="step-card ' + s.status + '" data-step-id="' + s.id + '" title="' + escapeHtml(s.notes || '') + '">' +
7102
+ '<span class="step-num">' + s.id + '</span>' +
7103
+ '<span class="step-status" style="font-size:10px;color:' + stepColor + ';font-weight:600;text-transform:uppercase">' + s.status.replace('_', ' ') + '</span>' +
7104
+ '<div class="step-desc">' + escapeHtml(s.description) + '</div>' +
7105
+ (s.assignee ? '<div class="step-assignee">' + escapeHtml(s.assignee) + '</div>' : '') +
7106
+ stepTimeHtml +
7107
+ '</div>';
7108
+ }
7109
+
7110
+ var controlsHtml = wf.status === 'active' ? '<div class="wf-advance-controls" style="margin-top:8px;display:flex;gap:6px"><button class="btn btn-primary" onclick="dashAdvanceWorkflow(\'' + wf.id + '\')">Complete Step</button></div>' : '';
7111
+
7112
+ return '<div class="pipeline" data-wf-id="' + wf.id + '" style="cursor:pointer" onclick="showWorkflowDetail(\'' + wf.id + '\')">' +
7113
+ '<div class="pipeline-header">' +
7114
+ '<div class="pipeline-name">' + escapeHtml(wf.name) + ' <span class="wf-status-label" style="font-size:11px;color:' + statusColor + ';font-weight:600">' + escapeHtml(wf.status) + '</span></div>' +
7115
+ '<div class="pipeline-progress">' + doneCount + '/' + wf.steps.length + ' (' + pct + '%)</div>' +
7116
+ '</div>' +
7117
+ '<div class="pipeline-bar"><div class="pipeline-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div></div>' +
7118
+ '<div class="pipeline-steps">' + stepsHtml + '</div>' +
7119
+ controlsHtml +
7120
+ '</div>';
7121
+ }
7122
+
6667
7123
  function renderWorkflows(workflows) {
6668
7124
  var el = document.getElementById('workflows-area');
6669
7125
  if (!workflows.length) {
6670
- el.innerHTML = '<div class="tasks-empty">No workflows yet. Agents create workflows with <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--orange)">create_workflow(name, steps)</code></div>';
7126
+ if (!el.querySelector('.tasks-empty')) {
7127
+ el.innerHTML = '<div class="tasks-empty">No workflows yet. Agents create workflows with <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--orange)">create_workflow(name, steps)</code></div>';
7128
+ }
6671
7129
  return;
6672
7130
  }
6673
7131
 
6674
- var html = '';
7132
+ // Remove empty-state if present
7133
+ var emptyEl = el.querySelector('.tasks-empty');
7134
+ if (emptyEl) emptyEl.remove();
7135
+ // Remove loading spinner if present
7136
+ var spinner = el.querySelector('.loading-spinner');
7137
+ if (spinner) spinner.remove();
7138
+
7139
+ // Build map of existing workflow DOM elements
7140
+ var existingEls = {};
7141
+ el.querySelectorAll('[data-wf-id]').forEach(function(node) {
7142
+ existingEls[node.getAttribute('data-wf-id')] = node;
7143
+ });
7144
+
7145
+ // Track which IDs are in the new data
7146
+ var activeIds = {};
7147
+
6675
7148
  for (var i = 0; i < workflows.length; i++) {
6676
7149
  var wf = workflows[i];
6677
- var doneCount = wf.steps.filter(function(s) { return s.status === 'done'; }).length;
6678
- var pct = Math.round((doneCount / wf.steps.length) * 100);
6679
- var statusColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
6680
-
6681
- var barColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
6682
- var metaHtml = '<div class="pipeline-meta">';
6683
- if (wf.created_by) metaHtml += '<span>Created by ' + escapeHtml(wf.created_by) + '</span>';
6684
- if (wf.created_at) metaHtml += '<span>' + timeAgo(wf.created_at) + '</span>';
6685
- if (wf.updated_at && wf.updated_at !== wf.created_at) metaHtml += '<span>Updated ' + timeAgo(wf.updated_at) + '</span>';
6686
- metaHtml += '</div>';
6687
-
6688
- html += '<div class="pipeline">' +
6689
- '<div class="pipeline-header">' +
6690
- '<div class="pipeline-name">' + escapeHtml(wf.name) + ' <span style="font-size:11px;color:' + statusColor + ';font-weight:600">' + escapeHtml(wf.status) + '</span></div>' +
6691
- '<div class="pipeline-progress">' + doneCount + '/' + wf.steps.length + ' (' + pct + '%)</div>' +
6692
- '</div>' +
6693
- metaHtml +
6694
- '<div class="pipeline-bar"><div class="pipeline-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div></div>' +
6695
- '<div class="pipeline-steps">';
6696
-
6697
- for (var j = 0; j < wf.steps.length; j++) {
6698
- var s = wf.steps[j];
6699
- var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
6700
- if (j > 0) html += '<span class="step-arrow">&rarr;</span>';
6701
- var stepTimeHtml = '';
6702
- if (s.status === 'done' && s.started_at && s.completed_at) {
6703
- var dur = Math.round((new Date(s.completed_at) - new Date(s.started_at)) / 60000);
6704
- stepTimeHtml = '<div style="font-size:9px;color:var(--text-muted);margin-top:2px">' + (dur > 0 ? dur + 'm' : '<1m') + '</div>';
6705
- } else if (s.status === 'in_progress' && s.started_at) {
6706
- var elapsed = Math.round((Date.now() - new Date(s.started_at)) / 60000);
6707
- stepTimeHtml = '<div style="font-size:9px;color:var(--accent);margin-top:2px">' + elapsed + 'm elapsed</div>';
7150
+ activeIds[wf.id] = true;
7151
+ var existing = existingEls[wf.id];
7152
+
7153
+ if (existing) {
7154
+ // PATCH existing workflow element in place
7155
+ var doneCount = wf.steps.filter(function(s) { return s.status === 'done'; }).length;
7156
+ var pct = Math.round((doneCount / wf.steps.length) * 100);
7157
+
7158
+ // Update progress text
7159
+ var progEl = existing.querySelector('.pipeline-progress');
7160
+ var newProg = doneCount + '/' + wf.steps.length + ' (' + pct + '%)';
7161
+ if (progEl && progEl.textContent !== newProg) progEl.textContent = newProg;
7162
+
7163
+ // Update progress bar
7164
+ var barFill = existing.querySelector('.pipeline-bar-fill');
7165
+ if (barFill) {
7166
+ var newWidth = pct + '%';
7167
+ if (barFill.style.width !== newWidth) barFill.style.width = newWidth;
7168
+ var barColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
7169
+ if (barFill.style.background !== barColor) barFill.style.background = barColor;
6708
7170
  }
6709
7171
 
6710
- html += '<div class="step-card ' + s.status + '" title="' + escapeHtml(s.notes || '') + '">' +
6711
- '<span class="step-num">' + s.id + '</span>' +
6712
- '<span style="font-size:10px;color:' + stepColor + ';font-weight:600;text-transform:uppercase">' + s.status.replace('_', ' ') + '</span>' +
6713
- '<div class="step-desc">' + escapeHtml(s.description) + '</div>' +
6714
- (s.assignee ? '<div class="step-assignee">&#x1f464; ' + escapeHtml(s.assignee) + '</div>' : '') +
6715
- stepTimeHtml +
6716
- '</div>';
6717
- }
6718
- html += '</div>';
7172
+ // Update status label
7173
+ var statusLabel = existing.querySelector('.wf-status-label');
7174
+ if (statusLabel && statusLabel.textContent !== wf.status) {
7175
+ statusLabel.textContent = wf.status;
7176
+ statusLabel.style.color = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
7177
+ }
6719
7178
 
6720
- // Dashboard controls — advance or skip
6721
- if (wf.status === 'active') {
6722
- html += '<div style="margin-top:8px;display:flex;gap:6px">' +
6723
- '<button class="btn btn-primary" onclick="dashAdvanceWorkflow(\'' + wf.id + '\')">Advance Step</button>' +
6724
- '</div>';
7179
+ // Update each step
7180
+ for (var j = 0; j < wf.steps.length; j++) {
7181
+ var s = wf.steps[j];
7182
+ var stepEl = existing.querySelector('[data-step-id="' + s.id + '"]');
7183
+ if (stepEl) {
7184
+ var newClass = 'step-card ' + s.status;
7185
+ if (stepEl.className !== newClass) stepEl.className = newClass;
7186
+ // Update step status text
7187
+ var stepStatusEl = stepEl.querySelector('.step-status');
7188
+ var newStatusText = s.status.replace('_', ' ');
7189
+ if (stepStatusEl && stepStatusEl.textContent !== newStatusText) {
7190
+ var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
7191
+ stepStatusEl.textContent = newStatusText;
7192
+ stepStatusEl.style.color = stepColor;
7193
+ }
7194
+ // Update elapsed time
7195
+ var timeEl = stepEl.querySelector('.step-time');
7196
+ if (s.status === 'in_progress' && s.started_at) {
7197
+ var elapsed = Math.round((Date.now() - new Date(s.started_at)) / 60000);
7198
+ var newTime = elapsed + 'm elapsed';
7199
+ if (timeEl) { if (timeEl.textContent !== newTime) timeEl.textContent = newTime; }
7200
+ else { stepEl.insertAdjacentHTML('beforeend', '<div class="step-time" style="font-size:9px;color:var(--accent);margin-top:2px">' + newTime + '</div>'); }
7201
+ } else if (timeEl && s.status !== 'done') {
7202
+ timeEl.remove();
7203
+ }
7204
+ }
7205
+ }
7206
+
7207
+ var advControls = existing.querySelector('.wf-advance-controls');
7208
+ if (wf.status === 'active') {
7209
+ if (!advControls) {
7210
+ existing.insertAdjacentHTML('beforeend', '<div class="wf-advance-controls" style="margin-top:8px;display:flex;gap:6px"><button class="btn btn-primary" onclick="dashAdvanceWorkflow(\'' + wf.id + '\')">Complete Step</button></div>');
7211
+ }
7212
+ } else if (advControls) {
7213
+ advControls.remove();
7214
+ }
7215
+ } else {
7216
+ // NEW workflow — append
7217
+ el.insertAdjacentHTML('beforeend', buildWorkflowHTML(wf));
6725
7218
  }
6726
- html += '</div>';
6727
7219
  }
6728
- el.innerHTML = html;
7220
+
7221
+ // Remove workflows that no longer exist
7222
+ Object.keys(existingEls).forEach(function(id) {
7223
+ if (!activeIds[id]) existingEls[id].remove();
7224
+ });
6729
7225
  }
6730
7226
 
6731
7227
  function dashAdvanceWorkflow(wfId) {
@@ -6736,6 +7232,150 @@ function dashAdvanceWorkflow(wfId) {
6736
7232
  }).then(function() { fetchWorkflows(); }).catch(function() {});
6737
7233
  }
6738
7234
 
7235
+ // ==================== WORKFLOW DETAIL VIEW ====================
7236
+
7237
+ var _cachedWorkflows = [];
7238
+
7239
+ // Override fetchWorkflows to cache
7240
+ var _origFetchWorkflows = fetchWorkflows;
7241
+ fetchWorkflows = function() {
7242
+ var pq = projectParam();
7243
+ var wfArea = document.getElementById('workflows-area');
7244
+ if (wfArea && !wfArea.querySelector('[data-wf-id]') && !wfArea.querySelector('.tasks-empty')) {
7245
+ wfArea.innerHTML = '<div class="loading-spinner">Loading workflows...</div>';
7246
+ }
7247
+ lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }).then(function(wfs) {
7248
+ _cachedWorkflows = Array.isArray(wfs) ? wfs : [];
7249
+ renderWorkflows(_cachedWorkflows);
7250
+ }).catch(function() { _cachedWorkflows = []; renderWorkflows([]); });
7251
+ };
7252
+
7253
+ function showWorkflowDetail(wfId) {
7254
+ var wf = _cachedWorkflows.find(function(w) { return w.id === wfId; });
7255
+ if (!wf) return;
7256
+ var el = document.getElementById('wf-detail-content');
7257
+ if (!el) return;
7258
+ var steps = wf.steps || [];
7259
+ var doneCount = steps.filter(function(s) { return s.status === 'done'; }).length;
7260
+ var pct = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;
7261
+ var statusColors = { active: 'var(--accent)', completed: 'var(--green)', paused: 'var(--orange)' };
7262
+ var statusColor = statusColors[wf.status] || 'var(--text-muted)';
7263
+
7264
+ var html = '';
7265
+ // Header
7266
+ html += '<div style="margin-bottom:16px">';
7267
+ html += '<div style="font-size:18px;font-weight:700;color:var(--text);margin-bottom:6px">' + escapeHtml(wf.name || 'Workflow') + '</div>';
7268
+ html += '<span style="font-size:11px;padding:3px 8px;border-radius:6px;font-weight:600;background:' + statusColor + '20;color:' + statusColor + '">' + escapeHtml(wf.status) + '</span>';
7269
+ if (wf.autonomous) html += ' <span style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--accent-dim);color:var(--accent);font-weight:600">AUTO</span>';
7270
+ if (wf.parallel) html += ' <span style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--purple-dim);color:var(--purple);font-weight:600">PARALLEL</span>';
7271
+ html += '</div>';
7272
+
7273
+ // Info grid
7274
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:16px;font-size:12px">';
7275
+ html += '<div style="color:var(--text-muted)">Progress <div style="color:var(--text);font-weight:700;font-size:16px;margin-top:2px">' + pct + '<span style="font-size:12px;margin-left:1px">%</span></div></div>';
7276
+ html += '<div style="color:var(--text-muted)">Created by <div style="color:var(--text);font-weight:600;margin-top:2px">' + escapeHtml(wf.created_by || '?') + '</div></div>';
7277
+ html += '<div style="color:var(--text-muted)">Created <div style="color:var(--text);margin-top:2px">' + (wf.created_at ? timeAgo(wf.created_at) : '?') + '</div></div>';
7278
+ html += '</div>';
7279
+
7280
+ // Progress bar
7281
+ html += '<div style="height:6px;background:var(--surface-2);border-radius:3px;overflow:hidden;margin-bottom:16px"><div style="height:100%;width:' + pct + '%;background:' + statusColor + ';border-radius:3px;transition:width 0.3s"></div></div>';
7282
+
7283
+ // Steps
7284
+ html += '<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:8px">Steps (' + doneCount + '/' + steps.length + ')</div>';
7285
+ for (var i = 0; i < steps.length; i++) {
7286
+ var s = steps[i];
7287
+ var sColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : s.status === 'awaiting_approval' ? 'var(--orange)' : 'var(--text-muted)';
7288
+ html += '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin-bottom:6px;border-left:3px solid ' + sColor + '">';
7289
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">';
7290
+ html += '<span style="font-size:13px;font-weight:600;color:var(--text)">Step ' + (s.id || i+1) + ': ' + escapeHtml((s.description || '').substring(0, 80)) + '</span>';
7291
+ html += '<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:' + sColor + '20;color:' + sColor + ';font-weight:600">' + escapeHtml(s.status || 'pending').replace('_', ' ') + '</span>';
7292
+ html += '</div>';
7293
+ html += '<div style="font-size:11px;color:var(--text-muted)">';
7294
+ if (s.assignee) html += 'Assignee: <span style="color:var(--text)">' + escapeHtml(s.assignee) + '</span>';
7295
+ if (s.started_at) html += ' | Started: ' + timeAgo(s.started_at);
7296
+ if (s.completed_at) html += ' | Done: ' + timeAgo(s.completed_at);
7297
+ html += '</div>';
7298
+ if (s.notes) html += '<div style="font-size:11px;color:var(--text-dim);margin-top:4px;font-style:italic">' + escapeHtml(s.notes.substring(0, 200)) + '</div>';
7299
+ html += '</div>';
7300
+ }
7301
+
7302
+ // Checkpoints
7303
+ if (wf.checkpoints && wf.checkpoints.length > 0) {
7304
+ html += '<div style="font-size:13px;font-weight:600;color:var(--text);margin:12px 0 8px">Checkpoints (' + wf.checkpoints.length + ')</div>';
7305
+ for (var c = 0; c < wf.checkpoints.length; c++) {
7306
+ var cp = wf.checkpoints[c];
7307
+ html += '<div style="font-size:12px;padding:6px 8px;background:var(--surface-2);border-radius:6px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">';
7308
+ html += '<span style="color:var(--text-dim)">Step ' + cp.step_id + ' — ' + escapeHtml((cp.step_description || '').substring(0, 50)) + ' <span style="color:var(--text-muted)">' + (cp.completed_at ? timeAgo(cp.completed_at) : '') + '</span></span>';
7309
+ html += '</div>';
7310
+ }
7311
+ }
7312
+
7313
+ // Actions
7314
+ var canDelete = wf.status !== 'active';
7315
+ html += '<div style="border-top:1px solid var(--border);padding-top:12px;margin-top:12px;display:flex;justify-content:flex-end;gap:8px">';
7316
+ if (wf.status === 'active') {
7317
+ html += '<button class="btn btn-primary" onclick="event.stopPropagation();dashAdvanceWorkflow(\'' + wf.id + '\');closeWfDetail()" style="font-size:12px">Complete Step</button>';
7318
+ }
7319
+ if (canDelete) {
7320
+ html += '<button class="btn btn-danger" id="wf-delete-btn" onclick="event.stopPropagation();confirmDeleteWorkflow(\'' + wf.id + '\')" style="font-size:12px">Delete</button>';
7321
+ } else {
7322
+ html += '<button class="btn btn-danger" disabled style="font-size:12px;opacity:0.4;cursor:not-allowed" title="Cannot delete active workflow">Delete</button>';
7323
+ }
7324
+ html += '</div>';
7325
+
7326
+ el.innerHTML = html;
7327
+ var wfOverlay = document.getElementById('wf-detail-overlay');
7328
+ if (wfOverlay) wfOverlay.classList.add('open');
7329
+ }
7330
+
7331
+ function closeWfDetail() {
7332
+ var wfOv = document.getElementById('wf-detail-overlay');
7333
+ if (wfOv) wfOv.classList.remove('open');
7334
+ var btn = document.getElementById('wf-delete-btn');
7335
+ if (btn) { btn.textContent = 'Delete'; btn._confirming = false; }
7336
+ }
7337
+
7338
+ var _wfDeleteTimer = null;
7339
+ function confirmDeleteWorkflow(wfId) {
7340
+ var btn = document.getElementById('wf-delete-btn');
7341
+ if (!btn) return;
7342
+ if (!btn._confirming) {
7343
+ btn._confirming = true;
7344
+ btn.textContent = 'Confirm Delete';
7345
+ btn.style.background = 'var(--red-dim)';
7346
+ _wfDeleteTimer = setTimeout(function() {
7347
+ btn.textContent = 'Delete';
7348
+ btn.style.background = '';
7349
+ btn._confirming = false;
7350
+ }, 3000);
7351
+ } else {
7352
+ if (_wfDeleteTimer) clearTimeout(_wfDeleteTimer);
7353
+ btn.textContent = 'Deleting...';
7354
+ btn.disabled = true;
7355
+ lttFetch('/api/workflows' + projectParam(), {
7356
+ method: 'DELETE',
7357
+ headers: { 'Content-Type': 'application/json' },
7358
+ body: JSON.stringify({ workflow_id: wfId })
7359
+ }).then(function(r) { return r.json(); }).then(function(data) {
7360
+ if (data.error) {
7361
+ showToast('!', 'Delete failed: ' + data.error);
7362
+ btn.textContent = 'Delete';
7363
+ btn.disabled = false;
7364
+ btn._confirming = false;
7365
+ } else {
7366
+ showToast('&#x2713;', 'Workflow deleted');
7367
+ closeWfDetail();
7368
+ fetchWorkflows();
7369
+ }
7370
+ }).catch(function() {
7371
+ showToast('!', 'Delete failed');
7372
+ btn.textContent = 'Delete';
7373
+ btn.disabled = false;
7374
+ btn._confirming = false;
7375
+ });
7376
+ }
7377
+ }
7378
+
6739
7379
  // ==================== v5.0: PLAN EXECUTION VIEW ====================
6740
7380
 
6741
7381
  var planRefreshInterval = null;
@@ -6823,7 +7463,7 @@ function renderPlanView(data) {
6823
7463
  // Step cards
6824
7464
  for (var j = 0; j < steps.length; j++) {
6825
7465
  var s = steps[j];
6826
- var icon = s.status === 'done' ? '&#x2705;' : s.status === 'in_progress' ? '&#x1f504;' : '&#x23f3;';
7466
+ var icon = s.status === 'done' ? '<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 8l3 3 7-7"/></svg>' : s.status === 'in_progress' ? '<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 8a6 6 0 0111 0M14 8a6 6 0 01-11 0"/></svg>' : '...';
6827
7467
  var bgColor = s.status === 'done' ? 'rgba(34,197,94,0.08)' : s.status === 'in_progress' ? 'rgba(59,130,246,0.08)' : 'var(--surface-2)';
6828
7468
  var borderColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--border)';
6829
7469
 
@@ -6852,15 +7492,15 @@ function renderPlanView(data) {
6852
7492
  confidence +
6853
7493
  '</div>' +
6854
7494
  '</div>' +
6855
- (s.flagged ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">&#x26a0;&#xfe0f; ' + escapeHtml(s.flag_reason || 'Flagged for review') + '</div>' : '') +
6856
- (s.verification && s.verification.learnings ? '<div style="font-size:10px;color:var(--purple,#a855f7);margin-top:2px">&#x1f4da; Learned: ' + escapeHtml(s.verification.learnings.substring(0, 100)) + '</div>' : '') +
7495
+ (s.flagged ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">' + escapeHtml(s.flag_reason || 'Flagged for review') + '</div>' : '') +
7496
+ (s.verification && s.verification.learnings ? '<div style="font-size:10px;color:var(--purple,#a855f7);margin-top:2px">Learned: ' + escapeHtml(s.verification.learnings.substring(0, 100)) + '</div>' : '') +
6857
7497
  '</div>';
6858
7498
 
6859
7499
  // Step control buttons (skip/reassign) for in_progress/pending steps
6860
7500
  if (wf.status === 'active' && (s.status === 'in_progress' || s.status === 'pending')) {
6861
7501
  html += '<div style="display:flex;gap:4px">' +
6862
7502
  '<button onclick="planAction(\'skip\',' + s.id + ',\'' + wf.id + '\')" style="font-size:10px;padding:3px 8px;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-muted)" title="Skip this step">Skip</button>' +
6863
- '<button onclick="planReassign(' + s.id + ',\'' + wf.id + '\')" style="font-size:10px;padding:3px 8px;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-muted)" title="Reassign">&#x1f504;</button>' +
7503
+ '<button onclick="planReassign(' + s.id + ',\'' + wf.id + '\')" style="font-size:10px;padding:3px 8px;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-muted)" title="Reassign">Re</button>' +
6864
7504
  '</div>';
6865
7505
  }
6866
7506
  html += '</div>';
@@ -6868,9 +7508,9 @@ function renderPlanView(data) {
6868
7508
 
6869
7509
  // Stats footer
6870
7510
  html += '<div style="display:flex;gap:16px;margin-top:12px;padding-top:10px;border-top:1px solid var(--border);font-size:11px;color:var(--text-muted)">';
6871
- if (retryCount > 0) html += '<span>&#x1f504; Retries: ' + retryCount + '</span>';
6872
- if (flaggedSteps.length > 0) html += '<span>&#x26a0;&#xfe0f; Flagged: ' + flaggedSteps.length + '</span>';
6873
- html += '<span>&#x2705; Done: ' + doneCount + '/' + steps.length + '</span>';
7511
+ if (retryCount > 0) html += '<span>Retries: ' + retryCount + '</span>';
7512
+ if (flaggedSteps.length > 0) html += '<span>Flagged: ' + flaggedSteps.length + '</span>';
7513
+ html += '<span>Done: ' + doneCount + '/' + steps.length + '</span>';
6874
7514
  html += '</div>';
6875
7515
 
6876
7516
  // Control buttons
@@ -7007,7 +7647,7 @@ function renderMonitorPanel(data) {
7007
7647
  for (var i = 0; i < agentNames.length; i++) {
7008
7648
  var name = agentNames[i];
7009
7649
  var a = agents[name];
7010
- var color = a.status === 'active' ? 'var(--green)' : a.status === 'idle' ? 'var(--orange)' : 'var(--red,#ef4444)';
7650
+ var color = a.status === 'working' ? 'var(--green)' : a.status === 'listening' ? '#3b82f6' : a.status === 'idle' ? 'var(--orange)' : 'var(--red,#ef4444)';
7011
7651
  var role = a.role ? ' (' + a.role + ')' : '';
7012
7652
  html += '<div style="padding:4px 8px;background:var(--surface-2);border-left:3px solid ' + color + ';border-radius:4px;font-size:11px">' +
7013
7653
  '<span style="font-weight:600">' + escapeHtml(name) + '</span>' +
@@ -7084,6 +7724,7 @@ var notifData = [];
7084
7724
  var notifSeen = 0;
7085
7725
 
7086
7726
  function toggleNotifPanel() {
7727
+ if (!document.getElementById('notif-panel')) return;
7087
7728
  var panel = document.getElementById('notif-panel');
7088
7729
  var isOpen = panel.style.display !== 'none';
7089
7730
  panel.style.display = isOpen ? 'none' : 'block';
@@ -7109,7 +7750,7 @@ function renderNotifList() {
7109
7750
  var html = '';
7110
7751
  for (var i = notifData.length - 1; i >= 0; i--) {
7111
7752
  var n = notifData[i];
7112
- var icon = n.type === 'online' ? '&#x1f7e2;' : n.type === 'offline' ? '&#x1f534;' : n.type === 'listening' ? '&#x1f50a;' : '&#x1f515;';
7753
+ var icon = n.type === 'online' ? '+' : n.type === 'offline' ? '-' : n.type === 'listening' ? '~' : 'x';
7113
7754
  var time = new Date(n.timestamp).toLocaleTimeString();
7114
7755
  html += '<div style="padding:8px 16px;border-bottom:1px solid var(--border);font-size:12px;display:flex;gap:8px;align-items:center">' +
7115
7756
  '<span>' + icon + '</span>' +
@@ -7120,6 +7761,7 @@ function renderNotifList() {
7120
7761
  }
7121
7762
 
7122
7763
  function updateNotifBadge() {
7764
+ if (!document.getElementById('notif-badge')) return;
7123
7765
  var badge = document.getElementById('notif-badge');
7124
7766
  var unseen = notifData.length - notifSeen;
7125
7767
  if (unseen > 0) { badge.textContent = unseen; badge.style.display = 'block'; }
@@ -7127,6 +7769,7 @@ function updateNotifBadge() {
7127
7769
  }
7128
7770
 
7129
7771
  function clearNotifications() {
7772
+ if (!document.getElementById('notif-list')) return;
7130
7773
  notifData = [];
7131
7774
  notifSeen = 0;
7132
7775
  renderNotifList();
@@ -7136,7 +7779,7 @@ function clearNotifications() {
7136
7779
  // Close notif panel on outside click
7137
7780
  document.addEventListener('click', function(e) {
7138
7781
  var panel = document.getElementById('notif-panel');
7139
- var bell = document.getElementById('notif-bell');
7782
+ var bell = document.getElementById('notif-bell') || document.createElement('div');
7140
7783
  if (panel && panel.style.display !== 'none' && !panel.contains(e.target) && e.target !== bell && !bell.contains(e.target)) {
7141
7784
  panel.style.display = 'none';
7142
7785
  }
@@ -7226,7 +7869,7 @@ function renderScores(data, repData) {
7226
7869
  var n = names[i];
7227
7870
  var a = agents[n];
7228
7871
  var agentRep = rep ? rep[n] : null;
7229
- var medal = i === 0 ? '&#x1f947;' : i === 1 ? '&#x1f948;' : i === 2 ? '&#x1f949;' : '#' + (i + 1);
7872
+ var medal = i === 0 ? '#1' : i === 1 ? '#2' : i === 2 ? '#3' : '#' + (i + 1);
7230
7873
  var scoreColor = a.score >= 80 ? 'var(--green)' : a.score >= 60 ? 'var(--accent)' : a.score >= 40 ? 'var(--orange)' : 'var(--red)';
7231
7874
  html += '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:12px 16px;display:flex;align-items:center;gap:12px">' +
7232
7875
  '<div style="font-size:18px;width:28px;text-align:center">' + medal + '</div>' +
@@ -7271,13 +7914,13 @@ function poll() {
7271
7914
  sendBrowserNotification(cachedHistory[cachedHistory.length - 1]);
7272
7915
  }
7273
7916
 
7274
- document.getElementById('stat-messages').textContent = status.messageCount;
7275
- document.getElementById('stat-agents').textContent = status.agentCount;
7276
- document.getElementById('stat-sleeping').textContent = status.sleepingCount || 0;
7277
- document.getElementById('stat-threads').textContent = status.threadCount;
7917
+ $id('stat-messages').textContent = status.messageCount;
7918
+ $id('stat-agents').textContent = status.agentCount;
7919
+ $id('stat-sleeping').textContent = status.sleepingCount || 0;
7920
+ $id('stat-threads').textContent = status.threadCount;
7278
7921
 
7279
7922
  // Managed mode badge
7280
- var managedBadge = document.getElementById('managed-badge');
7923
+ var managedBadge = $id('managed-badge');
7281
7924
  if (status.conversation_mode === 'managed' && status.managed) {
7282
7925
  managedBadge.style.display = '';
7283
7926
  var phaseLabel = String(status.managed.phase || 'discussion').toUpperCase();
@@ -7285,8 +7928,8 @@ function poll() {
7285
7928
  status.managed.floor === 'directed' ? (status.managed.turn_current || '?') + ' speaking' :
7286
7929
  status.managed.floor === 'open' ? 'round-robin' :
7287
7930
  status.managed.floor === 'execution' ? 'working' : '';
7288
- document.getElementById('managed-phase').textContent = phaseLabel;
7289
- document.getElementById('managed-floor-info').textContent = floorInfo ? ' | ' + floorInfo : '';
7931
+ $id('managed-phase').textContent = phaseLabel;
7932
+ $id('managed-floor-info').textContent = floorInfo ? ' | ' + floorInfo : '';
7290
7933
  } else {
7291
7934
  managedBadge.style.display = 'none';
7292
7935
  }
@@ -7298,7 +7941,7 @@ function poll() {
7298
7941
  var ag = cachedAgents[agentKeys[p]];
7299
7942
  if (ag.alive && ag.provider && ag.provider !== 'unknown') providers[ag.provider] = true;
7300
7943
  }
7301
- document.getElementById('stat-clis').textContent = Object.keys(providers).length;
7944
+ $id('stat-clis').textContent = Object.keys(providers).length;
7302
7945
 
7303
7946
  // Update tab title with message count
7304
7947
  document.title = status.messageCount > 0
@@ -7325,7 +7968,7 @@ function poll() {
7325
7968
  checkPlanCompletion();
7326
7969
  }).catch(function(e) {
7327
7970
  console.error('Poll failed:', e);
7328
- document.getElementById('conn-detail').textContent = ' ERR: ' + e.message;
7971
+ var _cd = document.getElementById('conn-detail'); if(_cd) _cd.textContent = ' ERR: ' + e.message;
7329
7972
  });
7330
7973
  }
7331
7974
 
@@ -7342,6 +7985,14 @@ function doReset() {
7342
7985
 
7343
7986
  // ==================== PROJECT MANAGEMENT ====================
7344
7987
 
7988
+ function setProjectRemoveButtonsVisible(visible) {
7989
+ var disp = visible ? '' : 'none';
7990
+ var icon = document.getElementById('remove-project-btn');
7991
+ var text = document.getElementById('remove-project-text-btn');
7992
+ if (icon) icon.style.display = disp;
7993
+ if (text) text.style.display = disp;
7994
+ }
7995
+
7345
7996
  function loadProjects() {
7346
7997
  return lttFetch('/api/projects').then(function(r) { return r.json(); }).then(function(projects) {
7347
7998
  console.log('[LTT] loadProjects:', projects.length, 'projects, activeProject:', activeProject);
@@ -7355,10 +8006,12 @@ function loadProjects() {
7355
8006
  sel.appendChild(opt);
7356
8007
  }
7357
8008
 
7358
- // Auto-select first project if none selected
7359
- if (!activeProject && projects.length > 0) {
7360
- activeProject = projects[0].path;
7361
- console.log('[LTT] auto-selected first project:', activeProject);
8009
+ // If selection pointed at a project the server removed (e.g. redundant with Default), fall back to Default
8010
+ if (activeProject && !projects.some(function(p) { return p.path === activeProject; })) {
8011
+ activeProject = '';
8012
+ sel.value = '';
8013
+ setProjectRemoveButtonsVisible(false);
8014
+ poll();
7362
8015
  }
7363
8016
 
7364
8017
  // If project came from URL param, try fuzzy match (handles path separator differences)
@@ -7374,7 +8027,7 @@ function loadProjects() {
7374
8027
 
7375
8028
  if (activeProject) {
7376
8029
  sel.value = activeProject;
7377
- document.getElementById('remove-project-btn').style.display = '';
8030
+ setProjectRemoveButtonsVisible(true);
7378
8031
  // Update header project indicator for mobile
7379
8032
  var indicator = document.getElementById('mobile-project-name');
7380
8033
  if (indicator) {
@@ -7384,8 +8037,8 @@ function loadProjects() {
7384
8037
  }
7385
8038
  }
7386
8039
 
7387
- // Show/hide remove button
7388
- document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
8040
+ // Show/hide remove buttons (not shown for Default / local)
8041
+ setProjectRemoveButtonsVisible(!!activeProject);
7389
8042
  // Sync mobile header project select
7390
8043
  syncMobileProjectSelect();
7391
8044
  }).catch(function(e) {
@@ -7396,7 +8049,7 @@ function loadProjects() {
7396
8049
  function switchProject() {
7397
8050
  activeProject = document.getElementById('project-select').value;
7398
8051
  lastMessageCount = 0;
7399
- document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
8052
+ setProjectRemoveButtonsVisible(!!activeProject);
7400
8053
  syncMobileProjectSelect();
7401
8054
  loadConversationList();
7402
8055
  poll();
@@ -7408,7 +8061,7 @@ function mobileSelectProject(val) {
7408
8061
  lastMessageCount = 0;
7409
8062
  // Sync sidebar select
7410
8063
  document.getElementById('project-select').value = val;
7411
- document.getElementById('remove-project-btn').style.display = val ? '' : 'none';
8064
+ setProjectRemoveButtonsVisible(!!val);
7412
8065
  poll();
7413
8066
  }
7414
8067
 
@@ -7429,6 +8082,7 @@ function syncMobileProjectSelect() {
7429
8082
 
7430
8083
  function showAddProject() {
7431
8084
  var input = document.getElementById('project-path-input');
8085
+ if (!input) return;
7432
8086
  if (input.classList.contains('visible')) {
7433
8087
  input.classList.remove('visible');
7434
8088
  } else {
@@ -7439,8 +8093,11 @@ function showAddProject() {
7439
8093
 
7440
8094
  function addProject() {
7441
8095
  var input = document.getElementById('project-path-input');
8096
+ if (!input) return;
7442
8097
  var projectPath = input.value.trim();
7443
8098
  if (!projectPath) return;
8099
+ // If user pasted .../repo/.neohive, use repo root (matches server rules)
8100
+ projectPath = projectPath.replace(/[\\/]+\.neohive\/?$/i, '');
7444
8101
 
7445
8102
  lttFetch('/api/projects', {
7446
8103
  method: 'POST',
@@ -7458,13 +8115,14 @@ function addProject() {
7458
8115
  switchProject();
7459
8116
  }, 200);
7460
8117
  } else {
7461
- alert(res.error || 'Failed to add project');
8118
+ showToast('!', res.error || 'Failed to add project');
7462
8119
  }
7463
8120
  }).catch(function(e) { console.error('Add project failed:', e); });
7464
8121
  }
7465
8122
 
7466
8123
  function discoverProjects() {
7467
8124
  var resultsEl = document.getElementById('discover-results');
8125
+ if (!resultsEl) return;
7468
8126
  resultsEl.style.display = 'block';
7469
8127
  resultsEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px;">Scanning...</div>';
7470
8128
 
@@ -7500,7 +8158,7 @@ function addDiscovered(projectPath, name) {
7500
8158
  body: JSON.stringify({ path: projectPath, name: name })
7501
8159
  }).then(function(r) { return r.json(); }).then(function(res) {
7502
8160
  if (res.success) {
7503
- document.getElementById('discover-results').style.display = 'none';
8161
+ var _dr = document.getElementById('discover-results'); if (_dr) _dr.style.display = 'none';
7504
8162
  loadProjects();
7505
8163
  }
7506
8164
  }).catch(function() {});
@@ -7508,7 +8166,9 @@ function addDiscovered(projectPath, name) {
7508
8166
 
7509
8167
  function removeProject() {
7510
8168
  if (!activeProject) return;
7511
- if (!confirm('Remove this project from the dashboard?')) return;
8169
+ var sel = document.getElementById('project-select');
8170
+ var label = sel && sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].textContent : activeProject;
8171
+ if (!confirm('Remove “' + label + '” from the dashboard?\n\nYour .neohive folder on disk is not deleted — only this shortcut in the list.')) return;
7512
8172
 
7513
8173
  lttFetch('/api/projects', {
7514
8174
  method: 'DELETE',
@@ -7518,10 +8178,17 @@ function removeProject() {
7518
8178
  if (res.success) {
7519
8179
  activeProject = '';
7520
8180
  document.getElementById('project-select').value = '';
8181
+ setProjectRemoveButtonsVisible(false);
7521
8182
  loadProjects();
7522
8183
  poll();
8184
+ showToast('\u2713', 'Project removed from list');
8185
+ } else {
8186
+ showToast('!', res.error || 'Could not remove project');
7523
8187
  }
7524
- }).catch(function(e) { console.error('Remove project failed:', e); });
8188
+ }).catch(function(e) {
8189
+ console.error('Remove project failed:', e);
8190
+ showToast('!', 'Remove project failed');
8191
+ });
7525
8192
  }
7526
8193
 
7527
8194
  // ==================== SOUND NOTIFICATIONS ====================
@@ -7531,18 +8198,75 @@ var audioCtx = null;
7531
8198
 
7532
8199
  function toggleSound() {
7533
8200
  soundEnabled = !soundEnabled;
7534
- var btn = document.getElementById('sound-toggle');
8201
+ var btn = document.getElementById('sound-toggle') || document.createElement('div');
7535
8202
  if (soundEnabled) {
7536
8203
  btn.classList.add('active');
7537
- btn.innerHTML = '&#x1f50a;';
7538
- // Init audio context on first user interaction
8204
+ btn.innerHTML = '~';
7539
8205
  if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
7540
8206
  } else {
7541
8207
  btn.classList.remove('active');
7542
- btn.innerHTML = '&#x1f508;';
8208
+ btn.innerHTML = 'Off';
7543
8209
  }
7544
8210
  }
7545
8211
 
8212
+ function toggleCombinedNotifications() {
8213
+ var label = document.getElementById('settings-notif-label');
8214
+ if (typeof Notification === 'undefined') {
8215
+ if (label) label.textContent = 'Notifications (not supported)';
8216
+ return;
8217
+ }
8218
+ if (!notifEnabled && !soundEnabled) {
8219
+ // Turn both on — request permission if needed
8220
+ if (notifPermission !== 'granted') {
8221
+ Notification.requestPermission().then(function(perm) {
8222
+ notifPermission = perm;
8223
+ if (perm === 'granted') {
8224
+ notifEnabled = true;
8225
+ soundEnabled = true;
8226
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
8227
+ localStorage.setItem('ltt-notif', 'true');
8228
+ updateNotifBtn();
8229
+ updateCombinedNotifLabel();
8230
+ } else {
8231
+ if (label) label.textContent = 'Notifications (blocked by browser)';
8232
+ }
8233
+ });
8234
+ } else {
8235
+ notifEnabled = true;
8236
+ soundEnabled = true;
8237
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
8238
+ localStorage.setItem('ltt-notif', 'true');
8239
+ updateNotifBtn();
8240
+ updateCombinedNotifLabel();
8241
+ }
8242
+ } else {
8243
+ // Turn both off
8244
+ notifEnabled = false;
8245
+ soundEnabled = false;
8246
+ localStorage.setItem('ltt-notif', 'false');
8247
+ updateNotifBtn();
8248
+ updateCombinedNotifLabel();
8249
+ }
8250
+ }
8251
+
8252
+ function updateCombinedNotifLabel() {
8253
+ var label = document.getElementById('settings-notif-label');
8254
+ if (!label) return;
8255
+ if (notifEnabled && soundEnabled) {
8256
+ label.textContent = 'Notifications: On';
8257
+ label.style.color = 'var(--green)';
8258
+ } else if (notifPermission === 'denied') {
8259
+ label.textContent = 'Notifications (blocked by browser)';
8260
+ label.style.color = 'var(--red)';
8261
+ } else {
8262
+ label.textContent = 'Notifications: Off';
8263
+ label.style.color = '';
8264
+ }
8265
+ }
8266
+
8267
+ // Init label on load
8268
+ setTimeout(updateCombinedNotifLabel, 500);
8269
+
7546
8270
  function playNotifSound() {
7547
8271
  if (!audioCtx) return;
7548
8272
  try {
@@ -7588,14 +8312,6 @@ var pollLatency = 0;
7588
8312
 
7589
8313
  function updateConnectionInfo(latency) {
7590
8314
  pollLatency = latency;
7591
- var detail = document.getElementById('conn-detail');
7592
- if (latency < 100) {
7593
- detail.textContent = ' ' + latency + 'ms';
7594
- } else if (latency < 500) {
7595
- detail.textContent = ' ' + latency + 'ms';
7596
- } else {
7597
- detail.textContent = ' slow';
7598
- }
7599
8315
  }
7600
8316
 
7601
8317
  // ==================== REPLAY MODE ====================
@@ -7616,7 +8332,7 @@ function enterReplay() {
7616
8332
  document.getElementById('search-bar').style.display = 'none';
7617
8333
  document.getElementById('replay-slider').max = cachedHistory.length;
7618
8334
  document.getElementById('replay-slider').value = 0;
7619
- document.getElementById('replay-header-btn').style.display = 'none';
8335
+ var _rh = document.getElementById('replay-header-btn'); if(_rh) _rh.style.display = 'none';
7620
8336
  renderReplayMessages();
7621
8337
  }
7622
8338
 
@@ -7627,7 +8343,7 @@ function exitReplay() {
7627
8343
  document.getElementById('replay-bar').classList.remove('visible');
7628
8344
  document.getElementById('view-tabs').style.display = '';
7629
8345
  document.getElementById('search-bar').style.display = '';
7630
- document.getElementById('replay-header-btn').style.display = '';
8346
+ var _rh = document.getElementById('replay-header-btn'); if(_rh) _rh.style.display = '';
7631
8347
  lastMessageCount = 0;
7632
8348
  renderMessages(cachedHistory);
7633
8349
  }
@@ -7744,13 +8460,13 @@ function toggleNotifications() {
7744
8460
  }
7745
8461
 
7746
8462
  function updateNotifBtn() {
7747
- var btn = document.getElementById('notif-toggle');
8463
+ var btn = document.getElementById('notif-toggle') || document.createElement('div');
7748
8464
  if (notifEnabled) {
7749
8465
  btn.classList.add('active');
7750
- btn.innerHTML = '&#x1f514;';
8466
+ btn.innerHTML = 'On';
7751
8467
  } else {
7752
8468
  btn.classList.remove('active');
7753
- btn.innerHTML = '&#x1f515;';
8469
+ btn.innerHTML = 'x';
7754
8470
  }
7755
8471
  }
7756
8472
 
@@ -7761,7 +8477,7 @@ function sendBrowserNotification(msg) {
7761
8477
  var preview = msg.content.substring(0, 80);
7762
8478
  var n = new Notification(msg.from + ' sent a message', {
7763
8479
  body: preview,
7764
- icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect rx="20" width="100" height="100" fill="%230d1117"/><path d="M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z" fill="%2358a6ff"/></svg>',
8480
+ icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect rx="20" width="100" height="100" fill="%230d1117"/><path d="M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z" fill="%23f59e0b"/></svg>',
7765
8481
  tag: 'ltt-' + msg.id
7766
8482
  });
7767
8483
  n.onclick = function() { window.focus(); n.close(); };
@@ -7784,21 +8500,23 @@ function fetchLanState() {
7784
8500
 
7785
8501
  function updateLanUI() {
7786
8502
  // Update phone button indicator
7787
- var phoneBtn = document.getElementById('phone-btn');
8503
+ var phoneBtn = document.getElementById('phone-btn') || document.createElement('div');
7788
8504
  phoneBtn.classList.toggle('lan-active', lanState.lan_mode);
7789
8505
 
7790
8506
  // Update LAN badge in connection area
7791
8507
  var existing = document.getElementById('lan-badge');
7792
8508
  if (lanState.lan_mode && lanState.lan_ip) {
7793
8509
  if (!existing) {
7794
- var conn = document.querySelector('.connection');
7795
- var badge = document.createElement('span');
7796
- badge.className = 'lan-badge';
7797
- badge.id = 'lan-badge';
7798
- conn.appendChild(badge);
7799
- existing = badge;
8510
+ var conn = document.querySelector('.connection') || document.querySelector('.header-actions');
8511
+ if (conn) {
8512
+ var badge = document.createElement('span');
8513
+ badge.className = 'lan-badge';
8514
+ badge.id = 'lan-badge';
8515
+ conn.appendChild(badge);
8516
+ existing = badge;
8517
+ }
7800
8518
  }
7801
- existing.textContent = 'LAN ' + lanState.lan_ip + ':' + lanState.port;
8519
+ if (existing) existing.textContent = 'LAN ' + lanState.lan_ip + ':' + lanState.port;
7802
8520
  } else if (existing) {
7803
8521
  existing.remove();
7804
8522
  }
@@ -7812,6 +8530,10 @@ function openPhoneAccess() {
7812
8530
  fetchLanState().then(function() {
7813
8531
  renderPhoneModalContent();
7814
8532
  document.getElementById('phone-modal-overlay').classList.add('open');
8533
+ }).catch(function() {
8534
+ // Still open the modal even if fetch fails — show current state
8535
+ renderPhoneModalContent();
8536
+ document.getElementById('phone-modal-overlay').classList.add('open');
7815
8537
  });
7816
8538
  }
7817
8539
 
@@ -7903,87 +8625,336 @@ var selectedCli = 'claude';
7903
8625
  var _launchWatchers = []; // [{name, cli, prompt, state}] for tracking launch status
7904
8626
  var _launchWatchInterval = null;
7905
8627
 
8628
+ var ROLE_SKILLS = {
8629
+ lead: ['planning', 'delegation', 'tracking', 'review'],
8630
+ backend: ['javascript', 'typescript', 'nodejs', 'coding', 'debugging'],
8631
+ frontend: ['ui', 'ux', 'css', 'html', 'frontend', 'design'],
8632
+ quality: ['review', 'testing', 'quality', 'security', 'standards'],
8633
+ monitor: ['observability', 'logging', 'performance', 'health-checks']
8634
+ };
8635
+ var ROLE_PROMPTS = {
8636
+ lead: function(name) { return 'You are ' + name + ', the Coordinator in a multi-agent team. Register as "' + name + '".\n\nYour job is to:\n1. Break the user\'s request into tasks and delegate to team agents via send_message\n2. Use create_task() and create_workflow() to formally track work\n3. Monitor progress with workflow_status() and list_tasks()\n4. Use consume_messages() to check agent updates without blocking\n5. Synthesize results and present to the user\n\nYou MUST NOT edit files or write code. Delegate ALL code work to other agents. Your tools: send_message, create_task, create_workflow, advance_workflow, workflow_status, list_tasks, consume_messages, broadcast.'; },
8637
+ backend: function(name) { return 'You are ' + name + ', a Developer in a multi-agent team. Register as "' + name + '". Call listen() to wait for tasks.\n\nWhen you receive a task:\n1. Use lock_file() before editing shared code\n2. Implement the requested changes with clean, tested code\n3. Use unlock_file() when done editing\n4. Update your task with update_task(status="done")\n5. Send a summary to the Coordinator with file paths and key decisions\n6. Call listen() again for the next task\n\nFocus on production-quality code. Include file paths in reports.'; },
8638
+ frontend: function(name) { return 'You are ' + name + ', a Frontend Developer in a multi-agent team. Register as "' + name + '". Call listen() to wait for tasks.\n\nWhen you receive a task:\n1. Use lock_file() before editing shared frontend code\n2. Implement UI/UX changes following design conventions\n3. Use unlock_file() when done\n4. Update your task with update_task(status="done")\n5. Send a summary to the Coordinator with file paths and screenshots if relevant\n6. Call listen() again for the next task\n\nFocus on clean UI, accessibility, and responsive design.'; },
8639
+ quality: function(name) { return 'You are ' + name + ', a Reviewer in a multi-agent team. Register as "' + name + '". Call listen() to wait for review requests.\n\nWhen you receive work to review:\n1. Read the actual files that were changed\n2. Check for bugs, security issues, code style, edge cases\n3. Use submit_review() to formally approve or request changes\n4. Send structured feedback: blockers vs suggestions\n5. Call listen() again\n\nBe constructive and specific. Reference line numbers. Never let mediocre work pass.'; },
8640
+ monitor: function(name) { return 'You are ' + name + ', a System Monitor in a multi-agent team. Register as "' + name + '". Call listen() to wait for events.\n\nYour job:\n1. Watch for idle agents, stuck tasks, and circular escalations\n2. Use send_message to nudge idle agents\n3. Use update_task to reassign stuck tasks\n4. Log interventions to your workspace via workspace_write\n5. Call listen() again\n\nNever stop monitoring. You ARE the system intelligence.'; }
8641
+ };
8642
+
7906
8643
  function renderLaunchPanel() {
7907
8644
  var el = document.getElementById('launch-area');
7908
- // Fetch unified templates (serves both templates/ and conversation-templates/ dirs)
7909
- lttFetch('/api/templates').then(function(r) { return r.json(); }).then(function(allTemplates) {
7910
- // Split by source field "templates" = quick start, "conversation-templates" = workflows
8645
+ var pq = projectParam();
8646
+ Promise.all([
8647
+ lttFetch('/api/templates').then(function(r) { return r.json(); }).catch(function() { return []; }),
8648
+ lttFetch('/api/agents' + pq).then(function(r) { return r.json(); }).catch(function() { return {}; })
8649
+ ]).then(function(results) {
8650
+ var allTemplates = results[0];
8651
+ var existingAgents = results[1];
8652
+ var existingNames = Object.keys(existingAgents).map(function(n) { return n.toLowerCase(); });
8653
+
7911
8654
  var simpleTemplates = [];
7912
8655
  var convTemplates = [];
7913
8656
  for (var k = 0; k < allTemplates.length; k++) {
7914
- if (allTemplates[k].source === 'conversation-templates') {
7915
- convTemplates.push(allTemplates[k]);
7916
- } else {
7917
- simpleTemplates.push(allTemplates[k]);
7918
- }
8657
+ if (allTemplates[k].source === 'conversation-templates') convTemplates.push(allTemplates[k]);
8658
+ else simpleTemplates.push(allTemplates[k]);
7919
8659
  }
7920
8660
  launchTemplates = simpleTemplates;
8661
+ window._convTemplates = convTemplates;
8662
+
8663
+ var html = '';
8664
+ // Section 1: Add Agent
8665
+ html += '<div class="launch-section">';
8666
+ html += '<h3>Add Agent</h3>';
8667
+ html += '<div class="launch-form-row">';
8668
+ html += '<div class="launch-form-field"><label>Agent Name</label><input type="text" id="launch-name" placeholder="e.g. Backend, Reviewer" maxlength="20" oninput="validateLaunchName()"><div class="field-hint" id="launch-name-hint">1-20 alphanumeric, underscore, or hyphen</div></div>';
8669
+ html += '<div class="launch-form-field"><label>Role</label><select id="launch-role" onchange="onLaunchRoleChange()"><option value="">Select role...</option><option value="lead">Lead</option><option value="backend">Backend</option><option value="frontend">Frontend</option><option value="quality">Quality</option><option value="monitor">Monitor</option></select></div>';
8670
+ html += '</div>';
8671
+ html += '<div class="launch-form-row">';
8672
+ html += '<div class="launch-form-field"><label>CLI Type</label><select id="launch-cli"><option value="claude"' + (selectedCli === 'claude' ? ' selected' : '') + '>Claude Code</option><option value="gemini"' + (selectedCli === 'gemini' ? ' selected' : '') + '>Gemini CLI</option><option value="codex"' + (selectedCli === 'codex' ? ' selected' : '') + '>Codex CLI</option><option value="cursor"' + (selectedCli === 'cursor' ? ' selected' : '') + '>Cursor IDE</option></select></div>';
8673
+ html += '<div class="launch-form-field"><label>Skills</label><input type="text" id="launch-skills" placeholder="Auto-filled from role" readonly style="color:var(--text-muted)"></div>';
8674
+ html += '</div>';
8675
+ html += '<div style="margin-top:8px"><button class="btn btn-primary" onclick="generateAgentPrompt()" style="margin-right:8px">Generate Prompt</button><button class="btn" onclick="doLaunch()">Launch Terminal</button></div>';
8676
+ html += '<div id="launch-prompt-output" style="display:none;margin-top:12px"></div>';
8677
+ html += '<div class="launch-result" id="launch-result"></div>';
8678
+ html += '</div>';
7921
8679
 
7922
- // Build unified template picker with optgroups
7923
- var templateOpts = '<option value="">-- No template --</option>';
7924
- if (simpleTemplates.length) {
7925
- templateOpts += '<optgroup label="Quick Start">';
7926
- for (var i = 0; i < simpleTemplates.length; i++) {
7927
- templateOpts += '<option value="simple:' + escapeHtml(simpleTemplates[i].name) + '">' + escapeHtml(simpleTemplates[i].name) + ' \u2014 ' + escapeHtml(simpleTemplates[i].description || '') + '</option>';
8680
+ // Section 2: Team Templates
8681
+ html += '<div class="launch-section">';
8682
+ html += '<h3 style="display:flex;justify-content:space-between;align-items:center">Team Templates <button class="btn btn-primary" onclick="showTemplateBuilder()" style="font-size:12px;padding:5px 12px">+ Create Template</button></h3>';
8683
+ if (allTemplates.length > 0) {
8684
+ html += '<div class="template-grid">';
8685
+ var allTpls = simpleTemplates.concat(convTemplates);
8686
+ for (var t = 0; t < allTpls.length; t++) {
8687
+ var tpl = allTpls[t];
8688
+ var agents = tpl.agents || [];
8689
+ var isConv = tpl.source === 'conversation-templates';
8690
+ html += '<div class="template-card" onclick="expandTemplate(' + t + ')">';
8691
+ html += '<div class="template-card-name">' + escapeHtml(tpl.name || tpl.id || 'Template') + '</div>';
8692
+ html += '<div class="template-card-desc">' + escapeHtml(tpl.description || '') + '</div>';
8693
+ html += '<div class="template-card-agents">';
8694
+ if (agents.length > 0) {
8695
+ for (var a = 0; a < agents.length; a++) {
8696
+ var chipLabel = agents[a].display_name || agents[a].name || 'Agent';
8697
+ if (agents[a].role) chipLabel += ' (' + agents[a].role + ')';
8698
+ html += '<span class="template-agent-chip">' + escapeHtml(chipLabel) + '</span>';
8699
+ }
8700
+ } else if (tpl.roles) {
8701
+ var roleNames = Object.keys(tpl.roles);
8702
+ for (var r = 0; r < roleNames.length; r++) {
8703
+ html += '<span class="template-agent-chip">' + escapeHtml(roleNames[r]) + '</span>';
8704
+ }
8705
+ }
8706
+ html += '</div></div>';
7928
8707
  }
7929
- templateOpts += '</optgroup>';
8708
+ html += '</div>';
8709
+ html += '<div id="template-expanded" style="display:none;margin-top:16px"></div>';
8710
+ } else {
8711
+ html += '<div style="color:var(--text-muted);font-size:13px;padding:8px 0">No templates yet. Create one to get started.</div>';
7930
8712
  }
7931
- if (convTemplates.length) {
7932
- templateOpts += '<optgroup label="Workflows (multi-agent)">';
7933
- for (var j = 0; j < convTemplates.length; j++) {
7934
- var ct = convTemplates[j];
7935
- var agentCount = ct.agents ? ct.agents.length : 0;
7936
- templateOpts += '<option value="conv:' + escapeHtml(ct.id || ct.name) + '">' + escapeHtml(ct.name) + ' (' + agentCount + ' agents) \u2014 ' + escapeHtml(ct.description || '') + '</option>';
8713
+ html += '<div id="template-builder" style="display:none;margin-top:16px"></div>';
8714
+ html += '</div>';
8715
+
8716
+ // Store data for JS functions
8717
+ window._launchExistingNames = existingNames;
8718
+ window._allLaunchTemplates = allTpls;
8719
+
8720
+ el.innerHTML = html;
8721
+ }).catch(function() {
8722
+ el.innerHTML = '<div class="launch-section"><h3>Add Agent</h3><p style="color:var(--text-dim)">Failed to load data.</p></div>';
8723
+ });
8724
+ }
8725
+
8726
+ function validateLaunchName() {
8727
+ var input = document.getElementById('launch-name');
8728
+ var hint = document.getElementById('launch-name-hint');
8729
+ var name = (input.value || '').trim();
8730
+ if (!name) { hint.className = 'field-hint'; hint.textContent = '1-20 alphanumeric, underscore, or hyphen'; input.classList.remove('invalid'); return; }
8731
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) { hint.className = 'field-hint error'; hint.textContent = 'Invalid: only letters, numbers, _ and -'; input.classList.add('invalid'); return; }
8732
+ if (window._launchExistingNames && window._launchExistingNames.indexOf(name.toLowerCase()) !== -1) { hint.className = 'field-hint error'; hint.textContent = 'Name already taken by an existing agent'; input.classList.add('invalid'); return; }
8733
+ hint.className = 'field-hint success'; hint.textContent = 'Name available'; input.classList.remove('invalid');
8734
+ }
8735
+
8736
+ function onLaunchRoleChange() {
8737
+ var role = document.getElementById('launch-role').value;
8738
+ var skillsInput = document.getElementById('launch-skills');
8739
+ if (role && ROLE_SKILLS[role]) {
8740
+ skillsInput.value = ROLE_SKILLS[role].join(', ');
8741
+ } else {
8742
+ skillsInput.value = '';
8743
+ }
8744
+ }
8745
+
8746
+ function generateAgentPrompt() {
8747
+ var name = (document.getElementById('launch-name').value || '').trim();
8748
+ var role = document.getElementById('launch-role').value;
8749
+ var cli = document.getElementById('launch-cli').value;
8750
+ if (!name) { showToast('!', 'Enter an agent name'); return; }
8751
+ if (!/^[a-zA-Z0-9_-]{1,20}$/.test(name)) { showToast('!', 'Invalid agent name'); return; }
8752
+
8753
+ var prompt;
8754
+ if (role && ROLE_PROMPTS[role]) {
8755
+ prompt = ROLE_PROMPTS[role](name);
8756
+ } else {
8757
+ prompt = 'You are ' + name + ' in a multi-agent team. Register as "' + name + '". Call listen() to wait for tasks. When you receive a task, implement it, report what you did, and call listen() again.';
8758
+ }
8759
+
8760
+ var outputEl = document.getElementById('launch-prompt-output');
8761
+ outputEl.style.display = 'block';
8762
+ outputEl.innerHTML = '<div class="launch-prompt-output" onclick="copyGeneratedPrompt()"><div style="font-size:11px;color:var(--text-muted);margin-bottom:6px">Click to copy</div>' + escapeHtml(prompt) + '</div>';
8763
+ window._generatedLaunchPrompt = prompt;
8764
+ }
8765
+
8766
+ function copyGeneratedPrompt() {
8767
+ if (window._generatedLaunchPrompt) {
8768
+ navigator.clipboard.writeText(window._generatedLaunchPrompt).then(function() {
8769
+ showToast('&#x2713;', 'Prompt copied to clipboard');
8770
+ });
8771
+ }
8772
+ }
8773
+
8774
+ function expandTemplate(index) {
8775
+ var tpls = window._allLaunchTemplates;
8776
+ if (!tpls || !tpls[index]) return;
8777
+ var tpl = tpls[index];
8778
+ var agents = tpl.agents || [];
8779
+ var expanded = document.getElementById('template-expanded');
8780
+ if (!expanded) return;
8781
+
8782
+ var html = '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:16px">';
8783
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"><span style="font-size:14px;font-weight:600">' + escapeHtml(tpl.name) + '</span><button class="btn" onclick="document.getElementById(\'template-expanded\').style.display=\'none\'" style="font-size:11px;padding:4px 10px">Close</button></div>';
8784
+
8785
+ if (agents.length > 0) {
8786
+ for (var i = 0; i < agents.length; i++) {
8787
+ var ag = agents[i];
8788
+ var agPrompt = ag.prompt || 'You are ' + (ag.name || 'Agent') + '. Register as "' + (ag.name || 'Agent') + '". Call listen() for tasks.';
8789
+ html += '<div style="margin-bottom:10px;padding:10px;background:var(--surface);border:1px solid var(--border);border-radius:8px">';
8790
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">';
8791
+ html += '<span style="font-weight:600;font-size:13px">' + escapeHtml(ag.display_name || ag.name || 'Agent') + '</span>';
8792
+ html += '<span style="font-size:11px;color:var(--text-muted)">' + escapeHtml(ag.role || '') + '</span>';
8793
+ html += '</div>';
8794
+ if (ag.skills && ag.skills.length) {
8795
+ html += '<div style="margin-bottom:4px;display:flex;gap:4px;flex-wrap:wrap">';
8796
+ for (var s = 0; s < ag.skills.length; s++) {
8797
+ html += '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--accent-dim);color:var(--accent)">' + escapeHtml(ag.skills[s]) + '</span>';
8798
+ }
8799
+ html += '</div>';
7937
8800
  }
7938
- templateOpts += '</optgroup>';
8801
+ if (ag.responsibilities && ag.responsibilities.length) {
8802
+ html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:6px">' + ag.responsibilities.map(function(r) { return escapeHtml(r); }).join(' / ') + '</div>';
8803
+ }
8804
+ html += '<div class="launch-prompt-output" style="font-size:11px;max-height:100px" onclick="navigator.clipboard.writeText(this.textContent).then(function(){showToast(\'&#x2713;\',\'Copied!\')})">' + escapeHtml(agPrompt) + '</div>';
8805
+ html += '</div>';
7939
8806
  }
8807
+ } else if (tpl.roles) {
8808
+ var roleNames = Object.keys(tpl.roles);
8809
+ for (var r = 0; r < roleNames.length; r++) {
8810
+ var rolePrompt = tpl.roles[roleNames[r]].prompt || 'Register as "' + roleNames[r] + '" and call listen().';
8811
+ html += '<div style="margin-bottom:8px;padding:10px;background:var(--surface);border:1px solid var(--border);border-radius:8px">';
8812
+ html += '<div style="font-weight:600;font-size:13px;margin-bottom:4px">' + escapeHtml(roleNames[r]) + '</div>';
8813
+ html += '<div class="launch-prompt-output" style="font-size:11px;max-height:100px" onclick="navigator.clipboard.writeText(this.textContent).then(function(){showToast(\'&#x2713;\',\'Copied!\')})">' + escapeHtml(rolePrompt) + '</div>';
8814
+ html += '</div>';
8815
+ }
8816
+ }
8817
+ html += '</div>';
7940
8818
 
7941
- // Store conv templates for later use
7942
- window._convTemplates = convTemplates;
8819
+ expanded.innerHTML = html;
8820
+ expanded.style.display = 'block';
8821
+ expanded.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
8822
+ }
7943
8823
 
7944
- el.innerHTML =
7945
- '<div class="launch-panel">' +
7946
- '<h3>Launch Agent Terminal</h3>' +
7947
- '<div class="launch-field">' +
7948
- '<label>Project Folder</label>' +
7949
- '<input type="text" id="launch-project" value="' + escapeHtml(activeProject) + '" placeholder="Path to project folder">' +
7950
- '</div>' +
7951
- '<div class="launch-field">' +
7952
- '<label>CLI Type</label>' +
7953
- '<div class="cli-btns">' +
7954
- '<div class="cli-btn' + (selectedCli === 'claude' ? ' selected' : '') + '" onclick="selectCli(\'claude\')">Claude</div>' +
7955
- '<div class="cli-btn' + (selectedCli === 'gemini' ? ' selected' : '') + '" onclick="selectCli(\'gemini\')">Gemini</div>' +
7956
- '<div class="cli-btn' + (selectedCli === 'codex' ? ' selected' : '') + '" onclick="selectCli(\'codex\')">Codex</div>' +
7957
- '</div>' +
7958
- '</div>' +
7959
- '<div class="launch-field">' +
7960
- '<label>Agent Name</label>' +
7961
- '<input type="text" id="launch-name" placeholder="e.g. Builder, Coder, Reviewer" maxlength="20">' +
7962
- '</div>' +
7963
- '<div class="launch-field">' +
7964
- '<label>Template</label>' +
7965
- '<select id="launch-template" onchange="onTemplateSelect()">' + templateOpts + '</select>' +
7966
- '</div>' +
7967
- '<div id="launch-template-preview"></div>' +
7968
- '<div class="launch-field">' +
7969
- '<label>Prompt (auto-copied to clipboard on launch)</label>' +
7970
- '<textarea id="launch-prompt" placeholder="Custom instructions for the agent..."></textarea>' +
7971
- '</div>' +
7972
- '<div class="launch-actions">' +
7973
- '<button class="btn btn-primary" onclick="doLaunch()">Launch Terminal</button>' +
7974
- '<button class="btn" onclick="copyLaunchPrompt()">Copy Prompt</button>' +
7975
- '</div>' +
7976
- '<div id="launch-all-section" style="display:none">' +
7977
- '<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">' +
7978
- '<button class="btn btn-primary" onclick="doLaunchAll()" style="width:100%;padding:12px;font-size:14px">Launch All Agents from Template</button>' +
7979
- '<p style="font-size:11px;color:var(--text-muted);margin-top:6px;text-align:center">Opens a terminal for each agent in the selected template. Prompts are copied to clipboard sequentially.</p>' +
7980
- '</div>' +
7981
- '</div>' +
7982
- '<div class="launch-result" id="launch-result"></div>' +
7983
- '<div id="launch-status-tracker" class="launch-status-list" style="display:none"></div>' +
7984
- '</div>';
8824
+ // ==================== CUSTOM TEMPLATE BUILDER ====================
8825
+
8826
+ var _tplBuilderAgents = [];
8827
+
8828
+ function showTemplateBuilder(prefill) {
8829
+ var el = document.getElementById('template-builder');
8830
+ if (!el) return;
8831
+ _tplBuilderAgents = prefill && prefill.agents ? prefill.agents.map(function(a) { return Object.assign({}, a); }) : [
8832
+ { name: 'Coordinator', role: 'lead', skills: 'planning, delegation', prompt: '' },
8833
+ { name: 'Developer', role: 'backend', skills: 'javascript, coding', prompt: '' }
8834
+ ];
8835
+
8836
+ renderBuilderForm(el, prefill);
8837
+ el.style.display = 'block';
8838
+ el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
8839
+ }
8840
+
8841
+ function renderBuilderForm(el, prefill) {
8842
+ var html = '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:20px">';
8843
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><span style="font-size:16px;font-weight:700;color:var(--text)">' + (prefill ? 'Edit Template' : 'Create Template') + '</span><button class="btn" onclick="hideTemplateBuilder()" style="font-size:11px;padding:4px 10px">Cancel</button></div>';
8844
+
8845
+ // Template info
8846
+ html += '<div class="launch-form-row">';
8847
+ html += '<div class="launch-form-field"><label>Template Name</label><input type="text" id="tpl-name" value="' + escapeHtml(prefill ? prefill.name || '' : '') + '" placeholder="e.g. My Dev Team"></div>';
8848
+ html += '<div class="launch-form-field"><label>Category</label><select id="tpl-category"><option value="development"' + (prefill && prefill.category === 'development' ? ' selected' : '') + '>Development</option><option value="research"' + (prefill && prefill.category === 'research' ? ' selected' : '') + '>Research</option><option value="review"' + (prefill && prefill.category === 'review' ? ' selected' : '') + '>Review</option><option value="custom"' + (prefill && prefill.category === 'custom' ? ' selected' : '') + '>Custom</option></select></div>';
8849
+ html += '</div>';
8850
+ html += '<div class="launch-form-field" style="margin-bottom:16px"><label>Description</label><input type="text" id="tpl-desc" value="' + escapeHtml(prefill ? prefill.description || '' : '') + '" placeholder="Brief description of this team template"></div>';
8851
+ html += '<div class="launch-form-field" style="margin-bottom:16px"><label>Mode</label><select id="tpl-mode"><option value="direct"' + (prefill && prefill.conversation_mode === 'direct' ? ' selected' : '') + '>Direct</option><option value="group"' + (prefill && prefill.conversation_mode === 'group' ? ' selected' : '') + '>Group</option><option value="managed"' + (prefill && prefill.conversation_mode === 'managed' ? ' selected' : '') + '>Managed</option></select></div>';
8852
+
8853
+ // Agents
8854
+ html += '<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:8px">Agents</div>';
8855
+ for (var i = 0; i < _tplBuilderAgents.length; i++) {
8856
+ var ag = _tplBuilderAgents[i];
8857
+ html += '<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:8px">';
8858
+ html += '<div class="launch-form-row">';
8859
+ html += '<div class="launch-form-field"><label>Name</label><input type="text" data-tpl-agent="' + i + '" data-field="name" value="' + escapeHtml(ag.name || '') + '" oninput="updateTplAgent(' + i + ',\'name\',this.value)"></div>';
8860
+ html += '<div class="launch-form-field"><label>Role</label><select data-tpl-agent="' + i + '" data-field="role" onchange="updateTplAgent(' + i + ',\'role\',this.value);autoFillTplSkills(' + i + ',this.value)"><option value="">Select...</option><option value="lead"' + (ag.role === 'lead' ? ' selected' : '') + '>Lead</option><option value="backend"' + (ag.role === 'backend' ? ' selected' : '') + '>Backend</option><option value="frontend"' + (ag.role === 'frontend' ? ' selected' : '') + '>Frontend</option><option value="quality"' + (ag.role === 'quality' ? ' selected' : '') + '>Quality</option><option value="monitor"' + (ag.role === 'monitor' ? ' selected' : '') + '>Monitor</option></select></div>';
8861
+ html += '</div>';
8862
+ html += '<div class="launch-form-field" style="margin-bottom:6px"><label>Skills</label><input type="text" data-tpl-agent="' + i + '" data-field="skills" value="' + escapeHtml(ag.skills || '') + '" oninput="updateTplAgent(' + i + ',\'skills\',this.value)" placeholder="comma-separated skills"></div>';
8863
+ html += '<div class="launch-form-field"><label>Prompt</label><textarea data-tpl-agent="' + i + '" data-field="prompt" oninput="updateTplAgent(' + i + ',\'prompt\',this.value)" style="min-height:60px;background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:8px;font-size:12px;color:var(--text);font-family:inherit;resize:vertical;outline:none;width:100%">' + escapeHtml(ag.prompt || '') + '</textarea></div>';
8864
+ if (_tplBuilderAgents.length > 1) {
8865
+ html += '<button class="btn btn-danger" onclick="removeTplAgent(' + i + ')" style="font-size:11px;padding:3px 8px;margin-top:6px">Remove</button>';
8866
+ }
8867
+ html += '</div>';
8868
+ }
8869
+ html += '<button class="btn" onclick="addTplAgent()" style="font-size:12px;margin-bottom:16px">+ Add Agent</button>';
8870
+
8871
+ // Save
8872
+ html += '<div style="display:flex;gap:8px;justify-content:flex-end;border-top:1px solid var(--border);padding-top:12px">';
8873
+ if (prefill && prefill.id) {
8874
+ html += '<button class="btn btn-primary" onclick="saveCustomTemplate(\'' + escapeHtml(prefill.id) + '\')" style="font-size:12px">Update Template</button>';
8875
+ } else {
8876
+ html += '<button class="btn btn-primary" onclick="saveCustomTemplate()" style="font-size:12px">Save Template</button>';
8877
+ }
8878
+ html += '</div>';
8879
+ html += '</div>';
8880
+ el.innerHTML = html;
8881
+ }
8882
+
8883
+ function hideTemplateBuilder() {
8884
+ var el = document.getElementById('template-builder');
8885
+ if (el) el.style.display = 'none';
8886
+ }
8887
+
8888
+ function updateTplAgent(idx, field, value) {
8889
+ if (_tplBuilderAgents[idx]) _tplBuilderAgents[idx][field] = value;
8890
+ }
8891
+
8892
+ function autoFillTplSkills(idx, role) {
8893
+ if (ROLE_SKILLS[role]) {
8894
+ _tplBuilderAgents[idx].skills = ROLE_SKILLS[role].join(', ');
8895
+ var el = document.getElementById('template-builder');
8896
+ if (el) renderBuilderForm(el);
8897
+ }
8898
+ }
8899
+
8900
+ function addTplAgent() {
8901
+ _tplBuilderAgents.push({ name: '', role: '', skills: '', prompt: '' });
8902
+ var el = document.getElementById('template-builder');
8903
+ if (el) renderBuilderForm(el);
8904
+ }
8905
+
8906
+ function removeTplAgent(idx) {
8907
+ _tplBuilderAgents.splice(idx, 1);
8908
+ var el = document.getElementById('template-builder');
8909
+ if (el) renderBuilderForm(el);
8910
+ }
8911
+
8912
+ function saveCustomTemplate(existingId) {
8913
+ var name = (document.getElementById('tpl-name').value || '').trim();
8914
+ var desc = (document.getElementById('tpl-desc').value || '').trim();
8915
+ var category = document.getElementById('tpl-category').value;
8916
+ var mode = document.getElementById('tpl-mode').value;
8917
+
8918
+ if (!name) { showToast('!', 'Template name is required'); return; }
8919
+ if (_tplBuilderAgents.length < 1) { showToast('!', 'Add at least one agent'); return; }
8920
+ var hasEmptyName = _tplBuilderAgents.some(function(a) { return !a.name || !a.name.trim(); });
8921
+ if (hasEmptyName) { showToast('!', 'All agents must have a name'); return; }
8922
+
8923
+ var agents = _tplBuilderAgents.map(function(a) {
8924
+ return {
8925
+ name: a.name.trim(),
8926
+ role: a.role || '',
8927
+ display_name: a.name.trim(),
8928
+ skills: (a.skills || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean),
8929
+ prompt: a.prompt || 'You are ' + a.name.trim() + '. Register as "' + a.name.trim() + '" and call listen() for tasks.'
8930
+ };
8931
+ });
8932
+
8933
+ var body = {
8934
+ name: name,
8935
+ description: desc,
8936
+ category: category,
8937
+ conversation_mode: mode,
8938
+ agents: agents
8939
+ };
8940
+
8941
+ var method = existingId ? 'PUT' : 'POST';
8942
+ if (existingId) body.id = existingId;
8943
+
8944
+ lttFetch('/api/custom-templates' + projectParam(), {
8945
+ method: method,
8946
+ headers: { 'Content-Type': 'application/json' },
8947
+ body: JSON.stringify(body)
8948
+ }).then(function(r) { return r.json(); }).then(function(data) {
8949
+ if (data.error) {
8950
+ showToast('!', data.error);
8951
+ } else {
8952
+ showToast('&#x2713;', 'Template saved');
8953
+ hideTemplateBuilder();
8954
+ renderLaunchPanel();
8955
+ }
7985
8956
  }).catch(function() {
7986
- el.innerHTML = '<div class="launch-panel"><h3>Launch Agent Terminal</h3><p style="color:var(--text-dim)">Failed to load templates.</p></div>';
8957
+ showToast('!', 'Failed to save template');
7987
8958
  });
7988
8959
  }
7989
8960
 
@@ -8087,10 +9058,10 @@ function fillAgentFromConvTemplate(templateId, agentName) {
8087
9058
 
8088
9059
  // ==================== LAUNCH SINGLE AGENT ====================
8089
9060
  function doLaunch() {
8090
- var projectDir = document.getElementById('launch-project').value.trim();
8091
9061
  var agentName = document.getElementById('launch-name').value.trim();
8092
- var prompt = document.getElementById('launch-prompt').value.trim();
8093
9062
  var resultEl = document.getElementById('launch-result');
9063
+ var cliSelect = document.getElementById('launch-cli');
9064
+ var cli = cliSelect ? cliSelect.value : selectedCli;
8094
9065
 
8095
9066
  if (!agentName) {
8096
9067
  resultEl.className = 'launch-result error';
@@ -8098,9 +9069,10 @@ function doLaunch() {
8098
9069
  return;
8099
9070
  }
8100
9071
 
8101
- // Copy prompt to clipboard before launching
8102
- var launchPrompt = prompt || 'You are agent "' + agentName + '". Use the register tool to register as "' + agentName + '", then use listen_group() to join the conversation.';
9072
+ // Use generated prompt if available, otherwise build a default
9073
+ var launchPrompt = window._generatedLaunchPrompt || 'You are agent "' + agentName + '". Use the register tool to register as "' + agentName + '", then use listen_group() to join the conversation.';
8103
9074
  navigator.clipboard.writeText(launchPrompt).catch(function() {});
9075
+ selectedCli = cli;
8104
9076
 
8105
9077
  resultEl.className = 'launch-result info';
8106
9078
  resultEl.innerHTML = 'Launching terminal for <strong>' + escapeHtml(agentName) + '</strong>...';
@@ -8108,7 +9080,7 @@ function doLaunch() {
8108
9080
  lttFetch('/api/launch', {
8109
9081
  method: 'POST',
8110
9082
  headers: { 'Content-Type': 'application/json' },
8111
- body: JSON.stringify({ cli: selectedCli, project_dir: projectDir || undefined, agent_name: agentName, prompt: launchPrompt })
9083
+ body: JSON.stringify({ cli: cli, project_dir: undefined, agent_name: agentName, prompt: launchPrompt })
8112
9084
  }).then(function(r) { return r.json(); }).then(function(data) {
8113
9085
  if (data.error) {
8114
9086
  resultEl.className = 'launch-result error';
@@ -8125,11 +9097,18 @@ function doLaunch() {
8125
9097
  startLaunchWatcher(agentName);
8126
9098
  } else {
8127
9099
  resultEl.className = 'launch-result success';
8128
- var html = '<strong>MCP config written.</strong> Run this command in a terminal:<br>';
8129
- html += '<div class="launch-prompt-box" onclick="copyText(this)" data-text="' + escapeHtml(data.command).replace(/"/g, '&quot;') + '">' + escapeHtml(data.command) + '</div>';
8130
- html += '<br>Then paste this prompt:<br>';
8131
- html += '<div class="launch-prompt-box" onclick="copyText(this)" data-text="' + escapeHtml(launchPrompt).replace(/"/g, '&quot;') + '">' + escapeHtml(launchPrompt.substring(0, 300)) + (launchPrompt.length > 300 ? '...' : '') + '</div>';
8132
- resultEl.innerHTML = html;
9100
+ if (data.cli === 'cursor') {
9101
+ resultEl.innerHTML =
9102
+ '<strong>.cursor/mcp.json updated</strong> with <code>NEOHIVE_DATA_DIR</code>. Open this project in Cursor, reload MCP tools if needed, then paste the prompt (already copied).<br>' +
9103
+ '<span style="font-size:11px;color:var(--text-dim)">' + escapeHtml(data.project_dir || '') + '</span><br><br>' +
9104
+ '<div class="launch-prompt-box" onclick="copyText(this)" data-text="' + escapeHtml(launchPrompt).replace(/"/g, '&quot;') + '">' + escapeHtml(launchPrompt.substring(0, 300)) + (launchPrompt.length > 300 ? '...' : '') + '</div>';
9105
+ } else {
9106
+ var html = '<strong>MCP config written.</strong> Run this command in a terminal:<br>';
9107
+ html += '<div class="launch-prompt-box" onclick="copyText(this)" data-text="' + escapeHtml(data.command).replace(/"/g, '&quot;') + '">' + escapeHtml(data.command) + '</div>';
9108
+ html += '<br>Then paste this prompt:<br>';
9109
+ html += '<div class="launch-prompt-box" onclick="copyText(this)" data-text="' + escapeHtml(launchPrompt).replace(/"/g, '&quot;') + '">' + escapeHtml(launchPrompt.substring(0, 300)) + (launchPrompt.length > 300 ? '...' : '') + '</div>';
9110
+ resultEl.innerHTML = html;
9111
+ }
8133
9112
  startLaunchWatcher(agentName);
8134
9113
  }
8135
9114
  }).catch(function(err) {
@@ -8276,7 +9255,9 @@ function renderLaunchStatusTracker() {
8276
9255
  var h = '<div style="font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Launch Status</div>';
8277
9256
  for (var i = 0; i < _launchWatchers.length; i++) {
8278
9257
  var w = _launchWatchers[i];
8279
- var stateLabel = { pending: 'Waiting...', launching: 'Opening terminal...', launched: 'Terminal open \u2014 paste prompt', registered: 'Online', failed: 'Failed' }[w.state] || w.state;
9258
+ var launchingLabel = w.cli === 'cursor' ? 'Writing MCP config...' : 'Opening terminal...';
9259
+ var launchedLabel = w.cli === 'cursor' ? 'Open Cursor \u2014 paste prompt' : 'Terminal open \u2014 paste prompt';
9260
+ var stateLabel = { pending: 'Waiting...', launching: launchingLabel, launched: launchedLabel, registered: 'Online', failed: 'Failed' }[w.state] || w.state;
8280
9261
  if (w.state === 'failed' && w.error) stateLabel = 'Failed: ' + w.error;
8281
9262
  h += '<div class="launch-status-item">' +
8282
9263
  '<span class="ls-dot ' + w.state + '"></span>' +
@@ -8309,8 +9290,8 @@ function renderDocs() {
8309
9290
  el.innerHTML =
8310
9291
  '<div class="docs-container">' +
8311
9292
 
8312
- '<h2>Neohive v5.0</h2>' +
8313
- '<p class="docs-subtitle">True Autonomy Engine \u2014 AI agents that self-organize, self-verify, and never stop working. Works with Claude Code, Gemini CLI, and Codex CLI.</p>' +
9293
+ '<h2>Neohive v6.0</h2>' +
9294
+ '<p class="docs-subtitle">True Autonomy Engine \u2014 AI agents that self-organize, self-verify, and never stop working. Works with Claude Code, Gemini CLI, Codex CLI, and Cursor IDE.</p>' +
8314
9295
 
8315
9296
  // Quick Start — One Command
8316
9297
  '<div class="docs-section">' +
@@ -8423,7 +9404,7 @@ function renderDocs() {
8423
9404
  '<div class="docs-section">' +
8424
9405
  '<h3>Frequently Asked Questions</h3>' +
8425
9406
  '<h4>Do agents need internet access?</h4>' +
8426
- '<p>No. Agents communicate through local files in your project\'s <code>.neohive/</code> folder. No cloud servers, no API calls between agents. The AI CLIs themselves may need internet for their LLM providers, but the agent bridge is fully local.</p>' +
9407
+ '<p>No. Agents communicate through local files in your project\'s <code>.neohive/</code> folder. No cloud servers, no API calls between agents. The AI CLIs themselves may need internet for their LLM providers, but Neohive is fully local.</p>' +
8427
9408
  '<h4>Can I mix different AI CLIs?</h4>' +
8428
9409
  '<p>Yes! Claude Code, Gemini CLI, and Codex CLI can all talk to each other in the same conversation. Run <code>npx neohive init --all</code> to configure all of them.</p>' +
8429
9410
  '<h4>What happens when message files get large?</h4>' +
@@ -8461,6 +9442,316 @@ function copyLaunchPrompt() {
8461
9442
  });
8462
9443
  }
8463
9444
 
9445
+ // ==================== SETTINGS MENU ====================
9446
+
9447
+ function toggleSettingsMenu() {
9448
+ var menu = document.getElementById('settings-menu');
9449
+ if (menu) menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
9450
+ }
9451
+ // Close settings when clicking outside
9452
+ document.addEventListener('click', function(e) {
9453
+ var menu = document.getElementById('settings-menu');
9454
+ if (menu && menu.style.display === 'block' && !e.target.closest('.header-settings-btn') && !e.target.closest('#settings-menu')) {
9455
+ menu.style.display = 'none';
9456
+ }
9457
+ });
9458
+
9459
+ // ==================== SIDEBAR TOGGLE ====================
9460
+
9461
+ function toggleNavSidebar() {
9462
+ var sidebar = document.getElementById('nav-sidebar');
9463
+ if (!sidebar) return;
9464
+ var isExpanded = sidebar.classList.contains('expanded');
9465
+ sidebar.classList.toggle('expanded');
9466
+ sidebar.classList.toggle('collapsed');
9467
+ var btn = sidebar.querySelector('.nav-sidebar-toggle');
9468
+ if (btn) btn.innerHTML = isExpanded ? '&#x25B6;' : '&#x25C0;';
9469
+ try { localStorage.setItem('neohive-sidebar-collapsed', isExpanded ? '1' : '0'); } catch(e) {}
9470
+ }
9471
+ // Restore sidebar state from localStorage
9472
+ (function() {
9473
+ try {
9474
+ var saved = localStorage.getItem('neohive-sidebar-collapsed');
9475
+ if (saved === '1') {
9476
+ var sidebar = document.getElementById('nav-sidebar');
9477
+ if (sidebar) {
9478
+ sidebar.classList.remove('expanded');
9479
+ sidebar.classList.add('collapsed');
9480
+ var btn = sidebar.querySelector('.nav-sidebar-toggle');
9481
+ if (btn) btn.innerHTML = '&#x25B6;';
9482
+ }
9483
+ }
9484
+ } catch(e) {}
9485
+ })();
9486
+
9487
+ // ==================== AGENT STATUS BAR ====================
9488
+
9489
+ var prevAgentBarState = {};
9490
+
9491
+ function updateAgentBar(agents) {
9492
+ var bar = document.getElementById('agent-bar');
9493
+ if (!bar || !agents) return;
9494
+ var html = '';
9495
+ var sorted = Object.entries(agents).sort(function(a, b) {
9496
+ var aAlive = a[1].alive ? 1 : 0;
9497
+ var bAlive = b[1].alive ? 1 : 0;
9498
+ return bAlive - aAlive;
9499
+ });
9500
+ for (var i = 0; i < sorted.length; i++) {
9501
+ var name = sorted[i][0];
9502
+ var agent = sorted[i][1];
9503
+ if (name === '__system__' || name === 'Dashboard') continue;
9504
+ var cls = 'offline';
9505
+ var activity = 'offline';
9506
+ if (agent.alive) {
9507
+ if (agent.is_listening || agent.status === 'listening') { cls = 'listening'; activity = 'listening'; }
9508
+ else if (agent.status === 'idle') { cls = 'idle'; activity = 'idle'; }
9509
+ else { cls = 'working'; activity = agent.status === 'working' ? 'working' : 'active'; }
9510
+ }
9511
+ // Toast on state change
9512
+ var prevState = prevAgentBarState[name];
9513
+ if (prevState !== undefined) {
9514
+ if (!prevState && agent.alive) showToast('<span style="color:var(--green)">+</span>', name + ' came online');
9515
+ if (prevState && !agent.alive) showToast('<span style="color:var(--text-muted)">-</span>', name + ' went offline');
9516
+ }
9517
+ prevAgentBarState[name] = agent.alive;
9518
+ html += '<div class="agent-pill ' + cls + '" onclick="showAgentPopup(\'' + name + '\')">' +
9519
+ '<span class="status-dot"></span>' +
9520
+ '<span class="agent-name">' + name + '</span>' +
9521
+ '<span class="agent-activity">' + activity + '</span>' +
9522
+ '</div>';
9523
+ }
9524
+ if (bar.innerHTML !== html) bar.innerHTML = html;
9525
+ }
9526
+
9527
+ // ==================== OVERVIEW PAGE ====================
9528
+
9529
+ function renderOverview() {
9530
+ var area = document.getElementById('overview-area');
9531
+ if (!area) return;
9532
+ var savedScroll = area.scrollTop;
9533
+ var pq = projectParam();
9534
+ var histQ = pq + (pq ? '&' : '?') + 'limit=5' + (activeBranch && activeBranch !== 'main' ? '&branch=' + encodeURIComponent(activeBranch) : '');
9535
+ Promise.all([
9536
+ lttFetch('/api/status' + pq).then(function(r) { return r.json(); }),
9537
+ lttFetch('/api/agents' + pq).then(function(r) { return r.json(); }),
9538
+ lttFetch('/api/history' + histQ).then(function(r) { return r.json(); }),
9539
+ lttFetch('/api/tasks' + pq).then(function(r) { return r.json(); }),
9540
+ lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }),
9541
+ lttFetch('/api/notifications' + pq + (pq ? '&' : '?') + 'limit=10').then(function(r) { return r.json(); }).catch(function() { return []; })
9542
+ ]).then(function(results) {
9543
+ var status = results[0];
9544
+ var agents = results[1];
9545
+ var history = Array.isArray(results[2]) ? results[2] : (results[2].messages || []);
9546
+ var tasks = Array.isArray(results[3]) ? results[3] : [];
9547
+ var workflows = Array.isArray(results[4]) ? results[4] : [];
9548
+ var notifications = Array.isArray(results[5]) ? results[5] : [];
9549
+
9550
+ var agentEntries = Object.entries(agents).filter(function(e) { return e[0] !== '__system__'; });
9551
+
9552
+ // Data hash to skip unnecessary rebuilds (excludes timestamps)
9553
+ var overviewHash = (status.aliveCount || 0) + ':' + (status.messageCount || 0) + ':' + agentEntries.length + ':' + tasks.map(function(t) { return t.status; }).join(',') + ':' + workflows.map(function(w) { return w.status; }).join(',') + ':' + history.length + ':' + notifications.length + ':b:' + (activeBranch || 'main');
9554
+ if (overviewHash === renderOverview._lastHash) { return; }
9555
+ renderOverview._lastHash = overviewHash;
9556
+
9557
+ var pending = tasks.filter(function(t) { return t.status === 'pending'; }).length;
9558
+ var inProgress = tasks.filter(function(t) { return t.status === 'in_progress'; }).length;
9559
+ var done = tasks.filter(function(t) { return t.status === 'done'; }).length;
9560
+ var taskPct = tasks.length > 0 ? Math.round((done / tasks.length) * 100) : 0;
9561
+ var activeWfs = workflows.filter(function(w) { return w.status === 'active'; });
9562
+ var hasAgents = agentEntries.length > 0;
9563
+ var hasMessages = history.length > 0;
9564
+ var hasWorkflows = activeWfs.length > 0;
9565
+ var hasTasks = tasks.length > 0;
9566
+
9567
+ var html = '';
9568
+
9569
+ // Welcome header with coordinator mode toggle
9570
+ var coordMode = status.coordinator_mode || 'responsive';
9571
+ html += '<div class="overview-welcome" style="display:flex;justify-content:space-between;align-items:flex-start">';
9572
+ html += '<div><h2>Dashboard</h2>';
9573
+ html += '<p>' + (status.conversation_mode === 'managed' ? 'Managed mode' : 'Direct mode') + ' &middot; ' + (status.agentCount || 0) + ' agents registered</p></div>';
9574
+ html += '<div style="display:flex;align-items:center;gap:8px;padding-top:4px">';
9575
+ html += '<span style="font-size:12px;color:var(--text-muted)">Coordinator</span>';
9576
+ html += '<div style="display:flex;border:1px solid var(--border);border-radius:8px;overflow:hidden;font-size:12px">';
9577
+ html += '<button onclick="setCoordinatorMode(\'responsive\')" style="padding:5px 12px;border:none;cursor:pointer;font-size:12px;font-weight:' + (coordMode === 'responsive' ? '600' : '400') + ';background:' + (coordMode === 'responsive' ? 'var(--accent-dim)' : 'var(--surface-2)') + ';color:' + (coordMode === 'responsive' ? 'var(--accent)' : 'var(--text-muted)') + ';transition:all 0.15s">Stay with me</button>';
9578
+ html += '<button onclick="setCoordinatorMode(\'autonomous\')" style="padding:5px 12px;border:none;border-left:1px solid var(--border);cursor:pointer;font-size:12px;font-weight:' + (coordMode === 'autonomous' ? '600' : '400') + ';background:' + (coordMode === 'autonomous' ? 'var(--green-dim)' : 'var(--surface-2)') + ';color:' + (coordMode === 'autonomous' ? 'var(--green)' : 'var(--text-muted)') + ';transition:all 0.15s">Run autonomously</button>';
9579
+ html += '</div></div>';
9580
+ html += '</div>';
9581
+
9582
+ // Metric cards with icons
9583
+ html += '<div class="overview-metrics">';
9584
+ html += '<div class="metric-card">';
9585
+ html += '<div class="metric-icon" style="background:var(--green-dim)"><svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="var(--green)" stroke-width="1.5"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6"/></svg></div>';
9586
+ html += '<div class="metric-label">Active Agents</div>';
9587
+ html += '<div class="metric-value">' + (status.aliveCount || 0) + '</div>';
9588
+ html += '<div class="metric-sub">' + (status.sleepingCount || 0) + ' sleeping</div>';
9589
+ html += '</div>';
9590
+
9591
+ html += '<div class="metric-card">';
9592
+ html += '<div class="metric-icon" style="background:var(--accent-dim)"><svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="var(--accent)" stroke-width="1.5"><path d="M2 3h12v8H6l-3 2.5V11H2z" stroke-linejoin="round"/></svg></div>';
9593
+ html += '<div class="metric-label">Messages</div>';
9594
+ html += '<div class="metric-value">' + (status.messageCount || 0) + '</div>';
9595
+ html += '<div class="metric-sub">' + (status.threadCount || 0) + ' threads</div>';
9596
+ html += '</div>';
9597
+
9598
+ html += '<div class="metric-card">';
9599
+ html += '<div class="metric-icon" style="background:' + (taskPct === 100 ? 'var(--green-dim)' : 'var(--purple-dim)') + '"><svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="' + (taskPct === 100 ? 'var(--green)' : 'var(--purple)') + '" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><path d="M5 8l2 2 4-4"/></svg></div>';
9600
+ html += '<div class="metric-label">Task Progress</div>';
9601
+ html += '<div class="metric-value">' + taskPct + '<span style="font-size:18px;font-weight:600;color:var(--text-muted);margin-left:2px">%</span></div>';
9602
+ html += '<div class="metric-sub">' + done + ' done, ' + inProgress + ' active, ' + pending + ' pending</div>';
9603
+ html += '</div>';
9604
+ html += '</div>';
9605
+
9606
+ if (!hasAgents && !hasMessages && !hasWorkflows && !hasTasks) {
9607
+ // Empty state with branded feel
9608
+ html += '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;background:var(--surface);border:1px solid var(--border);border-radius:16px">';
9609
+ html += '<div style="width:64px;height:64px;border-radius:16px;background:var(--accent-dim);display:flex;align-items:center;justify-content:center;margin-bottom:20px"><svg viewBox="0 0 16 16" width="28" height="28" fill="none" stroke="var(--accent)" stroke-width="1.2"><path d="M8 1.5L14 5v6l-6 3.5L2 11V5z"/></svg></div>';
9610
+ html += '<div style="font-size:18px;font-weight:700;color:var(--text);margin-bottom:6px">No agents online yet</div>';
9611
+ html += '<div style="font-size:13px;color:var(--text-muted);max-width:360px;line-height:1.6;margin-bottom:4px">Open a CLI terminal and register an agent to start collaborating.</div>';
9612
+ html += '<button class="overview-launch-btn" onclick="switchView(\'launch\')"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 2v12M2 8h12"/></svg> Launch Agents</button>';
9613
+ html += '</div>';
9614
+ } else {
9615
+ html += '<div class="overview-grid">';
9616
+
9617
+ // Active agents panel
9618
+ if (hasAgents) {
9619
+ html += '<div class="overview-panel"><h3>Agents <a onclick="switchView(\'messages\')">View messages &rarr;</a></h3>';
9620
+ for (var i = 0; i < agentEntries.length; i++) {
9621
+ var aName = agentEntries[i][0];
9622
+ var aInfo = agentEntries[i][1];
9623
+ var dotColor = !aInfo.alive ? '#6b7280' : aInfo.is_listening ? '#3b82f6' : aInfo.status === 'idle' ? '#f59e0b' : '#22c55e';
9624
+ var dotGlow = !aInfo.alive ? '' : ';box-shadow:0 0 6px ' + dotColor;
9625
+ var statusText = !aInfo.alive ? 'offline' : aInfo.is_listening ? 'listening' : aInfo.status === 'idle' ? 'idle' : 'working';
9626
+ html += '<div class="overview-agent-row"><span class="overview-agent-dot" style="background:' + dotColor + dotGlow + '"></span><span class="overview-agent-name">' + aName + '</span><span class="overview-agent-status">' + statusText + ' &middot; ' + (aInfo.provider || '?') + '</span></div>';
9627
+ }
9628
+ html += '</div>';
9629
+ }
9630
+
9631
+ // Workflow progress panel
9632
+ if (hasWorkflows) {
9633
+ html += '<div class="overview-panel"><h3>Workflows <a onclick="switchView(\'workflows\')">View all &rarr;</a></h3>';
9634
+ for (var w = 0; w < activeWfs.length; w++) {
9635
+ var wf = activeWfs[w];
9636
+ var wfDone = wf.steps ? wf.steps.filter(function(s) { return s.status === 'done'; }).length : 0;
9637
+ var wfTotal = wf.steps ? wf.steps.length : 1;
9638
+ var wfPct = Math.round((wfDone / wfTotal) * 100);
9639
+ html += '<div style="margin-bottom:12px"><div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px"><span style="font-size:13px;font-weight:600;color:var(--text)">' + (wf.name || 'Workflow') + '</span><span style="font-size:12px;color:var(--text-muted)">' + wfPct + '<span style="font-size:0.8em;margin-left:1px">%</span></span></div>';
9640
+ html += '<div style="height:6px;background:var(--surface-2);border-radius:3px;overflow:hidden"><div style="height:100%;width:' + wfPct + '%;background:var(--gradient-accent);border-radius:3px;transition:width 0.3s"></div></div></div>';
9641
+ }
9642
+ html += '</div>';
9643
+ }
9644
+
9645
+ // Task summary
9646
+ if (hasTasks && !hasWorkflows) {
9647
+ html += '<div class="overview-panel"><h3>Tasks <a onclick="switchView(\'tasks\')">View board &rarr;</a></h3>';
9648
+ html += '<div class="overview-task-summary">';
9649
+ if (pending > 0) html += '<span class="overview-task-badge pending">' + pending + ' pending</span>';
9650
+ if (inProgress > 0) html += '<span class="overview-task-badge active">' + inProgress + ' active</span>';
9651
+ if (done > 0) html += '<span class="overview-task-badge done">' + done + ' done</span>';
9652
+ html += '</div>';
9653
+ html += '</div>';
9654
+ }
9655
+
9656
+ // Recent messages
9657
+ if (hasMessages) {
9658
+ html += '<div class="overview-panel overview-full-width"><h3>Recent Activity <a onclick="switchView(\'messages\')">View all &rarr;</a></h3>';
9659
+ for (var m = 0; m < history.length; m++) {
9660
+ var msg = history[m];
9661
+ var time = new Date(msg.timestamp);
9662
+ var timeStr = time.getHours() + ':' + String(time.getMinutes()).padStart(2, '0');
9663
+ var preview = (msg.content || '').replace(/[#*`_~\[\]]/g, '').substring(0, 100);
9664
+ html += '<div class="overview-msg-row"><span class="overview-msg-from">' + (msg.from || '?') + '</span><span class="overview-msg-text">' + preview + '</span><span class="overview-msg-time">' + timeStr + '</span></div>';
9665
+ }
9666
+ html += '</div>';
9667
+ }
9668
+
9669
+ // Task summary (if workflows already shown, put tasks as full width)
9670
+ if (hasTasks && hasWorkflows) {
9671
+ html += '<div class="overview-panel overview-full-width"><h3>Tasks <a onclick="switchView(\'tasks\')">View board &rarr;</a></h3>';
9672
+ html += '<div class="overview-task-summary">';
9673
+ if (pending > 0) html += '<span class="overview-task-badge pending">' + pending + ' pending</span>';
9674
+ if (inProgress > 0) html += '<span class="overview-task-badge active">' + inProgress + ' active</span>';
9675
+ if (done > 0) html += '<span class="overview-task-badge done">' + done + ' done</span>';
9676
+ html += '</div>';
9677
+ html += '</div>';
9678
+ }
9679
+
9680
+ html += '</div>';
9681
+ }
9682
+
9683
+ // Notifications feed (always show if there are recent notifications)
9684
+ if (notifications.length > 0) {
9685
+ var NOTIF_ICONS = { task_done: '&#x2713;', workflow_advanced: '&#x25B6;', agent_online: '+', agent_offline: '-', approval_needed: '!' };
9686
+ var NOTIF_COLORS = { task_done: 'var(--green)', workflow_advanced: 'var(--accent)', agent_online: 'var(--green)', agent_offline: 'var(--text-muted)', approval_needed: 'var(--orange)' };
9687
+ html += '<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px 20px">';
9688
+ html += '<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px">Recent Notifications</div>';
9689
+ var recent = notifications.slice(-8).reverse();
9690
+ for (var ni = 0; ni < recent.length; ni++) {
9691
+ var n = recent[ni];
9692
+ var ntime = new Date(n.timestamp);
9693
+ var ntimeStr = ntime.getHours() + ':' + String(ntime.getMinutes()).padStart(2, '0');
9694
+ var ncolor = NOTIF_COLORS[n.type] || 'var(--text-muted)';
9695
+ var nicon = NOTIF_ICONS[n.type] || '&#x2022;';
9696
+ html += '<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);font-size:13px">';
9697
+ html += '<span style="width:22px;height:22px;border-radius:6px;background:' + ncolor + '15;color:' + ncolor + ';display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0">' + nicon + '</span>';
9698
+ html += '<span style="flex:1;color:var(--text-dim)">' + (n.summary || n.type) + '</span>';
9699
+ html += '<span style="font-size:11px;color:var(--text-muted);flex-shrink:0">' + ntimeStr + '</span>';
9700
+ html += '</div>';
9701
+ }
9702
+ html += '</div>';
9703
+ }
9704
+
9705
+ if (area.innerHTML !== html) {
9706
+ area.innerHTML = html;
9707
+ area.scrollTop = savedScroll;
9708
+ }
9709
+ }).catch(function(e) {
9710
+ area.innerHTML = '<div style="padding:28px 32px;color:var(--text-muted)">Failed to load overview: ' + e.message + '</div>';
9711
+ });
9712
+ }
9713
+
9714
+ // ==================== COORDINATOR MODE ====================
9715
+
9716
+ function setCoordinatorMode(mode) {
9717
+ lttFetch('/api/coordinator-mode', {
9718
+ method: 'POST',
9719
+ headers: { 'Content-Type': 'application/json' },
9720
+ body: JSON.stringify({ mode: mode })
9721
+ }).then(function(r) { return r.json(); }).then(function(data) {
9722
+ if (data.error) {
9723
+ showToast('!', 'Failed: ' + data.error);
9724
+ } else {
9725
+ showToast('&#x2713;', 'Coordinator mode: ' + (mode === 'responsive' ? 'Stay with me' : 'Run autonomously'));
9726
+ renderOverview();
9727
+ }
9728
+ }).catch(function() {
9729
+ showToast('!', 'Failed to change coordinator mode');
9730
+ });
9731
+ }
9732
+
9733
+ // ==================== TOAST NOTIFICATIONS ====================
9734
+
9735
+ var toastQueue = [];
9736
+ function showToast(icon, text) {
9737
+ var container = document.getElementById('toast-container');
9738
+ if (!container) return;
9739
+ // Max 3 toasts
9740
+ while (container.children.length >= 3) {
9741
+ container.removeChild(container.firstChild);
9742
+ }
9743
+ var toast = document.createElement('div');
9744
+ var isError = icon === '!' || text.toLowerCase().indexOf('failed') !== -1 || text.toLowerCase().indexOf('error') !== -1;
9745
+ var isSuccess = icon.indexOf('2713') !== -1 || icon.indexOf('+') !== -1;
9746
+ toast.className = 'toast' + (isError ? ' toast-error' : isSuccess ? ' toast-success' : '');
9747
+ toast.innerHTML = '<span class="toast-icon">' + icon + '</span><span class="toast-text">' + text + '</span>';
9748
+ container.appendChild(toast);
9749
+ setTimeout(function() {
9750
+ toast.classList.add('leaving');
9751
+ setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
9752
+ }, 5000);
9753
+ }
9754
+
8464
9755
  // ==================== SSE + POLLING ====================
8465
9756
 
8466
9757
  var sseConnected = false;
@@ -8470,19 +9761,16 @@ var ssePollFallback = null;
8470
9761
 
8471
9762
  function setConnStatus(status) {
8472
9763
  var dot = document.querySelector('.conn-dot');
8473
- var label = document.getElementById('conn-label');
9764
+ if (!dot) return;
8474
9765
  if (status === 'live') {
8475
9766
  dot.style.background = 'var(--green)';
8476
9767
  dot.style.animation = 'pulse 2s infinite';
8477
- label.textContent = 'Live (SSE)';
8478
9768
  } else if (status === 'reconnecting') {
8479
9769
  dot.style.background = 'var(--yellow, #f0ad4e)';
8480
9770
  dot.style.animation = 'pulse 0.8s infinite';
8481
- label.textContent = 'Reconnecting...';
8482
9771
  } else if (status === 'poll') {
8483
9772
  dot.style.background = 'var(--green)';
8484
9773
  dot.style.animation = 'pulse 2s infinite';
8485
- label.textContent = 'Live (poll)';
8486
9774
  }
8487
9775
  }
8488
9776
 
@@ -8501,10 +9789,17 @@ function initSSE() {
8501
9789
  if (needMessages || needAgents) {
8502
9790
  // Full poll for messages/agents (they're coupled in rendering)
8503
9791
  poll();
8504
- } else {
8505
- // Targeted fetch for tasks/workflows only — no full poll needed
8506
- if (needTasks && activeView === 'tasks') fetchTasks();
8507
- if (needWorkflows && activeView === 'workflows') fetchWorkflows();
9792
+ // Also refresh overview if currently viewing it
9793
+ if (activeView === 'overview') renderOverview();
9794
+ }
9795
+ // Targeted fetch for tasks/workflows — refresh on any view that needs them
9796
+ if (needTasks) {
9797
+ if (activeView === 'tasks') fetchTasks();
9798
+ else if (activeView === 'overview') renderOverview();
9799
+ }
9800
+ if (needWorkflows) {
9801
+ if (activeView === 'workflows') fetchWorkflows();
9802
+ else if (activeView === 'overview') renderOverview();
8508
9803
  }
8509
9804
  };
8510
9805
  eventSource.onopen = function() {
@@ -8556,16 +9851,31 @@ loadProjects().then(function() {
8556
9851
  loadConversationList();
8557
9852
  poll();
8558
9853
  initSSE();
8559
- switchView('office');
9854
+ switchView(activeView);
8560
9855
  }).catch(function(e) {
8561
9856
  console.error('[LTT] init: loadProjects failed, polling anyway:', e);
8562
9857
  poll();
8563
9858
  initSSE();
8564
- switchView('office');
9859
+ switchView(activeView);
8565
9860
  });
8566
9861
  // Safety-net poll at 10s (SSE handles real-time, this catches any missed updates)
8567
9862
  setInterval(poll, 10000);
8568
9863
  </script>
8569
9864
 
9865
+ <!-- Workflow Detail Popup -->
9866
+ <div class="phone-modal-overlay" id="wf-detail-overlay" onclick="if(event.target===this)closeWfDetail()">
9867
+ <div class="phone-modal" style="width:620px;text-align:left;max-height:85vh;overflow-y:auto">
9868
+ <button class="phone-modal-close" onclick="closeWfDetail()">&times;</button>
9869
+ <div id="wf-detail-content"></div>
9870
+ </div>
9871
+ </div>
9872
+ <!-- Task Detail Popup -->
9873
+ <div class="phone-modal-overlay" id="task-detail-overlay" onclick="if(event.target===this)closeTaskDetail()">
9874
+ <div class="phone-modal" style="width:520px;text-align:left;max-height:80vh;overflow-y:auto">
9875
+ <button class="phone-modal-close" onclick="closeTaskDetail()">&times;</button>
9876
+ <div id="task-detail-content"></div>
9877
+ </div>
9878
+ </div>
9879
+ <div class="toast-container" id="toast-container"></div>
8570
9880
  </body>
8571
9881
  </html>