@cluesmith/codev 1.4.1 → 1.4.3

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 (49) hide show
  1. package/dist/agent-farm/servers/dashboard-server.js +487 -9
  2. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  3. package/dist/agent-farm/servers/tower-server.js +141 -40
  4. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  5. package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
  6. package/dist/agent-farm/utils/port-registry.js +19 -5
  7. package/dist/agent-farm/utils/port-registry.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +2 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/adopt.d.ts.map +1 -1
  12. package/dist/commands/adopt.js +10 -0
  13. package/dist/commands/adopt.js.map +1 -1
  14. package/dist/commands/consult/index.d.ts +1 -0
  15. package/dist/commands/consult/index.d.ts.map +1 -1
  16. package/dist/commands/consult/index.js +56 -8
  17. package/dist/commands/consult/index.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +8 -0
  20. package/dist/commands/init.js.map +1 -1
  21. package/package.json +1 -1
  22. package/skeleton/resources/commands/consult.md +50 -0
  23. package/skeleton/templates/projectlist-archive.md +21 -0
  24. package/skeleton/templates/projectlist.md +17 -0
  25. package/templates/dashboard/css/activity.css +151 -0
  26. package/templates/dashboard/css/dialogs.css +149 -0
  27. package/templates/dashboard/css/files.css +530 -0
  28. package/templates/dashboard/css/layout.css +124 -0
  29. package/templates/dashboard/css/projects.css +501 -0
  30. package/templates/dashboard/css/statusbar.css +23 -0
  31. package/templates/dashboard/css/tabs.css +314 -0
  32. package/templates/dashboard/css/utilities.css +50 -0
  33. package/templates/dashboard/css/variables.css +45 -0
  34. package/templates/dashboard/index.html +158 -0
  35. package/templates/dashboard/js/activity.js +238 -0
  36. package/templates/dashboard/js/dialogs.js +328 -0
  37. package/templates/dashboard/js/files.js +436 -0
  38. package/templates/dashboard/js/main.js +487 -0
  39. package/templates/dashboard/js/projects.js +544 -0
  40. package/templates/dashboard/js/state.js +91 -0
  41. package/templates/dashboard/js/tabs.js +500 -0
  42. package/templates/dashboard/js/utils.js +57 -0
  43. package/templates/tower.html +172 -4
  44. package/dist/commands/eject.d.ts +0 -18
  45. package/dist/commands/eject.d.ts.map +0 -1
  46. package/dist/commands/eject.js +0 -149
  47. package/dist/commands/eject.js.map +0 -1
  48. package/templates/dashboard-split.html +0 -3721
  49. package/templates/dashboard.html +0 -149
