neohive 6.0.3 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard.html CHANGED
@@ -4,78 +4,116 @@
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='%23f59e0b'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>">
7
+ <link rel="icon" href="favicon.png" type="image/png" sizes="16x16">
8
+ <link rel="icon" href="logo.svg" type="image/svg+xml">
8
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/design-system.css">
13
+ <!-- xterm.js dependencies now lazy-loaded on terminal tab activation to prevent dashboard stalling -->
11
14
  <style>
12
15
  * { margin: 0; padding: 0; box-sizing: border-box; }
13
16
 
17
+ /*
18
+ * Token fallbacks: specificity 0 via :where() so a loaded design-system.css :root
19
+ * and [data-theme="light"] still win. If /design-system.css is blocked (Safari
20
+ * extensions / networking), these keep cards, padding, and colors defined.
21
+ */
22
+ :where(:root) {
23
+ --nh-space-1: 4px;
24
+ --nh-space-2: 8px;
25
+ --nh-space-3: 12px;
26
+ --nh-space-4: 16px;
27
+ --nh-space-5: 20px;
28
+ --nh-space-6: 24px;
29
+ --nh-space-8: 32px;
30
+ --nh-radius-sm: 6px;
31
+ --nh-radius-md: 8px;
32
+ --nh-radius-lg: 10px;
33
+ --nh-radius-xl: 12px;
34
+ --nh-bg: #0f0f10;
35
+ --nh-sidebar: #161618;
36
+ --nh-card: #1c1c1f;
37
+ --nh-card-hover: #232326;
38
+ --nh-border: #27272a;
39
+ --nh-text-primary: #f3f4f6;
40
+ --nh-text-secondary: #9ca3af;
41
+ --nh-text-muted: #6b7280;
42
+ --nh-accent: #f59e0b;
43
+ --nh-green: #10b981;
44
+ --nh-red: #ef4444;
45
+ --nh-orange: #f97316;
46
+ --nh-purple: #a78bfa;
47
+ --nh-yellow: #eab308;
48
+ --nh-divider: rgba(255, 255, 255, 0.08);
49
+ --nh-divider-strong: rgba(255, 255, 255, 0.14);
50
+ --nh-accent-5: rgba(245, 158, 11, 0.05);
51
+ --nh-accent-10: rgba(245, 158, 11, 0.1);
52
+ --nh-accent-20: rgba(245, 158, 11, 0.2);
53
+ --nh-green-10: rgba(16, 185, 129, 0.1);
54
+ --nh-red-10: rgba(239, 68, 68, 0.12);
55
+ --nh-orange-10: rgba(249, 115, 22, 0.12);
56
+ --nh-purple-10: rgba(167, 139, 250, 0.12);
57
+ --nh-purple-20: rgba(167, 139, 250, 0.22);
58
+ --nh-shadow-card: 0 4px 24px rgba(0, 0, 0, 0.35);
59
+ --nh-code-inline-fg: #f97316;
60
+ --nh-code-block-bg: #0f0f10;
61
+ --nh-on-dark: #ffffff;
62
+ --nh-on-accent: #3a2201;
63
+ --nh-blue: #3b82f6;
64
+ --nh-green-glow: 0 0 8px rgba(16, 185, 129, 0.5);
65
+ --nh-blue-glow: 0 0 8px rgba(59, 130, 246, 0.45);
66
+ }
67
+
14
68
  :root {
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;
69
+ /* Bridge legacy dashboard tokens → design-system.css (--nh-*) */
70
+ --bg: var(--nh-bg);
71
+ --surface: var(--nh-sidebar);
72
+ --surface-2: var(--nh-card);
73
+ --surface-3: var(--nh-card-hover);
74
+ --border: var(--nh-divider);
75
+ --border-light: var(--nh-divider-strong);
76
+ --text: var(--nh-text-primary);
77
+ --text-dim: var(--nh-text-secondary);
78
+ --text-muted: var(--nh-text-muted);
79
+ --accent: var(--nh-accent);
80
+ --accent-dim: var(--nh-accent-10);
81
+ --accent-glow: var(--nh-accent-20);
82
+ --green: var(--nh-green);
83
+ --green-dim: var(--nh-green-10);
84
+ --red: var(--nh-red);
85
+ --red-dim: var(--nh-red-10);
86
+ --orange: var(--nh-orange);
87
+ --orange-dim: var(--nh-orange-10);
88
+ --purple: var(--nh-purple);
89
+ --purple-dim: var(--nh-purple-10);
90
+ --yellow: var(--nh-yellow);
91
+ --bg-primary: var(--nh-card);
92
+ --bg-secondary: var(--nh-sidebar);
93
+ --bg-tertiary: var(--nh-card-hover);
94
+ --text-primary: var(--nh-text-primary);
95
+ --text-secondary: var(--nh-text-secondary);
36
96
  --text-xs: 12px;
37
97
  --text-sm: 13px;
38
98
  --text-base: 14px;
39
99
  --text-lg: 16px;
40
100
  --text-xl: 24px;
41
- --sidebar-w: 220px;
42
- --header-h: 48px;
43
- --glow: 0 0 24px rgba(245, 158, 11, 0.06);
101
+ --sidebar-w: 256px;
102
+ --header-h: 56px;
103
+ --glow: 0 0 24px var(--nh-accent-10);
44
104
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
45
105
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
46
106
  --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%);
107
+ --gradient-accent: linear-gradient(135deg, var(--nh-accent), var(--nh-orange));
108
+ --gradient-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
49
109
  }
50
110
 
51
111
  [data-theme="light"] {
52
- --bg: #fafaf9;
53
- --surface: #ffffff;
54
- --surface-2: #f5f5f4;
55
- --surface-3: #e7e5e4;
56
- --border: rgba(0, 0, 0, 0.08);
57
- --border-light: rgba(0, 0, 0, 0.12);
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);
66
- --red: #dc2626;
67
- --red-dim: rgba(220, 38, 38, 0.08);
68
- --orange: #ea580c;
69
- --orange-dim: rgba(234, 88, 12, 0.08);
70
- --purple: #7c3aed;
71
- --purple-dim: rgba(124, 58, 237, 0.08);
72
- --yellow: #ca8a04;
73
- --glow: 0 0 20px rgba(217, 119, 6, 0.05);
112
+ --glow: 0 0 20px var(--nh-accent-10);
74
113
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
75
114
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
76
115
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1);
77
- --gradient-accent: linear-gradient(135deg, #d97706, #ea580c);
78
- --gradient-surface: linear-gradient(180deg, rgba(0,0,0,0.01) 0%, transparent 100%);
116
+ --gradient-surface: linear-gradient(180deg, rgba(0, 0, 0, 0.02) 0%, transparent 100%);
79
117
  }
80
118
 
