agent-relay 1.0.7 → 1.0.8

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.
Files changed (72) hide show
  1. package/README.md +18 -6
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +344 -3
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/daemon/agent-registry.d.ts +60 -0
  7. package/dist/daemon/agent-registry.d.ts.map +1 -0
  8. package/dist/daemon/agent-registry.js +158 -0
  9. package/dist/daemon/agent-registry.js.map +1 -0
  10. package/dist/daemon/connection.d.ts +11 -1
  11. package/dist/daemon/connection.d.ts.map +1 -1
  12. package/dist/daemon/connection.js +31 -2
  13. package/dist/daemon/connection.js.map +1 -1
  14. package/dist/daemon/index.d.ts +2 -0
  15. package/dist/daemon/index.d.ts.map +1 -1
  16. package/dist/daemon/index.js +2 -0
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/daemon/registry.d.ts +9 -0
  19. package/dist/daemon/registry.d.ts.map +1 -0
  20. package/dist/daemon/registry.js +9 -0
  21. package/dist/daemon/registry.js.map +1 -0
  22. package/dist/daemon/router.d.ts +34 -2
  23. package/dist/daemon/router.d.ts.map +1 -1
  24. package/dist/daemon/router.js +111 -1
  25. package/dist/daemon/router.js.map +1 -1
  26. package/dist/daemon/server.d.ts +1 -0
  27. package/dist/daemon/server.d.ts.map +1 -1
  28. package/dist/daemon/server.js +60 -13
  29. package/dist/daemon/server.js.map +1 -1
  30. package/dist/dashboard/public/index.html +625 -16
  31. package/dist/dashboard/server.d.ts +1 -1
  32. package/dist/dashboard/server.d.ts.map +1 -1
  33. package/dist/dashboard/server.js +125 -7
  34. package/dist/dashboard/server.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/protocol/types.d.ts +15 -1
  38. package/dist/protocol/types.d.ts.map +1 -1
  39. package/dist/storage/adapter.d.ts +53 -0
  40. package/dist/storage/adapter.d.ts.map +1 -1
  41. package/dist/storage/adapter.js +3 -0
  42. package/dist/storage/adapter.js.map +1 -1
  43. package/dist/storage/sqlite-adapter.d.ts +58 -1
  44. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  45. package/dist/storage/sqlite-adapter.js +374 -47
  46. package/dist/storage/sqlite-adapter.js.map +1 -1
  47. package/dist/utils/project-namespace.d.ts.map +1 -1
  48. package/dist/utils/project-namespace.js +22 -1
  49. package/dist/utils/project-namespace.js.map +1 -1
  50. package/dist/wrapper/client.d.ts +22 -3
  51. package/dist/wrapper/client.d.ts.map +1 -1
  52. package/dist/wrapper/client.js +59 -9
  53. package/dist/wrapper/client.js.map +1 -1
  54. package/dist/wrapper/parser.d.ts +110 -4
  55. package/dist/wrapper/parser.d.ts.map +1 -1
  56. package/dist/wrapper/parser.js +296 -84
  57. package/dist/wrapper/parser.js.map +1 -1
  58. package/dist/wrapper/tmux-wrapper.d.ts +100 -9
  59. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  60. package/dist/wrapper/tmux-wrapper.js +437 -77
  61. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  62. package/docs/AGENTS.md +27 -27
  63. package/docs/CHANGELOG.md +1 -1
  64. package/docs/DESIGN_V2.md +1079 -0
  65. package/docs/INTEGRATION-GUIDE.md +926 -0
  66. package/docs/PROPOSAL-trajectories.md +1582 -0
  67. package/docs/PROTOCOL.md +3 -3
  68. package/docs/SCALING_ANALYSIS.md +280 -0
  69. package/docs/TMUX_IMPLEMENTATION_NOTES.md +9 -9
  70. package/docs/TMUX_IMPROVEMENTS.md +968 -0
  71. package/docs/competitive-analysis-mcp-agent-mail.md +389 -0
  72. package/package.json +6 -2
@@ -2,6 +2,9 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Agent Relay</title>
5
+ <link rel="preconnect" href="https://fonts.googleapis.com">
6
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
5
8
  <style>
