forge-cw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/console/assets/addon-fit-BlxrFPDK.js +1 -0
  3. package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
  4. package/console/assets/index-Digctq9W.css +32 -0
  5. package/console/assets/index-ItVUTuQf.js +31 -0
  6. package/console/assets/xterm-DlVFs1Kw.js +9 -0
  7. package/console/index.html +13 -0
  8. package/cw/.bundle.json +4 -0
  9. package/cw/agents/presentation-curator.md +119 -0
  10. package/cw/agents/weather-agent.md +67 -0
  11. package/cw/cw +6188 -0
  12. package/cw/cw-shell-integration.sh +132 -0
  13. package/cw/hooks/HOOKS-README.md +471 -0
  14. package/cw/hooks/config/hooks-config.json +24 -0
  15. package/cw/hooks/scripts/auto-compact.py +133 -0
  16. package/cw/hooks/scripts/hooks.py +472 -0
  17. package/cw/hooks/scripts/review-autoclose.py +171 -0
  18. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
  19. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
  20. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
  21. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
  22. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
  23. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
  24. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
  25. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
  26. package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
  27. package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
  28. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
  29. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
  30. package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
  31. package/cw/hooks/sounds/configchange/configchange.wav +0 -0
  32. package/cw/hooks/sounds/notification/notification.mp3 +0 -0
  33. package/cw/hooks/sounds/notification/notification.wav +0 -0
  34. package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
  35. package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
  36. package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
  37. package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
  38. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
  39. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
  40. package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
  41. package/cw/hooks/sounds/precompact/precompact.wav +0 -0
  42. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
  43. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
  44. package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
  45. package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
  46. package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
  47. package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
  48. package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
  49. package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
  50. package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
  51. package/cw/hooks/sounds/setup/Setup.wav +0 -0
  52. package/cw/hooks/sounds/stop/stop.mp3 +0 -0
  53. package/cw/hooks/sounds/stop/stop.wav +0 -0
  54. package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
  55. package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
  56. package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
  57. package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
  58. package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
  59. package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
  60. package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
  61. package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
  62. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
  63. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
  64. package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
  65. package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
  66. package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
  67. package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
  68. package/cw/install.sh +243 -0
  69. package/cw/lib/context/github.sh +51 -0
  70. package/cw/lib/context/linear.sh +49 -0
  71. package/cw/lib/context/notion.sh +38 -0
  72. package/cw/lib/dashboard/activity-hook.py +134 -0
  73. package/cw/lib/dashboard/index.html +1515 -0
  74. package/cw/lib/dashboard/server.py +379 -0
  75. package/cw/lib/dashboard/start.sh +15 -0
  76. package/cw/lib/harnesses/claude.sh +92 -0
  77. package/cw/lib/harnesses/codex.sh +245 -0
  78. package/cw/lib/harnesses/opencode.sh +120 -0
  79. package/cw/lib/harnesses/pi.sh +66 -0
  80. package/cw/lib/iterm2.sh +140 -0
  81. package/cw/templates/CLAUDE.template.md +48 -0
  82. package/dist/index.js +4376 -0
  83. package/package.json +51 -0
