agent-relay 1.0.8 → 1.0.11

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 (126) hide show
  1. package/README.md +129 -8
  2. package/bin/.gitkeep +0 -0
  3. package/bin/tmux +0 -0
  4. package/dist/bridge/config.d.ts +41 -0
  5. package/dist/bridge/config.d.ts.map +1 -0
  6. package/dist/bridge/config.js +143 -0
  7. package/dist/bridge/config.js.map +1 -0
  8. package/dist/bridge/index.d.ts +10 -0
  9. package/dist/bridge/index.d.ts.map +1 -0
  10. package/dist/bridge/index.js +10 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/bridge/multi-project-client.d.ts +99 -0
  13. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  14. package/dist/bridge/multi-project-client.js +386 -0
  15. package/dist/bridge/multi-project-client.js.map +1 -0
  16. package/dist/bridge/spawner.d.ts +47 -0
  17. package/dist/bridge/spawner.d.ts.map +1 -0
  18. package/dist/bridge/spawner.js +227 -0
  19. package/dist/bridge/spawner.js.map +1 -0
  20. package/dist/bridge/types.d.ts +55 -0
  21. package/dist/bridge/types.d.ts.map +1 -0
  22. package/dist/bridge/types.js +6 -0
  23. package/dist/bridge/types.js.map +1 -0
  24. package/dist/bridge/utils.d.ts +30 -0
  25. package/dist/bridge/utils.d.ts.map +1 -0
  26. package/dist/bridge/utils.js +54 -0
  27. package/dist/bridge/utils.js.map +1 -0
  28. package/dist/cli/index.js +599 -8
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/daemon/agent-registry.d.ts.map +1 -1
  31. package/dist/daemon/agent-registry.js +6 -1
  32. package/dist/daemon/agent-registry.js.map +1 -1
  33. package/dist/daemon/connection.d.ts +22 -0
  34. package/dist/daemon/connection.d.ts.map +1 -1
  35. package/dist/daemon/connection.js +59 -13
  36. package/dist/daemon/connection.js.map +1 -1
  37. package/dist/daemon/router.d.ts +27 -0
  38. package/dist/daemon/router.d.ts.map +1 -1
  39. package/dist/daemon/router.js +108 -3
  40. package/dist/daemon/router.js.map +1 -1
  41. package/dist/daemon/server.d.ts +8 -0
  42. package/dist/daemon/server.d.ts.map +1 -1
  43. package/dist/daemon/server.js +95 -23
  44. package/dist/daemon/server.js.map +1 -1
  45. package/dist/dashboard/metrics.d.ts +105 -0
  46. package/dist/dashboard/metrics.d.ts.map +1 -0
  47. package/dist/dashboard/metrics.js +192 -0
  48. package/dist/dashboard/metrics.js.map +1 -0
  49. package/dist/dashboard/needs-attention.d.ts +24 -0
  50. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  51. package/dist/dashboard/needs-attention.js +78 -0
  52. package/dist/dashboard/needs-attention.js.map +1 -0
  53. package/dist/dashboard/public/bridge.html +1272 -0
  54. package/dist/dashboard/public/index.html +2017 -879
  55. package/dist/dashboard/public/js/app.js +184 -0
  56. package/dist/dashboard/public/js/app.js.map +7 -0
  57. package/dist/dashboard/public/metrics.html +999 -0
  58. package/dist/dashboard/server.d.ts +13 -0
  59. package/dist/dashboard/server.d.ts.map +1 -1
  60. package/dist/dashboard/server.js +594 -13
  61. package/dist/dashboard/server.js.map +1 -1
  62. package/dist/dashboard/start.js +1 -1
  63. package/dist/dashboard/start.js.map +1 -1
  64. package/dist/dashboard-v2/index.d.ts +10 -0
  65. package/dist/dashboard-v2/index.d.ts.map +1 -0
  66. package/dist/dashboard-v2/index.js +54 -0
  67. package/dist/dashboard-v2/index.js.map +1 -0
  68. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  69. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  70. package/dist/dashboard-v2/lib/api.js +270 -0
  71. package/dist/dashboard-v2/lib/api.js.map +1 -0
  72. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  73. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  74. package/dist/dashboard-v2/lib/colors.js +198 -0
  75. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  76. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  77. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  78. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  79. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  80. package/dist/dashboard-v2/types/index.d.ts +154 -0
  81. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  82. package/dist/dashboard-v2/types/index.js +6 -0
  83. package/dist/dashboard-v2/types/index.js.map +1 -0
  84. package/dist/storage/adapter.d.ts +21 -1
  85. package/dist/storage/adapter.d.ts.map +1 -1
  86. package/dist/storage/adapter.js +36 -0
  87. package/dist/storage/adapter.js.map +1 -1
  88. package/dist/storage/sqlite-adapter.d.ts +34 -0
  89. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  90. package/dist/storage/sqlite-adapter.js +253 -12
  91. package/dist/storage/sqlite-adapter.js.map +1 -1
  92. package/dist/utils/agent-config.d.ts +45 -0
  93. package/dist/utils/agent-config.d.ts.map +1 -0
  94. package/dist/utils/agent-config.js +118 -0
  95. package/dist/utils/agent-config.js.map +1 -0
  96. package/dist/utils/tmux-resolver.d.ts +55 -0
  97. package/dist/utils/tmux-resolver.d.ts.map +1 -0
  98. package/dist/utils/tmux-resolver.js +175 -0
  99. package/dist/utils/tmux-resolver.js.map +1 -0
  100. package/dist/wrapper/client.d.ts +8 -0
  101. package/dist/wrapper/client.d.ts.map +1 -1
  102. package/dist/wrapper/client.js +26 -0
  103. package/dist/wrapper/client.js.map +1 -1
  104. package/dist/wrapper/parser.d.ts +17 -0
  105. package/dist/wrapper/parser.d.ts.map +1 -1
  106. package/dist/wrapper/parser.js +334 -10
  107. package/dist/wrapper/parser.js.map +1 -1
  108. package/dist/wrapper/tmux-wrapper.d.ts +38 -2
  109. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  110. package/dist/wrapper/tmux-wrapper.js +196 -32
  111. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  112. package/docs/AGENTS.md +105 -0
  113. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  114. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  115. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  116. package/docs/MONETIZATION.md +1679 -0
  117. package/docs/agent-relay-snippet.md +61 -0
  118. package/docs/dashboard-v2-plan.md +179 -0
  119. package/package.json +8 -3
  120. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +88 -0
  121. package/scripts/dev/dev-team-setup.sh +431 -0
  122. package/scripts/e2e-test.sh +119 -0
  123. package/scripts/games/game-protocol.md +79 -0
  124. package/scripts/games/hearts-setup.sh +264 -0
  125. package/scripts/postinstall.js +225 -0
  126. package/scripts/tictactoe-setup.sh +181 -0
@@ -1,1124 +1,2262 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4
6
  <title>Agent Relay</title>
5
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
6
8
  <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">
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">
8
10
  <style>
11
+ /* ========================================
12
+ Design Tokens
13
+ ======================================== */
9
14
  :root {
10
- --bg-primary: #09090b;
11
- --bg-secondary: #18181b;
12
- --bg-card: #18181b;
13
- --bg-elevated: #27272a;
14
- --border-color: #27272a;
15
- --border-hover: #3f3f46;
16
- --primary: #3b82f6;
17
- --primary-muted: rgba(59, 130, 246, 0.15);
18
- --text: #fafafa;
19
- --text-secondary: #a1a1aa;
20
- --text-muted: #71717a;
21
- --success: #22c55e;
22
- --success-muted: rgba(34, 197, 94, 0.15);
23
- --error: #ef4444;
24
- --error-muted: rgba(239, 68, 68, 0.15);
25
- --warning: #f59e0b;
26
- --warning-muted: rgba(245, 158, 11, 0.15);
27
- }
28
-
29
- * { box-sizing: border-box; margin: 0; padding: 0; }
30
-
31
- body {
32
- font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
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;
38
- color: var(--text);
39
- min-height: 100vh;
40
- overflow-x: hidden;
41
- }
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); }
15
+ /* Background colors */
16
+ --bg-workspace: #1a1d21;
17
+ --bg-sidebar: #19171d;
18
+ --bg-channel-active: #1164a3;
19
+ --bg-channel-hover: rgba(255, 255, 255, 0.04);
20
+ --bg-main: #222529;
21
+ --bg-message-hover: rgba(255, 255, 255, 0.03);
22
+ --bg-composer: #222529;
23
+ --bg-input: #222529;
24
+ --bg-modal: #1a1d21;
25
+ --bg-tooltip: #1d1c21;
26
+
27
+ /* Text colors */
28
+ --text-primary: #d1d2d3;
29
+ --text-secondary: #ababad;
30
+ --text-muted: #8d8d8e;
31
+ --text-channel: #bcabbc;
32
+ --text-channel-active: #ffffff;
33
+ --text-link: #1d9bd1;
34
+
35
+ /* Accent colors */
36
+ --accent-primary: #1264a3;
37
+ --accent-green: #2bac76;
38
+ --accent-yellow: #e8a427;
39
+ --accent-red: #e01e5a;
40
+ --accent-purple: #7c3aed;
41
+
42
+ /* Status colors */
43
+ --status-online: #2bac76;
44
+ --status-away: #e8a427;
45
+ --status-offline: #616061;
46
+ --status-busy: #e01e5a;
47
+
48
+ /* Border colors */
49
+ --border-subtle: rgba(255, 255, 255, 0.1);
50
+ --border-divider: rgba(255, 255, 255, 0.06);
51
+
52
+ /* Badge colors */
53
+ --badge-bg: #e01e5a;
54
+ --badge-text: #ffffff;
55
+
56
+ /* Shadows */
57
+ --shadow-modal: 0 18px 50px rgba(0, 0, 0, 0.6);
58
+ --shadow-message: 0 1px 2px rgba(0, 0, 0, 0.1);
59
+
60
+ /* Dimensions */
61
+ --sidebar-width: 260px;
62
+ --header-height: 49px;
63
+ --composer-min-height: 44px;
64
+
65
+ /* Typography */
66
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
67
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
68
+
69
+ /* Transitions */
70
+ --transition-fast: 0.1s ease;
71
+ --transition-normal: 0.2s ease;
72
+ }
73
+
74
+ /* ========================================
75
+ Reset & Base
76
+ ======================================== */
77
+ *, *::before, *::after {
78
+ box-sizing: border-box;
79
+ margin: 0;
80
+ padding: 0;
81
+ }
82
+
83
+ html, body {
84
+ height: 100%;
85
+ overflow: hidden;
51
86
  }
52
87
 