6
9
  :root {
7
10
  --bg-primary: #09090b;
@@ -26,13 +29,27 @@
26
29
  * { box-sizing: border-box; margin: 0; padding: 0; }
27
30
 
28
31
  body {
29
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
32
+ font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
30
33
  background: var(--bg-primary);
34
+ background-image:
35
+ linear-gradient(rgba(59, 130, 246, 0.03) 1px, transparent 1px),
36
+ linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px);
37
+ background-size: 20px 20px;
31
38
  color: var(--text);
32
39
  min-height: 100vh;
33
40
  overflow-x: hidden;
34
41
  }
35
42
 
43
+ @keyframes pulse-glow {
44
+ 0%, 100% { box-shadow: 0 0 4px currentColor, 0 0 8px currentColor; }
45
+ 50% { box-shadow: 0 0 8px currentColor, 0 0 16px currentColor; }
46
+ }
47
+
48
+ @keyframes slideIn {
49
+ from { opacity: 0; transform: translateY(-8px); }
50
+ to { opacity: 1; transform: translateY(0); }
51
+ }
52
+
36
53
  .container {
37
54
  max-width: 1400px;
38
55
  margin: 0 auto;
@@ -63,6 +80,7 @@
63
80
  align-items: center;
64
81
  padding: 16px 20px;
65
82
  border-bottom: 1px solid var(--border-color);
83
+ box-shadow: 0 1px 0 rgba(59, 130, 246, 0.2);
66
84
  }
67
85
 
68
86
  .logo {
@@ -115,8 +133,20 @@
115
133
  padding: 12px;
116
134
  border-radius: 8px;
117
135
  margin-bottom: 4px;
118
- transition: background 0.15s;
136
+ transition: background 0.15s, border-color 0.3s, opacity 0.3s;
119
137
  cursor: default;
138
+ border-left: 3px solid var(--success);
139
+ }
140
+
141
+ .agent-card.disconnected {
142
+ border-left-color: var(--error);
143
+ opacity: 0.6;
144
+ }
145
+
146
+ .agent-card.disconnected .status-dot {
147
+ background: var(--error);
148
+ color: var(--error);
149
+ animation: none;
120
150
  }
121
151
 
122
152
  .agent-card:hover {
@@ -174,6 +204,8 @@
174
204
  height: 6px;
175
205
  border-radius: 50%;
176
206
  background: var(--success);
207
+ color: var(--success);
208
+ animation: pulse-glow 2s ease-in-out infinite;
177
209
  }
178
210
 
179
211
  .badge {
@@ -214,6 +246,8 @@
214
246
  height: 6px;
215
247
  border-radius: 50%;
216
248
  background: var(--success);
249
+ color: var(--success);
250
+ animation: pulse-glow 2s ease-in-out infinite;
217
251
  }
218
252
 
219
253
  .log-content {
@@ -229,6 +263,22 @@
229
263
  display: flex;
230
264
  gap: 12px;
231
265
  transition: background 0.15s;
266
+ animation: slideIn 0.2s ease-out;
267
+ border-left: 3px solid transparent;
268
+ }
269
+
270
+ .message.broadcast {
271
+ border-left-color: var(--warning);
272
+ background: var(--warning-muted);
273
+ }
274
+
275
+ .message.broadcast .msg-target {
276
+ color: var(--warning);
277
+ font-weight: 600;
278
+ }
279
+
280
+ .message.direct {
281
+ border-left-color: var(--primary);
232
282
  }
233
283
 
234
284
  .message:hover {
@@ -283,6 +333,36 @@
283
333
  margin-left: auto;
284
334
  }
285
335
 