@@ -1,3721 +0,0 @@
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>AF: {{PROJECT_NAME}}</title>
7
- <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
-
14
- :root {
15
- --bg-primary: #1a1a1a;
16
- --bg-secondary: #252525;
17
- --bg-tertiary: #2a2a2a;
18
- --border: #333;
19
- --text-primary: #fff;
20
- --text-secondary: #ccc;
21
- --text-muted: #666;
22
- --accent: #3b82f6;
23
- --tab-active: #333;
24
- --tab-hover: #2a2a2a;
25
- /* Status indicator colors per spec 0019 */
26
- --status-active: #22c55e; /* Green: spawning, implementing */
27
- --status-waiting: #eab308; /* Yellow: pr-ready (waiting for review) */
28
- --status-error: #ef4444; /* Red: blocked */
29
- --status-complete: #9e9e9e; /* Gray: complete */
30
- /* Project lifecycle status colors per spec 0045 */
31
- --project-conceived: #eab308; /* Yellow */
32
- --project-specified: #3b82f6; /* Blue */
33
- --project-planned: #3b82f6; /* Blue */
34
- --project-implementing: #f97316; /* Orange */
35
- --project-implemented: #a855f7; /* Purple */
36
- --project-committed: #22c55e; /* Green */
37
- --project-integrated: #9e9e9e; /* Gray */
38
- --project-abandoned: #ef4444; /* Red */
39
- --project-on-hold: #9e9e9e; /* Gray */
40
- }
41
-
42
- body {
43
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
- background: var(--bg-primary);
45
- color: var(--text-primary);
46
- height: 100vh;
47
- display: flex;
48
- flex-direction: column;
49
- overflow: hidden;
50
- }
51
-
52
- /* Header */
53
- .header {
54
- display: flex;
55
- justify-content: space-between;
56
- align-items: center;
57
- padding: 12px 16px;
58
- background: var(--bg-secondary);
59
- border-bottom: 1px solid var(--border);
60
- }
61
-
62
- .header h1 {
63
- font-size: 16px;
64
- font-weight: 600;
65
- }
66
-
67
- .header-actions {
68
- display: flex;
69
- gap: 8px;
70
- }
71
-
72
- .btn {
73
- padding: 6px 12px;
74
- border-radius: 4px;
75
- border: 1px solid var(--border);
76
- background: var(--bg-tertiary);
77
- color: var(--text-secondary);
78
- cursor: pointer;
79
- font-size: 13px;
80
- }
81
-
82
- .btn:hover {
83
- background: var(--tab-active);
84
- }
85
-
86
- .btn-danger {
87
- border-color: #ef4444;
88
- color: #ef4444;
89
- }
90
-
91
- .btn-danger:hover {
92
- background: rgba(239, 68, 68, 0.1);
93
- }
94
-
95
- /* Main content area */
96
- .main {
97
- display: flex;
98
- flex: 1;
99
- overflow: hidden;
100
- }
101
-
102
- /* Left pane - Architect */
103
- .left-pane {
104
- width: 50%;
105
- min-width: 20%;
106
- max-width: 80%;
107
- resize: horizontal;
108
- overflow: auto;
109
- border-right: 1px solid var(--border);
110
- display: flex;
111
- flex-direction: column;
112
- }
113
-
114
- .pane-header {
115
- padding: 8px 12px;
116
- background: var(--bg-secondary);
117
- border-bottom: 1px solid var(--border);
118
- font-size: 12px;
119
- color: var(--text-muted);
120
- text-transform: uppercase;
121
- letter-spacing: 0.5px;
122
- display: flex;
123
- align-items: center;
124
- gap: 6px;
125
- }
126
-
127
- .pane-header .status-dot {
128
- width: 8px;
129
- height: 8px;
130
- border-radius: 50%;
131
- background: var(--status-active);
132
- }
133
-
134
- .pane-header .status-dot.inactive {
135
- background: var(--text-muted);
136
- }
137
-
138
- #architect-content {
139
- flex: 1;
140
- display: flex;
141
- flex-direction: column;
142
- }
143
-
144
- .left-pane iframe {
145
- flex: 1;
146
- width: 100%;
147
- border: none;
148
- background: #000;
149
- }
150
-
151
- .architect-placeholder {
152
- flex: 1;
153
- display: flex;
154
- flex-direction: column;
155
- align-items: center;
156
- justify-content: center;
157
- color: var(--text-muted);
158
- gap: 16px;
159
- }
160
-
161
- .architect-placeholder code {
162
- background: var(--bg-tertiary);
163
- padding: 4px 8px;
164
- border-radius: 4px;
165
- font-size: 13px;
166
- }
167
-
168
- /* Right pane - Tabs */
169
- .right-pane {
170
- width: 50%;
171
- display: flex;
172
- flex-direction: column;
173
- }
174
-
175
- /* Tab bar */
176
- .tab-bar {
177
- display: flex;
178
- align-items: center;
179
- background: var(--bg-secondary);
180
- border-bottom: 1px solid var(--border);
181
- min-height: 40px;
182
- overflow: visible; /* Allow overflow menu dropdown to be visible */
183
- position: relative; /* Position context for overflow menu */
184
- }
185
-
186
- .tabs-scroll {
187
- display: flex;
188
- overflow-x: auto;
189
- flex: 1;
190
- scrollbar-width: none;
191
- }
192
-
193
- .tabs-scroll::-webkit-scrollbar {
194
- display: none;
195
- }
196
-
197
- .tab {
198
- display: flex;
199
- align-items: center;
200
- gap: 6px;
201
- padding: 8px 12px;
202
- cursor: pointer;
203
- border-right: 1px solid var(--border);
204
- border-bottom: 2px solid transparent; /* Reserve space for active indicator */
205
- white-space: nowrap;
206
- flex-shrink: 0;
207
- position: relative;
208
- }
209
-
210
- .tab:hover {
211
- background: var(--tab-hover);
212
- }
213
-
214
- .tab.active {
215
- background: var(--bg-tertiary);
216
- border-bottom: 2px solid var(--accent); /* Blue accent line */
217
- }
218
-
219
- .tab.new-tab {
220
- animation: tab-pulse 0.5s ease-out;
221
- }
222
-
223
- @keyframes tab-pulse {
224
- 0% { background: var(--accent); }
225
- 100% { background: var(--tab-active); }
226
- }
227
-
228
- .tab .icon {
229
- font-size: 14px;
230
- }
231
-
232
- .tab .name {
233
- font-size: 13px;
234
- max-width: 120px;
235
- overflow: hidden;
236
- text-overflow: ellipsis;
237
- color: var(--text-secondary);
238
- }
239
-
240
- .tab.active .name {
241
- color: var(--text-primary);
242
- }
243
-
244
- .tab .status-dot {
245
- width: 6px;
246
- height: 6px;
247
- border-radius: 50%;
248
- }
249
-
250
- /* Shape modifiers for accessibility (not just color) */
251
- .tab .status-dot--diamond {
252
- border-radius: 1px;
253
- transform: rotate(45deg);
254
- }
255
-
256
- /* Ring shape for pr-ready (accessibility: distinct from circle) */
257
- .tab .status-dot--ring {
258
- box-shadow: inset 0 0 0 1.5px currentColor;
259
- background: transparent !important;
260
- color: var(--status-waiting);
261
- }
262
-
263
- /* Distinct animations per status category (spec 0019) */
264
- @keyframes status-pulse {
265
- /* Pulsing: Active/working (spawning, implementing) */
266
- 0%, 100% { opacity: 1; transform: scale(1); }
267
- 50% { opacity: 0.7; transform: scale(0.9); }
268
- }
269
-
270
- @keyframes status-blink-slow {
271
- /* Slow blink: Idle/waiting (pr-ready) */
272
- 0%, 100% { opacity: 1; }
273
- 50% { opacity: 0.3; }
274
- }
275
-
276
- @keyframes status-blink-fast {
277
- /* Fast blink: Error/blocked */
278
- 0%, 100% { opacity: 1; }
279
- 50% { opacity: 0.2; }
280
- }
281
-
282
- .tab .status-dot--pulse {
283
- animation: status-pulse 2s ease-in-out infinite;
284
- }
285
-
286
- .tab .status-dot--blink-slow {
287
- animation: status-blink-slow 3s ease-in-out infinite;
288
- }
289
-
290
- .tab .status-dot--blink-fast {
291
- animation: status-blink-fast 0.8s ease-in-out infinite;
292
- }
293
-
294
- /* Respect reduced motion preference (WCAG 2.3.3) */
295
- /* Motion-independent differentiators remain: diamond for blocked, ring for pr-ready */
296
- @media (prefers-reduced-motion: reduce) {
297
- .tab .status-dot--pulse,
298
- .tab .status-dot--blink-slow,
299
- .tab .status-dot--blink-fast {
300
- animation: none;
301
- }
302
- }
303
-
304
- .tab .close {
305
- opacity: 0.6; /* Always clearly visible */
306
- margin-left: 6px;
307
- font-size: 16px;
308
- font-weight: 500;
309
- color: var(--text-secondary);
310
- padding: 4px 8px;
311
- border-radius: 4px;
312
- cursor: pointer;
313
- line-height: 1;
314
- min-width: 24px;
315
- min-height: 24px;
316
- display: flex;
317
- align-items: center;
318
- justify-content: center;
319
- }
320
-
321
- .tab:hover .close {
322
- opacity: 0.9;
323
- }
324
-
325
- .tab .close:hover {
326
- opacity: 1;
327
- background: rgba(239, 68, 68, 0.2); /* Red tint on hover */
328
- color: #ef4444;
329
- }
330
-
331
- /* Add buttons */
332
- .add-buttons {
333
- display: flex;
334
- gap: 4px;
335
- padding: 0 8px;
336
- flex-shrink: 0;
337
- }
338
-
339
- .add-btn {
340
- padding: 4px 8px;
341
- border-radius: 4px;
342
- border: 1px dashed var(--border);
343
- background: transparent;
344
- color: var(--text-muted);
345
- cursor: pointer;
346
- font-size: 12px;
347
- display: flex;
348
- align-items: center;
349
- gap: 4px;
350
- }
351
-
352
- .add-btn:hover {
353
- border-style: solid;
354
- color: var(--text-secondary);
355
- background: var(--bg-tertiary);
356
- }
357
-
358
- /* Overflow indicator */
359
- .overflow-btn {
360
- padding: 8px 12px;
361
- background: var(--bg-tertiary);
362
- border: none;
363
- border-left: 1px solid var(--border);
364
- color: var(--text-secondary);
365
- cursor: pointer;
366
- display: none; /* Hidden by default, shown via JS */
367
- align-items: center;
368
- gap: 4px;
369
- flex-shrink: 0;
370
- }
371
-
372
- .overflow-btn:hover {
373
- background: var(--tab-hover);
374
- }
375
-
376
- .overflow-btn:focus {
377
- outline: 2px solid var(--accent);
378
- outline-offset: -2px;
379
- }
380
-
381
- .overflow-count {
382
- font-size: 11px;
383
- background: var(--accent);
384
- color: white;
385
- padding: 1px 5px;
386
- border-radius: 8px;
387
- }
388
-
389
- /* Overflow menu dropdown */
390
- .overflow-menu {
391
- position: absolute;
392
- right: 0;
393
- top: 100%;
394
- background: var(--bg-secondary);
395
- border: 1px solid var(--border);
396
- border-radius: 4px;
397
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
398
- max-height: 300px;
399
- overflow-y: auto;
400
- min-width: 200px;
401
- z-index: 100;
402
- }
403
-
404
- .overflow-menu.hidden {
405
- display: none;
406
- }
407
-
408
- .overflow-menu-item {
409
- padding: 8px 12px;
410
- cursor: pointer;
411
- display: flex;
412
- align-items: center;
413
- gap: 8px;
414
- font-size: 13px;
415
- }
416
-
417
- .overflow-menu-item:hover,
418
- .overflow-menu-item:focus {
419
- background: var(--tab-hover);
420
- outline: none;
421
- }
422
-
423
- .overflow-menu-item.active {
424
- background: var(--tab-active);
425
- border-left: 2px solid var(--accent);
426
- }
427
-
428
- .overflow-menu-item .icon {
429
- font-size: 14px;
430
- }
431
-
432
- .overflow-menu-item .name {
433
- flex: 1;
434
- overflow: hidden;
435
- text-overflow: ellipsis;
436
- white-space: nowrap;
437
- }
438
-
439
- .overflow-menu-item .open-external {
440
- opacity: 0.5;
441
- cursor: pointer;
442
- padding: 2px 6px;
443
- font-size: 12px;
444
- border-radius: 3px;
445
- }
446
-
447
- .overflow-menu-item .open-external:hover {
448
- opacity: 1;
449
- background: rgba(255, 255, 255, 0.1);
450
- }
451
-
452
- /* Tab content */
453
- .tab-content {
454
- flex: 1;
455
- display: flex;
456
- flex-direction: column;
457
- overflow: hidden;
458
- }
459
-
460
- .tab-content iframe {
461
- flex: 1;
462
- width: 100%;
463
- border: none;
464
- background: #000;
465
- }
466
-
467
- .empty-state {
468
- flex: 1;
469
- display: flex;
470
- flex-direction: column;
471
- align-items: center;
472
- justify-content: center;
473
- color: var(--text-muted);
474
- gap: 12px;
475
- }
476
-
477
- .empty-state .hint {
478
- font-size: 13px;
479
- text-align: center;
480
- max-width: 300px;
481
- }
482
-
483
- /* Status bar */
484
- .status-bar {
485
- padding: 8px 16px;
486
- background: var(--bg-secondary);
487
- border-top: 1px solid var(--border);
488
- font-size: 12px;
489
- color: var(--text-muted);
490
- display: flex;
491
- gap: 16px;
492
- }
493
-
494
- .status-item {
495
- display: flex;
496
- align-items: center;
497
- gap: 6px;
498
- }
499
-
500
- .status-item .dot {
501
- width: 6px;
502
- height: 6px;
503
- border-radius: 50%;
504
- }
505
-
506
- /* Dialogs */
507
- .dialog-overlay {
508
- position: fixed;
509
- top: 0;
510
- left: 0;
511
- right: 0;
512
- bottom: 0;
513
- background: rgba(0, 0, 0, 0.6);
514
- display: flex;
515
- align-items: center;
516
- justify-content: center;
517
- z-index: 1000;
518
- }
519
-
520
- .dialog-overlay.hidden {
521
- display: none;
522
- }
523
-
524
- .dialog {
525
- background: var(--bg-secondary);
526
- border: 1px solid var(--border);
527
- border-radius: 8px;
528
- padding: 20px;
529
- min-width: 320px;
530
- max-width: 90%;
531
- }
532
-
533
- .dialog h3 {
534
- margin-bottom: 16px;
535
- font-size: 16px;
536
- font-weight: 500;
537
- }
538
-
539
- .dialog input {
540
- width: 100%;
541
- padding: 8px 12px;
542
- border-radius: 4px;
543
- border: 1px solid var(--border);
544
- background: var(--bg-tertiary);
545
- color: var(--text-primary);
546
- font-size: 14px;
547
- margin-bottom: 16px;
548
- }
549
-
550
- .dialog input:focus {
551
- outline: none;
552
- border-color: var(--accent);
553
- }
554
-
555
- .dialog-actions {
556
- display: flex;
557
- justify-content: flex-end;
558
- gap: 8px;
559
- }
560
-
561
- .quick-paths {
562
- display: flex;
563
- flex-wrap: wrap;
564
- gap: 8px;
565
- margin-bottom: 12px;
566
- }
567
-
568
- .quick-path {
569
- padding: 4px 8px;
570
- border-radius: 4px;
571
- background: var(--bg-tertiary);
572
- border: 1px solid var(--border);
573
- color: var(--text-secondary);
574
- cursor: pointer;
575
- font-size: 12px;
576
- }
577
-
578
- .quick-path:hover {
579
- background: var(--tab-hover);
580
- border-color: var(--accent);
581
- }
582
-
583
- /* Toast notifications */
584
- .toast-container {
585
- position: fixed;
586
- bottom: 60px;
587
- right: 16px;
588
- z-index: 2000;
589
- display: flex;
590
- flex-direction: column;
591
- gap: 8px;
592
- }
593
-
594
- .toast {
595
- padding: 12px 16px;
596
- background: var(--bg-secondary);
597
- border: 1px solid var(--border);
598
- border-radius: 6px;
599
- font-size: 13px;
600
- display: flex;
601
- align-items: center;
602
- gap: 8px;
603
- animation: toast-in 0.3s ease-out;
604
- }
605
-
606
- .toast.error {
607
- border-color: #ef4444;
608
- }
609
-
610
- .toast.success {
611
- border-color: #22c55e;
612
- }
613
-
614
- @keyframes toast-in {
615
- from {
616
- opacity: 0;
617
- transform: translateY(10px);
618
- }
619
- to {
620
- opacity: 1;
621
- transform: translateY(0);
622
- }
623
- }
624
-
625
- /* Context menu */
626
- .context-menu {
627
- position: fixed;
628
- background: var(--bg-secondary);
629
- border: 1px solid var(--border);
630
- border-radius: 4px;
631
- padding: 4px 0;
632
- min-width: 150px;
633
- z-index: 1000;
634
- }
635
-
636
- .context-menu.hidden {
637
- display: none;
638
- }
639
-
640
- .context-menu-item {
641
- padding: 8px 12px;
642
- cursor: pointer;
643
- font-size: 13px;
644
- }
645
-
646
- .context-menu-item:hover {
647
- background: var(--tab-hover);
648
- }
649
-
650
- .context-menu-item.danger {
651
- color: #ef4444;
652
- }
653
-
654
- /* Projects Tab Styles (Spec 0045) */
655
- .projects-container {
656
- flex: 1;
657
- overflow-y: auto;
658
- padding: 16px;
659
- display: flex;
660
- flex-direction: column;
661
- gap: 16px;
662
- }
663
-
664
- /* Welcome Screen */
665
- .projects-welcome {
666
- max-width: 600px;
667
- margin: 40px auto;
668
- text-align: center;
669
- }
670
-
671
- .projects-welcome h2 {
672
- font-size: 24px;
673
- margin-bottom: 16px;
674
- color: var(--text-primary);
675
- }
676
-
677
- .projects-welcome p {
678
- color: var(--text-secondary);
679
- line-height: 1.6;
680
- margin-bottom: 16px;
681
- }
682
-
683
- .projects-welcome ol {
684
- text-align: left;
685
- margin: 24px 0;
686
- padding-left: 24px;
687
- }
688
-
689
- .projects-welcome li {
690
- margin-bottom: 8px;
691
- color: var(--text-secondary);
692
- }
693
-
694
- .projects-welcome li strong {
695
- color: var(--text-primary);
696
- }
697
-
698
- .projects-welcome .quick-tip {
699
- margin-top: 24px;
700
- padding: 12px;
701
- background: var(--bg-tertiary);
702
- border-radius: 6px;
703
- border-left: 3px solid var(--accent);
704
- color: var(--text-secondary);
705
- }
706
-
707
- .projects-welcome hr {
708
- border: none;
709
- border-top: 1px solid var(--border);
710
- margin: 24px 0;
711
- }
712
-
713
- /* Status Summary */
714
- .status-summary {
715
- background: var(--bg-secondary);
716
- border: 1px solid var(--border);
717
- border-radius: 6px;
718
- padding: 12px 16px;
719
- }
720
-
721
- .status-summary-header {
722
- display: flex;
723
- justify-content: space-between;
724
- align-items: center;
725
- margin-bottom: 8px;
726
- }
727
-
728
- .status-summary-header span {
729
- font-size: 11px;
730
- text-transform: uppercase;
731
- letter-spacing: 0.5px;
732
- color: var(--text-muted);
733
- }
734
-
735
- .status-summary-header button {
736
- padding: 4px 8px;
737
- border-radius: 4px;
738
- border: 1px solid var(--border);
739
- background: var(--bg-tertiary);
740
- color: var(--text-secondary);
741
- cursor: pointer;
742
- font-size: 14px;
743
- }
744
-
745
- .status-summary-header button:hover {
746
- background: var(--tab-hover);
747
- }
748
-
749
- .status-summary .active-projects {
750
- margin-bottom: 8px;
751
- }
752
-
753
- .status-summary .active-count {
754
- font-size: 14px;
755
- color: var(--text-primary);
756
- }
757
-
758
- .status-summary .active-list {
759
- margin-top: 4px;
760
- padding-left: 16px;
761
- font-size: 13px;
762
- color: var(--text-secondary);
763
- }
764
-
765
- .status-summary .active-list li {
766
- margin: 2px 0;
767
- }
768
-
769
- .status-summary .completed {
770
- font-size: 13px;
771
- color: var(--text-muted);
772
- }
773
-
774
- /* Kanban Grid */
775
- .kanban-grid {
776
- width: 100%;
777
- border-collapse: collapse;
778
- font-size: 13px;
779
- }
780
-
781
- .kanban-grid th,
782
- .kanban-grid td {
783
- padding: 8px 6px;
784
- text-align: center;
785
- border-bottom: 1px solid var(--border);
786
- }
787
-
788
- .kanban-grid th {
789
- background: var(--bg-secondary);
790
- font-size: 10px;
791
- text-transform: uppercase;
792
- letter-spacing: 0.5px;
793
- color: var(--text-muted);
794
- position: sticky;
795
- top: 0;
796
- z-index: 1;
797
- }
798
-
799
- .kanban-grid th:first-child,
800
- .kanban-grid td:first-child {
801
- text-align: left;
802
- padding-left: 12px;
803
- width: 40%;
804
- }
805
-
806
- .kanban-grid th:not(:first-child),
807
- .kanban-grid td:not(:first-child) {
808
- width: 8%;
809
- }
810
-
811
- .kanban-grid tbody tr {
812
- cursor: default;
813
- transition: background 0.15s;
814
- }
815
-
816
- .kanban-grid tbody tr:hover {
817
- background: var(--bg-secondary);
818
- }
819
-
820
- .kanban-grid tbody tr:focus {
821
- outline: 2px solid var(--accent);
822
- outline-offset: -2px;
823
- }
824
-
825
- .kanban-grid .project-cell {
826
- display: flex;
827
- align-items: center;
828
- gap: 8px;
829
- }
830
-
831
- .kanban-grid .project-id {
832
- font-family: monospace;
833
- color: var(--text-muted);
834
- }
835
-
836
- .kanban-grid .project-title {
837
- overflow: hidden;
838
- text-overflow: ellipsis;
839
- white-space: nowrap;
840
- }
841
-
842
- .kanban-grid .project-cell.clickable {
843
- cursor: pointer;
844
- }
845
-
846
- .kanban-grid .project-cell.clickable:hover .project-title {
847
- text-decoration: underline;
848
- color: var(--accent);
849
- }
850
-
851
- .kanban-grid .tick-badge {
852
- font-size: 10px;
853
- padding: 1px 4px;
854
- background: var(--bg-tertiary);
855
- border-radius: 3px;
856
- color: var(--text-muted);
857
- }
858
-
859
- /* Stage cell styling */
860
- .stage-cell {
861
- font-size: 12px;
862
- position: relative;
863
- }
864
-
865
- .stage-cell .checkmark {
866
- color: #22c55e;
867
- font-weight: bold;
868
- }
869
-
870
- .stage-cell .current-indicator {
871
- display: inline-block;
872
- width: 12px;
873
- height: 12px;
874
- border: 2px solid #f97316;
875
- border-radius: 50%;
876
- }
877
-
878
- .stage-cell .celebration {
879
- font-size: 16px;
880
- }
881
-
882
- .stage-cell a {
883
- color: var(--text-primary);
884
- text-decoration: underline;
885
- }
886
-
887
- /* Arrow between columns */
888
- .kanban-grid th:not(:first-child):not(:last-child)::after,
889
- .kanban-grid td.stage-cell:not(:last-child)::after {
890
- content: '→';
891
- position: absolute;
892
- right: -8px;
893
- color: var(--text-muted);
894
- font-size: 10px;
895
- }
896
-
897
- /* Projects info header */
898
- .projects-info {
899
- background: var(--bg-secondary);
900
- border: 1px solid var(--border);
901
- border-radius: 6px;
902
- padding: 12px 16px;
903
- margin-bottom: 12px;
904
- }
905
-
906
- .projects-info p {
907
- color: var(--text-secondary);
908
- font-size: 13px;
909
- margin: 0 0 8px 0;
910
- }
911
-
912
- .projects-info p:last-child {
913
- margin-bottom: 0;
914
- }
915
-
916
- .projects-info strong {
917
- color: var(--text-primary);
918
- }
919
-
920
- .projects-info a {
921
- color: var(--accent);
922
- text-decoration: none;
923
- }
924
-
925
- .projects-info a:hover {
926
- text-decoration: underline;
927
- }
928
-
929
- /* Project details row */
930
- .project-details-row td {
931
- padding: 0 !important;
932
- border-bottom: 1px solid var(--border);
933
- }
934
-
935
- .project-details-content {
936
- padding: 16px;
937
- background: var(--bg-secondary);
938
- }
939
-
940
- .project-details-content h3 {
941
- font-size: 16px;
942
- margin-bottom: 8px;
943
- color: var(--text-primary);
944
- }
945
-
946
- .project-details-content p {
947
- margin-bottom: 8px;
948
- color: var(--text-secondary);
949
- font-size: 13px;
950
- }
951
-
952
- .project-details-content .notes {
953
- font-style: italic;
954
- color: var(--text-muted);
955
- }
956
-
957
- .project-details-links {
958
- display: flex;
959
- gap: 8px;
960
- margin-top: 12px;
961
- }
962
-
963
- .project-details-links a {
964
- padding: 4px 10px;
965
- background: var(--bg-tertiary);
966
- border: 1px solid var(--border);
967
- border-radius: 4px;
968
- color: var(--text-secondary);
969
- text-decoration: none;
970
- font-size: 12px;
971
- }
972
-
973
- .project-details-links a:hover {
974
- background: var(--tab-hover);
975
- color: var(--text-primary);
976
- }
977
-
978
- .project-dependencies {
979
- margin-top: 8px;
980
- font-size: 12px;
981
- color: var(--text-muted);
982
- }
983
-
984
- .project-ticks {
985
- margin-top: 8px;
986
- font-size: 12px;
987
- display: flex;
988
- align-items: center;
989
- gap: 6px;
990
- flex-wrap: wrap;
991
- }
992
-
993
- .project-ticks .tick-badge {
994
- background: #238636;
995
- color: white;
996
- padding: 2px 6px;
997
- border-radius: 3px;
998
- font-size: 11px;
999
- }
1000
-
1001
- /* Collapsible project sections */
1002
- .project-section {
1003
- border: 1px solid var(--border);
1004
- border-radius: 6px;
1005
- background: var(--bg-secondary);
1006
- margin-bottom: 12px;
1007
- }
1008
-
1009
- .project-section summary {
1010
- padding: 12px 16px;
1011
- cursor: pointer;
1012
- font-size: 14px;
1013
- font-weight: 500;
1014
- color: var(--text-primary);
1015
- display: flex;
1016
- align-items: center;
1017
- gap: 8px;
1018
- user-select: none;
1019
- }
1020
-
1021
- .project-section summary:hover {
1022
- background: var(--bg-tertiary);
1023
- }
1024
-
1025
- .project-section summary::marker {
1026
- content: '';
1027
- }
1028
-
1029
- .project-section summary::before {
1030
- content: '▶';
1031
- font-size: 10px;
1032
- transition: transform 0.2s;
1033
- color: var(--text-muted);
1034
- }
1035
-
1036
- .project-section[open] summary::before {
1037
- transform: rotate(90deg);
1038
- }
1039
-
1040
- .project-section .section-count {
1041
- font-size: 12px;
1042
- color: var(--text-muted);
1043
- font-weight: normal;
1044
- }
1045
-
1046
- .project-section .kanban-grid {
1047
- margin: 0;
1048
- border-radius: 0 0 6px 6px;
1049
- }
1050
-
1051
- /* Terminal projects section */
1052
- .terminal-projects {
1053
- margin-top: 16px;
1054
- border: 1px solid var(--border);
1055
- border-radius: 6px;
1056
- background: var(--bg-secondary);
1057
- }
1058
-
1059
- .terminal-projects summary {
1060
- padding: 12px 16px;
1061
- cursor: pointer;
1062
- font-size: 13px;
1063
- color: var(--text-muted);
1064
- display: flex;
1065
- align-items: center;
1066
- gap: 8px;
1067
- }
1068
-
1069
- .terminal-projects summary:hover {
1070
- background: var(--bg-tertiary);
1071
- }
1072
-
1073
- .terminal-projects summary::marker {
1074
- content: '';
1075
- }
1076
-
1077
- .terminal-projects summary::before {
1078
- content: '▶';
1079
- font-size: 10px;
1080
- transition: transform 0.2s;
1081
- color: var(--text-muted);
1082
- }
1083
-
1084
- .terminal-projects[open] summary::before {
1085
- transform: rotate(90deg);
1086
- }
1087
-
1088
- .terminal-projects ul {
1089
- list-style: none;
1090
- padding: 0 16px 16px;
1091
- }
1092
-
1093
- .terminal-projects li {
1094
- padding: 8px 0;
1095
- border-bottom: 1px solid var(--border);
1096
- display: flex;
1097
- gap: 8px;
1098
- align-items: center;
1099
- }
1100
-
1101
- .terminal-projects li:last-child {
1102
- border-bottom: none;
1103
- }
1104
-
1105
- .terminal-projects .project-abandoned {
1106
- color: var(--project-abandoned);
1107
- text-decoration: line-through;
1108
- }
1109
-
1110
- .terminal-projects .project-on-hold {
1111
- color: var(--project-on-hold);
1112
- font-style: italic;
1113
- }
1114
-
1115
- /* Error banner */
1116
- .projects-error {
1117
- padding: 16px;
1118
- background: rgba(239, 68, 68, 0.1);
1119
- border: 1px solid var(--status-error);
1120
- border-radius: 6px;
1121
- display: flex;
1122
- align-items: center;
1123
- gap: 12px;
1124
- }
1125
-
1126
- .projects-error-message {
1127
- flex: 1;
1128
- color: var(--text-secondary);
1129
- }
1130
-
1131
- .projects-error button {
1132
- padding: 6px 12px;
1133
- background: var(--bg-tertiary);
1134
- border: 1px solid var(--border);
1135
- border-radius: 4px;
1136
- color: var(--text-secondary);
1137
- cursor: pointer;
1138
- }
1139
-
1140
- .projects-error button:hover {
1141
- background: var(--tab-hover);
1142
- }
1143
-
1144
- /* Stage link styling */
1145
- .stage-link {
1146
- text-decoration: none;
1147
- color: inherit;
1148
- cursor: pointer;
1149
- }
1150
-
1151
- .stage-link:hover .stage-indicator {
1152
- transform: scale(1.2);
1153
- }
1154
-
1155
- /* Projects tab without close button */
1156
- .tab.tab-uncloseable .close {
1157
- display: none;
1158
- }
1159
-
1160
- /* Tree Styles (used by dashboard file browser) */
1161
- .tree-item {
1162
- display: flex;
1163
- align-items: center;
1164
- padding: 4px 8px;
1165
- cursor: pointer;
1166
- user-select: none;
1167
- }
1168
-
1169
- .tree-item:hover {
1170
- background: var(--bg-secondary);
1171
- }
1172
-
1173
- .tree-item.selected {
1174
- background: var(--tab-active);
1175
- }
1176
-
1177
- .tree-item-icon {
1178
- width: 16px;
1179
- height: 16px;
1180
- margin-right: 4px;
1181
- display: flex;
1182
- align-items: center;
1183
- justify-content: center;
1184
- font-size: 10px;
1185
- color: var(--text-muted);
1186
- }
1187
-
1188
- .tree-item-icon.folder-toggle {
1189
- cursor: pointer;
1190
- }
1191
-
1192
- .tree-item-icon.folder-toggle:hover {
1193
- color: var(--text-secondary);
1194
- }
1195
-
1196
- .tree-item-name {
1197
- font-size: 13px;
1198
- color: var(--text-secondary);
1199
- overflow: hidden;
1200
- text-overflow: ellipsis;
1201
- white-space: nowrap;
1202
- }
1203
-
1204
- .tree-item:hover .tree-item-name {
1205
- color: var(--text-primary);
1206
- }
1207
-
1208
- .tree-item[data-type="dir"] .tree-item-name {
1209
- color: var(--text-primary);
1210
- }
1211
-
1212
- .tree-item[data-type="file"]:hover .tree-item-name {
1213
- color: var(--accent);
1214
- }
1215
-
1216
- .tree-children {
1217
- overflow: hidden;
1218
- }
1219
-
1220
- .tree-children.collapsed {
1221
- display: none;
1222
- }
1223
-
1224
- /* Dashboard Tab Styles (Spec 0057) */
1225
- .dashboard-container {
1226
- flex: 1;
1227
- overflow-y: auto;
1228
- display: flex;
1229
- flex-direction: column;
1230
- }
1231
-
1232
- .dashboard-header {
1233
- display: flex;
1234
- gap: 16px;
1235
- padding: 16px;
1236
- flex-shrink: 0;
1237
- }
1238
-
1239
- @media (max-width: 900px) {
1240
- .dashboard-header {
1241
- flex-direction: column;
1242
- }
1243
- }
1244
-
1245
- /* Collapsible section styles */
1246
- .dashboard-section {
1247
- background: var(--bg-secondary);
1248
- border: 1px solid var(--border);
1249
- border-radius: 8px;
1250
- overflow: hidden;
1251
- display: flex;
1252
- flex-direction: column;
1253
- }
1254
-
1255
- .dashboard-section.section-tabs,
1256
- .dashboard-section.section-files {
1257
- flex: 1;
1258
- max-height: 280px;
1259
- }
1260
-
1261
- .dashboard-section.section-projects {
1262
- flex: 0 0 auto;
1263
- margin: 0 16px 16px 16px;
1264
- max-height: 50%;
1265
- overflow-y: auto;
1266
- }
1267
-
1268
- .dashboard-section.section-projects .dashboard-section-content {
1269
- flex: 0 0 auto;
1270
- }
1271
-
1272
- /* Tabs/Files expand to fill remaining space above Projects */
1273
- .dashboard-header {
1274
- flex: 1;
1275
- min-height: 0;
1276
- }
1277
-
1278
- .dashboard-section.section-tabs,
1279
- .dashboard-section.section-files {
1280
- max-height: none;
1281
- }
1282
-
1283
- .dashboard-section-header {
1284
- display: flex;
1285
- justify-content: space-between;
1286
- align-items: center;
1287
- padding: 8px 12px;
1288
- cursor: pointer;
1289
- user-select: none;
1290
- flex-shrink: 0;
1291
- border-bottom: 1px solid var(--border);
1292
- }
1293
-
1294
- .dashboard-section-header:hover {
1295
- background: var(--bg-tertiary);
1296
- }
1297
-
1298
- .dashboard-section-header h3 {
1299
- font-size: 12px;
1300
- text-transform: uppercase;
1301
- color: var(--text-muted);
1302
- letter-spacing: 0.5px;
1303
- margin: 0;
1304
- display: flex;
1305
- align-items: center;
1306
- gap: 6px;
1307
- }
1308
-
1309
- .dashboard-section-header .collapse-icon {
1310
- font-size: 10px;
1311
- transition: transform 0.2s;
1312
- }
1313
-
1314
- .dashboard-section.collapsed .collapse-icon {
1315
- transform: rotate(-90deg);
1316
- }
1317
-
1318
- .dashboard-section.collapsed .dashboard-section-header {
1319
- border-bottom: none;
1320
- }
1321
-
1322
- .dashboard-section-header .header-actions {
1323
- display: flex;
1324
- gap: 4px;
1325
- }
1326
-
1327
- .dashboard-section-header .header-actions button {
1328
- padding: 4px 8px;
1329
- border-radius: 4px;
1330
- border: 1px solid var(--border);
1331
- background: var(--bg-tertiary);
1332
- color: var(--text-secondary);
1333
- cursor: pointer;
1334
- font-size: 11px;
1335
- }
1336
-
1337
- .dashboard-section-header .header-actions button:hover {
1338
- background: var(--tab-hover);
1339
- color: var(--text-primary);
1340
- }
1341
-
1342
- .dashboard-section-content {
1343
- flex: 1;
1344
- overflow-y: auto;
1345
- padding: 8px 12px;
1346
- }
1347
-
1348
- .dashboard-section.collapsed .dashboard-section-content {
1349
- display: none;
1350
- }
1351
-
1352
- /* Legacy support */
1353
- .dashboard-column {
1354
- background: var(--bg-secondary);
1355
- border: 1px solid var(--border);
1356
- border-radius: 8px;
1357
- padding: 12px;
1358
- overflow: hidden;
1359
- display: flex;
1360
- flex-direction: column;
1361
- max-height: 280px;
1362
- }
1363
-
1364
- .dashboard-column-header {
1365
- display: flex;
1366
- justify-content: space-between;
1367
- align-items: center;
1368
- margin-bottom: 8px;
1369
- flex-shrink: 0;
1370
- }
1371
-
1372
- .dashboard-column-header h3 {
1373
- font-size: 12px;
1374
- text-transform: uppercase;
1375
- color: var(--text-muted);
1376
- letter-spacing: 0.5px;
1377
- margin: 0;
1378
- }
1379
-
1380
- .dashboard-column-header .header-actions {
1381
- display: flex;
1382
- gap: 4px;
1383
- }
1384
-
1385
- .dashboard-column-header .header-actions button {
1386
- padding: 4px 8px;
1387
- border-radius: 4px;
1388
- border: 1px solid var(--border);
1389
- background: var(--bg-tertiary);
1390
- color: var(--text-secondary);
1391
- cursor: pointer;
1392
- font-size: 11px;
1393
- }
1394
-
1395
- .dashboard-column-header .header-actions button:hover {
1396
- background: var(--tab-hover);
1397
- color: var(--text-primary);
1398
- }
1399
-
1400
- .dashboard-tabs-list {
1401
- flex: 1;
1402
- overflow-y: auto;
1403
- margin-bottom: 8px;
1404
- }
1405
-
1406
- .dashboard-tab-item {
1407
- display: flex;
1408
- align-items: center;
1409
- gap: 8px;
1410
- padding: 6px 8px;
1411
- border-radius: 4px;
1412
- cursor: pointer;
1413
- font-size: 13px;
1414
- color: var(--text-secondary);
1415
- }
1416
-
1417
- .dashboard-tab-item:hover {
1418
- background: var(--bg-tertiary);
1419
- }
1420
-
1421
- .dashboard-tab-item.active {
1422
- background: var(--accent);
1423
- color: white;
1424
- }
1425
-
1426
- .dashboard-tab-item .tab-icon {
1427
- font-size: 14px;
1428
- flex-shrink: 0;
1429
- }
1430
-
1431
- .dashboard-tab-item .tab-name {
1432
- flex: 1;
1433
- overflow: hidden;
1434
- text-overflow: ellipsis;
1435
- white-space: nowrap;
1436
- }
1437
-
1438
- .dashboard-actions {
1439
- flex-shrink: 0;
1440
- display: flex;
1441
- gap: 8px;
1442
- }
1443
-
1444
- .dashboard-actions .btn-action {
1445
- flex: 1;
1446
- padding: 8px 12px;
1447
- border-radius: 4px;
1448
- border: 1px dashed var(--border);
1449
- background: transparent;
1450
- color: var(--text-muted);
1451
- cursor: pointer;
1452
- font-size: 12px;
1453
- display: flex;
1454
- align-items: center;
1455
- justify-content: center;
1456
- gap: 4px;
1457
- }
1458
-
1459
- .dashboard-actions .btn-action:hover {
1460
- border-style: solid;
1461
- color: var(--text-secondary);
1462
- background: var(--bg-tertiary);
1463
- }
1464
-
1465
- .dashboard-files-list {
1466
- flex: 1;
1467
- overflow-y: auto;
1468
- }
1469
-
1470
- .dashboard-files-list .tree-item {
1471
- padding: 3px 6px;
1472
- font-size: 12px;
1473
- }
1474
-
1475
- .dashboard-files-list .tree-item-name {
1476
- font-size: 12px;
1477
- }
1478
-
1479
- .dashboard-empty-state {
1480
- color: var(--text-muted);
1481
- font-size: 13px;
1482
- padding: 12px;
1483
- text-align: center;
1484
- }
1485
-
1486
- /* Status indicators in dashboard tab list */
1487
- .dashboard-status-indicator {
1488
- width: 8px;
1489
- height: 8px;
1490
- border-radius: 50%;
1491
- flex-shrink: 0;
1492
- }
1493
-
1494
- .dashboard-status-working {
1495
- background: var(--status-active);
1496
- animation: status-pulse 2s ease-in-out infinite;
1497
- }
1498
-
1499
- .dashboard-status-idle {
1500
- background: var(--status-waiting);
1501
- animation: status-blink-slow 3s ease-in-out infinite;
1502
- }
1503
-
1504
- .dashboard-status-blocked {
1505
- background: var(--status-error);
1506
- animation: status-blink-fast 0.8s ease-in-out infinite;
1507
- }
1508
-
1509
- @media (prefers-reduced-motion: reduce) {
1510
- .dashboard-status-working,
1511
- .dashboard-status-idle,
1512
- .dashboard-status-blocked {
1513
- animation: none;
1514
- }
1515
- }
1516
- </style>
1517
- </head>
1518
- <body>
1519
- <header class="header">
1520
- <h1>Agent Farm - {{PROJECT_NAME}}</h1>
1521
- </header>
1522
-
1523
- <main class="main">
1524
- <!-- Left pane: Architect terminal -->
1525
- <div class="left-pane">
1526
- <div class="pane-header">
1527
- <span class="status-dot" id="architect-status"></span>
1528
- <span>Architect</span>
1529
- </div>
1530
- <div id="architect-content"></div>
1531
- </div>
1532
-
1533
- <!-- Right pane: Tabbed interface -->
1534
- <div class="right-pane">
1535
- <div class="tab-bar">
1536
- <div class="tabs-scroll" id="tabs-container"></div>
1537
- <button class="overflow-btn" id="overflow-btn" onclick="toggleOverflowMenu()" aria-haspopup="true" aria-expanded="false" title="Show all tabs">
1538
- <span>...</span>
1539
- <span class="overflow-count" id="overflow-count">+0</span>
1540
- </button>
1541
- <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
1542
- <div class="add-buttons">
1543
- <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
1544
- <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
1545
- </div>
1546
- </div>
1547
- <div class="tab-content" id="tab-content"></div>
1548
- </div>
1549
- </main>
1550
-
1551
- <footer class="status-bar">
1552
- <div class="status-item" id="status-architect">
1553
- <span class="dot" style="background: var(--text-muted)"></span>
1554
- <span>Architect: stopped</span>
1555
- </div>
1556
- <div class="status-item" id="status-builders">
1557
- <span>0 builders</span>
1558
- </div>
1559
- <div class="status-item" id="status-shells">
1560
- <span>0 shells</span>
1561
- </div>
1562
- <div class="status-item" id="status-files">
1563
- <span>0 files</span>
1564
- </div>
1565
- </footer>
1566
-
1567
- <!-- File picker dialog -->
1568
- <div class="dialog-overlay hidden" id="file-dialog">
1569
- <div class="dialog">
1570
- <h3>Open File</h3>
1571
- <div class="quick-paths">
1572
- <button class="quick-path" onclick="setFilePath('codev/specs/')">codev/specs/</button>
1573
- <button class="quick-path" onclick="setFilePath('codev/plans/')">codev/plans/</button>
1574
- <button class="quick-path" onclick="setFilePath('codev/reviews/')">codev/reviews/</button>
1575
- </div>
1576
- <input type="text" id="file-path-input" placeholder="Enter file path..." />
1577
- <div class="dialog-actions">
1578
- <button class="btn" onclick="hideFileDialog()">Cancel</button>
1579
- <button class="btn" onclick="openFile()">Open</button>
1580
- </div>
1581
- </div>
1582
- </div>
1583
-
1584
- <!-- Close confirmation dialog -->
1585
- <div class="dialog-overlay hidden" id="close-dialog">
1586
- <div class="dialog">
1587
- <h3 id="close-dialog-title">Close tab?</h3>
1588
- <p id="close-dialog-message" style="color: var(--text-secondary); margin-bottom: 16px; font-size: 14px;"></p>
1589
- <div class="dialog-actions">
1590
- <button class="btn" onclick="hideCloseDialog()">Cancel</button>
1591
- <button class="btn btn-danger" onclick="confirmClose()">Close</button>
1592
- </div>
1593
- </div>
1594
- </div>
1595
-
1596
- <!-- Context menu -->
1597
- <div class="context-menu hidden" id="context-menu" role="menu">
1598
- <div class="context-menu-item" role="menuitem" tabindex="0" data-action="openContextTab" onclick="openContextTab()" onkeydown="handleContextMenuKeydown(event)">Open in New Tab</div>
1599
- <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeActiveTab" onclick="closeActiveTab()" onkeydown="handleContextMenuKeydown(event)">Close</div>
1600
- <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeOtherTabs" onclick="closeOtherTabs()" onkeydown="handleContextMenuKeydown(event)">Close Others</div>
1601
- <div class="context-menu-item danger" role="menuitem" tabindex="-1" data-action="closeAllTabs" onclick="closeAllTabs()" onkeydown="handleContextMenuKeydown(event)">Close All</div>
1602
- </div>
1603
-
1604
- <!-- Toast container -->
1605
- <div class="toast-container" id="toast-container"></div>
1606
-
1607
- <script>
1608
- // STATE_INJECTION_POINT
1609
-
1610
- // State management
1611
- const state = window.INITIAL_STATE || {
1612
- architect: null,
1613
- builders: [],
1614
- utils: [],
1615
- annotations: []
1616
- };
1617
-
1618
- // Tab state
1619
- let tabs = [];
1620
- let activeTabId = null;
1621
- let pendingCloseTabId = null;
1622
- let contextMenuTabId = null;
1623
-
1624
- // Collapsible section state (persisted to localStorage)
1625
- const SECTION_STATE_KEY = 'codev-dashboard-sections';
1626
- let sectionState = loadSectionState();
1627
-
1628
- function loadSectionState() {
1629
- try {
1630
- const saved = localStorage.getItem(SECTION_STATE_KEY);
1631
- if (saved) return JSON.parse(saved);
1632
- } catch (e) { /* ignore */ }
1633
- return { tabs: true, files: true, projects: true };
1634
- }
1635
-
1636
- function saveSectionState() {
1637
- try {
1638
- localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(sectionState));
1639
- } catch (e) { /* ignore */ }
1640
- }
1641
-
1642
- function toggleSection(section) {
1643
- sectionState[section] = !sectionState[section];
1644
- saveSectionState();
1645
- renderDashboardTabContent();
1646
- }
1647
-
1648
- // Initialize
1649
- function init() {
1650
- buildTabsFromState();
1651
- renderArchitect();
1652
- renderTabs();
1653
- renderTabContent();
1654
- updateStatusBar();
1655
- startPolling();
1656
- setupBroadcastChannel();
1657
- setupOverflowDetection();
1658
- }
1659
-
1660
- // Set up overflow detection for the tab bar
1661
- function setupOverflowDetection() {
1662
- const container = document.getElementById('tabs-container');
1663
-
1664
- // Check on load
1665
- checkTabOverflow();
1666
-
1667
- // Check on window resize (debounced)
1668
- let resizeTimeout;
1669
- window.addEventListener('resize', () => {
1670
- clearTimeout(resizeTimeout);
1671
- resizeTimeout = setTimeout(checkTabOverflow, 100);
1672
- });
1673
-
1674
- // Check on scroll (debounced) - updates +N count when user scrolls tabs
1675
- if (container) {
1676
- let scrollTimeout;
1677
- container.addEventListener('scroll', () => {
1678
- clearTimeout(scrollTimeout);
1679
- scrollTimeout = setTimeout(checkTabOverflow, 50);
1680
- });
1681
- }
1682
-
1683
- // Also use ResizeObserver for the tabs container if available
1684
- if (typeof ResizeObserver !== 'undefined') {
1685
- if (container) {
1686
- const observer = new ResizeObserver(() => {
1687
- checkTabOverflow();
1688
- });
1689
- observer.observe(container);
1690
- }
1691
- }
1692
- }
1693
-
1694
- // Set up BroadcastChannel for cross-tab communication
1695
- // This allows terminal file clicks to open files in the dashboard
1696
- function setupBroadcastChannel() {
1697
- const channel = new BroadcastChannel('agent-farm');
1698
- channel.onmessage = async (event) => {
1699
- const { type, path, line } = event.data;
1700
- if (type === 'openFile' && path) {
1701
- await openFileFromMessage(path, line);
1702
- }
1703
- };
1704
- }
1705
-
1706
- // Open a file from a BroadcastChannel message
1707
- async function openFileFromMessage(filePath, lineNumber) {
1708
- try {
1709
- // Check if file is already open
1710
- const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
1711
- if (existingTab) {
1712
- // Just switch to the existing tab
1713
- selectTab(existingTab.id);
1714
- showToast(`Switched to ${getFileName(filePath)}`, 'success');
1715
- // TODO: scroll to line if lineNumber provided
1716
- return;
1717
- }
1718
-
1719
- // Open the file via API
1720
- const response = await fetch('/api/tabs/file', {
1721
- method: 'POST',
1722
- headers: { 'Content-Type': 'application/json' },
1723
- body: JSON.stringify({ path: filePath })
1724
- });
1725
-
1726
- if (!response.ok) {
1727
- throw new Error(await response.text());
1728
- }
1729
-
1730
- const result = await response.json();
1731
-
1732
- // Refresh state and switch to the new tab
1733
- await refresh();
1734
-
1735
- // Find and select the new file tab
1736
- const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
1737
- if (newTab) {
1738
- selectTab(newTab.id);
1739
- }
1740
-
1741
- showToast(`Opened ${getFileName(filePath)}${lineNumber ? ':' + lineNumber : ''}`, 'success');
1742
- } catch (err) {
1743
- showToast('Failed to open file: ' + err.message, 'error');
1744
- }
1745
- }
1746
-
1747
- // Track known tab IDs to detect new tabs
1748
- let knownTabIds = new Set();
1749
-
1750
- // Projects tab state
1751
- let projectsData = [];
1752
- let projectlistHash = null;
1753
- let expandedProjectId = null;
1754
- let projectlistError = null;
1755
- let projectlistDebounce = null;
1756
-
1757
- // Files tab state (Spec 0055)
1758
- let filesTreeData = [];
1759
- let filesTreeExpanded = new Set(); // Set of expanded folder paths
1760
- let filesTreeError = null;
1761
- let filesTreeLoaded = false;
1762
-
1763
- // Build tabs from initial state
1764
- function buildTabsFromState() {
1765
- const previousTabIds = new Set(tabs.map(t => t.id));
1766
- tabs = [];
1767
-
1768
- // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
1769
- tabs.push({
1770
- id: 'dashboard',
1771
- type: 'dashboard',
1772
- name: 'Dashboard',
1773
- closeable: false
1774
- });
1775
-
1776
- // Add file tabs from annotations
1777
- for (const annotation of state.annotations || []) {
1778
- tabs.push({
1779
- id: `file-${annotation.id}`,
1780
- type: 'file',
1781
- name: getFileName(annotation.file),
1782
- path: annotation.file,
1783
- port: annotation.port,
1784
- annotationId: annotation.id
1785
- });
1786
- }
1787
-
1788
- // Add builder tabs
1789
- for (const builder of state.builders || []) {
1790
- tabs.push({
1791
- id: `builder-${builder.id}`,
1792
- type: 'builder',
1793
- name: builder.name || `Builder ${builder.id}`,
1794
- projectId: builder.id,
1795
- port: builder.port,
1796
- status: builder.status
1797
- });
1798
- }
1799
-
1800
- // Add shell tabs
1801
- for (const util of state.utils || []) {
1802
- tabs.push({
1803
- id: `shell-${util.id}`,
1804
- type: 'shell',
1805
- name: util.name,
1806
- port: util.port,
1807
- utilId: util.id
1808
- });
1809
- }
1810
-
1811
- // Detect new tabs and auto-switch to them (skip projects tab)
1812
- for (const tab of tabs) {
1813
- if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1814
- // This is a new tab - switch to it
1815
- activeTabId = tab.id;
1816
- break;
1817
- }
1818
- }
1819
-
1820
- // Update known tab IDs
1821
- knownTabIds = new Set(tabs.map(t => t.id));
1822
-
1823
- // Set active tab to Dashboard on first load if none selected
1824
- if (!activeTabId) {
1825
- activeTabId = 'dashboard';
1826
- }
1827
- }
1828
-
1829
- // Get filename from path
1830
- function getFileName(path) {
1831
- const parts = path.split('/');
1832
- return parts[parts.length - 1];
1833
- }
1834
-
1835
- // Track current architect port to avoid re-rendering iframe unnecessarily
1836
- let currentArchitectPort = null;
1837
-
1838
- // Render architect pane
1839
- function renderArchitect() {
1840
- const content = document.getElementById('architect-content');
1841
- const statusDot = document.getElementById('architect-status');
1842
-
1843
- if (state.architect && state.architect.port) {
1844
- statusDot.classList.remove('inactive');
1845
- // Only update iframe if port changed (avoid flashing on poll)
1846
- if (currentArchitectPort !== state.architect.port) {
1847
- currentArchitectPort = state.architect.port;
1848
- content.innerHTML = `<iframe src="http://localhost:${state.architect.port}" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
1849
- }
1850
- } else {
1851
- if (currentArchitectPort !== null) {
1852
- currentArchitectPort = null;
1853
- content.innerHTML = `
1854
- <div class="architect-placeholder">
1855
- <p>Architect not running</p>
1856
- <p>Run <code>agent-farm start</code> to begin</p>
1857
- </div>
1858
- `;
1859
- }
1860
- statusDot.classList.add('inactive');
1861
- }
1862
- }
1863
-
1864
- // Render tabs
1865
- function renderTabs() {
1866
- const container = document.getElementById('tabs-container');
1867
-
1868
- if (tabs.length === 0) {
1869
- container.innerHTML = '';
1870
- checkTabOverflow(); // Update overflow state when tabs cleared
1871
- return;
1872
- }
1873
-
1874
- container.innerHTML = tabs.map(tab => {
1875
- const isActive = tab.id === activeTabId;
1876
- const icon = getTabIcon(tab.type);
1877
- const statusDot = tab.type === 'builder' ? getStatusDot(tab.status) : '';
1878
- const tooltip = getTabTooltip(tab);
1879
- const isUncloseable = tab.closeable === false;
1880
-
1881
- return `
1882
- <div class="tab ${isActive ? 'active' : ''} ${isUncloseable ? 'tab-uncloseable' : ''}"
1883
- onclick="selectTab('${tab.id}')"
1884
- oncontextmenu="showContextMenu(event, '${tab.id}')"
1885
- data-tab-id="${tab.id}"
1886
- title="${tooltip}">
1887
- <span class="icon">${icon}</span>
1888
- <span class="name">${tab.name}</span>
1889
- ${statusDot}
1890
- ${!isUncloseable ? `<span class="close"
1891
- onclick="event.stopPropagation(); closeTab('${tab.id}', event)"
1892
- role="button"
1893
- tabindex="0"
1894
- aria-label="Close ${tab.name}"
1895
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();closeTab('${tab.id}',event)}">&times;</span>` : ''}
1896
- </div>
1897
- `;
1898
- }).join('');
1899
-
1900
- // Check overflow after tabs are rendered
1901
- checkTabOverflow();
1902
- }
1903
-
1904
- // Get tab icon
1905
- function getTabIcon(type) {
1906
- switch (type) {
1907
- case 'dashboard': return '🏠';
1908
- case 'files': return '📁';
1909
- case 'file': return '📄';
1910
- case 'builder': return '🔨';
1911
- case 'shell': return '>_';
1912
- default: return '?';
1913
- }
1914
- }
1915
-
1916
- // Status configuration - hoisted for performance (per Codex review)
1917
- // Colors per spec 0019: green=active, yellow=waiting, red=blocked, gray=complete
1918
- // Animations per spec 0019: pulse=active, blink-slow=waiting, blink-fast=blocked, static=complete
1919
- // Shapes for accessibility: circle=default, diamond=blocked, ring=waiting
1920
- const STATUS_CONFIG = {
1921
- 'spawning': { color: 'var(--status-active)', label: 'Spawning', shape: 'circle', animation: 'pulse' },
1922
- 'implementing': { color: 'var(--status-active)', label: 'Implementing', shape: 'circle', animation: 'pulse' },
1923
- 'blocked': { color: 'var(--status-error)', label: 'Blocked', shape: 'diamond', animation: 'blink-fast' },
1924
- 'pr-ready': { color: 'var(--status-waiting)', label: 'PR Ready', shape: 'ring', animation: 'blink-slow' },
1925
- 'complete': { color: 'var(--status-complete)', label: 'Complete', shape: 'circle', animation: null }
1926
- };
1927
- const DEFAULT_STATUS_CONFIG = { color: 'var(--text-muted)', label: 'Unknown', shape: 'circle', animation: null };
1928
-
1929
- // Get status dot HTML with accessibility support
1930
- // Accessibility: distinct animations per status, shapes for reduced-motion users
1931
- // Uses role="img" instead of role="status" to avoid screen reader chatter on poll (per Codex review)
1932
- function getStatusDot(status) {
1933
- const config = STATUS_CONFIG[status] || { ...DEFAULT_STATUS_CONFIG, label: status || 'Unknown' };
1934
- // Build CSS classes for accessibility
1935
- const classes = ['status-dot'];
1936
- if (config.shape === 'diamond') classes.push('status-dot--diamond');
1937
- if (config.shape === 'ring') classes.push('status-dot--ring');
1938
- if (config.animation === 'pulse') classes.push('status-dot--pulse');
1939
- if (config.animation === 'blink-slow') classes.push('status-dot--blink-slow');
1940
- if (config.animation === 'blink-fast') classes.push('status-dot--blink-fast');
1941
- return `<span class="${classes.join(' ')}" style="background: ${config.color}" title="${config.label}" role="img" aria-label="${config.label}"></span>`;
1942
- }
1943
-
1944
- // Escape HTML special characters to prevent XSS
1945
- function escapeHtml(text) {
1946
- return String(text)
1947
- .replace(/&/g, '&amp;')
1948
- .replace(/</g, '&lt;')
1949
- .replace(/>/g, '&gt;')
1950
- .replace(/"/g, '&quot;')
1951
- .replace(/'/g, '&#39;');
1952
- }
1953
-
1954
- // Generate tooltip text for tab hover
1955
- function getTabTooltip(tab) {
1956
- const lines = [tab.name];
1957
-
1958
- if (tab.type === 'builder') {
1959
- if (tab.port) lines.push(`Port: ${tab.port}`);
1960
- lines.push(`Status: ${tab.status || 'unknown'}`);
1961
- // Extract project ID from tab id (e.g., "builder-0037" -> "0037")
1962
- const projectId = tab.id.replace('builder-', '');
1963
- lines.push(`Worktree: .builders/${projectId}`);
1964
- } else if (tab.type === 'file') {
1965
- lines.push(`Path: ${tab.path}`);
1966
- if (tab.port) lines.push(`Port: ${tab.port}`);
1967
- } else if (tab.type === 'shell') {
1968
- if (tab.port) lines.push(`Port: ${tab.port}`);
1969
- }
1970
-
1971
- return escapeHtml(lines.join('\n'));
1972
- }
1973
-
1974
- // Track current tab content to avoid re-rendering iframe unnecessarily
1975
- let currentTabPort = null;
1976
- let currentTabType = null;
1977
-
1978
- // Render tab content
1979
- function renderTabContent() {
1980
- const content = document.getElementById('tab-content');
1981
-
1982
- if (!activeTabId || tabs.length === 0) {
1983
- if (currentTabPort !== null || currentTabType !== null) {
1984
- currentTabPort = null;
1985
- currentTabType = null;
1986
- content.innerHTML = `
1987
- <div class="empty-state">
1988
- <p>No tabs open</p>
1989
- <p class="hint">Click the + buttons above or ask the architect to open files/builders</p>
1990
- </div>
1991
- `;
1992
- }
1993
- return;
1994
- }
1995
-
1996
- const tab = tabs.find(t => t.id === activeTabId);
1997
- if (!tab) {
1998
- if (currentTabPort !== null || currentTabType !== null) {
1999
- currentTabPort = null;
2000
- currentTabType = null;
2001
- content.innerHTML = '<div class="empty-state"><p>Tab not found</p></div>';
2002
- }
2003
- return;
2004
- }
2005
-
2006
- // Handle dashboard tab specially (no iframe, inline content)
2007
- if (tab.type === 'dashboard') {
2008
- if (currentTabType !== 'dashboard') {
2009
- currentTabType = 'dashboard';
2010
- currentTabPort = null;
2011
- renderDashboardTab();
2012
- }
2013
- return;
2014
- }
2015
-
2016
- // For other tabs, only update iframe if port changed (avoid flashing on poll)
2017
- if (currentTabPort !== tab.port || currentTabType !== tab.type) {
2018
- currentTabPort = tab.port;
2019
- currentTabType = tab.type;
2020
- content.innerHTML = `<iframe src="http://localhost:${tab.port}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
2021
- }
2022
- }
2023
-
2024
- // Update status bar
2025
- function updateStatusBar() {
2026
- // Architect status
2027
- const archStatus = document.getElementById('status-architect');
2028
- if (state.architect) {
2029
- archStatus.innerHTML = `
2030
- <span class="dot" style="background: var(--status-active)"></span>
2031
- <span>Architect: running</span>
2032
- `;
2033
- } else {
2034
- archStatus.innerHTML = `
2035
- <span class="dot" style="background: var(--text-muted)"></span>
2036
- <span>Architect: stopped</span>
2037
- `;
2038
- }
2039
-
2040
- // Counts
2041
- const builderCount = (state.builders || []).length;
2042
- const shellCount = (state.utils || []).length;
2043
- const fileCount = (state.annotations || []).length;
2044
-
2045
- document.getElementById('status-builders').innerHTML = `<span>${builderCount} builder${builderCount !== 1 ? 's' : ''}</span>`;
2046
- document.getElementById('status-shells').innerHTML = `<span>${shellCount} shell${shellCount !== 1 ? 's' : ''}</span>`;
2047
- document.getElementById('status-files').innerHTML = `<span>${fileCount} file${fileCount !== 1 ? 's' : ''}</span>`;
2048
- }
2049
-
2050
- // Select tab
2051
- function selectTab(tabId) {
2052
- activeTabId = tabId;
2053
- renderTabs();
2054
- renderTabContent();
2055
- // Scroll the active tab into view if needed
2056
- scrollActiveTabIntoView();
2057
- }
2058
-
2059
- // Scroll the active tab into view
2060
- function scrollActiveTabIntoView() {
2061
- const container = document.getElementById('tabs-container');
2062
- const activeTab = container.querySelector('.tab.active');
2063
- if (activeTab) {
2064
- activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
2065
- }
2066
- }
2067
-
2068
- // Check if tabs are overflowing and update the overflow button
2069
- function checkTabOverflow() {
2070
- const container = document.getElementById('tabs-container');
2071
- const overflowBtn = document.getElementById('overflow-btn');
2072
- const overflowCount = document.getElementById('overflow-count');
2073
-
2074
- if (!container || !overflowBtn) return;
2075
-
2076
- const isOverflowing = container.scrollWidth > container.clientWidth;
2077
- overflowBtn.style.display = isOverflowing ? 'flex' : 'none';
2078
-
2079
- if (isOverflowing) {
2080
- // Count hidden tabs (those partially or fully outside visible area - both sides)
2081
- const tabElements = container.querySelectorAll('.tab');
2082
- const containerRect = container.getBoundingClientRect();
2083
- let hiddenCount = 0;
2084
-
2085
- tabElements.forEach(tab => {
2086
- const rect = tab.getBoundingClientRect();
2087
- // Tab is hidden if scrolled off the right edge
2088
- if (rect.right > containerRect.right + 1) {
2089
- hiddenCount++;
2090
- }
2091
- // Tab is hidden if scrolled off the left edge
2092
- else if (rect.left < containerRect.left - 1) {
2093
- hiddenCount++;
2094
- }
2095
- });
2096
-
2097
- overflowCount.textContent = `+${hiddenCount}`;
2098
- }
2099
- }
2100
-
2101
- // Toggle the overflow menu
2102
- function toggleOverflowMenu() {
2103
- const menu = document.getElementById('overflow-menu');
2104
- const btn = document.getElementById('overflow-btn');
2105
- const isHidden = menu.classList.contains('hidden');
2106
-
2107
- if (isHidden) {
2108
- showOverflowMenu();
2109
- } else {
2110
- hideOverflowMenu();
2111
- }
2112
- }
2113
-
2114
- // Show the overflow menu
2115
- function showOverflowMenu() {
2116
- const menu = document.getElementById('overflow-menu');
2117
- const btn = document.getElementById('overflow-btn');
2118
-
2119
- // Build menu items for all tabs
2120
- menu.innerHTML = tabs.map((tab, index) => {
2121
- const icon = getTabIcon(tab.type);
2122
- const isActive = tab.id === activeTabId;
2123
- return `
2124
- <div class="overflow-menu-item ${isActive ? 'active' : ''}"
2125
- role="menuitem"
2126
- tabindex="${index === 0 ? 0 : -1}"
2127
- data-tab-id="${tab.id}"
2128
- onclick="selectTabFromMenu('${tab.id}')"
2129
- onkeydown="handleOverflowMenuKeydown(event, '${tab.id}')">
2130
- <span class="icon">${icon}</span>
2131
- <span class="name">${tab.name}</span>
2132
- <span class="open-external"
2133
- onclick="event.stopPropagation(); openInNewTabFromMenu('${tab.id}')"
2134
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();openInNewTabFromMenu('${tab.id}')}"
2135
- title="Open in new tab"
2136
- role="button"
2137
- tabindex="0"
2138
- aria-label="Open ${tab.name} in new tab">↗</span>
2139
- </div>
2140
- `;
2141
- }).join('');
2142
-
2143
- menu.classList.remove('hidden');
2144
- btn.setAttribute('aria-expanded', 'true');
2145
-
2146
- // Focus the first item
2147
- const firstItem = menu.querySelector('.overflow-menu-item');
2148
- if (firstItem) firstItem.focus();
2149
-
2150
- // Close on click outside (after a small delay to avoid immediate close)
2151
- setTimeout(() => {
2152
- document.addEventListener('click', handleOverflowClickOutside);
2153
- }, 0);
2154
- }
2155
-
2156
- // Hide the overflow menu
2157
- function hideOverflowMenu() {
2158
- const menu = document.getElementById('overflow-menu');
2159
- const btn = document.getElementById('overflow-btn');
2160
- menu.classList.add('hidden');
2161
- btn.setAttribute('aria-expanded', 'false');
2162
- document.removeEventListener('click', handleOverflowClickOutside);
2163
- }
2164
-
2165
- // Handle click outside overflow menu
2166
- function handleOverflowClickOutside(event) {
2167
- const menu = document.getElementById('overflow-menu');
2168
- const btn = document.getElementById('overflow-btn');
2169
- if (!menu.contains(event.target) && !btn.contains(event.target)) {
2170
- hideOverflowMenu();
2171
- }
2172
- }
2173
-
2174
- // Select tab from overflow menu
2175
- function selectTabFromMenu(tabId) {
2176
- hideOverflowMenu();
2177
- selectTab(tabId);
2178
- }
2179
-
2180
- // Open tab in new window from overflow menu
2181
- function openInNewTabFromMenu(tabId) {
2182
- hideOverflowMenu();
2183
- openInNewTab(tabId);
2184
- }
2185
-
2186
- // Handle keyboard navigation in overflow menu
2187
- function handleOverflowMenuKeydown(event, tabId) {
2188
- const menu = document.getElementById('overflow-menu');
2189
- const items = Array.from(menu.querySelectorAll('.overflow-menu-item'));
2190
- const currentIndex = items.findIndex(item => item === document.activeElement);
2191
-
2192
- switch (event.key) {
2193
- case 'ArrowDown':
2194
- event.preventDefault();
2195
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
2196
- items[nextIndex].focus();
2197
- break;
2198
- case 'ArrowUp':
2199
- event.preventDefault();
2200
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
2201
- items[prevIndex].focus();
2202
- break;
2203
- case 'Enter':
2204
- case ' ':
2205
- event.preventDefault();
2206
- selectTabFromMenu(tabId);
2207
- break;
2208
- case 'Escape':
2209
- event.preventDefault();
2210
- hideOverflowMenu();
2211
- document.getElementById('overflow-btn').focus();
2212
- break;
2213
- case 'Tab':
2214
- // Allow Tab to close menu and move focus
2215
- hideOverflowMenu();
2216
- break;
2217
- }
2218
- }
2219
-
2220
- // Close tab
2221
- function closeTab(tabId, event) {
2222
- const tab = tabs.find(t => t.id === tabId);
2223
- if (!tab) return;
2224
-
2225
- // Shift+click bypasses confirmation
2226
- if (event && event.shiftKey) {
2227
- doCloseTab(tabId);
2228
- return;
2229
- }
2230
-
2231
- // Files don't need confirmation
2232
- if (tab.type === 'file') {
2233
- doCloseTab(tabId);
2234
- return;
2235
- }
2236
-
2237
- // Show confirmation for builders and shells
2238
- pendingCloseTabId = tabId;
2239
- const dialog = document.getElementById('close-dialog');
2240
- const title = document.getElementById('close-dialog-title');
2241
- const message = document.getElementById('close-dialog-message');
2242
-
2243
- if (tab.type === 'builder') {
2244
- title.textContent = `Stop builder ${tab.name}?`;
2245
- message.textContent = 'This will terminate the builder process.';
2246
- } else {
2247
- title.textContent = `Close shell ${tab.name}?`;
2248
- message.textContent = 'This will terminate the shell process.';
2249
- }
2250
-
2251
- dialog.classList.remove('hidden');
2252
- }
2253
-
2254
- // Actually close the tab
2255
- async function doCloseTab(tabId) {
2256
- const tab = tabs.find(t => t.id === tabId);
2257
- if (!tab) return;
2258
-
2259
- try {
2260
- // Call API to close the tab
2261
- await fetch(`/api/tabs/${encodeURIComponent(tabId)}`, { method: 'DELETE' });
2262
-
2263
- // Remove from local state
2264
- tabs = tabs.filter(t => t.id !== tabId);
2265
-
2266
- // If closing active tab, switch to another
2267
- if (activeTabId === tabId) {
2268
- activeTabId = tabs.length > 0 ? tabs[tabs.length - 1].id : null;
2269
- }
2270
-
2271
- renderTabs();
2272
- renderTabContent();
2273
- showToast('Tab closed', 'success');
2274
- } catch (err) {
2275
- showToast('Failed to close tab: ' + err.message, 'error');
2276
- }
2277
- }
2278
-
2279
- // Confirm close from dialog
2280
- function confirmClose() {
2281
- if (pendingCloseTabId) {
2282
- doCloseTab(pendingCloseTabId);
2283
- hideCloseDialog();
2284
- }
2285
- }
2286
-
2287
- function hideCloseDialog() {
2288
- document.getElementById('close-dialog').classList.add('hidden');
2289
- pendingCloseTabId = null;
2290
- }
2291
-
2292
- // Context menu
2293
- function showContextMenu(event, tabId) {
2294
- event.preventDefault();
2295
- contextMenuTabId = tabId;
2296
-
2297
- const menu = document.getElementById('context-menu');
2298
- menu.style.left = event.clientX + 'px';
2299
- menu.style.top = event.clientY + 'px';
2300
- menu.classList.remove('hidden');
2301
-
2302
- // Focus first item for keyboard navigation
2303
- const firstItem = menu.querySelector('.context-menu-item');
2304
- if (firstItem) firstItem.focus();
2305
-
2306
- // Close on click outside
2307
- setTimeout(() => {
2308
- document.addEventListener('click', hideContextMenu, { once: true });
2309
- }, 0);
2310
- }
2311
-
2312
- function hideContextMenu() {
2313
- document.getElementById('context-menu').classList.add('hidden');
2314
- contextMenuTabId = null;
2315
- }
2316
-
2317
- // Handle keyboard navigation in context menu
2318
- function handleContextMenuKeydown(event) {
2319
- const menu = document.getElementById('context-menu');
2320
- const items = Array.from(menu.querySelectorAll('.context-menu-item'));
2321
- const currentIndex = items.findIndex(item => item === document.activeElement);
2322
-
2323
- switch (event.key) {
2324
- case 'ArrowDown':
2325
- event.preventDefault();
2326
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
2327
- items[nextIndex].focus();
2328
- break;
2329
- case 'ArrowUp':
2330
- event.preventDefault();
2331
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
2332
- items[prevIndex].focus();
2333
- break;
2334
- case 'Enter':
2335
- case ' ':
2336
- event.preventDefault();
2337
- const actionName = event.target.dataset.action;
2338
- if (actionName && typeof window[actionName] === 'function') {
2339
- window[actionName]();
2340
- }
2341
- break;
2342
- case 'Escape':
2343
- event.preventDefault();
2344
- hideContextMenu();
2345
- break;
2346
- case 'Tab':
2347
- hideContextMenu();
2348
- break;
2349
- }
2350
- }
2351
-
2352
- function closeActiveTab() {
2353
- if (contextMenuTabId) {
2354
- closeTab(contextMenuTabId);
2355
- }
2356
- hideContextMenu();
2357
- }
2358
-
2359
- function closeOtherTabs() {
2360
- if (contextMenuTabId) {
2361
- // Skip uncloseable tabs (Projects tab)
2362
- const otherTabs = tabs.filter(t => t.id !== contextMenuTabId && t.closeable !== false);
2363
- otherTabs.forEach(t => doCloseTab(t.id));
2364
- }
2365
- hideContextMenu();
2366
- }
2367
-
2368
- function closeAllTabs() {
2369
- // Skip uncloseable tabs (Projects tab)
2370
- tabs.filter(t => t.closeable !== false).forEach(t => doCloseTab(t.id));
2371
- hideContextMenu();
2372
- }
2373
-
2374
- // Open tab content in a new browser tab
2375
- function openInNewTab(tabId) {
2376
- const tab = tabs.find(t => t.id === tabId);
2377
- if (!tab) return;
2378
-
2379
- let url;
2380
- if (tab.type === 'file') {
2381
- // File tabs use the annotation port
2382
- if (!tab.port) {
2383
- showToast('Tab not ready', 'error');
2384
- return;
2385
- }
2386
- url = `http://localhost:${tab.port}`;
2387
- } else {
2388
- // Builder or shell - direct port access
2389
- if (!tab.port) {
2390
- showToast('Tab not ready', 'error');
2391
- return;
2392
- }
2393
- url = `http://localhost:${tab.port}`;
2394
- }
2395
-
2396
- window.open(url, '_blank', 'noopener,noreferrer');
2397
- }
2398
-
2399
- // Open context menu tab in new tab
2400
- function openContextTab() {
2401
- if (contextMenuTabId) {
2402
- openInNewTab(contextMenuTabId);
2403
- }
2404
- hideContextMenu();
2405
- }
2406
-
2407
- // File dialog
2408
- function showFileDialog() {
2409
- document.getElementById('file-dialog').classList.remove('hidden');
2410
- document.getElementById('file-path-input').focus();
2411
- }
2412
-
2413
- function hideFileDialog() {
2414
- document.getElementById('file-dialog').classList.add('hidden');
2415
- document.getElementById('file-path-input').value = '';
2416
- }
2417
-
2418
- function setFilePath(path) {
2419
- document.getElementById('file-path-input').value = path;
2420
- document.getElementById('file-path-input').focus();
2421
- }
2422
-
2423
- async function openFile() {
2424
- const path = document.getElementById('file-path-input').value.trim();
2425
- if (!path) return;
2426
-
2427
- try {
2428
- const response = await fetch('/api/tabs/file', {
2429
- method: 'POST',
2430
- headers: { 'Content-Type': 'application/json' },
2431
- body: JSON.stringify({ path })
2432
- });
2433
-
2434
- if (!response.ok) {
2435
- throw new Error(await response.text());
2436
- }
2437
-
2438
- hideFileDialog();
2439
- await refresh();
2440
- showToast(`Opened ${path}`, 'success');
2441
- } catch (err) {
2442
- showToast('Failed to open file: ' + err.message, 'error');
2443
- }
2444
- }
2445
-
2446
- // Spawn worktree builder (no dialog - spawns with random ID)
2447
- async function spawnBuilder() {
2448
- try {
2449
- const response = await fetch('/api/tabs/builder', {
2450
- method: 'POST',
2451
- headers: { 'Content-Type': 'application/json' },
2452
- body: JSON.stringify({})
2453
- });
2454
-
2455
- if (!response.ok) {
2456
- throw new Error(await response.text());
2457
- }
2458
-
2459
- const result = await response.json();
2460
-
2461
- // Add to local tabs and select it
2462
- const newTab = {
2463
- id: `builder-${result.id}`,
2464
- type: 'builder',
2465
- name: result.name,
2466
- port: result.port
2467
- };
2468
- tabs.push(newTab);
2469
- activeTabId = newTab.id;
2470
- renderTabs();
2471
- renderTabContent();
2472
- showToast(`Builder ${result.name} spawned`, 'success');
2473
- } catch (err) {
2474
- showToast('Failed to spawn builder: ' + err.message, 'error');
2475
- }
2476
- }
2477
-
2478
- // Spawn shell
2479
- async function spawnShell() {
2480
- try {
2481
- const response = await fetch('/api/tabs/shell', {
2482
- method: 'POST',
2483
- headers: { 'Content-Type': 'application/json' },
2484
- body: JSON.stringify({})
2485
- });
2486
-
2487
- if (!response.ok) {
2488
- throw new Error(await response.text());
2489
- }
2490
-
2491
- const result = await response.json();
2492
-
2493
- // Add to local tabs and select it
2494
- const newTab = {
2495
- id: `shell-${result.id}`,
2496
- type: 'shell',
2497
- name: result.name,
2498
- port: result.port,
2499
- utilId: result.id,
2500
- pendingLoad: true // Mark as pending to delay iframe
2501
- };
2502
- tabs.push(newTab);
2503
- activeTabId = newTab.id;
2504
- renderTabs();
2505
-
2506
- // Show loading state, then load iframe after delay
2507
- const content = document.getElementById('tab-content');
2508
- content.innerHTML = '<div class="empty-state"><p>Starting shell...</p></div>';
2509
-
2510
- setTimeout(() => {
2511
- delete newTab.pendingLoad;
2512
- currentTabPort = null; // Force re-render
2513
- renderTabContent();
2514
- }, 800);
2515
-
2516
- showToast('Shell spawned', 'success');
2517
- } catch (err) {
2518
- showToast('Failed to spawn shell: ' + err.message, 'error');
2519
- }
2520
- }
2521
-
2522
- // Refresh state from API
2523
- async function refresh() {
2524
- try {
2525
- const response = await fetch('/api/state');
2526
- if (!response.ok) throw new Error('Failed to fetch state');
2527
-
2528
- const newState = await response.json();
2529
- Object.assign(state, newState);
2530
-
2531
- buildTabsFromState();
2532
- renderArchitect();
2533
- renderTabs();
2534
- renderTabContent();
2535
- updateStatusBar();
2536
- } catch (err) {
2537
- console.error('Refresh error:', err);
2538
- }
2539
- }
2540
-
2541
- // Toast notifications
2542
- function showToast(message, type = 'info') {
2543
- const container = document.getElementById('toast-container');
2544
- const toast = document.createElement('div');
2545
- toast.className = `toast ${type}`;
2546
- toast.textContent = message;
2547
- container.appendChild(toast);
2548
-
2549
- setTimeout(() => {
2550
- toast.remove();
2551
- }, 3000);
2552
- }
2553
-
2554
- // Polling for state updates
2555
- let pollInterval = null;
2556
-
2557
- function startPolling() {
2558
- pollInterval = setInterval(refresh, 1000);
2559
- }
2560
-
2561
- function stopPolling() {
2562
- if (pollInterval) {
2563
- clearInterval(pollInterval);
2564
- pollInterval = null;
2565
- }
2566
- }
2567
-
2568
- // Keyboard shortcuts
2569
- document.addEventListener('keydown', (e) => {
2570
- // Escape to close dialogs and menus
2571
- if (e.key === 'Escape') {
2572
- hideFileDialog();
2573
- hideCloseDialog();
2574
- hideContextMenu();
2575
- hideOverflowMenu();
2576
- }
2577
-
2578
- // Enter in dialogs
2579
- if (e.key === 'Enter') {
2580
- if (!document.getElementById('file-dialog').classList.contains('hidden')) {
2581
- openFile();
2582
- }
2583
- }
2584
-
2585
- // Ctrl+Tab / Ctrl+Shift+Tab to switch tabs
2586
- if (e.ctrlKey && e.key === 'Tab') {
2587
- e.preventDefault();
2588
- if (tabs.length < 2) return;
2589
-
2590
- const currentIndex = tabs.findIndex(t => t.id === activeTabId);
2591
- let newIndex;
2592
-
2593
- if (e.shiftKey) {
2594
- newIndex = currentIndex <= 0 ? tabs.length - 1 : currentIndex - 1;
2595
- } else {
2596
- newIndex = currentIndex >= tabs.length - 1 ? 0 : currentIndex + 1;
2597
- }
2598
-
2599
- selectTab(tabs[newIndex].id);
2600
- }
2601
-
2602
- // Ctrl+W to close current tab
2603
- if (e.ctrlKey && e.key === 'w') {
2604
- e.preventDefault();
2605
- if (activeTabId) {
2606
- closeTab(activeTabId, e);
2607
- }
2608
- }
2609
- });
2610
-
2611
- // ============================================
2612
- // Projects Tab Functions (Spec 0045)
2613
- // ============================================
2614
-
2615
- // XSS-safe HTML escaping (used by escapeHtml above, same implementation)
2616
- function escapeProjectHtml(text) {
2617
- if (!text) return '';
2618
- const div = document.createElement('div');
2619
- div.textContent = String(text);
2620
- return div.innerHTML;
2621
- }
2622
-
2623
- // Simple DJB2 hash for change detection
2624
- function hashString(str) {
2625
- let hash = 5381;
2626
- for (let i = 0; i < str.length; i++) {
2627
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
2628
- }
2629
- return hash >>> 0;
2630
- }
2631
-
2632
- // Parse a single project entry from YAML-like text
2633
- function parseProjectEntry(text) {
2634
- const project = {};
2635
- const lines = text.split('\n');
2636
-
2637
- for (const line of lines) {
2638
- // Match key: value or key: "value"
2639
- // Also handle "- id:" YAML list format
2640
- const match = line.match(/^\s*-?\s*(\w+):\s*(.*)$/);
2641
- if (!match) continue;
2642
-
2643
- const [, key, rawValue] = match;
2644
- // Remove quotes if present
2645
- let value = rawValue.trim();
2646
- if ((value.startsWith('"') && value.endsWith('"')) ||
2647
- (value.startsWith("'") && value.endsWith("'"))) {
2648
- value = value.slice(1, -1);
2649
- }
2650
-
2651
- // Handle nested files object
2652
- if (key === 'files') {
2653
- project.files = {};
2654
- continue;
2655
- }
2656
- if (key === 'spec' || key === 'plan' || key === 'review') {
2657
- if (!project.files) project.files = {};
2658
- project.files[key] = value === 'null' ? null : value;
2659
- continue;
2660
- }
2661
-
2662
- // Handle nested timestamps object
2663
- if (key === 'timestamps') {
2664
- project.timestamps = {};
2665
- continue;
2666
- }
2667
- const timestampFields = ['conceived_at', 'specified_at', 'planned_at',
2668
- 'implementing_at', 'implemented_at', 'committed_at', 'integrated_at'];
2669
- if (timestampFields.includes(key)) {
2670
- if (!project.timestamps) project.timestamps = {};
2671
- project.timestamps[key] = value === 'null' ? null : value;
2672
- continue;
2673
- }
2674
-
2675
- // Handle arrays (simple inline format)
2676
- if (key === 'dependencies' || key === 'tags' || key === 'ticks') {
2677
- if (value.startsWith('[') && value.endsWith(']')) {
2678
- const inner = value.slice(1, -1);
2679
- if (inner.trim() === '') {
2680
- project[key] = [];
2681
- } else {
2682
- project[key] = inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
2683
- }
2684
- } else {
2685
- project[key] = [];
2686
- }
2687
- continue;
2688
- }
2689
-
2690
- // Regular string values
2691
- if (value !== 'null') {
2692
- project[key] = value;
2693
- }
2694
- }
2695
-
2696
- return project;
2697
- }
2698
-
2699
- // Validate that a project entry is valid
2700
- function isValidProject(project) {
2701
- // Must have id (4-digit string, not "NNNN")
2702
- if (!project.id || project.id === 'NNNN' || !/^\d{4}$/.test(project.id)) {
2703
- return false;
2704
- }
2705
-
2706
- // Must have status
2707
- const validStatuses = ['conceived', 'specified', 'planned', 'implementing',
2708
- 'implemented', 'committed', 'integrated', 'abandoned', 'on-hold'];
2709
- if (!project.status || !validStatuses.includes(project.status)) {
2710
- return false;
2711
- }
2712
-
2713
- // Must have title
2714
- if (!project.title) {
2715
- return false;
2716
- }
2717
-
2718
- // Filter out example entries
2719
- if (project.tags && project.tags.includes('example')) {
2720
- return false;
2721
- }
2722
-
2723
- return true;
2724
- }
2725
-
2726
- // Parse projectlist.md content into array of projects
2727
- function parseProjectlist(content) {
2728
- const projects = [];
2729
-
2730
- try {
2731
- // Extract YAML code blocks
2732
- const yamlBlockRegex = /```yaml\n([\s\S]*?)```/g;
2733
- let match;
2734
-
2735
- while ((match = yamlBlockRegex.exec(content)) !== null) {
2736
- const block = match[1];
2737
-
2738
- // Split by project entries (lines starting with " - id:")
2739
- // Handle both top-level and indented entries
2740
- const projectMatches = block.split(/\n(?=\s*- id:)/);
2741
-
2742
- for (const projectText of projectMatches) {
2743
- if (!projectText.trim() || !projectText.includes('id:')) continue;
2744
-
2745
- const project = parseProjectEntry(projectText);
2746
- if (isValidProject(project)) {
2747
- projects.push(project);
2748
- }
2749
- }
2750
- }
2751
- } catch (err) {
2752
- console.error('Error parsing projectlist:', err);
2753
- return [];
2754
- }
2755
-
2756
- return projects;
2757
- }
2758
-
2759
- // Render the welcome screen for new users
2760
- function renderWelcomeScreen() {
2761
- return `
2762
- <div class="projects-welcome">
2763
- <h2>Welcome to Codev</h2>
2764
- <p>Codev helps you build software with AI assistance. Projects flow through 7 stages from idea to production:</p>
2765
- <ol>
2766
- <li><strong>Conceived</strong> - Tell the architect what you want to build</li>
2767
- <li><strong>Specified</strong> - AI writes a spec, you approve it</li>
2768
- <li><strong>Planned</strong> - AI creates an implementation plan</li>
2769
- <li><strong>Implementing</strong> - Builder AI writes the code</li>
2770
- <li><strong>Implemented</strong> - Code complete, PR ready for review</li>
2771
- <li><strong>Committed</strong> - PR merged to main</li>
2772
- <li><strong>Integrated</strong> - Validated in production</li>
2773
- </ol>
2774
- <hr>
2775
- <p class="quick-tip">
2776
- <strong>Quick tip:</strong> Say "I want to build a [feature]" and the architect will guide you through the process.
2777
- </p>
2778
- </div>
2779
- `;
2780
- }
2781
-
2782
- // Render the error banner
2783
- function renderErrorBanner(message) {
2784
- return `
2785
- <div class="projects-error">
2786
- <span class="projects-error-message">${escapeProjectHtml(message)}</span>
2787
- <button onclick="reloadProjectlist()">Retry</button>
2788
- </div>
2789
- `;
2790
- }
2791
-
2792
- // Group projects by status for summary
2793
- function groupByStatus(projects, statuses) {
2794
- const groups = {};
2795
- for (const status of statuses) {
2796
- groups[status] = projects.filter(p => p.status === status);
2797
- }
2798
- return groups;
2799
- }
2800
-
2801
- // Render the status summary section
2802
- function renderStatusSummary(projects) {
2803
- const activeStatuses = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed'];
2804
- const active = projects.filter(p => activeStatuses.includes(p.status));
2805
- const completed = projects.filter(p => p.status === 'integrated');
2806
- const byStatus = groupByStatus(active, activeStatuses);
2807
-
2808
- const activeListItems = [];
2809
- for (const status of activeStatuses) {
2810
- const statusProjects = byStatus[status] || [];
2811
- if (statusProjects.length > 0) {
2812
- const names = statusProjects.slice(0, 3).map(p => `${p.id} ${p.title}`).join(', ');
2813
- const more = statusProjects.length > 3 ? ` (+${statusProjects.length - 3} more)` : '';
2814
- activeListItems.push(`<li>${statusProjects.length} ${status}: ${escapeProjectHtml(names)}${more}</li>`);
2815
- }
2816
- }
2817
-
2818
- return `
2819
- <div class="status-summary">
2820
- <div class="status-summary-header">
2821
- <span>Status Summary</span>
2822
- <button onclick="reloadProjectlist()" title="Reload">↻</button>
2823
- </div>
2824
- <div class="active-projects">
2825
- <span class="active-count">Active: ${active.length} project${active.length !== 1 ? 's' : ''}</span>
2826
- ${activeListItems.length > 0 ? `<ul class="active-list">${activeListItems.join('')}</ul>` : ''}
2827
- </div>
2828
- <div class="completed">Completed: ${completed.length} integrated</div>
2829
- </div>
2830
- `;
2831
- }
2832
-
2833
- // Get the lifecycle stages in order
2834
- const LIFECYCLE_STAGES = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed', 'integrated'];
2835
-
2836
- // Abbreviated column headers
2837
- const STAGE_HEADERS = {
2838
- 'conceived': "CONC'D",
2839
- 'specified': "SPEC'D",
2840
- 'planned': 'PLANNED',
2841
- 'implementing': 'IMPLING',
2842
- 'implemented': 'IMPLED',
2843
- 'committed': 'CMTD',
2844
- 'integrated': "INTGR'D"
2845
- };
2846
-
2847
- // Stage tooltips explaining purpose and exit criteria
2848
- const STAGE_TOOLTIPS = {
2849
- 'conceived': "CONCEIVED: Idea has been captured.\nExit: Human approves the specification.",
2850
- 'specified': "SPECIFIED: Human approved the spec.\nExit: Architect creates an implementation plan.",
2851
- 'planned': "PLANNED: Implementation plan is ready.\nExit: Architect spawns a Builder.",
2852
- 'implementing': "IMPLEMENTING: Builder is working on the code.\nExit: Builder creates a PR.",
2853
- 'implemented': "IMPLEMENTED: PR is ready for review.\nExit: Builder merges after Architect review.",
2854
- 'committed': "COMMITTED: PR has been merged.\nExit: Human validates in production.",
2855
- 'integrated': "INTEGRATED: Validated in production.\nThis is the goal state."
2856
- };
2857
-
2858
- // Get stage index (for comparison)
2859
- function getStageIndex(status) {
2860
- return LIFECYCLE_STAGES.indexOf(status);
2861
- }
2862
-
2863
- // Get the label and link for a stage cell
2864
- function getStageCellContent(project, stage) {
2865
- switch (stage) {
2866
- case 'specified':
2867
- if (project.files && project.files.spec) {
2868
- return { label: 'Spec', link: project.files.spec };
2869
- }
2870
- return { label: '', link: null };
2871
- case 'planned':
2872
- if (project.files && project.files.plan) {
2873
- return { label: 'Plan', link: project.files.plan };
2874
- }
2875
- return { label: '', link: null };
2876
- case 'implemented':
2877
- if (project.files && project.files.review) {
2878
- return { label: 'Revw', link: project.files.review };
2879
- }
2880
- return { label: '', link: null };
2881
- case 'committed':
2882
- // PR link from notes (format: "PR #N merged")
2883
- if (project.notes) {
2884
- const prMatch = project.notes.match(/PR\s*#?(\d+)/i);
2885
- if (prMatch) {
2886
- return { label: 'PR', link: `https://github.com/cluesmith/codev/pull/${prMatch[1]}`, external: true };
2887
- }
2888
- }
2889
- return { label: '', link: null };
2890
- default:
2891
- return { label: '', link: null };
2892
- }
2893
- }
2894
-
2895
- // Render a stage cell with appropriate styling
2896
- function renderStageCell(project, stage) {
2897
- const currentIndex = getStageIndex(project.status);
2898
- const stageIndex = getStageIndex(stage);
2899
-
2900
- let cellClass = 'stage-cell';
2901
- let content = '';
2902
- let ariaLabel = '';
2903
-
2904
- if (stageIndex < currentIndex) {
2905
- // Completed stage - green checkmark
2906
- ariaLabel = `${stage}: completed`;
2907
-
2908
- const cellContent = getStageCellContent(project, stage);
2909
- if (cellContent.label && cellContent.link) {
2910
- if (cellContent.external) {
2911
- content = `<span class="checkmark">✓</span> <a href="${cellContent.link}" target="_blank" rel="noopener">${cellContent.label}</a>`;
2912
- } else {
2913
- content = `<span class="checkmark">✓</span> <a href="#" onclick="openProjectFile('${cellContent.link}'); return false;">${cellContent.label}</a>`;
2914
- }
2915
- } else {
2916
- content = '<span class="checkmark">✓</span>';
2917
- }
2918
- } else if (stageIndex === currentIndex) {
2919
- // Current stage - hollow orange circle (or confetti if recently integrated)
2920
- if (stage === 'integrated' && isRecentlyIntegrated(project)) {
2921
- ariaLabel = `${stage}: recently completed!`;
2922
- content = '<span class="celebration">🎉</span>';
2923
- } else {
2924
- ariaLabel = `${stage}: in progress`;
2925
-
2926
- const cellContent = getStageCellContent(project, stage);
2927
- if (cellContent.label && cellContent.link) {
2928
- if (cellContent.external) {
2929
- content = `<span class="current-indicator"></span> <a href="${cellContent.link}" target="_blank" rel="noopener">${cellContent.label}</a>`;
2930
- } else {
2931
- content = `<span class="current-indicator"></span> <a href="#" onclick="openProjectFile('${cellContent.link}'); return false;">${cellContent.label}</a>`;
2932
- }
2933
- } else {
2934
- content = '<span class="current-indicator"></span>';
2935
- }
2936
- }
2937
- } else {
2938
- // Future stage - empty
2939
- ariaLabel = `${stage}: pending`;
2940
- }
2941
-
2942
- return `<td role="gridcell" class="${cellClass}" aria-label="${ariaLabel}">${content}</td>`;
2943
- }
2944
-
2945
- // Get URL for stage-specific artifact
2946
- function getStageLinkUrl(project, stage) {
2947
- if (!project.files) return null;
2948
-
2949
- switch (stage) {
2950
- case 'specified':
2951
- return project.files.spec || null;
2952
- case 'planned':
2953
- return project.files.plan || null;
2954
- case 'integrated':
2955
- return project.files.review || null;
2956
- default:
2957
- return null;
2958
- }
2959
- }
2960
-
2961
- // Open a project file in a new annotation tab
2962
- async function openProjectFile(path) {
2963
- try {
2964
- const response = await fetch('/api/tabs/file', {
2965
- method: 'POST',
2966
- headers: { 'Content-Type': 'application/json' },
2967
- body: JSON.stringify({ path })
2968
- });
2969
-
2970
- if (!response.ok) {
2971
- throw new Error(await response.text());
2972
- }
2973
-
2974
- await refresh();
2975
- showToast(`Opened ${path}`, 'success');
2976
- } catch (err) {
2977
- showToast('Failed to open file: ' + err.message, 'error');
2978
- }
2979
- }
2980
-
2981
- // Render a single project row
2982
- function renderProjectRow(project) {
2983
- const isExpanded = expandedProjectId === project.id;
2984
-
2985
- const row = `
2986
- <tr class="status-${project.status}"
2987
- role="row"
2988
- tabindex="0"
2989
- aria-expanded="${isExpanded}"
2990
- onkeydown="handleProjectRowKeydown(event, '${project.id}')">
2991
- <td role="gridcell">
2992
- <div class="project-cell clickable" onclick="toggleProjectDetails('${project.id}'); event.stopPropagation();">
2993
- <span class="project-id">${escapeProjectHtml(project.id)}</span>
2994
- <span class="project-title" title="${escapeProjectHtml(project.title)}">${escapeProjectHtml(project.title)}</span>
2995
- </div>
2996
- </td>
2997
- ${LIFECYCLE_STAGES.map(stage => renderStageCell(project, stage)).join('')}
2998
- </tr>
2999
- `;
3000
-
3001
- if (isExpanded) {
3002
- return row + renderProjectDetailsRow(project);
3003
- }
3004
- return row;
3005
- }
3006
-
3007
- // Render the details row when expanded
3008
- function renderProjectDetailsRow(project) {
3009
- const links = [];
3010
- if (project.files && project.files.review) {
3011
- links.push(`<a href="#" onclick="openProjectFile('${project.files.review}'); return false;">Review</a>`);
3012
- }
3013
-
3014
- const dependencies = project.dependencies && project.dependencies.length > 0
3015
- ? `<div class="project-dependencies">Dependencies: ${project.dependencies.map(d => escapeProjectHtml(d)).join(', ')}</div>`
3016
- : '';
3017
-
3018
- // Render TICKs if present
3019
- const ticks = project.ticks && project.ticks.length > 0
3020
- ? `<div class="project-ticks">TICKs: ${project.ticks.map(t => `<span class="tick-badge">TICK-${escapeProjectHtml(t)}</span>`).join(' ')}</div>`
3021
- : '';
3022
-
3023
- return `
3024
- <tr class="project-details-row" role="row">
3025
- <td colspan="8">
3026
- <div class="project-details-content">
3027
- <h3>${escapeProjectHtml(project.title)}</h3>
3028
- ${project.summary ? `<p>${escapeProjectHtml(project.summary)}</p>` : ''}
3029
- ${project.notes ? `<p class="notes">${escapeProjectHtml(project.notes)}</p>` : ''}
3030
- ${ticks}
3031
- ${links.length > 0 ? `<div class="project-details-links">${links.join('')}</div>` : ''}
3032
- ${dependencies}
3033
- </div>
3034
- </td>
3035
- </tr>
3036
- `;
3037
- }
3038
-
3039
- // Handle keyboard navigation on project rows
3040
- function handleProjectRowKeydown(event, projectId) {
3041
- if (event.key === 'Enter' || event.key === ' ') {
3042
- event.preventDefault();
3043
- toggleProjectDetails(projectId);
3044
- } else if (event.key === 'ArrowDown') {
3045
- event.preventDefault();
3046
- const currentRow = event.target.closest('tr');
3047
- let nextRow = currentRow.nextElementSibling;
3048
- // Skip details rows
3049
- while (nextRow && nextRow.classList.contains('project-details-row')) {
3050
- nextRow = nextRow.nextElementSibling;
3051
- }
3052
- if (nextRow) nextRow.focus();
3053
- } else if (event.key === 'ArrowUp') {
3054
- event.preventDefault();
3055
- const currentRow = event.target.closest('tr');
3056
- let prevRow = currentRow.previousElementSibling;
3057
- // Skip details rows
3058
- while (prevRow && prevRow.classList.contains('project-details-row')) {
3059
- prevRow = prevRow.previousElementSibling;
3060
- }
3061
- if (prevRow && prevRow.getAttribute('tabindex') === '0') prevRow.focus();
3062
- }
3063
- }
3064
-
3065
- // Toggle project details expansion
3066
- function toggleProjectDetails(projectId) {
3067
- if (expandedProjectId === projectId) {
3068
- expandedProjectId = null;
3069
- } else {
3070
- expandedProjectId = projectId;
3071
- }
3072
- // Re-render the projects tab to update expansion state
3073
- renderProjectsTabContent();
3074
- }
3075
-
3076
- // Render a table for a list of projects
3077
- function renderProjectTable(projectList) {
3078
- if (projectList.length === 0) {
3079
- return '<p style="color: var(--text-muted); text-align: center; padding: 20px;">No projects</p>';
3080
- }
3081
-
3082
- return `
3083
- <table class="kanban-grid" role="grid" aria-label="Project status grid">
3084
- <thead>
3085
- <tr role="row">
3086
- <th role="columnheader">Project</th>
3087
- ${LIFECYCLE_STAGES.map(stage => `<th role="columnheader" title="${STAGE_TOOLTIPS[stage]}">${STAGE_HEADERS[stage]}</th>`).join('')}
3088
- </tr>
3089
- </thead>
3090
- <tbody>
3091
- ${projectList.map(p => renderProjectRow(p)).join('')}
3092
- </tbody>
3093
- </table>
3094
- `;
3095
- }
3096
-
3097
- // Check if a project was integrated in the last 24 hours
3098
- function isRecentlyIntegrated(project) {
3099
- if (project.status !== 'integrated') return false;
3100
-
3101
- // Look in timestamps.integrated_at (new format)
3102
- const integratedAt = project.timestamps?.integrated_at;
3103
- if (!integratedAt) return false;
3104
-
3105
- const integratedDate = new Date(integratedAt);
3106
- if (isNaN(integratedDate.getTime())) return false;
3107
-
3108
- const now = new Date();
3109
- const hoursDiff = (now - integratedDate) / (1000 * 60 * 60);
3110
-
3111
- return hoursDiff <= 24;
3112
- }
3113
-
3114
- // Render the Kanban grid with Active/Inactive sections
3115
- function renderKanbanGrid(projects) {
3116
- // Separate active (conceived through committed) from inactive (integrated)
3117
- const activeStatuses = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed'];
3118
-
3119
- // Status order for sorting (higher index = further along)
3120
- const statusOrder = {
3121
- 'conceived': 0,
3122
- 'specified': 1,
3123
- 'planned': 2,
3124
- 'implementing': 3,
3125
- 'implemented': 4,
3126
- 'committed': 5,
3127
- 'integrated': 6
3128
- };
3129
-
3130
- // Include recently integrated projects in Active section
3131
- const activeProjects = projects.filter(p =>
3132
- activeStatuses.includes(p.status) || isRecentlyIntegrated(p)
3133
- );
3134
-
3135
- // Sort active projects by completion (most complete first)
3136
- activeProjects.sort((a, b) => {
3137
- const orderA = statusOrder[a.status] || 0;
3138
- const orderB = statusOrder[b.status] || 0;
3139
- // Higher status first (descending), then by ID (ascending) for tie-breaker
3140
- if (orderB !== orderA) return orderB - orderA;
3141
- return a.id.localeCompare(b.id);
3142
- });
3143
-
3144
- const inactiveProjects = projects.filter(p =>
3145
- p.status === 'integrated' && !isRecentlyIntegrated(p)
3146
- );
3147
-
3148
- let html = '';
3149
-
3150
- // Active section - expanded by default
3151
- if (activeProjects.length > 0 || inactiveProjects.length === 0) {
3152
- html += `
3153
- <details class="project-section" open>
3154
- <summary>Active <span class="section-count">(${activeProjects.length})</span></summary>
3155
- ${renderProjectTable(activeProjects)}
3156
- </details>
3157
- `;
3158
- }
3159
-
3160
- // Inactive section - collapsed by default
3161
- if (inactiveProjects.length > 0) {
3162
- html += `
3163
- <details class="project-section">
3164
- <summary>Completed <span class="section-count">(${inactiveProjects.length})</span></summary>
3165
- ${renderProjectTable(inactiveProjects)}
3166
- </details>
3167
- `;
3168
- }
3169
-
3170
- return html;
3171
- }
3172
-
3173
- // Render the terminal projects section (abandoned, on-hold)
3174
- function renderTerminalProjects(projects) {
3175
- const terminal = projects.filter(p => ['abandoned', 'on-hold'].includes(p.status));
3176
-
3177
- if (terminal.length === 0) return '';
3178
-
3179
- const items = terminal.map(p => {
3180
- const className = p.status === 'abandoned' ? 'project-abandoned' : 'project-on-hold';
3181
- const statusText = p.status === 'on-hold' ? ' (on-hold)' : '';
3182
- return `
3183
- <li>
3184
- <span class="${className}">
3185
- <span class="project-id">${escapeProjectHtml(p.id)}</span>
3186
- ${escapeProjectHtml(p.title)}${statusText}
3187
- </span>
3188
- </li>
3189
- `;
3190
- }).join('');
3191
-
3192
- return `
3193
- <details class="terminal-projects">
3194
- <summary>Terminal Projects (${terminal.length})</summary>
3195
- <ul>${items}</ul>
3196
- </details>
3197
- `;
3198
- }
3199
-
3200
- // ========================================
3201
- // Files Tab Functions (Spec 0055)
3202
- // ========================================
3203
-
3204
- // Load the file tree from the API
3205
- async function loadFilesTree() {
3206
- try {
3207
- const response = await fetch('/api/files');
3208
- if (!response.ok) {
3209
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
3210
- }
3211
- filesTreeData = await response.json();
3212
- filesTreeError = null;
3213
- filesTreeLoaded = true;
3214
- } catch (err) {
3215
- console.error('Failed to load files tree:', err);
3216
- filesTreeError = 'Could not load file tree: ' + err.message;
3217
- filesTreeData = [];
3218
- }
3219
- }
3220
-
3221
- // Escape a string for use inside a JavaScript string literal in onclick handlers
3222
- // This handles quotes, backslashes, and other special characters
3223
- function escapeJsString(str) {
3224
- return str
3225
- .replace(/\\/g, '\\\\')
3226
- .replace(/'/g, "\\'")
3227
- .replace(/"/g, '\\"')
3228
- .replace(/\n/g, '\\n')
3229
- .replace(/\r/g, '\\r');
3230
- }
3231
-
3232
- // Render tree nodes recursively
3233
- function renderTreeNodes(nodes, depth) {
3234
- if (!nodes || nodes.length === 0) return '';
3235
-
3236
- return nodes.map(node => {
3237
- const indent = depth * 16;
3238
- const isExpanded = filesTreeExpanded.has(node.path);
3239
- // Use escapeJsString for onclick handlers (handles quotes correctly)
3240
- // Use escapeHtml for data attributes and display text (handles XSS)
3241
- const jsPath = escapeJsString(node.path);
3242
-
3243
- if (node.type === 'dir') {
3244
- const icon = isExpanded ? '▼' : '▶';
3245
- const childrenHtml = node.children && node.children.length > 0
3246
- ? `<div class="tree-children ${isExpanded ? '' : 'collapsed'}" data-path="${escapeHtml(node.path)}">${renderTreeNodes(node.children, depth + 1)}</div>`
3247
- : '';
3248
-
3249
- return `
3250
- <div class="tree-item" data-type="dir" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="toggleFolder('${jsPath}')">
3251
- <span class="tree-item-icon folder-toggle">${icon}</span>
3252
- <span class="tree-item-name">${escapeHtml(node.name)}</span>
3253
- </div>
3254
- ${childrenHtml}
3255
- `;
3256
- } else {
3257
- return `
3258
- <div class="tree-item" data-type="file" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="openFileFromTree('${jsPath}')">
3259
- <span class="tree-item-icon">${getFileIcon(node.name)}</span>
3260
- <span class="tree-item-name">${escapeHtml(node.name)}</span>
3261
- </div>
3262
- `;
3263
- }
3264
- }).join('');
3265
- }
3266
-
3267
- // Get file icon based on extension
3268
- function getFileIcon(filename) {
3269
- const ext = filename.split('.').pop().toLowerCase();
3270
- const iconMap = {
3271
- 'js': '📜',
3272
- 'ts': '📜',
3273
- 'jsx': '⚛️',
3274
- 'tsx': '⚛️',
3275
- 'json': '{}',
3276
- 'md': '📝',
3277
- 'html': '🌐',
3278
- 'css': '🎨',
3279
- 'py': '🐍',
3280
- 'sh': '⚙️',
3281
- 'bash': '⚙️',
3282
- 'yml': '⚙️',
3283
- 'yaml': '⚙️',
3284
- 'png': '🖼️',
3285
- 'jpg': '🖼️',
3286
- 'jpeg': '🖼️',
3287
- 'gif': '🖼️',
3288
- 'svg': '🖼️',
3289
- };
3290
- return iconMap[ext] || '📄';
3291
- }
3292
-
3293
- // Toggle folder expanded/collapsed state
3294
- function toggleFolder(path) {
3295
- if (filesTreeExpanded.has(path)) {
3296
- filesTreeExpanded.delete(path);
3297
- } else {
3298
- filesTreeExpanded.add(path);
3299
- }
3300
- rerenderFilesBrowser();
3301
- }
3302
-
3303
- // Re-render file browser in current context (dashboard or files tab)
3304
- function rerenderFilesBrowser() {
3305
- if (activeTabId === 'dashboard') {
3306
- // Re-render just the files column in dashboard
3307
- const filesListEl = document.getElementById('dashboard-files-list');
3308
- if (filesListEl) {
3309
- filesListEl.innerHTML = renderDashboardFilesBrowser();
3310
- }
3311
- }
3312
- }
3313
-
3314
- // Collapse all folders
3315
- function collapseAllFolders() {
3316
- filesTreeExpanded.clear();
3317
- rerenderFilesBrowser();
3318
- }
3319
-
3320
- // Expand all folders
3321
- function expandAllFolders() {
3322
- function collectPaths(nodes) {
3323
- for (const node of nodes) {
3324
- if (node.type === 'dir') {
3325
- filesTreeExpanded.add(node.path);
3326
- if (node.children) {
3327
- collectPaths(node.children);
3328
- }
3329
- }
3330
- }
3331
- }
3332
- collectPaths(filesTreeData);
3333
- rerenderFilesBrowser();
3334
- }
3335
-
3336
- // Refresh files tree
3337
- async function refreshFilesTree() {
3338
- await loadFilesTree();
3339
- rerenderFilesBrowser();
3340
- showToast('Files refreshed', 'success');
3341
- }
3342
-
3343
- // Open file from tree click
3344
- async function openFileFromTree(filePath) {
3345
- try {
3346
- // Check if file is already open
3347
- const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3348
- if (existingTab) {
3349
- selectTab(existingTab.id);
3350
- return;
3351
- }
3352
-
3353
- // Open the file via API
3354
- const response = await fetch('/api/tabs/file', {
3355
- method: 'POST',
3356
- headers: { 'Content-Type': 'application/json' },
3357
- body: JSON.stringify({ path: filePath })
3358
- });
3359
-
3360
- if (!response.ok) {
3361
- throw new Error(await response.text());
3362
- }
3363
-
3364
- // Refresh state and switch to the new tab
3365
- await refresh();
3366
-
3367
- // Find and select the new file tab
3368
- const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3369
- if (newTab) {
3370
- selectTab(newTab.id);
3371
- }
3372
-
3373
- showToast(`Opened ${getFileName(filePath)}`, 'success');
3374
- } catch (err) {
3375
- showToast('Failed to open file: ' + err.message, 'error');
3376
- }
3377
- }
3378
-
3379
- // ========================================
3380
- // Projects Tab Functions (Spec 0045)
3381
- // ========================================
3382
-
3383
- // Render the info header with helpful links
3384
- function renderInfoHeader() {
3385
- return `
3386
- <div class="projects-info">
3387
- <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Agent Farm Dashboard</h1>
3388
- <p>Coordinate AI builders working on your codebase. The left panel shows the Architect terminal – tell it what you want to build. <strong>Tabs</strong> shows open terminals (Architect, Builders, utility shells). <strong>Files</strong> lets you browse and open project files. <strong>Projects</strong> tracks work as it moves from conception to integration.</p>
3389
- <p>Docs: <a href="#" onclick="openProjectFile('codev/resources/cheatsheet.md'); return false;">Cheatsheet</a> · <a href="#" onclick="openProjectFile('codev/resources/lifecycle.md'); return false;">Lifecycle</a> · <a href="#" onclick="openProjectFile('codev/resources/commands/overview.md'); return false;">CLI Reference</a> · <a href="#" onclick="openProjectFile('codev/protocols/spider/protocol.md'); return false;">SPIDER Protocol</a> · <a href="https://github.com/cluesmith/codev#readme" target="_blank">README</a> · <a href="https://discord.gg/mJ92DhDa6n" target="_blank">Discord</a></p>
3390
- </div>
3391
- `;
3392
- }
3393
-
3394
- // Render the dashboard tab content (internal - called after data is loaded)
3395
- function renderDashboardTabContent() {
3396
- const content = document.getElementById('tab-content');
3397
-
3398
- content.innerHTML = `
3399
- <div class="dashboard-container">
3400
- ${renderInfoHeader()}
3401
- <div class="dashboard-header">
3402
- <!-- Tabs Section -->
3403
- <div class="dashboard-section section-tabs ${sectionState.tabs ? '' : 'collapsed'}">
3404
- <div class="dashboard-section-header" onclick="toggleSection('tabs')">
3405
- <h3><span class="collapse-icon">▼</span> Tabs</h3>
3406
- </div>
3407
- <div class="dashboard-section-content">
3408
- <div class="dashboard-tabs-list" id="dashboard-tabs-list">
3409
- ${renderDashboardTabsList()}
3410
- </div>
3411
- </div>
3412
- </div>
3413
- <!-- Files Section -->
3414
- <div class="dashboard-section section-files ${sectionState.files ? '' : 'collapsed'}">
3415
- <div class="dashboard-section-header" onclick="toggleSection('files')">
3416
- <h3><span class="collapse-icon">▼</span> Files</h3>
3417
- <div class="header-actions" onclick="event.stopPropagation()">
3418
- <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3419
- <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3420
- </div>
3421
- </div>
3422
- <div class="dashboard-section-content">
3423
- <div class="dashboard-files-list" id="dashboard-files-list">
3424
- ${renderDashboardFilesBrowser()}
3425
- </div>
3426
- </div>
3427
- </div>
3428
- </div>
3429
- <!-- Projects Section -->
3430
- <div class="dashboard-section section-projects ${sectionState.projects ? '' : 'collapsed'}">
3431
- <div class="dashboard-section-header" onclick="toggleSection('projects')">
3432
- <h3><span class="collapse-icon">▼</span> Projects</h3>
3433
- </div>
3434
- <div class="dashboard-section-content" id="dashboard-projects">
3435
- ${renderDashboardProjectsSection()}
3436
- </div>
3437
- </div>
3438
- </div>
3439
- `;
3440
- }
3441
-
3442
- // Render the tabs list for dashboard
3443
- function renderDashboardTabsList() {
3444
- // Filter to show terminal tabs only (not Dashboard/Files tabs)
3445
- const terminalTabs = tabs.filter(t => t.type !== 'dashboard' && t.type !== 'files');
3446
-
3447
- if (terminalTabs.length === 0) {
3448
- return '<div class="dashboard-empty-state">No tabs open</div>';
3449
- }
3450
-
3451
- return terminalTabs.map(tab => {
3452
- const isActive = tab.id === activeTabId;
3453
- const icon = getTabIcon(tab.type);
3454
- const statusIndicator = getDashboardStatusIndicator(tab);
3455
-
3456
- return `
3457
- <div class="dashboard-tab-item ${isActive ? 'active' : ''}" onclick="selectTab('${tab.id}')">
3458
- ${statusIndicator}
3459
- <span class="tab-icon">${icon}</span>
3460
- <span class="tab-name">${escapeHtml(tab.name)}</span>
3461
- </div>
3462
- `;
3463
- }).join('');
3464
- }
3465
-
3466
- // Get status indicator for dashboard tab list
3467
- function getDashboardStatusIndicator(tab) {
3468
- if (tab.type !== 'builder') return '';
3469
-
3470
- // Use builder status from state
3471
- const builderState = (state.builders || []).find(b => `builder-${b.id}` === tab.id);
3472
- if (!builderState) return '';
3473
-
3474
- const status = builderState.status;
3475
- if (['spawning', 'implementing'].includes(status)) {
3476
- return '<span class="dashboard-status-indicator dashboard-status-working" title="Working"></span>';
3477
- }
3478
- if (status === 'blocked') {
3479
- return '<span class="dashboard-status-indicator dashboard-status-blocked" title="Blocked"></span>';
3480
- }
3481
- if (['pr-ready', 'complete'].includes(status)) {
3482
- return '<span class="dashboard-status-indicator dashboard-status-idle" title="Idle"></span>';
3483
- }
3484
- return '';
3485
- }
3486
-
3487
- // Render compact file browser for dashboard
3488
- function renderDashboardFilesBrowser() {
3489
- if (filesTreeError) {
3490
- return `<div class="dashboard-empty-state">${escapeHtml(filesTreeError)}</div>`;
3491
- }
3492
-
3493
- if (!filesTreeLoaded || filesTreeData.length === 0) {
3494
- return '<div class="dashboard-empty-state">Loading files...</div>';
3495
- }
3496
-
3497
- return renderTreeNodes(filesTreeData, 0);
3498
- }
3499
-
3500
- // Render the projects section for dashboard
3501
- function renderDashboardProjectsSection() {
3502
- if (projectlistError) {
3503
- return renderErrorBanner(projectlistError);
3504
- }
3505
-
3506
- if (projectsData.length === 0) {
3507
- // No welcome screen - just a helpful message
3508
- return `
3509
- <div class="dashboard-empty-state" style="padding: 24px;">
3510
- No projects yet. Ask the Architect to create your first project.
3511
- </div>
3512
- `;
3513
- }
3514
-
3515
- // Render the existing project view
3516
- return `
3517
- ${renderKanbanGrid(projectsData)}
3518
- ${renderTerminalProjects(projectsData)}
3519
- `;
3520
- }
3521
-
3522
- // Create new utility shell (quick action button)
3523
- async function createNewShell() {
3524
- try {
3525
- const response = await fetch('/api/tabs/shell', { method: 'POST' });
3526
- const data = await response.json();
3527
- if (!data.success && data.error) {
3528
- showToast(data.error || 'Failed to create shell', 'error');
3529
- return;
3530
- }
3531
- await refresh();
3532
- if (data.id) {
3533
- selectTab(`shell-${data.id}`);
3534
- }
3535
- showToast('Shell created', 'success');
3536
- } catch (err) {
3537
- showToast('Network error: ' + err.message, 'error');
3538
- }
3539
- }
3540
-
3541
- // Create new worktree shell (quick action button)
3542
- async function createNewWorktreeShell() {
3543
- const branch = prompt('Branch name (leave empty for temp worktree):');
3544
- if (branch === null) return; // User cancelled
3545
-
3546
- try {
3547
- const response = await fetch('/api/tabs/shell', {
3548
- method: 'POST',
3549
- headers: { 'Content-Type': 'application/json' },
3550
- body: JSON.stringify({ worktree: true, branch: branch || undefined })
3551
- });
3552
- const data = await response.json();
3553
- if (!data.success && data.error) {
3554
- showToast(data.error || 'Failed to create worktree shell', 'error');
3555
- return;
3556
- }
3557
- await refresh();
3558
- // Auto-select the newly created tab (consistent with createNewShell behavior)
3559
- if (data.id) {
3560
- selectTab(`shell-${data.id}`);
3561
- }
3562
- showToast('Worktree shell created', 'success');
3563
- } catch (err) {
3564
- showToast('Network error: ' + err.message, 'error');
3565
- }
3566
- }
3567
-
3568
- // Render the dashboard tab (entry point - loads data first)
3569
- async function renderDashboardTab() {
3570
- const content = document.getElementById('tab-content');
3571
- content.innerHTML = '<div class="dashboard-container"><p style="color: var(--text-muted); padding: 16px;">Loading dashboard...</p></div>';
3572
-
3573
- // Load both projectlist and files tree in parallel
3574
- await Promise.all([
3575
- loadProjectlist(),
3576
- loadFilesTreeIfNeeded()
3577
- ]);
3578
-
3579
- renderDashboardTabContent();
3580
- checkStarterMode(); // Update polling state after initial load
3581
- }
3582
-
3583
- // Load files tree if not already loaded
3584
- async function loadFilesTreeIfNeeded() {
3585
- if (!filesTreeLoaded) {
3586
- await loadFilesTree();
3587
- }
3588
- }
3589
-
3590
- // Legacy function for backward compatibility (still used by polling)
3591
- function renderProjectsTabContent() {
3592
- // If dashboard tab is active, re-render dashboard instead
3593
- if (activeTabId === 'dashboard') {
3594
- renderDashboardTabContent();
3595
- }
3596
- }
3597
-
3598
- // Legacy function alias
3599
- async function renderProjectsTab() {
3600
- await renderDashboardTab();
3601
- }
3602
-
3603
- // Load projectlist.md from disk
3604
- async function loadProjectlist() {
3605
- try {
3606
- const response = await fetch('/file?path=codev/projectlist.md');
3607
-
3608
- if (!response.ok) {
3609
- if (response.status === 404) {
3610
- // File not found - show welcome screen
3611
- projectsData = [];
3612
- projectlistError = null;
3613
- return;
3614
- }
3615
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
3616
- }
3617
-
3618
- const text = await response.text();
3619
- const newHash = hashString(text);
3620
-
3621
- // Only re-parse if content changed
3622
- if (newHash !== projectlistHash) {
3623
- projectlistHash = newHash;
3624
- projectsData = parseProjectlist(text);
3625
- projectlistError = null;
3626
- }
3627
- } catch (err) {
3628
- console.error('Failed to load projectlist:', err);
3629
- projectlistError = 'Could not load projectlist.md: ' + err.message;
3630
- // Preserve last good state if available
3631
- if (projectsData.length === 0) {
3632
- projectsData = [];
3633
- }
3634
- }
3635
- }
3636
-
3637
- // Reload projectlist (manual refresh button)
3638
- async function reloadProjectlist() {
3639
- projectlistHash = null; // Force re-parse
3640
- await loadProjectlist();
3641
- renderProjectsTabContent();
3642
- checkStarterMode(); // Update polling state after reload
3643
- }
3644
-
3645
- // Poll projectlist for changes (every 5 seconds)
3646
- async function pollProjectlist() {
3647
- // Only poll if dashboard tab is active
3648
- if (activeTabId !== 'dashboard') return;
3649
-
3650
- try {
3651
- const response = await fetch('/file?path=codev/projectlist.md');
3652
- if (!response.ok) return;
3653
-
3654
- const text = await response.text();
3655
- const newHash = hashString(text);
3656
-
3657
- if (newHash !== projectlistHash) {
3658
- // Content changed - debounce to avoid reading mid-write
3659
- clearTimeout(projectlistDebounce);
3660
- projectlistDebounce = setTimeout(async () => {
3661
- projectlistHash = newHash;
3662
- projectsData = parseProjectlist(text);
3663
- projectlistError = null;
3664
- renderProjectsTabContent();
3665
- checkStarterMode(); // Update polling state after content change
3666
- }, 500);
3667
- }
3668
- } catch (err) {
3669
- // Silently ignore polling errors
3670
- }
3671
- }
3672
-
3673
- // Poll for projectlist.md creation when in starter mode (every 15 seconds)
3674
- let starterModePollingInterval = null;
3675
-
3676
- async function pollForProjectlistCreation() {
3677
- try {
3678
- const response = await fetch('/api/projectlist-exists');
3679
- if (!response.ok) return;
3680
-
3681
- const { exists } = await response.json();
3682
- if (exists) {
3683
- // projectlist.md was created - stop polling and reload
3684
- if (starterModePollingInterval) {
3685
- clearInterval(starterModePollingInterval);
3686
- starterModePollingInterval = null;
3687
- }
3688
- window.location.reload();
3689
- }
3690
- } catch (err) {
3691
- // Silently ignore polling errors
3692
- }
3693
- }
3694
-
3695
- // Check if we should start starter mode polling
3696
- function checkStarterMode() {
3697
- // We're in starter mode ONLY if:
3698
- // 1. projectsData is empty (no projects loaded)
3699
- // 2. No error occurred
3700
- // 3. projectlistHash is null (file was not found, not just empty)
3701
- // This prevents infinite reload loop when file exists but is empty
3702
- const isStarterMode = projectsData.length === 0 && !projectlistError && projectlistHash === null;
3703
-
3704
- if (isStarterMode && !starterModePollingInterval) {
3705
- // Start polling for projectlist.md creation
3706
- starterModePollingInterval = setInterval(pollForProjectlistCreation, 15000);
3707
- } else if (!isStarterMode && starterModePollingInterval) {
3708
- // Stop polling - file exists now (even if empty)
3709
- clearInterval(starterModePollingInterval);
3710
- starterModePollingInterval = null;
3711
- }
3712
- }
3713
-
3714
- // Start projectlist polling (separate from main state polling)
3715
- setInterval(pollProjectlist, 5000);
3716
-
3717
- // Initialize on load
3718
- init();
3719
- </script>
3720
- </body>
3721
- </html>