@@ -0,0 +1,1515 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CW STATION</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --bg: #07090e;
12
+ --surface: #0c1018;
13
+ --surface-2: #111825;
14
+ --surface-3: #162030;
15
+ --border: #1c2a40;
16
+ --border-hi: #2a4060;
17
+ --border-active: #3a5a80;
18
+
19
+ --text: #c8d6e5;
20
+ --text-dim: #5a7090;
21
+ --text-muted: #3a5070;
22
+
23
+ --cyan: #00d4ff;
24
+ --cyan-soft: #0090b0;
25
+ --cyan-bg: rgba(0,212,255,0.06);
26
+ --green: #00e676;
27
+ --green-soft: #009050;
28
+ --green-bg: rgba(0,230,118,0.06);
29
+ --amber: #ffb300;
30
+ --amber-soft: #b07800;
31
+ --amber-bg: rgba(255,179,0,0.06);
32
+ --magenta: #e040fb;
33
+ --magenta-soft: #9020a0;
34
+ --magenta-bg: rgba(224,64,251,0.06);
35
+ --red: #ff1744;
36
+ --red-soft: #a01030;
37
+ --red-bg: rgba(255,23,68,0.06);
38
+
39
+ --mono: 'IBM Plex Mono', 'Menlo', 'Consolas', monospace;
40
+ --sans: 'Space Grotesk', system-ui, sans-serif;
41
+
42
+ --radius: 6px;
43
+ --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
44
+ }
45
+
46
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
47
+
48
+ html { font-size: 14px; }
49
+ body {
50
+ font-family: var(--mono);
51
+ background: var(--bg);
52
+ color: var(--text);
53
+ min-height: 100vh;
54
+ overflow-x: hidden;
55
+ -webkit-font-smoothing: antialiased;
56
+ }
57
+
58
+ /* ── Starfield ────────────────────────────────────── */
59
+ #starfield {
60
+ position: fixed;
61
+ inset: 0;
62
+ z-index: 0;
63
+ opacity: 0.35;
64
+ }
65
+
66
+ /* ── CRT (very subtle) ────────────────────────────── */
67
+ body::after {
68
+ content: '';
69
+ position: fixed;
70
+ inset: 0;
71
+ background: repeating-linear-gradient(
72
+ 0deg,
73
+ transparent 0px,
74
+ transparent 2px,
75
+ rgba(0,0,0,0.03) 2px,
76
+ rgba(0,0,0,0.03) 4px
77
+ );
78
+ pointer-events: none;
79
+ z-index: 9999;
80
+ }
81
+
82
+ /* ── Boot Screen ──────────────────────────────────── */
83
+ #boot {
84
+ position: fixed;
85
+ inset: 0;
86
+ z-index: 10000;
87
+ background: var(--bg);
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ flex-direction: column;
92
+ gap: 16px;
93
+ transition: opacity 0.3s ease-out;
94
+ }
95
+ #boot.done { opacity: 0; pointer-events: none; }
96
+ #boot-text {
97
+ font-family: var(--mono);
98
+ font-size: 12px;
99
+ color: var(--cyan);
100
+ white-space: pre;
101
+ line-height: 1.6;
102
+ text-shadow: 0 0 12px rgba(0,212,255,0.4);
103
+ }
104
+ #boot-bar {
105
+ width: 240px;
106
+ height: 3px;
107
+ background: var(--surface-2);
108
+ border-radius: 2px;
109
+ overflow: hidden;
110
+ }
111
+ #boot-bar-fill {
112
+ height: 100%;
113
+ width: 0%;
114
+ background: var(--cyan);
115
+ box-shadow: 0 0 8px var(--cyan);
116
+ transition: width 0.08s linear;
117
+ }
118
+
119
+ /* ── Layout Shell ─────────────────────────────────── */
120
+ #app {
121
+ position: relative;
122
+ z-index: 1;
123
+ max-width: 1320px;
124
+ margin: 0 auto;
125
+ padding: 0 20px 40px;
126
+ display: none;
127
+ }
128
+
129
+ /* ── Header ───────────────────────────────────────── */
130
+ .header {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 16px;
134
+ padding: 18px 0 14px;
135
+ border-bottom: 1px solid var(--border);
136
+ }
137
+ .header-brand {
138
+ display: flex;
139
+ align-items: baseline;
140
+ gap: 10px;
141
+ }
142
+ .header-title {
143
+ font-family: var(--sans);
144
+ font-size: 20px;
145
+ font-weight: 700;
146
+ color: var(--cyan);
147
+ letter-spacing: -0.5px;
148
+ text-shadow: 0 0 20px rgba(0,212,255,0.25);
149
+ }
150
+ .header-version {
151
+ font-size: 10px;
152
+ color: var(--text-muted);
153
+ font-weight: 400;
154
+ }
155
+ .header-right {
156
+ margin-left: auto;
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 14px;
160
+ font-size: 12px;
161
+ color: var(--text-dim);
162
+ }
163
+ .header-clock {
164
+ font-variant-numeric: tabular-nums;
165
+ color: var(--text-dim);
166
+ }
167
+ .header-dot {
168
+ width: 6px; height: 6px;
169
+ border-radius: 50%;
170
+ background: var(--green);
171
+ box-shadow: 0 0 8px var(--green);
172
+ animation: pulse 2s ease-in-out infinite;
173
+ }
174
+ @keyframes pulse {
175
+ 0%,100% { opacity: 1; }
176
+ 50% { opacity: 0.4; }
177
+ }
178
+
179
+ /* ── Stats Row ────────────────────────────────────── */
180
+ .stats-row {
181
+ display: flex;
182
+ gap: 1px;
183
+ margin: 14px 0;
184
+ background: var(--border);
185
+ border-radius: var(--radius);
186
+ overflow: hidden;
187
+ }
188
+ .stat-card {
189
+ flex: 1;
190
+ background: var(--surface);
191
+ padding: 14px 16px;
192
+ position: relative;
193
+ }
194
+ .stat-card::after {
195
+ content: '';
196
+ position: absolute;
197
+ bottom: 0; left: 0; right: 0;
198
+ height: 2px;
199
+ }
200
+ .stat-card.c-cyan::after { background: var(--cyan); }
201
+ .stat-card.c-amber::after { background: var(--amber); }
202
+ .stat-card.c-magenta::after { background: var(--magenta); }
203
+ .stat-card.c-green::after { background: var(--green); }
204
+ .stat-value {
205
+ font-family: var(--sans);
206
+ font-size: 26px;
207
+ font-weight: 700;
208
+ line-height: 1;
209
+ }
210
+ .stat-card.c-cyan .stat-value { color: var(--cyan); }
211
+ .stat-card.c-amber .stat-value { color: var(--amber); }
212
+ .stat-card.c-magenta .stat-value { color: var(--magenta); }
213
+ .stat-card.c-green .stat-value { color: var(--green); }
214
+ .stat-label {
215
+ font-size: 10px;
216
+ color: var(--text-muted);
217
+ margin-top: 6px;
218
+ text-transform: uppercase;
219
+ letter-spacing: 1px;
220
+ }
221
+
222
+ /* ── Tab Navigation ───────────────────────────────── */
223
+ .tabs {
224
+ display: flex;
225
+ gap: 0;
226
+ border-bottom: 1px solid var(--border);
227
+ margin-bottom: 16px;
228
+ position: sticky;
229
+ top: 0;
230
+ z-index: 100;
231
+ background: var(--bg);
232
+ padding-top: 4px;
233
+ }
234
+ .tab {
235
+ padding: 10px 18px;
236
+ font-family: var(--mono);
237
+ font-size: 11px;
238
+ font-weight: 500;
239
+ color: var(--text-muted);
240
+ cursor: pointer;
241
+ border: none;
242
+ background: none;
243
+ border-bottom: 2px solid transparent;
244
+ transition: all var(--transition);
245
+ text-transform: uppercase;
246
+ letter-spacing: 1px;
247
+ white-space: nowrap;
248
+ position: relative;
249
+ }
250
+ .tab:hover {
251
+ color: var(--text-dim);
252
+ background: var(--surface);
253
+ }
254
+ .tab.active {
255
+ color: var(--cyan);
256
+ border-bottom-color: var(--cyan);
257
+ }
258
+ .tab .tab-count {
259
+ font-size: 9px;
260
+ margin-left: 6px;
261
+ padding: 1px 5px;
262
+ border-radius: 8px;
263
+ background: var(--surface-2);
264
+ color: var(--text-dim);
265
+ font-weight: 400;
266
+ }
267
+ .tab.active .tab-count {
268
+ background: rgba(0,212,255,0.12);
269
+ color: var(--cyan);
270
+ }
271
+ .tab-live-dot {
272
+ display: inline-block;
273
+ width: 5px; height: 5px;
274
+ border-radius: 50%;
275
+ background: var(--green);
276
+ box-shadow: 0 0 6px var(--green);
277
+ margin-left: 6px;
278
+ animation: pulse 1.5s ease-in-out infinite;
279
+ vertical-align: middle;
280
+ }
281
+
282
+ /* ── Filter Bar ───────────────────────────────────── */
283
+ .filter-bar {
284
+ display: flex;
285
+ gap: 8px;
286
+ margin-bottom: 16px;
287
+ flex-wrap: wrap;
288
+ align-items: center;
289
+ }
290
+ .filter-group {
291
+ display: flex;
292
+ gap: 0;
293
+ border-radius: var(--radius);
294
+ overflow: hidden;
295
+ border: 1px solid var(--border);
296
+ }
297
+ .filter-btn {
298
+ padding: 6px 12px;
299
+ font-family: var(--mono);
300
+ font-size: 11px;
301
+ color: var(--text-dim);
302
+ background: var(--surface);
303
+ border: none;
304
+ cursor: pointer;
305
+ transition: all var(--transition);
306
+ white-space: nowrap;
307
+ }
308
+ .filter-btn:hover { background: var(--surface-2); color: var(--text); }
309
+ .filter-btn.active {
310
+ background: var(--surface-3);
311
+ color: var(--cyan);
312
+ }
313
+ .filter-btn + .filter-btn { border-left: 1px solid var(--border); }
314
+
315
+ .filter-label {
316
+ font-size: 10px;
317
+ color: var(--text-muted);
318
+ text-transform: uppercase;
319
+ letter-spacing: 1px;
320
+ margin-right: 4px;
321
+ }
322
+
323
+ /* ── Tab Content ──────────────────────────────────── */
324
+ .tab-content { display: none; }
325
+ .tab-content.active { display: block; animation: fadeIn 0.2s ease-out; }
326
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
327
+
328
+ /* ── Session Cards ────────────────────────────────── */
329
+ .session-group {
330
+ margin-bottom: 20px;
331
+ }
332
+ .session-group-header {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 10px;
336
+ padding: 8px 0;
337
+ margin-bottom: 6px;
338
+ }
339
+ .session-group-name {
340
+ font-family: var(--sans);
341
+ font-size: 15px;
342
+ font-weight: 600;
343
+ color: var(--text);
344
+ }
345
+ .session-group-account {
346
+ font-size: 10px;
347
+ color: var(--text-muted);
348
+ padding: 2px 8px;
349
+ border: 1px solid var(--border);
350
+ border-radius: 10px;
351
+ }
352
+ .session-group-count {
353
+ font-size: 10px;
354
+ color: var(--text-muted);
355
+ margin-left: auto;
356
+ }
357
+
358
+ .session-card {
359
+ background: var(--surface);
360
+ border: 1px solid var(--border);
361
+ border-radius: var(--radius);
362
+ margin-bottom: 6px;
363
+ transition: all var(--transition);
364
+ overflow: hidden;
365
+ }
366
+ .session-card:hover {
367
+ border-color: var(--border-hi);
368
+ }
369
+ .session-card.is-live {
370
+ border-color: var(--green-soft);
371
+ box-shadow: 0 0 0 1px rgba(0,230,118,0.08);
372
+ }
373
+ .session-card.expanded {
374
+ border-color: var(--border-active);
375
+ }
376
+
377
+ .session-head {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 12px;
381
+ padding: 12px 16px;
382
+ cursor: pointer;
383
+ user-select: none;
384
+ }
385
+ .session-head:hover { background: var(--surface-2); }
386
+
387
+ .session-type-badge {
388
+ font-size: 9px;
389
+ font-weight: 600;
390
+ padding: 3px 8px;
391
+ border-radius: 3px;
392
+ text-transform: uppercase;
393
+ letter-spacing: 1px;
394
+ flex-shrink: 0;
395
+ }
396
+ .session-type-badge.task {
397
+ background: var(--amber-bg);
398
+ color: var(--amber);
399
+ border: 1px solid rgba(255,179,0,0.2);
400
+ }
401
+ .session-type-badge.review {
402
+ background: var(--magenta-bg);
403
+ color: var(--magenta);
404
+ border: 1px solid rgba(224,64,251,0.2);
405
+ }
406
+
407
+ .session-name {
408
+ font-size: 14px;
409
+ font-weight: 500;
410
+ color: var(--text);
411
+ flex: 1;
412
+ min-width: 0;
413
+ overflow: hidden;
414
+ text-overflow: ellipsis;
415
+ white-space: nowrap;
416
+ }
417
+
418
+ .session-live-indicator {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 5px;
422
+ font-size: 10px;
423
+ color: var(--green);
424
+ flex-shrink: 0;
425
+ padding: 3px 8px;
426
+ background: var(--green-bg);
427
+ border-radius: 10px;
428
+ }
429
+ .session-live-indicator .dot {
430
+ width: 5px; height: 5px;
431
+ border-radius: 50%;
432
+ background: var(--green);
433
+ box-shadow: 0 0 6px var(--green);
434
+ animation: pulse 1s ease-in-out infinite;
435
+ }
436
+
437
+ .session-meta-inline {
438
+ display: flex;
439
+ gap: 14px;
440
+ font-size: 11px;
441
+ color: var(--text-dim);
442
+ flex-shrink: 0;
443
+ }
444
+ .session-meta-inline span {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 4px;
448
+ }
449
+
450
+ .session-chevron {
451
+ color: var(--text-muted);
452
+ font-size: 10px;
453
+ transition: transform var(--transition);
454
+ flex-shrink: 0;
455
+ width: 16px;
456
+ text-align: center;
457
+ }
458
+ .session-card.expanded .session-chevron {
459
+ transform: rotate(90deg);
460
+ }
461
+
462
+ /* ── Session Expanded Body ────────────────────────── */
463
+ .session-body {
464
+ display: none;
465
+ border-top: 1px solid var(--border);
466
+ padding: 14px 16px;
467
+ background: var(--surface);
468
+ }
469
+ .session-card.expanded .session-body { display: block; }
470
+
471
+ .session-detail-grid {
472
+ display: grid;
473
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
474
+ gap: 12px;
475
+ margin-bottom: 14px;
476
+ }
477
+ .session-detail-item {
478
+ display: flex;
479
+ flex-direction: column;
480
+ gap: 3px;
481
+ }
482
+ .session-detail-label {
483
+ font-size: 10px;
484
+ color: var(--text-muted);
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.5px;
487
+ }
488
+ .session-detail-value {
489
+ font-size: 13px;
490
+ color: var(--text);
491
+ word-break: break-all;
492
+ }
493
+
494
+ .session-live-section {
495
+ background: var(--surface-2);
496
+ border: 1px solid var(--border);
497
+ border-radius: var(--radius);
498
+ padding: 10px 14px;
499
+ margin-top: 10px;
500
+ }
501
+ .session-live-title {
502
+ font-size: 10px;
503
+ color: var(--green);
504
+ text-transform: uppercase;
505
+ letter-spacing: 1px;
506
+ margin-bottom: 8px;
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 6px;
510
+ }
511
+ .session-live-title .dot {
512
+ width: 5px; height: 5px;
513
+ border-radius: 50%;
514
+ background: var(--green);
515
+ animation: pulse 1s ease-in-out infinite;
516
+ }
517
+
518
+ .session-activity-list {
519
+ display: flex;
520
+ flex-direction: column;
521
+ gap: 4px;
522
+ }
523
+ .session-activity-entry {
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 8px;
527
+ font-size: 12px;
528
+ padding: 4px 0;
529
+ }
530
+ .tool-icon {
531
+ width: 22px;
532
+ height: 22px;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: center;
536
+ font-size: 11px;
537
+ border-radius: 3px;
538
+ flex-shrink: 0;
539
+ font-weight: 600;
540
+ }
541
+ .tool-icon.bash { background: var(--amber-bg); color: var(--amber); border: 1px solid rgba(255,179,0,0.15); }
542
+ .tool-icon.read { background: var(--cyan-bg); color: var(--cyan); border: 1px solid rgba(0,212,255,0.15); }
543
+ .tool-icon.edit { background: var(--green-bg); color: var(--green); border: 1px solid rgba(0,230,118,0.15); }
544
+ .tool-icon.write { background: var(--green-bg); color: var(--green); border: 1px solid rgba(0,230,118,0.15); }
545
+ .tool-icon.search { background: var(--cyan-bg); color: var(--cyan-soft); border: 1px solid rgba(0,212,255,0.1); }
546
+ .tool-icon.agent { background: var(--magenta-bg); color: var(--magenta); border: 1px solid rgba(224,64,251,0.15); }
547
+ .tool-icon.stop { background: var(--surface-3); color: var(--text-muted); border: 1px solid var(--border); }
548
+ .tool-icon.session { background: var(--cyan-bg); color: var(--cyan); border: 1px solid rgba(0,212,255,0.15); }
549
+
550
+ .activity-summary {
551
+ color: var(--text);
552
+ flex: 1;
553
+ min-width: 0;
554
+ overflow: hidden;
555
+ text-overflow: ellipsis;
556
+ white-space: nowrap;
557
+ }
558
+ .activity-time {
559
+ font-size: 10px;
560
+ color: var(--text-muted);
561
+ flex-shrink: 0;
562
+ font-variant-numeric: tabular-nums;
563
+ }
564
+
565
+ /* ── Crew Cards ───────────────────────────────────── */
566
+ .crew-grid {
567
+ display: grid;
568
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
569
+ gap: 12px;
570
+ }
571
+ .crew-card {
572
+ background: var(--surface);
573
+ border: 1px solid var(--border);
574
+ border-radius: var(--radius);
575
+ padding: 20px;
576
+ display: flex;
577
+ align-items: center;
578
+ gap: 16px;
579
+ transition: border-color var(--transition);
580
+ }
581
+ .crew-card:hover { border-color: var(--border-hi); }
582
+ .crew-avatar {
583
+ width: 48px; height: 48px;
584
+ border-radius: var(--radius);
585
+ display: flex;
586
+ align-items: center;
587
+ justify-content: center;
588
+ font-size: 22px;
589
+ font-weight: 700;
590
+ flex-shrink: 0;
591
+ font-family: var(--sans);
592
+ text-transform: uppercase;
593
+ }
594
+ .crew-avatar.online {
595
+ background: var(--green-bg);
596
+ color: var(--green);
597
+ border: 1px solid rgba(0,230,118,0.2);
598
+ }
599
+ .crew-avatar.offline {
600
+ background: var(--red-bg);
601
+ color: var(--red-soft);
602
+ border: 1px solid rgba(255,23,68,0.15);
603
+ }
604
+ .crew-info { flex: 1; }
605
+ .crew-name {
606
+ font-family: var(--sans);
607
+ font-size: 16px;
608
+ font-weight: 600;
609
+ }
610
+ .crew-status {
611
+ font-size: 11px;
612
+ margin-top: 4px;
613
+ }
614
+ .crew-status.online { color: var(--green); }
615
+ .crew-status.offline { color: var(--red-soft); }
616
+ .crew-detail {
617
+ font-size: 11px;
618
+ color: var(--text-dim);
619
+ margin-top: 6px;
620
+ }
621
+
622
+ /* ── Sector Cards ─────────────────────────────────── */
623
+ .sector-grid {
624
+ display: grid;
625
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
626
+ gap: 8px;
627
+ }
628
+ .sector-card {
629
+ background: var(--surface);
630
+ border: 1px solid var(--border);
631
+ border-radius: var(--radius);
632
+ padding: 12px 14px;
633
+ transition: all var(--transition);
634
+ display: flex;
635
+ flex-direction: column;
636
+ gap: 8px;
637
+ border-left: 3px solid var(--border);
638
+ }
639
+ .sector-card:hover { border-color: var(--border-hi); }
640
+ .sector-card[data-acct="monoku"] { border-left-color: var(--cyan); }
641
+ .sector-card[data-acct="meridian"] { border-left-color: var(--magenta); }
642
+ .sector-card[data-acct="work"] { border-left-color: var(--amber); }
643
+ .sector-card[data-acct="personal"] { border-left-color: var(--green); }
644
+
645
+ .sector-name {
646
+ font-size: 13px;
647
+ font-weight: 500;
648
+ color: var(--text);
649
+ }
650
+ .sector-badges {
651
+ display: flex;
652
+ gap: 4px;
653
+ flex-wrap: wrap;
654
+ }
655
+ .badge {
656
+ font-size: 9px;
657
+ padding: 2px 6px;
658
+ border-radius: 3px;
659
+ font-weight: 500;
660
+ text-transform: uppercase;
661
+ letter-spacing: 0.5px;
662
+ }
663
+ .badge.md { background: var(--green-bg); color: var(--green); border: 1px solid rgba(0,230,118,0.15); }
664
+ .badge.ag { background: var(--cyan-bg); color: var(--cyan-soft); border: 1px solid rgba(0,212,255,0.1); }
665
+ .badge.cm { background: var(--cyan-bg); color: var(--cyan-soft); border: 1px solid rgba(0,212,255,0.1); }
666
+ .badge.mcp { background: var(--amber-bg); color: var(--amber-soft); border: 1px solid rgba(255,179,0,0.1); }
667
+ .badge.git { background: var(--surface-2); color: var(--text-muted); border: 1px solid var(--border); }
668
+ .sector-account {
669
+ font-size: 10px;
670
+ color: var(--text-muted);
671
+ }
672
+
673
+ /* ── Live Feed ────────────────────────────────────── */
674
+ .feed-list {
675
+ display: flex;
676
+ flex-direction: column;
677
+ gap: 2px;
678
+ }
679
+ .feed-item {
680
+ display: flex;
681
+ align-items: center;
682
+ gap: 10px;
683
+ padding: 8px 12px;
684
+ background: var(--surface);
685
+ border-radius: var(--radius);
686
+ font-size: 12px;
687
+ animation: feedSlide 0.25s ease-out;
688
+ border: 1px solid transparent;
689
+ transition: border-color var(--transition);
690
+ }
691
+ .feed-item:hover { border-color: var(--border); }
692
+ .feed-item.fresh {
693
+ background: var(--green-bg);
694
+ border-color: rgba(0,230,118,0.1);
695
+ }
696
+ @keyframes feedSlide {
697
+ from { opacity: 0; transform: translateY(-6px); }
698
+ to { opacity: 1; transform: none; }
699
+ }
700
+ .feed-time {
701
+ font-size: 10px;
702
+ color: var(--text-muted);
703
+ font-variant-numeric: tabular-nums;
704
+ min-width: 40px;
705
+ flex-shrink: 0;
706
+ }
707
+ .feed-project {
708
+ color: var(--cyan);
709
+ font-size: 11px;
710
+ font-weight: 500;
711
+ min-width: 80px;
712
+ max-width: 120px;
713
+ overflow: hidden;
714
+ text-overflow: ellipsis;
715
+ white-space: nowrap;
716
+ flex-shrink: 0;
717
+ }
718
+ .feed-task {
719
+ color: var(--amber);
720
+ font-size: 11px;
721
+ min-width: 60px;
722
+ max-width: 100px;
723
+ overflow: hidden;
724
+ text-overflow: ellipsis;
725
+ white-space: nowrap;
726
+ flex-shrink: 0;
727
+ }
728
+ .feed-desc {
729
+ color: var(--text);
730
+ flex: 1;
731
+ min-width: 0;
732
+ overflow: hidden;
733
+ text-overflow: ellipsis;
734
+ white-space: nowrap;
735
+ opacity: 0.75;
736
+ }
737
+
738
+ /* ── Log Entries ──────────────────────────────────── */
739
+ .log-list {
740
+ display: flex;
741
+ flex-direction: column;
742
+ gap: 1px;
743
+ }
744
+ .log-item {
745
+ display: flex;
746
+ gap: 12px;
747
+ padding: 8px 12px;
748
+ font-size: 12px;
749
+ background: var(--surface);
750
+ border-radius: 2px;
751
+ align-items: baseline;
752
+ }
753
+ .log-time {
754
+ color: var(--text-muted);
755
+ font-size: 10px;
756
+ min-width: 44px;
757
+ flex-shrink: 0;
758
+ font-variant-numeric: tabular-nums;
759
+ }
760
+ .log-action {
761
+ font-weight: 600;
762
+ min-width: 56px;
763
+ font-size: 11px;
764
+ }
765
+ .log-action.WORK { color: var(--amber); }
766
+ .log-action.REVIEW { color: var(--magenta); }
767
+ .log-action.DONE { color: var(--green); }
768
+ .log-action.OPEN { color: var(--cyan); }
769
+ .log-detail { color: var(--text); opacity: 0.75; }
770
+
771
+ /* ── Empty State ──────────────────────────────────── */
772
+ .empty-state {
773
+ text-align: center;
774
+ padding: 40px 20px;
775
+ color: var(--text-muted);
776
+ font-size: 13px;
777
+ line-height: 1.8;
778
+ }
779
+ .empty-state code {
780
+ background: var(--surface-2);
781
+ color: var(--cyan);
782
+ padding: 2px 8px;
783
+ border-radius: 3px;
784
+ font-size: 12px;
785
+ }
786
+
787
+ /* ── Refresh Flash ────────────────────────────────── */
788
+ .refresh-bar {
789
+ position: fixed;
790
+ top: 0; left: 0; right: 0;
791
+ height: 2px;
792
+ z-index: 10000;
793
+ opacity: 0;
794
+ transition: opacity 0.15s;
795
+ }
796
+ .refresh-bar.on {
797
+ opacity: 1;
798
+ background: linear-gradient(90deg, transparent, var(--cyan), transparent);
799
+ animation: scanX 0.6s ease-out;
800
+ }
801
+ @keyframes scanX {
802
+ from { transform: translateX(-100%); }
803
+ to { transform: translateX(100%); }
804
+ }
805
+
806
+ /* ── Scrollbar ────────────────────────────────────── */
807
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
808
+ ::-webkit-scrollbar-track { background: transparent; }
809
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
810
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-hi); }
811
+
812
+ /* ── Responsive ───────────────────────────────────── */
813
+ @media (max-width: 768px) {
814
+ .stats-row { flex-wrap: wrap; }
815
+ .stat-card { min-width: calc(50% - 1px); }
816
+ .session-meta-inline { display: none; }
817
+ .tabs { overflow-x: auto; }
818
+ .filter-bar { gap: 4px; }
819
+ }
820
+ </style>
821
+ </head>
822
+ <body>
823
+
824
+ <div class="refresh-bar" id="refresh-bar"></div>
825
+ <canvas id="starfield"></canvas>
826
+
827
+ <!-- ── Boot ── -->
828
+ <div id="boot">
829
+ <pre id="boot-text"></pre>
830
+ <div id="boot-bar"><div id="boot-bar-fill"></div></div>
831
+ </div>
832
+
833
+ <!-- ── App ── -->
834
+ <div id="app">
835
+
836
+ <!-- Header -->
837
+ <div class="header">
838
+ <div class="header-brand">
839
+ <span class="header-title">CW STATION</span>
840
+ <span class="header-version">v0.1.0</span>
841
+ </div>
842
+ <div class="header-right">
843
+ <div class="header-dot"></div>
844
+ <span class="header-clock" id="clock">00:00:00</span>
845
+ </div>
846
+ </div>
847
+
848
+ <!-- Stats -->
849
+ <div class="stats-row" id="stats-row"></div>
850
+
851
+ <!-- Tabs -->
852
+ <div class="tabs" id="tabs">
853
+ <button class="tab active" data-tab="sessions">Sessions <span class="tab-count" id="tc-sessions">0</span></button>
854
+ <button class="tab" data-tab="crew">Crew <span class="tab-count" id="tc-crew">0</span></button>
855
+ <button class="tab" data-tab="sectors">Sectors <span class="tab-count" id="tc-sectors">0</span></button>
856
+ <button class="tab" data-tab="feed">Live Feed <span class="tab-live-dot" id="feed-dot" style="display:none"></span></button>
857
+ <button class="tab" data-tab="log">Log</button>
858
+ </div>
859
+
860
+ <!-- Filter Bar -->
861
+ <div class="filter-bar" id="filter-bar"></div>
862
+
863
+ <!-- Sessions -->
864
+ <div class="tab-content active" id="panel-sessions"></div>
865
+
866
+ <!-- Crew -->
867
+ <div class="tab-content" id="panel-crew"></div>
868
+
869
+ <!-- Sectors -->
870
+ <div class="tab-content" id="panel-sectors"></div>
871
+
872
+ <!-- Live Feed -->
873
+ <div class="tab-content" id="panel-feed"></div>
874
+
875
+ <!-- Log -->
876
+ <div class="tab-content" id="panel-log"></div>
877
+
878
+ </div>
879
+
880
+ <script>
881
+ /* ═══════════════════════════════════════════════════════════════
882
+ CW STATION — Engine
883
+ ═══════════════════════════════════════════════════════════════ */
884
+
885
+ // ── State ────────────────────────────────────────────────
886
+ let cwData = null;
887
+ let activityData = { events: [], live: [] };
888
+ let lastActivityTs = 0;
889
+ let feedEntries = [];
890
+ let currentTab = 'sessions';
891
+ let filters = { account: 'all', type: 'all' };
892
+ const MAX_FEED = 120;
893
+
894
+ // ── Starfield ────────────────────────────────────────────
895
+ function initStarfield() {
896
+ const c = document.getElementById('starfield');
897
+ const ctx = c.getContext('2d');
898
+ let w, h, stars = [];
899
+ function resize() { w = c.width = innerWidth; h = c.height = innerHeight; }
900
+ function make() {
901
+ stars = Array.from({ length: 80 }, () => ({
902
+ x: Math.random() * w, y: Math.random() * h,
903
+ s: Math.random() * 1.2 + 0.3, sp: Math.random() * 0.15 + 0.02,
904
+ o: Math.random() * 0.4 + 0.1, t: Math.random() * Math.PI * 2
905
+ }));
906
+ }
907
+ function draw() {
908
+ ctx.clearRect(0, 0, w, h);
909
+ stars.forEach(s => {
910
+ s.t += 0.015;
911
+ const a = s.o * (0.6 + 0.4 * Math.sin(s.t));
912
+ ctx.fillStyle = `rgba(180,200,220,${a})`;
913
+ ctx.fillRect(s.x | 0, s.y | 0, Math.ceil(s.s), Math.ceil(s.s));
914
+ s.y += s.sp;
915
+ if (s.y > h) { s.y = 0; s.x = Math.random() * w; }
916
+ });
917
+ requestAnimationFrame(draw);
918
+ }
919
+ resize(); make(); draw();
920
+ addEventListener('resize', () => { resize(); make(); });
921
+ }
922
+
923
+ // ── Boot ─────────────────────────────────────────────────
924
+ const BOOT_LINES = [
925
+ 'CW STATION v0.1.0',
926
+ 'SYSTEMS CHECK.......... OK',
927
+ 'LAUNCHING COMMAND CENTER',
928
+ ];
929
+
930
+ async function boot() {
931
+ const txt = document.getElementById('boot-text');
932
+ const bar = document.getElementById('boot-bar-fill');
933
+ const el = document.getElementById('boot');
934
+ let progress = 0;
935
+ for (const line of BOOT_LINES) {
936
+ for (const ch of line) {
937
+ txt.textContent += ch;
938
+ progress += 100 / (BOOT_LINES.join('').length);
939
+ bar.style.width = Math.min(progress, 100) + '%';
940
+ await sleep(4);
941
+ }
942
+ txt.textContent += '\n';
943
+ await sleep(30);
944
+ }
945
+ bar.style.width = '100%';
946
+ await sleep(120);
947
+ el.classList.add('done');
948
+ document.getElementById('app').style.display = 'block';
949
+ await sleep(300);
950
+ el.remove();
951
+ }
952
+
953
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
954
+
955
+ // ── Clock ────────────────────────────────────────────────
956
+ function tickClock() {
957
+ const n = new Date();
958
+ document.getElementById('clock').textContent =
959
+ [n.getHours(), n.getMinutes(), n.getSeconds()].map(v => String(v).padStart(2,'0')).join(':');
960
+ }
961
+
962
+ // ── Beep ─────────────────────────────────────────────────
963
+ function beep(freq=660, dur=40, vol=0.025) {
964
+ try {
965
+ const ac = new (window.AudioContext || window.webkitAudioContext)();
966
+ const o = ac.createOscillator();
967
+ const g = ac.createGain();
968
+ o.type = 'square'; o.frequency.value = freq; g.gain.value = vol;
969
+ o.connect(g); g.connect(ac.destination);
970
+ o.start(); o.stop(ac.currentTime + dur / 1000);
971
+ } catch(e) {}
972
+ }
973
+
974
+ // ── Tabs ─────────────────────────────────────────────────
975
+ function initTabs() {
976
+ document.querySelectorAll('.tab').forEach(t => {
977
+ t.addEventListener('click', () => {
978
+ const id = t.dataset.tab;
979
+ if (id === currentTab) return;
980
+ currentTab = id;
981
+ sessionsFingerprint = '';
982
+ document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === id));
983
+ document.querySelectorAll('.tab-content').forEach(p => p.classList.toggle('active', p.id === 'panel-' + id));
984
+ buildFilters();
985
+ renderCurrentTab();
986
+ });
987
+ });
988
+ }
989
+
990
+ // ── Filters ──────────────────────────────────────────────
991
+ function buildFilters() {
992
+ const bar = document.getElementById('filter-bar');
993
+ if (!cwData) { bar.innerHTML = ''; return; }
994
+
995
+ // Accounts
996
+ const accounts = cwData.accounts.map(a => a.name);
997
+ // Types
998
+ const showTypeFilter = ['sessions', 'feed'].includes(currentTab);
999
+
1000
+ let html = '<span class="filter-label">Account</span><div class="filter-group">';
1001
+ html += `<button class="filter-btn ${filters.account === 'all' ? 'active' : ''}" data-filter="account" data-val="all">All</button>`;
1002
+ accounts.forEach(a => {
1003
+ html += `<button class="filter-btn ${filters.account === a ? 'active' : ''}" data-filter="account" data-val="${a}">${a}</button>`;
1004
+ });
1005
+ html += '</div>';
1006
+
1007
+ if (showTypeFilter) {
1008
+ html += '<span class="filter-label" style="margin-left:12px">Type</span><div class="filter-group">';
1009
+ html += `<button class="filter-btn ${filters.type === 'all' ? 'active' : ''}" data-filter="type" data-val="all">All</button>`;
1010
+ html += `<button class="filter-btn ${filters.type === 'task' ? 'active' : ''}" data-filter="type" data-val="task">Tasks</button>`;
1011
+ html += `<button class="filter-btn ${filters.type === 'review' ? 'active' : ''}" data-filter="type" data-val="review">Reviews</button>`;
1012
+ html += '</div>';
1013
+ }
1014
+
1015
+ bar.innerHTML = html;
1016
+ bar.querySelectorAll('.filter-btn').forEach(b => {
1017
+ b.addEventListener('click', () => {
1018
+ filters[b.dataset.filter] = b.dataset.val;
1019
+ buildFilters();
1020
+ renderCurrentTab();
1021
+ });
1022
+ });
1023
+ }
1024
+
1025
+ // ── Helpers ──────────────────────────────────────────────
1026
+ function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
1027
+ function relTime(iso) {
1028
+ if (!iso) return '—';
1029
+ const d = new Date(iso);
1030
+ const now = Date.now();
1031
+ const diff = now - d.getTime();
1032
+ if (diff < 60000) return 'just now';
1033
+ if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
1034
+ if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
1035
+ return d.toLocaleDateString();
1036
+ }
1037
+ function shortDate(iso) { return iso ? iso.slice(0,10) : '—'; }
1038
+
1039
+ // ── Tool Icons ───────────────────────────────────────────
1040
+ const TOOLS = {
1041
+ Bash: { label: '$', cls: 'bash' },
1042
+ Read: { label: 'R', cls: 'read' },
1043
+ Edit: { label: 'E', cls: 'edit' },
1044
+ Write: { label: 'W', cls: 'write' },
1045
+ Glob: { label: '*', cls: 'search' },
1046
+ Grep: { label: '?', cls: 'search' },
1047
+ Agent: { label: 'A', cls: 'agent' },
1048
+ WebFetch: { label: 'F', cls: 'search' },
1049
+ WebSearch:{ label: 'S', cls: 'search' },
1050
+ };
1051
+ function toolInfo(tool, event) {
1052
+ if (TOOLS[tool]) return TOOLS[tool];
1053
+ if (event?.startsWith('Subagent')) return { label: 'A', cls: 'agent' };
1054
+ if (event === 'TeammateIdle') return { label: 'T', cls: 'agent' };
1055
+ if (event === 'TaskCompleted') return { label: '\u2713', cls: 'write' };
1056
+ if (event?.startsWith('Session')) return { label: '\u25CF', cls: 'session' };
1057
+ if (event === 'Stop') return { label: '\u25A0', cls: 'stop' };
1058
+ return { label: '\u00B7', cls: 'stop' };
1059
+ }
1060
+
1061
+ // ── Render: Stats ────────────────────────────────────────
1062
+ function renderStats(stats) {
1063
+ document.getElementById('stats-row').innerHTML = `
1064
+ <div class="stat-card c-cyan">
1065
+ <div class="stat-value">${stats.active_sessions||0}</div>
1066
+ <div class="stat-label">Active</div>
1067
+ </div>
1068
+ <div class="stat-card c-amber">
1069
+ <div class="stat-value">${stats.active_tasks||0}</div>
1070
+ <div class="stat-label">Tasks</div>
1071
+ </div>
1072
+ <div class="stat-card c-magenta">
1073
+ <div class="stat-value">${stats.active_reviews||0}</div>
1074
+ <div class="stat-label">Reviews</div>
1075
+ </div>
1076
+ <div class="stat-card c-green">
1077
+ <div class="stat-value">${stats.total_projects||0}</div>
1078
+ <div class="stat-label">Sectors</div>
1079
+ </div>`;
1080
+ }
1081
+
1082
+ // ── Render: Sessions ─────────────────────────────────────
1083
+ const expandedSessions = new Set();
1084
+
1085
+ let sessionsFingerprint = '';
1086
+
1087
+ function renderSessions(forceRebuild) {
1088
+ const el = document.getElementById('panel-sessions');
1089
+ const active = cwData.sessions.filter(s => s.status === 'active');
1090
+ const filtered = active.filter(s => {
1091
+ if (filters.account !== 'all' && s.account !== filters.account) return false;
1092
+ if (filters.type !== 'all' && s.type !== filters.type) return false;
1093
+ return true;
1094
+ });
1095
+
1096
+ document.getElementById('tc-sessions').textContent = filtered.length;
1097
+
1098
+ if (!filtered.length) {
1099
+ sessionsFingerprint = '';
1100
+ el.innerHTML = '<div class="empty-state">No active sessions.<br>Start one with <code>cw work &lt;project&gt; &lt;task&gt;</code></div>';
1101
+ return;
1102
+ }
1103
+
1104
+ // Build fingerprint to detect structural changes (new/removed sessions, filter changes)
1105
+ const fp = filters.account + ':' + filters.type + '|' + filtered.map(s => s.project + '/' + (s.task || s.pr || '') + ':' + s.status + ':' + (s.opens||0)).sort().join('|');
1106
+ const structureChanged = fp !== sessionsFingerprint || forceRebuild;
1107
+
1108
+ if (!structureChanged) {
1109
+ // Just update live indicators and activity in-place
1110
+ el.querySelectorAll('.session-card').forEach(card => {
1111
+ const proj = card.dataset.project;
1112
+ const taskKey = card.dataset.task;
1113
+ const liveInfo = activityData.live.find(l => l.project === proj && l.task === taskKey);
1114
+ const isLive = !!liveInfo;
1115
+
1116
+ card.classList.toggle('is-live', isLive);
1117
+
1118
+ // Update live indicator in header
1119
+ let indicator = card.querySelector('.session-live-indicator');
1120
+ if (isLive && !indicator) {
1121
+ const chevron = card.querySelector('.session-chevron');
1122
+ const span = document.createElement('span');
1123
+ span.className = 'session-live-indicator';
1124
+ span.innerHTML = '<span class="dot"></span>LIVE';
1125
+ chevron.parentElement.insertBefore(span, card.querySelector('.session-meta-inline'));
1126
+ } else if (!isLive && indicator) {
1127
+ indicator.remove();
1128
+ }
1129
+
1130
+ // Update session body live section
1131
+ const body = card.querySelector('.session-body');
1132
+ if (body && card.classList.contains('expanded')) {
1133
+ const liveSection = body.querySelector('.session-live-section');
1134
+ const noActivity = body.querySelector('[style*="No live activity"]');
1135
+ if (isLive) {
1136
+ const newLiveHtml = renderSessionLive(liveInfo, proj, taskKey);
1137
+ if (liveSection) {
1138
+ liveSection.outerHTML = newLiveHtml;
1139
+ } else if (noActivity) {
1140
+ noActivity.outerHTML = newLiveHtml;
1141
+ }
1142
+ } else {
1143
+ if (liveSection) {
1144
+ liveSection.outerHTML = '<div style="font-size:12px;color:var(--text-muted);padding:6px 0">No live activity right now</div>';
1145
+ }
1146
+ }
1147
+ }
1148
+ });
1149
+ return;
1150
+ }
1151
+
1152
+ sessionsFingerprint = fp;
1153
+
1154
+ // Group by project
1155
+ const groups = {};
1156
+ filtered.forEach(s => {
1157
+ const key = s.project || 'unknown';
1158
+ if (!groups[key]) groups[key] = { account: s.account, sessions: [] };
1159
+ groups[key].sessions.push(s);
1160
+ });
1161
+
1162
+ // Sort groups, then sessions by last_opened desc
1163
+ let html = '';
1164
+ for (const [proj, g] of Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0]))) {
1165
+ g.sessions.sort((a, b) => (b.last_opened || '').localeCompare(a.last_opened || ''));
1166
+ html += `<div class="session-group">
1167
+ <div class="session-group-header">
1168
+ <span class="session-group-name">${esc(proj)}</span>
1169
+ <span class="session-group-account">${esc(g.account)}</span>
1170
+ <span class="session-group-count">${g.sessions.length} session${g.sessions.length > 1 ? 's' : ''}</span>
1171
+ </div>`;
1172
+
1173
+ for (const s of g.sessions) {
1174
+ const isTask = s.type === 'task';
1175
+ const name = isTask ? (s.task || '?') : `PR #${s.pr || '?'}`;
1176
+ const taskKey = isTask ? (s.task || '') : ('pr-' + (s.pr || ''));
1177
+ const liveInfo = activityData.live.find(l => l.project === s.project && l.task === taskKey);
1178
+ const isLive = !!liveInfo;
1179
+ const isExpanded = expandedSessions.has(s.project + '/' + taskKey);
1180
+
1181
+ html += `<div class="session-card ${isLive ? 'is-live' : ''} ${isExpanded ? 'expanded' : ''}" data-project="${esc(s.project)}" data-task="${esc(taskKey)}">
1182
+ <div class="session-head" onclick="toggleSession(this)">
1183
+ <span class="session-type-badge ${s.type}">${s.type}</span>
1184
+ <span class="session-name">${esc(name)}</span>
1185
+ ${isLive ? `<span class="session-live-indicator"><span class="dot"></span>LIVE</span>` : ''}
1186
+ <span class="session-meta-inline">
1187
+ <span>${s.opens || 0}x</span>
1188
+ <span>${relTime(s.last_opened)}</span>
1189
+ </span>
1190
+ <span class="session-chevron">\u25B8</span>
1191
+ </div>
1192
+ <div class="session-body">
1193
+ <div class="session-detail-grid">
1194
+ <div class="session-detail-item"><div class="session-detail-label">Created</div><div class="session-detail-value">${shortDate(s.created)}</div></div>
1195
+ <div class="session-detail-item"><div class="session-detail-label">Last Opened</div><div class="session-detail-value">${relTime(s.last_opened)}</div></div>
1196
+ <div class="session-detail-item"><div class="session-detail-label">Opens</div><div class="session-detail-value">${s.opens || 0}</div></div>
1197
+ <div class="session-detail-item"><div class="session-detail-label">Account</div><div class="session-detail-value">${esc(s.account || '')}</div></div>
1198
+ ${s.source ? `<div class="session-detail-item"><div class="session-detail-label">Source</div><div class="session-detail-value">${esc(s.source)}</div></div>` : ''}
1199
+ ${s.source_url ? `<div class="session-detail-item"><div class="session-detail-label">URL</div><div class="session-detail-value" style="font-size:11px">${esc(s.source_url)}</div></div>` : ''}
1200
+ </div>
1201
+ ${isLive ? renderSessionLive(liveInfo, s.project, taskKey) : '<div style="font-size:12px;color:var(--text-muted);padding:6px 0">No live activity right now</div>'}
1202
+ </div>
1203
+ </div>`;
1204
+ }
1205
+ html += '</div>';
1206
+ }
1207
+ el.innerHTML = html;
1208
+ }
1209
+
1210
+ function renderSessionLive(liveInfo, project, taskKey) {
1211
+ // Get recent events for this session
1212
+ const recent = activityData.events
1213
+ .filter(e => e.project === project && e.task === taskKey)
1214
+ .slice(-8)
1215
+ .reverse();
1216
+
1217
+ let items = '';
1218
+ for (const e of recent) {
1219
+ const ti = toolInfo(e.tool, e.event);
1220
+ const ts = new Date(e.ts * 1000);
1221
+ const t = ts.toTimeString().slice(0, 8);
1222
+ items += `<div class="session-activity-entry">
1223
+ <span class="tool-icon ${ti.cls}">${ti.label}</span>
1224
+ <span class="activity-summary">${e.tool ? esc(e.tool) + ' ' : ''}${esc(e.summary || e.event || '')}</span>
1225
+ <span class="activity-time">${t}</span>
1226
+ </div>`;
1227
+ }
1228
+
1229
+ return `<div class="session-live-section">
1230
+ <div class="session-live-title"><span class="dot"></span> Live Activity</div>
1231
+ ${liveInfo.tool ? `<div style="font-size:12px;margin-bottom:8px;color:var(--text)">Currently: <strong style="color:var(--amber)">${esc(liveInfo.tool)}</strong> <span style="color:var(--text-dim)">${esc(liveInfo.summary || '')}</span></div>` : ''}
1232
+ <div class="session-activity-list">${items || '<div style="color:var(--text-muted);font-size:11px">Waiting for events...</div>'}</div>
1233
+ </div>`;
1234
+ }
1235
+
1236
+ function toggleSession(head) {
1237
+ const card = head.parentElement;
1238
+ const key = card.dataset.project + '/' + card.dataset.task;
1239
+ card.classList.toggle('expanded');
1240
+ if (card.classList.contains('expanded')) {
1241
+ expandedSessions.add(key);
1242
+ } else {
1243
+ expandedSessions.delete(key);
1244
+ }
1245
+ }
1246
+
1247
+ // ── Render: Crew ─────────────────────────────────────────
1248
+ function renderCrew() {
1249
+ const el = document.getElementById('panel-crew');
1250
+ const accounts = cwData.accounts;
1251
+ document.getElementById('tc-crew').textContent = accounts.length;
1252
+
1253
+ if (!accounts.length) {
1254
+ el.innerHTML = '<div class="empty-state">No accounts configured.<br>Run <code>cw account add &lt;name&gt;</code></div>';
1255
+ return;
1256
+ }
1257
+
1258
+ // Count projects per account
1259
+ const projCount = {};
1260
+ cwData.projects.forEach(p => { projCount[p.account] = (projCount[p.account] || 0) + 1; });
1261
+ const sessCount = {};
1262
+ cwData.sessions.filter(s => s.status === 'active').forEach(s => { sessCount[s.account] = (sessCount[s.account] || 0) + 1; });
1263
+
1264
+ el.innerHTML = '<div class="crew-grid">' + accounts.map(a => {
1265
+ const status = a.authenticated ? 'online' : 'offline';
1266
+ return `<div class="crew-card">
1267
+ <div class="crew-avatar ${status}">${a.name.charAt(0)}</div>
1268
+ <div>
1269
+ <div class="crew-name">${esc(a.name)}</div>
1270
+ <div class="crew-status ${status}">${a.authenticated ? 'Authenticated' : 'Not authenticated'}</div>
1271
+ <div class="crew-detail">${projCount[a.name] || 0} projects \u00B7 ${sessCount[a.name] || 0} active sessions</div>
1272
+ </div>
1273
+ </div>`;
1274
+ }).join('') + '</div>';
1275
+ }
1276
+
1277
+ // ── Render: Sectors ──────────────────────────────────────
1278
+ function renderSectors() {
1279
+ const el = document.getElementById('panel-sectors');
1280
+ let projects = cwData.projects;
1281
+ if (filters.account !== 'all') projects = projects.filter(p => p.account === filters.account);
1282
+ document.getElementById('tc-sectors').textContent = projects.length;
1283
+
1284
+ if (!projects.length) {
1285
+ el.innerHTML = '<div class="empty-state">No sectors mapped.<br>Register with <code>cw project add &lt;name&gt;</code></div>';
1286
+ return;
1287
+ }
1288
+
1289
+ projects.sort((a, b) => a.name.localeCompare(b.name));
1290
+
1291
+ el.innerHTML = '<div class="sector-grid">' + projects.map(p => {
1292
+ let badges = '';
1293
+ if (p.has_claude_md) badges += '<span class="badge md">MD</span>';
1294
+ if (p.agents > 0) badges += `<span class="badge ag">${p.agents} AG</span>`;
1295
+ if (p.commands > 0) badges += `<span class="badge cm">${p.commands} CM</span>`;
1296
+ if (p.has_mcp) badges += '<span class="badge mcp">MCP</span>';
1297
+ if (p.git) badges += '<span class="badge git">GIT</span>';
1298
+
1299
+ return `<div class="sector-card" data-acct="${esc(p.account)}">
1300
+ <div class="sector-name">${esc(p.name)}</div>
1301
+ <div class="sector-account">${esc(p.account)}</div>
1302
+ <div class="sector-badges">${badges}</div>
1303
+ </div>`;
1304
+ }).join('') + '</div>';
1305
+ }
1306
+
1307
+ // ── Render: Live Feed ────────────────────────────────────
1308
+ function renderFeed(newEvents) {
1309
+ const el = document.getElementById('panel-feed');
1310
+
1311
+ if (newEvents && newEvents.length) {
1312
+ // Apply filters
1313
+ const filtered = newEvents.filter(e => {
1314
+ if (filters.account !== 'all' && e.account !== filters.account) return false;
1315
+ if (filters.type !== 'all' && e.type !== filters.type) return false;
1316
+ return true;
1317
+ });
1318
+ feedEntries = [...filtered.reverse(), ...feedEntries].slice(0, MAX_FEED);
1319
+ document.getElementById('feed-dot').style.display = '';
1320
+ }
1321
+
1322
+ if (!feedEntries.length) {
1323
+ el.innerHTML = `<div class="empty-state">No live signals detected.<br>Run <code>cw arcade --setup</code> to install activity hooks,<br>then open a CW session to see live events here.</div>`;
1324
+ return;
1325
+ }
1326
+
1327
+ el.innerHTML = '<div class="feed-list">' + feedEntries.map((e, i) => {
1328
+ const ti = toolInfo(e.tool, e.event);
1329
+ const ts = new Date(e.ts * 1000);
1330
+ const t = ts.toTimeString().slice(0, 5);
1331
+ return `<div class="feed-item ${i < (newEvents?.length||0) ? 'fresh' : ''}">
1332
+ <span class="feed-time">${t}</span>
1333
+ <span class="tool-icon ${ti.cls}" style="width:20px;height:20px;font-size:10px">${ti.label}</span>
1334
+ <span class="feed-project">${esc(e.project || '')}</span>
1335
+ <span class="feed-task">${esc(e.task || '')}</span>
1336
+ <span class="feed-desc">${e.tool ? esc(e.tool) + ' ' : ''}${esc(e.summary || e.event || '')}</span>
1337
+ </div>`;
1338
+ }).join('') + '</div>';
1339
+
1340
+ // Remove fresh class after animation
1341
+ setTimeout(() => el.querySelectorAll('.fresh').forEach(e => e.classList.remove('fresh')), 800);
1342
+ }
1343
+
1344
+ // ── Render: Log ──────────────────────────────────────────
1345
+ function renderLog() {
1346
+ const el = document.getElementById('panel-log');
1347
+ const log = cwData.recent_log || [];
1348
+
1349
+ if (!log.length) {
1350
+ el.innerHTML = '<div class="empty-state">No transmissions recorded yet.</div>';
1351
+ return;
1352
+ }
1353
+
1354
+ const entries = log.slice().reverse().map(line => {
1355
+ const parts = line.split(' ');
1356
+ const time = (parts[0] || '').slice(11, 16) || parts[0]?.slice(0, 10) || '';
1357
+ const action = parts[1] || '';
1358
+ const rest = parts.slice(2).join(' ');
1359
+ return { time, action, rest };
1360
+ });
1361
+
1362
+ el.innerHTML = '<div class="log-list">' + entries.map(e =>
1363
+ `<div class="log-item">
1364
+ <span class="log-time">${esc(e.time)}</span>
1365
+ <span class="log-action ${e.action}">${esc(e.action)}</span>
1366
+ <span class="log-detail">${esc(e.rest)}</span>
1367
+ </div>`
1368
+ ).join('') + '</div>';
1369
+ }
1370
+
1371
+ // ── Render Current Tab ───────────────────────────────────
1372
+ function renderCurrentTab() {
1373
+ switch (currentTab) {
1374
+ case 'sessions': renderSessions(); break;
1375
+ case 'crew': renderCrew(); break;
1376
+ case 'sectors': renderSectors(); break;
1377
+ case 'feed': renderFeed(); break;
1378
+ case 'log': renderLog(); break;
1379
+ }
1380
+ }
1381
+
1382
+ // ── Data Fetching ────────────────────────────────────────
1383
+ async function fetchData() {
1384
+ try {
1385
+ const r = await fetch('/api/data');
1386
+ if (!r.ok) return;
1387
+ cwData = await r.json();
1388
+ renderStats(cwData.stats);
1389
+ buildFilters();
1390
+ renderCurrentTab();
1391
+ flashRefresh();
1392
+ } catch (e) {}
1393
+ }
1394
+
1395
+ async function fetchActivity() {
1396
+ try {
1397
+ const r = await fetch(`/api/activity?after=${lastActivityTs}&limit=60`);
1398
+ if (!r.ok) return;
1399
+ const data = await r.json();
1400
+ activityData = data;
1401
+ if (data.events.length) {
1402
+ lastActivityTs = data.events[data.events.length - 1].ts;
1403
+ if (currentTab === 'feed') renderFeed(data.events);
1404
+ if (currentTab === 'sessions') renderSessions();
1405
+ }
1406
+ } catch (e) {}
1407
+ }
1408
+
1409
+ // ── SSE: Real-time activity stream ──────────────────────
1410
+ function initSSE() {
1411
+ const es = new EventSource('/api/stream');
1412
+ es.onmessage = (msg) => {
1413
+ try {
1414
+ const entry = JSON.parse(msg.data);
1415
+
1416
+ // Add to activityData.events (keep last 200)
1417
+ activityData.events.push(entry);
1418
+ if (activityData.events.length > 200) activityData.events.shift();
1419
+
1420
+ // Update lastActivityTs
1421
+ if (entry.ts > lastActivityTs) lastActivityTs = entry.ts;
1422
+
1423
+ // Update live sessions (30s window)
1424
+ const now = Date.now() / 1000;
1425
+ const key = (entry.project || '') + '/' + (entry.task || '');
1426
+ const liveEntry = {
1427
+ project: entry.project || '',
1428
+ task: entry.task || '',
1429
+ type: entry.type || '',
1430
+ account: entry.account || '',
1431
+ session: entry.session || '',
1432
+ event: entry.event || '',
1433
+ tool: entry.tool || '',
1434
+ summary: entry.summary || '',
1435
+ ts: entry.ts,
1436
+ age: Math.round((now - entry.ts) * 10) / 10,
1437
+ };
1438
+ const idx = activityData.live.findIndex(l => (l.project + '/' + l.task) === key);
1439
+ if (idx >= 0) activityData.live[idx] = liveEntry;
1440
+ else activityData.live.push(liveEntry);
1441
+
1442
+ // Expire old live entries
1443
+ activityData.live = activityData.live.filter(l => (now - l.ts) < 30);
1444
+
1445
+ // Add to feed
1446
+ feedEntries.unshift(entry);
1447
+ if (feedEntries.length > MAX_FEED) feedEntries.pop();
1448
+
1449
+ // Update UI
1450
+ if (currentTab === 'sessions') renderSessions();
1451
+ if (currentTab === 'feed') renderFeed();
1452
+ beep(660, 25, 0.015);
1453
+ } catch (e) {}
1454
+ };
1455
+ es.onerror = () => {
1456
+ // Auto-reconnects after a few seconds (built into EventSource)
1457
+ };
1458
+ return es;
1459
+ }
1460
+
1461
+ function flashRefresh() {
1462
+ const el = document.getElementById('refresh-bar');
1463
+ el.classList.add('on');
1464
+ setTimeout(() => el.classList.remove('on'), 600);
1465
+ }
1466
+
1467
+ // ── Safety: force show app if boot hangs ─────────────────
1468
+ setTimeout(() => {
1469
+ const b = document.getElementById('boot');
1470
+ if (b) { b.classList.add('done'); setTimeout(() => b.remove(), 400); }
1471
+ document.getElementById('app').style.display = 'block';
1472
+ }, 2000);
1473
+
1474
+ // ── Init ─────────────────────────────────────────────────
1475
+ async function init() {
1476
+ try {
1477
+ initStarfield();
1478
+ } catch(e) { console.error('starfield:', e); }
1479
+
1480
+ tickClock();
1481
+ setInterval(tickClock, 1000);
1482
+ initTabs();
1483
+
1484
+ try {
1485
+ await boot();
1486
+ beep(880, 60); setTimeout(() => beep(1320, 80), 80);
1487
+ } catch(e) {
1488
+ console.error('boot:', e);
1489
+ const b = document.getElementById('boot');
1490
+ if (b) b.remove();
1491
+ document.getElementById('app').style.display = 'block';
1492
+ }
1493
+
1494
+ try {
1495
+ await fetchData();
1496
+ await fetchActivity();
1497
+ if (feedEntries.length === 0 && activityData.events.length) {
1498
+ feedEntries = activityData.events.slice().reverse().slice(0, MAX_FEED);
1499
+ }
1500
+ renderCurrentTab();
1501
+ } catch(e) { console.error('data:', e); }
1502
+
1503
+ setInterval(fetchData, 5000);
1504
+ initSSE();
1505
+ }
1506
+
1507
+ init().catch(e => {
1508
+ console.error('init failed:', e);
1509
+ const b = document.getElementById('boot');
1510
+ if (b) b.remove();
1511
+ document.getElementById('app').style.display = 'block';
1512
+ });
1513
+ </script>
1514
+ </body>
1515
+ </html>