agent-relay 1.0.7 → 1.0.9

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 (140) hide show
  1. package/README.md +176 -6
  2. package/dist/bridge/config.d.ts +41 -0
  3. package/dist/bridge/config.d.ts.map +1 -0
  4. package/dist/bridge/config.js +143 -0
  5. package/dist/bridge/config.js.map +1 -0
  6. package/dist/bridge/index.d.ts +10 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +10 -0
  9. package/dist/bridge/index.js.map +1 -0
  10. package/dist/bridge/multi-project-client.d.ts +99 -0
  11. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  12. package/dist/bridge/multi-project-client.js +386 -0
  13. package/dist/bridge/multi-project-client.js.map +1 -0
  14. package/dist/bridge/spawner.d.ts +46 -0
  15. package/dist/bridge/spawner.d.ts.map +1 -0
  16. package/dist/bridge/spawner.js +223 -0
  17. package/dist/bridge/spawner.js.map +1 -0
  18. package/dist/bridge/types.d.ts +55 -0
  19. package/dist/bridge/types.d.ts.map +1 -0
  20. package/dist/bridge/types.js +6 -0
  21. package/dist/bridge/types.js.map +1 -0
  22. package/dist/bridge/utils.d.ts +30 -0
  23. package/dist/bridge/utils.d.ts.map +1 -0
  24. package/dist/bridge/utils.js +54 -0
  25. package/dist/bridge/utils.js.map +1 -0
  26. package/dist/cli/index.d.ts +2 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +906 -6
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/daemon/agent-registry.d.ts +60 -0
  31. package/dist/daemon/agent-registry.d.ts.map +1 -0
  32. package/dist/daemon/agent-registry.js +163 -0
  33. package/dist/daemon/agent-registry.js.map +1 -0
  34. package/dist/daemon/connection.d.ts +33 -1
  35. package/dist/daemon/connection.d.ts.map +1 -1
  36. package/dist/daemon/connection.js +86 -11
  37. package/dist/daemon/connection.js.map +1 -1
  38. package/dist/daemon/index.d.ts +2 -0
  39. package/dist/daemon/index.d.ts.map +1 -1
  40. package/dist/daemon/index.js +2 -0
  41. package/dist/daemon/index.js.map +1 -1
  42. package/dist/daemon/registry.d.ts +9 -0
  43. package/dist/daemon/registry.d.ts.map +1 -0
  44. package/dist/daemon/registry.js +9 -0
  45. package/dist/daemon/registry.js.map +1 -0
  46. package/dist/daemon/router.d.ts +61 -2
  47. package/dist/daemon/router.d.ts.map +1 -1
  48. package/dist/daemon/router.js +219 -4
  49. package/dist/daemon/router.js.map +1 -1
  50. package/dist/daemon/server.d.ts +9 -0
  51. package/dist/daemon/server.d.ts.map +1 -1
  52. package/dist/daemon/server.js +135 -16
  53. package/dist/daemon/server.js.map +1 -1
  54. package/dist/dashboard/metrics.d.ts +105 -0
  55. package/dist/dashboard/metrics.d.ts.map +1 -0
  56. package/dist/dashboard/metrics.js +192 -0
  57. package/dist/dashboard/metrics.js.map +1 -0
  58. package/dist/dashboard/needs-attention.d.ts +24 -0
  59. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  60. package/dist/dashboard/needs-attention.js +78 -0
  61. package/dist/dashboard/needs-attention.js.map +1 -0
  62. package/dist/dashboard/public/bridge.html +1272 -0
  63. package/dist/dashboard/public/index.html +2094 -347
  64. package/dist/dashboard/public/js/app.js +184 -0
  65. package/dist/dashboard/public/js/app.js.map +7 -0
  66. package/dist/dashboard/public/metrics.html +999 -0
  67. package/dist/dashboard/server.d.ts +14 -1
  68. package/dist/dashboard/server.d.ts.map +1 -1
  69. package/dist/dashboard/server.js +689 -16
  70. package/dist/dashboard/server.js.map +1 -1
  71. package/dist/dashboard/start.js +1 -1
  72. package/dist/dashboard/start.js.map +1 -1
  73. package/dist/dashboard-v2/index.d.ts +10 -0
  74. package/dist/dashboard-v2/index.d.ts.map +1 -0
  75. package/dist/dashboard-v2/index.js +54 -0
  76. package/dist/dashboard-v2/index.js.map +1 -0
  77. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  78. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  79. package/dist/dashboard-v2/lib/api.js +270 -0
  80. package/dist/dashboard-v2/lib/api.js.map +1 -0
  81. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  82. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  83. package/dist/dashboard-v2/lib/colors.js +198 -0
  84. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  85. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  86. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  87. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  88. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  89. package/dist/dashboard-v2/types/index.d.ts +154 -0
  90. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  91. package/dist/dashboard-v2/types/index.js +6 -0
  92. package/dist/dashboard-v2/types/index.js.map +1 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/protocol/types.d.ts +15 -1
  96. package/dist/protocol/types.d.ts.map +1 -1
  97. package/dist/storage/adapter.d.ts +74 -1
  98. package/dist/storage/adapter.d.ts.map +1 -1
  99. package/dist/storage/adapter.js +39 -0
  100. package/dist/storage/adapter.js.map +1 -1
  101. package/dist/storage/sqlite-adapter.d.ts +92 -1
  102. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  103. package/dist/storage/sqlite-adapter.js +615 -47
  104. package/dist/storage/sqlite-adapter.js.map +1 -1
  105. package/dist/utils/agent-config.d.ts +45 -0
  106. package/dist/utils/agent-config.d.ts.map +1 -0
  107. package/dist/utils/agent-config.js +118 -0
  108. package/dist/utils/agent-config.js.map +1 -0
  109. package/dist/utils/project-namespace.d.ts.map +1 -1
  110. package/dist/utils/project-namespace.js +22 -1
  111. package/dist/utils/project-namespace.js.map +1 -1
  112. package/dist/wrapper/client.d.ts +30 -3
  113. package/dist/wrapper/client.d.ts.map +1 -1
  114. package/dist/wrapper/client.js +85 -9
  115. package/dist/wrapper/client.js.map +1 -1
  116. package/dist/wrapper/parser.d.ts +127 -4
  117. package/dist/wrapper/parser.d.ts.map +1 -1
  118. package/dist/wrapper/parser.js +622 -86
  119. package/dist/wrapper/parser.js.map +1 -1
  120. package/dist/wrapper/tmux-wrapper.d.ts +136 -10
  121. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  122. package/dist/wrapper/tmux-wrapper.js +599 -79
  123. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  124. package/docs/AGENTS.md +132 -27
  125. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  126. package/docs/CHANGELOG.md +1 -1
  127. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  128. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  129. package/docs/DESIGN_V2.md +1079 -0
  130. package/docs/INTEGRATION-GUIDE.md +926 -0
  131. package/docs/MONETIZATION.md +1679 -0
  132. package/docs/PROPOSAL-trajectories.md +1582 -0
  133. package/docs/PROTOCOL.md +3 -3
  134. package/docs/SCALING_ANALYSIS.md +280 -0
  135. package/docs/TMUX_IMPLEMENTATION_NOTES.md +9 -9
  136. package/docs/TMUX_IMPROVEMENTS.md +968 -0
  137. package/docs/agent-relay-snippet.md +61 -0
  138. package/docs/competitive-analysis-mcp-agent-mail.md +389 -0
  139. package/docs/dashboard-v2-plan.md +179 -0
  140. package/package.json +10 -3