336
+ .thread-badge {
337
+ background: var(--primary-muted);
338
+ color: var(--primary);
339
+ padding: 2px 6px;
340
+ border-radius: 4px;
341
+ font-size: 0.7rem;
342
+ font-weight: 500;
343
+ }
344
+
345
+ .filter-section {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 8px;
349
+ }
350
+
351
+ .thread-filter {
352
+ background: var(--bg-secondary);
353
+ border: 1px solid var(--border-color);
354
+ border-radius: 6px;
355
+ color: var(--text);
356
+ font-size: 0.75rem;
357
+ padding: 4px 8px;
358
+ cursor: pointer;
359
+ }
360
+
361
+ .thread-filter:focus {
362
+ border-color: var(--primary);
363
+ outline: none;
364
+ }
365
+
286
366
  .msg-text {
287
367
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
288
368
  font-size: 0.8rem;
@@ -324,6 +404,36 @@
324
404
  background: var(--error) !important;
325
405
  }
326
406
 
407
+ @keyframes spin {
408
+ from { transform: rotate(0deg); }
409
+ to { transform: rotate(360deg); }
410
+ }
411
+
412
+ #connection-status.reconnecting,
413
+ #connection-status.connecting {
414
+ background: var(--warning-muted);
415
+ color: var(--warning);
416
+ }
417
+
418
+ #connection-status.reconnecting .dot,
419
+ #connection-status.connecting .dot {
420
+ background: transparent;
421
+ border: 2px solid var(--warning);
422
+ border-top-color: transparent;
423
+ animation: spin 0.8s linear infinite;
424
+ }
425
+
426
+ #connection-status .dot {
427
+ color: inherit;
428
+ animation: pulse-glow 2s ease-in-out infinite;
429
+ }
430
+
431
+ #connection-status .uptime {
432
+ font-size: 0.65rem;
433
+ opacity: 0.7;
434
+ margin-left: 4px;
435
+ }
436
+
327
437
  .empty-state {
328
438
  text-align: center;
329
439
  padding: 48px 24px;
@@ -335,6 +445,247 @@
335
445
  line-height: 1.6;
336
446
  }
337
447
 
