let-them-talk 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard.html ADDED
@@ -0,0 +1,1719 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Let Them Talk</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%230d1117'/><path d='M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z' fill='%2358a6ff'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>">
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+
11
+ :root {
12
+ --bg: #0d1117;
13
+ --surface: #161b22;
14
+ --surface-2: #21262d;
15
+ --surface-3: #2d333b;
16
+ --border: #30363d;
17
+ --border-light: #3d444d;
18
+ --text: #e6edf3;
19
+ --text-dim: #8b949e;
20
+ --text-muted: #656d76;
21
+ --accent: #58a6ff;
22
+ --accent-dim: rgba(88, 166, 255, 0.15);
23
+ --green: #3fb950;
24
+ --green-dim: rgba(63, 185, 80, 0.15);
25
+ --red: #f85149;
26
+ --red-dim: rgba(248, 81, 73, 0.15);
27
+ --orange: #d29922;
28
+ --orange-dim: rgba(210, 153, 34, 0.15);
29
+ --purple: #bc8cff;
30
+ --purple-dim: rgba(188, 140, 255, 0.15);
31
+ --yellow: #e3b341;
32
+ --sidebar-w: 280px;
33
+ --header-h: 56px;
34
+ }
35
+
36
+ body {
37
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
38
+ background: var(--bg);
39
+ color: var(--text);
40
+ min-height: 100vh;
41
+ overflow: hidden;
42
+ }
43
+
44
+ /* ===== HEADER ===== */
45
+ .header {
46
+ background: var(--surface);
47
+ border-bottom: 1px solid var(--border);
48
+ padding: 0 20px;
49
+ height: var(--header-h);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ position: fixed;
54
+ top: 0;
55
+ left: 0;
56
+ right: 0;
57
+ z-index: 100;
58
+ }
59
+
60
+ .header-left {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 16px;
64
+ }
65
+
66
+ .logo {
67
+ font-size: 18px;
68
+ font-weight: 700;
69
+ background: linear-gradient(135deg, var(--accent), var(--purple));
70
+ -webkit-background-clip: text;
71
+ -webkit-text-fill-color: transparent;
72
+ background-clip: text;
73
+ white-space: nowrap;
74
+ }
75
+
76
+ .header-stats {
77
+ display: flex;
78
+ gap: 16px;
79
+ align-items: center;
80
+ }
81
+
82
+ .h-stat {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 5px;
86
+ font-size: 12px;
87
+ color: var(--text-dim);
88
+ }
89
+
90
+ .h-stat-val {
91
+ font-weight: 700;
92
+ color: var(--accent);
93
+ font-size: 13px;
94
+ }
95
+
96
+ .header-actions {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 8px;
100
+ }
101
+
102
+ .connection {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 5px;
106
+ font-size: 11px;
107
+ color: var(--text-dim);
108
+ margin-right: 8px;
109
+ }
110
+
111
+ .conn-dot {
112
+ width: 6px;
113
+ height: 6px;
114
+ border-radius: 50%;
115
+ background: var(--green);
116
+ animation: pulse 2s infinite;
117
+ }
118
+
119
+ @keyframes pulse {
120
+ 0%, 100% { opacity: 1; }
121
+ 50% { opacity: 0.3; }
122
+ }
123
+
124
+ .btn {
125
+ background: var(--surface-2);
126
+ color: var(--text);
127
+ border: 1px solid var(--border);
128
+ padding: 6px 12px;
129
+ border-radius: 6px;
130
+ cursor: pointer;
131
+ font-size: 12px;
132
+ font-weight: 500;
133
+ transition: all 0.15s;
134
+ white-space: nowrap;
135
+ }
136
+
137
+ .btn:hover { border-color: var(--border-light); background: var(--surface-3); }
138
+ .btn-danger { color: var(--red); }
139
+ .btn-danger:hover { background: var(--red-dim); border-color: var(--red); }
140
+ .btn-primary { color: var(--accent); }
141
+ .btn-primary:hover { background: var(--accent-dim); border-color: var(--accent); }
142
+
143
+ .mobile-toggle {
144
+ display: none;
145
+ background: none;
146
+ border: none;
147
+ color: var(--text);
148
+ font-size: 20px;
149
+ cursor: pointer;
150
+ padding: 4px 8px;
151
+ }
152
+
153
+ /* ===== LAYOUT ===== */
154
+ .app {
155
+ display: flex;
156
+ height: calc(100vh - var(--header-h));
157
+ margin-top: var(--header-h);
158
+ }
159
+
160
+ /* ===== SIDEBAR ===== */
161
+ .sidebar {
162
+ width: var(--sidebar-w);
163
+ min-width: var(--sidebar-w);
164
+ background: var(--surface);
165
+ border-right: 1px solid var(--border);
166
+ display: flex;
167
+ flex-direction: column;
168
+ overflow: hidden;
169
+ transition: transform 0.25s ease;
170
+ }
171
+
172
+ .sidebar-scroll {
173
+ flex: 1;
174
+ overflow-y: auto;
175
+ padding: 12px;
176
+ }
177
+
178
+ .sidebar-section {
179
+ margin-bottom: 20px;
180
+ }
181
+
182
+ .sidebar-title {
183
+ font-size: 10px;
184
+ font-weight: 700;
185
+ color: var(--text-muted);
186
+ text-transform: uppercase;
187
+ letter-spacing: 1px;
188
+ margin-bottom: 8px;
189
+ padding: 0 4px;
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ }
194
+
195
+ .sidebar-title .alert-count {
196
+ background: var(--orange);
197
+ color: #000;
198
+ font-size: 9px;
199
+ font-weight: 700;
200
+ padding: 1px 5px;
201
+ border-radius: 8px;
202
+ min-width: 16px;
203
+ text-align: center;
204
+ }
205
+
206
+ /* ===== AGENT CARDS ===== */
207
+ .agent-card {
208
+ background: var(--surface-2);
209
+ border: 1px solid var(--border);
210
+ border-radius: 8px;
211
+ padding: 10px 12px;
212
+ margin-bottom: 6px;
213
+ transition: all 0.15s;
214
+ }
215
+
216
+ .agent-card.sleeping {
217
+ border-color: var(--orange);
218
+ border-left: 3px solid var(--orange);
219
+ }
220
+
221
+ .agent-card.dead {
222
+ opacity: 0.5;
223
+ }
224
+
225
+ .agent-top {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 8px;
229
+ margin-bottom: 6px;
230
+ }
231
+
232
+ .agent-avatar {
233
+ width: 28px;
234
+ height: 28px;
235
+ border-radius: 50%;
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ font-weight: 700;
240
+ font-size: 12px;
241
+ color: #fff;
242
+ flex-shrink: 0;
243
+ }
244
+
245
+ .agent-info { flex: 1; min-width: 0; }
246
+
247
+ .agent-name {
248
+ font-weight: 600;
249
+ font-size: 13px;
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 6px;
253
+ }
254
+
255
+ .agent-badge {
256
+ font-size: 9px;
257
+ padding: 1px 5px;
258
+ border-radius: 6px;
259
+ font-weight: 600;
260
+ text-transform: uppercase;
261
+ letter-spacing: 0.3px;
262
+ }
263
+
264
+ .agent-badge.active { background: var(--green-dim); color: var(--green); }
265
+ .agent-badge.sleeping { background: var(--orange-dim); color: var(--orange); }
266
+ .agent-badge.dead { background: var(--red-dim); color: var(--red); }
267
+
268
+ .listen-badge {
269
+ font-size: 9px;
270
+ padding: 2px 6px;
271
+ border-radius: 6px;
272
+ font-weight: 700;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.3px;
275
+ display: inline-flex;
276
+ align-items: center;
277
+ gap: 4px;
278
+ margin-top: 5px;
279
+ }
280
+
281
+ .listen-badge.listening {
282
+ background: var(--green-dim);
283
+ color: var(--green);
284
+ border: 1px solid rgba(63, 185, 80, 0.3);
285
+ }
286
+
287
+ .listen-badge.busy {
288
+ background: var(--orange-dim);
289
+ color: var(--yellow);
290
+ border: 1px solid rgba(227, 179, 65, 0.3);
291
+ }
292
+
293
+ .listen-badge.not-listening {
294
+ background: var(--red-dim);
295
+ color: var(--red);
296
+ border: 1px solid rgba(248, 81, 73, 0.3);
297
+ animation: pulseAlert 1.5s infinite;
298
+ }
299
+
300
+ .listen-badge.offline {
301
+ background: var(--surface-3);
302
+ color: var(--text-muted);
303
+ border: 1px solid var(--border);
304
+ }
305
+
306
+ .listen-dot {
307
+ width: 5px;
308
+ height: 5px;
309
+ border-radius: 50%;
310
+ display: inline-block;
311
+ }
312
+
313
+ .listen-dot.on { background: var(--green); box-shadow: 0 0 4px var(--green); }
314
+ .listen-dot.off { background: var(--red); }
315
+
316
+ @keyframes pulseAlert {
317
+ 0%, 100% { opacity: 1; }
318
+ 50% { opacity: 0.6; }
319
+ }
320
+
321
+ .agent-meta {
322
+ font-size: 10px;
323
+ color: var(--text-muted);
324
+ display: flex;
325
+ gap: 8px;
326
+ }
327
+
328
+ .agent-activity {
329
+ font-size: 10px;
330
+ color: var(--text-dim);
331
+ margin-top: 4px;
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 4px;
335
+ }
336
+
337
+ .agent-activity-icon {
338
+ display: inline-block;
339
+ width: 6px;
340
+ height: 6px;
341
+ border-radius: 50%;
342
+ flex-shrink: 0;
343
+ }
344
+
345
+ .agent-activity-icon.active { background: var(--green); box-shadow: 0 0 4px var(--green); }
346
+ .agent-activity-icon.sleeping { background: var(--orange); animation: pulse 2s infinite; }
347
+ .agent-activity-icon.dead { background: var(--red); }
348
+
349
+ .nudge-btn {
350
+ background: var(--orange-dim);
351
+ color: var(--orange);
352
+ border: 1px solid rgba(210, 153, 34, 0.3);
353
+ border-radius: 4px;
354
+ padding: 3px 8px;
355
+ font-size: 10px;
356
+ font-weight: 600;
357
+ cursor: pointer;
358
+ margin-top: 6px;
359
+ transition: all 0.15s;
360
+ display: block;
361
+ width: 100%;
362
+ text-align: center;
363
+ }
364
+
365
+ .nudge-btn:hover {
366
+ background: rgba(210, 153, 34, 0.25);
367
+ border-color: var(--orange);
368
+ }
369
+
370
+ /* ===== PROJECT SWITCHER ===== */
371
+ .project-switcher {
372
+ padding: 12px;
373
+ border-bottom: 1px solid var(--border);
374
+ }
375
+
376
+ .project-select {
377
+ width: 100%;
378
+ background: var(--surface-2);
379
+ color: var(--text);
380
+ border: 1px solid var(--border);
381
+ border-radius: 6px;
382
+ padding: 7px 10px;
383
+ font-size: 12px;
384
+ cursor: pointer;
385
+ outline: none;
386
+ margin-bottom: 6px;
387
+ }
388
+
389
+ .project-select:focus { border-color: var(--accent); }
390
+
391
+ .project-actions {
392
+ display: flex;
393
+ gap: 4px;
394
+ }
395
+
396
+ .project-actions .btn {
397
+ flex: 1;
398
+ font-size: 10px;
399
+ padding: 4px 8px;
400
+ text-align: center;
401
+ }
402
+
403
+ .project-input {
404
+ width: 100%;
405
+ background: var(--surface-2);
406
+ color: var(--text);
407
+ border: 1px solid var(--border);
408
+ border-radius: 6px;
409
+ padding: 6px 10px;
410
+ font-size: 11px;
411
+ outline: none;
412
+ margin-bottom: 6px;
413
+ display: none;
414
+ }
415
+
416
+ .project-input:focus { border-color: var(--accent); }
417
+ .project-input.visible { display: block; }
418
+
419
+ /* ===== THREAD LIST ===== */
420
+ .thread-item {
421
+ background: var(--surface-2);
422
+ border: 1px solid var(--border);
423
+ border-radius: 6px;
424
+ padding: 8px 10px;
425
+ margin-bottom: 4px;
426
+ cursor: pointer;
427
+ transition: all 0.15s;
428
+ font-size: 12px;
429
+ }
430
+
431
+ .thread-item:hover, .thread-item.active {
432
+ border-color: var(--accent);
433
+ background: var(--accent-dim);
434
+ }
435
+
436
+ .thread-preview {
437
+ font-weight: 500;
438
+ margin-bottom: 2px;
439
+ white-space: nowrap;
440
+ overflow: hidden;
441
+ text-overflow: ellipsis;
442
+ }
443
+
444
+ .thread-meta {
445
+ font-size: 10px;
446
+ color: var(--text-muted);
447
+ }
448
+
449
+ .filter-clear {
450
+ font-size: 11px;
451
+ color: var(--accent);
452
+ cursor: pointer;
453
+ margin-top: 6px;
454
+ display: none;
455
+ padding: 0 4px;
456
+ }
457
+
458
+ .filter-clear.visible { display: block; }
459
+
460
+ /* ===== MAIN CONTENT ===== */
461
+ .main {
462
+ flex: 1;
463
+ display: flex;
464
+ flex-direction: column;
465
+ min-width: 0;
466
+ overflow: hidden;
467
+ }
468
+
469
+ /* ===== MESSAGES ===== */
470
+ .messages-area {
471
+ flex: 1;
472
+ overflow-y: auto;
473
+ padding: 16px 20px;
474
+ display: flex;
475
+ flex-direction: column;
476
+ gap: 2px;
477
+ }
478
+
479
+ .message {
480
+ display: flex;
481
+ gap: 10px;
482
+ padding: 10px 14px;
483
+ border-radius: 8px;
484
+ transition: background 0.15s;
485
+ }
486
+
487
+ .message:hover { background: var(--surface); }
488
+
489
+ .msg-avatar {
490
+ width: 32px;
491
+ height: 32px;
492
+ border-radius: 50%;
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: center;
496
+ font-weight: 700;
497
+ font-size: 13px;
498
+ flex-shrink: 0;
499
+ color: #fff;
500
+ margin-top: 2px;
501
+ }
502
+
503
+ .msg-body { flex: 1; min-width: 0; }
504
+
505
+ .msg-header {
506
+ display: flex;
507
+ align-items: baseline;
508
+ gap: 6px;
509
+ margin-bottom: 3px;
510
+ flex-wrap: wrap;
511
+ }
512
+
513
+ .msg-from { font-weight: 600; font-size: 13px; }
514
+ .msg-arrow { color: var(--text-muted); font-size: 11px; }
515
+ .msg-to { font-size: 12px; color: var(--text-dim); }
516
+ .msg-time { font-size: 10px; color: var(--text-muted); }
517
+
518
+ .msg-badges {
519
+ display: flex;
520
+ gap: 4px;
521
+ margin-left: auto;
522
+ }
523
+
524
+ .badge {
525
+ font-size: 9px;
526
+ padding: 1px 5px;
527
+ border-radius: 8px;
528
+ font-weight: 600;
529
+ }
530
+
531
+ .badge-ack { background: var(--green-dim); color: var(--green); }
532
+ .badge-thread { background: var(--purple-dim); color: var(--purple); }
533
+
534
+ /* ===== MARKDOWN CONTENT ===== */
535
+ .msg-content {
536
+ font-size: 13px;
537
+ line-height: 1.6;
538
+ word-break: break-word;
539
+ color: var(--text);
540
+ }
541
+
542
+ .msg-content h1 { font-size: 20px; font-weight: 700; margin: 12px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
543
+ .msg-content h2 { font-size: 17px; font-weight: 700; margin: 10px 0 5px; padding-bottom: 3px; border-bottom: 1px solid var(--border); }
544
+ .msg-content h3 { font-size: 15px; font-weight: 600; margin: 8px 0 4px; }
545
+ .msg-content h4 { font-size: 14px; font-weight: 600; margin: 6px 0 3px; }
546
+
547
+ .msg-content p { margin: 4px 0; }
548
+
549
+ .msg-content strong { font-weight: 700; }
550
+ .msg-content em { font-style: italic; color: var(--text-dim); }
551
+
552
+ .msg-content code {
553
+ background: var(--surface-3);
554
+ padding: 1px 5px;
555
+ border-radius: 4px;
556
+ font-size: 12px;
557
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
558
+ color: var(--orange);
559
+ }
560
+
561
+ .msg-content pre {
562
+ background: var(--surface-2);
563
+ border: 1px solid var(--border);
564
+ border-radius: 6px;
565
+ padding: 12px 14px;
566
+ margin: 6px 0;
567
+ overflow-x: auto;
568
+ font-size: 12px;
569
+ line-height: 1.5;
570
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
571
+ position: relative;
572
+ }
573
+
574
+ .msg-content pre code {
575
+ background: none;
576
+ padding: 0;
577
+ color: var(--text);
578
+ font-size: inherit;
579
+ }
580
+
581
+ .msg-content pre .lang-tag {
582
+ position: absolute;
583
+ top: 4px;
584
+ right: 8px;
585
+ font-size: 10px;
586
+ color: var(--text-muted);
587
+ text-transform: uppercase;
588
+ }
589
+
590
+ .msg-content ul, .msg-content ol {
591
+ padding-left: 20px;
592
+ margin: 4px 0;
593
+ }
594
+
595
+ .msg-content li {
596
+ margin: 2px 0;
597
+ }
598
+
599
+ .msg-content blockquote {
600
+ border-left: 3px solid var(--border-light);
601
+ padding: 2px 12px;
602
+ margin: 4px 0;
603
+ color: var(--text-dim);
604
+ }
605
+
606
+ .msg-content hr {
607
+ border: none;
608
+ border-top: 1px solid var(--border);
609
+ margin: 8px 0;
610
+ }
611
+
612
+ .msg-content table {
613
+ border-collapse: collapse;
614
+ margin: 6px 0;
615
+ width: 100%;
616
+ font-size: 12px;
617
+ }
618
+
619
+ .msg-content th, .msg-content td {
620
+ border: 1px solid var(--border);
621
+ padding: 5px 10px;
622
+ text-align: left;
623
+ }
624
+
625
+ .msg-content th {
626
+ background: var(--surface-2);
627
+ font-weight: 600;
628
+ }
629
+
630
+ .msg-content a {
631
+ color: var(--accent);
632
+ text-decoration: none;
633
+ }
634
+
635
+ .msg-content a:hover { text-decoration: underline; }
636
+
637
+ /* ===== MESSAGE INPUT ===== */
638
+ .msg-input-bar {
639
+ border-top: 1px solid var(--border);
640
+ background: var(--surface);
641
+ padding: 12px 20px;
642
+ display: flex;
643
+ gap: 8px;
644
+ align-items: flex-end;
645
+ }
646
+
647
+ .input-target {
648
+ display: flex;
649
+ flex-direction: column;
650
+ gap: 4px;
651
+ min-width: 100px;
652
+ }
653
+
654
+ .input-target label {
655
+ font-size: 10px;
656
+ color: var(--text-muted);
657
+ text-transform: uppercase;
658
+ letter-spacing: 0.5px;
659
+ }
660
+
661
+ .input-target select {
662
+ background: var(--surface-2);
663
+ color: var(--text);
664
+ border: 1px solid var(--border);
665
+ border-radius: 6px;
666
+ padding: 7px 10px;
667
+ font-size: 12px;
668
+ cursor: pointer;
669
+ outline: none;
670
+ }
671
+
672
+ .input-target select:focus { border-color: var(--accent); }
673
+
674
+ .input-msg {
675
+ flex: 1;
676
+ display: flex;
677
+ flex-direction: column;
678
+ gap: 4px;
679
+ }
680
+
681
+ .input-msg label {
682
+ font-size: 10px;
683
+ color: var(--text-muted);
684
+ text-transform: uppercase;
685
+ letter-spacing: 0.5px;
686
+ }
687
+
688
+ .input-msg textarea {
689
+ background: var(--surface-2);
690
+ color: var(--text);
691
+ border: 1px solid var(--border);
692
+ border-radius: 6px;
693
+ padding: 7px 10px;
694
+ font-size: 12px;
695
+ font-family: inherit;
696
+ resize: none;
697
+ height: 34px;
698
+ max-height: 80px;
699
+ outline: none;
700
+ }
701
+
702
+ .input-msg textarea:focus { border-color: var(--accent); }
703
+
704
+ .send-btn {
705
+ background: var(--accent);
706
+ color: #fff;
707
+ border: none;
708
+ border-radius: 6px;
709
+ padding: 7px 16px;
710
+ font-size: 12px;
711
+ font-weight: 600;
712
+ cursor: pointer;
713
+ white-space: nowrap;
714
+ transition: opacity 0.15s;
715
+ height: 34px;
716
+ }
717
+
718
+ .send-btn:hover { opacity: 0.85; }
719
+ .send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
720
+
721
+ /* ===== EMPTY STATE ===== */
722
+ .empty-state {
723
+ display: flex;
724
+ flex-direction: column;
725
+ align-items: center;
726
+ justify-content: center;
727
+ height: 100%;
728
+ color: var(--text-dim);
729
+ gap: 8px;
730
+ }
731
+
732
+ .empty-icon { font-size: 40px; opacity: 0.2; }
733
+ .empty-text { font-size: 15px; }
734
+ .empty-sub { font-size: 12px; color: var(--text-muted); }
735
+
736
+ /* ===== MESSAGE FLASH ===== */
737
+ .message-new { animation: flashIn 0.6s ease-out; }
738
+
739
+ @keyframes flashIn {
740
+ from { background: var(--accent-dim); }
741
+ to { background: transparent; }
742
+ }
743
+
744
+ /* ===== SCROLLBAR ===== */
745
+ ::-webkit-scrollbar { width: 6px; }
746
+ ::-webkit-scrollbar-track { background: transparent; }
747
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
748
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
749
+
750
+ /* ===== SCROLL TO BOTTOM ===== */
751
+ .scroll-bottom {
752
+ position: absolute;
753
+ bottom: 80px;
754
+ right: 24px;
755
+ background: var(--accent);
756
+ color: #fff;
757
+ border: none;
758
+ border-radius: 50%;
759
+ width: 36px;
760
+ height: 36px;
761
+ display: none;
762
+ align-items: center;
763
+ justify-content: center;
764
+ cursor: pointer;
765
+ font-size: 16px;
766
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
767
+ z-index: 10;
768
+ transition: opacity 0.2s;
769
+ }
770
+
771
+ .scroll-bottom.visible { display: flex; }
772
+ .scroll-bottom:hover { opacity: 0.85; }
773
+
774
+ .scroll-bottom .new-count {
775
+ position: absolute;
776
+ top: -6px;
777
+ right: -6px;
778
+ background: var(--red);
779
+ color: #fff;
780
+ font-size: 9px;
781
+ font-weight: 700;
782
+ min-width: 16px;
783
+ height: 16px;
784
+ border-radius: 8px;
785
+ display: flex;
786
+ align-items: center;
787
+ justify-content: center;
788
+ padding: 0 4px;
789
+ }
790
+
791
+ /* ===== SEARCH BAR ===== */
792
+ .search-bar {
793
+ padding: 8px 20px;
794
+ border-bottom: 1px solid var(--border);
795
+ background: var(--surface);
796
+ display: flex;
797
+ gap: 8px;
798
+ align-items: center;
799
+ }
800
+
801
+ .search-input {
802
+ flex: 1;
803
+ background: var(--surface-2);
804
+ color: var(--text);
805
+ border: 1px solid var(--border);
806
+ border-radius: 6px;
807
+ padding: 6px 10px;
808
+ font-size: 12px;
809
+ font-family: inherit;
810
+ outline: none;
811
+ }
812
+
813
+ .search-input:focus { border-color: var(--accent); }
814
+ .search-input::placeholder { color: var(--text-muted); }
815
+
816
+ .search-count {
817
+ font-size: 11px;
818
+ color: var(--text-muted);
819
+ white-space: nowrap;
820
+ }
821
+
822
+ /* ===== EMPTY STATE ONBOARDING ===== */
823
+ .onboard-steps {
824
+ text-align: left;
825
+ margin-top: 12px;
826
+ max-width: 400px;
827
+ }
828
+
829
+ .onboard-step {
830
+ display: flex;
831
+ align-items: flex-start;
832
+ gap: 10px;
833
+ margin-bottom: 10px;
834
+ font-size: 13px;
835
+ color: var(--text-dim);
836
+ }
837
+
838
+ .onboard-num {
839
+ background: var(--accent);
840
+ color: #fff;
841
+ width: 22px;
842
+ height: 22px;
843
+ border-radius: 50%;
844
+ display: flex;
845
+ align-items: center;
846
+ justify-content: center;
847
+ font-size: 11px;
848
+ font-weight: 700;
849
+ flex-shrink: 0;
850
+ }
851
+
852
+ .onboard-step code {
853
+ background: var(--surface-2);
854
+ padding: 1px 5px;
855
+ border-radius: 4px;
856
+ font-size: 12px;
857
+ font-family: 'SFMono-Regular', Consolas, monospace;
858
+ color: var(--orange);
859
+ }
860
+
861
+ /* ===== SYSTEM MESSAGE ===== */
862
+ .message.system-msg {
863
+ justify-content: center;
864
+ padding: 6px 14px;
865
+ }
866
+
867
+ .message.system-msg .msg-body {
868
+ max-width: 600px;
869
+ text-align: center;
870
+ }
871
+
872
+ .message.system-msg .msg-content {
873
+ color: var(--text-dim);
874
+ font-style: italic;
875
+ font-size: 12px;
876
+ }
877
+
878
+ /* ===== RESPONSIVE ===== */
879
+ @media (max-width: 768px) {
880
+ :root {
881
+ --sidebar-w: 260px;
882
+ }
883
+
884
+ .mobile-toggle { display: block; }
885
+
886
+ .sidebar {
887
+ position: fixed;
888
+ top: var(--header-h);
889
+ left: 0;
890
+ bottom: 0;
891
+ z-index: 90;
892
+ transform: translateX(-100%);
893
+ box-shadow: 4px 0 20px rgba(0,0,0,0.3);
894
+ }
895
+
896
+ .sidebar.open { transform: translateX(0); }
897
+
898
+ .sidebar-overlay {
899
+ display: none;
900
+ position: fixed;
901
+ inset: 0;
902
+ top: var(--header-h);
903
+ background: rgba(0,0,0,0.5);
904
+ z-index: 89;
905
+ }
906
+
907
+ .sidebar-overlay.open { display: block; }
908
+
909
+ .header-stats { display: none; }
910
+
911
+ .msg-input-bar {
912
+ flex-wrap: wrap;
913
+ }
914
+
915
+ .input-target { min-width: 80px; }
916
+
917
+ .messages-area { padding: 12px; }
918
+ }
919
+
920
+ @media (max-width: 480px) {
921
+ .header { padding: 0 12px; }
922
+ .logo { font-size: 15px; }
923
+ .msg-input-bar { padding: 10px 12px; }
924
+ }
925
+ </style>
926
+ </head>
927
+ <body>
928
+
929
+ <!-- HEADER -->
930
+ <div class="header">
931
+ <div class="header-left">
932
+ <button class="mobile-toggle" onclick="toggleSidebar()" aria-label="Menu">&#9776;</button>
933
+ <div class="logo">Let Them Talk</div>
934
+ <div class="header-stats">
935
+ <div class="h-stat"><span class="h-stat-val" id="stat-messages">0</span> msgs</div>
936
+ <div class="h-stat"><span class="h-stat-val" id="stat-agents">0</span> agents</div>
937
+ <div class="h-stat"><span class="h-stat-val" id="stat-sleeping" style="color:var(--orange)">0</span> sleeping</div>
938
+ <div class="h-stat"><span class="h-stat-val" id="stat-threads">0</span> threads</div>
939
+ </div>
940
+ </div>
941
+ <div class="header-actions">
942
+ <div class="connection"><span class="conn-dot"></span><span>Live</span></div>
943
+ <button class="btn btn-primary" onclick="exportConversation()" title="Export as Markdown">Export</button>
944
+ <button class="btn btn-danger" onclick="doReset()">Reset</button>
945
+ </div>
946
+ </div>
947
+
948
+ <!-- APP LAYOUT -->
949
+ <div class="app">
950
+ <div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
951
+
952
+ <!-- SIDEBAR -->
953
+ <div class="sidebar" id="sidebar">
954
+ <!-- Project Switcher -->
955
+ <div class="project-switcher">
956
+ <select class="project-select" id="project-select" onchange="switchProject()">
957
+ <option value="">Default (local)</option>
958
+ </select>
959
+ <input class="project-input" id="project-path-input" placeholder="Enter project folder path..."
960
+ onkeydown="if(event.key==='Enter'){addProject();}">
961
+ <div class="project-actions">
962
+ <button class="btn btn-primary" onclick="showAddProject()">+ Add</button>
963
+ <button class="btn btn-danger" onclick="removeProject()" id="remove-project-btn" style="display:none">Remove</button>
964
+ </div>
965
+ </div>
966
+ <div class="sidebar-scroll">
967
+ <!-- Agents Section -->
968
+ <div class="sidebar-section">
969
+ <div class="sidebar-title">
970
+ <span>Agents</span>
971
+ <span class="alert-count" id="alert-count" style="display:none">0</span>
972
+ </div>
973
+ <div id="agents-list"></div>
974
+ </div>
975
+
976
+ <!-- Threads Section -->
977
+ <div class="sidebar-section">
978
+ <div class="sidebar-title"><span>Threads</span></div>
979
+ <div id="threads-list"></div>
980
+ <div class="filter-clear" id="filter-clear" onclick="clearThreadFilter()">&#x2715; Clear filter</div>
981
+ </div>
982
+ </div>
983
+ </div>
984
+
985
+ <!-- MAIN -->
986
+ <div class="main" style="position:relative">
987
+ <div class="search-bar">
988
+ <input class="search-input" id="search-input" placeholder="Search messages..." oninput="onSearch()">
989
+ <span class="search-count" id="search-count"></span>
990
+ </div>
991
+ <div class="messages-area" id="messages"></div>
992
+ <button class="scroll-bottom" id="scroll-bottom" onclick="scrollToBottom()">&#x2193;<span class="new-count" id="new-msg-count" style="display:none">0</span></button>
993
+
994
+ <!-- MESSAGE INPUT -->
995
+ <div class="msg-input-bar">
996
+ <div class="input-target">
997
+ <label>Send to</label>
998
+ <select id="inject-target">
999
+ <option value="">Select agent...</option>
1000
+ </select>
1001
+ </div>
1002
+ <div class="input-msg">
1003
+ <label>Message</label>
1004
+ <textarea id="inject-content" placeholder="Type a message to inject..." rows="1"
1005
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();doInject();}"></textarea>
1006
+ </div>
1007
+ <button class="send-btn" onclick="doInject()" id="inject-btn" disabled>Send</button>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <script>
1013
+ var POLL_INTERVAL = 2000;
1014
+ var SLEEP_THRESHOLD = 60; // seconds
1015
+ var lastMessageCount = 0;
1016
+ var autoScroll = true;
1017
+ var activeThread = null;
1018
+ var activeProject = ''; // empty = default/local
1019
+ var cachedHistory = [];
1020
+ var cachedAgents = {};
1021
+
1022
+ // Agent color palette
1023
+ var COLORS = [
1024
+ '#58a6ff','#3fb950','#d29922','#f85149','#bc8cff',
1025
+ '#f778ba','#79c0ff','#7ee787','#e3b341','#ffa198'
1026
+ ];
1027
+ var agentColors = {};
1028
+ var colorIdx = 0;
1029
+
1030
+ function getColor(name) {
1031
+ if (!agentColors[name]) { agentColors[name] = COLORS[colorIdx % COLORS.length]; colorIdx++; }
1032
+ return agentColors[name];
1033
+ }
1034
+
1035
+ function initial(name) { return name.charAt(0).toUpperCase(); }
1036
+
1037
+ function formatTime(ts) {
1038
+ return new Date(ts).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit', second:'2-digit' });
1039
+ }
1040
+
1041
+ function timeAgo(ts) {
1042
+ var secs = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
1043
+ if (secs < 5) return 'just now';
1044
+ if (secs < 60) return secs + 's ago';
1045
+ var mins = Math.floor(secs / 60);
1046
+ if (mins < 60) return mins + 'm ago';
1047
+ var hrs = Math.floor(mins / 60);
1048
+ return hrs + 'h ago';
1049
+ }
1050
+
1051
+ function escapeHtml(text) {
1052
+ var d = document.createElement('div');
1053
+ d.textContent = text;
1054
+ return d.innerHTML;
1055
+ }
1056
+
1057
+ // ==================== MARKDOWN RENDERER ====================
1058
+
1059
+ function renderMarkdown(text) {
1060
+ // Extract fenced code blocks first to protect them
1061
+ var codeBlocks = [];
1062
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(match, lang, code) {
1063
+ var idx = codeBlocks.length;
1064
+ codeBlocks.push({ lang: lang, code: code.replace(/\n$/, '') });
1065
+ return '\x00CODEBLOCK' + idx + '\x00';
1066
+ });
1067
+ // Also handle ``` without newline after lang
1068
+ text = text.replace(/```(\w*)([\s\S]*?)```/g, function(match, lang, code) {
1069
+ var idx = codeBlocks.length;
1070
+ codeBlocks.push({ lang: lang, code: code.replace(/^\n/, '').replace(/\n$/, '') });
1071
+ return '\x00CODEBLOCK' + idx + '\x00';
1072
+ });
1073
+
1074
+ // Split into lines for block-level processing
1075
+ var lines = text.split('\n');
1076
+ var html = '';
1077
+ var inList = false;
1078
+ var listType = '';
1079
+ var inTable = false;
1080
+ var tableRows = [];
1081
+ var inBlockquote = false;
1082
+
1083
+ for (var i = 0; i < lines.length; i++) {
1084
+ var line = lines[i];
1085
+
1086
+ // Code block placeholder
1087
+ var cbMatch = line.match(/^\x00CODEBLOCK(\d+)\x00$/);
1088
+ if (cbMatch) {
1089
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1090
+ if (inTable) { html += renderTable(tableRows); inTable = false; tableRows = []; }
1091
+ if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
1092
+ var cb = codeBlocks[parseInt(cbMatch[1])];
1093
+ var langTag = cb.lang ? '<span class="lang-tag">' + escapeHtml(cb.lang) + '</span>' : '';
1094
+ html += '<pre>' + langTag + '<code>' + escapeHtml(cb.code) + '</code></pre>';
1095
+ continue;
1096
+ }
1097
+
1098
+ // Horizontal rule
1099
+ if (/^(---|\*\*\*|___)$/.test(line.trim())) {
1100
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1101
+ if (inTable) { html += renderTable(tableRows); inTable = false; tableRows = []; }
1102
+ html += '<hr>';
1103
+ continue;
1104
+ }
1105
+
1106
+ // Table detection
1107
+ if (line.indexOf('|') !== -1 && line.trim().charAt(0) === '|') {
1108
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1109
+ if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
1110
+ // Check if this is a separator row
1111
+ if (/^\|[\s\-:|]+\|$/.test(line.trim())) {
1112
+ // separator, skip but mark table as started
1113
+ if (!inTable) inTable = true;
1114
+ continue;
1115
+ }
1116
+ inTable = true;
1117
+ tableRows.push(line);
1118
+ continue;
1119
+ } else if (inTable) {
1120
+ html += renderTable(tableRows);
1121
+ inTable = false;
1122
+ tableRows = [];
1123
+ }
1124
+
1125
+ // Headers
1126
+ var hMatch = line.match(/^(#{1,4})\s+(.+)/);
1127
+ if (hMatch) {
1128
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1129
+ if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
1130
+ var level = hMatch[1].length;
1131
+ html += '<h' + level + '>' + inlineMarkdown(hMatch[2]) + '</h' + level + '>';
1132
+ continue;
1133
+ }
1134
+
1135
+ // Blockquote
1136
+ if (line.match(/^>\s?(.*)$/)) {
1137
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1138
+ var bqContent = line.replace(/^>\s?/, '');
1139
+ if (!inBlockquote) { html += '<blockquote>'; inBlockquote = true; }
1140
+ html += inlineMarkdown(bqContent) + '<br>';
1141
+ continue;
1142
+ } else if (inBlockquote) {
1143
+ html += '</blockquote>';
1144
+ inBlockquote = false;
1145
+ }
1146
+
1147
+ // Unordered list
1148
+ if (line.match(/^[\s]*[-*+]\s+(.+)/)) {
1149
+ if (!inList || listType !== 'ul') {
1150
+ if (inList) html += '</' + listType + '>';
1151
+ html += '<ul>';
1152
+ inList = true;
1153
+ listType = 'ul';
1154
+ }
1155
+ html += '<li>' + inlineMarkdown(line.replace(/^[\s]*[-*+]\s+/, '')) + '</li>';
1156
+ continue;
1157
+ }
1158
+
1159
+ // Ordered list
1160
+ if (line.match(/^[\s]*\d+\.\s+(.+)/)) {
1161
+ if (!inList || listType !== 'ol') {
1162
+ if (inList) html += '</' + listType + '>';
1163
+ html += '<ol>';
1164
+ inList = true;
1165
+ listType = 'ol';
1166
+ }
1167
+ html += '<li>' + inlineMarkdown(line.replace(/^[\s]*\d+\.\s+/, '')) + '</li>';
1168
+ continue;
1169
+ }
1170
+
1171
+ // Close list if line doesn't match
1172
+ if (inList && line.trim() === '') {
1173
+ html += '</' + listType + '>';
1174
+ inList = false;
1175
+ }
1176
+
1177
+ // Empty line
1178
+ if (line.trim() === '') {
1179
+ continue;
1180
+ }
1181
+
1182
+ // Regular paragraph
1183
+ if (inList) { html += '</' + listType + '>'; inList = false; }
1184
+ html += '<p>' + inlineMarkdown(line) + '</p>';
1185
+ }
1186
+
1187
+ // Close any open elements
1188
+ if (inList) html += '</' + listType + '>';
1189
+ if (inTable) html += renderTable(tableRows);
1190
+ if (inBlockquote) html += '</blockquote>';
1191
+
1192
+ return html;
1193
+ }
1194
+
1195
+ function inlineMarkdown(text) {
1196
+ var html = escapeHtml(text);
1197
+ // Inline code (must be before bold/italic to avoid conflicts)
1198
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1199
+ // Bold + italic
1200
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
1201
+ // Bold
1202
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1203
+ // Italic
1204
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
1205
+ // Strikethrough
1206
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
1207
+ return html;
1208
+ }
1209
+
1210
+ function renderTable(rows) {
1211
+ if (rows.length === 0) return '';
1212
+ var html = '<table>';
1213
+ for (var i = 0; i < rows.length; i++) {
1214
+ var cells = rows[i].split('|').filter(function(c, idx, arr) {
1215
+ return idx > 0 && idx < arr.length - 1;
1216
+ });
1217
+ var tag = (i === 0) ? 'th' : 'td';
1218
+ html += '<tr>';
1219
+ for (var j = 0; j < cells.length; j++) {
1220
+ html += '<' + tag + '>' + inlineMarkdown(cells[j].trim()) + '</' + tag + '>';
1221
+ }
1222
+ html += '</tr>';
1223
+ }
1224
+ html += '</table>';
1225
+ return html;
1226
+ }
1227
+
1228
+ // ==================== AGENT MONITORING ====================
1229
+
1230
+ function renderAgents(agents) {
1231
+ var el = document.getElementById('agents-list');
1232
+ var keys = Object.keys(agents);
1233
+ if (!keys.length) {
1234
+ el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No agents registered</div>';
1235
+ return;
1236
+ }
1237
+
1238
+ var sleepCount = 0;
1239
+ var html = '';
1240
+ for (var i = 0; i < keys.length; i++) {
1241
+ var name = keys[i];
1242
+ var info = agents[name];
1243
+ var color = getColor(name);
1244
+ // Use server-provided status if available, fallback to client-side detection
1245
+ var state = info.status || (info.alive ? 'active' : 'dead');
1246
+ if (state === 'sleeping') sleepCount++;
1247
+
1248
+ var activityText = '';
1249
+ if (state === 'dead') {
1250
+ activityText = 'Process not running';
1251
+ } else if (info.idle_seconds != null) {
1252
+ if (info.idle_seconds < 5) activityText = 'Active just now';
1253
+ else if (info.idle_seconds < 60) activityText = 'Active ' + info.idle_seconds + 's ago';
1254
+ else activityText = 'Idle ' + Math.floor(info.idle_seconds / 60) + 'm ' + (info.idle_seconds % 60) + 's';
1255
+ } else if (info.last_message) {
1256
+ activityText = 'Last msg ' + timeAgo(info.last_message);
1257
+ } else {
1258
+ activityText = 'Registered ' + timeAgo(info.registered_at);
1259
+ }
1260
+
1261
+ var stateLabel = state.charAt(0).toUpperCase() + state.slice(1);
1262
+ var cardClass = 'agent-card' + (state !== 'active' ? ' ' + state : '');
1263
+
1264
+ var nudgeHtml = '';
1265
+ if (state === 'sleeping' || (state !== 'dead' && !info.is_listening)) {
1266
+ nudgeHtml = '<button class="nudge-btn" onclick="sendNudge(\'' + escapeHtml(name) + '\')">Send Nudge</button>';
1267
+ }
1268
+
1269
+ // Listening status — simplified: skip for dead (already shown via badge)
1270
+ var listenHtml = '';
1271
+ if (state !== 'dead') {
1272
+ if (info.is_listening) {
1273
+ var sinceTxt = info.listening_since ? ' since ' + formatTime(info.listening_since) : '';
1274
+ listenHtml = '<div class="listen-badge listening" title="Agent is waiting for messages — can receive dashboard injections"><span class="listen-dot on"></span>Listening' + sinceTxt + '</div>';
1275
+ } else if (state === 'active') {
1276
+ listenHtml = '<div class="listen-badge busy" title="Agent is working, not currently listening for messages"><span class="listen-dot off"></span>Busy</div>';
1277
+ } else {
1278
+ listenHtml = '<div class="listen-badge not-listening" title="Agent is not listening. Send a nudge or type \'listen\' in the terminal."><span class="listen-dot off"></span>Not Listening</div>';
1279
+ }
1280
+ }
1281
+
1282
+ html += '<div class="' + cardClass + '">' +
1283
+ '<div class="agent-top">' +
1284
+ '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>' +
1285
+ '<div class="agent-info">' +
1286
+ '<div class="agent-name" style="color:' + color + '">' +
1287
+ escapeHtml(name) +
1288
+ ' <span class="agent-badge ' + state + '">' + stateLabel + '</span>' +
1289
+ '</div>' +
1290
+ '<div class="agent-meta"><span>PID ' + info.pid + '</span></div>' +
1291
+ '</div>' +
1292
+ '</div>' +
1293
+ '<div class="agent-activity">' +
1294
+ '<span class="agent-activity-icon ' + state + '"></span>' +
1295
+ activityText +
1296
+ '</div>' +
1297
+ listenHtml +
1298
+ nudgeHtml +
1299
+ '</div>';
1300
+ }
1301
+ el.innerHTML = html;
1302
+
1303
+ // Update alert badge
1304
+ var alertEl = document.getElementById('alert-count');
1305
+ if (sleepCount > 0) {
1306
+ alertEl.textContent = sleepCount;
1307
+ alertEl.style.display = '';
1308
+ } else {
1309
+ alertEl.style.display = 'none';
1310
+ }
1311
+
1312
+ // Update inject target dropdown
1313
+ updateInjectTargets(keys);
1314
+ }
1315
+
1316
+ function sendNudge(agentName) {
1317
+ var body = JSON.stringify({ to: agentName, content: 'Hey ' + agentName + ', the user is waiting for you. Please check for new messages and continue your work.' });
1318
+ fetch('/api/inject' + projectParam(), {
1319
+ method: 'POST',
1320
+ headers: { 'Content-Type': 'application/json' },
1321
+ body: body
1322
+ }).then(function(r) { return r.json(); }).then(function() {
1323
+ poll();
1324
+ }).catch(function(e) { console.error('Nudge failed:', e); });
1325
+ }
1326
+
1327
+ // ==================== INJECT TARGETS ====================
1328
+
1329
+ var lastAgentKeys = '';
1330
+
1331
+ function updateInjectTargets(keys) {
1332
+ var joined = keys.join(',');
1333
+ if (joined === lastAgentKeys) return;
1334
+ lastAgentKeys = joined;
1335
+
1336
+ var sel = document.getElementById('inject-target');
1337
+ var current = sel.value;
1338
+ sel.innerHTML = '<option value="">Select agent...</option>';
1339
+ for (var i = 0; i < keys.length; i++) {
1340
+ var opt = document.createElement('option');
1341
+ opt.value = keys[i];
1342
+ opt.textContent = keys[i];
1343
+ sel.appendChild(opt);
1344
+ }
1345
+ // Broadcast option
1346
+ if (keys.length > 1) {
1347
+ var allOpt = document.createElement('option');
1348
+ allOpt.value = '__all__';
1349
+ allOpt.textContent = 'All agents';
1350
+ sel.appendChild(allOpt);
1351
+ }
1352
+
1353
+ if (current) sel.value = current;
1354
+ updateSendBtn();
1355
+ }
1356
+
1357
+ function updateSendBtn() {
1358
+ var target = document.getElementById('inject-target').value;
1359
+ var content = document.getElementById('inject-content').value.trim();
1360
+ document.getElementById('inject-btn').disabled = !target || !content;
1361
+ }
1362
+
1363
+ document.getElementById('inject-target').addEventListener('change', updateSendBtn);
1364
+ document.getElementById('inject-content').addEventListener('input', updateSendBtn);
1365
+
1366
+ function projectParam() {
1367
+ return activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
1368
+ }
1369
+
1370
+ function doInject() {
1371
+ var target = document.getElementById('inject-target').value;
1372
+ var content = document.getElementById('inject-content').value.trim();
1373
+ if (!target || !content) return;
1374
+
1375
+ var body = JSON.stringify({ to: target, content: content });
1376
+ fetch('/api/inject' + projectParam(), {
1377
+ method: 'POST',
1378
+ headers: { 'Content-Type': 'application/json' },
1379
+ body: body
1380
+ }).then(function(r) { return r.json(); }).then(function(res) {
1381
+ if (res.success) {
1382
+ document.getElementById('inject-content').value = '';
1383
+ updateSendBtn();
1384
+ poll();
1385
+ }
1386
+ }).catch(function(e) { console.error('Inject failed:', e); });
1387
+ }
1388
+
1389
+ // ==================== THREADS ====================
1390
+
1391
+ function renderThreads(messages) {
1392
+ var threads = {};
1393
+ for (var i = 0; i < messages.length; i++) {
1394
+ var m = messages[i];
1395
+ if (!m.thread_id) continue;
1396
+ if (!threads[m.thread_id]) threads[m.thread_id] = { count: 0, preview: '', agents: {} };
1397
+ threads[m.thread_id].count++;
1398
+ threads[m.thread_id].agents[m.from] = true;
1399
+ if (!threads[m.thread_id].preview) threads[m.thread_id].preview = m.content.substring(0, 50);
1400
+ }
1401
+
1402
+ var el = document.getElementById('threads-list');
1403
+ var clearEl = document.getElementById('filter-clear');
1404
+ var ids = Object.keys(threads);
1405
+
1406
+ if (!ids.length) {
1407
+ el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px;">No threads yet</div>';
1408
+ clearEl.classList.remove('visible');
1409
+ return;
1410
+ }
1411
+
1412
+ clearEl.classList.toggle('visible', !!activeThread);
1413
+
1414
+ var html = '';
1415
+ for (var j = 0; j < ids.length; j++) {
1416
+ var tid = ids[j];
1417
+ var t = threads[tid];
1418
+ var active = activeThread === tid ? ' active' : '';
1419
+ var agentList = Object.keys(t.agents).join(', ');
1420
+ html += '<div class="thread-item' + active + '" onclick="filterThread(\'' + tid + '\')">' +
1421
+ '<div class="thread-preview">' + escapeHtml(t.preview) + '</div>' +
1422
+ '<div class="thread-meta">' + t.count + ' msgs &middot; ' + agentList + '</div>' +
1423
+ '</div>';
1424
+ }
1425
+ el.innerHTML = html;
1426
+ }
1427
+
1428
+ // ==================== MESSAGES ====================
1429
+
1430
+ function renderMessages(messages) {
1431
+ var el = document.getElementById('messages');
1432
+
1433
+ if (!messages.length) {
1434
+ el.innerHTML = '<div class="empty-state">' +
1435
+ '<div class="empty-icon">&#x1f4ac;</div>' +
1436
+ '<div class="empty-text">No messages yet</div>' +
1437
+ '<div class="empty-sub">Get two AI agents talking in minutes</div>' +
1438
+ '<div class="onboard-steps">' +
1439
+ '<div class="onboard-step"><span class="onboard-num">1</span><span>Open two terminals and run <code>claude</code> in each</span></div>' +
1440
+ '<div class="onboard-step"><span class="onboard-num">2</span><span>Tell Terminal 1: <code>Register as A, say hello, then listen</code></span></div>' +
1441
+ '<div class="onboard-step"><span class="onboard-num">3</span><span>Tell Terminal 2: <code>Register as B, then listen</code></span></div>' +
1442
+ '<div class="onboard-step"><span class="onboard-num">4</span><span>Watch them talk here in real time!</span></div>' +
1443
+ '</div>' +
1444
+ '</div>';
1445
+ return;
1446
+ }
1447
+
1448
+ var filtered = messages;
1449
+ if (activeThread) {
1450
+ filtered = messages.filter(function(m) {
1451
+ return m.thread_id === activeThread || m.id === activeThread;
1452
+ });
1453
+ }
1454
+
1455
+ // Search filter
1456
+ if (searchQuery) {
1457
+ filtered = filtered.filter(function(m) {
1458
+ return m.content.toLowerCase().indexOf(searchQuery) !== -1 ||
1459
+ m.from.toLowerCase().indexOf(searchQuery) !== -1 ||
1460
+ m.to.toLowerCase().indexOf(searchQuery) !== -1;
1461
+ });
1462
+ document.getElementById('search-count').textContent = filtered.length + ' result' + (filtered.length !== 1 ? 's' : '');
1463
+ } else {
1464
+ document.getElementById('search-count').textContent = '';
1465
+ }
1466
+
1467
+ var isNew = filtered.length > lastMessageCount;
1468
+ // Track new messages while scrolled up
1469
+ if (isNew && !autoScroll) {
1470
+ newWhileScrolled += (filtered.length - lastMessageCount);
1471
+ var countEl = document.getElementById('new-msg-count');
1472
+ countEl.textContent = newWhileScrolled;
1473
+ countEl.style.display = '';
1474
+ document.getElementById('scroll-bottom').classList.add('visible');
1475
+ }
1476
+ var html = '';
1477
+ for (var i = 0; i < filtered.length; i++) {
1478
+ var m = filtered[i];
1479
+ var color = getColor(m.from);
1480
+ var newClass = (isNew && i >= lastMessageCount) ? ' message-new' : '';
1481
+ var isSystem = m.system === true;
1482
+
1483
+ var badges = '';
1484
+ if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
1485
+ if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
1486
+
1487
+ if (isSystem) {
1488
+ html += '<div class="message system-msg' + newClass + '">' +
1489
+ '<div class="msg-body">' +
1490
+ '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
1491
+ '</div></div>';
1492
+ } else {
1493
+ html += '<div class="message' + newClass + '">' +
1494
+ '<div class="msg-avatar" style="background:' + color + '">' + initial(m.from) + '</div>' +
1495
+ '<div class="msg-body">' +
1496
+ '<div class="msg-header">' +
1497
+ '<span class="msg-from" style="color:' + color + '">' + escapeHtml(m.from) + '</span>' +
1498
+ '<span class="msg-arrow">&rarr;</span>' +
1499
+ '<span class="msg-to">' + escapeHtml(m.to) + '</span>' +
1500
+ '<span class="msg-time">' + formatTime(m.timestamp) + '</span>' +
1501
+ '<div class="msg-badges">' + badges + '</div>' +
1502
+ '</div>' +
1503
+ '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
1504
+ '</div></div>';
1505
+ }
1506
+ }
1507
+ el.innerHTML = html;
1508
+ lastMessageCount = filtered.length;
1509
+
1510
+ if (autoScroll && isNew) {
1511
+ el.scrollTop = el.scrollHeight;
1512
+ }
1513
+ }
1514
+
1515
+ // ==================== FILTERS & CONTROLS ====================
1516
+
1517
+ function filterThread(tid) {
1518
+ activeThread = activeThread === tid ? null : tid;
1519
+ lastMessageCount = 0;
1520
+ renderThreads(cachedHistory);
1521
+ renderMessages(cachedHistory);
1522
+ }
1523
+
1524
+ function clearThreadFilter() {
1525
+ activeThread = null;
1526
+ lastMessageCount = 0;
1527
+ renderThreads(cachedHistory);
1528
+ renderMessages(cachedHistory);
1529
+ }
1530
+
1531
+ function toggleSidebar() {
1532
+ document.getElementById('sidebar').classList.toggle('open');
1533
+ document.getElementById('sidebar-overlay').classList.toggle('open');
1534
+ }
1535
+
1536
+ var newWhileScrolled = 0;
1537
+
1538
+ document.getElementById('messages').addEventListener('scroll', function() {
1539
+ var atBottom = (this.scrollHeight - this.scrollTop - this.clientHeight) < 50;
1540
+ autoScroll = atBottom;
1541
+ var scrollBtn = document.getElementById('scroll-bottom');
1542
+ if (atBottom) {
1543
+ scrollBtn.classList.remove('visible');
1544
+ newWhileScrolled = 0;
1545
+ document.getElementById('new-msg-count').style.display = 'none';
1546
+ } else {
1547
+ scrollBtn.classList.add('visible');
1548
+ }
1549
+ });
1550
+
1551
+ function scrollToBottom() {
1552
+ var el = document.getElementById('messages');
1553
+ el.scrollTop = el.scrollHeight;
1554
+ autoScroll = true;
1555
+ newWhileScrolled = 0;
1556
+ document.getElementById('scroll-bottom').classList.remove('visible');
1557
+ document.getElementById('new-msg-count').style.display = 'none';
1558
+ }
1559
+
1560
+ // ==================== SEARCH ====================
1561
+
1562
+ var searchQuery = '';
1563
+
1564
+ function onSearch() {
1565
+ searchQuery = document.getElementById('search-input').value.toLowerCase().trim();
1566
+ lastMessageCount = 0;
1567
+ renderMessages(cachedHistory);
1568
+ }
1569
+
1570
+ // ==================== EXPORT ====================
1571
+
1572
+ function exportConversation() {
1573
+ if (!cachedHistory.length) return;
1574
+ var md = '# Let Them Talk — Conversation Export\n\n';
1575
+ md += 'Exported: ' + new Date().toLocaleString() + '\n\n---\n\n';
1576
+ for (var i = 0; i < cachedHistory.length; i++) {
1577
+ var m = cachedHistory[i];
1578
+ md += '**' + m.from + '** → ' + m.to + ' _(' + formatTime(m.timestamp) + ')_\n\n';
1579
+ md += m.content + '\n\n---\n\n';
1580
+ }
1581
+ var blob = new Blob([md], { type: 'text/markdown' });
1582
+ var url = URL.createObjectURL(blob);
1583
+ var a = document.createElement('a');
1584
+ a.href = url;
1585
+ a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '.md';
1586
+ a.click();
1587
+ URL.revokeObjectURL(url);
1588
+ }
1589
+
1590
+ // ==================== AUTO-GROW TEXTAREA ====================
1591
+
1592
+ var textarea = document.getElementById('inject-content');
1593
+ textarea.addEventListener('input', function() {
1594
+ this.style.height = '34px';
1595
+ var scrollH = this.scrollHeight;
1596
+ this.style.height = Math.min(scrollH, 100) + 'px';
1597
+ });
1598
+
1599
+ // ==================== POLLING ====================
1600
+
1601
+ function poll() {
1602
+ var pp = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
1603
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
1604
+ Promise.all([
1605
+ fetch('/api/history?limit=500' + pp).then(function(r) { return r.json(); }),
1606
+ fetch('/api/agents' + pq).then(function(r) { return r.json(); }),
1607
+ fetch('/api/status' + pq).then(function(r) { return r.json(); }),
1608
+ ]).then(function(results) {
1609
+ cachedHistory = results[0];
1610
+ cachedAgents = results[1];
1611
+ var status = results[2];
1612
+
1613
+ document.getElementById('stat-messages').textContent = status.messageCount;
1614
+ document.getElementById('stat-agents').textContent = status.agentCount;
1615
+ document.getElementById('stat-sleeping').textContent = status.sleepingCount || 0;
1616
+ document.getElementById('stat-threads').textContent = status.threadCount;
1617
+
1618
+ renderAgents(cachedAgents);
1619
+ renderThreads(cachedHistory);
1620
+ renderMessages(cachedHistory);
1621
+ }).catch(function(e) {
1622
+ console.error('Poll failed:', e);
1623
+ });
1624
+ }
1625
+
1626
+ function doReset() {
1627
+ if (!confirm('Clear all messages, agents, and history?')) return;
1628
+ fetch('/api/reset', { method: 'POST' }).then(function() {
1629
+ lastMessageCount = 0;
1630
+ activeThread = null;
1631
+ cachedHistory = [];
1632
+ cachedAgents = {};
1633
+ poll();
1634
+ }).catch(function(e) { console.error('Reset failed:', e); });
1635
+ }
1636
+
1637
+ // ==================== PROJECT MANAGEMENT ====================
1638
+
1639
+ function loadProjects() {
1640
+ fetch('/api/projects').then(function(r) { return r.json(); }).then(function(projects) {
1641
+ var sel = document.getElementById('project-select');
1642
+ // Keep the first option (Default)
1643
+ while (sel.options.length > 1) sel.remove(1);
1644
+ for (var i = 0; i < projects.length; i++) {
1645
+ var opt = document.createElement('option');
1646
+ opt.value = projects[i].path;
1647
+ opt.textContent = projects[i].name;
1648
+ sel.appendChild(opt);
1649
+ }
1650
+ if (activeProject) sel.value = activeProject;
1651
+
1652
+ // Show/hide remove button
1653
+ document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
1654
+ }).catch(function() {});
1655
+ }
1656
+
1657
+ function switchProject() {
1658
+ activeProject = document.getElementById('project-select').value;
1659
+ lastMessageCount = 0;
1660
+ document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
1661
+ poll();
1662
+ }
1663
+
1664
+ function showAddProject() {
1665
+ var input = document.getElementById('project-path-input');
1666
+ if (input.classList.contains('visible')) {
1667
+ input.classList.remove('visible');
1668
+ } else {
1669
+ input.classList.add('visible');
1670
+ input.focus();
1671
+ }
1672
+ }
1673
+
1674
+ function addProject() {
1675
+ var input = document.getElementById('project-path-input');
1676
+ var projectPath = input.value.trim();
1677
+ if (!projectPath) return;
1678
+
1679
+ fetch('/api/projects', {
1680
+ method: 'POST',
1681
+ headers: { 'Content-Type': 'application/json' },
1682
+ body: JSON.stringify({ path: projectPath })
1683
+ }).then(function(r) { return r.json(); }).then(function(res) {
1684
+ if (res.success) {
1685
+ input.value = '';
1686
+ input.classList.remove('visible');
1687
+ loadProjects();
1688
+ } else {
1689
+ alert(res.error || 'Failed to add project');
1690
+ }
1691
+ }).catch(function(e) { console.error('Add project failed:', e); });
1692
+ }
1693
+
1694
+ function removeProject() {
1695
+ if (!activeProject) return;
1696
+ if (!confirm('Remove this project from the dashboard?')) return;
1697
+
1698
+ fetch('/api/projects', {
1699
+ method: 'DELETE',
1700
+ headers: { 'Content-Type': 'application/json' },
1701
+ body: JSON.stringify({ path: activeProject })
1702
+ }).then(function(r) { return r.json(); }).then(function(res) {
1703
+ if (res.success) {
1704
+ activeProject = '';
1705
+ document.getElementById('project-select').value = '';
1706
+ loadProjects();
1707
+ poll();
1708
+ }
1709
+ }).catch(function(e) { console.error('Remove project failed:', e); });
1710
+ }
1711
+
1712
+ // ==================== INIT ====================
1713
+ loadProjects();
1714
+ poll();
1715
+ setInterval(poll, POLL_INTERVAL);
1716
+ </script>
1717
+
1718
+ </body>
1719
+ </html>