@@ -0,0 +1,1272 @@
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>Agent Relay - Bridge</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* ========================================
12
+ Design Tokens (matching index.html)
13
+ ======================================== */
14
+ :root {
15
+ --bg-workspace: #1a1d21;
16
+ --bg-sidebar: #19171d;
17
+ --bg-channel-active: #1164a3;
18
+ --bg-channel-hover: rgba(255, 255, 255, 0.04);
19
+ --bg-main: #222529;
20
+ --bg-message-hover: rgba(255, 255, 255, 0.03);
21
+ --bg-input: rgba(255, 255, 255, 0.04);
22
+ --bg-modal: #2a2d32;
23
+ --bg-card: #2a2d32;
24
+ --bg-card-hover: #32363c;
25
+ --text-primary: #d1d2d3;
26
+ --text-secondary: #ababad;
27
+ --text-muted: #8d8d8e;
28
+ --text-channel: #bcabbc;
29
+ --text-channel-active: #ffffff;
30
+ --text-link: #1d9bd1;
31
+ --accent-primary: #1264a3;
32
+ --accent-green: #2bac76;
33
+ --accent-yellow: #e8a427;
34
+ --accent-red: #e01e5a;
35
+ --accent-purple: #7c3aed;
36
+ --status-online: #2bac76;
37
+ --status-away: #e8a427;
38
+ --status-offline: #616061;
39
+ --border-subtle: rgba(255, 255, 255, 0.1);
40
+ --border-divider: rgba(255, 255, 255, 0.06);
41
+ --sidebar-width: 260px;
42
+ --header-height: 49px;
43
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
45
+ --transition-fast: 0.1s ease;
46
+ --transition-normal: 0.2s ease;
47
+ --shadow-modal: 0 18px 50px rgba(0, 0, 0, 0.6);
48
+ }
49
+
50
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
51
+ html, body { height: 100%; overflow: hidden; }
52
+ body {
53
+ font-family: var(--font-family);
54
+ font-size: 15px;
55
+ line-height: 1.46668;
56
+ color: var(--text-primary);
57
+ background: var(--bg-workspace);
58
+ -webkit-font-smoothing: antialiased;
59
+ }
60
+
61
+ .app-container { display: flex; height: 100vh; width: 100vw; }
62
+
63
+ /* Sidebar */
64
+ .sidebar {
65
+ width: var(--sidebar-width);
66
+ background: var(--bg-sidebar);
67
+ display: flex;
68
+ flex-direction: column;
69
+ flex-shrink: 0;
70
+ border-right: 1px solid var(--border-divider);
71
+ }
72
+
73
+ .workspace-header {
74
+ height: var(--header-height);
75
+ padding: 0 16px;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: space-between;
79
+ border-bottom: 1px solid var(--border-divider);
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .workspace-name {
84
+ font-size: 18px;
85
+ font-weight: 700;
86
+ color: var(--text-primary);
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ }
91
+
92
+ .workspace-name .status-dot {
93
+ width: 10px;
94
+ height: 10px;
95
+ border-radius: 50%;
96
+ background: var(--status-online);
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ .workspace-name .status-dot.offline { background: var(--status-offline); }
101
+
102
+ .sidebar-content {
103
+ flex: 1;
104
+ overflow-y: auto;
105
+ padding: 12px 0;
106
+ }
107
+
108
+ .section { margin-bottom: 8px; }
109
+
110
+ .section-header {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ padding: 0 16px 0 12px;
115
+ height: 26px;
116
+ }
117
+
118
+ .section-title {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 4px;
122
+ font-size: 15px;
123
+ color: var(--text-channel);
124
+ }
125
+
126
+ .section-title svg { width: 10px; height: 10px; opacity: 0.7; }
127
+
128
+ .section-add-btn {
129
+ width: 20px;
130
+ height: 20px;
131
+ border: none;
132
+ background: transparent;
133
+ color: var(--text-muted);
134
+ border-radius: 4px;
135
+ cursor: pointer;
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ transition: all var(--transition-fast);
140
+ }
141
+
142
+ .section-add-btn:hover { background: rgba(255, 255, 255, 0.1); color: var(--text-primary); }
143
+
144
+ .channel-list { list-style: none; padding: 4px 8px; }
145
+
146
+ .channel-item {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 8px;
150
+ padding: 6px 8px;
151
+ border-radius: 6px;
152
+ cursor: pointer;
153
+ transition: background var(--transition-fast);
154
+ color: var(--text-channel);
155
+ font-size: 15px;
156
+ }
157
+
158
+ .channel-item:hover { background: var(--bg-channel-hover); }
159
+ .channel-item.active { background: var(--bg-channel-active); color: var(--text-channel-active); }
160
+
161
+ .project-status-dot {
162
+ width: 9px;
163
+ height: 9px;
164
+ border-radius: 50%;
165
+ background: var(--status-offline);
166
+ flex-shrink: 0;
167
+ }
168
+
169
+ .channel-item.connected .project-status-dot { background: var(--status-online); }
170
+ .channel-item.reconnecting .project-status-dot { background: var(--status-away); }
171
+
172
+ .channel-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
173
+
174
+ .agent-avatar {
175
+ width: 24px;
176
+ height: 24px;
177
+ border-radius: 4px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ font-size: 11px;
182
+ font-weight: 600;
183
+ color: white;
184
+ flex-shrink: 0;
185
+ position: relative;
186
+ }
187
+
188
+ .presence-indicator {
189
+ position: absolute;
190
+ bottom: -2px;
191
+ right: -2px;
192
+ width: 9px;
193
+ height: 9px;
194
+ border-radius: 50%;
195
+ background: var(--status-offline);
196
+ border: 2px solid var(--bg-sidebar);
197
+ }
198
+
199
+ .presence-indicator.online { background: var(--status-online); }
200
+
201
+ .sidebar-footer {
202
+ padding: 12px 16px;
203
+ border-top: 1px solid var(--border-divider);
204
+ }
205
+
206
+ .nav-link {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 8px;
210
+ padding: 6px 8px;
211
+ color: var(--text-channel);
212
+ text-decoration: none;
213
+ font-size: 15px;
214
+ border-radius: 6px;
215
+ transition: background var(--transition-fast);
216
+ }
217
+
218
+ .nav-link:hover { background: var(--bg-channel-hover); }
219
+ .nav-link svg { width: 18px; height: 18px; opacity: 0.7; }
220
+
221
+ /* Main Panel */
222
+ .main-panel {
223
+ flex: 1;
224
+ display: flex;
225
+ flex-direction: column;
226
+ min-width: 0;
227
+ background: var(--bg-main);
228
+ }
229
+
230
+ .channel-header {
231
+ height: var(--header-height);
232
+ padding: 0 20px;
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: space-between;
236
+ border-bottom: 1px solid var(--border-divider);
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .channel-header-name {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 4px;
244
+ font-size: 18px;
245
+ font-weight: 700;
246
+ color: var(--text-primary);
247
+ }
248
+
249
+ .channel-header-name .prefix { color: var(--text-muted); font-weight: 400; }
250
+
251
+ .channel-stats {
252
+ display: flex;
253
+ gap: 12px;
254
+ font-size: 13px;
255
+ color: var(--text-muted);
256
+ }
257
+
258
+ .stat-badge {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 4px;
262
+ }
263
+
264
+ .stat-badge .value {
265
+ font-weight: 600;
266
+ color: var(--text-secondary);
267
+ font-family: var(--font-mono);
268
+ }
269
+
270
+ /* Messages Area - Dashboard Style */
271
+ .messages-area {
272
+ flex: 1;
273
+ overflow-y: auto;
274
+ padding: 20px;
275
+ }
276
+
277
+ .messages-list { display: flex; flex-direction: column; gap: 1px; }
278
+
279
+ .message {
280
+ display: flex;
281
+ gap: 12px;
282
+ padding: 8px 20px;
283
+ transition: background var(--transition-fast);
284
+ }
285
+
286
+ .message:hover { background: var(--bg-message-hover); }
287
+
288
+ .message-avatar {
289
+ width: 36px;
290
+ height: 36px;
291
+ border-radius: 4px;
292
+ display: flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ font-size: 14px;
296
+ font-weight: 600;
297
+ color: white;
298
+ flex-shrink: 0;
299
+ }
300
+
301
+ .message-content { flex: 1; min-width: 0; }
302
+
303
+ .message-header {
304
+ display: flex;
305
+ align-items: baseline;
306
+ gap: 8px;
307
+ margin-bottom: 4px;
308
+ }
309
+
310
+ .message-sender { font-weight: 700; color: var(--text-primary); }
311
+ .message-recipient { font-size: 13px; color: var(--text-muted); }
312
+ .message-recipient .target { color: var(--text-secondary); }
313
+ .message-timestamp { font-size: 12px; color: var(--text-muted); margin-left: auto; }
314
+
315
+ .message-body {
316
+ font-size: 15px;
317
+ color: var(--text-primary);
318
+ line-height: 1.46668;
319
+ word-wrap: break-word;
320
+ white-space: pre-wrap;
321
+ }
322
+
323
+ .project-badge {
324
+ display: inline-block;
325
+ padding: 1px 5px;
326
+ background: var(--accent-purple);
327
+ color: white;
328
+ font-size: 11px;
329
+ border-radius: 3px;
330
+ margin-right: 4px;
331
+ font-weight: 500;
332
+ }
333
+
334
+ /* Overview Cards (for All Projects view) */
335
+ .cards-container { flex: 1; overflow-y: auto; padding: 20px; }
336
+
337
+ .cards-grid {
338
+ display: grid;
339
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
340
+ gap: 16px;
341
+ }
342
+
343
+ .project-card {
344
+ background: var(--bg-card);
345
+ border: 1px solid var(--border-divider);
346
+ border-radius: 8px;
347
+ padding: 16px;
348
+ cursor: pointer;
349
+ transition: all var(--transition-normal);
350
+ }
351
+
352
+ .project-card:hover { background: var(--bg-card-hover); border-color: var(--border-subtle); }
353
+ .project-card.offline { opacity: 0.6; }
354
+
355
+ .card-header {
356
+ display: flex;
357
+ align-items: flex-start;
358
+ justify-content: space-between;
359
+ margin-bottom: 12px;
360
+ }
361
+
362
+ .card-title-group { display: flex; align-items: center; gap: 10px; }
363
+
364
+ .card-icon {
365
+ width: 36px;
366
+ height: 36px;
367
+ background: var(--accent-primary);
368
+ border-radius: 8px;
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ color: white;
373
+ }
374
+
375
+ .card-icon svg { width: 18px; height: 18px; }
376
+ .card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
377
+ .card-path { font-size: 12px; font-family: var(--font-mono); color: var(--text-muted); margin-top: 2px; }
378
+
379
+ .card-status {
380
+ display: flex;
381
+ align-items: center;
382
+ gap: 6px;
383
+ padding: 4px 8px;
384
+ border-radius: 12px;
385
+ font-size: 12px;
386
+ font-weight: 500;
387
+ }
388
+
389
+ .card-status.online { background: rgba(43, 172, 118, 0.15); color: var(--status-online); }
390
+ .card-status.offline { background: rgba(97, 96, 97, 0.15); color: var(--status-offline); }
391
+ .card-status.reconnecting { background: rgba(232, 164, 39, 0.15); color: var(--status-away); }
392
+ .card-status .dot { width: 6px; height: 6px; background: currentColor; border-radius: 50%; }
393
+
394
+ .agents-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-divider); }
395
+
396
+ .agents-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
397
+ .agents-label { font-size: 12px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
398
+ .agents-count { font-size: 12px; font-family: var(--font-mono); color: var(--text-muted); }
399
+
400
+ .card-agents-list { display: flex; flex-direction: column; gap: 6px; }
401
+
402
+ .card-agent-item {
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 8px;
406
+ padding: 6px 8px;
407
+ background: rgba(255, 255, 255, 0.03);
408
+ border-radius: 6px;
409
+ }
410
+
411
+ .card-agent-dot { width: 8px; height: 8px; background: var(--status-online); border-radius: 50%; }
412
+ .card-agent-name { font-size: 14px; font-weight: 500; color: var(--text-primary); }
413
+ .card-agent-cli { margin-left: auto; font-size: 12px; font-family: var(--font-mono); color: var(--text-muted); }
414
+
415
+ .no-agents {
416
+ padding: 12px;
417
+ text-align: center;
418
+ font-size: 13px;
419
+ color: var(--text-muted);
420
+ background: rgba(255, 255, 255, 0.02);
421
+ border-radius: 6px;
422
+ }
423
+
424
+ /* Empty State */
425
+ .empty-state {
426
+ display: flex;
427
+ flex-direction: column;
428
+ align-items: center;
429
+ justify-content: center;
430
+ height: 100%;
431
+ padding: 48px;
432
+ text-align: center;
433
+ }
434
+
435
+ .empty-state-icon {
436
+ width: 64px;
437
+ height: 64px;
438
+ margin-bottom: 16px;
439
+ color: var(--text-muted);
440
+ }
441
+
442
+ .empty-state-title { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
443
+ .empty-state-text { font-size: 14px; color: var(--text-muted); max-width: 300px; }
444
+
445
+ /* Composer */
446
+ .message-composer {
447
+ padding: 16px 20px;
448
+ border-top: 1px solid var(--border-divider);
449
+ background: var(--bg-main);
450
+ }
451
+
452
+ .composer-container {
453
+ background: var(--bg-input);
454
+ border: 1px solid var(--border-subtle);
455
+ border-radius: 8px;
456
+ padding: 12px;
457
+ }
458
+
459
+ .composer-input {
460
+ width: 100%;
461
+ background: transparent;
462
+ border: none;
463
+ color: var(--text-primary);
464
+ font-family: var(--font-family);
465
+ font-size: 15px;
466
+ resize: none;
467
+ outline: none;
468
+ min-height: 24px;
469
+ max-height: 200px;
470
+ }
471
+
472
+ .composer-input::placeholder { color: var(--text-muted); }
473
+
474
+ .composer-row {
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 8px;
478
+ margin-bottom: 8px;
479
+ }
480
+
481
+ .composer-select {
482
+ padding: 6px 10px;
483
+ font-size: 13px;
484
+ font-family: var(--font-family);
485
+ color: var(--text-primary);
486
+ background: var(--bg-main);
487
+ border: 1px solid var(--border-subtle);
488
+ border-radius: 4px;
489
+ cursor: pointer;
490
+ }
491
+
492
+ .composer-select:hover { border-color: var(--accent-primary); }
493
+ .composer-select:focus { outline: none; border-color: var(--accent-primary); }
494
+ .composer-select:disabled { opacity: 0.5; cursor: not-allowed; }
495
+
496
+ .composer-actions {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ align-items: center;
500
+ margin-top: 8px;
501
+ }
502
+
503
+ .composer-status { font-size: 12px; color: var(--text-muted); }
504
+ .composer-status.success { color: var(--status-online); }
505
+ .composer-status.error { color: var(--accent-red); }
506
+
507
+ .composer-send {
508
+ padding: 6px 16px;
509
+ background: var(--accent-green);
510
+ color: white;
511
+ border: none;
512
+ border-radius: 4px;
513
+ font-size: 13px;
514
+ font-weight: 600;
515
+ cursor: pointer;
516
+ transition: background var(--transition-fast);
517
+ }
518
+
519
+ .composer-send:hover:not(:disabled) { background: #249966; }
520
+ .composer-send:disabled { opacity: 0.5; cursor: not-allowed; }
521
+
522
+ /* Spawn Modal */
523
+ .spawn-modal-overlay {
524
+ position: fixed;
525
+ inset: 0;
526
+ background: rgba(0, 0, 0, 0.6);
527
+ display: none;
528
+ align-items: center;
529
+ justify-content: center;
530
+ z-index: 1000;
531
+ }
532
+
533
+ .spawn-modal-overlay.visible { display: flex; }
534
+
535
+ .spawn-modal {
536
+ width: 480px;
537
+ max-width: 90vw;
538
+ background: var(--bg-modal);
539
+ border-radius: 12px;
540
+ box-shadow: var(--shadow-modal);
541
+ overflow: hidden;
542
+ animation: modalSlideIn 0.2s ease-out;
543
+ }
544
+
545
+ @keyframes modalSlideIn {
546
+ from { opacity: 0; transform: translateY(-20px) scale(0.95); }
547
+ to { opacity: 1; transform: translateY(0) scale(1); }
548
+ }
549
+
550
+ .spawn-modal-header {
551
+ padding: 16px 20px;
552
+ border-bottom: 1px solid var(--border-divider);
553
+ display: flex;
554
+ align-items: center;
555
+ justify-content: space-between;
556
+ }
557
+
558
+ .spawn-modal-title {
559
+ display: flex;
560
+ align-items: center;
561
+ gap: 10px;
562
+ font-size: 18px;
563
+ font-weight: 600;
564
+ color: var(--text-primary);
565
+ }
566
+
567
+ .spawn-modal-title svg { color: var(--accent-green); }
568
+
569
+ .spawn-modal-close {
570
+ width: 32px;
571
+ height: 32px;
572
+ border: none;
573
+ background: transparent;
574
+ color: var(--text-muted);
575
+ border-radius: 6px;
576
+ cursor: pointer;
577
+ display: flex;
578
+ align-items: center;
579
+ justify-content: center;
580
+ transition: all var(--transition-fast);
581
+ }
582
+
583
+ .spawn-modal-close:hover { background: rgba(255, 255, 255, 0.1); color: var(--text-primary); }
584
+
585
+ .spawn-modal-body { padding: 20px; }
586
+
587
+ .spawn-form-group { margin-bottom: 16px; }
588
+ .spawn-form-group:last-child { margin-bottom: 0; }
589
+ .spawn-form-group label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
590
+
591
+ .spawn-input, .spawn-textarea, .spawn-select {
592
+ width: 100%;
593
+ padding: 10px 12px;
594
+ background: var(--bg-input);
595
+ border: 1px solid var(--border-subtle);
596
+ border-radius: 6px;
597
+ color: var(--text-primary);
598
+ font-family: var(--font-family);
599
+ font-size: 14px;
600
+ transition: border-color var(--transition-fast);
601
+ }
602
+
603
+ .spawn-input:focus, .spawn-textarea:focus, .spawn-select:focus { outline: none; border-color: var(--accent-primary); }
604
+ .spawn-input::placeholder, .spawn-textarea::placeholder { color: var(--text-muted); }
605
+ .spawn-textarea { resize: vertical; min-height: 80px; line-height: 1.4; }
606
+ .spawn-hint { display: block; font-size: 12px; color: var(--text-muted); margin-top: 4px; }
607
+ .spawn-status { min-height: 20px; font-size: 13px; margin-top: 12px; }
608
+ .spawn-status.success { color: var(--accent-green); }
609
+ .spawn-status.error { color: var(--accent-red); }
610
+ .spawn-status.loading { color: var(--text-muted); }
611
+
612
+ .spawn-modal-footer {
613
+ padding: 16px 20px;
614
+ border-top: 1px solid var(--border-divider);
615
+ display: flex;
616
+ justify-content: flex-end;
617
+ gap: 10px;
618
+ }
619
+
620
+ .spawn-cancel-btn {
621
+ padding: 8px 16px;
622
+ background: transparent;
623
+ border: 1px solid var(--border-subtle);
624
+ border-radius: 6px;
625
+ color: var(--text-secondary);
626
+ font-size: 14px;
627
+ font-weight: 500;
628
+ cursor: pointer;
629
+ transition: all var(--transition-fast);
630
+ }
631
+
632
+ .spawn-cancel-btn:hover { background: rgba(255, 255, 255, 0.05); color: var(--text-primary); }
633
+
634
+ .spawn-submit-btn {
635
+ display: flex;
636
+ align-items: center;
637
+ gap: 6px;
638
+ padding: 8px 16px;
639
+ background: var(--accent-green);
640
+ border: none;
641
+ border-radius: 6px;
642
+ color: white;
643
+ font-size: 14px;
644
+ font-weight: 600;
645
+ cursor: pointer;
646
+ transition: all var(--transition-fast);
647
+ }
648
+
649
+ .spawn-submit-btn:hover:not(:disabled) { background: #249966; }
650
+ .spawn-submit-btn:disabled { opacity: 0.5; cursor: not-allowed; }
651
+
652
+ /* Scrollbars */
653
+ ::-webkit-scrollbar { width: 8px; }
654
+ ::-webkit-scrollbar-track { background: transparent; }
655
+ ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }
656
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
657
+ </style>
658
+ </head>
659
+ <body>
660
+ <div class="app-container">
661
+ <!-- Sidebar -->
662
+ <aside class="sidebar">
663
+ <div class="workspace-header">
664
+ <div class="workspace-name">
665
+ <span class="status-dot" id="status-dot"></span>
666
+ Bridge
667
+ </div>
668
+ </div>
669
+
670
+ <div class="sidebar-content">
671
+ <!-- Projects Section -->
672
+ <div class="section">
673
+ <div class="section-header">
674
+ <div class="section-title">
675
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
676
+ <path d="M6 9l6 6 6-6"/>
677
+ </svg>
678
+ Projects
679
+ </div>
680
+ </div>
681
+ <ul class="channel-list" id="projects-list">
682
+ <li class="channel-item active" data-project="all">
683
+ <span class="project-status-dot" style="background: var(--accent-purple);"></span>
684
+ <span class="channel-name">All Projects</span>
685
+ </li>
686
+ <!-- Projects injected here -->
687
+ </ul>
688
+ </div>
689
+
690
+ <!-- Agents Section (shown when project selected) -->
691
+ <div class="section" id="agents-section" style="display: none;">
692
+ <div class="section-header">
693
+ <div class="section-title">
694
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
695
+ <path d="M6 9l6 6 6-6"/>
696
+ </svg>
697
+ Agents
698
+ </div>
699
+ <button class="section-add-btn" id="spawn-btn" title="Spawn new agent">
700
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
701
+ <line x1="12" y1="5" x2="12" y2="19"/>
702
+ <line x1="5" y1="12" x2="19" y2="12"/>
703
+ </svg>
704
+ </button>
705
+ </div>
706
+ <ul class="channel-list" id="agents-list">
707
+ <!-- Agents injected here -->
708
+ </ul>
709
+ </div>
710
+ </div>
711
+
712
+ <div class="sidebar-footer">
713
+ <a href="/" class="nav-link">
714
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
715
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
716
+ <polyline points="9 22 9 12 15 12 15 22"/>
717
+ </svg>
718
+ Local Dashboard
719
+ </a>
720
+ </div>
721
+ </aside>
722
+
723
+ <!-- Main Panel -->
724
+ <main class="main-panel">
725
+ <header class="channel-header">
726
+ <div class="channel-header-name">
727
+ <span class="prefix" id="header-prefix">#</span>
728
+ <span id="header-name">All Projects</span>
729
+ </div>
730
+ <div class="channel-stats">
731
+ <div class="stat-badge">
732
+ <span class="value" id="stat-projects">0</span>
733
+ <span>projects</span>
734
+ </div>
735
+ <div class="stat-badge">
736
+ <span class="value" id="stat-agents">0</span>
737
+ <span>agents</span>
738
+ </div>
739
+ </div>
740
+ </header>
741
+
742
+ <!-- Cards View (for All Projects) -->
743
+ <div class="cards-container" id="cards-view">
744
+ <div class="cards-grid" id="cards-grid">
745
+ <div class="empty-state" id="empty-state">
746
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
747
+ <path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2v-4M9 21H5a2 2 0 0 1-2-2v-4m0-6v6"/>
748
+ </svg>
749
+ <div class="empty-state-title">No Projects Connected</div>
750
+ <div class="empty-state-text">Start the bridge to orchestrate multiple projects</div>
751
+ </div>
752
+ </div>
753
+ </div>
754
+
755
+ <!-- Messages View (for individual project) -->
756
+ <div class="messages-area" id="messages-view" style="display: none;">
757
+ <div class="messages-list" id="messages-list">
758
+ <div class="empty-state">
759
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
760
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
761
+ </svg>
762
+ <div class="empty-state-title">No messages yet</div>
763
+ <div class="empty-state-text">Messages in this project will appear here</div>
764
+ </div>
765
+ </div>
766
+ </div>
767
+
768
+ <!-- Message Composer (for individual project) -->
769
+ <div class="message-composer" id="composer" style="display: none;">
770
+ <div class="composer-container">
771
+ <div class="composer-row">
772
+ <span style="font-size: 13px; color: var(--text-muted);">To:</span>
773
+ <select class="composer-select" id="composer-agent">
774
+ <option value="">Select agent...</option>
775
+ <option value="*">* (Broadcast)</option>
776
+ </select>
777
+ </div>
778
+ <textarea class="composer-input" id="composer-message" placeholder="Type a message..." rows="1"></textarea>
779
+ <div class="composer-actions">
780
+ <span class="composer-status" id="composer-status"></span>
781
+ <button class="composer-send" id="composer-send" disabled>Send</button>
782
+ </div>
783
+ </div>
784
+ </div>
785
+ </main>
786
+ </div>
787
+
788
+ <!-- Spawn Agent Modal -->
789
+ <div class="spawn-modal-overlay" id="spawn-modal-overlay">
790
+ <div class="spawn-modal">
791
+ <div class="spawn-modal-header">
792
+ <div class="spawn-modal-title">
793
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
794
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
795
+ <circle cx="12" cy="7" r="4"/>
796
+ <line x1="12" y1="11" x2="12" y2="17"/>
797
+ <line x1="9" y1="14" x2="15" y2="14"/>
798
+ </svg>
799
+ Spawn New Agent
800
+ </div>
801
+ <button class="spawn-modal-close" id="spawn-modal-close">
802
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
803
+ <line x1="18" y1="6" x2="6" y2="18"/>
804
+ <line x1="6" y1="6" x2="18" y2="18"/>
805
+ </svg>
806
+ </button>
807
+ </div>
808
+ <div class="spawn-modal-body">
809
+ <div class="spawn-form-group">
810
+ <label>Project</label>
811
+ <select class="spawn-select" id="spawn-project">
812
+ <option value="">Select project...</option>
813
+ </select>
814
+ </div>
815
+ <div class="spawn-form-group">
816
+ <label>Agent Name</label>
817
+ <input type="text" class="spawn-input" id="spawn-name" placeholder="e.g., Developer, Reviewer" autocomplete="off"/>
818
+ </div>
819
+ <div class="spawn-form-group">
820
+ <label>CLI Command</label>
821
+ <input type="text" class="spawn-input" id="spawn-cli" value="claude" placeholder="e.g., claude, aider"/>
822
+ <span class="spawn-hint">The AI CLI tool to wrap</span>
823
+ </div>
824
+ <div class="spawn-form-group">
825
+ <label>Initial Task (optional)</label>
826
+ <textarea class="spawn-textarea" id="spawn-task" placeholder="Enter an initial task..." rows="3"></textarea>
827
+ </div>
828
+ <div class="spawn-status" id="spawn-status"></div>
829
+ </div>
830
+ <div class="spawn-modal-footer">
831
+ <button class="spawn-cancel-btn" id="spawn-cancel">Cancel</button>
832
+ <button class="spawn-submit-btn" id="spawn-submit">
833
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
834
+ <polygon points="5 3 19 12 5 21 5 3"/>
835
+ </svg>
836
+ Spawn Agent
837
+ </button>
838
+ </div>
839
+ </div>
840
+ </div>
841
+
842
+ <script>
843
+ // State
844
+ let currentData = { projects: [], messages: [] };
845
+ let selectedProjectId = null;
846
+
847
+ // Elements
848
+ const statusDot = document.getElementById('status-dot');
849
+ const projectsList = document.getElementById('projects-list');
850
+ const agentsSection = document.getElementById('agents-section');
851
+ const agentsList = document.getElementById('agents-list');
852
+ const cardsView = document.getElementById('cards-view');
853
+ const cardsGrid = document.getElementById('cards-grid');
854
+ const messagesView = document.getElementById('messages-view');
855
+ const messagesList = document.getElementById('messages-list');
856
+ const composer = document.getElementById('composer');
857
+ const headerPrefix = document.getElementById('header-prefix');
858
+ const headerName = document.getElementById('header-name');
859
+
860
+ // Composer elements
861
+ const composerAgent = document.getElementById('composer-agent');
862
+ const composerMessage = document.getElementById('composer-message');
863
+ const composerSend = document.getElementById('composer-send');
864
+ const composerStatus = document.getElementById('composer-status');
865
+
866
+ // Spawn modal elements
867
+ const spawnModalOverlay = document.getElementById('spawn-modal-overlay');
868
+ const spawnBtn = document.getElementById('spawn-btn');
869
+ const spawnModalClose = document.getElementById('spawn-modal-close');
870
+ const spawnCancel = document.getElementById('spawn-cancel');
871
+ const spawnSubmit = document.getElementById('spawn-submit');
872
+ const spawnProject = document.getElementById('spawn-project');
873
+ const spawnName = document.getElementById('spawn-name');
874
+ const spawnCli = document.getElementById('spawn-cli');
875
+ const spawnTask = document.getElementById('spawn-task');
876
+ const spawnStatus = document.getElementById('spawn-status');
877
+
878
+ // Helpers
879
+ function escapeHtml(text) {
880
+ if (!text) return '';
881
+ const div = document.createElement('div');
882
+ div.textContent = text;
883
+ return div.innerHTML;
884
+ }
885
+
886
+ function formatTime(ts) {
887
+ if (!ts) return '';
888
+ return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
889
+ }
890
+
891
+ function getAvatarColor(name) {
892
+ const colors = ['#e01e5a', '#36c5f0', '#2eb67d', '#ecb22e', '#7c3aed', '#1264a3'];
893
+ let hash = 0;
894
+ for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
895
+ return colors[Math.abs(hash) % colors.length];
896
+ }
897
+
898
+ function getInitials(name) {
899
+ return name.split(/[\s_-]+/).map(w => w[0]).join('').substring(0, 2).toUpperCase();
900
+ }
901
+
902
+ // Select project
903
+ function selectProject(projectId) {
904
+ selectedProjectId = projectId;
905
+
906
+ // Update sidebar active state
907
+ projectsList.querySelectorAll('.channel-item').forEach(item => {
908
+ item.classList.toggle('active', item.dataset.project === projectId);
909
+ });
910
+
911
+ if (projectId === 'all' || !projectId) {
912
+ // Show all projects view
913
+ headerPrefix.textContent = '#';
914
+ headerName.textContent = 'All Projects';
915
+ cardsView.style.display = 'block';
916
+ messagesView.style.display = 'none';
917
+ composer.style.display = 'none';
918
+ agentsSection.style.display = 'none';
919
+ renderProjectCards(currentData.projects);
920
+ } else {
921
+ // Show project detail view
922
+ const project = (currentData.projects || []).find(p => p.id === projectId);
923
+ if (project) {
924
+ headerPrefix.textContent = '@';
925
+ headerName.textContent = project.name || project.id;
926
+ cardsView.style.display = 'none';
927
+ messagesView.style.display = 'block';
928
+ composer.style.display = 'block';
929
+ agentsSection.style.display = 'block';
930
+ renderAgentsList(project.agents || []);
931
+ renderProjectMessages(projectId);
932
+ updateComposerAgents(project.agents || []);
933
+ }
934
+ }
935
+ }
936
+
937
+ // Render sidebar projects
938
+ function renderSidebarProjects(projects) {
939
+ const allItem = '<li class="channel-item ' + (selectedProjectId === 'all' ? 'active' : '') + '" data-project="all"><span class="project-status-dot" style="background: var(--accent-purple);"></span><span class="channel-name">All Projects</span></li>';
940
+
941
+ const projectItems = (projects || []).map(p => {
942
+ const statusClass = p.connected ? 'connected' : (p.reconnecting ? 'reconnecting' : '');
943
+ const activeClass = selectedProjectId === p.id ? 'active' : '';
944
+ return `
945
+ <li class="channel-item ${statusClass} ${activeClass}" data-project="${escapeHtml(p.id)}">
946
+ <span class="project-status-dot"></span>
947
+ <span class="channel-name">${escapeHtml(p.name || p.id)}</span>
948
+ </li>
949
+ `;
950
+ }).join('');
951
+
952
+ projectsList.innerHTML = allItem + projectItems;
953
+
954
+ // Update stats
955
+ document.getElementById('stat-projects').textContent = projects?.length || 0;
956
+ let totalAgents = 0;
957
+ (projects || []).forEach(p => totalAgents += (p.agents || []).length);
958
+ document.getElementById('stat-agents').textContent = totalAgents;
959
+ }
960
+
961
+ // Render agents list for selected project
962
+ function renderAgentsList(agents) {
963
+ if (!agents || agents.length === 0) {
964
+ agentsList.innerHTML = '<li class="channel-item" style="color: var(--text-muted); cursor: default;">No agents</li>';
965
+ return;
966
+ }
967
+
968
+ agentsList.innerHTML = agents.map(a => `
969
+ <li class="channel-item" data-agent="${escapeHtml(a.name)}">
970
+ <div class="agent-avatar" style="background: ${getAvatarColor(a.name)}">
971
+ ${getInitials(a.name)}
972
+ <span class="presence-indicator online"></span>
973
+ </div>
974
+ <span class="channel-name">${escapeHtml(a.name)}</span>
975
+ </li>
976
+ `).join('');
977
+ }
978
+
979
+ // Render project cards
980
+ function renderProjectCards(projects) {
981
+ if (!projects || projects.length === 0) {
982
+ cardsGrid.innerHTML = document.getElementById('empty-state').outerHTML;
983
+ return;
984
+ }
985
+
986
+ cardsGrid.innerHTML = projects.map(p => {
987
+ const agents = p.agents || [];
988
+ const agentsHtml = agents.length > 0
989
+ ? agents.map(a => `
990
+ <div class="card-agent-item">
991
+ <span class="card-agent-dot"></span>
992
+ <span class="card-agent-name">${escapeHtml(a.name)}</span>
993
+ <span class="card-agent-cli">${escapeHtml(a.cli || '')}</span>
994
+ </div>
995
+ `).join('')
996
+ : '<div class="no-agents">No agents connected</div>';
997
+
998
+ return `
999
+ <div class="project-card ${p.connected ? '' : 'offline'}" data-project-id="${escapeHtml(p.id)}">
1000
+ <div class="card-header">
1001
+ <div class="card-title-group">
1002
+ <div class="card-icon">
1003
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1004
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
1005
+ </svg>
1006
+ </div>
1007
+ <div>
1008
+ <div class="card-title">${escapeHtml(p.name || p.id)}</div>
1009
+ <div class="card-path">${escapeHtml(p.path || '')}</div>
1010
+ </div>
1011
+ </div>
1012
+ <div class="card-status ${p.connected ? 'online' : p.reconnecting ? 'reconnecting' : 'offline'}">
1013
+ <span class="dot"></span>
1014
+ <span>${p.connected ? 'Online' : p.reconnecting ? 'Reconnecting...' : 'Offline'}</span>
1015
+ </div>
1016
+ </div>
1017
+ <div class="agents-section">
1018
+ <div class="agents-header">
1019
+ <span class="agents-label">Agents</span>
1020
+ <span class="agents-count">${agents.length} active</span>
1021
+ </div>
1022
+ <div class="card-agents-list">${agentsHtml}</div>
1023
+ </div>
1024
+ </div>
1025
+ `;
1026
+ }).join('');
1027
+ }
1028
+
1029
+ // Render messages for a project
1030
+ function renderProjectMessages(projectId) {
1031
+ // Filter messages for this project
1032
+ const messages = (currentData.messages || []).filter(m =>
1033
+ m.sourceProject === projectId || m.targetProject === projectId || !m.sourceProject
1034
+ );
1035
+
1036
+ if (messages.length === 0) {
1037
+ messagesList.innerHTML = `
1038
+ <div class="empty-state">
1039
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1040
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
1041
+ </svg>
1042
+ <div class="empty-state-title">No messages yet</div>
1043
+ <div class="empty-state-text">Messages in this project will appear here</div>
1044
+ </div>
1045
+ `;
1046
+ return;
1047
+ }
1048
+
1049
+ messagesList.innerHTML = messages.slice(-50).map(m => {
1050
+ const isBroadcast = m.to === '*';
1051
+ const recipientDisplay = isBroadcast ? '@everyone' : `@${escapeHtml(m.to || '')}`;
1052
+ const projectBadge = m.sourceProject && m.sourceProject !== projectId
1053
+ ? `<span class="project-badge">${escapeHtml(m.sourceProject)}</span>`
1054
+ : '';
1055
+
1056
+ return `
1057
+ <div class="message">
1058
+ <div class="message-avatar" style="background: ${getAvatarColor(m.from || 'Unknown')}">
1059
+ ${getInitials(m.from || '?')}
1060
+ </div>
1061
+ <div class="message-content">
1062
+ <div class="message-header">
1063
+ <span class="message-sender">${projectBadge}@${escapeHtml(m.from || 'Unknown')}</span>
1064
+ <span class="message-recipient">→ <span class="target">${recipientDisplay}</span></span>
1065
+ <span class="message-timestamp">${formatTime(m.timestamp)}</span>
1066
+ </div>
1067
+ <div class="message-body">${escapeHtml(m.body || '')}</div>
1068
+ </div>
1069
+ </div>
1070
+ `;
1071
+ }).join('');
1072
+
1073
+ // Scroll to bottom
1074
+ messagesView.scrollTop = messagesView.scrollHeight;
1075
+ }
1076
+
1077
+ // Update composer agents dropdown
1078
+ function updateComposerAgents(agents) {
1079
+ composerAgent.innerHTML = '<option value="">Select agent...</option><option value="*">* (Broadcast)</option>' +
1080
+ (agents || []).map(a => `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`).join('');
1081
+ }
1082
+
1083
+ // Send message
1084
+ async function sendMessage() {
1085
+ if (!selectedProjectId || selectedProjectId === 'all') return;
1086
+
1087
+ const to = composerAgent.value;
1088
+ const message = composerMessage.value.trim();
1089
+ if (!to || !message) return;
1090
+
1091
+ composerSend.disabled = true;
1092
+ composerStatus.textContent = 'Sending...';
1093
+ composerStatus.className = 'composer-status';
1094
+
1095
+ try {
1096
+ const response = await fetch('/api/bridge/send', {
1097
+ method: 'POST',
1098
+ headers: { 'Content-Type': 'application/json' },
1099
+ body: JSON.stringify({ projectId: selectedProjectId, to, message })
1100
+ });
1101
+
1102
+ const result = await response.json();
1103
+ if (response.ok && result.success) {
1104
+ composerMessage.value = '';
1105
+ composerStatus.textContent = 'Sent!';
1106
+ composerStatus.className = 'composer-status success';
1107
+ setTimeout(() => { composerStatus.textContent = ''; }, 2000);
1108
+ } else {
1109
+ throw new Error(result.error || 'Failed');
1110
+ }
1111
+ } catch (err) {
1112
+ composerStatus.textContent = err.message;
1113
+ composerStatus.className = 'composer-status error';
1114
+ }
1115
+
1116
+ composerSend.disabled = false;
1117
+ }
1118
+
1119
+ // Spawn modal
1120
+ function openSpawnModal() {
1121
+ // Populate project dropdown
1122
+ const connectedProjects = (currentData.projects || []).filter(p => p.connected);
1123
+ spawnProject.innerHTML = '<option value="">Select project...</option>' +
1124
+ connectedProjects.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name || p.id)}</option>`).join('');
1125
+
1126
+ // Pre-select current project if viewing one
1127
+ if (selectedProjectId && selectedProjectId !== 'all') {
1128
+ spawnProject.value = selectedProjectId;
1129
+ }
1130
+
1131
+ spawnName.value = '';
1132
+ spawnCli.value = 'claude';
1133
+ spawnTask.value = '';
1134
+ spawnStatus.textContent = '';
1135
+ spawnModalOverlay.classList.add('visible');
1136
+ spawnName.focus();
1137
+ }
1138
+
1139
+ function closeSpawnModal() {
1140
+ spawnModalOverlay.classList.remove('visible');
1141
+ }
1142
+
1143
+ async function submitSpawn() {
1144
+ const projectId = spawnProject.value;
1145
+ const name = spawnName.value.trim();
1146
+ const cli = spawnCli.value.trim() || 'claude';
1147
+ const task = spawnTask.value.trim();
1148
+
1149
+ if (!projectId) {
1150
+ spawnStatus.textContent = 'Please select a project';
1151
+ spawnStatus.className = 'spawn-status error';
1152
+ return;
1153
+ }
1154
+ if (!name) {
1155
+ spawnStatus.textContent = 'Agent name is required';
1156
+ spawnStatus.className = 'spawn-status error';
1157
+ return;
1158
+ }
1159
+
1160
+ spawnSubmit.disabled = true;
1161
+ spawnStatus.textContent = 'Spawning...';
1162
+ spawnStatus.className = 'spawn-status loading';
1163
+
1164
+ try {
1165
+ // Note: This calls the local dashboard's spawn API
1166
+ // For bridge-level spawning, we'd need project-specific spawn endpoints
1167
+ const response = await fetch('/api/spawn', {
1168
+ method: 'POST',
1169
+ headers: { 'Content-Type': 'application/json' },
1170
+ body: JSON.stringify({ name, cli, task })
1171
+ });
1172
+
1173
+ const result = await response.json();
1174
+ if (response.ok && result.success) {
1175
+ spawnStatus.textContent = `Agent "${name}" spawned!`;
1176
+ spawnStatus.className = 'spawn-status success';
1177
+ setTimeout(closeSpawnModal, 1000);
1178
+ } else {
1179
+ throw new Error(result.error || 'Failed to spawn');
1180
+ }
1181
+ } catch (err) {
1182
+ spawnStatus.textContent = err.message;
1183
+ spawnStatus.className = 'spawn-status error';
1184
+ }
1185
+
1186
+ spawnSubmit.disabled = false;
1187
+ }
1188
+
1189
+ // Event listeners
1190
+ projectsList.addEventListener('click', (e) => {
1191
+ const item = e.target.closest('.channel-item');
1192
+ if (item) selectProject(item.dataset.project);
1193
+ });
1194
+
1195
+ cardsGrid.addEventListener('click', (e) => {
1196
+ const card = e.target.closest('.project-card');
1197
+ if (card) selectProject(card.dataset.projectId);
1198
+ });
1199
+
1200
+ composerAgent.addEventListener('change', () => {
1201
+ composerSend.disabled = !composerAgent.value || !composerMessage.value.trim();
1202
+ });
1203
+
1204
+ composerMessage.addEventListener('input', () => {
1205
+ composerSend.disabled = !composerAgent.value || !composerMessage.value.trim();
1206
+ });
1207
+
1208
+ composerMessage.addEventListener('keydown', (e) => {
1209
+ if (e.key === 'Enter' && !e.shiftKey && !composerSend.disabled) {
1210
+ e.preventDefault();
1211
+ sendMessage();
1212
+ }
1213
+ });
1214
+
1215
+ composerSend.addEventListener('click', sendMessage);
1216
+
1217
+ spawnBtn.addEventListener('click', openSpawnModal);
1218
+ spawnModalClose.addEventListener('click', closeSpawnModal);
1219
+ spawnCancel.addEventListener('click', closeSpawnModal);
1220
+ spawnSubmit.addEventListener('click', submitSpawn);
1221
+ spawnModalOverlay.addEventListener('click', (e) => {
1222
+ if (e.target === spawnModalOverlay) closeSpawnModal();
1223
+ });
1224
+
1225
+ document.addEventListener('keydown', (e) => {
1226
+ if (e.key === 'Escape' && spawnModalOverlay.classList.contains('visible')) {
1227
+ closeSpawnModal();
1228
+ }
1229
+ });
1230
+
1231
+ // Main render
1232
+ function render(data) {
1233
+ currentData = data;
1234
+ renderSidebarProjects(data.projects);
1235
+
1236
+ if (selectedProjectId === 'all' || !selectedProjectId) {
1237
+ renderProjectCards(data.projects);
1238
+ } else {
1239
+ const project = (data.projects || []).find(p => p.id === selectedProjectId);
1240
+ if (project) {
1241
+ renderAgentsList(project.agents || []);
1242
+ renderProjectMessages(selectedProjectId);
1243
+ updateComposerAgents(project.agents || []);
1244
+ }
1245
+ }
1246
+ }
1247
+
1248
+ // WebSocket connection
1249
+ function connect() {
1250
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1251
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/bridge`);
1252
+
1253
+ ws.onopen = () => statusDot.classList.remove('offline');
1254
+ ws.onclose = () => { statusDot.classList.add('offline'); setTimeout(connect, 3000); };
1255
+ ws.onerror = () => statusDot.classList.add('offline');
1256
+
1257
+ ws.onmessage = (e) => {
1258
+ try {
1259
+ const data = JSON.parse(e.data);
1260
+ render(data);
1261
+ } catch (err) {
1262
+ console.error('Parse error:', err);
1263
+ }
1264
+ };
1265
+ }
1266
+
1267
+ // Initialize
1268
+ selectProject('all');
1269
+ connect();
1270
+ </script>
1271
+ </body>
1272
+ </html>