81
119
  .theme-toggle {
@@ -117,10 +155,12 @@
117
155
 
118
156
  /* ===== HEADER ===== */
119
157
  .header {
120
- background: var(--bg);
121
- border-bottom: 1px solid var(--border);
122
- padding: 0 16px;
123
- height: 48px;
158
+ background: var(--nh-glass-bg);
159
+ backdrop-filter: var(--nh-blur);
160
+ -webkit-backdrop-filter: var(--nh-blur);
161
+ border-bottom: 1px solid var(--nh-glass-border);
162
+ padding: 0 var(--nh-space-6);
163
+ height: var(--header-h);
124
164
  display: flex;
125
165
  align-items: center;
126
166
  justify-content: space-between;
@@ -129,6 +169,7 @@
129
169
  left: 0;
130
170
  right: 0;
131
171
  z-index: 100;
172
+ box-shadow: 0 1px 1px rgba(0,0,0,0.2);
132
173
  }
133
174
 
134
175
  [data-theme="light"] .header {
@@ -142,8 +183,9 @@
142
183
  }
143
184
 
144
185
  .logo {
145
- font-size: 15px;
186
+ font-size: 20px;
146
187
  font-weight: 700;
188
+ letter-spacing: -0.01em;
147
189
  color: var(--text);
148
190
  white-space: nowrap;
149
191
  letter-spacing: -0.3px;
@@ -164,7 +206,16 @@
164
206
  .header-actions {
165
207
  display: flex;
166
208
  align-items: center;
167
- gap: 4px;
209
+ gap: 10px;
210
+ }
211
+
212
+ .header-conn {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 8px;
216
+ font-size: 13px;
217
+ color: var(--text-dim);
218
+ margin-right: 4px;
168
219
  }
169
220
 
170
221
  .connection {
@@ -177,11 +228,11 @@
177
228
  }
178
229
 
179
230
  .conn-dot {
180
- width: 7px;
181
- height: 7px;
231
+ width: 8px;
232
+ height: 8px;
182
233
  border-radius: 50%;
183
234
  background: var(--green);
184
- box-shadow: 0 0 6px var(--green);
235
+ box-shadow: var(--nh-green-glow);
185
236
  animation: pulse 2s infinite;
186
237
  }
187
238
 
@@ -232,13 +283,15 @@
232
283
  .sidebar {
233
284
  width: var(--sidebar-w);
234
285
  min-width: var(--sidebar-w);
235
- background: var(--surface);
236
- background-image: var(--gradient-surface);
237
- border-right: 1px solid var(--border);
286
+ background: var(--nh-glass-bg);
287
+ backdrop-filter: var(--nh-blur);
288
+ -webkit-backdrop-filter: var(--nh-blur);
289
+ border-right: 1px solid var(--nh-glass-border);
238
290
  display: flex;
239
291
  flex-direction: column;
240
292
  overflow: hidden;
241
293
  transition: width 0.25s ease, min-width 0.25s ease, margin-left 0.25s ease;
294
+ box-shadow: 1px 0 0 rgba(0,0,0,0.1);
242
295
  }
243
296
  .sidebar.collapsed {
244
297
  width: 0;
@@ -348,24 +401,40 @@
348
401
  /* ===== AGENT CARDS ===== */
349
402
  .agent-card {
350
403
  background: var(--surface-2);
351
- background-image: var(--gradient-surface);
352
404
  border: 1px solid var(--border);
353
405
  border-radius: 8px;
354
- padding: 6px 10px;
355
- margin-bottom: 4px;
356
- transition: all 0.25s ease;
406
+ padding: 8px 12px;
407
+ margin-bottom: 6px;
408
+ transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
357
409
  cursor: pointer;
410
+ position: relative;
411
+ overflow: hidden;
358
412
  }
359
413
 
360
- .agent-card:hover { border-color: var(--border-light); box-shadow: var(--shadow-sm); }
414
+ .agent-card:hover {
415
+ transform: translateX(4px);
416
+ border-color: var(--nh-accent-50);
417
+ background: var(--surface-3);
418
+ box-shadow: var(--shadow-sm);
419
+ }
361
420
 
362
- .agent-card.sleeping, .agent-card.idle {
363
- border-left: 3px solid var(--orange);
421
+ .agent-card.working, .agent-card.active {
422
+ border-left: 3px solid var(--green);
423
+ box-shadow: 0 0 15px var(--green-dim);
424
+ animation: nh-glow-pulse 2s infinite ease-in-out;
364
425
  }
365
426
 
366
427
  .agent-card.dead, .agent-card.offline {
367
428
  opacity: 0.5;
368
429
  }
430
+ .agent-card.unknown {
431
+ opacity: 0.4;
432
+ border-left: 3px solid var(--nh-text-muted);
433
+ }
434
+ .agent-card.stale {
435
+ opacity: 0.65;
436
+ border-left: 3px solid var(--orange);
437
+ }
369
438
 
370
439
  .agent-top {
371
440
  display: flex;
@@ -394,10 +463,22 @@
394
463
  border-radius: 50%;
395
464
  flex-shrink: 0;
396
465
  }
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; }
466
+ .agent-status-dot.active, .agent-status-dot.working { background: var(--green); box-shadow: var(--nh-green-glow); }
467
+ .agent-status-dot.listening { background: var(--nh-blue); box-shadow: 0 0 6px var(--nh-blue); animation: pulse 2s infinite; }
468
+ .agent-status-dot.sleeping, .agent-status-dot.idle { background: var(--nh-accent); animation: pulse 2s infinite; }
469
+ .agent-status-dot.dead, .agent-status-dot.offline { background: var(--nh-text-muted); }
470
+ .agent-status-dot.unknown { background: var(--nh-text-muted); opacity: 0.5; }
471
+ .agent-status-dot.stale { background: var(--orange); animation: pulse 2s infinite; }
472
+ .agent-status-dot.unresponsive { background: var(--orange, #f97316); box-shadow: 0 0 6px var(--orange, #f97316); animation: pulse 2s infinite; }
473
+ .agent-status-dot.stuck { background: var(--red, #ef4444); box-shadow: 0 0 8px var(--red, #ef4444); animation: pulse 1.2s infinite; }
474
+
475
+ .agent-state-pill { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 99px; font-size: 10px; font-weight: 600; letter-spacing: 0.02em; line-height: 16px; flex-shrink: 0; }
476
+ .agent-state-pill.listening { background: rgba(96,165,250,0.15); color: #60a5fa; }
477
+ .agent-state-pill.working { background: rgba(251,191,36,0.15); color: #fbbf24; }
478
+ .agent-current-tool { font-size: 10px; color: var(--nh-text-muted, #6b7280); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
479
+
480
+ .agent-card.stuck { border-color: rgba(239, 68, 68, 0.3); }
481
+ .agent-card.unresponsive { border-color: rgba(249, 115, 22, 0.3); }
401
482
 
402
483
  .agent-name {
403
484
  font-weight: 600;
@@ -415,6 +496,147 @@
415
496
  text-overflow: ellipsis;
416
497
  }
417
498
 
499
+ /* ===== AGENT SKILLS ===== */
500
+ .agent-skills {
501
+ display: flex;
502
+ flex-wrap: wrap;
503
+ gap: 4px;
504
+ margin-top: 6px;
505
+ }
506
+
507
+ .skill-tag {
508
+ font-size: 9px;
509
+ padding: 1px 5px;
510
+ border-radius: 4px;
511
+ background: var(--surface-3);
512
+ color: var(--text-dim);
513
+ border: 1px solid var(--border);
514
+ }
515
+
516
+ /* ===== NUDGE & SPARKLINE ===== */
517
+ .agent-nudge-btn {
518
+ position: absolute;
519
+ top: 8px;
520
+ right: 8px;
521
+ width: 24px;
522
+ height: 24px;
523
+ border-radius: 6px;
524
+ background: var(--nh-red-10);
525
+ border: 1px solid var(--nh-red);
526
+ color: var(--nh-red);
527
+ display: flex;
528
+ align-items: center;
529
+ justify-content: center;
530
+ cursor: pointer;
531
+ opacity: 0;
532
+ transition: all 0.2s ease;
533
+ z-index: 10;
534
+ }
535
+ .agent-card:hover .agent-nudge-btn { opacity: 1; }
536
+ .agent-nudge-btn:hover { background: var(--nh-red); color: #fff; transform: scale(1.1); box-shadow: 0 0 10px rgba(239, 68, 68, 0.4); }
537
+
538
+ .listen-sparkline {
539
+ display: flex;
540
+ gap: 3px;
541
+ margin-top: 8px;
542
+ padding-top: 4px;
543
+ border-top: 1px solid var(--nh-divider);
544
+ align-items: center;
545
+ }
546
+ .listen-sparkline-label {
547
+ font-size: 9px;
548
+ color: var(--text-muted);
549
+ text-transform: uppercase;
550
+ font-weight: 700;
551
+ margin-right: 4px;
552
+ letter-spacing: 0.5px;
553
+ }
554
+ .hb-dot {
555
+ width: 6px;
556
+ height: 6px;
557
+ border-radius: 50%;
558
+ background: var(--nh-divider-strong);
559
+ transition: all 0.3s ease;
560
+ }
561
+ .hb-dot.active {
562
+ background: var(--nh-green);
563
+ box-shadow: 0 0 6px var(--nh-green);
564
+ }
565
+ .hb-dot.stale { background: var(--nh-orange); opacity: 0.5; }
566
+ .hb-dot.empty { background: transparent; border: 1px solid var(--nh-divider); }
567
+
568
+ @keyframes stuck-pulse {
569
+ 0% { border-color: var(--nh-divider); }
570
+ 50% { border-color: var(--nh-red); box-shadow: inset 0 0 10px var(--nh-red-10); }
571
+ 100% { border-color: var(--nh-divider); }
572
+ }
573
+ .agent-card.stuck {
574
+ animation: stuck-pulse 1.5s infinite ease-in-out;
575
+ }
576
+
577
+ /* Platform colors */
578
+ .platform-claude { background: var(--nh-accent-10); color: var(--nh-accent); border-color: var(--nh-accent-20); }
579
+ .platform-gemini { background: rgba(59, 130, 246, 0.1); color: var(--nh-blue); border-color: rgba(59, 130, 246, 0.2); }
580
+ .platform-openai { background: var(--nh-green-10); color: var(--nh-green); border-color: var(--nh-green-20); }
581
+ .platform-deepseek { background: var(--nh-purple-10); color: var(--nh-purple); border-color: var(--nh-purple-20); }
582
+ .platform-llama { background: var(--nh-orange-10); color: var(--nh-orange); border-color: var(--nh-orange-20); }
583
+ .platform-mistral { background: var(--nh-red-10); color: var(--nh-red); border-color: var(--nh-red-20); }
584
+
585
+ /* Skill Editor in Profile */
586
+ .skill-editor {
587
+ display: flex;
588
+ flex-wrap: wrap;
589
+ gap: 6px;
590
+ margin-top: 12px;
591
+ padding: 12px;
592
+ background: var(--surface-2);
593
+ border-radius: 8px;
594
+ border: 1px solid var(--border);
595
+ }
596
+
597
+ .skill-editor-tag {
598
+ display: flex;
599
+ align-items: center;
600
+ gap: 6px;
601
+ font-size: 11px;
602
+ font-weight: 600;
603
+ padding: 4px 8px;
604
+ background: var(--surface-3);
605
+ border: 1px solid var(--border);
606
+ border-radius: 6px;
607
+ color: var(--text);
608
+ }
609
+
610
+ .remove-skill-btn {
611
+ cursor: pointer;
612
+ opacity: 0.6;
613
+ transition: opacity 0.2s;
614
+ font-size: 14px;
615
+ line-height: 1;
616
+ }
617
+
618
+ .remove-skill-btn:hover { opacity: 1; color: var(--red); }
619
+
620
+ .add-skill-box {
621
+ display: flex;
622
+ gap: 6px;
623
+ width: 100%;
624
+ margin-top: 8px;
625
+ }
626
+
627
+ .add-skill-input {
628
+ flex: 1;
629
+ background: var(--bg);
630
+ border: 1px solid var(--border);
631
+ border-radius: 6px;
632
+ padding: 4px 10px;
633
+ font-size: 12px;
634
+ color: var(--text);
635
+ outline: none;
636
+ }
637
+
638
+ .add-skill-input:focus { border-color: var(--accent); }
639
+
418
640
  .nudge-btn {
419
641
  background: var(--orange-dim);
420
642
  color: var(--orange);
@@ -638,37 +860,88 @@
638
860
  flex-direction: column;
639
861
  min-width: 0;
640
862
  overflow: hidden;
863
+ background: var(--nh-bg);
641
864
  }
642
865
 
643
- /* ===== MESSAGES ===== */
866
+ /* ===== MESSAGES (card feed — nh-msg-card from design-system.css) ===== */
644
867
  .messages-area {
645
868
  flex: 1;
646
869
  overflow-y: auto;
870
+ -webkit-overflow-scrolling: touch;
647
871
  padding: 16px 20px;
872
+ padding: var(--nh-space-4) var(--nh-space-5);
873
+ display: -webkit-flex;
648
874
  display: flex;
875
+ -webkit-flex-direction: column;
649
876
  flex-direction: column;
650
- gap: 2px;
877
+ /* margin between rows — do not rely on flex gap alone (Safari 14.0 reports @supports(gap) true for grid while flex had no gap) */
651
878
  }
652
879
 
653
- .message {
880
+ .messages-area > * + * {
881
+ margin-top: 12px;
882
+ margin-top: var(--nh-space-3, 12px);
883
+ }
884
+
885
+ /*
886
+ * Card chrome duplicated here (higher specificity than .nh-msg-card alone) with
887
+ * px fallbacks so messages stay “in cards” even if external CSS or vars fail.
888
+ * Horizontal spacing uses margin, not flex gap, for the same Safari reason.
889
+ */
890
+ .message.nh-msg-card {
891
+ display: -webkit-flex;
654
892
  display: flex;
655
- gap: 10px;
656
- padding: 10px 14px;
657
- border-radius: 10px;
658
- transition: all 0.2s ease;
893
+ -webkit-flex-direction: row;
894
+ flex-direction: row;
895
+ -webkit-align-items: flex-start;
896
+ align-items: flex-start;
897
+ box-sizing: border-box;
898
+ padding: 16px 20px;
899
+ padding: var(--nh-space-4) var(--nh-space-5);
900
+ background-color: #1c1c1f;
901
+ background-color: var(--nh-card, #1c1c1f);
902
+ border: 1px solid #27272a;
903
+ border-color: var(--nh-border, #27272a);
904
+ border-radius: 12px;
905
+ border-radius: var(--nh-radius-xl, 12px);
906
+ }
907
+
908
+ .message.nh-msg-card > .msg-avatar + .msg-body,
909
+ .message.nh-msg-card > .msg-avatar-img + .msg-body {
910
+ margin-left: 12px;
911
+ margin-left: var(--nh-space-3, 12px);
912
+ }
913
+
914
+ .message.nh-msg-card:hover {
915
+ border-color: rgba(156, 163, 175, 0.45);
916
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
917
+ box-shadow: var(--nh-shadow-card, 0 4px 24px rgba(0, 0, 0, 0.35));
918
+ }
919
+
920
+ .message {
659
921
  position: relative;
922
+ transition: box-shadow 0.2s ease, border-color 0.2s ease;
923
+ overflow: visible;
660
924
  }
661
925
 
662
- .message:hover { background: var(--surface); box-shadow: var(--glow); }
926
+ .message .msg-actions {
927
+ top: -10px;
928
+ right: var(--nh-space-3);
929
+ }
663
930
 
664
931
  /* ===== REACTIONS ===== */
665
932
  .msg-actions {
666
933
  position: absolute;
667
- top: 6px;
934
+ top: -10px;
668
935
  right: 6px;
669
936
  display: none;
670
937
  gap: 2px;
671
938
  align-items: center;
939
+ z-index: 2;
940
+ background: var(--nh-card, #1c1c1f);
941
+ border: 1px solid var(--nh-border, #27272a);
942
+ border-radius: var(--nh-radius-md, 8px);
943
+ padding: 2px 4px;
944
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
672
945
  }
673
946
 
674
947
  .message:hover .msg-actions { display: flex; }
@@ -693,11 +966,11 @@
693
966
  position: absolute;
694
967
  top: -4px;
695
968
  right: 56px;
696
- background: var(--surface-2);
969
+ background: var(--nh-card);
697
970
  backdrop-filter: blur(12px);
698
971
  -webkit-backdrop-filter: blur(12px);
699
- border: 1px solid var(--border-light);
700
- border-radius: 10px;
972
+ border: 1px solid var(--nh-border);
973
+ border-radius: var(--nh-radius-lg);
701
974
  padding: 4px;
702
975
  gap: 2px;
703
976
  z-index: 20;
@@ -717,7 +990,7 @@
717
990
  line-height: 1;
718
991
  }
719
992
 
720
- .react-emoji:hover { background: var(--surface-3); }
993
+ .react-emoji:hover { background: var(--nh-card-hover); }
721
994
 
722
995
  .msg-reactions {
723
996
  display: flex;
@@ -730,9 +1003,9 @@
730
1003
  display: inline-flex;
731
1004
  align-items: center;
732
1005
  gap: 3px;
733
- background: var(--surface-2);
734
- border: 1px solid var(--border);
735
- border-radius: 10px;
1006
+ background: var(--nh-card-hover);
1007
+ border: 1px solid var(--nh-border);
1008
+ border-radius: var(--nh-radius-lg);
736
1009
  padding: 1px 6px;
737
1010
  font-size: 12px;
738
1011
  cursor: pointer;
@@ -768,22 +1041,29 @@
768
1041
 
769
1042
  .pinned-toggle { font-size: 11px; color: var(--accent); }
770
1043
 
771
- .pinned-section .message { padding: 6px 20px; }
1044
+ .pinned-section .message.nh-msg-card {
1045
+ padding: var(--nh-space-2) var(--nh-space-4);
1046
+ margin-bottom: var(--nh-space-2);
1047
+ }
1048
+ .pinned-section .message.nh-msg-card:last-child { margin-bottom: 0; }
772
1049
  .pinned-section .msg-content { font-size: 12px; max-height: 40px; overflow: hidden; }
773
1050
 
774
1051
  .bookmark-btn {
775
1052
  position: absolute;
776
- top: 8px;
777
- right: 8px;
778
- background: none;
779
- border: none;
1053
+ top: -10px;
1054
+ right: 40px;
1055
+ background: var(--nh-card, #1c1c1f);
1056
+ border: 1px solid var(--nh-border, #27272a);
1057
+ border-radius: var(--nh-radius-md, 8px);
780
1058
  cursor: pointer;
781
1059
  font-size: 14px;
782
1060
  opacity: 0;
783
1061
  transition: opacity 0.15s;
784
1062
  color: var(--text-muted);
785
- padding: 2px;
1063
+ padding: 2px 4px;
786
1064
  line-height: 1;
1065
+ z-index: 2;
1066
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
787
1067
  }
788
1068
 
789
1069
  .message:hover .bookmark-btn { opacity: 0.6; }
@@ -791,21 +1071,81 @@
791
1071
  .bookmark-btn.active { opacity: 1 !important; color: var(--yellow); }
792
1072
 
793
1073
  .msg-avatar {
794
- width: 32px;
795
- height: 32px;
796
- border-radius: 50%;
1074
+ width: 36px;
1075
+ height: 36px;
1076
+ border-radius: 9999px;
797
1077
  display: flex;
798
1078
  align-items: center;
799
1079
  justify-content: center;
800
1080
  font-weight: 700;
801
1081
  font-size: 13px;
802
1082
  flex-shrink: 0;
803
- color: #fff;
1083
+ color: var(--nh-on-dark);
804
1084
  margin-top: 2px;
1085
+ border: 1px solid var(--nh-border);
1086
+ }
1087
+
1088
+ /* min-height:0 helps WebKit flex descendants size correctly; overflow visible avoids clipping receipts/shadows */
1089
+ .msg-body { flex: 1; min-width: 0; min-height: 0; overflow: visible; }
1090
+
1091
+ .msg-card-head {
1092
+ display: flex;
1093
+ align-items: flex-start;
1094
+ justify-content: space-between;
1095
+ gap: var(--nh-space-4);
1096
+ margin-bottom: var(--nh-space-2);
1097
+ width: 100%;
1098
+ }
1099
+
1100
+ .msg-card-ident {
1101
+ min-width: 0;
1102
+ flex: 1;
1103
+ }
1104
+
1105
+ .msg-from {
1106
+ font-weight: 600;
1107
+ font-size: 14px;
1108
+ line-height: 1.3;
1109
+ color: var(--nh-text-primary);
805
1110
  }
806
1111
 
807
- .msg-body { flex: 1; min-width: 0; }
1112
+ .msg-to-line {
1113
+ font-size: 12px;
1114
+ color: var(--nh-text-secondary);
1115
+ margin-top: var(--nh-space-1);
1116
+ line-height: 1.35;
1117
+ }
1118
+
1119
+ .msg-to-label {
1120
+ color: var(--nh-text-muted);
1121
+ margin-right: var(--nh-space-1);
1122
+ font-weight: 500;
1123
+ }
1124
+
1125
+ .msg-card-head-right {
1126
+ display: flex;
1127
+ flex-direction: column;
1128
+ align-items: flex-end;
1129
+ gap: var(--nh-space-1);
1130
+ flex-shrink: 0;
1131
+ text-align: right;
1132
+ }
1133
+
1134
+ .msg-time {
1135
+ font-size: 12px;
1136
+ color: var(--nh-text-muted);
1137
+ font-variant-numeric: tabular-nums;
1138
+ white-space: nowrap;
1139
+ }
808
1140
 
1141
+ .msg-badges {
1142
+ display: flex;
1143
+ flex-wrap: wrap;
1144
+ gap: 4px;
1145
+ justify-content: flex-end;
1146
+ }
1147
+
1148
+ /* Legacy row layout (compact / grouped fallbacks) */
809
1149
  .msg-header {
810
1150
  display: flex;
811
1151
  align-items: baseline;
@@ -814,10 +1154,8 @@
814
1154
  flex-wrap: wrap;
815
1155
  }
816
1156
 
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); }
1157
+ .msg-arrow { color: var(--nh-text-muted); font-size: 12px; }
1158
+ .msg-to { font-size: 13px; color: var(--nh-text-secondary); }
821
1159
 
822
1160
  .badge-channel {
823
1161
  background: var(--purple-dim);
@@ -884,56 +1222,58 @@
884
1222
  font-size: 14px;
885
1223
  line-height: 1.6;
886
1224
  word-break: break-word;
887
- color: var(--text);
1225
+ color: var(--nh-text-primary);
888
1226
  }
889
1227
 
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); }
1228
+ .msg-content h1 { font-size: 22px; font-weight: 700; margin: 12px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--nh-border); }
1229
+ .msg-content h2 { font-size: 18px; font-weight: 700; margin: 10px 0 5px; padding-bottom: 3px; border-bottom: 1px solid var(--nh-border); }
892
1230
  .msg-content h3 { font-size: 16px; font-weight: 600; margin: 8px 0 4px; }
893
1231
  .msg-content h4 { font-size: 15px; font-weight: 600; margin: 6px 0 3px; }
894
1232
 
895
1233
  .msg-content p { margin: 4px 0; }
896
1234
 
897
1235
  .msg-content strong { font-weight: 700; }
898
- .msg-content em { font-style: italic; color: var(--text-dim); }
1236
+ .msg-content em { font-style: italic; color: var(--nh-text-secondary); }
899
1237
 
900
1238
  .msg-content code {
901
- background: var(--surface-3);
902
- padding: 1px 5px;
903
- border-radius: 4px;
1239
+ background: var(--nh-card-hover);
1240
+ padding: 2px 6px;
1241
+ border-radius: var(--nh-radius-sm);
904
1242
  font-size: 12px;
905
1243
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
906
- color: var(--orange);
1244
+ color: var(--nh-code-inline-fg);
1245
+ border: 1px solid var(--nh-border);
907
1246
  }
908
1247
 
909
1248
  .msg-content pre {
910
- background: var(--bg);
911
- border: 1px solid var(--border);
912
- border-radius: 8px;
913
- padding: 14px 16px;
914
- margin: 8px 0;
1249
+ background: var(--nh-code-block-bg);
1250
+ border: 1px solid var(--nh-border);
1251
+ border-radius: var(--nh-radius-md);
1252
+ padding: var(--nh-space-3) var(--nh-space-4);
1253
+ margin: var(--nh-space-2) 0;
915
1254
  overflow-x: auto;
916
1255
  font-size: 12px;
917
1256
  line-height: 1.6;
918
1257
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
919
1258
  position: relative;
920
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
921
1259
  }
922
1260
 
923
1261
  .msg-content pre code {
924
1262
  background: none;
925
1263
  padding: 0;
926
- color: var(--text);
1264
+ border: none;
1265
+ color: var(--nh-text-primary);
927
1266
  font-size: inherit;
928
1267
  }
929
1268
 
930
1269
  .msg-content pre .lang-tag {
931
1270
  position: absolute;
932
- top: 4px;
933
- right: 8px;
1271
+ top: 6px;
1272
+ right: var(--nh-space-2);
934
1273
  font-size: 10px;
935
- color: var(--text-muted);
1274
+ color: var(--nh-text-muted);
936
1275
  text-transform: uppercase;
1276
+ font-weight: 600;
937
1277
  }
938
1278
 
939
1279
  .msg-content ul, .msg-content ol {
@@ -946,15 +1286,15 @@
946
1286
  }
947
1287
 
948
1288
  .msg-content blockquote {
949
- border-left: 3px solid var(--border-light);
1289
+ border-left: 3px solid var(--nh-border);
950
1290
  padding: 2px 12px;
951
1291
  margin: 4px 0;
952
- color: var(--text-dim);
1292
+ color: var(--nh-text-secondary);
953
1293
  }
954
1294
 
955
1295
  .msg-content hr {
956
1296
  border: none;
957
- border-top: 1px solid var(--border);
1297
+ border-top: 1px solid var(--nh-border);
958
1298
  margin: 8px 0;
959
1299
  }
960
1300
 
@@ -966,18 +1306,18 @@
966
1306
  }
967
1307
 
968
1308
  .msg-content th, .msg-content td {
969
- border: 1px solid var(--border);
1309
+ border: 1px solid var(--nh-border);
970
1310
  padding: 5px 10px;
971
1311
  text-align: left;
972
1312
  }
973
1313
 
974
1314
  .msg-content th {
975
- background: var(--surface-2);
1315
+ background: var(--nh-card-hover);
976
1316
  font-weight: 600;
977
1317
  }
978
1318
 
979
1319
  .msg-content a {
980
- color: var(--accent);
1320
+ color: var(--nh-accent);
981
1321
  text-decoration: none;
982
1322
  }
983
1323
 
@@ -988,6 +1328,7 @@
988
1328
  display: flex;
989
1329
  gap: 2px;
990
1330
  margin-top: 4px;
1331
+ margin-bottom: 2px;
991
1332
  align-items: center;
992
1333
  }
993
1334
  .read-receipt-dot {
@@ -1066,6 +1407,7 @@
1066
1407
  font-size: 12px;
1067
1408
  font-family: inherit;
1068
1409
  resize: none;
1410
+ width: 100%;
1069
1411
  height: 34px;
1070
1412
  max-height: 80px;
1071
1413
  outline: none;
@@ -1125,46 +1467,33 @@
1125
1467
  [data-theme="light"] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); }
1126
1468
  [data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
1127
1469
 
1128
- /* ===== SCROLL TO BOTTOM ===== */
1470
+ /* ===== SCROLL TO BOTTOM PILL ===== */
1129
1471
  .scroll-bottom {
1130
1472
  position: absolute;
1131
1473
  bottom: 80px;
1132
- right: 24px;
1133
- background: var(--gradient-accent);
1134
- color: #fff;
1135
- border: none;
1136
- border-radius: 50%;
1137
- width: 36px;
1138
- height: 36px;
1474
+ left: 50%;
1475
+ transform: translateX(-50%);
1476
+ background: var(--surface);
1477
+ background-image: var(--gradient-surface);
1478
+ color: var(--text);
1479
+ border: 1px solid var(--border-light);
1480
+ border-radius: 20px;
1481
+ padding: 8px 16px;
1139
1482
  display: none;
1140
1483
  align-items: center;
1141
1484
  justify-content: center;
1142
1485
  cursor: pointer;
1143
- font-size: 16px;
1144
- box-shadow: 0 4px 16px var(--accent-glow);
1486
+ font-size: 13px;
1487
+ font-weight: 600;
1488
+ box-shadow: var(--shadow-lg), 0 4px 16px rgba(0,0,0,0.1);
1489
+ backdrop-filter: blur(8px);
1490
+ -webkit-backdrop-filter: blur(8px);
1145
1491
  z-index: 10;
1146
1492
  transition: all 0.25s ease;
1147
1493
  }
1148
1494
 
1149
- .scroll-bottom.visible { display: flex; }
1150
- .scroll-bottom:hover { transform: translateY(-2px); box-shadow: 0 6px 24px var(--accent-glow); }
1151
-
1152
- .scroll-bottom .new-count {
1153
- position: absolute;
1154
- top: -6px;
1155
- right: -6px;
1156
- background: var(--red);
1157
- color: #fff;
1158
- font-size: 9px;
1159
- font-weight: 700;
1160
- min-width: 16px;
1161
- height: 16px;
1162
- border-radius: 8px;
1163
- display: flex;
1164
- align-items: center;
1165
- justify-content: center;
1166
- padding: 0 4px;
1167
- }
1495
+ .scroll-bottom.visible { display: flex; transform: translateX(-50%) translateY(0); }
1496
+ .scroll-bottom:hover { transform: translateX(-50%) translateY(-2px); box-shadow: var(--shadow-lg), 0 6px 20px rgba(0,0,0,0.15); color: var(--accent); }
1168
1497
 
1169
1498
  /* ===== SEARCH BAR ===== */
1170
1499
  .search-bar {
@@ -1207,14 +1536,28 @@
1207
1536
  .compact-toggle.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
1208
1537
 
1209
1538
  /* Compact mode styles */
1210
- .messages-area.compact-mode { gap: 0; padding: 8px 12px; }
1211
- .messages-area.compact-mode .message { padding: 3px 8px; gap: 6px; }
1539
+ .messages-area.compact-mode { padding: 8px 12px; }
1540
+ .messages-area.compact-mode > * + * {
1541
+ margin-top: 4px;
1542
+ margin-top: var(--nh-space-1, 4px);
1543
+ }
1544
+ .messages-area.compact-mode .message.nh-msg-card { padding: var(--nh-space-2) var(--nh-space-3); }
1212
1545
  .messages-area.compact-mode .msg-avatar,
1213
1546
  .messages-area.compact-mode .msg-avatar-img { display: none; }
1214
- .messages-area.compact-mode .msg-header { margin-bottom: 0; display: inline; }
1215
- .messages-area.compact-mode .msg-body { display: inline; }
1216
- .messages-area.compact-mode .msg-from { font-size: 12px; }
1547
+ .messages-area.compact-mode .msg-card-head {
1548
+ display: inline-flex;
1549
+ flex-wrap: wrap;
1550
+ align-items: baseline;
1551
+ gap: 6px;
1552
+ margin-bottom: 0;
1553
+ width: auto;
1554
+ }
1555
+ .messages-area.compact-mode .msg-card-ident { display: inline; flex: none; }
1556
+ .messages-area.compact-mode .msg-from { font-size: 12px; display: inline; }
1557
+ .messages-area.compact-mode .msg-to-line { display: inline; margin-top: 0; margin-left: 4px; }
1558
+ .messages-area.compact-mode .msg-card-head-right { flex-direction: row; align-items: baseline; }
1217
1559
  .messages-area.compact-mode .msg-time { font-size: 9px; }
1560
+ .messages-area.compact-mode .msg-body { display: block; }
1218
1561
  .messages-area.compact-mode .msg-content { display: inline; font-size: 12px; }
1219
1562
  .messages-area.compact-mode .msg-content p { display: inline; margin: 0; }
1220
1563
 
@@ -1310,18 +1653,26 @@
1310
1653
  .app-footer a:hover { color: var(--accent); }
1311
1654
 
1312
1655
  /* ===== MESSAGE GROUPING ===== */
1313
- .message.grouped {
1314
- padding-top: 2px;
1315
- padding-bottom: 2px;
1656
+ .message.grouped.nh-msg-card {
1657
+ padding-top: var(--nh-space-2);
1658
+ padding-bottom: var(--nh-space-2);
1316
1659
  }
1317
1660
 
1318
- .message.grouped .msg-avatar {
1661
+ .message.grouped .msg-avatar,
1662
+ .message.grouped .msg-avatar-img {
1319
1663
  visibility: hidden;
1320
- width: 32px;
1664
+ width: 36px;
1321
1665
  height: 0;
1666
+ min-height: 0;
1667
+ border: none;
1668
+ margin: 0;
1322
1669
  }
1323
1670
 
1324
- .message.grouped .msg-header {
1671
+ .message.grouped .msg-card-head {
1672
+ display: none;
1673
+ }
1674
+
1675
+ .message.grouped .handoff-banner {
1325
1676
  display: none;
1326
1677
  }
1327
1678
 
@@ -1397,10 +1748,10 @@
1397
1748
 
1398
1749
  /* ===== UNIFIED NAV SIDEBAR ===== */
1399
1750
  .nav-sidebar {
1400
- width: 220px;
1401
- min-width: 220px;
1751
+ width: var(--sidebar-w);
1752
+ min-width: var(--sidebar-w);
1402
1753
  background: var(--surface);
1403
- border-right: 1px solid var(--border);
1754
+ border-right: 1px solid var(--nh-border);
1404
1755
  display: flex;
1405
1756
  flex-direction: column;
1406
1757
  overflow-y: auto;
@@ -1416,13 +1767,30 @@
1416
1767
  display: flex;
1417
1768
  align-items: center;
1418
1769
  justify-content: space-between;
1419
- padding: 16px 16px 12px;
1770
+ padding: var(--nh-space-4) var(--nh-space-4) var(--nh-space-3);
1420
1771
  flex-shrink: 0;
1772
+ border-bottom: 1px solid var(--nh-border);
1421
1773
  }
1422
- .nav-sidebar-logo {
1423
- font-size: 13px;
1774
+ .nh-brand-mark {
1775
+ width: 24px;
1776
+ height: 24px;
1777
+ border-radius: 0;
1778
+ background: none;
1779
+ color: var(--nh-on-accent);
1780
+ font-size: 12px;
1424
1781
  font-weight: 700;
1425
- color: var(--text-dim);
1782
+ display: inline-flex;
1783
+ align-items: center;
1784
+ justify-content: center;
1785
+ flex-shrink: 0;
1786
+ }
1787
+ .nav-sidebar-logo {
1788
+ display: inline-flex;
1789
+ align-items: center;
1790
+ gap: var(--nh-space-2);
1791
+ font-size: 14px;
1792
+ font-weight: 600;
1793
+ color: var(--text);
1426
1794
  white-space: nowrap;
1427
1795
  overflow: hidden;
1428
1796
  letter-spacing: 0.02em;
@@ -1447,11 +1815,11 @@
1447
1815
  .nav-section-main { display: flex; flex-direction: column; gap: 1px; }
1448
1816
  .nav-sidebar-divider { height: 1px; background: var(--border); margin: 10px 14px; flex-shrink: 0; }
1449
1817
  .nav-sidebar-label {
1450
- font-size: 10px;
1818
+ font-size: 12px;
1451
1819
  font-weight: 600;
1452
- color: var(--text-muted);
1453
- letter-spacing: 0.08em;
1454
- padding: 8px 14px 6px;
1820
+ color: var(--text-dim);
1821
+ letter-spacing: 0.05em;
1822
+ padding: var(--nh-space-2) var(--nh-space-3) var(--nh-space-2);
1455
1823
  white-space: nowrap;
1456
1824
  overflow: hidden;
1457
1825
  text-transform: uppercase;
@@ -1467,23 +1835,28 @@
1467
1835
  display: flex;
1468
1836
  align-items: center;
1469
1837
  gap: 10px;
1470
- padding: 8px 12px;
1838
+ padding: var(--nh-space-2) var(--nh-space-3);
1471
1839
  cursor: pointer;
1472
- border-radius: 8px;
1473
- transition: all 0.15s;
1840
+ border-radius: var(--nh-radius-sm);
1841
+ border: 1px solid transparent;
1842
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
1474
1843
  white-space: nowrap;
1475
1844
  overflow: hidden;
1476
1845
  margin: 1px 6px;
1477
1846
  position: relative;
1478
1847
  }
1479
- .nav-item:hover { background: rgba(255,255,255,0.04); }
1480
- .nav-item.active { background: var(--accent-dim); }
1848
+ .nav-item:hover { background: var(--nh-card); color: var(--text); }
1849
+ .nav-item.active {
1850
+ background: rgba(245, 158, 11, 0.1);
1851
+ border-color: transparent;
1852
+ box-shadow: inset 3px 0 0 var(--nh-accent);
1853
+ }
1481
1854
  .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
1855
  .nav-item .nav-icon svg { flex-shrink: 0; width: 16px; height: 16px; }
1483
1856
  .nav-item.active .nav-icon { color: var(--accent); }
1484
1857
  .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; }
1858
+ .nav-item .nav-text { font-size: 14px; font-weight: 500; color: var(--text-dim); }
1859
+ .nav-item.active .nav-text { color: var(--accent); font-weight: 500; }
1487
1860
  .nav-item:hover .nav-text { color: var(--text); }
1488
1861
  .nav-sidebar.collapsed .nav-item { justify-content: center; padding: 10px 4px; margin: 0; }
1489
1862
  .nav-sidebar.collapsed .nav-item .nav-text { display: none; }
@@ -1492,123 +1865,450 @@
1492
1865
  .agent-bar {
1493
1866
  display: flex;
1494
1867
  align-items: center;
1495
- gap: 6px;
1496
- padding: 6px 12px;
1497
- background: var(--surface);
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 {
1868
+ gap: 6px;
1869
+ padding: 6px var(--nh-space-4);
1870
+ background: var(--surface);
1871
+ border-bottom: 1px solid var(--nh-border);
1872
+ overflow-x: auto;
1873
+ flex-shrink: 0;
1874
+ min-height: 36px;
1875
+ }
1876
+ .agent-bar::-webkit-scrollbar { height: 0; }
1877
+ .agent-pill {
1878
+ display: flex;
1879
+ align-items: center;
1880
+ gap: 5px;
1881
+ padding: 3px 10px 3px 6px;
1882
+ border-radius: 20px;
1883
+ background: var(--surface-2);
1884
+ border: 1px solid var(--border);
1885
+ font-size: 13px;
1886
+ white-space: nowrap;
1887
+ cursor: pointer;
1888
+ transition: all 0.15s;
1889
+ flex-shrink: 0;
1890
+ }
1891
+ .agent-pill:hover { border-color: var(--accent); }
1892
+ .agent-pill .status-dot {
1893
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
1894
+ }
1895
+ .agent-pill.online .status-dot, .agent-pill.working .status-dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
1896
+ .agent-pill.listening .status-dot { background: var(--nh-blue); animation: pulse 2s infinite; box-shadow: 0 0 6px var(--nh-blue); }
1897
+ .agent-pill.idle .status-dot { background: var(--accent); }
1898
+ .agent-pill.offline .status-dot { background: var(--nh-text-muted); opacity: 0.65; }
1899
+ .agent-pill.unknown .status-dot { background: var(--nh-text-muted); opacity: 0.4; }
1900
+ .agent-pill.stale .status-dot { background: var(--accent); opacity: 0.8; }
1901
+ .agent-pill .agent-name { font-weight: 600; color: var(--text); }
1902
+ .agent-pill .agent-activity { color: var(--text-muted); font-size: 12px; }
1903
+ .agent-pill.offline .agent-name { color: var(--text-muted); opacity: 0.6; }
1904
+ .agent-pill.unknown .agent-name { color: var(--text-muted); opacity: 0.4; }
1905
+ .agent-pill.stale .agent-name { color: var(--text-muted); opacity: 0.7; }
1906
+
1907
+ /* ===== OVERVIEW PAGE ===== */
1908
+ .overview-area {
1909
+ display: none;
1910
+ flex-direction: column;
1911
+ gap: var(--nh-space-6);
1912
+ padding: var(--nh-space-6) var(--nh-space-8);
1913
+ overflow-y: auto;
1914
+ background: var(--nh-bg);
1915
+ }
1916
+ .overview-area.visible { display: flex; }
1917
+
1918
+ .overview-welcome {
1919
+ margin-bottom: 4px;
1920
+ display: flex;
1921
+ flex-wrap: wrap;
1922
+ align-items: flex-start;
1923
+ justify-content: space-between;
1924
+ gap: var(--nh-space-4);
1925
+ }
1926
+ .overview-welcome > div:first-child {
1927
+ flex: 1;
1928
+ min-width: min(100%, 220px);
1929
+ }
1930
+ .overview-welcome h2 {
1931
+ font-size: 20px;
1932
+ font-weight: 600;
1933
+ color: var(--text);
1934
+ margin-bottom: var(--nh-space-1);
1935
+ letter-spacing: -0.02em;
1936
+ }
1937
+ .overview-welcome p {
1938
+ font-size: 14px;
1939
+ color: var(--text-dim);
1940
+ }
1941
+
1942
+ .overview-coordinator {
1943
+ display: flex;
1944
+ align-items: center;
1945
+ gap: var(--nh-space-2);
1946
+ flex-shrink: 0;
1947
+ }
1948
+ .overview-coordinator-label {
1949
+ font-size: 12px;
1950
+ color: var(--text-dim);
1951
+ }
1952
+ .overview-coordinator-toggle {
1953
+ display: flex;
1954
+ border: 1px solid var(--nh-border);
1955
+ border-radius: var(--nh-radius-sm);
1956
+ overflow: hidden;
1957
+ font-size: 12px;
1958
+ }
1959
+ .overview-coordinator-btn {
1960
+ padding: 6px 14px;
1961
+ border: none;
1962
+ cursor: pointer;
1963
+ font-size: 12px;
1964
+ font-family: inherit;
1965
+ font-weight: 400;
1966
+ background: var(--nh-card);
1967
+ color: var(--text-dim);
1968
+ transition: background-color 0.15s ease, color 0.15s ease;
1969
+ }
1970
+ .overview-coordinator-btn + .overview-coordinator-btn {
1971
+ border-left: 1px solid var(--nh-border);
1972
+ }
1973
+ .overview-coordinator-btn:hover {
1974
+ color: var(--text);
1975
+ }
1976
+ /* Stitch: selected “Stay with me” = dark graphite; “Run autonomously” selected = solid amber */
1977
+ .overview-coordinator-btn--mode-responsive {
1978
+ font-weight: 600;
1979
+ background: var(--nh-graphite);
1980
+ color: var(--nh-text-primary);
1981
+ border-color: var(--nh-border);
1982
+ }
1983
+ .overview-coordinator-btn--mode-autonomous {
1984
+ font-weight: 600;
1985
+ background: var(--nh-accent);
1986
+ color: var(--nh-on-accent);
1987
+ border-color: var(--nh-accent);
1988
+ }
1989
+
1990
+ .overview-metrics {
1991
+ display: grid;
1992
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1993
+ gap: var(--nh-space-4);
1994
+ }
1995
+ .metric-card {
1996
+ background: var(--nh-card);
1997
+ border: 1px solid var(--nh-border);
1998
+ border-radius: var(--nh-radius-lg);
1999
+ padding: var(--nh-space-5) var(--nh-space-5);
2000
+ display: flex;
2001
+ flex-direction: column;
2002
+ gap: var(--nh-space-2);
2003
+ position: relative;
2004
+ overflow: hidden;
2005
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
2006
+ }
2007
+ .metric-card:hover {
2008
+ border-color: rgba(156, 163, 175, 0.45);
2009
+ box-shadow: var(--nh-shadow-card);
2010
+ }
2011
+ .metric-card .metric-icon {
2012
+ width: 40px;
2013
+ height: 40px;
2014
+ border-radius: var(--nh-radius-md);
2015
+ display: flex;
2016
+ align-items: center;
2017
+ justify-content: center;
2018
+ margin-bottom: 2px;
2019
+ flex-shrink: 0;
2020
+ }
2021
+ .metric-card .metric-label {
2022
+ font-size: 12px;
2023
+ color: var(--text-dim);
2024
+ font-weight: 500;
2025
+ letter-spacing: 0.02em;
2026
+ }
2027
+ .metric-card .metric-value {
2028
+ font-size: 28px;
2029
+ font-weight: 700;
2030
+ color: var(--text);
2031
+ line-height: 1.15;
2032
+ letter-spacing: -0.03em;
2033
+ }
2034
+ .metric-card .metric-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
2035
+
2036
+ .metric-card__progress {
2037
+ height: 6px;
2038
+ background: var(--nh-card-hover);
2039
+ border-radius: 3px;
2040
+ overflow: hidden;
2041
+ margin-top: var(--nh-space-2);
2042
+ }
2043
+ .metric-card__progress-fill {
2044
+ height: 100%;
2045
+ border-radius: 3px;
2046
+ background: var(--nh-accent);
2047
+ transition: width 0.35s ease;
2048
+ }
2049
+
2050
+ .overview-stitch-rows {
2051
+ display: flex;
2052
+ flex-direction: column;
2053
+ gap: var(--nh-space-4);
2054
+ width: 100%;
2055
+ }
2056
+ .overview-stitch-rows > .overview-panel {
2057
+ width: 100%;
2058
+ box-sizing: border-box;
2059
+ }
2060
+
2061
+ .overview-grid {
2062
+ display: grid;
2063
+ grid-template-columns: 1fr 1fr;
2064
+ gap: var(--nh-space-4);
2065
+ }
2066
+ .overview-grid--pair {
2067
+ align-items: stretch;
2068
+ }
2069
+ @media (max-width: 900px) {
2070
+ .overview-grid--pair {
2071
+ grid-template-columns: 1fr;
2072
+ }
2073
+ }
2074
+ .overview-panel {
2075
+ background: var(--nh-sidebar);
2076
+ border: 1px solid var(--nh-border);
2077
+ border-radius: var(--nh-radius-xl);
2078
+ padding: var(--nh-space-5);
2079
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
2080
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
2081
+ }
2082
+ [data-theme="light"] .overview-panel {
2083
+ box-shadow: none;
2084
+ }
2085
+ .overview-panel:hover {
2086
+ border-color: rgba(156, 163, 175, 0.45);
2087
+ box-shadow: var(--nh-shadow-card);
2088
+ }
2089
+ .overview-panel h3 {
2090
+ font-size: 14px;
2091
+ font-weight: 600;
2092
+ color: var(--text);
2093
+ margin-bottom: var(--nh-space-3);
2094
+ display: flex;
2095
+ align-items: center;
2096
+ justify-content: space-between;
2097
+ }
2098
+ .overview-panel h3 a {
2099
+ font-size: 12px;
2100
+ font-weight: 600;
2101
+ color: var(--nh-accent);
2102
+ cursor: pointer;
2103
+ text-decoration: none;
2104
+ transition: opacity 0.15s;
2105
+ }
2106
+ .overview-panel h3 a:hover { opacity: 0.85; }
2107
+ .overview-panel-empty {
2108
+ font-size: 13px;
2109
+ color: var(--text-dim);
2110
+ margin: 0;
2111
+ }
2112
+ .overview-task-stitch {
2113
+ display: flex;
2114
+ flex-direction: column;
2115
+ gap: var(--nh-space-2);
2116
+ font-size: 14px;
2117
+ }
2118
+ .overview-task-stitch-done { color: var(--text); font-weight: 600; }
2119
+ .overview-task-stitch-rem { color: var(--text-dim); }
2120
+ .overview-agent-row {
2121
+ display: flex;
2122
+ align-items: center;
2123
+ gap: var(--nh-space-2);
2124
+ flex-wrap: wrap;
2125
+ padding: var(--nh-space-2) 0;
2126
+ border-bottom: 1px solid var(--nh-border);
2127
+ }
2128
+ .overview-agent-row:last-child { border-bottom: none; }
2129
+ .overview-agent-name { font-size: 13px; font-weight: 600; color: var(--text); }
2130
+ .overview-agent-status { font-size: 12px; color: var(--text-muted); }
2131
+ .overview-activity-row {
2132
+ display: flex;
2133
+ gap: var(--nh-space-3);
2134
+ padding: var(--nh-space-2) 0;
2135
+ border-bottom: 1px solid var(--nh-border);
2136
+ font-size: 13px;
2137
+ align-items: flex-start;
2138
+ }
2139
+ .overview-activity-row:last-child { border-bottom: none; }
2140
+ .overview-activity-time {
2141
+ color: var(--text-dim);
2142
+ font-size: 12px;
2143
+ flex-shrink: 0;
2144
+ min-width: 100px;
2145
+ font-variant-numeric: tabular-nums;
2146
+ }
2147
+ .overview-activity-body {
2148
+ flex: 1;
2149
+ min-width: 0;
2150
+ color: var(--text-dim);
2151
+ line-height: 1.45;
2152
+ }
2153
+ .overview-msg-from--agent { font-weight: 600; color: var(--nh-accent); }
2154
+ .overview-msg-from--coord { font-weight: 600; color: var(--nh-yellow); }
2155
+ .overview-msg-from--system { font-weight: 600; color: var(--nh-text-muted); }
2156
+ .overview-activity-preview {
2157
+ color: var(--text-dim);
2158
+ word-break: break-word;
2159
+ }
2160
+ .overview-full-width { grid-column: 1 / -1; }
2161
+ .overview-task-summary { display: flex; gap: 10px; flex-wrap: wrap; }
2162
+ .overview-task-badge {
2163
+ display: inline-flex;
2164
+ align-items: center;
2165
+ padding: 4px 10px;
2166
+ border-radius: 9999px;
2167
+ font-size: 12px;
2168
+ font-weight: 500;
2169
+ line-height: 1.25;
2170
+ border: 1px solid transparent;
2171
+ }
2172
+ .overview-task-badge.pending {
2173
+ background: var(--nh-yellow-10);
2174
+ color: var(--yellow);
2175
+ border-color: var(--nh-yellow-20);
2176
+ }
2177
+ .overview-task-badge.active {
2178
+ background: var(--nh-accent-10);
2179
+ color: var(--accent);
2180
+ border-color: var(--nh-accent-20);
2181
+ }
2182
+ .overview-task-badge.done {
2183
+ background: var(--nh-green-10);
2184
+ color: var(--green);
2185
+ border-color: var(--nh-green-20);
2186
+ }
2187
+ .overview-empty-state {
2188
+ display: flex;
2189
+ flex-direction: column;
2190
+ align-items: center;
2191
+ justify-content: center;
2192
+ padding: 60px var(--nh-space-5);
2193
+ text-align: center;
2194
+ background: var(--nh-sidebar);
2195
+ border: 1px solid var(--nh-border);
2196
+ border-radius: var(--nh-radius-xl);
2197
+ box-shadow: var(--nh-shadow-card);
2198
+ }
2199
+ .overview-empty-state-icon {
2200
+ width: 64px;
2201
+ height: 64px;
2202
+ border-radius: var(--nh-radius-xl);
2203
+ background: var(--nh-accent-10);
2204
+ border: 1px solid var(--nh-accent-20);
1505
2205
  display: flex;
1506
2206
  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;
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; }
1529
-
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;
2207
+ justify-content: center;
2208
+ margin-bottom: var(--nh-space-5);
1536
2209
  }
1537
- .overview-welcome h2 {
1538
- font-size: 22px;
1539
- font-weight: 700;
2210
+ .overview-empty-state-title {
2211
+ font-size: 18px;
2212
+ font-weight: 600;
1540
2213
  color: var(--text);
1541
- margin-bottom: 4px;
1542
- letter-spacing: -0.3px;
2214
+ margin-bottom: var(--nh-space-2);
1543
2215
  }
1544
- .overview-welcome p {
1545
- font-size: 13px;
1546
- color: var(--text-muted);
2216
+ .overview-empty-state-desc {
2217
+ font-size: 14px;
2218
+ color: var(--text-dim);
2219
+ max-width: 360px;
2220
+ line-height: 1.6;
2221
+ margin-bottom: var(--nh-space-1);
1547
2222
  }
1548
-
1549
- .overview-metrics {
1550
- display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;
2223
+ .overview-launch-btn {
2224
+ display: inline-flex;
2225
+ align-items: center;
2226
+ gap: var(--nh-space-2);
2227
+ padding: 10px var(--nh-space-5);
2228
+ background: var(--nh-accent);
2229
+ color: var(--nh-on-accent);
2230
+ border: none;
2231
+ border-radius: var(--nh-radius-sm);
2232
+ font-size: 14px;
2233
+ font-weight: 600;
2234
+ cursor: pointer;
2235
+ font-family: inherit;
2236
+ margin-top: var(--nh-space-3);
2237
+ transition: background-color 0.15s ease, box-shadow 0.2s ease, transform 0.15s ease;
2238
+ box-shadow: 0 2px 12px var(--nh-accent-20);
1551
2239
  }
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;
2240
+ .overview-launch-btn:hover {
2241
+ background: var(--nh-accent-90);
2242
+ transform: translateY(-1px);
2243
+ box-shadow: 0 4px 20px var(--nh-accent-20);
1557
2244
  }
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;
2245
+ .overview-notif-row-stitch {
2246
+ display: flex;
2247
+ align-items: flex-start;
2248
+ gap: var(--nh-space-3);
2249
+ padding: var(--nh-space-2) 0;
2250
+ border-bottom: 1px solid var(--nh-border);
2251
+ font-size: 13px;
1563
2252
  }
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;
2253
+ .overview-notif-row-stitch:last-child { border-bottom: none; }
2254
+ .overview-notif-icon-stitch {
2255
+ width: 24px;
2256
+ height: 24px;
2257
+ border-radius: var(--nh-radius-sm);
2258
+ display: flex;
2259
+ align-items: center;
2260
+ justify-content: center;
2261
+ font-size: 12px;
2262
+ font-weight: 700;
2263
+ flex-shrink: 0;
1570
2264
  }
1571
- .overview-panel {
1572
- background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px;
1573
- transition: border-color 0.2s;
2265
+ .overview-notif--success .overview-notif-icon-stitch {
2266
+ background: var(--nh-green-10);
2267
+ color: var(--nh-green);
1574
2268
  }
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;
2269
+ .overview-notif--info .overview-notif-icon-stitch {
2270
+ background: rgba(59, 130, 246, 0.12);
2271
+ color: var(--nh-blue);
1579
2272
  }
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);
2273
+ .overview-notif--neutral .overview-notif-icon-stitch {
2274
+ background: var(--nh-card-hover);
2275
+ color: var(--text-dim);
1585
2276
  }
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;
2277
+ .overview-notif--warn .overview-notif-icon-stitch {
2278
+ background: var(--nh-orange-10);
2279
+ color: var(--nh-orange);
1592
2280
  }
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;
2281
+ .overview-notif-tag {
2282
+ font-size: 11px;
2283
+ font-weight: 700;
2284
+ margin-right: 6px;
1601
2285
  }
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);
2286
+ .overview-notif--success .overview-notif-tag { color: var(--nh-green); }
2287
+ .overview-notif--info .overview-notif-tag { color: var(--nh-blue); }
2288
+ .overview-notif--neutral .overview-notif-tag { color: var(--text-dim); }
2289
+ .overview-notif--warn .overview-notif-tag { color: var(--nh-orange); }
2290
+ .overview-notif-msg { color: var(--text-dim); line-height: 1.4; }
2291
+ .overview-notif-time-stitch {
2292
+ font-size: 11px;
2293
+ color: var(--text-dim);
2294
+ flex-shrink: 0;
2295
+ margin-left: auto;
2296
+ padding-left: var(--nh-space-2);
2297
+ }
2298
+ .overview-role-pill {
2299
+ display: inline-block;
2300
+ font-size: 10px;
2301
+ font-weight: 600;
2302
+ text-transform: uppercase;
2303
+ letter-spacing: 0.04em;
2304
+ padding: 2px 6px;
2305
+ border-radius: 4px;
2306
+ background: var(--nh-accent-10);
2307
+ color: var(--nh-accent);
2308
+ border: 1px solid var(--nh-accent-20);
2309
+ margin-left: var(--nh-space-2);
2310
+ vertical-align: middle;
1610
2311
  }
1611
- .overview-launch-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
1612
2312
 
1613
2313
  /* ===== TOAST NOTIFICATIONS ===== */
1614
2314
  .toast-container {
@@ -1631,7 +2331,11 @@
1631
2331
  @keyframes toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(12px); } }
1632
2332
 
1633
2333
  /* ===== USER MESSAGES ===== */
1634
- .message.user-msg { background: rgba(245, 158, 11, 0.04); border-left: 2px solid var(--accent); }
2334
+ .message.user-msg.nh-msg-card {
2335
+ background: var(--nh-accent-5);
2336
+ border-left: 3px solid var(--nh-accent);
2337
+ padding-left: calc(var(--nh-space-5) - 2px);
2338
+ }
1635
2339
 
1636
2340
  /* Hide old view-tabs (replaced by nav sidebar) */
1637
2341
  .view-tabs { display: none !important; }
@@ -1878,6 +2582,7 @@
1878
2582
  .kanban-title.in_review { background: rgba(168,85,247,0.15); color: var(--purple,#a855f7); }
1879
2583
  .kanban-title.done { background: var(--green-dim); color: var(--green); }
1880
2584
  .kanban-title.blocked { background: var(--red-dim); color: var(--red); }
2585
+ .kanban-title.blocked_permanent { background: var(--red); color: #fff; }
1881
2586
 
1882
2587
  .kanban-count {
1883
2588
  font-size: 10px;
@@ -1911,8 +2616,10 @@
1911
2616
  color: var(--text-dim);
1912
2617
  margin-bottom: 6px;
1913
2618
  display: -webkit-box;
1914
- -webkit-line-clamp: 2;
2619
+ -webkit-line-clamp: 3;
2620
+ line-clamp: 3;
1915
2621
  -webkit-box-orient: vertical;
2622
+ box-orient: vertical;
1916
2623
  overflow: hidden;
1917
2624
  }
1918
2625
 
@@ -2182,10 +2889,11 @@
2182
2889
  }
2183
2890
 
2184
2891
  /* ===== HANDOFF MESSAGE ===== */
2185
- .message.handoff-msg {
2186
- background: var(--purple-dim);
2187
- border: 1px solid rgba(188, 140, 255, 0.2);
2188
- border-left: 3px solid var(--purple);
2892
+ .message.handoff-msg.nh-msg-card {
2893
+ background: var(--nh-purple-10);
2894
+ border-color: var(--nh-purple-20);
2895
+ border-left: 3px solid var(--nh-purple);
2896
+ padding-left: calc(var(--nh-space-5) - 2px);
2189
2897
  }
2190
2898
 
2191
2899
  .handoff-banner {
@@ -2194,10 +2902,10 @@
2194
2902
  gap: 6px;
2195
2903
  font-size: 11px;
2196
2904
  font-weight: 600;
2197
- color: var(--purple);
2905
+ color: var(--nh-purple);
2198
2906
  text-transform: uppercase;
2199
2907
  letter-spacing: 0.5px;
2200
- margin-bottom: 4px;
2908
+ margin-bottom: var(--nh-space-2);
2201
2909
  }
2202
2910
 
2203
2911
  /* ===== FILE SHARE MESSAGE ===== */
@@ -2205,37 +2913,119 @@
2205
2913
  display: inline-flex;
2206
2914
  align-items: center;
2207
2915
  gap: 6px;
2208
- background: var(--surface-2);
2209
- border: 1px solid var(--border);
2210
- border-radius: 6px;
2916
+ background: var(--nh-card-hover);
2917
+ border: 1px solid var(--nh-border);
2918
+ border-radius: var(--nh-radius-sm);
2211
2919
  padding: 4px 10px;
2212
2920
  font-size: 11px;
2213
- color: var(--text-dim);
2214
- margin-bottom: 6px;
2921
+ color: var(--nh-text-secondary);
2922
+ margin-bottom: var(--nh-space-2);
2215
2923
  }
2216
2924
 
2217
2925
  .file-icon { font-size: 14px; }
2218
2926
 
2219
2927
  .file-size {
2220
- color: var(--text-muted);
2928
+ color: var(--nh-text-muted);
2221
2929
  font-size: 10px;
2222
2930
  }
2223
2931
 
2224
- /* ===== SYSTEM MESSAGE ===== */
2225
- .message.system-msg {
2226
- justify-content: center;
2227
- padding: 6px 14px;
2932
+ /* ===== SYSTEM EVENT BANNER (all system messages) ===== */
2933
+ .message.system-event-banner {
2934
+ background: none;
2935
+ border: none;
2936
+ border-radius: 6px;
2937
+ box-shadow: none;
2938
+ padding: 10px 16px;
2939
+ margin: 0 0 6px 0;
2940
+ min-height: auto;
2941
+ display: flex;
2942
+ align-items: flex-start;
2943
+ gap: 8px;
2944
+ border-left: 3px solid var(--nh-border, var(--border));
2945
+ }
2946
+ .system-event-banner .event-icon {
2947
+ flex-shrink: 0;
2948
+ width: 16px;
2949
+ height: 16px;
2950
+ margin-top: 1px;
2951
+ opacity: 0.8;
2952
+ }
2953
+ .system-event-banner .event-icon svg {
2954
+ width: 14px;
2955
+ height: 14px;
2956
+ display: block;
2957
+ }
2958
+ .system-event-banner .event-body {
2959
+ flex: 1;
2960
+ min-width: 0;
2961
+ }
2962
+ .system-event-banner .event-text {
2963
+ font-size: 12px;
2964
+ color: var(--nh-text-secondary, var(--text-muted));
2965
+ line-height: 1.4;
2966
+ word-wrap: break-word;
2967
+ }
2968
+ .system-event-banner .event-text p { margin: 0; }
2969
+ .system-event-banner .event-time {
2970
+ font-size: 10px;
2971
+ color: var(--nh-text-secondary, var(--text-muted));
2972
+ opacity: 0.5;
2973
+ flex-shrink: 0;
2974
+ margin-top: 2px;
2975
+ }
2976
+ /* Type colors — left border + icon color */
2977
+ .system-event-banner.evt-critical { border-left-color: #ef4444; }
2978
+ .system-event-banner.evt-critical .event-icon { color: #ef4444; }
2979
+ .system-event-banner.evt-critical .event-text { color: #ef4444; font-weight: 500; }
2980
+ .system-event-banner.evt-warning { border-left-color: #f59e0b; }
2981
+ .system-event-banner.evt-warning .event-icon { color: #f59e0b; }
2982
+ .system-event-banner.evt-warning .event-text { color: #f59e0b; }
2983
+ .system-event-banner.evt-event { border-left-color: #22c55e; }
2984
+ .system-event-banner.evt-event .event-icon { color: #22c55e; }
2985
+ .system-event-banner.evt-review { border-left-color: #3b82f6; }
2986
+ .system-event-banner.evt-review .event-icon { color: #3b82f6; }
2987
+ .system-event-banner.evt-status { border-left-color: #6b7280; }
2988
+ .system-event-banner.evt-status .event-icon { color: #6b7280; }
2989
+ .system-event-banner.evt-role { border-left-color: #8b5cf6; }
2990
+ .system-event-banner.evt-role .event-icon { color: #8b5cf6; }
2991
+ .system-event-banner.evt-violation { border-left-color: #ef4444; background: rgba(239,68,68,0.05); }
2992
+ .system-event-banner.evt-violation .event-icon { color: #ef4444; }
2993
+ .system-event-banner.evt-violation .event-text { color: var(--text); font-weight: 500; }
2994
+
2995
+ /* ===== VIOLATION MESSAGES ===== */
2996
+ .message.violation-msg.nh-msg-card {
2997
+ border: 1px solid var(--nh-amber, #f59e0b);
2998
+ background: rgba(245, 158, 11, 0.05);
2999
+ }
3000
+
3001
+ .violation-content {
3002
+ color: var(--text);
3003
+ font-weight: 500;
3004
+ font-style: normal;
2228
3005
  }
2229
3006
 
2230
- .message.system-msg .msg-body {
2231
- max-width: 600px;
2232
- text-align: center;
3007
+ .violation-icon {
3008
+ color: var(--nh-amber, #f59e0b);
3009
+ margin-right: 6px;
3010
+ vertical-align: middle;
3011
+ display: inline-flex;
3012
+ align-items: center;
2233
3013
  }
2234
3014
 
2235
- .message.system-msg .msg-content {
2236
- color: var(--text-dim);
2237
- font-style: italic;
2238
- font-size: 12px;
3015
+ .violation-label {
3016
+ color: var(--nh-amber, #f59e0b);
3017
+ font-weight: 700;
3018
+ margin-right: 4px;
3019
+ }
3020
+
3021
+ /* ===== STEP SHIELD ICON ===== */
3022
+ .step-shield {
3023
+ display: inline-flex;
3024
+ align-items: center;
3025
+ color: var(--nh-amber, #f59e0b);
3026
+ margin-right: 4px;
3027
+ vertical-align: middle;
3028
+ opacity: 0.85;
2239
3029
  }
2240
3030
 
2241
3031
  /* ===== RESPONSIVE ===== */
@@ -2261,6 +3051,24 @@
2261
3051
 
2262
3052
  .sidebar.open { transform: translateX(0); }
2263
3053
 
3054
+ .nav-sidebar {
3055
+ position: fixed;
3056
+ top: var(--header-h);
3057
+ left: 0;
3058
+ bottom: 0;
3059
+ z-index: 90;
3060
+ transform: translateX(-100%);
3061
+ box-shadow: 4px 0 20px rgba(0,0,0,0.3);
3062
+ transition: transform 0.25s ease;
3063
+ }
3064
+
3065
+ .nav-sidebar.open {
3066
+ display: flex !important;
3067
+ width: var(--sidebar-w) !important;
3068
+ min-width: var(--sidebar-w) !important;
3069
+ transform: translateX(0);
3070
+ }
3071
+
2264
3072
  .sidebar-overlay {
2265
3073
  display: none;
2266
3074
  position: fixed;
@@ -2305,9 +3113,14 @@
2305
3113
  .mobile-project-select:focus { border-color: var(--orange); }
2306
3114
 
2307
3115
  /* Messages: better mobile spacing */
2308
- .messages-area { padding: 8px; gap: 1px; }
2309
- .message { padding: 8px 10px; gap: 8px; }
3116
+ .messages-area { padding: 8px; }
3117
+ .messages-area > * + * {
3118
+ margin-top: 8px;
3119
+ margin-top: var(--nh-space-2, 8px);
3120
+ }
3121
+ .message.nh-msg-card { padding: var(--nh-space-3); }
2310
3122
  .msg-avatar { width: 28px; height: 28px; font-size: 11px; }
3123
+ .msg-avatar-img { width: 28px; height: 28px; }
2311
3124
  .msg-from { font-size: 12px; }
2312
3125
  .msg-content { font-size: 13px; line-height: 1.5; }
2313
3126
  .msg-time { font-size: 9px; }
@@ -2319,6 +3132,7 @@
2319
3132
  /* Input bar: stack vertically */
2320
3133
  .msg-input-bar {
2321
3134
  flex-direction: column;
3135
+ align-items: stretch;
2322
3136
  padding: 8px 10px;
2323
3137
  gap: 6px;
2324
3138
  }
@@ -2371,7 +3185,7 @@
2371
3185
  }
2372
3186
 
2373
3187
  .header { padding: 0 8px; }
2374
- .logo { font-size: 14px; }
3188
+ .logo { font-size: 17px; }
2375
3189
  .header-left { gap: 6px; }
2376
3190
 
2377
3191
  /* Compact header buttons */
@@ -2384,8 +3198,12 @@
2384
3198
 
2385
3199
  /* Messages: tighter for small screens */
2386
3200
  .messages-area { padding: 6px; }
2387
- .message { padding: 6px 8px; gap: 6px; border-radius: 6px; }
2388
- .msg-avatar { width: 24px; height: 24px; font-size: 10px; }
3201
+ .messages-area > * + * {
3202
+ margin-top: 6px;
3203
+ margin-top: var(--nh-space-2, 6px);
3204
+ }
3205
+ .message.nh-msg-card { padding: var(--nh-space-2) var(--nh-space-3); }
3206
+ .msg-avatar { width: 28px; height: 28px; font-size: 10px; }
2389
3207
  .msg-header { gap: 4px; }
2390
3208
  .msg-from { font-size: 11px; }
2391
3209
  .msg-to { font-size: 11px; }
@@ -2418,7 +3236,7 @@
2418
3236
 
2419
3237
  /* ===== MOBILE: VERY SMALL PHONE (360px) ===== */
2420
3238
  @media (max-width: 360px) {
2421
- .logo { font-size: 13px; }
3239
+ .logo { font-size: 15px; }
2422
3240
  .mobile-project-select { max-width: 80px; font-size: 9px; }
2423
3241
  .msg-avatar { display: none; }
2424
3242
  .message { gap: 0; }
@@ -2434,12 +3252,13 @@
2434
3252
  }
2435
3253
 
2436
3254
  .msg-avatar-img {
2437
- width: 32px;
2438
- height: 32px;
3255
+ width: 36px;
3256
+ height: 36px;
2439
3257
  border-radius: 50%;
2440
3258
  object-fit: cover;
2441
3259
  flex-shrink: 0;
2442
3260
  margin-top: 2px;
3261
+ border: 1px solid var(--nh-border);
2443
3262
  }
2444
3263
 
2445
3264
  .role-badge {
@@ -2452,7 +3271,7 @@
2452
3271
  margin-left: 4px;
2453
3272
  }
2454
3273
 
2455
- /* ===== v3.0: PROFILE POPUP ===== */
3274
+ /* ===== v3.0: PROFILE POPUP (3-TAB) ===== */
2456
3275
  .profile-popup {
2457
3276
  display: none;
2458
3277
  position: fixed;
@@ -2461,11 +3280,16 @@
2461
3280
  background-image: var(--gradient-surface);
2462
3281
  border: 1px solid var(--border-light);
2463
3282
  border-radius: 14px;
2464
- padding: 18px;
2465
- width: 260px;
3283
+ padding: 0;
3284
+ width: 310px;
3285
+ box-sizing: border-box;
3286
+ max-height: 80vh;
3287
+ overflow-x: hidden;
3288
+ overflow-y: hidden;
3289
+ word-break: break-word;
2466
3290
  box-shadow: var(--shadow-lg), 0 0 40px rgba(0,0,0,0.2);
2467
- backdrop-filter: blur(8px);
2468
- -webkit-backdrop-filter: blur(8px);
3291
+ backdrop-filter: blur(12px);
3292
+ -webkit-backdrop-filter: blur(12px);
2469
3293
  }
2470
3294
 
2471
3295
  .profile-popup.open { display: block; }
@@ -2474,19 +3298,21 @@
2474
3298
  display: flex;
2475
3299
  align-items: center;
2476
3300
  gap: 12px;
2477
- margin-bottom: 10px;
3301
+ padding: 16px 18px 12px;
2478
3302
  }
2479
3303
 
2480
3304
  .profile-popup-avatar {
2481
- width: 48px;
2482
- height: 48px;
3305
+ width: 44px;
3306
+ height: 44px;
2483
3307
  border-radius: 50%;
2484
3308
  object-fit: cover;
3309
+ flex-shrink: 0;
2485
3310
  }
2486
3311
 
2487
3312
  .profile-popup-name {
2488
3313
  font-weight: 700;
2489
3314
  font-size: 15px;
3315
+ line-height: 1.2;
2490
3316
  }
2491
3317
 
2492
3318
  .profile-popup-id {
@@ -2494,6 +3320,73 @@
2494
3320
  color: var(--text-muted);
2495
3321
  }
2496
3322
 
3323
+ .pp-status-badge {
3324
+ display: inline-flex;
3325
+ align-items: center;
3326
+ gap: 4px;
3327
+ font-size: 10px;
3328
+ font-weight: 600;
3329
+ text-transform: uppercase;
3330
+ letter-spacing: 0.3px;
3331
+ padding: 2px 8px;
3332
+ border-radius: 10px;
3333
+ margin-left: 6px;
3334
+ }
3335
+ .pp-status-badge.online { background: rgba(52,211,153,0.15); color: #34d399; }
3336
+ .pp-status-badge.offline { background: rgba(156,163,175,0.15); color: #9ca3af; }
3337
+ .pp-status-badge.listening { background: rgba(96,165,250,0.15); color: #60a5fa; }
3338
+ .pp-status-badge.unresponsive { background: rgba(249,115,22,0.15); color: #f97316; }
3339
+ .pp-status-badge.stuck { background: rgba(239,68,68,0.15); color: #ef4444; }
3340
+
3341
+ /* Tab bar */
3342
+ .pp-tabs {
3343
+ display: flex;
3344
+ border-bottom: 1px solid var(--border);
3345
+ padding: 0 18px;
3346
+ gap: 0;
3347
+ }
3348
+ .pp-tab {
3349
+ flex: 1;
3350
+ padding: 8px 0;
3351
+ font-size: 11px;
3352
+ font-weight: 600;
3353
+ text-transform: uppercase;
3354
+ letter-spacing: 0.4px;
3355
+ color: var(--text-muted);
3356
+ background: none;
3357
+ border: none;
3358
+ border-bottom: 2px solid transparent;
3359
+ cursor: pointer;
3360
+ text-align: center;
3361
+ transition: color 0.15s, border-color 0.15s;
3362
+ }
3363
+ .pp-tab:hover { color: var(--text); }
3364
+ .pp-tab.active { color: var(--nh-accent); border-bottom-color: var(--nh-accent); }
3365
+
3366
+ /* Tab content */
3367
+ .pp-tab-content {
3368
+ display: none;
3369
+ padding: 14px 18px 16px;
3370
+ max-height: calc(80vh - 140px);
3371
+ overflow-y: auto;
3372
+ overflow-x: hidden;
3373
+ }
3374
+ .pp-tab-content.active { display: block; }
3375
+ .terminal-wrapper {
3376
+ background: #000;
3377
+ border-radius: var(--nh-radius-md);
3378
+ padding: 10px;
3379
+ margin-top: 8px;
3380
+ height: 320px;
3381
+ position: relative;
3382
+ overflow: hidden;
3383
+ border: 1px solid var(--nh-border);
3384
+ }
3385
+ #pp-terminal-container {
3386
+ width: 100%;
3387
+ height: 100%;
3388
+ }
3389
+
2497
3390
  .profile-popup-bio {
2498
3391
  font-size: 12px;
2499
3392
  color: var(--text-dim);
@@ -2504,19 +3397,49 @@
2504
3397
  .profile-popup-stats {
2505
3398
  display: grid;
2506
3399
  grid-template-columns: 1fr 1fr;
2507
- gap: 4px;
3400
+ gap: 6px;
2508
3401
  margin-top: 8px;
2509
3402
  }
2510
3403
 
2511
3404
  .profile-popup-stat {
2512
3405
  font-size: 11px;
2513
3406
  color: var(--text-dim);
3407
+ background: var(--surface-2);
3408
+ border-radius: 8px;
3409
+ padding: 8px 10px;
2514
3410
  }
2515
3411
 
2516
3412
  .profile-popup-stat span {
2517
3413
  font-weight: 600;
2518
3414
  color: var(--text);
3415
+ display: block;
3416
+ font-size: 16px;
3417
+ margin-bottom: 2px;
3418
+ }
3419
+
3420
+ /* Actions tab */
3421
+ .pp-actions-grid {
3422
+ display: grid;
3423
+ grid-template-columns: 1fr 1fr;
3424
+ gap: 8px;
3425
+ }
3426
+ .pp-action-btn {
3427
+ display: flex;
3428
+ flex-direction: column;
3429
+ align-items: center;
3430
+ gap: 6px;
3431
+ padding: 14px 8px;
3432
+ background: var(--surface-2);
3433
+ border: 1px solid var(--border);
3434
+ border-radius: 10px;
3435
+ cursor: pointer;
3436
+ color: var(--text);
3437
+ font-size: 11px;
3438
+ font-weight: 500;
3439
+ transition: background 0.15s, border-color 0.15s;
2519
3440
  }
3441
+ .pp-action-btn:hover { background: var(--surface-3); border-color: var(--nh-accent-dim); }
3442
+ .pp-action-btn svg { width: 20px; height: 20px; stroke: var(--nh-accent); fill: none; stroke-width: 1.5; }
2520
3443
 
2521
3444
  /* ===== v3.0: WORKSPACES TAB ===== */
2522
3445
  .workspaces-area {
@@ -2721,6 +3644,7 @@
2721
3644
  margin-top: 4px;
2722
3645
  display: -webkit-box;
2723
3646
  -webkit-line-clamp: 2;
3647
+ line-clamp: 2;
2724
3648
  -webkit-box-orient: vertical;
2725
3649
  overflow: hidden;
2726
3650
  }
@@ -3242,7 +4166,7 @@
3242
4166
  }
3243
4167
  .launch-status-item .ls-dot.pending { background: var(--text-muted); }
3244
4168
  .launch-status-item .ls-dot.launching { background: var(--yellow); animation: pulse-dot 1s infinite; }
3245
- .launch-status-item .ls-dot.launched { background: var(--orange, #f59e0b); }
4169
+ .launch-status-item .ls-dot.launched { background: var(--nh-accent); }
3246
4170
  .launch-status-item .ls-dot.registered { background: var(--green); }
3247
4171
  .launch-status-item .ls-dot.failed { background: var(--red); }
3248
4172
  @keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
@@ -3523,8 +4447,8 @@
3523
4447
  <div class="header">
3524
4448
  <div class="header-left">
3525
4449
  <button class="mobile-toggle" onclick="toggleSidebar()" aria-label="Menu">&#9776;</button>
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>
3527
- <div class="logo">Neohive</div>
4450
+ <img src="logo.svg" width="40" height="40" alt="" aria-hidden="true" style="flex-shrink:0">
4451
+ <div class="logo">NeoHive</div>
3528
4452
  <!-- Hidden stat elements for JS compatibility -->
3529
4453
  <span id="stat-messages" style="display:none">0</span>
3530
4454
  <span id="stat-agents" style="display:none">0</span>
@@ -3534,7 +4458,10 @@
3534
4458
  <div id="managed-badge" style="display:none"><span id="managed-phase"></span><span id="managed-floor-info"></span></div>
3535
4459
  </div>
3536
4460
  <div class="header-actions">
3537
- <span class="conn-dot" title="Connected"></span>
4461
+ <div class="header-conn" title="Live updates connected">
4462
+ <span class="conn-dot" aria-hidden="true"></span>
4463
+ <span>Connected</span>
4464
+ </div>
3538
4465
  <!-- Settings dropdown -->
3539
4466
  <div style="position:relative;display:inline-block">
3540
4467
  <button class="header-settings-btn" onclick="toggleSettingsMenu()" title="Settings">
@@ -3588,9 +4515,10 @@
3588
4515
  <!-- APP LAYOUT -->
3589
4516
  <div class="app">
3590
4517
  <!-- UNIFIED NAV SIDEBAR -->
4518
+ <div class="sidebar-overlay" id="sidebar-overlay" onclick="closeSidebar()"></div>
3591
4519
  <nav class="nav-sidebar expanded" id="nav-sidebar">
3592
4520
  <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>
4521
+ <span class="nav-sidebar-logo">Workspace</span>
3594
4522
  <button class="nav-sidebar-toggle" onclick="toggleNavSidebar()" title="Collapse sidebar">&#x25C0;</button>
3595
4523
  </div>
3596
4524
  <!-- Navigation -->
@@ -3646,8 +4574,16 @@
3646
4574
  <div class="nav-item" data-view="plan" onclick="switchView('plan')">
3647
4575
  <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
4576
  </div>
4577
+ </div>
4578
+ <div class="nav-sidebar-divider"></div>
4579
+ <!-- Governance -->
4580
+ <div class="nav-sidebar-section nav-sidebar-governance">
4581
+ <div class="nav-sidebar-label">GOVERNANCE</div>
3649
4582
  <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>
4583
+ <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">Policies</span>
4584
+ </div>
4585
+ <div class="nav-item" data-view="audit-log" onclick="switchView('audit-log')">
4586
+ <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 4v8h10V4H3zm2 2h6v1H5V6zm0 2h6v1H5V8z"/></svg></span><span class="nav-text">Audit Log</span>
3651
4587
  </div>
3652
4588
  </div>
3653
4589
  </nav>
@@ -3676,7 +4612,8 @@
3676
4612
  <div class="view-tab" id="tab-workflows" onclick="switchView('workflows')">Workflows</div>
3677
4613
  <div class="view-tab" id="tab-plan" onclick="switchView('plan')">Plan</div>
3678
4614
  <div class="view-tab" id="tab-launch" onclick="switchView('launch')">Launch</div>
3679
- <div class="view-tab" id="tab-rules" onclick="switchView('rules')">Rules</div>
4615
+ <div class="view-tab" id="tab-rules" onclick="switchView('rules')">Policies</div>
4616
+ <div class="view-tab" id="tab-audit-log" onclick="switchView('audit-log')">Audit Log</div>
3680
4617
  <div class="view-tab" id="tab-stats" onclick="switchView('stats')">Stats</div>
3681
4618
  <div class="view-tab" id="tab-docs" onclick="switchView('docs')">Docs</div>
3682
4619
  </div>
@@ -3705,9 +4642,13 @@
3705
4642
  <div class="plan-area" id="monitor-panel" style="display:none"></div>
3706
4643
  <div class="launch-area" id="launch-area"></div>
3707
4644
  <div class="rules-area" id="rules-area"></div>
4645
+ <div class="audit-log-area" id="audit-log-area" style="display:none; padding:16px 20px; overflow-y:auto; flex:1"></div>
3708
4646
  <div class="stats-area" id="stats-area"></div>
3709
4647
  <div class="docs-area" id="docs-area"><div id="docs-content"></div></div>
3710
- <button class="scroll-bottom" id="scroll-bottom" onclick="scrollToBottom()">&#x2193;<span class="new-count" id="new-msg-count" style="display:none">0</span></button>
4648
+ <button class="scroll-bottom" id="scroll-bottom" type="button" onclick="scrollToBottom()" aria-label="Jump to latest messages.">
4649
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
4650
+ <span id="new-msg-count" style="margin-right:4px">0</span> <span id="new-msg-label">new messages</span>
4651
+ </button>
3711
4652
  <div class="typing-bar" id="typing-bar"></div>
3712
4653
 
3713
4654
  <!-- MESSAGE INPUT -->
@@ -3733,16 +4674,48 @@
3733
4674
  <div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
3734
4675
  <div class="profile-popup-header">
3735
4676
  <img class="profile-popup-avatar" id="pp-avatar" src="data:," style="display:none">
3736
- <div>
3737
- <div class="profile-popup-name" id="pp-name"></div>
4677
+ <div style="flex:1;min-width:0">
4678
+ <div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
4679
+ <div class="profile-popup-name" id="pp-name"></div>
4680
+ <span id="pp-status-badge" class="pp-status-badge"></span>
4681
+ </div>
3738
4682
  <div class="profile-popup-id" id="pp-id"></div>
3739
4683
  </div>
3740
4684
  </div>
3741
- <div id="pp-role"></div>
3742
- <div class="profile-popup-bio" id="pp-bio"></div>
3743
- <div class="profile-popup-stats" id="pp-stats"></div>
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>
4685
+ <div class="pp-tabs">
4686
+ <button class="pp-tab active" data-tab="stats" onclick="switchPpTab('stats')">Stats</button>
4687
+ <button class="pp-tab" data-tab="actions" onclick="switchPpTab('actions')">Actions</button>
4688
+ <button class="pp-tab" data-tab="profile" onclick="switchPpTab('profile')">Profile</button>
4689
+ <button class="pp-tab" data-tab="terminal" onclick="switchPpTab('terminal', true)" style="display:none">Terminal</button>
4690
+ </div>
4691
+ <!-- Stats Tab -->
4692
+ <div class="pp-tab-content active" id="pp-tab-stats">
4693
+ <div id="pp-badges" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px"></div>
4694
+ <div class="profile-popup-stats" id="pp-stats"></div>
4695
+ <div id="pp-skills-readonly" style="margin-top:12px"></div>
4696
+ </div>
4697
+ <!-- Actions Tab -->
4698
+ <div class="pp-tab-content" id="pp-tab-actions">
4699
+ <div class="pp-actions-grid" id="pp-actions"></div>
4700
+ </div>
4701
+ <!-- Profile Tab -->
4702
+ <div class="pp-tab-content" id="pp-tab-profile">
4703
+ <div class="profile-popup-bio" id="pp-bio"></div>
4704
+ <div id="pp-skills-container" style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border)"></div>
4705
+ <button class="btn btn-primary" style="width:100%;margin-top:12px" id="pp-edit-btn" onclick="openProfileEditor()">Edit Profile</button>
4706
+ <button class="btn btn-danger" style="width:100%;margin-top:6px" id="pp-delete-btn" onclick="confirmDeleteAgent()">Delete Agent</button>
4707
+ </div>
4708
+ <!-- Terminal Tab -->
4709
+ <div class="pp-tab-content" id="pp-tab-terminal" style="display:none">
4710
+ <div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:6px">Live Terminal Bridge</div>
4711
+ <div class="terminal-wrapper">
4712
+ <div id="pp-terminal-container"></div>
4713
+ </div>
4714
+ <div style="font-size:10px;color:var(--text-muted);margin-top:8px;display:flex;justify-content:space-between">
4715
+ <span>Real-time agent stream</span>
4716
+ <span id="terminal-status">Ready</span>
4717
+ </div>
4718
+ </div>
3746
4719
  </div>
3747
4720
 
3748
4721
  <!-- Character Designer Panel -->
@@ -3790,6 +4763,12 @@
3790
4763
  <button class="cd-tab" onclick="cdSwitchTab('face', this)">Face</button>
3791
4764
  <button class="cd-tab" onclick="cdSwitchTab('outfit', this)">Outfit</button>
3792
4765
  <button class="cd-tab" onclick="cdSwitchTab('accessories', this)">Acc.</button>
4766
+ <button class="cd-tab" onclick="cdSwitchTab('stats', this)">Stats</button>
4767
+ </div>
4768
+
4769
+ <!-- Tab: Stats -->
4770
+ <div class="cd-tab-content" id="cd-tab-stats">
4771
+ <div id="pe-stats-container" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px;color:var(--text)"></div>
3793
4772
  </div>
3794
4773
 
3795
4774
  <!-- Tab: Body -->
@@ -4181,6 +5160,28 @@ function formatTime(ts) {
4181
5160
  return new Date(ts).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit', second:'2-digit' });
4182
5161
  }
4183
5162
 
5163
+ function formatShortTime(ts) {
5164
+ return new Date(ts).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
5165
+ }
5166
+
5167
+ /** Message card header: name, To line, clock time, badges (Stitch-style). */
5168
+ function buildMsgCardHead(fromDisplay, toDisplay, color, ts, badgesHtml, opt) {
5169
+ opt = opt || {};
5170
+ var showTo = opt.showTo !== false;
5171
+ var timeStr = formatShortTime(ts);
5172
+ var badges = badgesHtml || '';
5173
+ return '<div class="msg-card-head">' +
5174
+ '<div class="msg-card-ident">' +
5175
+ '<span class="msg-from" style="color:' + color + '">' + escapeHtml(fromDisplay) + '</span>' +
5176
+ (showTo ? '<div class="msg-to-line"><span class="msg-to-label">To</span> ' + escapeHtml(toDisplay) + '</div>' : '') +
5177
+ '</div>' +
5178
+ '<div class="msg-card-head-right">' +
5179
+ '<span class="msg-time" title="' + formatTime(ts) + '">' + timeStr + '</span>' +
5180
+ (badges ? '<div class="msg-badges">' + badges + '</div>' : '') +
5181
+ '</div>' +
5182
+ '</div>';
5183
+ }
5184
+
4184
5185
  function timeAgo(ts) {
4185
5186
  var secs = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
4186
5187
  if (secs < 5) return 'just now';
@@ -4372,12 +5373,13 @@ function renderTable(rows) {
4372
5373
 
4373
5374
  // ==================== AGENT MONITORING ====================
4374
5375
 
4375
- var PROVIDER_COLORS = { claude: '#d97706', anthropic: '#d97706', openai: '#10b981', gemini: '#3b82f6', google: '#3b82f6', cursor: '#6366f1' };
5376
+ /* Provider label colors: use design-system tokens (theme-aware). */
5377
+ var PROVIDER_COLOR_VARS = { claude: 'var(--nh-accent)', anthropic: 'var(--nh-accent)', openai: 'var(--nh-green)', gemini: 'var(--nh-blue)', google: 'var(--nh-blue)', cursor: 'var(--nh-purple)' };
4376
5378
 
4377
5379
  function getProviderBadge(provider) {
4378
5380
  if (!provider) return '';
4379
5381
  var key = provider.toLowerCase();
4380
- var c = PROVIDER_COLORS[key] || 'var(--text-muted)';
5382
+ var c = PROVIDER_COLOR_VARS[key] || 'var(--text-muted)';
4381
5383
  return '<span style="color:' + c + ';font-size:9px;font-weight:600;margin-right:4px">' + escapeHtml(provider) + '</span>';
4382
5384
  }
4383
5385
 
@@ -4423,7 +5425,7 @@ function renderAgents(agents) {
4423
5425
  }
4424
5426
 
4425
5427
  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('|');
5428
+ 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 || '') + ':' + (a.skills || []).join(','); }).join('|');
4427
5429
  var useGrouping = filtered.length > 6;
4428
5430
  var layoutHash = structuralPart + '|F:' + agentFilterSearch + ':' + agentFilterRole + ':' + agentFilterStatus + '|C:' + collapsedSig + '|G:' + (useGrouping ? '1' : '0');
4429
5431
 
@@ -4432,14 +5434,14 @@ function renderAgents(agents) {
4432
5434
  _lastAgentsHash = layoutHash;
4433
5435
  var alertEl0 = document.getElementById('alert-count');
4434
5436
  if (alertEl0) alertEl0.style.display = 'none';
4435
- updateInjectTargets(keys);
5437
+ updateInjectTargets(agents);
4436
5438
  return;
4437
5439
  }
4438
5440
 
4439
5441
  // Group by role
4440
5442
  var ROLE_ORDER = ['lead', 'backend', 'frontend', 'quality', 'monitor', 'advisor', ''];
4441
5443
  var ROLE_LABELS = { lead: 'Lead', backend: 'Backend', frontend: 'Frontend', quality: 'Quality', monitor: 'Monitor', advisor: 'Advisor', '': 'Unassigned' };
4442
- var ROLE_COLORS = { lead: 'var(--accent)', backend: '#f59e0b', frontend: '#8b5cf6', quality: 'var(--green)', monitor: 'var(--red,#ef4444)', advisor: 'var(--purple,#a855f7)', '': 'var(--text-muted)' };
5444
+ var ROLE_COLORS = { lead: 'var(--nh-accent)', backend: 'var(--nh-amber)', frontend: 'var(--nh-purple)', quality: 'var(--nh-green)', monitor: 'var(--nh-red)', advisor: 'var(--nh-purple)', '': 'var(--nh-text-muted)' };
4443
5445
  var roleGroups = {};
4444
5446
  for (var g = 0; g < filtered.length; g++) {
4445
5447
  var gname = filtered[g];
@@ -4457,13 +5459,24 @@ function renderAgents(agents) {
4457
5459
  var name = agentNames[i];
4458
5460
  var info = agents[name];
4459
5461
  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++;
5462
+ var state = info.status || (!info.alive ? 'offline' : info.is_listening ? 'listening' : 'working');
5463
+ if (state === 'idle' || state === 'stale') sleepCount++;
5464
+ var currentToolHtml = (state === 'working' && info.current_tool)
5465
+ ? '<div class="agent-current-tool">\u2699 ' + escapeHtml(info.current_tool) + '</div>'
5466
+ : '';
4462
5467
 
4463
5468
  // Compact activity text
4464
5469
  var activityText = '';
4465
- if (state === 'offline' || state === 'dead') {
5470
+ if (state === 'unknown') {
5471
+ activityText = 'never started';
5472
+ } else if (state === 'stale') {
5473
+ activityText = 'stale';
5474
+ } else if (state === 'offline' || state === 'dead') {
4466
5475
  activityText = 'offline';
5476
+ } else if (state === 'stuck') {
5477
+ activityText = '⚠ no listen()';
5478
+ } else if (state === 'unresponsive') {
5479
+ activityText = 'unresponsive';
4467
5480
  } else if (info.idle_seconds != null) {
4468
5481
  if (info.idle_seconds < 5) activityText = 'just now';
4469
5482
  else if (info.idle_seconds < 60) activityText = info.idle_seconds + 's ago';
@@ -4484,20 +5497,97 @@ function renderAgents(agents) {
4484
5497
  if (activityText) subtitleParts.push(activityText);
4485
5498
  var subtitleText = subtitleParts.join(' \u00B7 ');
4486
5499
 
5500
+ // Skills: limited to 3 for sidebar cards
5501
+ var skillsHtml = '';
5502
+ if (info.skills && info.skills.length > 0) {
5503
+ var platformClass = 'platform-' + (info.provider || 'unknown').toLowerCase();
5504
+ var platformSkills = info.platform_skills || [];
5505
+ skillsHtml = '<div class="agent-skills">';
5506
+ for (var s = 0; s < Math.min(info.skills.length, 3); s++) {
5507
+ var skill = info.skills[s];
5508
+ var isPlatform = platformSkills.indexOf(skill) !== -1;
5509
+ skillsHtml += '<span class="skill-tag' + (isPlatform ? ' ' + platformClass : '') + '">' + escapeHtml(skill) + '</span>';
5510
+ }
5511
+ if (info.skills.length > 3) {
5512
+ skillsHtml += '<span class="skill-tag" style="opacity:0.5">+' + (info.skills.length - 3) + '</span>';
5513
+ }
5514
+ skillsHtml += '</div>';
5515
+ }
5516
+
5517
+ // Nudge button (Bell icon)
5518
+ var nudgeBtnHtml = '';
5519
+ if (state !== 'offline' && state !== 'dead' && state !== 'unknown') {
5520
+ nudgeBtnHtml = '<div class="agent-nudge-btn" title="Nudge agent to listen()" onclick="sendNudge(\'' + escapeHtml(name) + '\', event)">' +
5521
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>' +
5522
+ '</div>';
5523
+ }
5524
+
5525
+ // Liveness Sparkline (Heartbeat dots)
5526
+ var sparklineHtml = '';
5527
+ if (state !== 'offline' && state !== 'dead' && state !== 'unknown') {
5528
+ var dots = '';
5529
+ var history = info.listen_history || [];
5530
+ // Fallback: synthesize from last_listen_call if listen_history is missing
5531
+ if (history.length === 0) {
5532
+ var llc = info.last_listen_call || info.last_listened_at;
5533
+ if (llc) {
5534
+ var llcTs = typeof llc === 'string' ? new Date(llc).getTime() : llc;
5535
+ if (llcTs > 0) history = [llcTs];
5536
+ }
5537
+ }
5538
+ for (var d = 0; d < 10; d++) {
5539
+ var dotClass = 'hb-dot';
5540
+ if (d < history.length) {
5541
+ var age = (Date.now() - history[d]) / 1000;
5542
+ if (age < 30) dotClass += ' active';
5543
+ else if (age < 120) dotClass += ' stale';
5544
+ } else {
5545
+ dotClass += ' empty';
5546
+ }
5547
+ dots += '<div class="' + dotClass + '"></div>';
5548
+ }
5549
+ sparklineHtml = '<div class="listen-sparkline"><span class="listen-sparkline-label">Liveness</span>' + dots + '</div>';
5550
+ }
5551
+
5552
+ var indicatorHtml = (state === 'listening' || state === 'working')
5553
+ ? '<span class="agent-state-pill ' + state + '">' + state + '</span>'
5554
+ : '<span class="agent-status-dot ' + state + '" title="' + state + '"></span>';
4487
5555
  cardHtml += '<div class="' + cardClass + '" data-agent="' + escapeHtml(name) + '" onclick="showProfilePopup(\'' + escapeHtml(name) + '\', event)">' +
5556
+ nudgeBtnHtml +
4488
5557
  '<div class="agent-top">' +
4489
5558
  avatarHtml +
4490
5559
  '<div class="agent-info">' +
4491
5560
  '<div class="agent-name" style="color:' + color + '">' + escapeHtml(displayName) + '</div>' +
4492
5561
  '<div class="agent-subtitle">' + escapeHtml(subtitleText) + '</div>' +
5562
+ currentToolHtml +
5563
+ skillsHtml +
5564
+ sparklineHtml +
4493
5565
  '</div>' +
4494
- '<span class="agent-status-dot ' + state + '" title="' + state + '"></span>' +
5566
+ indicatorHtml +
4495
5567
  '</div>' +
4496
5568
  '</div>';
4497
5569
  }
4498
5570
  return cardHtml;
4499
5571
  }
4500
5572
 
5573
+ function sendNudge(name, event) {
5574
+ if (event) event.stopPropagation();
5575
+ fetch('/api/inject', {
5576
+ method: 'POST',
5577
+ headers: { 'Content-Type': 'application/json' },
5578
+ body: JSON.stringify({
5579
+ to: name,
5580
+ content: '[NUDGE] Your neohive session appears stuck. Please call listen() now to process messages.',
5581
+ priority: 'critical'
5582
+ })
5583
+ })
5584
+ .then(r => r.json())
5585
+ .then(d => {
5586
+ if (d.success) showToast('Nudge sent to ' + name, 'success');
5587
+ else showToast('Failed to nudge: ' + (d.error || 'unknown error'), 'error');
5588
+ });
5589
+ }
5590
+
4501
5591
  if (useGrouping) {
4502
5592
  for (var r = 0; r < ROLE_ORDER.length; r++) {
4503
5593
  var role = ROLE_ORDER[r];
@@ -4531,9 +5621,13 @@ function renderAgents(agents) {
4531
5621
  var pCard = el.querySelector('[data-agent="' + pName + '"]');
4532
5622
  if (!pCard) { didPatch = false; break; }
4533
5623
  var pInfo = agents[pName];
4534
- var pState = !pInfo.alive ? 'offline' : pInfo.is_listening ? 'listening' : (pInfo.status === 'idle' || pInfo.status === 'sleeping') ? 'idle' : 'working';
5624
+ var pState = pInfo.status || (!pInfo.alive ? 'offline' : pInfo.is_listening ? 'listening' : 'working');
4535
5625
  var dot = pCard.querySelector('.agent-status-dot');
4536
5626
  if (dot) { dot.className = 'agent-status-dot ' + pState; dot.title = pState; }
5627
+ var pill = pCard.querySelector('.agent-state-pill');
5628
+ if (pill) { pill.className = 'agent-state-pill ' + pState; pill.textContent = pState; }
5629
+ var ctEl = pCard.querySelector('.agent-current-tool');
5630
+ if (ctEl) { ctEl.textContent = (pState === 'working' && pInfo.current_tool) ? '\u2699 ' + pInfo.current_tool : ''; }
4537
5631
  var sub = pCard.querySelector('.agent-subtitle');
4538
5632
  if (sub) {
4539
5633
  var parts = [];
@@ -4558,6 +5652,9 @@ function renderAgents(agents) {
4558
5652
  _lastAgentsHash = layoutHash;
4559
5653
  }
4560
5654
 
5655
+ // Update inject target dropdown — must run BEFORE the alertEl guard which may early-return
5656
+ updateInjectTargets(agents);
5657
+
4561
5658
  // Update alert badge
4562
5659
  var alertEl = document.getElementById('alert-count');
4563
5660
  if (!alertEl) return;
@@ -4567,9 +5664,6 @@ function renderAgents(agents) {
4567
5664
  } else {
4568
5665
  alertEl.style.display = 'none';
4569
5666
  }
4570
-
4571
- // Update inject target dropdown
4572
- updateInjectTargets(keys);
4573
5667
  }
4574
5668
 
4575
5669
  function sendNudge(agentName) {
@@ -4679,18 +5773,23 @@ function copyRespawnPrompt() {
4679
5773
 
4680
5774
  var lastAgentKeys = '';
4681
5775
 
4682
- function updateInjectTargets(keys) {
5776
+ function updateInjectTargets(agents) {
5777
+ // Build sorted list of real agents (exclude system/internal names)
5778
+ var keys = Object.keys(agents).filter(function(k) {
5779
+ return k !== '__system__' && k !== '__user__' && k !== 'Dashboard';
5780
+ }).sort();
4683
5781
  var joined = keys.join(',');
4684
5782
  if (joined === lastAgentKeys) return;
4685
5783
  lastAgentKeys = joined;
4686
5784
 
4687
5785
  var sel = document.getElementById('inject-target');
5786
+ if (!sel) return;
4688
5787
  var current = sel.value;
4689
5788
  sel.innerHTML = '<option value="">Select agent...</option>';
4690
5789
  for (var i = 0; i < keys.length; i++) {
4691
5790
  var opt = document.createElement('option');
4692
5791
  opt.value = keys[i];
4693
- opt.textContent = keys[i];
5792
+ opt.textContent = (agents[keys[i]] && agents[keys[i]].display_name) || keys[i];
4694
5793
  sel.appendChild(opt);
4695
5794
  }
4696
5795
  // Broadcast option
@@ -4782,6 +5881,33 @@ function renderThreads(messages) {
4782
5881
 
4783
5882
  // ==================== MESSAGES ====================
4784
5883
 
5884
+ /** Before replacing #messages innerHTML, remember which row was at the top of the viewport (stable vs raw scrollTop). */
5885
+ function captureMessageScrollAnchor(container) {
5886
+ if (!container) return null;
5887
+ var st = container.scrollTop;
5888
+ var nodes = container.querySelectorAll('.nh-msg-card[data-msg-id]');
5889
+ for (var i = 0; i < nodes.length; i++) {
5890
+ var n = nodes[i];
5891
+ var mid = n.getAttribute('data-msg-id');
5892
+ if (!mid) continue;
5893
+ if (n.offsetTop + n.offsetHeight > st + 0.5) {
5894
+ return { id: mid, delta: n.offsetTop - st };
5895
+ }
5896
+ }
5897
+ return null;
5898
+ }
5899
+
5900
+ function restoreMessageScrollAnchor(container, anchor) {
5901
+ if (!anchor || !container) return;
5902
+ var nodes = container.querySelectorAll('.nh-msg-card[data-msg-id]');
5903
+ for (var i = 0; i < nodes.length; i++) {
5904
+ if (nodes[i].getAttribute('data-msg-id') === anchor.id) {
5905
+ container.scrollTop = Math.max(0, nodes[i].offsetTop - anchor.delta);
5906
+ return;
5907
+ }
5908
+ }
5909
+ }
5910
+
4785
5911
  function renderMessages(messages) {
4786
5912
  var el = document.getElementById('messages');
4787
5913
 
@@ -4854,9 +5980,7 @@ function renderMessages(messages) {
4854
5980
  // Track new messages while scrolled up
4855
5981
  if (isNew && !autoScroll) {
4856
5982
  newWhileScrolled += (filtered.length - lastMessageCount);
4857
- var countEl = document.getElementById('new-msg-count');
4858
- countEl.textContent = newWhileScrolled;
4859
- countEl.style.display = '';
5983
+ updateNewMsgPill(newWhileScrolled);
4860
5984
  document.getElementById('scroll-bottom').classList.add('visible');
4861
5985
  }
4862
5986
  // Play sound if new messages arrived
@@ -4915,33 +6039,56 @@ function renderMessages(messages) {
4915
6039
  }
4916
6040
 
4917
6041
  var groupClass = isGrouped ? ' grouped' : '';
6042
+ var msgIdAttr = m.id ? ' data-msg-id="' + escapeHtml(m.id) + '"' : '';
4918
6043
 
4919
6044
  var badges = '';
4920
6045
  if (m.channel && m.channel !== 'general') badges += '<span class="badge-channel" onclick="filterChannel(\'' + escapeHtml(m.channel) + '\')" title="Channel: #' + escapeHtml(m.channel) + '">#' + escapeHtml(m.channel) + '</span>';
4921
6046
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
4922
6047
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
4923
- if (m.edited) badges += '<span class="badge" style="background:var(--orange-dim);color:var(--orange)" title="Edited ' + (m.edited_at ? new Date(m.edited_at).toLocaleString() : '') + '">edited</span>';
6048
+ if (m.edited) badges += '<span class="nh-pill nh-pill--review nh-pill--sm" title="Edited ' + (m.edited_at ? new Date(m.edited_at).toLocaleString() : '') + '">edited</span>';
4924
6049
 
4925
6050
  var msgAvatarHtml = getMsgAvatar(m.from, color);
4926
6051
 
4927
6052
  if (isSystem) {
4928
- html += '<div class="message system-msg' + newClass + '">' +
4929
- '<div class="msg-body">' +
4930
- '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4931
- '</div></div>';
6053
+ var evtType = 'status';
6054
+ var evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
6055
+ if (/\[CRITICAL\]/.test(m.content)) {
6056
+ evtType = 'critical';
6057
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
6058
+ } else if (/\[WARNING\]/.test(m.content)) {
6059
+ evtType = 'warning';
6060
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
6061
+ } else if (/\[VIOLATION\]/i.test(m.content)) {
6062
+ evtType = 'violation';
6063
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>';
6064
+ } else if (/\[EVENT\]/.test(m.content)) {
6065
+ evtType = 'event';
6066
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
6067
+ } else if (/\[REVIEW/.test(m.content)) {
6068
+ evtType = 'review';
6069
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
6070
+ } else if (/\[ROLE/.test(m.content)) {
6071
+ evtType = 'role';
6072
+ evtIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
6073
+ } else if (/\[STATUS\]/.test(m.content) || /\[QUALITY/.test(m.content)) {
6074
+ evtType = 'status';
6075
+ }
6076
+ var evtTime = new Date(m.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
6077
+ var evtText = m.content.replace(/^\[[\w\s]+\]\s*/, '');
6078
+ html += '<div class="message system-event-banner evt-' + evtType + newClass + '"' + msgIdAttr + '>' +
6079
+ '<span class="event-icon">' + evtIcon + '</span>' +
6080
+ '<div class="event-body"><span class="event-text">' + renderMarkdown(evtText) + '</span></div>' +
6081
+ '<span class="event-time">' + evtTime + '</span>' +
6082
+ '</div>';
4932
6083
  lastFrom = '';
4933
6084
  lastTo = '';
4934
6085
  } else if (m.type === 'handoff') {
4935
- html += '<div class="message handoff-msg' + newClass + '">' +
6086
+ var handoffBadges = '<span class="nh-pill nh-pill--review nh-pill--sm">Handoff</span>' + badges;
6087
+ html += '<div class="message nh-msg-card handoff-msg' + newClass + '"' + msgIdAttr + '>' +
4936
6088
  buildMsgActions(m.id) +
4937
6089
  msgAvatarHtml +
4938
6090
  '<div class="msg-body">' +
4939
- '<div class="handoff-banner">Handoff: ' + escapeHtml(fromDisplay) + ' &rarr; ' + escapeHtml(toDisplay) + '</div>' +
4940
- '<div class="msg-header">' +
4941
- '<span class="msg-from" style="color:' + color + '" title="@' + escapeHtml(m.from) + '">' + escapeHtml(fromDisplay) + '</span>' +
4942
- '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
4943
- '<div class="msg-badges">' + badges + '</div>' +
4944
- '</div>' +
6091
+ buildMsgCardHead(fromDisplay, toDisplay, color, m.timestamp, handoffBadges) +
4945
6092
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4946
6093
  buildReactionsHtml(m.id) +
4947
6094
  buildReadReceipts(m.id) +
@@ -4952,17 +6099,11 @@ function renderMessages(messages) {
4952
6099
  var fileMeta = m.file || {};
4953
6100
  var fileName = fileMeta.name || 'file';
4954
6101
  var fileSize = fileMeta.size ? (fileMeta.size > 1024 ? Math.round(fileMeta.size / 1024) + ' KB' : fileMeta.size + ' B') : '';
4955
- html += '<div class="message' + newClass + '">' +
6102
+ html += '<div class="message nh-msg-card' + newClass + '"' + msgIdAttr + '>' +
4956
6103
  buildMsgActions(m.id) +
4957
6104
  msgAvatarHtml +
4958
6105
  '<div class="msg-body">' +
4959
- '<div class="msg-header">' +
4960
- '<span class="msg-from" style="color:' + color + '" title="@' + escapeHtml(m.from) + '">' + escapeHtml(fromDisplay) + '</span>' +
4961
- '<span class="msg-arrow">&rarr;</span>' +
4962
- '<span class="msg-to">' + escapeHtml(toDisplay) + '</span>' +
4963
- '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
4964
- '<div class="msg-badges">' + badges + '</div>' +
4965
- '</div>' +
6106
+ buildMsgCardHead(fromDisplay, toDisplay, color, m.timestamp, badges) +
4966
6107
  '<div class="file-meta"><span class="file-icon" style="opacity:0.5">[ ]</span>' + escapeHtml(fileName) + '<span class="file-size">' + fileSize + '</span></div>' +
4967
6108
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4968
6109
  buildReactionsHtml(m.id) +
@@ -4972,18 +6113,12 @@ function renderMessages(messages) {
4972
6113
  lastTo = m.to;
4973
6114
  } else {
4974
6115
  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 + '">' +
6116
+ var replyBadge = isReplyToUser ? '<span class="nh-pill nh-pill--review nh-pill--sm">Reply to you</span>' : '';
6117
+ html += '<div class="message nh-msg-card' + newClass + groupClass + userMsgClass + '"' + msgIdAttr + '>' +
4977
6118
  buildMsgActions(m.id) +
4978
6119
  msgAvatarHtml +
4979
6120
  '<div class="msg-body">' +
4980
- '<div class="msg-header">' +
4981
- '<span class="msg-from" style="color:' + color + '" title="@' + escapeHtml(m.from) + '">' + escapeHtml(fromDisplay) + '</span>' +
4982
- '<span class="msg-arrow">&rarr;</span>' +
4983
- '<span class="msg-to">' + escapeHtml(toDisplay) + '</span>' +
4984
- '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
4985
- '<div class="msg-badges">' + replyBadge + badges + '</div>' +
4986
- '</div>' +
6121
+ buildMsgCardHead(fromDisplay, toDisplay, color, m.timestamp, replyBadge + badges) +
4987
6122
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
4988
6123
  buildReactionsHtml(m.id) +
4989
6124
  buildReadReceipts(m.id) +
@@ -5029,20 +6164,33 @@ function renderMessages(messages) {
5029
6164
  var prevTo = lastRenderedIds.length > 0 ? filtered[appendStart - 1].to : '';
5030
6165
  for (var ni = appendStart; ni < filtered.length; ni++) {
5031
6166
  var nm = filtered[ni];
5032
- var nColor = getColor(nm.from);
5033
- var nIsSystem = nm.system === true;
5034
- var nMsgDate = new Date(nm.timestamp).toLocaleDateString();
5035
- // Simplified: render each new message with full formatting
5036
- var nDisplayName = nm.from;
6167
+ var nColor = nm.from === '__user__' ? 'var(--accent)' : getColor(nm.from);
6168
+ var nFromDisp = nm.from === '__user__' ? 'You' : (cachedAgents[nm.from] && cachedAgents[nm.from].display_name) || nm.from;
6169
+ var nToDisp = nm.to === '__user__' ? 'You' : (cachedAgents[nm.to] && cachedAgents[nm.to].display_name) || nm.to;
5037
6170
  var nContent = renderMarkdown(nm.content);
5038
- var nTime = formatTime(nm.timestamp);
5039
- var nToLabel = nm.to === '__group__' ? '' : ' &rarr; ' + escapeHtml(nm.to);
5040
- newMsgHtml += '<div class="message message-new" data-id="' + nm.id + '">' +
5041
- '<div class="msg-body"><div class="msg-header">' +
5042
- '<span class="msg-from" style="color:' + nColor + '">' + escapeHtml(nDisplayName) + '</span>' +
5043
- '<span class="msg-to">' + nToLabel + '</span>' +
5044
- '<span class="msg-time">' + nTime + '</span>' +
5045
- '</div>' +
6171
+ if (nm.system === true) {
6172
+ var neType = 'status';
6173
+ var neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
6174
+ if (/\[CRITICAL\]/.test(nm.content)) { neType = 'critical'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'; }
6175
+ else if (/\[WARNING\]/.test(nm.content)) { neType = 'warning'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'; }
6176
+ else if (/\[VIOLATION\]/i.test(nm.content)) { neType = 'violation'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>'; }
6177
+ else if (/\[EVENT\]/.test(nm.content)) { neType = 'event'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; }
6178
+ else if (/\[REVIEW/.test(nm.content)) { neType = 'review'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'; }
6179
+ else if (/\[ROLE/.test(nm.content)) { neType = 'role'; neIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'; }
6180
+ var neTime = new Date(nm.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
6181
+ var neText = nm.content.replace(/^\[[\w\s]+\]\s*/, '');
6182
+ newMsgHtml += '<div class="message system-event-banner evt-' + neType + ' message-new" data-msg-id="' + escapeHtml(nm.id) + '">' +
6183
+ '<span class="event-icon">' + neIcon + '</span>' +
6184
+ '<div class="event-body"><span class="event-text">' + renderMarkdown(neText) + '</span></div>' +
6185
+ '<span class="event-time">' + neTime + '</span>' +
6186
+ '</div>';
6187
+ continue;
6188
+ }
6189
+ var nAvatar = getMsgAvatar(nm.from, nColor);
6190
+ newMsgHtml += '<div class="message nh-msg-card message-new" data-msg-id="' + escapeHtml(nm.id) + '">' +
6191
+ nAvatar +
6192
+ '<div class="msg-body">' +
6193
+ buildMsgCardHead(nFromDisp, nToDisp, nColor, nm.timestamp, '') +
5046
6194
  '<div class="msg-content">' + nContent + '</div>' +
5047
6195
  '</div></div>';
5048
6196
  }
@@ -5052,8 +6200,10 @@ function renderMessages(messages) {
5052
6200
  while (tempDiv.firstChild) el.appendChild(tempDiv.firstChild);
5053
6201
  }
5054
6202
  } else {
5055
- // Full re-render (filters active, messages changed, or first render)
6203
+ // Full re-render innerHTML clears the DOM; restore view via message anchor, not raw scrollTop
6204
+ var scrollAnchor = !autoScroll ? captureMessageScrollAnchor(el) : null;
5056
6205
  el.innerHTML = loadMoreHtml + html;
6206
+ if (scrollAnchor) restoreMessageScrollAnchor(el, scrollAnchor);
5057
6207
  }
5058
6208
 
5059
6209
  lastRenderedIds = currentIds;
@@ -5153,18 +6303,55 @@ function renderChannelBar(messages) {
5153
6303
 
5154
6304
  function toggleSidebar() {
5155
6305
  var sb = document.getElementById('sidebar') || document.getElementById('nav-sidebar');
5156
- if (sb) sb.classList.toggle('open');
6306
+ if (!sb) return;
6307
+ var isOpen = sb.classList.toggle('open');
6308
+ if (isOpen) {
6309
+ sb.style.display = 'flex';
6310
+ sb.style.width = 'var(--sidebar-w)';
6311
+ sb.style.minWidth = 'var(--sidebar-w)';
6312
+ sb.style.transform = 'translateX(0)';
6313
+ } else {
6314
+ sb.style.display = '';
6315
+ sb.style.width = '';
6316
+ sb.style.minWidth = '';
6317
+ sb.style.transform = '';
6318
+ }
5157
6319
  var ov = document.getElementById('sidebar-overlay');
5158
6320
  if (ov) ov.classList.toggle('open');
5159
6321
  }
5160
6322
 
5161
6323
  function closeSidebar() {
5162
6324
  var sb = document.getElementById('sidebar') || document.getElementById('nav-sidebar');
5163
- if (sb) sb.classList.remove('open');
6325
+ if (sb) {
6326
+ sb.classList.remove('open');
6327
+ sb.style.display = '';
6328
+ sb.style.width = '';
6329
+ sb.style.minWidth = '';
6330
+ sb.style.transform = '';
6331
+ }
5164
6332
  var ov = document.getElementById('sidebar-overlay');
5165
6333
  if (ov) ov.classList.remove('open');
5166
6334
  }
5167
6335
 
6336
+ function updateNewMsgPill(count) {
6337
+ count = Math.max(0, Number(count) || 0);
6338
+
6339
+ var scrollBtn = document.getElementById('scroll-bottom');
6340
+ var countEl = document.getElementById('new-msg-count');
6341
+ var labelEl = document.getElementById('new-msg-label');
6342
+ if (!scrollBtn || !countEl) return;
6343
+
6344
+ countEl.textContent = count;
6345
+ if (labelEl) labelEl.textContent = count === 1 ? 'new message' : 'new messages';
6346
+
6347
+ if (count > 0) {
6348
+ var label = count === 1 ? 'new message' : 'new messages';
6349
+ scrollBtn.setAttribute('aria-label', 'Jump to latest messages. ' + count + ' ' + label + '.');
6350
+ } else {
6351
+ scrollBtn.setAttribute('aria-label', 'Jump to latest messages.');
6352
+ }
6353
+ }
6354
+
5168
6355
  var newWhileScrolled = 0;
5169
6356
 
5170
6357
  document.getElementById('messages').addEventListener('scroll', function() {
@@ -5174,9 +6361,9 @@ document.getElementById('messages').addEventListener('scroll', function() {
5174
6361
  if (atBottom) {
5175
6362
  scrollBtn.classList.remove('visible');
5176
6363
  newWhileScrolled = 0;
5177
- document.getElementById('new-msg-count').style.display = 'none';
6364
+ updateNewMsgPill(0);
5178
6365
  } else {
5179
- scrollBtn.classList.add('visible');
6366
+ if (newWhileScrolled > 0) scrollBtn.classList.add('visible');
5180
6367
  }
5181
6368
  });
5182
6369
 
@@ -5185,8 +6372,8 @@ function scrollToBottom() {
5185
6372
  el.scrollTop = el.scrollHeight;
5186
6373
  autoScroll = true;
5187
6374
  newWhileScrolled = 0;
6375
+ updateNewMsgPill(0);
5188
6376
  document.getElementById('scroll-bottom').classList.remove('visible');
5189
- document.getElementById('new-msg-count').style.display = 'none';
5190
6377
  }
5191
6378
 
5192
6379
  // ==================== COMPACT MODE ====================
@@ -5418,10 +6605,12 @@ function renderPinnedMessages() {
5418
6605
  var m = cachedHistory[i];
5419
6606
  if (!pins[m.id]) continue;
5420
6607
  var color = getColor(m.from);
5421
- html += '<div class="message">' +
5422
- '<div class="msg-avatar" style="background:' + color + ';width:20px;height:20px;font-size:9px">' + initial(m.from) + '</div>' +
6608
+ var pFrom = (cachedAgents[m.from] && cachedAgents[m.from].display_name) || m.from;
6609
+ var pTo = (cachedAgents[m.to] && cachedAgents[m.to].display_name) || m.to;
6610
+ html += '<div class="message nh-msg-card">' +
6611
+ '<div class="msg-avatar" style="background:' + color + ';width:24px;height:24px;font-size:9px">' + initial(m.from) + '</div>' +
5423
6612
  '<div class="msg-body">' +
5424
- '<div class="msg-header"><span class="msg-from" style="color:' + color + ';font-size:11px">' + escapeHtml(m.from) + '</span></div>' +
6613
+ buildMsgCardHead(pFrom, pTo, color, m.timestamp, '') +
5425
6614
  '<div class="msg-content">' + escapeHtml(m.content.substring(0, 120)) + (m.content.length > 120 ? '...' : '') + '</div>' +
5426
6615
  '</div></div>';
5427
6616
  }
@@ -5897,7 +7086,11 @@ function exportJSON() {
5897
7086
 
5898
7087
  // ==================== VIEW SWITCHING ====================
5899
7088
 
5900
- var activeView = localStorage.getItem('neohive_activeView') || 'overview';
7089
+ var activeView = 'overview';
7090
+ try {
7091
+ var _savedView = localStorage.getItem('neohive_activeView');
7092
+ if (_savedView) activeView = _savedView;
7093
+ } catch (e) { /* Safari / private mode: storage can throw; keep default */ }
5901
7094
 
5902
7095
  function switchView(view) {
5903
7096
  activeView = view;
@@ -5905,6 +7098,7 @@ function switchView(view) {
5905
7098
  // Reset render hashes so switching tabs forces a fresh render
5906
7099
  _lastTasksHash = '';
5907
7100
  _lastAgentsHash = '';
7101
+ if (view === 'messages') lastAgentKeys = '';
5908
7102
  if (renderOverview._lastHash) renderOverview._lastHash = '';
5909
7103
  // Update nav sidebar active state
5910
7104
  var navItems = document.querySelectorAll('.nav-item');
@@ -5912,7 +7106,7 @@ function switchView(view) {
5912
7106
  navItems[i].classList.toggle('active', navItems[i].getAttribute('data-view') === view);
5913
7107
  }
5914
7108
  // Update old tab active states (kept for compatibility)
5915
- var tabIds = ['messages','tasks','workspaces','workflows','plan','launch','rules','stats','docs'];
7109
+ var tabIds = ['messages','tasks','workspaces','workflows','plan','launch','rules','audit-log','stats','docs'];
5916
7110
  for (var t = 0; t < tabIds.length; t++) {
5917
7111
  var el = document.getElementById('tab-' + tabIds[t]);
5918
7112
  if (el) el.classList.toggle('active', view === tabIds[t]);
@@ -5927,6 +7121,10 @@ function switchView(view) {
5927
7121
  document.getElementById('monitor-panel').style.display = view === 'plan' ? 'block' : 'none';
5928
7122
  document.getElementById('launch-area').classList.toggle('visible', view === 'launch');
5929
7123
  document.getElementById('rules-area').classList.toggle('visible', view === 'rules');
7124
+ if (document.getElementById('audit-log-area')) {
7125
+ document.getElementById('audit-log-area').classList.toggle('visible', view === 'audit-log');
7126
+ document.getElementById('audit-log-area').style.display = view === 'audit-log' ? 'block' : 'none';
7127
+ }
5930
7128
  document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
5931
7129
  document.getElementById('docs-area').classList.toggle('visible', view === 'docs');
5932
7130
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
@@ -5942,6 +7140,7 @@ function switchView(view) {
5942
7140
  if (view === 'docs') renderDocs();
5943
7141
  if (view === 'launch') renderLaunchPanel();
5944
7142
  if (view === 'rules') fetchRules();
7143
+ if (view === 'audit-log') fetchAuditLog();
5945
7144
  if (view === 'stats') fetchStats();
5946
7145
  if (isMobile) closeSidebar();
5947
7146
  }
@@ -5999,7 +7198,7 @@ function renderTasks() {
5999
7198
  if (tasksHash === _lastTasksHash) return;
6000
7199
  _lastTasksHash = tasksHash;
6001
7200
 
6002
- var groups = { pending: [], in_progress: [], in_review: [], done: [], blocked: [] };
7201
+ var groups = { pending: [], in_progress: [], in_review: [], done: [], blocked: [], blocked_permanent: [] };
6003
7202
  for (var i = 0; i < cachedTasks.length; i++) {
6004
7203
  var t = cachedTasks[i];
6005
7204
  var status = t.status || 'pending';
@@ -6012,7 +7211,8 @@ function renderTasks() {
6012
7211
  { key: 'in_progress', label: 'In Progress' },
6013
7212
  { key: 'in_review', label: 'In Review' },
6014
7213
  { key: 'done', label: 'Done' },
6015
- { key: 'blocked', label: 'Blocked' }
7214
+ { key: 'blocked', label: 'Blocked' },
7215
+ { key: 'blocked_permanent', label: '⛔ Needs Intervention' }
6016
7216
  ];
6017
7217
 
6018
7218
  var html = '<div class="kanban">';
@@ -6051,6 +7251,7 @@ function buildTaskCard(t) {
6051
7251
 
6052
7252
  var badgesHtml = '';
6053
7253
  if (t.escalated_at) badgesHtml += '<span style="font-size:9px;padding:1px 5px;border-radius:6px;font-weight:600;background:var(--red-dim);color:var(--red)">ESCALATED</span>';
7254
+ if (t.retry_count > 0) badgesHtml += '<span style="font-size:9px;padding:1px 5px;border-radius:6px;font-weight:600;background:var(--yellow)20;color:var(--yellow)" title="Reclaimed ' + t.retry_count + ' time(s) by watchdog">↺' + t.retry_count + '</span>';
6054
7255
  if (t.channel) badgesHtml += '<span class="badge-channel" onclick="filterChannel(\'' + escapeHtml(t.channel) + '\')">#' + escapeHtml(t.channel) + '</span>';
6055
7256
 
6056
7257
  var notesHtml = '';
@@ -6059,9 +7260,14 @@ function buildTaskCard(t) {
6059
7260
  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>';
6060
7261
  }
6061
7262
 
7263
+ var blockedReasonHtml = (t.status === 'blocked_permanent' && t.blocked_reason)
7264
+ ? '<div style="font-size:10px;color:var(--red);margin-top:4px;font-style:italic">' + escapeHtml(t.blocked_reason) + '</div>'
7265
+ : '';
7266
+
6062
7267
  return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)" onclick="showTaskDetail(\'' + t.id + '\')">' +
6063
7268
  '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + (badgesHtml ? ' ' + badgesHtml : '') + '</div>' +
6064
7269
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
7270
+ blockedReasonHtml +
6065
7271
  notesHtml +
6066
7272
  '<div class="task-footer">' +
6067
7273
  assigneeHtml +
@@ -6088,7 +7294,7 @@ function showTaskDetail(taskId) {
6088
7294
  if (!task) return;
6089
7295
  var el = document.getElementById('task-detail-content');
6090
7296
  if (!el) return;
6091
- var statusColors = { pending: 'var(--yellow)', in_progress: 'var(--accent)', in_review: 'var(--purple)', done: 'var(--green)', blocked: 'var(--red)' };
7297
+ var statusColors = { pending: 'var(--yellow)', in_progress: 'var(--accent)', in_review: 'var(--purple)', done: 'var(--green)', blocked: 'var(--red)', blocked_permanent: 'var(--red)' };
6092
7298
  var statusColor = statusColors[task.status] || 'var(--text-muted)';
6093
7299
 
6094
7300
  var html = '';
@@ -6271,7 +7477,7 @@ function onKanbanDrop(e) {
6271
7477
 
6272
7478
  // ==================== STATS VIEW ====================
6273
7479
 
6274
- var AGENT_COLORS = ['#f59e0b', '#f97316', '#7ee787', '#d29922', '#ffa657', '#14b8a6', '#f778ba', '#56d364'];
7480
+ var AGENT_COLORS = ['var(--nh-accent)', 'var(--nh-orange)', 'var(--nh-green)', 'var(--nh-blue)', 'var(--nh-purple)', 'var(--nh-red)', 'var(--nh-amber)', 'var(--nh-yellow)'];
6275
7481
 
6276
7482
  function fetchStats() {
6277
7483
  var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
@@ -6313,7 +7519,7 @@ function renderStats(data, tasks, workflows) {
6313
7519
  var ts = tasks[ti].status;
6314
7520
  if (ts === 'done') tasksDone++;
6315
7521
  else if (ts === 'in_progress' || ts === 'in_review') tasksActive++;
6316
- else if (ts === 'blocked') tasksBlocked++;
7522
+ else if (ts === 'blocked' || ts === 'blocked_permanent') tasksBlocked++;
6317
7523
  else tasksPending++;
6318
7524
  }
6319
7525
 
@@ -6464,6 +7670,7 @@ function renderInsights(decisions, history) {
6464
7670
  // ==================== RULES TAB ====================
6465
7671
 
6466
7672
  var cachedRules = [];
7673
+ var cachedAuditLog = [];
6467
7674
 
6468
7675
  function fetchRules() {
6469
7676
  var pq = projectParam();
@@ -6473,47 +7680,101 @@ function fetchRules() {
6473
7680
  }).catch(function() { cachedRules = []; renderRules(); });
6474
7681
  }
6475
7682
 
7683
+ function fetchAuditLog() {
7684
+ var pq = projectParam();
7685
+ lttFetch('/api/audit-log' + pq).then(function(r) { return r.json(); }).then(function(log) {
7686
+ cachedAuditLog = Array.isArray(log) ? log : [];
7687
+ renderAuditLog();
7688
+ }).catch(function() { cachedAuditLog = []; renderAuditLog(); });
7689
+ }
7690
+
6476
7691
  function renderRules() {
6477
7692
  var el = document.getElementById('rules-area');
6478
- var html = '<h3 style="font-size:16px;font-weight:700;margin-bottom:16px">Project Rules</h3>';
6479
-
6480
- // Add form
6481
- html += '<div class="rule-add-form">' +
6482
- '<textarea id="new-rule-text" placeholder="Enter a new rule for all agents to follow..."></textarea>' +
6483
- '<div class="rule-form-row">' +
6484
- '<select id="new-rule-category">' +
7693
+ var html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3 style="font-size:16px;font-weight:700">Governance Policies</h3></div>';
7694
+
7695
+ // No-code Rule Builder Form
7696
+ html += '<div class="rule-add-form" style="background:var(--nh-glass-bg);backdrop-filter:blur(var(--nh-blur));border:1px solid var(--nh-glass-border);padding:16px;border-radius:12px;margin-bottom:20px">' +
7697
+ '<h4 style="font-size:13px;font-weight:700;color:var(--text);margin-bottom:12px">CREATE NEW POLICY</h4>' +
7698
+ '<div style="display:flex;gap:12px;margin-bottom:12px">' +
7699
+ '<div style="flex:1">' +
7700
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">WHEN (TRIGGER)</label>' +
7701
+ '<select id="builder-trigger" style="width:100%;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:6px">' +
7702
+ '<option value="Anytime">Anytime</option>' +
7703
+ '<option value="Before executing a workflow step">Before workflow step</option>' +
7704
+ '<option value="After executing a workflow step">After workflow step</option>' +
7705
+ '<option value="When writing a file">When writing a file</option>' +
7706
+ '<option value="When sending a message">When sending a message</option>' +
7707
+ '</select>' +
7708
+ '</div>' +
7709
+ '<div style="flex:2">' +
7710
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">IF (CONDITION)</label>' +
7711
+ '<input id="builder-condition" type="text" placeholder="e.g. the file ends with .js (optional)" style="width:100%;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:6px">' +
7712
+ '</div>' +
7713
+ '</div>' +
7714
+ '<div style="margin-bottom:16px">' +
7715
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">THEN (ACTION)</label>' +
7716
+ '<textarea id="builder-action" placeholder="What must the agent do? e.g. Ensure strict type checking is added." style="width:100%;min-height:50px;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:6px"></textarea>' +
7717
+ '</div>' +
7718
+ '<div style="display:flex;gap:12px;margin-bottom:16px;background:var(--surface-3);padding:10px;border-radius:8px;border:1px dashed var(--border)">' +
7719
+ '<div style="flex:1">' +
7720
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">SCOPE: ROLE</label>' +
7721
+ '<input id="builder-scope-role" type="text" placeholder="e.g. quality" style="width:100%;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:6px;border-radius:6px;font-size:12px">' +
7722
+ '</div>' +
7723
+ '<div style="flex:1">' +
7724
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">SCOPE: PROVIDER</label>' +
7725
+ '<input id="builder-scope-provider" type="text" placeholder="e.g. claude" style="width:100%;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:6px;border-radius:6px;font-size:12px">' +
7726
+ '</div>' +
7727
+ '<div style="flex:1">' +
7728
+ '<label style="font-size:10px;color:var(--text-muted);display:block;margin-bottom:4px;font-weight:600;letter-spacing:0.5px">SCOPE: AGENT</label>' +
7729
+ '<input id="builder-scope-agent" type="text" placeholder="e.g. Victor" style="width:100%;background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:6px;border-radius:6px;font-size:12px">' +
7730
+ '</div>' +
7731
+ '</div>' +
7732
+ '<div class="rule-form-row" style="display:flex;gap:10px;align-items:center">' +
7733
+ '<select id="new-rule-category" style="background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:6px;border-radius:6px">' +
6485
7734
  '<option value="general">General</option>' +
6486
7735
  '<option value="safety">Safety</option>' +
6487
7736
  '<option value="workflow">Workflow</option>' +
6488
7737
  '<option value="coding">Coding Standards</option>' +
6489
7738
  '<option value="forbidden">Forbidden Actions</option>' +
6490
7739
  '</select>' +
6491
- '<select id="new-rule-priority">' +
7740
+ '<select id="new-rule-priority" style="background:var(--surface-2);color:var(--text);border:1px solid var(--border);padding:6px;border-radius:6px">' +
6492
7741
  '<option value="normal">Normal</option>' +
6493
7742
  '<option value="important">Important</option>' +
6494
7743
  '<option value="critical">Critical</option>' +
6495
7744
  '</select>' +
6496
- '<button class="btn btn-primary" onclick="addRule()" style="margin-left:auto">Add Rule</button>' +
7745
+ '<button class="btn btn-primary" onclick="addRule()" style="margin-left:auto;background:var(--gradient-accent);color:#fff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-weight:600">Save Policy</button>' +
6497
7746
  '</div>' +
6498
7747
  '</div>';
6499
7748
 
6500
7749
  if (!cachedRules.length) {
6501
- html += '<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px 0">No rules defined yet. Add rules above — agents will see them in their guide text automatically.</div>';
7750
+ html += '<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px 0;background:var(--nh-glass-bg);backdrop-filter:blur(var(--nh-blur));border:1px solid var(--nh-glass-border);border-radius:12px">No policies defined yet.</div>';
6502
7751
  } else {
6503
7752
  for (var i = 0; i < cachedRules.length; i++) {
6504
7753
  var r = cachedRules[i];
6505
7754
  var cardClass = 'rule-card' + (r.active === false ? ' inactive' : '') + (r.priority === 'critical' ? ' critical' : r.priority === 'important' ? ' important' : '');
6506
- html += '<div class="' + cardClass + '">' +
7755
+ var scopeObj = {};
7756
+ if (r.scope_role) scopeObj.Role = r.scope_role;
7757
+ if (r.scope_provider) scopeObj.Provider = r.scope_provider;
7758
+ if (r.scope_agent) scopeObj.Agent = r.scope_agent;
7759
+ var scopeKeys = Object.keys(scopeObj);
7760
+ var scopeStr = scopeKeys.length > 0
7761
+ ? scopeKeys.map(function(k) { return k + ':' + scopeObj[k]; }).join(', ')
7762
+ : escapeHtml(r.scope || 'global');
7763
+ var hasScope = scopeKeys.length > 0 || (r.scope && r.scope !== 'global');
7764
+ var scopeBadge = hasScope ? '<span style="font-size:9px;padding:2px 6px;border-radius:4px;background:var(--surface-3);color:var(--text-dim);border:1px solid var(--border);margin-right:6px;text-transform:uppercase" title="Scope">' + escapeHtml(scopeStr) + '</span>' : '';
7765
+
7766
+ html += '<div class="' + cardClass + '" style="background:var(--nh-glass-bg);backdrop-filter:blur(var(--nh-blur));border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:12px;display:flex;align-items:center">' +
6507
7767
  '<div style="flex:1">' +
6508
- '<div class="rule-text">' + escapeHtml(r.text) + '</div>' +
7768
+ '<div class="rule-text" style="font-size:14px;color:var(--text);margin-bottom:6px">' + escapeHtml(r.text) + '</div>' +
6509
7769
  '<div style="display:flex;gap:6px;align-items:center;margin-top:4px">' +
6510
- '<span class="rule-category">' + escapeHtml(r.category || 'general') + '</span>' +
7770
+ scopeBadge +
7771
+ '<span class="rule-category" style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--purple-dim);color:var(--purple)">' + escapeHtml(r.category || 'general') + '</span>' +
6511
7772
  '<span style="font-size:9px;color:var(--text-muted)">by ' + escapeHtml(r.created_by || '?') + ' &middot; ' + timeAgo(r.created_at) + '</span>' +
6512
7773
  '</div>' +
6513
7774
  '</div>' +
6514
- '<div class="rule-actions">' +
6515
- '<button onclick="toggleRule(\'' + r.id + '\',' + (r.active === false ? 'true' : 'false') + ')">' + (r.active === false ? 'Enable' : 'Disable') + '</button>' +
6516
- '<button onclick="deleteRule(\'' + r.id + '\')" style="color:var(--red,#ef4444)">Delete</button>' +
7775
+ '<div class="rule-actions" style="display:flex;gap:8px;align-items:center">' +
7776
+ '<button class="btn" style="padding:4px 8px;font-size:11px;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:4px;cursor:pointer" onclick="toggleRule(\'' + r.id + '\',' + (r.active === false ? 'true' : 'false') + ')">' + (r.active === false ? 'Enable' : 'Disable') + '</button>' +
7777
+ '<button class="btn" style="padding:4px 8px;font-size:11px;color:var(--red,#ef4444);background:transparent;border:none;cursor:pointer" onclick="deleteRule(\'' + r.id + '\')">Delete</button>' +
6517
7778
  '</div>' +
6518
7779
  '</div>';
6519
7780
  }
@@ -6522,19 +7783,74 @@ function renderRules() {
6522
7783
  el.innerHTML = html;
6523
7784
  }
6524
7785
 
7786
+ function renderAuditLog() {
7787
+ var el = document.getElementById('audit-log-area');
7788
+ if (!el) return;
7789
+ var html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3 style="font-size:16px;font-weight:700">Governance Audit Log</h3></div>';
7790
+
7791
+ if (!cachedAuditLog.length) {
7792
+ html += '<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px 0;background:var(--nh-glass-bg);backdrop-filter:blur(var(--nh-blur));border:1px solid var(--nh-glass-border);border-radius:12px">No enforcement events recorded yet.</div>';
7793
+ } else {
7794
+ html += '<div style="background:var(--nh-glass-bg);backdrop-filter:blur(var(--nh-blur));border:1px solid var(--border);border-radius:12px;overflow:hidden">';
7795
+ for (var i = 0; i < cachedAuditLog.length; i++) {
7796
+ var log = cachedAuditLog[i];
7797
+ var isViolation = log.type === 'violation' || log.event === 'violation' || log.status === 'violation';
7798
+ var icon = isViolation ? '<span style="color:var(--red)">&#x26A0;</span>' : '<span style="color:var(--green)">&#x2713;</span>';
7799
+ var borderLeft = isViolation ? 'border-left:3px solid var(--red)' : 'border-left:3px solid transparent';
7800
+
7801
+ html += '<div style="padding:12px 16px;border-bottom:1px solid var(--border);display:flex;gap:12px;align-items:flex-start;' + borderLeft + '">' +
7802
+ '<div style="font-size:16px;margin-top:2px">' + icon + '</div>' +
7803
+ '<div style="flex:1">' +
7804
+ '<div style="font-size:13px;color:var(--text);margin-bottom:4px"><strong>' + escapeHtml(log.agent || 'System') + '</strong>: ' + escapeHtml(log.description || log.message || '') + '</div>' +
7805
+ '<div style="font-size:11px;color:var(--text-muted)">' + timeAgo(log.timestamp) + (log.rule_id ? ' &middot; Rule ' + escapeHtml(log.rule_id) : '') + '</div>' +
7806
+ '</div>' +
7807
+ '</div>';
7808
+ }
7809
+ html += '</div>';
7810
+ }
7811
+ el.innerHTML = html;
7812
+ }
7813
+
6525
7814
  function addRule() {
6526
- var text = document.getElementById('new-rule-text').value.trim();
6527
- if (!text) return;
7815
+ var trigger = document.getElementById('builder-trigger').value;
7816
+ var condition = document.getElementById('builder-condition').value.trim();
7817
+ var action = document.getElementById('builder-action').value.trim();
7818
+
7819
+ if (!action) {
7820
+ alert("Action field is required to create a rule.");
7821
+ return;
7822
+ }
7823
+
7824
+ var text = "";
7825
+ if (trigger === "Anytime") {
7826
+ text = condition ? "If " + condition + ", then " + action : action;
7827
+ } else {
7828
+ text = trigger + (condition ? " and " + condition : "") + ", then " + action;
7829
+ }
7830
+
7831
+ var sRole = document.getElementById('builder-scope-role').value.trim();
7832
+ var sProv = document.getElementById('builder-scope-provider').value.trim();
7833
+ var sAgent = document.getElementById('builder-scope-agent').value.trim();
7834
+ var scopeObj = {};
7835
+ if (sRole) scopeObj.role = sRole;
7836
+ if (sProv) scopeObj.provider = sProv;
7837
+ if (sAgent) scopeObj.agent = sAgent;
7838
+ var scope = Object.keys(scopeObj).length > 0 ? scopeObj : 'global';
7839
+
6528
7840
  var category = document.getElementById('new-rule-category').value;
6529
7841
  var priority = document.getElementById('new-rule-priority').value;
6530
7842
  var pq = projectParam();
6531
7843
  lttFetch('/api/rules' + pq, {
6532
7844
  method: 'POST',
6533
7845
  headers: { 'Content-Type': 'application/json' },
6534
- body: JSON.stringify({ action: 'add', text: text, category: category, priority: priority, created_by: 'Dashboard' })
7846
+ body: JSON.stringify({ action: 'add', text: text, category: category, priority: priority, scope: scope, created_by: 'Dashboard' })
6535
7847
  }).then(function(r) { return r.json(); }).then(function(res) {
6536
7848
  if (res.success) {
6537
- document.getElementById('new-rule-text').value = '';
7849
+ document.getElementById('builder-condition').value = '';
7850
+ document.getElementById('builder-action').value = '';
7851
+ document.getElementById('builder-scope-role').value = '';
7852
+ document.getElementById('builder-scope-provider').value = '';
7853
+ document.getElementById('builder-scope-agent').value = '';
6538
7854
  fetchRules();
6539
7855
  }
6540
7856
  }).catch(function(e) { console.error('Add rule failed:', e); });
@@ -6603,6 +7919,181 @@ function getMsgAvatar(name, color) {
6603
7919
 
6604
7920
  // ==================== v3.0: PROFILE POPUP ====================
6605
7921
 
7922
+ function switchPpTab(tabName, forceInit) {
7923
+ var popup = document.getElementById('profile-popup');
7924
+ var agentName = popup.dataset.agent || editingAgent;
7925
+
7926
+ var tabs = document.querySelectorAll('.pp-tab');
7927
+ var contents = document.querySelectorAll('.pp-tab-content');
7928
+
7929
+ tabs.forEach(function(t) { t.classList.toggle('active', t.dataset.tab === tabName); });
7930
+ contents.forEach(function(c) { c.classList.toggle('active', c.id === 'pp-tab-' + tabName); });
7931
+
7932
+ activePpTab = tabName;
7933
+ if (tabName === 'terminal' || forceInit) {
7934
+ if (agentName) initTerminal(agentName);
7935
+ }
7936
+ }
7937
+
7938
+ var _xtermLoadPromise = null;
7939
+ function loadXterm() {
7940
+ if (_xtermLoadPromise) return _xtermLoadPromise;
7941
+ _xtermLoadPromise = new Promise(function(resolve, reject) {
7942
+ console.log('[Neohive] Lazy-loading xterm.js dependencies...');
7943
+ var css = document.createElement('link');
7944
+ css.rel = 'stylesheet';
7945
+ css.href = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css';
7946
+ document.head.appendChild(css);
7947
+
7948
+ var s1 = document.createElement('script');
7949
+ s1.src = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js';
7950
+ s1.onload = function() {
7951
+ var s2 = document.createElement('script');
7952
+ s2.src = 'https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js';
7953
+ s2.onload = function() {
7954
+ console.log('[Neohive] xterm.js loaded successfully.');
7955
+ resolve();
7956
+ };
7957
+ s2.onerror = reject;
7958
+ document.head.appendChild(s2);
7959
+ };
7960
+ s1.onerror = reject;
7961
+ document.head.appendChild(s1);
7962
+ });
7963
+ return _xtermLoadPromise;
7964
+ }
7965
+
7966
+ var terminalInstances = new Map(); // agentName -> { term: Terminal, fit: FitAddon, lastMsgId: string }
7967
+
7968
+ async function initTerminal(agentName) {
7969
+ var container = document.getElementById('pp-terminal-container');
7970
+ if (!container || !agentName) return;
7971
+
7972
+ // Clear previous terminal output
7973
+ container.innerHTML = '<div style="padding:20px; color:var(--nh-text-muted); font-family:monospace;">Connecting terminal stream...</div>';
7974
+
7975
+ try {
7976
+ await loadXterm();
7977
+ } catch (err) {
7978
+ console.error('[Neohive] Failed to load xterm.js:', err);
7979
+ container.innerHTML = '<div style="padding:20px; color:var(--nh-red); font-family:monospace;">Terminal unavailable — check network connection.</div>';
7980
+ return;
7981
+ }
7982
+
7983
+ container.innerHTML = '';
7984
+
7985
+ if (terminalInstances.has(agentName)) {
7986
+ var inst = terminalInstances.get(agentName);
7987
+ inst.term.open(container);
7988
+ if (inst.fit) {
7989
+ setTimeout(function() { inst.fit.fit(); }, 50);
7990
+ }
7991
+ return inst.term;
7992
+ }
7993
+
7994
+ var term = new Terminal({
7995
+ cursorBlink: true,
7996
+ theme: { background: '#111827', cursor: '#fbbf24' },
7997
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
7998
+ fontSize: 12
7999
+ });
8000
+ var fit = new FitAddon.FitAddon();
8001
+ term.loadAddon(fit);
8002
+
8003
+ // Ensure DOM is ready
8004
+ setTimeout(function() {
8005
+ term.open(container);
8006
+ fit.fit();
8007
+ terminalInstances.set(agentName, { term: term, fit: fit, lastMsgId: null });
8008
+
8009
+ term.writeln('\x1b[33m[Neohive]\x1b[0m Connecting terminal stream for \x1b[32m' + agentName + '\x1b[0m...');
8010
+
8011
+ // Real Stream: sync current history immediately
8012
+ syncTerminalsFromHistory(agentName, term);
8013
+ }, 50);
8014
+
8015
+ return term;
8016
+ }
8017
+
8018
+ function syncTerminalsFromHistory(targetAgent, specificTerm) {
8019
+ // If targetAgent provided, sync only that one, else sync all open instances
8020
+ var targets = targetAgent ? [targetAgent] : Array.from(terminalInstances.keys());
8021
+
8022
+ targets.forEach(function(agent) {
8023
+ var inst = terminalInstances.get(agent);
8024
+ if (!inst) return;
8025
+ var term = specificTerm || inst.term;
8026
+
8027
+ // Find messages from this agent starting with [terminal:
8028
+ // cachedHistory is global, oldest to newest
8029
+ cachedHistory.forEach(function(msg) {
8030
+ if (msg.from === agent && msg.content && msg.content.startsWith('[terminal:')) {
8031
+ // Simple deduplication based on unique message ID
8032
+ if (inst.lastMsgId && msg.id <= inst.lastMsgId) return;
8033
+
8034
+ // Extract content: "[terminal: some command] output..." -> "output..."
8035
+ var clean = msg.content.replace(/^\[terminal:.*?\]/, '');
8036
+ if (clean) {
8037
+ // If the clean content is empty, it might be just the command tag
8038
+ // We can echo the command tag if we want, or just the output
8039
+ term.writeln(clean);
8040
+ }
8041
+ inst.lastMsgId = msg.id;
8042
+ }
8043
+ });
8044
+ });
8045
+ }
8046
+
8047
+ function ppPing(agentName) {
8048
+ var pq = projectParam();
8049
+ lttFetch('/api/inject' + pq, {
8050
+ method: 'POST',
8051
+ headers: { 'Content-Type': 'application/json' },
8052
+ body: JSON.stringify({ from: '__user__', to: agentName, content: 'ping' })
8053
+ }).then(function(r) { return r.json(); }).then(function(res) {
8054
+ if (res.success) {
8055
+ var btn = document.querySelector('#pp-actions .pp-ping-btn');
8056
+ if (btn) { btn.style.borderColor = 'var(--nh-green)'; setTimeout(function() { btn.style.borderColor = ''; }, 1000); }
8057
+ }
8058
+ }).catch(function(e) { log.error('Ping failed:', e); });
8059
+ }
8060
+
8061
+ function ppMessage(agentName) {
8062
+ var sel = document.getElementById('inject-target');
8063
+ if (sel) sel.value = agentName;
8064
+ var ta = document.getElementById('inject-content');
8065
+ if (ta) { ta.value = ''; ta.focus(); }
8066
+ updateSendBtn();
8067
+ document.getElementById('profile-popup').classList.remove('open');
8068
+ }
8069
+
8070
+ function ppNudge(agentName) {
8071
+ var sel = document.getElementById('inject-target');
8072
+ if (sel) sel.value = agentName;
8073
+ var ta = document.getElementById('inject-content');
8074
+ if (ta) { ta.value = '@' + agentName + ' '; ta.focus(); }
8075
+ updateSendBtn();
8076
+ document.getElementById('profile-popup').classList.remove('open');
8077
+ }
8078
+
8079
+ function ppFilterChat(agentName) {
8080
+ searchQuery = agentName.toLowerCase();
8081
+ var input = document.getElementById('search-input');
8082
+ if (input) input.value = agentName;
8083
+ lastMessageCount = 0;
8084
+ renderMessages(cachedHistory);
8085
+ document.getElementById('profile-popup').classList.remove('open');
8086
+ }
8087
+
8088
+ function ppViewHistory(agentName) {
8089
+ searchQuery = agentName.toLowerCase();
8090
+ var input = document.getElementById('search-input');
8091
+ if (input) input.value = agentName;
8092
+ lastMessageCount = 0;
8093
+ renderMessages(cachedHistory);
8094
+ document.getElementById('profile-popup').classList.remove('open');
8095
+ }
8096
+
6606
8097
  function showProfilePopup(agentName, event) {
6607
8098
  event.stopPropagation();
6608
8099
  var popup = document.getElementById('profile-popup');
@@ -6610,37 +8101,206 @@ function showProfilePopup(agentName, event) {
6610
8101
  if (!agent) return;
6611
8102
 
6612
8103
  editingAgent = agentName;
8104
+ popup.dataset.agent = agentName;
6613
8105
  var color = getColor(agentName);
8106
+
8107
+ // Header: avatar, name, status badge
6614
8108
  var ppAvatar = document.getElementById('pp-avatar');
6615
8109
  if (agent.avatar && (agent.avatar.startsWith('data:image/') || agent.avatar.startsWith('/'))) { ppAvatar.setAttribute('src', agent.avatar); ppAvatar.style.display = ''; } else { ppAvatar.style.display = 'none'; }
6616
8110
  document.getElementById('pp-name').textContent = agent.display_name || agentName;
6617
8111
  document.getElementById('pp-name').style.color = color;
6618
8112
  document.getElementById('pp-id').textContent = '@' + agentName;
6619
- document.getElementById('pp-role').innerHTML = agent.role ? '<span class="role-badge">' + escapeHtml(agent.role) + '</span>' : '';
6620
- document.getElementById('pp-bio').textContent = agent.bio || '';
6621
8113
 
6622
- // Stats
6623
- var sent = 0, received = 0;
6624
- for (var i = 0; i < cachedHistory.length; i++) {
6625
- if (cachedHistory[i].from === agentName) sent++;
6626
- if (cachedHistory[i].to === agentName) received++;
6627
- }
6628
- document.getElementById('pp-stats').innerHTML =
6629
- '<div class="profile-popup-stat">Sent: <span>' + sent + '</span></div>' +
6630
- '<div class="profile-popup-stat">Received: <span>' + received + '</span></div>' +
6631
- '<div class="profile-popup-stat">Provider: <span>' + escapeHtml(agent.provider || 'unknown') + '</span></div>' +
6632
- '<div class="profile-popup-stat">Branch: <span>' + escapeHtml(agent.branch || 'main') + '</span></div>';
6633
-
6634
- // Position near click
6635
- var rect = event.target.closest('.agent-top').getBoundingClientRect();
6636
- popup.style.top = Math.min(rect.bottom + 4, window.innerHeight - 280) + 'px';
6637
- popup.style.left = Math.min(rect.left, window.innerWidth - 280) + 'px';
8114
+ // Status badge
8115
+ var state = agent.status || (!agent.alive ? 'offline' : agent.is_listening ? 'listening' : 'working');
8116
+ var badgeClass = (state === 'offline' || state === 'dead') ? 'offline' : state === 'listening' ? 'listening' : (state === 'stuck') ? 'stuck' : (state === 'unresponsive') ? 'unresponsive' : 'online';
8117
+ var statusLabel = state === 'listening' ? 'Listening' : (state === 'offline' || state === 'dead') ? 'Offline' : state === 'stuck' ? 'Stuck' : state === 'unresponsive' ? 'Unresponsive' : state === 'idle' ? 'Idle' : 'Online';
8118
+ document.getElementById('pp-status-badge').className = 'pp-status-badge ' + badgeClass;
8119
+ document.getElementById('pp-status-badge').textContent = statusLabel;
8120
+
8121
+ // Default to Actions tab when opening agent popup
8122
+ switchPpTab('actions', false);
8123
+
8124
+ // === Stats Tab ===
8125
+ var badgesHtml = '';
8126
+ if (agent.provider) badgesHtml += '<span class="role-badge" style="font-size:10px">' + escapeHtml(agent.provider) + '</span>';
8127
+ if (agent.role) badgesHtml += '<span class="role-badge" style="font-size:10px">' + escapeHtml(agent.role) + '</span>';
8128
+ document.getElementById('pp-badges').innerHTML = badgesHtml;
8129
+
8130
+ // Count tasks completed and messages for this agent
8131
+ var tasksCompleted = 0;
8132
+ var msgCount = 0;
8133
+ if (cachedTasks) {
8134
+ for (var t = 0; t < cachedTasks.length; t++) {
8135
+ if (cachedTasks[t].assignee === agentName && cachedTasks[t].status === 'done') tasksCompleted++;
8136
+ }
8137
+ }
8138
+ if (cachedHistory) {
8139
+ for (var m = 0; m < cachedHistory.length; m++) {
8140
+ if (cachedHistory[m].from === agentName) msgCount++;
8141
+ }
8142
+ }
8143
+
8144
+ var statsHtml = '<div class="profile-popup-stat"><span>' + tasksCompleted + '</span>Tasks Done</div>' +
8145
+ '<div class="profile-popup-stat"><span>' + msgCount + '</span>Messages</div>';
8146
+ document.getElementById('pp-stats').innerHTML = statsHtml;
8147
+
8148
+ // Skills (read-only display)
8149
+ var skills = agent.skills || [];
8150
+ var skillsRoHtml = '';
8151
+ if (skills.length > 0) {
8152
+ var platformClass = 'platform-' + (agent.provider || 'unknown').toLowerCase();
8153
+ var platformSkills = agent.platform_skills || [];
8154
+ skillsRoHtml = '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:6px">Skills</div><div class="skill-tags-inline">';
8155
+ for (var s = 0; s < skills.length; s++) {
8156
+ var isPlatform = platformSkills.indexOf(skills[s]) !== -1;
8157
+ skillsRoHtml += '<span class="skill-tag' + (isPlatform ? ' ' + platformClass : '') + '">' + escapeHtml(skills[s]) + '</span>';
8158
+ }
8159
+ skillsRoHtml += '</div>';
8160
+ }
8161
+ document.getElementById('pp-skills-readonly').innerHTML = skillsRoHtml;
8162
+
8163
+ // === Actions Tab ===
8164
+ var eName = escapeHtml(agentName);
8165
+ var actionsHtml = '<button class="pp-action-btn" onclick="ppMessage(\'' + eName + '\')">' +
8166
+ '<svg viewBox="0 0 24 24"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>' +
8167
+ 'Message</button>' +
8168
+ '<button class="pp-action-btn pp-ping-btn" onclick="ppPing(\'' + eName + '\')">' +
8169
+ '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>' +
8170
+ 'Ping</button>' +
8171
+ '<button class="pp-action-btn" onclick="ppNudge(\'' + eName + '\')">' +
8172
+ '<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>' +
8173
+ 'Nudge</button>' +
8174
+ '<button class="pp-action-btn" onclick="ppFilterChat(\'' + eName + '\')">' +
8175
+ '<svg viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>' +
8176
+ 'Messages</button>' +
8177
+ '<button class="pp-action-btn" onclick="ppViewHistory(\'' + eName + '\')">' +
8178
+ '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>' +
8179
+ 'History</button>';
8180
+ document.getElementById('pp-actions').innerHTML = actionsHtml;
8181
+
8182
+ // === Profile Tab ===
8183
+ document.getElementById('pp-bio').textContent = agent.bio || 'No bio set.';
8184
+ renderSkillEditor(agentName);
8185
+
8186
+ // Position near click — find closest clickable ancestor
8187
+ var anchor = event.target.closest('.agent-top') || event.target.closest('.agent-card') || event.target;
8188
+ var rect = anchor.getBoundingClientRect();
8189
+ popup.style.visibility = 'hidden';
6638
8190
  popup.classList.add('open');
8191
+ var popupHeight = popup.offsetHeight;
8192
+
8193
+ var topPos = rect.bottom + 4;
8194
+ if (topPos + popupHeight > window.innerHeight - 10) {
8195
+ var topSpace = rect.top;
8196
+ var bottomSpace = window.innerHeight - rect.bottom;
8197
+ if (topSpace > bottomSpace && topSpace > popupHeight) {
8198
+ topPos = rect.top - popupHeight - 4;
8199
+ } else {
8200
+ topPos = window.innerHeight - popupHeight - 10;
8201
+ }
8202
+ }
8203
+
8204
+ popup.style.top = Math.max(10, topPos) + 'px';
8205
+ popup.style.left = Math.min(rect.left, window.innerWidth - 310) + 'px';
8206
+ popup.style.visibility = 'visible';
8207
+ }
8208
+
8209
+ function renderSkillEditor(agentName) {
8210
+ var agent = cachedAgents[agentName];
8211
+ var container = document.getElementById('pp-skills-container');
8212
+ if (!agent || !container) return;
8213
+
8214
+ var platformClass = 'platform-' + (agent.provider || 'unknown').toLowerCase();
8215
+ var skills = agent.skills || [];
8216
+ var platformSkills = agent.platform_skills || [];
8217
+
8218
+ var html = '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:8px">Skills & Expertise</div>';
8219
+ html += '<div class="skill-editor">';
8220
+
8221
+ // Existing skills
8222
+ html += '<div class="skill-tags-inline">';
8223
+ for (var i = 0; i < skills.length; i++) {
8224
+ var skill = skills[i];
8225
+ var isPlatform = platformSkills.indexOf(skill) !== -1;
8226
+ html += '<span class="skill-tag' + (isPlatform ? ' ' + platformClass : '') + '">' +
8227
+ escapeHtml(skill) +
8228
+ '<span class="skill-remove" onclick="removeSkill(\'' + escapeHtml(agentName) + '\', ' + i + ')">&times;</span>' +
8229
+ '</span>';
8230
+ }
8231
+ html += '</div>';
8232
+
8233
+ // Add new skill input
8234
+ html += '<div class="skill-input-row" style="margin-top:8px;display:flex;gap:4px">' +
8235
+ '<input type="text" id="new-skill-input" placeholder="Add skill..." onkeydown="if(event.key===' + "'Enter'" + ') addSkill(\'' + escapeHtml(agentName) + '\')" style="flex:1;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px;color:var(--text)">' +
8236
+ '<button onclick="addSkill(\'' + escapeHtml(agentName) + '\')" style="background:var(--nh-accent-dim);border:1px solid var(--nh-accent);border-radius:4px;padding:0 8px;color:var(--nh-accent);cursor:pointer;font-size:14px">+</button>' +
8237
+ '</div>';
8238
+
8239
+ html += '</div>';
8240
+ container.innerHTML = html;
8241
+ }
8242
+
8243
+ function addSkill(agentName) {
8244
+ var input = document.getElementById('new-skill-input');
8245
+ var skill = input.value.trim();
8246
+ if (!skill) return;
8247
+
8248
+ var agent = cachedAgents[agentName];
8249
+ var newSkills = (agent.skills || []).slice();
8250
+ if (newSkills.indexOf(skill) === -1) {
8251
+ newSkills.push(skill);
8252
+ updateAgentSkills(agentName, newSkills);
8253
+ }
8254
+ input.value = '';
8255
+ }
8256
+
8257
+ function removeSkill(agentName, index) {
8258
+ var agent = cachedAgents[agentName];
8259
+ var newSkills = (agent.skills || []).slice();
8260
+ newSkills.splice(index, 1);
8261
+ updateAgentSkills(agentName, newSkills);
8262
+ }
8263
+
8264
+ function updateAgentSkills(agentName, skills) {
8265
+ var pq = projectParam();
8266
+ lttFetch('/api/agent-cards' + pq, {
8267
+ method: 'POST',
8268
+ headers: { 'Content-Type': 'application/json' },
8269
+ body: JSON.stringify({ name: agentName, skills: skills })
8270
+ }).then(function(r) { return r.json(); }).then(function(data) {
8271
+ if (data.error) {
8272
+ showToast('!', 'Update failed: ' + data.error);
8273
+ } else {
8274
+ // Optimistic update of cache
8275
+ if (cachedAgents[agentName]) {
8276
+ cachedAgents[agentName].skills = skills;
8277
+ renderAgents(cachedAgents); // Refresh sidebar
8278
+ renderSkillEditor(agentName); // Refresh popup
8279
+ }
8280
+ }
8281
+ }).catch(function() {
8282
+ showToast('!', 'Update failed');
8283
+ });
8284
+ }
8285
+
8286
+ function showAgentPopup(agentName) {
8287
+ var agent = cachedAgents[agentName];
8288
+ if (!agent) return;
8289
+ editingAgent = agentName;
8290
+ openProfileEditor();
8291
+
8292
+ // Auto-switch to stats tab
8293
+ var tabs = document.querySelectorAll('.cd-tab');
8294
+ for (var i = 0; i < tabs.length; i++) {
8295
+ if (tabs[i].textContent === 'Stats') {
8296
+ cdSwitchTab('stats', tabs[i]);
8297
+ break;
8298
+ }
8299
+ }
6639
8300
  }
6640
8301
 
6641
8302
  document.addEventListener('click', function() {
6642
8303
  document.getElementById('profile-popup').classList.remove('open');
6643
- // Reset delete button if popup closes
6644
8304
  var delBtn = document.getElementById('pp-delete-btn');
6645
8305
  if (delBtn) { delBtn.textContent = 'Delete Agent'; delBtn._confirming = false; }
6646
8306
  });
@@ -6938,6 +8598,33 @@ function openProfileEditor() {
6938
8598
  // Reset to Body tab
6939
8599
  cdSwitchTab('body', document.querySelector('.cd-tab'));
6940
8600
 
8601
+ // Calculate and populate stats
8602
+ var sent = 0, received = 0;
8603
+ for (var i = 0; i < cachedHistory.length; i++) {
8604
+ if (cachedHistory[i].from === editingAgent) sent++;
8605
+ if (cachedHistory[i].to === editingAgent) received++;
8606
+ }
8607
+ var statsHtml =
8608
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Sent: <b style="color:var(--text)">' + sent + '</b></div>' +
8609
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Received: <b style="color:var(--text)">' + received + '</b></div>' +
8610
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Provider: <b style="color:var(--text)">' + escapeHtml(agent.provider || 'unknown') + '</b></div>' +
8611
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Branch: <b style="color:var(--text)">' + escapeHtml(agent.branch || 'main') + '</b></div>';
8612
+ document.getElementById('pe-stats-container').innerHTML = statsHtml;
8613
+
8614
+ var pq = projectParam();
8615
+ lttFetch('/api/token-usage' + pq).then(function(r) { return r.json(); }).then(function(data) {
8616
+ if (!data || !data.agents || !data.agents[editingAgent]) return;
8617
+ var tu = data.agents[editingAgent];
8618
+ var tokenHtml =
8619
+ '<div style="grid-column:1/-1;margin-top:12px;font-weight:800;color:var(--text);font-size:11px;text-transform:uppercase;border-bottom:1px solid var(--border);padding-bottom:4px">Token Usage (<span style="color:var(--accent);font-weight:600">' + escapeHtml(tu.model || 'unknown') + '</span>)</div>' +
8620
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Input: <b style="color:var(--text)">' + (tu.input_tokens || 0).toLocaleString() + '</b> </div>' +
8621
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Output: <b style="color:var(--text)">' + (tu.output_tokens || 0).toLocaleString() + '</b> </div>' +
8622
+ '<div style="background:var(--surface-2);padding:10px;border-radius:6px;border:1px solid var(--border)">Cache: <b style="color:var(--text)">' + ((tu.cache_creation_tokens || 0) + (tu.cache_read_tokens || 0)).toLocaleString() + '</b> </div>' +
8623
+ '<div style="background:var(--accent-dim);color:var(--accent);padding:10px;border-radius:6px;border:1px solid var(--accent);font-weight:700">Cost: $' + (tu.estimated_cost_usd || 0).toFixed(2) + '</div>';
8624
+ var el = document.getElementById('pe-stats-container');
8625
+ if (el) el.innerHTML += tokenHtml;
8626
+ }).catch(function() {});
8627
+
6941
8628
  // Open the panel
6942
8629
  document.getElementById('char-designer').classList.add('open');
6943
8630
  document.getElementById('cd-backdrop').classList.add('open');
@@ -7089,6 +8776,20 @@ function buildWorkflowHTML(wf) {
7089
8776
  for (var j = 0; j < wf.steps.length; j++) {
7090
8777
  var s = wf.steps[j];
7091
8778
  var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
8779
+
8780
+ var hasActiveRule = false;
8781
+ if (typeof cachedRules !== 'undefined' && s.assignee && cachedAgents && cachedAgents[s.assignee]) {
8782
+ var a = cachedAgents[s.assignee];
8783
+ for (var r=0; r < cachedRules.length; r++) {
8784
+ if (!cachedRules[r].active) continue;
8785
+ var matchRole = !cachedRules[r].scope_role || cachedRules[r].scope_role === (a.role || '').toLowerCase();
8786
+ var matchProv = !cachedRules[r].scope_provider || cachedRules[r].scope_provider === (a.provider || '').toLowerCase();
8787
+ var matchAgt = !cachedRules[r].scope_agent || cachedRules[r].scope_agent === s.assignee;
8788
+ if (matchRole && matchProv && matchAgt) { hasActiveRule = true; break; }
8789
+ }
8790
+ }
8791
+ var shieldIcon = hasActiveRule ? '<span class="step-shield" title="Governed by active rules"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>' : '';
8792
+
7092
8793
  if (j > 0) stepsHtml += '<span class="step-arrow">&rarr;</span>';
7093
8794
  var stepTimeHtml = '';
7094
8795
  if (s.status === 'done' && s.started_at && s.completed_at) {
@@ -7100,6 +8801,7 @@ function buildWorkflowHTML(wf) {
7100
8801
  }
7101
8802
  stepsHtml += '<div class="step-card ' + s.status + '" data-step-id="' + s.id + '" title="' + escapeHtml(s.notes || '') + '">' +
7102
8803
  '<span class="step-num">' + s.id + '</span>' +
8804
+ shieldIcon +
7103
8805
  '<span class="step-status" style="font-size:10px;color:' + stepColor + ';font-weight:600;text-transform:uppercase">' + s.status.replace('_', ' ') + '</span>' +
7104
8806
  '<div class="step-desc">' + escapeHtml(s.description) + '</div>' +
7105
8807
  (s.assignee ? '<div class="step-assignee">' + escapeHtml(s.assignee) + '</div>' : '') +
@@ -7606,7 +9308,7 @@ function checkPlanCompletion() {
7606
9308
  if (Notification.permission === 'granted') {
7607
9309
  new Notification('Plan Complete: ' + wf.name, {
7608
9310
  body: 'All ' + wf.steps.length + ' steps done!',
7609
- icon: '/logo.png',
9311
+ icon: '/favicon.png',
7610
9312
  });
7611
9313
  }
7612
9314
  // Flash the tab title
@@ -7647,7 +9349,7 @@ function renderMonitorPanel(data) {
7647
9349
  for (var i = 0; i < agentNames.length; i++) {
7648
9350
  var name = agentNames[i];
7649
9351
  var a = agents[name];
7650
- var color = a.status === 'working' ? 'var(--green)' : a.status === 'listening' ? '#3b82f6' : a.status === 'idle' ? 'var(--orange)' : 'var(--red,#ef4444)';
9352
+ var color = a.status === 'working' ? 'var(--nh-green)' : a.status === 'listening' ? 'var(--nh-blue)' : a.status === 'idle' ? 'var(--nh-accent)' : 'var(--nh-red)';
7651
9353
  var role = a.role ? ' (' + a.role + ')' : '';
7652
9354
  html += '<div style="padding:4px 8px;background:var(--surface-2);border-left:3px solid ' + color + ';border-radius:4px;font-size:11px">' +
7653
9355
  '<span style="font-weight:600">' + escapeHtml(name) + '</span>' +
@@ -7912,6 +9614,8 @@ function poll() {
7912
9614
  // Browser notification for new messages
7913
9615
  if (prevHistLen > 0 && cachedHistory.length > prevHistLen) {
7914
9616
  sendBrowserNotification(cachedHistory[cachedHistory.length - 1]);
9617
+ // Real-time terminal sync: new messages arrived, update any open terminals
9618
+ syncTerminalsFromHistory();
7915
9619
  }
7916
9620
 
7917
9621
  $id('stat-messages').textContent = status.messageCount;
@@ -9501,12 +11205,16 @@ function updateAgentBar(agents) {
9501
11205
  var name = sorted[i][0];
9502
11206
  var agent = sorted[i][1];
9503
11207
  if (name === '__system__' || name === 'Dashboard') continue;
9504
- var cls = 'offline';
9505
- var activity = 'offline';
11208
+ var cls = agent.status || 'offline';
11209
+ var activity = cls;
9506
11210
  if (agent.alive) {
9507
11211
  if (agent.is_listening || agent.status === 'listening') { cls = 'listening'; activity = 'listening'; }
9508
11212
  else if (agent.status === 'idle') { cls = 'idle'; activity = 'idle'; }
9509
11213
  else { cls = 'working'; activity = agent.status === 'working' ? 'working' : 'active'; }
11214
+ } else if (agent.status === 'unknown') {
11215
+ activity = 'never started';
11216
+ } else if (agent.status === 'stale') {
11217
+ activity = 'stale';
9510
11218
  }
9511
11219
  // Toast on state change
9512
11220
  var prevState = prevAgentBarState[name];
@@ -9531,7 +11239,7 @@ function renderOverview() {
9531
11239
  if (!area) return;
9532
11240
  var savedScroll = area.scrollTop;
9533
11241
  var pq = projectParam();
9534
- var histQ = pq + (pq ? '&' : '?') + 'limit=5' + (activeBranch && activeBranch !== 'main' ? '&branch=' + encodeURIComponent(activeBranch) : '');
11242
+ var histQ = pq + (pq ? '&' : '?') + 'limit=12' + (activeBranch && activeBranch !== 'main' ? '&branch=' + encodeURIComponent(activeBranch) : '');
9535
11243
  Promise.all([
9536
11244
  lttFetch('/api/status' + pq).then(function(r) { return r.json(); }),
9537
11245
  lttFetch('/api/agents' + pq).then(function(r) { return r.json(); }),
@@ -9550,7 +11258,7 @@ function renderOverview() {
9550
11258
  var agentEntries = Object.entries(agents).filter(function(e) { return e[0] !== '__system__'; });
9551
11259
 
9552
11260
  // 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');
11261
+ 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') + ':c:' + (status.coordinator_mode || 'responsive');
9554
11262
  if (overviewHash === renderOverview._lastHash) { return; }
9555
11263
  renderOverview._lastHash = overviewHash;
9556
11264
 
@@ -9568,137 +11276,152 @@ function renderOverview() {
9568
11276
 
9569
11277
  // Welcome header with coordinator mode toggle
9570
11278
  var coordMode = status.coordinator_mode || 'responsive';
9571
- html += '<div class="overview-welcome" style="display:flex;justify-content:space-between;align-items:flex-start">';
11279
+ var regTotal = agentEntries.length || (status.agentCount || 0);
11280
+ var aliveN = status.aliveCount || 0;
11281
+ html += '<div class="overview-welcome">';
9572
11282
  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>';
11283
+ html += '<p>' + (status.conversation_mode === 'managed' ? 'Managed mode' : 'Direct mode') + ' ' + regTotal + ' agent' + (regTotal === 1 ? '' : 's') + ' registered</p></div>';
11284
+ html += '<div class="overview-coordinator">';
11285
+ html += '<span class="overview-coordinator-label">Coordinator</span>';
11286
+ html += '<div class="overview-coordinator-toggle" role="group" aria-label="Coordinator mode">';
11287
+ html += '<button type="button" class="overview-coordinator-btn' + (coordMode === 'responsive' ? ' overview-coordinator-btn--mode-responsive' : '') + '" onclick="setCoordinatorMode(\'responsive\')">Stay with me</button>';
11288
+ html += '<button type="button" class="overview-coordinator-btn' + (coordMode === 'autonomous' ? ' overview-coordinator-btn--mode-autonomous' : '') + '" onclick="setCoordinatorMode(\'autonomous\')">Run autonomously</button>';
9579
11289
  html += '</div></div>';
9580
11290
  html += '</div>';
9581
11291
 
9582
- // Metric cards with icons
9583
11292
  html += '<div class="overview-metrics">';
9584
11293
  html += '<div class="metric-card">';
9585
11294
  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>';
11295
+ html += '<div class="metric-label">Active agents</div>';
11296
+ html += '<div class="metric-value">' + aliveN + '/' + regTotal + '</div>';
11297
+ html += '<div class="metric-sub">' + regTotal + ' registered</div>';
9589
11298
  html += '</div>';
9590
11299
 
9591
11300
  html += '<div class="metric-card">';
9592
11301
  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
11302
  html += '<div class="metric-label">Messages</div>';
9594
11303
  html += '<div class="metric-value">' + (status.messageCount || 0) + '</div>';
9595
- html += '<div class="metric-sub">' + (status.threadCount || 0) + ' threads</div>';
11304
+ html += '<div class="metric-sub">Total exchanged</div>';
9596
11305
  html += '</div>';
9597
11306
 
9598
11307
  html += '<div class="metric-card">';
9599
11308
  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>';
11309
+ html += '<div class="metric-label">Task progress</div>';
9601
11310
  html += '<div class="metric-value">' + taskPct + '<span style="font-size:18px;font-weight:600;color:var(--text-muted);margin-left:2px">%</span></div>';
11311
+ html += '<div class="metric-card__progress"><div class="metric-card__progress-fill" style="width:' + taskPct + '%"></div></div>';
9602
11312
  html += '<div class="metric-sub">' + done + ' done, ' + inProgress + ' active, ' + pending + ' pending</div>';
9603
11313
  html += '</div>';
9604
11314
  html += '</div>';
9605
11315
 
9606
11316
  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>';
11317
+ html += '<div class="overview-empty-state">';
11318
+ html += '<div class="overview-empty-state-icon"><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>';
11319
+ html += '<div class="overview-empty-state-title">No agents online yet</div>';
11320
+ html += '<div class="overview-empty-state-desc">Open a CLI terminal and register an agent to start collaborating.</div>';
11321
+ html += '<button type="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
11322
  html += '</div>';
9614
11323
  } 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
- }
11324
+ html += '<div class="overview-stitch-rows">';
9630
11325
 
9631
- // Workflow progress panel
9632
11326
  if (hasWorkflows) {
9633
- html += '<div class="overview-panel"><h3>Workflows <a onclick="switchView(\'workflows\')">View all &rarr;</a></h3>';
11327
+ html += '<div class="overview-panel overview-full-width"><h3>Workflows <a onclick="switchView(\'workflows\')">View all</a></h3>';
9634
11328
  for (var w = 0; w < activeWfs.length; w++) {
9635
11329
  var wf = activeWfs[w];
9636
11330
  var wfDone = wf.steps ? wf.steps.filter(function(s) { return s.status === 'done'; }).length : 0;
9637
11331
  var wfTotal = wf.steps ? wf.steps.length : 1;
9638
11332
  var wfPct = Math.round((wfDone / wfTotal) * 100);
9639
11333
  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>';
11334
+ html += '<div class="metric-card__progress"><div class="metric-card__progress-fill" style="width:' + wfPct + '%"></div></div></div>';
9641
11335
  }
9642
11336
  html += '</div>';
9643
11337
  }
9644
11338
 
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">';
11339
+ html += '<div class="overview-grid overview-grid--pair">';
11340
+ html += '<div class="overview-panel"><h3>Agents <a onclick="switchView(\'messages\')">View details</a></h3>';
11341
+ if (hasAgents) {
11342
+ for (var i = 0; i < agentEntries.length; i++) {
11343
+ var aName = agentEntries[i][0];
11344
+ var aInfo = agentEntries[i][1];
11345
+ var dotMod = !aInfo.alive ? 'offline' : aInfo.is_listening ? 'listening' : aInfo.status === 'idle' ? 'idle' : 'working';
11346
+ var statusText = !aInfo.alive ? 'offline' : aInfo.is_listening ? 'listening' : aInfo.status === 'idle' ? 'idle' : 'working';
11347
+ var role = aInfo.role || '';
11348
+ html += '<div class="overview-agent-row"><span class="nh-presence-dot nh-presence-dot--' + dotMod + '" aria-hidden="true"></span><span class="overview-agent-name">' + aName + '</span>';
11349
+ if (role) html += '<span class="overview-role-pill">' + escapeHtml(role) + '</span>';
11350
+ html += '<span class="overview-agent-status" style="margin-left:auto">' + statusText + '</span></div>';
11351
+ }
11352
+ } else {
11353
+ html += '<p class="overview-panel-empty">No agents registered</p>';
11354
+ }
11355
+ html += '</div>';
11356
+
11357
+ html += '<div class="overview-panel"><h3>Tasks <a onclick="switchView(\'tasks\')">View board</a></h3>';
11358
+ if (hasTasks) {
11359
+ var rem = pending + inProgress;
11360
+ html += '<div class="overview-task-stitch"><span class="overview-task-stitch-done">' + done + ' done</span><span class="overview-task-stitch-rem">' + rem + ' remaining</span></div>';
11361
+ html += '<div class="overview-task-summary" style="margin-top:10px">';
9649
11362
  if (pending > 0) html += '<span class="overview-task-badge pending">' + pending + ' pending</span>';
9650
11363
  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
11364
  html += '</div>';
11365
+ } else {
11366
+ html += '<p class="overview-panel-empty">No tasks yet</p>';
9654
11367
  }
11368
+ html += '</div>';
11369
+ html += '</div>';
9655
11370
 
9656
- // Recent messages
11371
+ html += '<div class="overview-grid overview-grid--pair">';
11372
+ html += '<div class="overview-panel"><h3>Recent Activity <a onclick="switchView(\'messages\')">View all</a></h3>';
9657
11373
  if (hasMessages) {
9658
- html += '<div class="overview-panel overview-full-width"><h3>Recent Activity <a onclick="switchView(\'messages\')">View all &rarr;</a></h3>';
9659
11374
  for (var m = 0; m < history.length; m++) {
9660
11375
  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>';
11376
+ var fromN = msg.from || '?';
11377
+ var fromL = fromN.toLowerCase();
11378
+ var fk = fromL === 'coordinator' ? 'coord' : (fromL === 'system' || fromL === '__system__') ? 'system' : 'agent';
11379
+ var t = new Date(msg.timestamp);
11380
+ var th = t.getHours();
11381
+ var tm = String(t.getMinutes()).padStart(2, '0');
11382
+ var ap = th >= 12 ? 'PM' : 'AM';
11383
+ var th12 = th % 12;
11384
+ if (th12 === 0) th12 = 12;
11385
+ var timeBracket = '[' + th12 + ':' + tm + ' ' + ap + ']';
11386
+ var preview = (msg.content || '').replace(/[#*`_~\[\]]/g, '').substring(0, 120);
11387
+ html += '<div class="overview-activity-row"><span class="overview-activity-time">' + timeBracket + '</span><span class="overview-activity-body"><span class="overview-msg-from--' + fk + '">' + escapeHtml(fromN) + '</span><span class="overview-activity-preview">: ' + escapeHtml(preview) + '</span></span></div>';
9665
11388
  }
9666
- html += '</div>';
11389
+ } else {
11390
+ html += '<p class="overview-panel-empty">No recent messages</p>';
9667
11391
  }
11392
+ html += '</div>';
9668
11393
 
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>';
11394
+ html += '<div class="overview-panel"><h3>Recent Notifications</h3>';
11395
+ if (notifications.length > 0) {
11396
+ var NOTIF_STITCH = {
11397
+ task_done: { icon: '&#x2713;', label: 'Success', cls: 'overview-notif--success' },
11398
+ agent_online: { icon: '&#x2713;', label: 'Success', cls: 'overview-notif--success' },
11399
+ workflow_advanced: { icon: 'i', label: 'Info', cls: 'overview-notif--info' },
11400
+ agent_listening: { icon: 'i', label: 'Info', cls: 'overview-notif--info' },
11401
+ agent_busy: { icon: 'i', label: 'Info', cls: 'overview-notif--info' },
11402
+ agent_offline: { icon: '&#x2013;', label: 'Info', cls: 'overview-notif--neutral' },
11403
+ approval_needed: { icon: '!', label: 'Warning', cls: 'overview-notif--warn' }
11404
+ };
11405
+ var recentN = notifications.slice(-8).reverse();
11406
+ for (var ni = 0; ni < recentN.length; ni++) {
11407
+ var nv = recentN[ni];
11408
+ var meta = NOTIF_STITCH[nv.type] || { icon: '&#x2022;', label: 'Info', cls: 'overview-notif--neutral' };
11409
+ var ntime = new Date(nv.timestamp);
11410
+ var nth = ntime.getHours();
11411
+ var ntm = String(ntime.getMinutes()).padStart(2, '0');
11412
+ var nap = nth >= 12 ? 'PM' : 'AM';
11413
+ var nth12 = nth % 12;
11414
+ if (nth12 === 0) nth12 = 12;
11415
+ var ntimeStr = nth12 + ':' + ntm + ' ' + nap;
11416
+ var nbody = escapeHtml(nv.message || nv.summary || nv.type || '');
11417
+ html += '<div class="overview-notif-row-stitch ' + meta.cls + '"><span class="overview-notif-icon-stitch">' + meta.icon + '</span><div style="flex:1;min-width:0"><span class="overview-notif-tag">[' + meta.label + ']</span><span class="overview-notif-msg">' + nbody + '</span></div><span class="overview-notif-time-stitch">' + ntimeStr + '</span></div>';
11418
+ }
11419
+ } else {
11420
+ html += '<p class="overview-panel-empty">No notifications</p>';
9678
11421
  }
9679
-
9680
11422
  html += '</div>';
9681
- }
11423
+ html += '</div>';
9682
11424
 
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
11425
  html += '</div>';
9703
11426
  }
9704
11427