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.
- package/README.md +18 -6
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +344 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/agent-registry.d.ts +60 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -0
- package/dist/daemon/agent-registry.js +158 -0
- package/dist/daemon/agent-registry.js.map +1 -0
- package/dist/daemon/connection.d.ts +11 -1
- package/dist/daemon/connection.d.ts.map +1 -1
- package/dist/daemon/connection.js +31 -2
- package/dist/daemon/connection.js.map +1 -1
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +2 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/registry.d.ts +9 -0
- package/dist/daemon/registry.d.ts.map +1 -0
- package/dist/daemon/registry.js +9 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/router.d.ts +34 -2
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +111 -1
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +60 -13
- package/dist/daemon/server.js.map +1 -1
- package/dist/dashboard/public/index.html +625 -16
- package/dist/dashboard/server.d.ts +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +125 -7
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/protocol/types.d.ts +15 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/storage/adapter.d.ts +53 -0
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +3 -0
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts +58 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +374 -47
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +22 -1
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/client.d.ts +22 -3
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +59 -9
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +110 -4
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +296 -84
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +100 -9
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +437 -77
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/AGENTS.md +27 -27
- package/docs/CHANGELOG.md +1 -1
- package/docs/DESIGN_V2.md +1079 -0
- package/docs/INTEGRATION-GUIDE.md +926 -0
- package/docs/PROPOSAL-trajectories.md +1582 -0
- package/docs/PROTOCOL.md +3 -3
- package/docs/SCALING_ANALYSIS.md +280 -0
- package/docs/TMUX_IMPLEMENTATION_NOTES.md +9 -9
- package/docs/TMUX_IMPROVEMENTS.md +968 -0
- package/docs/competitive-analysis-mcp-agent-mail.md +389 -0
- 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:
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
const newCount = data.messages.length;
|
|
924
|
+
allMessages = data.messages;
|
|
464
925
|
|
|
465
|
-
|
|
466
|
-
|
|
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">${
|
|
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>
|