53
- .container {
54
- max-width: 1400px;
55
- margin: 0 auto;
56
- display: grid;
57
- grid-template-columns: 320px 1fr;
58
- gap: 1px;
88
+ body {
89
+ font-family: var(--font-family);
90
+ font-size: 15px;
91
+ line-height: 1.46668;
92
+ color: var(--text-primary);
93
+ background: var(--bg-workspace);
94
+ -webkit-font-smoothing: antialiased;
95
+ -moz-osx-font-smoothing: grayscale;
96
+ }
97
+
98
+ /* ========================================
99
+ Layout Structure
100
+ ======================================== */
101
+ .app-container {
102
+ display: flex;
59
103
  height: 100vh;
60
- background: var(--border-color);
104
+ width: 100vw;
61
105
  }
62
106
 
107
+ /* Sidebar */
63
108
  .sidebar {
109
+ width: var(--sidebar-width);
110
+ background: var(--bg-sidebar);
64
111
  display: flex;
65
112
  flex-direction: column;
66
- background: var(--bg-primary);
67
- overflow: hidden;
113
+ flex-shrink: 0;
114
+ border-right: 1px solid var(--border-divider);
68
115
  }
69
116
 
70
- .main-content {
117
+ /* Main Content */
118
+ .main-panel {
119
+ flex: 1;
71
120
  display: flex;
72
121
  flex-direction: column;
73
- background: var(--bg-primary);
74
- overflow: hidden;
122
+ min-width: 0;
123
+ background: var(--bg-main);
75
124
  }
76
125
 
77
- .header {
126
+ /* ========================================
127
+ Sidebar Components
128
+ ======================================== */
129
+
130
+ /* Workspace Header */
131
+ .workspace-header {
132
+ height: var(--header-height);
133
+ padding: 0 16px;
78
134
  display: flex;
79
- justify-content: space-between;
80
135
  align-items: center;
81
- padding: 16px 20px;
82
- border-bottom: 1px solid var(--border-color);
83
- box-shadow: 0 1px 0 rgba(59, 130, 246, 0.2);
136
+ justify-content: space-between;
137
+ border-bottom: 1px solid var(--border-divider);
138
+ flex-shrink: 0;
84
139
  }
85
140
 
86
- .logo {
141
+ .workspace-name {
142
+ font-size: 18px;
143
+ font-weight: 700;
144
+ color: var(--text-primary);
87
145
  display: flex;
88
146
  align-items: center;
89
- gap: 10px;
147
+ gap: 6px;
90
148
  }
91
149
 
92
- .logo-icon {
93
- width: 32px;
94
- height: 32px;
95
- background: var(--primary);
96
- border-radius: 8px;
97
- display: flex;
98
- align-items: center;
99
- justify-content: center;
100
- color: white;
101
- font-weight: 600;
102
- font-size: 14px;
150
+ .workspace-name .status-dot {
151
+ width: 10px;
152
+ height: 10px;
153
+ border-radius: 50%;
154
+ background: var(--status-online);
155
+ flex-shrink: 0;
103
156
  }
104
157
 
105
- h1 {
106
- font-size: 1rem;
107
- font-weight: 600;
108
- color: var(--text);
158
+ .workspace-name .status-dot.offline {
159
+ background: var(--status-offline);
109
160
  }
110
161
 
111
- h2 {
112
- font-size: 0.875rem;
113
- font-weight: 500;
114
- color: var(--text-secondary);
162
+ .compose-new-btn {
163
+ width: 36px;
164
+ height: 36px;
165
+ border-radius: 50%;
166
+ background: rgba(255, 255, 255, 0.1);
167
+ border: none;
168
+ color: var(--text-primary);
169
+ cursor: pointer;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ transition: background var(--transition-fast);
115
174
  }
116
175
 
117
- .agents-section {
118
- flex: 1;
119
- overflow-y: auto;
120
- padding: 12px;
176
+ .compose-new-btn:hover {
177
+ background: rgba(255, 255, 255, 0.2);
121
178
  }
122
179
 
123
- .agents-header {
124
- padding: 8px 8px 12px;
125
- font-size: 0.75rem;
126
- font-weight: 500;
127
- color: var(--text-muted);
128
- text-transform: uppercase;
129
- letter-spacing: 0.05em;
180
+ /* Command Palette Trigger */
181
+ .search-bar {
182
+ margin: 12px 12px 8px;
183
+ padding: 6px 12px;
184
+ background: rgba(255, 255, 255, 0.06);
185
+ border: 1px solid transparent;
186
+ border-radius: 6px;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ cursor: pointer;
191
+ transition: all var(--transition-fast);
130
192
  }
131
193
 
132
- .agent-card {
133
- padding: 12px;
134
- border-radius: 8px;
135
- margin-bottom: 4px;
136
- transition: background 0.15s, border-color 0.3s, opacity 0.3s;
137
- cursor: default;
138
- border-left: 3px solid var(--success);
194
+ .search-bar:hover {
195
+ background: rgba(255, 255, 255, 0.1);
139
196
  }
140
197
 
141
- .agent-card.disconnected {
142
- border-left-color: var(--error);
143
- opacity: 0.6;
198
+ .search-bar svg {
199
+ width: 16px;
200
+ height: 16px;
201
+ color: var(--text-muted);
144
202
  }
145
203
 
146
- .agent-card.disconnected .status-dot {
147
- background: var(--error);
148
- color: var(--error);
149
- animation: none;
204
+ .search-bar span {
205
+ font-size: 13px;
206
+ color: var(--text-muted);
207
+ flex: 1;
150
208
  }
151
209
 
152
- .agent-card:hover {
153
- background: var(--bg-secondary);
210
+ .search-bar kbd {
211
+ font-family: var(--font-family);
212
+ font-size: 11px;
213
+ color: var(--text-muted);
214
+ background: rgba(255, 255, 255, 0.1);
215
+ padding: 2px 6px;
216
+ border-radius: 4px;
154
217
  }
155
218
 
156
- .agent-card.active {
157
- background: var(--bg-elevated);
219
+ /* Sidebar Sections */
220
+ .sidebar-content {
221
+ flex: 1;
222
+ overflow-y: auto;
223
+ padding: 8px 0;
158
224
  }
159
225
 
160
- .agent-header {
226
+ .section-header {
227
+ padding: 8px 16px 4px;
161
228
  display: flex;
162
- justify-content: space-between;
163
229
  align-items: center;
164
- margin-bottom: 8px;
230
+ justify-content: space-between;
231
+ cursor: pointer;
165
232
  }
166
233
 
167
- .agent-name {
234
+ .section-title {
235
+ font-size: 15px;
168
236
  font-weight: 600;
169
- font-size: 0.875rem;
170
- color: var(--text);
171
- }
172
-
173
- .agent-meta {
237
+ color: var(--text-channel);
174
238
  display: flex;
175
239
  align-items: center;
176
- gap: 6px;
177
- margin-bottom: 10px;
240
+ gap: 4px;
178
241
  }
179
242
 
180
- .agent-role {
181
- font-size: 0.75rem;
182
- color: var(--text-muted);
243
+ .section-title svg {
244
+ width: 12px;
245
+ height: 12px;
246
+ transition: transform var(--transition-fast);
183
247
  }
184
248
 
185
- .cli-badge {
186
- background: var(--bg-elevated);
187
- color: var(--text-secondary);
188
- padding: 2px 6px;
189
- border-radius: 4px;
190
- font-size: 0.7rem;
191
- font-weight: 500;
249
+ .section-header.collapsed .section-title svg {
250
+ transform: rotate(-90deg);
192
251
  }
193
252
 
194
- .agent-status {
253
+ .section-add-btn {
254
+ width: 24px;
255
+ height: 24px;
256
+ border: none;
257
+ background: transparent;
258
+ color: var(--text-muted);
259
+ cursor: pointer;
260
+ border-radius: 4px;
195
261
  display: flex;
196
262
  align-items: center;
197
- gap: 6px;
198
- font-size: 0.8rem;
199
- color: var(--text-secondary);
263
+ justify-content: center;
264
+ opacity: 0;
265
+ transition: all var(--transition-fast);
200
266
  }
201
267
 
202
- .status-dot {
203
- width: 6px;
204
- height: 6px;
205
- border-radius: 50%;
206
- background: var(--success);
207
- color: var(--success);
208
- animation: pulse-glow 2s ease-in-out infinite;
268
+ .section-header:hover .section-add-btn {
269
+ opacity: 1;
209
270
  }
210
271
 
211
- .badge {
212
- background: var(--primary-muted);
213
- color: var(--primary);
214
- padding: 2px 8px;
215
- border-radius: 10px;
216
- font-size: 0.7rem;
217
- font-weight: 600;
272
+ .section-add-btn:hover {
273
+ background: rgba(255, 255, 255, 0.1);
274
+ color: var(--text-primary);
218
275
  }
219
276
 
220
- /* Activity Log */
221
- .activity-log {
222
- display: flex;
223
- flex-direction: column;
224
- flex: 1;
225
- overflow: hidden;
277
+ /* Channel/Agent List */
278
+ .channel-list {
279
+ list-style: none;
226
280
  }
227
281
 
228
- .log-header {
229
- padding: 16px 20px;
230
- border-bottom: 1px solid var(--border-color);
231
- display: flex;
232
- justify-content: space-between;
233
- align-items: center;
282
+ /* Sidebar Footer */
283
+ .sidebar-footer {
284
+ padding: 12px 16px;
285
+ border-top: 1px solid var(--border-divider);
234
286
  }
235
287
 
236
- .live-indicator {
288
+ .nav-link {
237
289
  display: flex;
238
290
  align-items: center;
239
- gap: 6px;
240
- font-size: 0.75rem;
291
+ gap: 8px;
292
+ padding: 8px 12px;
241
293
  color: var(--text-muted);
294
+ text-decoration: none;
295
+ font-size: 13px;
296
+ border-radius: 6px;
297
+ transition: all var(--transition-fast);
242
298
  }
243
299
 
244
- .live-dot {
245
- width: 6px;
246
- height: 6px;
247
- border-radius: 50%;
248
- background: var(--success);
249
- color: var(--success);
250
- animation: pulse-glow 2s ease-in-out infinite;
300
+ .nav-link:hover {
301
+ background: var(--bg-channel-hover);
302
+ color: var(--text-primary);
251
303
  }
252
304
 
253
- .log-content {
254
- overflow-y: auto;
255
- flex: 1;
256
- display: flex;
257
- flex-direction: column-reverse;
305
+ .nav-link svg {
306
+ width: 16px;
307
+ height: 16px;
258
308
  }
259
309
 
260
- .message {
261
- padding: 16px 20px;
262
- border-bottom: 1px solid var(--border-color);
310
+ .channel-item {
263
311
  display: flex;
264
- gap: 12px;
265
- transition: background 0.15s;
266
- animation: slideIn 0.2s ease-out;
267
- border-left: 3px solid transparent;
312
+ align-items: center;
313
+ padding: 6px 20px 6px 16px;
314
+ cursor: pointer;
315
+ transition: background var(--transition-fast);
316
+ gap: 8px;
317
+ position: relative;
268
318
  }
269
319
 
270
- .message.broadcast {
271
- border-left-color: var(--warning);
272
- background: var(--warning-muted);
320
+ .channel-item:hover {
321
+ background: var(--bg-channel-hover);
273
322
  }
274
323
 
275
- .message.broadcast .msg-target {
276
- color: var(--warning);
277
- font-weight: 600;
324
+ .channel-item.active {
325
+ background: var(--bg-channel-active);
278
326
  }
279
327
 
280
- .message.direct {
281
- border-left-color: var(--primary);
328
+ .channel-item.active .channel-name,
329
+ .channel-item.active .channel-prefix {
330
+ color: var(--text-channel-active);
282
331
  }
283
332
 
284
- .message:hover {
285
- background: var(--bg-secondary);
333
+ .channel-prefix {
334
+ font-size: 15px;
335
+ color: var(--text-muted);
336
+ width: 18px;
337
+ text-align: center;
338
+ flex-shrink: 0;
286
339
  }
287
340
 
288
- .message:last-child { border-bottom: none; }
341
+ .channel-name {
342
+ font-size: 15px;
343
+ color: var(--text-channel);
344
+ flex: 1;
345
+ white-space: nowrap;
346
+ overflow: hidden;
347
+ text-overflow: ellipsis;
348
+ }
289
349
 
290
- .msg-avatar {
291
- width: 36px;
292
- height: 36px;
293
- border-radius: 8px;
294
- background: var(--bg-elevated);
295
- color: var(--text-secondary);
296
- display: flex;
297
- align-items: center;
298
- justify-content: center;
350
+ .channel-item.unread .channel-name {
299
351
  font-weight: 600;
300
- font-size: 0.8rem;
301
- flex-shrink: 0;
352
+ color: var(--text-primary);
302
353
  }
303
354
 
304
- .msg-body { flex: 1; min-width: 0; }
305
-
306
- .msg-meta {
355
+ .unread-badge {
356
+ min-width: 18px;
357
+ height: 18px;
358
+ padding: 0 5px;
359
+ background: var(--badge-bg);
360
+ color: var(--badge-text);
361
+ font-size: 12px;
362
+ font-weight: 600;
363
+ border-radius: 9px;
307
364
  display: flex;
308
365
  align-items: center;
309
- gap: 8px;
310
- margin-bottom: 6px;
366
+ justify-content: center;
311
367
  }
312
368
 
313
- .msg-sender {
369
+ /* Agent item with presence */
370
+ .agent-avatar {
371
+ width: 20px;
372
+ height: 20px;
373
+ border-radius: 4px;
374
+ background: var(--accent-purple);
375
+ color: white;
376
+ font-size: 10px;
314
377
  font-weight: 600;
315
- font-size: 0.875rem;
316
- color: var(--text);
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ position: relative;
382
+ flex-shrink: 0;
317
383
  }
318
384
 
319
- .msg-arrow {
320
- color: var(--text-muted);
321
- font-size: 0.75rem;
385
+ .presence-indicator {
386
+ position: absolute;
387
+ bottom: -2px;
388
+ right: -2px;
389
+ width: 10px;
390
+ height: 10px;
391
+ border-radius: 50%;
392
+ border: 2px solid var(--bg-sidebar);
393
+ background: var(--status-offline);
322
394
  }
323
395
 
324
- .msg-target {
325
- color: var(--primary);
326
- font-weight: 500;
327
- font-size: 0.875rem;
396
+ .presence-indicator.online {
397
+ background: var(--status-online);
328
398
  }
329
399
 
330
- .msg-time {
331
- color: var(--text-muted);
332
- font-size: 0.75rem;
333
- margin-left: auto;
400
+ .presence-indicator.away {
401
+ background: var(--status-away);
334
402
  }
335
403
 
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;
404
+ .presence-indicator.busy {
405
+ background: var(--status-busy);
343
406
  }
344
407
 
345
- .filter-section {
346
- display: flex;
347
- align-items: center;
348
- gap: 8px;
408
+ /* Needs Attention Indicator */
409
+ .channel-item.needs-attention {
410
+ background: rgba(232, 164, 39, 0.1);
349
411
  }
350
412
 
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;
413
+ .channel-item.needs-attention::after {
414
+ content: '';
415
+ position: absolute;
416
+ top: 50%;
417
+ right: 12px;
418
+ transform: translateY(-50%);
419
+ width: 8px;
420
+ height: 8px;
421
+ background: var(--accent-yellow);
422
+ border-radius: 50%;
423
+ animation: attentionPulse 1.5s ease-in-out infinite;
359
424
  }
360
425
 
361
- .thread-filter:focus {
362
- border-color: var(--primary);
363
- outline: none;
426
+ .attention-badge {
427
+ display: inline-flex;
428
+ align-items: center;
429
+ gap: 4px;
430
+ padding: 2px 6px;
431
+ margin-left: 8px;
432
+ background: var(--accent-yellow);
433
+ color: #1a1d21;
434
+ font-size: 10px;
435
+ font-weight: 600;
436
+ border-radius: 4px;
437
+ text-transform: uppercase;
438
+ letter-spacing: 0.02em;
364
439
  }
365
440
 
366
- .msg-text {
367
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
368
- font-size: 0.8rem;
369
- white-space: pre-wrap;
370
- color: var(--text-secondary);
371
- background: var(--bg-secondary);
372
- padding: 12px;
373
- border-radius: 6px;
374
- line-height: 1.5;
441
+ @keyframes attentionPulse {
442
+ 0%, 100% {
443
+ opacity: 1;
444
+ transform: translateY(-50%) scale(1);
445
+ }
446
+ 50% {
447
+ opacity: 0.5;
448
+ transform: translateY(-50%) scale(1.2);
449
+ }
375
450
  }
376
451
 
377
- /* Connection Status */
378
- #connection-status {
379
- font-size: 0.75rem;
380
- font-weight: 500;
381
- padding: 4px 10px;
382
- border-radius: 6px;
383
- background: var(--success-muted);
384
- color: var(--success);
452
+ /* ========================================
453
+ Main Panel Components
454
+ ======================================== */
455
+
456
+ /* Channel Header */
457
+ .channel-header {
458
+ height: var(--header-height);
459
+ padding: 0 20px;
385
460
  display: flex;
386
461
  align-items: center;
387
- gap: 6px;
388
- transition: all 0.15s;
462
+ border-bottom: 1px solid var(--border-divider);
463
+ flex-shrink: 0;
464
+ gap: 8px;
389
465
  }
390
466
 
391
- #connection-status .dot {
392
- width: 6px;
393
- height: 6px;
394
- border-radius: 50%;
395
- background: var(--success);
467
+ .channel-header-name {
468
+ font-size: 18px;
469
+ font-weight: 700;
470
+ color: var(--text-primary);
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 6px;
396
474
  }
397
475
 
398
- .disconnected {
399
- background: var(--error-muted) !important;
400
- color: var(--error) !important;
476
+ .channel-header-name .prefix {
477
+ color: var(--text-muted);
478
+ font-weight: 500;
401
479
  }
402
480
 
403
- .disconnected .dot {
404
- background: var(--error) !important;
481
+ .header-divider {
482
+ width: 1px;
483
+ height: 20px;
484
+ background: var(--border-subtle);
485
+ margin: 0 8px;
405
486
  }
406
487
 
407
- @keyframes spin {
408
- from { transform: rotate(0deg); }
409
- to { transform: rotate(360deg); }
488
+ .channel-topic {
489
+ font-size: 13px;
490
+ color: var(--text-muted);
491
+ flex: 1;
492
+ white-space: nowrap;
493
+ overflow: hidden;
494
+ text-overflow: ellipsis;
410
495
  }
411
496
 
412
- #connection-status.reconnecting,
413
- #connection-status.connecting {
414
- background: var(--warning-muted);
415
- color: var(--warning);
497
+ .header-actions {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 4px;
416
501
  }
417
502
 
418
- #connection-status.reconnecting .dot,
419
- #connection-status.connecting .dot {
503
+ .header-action-btn {
504
+ width: 28px;
505
+ height: 28px;
506
+ border: none;
420
507
  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
-
437
- .empty-state {
438
- text-align: center;
439
- padding: 48px 24px;
440
508
  color: var(--text-muted);
509
+ border-radius: 4px;
510
+ cursor: pointer;
511
+ display: flex;
512
+ align-items: center;
513
+ justify-content: center;
514
+ transition: all var(--transition-fast);
441
515
  }
442
516
 
443
- .empty-state-text {
444
- font-size: 0.875rem;
445
- line-height: 1.6;
446
- }
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;
517
+ .header-action-btn:hover {
518
+ background: rgba(255, 255, 255, 0.1);
519
+ color: var(--text-primary);
454
520
  }
455
521
 
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;
522
+ .online-count {
523
+ font-size: 13px;
524
+ color: var(--text-muted);
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 4px;
465
528
  }
466
529
 
467
- .compose-wrapper {
468
- display: flex;
469
- gap: 16px;
470
- align-items: stretch;
530
+ .online-count .dot {
531
+ width: 8px;
532
+ height: 8px;
533
+ border-radius: 50%;
534
+ background: var(--status-online);
471
535
  }
472
536
 
473
- .compose-input-area {
537
+ /* Message List */
538
+ .messages-container {
474
539
  flex: 1;
540
+ overflow-y: auto;
541
+ overflow-x: hidden;
475
542
  display: flex;
476
543
  flex-direction: column;
477
- gap: 10px;
478
544
  }
479
545
 
480
- .compose-meta-row {
481
- display: flex;
482
- align-items: center;
483
- gap: 16px;
546
+ .messages-list {
547
+ flex: 1;
548
+ padding: 16px 0;
484
549
  }
485
550
 
486
- .compose-to {
551
+ /* Date Divider */
552
+ .date-divider {
487
553
  display: flex;
488
554
  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;
555
+ margin: 16px 20px;
499
556
  }
500
557
 
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;
558
+ .date-divider::before,
559
+ .date-divider::after {
560
+ content: '';
561
+ flex: 1;
562
+ height: 1px;
563
+ background: var(--border-divider);
518
564
  }
519
565
 
520
- .compose-to select:hover {
521
- border-color: var(--border-hover);
566
+ .date-divider-text {
567
+ padding: 4px 16px;
568
+ font-size: 13px;
569
+ font-weight: 600;
570
+ color: var(--text-secondary);
571
+ background: var(--bg-main);
572
+ border: 1px solid var(--border-divider);
573
+ border-radius: 24px;
522
574
  }
523
575
 
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);
576
+ /* Message */
577
+ .message {
578
+ padding: 8px 20px;
579
+ display: flex;
580
+ gap: 8px;
581
+ transition: background var(--transition-fast);
582
+ position: relative;
527
583
  }
528
584
 
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;
585
+ .message:hover {
586
+ background: var(--bg-message-hover);
536
587
  }
537
588
 
538
- .compose-section:focus-within .compose-hint {
589
+ .message:hover .message-actions {
539
590
  opacity: 1;
540
591
  }
541
592
 
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 {
593
+ .message-avatar {
594
+ width: 36px;
595
+ height: 36px;
596
+ border-radius: 4px;
597
+ flex-shrink: 0;
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ font-weight: 600;
602
+ font-size: 14px;
603
+ color: white;
604
+ }
605
+
606
+ .message-content {
607
+ flex: 1;
608
+ min-width: 0;
609
+ }
610
+
611
+ .message-header {
612
+ display: flex;
613
+ align-items: baseline;
614
+ gap: 8px;
615
+ margin-bottom: 4px;
616
+ }
617
+
618
+ .message-sender {
619
+ font-size: 15px;
620
+ font-weight: 700;
621
+ color: var(--text-primary);
622
+ }
623
+
624
+ .message-recipient {
625
+ font-size: 13px;
626
+ color: var(--text-muted);
627
+ }
628
+
629
+ .message-recipient .target {
630
+ color: var(--text-link);
631
+ }
632
+
633
+ .project-badge {
634
+ display: inline-block;
635
+ background: var(--accent-primary);
636
+ color: white;
637
+ font-size: 10px;
638
+ font-weight: 600;
639
+ padding: 2px 6px;
640
+ border-radius: 4px;
641
+ margin-right: 4px;
642
+ text-transform: uppercase;
643
+ letter-spacing: 0.5px;
644
+ }
645
+
646
+ .message-timestamp {
647
+ font-size: 12px;
648
+ color: var(--text-muted);
649
+ }
650
+
651
+ .message-body {
652
+ font-size: 15px;
653
+ line-height: 1.46668;
654
+ color: var(--text-primary);
655
+ word-wrap: break-word;
656
+ white-space: pre-wrap;
657
+ }
658
+
659
+ .message-body code {
660
+ font-family: var(--font-mono);
661
+ font-size: 13px;
662
+ background: rgba(255, 255, 255, 0.06);
663
+ padding: 2px 4px;
664
+ border-radius: 3px;
665
+ }
666
+
667
+ .message-body pre {
668
+ font-family: var(--font-mono);
669
+ font-size: 13px;
670
+ background: rgba(0, 0, 0, 0.3);
671
+ padding: 12px;
672
+ border-radius: 4px;
673
+ margin: 8px 0;
674
+ overflow-x: auto;
675
+ white-space: pre-wrap;
676
+ word-wrap: break-word;
677
+ }
678
+
679
+ .message-body pre code {
680
+ background: transparent;
681
+ padding: 0;
682
+ border-radius: 0;
683
+ }
684
+
685
+ .message-body strong {
686
+ font-weight: 700;
687
+ color: var(--text-primary);
688
+ }
689
+
690
+ .message-body em {
691
+ font-style: italic;
692
+ }
693
+
694
+ /* Broadcast message styling */
695
+ .message.broadcast {
696
+ border-left: 3px solid var(--accent-yellow);
697
+ margin-left: 17px;
698
+ padding-left: 17px;
699
+ }
700
+
701
+ .message.broadcast .message-recipient .target {
702
+ color: var(--accent-yellow);
703
+ }
704
+
705
+ /* Message Actions (hover toolbar) */
706
+ .message-actions {
707
+ position: absolute;
708
+ top: -16px;
709
+ right: 20px;
710
+ background: var(--bg-tooltip);
711
+ border: 1px solid var(--border-subtle);
712
+ border-radius: 6px;
713
+ padding: 4px;
714
+ display: flex;
715
+ gap: 2px;
716
+ opacity: 0;
717
+ transition: opacity var(--transition-fast);
718
+ box-shadow: var(--shadow-message);
719
+ }
720
+
721
+ .message-action-btn {
722
+ width: 28px;
723
+ height: 28px;
724
+ border: none;
725
+ background: transparent;
726
+ color: var(--text-muted);
727
+ border-radius: 4px;
728
+ cursor: pointer;
729
+ display: flex;
730
+ align-items: center;
731
+ justify-content: center;
732
+ transition: all var(--transition-fast);
733
+ }
734
+
735
+ .message-action-btn:hover {
736
+ background: rgba(255, 255, 255, 0.1);
737
+ color: var(--text-primary);
738
+ }
739
+
740
+ /* Thread Indicator */
741
+ .thread-indicator {
742
+ display: inline-flex;
743
+ align-items: center;
744
+ gap: 4px;
745
+ margin-top: 6px;
746
+ padding: 4px 8px;
747
+ background: rgba(255, 255, 255, 0.04);
748
+ border-radius: 4px;
749
+ font-size: 13px;
750
+ color: var(--text-link);
751
+ cursor: pointer;
752
+ transition: background var(--transition-fast);
753
+ }
754
+
755
+ .thread-indicator:hover {
756
+ background: rgba(255, 255, 255, 0.08);
757
+ }
758
+
759
+ .thread-indicator svg {
760
+ width: 14px;
761
+ height: 14px;
762
+ }
763
+
764
+ /* Typing Indicator */
765
+ .typing-indicator {
766
+ padding: 8px 20px;
767
+ font-size: 13px;
768
+ color: var(--text-muted);
769
+ display: none;
770
+ }
771
+
772
+ .typing-indicator.visible {
773
+ display: flex;
774
+ align-items: center;
775
+ gap: 8px;
776
+ }
777
+
778
+ .typing-dots {
779
+ display: flex;
780
+ gap: 3px;
781
+ }
782
+
783
+ .typing-dots span {
784
+ width: 6px;
785
+ height: 6px;
786
+ background: var(--text-muted);
787
+ border-radius: 50%;
788
+ animation: typingBounce 1.4s infinite;
789
+ }
790
+
791
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
792
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
793
+
794
+ @keyframes typingBounce {
795
+ 0%, 60%, 100% { transform: translateY(0); }
796
+ 30% { transform: translateY(-4px); }
797
+ }
798
+
799
+ /* ========================================
800
+ Composer
801
+ ======================================== */
802
+ .composer {
803
+ padding: 0 20px 20px;
804
+ flex-shrink: 0;
805
+ position: relative;
806
+ z-index: 10;
807
+ }
808
+
809
+ .composer-container {
810
+ background: var(--bg-input);
811
+ border: 1px solid var(--border-subtle);
812
+ border-radius: 8px;
813
+ overflow: visible;
814
+ transition: border-color var(--transition-fast);
815
+ position: relative;
816
+ }
817
+
818
+ .composer-container:focus-within {
819
+ border-color: var(--accent-primary);
820
+ }
821
+
822
+ .composer-input-row {
823
+ display: flex;
824
+ align-items: flex-end;
825
+ }
826
+
827
+ .composer-toolbar {
828
+ padding: 8px 12px;
829
+ display: flex;
830
+ align-items: center;
831
+ gap: 4px;
832
+ border-right: 1px solid var(--border-divider);
833
+ }
834
+
835
+ .toolbar-btn {
836
+ width: 28px;
837
+ height: 28px;
838
+ border: none;
839
+ background: transparent;
840
+ color: var(--text-muted);
841
+ border-radius: 4px;
842
+ cursor: pointer;
843
+ display: flex;
844
+ align-items: center;
845
+ justify-content: center;
846
+ transition: all var(--transition-fast);
847
+ }
848
+
849
+ .toolbar-btn:hover {
850
+ background: rgba(255, 255, 255, 0.1);
851
+ color: var(--text-primary);
852
+ }
853
+
854
+ .composer-input {
855
+ flex: 1;
856
+ padding: 12px;
857
+ background: transparent;
858
+ border: none;
859
+ color: var(--text-primary);
860
+ font-family: var(--font-family);
861
+ font-size: 15px;
862
+ line-height: 1.46668;
863
+ resize: none;
864
+ outline: none;
865
+ min-height: var(--composer-min-height);
866
+ max-height: 200px;
867
+ }
868
+
869
+ .composer-input::placeholder {
870
+ color: var(--text-muted);
871
+ }
872
+
873
+ .composer-send {
874
+ padding: 8px 12px;
875
+ display: flex;
876
+ align-items: center;
877
+ }
878
+
879
+ .send-btn {
880
+ width: 32px;
881
+ height: 32px;
882
+ border: none;
883
+ background: var(--accent-primary);
884
+ color: white;
885
+ border-radius: 4px;
886
+ cursor: pointer;
887
+ display: flex;
888
+ align-items: center;
889
+ justify-content: center;
890
+ transition: all var(--transition-fast);
891
+ }
892
+
893
+ .send-btn:hover {
894
+ background: #0b5d99;
895
+ }
896
+
897
+ .send-btn:disabled {
898
+ background: var(--border-subtle);
899
+ cursor: not-allowed;
900
+ }
901
+
902
+ .composer-hint {
903
+ padding: 8px 12px 0;
904
+ font-size: 12px;
905
+ color: var(--text-muted);
906
+ text-align: right;
907
+ }
908
+
909
+ .composer-hint kbd {
910
+ font-family: var(--font-family);
911
+ background: rgba(255, 255, 255, 0.06);
912
+ padding: 2px 5px;
913
+ border-radius: 3px;
914
+ font-size: 11px;
915
+ }
916
+
917
+ /* ========================================
918
+ @-Mention Autocomplete
919
+ ======================================== */
920
+ .mention-autocomplete {
921
+ position: absolute;
922
+ bottom: 100%;
923
+ left: 0;
924
+ right: 0;
925
+ margin-bottom: 8px;
926
+ background: var(--bg-modal);
927
+ border: 1px solid var(--border-subtle);
928
+ border-radius: 8px;
929
+ box-shadow: var(--shadow-modal);
930
+ max-height: 240px;
931
+ overflow-y: auto;
932
+ display: none;
933
+ z-index: 500;
934
+ }
935
+
936
+ .mention-autocomplete.visible {
937
+ display: block;
938
+ animation: autocompleteSlideIn 0.1s ease-out;
939
+ }
940
+
941
+ @keyframes autocompleteSlideIn {
942
+ from {
943
+ opacity: 0;
944
+ transform: translateY(4px);
945
+ }
946
+ to {
947
+ opacity: 1;
948
+ transform: translateY(0);
949
+ }
950
+ }
951
+
952
+ .mention-autocomplete-header {
953
+ padding: 8px 12px 4px;
954
+ font-size: 11px;
955
+ font-weight: 600;
956
+ color: var(--text-muted);
957
+ text-transform: uppercase;
958
+ letter-spacing: 0.05em;
959
+ border-bottom: 1px solid var(--border-divider);
960
+ }
961
+
962
+ .mention-autocomplete-item {
963
+ padding: 8px 12px;
964
+ display: flex;
965
+ align-items: center;
966
+ gap: 10px;
967
+ cursor: pointer;
968
+ transition: background var(--transition-fast);
969
+ }
970
+
971
+ .mention-autocomplete-item:hover,
972
+ .mention-autocomplete-item.selected {
973
+ background: rgba(255, 255, 255, 0.08);
974
+ }
975
+
976
+ .mention-autocomplete-item .agent-avatar {
977
+ width: 24px;
978
+ height: 24px;
979
+ font-size: 10px;
980
+ }
981
+
982
+ .mention-autocomplete-name {
983
+ font-size: 14px;
984
+ font-weight: 500;
985
+ color: var(--text-primary);
986
+ }
987
+
988
+ .mention-autocomplete-role {
989
+ font-size: 12px;
990
+ color: var(--text-muted);
991
+ margin-left: auto;
992
+ }
993
+
994
+ .mention-autocomplete-hint {
995
+ padding: 6px 12px;
996
+ font-size: 11px;
997
+ color: var(--text-muted);
998
+ border-top: 1px solid var(--border-divider);
553
999
  display: flex;
554
1000
  gap: 12px;
555
- align-items: stretch;
556
1001
  }
557
1002
 
558
- .compose-textarea-wrapper {
559
- flex: 1;
560
- position: relative;
1003
+ .mention-autocomplete-hint kbd {
1004
+ font-family: var(--font-family);
1005
+ background: rgba(255, 255, 255, 0.1);
1006
+ padding: 1px 5px;
1007
+ border-radius: 3px;
1008
+ font-size: 10px;
1009
+ }
1010
+
1011
+ /* ========================================
1012
+ Command Palette
1013
+ ======================================== */
1014
+ .command-palette-overlay {
1015
+ position: fixed;
1016
+ inset: 0;
1017
+ background: rgba(0, 0, 0, 0.6);
1018
+ display: none;
1019
+ align-items: flex-start;
1020
+ justify-content: center;
1021
+ padding-top: 15vh;
1022
+ z-index: 1000;
1023
+ }
1024
+
1025
+ .command-palette-overlay.visible {
1026
+ display: flex;
1027
+ }
1028
+
1029
+ .command-palette {
1030
+ width: 600px;
1031
+ max-width: 90vw;
1032
+ background: var(--bg-modal);
1033
+ border-radius: 8px;
1034
+ box-shadow: var(--shadow-modal);
1035
+ overflow: hidden;
1036
+ animation: paletteSlideIn 0.15s ease-out;
1037
+ }
1038
+
1039
+ @keyframes paletteSlideIn {
1040
+ from {
1041
+ opacity: 0;
1042
+ transform: translateY(-10px) scale(0.98);
1043
+ }
1044
+ to {
1045
+ opacity: 1;
1046
+ transform: translateY(0) scale(1);
1047
+ }
1048
+ }
1049
+
1050
+ .palette-search {
1051
+ padding: 16px;
1052
+ border-bottom: 1px solid var(--border-divider);
1053
+ }
1054
+
1055
+ .palette-search-input {
1056
+ width: 100%;
1057
+ background: transparent;
1058
+ border: none;
1059
+ color: var(--text-primary);
1060
+ font-family: var(--font-family);
1061
+ font-size: 18px;
1062
+ outline: none;
1063
+ }
1064
+
1065
+ .palette-search-input::placeholder {
1066
+ color: var(--text-muted);
1067
+ }
1068
+
1069
+ .palette-results {
1070
+ max-height: 400px;
1071
+ overflow-y: auto;
1072
+ }
1073
+
1074
+ .palette-section {
1075
+ padding: 8px 0;
1076
+ }
1077
+
1078
+ .palette-section-title {
1079
+ padding: 8px 16px 4px;
1080
+ font-size: 12px;
1081
+ font-weight: 600;
1082
+ color: var(--text-muted);
1083
+ text-transform: uppercase;
1084
+ letter-spacing: 0.05em;
1085
+ }
1086
+
1087
+ .palette-item {
1088
+ padding: 10px 16px;
1089
+ display: flex;
1090
+ align-items: center;
1091
+ gap: 12px;
1092
+ cursor: pointer;
1093
+ transition: background var(--transition-fast);
1094
+ }
1095
+
1096
+ .palette-item:hover,
1097
+ .palette-item.selected {
1098
+ background: rgba(255, 255, 255, 0.06);
1099
+ }
1100
+
1101
+ .palette-item-icon {
1102
+ width: 20px;
1103
+ height: 20px;
1104
+ color: var(--text-muted);
1105
+ display: flex;
1106
+ align-items: center;
1107
+ justify-content: center;
1108
+ }
1109
+
1110
+ .palette-item-content {
1111
+ flex: 1;
1112
+ }
1113
+
1114
+ .palette-item-title {
1115
+ font-size: 14px;
1116
+ color: var(--text-primary);
1117
+ }
1118
+
1119
+ .palette-item-subtitle {
1120
+ font-size: 12px;
1121
+ color: var(--text-muted);
1122
+ }
1123
+
1124
+ .palette-item-shortcut {
1125
+ font-size: 12px;
1126
+ color: var(--text-muted);
1127
+ }
1128
+
1129
+ .palette-item-shortcut kbd {
1130
+ font-family: var(--font-family);
1131
+ background: rgba(255, 255, 255, 0.1);
1132
+ padding: 2px 6px;
1133
+ border-radius: 3px;
1134
+ margin-left: 4px;
1135
+ }
1136
+
1137
+ /* ========================================
1138
+ Thread Panel
1139
+ ======================================== */
1140
+ .thread-panel-overlay {
1141
+ position: fixed;
1142
+ top: 0;
1143
+ right: 0;
1144
+ bottom: 0;
1145
+ width: 400px;
1146
+ background: var(--bg-modal);
1147
+ border-left: 1px solid var(--border-divider);
1148
+ display: none;
1149
+ flex-direction: column;
1150
+ z-index: 100;
1151
+ box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
1152
+ animation: threadSlideIn 0.2s ease-out;
1153
+ }
1154
+
1155
+ .thread-panel-overlay.visible {
1156
+ display: flex;
1157
+ }
1158
+
1159
+ @keyframes threadSlideIn {
1160
+ from {
1161
+ transform: translateX(100%);
1162
+ opacity: 0;
1163
+ }
1164
+ to {
1165
+ transform: translateX(0);
1166
+ opacity: 1;
1167
+ }
1168
+ }
1169
+
1170
+ .thread-panel {
1171
+ display: flex;
1172
+ flex-direction: column;
1173
+ height: 100%;
1174
+ }
1175
+
1176
+ .thread-panel-header {
1177
+ padding: 16px;
1178
+ border-bottom: 1px solid var(--border-divider);
1179
+ display: flex;
1180
+ align-items: center;
1181
+ justify-content: space-between;
1182
+ flex-shrink: 0;
1183
+ }
1184
+
1185
+ .thread-panel-title {
1186
+ display: flex;
1187
+ align-items: center;
1188
+ gap: 8px;
1189
+ font-size: 16px;
1190
+ font-weight: 600;
1191
+ color: var(--text-primary);
1192
+ }
1193
+
1194
+ .thread-panel-title .thread-id {
1195
+ font-size: 13px;
1196
+ font-weight: 500;
1197
+ color: var(--text-link);
1198
+ background: rgba(29, 155, 209, 0.1);
1199
+ padding: 2px 8px;
1200
+ border-radius: 4px;
1201
+ }
1202
+
1203
+ .thread-panel-close {
1204
+ width: 32px;
1205
+ height: 32px;
1206
+ border: none;
1207
+ background: transparent;
1208
+ color: var(--text-muted);
1209
+ border-radius: 4px;
1210
+ cursor: pointer;
1211
+ display: flex;
1212
+ align-items: center;
1213
+ justify-content: center;
1214
+ transition: all var(--transition-fast);
1215
+ }
1216
+
1217
+ .thread-panel-close:hover {
1218
+ background: rgba(255, 255, 255, 0.1);
1219
+ color: var(--text-primary);
1220
+ }
1221
+
1222
+ .thread-messages {
1223
+ flex: 1;
1224
+ overflow-y: auto;
1225
+ padding: 16px;
1226
+ }
1227
+
1228
+ .thread-message {
1229
+ padding: 12px;
1230
+ background: rgba(255, 255, 255, 0.02);
1231
+ border-radius: 8px;
1232
+ margin-bottom: 12px;
1233
+ }
1234
+
1235
+ .thread-message:last-child {
1236
+ margin-bottom: 0;
1237
+ }
1238
+
1239
+ .thread-message-header {
1240
+ display: flex;
1241
+ align-items: center;
1242
+ gap: 8px;
1243
+ margin-bottom: 6px;
1244
+ }
1245
+
1246
+ .thread-message-avatar {
1247
+ width: 28px;
1248
+ height: 28px;
1249
+ border-radius: 4px;
1250
+ display: flex;
1251
+ align-items: center;
1252
+ justify-content: center;
1253
+ font-weight: 600;
1254
+ font-size: 11px;
1255
+ color: white;
1256
+ }
1257
+
1258
+ .thread-message-sender {
1259
+ font-weight: 600;
1260
+ color: var(--text-primary);
1261
+ font-size: 14px;
1262
+ }
1263
+
1264
+ .thread-message-time {
1265
+ font-size: 12px;
1266
+ color: var(--text-muted);
1267
+ }
1268
+
1269
+ .thread-message-body {
1270
+ font-size: 14px;
1271
+ color: var(--text-primary);
1272
+ line-height: 1.4;
1273
+ word-wrap: break-word;
1274
+ white-space: pre-wrap;
1275
+ }
1276
+
1277
+ .thread-composer {
1278
+ padding: 16px;
1279
+ border-top: 1px solid var(--border-divider);
1280
+ display: flex;
1281
+ gap: 8px;
1282
+ align-items: flex-end;
1283
+ flex-shrink: 0;
1284
+ }
1285
+
1286
+ .thread-composer-input {
1287
+ flex: 1;
1288
+ padding: 10px 12px;
1289
+ background: var(--bg-input);
1290
+ border: 1px solid var(--border-subtle);
1291
+ border-radius: 6px;
1292
+ color: var(--text-primary);
1293
+ font-family: var(--font-family);
1294
+ font-size: 14px;
1295
+ resize: none;
1296
+ outline: none;
1297
+ min-height: 40px;
1298
+ max-height: 120px;
1299
+ }
1300
+
1301
+ .thread-composer-input:focus {
1302
+ border-color: var(--accent-primary);
1303
+ }
1304
+
1305
+ .thread-composer-input::placeholder {
1306
+ color: var(--text-muted);
1307
+ }
1308
+
1309
+ .thread-send-btn {
1310
+ width: 36px;
1311
+ height: 36px;
1312
+ border: none;
1313
+ background: var(--accent-primary);
1314
+ color: white;
1315
+ border-radius: 6px;
1316
+ cursor: pointer;
1317
+ display: flex;
1318
+ align-items: center;
1319
+ justify-content: center;
1320
+ transition: all var(--transition-fast);
1321
+ flex-shrink: 0;
1322
+ }
1323
+
1324
+ .thread-send-btn:hover {
1325
+ background: #0b5d99;
1326
+ }
1327
+
1328
+ .thread-empty {
1329
+ text-align: center;
1330
+ padding: 32px;
1331
+ color: var(--text-muted);
1332
+ }
1333
+
1334
+ /* Reply count badge on messages */
1335
+ .reply-count-badge {
1336
+ display: inline-flex;
1337
+ align-items: center;
1338
+ gap: 4px;
1339
+ margin-top: 8px;
1340
+ padding: 4px 8px;
1341
+ background: rgba(29, 155, 209, 0.1);
1342
+ border-radius: 4px;
1343
+ font-size: 12px;
1344
+ color: var(--text-link);
1345
+ cursor: pointer;
1346
+ transition: background var(--transition-fast);
1347
+ }
1348
+
1349
+ .reply-count-badge:hover {
1350
+ background: rgba(29, 155, 209, 0.2);
1351
+ }
1352
+
1353
+ .reply-count-badge svg {
1354
+ width: 14px;
1355
+ height: 14px;
1356
+ }
1357
+
1358
+ /* ========================================
1359
+ Empty States
1360
+ ======================================== */
1361
+ .empty-state {
1362
+ flex: 1;
1363
+ display: flex;
1364
+ flex-direction: column;
1365
+ align-items: center;
1366
+ justify-content: center;
1367
+ padding: 48px 24px;
1368
+ text-align: center;
1369
+ }
1370
+
1371
+ .empty-state-icon {
1372
+ width: 64px;
1373
+ height: 64px;
1374
+ margin-bottom: 16px;
1375
+ color: var(--text-muted);
1376
+ }
1377
+
1378
+ .empty-state-title {
1379
+ font-size: 18px;
1380
+ font-weight: 600;
1381
+ color: var(--text-primary);
1382
+ margin-bottom: 8px;
1383
+ }
1384
+
1385
+ .empty-state-text {
1386
+ font-size: 14px;
1387
+ color: var(--text-muted);
1388
+ max-width: 300px;
1389
+ }
1390
+
1391
+ /* ========================================
1392
+ Scrollbars
1393
+ ======================================== */
1394
+ ::-webkit-scrollbar {
1395
+ width: 8px;
1396
+ height: 8px;
1397
+ }
1398
+
1399
+ ::-webkit-scrollbar-track {
1400
+ background: transparent;
1401
+ }
1402
+
1403
+ ::-webkit-scrollbar-thumb {
1404
+ background: rgba(255, 255, 255, 0.2);
1405
+ border-radius: 4px;
1406
+ }
1407
+
1408
+ ::-webkit-scrollbar-thumb:hover {
1409
+ background: rgba(255, 255, 255, 0.3);
561
1410
  }
562
1411
 
563
- .compose-textarea-wrapper::before {
564
- content: '>';
1412
+ /* ========================================
1413
+ Utilities
1414
+ ======================================== */
1415
+ .visually-hidden {
565
1416
  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;
1417
+ width: 1px;
1418
+ height: 1px;
1419
+ padding: 0;
1420
+ margin: -1px;
1421
+ overflow: hidden;
1422
+ clip: rect(0, 0, 0, 0);
1423
+ border: 0;
1424
+ }
1425
+
1426
+ /* ========================================
1427
+ Spawn Modal
1428
+ ======================================== */
1429
+ .spawn-modal-overlay {
1430
+ position: fixed;
1431
+ inset: 0;
1432
+ background: rgba(0, 0, 0, 0.6);
1433
+ display: none;
1434
+ align-items: center;
1435
+ justify-content: center;
1436
+ z-index: 1000;
1437
+ }
1438
+
1439
+ .spawn-modal-overlay.visible {
1440
+ display: flex;
1441
+ }
1442
+
1443
+ .spawn-modal {
1444
+ width: 480px;
1445
+ max-width: 90vw;
1446
+ background: var(--bg-modal);
1447
+ border-radius: 12px;
1448
+ box-shadow: var(--shadow-modal);
1449
+ overflow: hidden;
1450
+ animation: modalSlideIn 0.2s ease-out;
1451
+ }
1452
+
1453
+ @keyframes modalSlideIn {
1454
+ from {
1455
+ opacity: 0;
1456
+ transform: translateY(-20px) scale(0.95);
1457
+ }
1458
+ to {
1459
+ opacity: 1;
1460
+ transform: translateY(0) scale(1);
1461
+ }
1462
+ }
1463
+
1464
+ .spawn-modal-header {
1465
+ padding: 16px 20px;
1466
+ border-bottom: 1px solid var(--border-divider);
1467
+ display: flex;
1468
+ align-items: center;
1469
+ justify-content: space-between;
1470
+ }
1471
+
1472
+ .spawn-modal-title {
1473
+ display: flex;
1474
+ align-items: center;
1475
+ gap: 10px;
1476
+ font-size: 18px;
571
1477
  font-weight: 600;
572
- opacity: 0.6;
573
- transition: opacity 0.2s;
574
- pointer-events: none;
575
- z-index: 1;
1478
+ color: var(--text-primary);
576
1479
  }
577
1480
 
578
- .compose-textarea-wrapper:focus-within::before {
579
- opacity: 1;
580
- animation: blink 1.2s step-end infinite;
1481
+ .spawn-modal-title svg {
1482
+ color: var(--accent-green);
1483
+ }
1484
+
1485
+ .spawn-modal-close {
1486
+ width: 32px;
1487
+ height: 32px;
1488
+ border: none;
1489
+ background: transparent;
1490
+ color: var(--text-muted);
1491
+ border-radius: 6px;
1492
+ cursor: pointer;
1493
+ display: flex;
1494
+ align-items: center;
1495
+ justify-content: center;
1496
+ transition: all var(--transition-fast);
581
1497
  }
582
1498
 
583
- @keyframes blink {
584
- 0%, 100% { opacity: 1; }
585
- 50% { opacity: 0.3; }
1499
+ .spawn-modal-close:hover {
1500
+ background: rgba(255, 255, 255, 0.1);
1501
+ color: var(--text-primary);
586
1502
  }
587
1503
 
588
- .compose-textarea-wrapper textarea {
1504
+ .spawn-modal-body {
1505
+ padding: 20px;
1506
+ }
1507
+
1508
+ .spawn-form-group {
1509
+ margin-bottom: 16px;
1510
+ }
1511
+
1512
+ .spawn-form-group:last-child {
1513
+ margin-bottom: 0;
1514
+ }
1515
+
1516
+ .spawn-form-group label {
1517
+ display: block;
1518
+ font-size: 13px;
1519
+ font-weight: 600;
1520
+ color: var(--text-secondary);
1521
+ margin-bottom: 6px;
1522
+ }
1523
+
1524
+ .spawn-input,
1525
+ .spawn-textarea {
589
1526
  width: 100%;
590
- background: var(--bg-primary);
591
- border: 1px solid var(--border-color);
1527
+ padding: 10px 12px;
1528
+ background: var(--bg-input);
1529
+ border: 1px solid var(--border-subtle);
592
1530
  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;
1531
+ color: var(--text-primary);
1532
+ font-family: var(--font-family);
1533
+ font-size: 14px;
1534
+ transition: border-color var(--transition-fast);
1535
+ }
1536
+
1537
+ .spawn-input:focus,
1538
+ .spawn-textarea:focus {
597
1539
  outline: none;
598
- transition: all 0.2s ease;
599
- resize: none;
1540
+ border-color: var(--accent-primary);
1541
+ }
1542
+
1543
+ .spawn-input::placeholder,
1544
+ .spawn-textarea::placeholder {
1545
+ color: var(--text-muted);
1546
+ }
1547
+
1548
+ .spawn-textarea {
1549
+ resize: vertical;
600
1550
  min-height: 80px;
601
- line-height: 1.6;
1551
+ line-height: 1.4;
1552
+ }
1553
+
1554
+ .spawn-hint {
1555
+ display: block;
1556
+ font-size: 12px;
1557
+ color: var(--text-muted);
1558
+ margin-top: 4px;
1559
+ }
1560
+
1561
+ .spawn-status {
1562
+ min-height: 20px;
1563
+ font-size: 13px;
1564
+ margin-top: 12px;
602
1565
  }
603
1566
 
604
- .compose-textarea-wrapper textarea:hover {
605
- border-color: var(--border-hover);
1567
+ .spawn-status.success {
1568
+ color: var(--accent-green);
606
1569
  }
607
1570
 
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%);
1571
+ .spawn-status.error {
1572
+ color: var(--accent-red);
612
1573
  }
613
1574
 
614
- .compose-textarea-wrapper textarea::placeholder {
1575
+ .spawn-status.loading {
615
1576
  color: var(--text-muted);
616
- opacity: 0.5;
617
1577
  }
618
1578
 
619
- .send-btn {
620
- background: linear-gradient(180deg, var(--primary) 0%, #2563eb 100%);
621
- color: white;
1579
+ .spawn-modal-footer {
1580
+ padding: 16px 20px;
1581
+ border-top: 1px solid var(--border-divider);
1582
+ display: flex;
1583
+ justify-content: flex-end;
1584
+ gap: 10px;
1585
+ }
1586
+
1587
+ .spawn-cancel-btn {
1588
+ padding: 8px 16px;
1589
+ background: transparent;
1590
+ border: 1px solid var(--border-subtle);
1591
+ border-radius: 6px;
1592
+ color: var(--text-secondary);
1593
+ font-size: 14px;
1594
+ font-weight: 500;
1595
+ cursor: pointer;
1596
+ transition: all var(--transition-fast);
1597
+ }
1598
+
1599
+ .spawn-cancel-btn:hover {
1600
+ background: rgba(255, 255, 255, 0.05);
1601
+ color: var(--text-primary);
1602
+ }
1603
+
1604
+ .spawn-submit-btn {
1605
+ display: flex;
1606
+ align-items: center;
1607
+ gap: 6px;
1608
+ padding: 8px 16px;
1609
+ background: var(--accent-green);
622
1610
  border: none;
623
1611
  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;
1612
+ color: white;
1613
+ font-size: 14px;
628
1614
  font-weight: 600;
629
- letter-spacing: 0.05em;
630
- text-transform: uppercase;
631
1615
  cursor: pointer;
632
- transition: all 0.2s ease;
633
- white-space: nowrap;
1616
+ transition: all var(--transition-fast);
1617
+ }
1618
+
1619
+ .spawn-submit-btn:hover:not(:disabled) {
1620
+ background: #249966;
1621
+ }
1622
+
1623
+ .spawn-submit-btn:disabled {
1624
+ opacity: 0.5;
1625
+ cursor: not-allowed;
1626
+ }
1627
+
1628
+ /* Spawned Agent Indicator */
1629
+ .channel-item .spawned-icon {
1630
+ width: 14px;
1631
+ height: 14px;
1632
+ color: var(--accent-green);
1633
+ flex-shrink: 0;
1634
+ }
1635
+
1636
+ .channel-item .release-btn {
1637
+ opacity: 0;
1638
+ width: 18px;
1639
+ height: 18px;
1640
+ border: none;
1641
+ background: var(--accent-red);
1642
+ color: white;
1643
+ border-radius: 4px;
1644
+ cursor: pointer;
634
1645
  display: flex;
635
1646
  align-items: center;
636
1647
  justify-content: center;
637
- gap: 8px;
638
- position: relative;
639
- overflow: hidden;
1648
+ transition: opacity var(--transition-fast);
1649
+ flex-shrink: 0;
640
1650
  }
641
1651
 
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;
1652
+ .channel-item:hover .release-btn {
1653
+ opacity: 1;
648
1654
  }
649
1655
 
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);
1656
+ .channel-item .release-btn:hover {
1657
+ background: #c41a4f;
654
1658
  }
655
1659
 
656
- .send-btn:active {
657
- transform: translateY(0);
658
- box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
1660
+ /* ========================================
1661
+ Fleet View Toggle
1662
+ ======================================== */
1663
+ .view-toggle {
1664
+ display: flex;
1665
+ background: rgba(255, 255, 255, 0.06);
1666
+ border-radius: 6px;
1667
+ padding: 2px;
1668
+ margin: 0 12px 8px;
659
1669
  }
660
1670
 
661
- .send-btn:disabled {
662
- opacity: 0.4;
663
- cursor: not-allowed;
664
- transform: none;
665
- box-shadow: none;
1671
+ .view-toggle-btn {
1672
+ flex: 1;
1673
+ padding: 6px 12px;
1674
+ border: none;
1675
+ background: transparent;
1676
+ color: var(--text-muted);
1677
+ font-size: 12px;
1678
+ font-weight: 500;
1679
+ border-radius: 4px;
1680
+ cursor: pointer;
1681
+ transition: all var(--transition-fast);
1682
+ display: flex;
1683
+ align-items: center;
1684
+ justify-content: center;
1685
+ gap: 6px;
1686
+ }
1687
+
1688
+ .view-toggle-btn:hover {
1689
+ color: var(--text-secondary);
1690
+ }
1691
+
1692
+ .view-toggle-btn.active {
1693
+ background: var(--accent-primary);
1694
+ color: white;
666
1695
  }
667
1696
 
668
- .send-btn svg {
1697
+ .view-toggle-btn svg {
669
1698
  width: 14px;
670
1699
  height: 14px;
671
1700
  }
672
1701
 
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;
1702
+ .view-toggle-btn .peer-count {
1703
+ font-size: 10px;
1704
+ background: rgba(255, 255, 255, 0.2);
1705
+ padding: 1px 5px;
1706
+ border-radius: 8px;
1707
+ margin-left: 4px;
679
1708
  }
680
1709
 
681
- .send-status.success {
682
- color: var(--success);
1710
+ /* Server badge for fleet agents */
1711
+ .server-badge {
1712
+ display: inline-flex;
1713
+ align-items: center;
1714
+ gap: 3px;
1715
+ padding: 1px 6px;
1716
+ background: var(--accent-purple);
1717
+ color: white;
1718
+ font-size: 9px;
1719
+ font-weight: 600;
1720
+ border-radius: 3px;
1721
+ margin-right: 6px;
1722
+ text-transform: uppercase;
1723
+ letter-spacing: 0.3px;
683
1724
  }
684
1725
 
685
- .send-status.error {
686
- color: var(--error);
1726
+ .server-badge.local {
1727
+ background: var(--accent-primary);
687
1728
  }
688
1729
 
689
- /* Scrollbar */
690
- ::-webkit-scrollbar { width: 6px; }
691
- ::-webkit-scrollbar-track { background: transparent; }
692
- ::-webkit-scrollbar-thumb {
693
- background: var(--border-hover);
694
- border-radius: 3px;
1730
+ .server-badge .server-dot {
1731
+ width: 5px;
1732
+ height: 5px;
1733
+ border-radius: 50%;
1734
+ background: var(--status-online);
695
1735
  }
696
- ::-webkit-scrollbar-thumb:hover {
697
- background: var(--text-muted);
1736
+
1737
+ .server-badge .server-dot.offline {
1738
+ background: var(--status-offline);
698
1739
  }
699
1740
 
700
- .last-active {
701
- margin-top: 8px;
702
- font-size: 0.7rem;
1741
+ /* Peer status in header */
1742
+ .peer-status {
1743
+ display: flex;
1744
+ align-items: center;
1745
+ gap: 8px;
1746
+ padding: 4px 10px;
1747
+ background: rgba(124, 58, 237, 0.15);
1748
+ border-radius: 12px;
1749
+ font-size: 12px;
1750
+ color: var(--accent-purple);
1751
+ margin-left: 8px;
1752
+ }
1753
+
1754
+ .peer-status .peer-dot {
1755
+ width: 6px;
1756
+ height: 6px;
1757
+ border-radius: 50%;
1758
+ background: var(--accent-purple);
1759
+ }
1760
+
1761
+ .peer-status.connected .peer-dot {
1762
+ background: var(--status-online);
1763
+ }
1764
+
1765
+ .peer-status.disconnected {
1766
+ background: rgba(97, 96, 97, 0.15);
1767
+ color: var(--status-offline);
1768
+ }
1769
+
1770
+ .peer-status.disconnected .peer-dot {
1771
+ background: var(--status-offline);
1772
+ }
1773
+
1774
+ /* Fleet section in sidebar */
1775
+ .fleet-servers-list {
1776
+ list-style: none;
1777
+ padding: 0 8px;
1778
+ margin-bottom: 8px;
1779
+ }
1780
+
1781
+ .server-item {
1782
+ display: flex;
1783
+ align-items: center;
1784
+ gap: 8px;
1785
+ padding: 6px 8px;
1786
+ border-radius: 6px;
1787
+ font-size: 13px;
1788
+ color: var(--text-channel);
1789
+ cursor: pointer;
1790
+ transition: background var(--transition-fast);
1791
+ }
1792
+
1793
+ .server-item:hover {
1794
+ background: var(--bg-channel-hover);
1795
+ }
1796
+
1797
+ .server-item .server-icon {
1798
+ width: 20px;
1799
+ height: 20px;
1800
+ border-radius: 4px;
1801
+ background: var(--accent-purple);
1802
+ display: flex;
1803
+ align-items: center;
1804
+ justify-content: center;
1805
+ position: relative;
1806
+ }
1807
+
1808
+ .server-item .server-icon svg {
1809
+ width: 12px;
1810
+ height: 12px;
1811
+ color: white;
1812
+ }
1813
+
1814
+ .server-item .server-icon .status-dot {
1815
+ position: absolute;
1816
+ bottom: -2px;
1817
+ right: -2px;
1818
+ width: 8px;
1819
+ height: 8px;
1820
+ border-radius: 50%;
1821
+ background: var(--status-online);
1822
+ border: 2px solid var(--bg-sidebar);
1823
+ }
1824
+
1825
+ .server-item .server-icon .status-dot.offline {
1826
+ background: var(--status-offline);
1827
+ }
1828
+
1829
+ .server-item .server-name {
1830
+ flex: 1;
1831
+ }
1832
+
1833
+ .server-item .agent-count {
1834
+ font-size: 11px;
703
1835
  color: var(--text-muted);
1836
+ font-family: var(--font-mono);
1837
+ }
1838
+
1839
+ /* Fleet agent avatar with server indicator */
1840
+ .channel-item .agent-avatar .server-indicator {
1841
+ position: absolute;
1842
+ top: -3px;
1843
+ right: -3px;
1844
+ width: 10px;
1845
+ height: 10px;
1846
+ border-radius: 2px;
1847
+ background: var(--accent-purple);
1848
+ border: 2px solid var(--bg-sidebar);
1849
+ display: flex;
1850
+ align-items: center;
1851
+ justify-content: center;
1852
+ }
1853
+
1854
+ .channel-item .agent-avatar .server-indicator.local {
1855
+ background: var(--accent-primary);
704
1856
  }
705
1857
  </style>
706
1858
  </head>
707
1859
  <body>
708
- <div class="container">
709
- <div class="sidebar">
710
- <div class="header">
711
- <div class="logo">
712
- <div class="logo-icon">AR</div>
713
- <h1>Agent Relay</h1>
1860
+ <div class="app-container">
1861
+ <!-- Sidebar -->
1862
+ <aside class="sidebar">
1863
+ <div class="workspace-header">
1864
+ <div class="workspace-name">
1865
+ <span class="status-dot" id="connection-dot"></span>
1866
+ Agent Relay
1867
+ </div>
1868
+ <button class="compose-new-btn" title="New message">
1869
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1870
+ <path d="M12 20h9"/>
1871
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
1872
+ </svg>
1873
+ </button>
1874
+ </div>
1875
+
1876
+ <div class="search-bar" id="search-trigger">
1877
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1878
+ <circle cx="11" cy="11" r="8"/>
1879
+ <path d="M21 21l-4.35-4.35"/>
1880
+ </svg>
1881
+ <span>Search</span>
1882
+ <kbd>Ctrl K</kbd>
1883
+ </div>
1884
+
1885
+ <div class="sidebar-content">
1886
+ <!-- View Toggle (Local/Fleet) -->
1887
+ <div class="view-toggle" id="view-toggle" style="display: none;">
1888
+ <button class="view-toggle-btn active" data-view="local">
1889
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1890
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
1891
+ <line x1="9" y1="9" x2="15" y2="15"/>
1892
+ <line x1="15" y1="9" x2="9" y2="15"/>
1893
+ </svg>
1894
+ Local
1895
+ </button>
1896
+ <button class="view-toggle-btn" data-view="fleet">
1897
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1898
+ <circle cx="12" cy="12" r="10"/>
1899
+ <circle cx="12" cy="12" r="4"/>
1900
+ <line x1="12" y1="2" x2="12" y2="6"/>
1901
+ <line x1="12" y1="18" x2="12" y2="22"/>
1902
+ <line x1="2" y1="12" x2="6" y2="12"/>
1903
+ <line x1="18" y1="12" x2="22" y2="12"/>
1904
+ </svg>
1905
+ Fleet
1906
+ <span class="peer-count" id="peer-count">0</span>
1907
+ </button>
1908
+ </div>
1909
+
1910
+ <!-- Peer Servers Section (shown in fleet mode) -->
1911
+ <div class="section" id="servers-section" style="display: none;">
1912
+ <div class="section-header">
1913
+ <div class="section-title">
1914
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1915
+ <path d="M6 9l6 6 6-6"/>
1916
+ </svg>
1917
+ Servers
714
1918
  </div>
715
- <div id="connection-status">
716
- <span class="dot"></span>
717
- <span>Connected</span>
1919
+ </div>
1920
+ <ul class="fleet-servers-list" id="servers-list">
1921
+ <!-- Servers injected here -->
1922
+ </ul>
1923
+ </div>
1924
+
1925
+ <!-- Channels Section -->
1926
+ <div class="section">
1927
+ <div class="section-header" id="channels-header">
1928
+ <div class="section-title">
1929
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1930
+ <path d="M6 9l6 6 6-6"/>
1931
+ </svg>
1932
+ Channels
718
1933
  </div>
1934
+ </div>
1935
+ <ul class="channel-list" id="channels-list">
1936
+ <li class="channel-item active" data-channel="general">
1937
+ <span class="channel-prefix">#</span>
1938
+ <span class="channel-name">general</span>
1939
+ </li>
1940
+ </ul>
719
1941
  </div>
720
- <div class="agents-section">
721
- <div class="agents-header">Agents</div>
722
- <div id="agents">
723
- <!-- Agents injected here -->
1942
+
1943
+ <!-- Agents Section -->
1944
+ <div class="section">
1945
+ <div class="section-header" id="agents-header">
1946
+ <div class="section-title">
1947
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1948
+ <path d="M6 9l6 6 6-6"/>
1949
+ </svg>
1950
+ Agents
724
1951
  </div>
1952
+ <button class="section-add-btn" id="spawn-btn" title="Spawn new agent" style="opacity: 1;">
1953
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1954
+ <line x1="12" y1="5" x2="12" y2="19"/>
1955
+ <line x1="5" y1="12" x2="19" y2="12"/>
1956
+ </svg>
1957
+ </button>
1958
+ </div>
1959
+ <ul class="channel-list" id="agents-list">
1960
+ <!-- Agents injected here -->
1961
+ </ul>
725
1962
  </div>
726
- </div>
727
1963
 
728
- <div class="main-content">
729
- <div class="activity-log">
730
- <div class="log-header">
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>
737
- <div class="live-indicator">
738
- <span class="live-dot"></span>
739
- <span>Live</span>
740
- </div>
1964
+ </div>
1965
+
1966
+ <div class="sidebar-footer">
1967
+ <a href="/metrics" class="nav-link">
1968
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1969
+ <path d="M3 3v18h18"/>
1970
+ <path d="M18 17V9"/>
1971
+ <path d="M13 17V5"/>
1972
+ <path d="M8 17v-3"/>
1973
+ </svg>
1974
+ Metrics
1975
+ </a>
1976
+ <a href="/bridge" class="nav-link">
1977
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1978
+ <rect x="3" y="3" width="7" height="7"/>
1979
+ <rect x="14" y="3" width="7" height="7"/>
1980
+ <rect x="14" y="14" width="7" height="7"/>
1981
+ <rect x="3" y="14" width="7" height="7"/>
1982
+ </svg>
1983
+ Bridge View
1984
+ </a>
1985
+ </div>
1986
+ </aside>
1987
+
1988
+ <!-- Main Panel -->
1989
+ <main class="main-panel">
1990
+ <header class="channel-header">
1991
+ <div class="channel-header-name">
1992
+ <span class="prefix">#</span>
1993
+ <span id="current-channel-name">general</span>
1994
+ </div>
1995
+ <div class="header-divider"></div>
1996
+ <div class="channel-topic" id="channel-topic">All agent communications</div>
1997
+ <div class="header-actions">
1998
+ <div class="online-count">
1999
+ <span class="dot"></span>
2000
+ <span id="online-count">0 online</span>
2001
+ </div>
2002
+ </div>
2003
+ </header>
2004
+
2005
+ <div class="messages-container">
2006
+ <div class="messages-list" id="messages-list">
2007
+ <!-- Messages injected here -->
2008
+ </div>
2009
+ <div class="typing-indicator" id="typing-indicator">
2010
+ <div class="typing-dots">
2011
+ <span></span>
2012
+ <span></span>
2013
+ <span></span>
2014
+ </div>
2015
+ <span id="typing-text">Someone is typing...</span>
2016
+ </div>
2017
+ </div>
2018
+
2019
+ <div class="composer">
2020
+ <div class="composer-container" style="position: relative;">
2021
+ <!-- @-Mention Autocomplete Dropdown -->
2022
+ <div class="mention-autocomplete" id="mention-autocomplete">
2023
+ <div class="mention-autocomplete-header">Agents</div>
2024
+ <div id="mention-autocomplete-list">
2025
+ <!-- Agent items will be injected here -->
2026
+ </div>
2027
+ <div class="mention-autocomplete-hint">
2028
+ <span><kbd>Tab</kbd> or <kbd>Enter</kbd> to select</span>
2029
+ <span><kbd>↑↓</kbd> to navigate</span>
2030
+ <span><kbd>Esc</kbd> to close</span>
2031
+ </div>
2032
+ </div>
2033
+ <div class="composer-input-row">
2034
+ <div class="composer-toolbar">
2035
+ <button class="toolbar-btn" id="bold-btn" title="Bold (Ctrl+B)">
2036
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2037
+ <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
2038
+ <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
2039
+ </svg>
2040
+ </button>
2041
+ <button class="toolbar-btn" id="emoji-btn" title="Add emoji">
2042
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2043
+ <circle cx="12" cy="12" r="10"/>
2044
+ <path d="M8 14s1.5 2 4 2 4-2 4-2"/>
2045
+ <line x1="9" y1="9" x2="9.01" y2="9"/>
2046
+ <line x1="15" y1="9" x2="15.01" y2="9"/>
2047
+ </svg>
2048
+ </button>
2049
+ </div>
2050
+ <textarea
2051
+ id="message-input"
2052
+ class="composer-input"
2053
+ placeholder="@AgentName message... (or @* to broadcast)"
2054
+ rows="1"
2055
+ ></textarea>
2056
+ <div class="composer-send">
2057
+ <button class="send-btn" id="send-btn" title="Send message">
2058
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2059
+ <line x1="22" y1="2" x2="11" y2="13"/>
2060
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
2061
+ </svg>
2062
+ </button>
2063
+ </div>
2064
+ </div>
2065
+ </div>
2066
+ <div class="composer-hint">
2067
+ <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to send
2068
+ </div>
2069
+ </div>
2070
+ </main>
2071
+ </div>
2072
+
2073
+ <!-- Command Palette -->
2074
+ <div class="command-palette-overlay" id="command-palette-overlay">
2075
+ <div class="command-palette">
2076
+ <div class="palette-search">
2077
+ <input
2078
+ type="text"
2079
+ class="palette-search-input"
2080
+ id="palette-search"
2081
+ placeholder="Search messages, agents, or type a command..."
2082
+ autocomplete="off"
2083
+ >
2084
+ </div>
2085
+ <div class="palette-results" id="palette-results">
2086
+ <div class="palette-section">
2087
+ <div class="palette-section-title">Commands</div>
2088
+ <div class="palette-item" data-command="broadcast">
2089
+ <div class="palette-item-icon">
2090
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2091
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
2092
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
2093
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
2094
+ </svg>
2095
+ </div>
2096
+ <div class="palette-item-content">
2097
+ <div class="palette-item-title">/broadcast</div>
2098
+ <div class="palette-item-subtitle">Send message to all agents</div>
2099
+ </div>
2100
+ </div>
2101
+ <div class="palette-item" data-command="status">
2102
+ <div class="palette-item-icon">
2103
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2104
+ <circle cx="12" cy="12" r="10"/>
2105
+ <path d="M12 6v6l4 2"/>
2106
+ </svg>
2107
+ </div>
2108
+ <div class="palette-item-content">
2109
+ <div class="palette-item-title">/status</div>
2110
+ <div class="palette-item-subtitle">Set your status message</div>
741
2111
  </div>
742
- <div class="log-content" id="log">
743
- <!-- Messages injected here -->
2112
+ </div>
2113
+ <div class="palette-item" data-command="clear">
2114
+ <div class="palette-item-icon">
2115
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2116
+ <path d="M3 6h18"/>
2117
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
2118
+ </svg>
744
2119
  </div>
2120
+ <div class="palette-item-content">
2121
+ <div class="palette-item-title">/clear</div>
2122
+ <div class="palette-item-subtitle">Clear message view</div>
2123
+ </div>
2124
+ </div>
745
2125
  </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>
2126
+ <div class="palette-section" id="palette-channels-section">
2127
+ <div class="palette-section-title">Channels</div>
2128
+ <div class="palette-item" data-jump-channel="general">
2129
+ <div class="palette-item-icon">
2130
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2131
+ <path d="M4 9l4-4 4 4"/>
2132
+ <path d="M12 15l4 4 4-4"/>
2133
+ <line x1="8" y1="5" x2="8" y2="19"/>
2134
+ <line x1="16" y1="5" x2="16" y2="19"/>
2135
+ </svg>
2136
+ </div>
2137
+ <div class="palette-item-content">
2138
+ <div class="palette-item-title">#general</div>
2139
+ <div class="palette-item-subtitle">All agent communications</div>
775
2140
  </div>
2141
+ </div>
2142
+ </div>
2143
+ <div class="palette-section" id="palette-agents-section">
2144
+ <div class="palette-section-title">Jump to Agent</div>
2145
+ <!-- Agents will be injected here -->
2146
+ </div>
2147
+ <div class="palette-section" id="palette-messages-section" style="display: none;">
2148
+ <div class="palette-section-title">Recent Messages</div>
2149
+ <!-- Filtered messages will be injected here -->
776
2150
  </div>
2151
+ </div>
777
2152
  </div>
778
2153
  </div>
779
2154
 
780
- <script>
781
- const agentsContainer = document.getElementById('agents');
782
- const logContainer = document.getElementById('log');
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');
789
-
790
- // Track last data hash to prevent unnecessary re-renders
791
- const STALE_THRESHOLD_MS = 30_000;
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
- }
829
-
830
- function connect() {
831
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
832
-
833
- updateConnectionStatus(isReconnect ? 'reconnecting' : 'connecting');
834
-
835
- const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
836
-
837
- ws.onopen = () => {
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);
848
- };
849
-
850
- ws.onclose = () => {
851
- if (uptimeInterval) {
852
- clearInterval(uptimeInterval);
853
- uptimeInterval = null;
854
- }
855
- connectionStart = null;
856
- updateConnectionStatus('disconnected');
857
- isReconnect = true;
858
- setTimeout(connect, 3000);
859
- };
860
-
861
- ws.onmessage = (e) => {
862
- // Skip re-render if data hasn't changed
863
- const dataHash = e.data;
864
- if (dataHash === lastDataHash) return;
865
- lastDataHash = dataHash;
866
-
867
- const data = JSON.parse(e.data);
868
- render(data);
869
- };
870
- }
871
-
872
- function render(data) {
873
- // Render Agents
874
- if (data.agents && data.agents.length > 0) {
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}">
881
- <div class="agent-header">
882
- <div class="agent-name">${a.name}</div>
883
- ${a.messageCount > 0 ? `<span class="badge">${a.messageCount}</span>` : ''}
884
- </div>
885
- <div class="agent-meta">
886
- <span class="cli-badge">${a.cli}</span>
887
- </div>
888
- <div class="agent-status">
889
- <span class="status-dot"></span>
890
- ${a.status && a.status !== 'Idle' ? a.status : 'Ready'}
891
- </div>
892
- <div class="last-active">
893
- ${a.lastActive ? timeAgo(new Date(a.lastActive)) : 'No activity'}
894
- </div>
895
- </div>
896
- `;
897
- }).join('');
898
-
899
- if (agentsContainer.innerHTML !== newAgentsHTML) {
900
- agentsContainer.innerHTML = newAgentsHTML;
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
- }
909
- } else if (!agentsContainer.querySelector('.empty-state')) {
910
- agentsContainer.innerHTML = `
911
- <div class="empty-state">
912
- <div class="empty-state-text">Waiting for agents...</div>
913
- </div>
914
- `;
915
- // Clear agent dropdown
916
- if (currentAgents.length > 0) {
917
- currentAgents = [];
918
- updateAgentDropdown([]);
919
- }
920
- }
921
-
922
- // Render Messages (Activity Log)
923
- if (data.messages && data.messages.length > 0) {
924
- allMessages = data.messages;
925
-
926
- // Update conversation filter dropdown
927
- updateConversationDropdown();
928
-
929
- renderMessages();
930
- } else if (!logContainer.querySelector('.empty-state')) {
931
- allMessages = [];
932
- logContainer.innerHTML = `
933
- <div class="empty-state">
934
- <div class="empty-state-text">No messages yet</div>
935
- </div>
936
- `;
937
- }
938
- }
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
-
1003
- function createMessageHTML(m) {
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;
1009
- return `
1010
- <div class="message ${msgClass}" ${m.thread ? `data-thread="${escapeHtml(m.thread)}"` : ''} data-from="${escapeHtml(m.from)}" data-to="${escapeHtml(m.to)}">
1011
- <div class="msg-avatar">${initials}</div>
1012
- <div class="msg-body">
1013
- <div class="msg-meta">
1014
- <span class="msg-sender">${m.from}</span>
1015
- <span class="msg-arrow">→</span>
1016
- <span class="msg-target">${targetDisplay}</span>
1017
- ${threadBadge}
1018
- <span class="msg-time">${new Date(m.timestamp).toLocaleTimeString()}</span>
1019
- </div>
1020
- <div class="msg-text">${escapeHtml(m.content)}</div>
1021
- </div>
1022
- </div>
1023
- `;
1024
- }
1025
-
1026
- function escapeHtml(text) {
1027
- if (!text) return '';
1028
- const div = document.createElement('div');
1029
- div.textContent = text;
1030
- return div.innerHTML;
1031
- }
1032
-
1033
- function timeAgo(date) {
1034
- const seconds = Math.floor((new Date() - date) / 1000);
1035
- if (seconds < 60) return 'just now';
1036
- const minutes = Math.floor(seconds / 60);
1037
- if (minutes < 60) return `${minutes}m ago`;
1038
- const hours = Math.floor(minutes / 60);
1039
- if (hours < 24) return `${hours}h ago`;
1040
- return `${Math.floor(hours / 24)}d ago`;
1041
- }
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
-
1121
- connect();
1122
- </script>
2155
+ <!-- Thread Panel -->
2156
+ <div class="thread-panel-overlay" id="thread-panel-overlay">
2157
+ <div class="thread-panel">
2158
+ <div class="thread-panel-header">
2159
+ <div class="thread-panel-title">
2160
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2161
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
2162
+ </svg>
2163
+ <span>Thread</span>
2164
+ <span class="thread-id" id="thread-panel-id"></span>
2165
+ </div>
2166
+ <button class="thread-panel-close" id="thread-panel-close" title="Close thread">
2167
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2168
+ <line x1="18" y1="6" x2="6" y2="18"/>
2169
+ <line x1="6" y1="6" x2="18" y2="18"/>
2170
+ </svg>
2171
+ </button>
2172
+ </div>
2173
+ <div class="thread-messages" id="thread-messages">
2174
+ <!-- Thread messages will be injected here -->
2175
+ </div>
2176
+ <div class="thread-composer">
2177
+ <textarea
2178
+ id="thread-message-input"
2179
+ class="thread-composer-input"
2180
+ placeholder="Reply in thread..."
2181
+ rows="1"
2182
+ ></textarea>
2183
+ <button class="thread-send-btn" id="thread-send-btn" title="Send reply">
2184
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2185
+ <line x1="22" y1="2" x2="11" y2="13"/>
2186
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
2187
+ </svg>
2188
+ </button>
2189
+ </div>
2190
+ </div>
2191
+ </div>
2192
+
2193
+ <!-- Spawn Agent Modal -->
2194
+ <div class="spawn-modal-overlay" id="spawn-modal-overlay">
2195
+ <div class="spawn-modal">
2196
+ <div class="spawn-modal-header">
2197
+ <div class="spawn-modal-title">
2198
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2199
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
2200
+ <circle cx="12" cy="7" r="4"/>
2201
+ <line x1="12" y1="11" x2="12" y2="17"/>
2202
+ <line x1="9" y1="14" x2="15" y2="14"/>
2203
+ </svg>
2204
+ Spawn New Agent
2205
+ </div>
2206
+ <button class="spawn-modal-close" id="spawn-modal-close" title="Close">
2207
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2208
+ <line x1="18" y1="6" x2="6" y2="18"/>
2209
+ <line x1="6" y1="6" x2="18" y2="18"/>
2210
+ </svg>
2211
+ </button>
2212
+ </div>
2213
+ <div class="spawn-modal-body">
2214
+ <div class="spawn-form-group">
2215
+ <label for="spawn-name-input">Agent Name</label>
2216
+ <input
2217
+ type="text"
2218
+ id="spawn-name-input"
2219
+ class="spawn-input"
2220
+ placeholder="e.g., Developer, Reviewer, Tester"
2221
+ autocomplete="off"
2222
+ />
2223
+ </div>
2224
+ <div class="spawn-form-group">
2225
+ <label for="spawn-cli-input">CLI Command</label>
2226
+ <input
2227
+ type="text"
2228
+ id="spawn-cli-input"
2229
+ class="spawn-input"
2230
+ value="claude"
2231
+ placeholder="e.g., claude, aider, cursor"
2232
+ />
2233
+ <span class="spawn-hint">The AI CLI tool to wrap</span>
2234
+ </div>
2235
+ <div class="spawn-form-group">
2236
+ <label for="spawn-task-input">Initial Task (optional)</label>
2237
+ <textarea
2238
+ id="spawn-task-input"
2239
+ class="spawn-textarea"
2240
+ placeholder="Enter an initial task to send to the agent..."
2241
+ rows="4"
2242
+ ></textarea>
2243
+ <span class="spawn-hint">This will be injected into the agent's terminal</span>
2244
+ </div>
2245
+ <div class="spawn-status" id="spawn-status"></div>
2246
+ </div>
2247
+ <div class="spawn-modal-footer">
2248
+ <button class="spawn-cancel-btn" id="spawn-cancel-btn">Cancel</button>
2249
+ <button class="spawn-submit-btn" id="spawn-submit-btn">
2250
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2251
+ <polygon points="5 3 19 12 5 21 5 3"/>
2252
+ </svg>
2253
+ Spawn Agent
2254
+ </button>
2255
+ </div>
2256
+ </div>
2257
+ </div>
2258
+
2259
+ <!-- Dashboard Application (TypeScript bundled) -->
2260
+ <script type="module" src="/js/app.js"></script>
1123
2261
  </body>
1124
2262
  </html>