448
+ /* Compose Section - Command Center Aesthetic */
449
+ .compose-section {
450
+ padding: 20px 24px;
451
+ border-top: 1px solid var(--border-color);
452
+ background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(9, 9, 11, 0.95) 100%);
453
+ position: relative;
454
+ }
455
+
456
+ .compose-section::before {
457
+ content: '';
458
+ position: absolute;
459
+ top: 0;
460
+ left: 24px;
461
+ right: 24px;
462
+ height: 1px;
463
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
464
+ opacity: 0.3;
465
+ }
466
+
467
+ .compose-wrapper {
468
+ display: flex;
469
+ gap: 16px;
470
+ align-items: stretch;
471
+ }
472
+
473
+ .compose-input-area {
474
+ flex: 1;
475
+ display: flex;
476
+ flex-direction: column;
477
+ gap: 10px;
478
+ }
479
+
480
+ .compose-meta-row {
481
+ display: flex;
482
+ align-items: center;
483
+ gap: 16px;
484
+ }
485
+
486
+ .compose-to {
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 10px;
490
+ }
491
+
492
+ .compose-to label {
493
+ font-size: 0.6875rem;
494
+ font-weight: 600;
495
+ color: var(--primary);
496
+ text-transform: uppercase;
497
+ letter-spacing: 0.12em;
498
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
499
+ }
500
+
501
+ .compose-to select {
502
+ background: var(--bg-primary);
503
+ border: 1px solid var(--border-color);
504
+ border-radius: 4px;
505
+ color: var(--text);
506
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
507
+ font-size: 0.8125rem;
508
+ padding: 8px 12px;
509
+ outline: none;
510
+ transition: all 0.2s ease;
511
+ cursor: pointer;
512
+ min-width: 180px;
513
+ appearance: none;
514
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
515
+ background-repeat: no-repeat;
516
+ background-position: right 10px center;
517
+ padding-right: 32px;
518
+ }
519
+
520
+ .compose-to select:hover {
521
+ border-color: var(--border-hover);
522
+ }
523
+
524
+ .compose-to select:focus {
525
+ border-color: var(--primary);
526
+ box-shadow: 0 0 0 3px var(--primary-muted), inset 0 0 20px rgba(59, 130, 246, 0.05);
527
+ }
528
+
529
+ .compose-hint {
530
+ font-size: 0.6875rem;
531
+ color: var(--text-muted);
532
+ margin-left: auto;
533
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
534
+ opacity: 0.7;
535
+ transition: opacity 0.2s;
536
+ }
537
+
538
+ .compose-section:focus-within .compose-hint {
539
+ opacity: 1;
540
+ }
541
+
542
+ .compose-hint kbd {
543
+ background: var(--bg-elevated);
544
+ border: 1px solid var(--border-color);
545
+ border-radius: 3px;
546
+ padding: 3px 6px;
547
+ font-family: inherit;
548
+ font-size: 0.625rem;
549
+ box-shadow: 0 1px 0 var(--border-color);
550
+ }
551
+
552
+ .compose-input-row {
553
+ display: flex;
554
+ gap: 12px;
555
+ align-items: stretch;
556
+ }
557
+
558
+ .compose-textarea-wrapper {
559
+ flex: 1;
560
+ position: relative;
561
+ }
562
+
563
+ .compose-textarea-wrapper::before {
564
+ content: '>';
565
+ position: absolute;
566
+ left: 14px;
567
+ top: 14px;
568
+ color: var(--primary);
569
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
570
+ font-size: 0.875rem;
571
+ font-weight: 600;
572
+ opacity: 0.6;
573
+ transition: opacity 0.2s;
574
+ pointer-events: none;
575
+ z-index: 1;
576
+ }
577
+
578
+ .compose-textarea-wrapper:focus-within::before {
579
+ opacity: 1;
580
+ animation: blink 1.2s step-end infinite;
581
+ }
582
+
583
+ @keyframes blink {
584
+ 0%, 100% { opacity: 1; }
585
+ 50% { opacity: 0.3; }
586
+ }
587
+
588
+ .compose-textarea-wrapper textarea {
589
+ width: 100%;
590
+ background: var(--bg-primary);
591
+ border: 1px solid var(--border-color);
592
+ border-radius: 6px;
593
+ color: var(--text);
594
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
595
+ font-size: 0.8125rem;
596
+ padding: 12px 14px 12px 32px;
597
+ outline: none;
598
+ transition: all 0.2s ease;
599
+ resize: none;
600
+ min-height: 80px;
601
+ line-height: 1.6;
602
+ }
603
+
604
+ .compose-textarea-wrapper textarea:hover {
605
+ border-color: var(--border-hover);
606
+ }
607
+
608
+ .compose-textarea-wrapper textarea:focus {
609
+ border-color: var(--primary);
610
+ box-shadow: 0 0 0 3px var(--primary-muted), inset 0 0 30px rgba(59, 130, 246, 0.03);
611
+ background: linear-gradient(180deg, var(--bg-primary) 0%, rgba(59, 130, 246, 0.02) 100%);
612
+ }
613
+
614
+ .compose-textarea-wrapper textarea::placeholder {
615
+ color: var(--text-muted);
616
+ opacity: 0.5;
617
+ }
618
+
619
+ .send-btn {
620
+ background: linear-gradient(180deg, var(--primary) 0%, #2563eb 100%);
621
+ color: white;
622
+ border: none;
623
+ border-radius: 6px;
624
+ padding: 0 24px;
625
+ min-width: 100px;
626
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
627
+ font-size: 0.8125rem;
628
+ font-weight: 600;
629
+ letter-spacing: 0.05em;
630
+ text-transform: uppercase;
631
+ cursor: pointer;
632
+ transition: all 0.2s ease;
633
+ white-space: nowrap;
634
+ display: flex;
635
+ align-items: center;
636
+ justify-content: center;
637
+ gap: 8px;
638
+ position: relative;
639
+ overflow: hidden;
640
+ }
641
+
642
+ .send-btn::before {
643
+ content: '';
644
+ position: absolute;
645
+ inset: 0;
646
+ background: linear-gradient(180deg, rgba(255,255,255,0.1) 0%, transparent 50%);
647
+ pointer-events: none;
648
+ }
649
+
650
+ .send-btn:hover {
651
+ background: linear-gradient(180deg, #4f8ff7 0%, var(--primary) 100%);
652
+ transform: translateY(-1px);
653
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
654
+ }
655
+
656
+ .send-btn:active {
657
+ transform: translateY(0);
658
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
659
+ }
660
+
661
+ .send-btn:disabled {
662
+ opacity: 0.4;
663
+ cursor: not-allowed;
664
+ transform: none;
665
+ box-shadow: none;
666
+ }
667
+
668
+ .send-btn svg {
669
+ width: 14px;
670
+ height: 14px;
671
+ }
672
+
673
+ .send-status {
674
+ font-size: 0.6875rem;
675
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
676
+ padding: 6px 0 0;
677
+ min-height: 22px;
678
+ letter-spacing: 0.02em;
679
+ }
680
+
681
+ .send-status.success {
682
+ color: var(--success);
683
+ }
684
+
685
+ .send-status.error {
686
+ color: var(--error);
687
+ }
688
+
338
689
  /* Scrollbar */
339
690
  ::-webkit-scrollbar { width: 6px; }
340
691
  ::-webkit-scrollbar-track { background: transparent; }
@@ -378,6 +729,11 @@
378
729
  <div class="activity-log">
379
730
  <div class="log-header">
380
731
  <h2>Activity</h2>
732
+ <div class="filter-section">
733
+ <select id="thread-filter" class="thread-filter">
734
+ <option value="">All threads</option>
735
+ </select>
736
+ </div>
381
737
  <div class="live-indicator">
382
738
  <span class="live-dot"></span>
383
739
  <span>Live</span>
@@ -387,6 +743,37 @@
387
743
  <!-- Messages injected here -->
388
744
  </div>
389
745
  </div>
746
+ <div class="compose-section">
747
+ <div class="compose-wrapper">
748
+ <div class="compose-input-area">
749
+ <div class="compose-meta-row">
750
+ <div class="compose-to">
751
+ <label for="agent-select">Target</label>
752
+ <select id="agent-select">
753
+ <option value="">Select agent...</option>
754
+ <option value="*">* (Broadcast all)</option>
755
+ </select>
756
+ </div>
757
+ <div class="compose-hint">
758
+ <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to send
759
+ </div>
760
+ </div>
761
+ <div class="compose-input-row">
762
+ <div class="compose-textarea-wrapper">
763
+ <textarea id="message-input" placeholder="Enter your message..."></textarea>
764
+ </div>
765
+ <button class="send-btn" id="send-btn">
766
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
767
+ <line x1="22" y1="2" x2="11" y2="13"></line>
768
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
769
+ </svg>
770
+ Send
771
+ </button>
772
+ </div>
773
+ <div class="send-status" id="send-status"></div>
774
+ </div>
775
+ </div>
776
+ </div>
390
777
  </div>
391
778
  </div>
392
779
 
@@ -394,22 +781,80 @@
394
781
  const agentsContainer = document.getElementById('agents');
395
782
  const logContainer = document.getElementById('log');
396
783
  const statusDiv = document.getElementById('connection-status');
784
+ const agentSelect = document.getElementById('agent-select');
785
+ const messageInput = document.getElementById('message-input');
786
+ const sendBtn = document.getElementById('send-btn');
787
+ const sendStatus = document.getElementById('send-status');
788
+ const threadFilter = document.getElementById('thread-filter');
397
789
 
398
790
  // Track last data hash to prevent unnecessary re-renders
791
+ const STALE_THRESHOLD_MS = 30_000;
399
792
  let lastDataHash = '';
793
+ let currentAgents = [];
794
+ let allMessages = [];
795
+ let connectionStart = null;
796
+ let uptimeInterval = null;
797
+ let isReconnect = false;
798
+
799
+ function isAgentOnline(lastSeen) {
800
+ if (!lastSeen) return false;
801
+ const ts = Date.parse(lastSeen);
802
+ if (Number.isNaN(ts)) return false;
803
+ return (Date.now() - ts) < STALE_THRESHOLD_MS;
804
+ }
805
+
806
+ function formatUptime(ms) {
807
+ const seconds = Math.floor(ms / 1000);
808
+ if (seconds < 60) return `${seconds}s`;
809
+ const minutes = Math.floor(seconds / 60);
810
+ if (minutes < 60) return `${minutes}m`;
811
+ const hours = Math.floor(minutes / 60);
812
+ return `${hours}h ${minutes % 60}m`;
813
+ }
814
+
815
+ function updateConnectionStatus(state, uptime = null) {
816
+ statusDiv.className = '';
817
+ if (state === 'connecting' || state === 'reconnecting') {
818
+ statusDiv.classList.add(state);
819
+ const label = state === 'reconnecting' ? 'Reconnecting...' : 'Connecting...';
820
+ statusDiv.innerHTML = `<span class="dot"></span><span>${label}</span>`;
821
+ } else if (state === 'connected') {
822
+ const uptimeStr = uptime ? `<span class="uptime">${formatUptime(uptime)}</span>` : '';
823
+ statusDiv.innerHTML = `<span class="dot"></span><span>Live</span>${uptimeStr}`;
824
+ } else {
825
+ statusDiv.classList.add('disconnected');
826
+ statusDiv.innerHTML = '<span class="dot"></span><span>Offline</span>';
827
+ }
828
+ }
400
829
 
401
830
  function connect() {
402
831
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
832
+
833
+ updateConnectionStatus(isReconnect ? 'reconnecting' : 'connecting');
834
+
403
835
  const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
404
836
 
405
837
  ws.onopen = () => {
406
- statusDiv.innerHTML = '<span class="dot"></span><span>Live</span>';
407
- statusDiv.classList.remove('disconnected');
838
+ connectionStart = Date.now();
839
+ updateConnectionStatus('connected');
840
+
841
+ // Start uptime counter
842
+ if (uptimeInterval) clearInterval(uptimeInterval);
843
+ uptimeInterval = setInterval(() => {
844
+ if (connectionStart) {
845
+ updateConnectionStatus('connected', Date.now() - connectionStart);
846
+ }
847
+ }, 1000);
408
848
  };
409
849
 
410
850
  ws.onclose = () => {
411
- statusDiv.innerHTML = '<span class="dot"></span><span>Offline</span>';
412
- statusDiv.classList.add('disconnected');
851
+ if (uptimeInterval) {
852
+ clearInterval(uptimeInterval);
853
+ uptimeInterval = null;
854
+ }
855
+ connectionStart = null;
856
+ updateConnectionStatus('disconnected');
857
+ isReconnect = true;
413
858
  setTimeout(connect, 3000);
414
859
  };
415
860
 
@@ -427,8 +872,12 @@
427
872
  function render(data) {
428
873
  // Render Agents
429
874
  if (data.agents && data.agents.length > 0) {
430
- const newAgentsHTML = data.agents.map((a, i) => `
431
- <div class="agent-card ${a.messageCount > 0 ? 'active' : ''}">
875
+ const newAgentsHTML = data.agents.map((a, i) => {
876
+ const lastSeen = a.lastSeen ?? a.lastActive;
877
+ const online = isAgentOnline(lastSeen);
878
+ const disconnectedClass = online ? '' : 'disconnected';
879
+ return `
880
+ <div class="agent-card ${a.messageCount > 0 ? 'active' : ''} ${disconnectedClass}">
432
881
  <div class="agent-header">
433
882
  <div class="agent-name">${a.name}</div>
434
883
  ${a.messageCount > 0 ? `<span class="badge">${a.messageCount}</span>` : ''}
@@ -444,28 +893,42 @@
444
893
  ${a.lastActive ? timeAgo(new Date(a.lastActive)) : 'No activity'}
445
894
  </div>
446
895
  </div>
447
- `).join('');
896
+ `;
897
+ }).join('');
448
898
 
449
899
  if (agentsContainer.innerHTML !== newAgentsHTML) {
450
900
  agentsContainer.innerHTML = newAgentsHTML;
451
901
  }
902
+
903
+ // Update agent dropdown if agents changed
904
+ const agentNames = data.agents.map(a => a.name).sort();
905
+ if (JSON.stringify(agentNames) !== JSON.stringify(currentAgents)) {
906
+ currentAgents = agentNames;
907
+ updateAgentDropdown(agentNames);
908
+ }
452
909
  } else if (!agentsContainer.querySelector('.empty-state')) {
453
910
  agentsContainer.innerHTML = `
454
911
  <div class="empty-state">
455
912
  <div class="empty-state-text">Waiting for agents...</div>
456
913
  </div>
457
914
  `;
915
+ // Clear agent dropdown
916
+ if (currentAgents.length > 0) {
917
+ currentAgents = [];
918
+ updateAgentDropdown([]);
919
+ }
458
920
  }
459
921
 
460
922
  // Render Messages (Activity Log)
461
923
  if (data.messages && data.messages.length > 0) {
462
- const currentCount = logContainer.querySelectorAll('.message').length;
463
- const newCount = data.messages.length;
924
+ allMessages = data.messages;
464
925
 
465
- if (currentCount !== newCount) {
466
- logContainer.innerHTML = data.messages.map(m => createMessageHTML(m)).join('');
467
- }
926
+ // Update conversation filter dropdown
927
+ updateConversationDropdown();
928
+
929
+ renderMessages();
468
930
  } else if (!logContainer.querySelector('.empty-state')) {
931
+ allMessages = [];
469
932
  logContainer.innerHTML = `
470
933
  <div class="empty-state">
471
934
  <div class="empty-state-text">No messages yet</div>
@@ -474,16 +937,84 @@
474
937
  }
475
938
  }
476
939
 
940
+ function renderMessages() {
941
+ const selectedFilter = threadFilter.value;
942
+ const filtered = selectedFilter
943
+ ? allMessages.filter(m => getConversationKey(m) === selectedFilter)
944
+ : allMessages;
945
+
946
+ if (filtered.length > 0) {
947
+ logContainer.innerHTML = filtered.map(m => createMessageHTML(m)).join('');
948
+ } else {
949
+ logContainer.innerHTML = `
950
+ <div class="empty-state">
951
+ <div class="empty-state-text">No messages${selectedFilter ? ' in this conversation' : ''}</div>
952
+ </div>
953
+ `;
954
+ }
955
+ }
956
+
957
+ function getConversationKey(m) {
958
+ // For broadcasts, group under "Broadcasts"
959
+ if (m.to === '*') return 'broadcast:*';
960
+ // For direct messages, create a sorted pair key
961
+ const pair = [m.from, m.to].sort();
962
+ return `conv:${pair[0]}↔${pair[1]}`;
963
+ }
964
+
965
+ function updateConversationDropdown() {
966
+ const currentValue = threadFilter.value;
967
+
968
+ // Build conversation map
969
+ const conversations = new Map();
970
+ allMessages.forEach(m => {
971
+ const key = getConversationKey(m);
972
+ if (!conversations.has(key)) {
973
+ conversations.set(key, { count: 0, label: '' });
974
+ }
975
+ conversations.get(key).count++;
976
+ });
977
+
978
+ // Create labels
979
+ conversations.forEach((data, key) => {
980
+ if (key === 'broadcast:*') {
981
+ data.label = '📢 Broadcasts';
982
+ } else {
983
+ data.label = key.replace('conv:', '');
984
+ }
985
+ });
986
+
987
+ // Sort by count descending
988
+ const sorted = [...conversations.entries()].sort((a, b) => b[1].count - a[1].count);
989
+
990
+ threadFilter.innerHTML = `
991
+ <option value="">All messages (${allMessages.length})</option>
992
+ ${sorted.map(([key, data]) =>
993
+ `<option value="${escapeHtml(key)}">${escapeHtml(data.label)} (${data.count})</option>`
994
+ ).join('')}
995
+ `;
996
+
997
+ // Restore selection if still valid
998
+ if (currentValue && conversations.has(currentValue)) {
999
+ threadFilter.value = currentValue;
1000
+ }
1001
+ }
1002
+
477
1003
  function createMessageHTML(m) {
478
1004
  const initials = m.from.substring(0, 2).toUpperCase();
1005
+ const threadBadge = m.thread ? `<span class="thread-badge" title="Thread: ${escapeHtml(m.thread)}">#${escapeHtml(m.thread)}</span>` : '';
1006
+ const isBroadcast = m.to === '*';
1007
+ const msgClass = isBroadcast ? 'broadcast' : 'direct';
1008
+ const targetDisplay = isBroadcast ? '* (all)' : m.to;
479
1009
  return `
480
- <div class="message">
1010
+ <div class="message ${msgClass}" ${m.thread ? `data-thread="${escapeHtml(m.thread)}"` : ''} data-from="${escapeHtml(m.from)}" data-to="${escapeHtml(m.to)}">
481
1011
  <div class="msg-avatar">${initials}</div>
482
1012
  <div class="msg-body">
483
1013
  <div class="msg-meta">
484
1014
  <span class="msg-sender">${m.from}</span>
485
1015
  <span class="msg-arrow">→</span>
486
- <span class="msg-target">${m.to}</span>
1016
+ <span class="msg-target">${targetDisplay}</span>
1017
+ ${threadBadge}
487
1018
  <span class="msg-time">${new Date(m.timestamp).toLocaleTimeString()}</span>
488
1019
  </div>
489
1020
  <div class="msg-text">${escapeHtml(m.content)}</div>
@@ -509,6 +1040,84 @@
509
1040
  return `${Math.floor(hours / 24)}d ago`;
510
1041
  }
511
1042
 
1043
+ function updateAgentDropdown(agents) {
1044
+ const currentValue = agentSelect.value;
1045
+ agentSelect.innerHTML = `
1046
+ <option value="">Select agent...</option>
1047
+ <option value="*">* (Broadcast)</option>
1048
+ ${agents.map(name => `<option value="${escapeHtml(name)}">${escapeHtml(name)}</option>`).join('')}
1049
+ `;
1050
+ // Restore selection if still valid
1051
+ if (currentValue && (currentValue === '*' || agents.includes(currentValue))) {
1052
+ agentSelect.value = currentValue;
1053
+ }
1054
+ }
1055
+
1056
+ async function sendMessage() {
1057
+ const to = agentSelect.value;
1058
+ const message = messageInput.value.trim();
1059
+
1060
+ if (!to) {
1061
+ showStatus('Please select an agent', 'error');
1062
+ return;
1063
+ }
1064
+ if (!message) {
1065
+ showStatus('Please enter a message', 'error');
1066
+ return;
1067
+ }
1068
+
1069
+ sendBtn.disabled = true;
1070
+ showStatus('Sending...', '');
1071
+
1072
+ try {
1073
+ const response = await fetch('/api/send', {
1074
+ method: 'POST',
1075
+ headers: { 'Content-Type': 'application/json' },
1076
+ body: JSON.stringify({ to, message })
1077
+ });
1078
+
1079
+ const result = await response.json();
1080
+
1081
+ if (response.ok && result.success) {
1082
+ showStatus('Message sent!', 'success');
1083
+ messageInput.value = '';
1084
+ } else {
1085
+ showStatus(result.error || 'Failed to send', 'error');
1086
+ }
1087
+ } catch (err) {
1088
+ showStatus('Network error', 'error');
1089
+ } finally {
1090
+ sendBtn.disabled = false;
1091
+ }
1092
+ }
1093
+
1094
+ function showStatus(text, type) {
1095
+ sendStatus.textContent = text;
1096
+ sendStatus.className = 'send-status' + (type ? ' ' + type : '');
1097
+ if (type === 'success') {
1098
+ setTimeout(() => {
1099
+ if (sendStatus.textContent === text) {
1100
+ sendStatus.textContent = '';
1101
+ sendStatus.className = 'send-status';
1102
+ }
1103
+ }, 3000);
1104
+ }
1105
+ }
1106
+
1107
+ // Send button click handler
1108
+ sendBtn.addEventListener('click', sendMessage);
1109
+
1110
+ // Thread filter change handler
1111
+ threadFilter.addEventListener('change', renderMessages);
1112
+
1113
+ // Allow Ctrl+Enter to send
1114
+ messageInput.addEventListener('keydown', (e) => {
1115
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
1116
+ e.preventDefault();
1117
+ sendMessage();
1118
+ }
1119
+ });
1120
+
512
1121
  connect();
513
1122
  </script>
514
1123
  </body>