bobs-workshop 0.1.8 → 0.2.1

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.
@@ -0,0 +1,2805 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🔧 Bob's Workshop - Manual Management Dashboard</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
8
+
9
+ <!-- Google Fonts: Poppins and Outfit -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
+
14
+ <style>
15
+ :root {
16
+ /* Color System */
17
+ --bg-primary: linear-gradient(to bottom, #000000 0%, #1a1a1a 50%, #2d2d30 100%);
18
+ --bg-glass: rgba(255, 255, 255, 0.1);
19
+ --bg-glass-hover: rgba(255, 255, 255, 0.15);
20
+ --border-glass: rgba(255, 255, 255, 0.2);
21
+ --text-primary: #ffffff;
22
+ --text-secondary: rgba(255, 255, 255, 0.8);
23
+ --text-muted: rgba(255, 255, 255, 0.6);
24
+ --accent-blue: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
25
+ --accent-green: #10b981;
26
+ --accent-orange: #f59e0b;
27
+ --accent-yellow: #f59e0b;
28
+ --accent-red: #ef4444;
29
+
30
+ /* Typography */
31
+ --font-heading: 'Poppins', sans-serif;
32
+ --font-body: 'Outfit', sans-serif;
33
+ }
34
+
35
+ * {
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ body {
40
+ margin: 0;
41
+ padding: 0;
42
+ font-family: var(--font-body);
43
+ background: var(--bg-primary);
44
+ color: var(--text-primary);
45
+ min-height: 100vh;
46
+ overflow-x: hidden;
47
+ zoom: 0.69;
48
+ }
49
+
50
+ /* Glass morphism utility classes */
51
+ .glass {
52
+ background: var(--bg-glass);
53
+ backdrop-filter: blur(12px);
54
+ -webkit-backdrop-filter: blur(12px);
55
+ border: 1px solid var(--border-glass);
56
+ border-radius: 16px;
57
+ }
58
+
59
+ .glass-hover {
60
+ transition: all 0.3s ease;
61
+ }
62
+
63
+ .glass-hover:hover {
64
+ background: var(--bg-glass-hover);
65
+ transform: translateY(-2px);
66
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
67
+ }
68
+
69
+ /* Typography */
70
+ .heading {
71
+ font-family: var(--font-heading);
72
+ font-weight: 600;
73
+ }
74
+
75
+ .text-gradient {
76
+ background: var(--accent-blue);
77
+ -webkit-background-clip: text;
78
+ -webkit-text-fill-color: transparent;
79
+ background-clip: text;
80
+ }
81
+
82
+ /* Header */
83
+ .header {
84
+ background: var(--bg-glass);
85
+ backdrop-filter: blur(20px);
86
+ border-bottom: 1px solid var(--border-glass);
87
+ padding: 1.5rem 2rem;
88
+ display: flex;
89
+ justify-content: space-between;
90
+ align-items: center;
91
+ position: sticky;
92
+ top: 0;
93
+ z-index: 50;
94
+ }
95
+
96
+ .logo {
97
+ font-family: var(--font-heading);
98
+ font-size: 1.5rem;
99
+ font-weight: 700;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 0.5rem;
103
+ }
104
+
105
+ /* Metrics Cards */
106
+ .metrics-grid {
107
+ display: grid;
108
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
109
+ gap: 1.5rem;
110
+ padding: 2rem;
111
+ }
112
+
113
+ .metric-card {
114
+ padding: 1.5rem;
115
+ background: var(--bg-glass);
116
+ backdrop-filter: blur(12px);
117
+ border-radius: 16px;
118
+ border: 1px solid var(--border-glass);
119
+ transition: all 0.3s ease;
120
+ }
121
+
122
+ .metric-card:hover {
123
+ background: var(--bg-glass-hover);
124
+ transform: translateY(-4px);
125
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
126
+ }
127
+
128
+ .metric-value {
129
+ font-family: var(--font-heading);
130
+ font-size: 2.5rem;
131
+ font-weight: 700;
132
+ margin-bottom: 0.5rem;
133
+ }
134
+
135
+ .metric-label {
136
+ font-size: 0.875rem;
137
+ color: var(--text-secondary);
138
+ text-transform: uppercase;
139
+ letter-spacing: 0.05em;
140
+ }
141
+
142
+ /* Spec Table */
143
+ .spec-container {
144
+ margin: 2rem;
145
+ background: var(--bg-glass);
146
+ backdrop-filter: blur(12px);
147
+ border-radius: 20px;
148
+ border: 1px solid var(--border-glass);
149
+ border-top: 3px solid var(--accent-orange);
150
+ overflow: hidden;
151
+ }
152
+
153
+ .spec-header {
154
+ padding: 1.5rem 2rem;
155
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
156
+ border-bottom: 1px solid var(--border-glass);
157
+ display: flex;
158
+ justify-content: space-between;
159
+ align-items: center;
160
+ }
161
+
162
+ .spec-table {
163
+ width: 100%;
164
+ border-collapse: collapse;
165
+ }
166
+
167
+ .spec-table th {
168
+ background: rgba(255, 255, 255, 0.05);
169
+ padding: 1rem 1.5rem;
170
+ text-align: left;
171
+ font-family: var(--font-heading);
172
+ font-weight: 600;
173
+ font-size: 0.875rem;
174
+ color: var(--text-secondary);
175
+ text-transform: uppercase;
176
+ letter-spacing: 0.05em;
177
+ border-bottom: 1px solid var(--border-glass);
178
+ cursor: pointer;
179
+ user-select: none;
180
+ position: relative;
181
+ transition: all 0.2s ease;
182
+ }
183
+
184
+ .spec-table th:hover {
185
+ background: rgba(255, 255, 255, 0.1);
186
+ color: var(--text-primary);
187
+ }
188
+
189
+ .spec-table th.sortable::after {
190
+ content: '↕';
191
+ position: absolute;
192
+ right: 0.5rem;
193
+ opacity: 0.3;
194
+ font-size: 0.8rem;
195
+ }
196
+
197
+ .spec-table th.sorted-asc::after {
198
+ content: '↑';
199
+ opacity: 1;
200
+ color: var(--accent-blue);
201
+ }
202
+
203
+ .spec-table th.sorted-desc::after {
204
+ content: '↓';
205
+ opacity: 1;
206
+ color: var(--accent-blue);
207
+ }
208
+
209
+ .spec-table td {
210
+ padding: 1rem 1.5rem;
211
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
212
+ transition: all 0.2s ease;
213
+ }
214
+
215
+ .spec-table tr:hover td {
216
+ background: rgba(255, 255, 255, 0.02);
217
+ }
218
+
219
+ /* Status badges */
220
+ .status-badge {
221
+ padding: 0.25rem 0.75rem;
222
+ border-radius: 20px;
223
+ font-size: 0.75rem;
224
+ font-weight: 600;
225
+ text-transform: uppercase;
226
+ letter-spacing: 0.05em;
227
+ }
228
+
229
+ .status-draft { background: rgba(156, 163, 175, 0.2); color: #d1d5db; }
230
+ .status-planning { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
231
+ .status-implementation { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
232
+ .status-review { background: rgba(147, 51, 234, 0.2); color: #a855f7; }
233
+ .status-completed { background: rgba(16, 185, 129, 0.2); color: #34d399; }
234
+ /* Legacy support */
235
+ .status-in-progress { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
236
+
237
+ /* Buttons */
238
+ .btn {
239
+ padding: 0.75rem 1.5rem;
240
+ border-radius: 12px;
241
+ font-family: var(--font-heading);
242
+ font-weight: 600;
243
+ font-size: 0.875rem;
244
+ border: none;
245
+ cursor: pointer;
246
+ transition: all 0.3s ease;
247
+ display: inline-flex;
248
+ align-items: center;
249
+ gap: 0.5rem;
250
+ }
251
+
252
+ .btn-primary {
253
+ background: var(--accent-blue);
254
+ color: white;
255
+ }
256
+
257
+ .btn-primary:hover {
258
+ transform: translateY(-2px);
259
+ box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4);
260
+ }
261
+
262
+ .btn-secondary {
263
+ background: var(--bg-glass);
264
+ backdrop-filter: blur(12px);
265
+ color: var(--text-primary);
266
+ border: 1px solid var(--border-glass);
267
+ }
268
+
269
+ .btn-secondary:hover {
270
+ background: var(--bg-glass-hover);
271
+ transform: translateY(-2px);
272
+ }
273
+
274
+ /* Modal */
275
+ .modal-overlay {
276
+ position: fixed;
277
+ inset: 0;
278
+ background: rgba(0, 0, 0, 0.8);
279
+ backdrop-filter: blur(8px);
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ z-index: 100;
284
+ padding: 1rem;
285
+ }
286
+
287
+ .modal-content {
288
+ background: var(--bg-glass);
289
+ backdrop-filter: blur(20px);
290
+ border: 1px solid var(--border-glass);
291
+ border-radius: 24px;
292
+ width: 99vw;
293
+ height: 92vh; /* Reduced from 98vh to 92vh */
294
+ max-width: none;
295
+ min-width: 1728px; /* Increased from 1440px by 20% (1440 * 1.2) */
296
+ min-height: 1000px; /* Reduced from 1152px */
297
+ display: grid;
298
+ grid-template-columns: 60% 40%;
299
+ grid-template-rows: auto 1fr;
300
+ overflow: hidden;
301
+ }
302
+
303
+ .modal-header {
304
+ grid-column: 1 / -1;
305
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
306
+ padding: 1.5rem 2rem;
307
+ border-bottom: 1px solid var(--border-glass);
308
+ display: flex;
309
+ justify-content: space-between;
310
+ align-items: center;
311
+ }
312
+
313
+ .modal-title-section {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 1rem;
317
+ }
318
+
319
+ .worktree-info {
320
+ background: rgba(255, 255, 255, 0.1);
321
+ padding: 0.5rem 1rem;
322
+ border-radius: 20px;
323
+ font-size: 0.875rem;
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 0.5rem;
327
+ }
328
+
329
+ .modal-main-container {
330
+ display: flex;
331
+ flex-direction: column;
332
+ overflow: hidden;
333
+ height: 100%; /* Ensure full height usage */
334
+ }
335
+
336
+ .modal-nav {
337
+ padding: 1rem 2rem 0;
338
+ border-bottom: 1px solid var(--border-glass);
339
+ }
340
+
341
+ .nav-tabs {
342
+ display: flex;
343
+ gap: 0.5rem;
344
+ margin-bottom: 1rem;
345
+ overflow-x: auto;
346
+ scrollbar-width: thin;
347
+ scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
348
+ padding-bottom: 0.5rem;
349
+ }
350
+
351
+ .nav-tabs::-webkit-scrollbar {
352
+ height: 4px;
353
+ }
354
+
355
+ .nav-tabs::-webkit-scrollbar-track {
356
+ background: rgba(255, 255, 255, 0.1);
357
+ border-radius: 2px;
358
+ }
359
+
360
+ .nav-tabs::-webkit-scrollbar-thumb {
361
+ background: rgba(255, 255, 255, 0.3);
362
+ border-radius: 2px;
363
+ }
364
+
365
+ .nav-tabs::-webkit-scrollbar-thumb:hover {
366
+ background: rgba(255, 255, 255, 0.5);
367
+ }
368
+
369
+ .nav-tab {
370
+ padding: 0.5rem 1rem;
371
+ border-radius: 20px;
372
+ background: transparent;
373
+ border: 1px solid rgba(255, 255, 255, 0.1);
374
+ color: var(--text-secondary);
375
+ font-family: var(--font-body);
376
+ font-size: 0.875rem;
377
+ cursor: pointer;
378
+ transition: all 0.2s ease;
379
+ white-space: nowrap;
380
+ flex-shrink: 0;
381
+ min-width: fit-content;
382
+ }
383
+
384
+ .nav-tab:hover {
385
+ background: rgba(255, 255, 255, 0.05);
386
+ color: var(--text-primary);
387
+ }
388
+
389
+ .nav-tab.active {
390
+ background: var(--accent-blue);
391
+ color: white;
392
+ border-color: var(--accent-blue);
393
+ }
394
+
395
+ .modal-main {
396
+ padding: 2rem;
397
+ overflow-y: auto;
398
+ flex: 1; /* Take all available space */
399
+ min-height: 0; /* Allow flex shrinking */
400
+ }
401
+
402
+ .spec-content {
403
+ background: rgba(255, 255, 255, 0.03);
404
+ padding: 2.5rem 3.5rem 2.5rem 3.5rem; /* Increased left padding for better breathing room */
405
+ border-radius: 12px;
406
+ border: 1px solid rgba(255, 255, 255, 0.05);
407
+ font-family: var(--font-body);
408
+ line-height: 1.7; /* Improved line height for better readability */
409
+ overflow-y: auto;
410
+ height: 100%; /* Use full available height */
411
+ }
412
+
413
+ .spec-content h1, .spec-content h2, .spec-content h3 {
414
+ color: var(--text-primary);
415
+ margin-top: 2.5rem; /* Increased top margin */
416
+ margin-bottom: 1.5rem; /* Increased bottom margin */
417
+ font-family: var(--font-heading);
418
+ }
419
+
420
+ .spec-content h1:first-child,
421
+ .spec-content h2:first-child,
422
+ .spec-content h3:first-child {
423
+ margin-top: 0; /* Remove top margin from first heading */
424
+ }
425
+
426
+ .spec-content h1 { font-size: 1.5rem; }
427
+ .spec-content h2 { font-size: 1.3rem; }
428
+ .spec-content h3 { font-size: 1.1rem; }
429
+
430
+ .spec-content p {
431
+ margin-bottom: 1.5rem; /* Increased paragraph spacing */
432
+ color: var(--text-secondary);
433
+ }
434
+
435
+ .spec-content ul, .spec-content ol {
436
+ margin-bottom: 1.5rem; /* Increased list spacing */
437
+ padding-left: 2rem; /* Better indentation */
438
+ color: var(--text-secondary);
439
+ }
440
+
441
+ .spec-content li {
442
+ margin-bottom: 0.75rem; /* Increased list item spacing */
443
+ line-height: 1.6;
444
+ }
445
+
446
+ .spec-content code {
447
+ background: rgba(255, 255, 255, 0.1);
448
+ padding: 0.2rem 0.4rem;
449
+ border-radius: 4px;
450
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
451
+ font-size: 0.9em;
452
+ color: var(--accent-blue);
453
+ }
454
+
455
+ .spec-content pre {
456
+ background: rgba(0, 0, 0, 0.4);
457
+ padding: 1.5rem; /* Increased padding for code blocks */
458
+ border-radius: 8px;
459
+ overflow-x: auto;
460
+ margin: 2rem 0; /* Increased margin around code blocks */
461
+ border: 1px solid rgba(255, 255, 255, 0.15);
462
+ line-height: 1.5;
463
+ }
464
+
465
+ .spec-content pre code {
466
+ background: none;
467
+ padding: 0;
468
+ color: var(--text-primary);
469
+ }
470
+
471
+ .spec-content blockquote {
472
+ border-left: 3px solid var(--accent-blue);
473
+ padding-left: 1rem;
474
+ margin: 1rem 0;
475
+ font-style: italic;
476
+ color: var(--text-secondary);
477
+ }
478
+
479
+ .spec-content table {
480
+ width: 100%;
481
+ border-collapse: collapse;
482
+ margin: 1rem 0;
483
+ }
484
+
485
+ .spec-content th, .spec-content td {
486
+ border: 1px solid rgba(255, 255, 255, 0.1);
487
+ padding: 0.75rem;
488
+ text-align: left;
489
+ }
490
+
491
+ .spec-content th {
492
+ background: rgba(255, 255, 255, 0.05);
493
+ font-weight: 600;
494
+ color: var(--text-primary);
495
+ }
496
+
497
+ .spec-content a {
498
+ color: var(--accent-blue);
499
+ text-decoration: underline;
500
+ }
501
+
502
+ .spec-content a:hover {
503
+ color: #60a5fa;
504
+ }
505
+
506
+ .modal-activity {
507
+ border-left: 1px solid var(--border-glass);
508
+ overflow: hidden;
509
+ background: rgba(0, 0, 0, 0.1);
510
+ display: flex;
511
+ flex-direction: column;
512
+ }
513
+
514
+ .activity-nav {
515
+ padding: 1rem 1.5rem 0.5rem;
516
+ }
517
+
518
+ .activity-tabs {
519
+ display: flex;
520
+ gap: 0.5rem;
521
+ margin-bottom: 1rem;
522
+ }
523
+
524
+ .activity-tab {
525
+ padding: 0.5rem 1rem;
526
+ border-radius: 20px;
527
+ background: transparent;
528
+ border: 1px solid rgba(255, 255, 255, 0.1);
529
+ color: var(--text-secondary);
530
+ font-family: var(--font-body);
531
+ font-size: 0.875rem;
532
+ cursor: pointer;
533
+ transition: all 0.2s ease;
534
+ }
535
+
536
+ .activity-tab:hover {
537
+ background: rgba(255, 255, 255, 0.05);
538
+ color: var(--text-primary);
539
+ }
540
+
541
+ .activity-tab.active {
542
+ background: var(--accent-blue);
543
+ color: white;
544
+ border-color: var(--accent-blue);
545
+ }
546
+
547
+ .activity-content {
548
+ flex: 1;
549
+ padding: 1.5rem;
550
+ overflow-y: auto;
551
+ height: 100%; /* Use full available height */
552
+ min-height: 0; /* Allow flex shrinking */
553
+ }
554
+
555
+ .sidebar-nav {
556
+ list-style: none;
557
+ padding: 0;
558
+ margin: 0;
559
+ }
560
+
561
+ .sidebar-nav li {
562
+ margin-bottom: 0.5rem;
563
+ }
564
+
565
+ .sidebar-nav button {
566
+ width: 100%;
567
+ text-align: left;
568
+ padding: 0.75rem;
569
+ border-radius: 8px;
570
+ background: transparent;
571
+ border: none;
572
+ color: var(--text-secondary);
573
+ font-family: var(--font-body);
574
+ cursor: pointer;
575
+ transition: all 0.2s ease;
576
+ }
577
+
578
+ .sidebar-nav button:hover {
579
+ background: rgba(255, 255, 255, 0.05);
580
+ color: var(--text-primary);
581
+ }
582
+
583
+ .sidebar-nav button.active {
584
+ background: var(--accent-blue);
585
+ color: white;
586
+ }
587
+
588
+ /* Timeline Activity Layout */
589
+ .timeline-container {
590
+ position: relative;
591
+ padding: 1rem 0;
592
+ }
593
+
594
+ .timeline-line {
595
+ position: absolute;
596
+ left: 200px; /* Align with fixed left column width */
597
+ top: 0;
598
+ bottom: 0;
599
+ width: 2px;
600
+ background: linear-gradient(to bottom, transparent, var(--accent-blue), transparent);
601
+ z-index: 1;
602
+ }
603
+
604
+ .timeline-item {
605
+ display: flex;
606
+ margin-bottom: 2.5rem; /* Increased spacing between timeline items */
607
+ position: relative;
608
+ align-items: flex-start; /* Better alignment */
609
+ }
610
+
611
+ .timeline-left {
612
+ flex: 0 0 200px; /* Fixed width for better alignment */
613
+ text-align: right;
614
+ padding-right: 1.5rem;
615
+ display: flex;
616
+ flex-direction: column;
617
+ justify-content: center;
618
+ min-height: 60px; /* Ensure minimum height */
619
+ }
620
+
621
+ .timeline-right {
622
+ flex: 1;
623
+ padding-left: 1.5rem;
624
+ min-width: 0; /* Allow content to shrink properly */
625
+ }
626
+
627
+ .timeline-marker {
628
+ position: absolute;
629
+ left: 200px; /* Align with timeline line */
630
+ top: 50%;
631
+ transform: translate(-50%, -50%);
632
+ width: 12px;
633
+ height: 12px;
634
+ border-radius: 50%;
635
+ background: var(--accent-blue);
636
+ border: 3px solid var(--bg-primary);
637
+ box-shadow: 0 0 0 2px var(--accent-blue);
638
+ z-index: 2;
639
+ }
640
+
641
+ .timeline-marker.debugger { background: var(--accent-red); box-shadow: 0 0 0 2px var(--accent-red); }
642
+ .timeline-marker.engineer { background: var(--accent-green); box-shadow: 0 0 0 2px var(--accent-green); }
643
+ .timeline-marker.architect { background: var(--accent-orange); box-shadow: 0 0 0 2px var(--accent-orange); }
644
+
645
+ .timeline-time {
646
+ font-size: 0.8rem; /* Slightly larger font */
647
+ color: var(--text-muted);
648
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
649
+ margin-bottom: 0.75rem; /* More space between time and role */
650
+ line-height: 1.4;
651
+ }
652
+
653
+ .timeline-role {
654
+ font-weight: 600;
655
+ color: var(--text-primary);
656
+ margin-bottom: 0.5rem; /* More space after role */
657
+ font-size: 0.9rem;
658
+ }
659
+
660
+ .timeline-content {
661
+ background: rgba(255, 255, 255, 0.05);
662
+ border-radius: 12px;
663
+ padding: 1.5rem 1.75rem; /* Increased padding for better readability */
664
+ border: 1px solid rgba(255, 255, 255, 0.1);
665
+ backdrop-filter: blur(8px);
666
+ transition: all 0.2s ease;
667
+ margin-bottom: 0.5rem; /* Space between content boxes */
668
+ }
669
+
670
+ .timeline-content:hover {
671
+ background: rgba(255, 255, 255, 0.08);
672
+ border-color: rgba(255, 255, 255, 0.2);
673
+ transform: translateY(-1px);
674
+ }
675
+
676
+ .activity-role {
677
+ font-weight: 600;
678
+ color: var(--text-primary);
679
+ margin-bottom: 0.25rem;
680
+ }
681
+
682
+ .activity-note {
683
+ font-size: 0.9rem; /* Slightly larger for better readability */
684
+ color: var(--text-secondary);
685
+ line-height: 1.5;
686
+ margin-bottom: 0.5rem;
687
+ }
688
+
689
+ /* Responsive design */
690
+ @media (max-width: 768px) {
691
+ .metrics-grid {
692
+ grid-template-columns: 1fr;
693
+ padding: 1rem;
694
+ }
695
+
696
+ .spec-container {
697
+ margin: 1rem;
698
+ }
699
+
700
+ .modal-content {
701
+ grid-template-columns: 1fr;
702
+ grid-template-rows: auto auto 1fr auto;
703
+ }
704
+
705
+ .modal-sidebar,
706
+ .modal-activity {
707
+ border: none;
708
+ border-top: 1px solid var(--border-glass);
709
+ }
710
+ }
711
+
712
+ /* Loading animation */
713
+ .loading {
714
+ display: inline-block;
715
+ width: 20px;
716
+ height: 20px;
717
+ border: 2px solid rgba(255, 255, 255, 0.3);
718
+ border-radius: 50%;
719
+ border-top-color: #ffffff;
720
+ animation: spin 1s ease-in-out infinite;
721
+ }
722
+
723
+ @keyframes spin {
724
+ to { transform: rotate(360deg); }
725
+ }
726
+
727
+ /* Fade in animation */
728
+ .fade-in {
729
+ animation: fadeIn 0.5s ease-out;
730
+ }
731
+
732
+ @keyframes fadeIn {
733
+ from { opacity: 0; transform: translateY(20px); }
734
+ to { opacity: 1; transform: translateY(0); }
735
+ }
736
+
737
+ /* Status Banner */
738
+ .status-banner {
739
+ background: var(--bg-glass);
740
+ backdrop-filter: blur(12px);
741
+ border: 1px solid var(--border-glass);
742
+ border-left: 4px solid var(--accent-orange);
743
+ border-radius: 16px;
744
+ margin: 1.5rem 2rem;
745
+ padding: 1.5rem 2rem;
746
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
747
+ }
748
+
749
+ .status-content {
750
+ display: grid;
751
+ grid-template-columns: 1fr auto 1fr;
752
+ align-items: center;
753
+ gap: 1rem;
754
+ }
755
+
756
+ .status-left {
757
+ display: flex;
758
+ align-items: center;
759
+ gap: 0.75rem;
760
+ font-size: 0.95rem;
761
+ }
762
+
763
+ .workshop-icon {
764
+ font-size: 1.2rem;
765
+ }
766
+
767
+ .workshop-name {
768
+ font-weight: 600;
769
+ color: var(--text-primary);
770
+ }
771
+
772
+ .status-separator {
773
+ color: var(--text-muted);
774
+ margin: 0 0.25rem;
775
+ }
776
+
777
+ .build-status .building {
778
+ color: var(--accent-yellow);
779
+ display: flex;
780
+ align-items: center;
781
+ gap: 0.5rem;
782
+ }
783
+
784
+ .build-status .idle {
785
+ color: var(--text-secondary);
786
+ display: flex;
787
+ align-items: center;
788
+ gap: 0.5rem;
789
+ }
790
+
791
+ .building-icon {
792
+ animation: spin 2s linear infinite;
793
+ }
794
+
795
+ .status-right {
796
+ font-size: 0.9rem;
797
+ }
798
+
799
+ .worktree-stats {
800
+ display: flex;
801
+ align-items: center;
802
+ gap: 0.5rem;
803
+ flex-wrap: wrap;
804
+ }
805
+
806
+ .stat-label {
807
+ color: var(--text-secondary);
808
+ font-weight: 600;
809
+ }
810
+
811
+ .stat-value {
812
+ color: var(--text-primary);
813
+ }
814
+
815
+ .stat-value.draft {
816
+ color: #9ca3af;
817
+ }
818
+
819
+ .stat-value.in-progress {
820
+ color: var(--accent-yellow);
821
+ }
822
+
823
+ .stat-value.done {
824
+ color: var(--accent-green);
825
+ }
826
+
827
+ .stat-value.committed {
828
+ color: var(--accent-green);
829
+ }
830
+
831
+ .stat-value.dirty {
832
+ color: var(--accent-yellow);
833
+ }
834
+
835
+ .stat-value.clean {
836
+ color: var(--text-secondary);
837
+ }
838
+
839
+ /* Responsive adjustments for status banner */
840
+ @media (max-width: 768px) {
841
+ .status-banner {
842
+ margin: 1rem;
843
+ padding: 1rem;
844
+ }
845
+
846
+ .status-content {
847
+ flex-direction: column;
848
+ align-items: flex-start;
849
+ }
850
+
851
+ .worktree-stats {
852
+ font-size: 0.8rem;
853
+ }
854
+ }
855
+
856
+ /* tmux Connect Modal Styles */
857
+ .session-item {
858
+ display: flex;
859
+ align-items: center;
860
+ justify-content: space-between;
861
+ padding: 1rem;
862
+ border-bottom: 1px solid var(--border-glass);
863
+ margin-bottom: 0.5rem;
864
+ background: rgba(0, 0, 0, 0.2);
865
+ border-radius: 8px;
866
+ transition: all 0.2s ease;
867
+ }
868
+
869
+ .session-item:hover {
870
+ background: rgba(0, 0, 0, 0.3);
871
+ transform: translateY(-1px);
872
+ }
873
+
874
+ .session-item.last-item {
875
+ border-bottom: none;
876
+ margin-bottom: 0;
877
+ }
878
+
879
+ .session-info {
880
+ flex: 1;
881
+ }
882
+
883
+ .session-name {
884
+ font-weight: 600;
885
+ color: var(--text-primary);
886
+ font-size: 1rem;
887
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
888
+ }
889
+
890
+ .session-details {
891
+ font-size: 0.85rem;
892
+ color: var(--text-secondary);
893
+ margin-top: 0.3rem;
894
+ display: flex;
895
+ gap: 1rem;
896
+ }
897
+
898
+ .session-actions {
899
+ display: flex;
900
+ gap: 0.5rem;
901
+ }
902
+
903
+ .session-stats {
904
+ font-size: 0.8rem;
905
+ color: var(--text-muted);
906
+ margin-top: 1rem;
907
+ text-align: center;
908
+ padding-top: 1rem;
909
+ border-top: 1px solid var(--border-glass);
910
+ }
911
+
912
+ .btn {
913
+ padding: 0.5rem 1rem;
914
+ border-radius: 8px;
915
+ font-size: 0.85rem;
916
+ font-weight: 500;
917
+ cursor: pointer;
918
+ transition: all 0.2s ease;
919
+ border: 1px solid transparent;
920
+ }
921
+
922
+ .btn-primary {
923
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.9) 0%, rgba(59, 130, 246, 1) 100%);
924
+ border: 1px solid rgba(59, 130, 246, 1);
925
+ color: white;
926
+ }
927
+
928
+ .btn-primary:hover {
929
+ background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(79, 150, 255, 1) 100%);
930
+ transform: translateY(-1px);
931
+ }
932
+
933
+ .btn-secondary {
934
+ background: rgba(34, 197, 94, 0.1);
935
+ border: 1px solid rgba(34, 197, 94, 0.3);
936
+ color: rgba(34, 197, 94, 1);
937
+ }
938
+
939
+ .btn-secondary:hover {
940
+ background: rgba(34, 197, 94, 0.2);
941
+ }
942
+
943
+ .btn-danger {
944
+ background: rgba(239, 68, 68, 0.1);
945
+ border: 1px solid rgba(239, 68, 68, 0.3);
946
+ color: rgba(239, 68, 68, 1);
947
+ }
948
+
949
+ .btn-danger:hover {
950
+ background: rgba(239, 68, 68, 0.2);
951
+ }
952
+ </style>
953
+ </head>
954
+
955
+ <script>
956
+ document.addEventListener('alpine:init', () => {
957
+ Alpine.data('dashboard', () => ({
958
+ selectedSpec: null,
959
+ section: 'summary',
960
+ activityTab: 'activity',
961
+ specs: [],
962
+ filteredSpecs: [],
963
+ loading: false,
964
+ searchTerm: '',
965
+ categoryFilter: 'all',
966
+ statusFilter: 'all',
967
+ showActivityLog: false,
968
+ showWorktreeModal: false,
969
+ showTmuxModal: false,
970
+ activities: [],
971
+
972
+ // tmux integration
973
+ tmuxAvailable: false,
974
+ tmuxSessions: { sessions: [], total_sessions: 0, active_sessions: 0, detached_sessions: 0 },
975
+ selectedSpecForTmux: null,
976
+ claudePrompt: '',
977
+
978
+ // Toast notifications
979
+ toasts: [],
980
+ sortBy: 'created_at',
981
+ sortDirection: 'desc',
982
+ confirmDelete: null,
983
+
984
+ // Button state tracking for copy buttons
985
+ buttonStates: {},
986
+ hasContent(section) {
987
+ try {
988
+ if (!this.selectedSpec?.sections) return false;
989
+ const sectionData = this.selectedSpec.sections[section];
990
+ return sectionData && sectionData.content && sectionData.content.trim() !== '';
991
+ } catch (error) {
992
+ console.warn('Error checking content for section:', section, error);
993
+ return false;
994
+ }
995
+ },
996
+ getAvailableSections() {
997
+ try {
998
+ if (!this.selectedSpec?.sections) return [
999
+ { id: 'summary', label: 'Summary', key: 'executive_summary' }
1000
+ ];
1001
+ const sections = [
1002
+ { id: 'summary', label: 'Summary', key: 'executive_summary' },
1003
+ { id: 'product', label: 'Product', key: 'product_specifications' },
1004
+ { id: 'architecture', label: 'Architecture', key: 'architecture_analysis' },
1005
+ { id: 'implementation', label: 'Implementation', key: 'implementation_plan' },
1006
+ { id: 'research', label: 'Research', key: 'research' },
1007
+ { id: 'testing', label: 'Testing', key: 'testing' },
1008
+ { id: 'review', label: 'Review', key: 'review' }
1009
+ ];
1010
+ return sections.filter(section => this.hasContent(section.key));
1011
+ } catch (error) {
1012
+ console.warn('Error getting available sections:', error);
1013
+ return [{ id: 'summary', label: 'Summary', key: 'executive_summary' }];
1014
+ }
1015
+ },
1016
+ async fetchSpecs() {
1017
+ this.loading = true;
1018
+ try {
1019
+ const response = await fetch('/api/tools/bob.spec.list');
1020
+ const data = await response.json();
1021
+ this.specs = data.specs || [];
1022
+ this.filteredSpecs = [...this.specs];
1023
+ this.filterSpecs();
1024
+ } catch (error) {
1025
+ console.error('Failed to fetch specs:', error);
1026
+ } finally {
1027
+ this.loading = false;
1028
+ }
1029
+ },
1030
+ sortSpecs(column) {
1031
+ if (this.sortBy === column) {
1032
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
1033
+ } else {
1034
+ this.sortBy = column;
1035
+ this.sortDirection = column === 'created_at' ? 'desc' : 'asc';
1036
+ }
1037
+ this.filterSpecs();
1038
+ },
1039
+ filterSpecs() {
1040
+ this.filteredSpecs = this.specs.filter(spec => {
1041
+ const matchesSearch = this.searchTerm === '' ||
1042
+ spec.title.toLowerCase().includes(this.searchTerm.toLowerCase());
1043
+ const matchesCategory = this.categoryFilter === 'all' ||
1044
+ spec.category === this.categoryFilter;
1045
+ const matchesStatus = this.statusFilter === 'all' ||
1046
+ spec.state === this.statusFilter;
1047
+ return matchesSearch && matchesCategory && matchesStatus;
1048
+ });
1049
+
1050
+ // Sort filtered results
1051
+ this.filteredSpecs.sort((a, b) => {
1052
+ let valueA, valueB;
1053
+
1054
+ switch (this.sortBy) {
1055
+ case 'title':
1056
+ valueA = a.title?.toLowerCase() || '';
1057
+ valueB = b.title?.toLowerCase() || '';
1058
+ break;
1059
+ case 'category':
1060
+ valueA = a.category?.toLowerCase() || '';
1061
+ valueB = b.category?.toLowerCase() || '';
1062
+ break;
1063
+ case 'state':
1064
+ valueA = a.state?.toLowerCase() || '';
1065
+ valueB = b.state?.toLowerCase() || '';
1066
+ break;
1067
+ case 'created_at':
1068
+ case 'modified':
1069
+ valueA = new Date(a.created_at || a.updated_at || '').getTime();
1070
+ valueB = new Date(b.created_at || b.updated_at || '').getTime();
1071
+ break;
1072
+ default:
1073
+ return 0;
1074
+ }
1075
+
1076
+ if (this.sortBy === 'created_at' || this.sortBy === 'modified') {
1077
+ return this.sortDirection === 'desc' ? valueB - valueA : valueA - valueB;
1078
+ } else {
1079
+ if (valueA < valueB) return this.sortDirection === 'desc' ? 1 : -1;
1080
+ if (valueA > valueB) return this.sortDirection === 'desc' ? -1 : 1;
1081
+ return 0;
1082
+ }
1083
+ });
1084
+ },
1085
+ confirmDeleteSpec(specId, title) {
1086
+ console.log('Confirming delete for:', specId, title);
1087
+ this.confirmDelete = { id: specId, title: title };
1088
+ },
1089
+ cancelDelete() {
1090
+ this.confirmDelete = null;
1091
+ },
1092
+ async deleteSpec() {
1093
+ if (!this.confirmDelete) {
1094
+ console.error('No confirmDelete object found');
1095
+ return;
1096
+ }
1097
+
1098
+ const specId = this.confirmDelete.id;
1099
+ const title = this.confirmDelete.title;
1100
+
1101
+ console.log('Deleting spec:', specId, title);
1102
+
1103
+ try {
1104
+ const response = await fetch('/api/tools/bob.spec.delete', {
1105
+ method: 'POST',
1106
+ headers: { 'Content-Type': 'application/json' },
1107
+ body: JSON.stringify({ spec_id: specId })
1108
+ });
1109
+
1110
+ const result = await response.json();
1111
+ console.log('Delete response:', result);
1112
+
1113
+ if (response.ok && (result.status === 'deleted' || result.success)) {
1114
+ // Remove from local arrays
1115
+ this.specs = this.specs.filter(spec => spec.spec_id !== specId);
1116
+ this.filteredSpecs = this.filteredSpecs.filter(spec => spec.spec_id !== specId);
1117
+ this.confirmDelete = null; // Close the confirmation modal
1118
+ this.showToast(`✅ Manual "${title}" deleted successfully`, 'success');
1119
+ } else {
1120
+ console.error('Delete failed:', result);
1121
+ this.showToast(`❌ Failed to delete manual: ${result.error || result.message || 'Unknown error'}`, 'error');
1122
+ }
1123
+ } catch (error) {
1124
+ console.error('Error deleting spec:', error);
1125
+ this.showToast('❌ Error deleting manual. Please try again.', 'error');
1126
+ }
1127
+ },
1128
+ async openSpec(specId) {
1129
+ try {
1130
+ const response = await fetch('/api/tools/bob.spec.get', {
1131
+ method: 'POST',
1132
+ headers: { 'Content-Type': 'application/json' },
1133
+ body: JSON.stringify({ spec_id: specId })
1134
+ });
1135
+ const spec = await response.json();
1136
+ this.selectedSpec = spec;
1137
+ // Set to first available section with content
1138
+ const availableSections = this.getAvailableSections();
1139
+ this.section = availableSections.length > 0 ? availableSections[0].id : 'summary';
1140
+ } catch (error) {
1141
+ console.error('Failed to fetch spec:', error);
1142
+ alert('Failed to load SPEC details');
1143
+ }
1144
+ },
1145
+ async exportToMarkdown(specId) {
1146
+ try {
1147
+ const response = await fetch('/api/tools/bob.spec.export', {
1148
+ method: 'POST',
1149
+ headers: { 'Content-Type': 'application/json' },
1150
+ body: JSON.stringify({ spec_id: specId })
1151
+ });
1152
+ const data = await response.json();
1153
+ if (data.markdown_path) {
1154
+ alert(`✅ SPEC exported to: ${data.markdown_path}`);
1155
+ } else {
1156
+ alert('❌ Export failed');
1157
+ }
1158
+ } catch (error) {
1159
+ console.error('Export error:', error);
1160
+ alert('❌ Export failed');
1161
+ }
1162
+ },
1163
+ getWorktreeName(spec) {
1164
+ // Generate worktree name from spec title
1165
+ if (!spec || !spec.title) return 'none';
1166
+ return 'feature/' + spec.title.toLowerCase()
1167
+ .replace(/[^a-z0-9\s]/g, '')
1168
+ .replace(/\s+/g, '-')
1169
+ .substring(0, 20);
1170
+ },
1171
+ renderMarkdown(text) {
1172
+ if (!text) return '';
1173
+
1174
+ // Escape HTML first
1175
+ text = text.replace(/&/g, '&amp;')
1176
+ .replace(/</g, '&lt;')
1177
+ .replace(/>/g, '&gt;');
1178
+
1179
+ // Handle code blocks with copy buttons first (before other processing)
1180
+ text = text.replace(/```bash\n([\s\S]*?)\n```/g, (match, code) => {
1181
+ const cleanCode = code.trim();
1182
+ const copyId = 'copy_' + Math.random().toString(36).substr(2, 9);
1183
+ return `&lt;div class="code-block-container"&gt;
1184
+ &lt;div class="code-block-header"&gt;
1185
+ &lt;span class="code-lang"&gt;bash&lt;/span&gt;
1186
+ &lt;button class="copy-code-btn" onclick="copyToClipboard('${cleanCode}', '${copyId}')" id="${copyId}"&gt;📋 Copy&lt;/button&gt;
1187
+ &lt;/div&gt;
1188
+ &lt;pre&gt;&lt;code&gt;${cleanCode}&lt;/code&gt;&lt;/pre&gt;
1189
+ &lt;/div&gt;`;
1190
+ });
1191
+
1192
+ // Handle generic code blocks
1193
+ text = text.replace(/```\n([\s\S]*?)\n```/g, (match, code) => {
1194
+ const cleanCode = code.trim();
1195
+ const copyId = 'copy_' + Math.random().toString(36).substr(2, 9);
1196
+ return `&lt;div class="code-block-container"&gt;
1197
+ &lt;div class="code-block-header"&gt;
1198
+ &lt;span class="code-lang"&gt;code&lt;/span&gt;
1199
+ &lt;button class="copy-code-btn" onclick="copyToClipboard('${cleanCode}', '${copyId}')" id="${copyId}"&gt;📋 Copy&lt;/button&gt;
1200
+ &lt;/div&gt;
1201
+ &lt;pre&gt;&lt;code&gt;${cleanCode}&lt;/code&gt;&lt;/pre&gt;
1202
+ &lt;/div&gt;`;
1203
+ });
1204
+
1205
+ // Simple markdown renderer
1206
+ let html = text
1207
+ // Headers
1208
+ .replace(/^### (.*$)/gim, '&lt;h3&gt;$1&lt;/h3&gt;')
1209
+ .replace(/^## (.*$)/gim, '&lt;h2&gt;$1&lt;/h2&gt;')
1210
+ .replace(/^# (.*$)/gim, '&lt;h1&gt;$1&lt;/h1&gt;')
1211
+ // Bold
1212
+ .replace(/\*\*(.*?)\*\*/g, '&lt;strong&gt;$1&lt;/strong&gt;')
1213
+ // Italic
1214
+ .replace(/\*(.*?)\*/g, '&lt;em&gt;$1&lt;/em&gt;')
1215
+ // Inline code
1216
+ .replace(/`([^`]+)`/g, '&lt;code class="inline-code"&gt;$1&lt;/code&gt;')
1217
+ // Line breaks
1218
+ .replace(/\n\n/g, '&lt;/p&gt;&lt;p&gt;')
1219
+ .replace(/\n/g, '&lt;br&gt;')
1220
+ // Lists
1221
+ .replace(/^\* (.*$)/gim, '&lt;li&gt;$1&lt;/li&gt;')
1222
+ .replace(/^- (.*$)/gim, '&lt;li&gt;$1&lt;/li&gt;')
1223
+ // Checkmarks and emojis
1224
+ .replace(/✅/g, '✅')
1225
+ .replace(/🚀/g, '🚀')
1226
+ .replace(/🔗/g, '🔗')
1227
+ .replace(/📋/g, '📋')
1228
+ // Wrap in paragraphs
1229
+ .replace(/^(?!&lt;)/gm, '&lt;p&gt;')
1230
+ .replace(/$/gm, '&lt;/p&gt;')
1231
+ // Clean up
1232
+ .replace(/&lt;\/p&gt;&lt;p&gt;/g, '&lt;/p&gt;\n&lt;p&gt;')
1233
+ .replace(/(&lt;li&gt;.*&lt;\/li&gt;)/gs, '&lt;ul&gt;$1&lt;/ul&gt;')
1234
+ .replace(/&lt;p&gt;&lt;\/p&gt;/g, '');
1235
+
1236
+ // Unescape the HTML tags we want
1237
+ return html.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
1238
+ },
1239
+ simplifyFilePath(fullPath) {
1240
+ if (!fullPath) return '';
1241
+ // Extract just the repo name and path after it
1242
+ const match = fullPath.match(/\/([^\/]+)\/(.+)$/);
1243
+ if (match && match[1] && match[2]) {
1244
+ return `${match[1]}/${match[2]}`;
1245
+ }
1246
+ return fullPath; // fallback to full path if pattern doesn't match
1247
+ },
1248
+ getActivityTimeline(spec) {
1249
+ if (!spec) return [];
1250
+
1251
+ const timeline = [];
1252
+
1253
+ // Add execution logs
1254
+ const executionLogs = spec.execution_logs || spec.execution_log || [];
1255
+ executionLogs.forEach(log => {
1256
+ timeline.push({
1257
+ ...log,
1258
+ type: 'execution',
1259
+ role: 'Engineer'
1260
+ });
1261
+ });
1262
+
1263
+ // Add debug logs
1264
+ const debugLogs = spec.debug_logs || spec.debug_log || [];
1265
+ debugLogs.forEach(log => {
1266
+ timeline.push({
1267
+ ...log,
1268
+ type: 'debug',
1269
+ role: 'Debugger'
1270
+ });
1271
+ });
1272
+
1273
+ // Add spec creation/update events if we have timeline data
1274
+ if (spec.timeline) {
1275
+ spec.timeline.forEach(event => {
1276
+ timeline.push({
1277
+ ...event,
1278
+ type: event.type || 'system'
1279
+ });
1280
+ });
1281
+ }
1282
+
1283
+ // Add synthetic spec creation event if timeline is empty
1284
+ if (timeline.length === 0 && spec.created_at) {
1285
+ timeline.push({
1286
+ timestamp: spec.created_at,
1287
+ type: 'spec_created',
1288
+ role: 'Architect',
1289
+ action: 'Created SPEC',
1290
+ note: `SPEC ${spec.spec_id} created`
1291
+ });
1292
+ }
1293
+
1294
+ // Add worktree creation if it exists
1295
+ if (spec.worktree && timeline.length <= 2) {
1296
+ const workTreeTime = new Date(spec.modified_at || Date.now()).toISOString();
1297
+ timeline.push({
1298
+ timestamp: workTreeTime,
1299
+ type: 'worktree_created',
1300
+ role: 'System',
1301
+ action: 'worktree_created',
1302
+ note: `Created worktree: ${spec.worktree}`,
1303
+ branch: spec.worktree
1304
+ });
1305
+ }
1306
+
1307
+ // Sort by timestamp (newest first for better UX)
1308
+ return timeline.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).slice(0, 15);
1309
+ },
1310
+ async loadActivityLog() {
1311
+ try {
1312
+ // Load real activity from dashboard API
1313
+ const response = await fetch('/api/dashboard-data');
1314
+ const data = await response.json();
1315
+
1316
+ // Format recent changes for display
1317
+ this.activities = (data.recent_changes || []).slice(0, 20).map(change => ({
1318
+ timestamp: change.timestamp,
1319
+ type: change.event || change.type,
1320
+ author: change.role || 'system',
1321
+ message: this.formatActivityMessage(change),
1322
+ spec_id: change.spec_id,
1323
+ data: change.data
1324
+ }));
1325
+
1326
+ } catch (error) {
1327
+ console.error('Failed to load activity log:', error);
1328
+ // Fallback to empty state
1329
+ this.activities = [];
1330
+ }
1331
+ this.showActivityLog = true;
1332
+ },
1333
+
1334
+ formatActivityMessage(change) {
1335
+ if (change.event === 'orchestrator_routing') {
1336
+ return `Orchestrator routed to ${change.data?.target_mode} mode`;
1337
+ } else if (change.event === 'mcp_tool_workshop') {
1338
+ return `MCP tool executed: ${change.data?.tool_name} (${change.data?.duration_ms}ms)`;
1339
+ } else if (change.type === 'file') {
1340
+ return `File changed: ${change.path}`;
1341
+ } else {
1342
+ return change.event || change.type || 'Unknown activity';
1343
+ }
1344
+ },
1345
+
1346
+ showToast(message, type = 'info') {
1347
+ const toast = {
1348
+ id: Date.now() + Math.random(),
1349
+ message,
1350
+ type,
1351
+ visible: true
1352
+ };
1353
+ this.toasts.push(toast);
1354
+
1355
+ // Auto-remove after 4 seconds
1356
+ setTimeout(() => {
1357
+ this.removeToast(toast.id);
1358
+ }, 4000);
1359
+ },
1360
+
1361
+ removeToast(id) {
1362
+ const index = this.toasts.findIndex(toast => toast.id === id);
1363
+ if (index !== -1) {
1364
+ this.toasts.splice(index, 1);
1365
+ }
1366
+ },
1367
+
1368
+ // tmux integration methods
1369
+ async checkTmuxAvailability() {
1370
+ try {
1371
+ const response = await fetch('/api/tmux/check-availability');
1372
+ const data = await response.json();
1373
+ this.tmuxAvailable = data.available;
1374
+ return data.available;
1375
+ } catch (error) {
1376
+ console.error('Failed to check tmux availability:', error);
1377
+ this.tmuxAvailable = false;
1378
+ return false;
1379
+ }
1380
+ },
1381
+
1382
+ async loadTmuxSessions() {
1383
+ try {
1384
+ const response = await fetch('/api/tmux/list-sessions');
1385
+ const data = await response.json();
1386
+ this.tmuxSessions = data || { sessions: [], total_sessions: 0, active_sessions: 0, detached_sessions: 0 };
1387
+ } catch (error) {
1388
+ console.error('Failed to load tmux sessions:', error);
1389
+ this.tmuxSessions = { sessions: [], total_sessions: 0, active_sessions: 0, detached_sessions: 0 };
1390
+ }
1391
+ },
1392
+
1393
+ showTmuxModalForSpec(spec) {
1394
+ this.selectedSpecForTmux = spec;
1395
+ this.claudePrompt = `Create SPEC and build worktree for ${spec.title}`;
1396
+ this.showTmuxModal = true;
1397
+ this.loadTmuxSessions(); // Load sessions when modal opens
1398
+ },
1399
+
1400
+ async createMcpTmuxSession() {
1401
+ if (!this.claudePrompt.trim()) {
1402
+ this.addToast('Please enter a prompt for the MCP session', 'error');
1403
+ return;
1404
+ }
1405
+
1406
+ try {
1407
+ const response = await fetch('/api/tmux/create-session', {
1408
+ method: 'POST',
1409
+ headers: { 'Content-Type': 'application/json' },
1410
+ body: JSON.stringify({
1411
+ spec_id: 'mcp-workshop',
1412
+ worktree_id: 'workshop',
1413
+ worktree_path: '/Users/pawanraviee/Documents/GitHub/bobs-workshop',
1414
+ claude_prompt: this.claudePrompt
1415
+ })
1416
+ });
1417
+
1418
+ const result = await response.json();
1419
+
1420
+ if (response.ok) {
1421
+ this.addToast(`🚀 MCP tmux session created successfully!`, 'success');
1422
+ this.addToast(`Attach command: tmux attach -t "${result.session_name}"`, 'info');
1423
+
1424
+ // Copy attach command to clipboard
1425
+ try {
1426
+ await navigator.clipboard.writeText(`tmux attach -t "${result.session_name}"`);
1427
+ this.addToast('📋 Attach command copied to clipboard', 'info');
1428
+ } catch (clipError) {
1429
+ console.warn('Could not copy to clipboard:', clipError);
1430
+ }
1431
+
1432
+ await this.loadTmuxSessions();
1433
+ this.showTmuxModal = false;
1434
+ this.claudePrompt = '';
1435
+ } else {
1436
+ throw new Error(result.error || 'Failed to create MCP tmux session');
1437
+ }
1438
+ } catch (error) {
1439
+ console.error('Failed to create MCP tmux session:', error);
1440
+ this.addToast(`Failed to create session: ${error.message}`, 'error');
1441
+ }
1442
+ },
1443
+
1444
+ async killTmuxSession(sessionName) {
1445
+ try {
1446
+ const response = await fetch('/api/tmux/kill-session', {
1447
+ method: 'POST',
1448
+ headers: { 'Content-Type': 'application/json' },
1449
+ body: JSON.stringify({ session_name: sessionName })
1450
+ });
1451
+
1452
+ const result = await response.json();
1453
+
1454
+ if (response.ok) {
1455
+ this.addToast(`tmux session killed: ${sessionName}`, 'success');
1456
+ await this.loadTmuxSessions();
1457
+ } else {
1458
+ throw new Error(result.error || 'Failed to kill tmux session');
1459
+ }
1460
+ } catch (error) {
1461
+ console.error('Failed to kill tmux session:', error);
1462
+ this.addToast(`Failed to kill tmux session: ${error.message}`, 'error');
1463
+ }
1464
+ },
1465
+ async connectToTmuxSession(sessionName) {
1466
+ try {
1467
+ // For now, we'll just copy the attach command to clipboard
1468
+ // In the future, this could open a web-based terminal
1469
+ const attachCommand = `tmux attach -t "${sessionName}"`;
1470
+ await navigator.clipboard.writeText(attachCommand);
1471
+ this.addToast(`📋 Attach command copied: ${attachCommand}`, 'success');
1472
+ this.addToast('🌐 Web terminal connection coming soon! Use desktop tmux for now.', 'info');
1473
+ } catch (error) {
1474
+ console.error('Failed to connect to tmux session:', error);
1475
+ this.addToast(`Failed to connect: ${error.message}`, 'error');
1476
+ }
1477
+ },
1478
+ async copyAttachCommand(attachCommand) {
1479
+ try {
1480
+ await navigator.clipboard.writeText(attachCommand);
1481
+ this.addToast(`📋 Copied to clipboard: ${attachCommand}`, 'success');
1482
+ } catch (error) {
1483
+ console.error('Failed to copy attach command:', error);
1484
+ this.addToast(`Failed to copy command: ${error.message}`, 'error');
1485
+ }
1486
+ },
1487
+ async openDesktopTmux(sessionName) {
1488
+ try {
1489
+ // Try to open tmux directly using the system's default terminal
1490
+ const command = `tmux attach -t "${sessionName}"`;
1491
+
1492
+ // First copy the command to clipboard
1493
+ await navigator.clipboard.writeText(command);
1494
+
1495
+ // Use browser alert for now since this.addToast is not working
1496
+ alert(`📋 Command copied: ${command}`);
1497
+ alert('🖥️ Please run the copied command in your terminal');
1498
+
1499
+ // Close the modal
1500
+ this.showTmuxModal = false;
1501
+
1502
+ // Note: Direct terminal opening from web browsers is limited for security
1503
+ // The command is copied to clipboard as the most reliable method
1504
+ } catch (error) {
1505
+ console.error('Failed to open desktop tmux:', error);
1506
+ alert(`Failed to open terminal: ${error.message}`);
1507
+ }
1508
+ },
1509
+
1510
+ getTmuxSessionForSpec(specId) {
1511
+ const worktreeName = `feature/${specId}`;
1512
+ return this.tmuxSessions.find(session =>
1513
+ session.worktree_id === worktreeName ||
1514
+ session.session_name.includes(worktreeName)
1515
+ );
1516
+ },
1517
+
1518
+ getTmuxStatusIcon(status) {
1519
+ switch (status) {
1520
+ case 'active': return '🟢';
1521
+ case 'detached': return '🟡';
1522
+ case 'inactive': return '🔴';
1523
+ default: return '❓';
1524
+ }
1525
+ },
1526
+
1527
+ // Toast notification system
1528
+ toasts: [],
1529
+ toastId: 0,
1530
+
1531
+ addToast(message, type = 'info', duration = 4000) {
1532
+ const toast = {
1533
+ id: ++this.toastId,
1534
+ message,
1535
+ type,
1536
+ timestamp: Date.now()
1537
+ };
1538
+
1539
+ this.toasts.push(toast);
1540
+ console.log(`[${type.toUpperCase()}] ${message}`);
1541
+
1542
+ // Auto-remove toast after duration
1543
+ setTimeout(() => {
1544
+ this.removeToast(toast.id);
1545
+ }, duration);
1546
+ },
1547
+
1548
+ removeToast(id) {
1549
+ const index = this.toasts.findIndex(toast => toast.id === id);
1550
+ if (index > -1) {
1551
+ this.toasts.splice(index, 1);
1552
+ }
1553
+ },
1554
+
1555
+ getToastIcon(type) {
1556
+ switch (type) {
1557
+ case 'success': return '✅';
1558
+ case 'error': return '❌';
1559
+ case 'warning': return '⚠️';
1560
+ case 'info': return 'ℹ️';
1561
+ default: return '💬';
1562
+ }
1563
+ },
1564
+
1565
+ getToastColor(type) {
1566
+ switch (type) {
1567
+ case 'success': return 'rgba(34, 197, 94, 0.9)';
1568
+ case 'error': return 'rgba(239, 68, 68, 0.9)';
1569
+ case 'warning': return 'rgba(245, 158, 11, 0.9)';
1570
+ case 'info': return 'rgba(59, 130, 246, 0.9)';
1571
+ default: return 'rgba(75, 85, 99, 0.9)';
1572
+ }
1573
+ },
1574
+
1575
+ // Copy one-shot tmux launch command
1576
+ copyOneShotCommand() {
1577
+ const escapedPrompt = this.claudePrompt.replace(/"/g, '\\"');
1578
+ const command = `tmux new-session -d -s mcp-workshop -c '/Users/pawanraviee/Documents/GitHub/bobs-workshop' 'claude -p "${escapedPrompt}" --dangerously-skip-permissions'; tmux attach -t mcp-workshop`;
1579
+
1580
+ navigator.clipboard.writeText(command).then(() => {
1581
+ this.addToast('📋 One-shot command copied to clipboard!', 'success');
1582
+ }).catch(() => {
1583
+ this.addToast('Failed to copy command', 'error');
1584
+ });
1585
+ },
1586
+
1587
+ // Session management functions
1588
+ connectToTmuxDesktop(session) {
1589
+ // This would typically open a desktop tmux client or provide instructions
1590
+ this.addToast(`🖥️ Opening desktop connection for ${session.session_name}`, 'info');
1591
+ // For now, copy the attach command as fallback
1592
+ this.copyTmuxAttachCommand(session.session_name);
1593
+ },
1594
+
1595
+ copyTmuxAttachCommand(sessionName) {
1596
+ const command = `tmux attach -t "${sessionName}"`;
1597
+ navigator.clipboard.writeText(command).then(() => {
1598
+ this.addToast(`📋 Attach command copied: ${command}`, 'success');
1599
+ }).catch(() => {
1600
+ this.addToast('Failed to copy attach command', 'error');
1601
+ });
1602
+ },
1603
+
1604
+ // Enhanced copy method for modal quick connect (with button text change)
1605
+ copyTmuxAttachFromWorktree(worktreeName) {
1606
+ const command = `tmux attach -t "mcp-${worktreeName}"`;
1607
+ const buttonId = 'modal-quick-connect';
1608
+
1609
+ navigator.clipboard.writeText(command).then(() => {
1610
+ // Change button text instead of showing toast
1611
+ this.buttonStates[buttonId] = 'Copied!';
1612
+
1613
+ // Reset button text after 2 seconds
1614
+ setTimeout(() => {
1615
+ this.buttonStates[buttonId] = '📋';
1616
+ }, 2000);
1617
+ }).catch((error) => {
1618
+ console.error('Failed to copy command:', error);
1619
+ // Show error state briefly
1620
+ this.buttonStates[buttonId] = '❌';
1621
+ setTimeout(() => {
1622
+ this.buttonStates[buttonId] = '📋';
1623
+ }, 2000);
1624
+ });
1625
+ },
1626
+
1627
+ copyTmuxKillCommand(sessionName) {
1628
+ const command = `tmux kill-session -t "${sessionName}"`;
1629
+ navigator.clipboard.writeText(command).then(() => {
1630
+ this.addToast(`🗑 Kill command copied: ${command}`, 'success');
1631
+ }).catch(() => {
1632
+ this.addToast('Failed to copy kill command', 'error');
1633
+ });
1634
+ },
1635
+
1636
+ async killTmuxSession(sessionName) {
1637
+ if (!confirm(`Are you sure you want to kill the session "${sessionName}"?`)) {
1638
+ return;
1639
+ }
1640
+
1641
+ try {
1642
+ const response = await fetch('/api/tmux/kill-session', {
1643
+ method: 'POST',
1644
+ headers: { 'Content-Type': 'application/json' },
1645
+ body: JSON.stringify({ session_name: sessionName })
1646
+ });
1647
+
1648
+ if (response.ok) {
1649
+ this.addToast(`🗑 Session "${sessionName}" killed successfully`, 'success');
1650
+ // Reload sessions
1651
+ await this.loadTmuxSessions();
1652
+ } else {
1653
+ const error = await response.text();
1654
+ this.addToast(`Failed to kill session: ${error}`, 'error');
1655
+ }
1656
+ } catch (error) {
1657
+ this.addToast(`Error killing session: ${error.message}`, 'error');
1658
+ }
1659
+ },
1660
+
1661
+ // Load sessions when modal opens
1662
+ async loadTmuxSessionsForModal() {
1663
+ if (this.showTmuxModal) {
1664
+ await this.loadTmuxSessions();
1665
+ }
1666
+ }
1667
+ }));
1668
+ });
1669
+ </script>
1670
+
1671
+ <body class="h-full" x-data="dashboard" x-init="fetchSpecs(); checkTmuxAvailability(); loadTmuxSessions()" @keydown.escape.window="selectedSpec = null; showActivityLog = false; showWorktreeModal = false; showTmuxModal = false">
1672
+
1673
+ <!-- Header -->
1674
+ <header class="header">
1675
+ <div class="logo">
1676
+ <span style="color: var(--accent-orange);">👷</span>
1677
+ <span class="text-gradient">Bob's Workshop</span>
1678
+ </div>
1679
+
1680
+ <!-- Separate Metric Boxes -->
1681
+ <div style="display: flex; align-items: center; gap: 1rem;"
1682
+ x-data="{
1683
+ worktrees: { committed: 0, dirty: 0, clean: 0 },
1684
+ specs: { draft: 0, 'in-progress': 0, completed: 0 },
1685
+ async loadMetrics() {
1686
+ try {
1687
+ const [worktreesRes, specsRes] = await Promise.all([
1688
+ fetch('/api/tools/bob.worktree.list').catch(() => ({ json: () => ({committed:0,dirty:0,clean:0}) })),
1689
+ fetch('/api/tools/bob.spec.list')
1690
+ ]);
1691
+ const worktreesData = await worktreesRes.json();
1692
+ this.worktrees = worktreesData;
1693
+ const specsData = await specsRes.json();
1694
+ const allSpecs = specsData.specs || [];
1695
+ this.specs = {
1696
+ draft: allSpecs.filter(s => s.state === 'draft').length,
1697
+ 'in-progress': allSpecs.filter(s => s.state === 'in-progress').length,
1698
+ completed: allSpecs.filter(s => s.state === 'completed').length
1699
+ };
1700
+ } catch (error) {
1701
+ console.error('Failed to load metrics:', error);
1702
+ }
1703
+ }
1704
+ }" x-init="loadMetrics()">
1705
+
1706
+ <!-- Manuals Box -->
1707
+ <div style="background: rgba(59, 130, 246, 0.12); backdrop-filter: blur(10px); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 10px; padding: 0.6rem 0.9rem; display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; color: var(--text-secondary);">
1708
+ <span>📖 Manuals:</span>
1709
+ <span style="color: #9ca3af;" x-text="specs.draft + 'd'"></span>
1710
+ <span style="color: var(--text-muted);">|</span>
1711
+ <span style="color: var(--accent-orange);" x-text="specs['in-progress'] + 'p'"></span>
1712
+ <span style="color: var(--text-muted);">|</span>
1713
+ <span style="color: var(--accent-green);" x-text="specs.completed + '✓'"></span>
1714
+ </div>
1715
+
1716
+ <!-- Trees Box -->
1717
+ <div style="background: rgba(16, 185, 129, 0.12); backdrop-filter: blur(10px); border: 1px solid rgba(16, 185, 129, 0.2); border-radius: 10px; padding: 0.6rem 0.9rem; display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; color: var(--text-secondary);">
1718
+ <span>🌿 Trees:</span>
1719
+ <span style="color: var(--accent-green);" x-text="(worktrees.committed?.length || 0) + 'c'"></span>
1720
+ <span style="color: var(--text-muted);">|</span>
1721
+ <span style="color: var(--accent-orange);" x-text="(worktrees.dirty?.length || 0) + '!'"></span>
1722
+ <span style="color: var(--text-muted);">|</span>
1723
+ <span style="color: var(--text-secondary);" x-text="(worktrees.clean?.length || 0) + '~'"></span>
1724
+ <button @click="showWorktreeModal = true"
1725
+ style="margin-left: 0.5rem; padding: 0.25rem 0.5rem; background: var(--accent-orange); border: 1px solid var(--accent-orange); border-radius: 6px; color: white; font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; font-weight: 600;"
1726
+ @mouseenter="$el.style.background = '#e97b00'"
1727
+ @mouseleave="$el.style.background = 'var(--accent-orange)'"
1728
+ title="View Active Worktrees">
1729
+ 🏗️ View
1730
+ </button>
1731
+ </div>
1732
+
1733
+ <!-- tmux Sessions Box -->
1734
+ <div x-show="tmuxAvailable" style="background: rgba(139, 69, 19, 0.12); backdrop-filter: blur(10px); border: 1px solid rgba(139, 69, 19, 0.2); border-radius: 10px; padding: 0.6rem 0.9rem; display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; color: var(--text-secondary);">
1735
+ <span>🖥️ tmux:</span>
1736
+ <span style="color: var(--accent-green);" x-text="(tmuxSessions.sessions || []).filter(s => s.status === 'active').length + 'a'"></span>
1737
+ <span style="color: var(--text-muted);">|</span>
1738
+ <span style="color: var(--accent-orange);" x-text="(tmuxSessions.sessions || []).filter(s => s.status === 'detached').length + 'd'"></span>
1739
+ <span style="color: var(--text-muted);">|</span>
1740
+ <span style="color: var(--accent-red);" x-text="(tmuxSessions.sessions || []).filter(s => s.status === 'inactive').length + 'x'"></span>
1741
+ <button @click="loadTmuxSessions()"
1742
+ style="margin-left: 0.5rem; padding: 0.25rem 0.5rem; background: rgba(139, 69, 19, 0.8); border: 1px solid rgba(139, 69, 19, 0.8); border-radius: 6px; color: white; font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; font-weight: 600;"
1743
+ @mouseenter="$el.style.background = 'rgba(139, 69, 19, 1)'"
1744
+ @mouseleave="$el.style.background = 'rgba(139, 69, 19, 0.8)'"
1745
+ title="Refresh tmux Sessions">
1746
+ 🔄 Sync
1747
+ </button>
1748
+ <button @click="showTmuxModal = true; claudePrompt = 'Help me work on this project using Bob\'s Workshop MCP tools'; loadTmuxSessions()"
1749
+ style="margin-left: 0.25rem; padding: 0.25rem 0.6rem; background: linear-gradient(135deg, rgba(139, 69, 19, 0.9) 0%, rgba(139, 69, 19, 1) 100%); border: 1px solid rgba(139, 69, 19, 1); border-radius: 6px; color: white; font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; font-weight: 600; box-shadow: 0 2px 8px rgba(139, 69, 19, 0.3);"
1750
+ @mouseenter="$el.style.background = 'linear-gradient(135deg, rgba(139, 69, 19, 1) 0%, rgba(160, 80, 20, 1) 100%)'; $el.style.transform = 'translateY(-1px)'"
1751
+ @mouseleave="$el.style.background = 'linear-gradient(135deg, rgba(139, 69, 19, 0.9) 0%, rgba(139, 69, 19, 1) 100%)'; $el.style.transform = 'translateY(0)'"
1752
+ title="Launch tmux Session - Create MCP development environment">
1753
+ 🚀 Launch
1754
+ </button>
1755
+ </div>
1756
+ </div>
1757
+
1758
+ <div style="font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap;"
1759
+ x-data="{
1760
+ projectName: 'bobs-workshop',
1761
+ workshopVerbs: ['servicing', 'working on', 'fixing', 'building', 'crafting', 'assembling', 'engineering', 'constructing', 'maintaining', 'upgrading'],
1762
+ currentVerb: 'servicing',
1763
+ init() {
1764
+ this.currentVerb = this.workshopVerbs[Math.floor(Math.random() * this.workshopVerbs.length)];
1765
+ }
1766
+ }">
1767
+ <span style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%); backdrop-filter: blur(8px); border: 1px solid rgba(245, 158, 11, 0.25); border-radius: 12px; padding: 0.7rem 1.1rem; display: inline-flex; align-items: center; gap: 0.5rem; font-family: var(--font-heading); font-weight: 500; font-size: 0.9rem; color: var(--text-primary); box-shadow: 0 2px 8px rgba(245, 158, 11, 0.1);">
1768
+ <span style="color: #fbbf24;">now</span>
1769
+ <span style="color: var(--accent-orange);" x-text="currentVerb"></span>
1770
+ <span style="color: var(--text-muted);">:</span>
1771
+ <span style="color: #fbbf24; font-weight: 600;" x-text="projectName"></span>
1772
+ </span>
1773
+ </div>
1774
+ </header>
1775
+
1776
+ <!-- Manual Table -->
1777
+ <div class="spec-container fade-in" style="margin: 2rem;">
1778
+ <!-- Header with integrated controls -->
1779
+ <div class="spec-header" style="padding: 1.5rem 2rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
1780
+ <div style="display: flex; align-items: center; gap: 1rem;">
1781
+ <h2 class="heading" style="margin: 0; font-size: 1.25rem;">🔧 Manual Explorer</h2>
1782
+ <span style="background: rgba(255, 255, 255, 0.1); color: var(--text-secondary); padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;"
1783
+ x-text="`${filteredSpecs.length} of ${specs.length} Manuals`"></span>
1784
+ <span x-show="loading" class="loading"></span>
1785
+ </div>
1786
+
1787
+ <!-- Integrated Controls Row -->
1788
+ <div style="display: flex; align-items: center; gap: 1rem; flex: 1; max-width: 800px;">
1789
+ <!-- Search Input -->
1790
+ <div style="flex: 2; min-width: 200px;">
1791
+ <input type="text"
1792
+ placeholder="🔍 Search Manuals..."
1793
+ x-model="searchTerm"
1794
+ @input="filterSpecs()"
1795
+ style="width: 100%; padding: 0.5rem 0.75rem; background: var(--bg-glass); border: 1px solid var(--border-glass); border-radius: 6px; color: var(--text-primary); font-family: var(--font-body); font-size: 0.875rem;">
1796
+ </div>
1797
+
1798
+ <!-- Category Filter -->
1799
+ <select x-model="categoryFilter"
1800
+ @change="filterSpecs()"
1801
+ style="padding: 0.5rem 0.75rem; background: var(--bg-glass); border: 1px solid var(--border-glass); border-radius: 6px; color: var(--text-primary); font-family: var(--font-body); font-size: 0.875rem;">
1802
+ <option value="all">All Categories</option>
1803
+ <option value="frontend">Frontend</option>
1804
+ <option value="backend">Backend</option>
1805
+ <option value="fullstack">Fullstack</option>
1806
+ <option value="general">General</option>
1807
+ </select>
1808
+
1809
+ <!-- Status Filter -->
1810
+ <select x-model="statusFilter"
1811
+ @change="filterSpecs()"
1812
+ style="padding: 0.5rem 0.75rem; background: var(--bg-glass); border: 1px solid var(--border-glass); border-radius: 6px; color: var(--text-primary); font-family: var(--font-body); font-size: 0.875rem;">
1813
+ <option value="all">All Status</option>
1814
+ <option value="draft">Draft</option>
1815
+ <option value="planning">Planning</option>
1816
+ <option value="in-progress">In Progress</option>
1817
+ <option value="completed">Completed</option>
1818
+ </select>
1819
+
1820
+ <!-- Clear Filters -->
1821
+ <button class="btn btn-secondary"
1822
+ @click="searchTerm = ''; categoryFilter = 'all'; statusFilter = 'all'; filterSpecs()"
1823
+ style="padding: 0.5rem 0.75rem; font-size: 0.875rem; white-space: nowrap;">
1824
+ ✕ Clear
1825
+ </button>
1826
+
1827
+ <!-- Refresh Button -->
1828
+ <button class="btn btn-secondary" @click="fetchSpecs()" style="padding: 0.5rem 0.75rem; font-size: 0.875rem; white-space: nowrap;">
1829
+ 🔄 Refresh
1830
+ </button>
1831
+ </div>
1832
+ </div>
1833
+
1834
+
1835
+ <table class="spec-table">
1836
+ <thead>
1837
+ <tr>
1838
+ <th class="sortable"
1839
+ :class="{ 'sorted-asc': sortBy === 'title' && sortDirection === 'asc', 'sorted-desc': sortBy === 'title' && sortDirection === 'desc' }"
1840
+ @click="sortSpecs('title')">Title</th>
1841
+ <th class="sortable"
1842
+ :class="{ 'sorted-asc': sortBy === 'category' && sortDirection === 'asc', 'sorted-desc': sortBy === 'category' && sortDirection === 'desc' }"
1843
+ @click="sortSpecs('category')">Category</th>
1844
+ <th class="sortable"
1845
+ :class="{ 'sorted-asc': sortBy === 'state' && sortDirection === 'asc', 'sorted-desc': sortBy === 'state' && sortDirection === 'desc' }"
1846
+ @click="sortSpecs('state')">Status</th>
1847
+ <th>Worktree</th>
1848
+ <th class="sortable"
1849
+ :class="{ 'sorted-asc': sortBy === 'modified' && sortDirection === 'asc', 'sorted-desc': sortBy === 'modified' && sortDirection === 'desc' }"
1850
+ @click="sortSpecs('modified')">Modified</th>
1851
+ <th>Actions</th>
1852
+ </tr>
1853
+ </thead>
1854
+ <tbody>
1855
+ <template x-for="spec in filteredSpecs" :key="spec.spec_id">
1856
+ <tr>
1857
+ <td>
1858
+ <div style="font-weight: 600; cursor: pointer; color: var(--text-primary);"
1859
+ x-text="spec.title"
1860
+ @click="openSpec(spec.spec_id)"></div>
1861
+ <div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;"
1862
+ x-text="spec.spec_id"></div>
1863
+ </td>
1864
+ <td style="color: var(--text-secondary);" x-text="spec.category || '—'"></td>
1865
+ <td>
1866
+ <span class="status-badge"
1867
+ :class="`status-${spec.state}`"
1868
+ x-text="spec.state"></span>
1869
+ </td>
1870
+ <td style="color: var(--text-secondary); font-family: monospace; font-size: 0.875rem;">
1871
+ <div x-data="{ worktreeName: getWorktreeName(spec) }">
1872
+ <span x-show="worktreeName !== 'none'" x-text="worktreeName" style="color: var(--accent-blue);"></span>
1873
+ <span x-show="worktreeName === 'none'" style="color: var(--text-muted);">—</span>
1874
+ <div x-show="worktreeName !== 'none'" style="font-size: 0.75rem; margin-top: 0.25rem;">
1875
+ <span style="color: var(--accent-green);">✓ clean</span>
1876
+ </div>
1877
+ </div>
1878
+ </td>
1879
+ <td style="color: var(--text-secondary); font-size: 0.875rem;"
1880
+ x-text="new Date(spec.updated_at).toLocaleDateString()"></td>
1881
+ <td>
1882
+ <div style="display: flex; gap: 0.5rem;">
1883
+ <button class="btn btn-primary" style="padding: 0.5rem 1rem; font-size: 0.75rem;"
1884
+ @click="openSpec(spec.spec_id)">
1885
+ View
1886
+ </button>
1887
+ <button class="btn btn-danger" style="padding: 0.5rem 0.75rem; font-size: 0.75rem; background: var(--accent-red); border: 1px solid var(--accent-red);"
1888
+ @click="confirmDeleteSpec(spec.spec_id, spec.title)"
1889
+ :title="`Delete ${spec.title}`">
1890
+ 🗑
1891
+ </button>
1892
+ </div>
1893
+ </td>
1894
+ </tr>
1895
+ </template>
1896
+ </tbody>
1897
+ </table>
1898
+
1899
+ <div x-show="filteredSpecs.length === 0 && !loading"
1900
+ style="text-align: center; padding: 3rem; color: var(--text-muted);">
1901
+ <div x-show="specs.length === 0">No Manuals found. Create your first Manual to get started!</div>
1902
+ <div x-show="specs.length > 0">No Manuals match your current filters.</div>
1903
+ </div>
1904
+
1905
+ <!-- Activity Log Button -->
1906
+ <div style="padding: 1.5rem; border-top: 1px solid var(--border-glass); text-center;">
1907
+ <button class="btn btn-secondary" @click="loadActivityLog()">
1908
+ 📊 View Activity Log
1909
+ </button>
1910
+ </div>
1911
+ </div>
1912
+
1913
+ <!-- Enhanced Modal -->
1914
+ <div class="modal-overlay"
1915
+ x-show="selectedSpec"
1916
+ x-transition:enter="transition ease-out duration-300"
1917
+ x-transition:enter-start="opacity-0"
1918
+ x-transition:enter-end="opacity-100"
1919
+ x-transition:leave="transition ease-in duration-200"
1920
+ x-transition:leave-start="opacity-100"
1921
+ x-transition:leave-end="opacity-0"
1922
+ @click.self="selectedSpec=null">
1923
+
1924
+ <div class="modal-content"
1925
+ x-show="selectedSpec"
1926
+ x-transition:enter="transition ease-out duration-300"
1927
+ x-transition:enter-start="opacity-0 scale-95"
1928
+ x-transition:enter-end="opacity-100 scale-100"
1929
+ x-transition:leave="transition ease-in duration-200"
1930
+ x-transition:leave-start="opacity-100 scale-100"
1931
+ x-transition:leave-end="opacity-0 scale-95">
1932
+
1933
+ <!-- Modal Header -->
1934
+ <div class="modal-header" style="flex-direction: column; align-items: stretch; gap: 0.75rem; background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%); border-bottom: 1px solid rgba(245, 158, 11, 0.2);">
1935
+ <!-- Top Row: Title, Status, and Worktree Info -->
1936
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1937
+ <div class="modal-title-section">
1938
+ <h2 class="heading" style="margin: 0; font-size: 1.5rem;" x-text="selectedSpec?.title"></h2>
1939
+ <div style="display: flex; align-items: center; gap: 1rem;">
1940
+ <!-- Workflow Status Badge -->
1941
+ <div style="background: rgba(255, 255, 255, 0.1); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem;">
1942
+ <span>📋</span>
1943
+ <span class="status-badge"
1944
+ :class="`status-${selectedSpec?.state}`"
1945
+ x-text="selectedSpec?.state || 'draft'"></span>
1946
+ </div>
1947
+ <!-- Worktree Info -->
1948
+ <div class="worktree-info" x-show="getWorktreeName(selectedSpec)">
1949
+ <span>🔧</span>
1950
+ <span x-text="getWorktreeName(selectedSpec)"></span>
1951
+ <span class="status-badge status-completed" style="font-size: 0.7rem; padding: 0.2rem 0.5rem;">✓ clean</span>
1952
+ </div>
1953
+ </div>
1954
+ </div>
1955
+ <div style="display: flex; gap: 1rem;">
1956
+ <button class="btn btn-primary" @click="exportToMarkdown(selectedSpec?.spec_id)">
1957
+ 📄 Export
1958
+ </button>
1959
+ <button class="btn btn-secondary" @click="selectedSpec=null"
1960
+ style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); color: var(--accent-orange);"
1961
+ @mouseenter="$el.style.background = 'rgba(245, 158, 11, 0.2)'"
1962
+ @mouseleave="$el.style.background = 'rgba(245, 158, 11, 0.1)'">
1963
+
1964
+ </button>
1965
+ </div>
1966
+ </div>
1967
+
1968
+ <!-- Second Row: Attach Command (Right-justified) -->
1969
+ <div x-show="getWorktreeName(selectedSpec)" style="display: flex; justify-content: flex-end;">
1970
+ <!-- Quick Connect -->
1971
+ <div style="background: rgba(34, 197, 94, 0.1); padding: 0.4rem 0.8rem; border-radius: 16px; font-size: 0.8rem; display: flex; align-items: center; gap: 0.4rem; border: 1px solid rgba(34, 197, 94, 0.3);">
1972
+ <span>💻</span>
1973
+ <span style="font-weight: 500; color: #22c55e; font-size: 0.75rem;">Attach</span>
1974
+ <span style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.7rem;" x-text="`tmux attach -t &quot;mcp-${getWorktreeName(selectedSpec)}&quot;`"></span>
1975
+ <button @click="copyTmuxAttachFromWorktree(getWorktreeName(selectedSpec))"
1976
+ id="modal-quick-connect"
1977
+ style="background: rgba(34, 197, 94, 0.2); border: 1px solid rgba(34, 197, 94, 0.4); color: #22c55e; padding: 0.15rem 0.3rem; border-radius: 4px; font-size: 0.65rem; cursor: pointer;"
1978
+ title="Copy tmux attach command"
1979
+ x-text="buttonStates['modal-quick-connect'] || '📋'">
1980
+ </button>
1981
+ </div>
1982
+ </div>
1983
+ </div>
1984
+
1985
+ <!-- Main Content Container -->
1986
+ <div class="modal-main-container">
1987
+ <!-- Navigation Tabs - Only show sections with content -->
1988
+ <div class="modal-nav">
1989
+ <div class="nav-tabs">
1990
+ <template x-for="sectionItem in getAvailableSections()" :key="sectionItem.id">
1991
+ <button class="nav-tab"
1992
+ :class="{'active': section === sectionItem.id}"
1993
+ @click="section = sectionItem.id"
1994
+ x-text="sectionItem.label">
1995
+ </button>
1996
+ </template>
1997
+ </div>
1998
+ </div>
1999
+
2000
+ <!-- Content Area -->
2001
+ <div class="modal-main">
2002
+ <template x-if="section==='summary'">
2003
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.executive_summary || selectedSpec?.sections?.executive_summary?.content || 'No executive summary yet')">
2004
+ </div>
2005
+ </template>
2006
+
2007
+ <template x-if="section==='product'">
2008
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.product_specifications?.content || 'No product specifications yet')">
2009
+ </div>
2010
+ </template>
2011
+
2012
+ <template x-if="section==='architecture'">
2013
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.architecture_analysis?.content || 'No architecture analysis yet')">
2014
+ </div>
2015
+ </template>
2016
+
2017
+ <template x-if="section==='implementation'">
2018
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.implementation_plan?.content || 'No implementation plan yet')">
2019
+ </div>
2020
+ </template>
2021
+
2022
+ <template x-if="section==='research'">
2023
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.research?.content || 'No research notes yet')">
2024
+ </div>
2025
+ </template>
2026
+
2027
+ <template x-if="section==='testing'">
2028
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.testing?.content || 'No testing notes yet')">
2029
+ </div>
2030
+ </template>
2031
+
2032
+ <template x-if="section==='review'">
2033
+ <div class="spec-content" x-html="renderMarkdown(selectedSpec?.sections?.review?.content || 'No review notes yet')">
2034
+ </div>
2035
+ </template>
2036
+ </div>
2037
+ </div>
2038
+
2039
+ <!-- Right Sidebar: Activity & Files -->
2040
+ <div class="modal-activity">
2041
+ <!-- Activity Navigation Tabs -->
2042
+ <div class="activity-nav">
2043
+ <div class="activity-tabs">
2044
+ <button class="activity-tab" :class="{'active': activityTab==='activity'}" @click="activityTab='activity'">📊 Activity</button>
2045
+ <button class="activity-tab" :class="{'active': activityTab==='files'}" @click="activityTab='files'">📁 Files</button>
2046
+ <button class="activity-tab" :class="{'active': activityTab==='tmux'}" @click="activityTab='tmux'" x-show="getWorktreeName(selectedSpec)">💻 tmux</button>
2047
+ </div>
2048
+ </div>
2049
+
2050
+ <!-- Activity Content -->
2051
+ <div class="activity-content">
2052
+ <!-- Activity Log Section - Timeline Visualization -->
2053
+ <div x-show="activityTab==='activity'">
2054
+ <div class="timeline-container" style="overflow-y: auto; height: 100%;">
2055
+ <div class="timeline-line"></div>
2056
+
2057
+ <!-- Combined Timeline -->
2058
+ <template x-for="event in getActivityTimeline(selectedSpec)" :key="event.timestamp + (event.type || 'unknown')">
2059
+ <div class="timeline-item">
2060
+ <!-- Left side: Time and Tool info -->
2061
+ <div class="timeline-left">
2062
+ <div class="timeline-time" x-text="new Date(event.timestamp).toLocaleString()"></div>
2063
+ <div style="display: flex; align-items: center; justify-content: flex-end; gap: 0.5rem;">
2064
+ <span class="status-badge"
2065
+ :class="{
2066
+ 'status-completed': event.type === 'execution',
2067
+ 'status-draft': event.type === 'debug',
2068
+ 'status-planning': event.type === 'spec_created' || event.type === 'spec_updated',
2069
+ 'status-warning': event.type === 'worktree_created' || event.type === 'system'
2070
+ }"
2071
+ x-text="event.role || 'System'">
2072
+ </span>
2073
+ <div style="font-size: 1.2rem;" x-text="event.role === 'DEBUGGER' ? '🐛' : event.role === 'ENGINEER' ? '⚙️' : event.role === 'ARCHITECT' ? '📐' : '🔧'"></div>
2074
+ </div>
2075
+ </div>
2076
+
2077
+ <!-- Timeline marker -->
2078
+ <div class="timeline-marker"
2079
+ :class="(event.role || 'system').toLowerCase()"></div>
2080
+
2081
+ <!-- Right side: Content -->
2082
+ <div class="timeline-right">
2083
+ <div class="timeline-content">
2084
+ <div class="timeline-role">
2085
+ <span x-text="event.action || event.note || event.message || event.issue || 'Activity recorded'"></span>
2086
+ </div>
2087
+
2088
+ <!-- Root cause & fix (debug logs) -->
2089
+ <div x-show="event.root_cause" style="margin: 0.75rem 0; font-size: 0.85rem; line-height: 1.5;">
2090
+ <div style="color: var(--text-secondary); background: rgba(239, 68, 68, 0.1); padding: 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; border-left: 3px solid var(--accent-red);"><strong style="color: var(--accent-red);">Root cause:</strong> <span x-text="event.root_cause" style="color: var(--text-primary);"></span></div>
2091
+ <div x-show="event.fix" style="color: var(--text-secondary); background: rgba(16, 185, 129, 0.1); padding: 0.75rem; border-radius: 6px; border-left: 3px solid var(--accent-green);"><strong style="color: var(--accent-green);">Fix:</strong> <span x-text="event.fix" style="color: var(--text-primary);"></span></div>
2092
+ </div>
2093
+
2094
+ <!-- Task/Commit Info -->
2095
+ <div x-show="event.task_id || event.commit_hash" style="display: flex; align-items: center; gap: 1rem; font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">
2096
+ <span x-show="event.task_id" x-text="'Task: ' + event.task_id"></span>
2097
+ <span x-show="event.commit_hash" x-text="'Commit: ' + event.commit_hash.substring(0, 8)"></span>
2098
+ </div>
2099
+
2100
+ <!-- Files Modified -->
2101
+ <div x-show="event.files_changed && event.files_changed.length > 0" style="margin-top: 0.5rem;">
2102
+ <div style="color: var(--text-muted); margin-bottom: 0.25rem; font-size: 0.8rem;">
2103
+ <span x-text="event.files_changed.length + ' file(s) changed'"></span>
2104
+ </div>
2105
+ </div>
2106
+
2107
+ <!-- Branch Info -->
2108
+ <div x-show="event.branch" style="margin-top: 0.5rem; font-size: 0.7rem; color: var(--text-muted); font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;">
2109
+ <span x-text="'Branch: ' + event.branch"></span>
2110
+ </div>
2111
+ </div>
2112
+ </div>
2113
+ </div>
2114
+ </template>
2115
+
2116
+ <div x-show="getActivityTimeline(selectedSpec).length === 0"
2117
+ style="text-align: center; padding: 3rem; color: var(--text-muted); font-size: 0.875rem;">
2118
+ <div style="font-size: 2rem; margin-bottom: 1rem;">📋</div>
2119
+ <div>No activity recorded yet</div>
2120
+ </div>
2121
+ </div>
2122
+ </div>
2123
+
2124
+ </div>
2125
+
2126
+ <!-- Files Modified Section -->
2127
+ <div x-show="activityTab==='files'">
2128
+ <div style="overflow-y: auto; padding: 0.5rem 0;">
2129
+ <!-- Files from execution logs -->
2130
+ <template x-for="log in (selectedSpec?.execution_logs || selectedSpec?.execution_log || [])" :key="log.timestamp + 'exec'">
2131
+ <template x-for="file in (log.files_changed || [])" :key="log.timestamp + file">
2132
+ <div style="background: rgba(255, 255, 255, 0.02); border-radius: 6px; padding: 1rem; margin-bottom: 0.75rem; border: 1px solid rgba(255, 255, 255, 0.03);">
2133
+ <div style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.85rem; color: var(--text-primary); margin-bottom: 0.5rem;"
2134
+ x-text="simplifyFilePath(file)"></div>
2135
+ <div style="display: flex; justify-content: space-between; align-items: center;">
2136
+ <span class="status-badge status-completed"
2137
+ style="font-size: 0.7rem; padding: 0.2rem 0.6rem;">MODIFIED</span>
2138
+ <span style="font-size: 0.75rem; color: var(--text-muted);"
2139
+ x-text="new Date(log.timestamp).toLocaleTimeString()"></span>
2140
+ </div>
2141
+ <div x-show="log.action"
2142
+ style="margin-top: 0.75rem; font-size: 0.75rem; color: var(--text-secondary); background: rgba(255, 255, 255, 0.02); padding: 0.5rem; border-radius: 4px;">
2143
+ <span x-text="log.action"></span>
2144
+ </div>
2145
+ </div>
2146
+ </template>
2147
+ </template>
2148
+
2149
+ <!-- Direct file changes if available -->
2150
+ <template x-for="fileChange in (selectedSpec?.file_changes || []).slice(-15)" :key="fileChange.timestamp + fileChange.file">
2151
+ <div style="background: rgba(255, 255, 255, 0.02); border-radius: 6px; padding: 0.75rem; margin-bottom: 0.5rem; border: 1px solid rgba(255, 255, 255, 0.03);">
2152
+ <div style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.8rem; color: var(--text-primary); margin-bottom: 0.25rem;"
2153
+ x-text="fileChange.file"></div>
2154
+ <div style="display: flex; justify-content: space-between; align-items: center;">
2155
+ <span :class="{
2156
+ 'status-completed': fileChange.type === 'modified',
2157
+ 'status-in-progress': fileChange.type === 'created',
2158
+ 'status-draft': fileChange.type === 'deleted'
2159
+ }"
2160
+ class="status-badge"
2161
+ style="font-size: 0.65rem; padding: 0.15rem 0.5rem;"
2162
+ x-text="fileChange.type || 'modified'"></span>
2163
+ <span style="font-size: 0.7rem; color: var(--text-muted);"
2164
+ x-text="new Date(fileChange.timestamp).toLocaleTimeString()"></span>
2165
+ </div>
2166
+ </div>
2167
+ </template>
2168
+
2169
+ <div x-show="(!selectedSpec?.execution_logs && !selectedSpec?.execution_log && !selectedSpec?.file_changes) ||
2170
+ ((!selectedSpec?.execution_logs || selectedSpec.execution_logs.length === 0) &&
2171
+ (!selectedSpec?.execution_log || selectedSpec.execution_log.length === 0) &&
2172
+ (!selectedSpec?.file_changes || selectedSpec.file_changes.length === 0))"
2173
+ style="text-align: center; padding: 2rem; color: var(--text-muted); font-size: 0.875rem;">
2174
+ No files modified yet
2175
+ </div>
2176
+ </div>
2177
+ </div>
2178
+
2179
+ <!-- tmux Logs Section -->
2180
+ <div x-show="activityTab==='tmux' && getWorktreeName(selectedSpec)">
2181
+ <div style="overflow-y: auto; padding: 0.5rem 0; height: 100%;">
2182
+ <div style="background: rgba(34, 197, 94, 0.05); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; border: 1px solid rgba(34, 197, 94, 0.2);">
2183
+ <h4 style="margin: 0 0 0.75rem 0; color: #22c55e; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem;">
2184
+ <span>💻</span>
2185
+ <span>Live tmux Session</span>
2186
+ </h4>
2187
+ <div style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.75rem;" x-text="`mcp-${getWorktreeName(selectedSpec)}`"></div>
2188
+
2189
+ <!-- Connect Instructions -->
2190
+ <div style="background: rgba(255, 255, 255, 0.02); padding: 0.75rem; border-radius: 6px; margin-bottom: 1rem;">
2191
+ <div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem;">Connect to view live logs:</div>
2192
+ <div style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.7rem; background: rgba(0, 0, 0, 0.3); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;" x-text="`tmux attach -t \"mcp-${getWorktreeName(selectedSpec)}\"`"></div>
2193
+ <button @click="copyTmuxAttachFromWorktree(getWorktreeName(selectedSpec))"
2194
+ style="background: rgba(34, 197, 94, 0.2); border: 1px solid rgba(34, 197, 94, 0.4); color: #22c55e; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.7rem; cursor: pointer;">
2195
+ 📋 Copy Command
2196
+ </button>
2197
+ </div>
2198
+
2199
+ <!-- Session Status -->
2200
+ <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; font-size: 0.75rem;">
2201
+ <div style="background: rgba(255, 255, 255, 0.02); padding: 0.5rem; border-radius: 4px;">
2202
+ <div style="color: var(--text-muted);">Status</div>
2203
+ <div style="color: #22c55e; font-weight: 500;">Active</div>
2204
+ </div>
2205
+ <div style="background: rgba(255, 255, 255, 0.02); padding: 0.5rem; border-radius: 4px;">
2206
+ <div style="color: var(--text-muted);">Location</div>
2207
+ <div style="font-family: monospace; font-size: 0.65rem;">./.bob/worktrees</div>
2208
+ </div>
2209
+ </div>
2210
+ </div>
2211
+
2212
+ <!-- Log Placeholder -->
2213
+ <div style="background: rgba(255, 255, 255, 0.02); border-radius: 6px; padding: 1rem; text-align: center; color: var(--text-muted);">
2214
+ <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">📺</div>
2215
+ <div style="font-size: 0.875rem; margin-bottom: 0.5rem;">Real-time logs available in tmux session</div>
2216
+ <div style="font-size: 0.75rem;">Connect to the session above to view live output</div>
2217
+ </div>
2218
+ </div>
2219
+ </div>
2220
+ </div>
2221
+ </div>
2222
+ </div>
2223
+ </div>
2224
+
2225
+ <!-- Activity Log Modal -->
2226
+ <div class="modal-overlay"
2227
+ x-show="showActivityLog"
2228
+ x-transition:enter="transition ease-out duration-300"
2229
+ x-transition:enter-start="opacity-0"
2230
+ x-transition:enter-end="opacity-100"
2231
+ x-transition:leave="transition ease-in duration-200"
2232
+ x-transition:leave-start="opacity-100"
2233
+ x-transition:leave-end="opacity-0"
2234
+ @click.self="showActivityLog=false">
2235
+
2236
+ <div class="modal-content"
2237
+ x-show="showActivityLog"
2238
+ x-transition:enter="transition ease-out duration-300"
2239
+ x-transition:enter-start="opacity-0 scale-95"
2240
+ x-transition:enter-end="opacity-100 scale-100"
2241
+ x-transition:leave="transition ease-in duration-200"
2242
+ x-transition:leave-start="opacity-100 scale-100"
2243
+ x-transition:leave-end="opacity-0 scale-95"
2244
+ style="grid-template-columns: 1fr; grid-template-rows: auto 1fr;">
2245
+
2246
+ <!-- Activity Log Header -->
2247
+ <div class="modal-header">
2248
+ <h2 class="heading" style="margin: 0; font-size: 1.5rem;">📊 Activity Log</h2>
2249
+ <div style="display: flex; gap: 1rem;">
2250
+ <button class="btn btn-secondary" @click="showActivityLog=false">
2251
+ ✕ Close
2252
+ </button>
2253
+ </div>
2254
+ </div>
2255
+
2256
+ <!-- Activity Log Content -->
2257
+ <div class="modal-main">
2258
+ <div style="margin-bottom: 1.5rem;">
2259
+ <h3 class="heading" style="margin: 0 0 1rem 0;">Recent Activity</h3>
2260
+ <div style="background: rgba(255, 255, 255, 0.03); padding: 1.5rem; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.05);">
2261
+
2262
+ <template x-for="activity in activities" :key="activity.timestamp">
2263
+ <div style="border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding: 1rem 0;">
2264
+ <div style="display: flex; justify-content: between; align-items: start; margin-bottom: 0.5rem;">
2265
+ <div>
2266
+ <span class="status-badge"
2267
+ :class="{
2268
+ 'status-completed': activity.type === 'spec_created',
2269
+ 'status-in-progress': activity.type === 'worktree_created',
2270
+ 'status-planning': activity.type === 'commit',
2271
+ 'status-draft': activity.type === 'spec_updated'
2272
+ }"
2273
+ x-text="activity.type.replace('_', ' ')"></span>
2274
+ <span style="margin-left: 1rem; color: var(--text-secondary); font-size: 0.875rem;"
2275
+ x-text="new Date(activity.timestamp).toLocaleString()"></span>
2276
+ </div>
2277
+ </div>
2278
+ <div style="color: var(--text-primary); margin-bottom: 0.5rem;"
2279
+ x-text="activity.message"></div>
2280
+ <div style="color: var(--text-secondary); font-size: 0.875rem;">
2281
+ <span>by </span>
2282
+ <span style="color: var(--accent-blue);" x-text="activity.author"></span>
2283
+ <template x-if="activity.files">
2284
+ <span>
2285
+ • Modified: <span x-text="activity.files.join(', ')"></span>
2286
+ </span>
2287
+ </template>
2288
+ </div>
2289
+ </div>
2290
+ </template>
2291
+
2292
+ <div x-show="activities.length === 0" style="text-align: center; padding: 2rem; color: var(--text-muted);">
2293
+ No recent activity
2294
+ </div>
2295
+ </div>
2296
+ </div>
2297
+ </div>
2298
+ </div>
2299
+ </div>
2300
+
2301
+ <!-- Worktree Modal -->
2302
+ <div class="modal-overlay"
2303
+ x-show="showWorktreeModal"
2304
+ x-transition:enter="transition ease-out duration-300"
2305
+ x-transition:enter-start="opacity-0"
2306
+ x-transition:enter-end="opacity-100"
2307
+ x-transition:leave="transition ease-in duration-200"
2308
+ x-transition:leave-start="opacity-100"
2309
+ x-transition:leave-end="opacity-0"
2310
+ @click.self="showWorktreeModal=false">
2311
+ <div class="modal-content"
2312
+ x-show="showWorktreeModal"
2313
+ x-transition:enter="transition ease-out duration-300"
2314
+ x-transition:enter-start="opacity-0 scale-95"
2315
+ x-transition:enter-end="opacity-100 scale-100"
2316
+ x-transition:leave="transition ease-in duration-200"
2317
+ x-transition:leave-start="opacity-100 scale-100"
2318
+ x-transition:leave-end="opacity-0 scale-95"
2319
+ style="width: 99vw; height: 98vh; max-width: none; min-width: 1440px; min-height: 960px; display: grid; grid-template-columns: 1fr; grid-template-rows: auto 1fr; overflow: hidden;">
2320
+ <!-- Worktree Modal Header -->
2321
+ <div class="modal-header" style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%); border-bottom: 1px solid rgba(245, 158, 11, 0.2);">
2322
+ <h2 class="heading" style="margin: 0; font-size: 1.5rem; color: var(--text-primary); display: flex; align-items: center; gap: 0.75rem;">
2323
+ <span style="color: var(--accent-orange); font-size: 1.75rem;">🏗️</span>
2324
+ <span>Active Worktrees</span>
2325
+ <span style="color: var(--text-muted); font-size: 0.9rem; font-weight: 400;">(Construction Zones)</span>
2326
+ </h2>
2327
+ <button class="btn btn-secondary" @click="showWorktreeModal=false"
2328
+ style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); color: var(--accent-orange);"
2329
+ @mouseenter="$el.style.background = 'rgba(245, 158, 11, 0.2)'"
2330
+ @mouseleave="$el.style.background = 'rgba(245, 158, 11, 0.1)'">
2331
+ 🔧 Close
2332
+ </button>
2333
+ </div>
2334
+
2335
+ <!-- Worktree Modal Content -->
2336
+ <div class="modal-main" x-data="{
2337
+ worktreeDetails: [],
2338
+ async loadWorktreeDetails() {
2339
+ try {
2340
+ const response = await fetch('/api/tools/bob.worktree.list');
2341
+ const data = await response.json();
2342
+ this.worktreeDetails = [
2343
+ ...data.committed?.map(name => ({ name, status: 'committed', color: 'var(--accent-green)', icon: '✅' })) || [],
2344
+ ...data.dirty?.map(name => ({ name, status: 'dirty', color: 'var(--accent-yellow)', icon: '⚠️' })) || [],
2345
+ ...data.clean?.map(name => ({ name, status: 'clean', color: 'var(--text-secondary)', icon: '💤' })) || []
2346
+ ];
2347
+ } catch (error) {
2348
+ console.error('Failed to load worktree details:', error);
2349
+ }
2350
+ }
2351
+ }" x-init="loadWorktreeDetails()">
2352
+
2353
+ <template x-if="worktreeDetails.length > 0">
2354
+ <div>
2355
+ <div style="display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
2356
+ <template x-for="worktree in worktreeDetails" :key="worktree.name">
2357
+ <div style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.04) 100%); border-radius: 12px; padding: 1.5rem; border: 1px solid rgba(245, 158, 11, 0.2); transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(245, 158, 11, 0.1);"
2358
+ @mouseenter="$el.style.background = 'linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(245, 158, 11, 0.06) 100%)'; $el.style.borderColor = 'rgba(245, 158, 11, 0.3)'"
2359
+ @mouseleave="$el.style.background = 'linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.04) 100%)'; $el.style.borderColor = 'rgba(245, 158, 11, 0.2)'"
2360
+
2361
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
2362
+ <h3 style="margin: 0; font-size: 1rem; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; color: var(--text-primary);"
2363
+ x-text="worktree.name"></h3>
2364
+ <span style="font-size: 1.4rem; color: var(--accent-orange);">🏗️</span>
2365
+ </div>
2366
+
2367
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
2368
+ <span class="status-badge"
2369
+ :class="{
2370
+ 'status-completed': worktree.status === 'committed',
2371
+ 'status-in-progress': worktree.status === 'dirty',
2372
+ 'status-draft': worktree.status === 'clean'
2373
+ }"
2374
+ x-text="worktree.status"></span>
2375
+ </div>
2376
+
2377
+ <div style="font-size: 0.875rem; color: var(--text-muted);">
2378
+ <div>Branch: <span style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;" x-text="worktree.name"></span></div>
2379
+ <div style="margin-top: 0.5rem;">
2380
+ Status: <span :style="'color: ' + worktree.color" x-text="worktree.status"></span>
2381
+ </div>
2382
+ </div>
2383
+ </div>
2384
+ </template>
2385
+ </div>
2386
+ </div>
2387
+ </template>
2388
+
2389
+ <template x-if="worktreeDetails.length === 0">
2390
+ <div style="text-align: center; padding: 3rem; color: var(--text-muted);">
2391
+ <div style="font-size: 3rem; margin-bottom: 1rem; color: var(--accent-orange);">🏗️</div>
2392
+ <h3 style="color: var(--text-primary); font-family: var(--font-heading);">No Active Construction Zones</h3>
2393
+ <p style="color: var(--text-secondary);">All workshop activity is currently on the main branch.</p>
2394
+ <div style="margin-top: 1.5rem; font-size: 0.9rem; color: var(--text-muted);">
2395
+ <span style="color: var(--accent-orange);">🔧</span> Ready for new projects
2396
+ </div>
2397
+ </div>
2398
+ </template>
2399
+ </div>
2400
+ </div>
2401
+ </div>
2402
+
2403
+ <!-- tmux Session Creation Modal -->
2404
+ <div class="modal-overlay"
2405
+ x-show="showTmuxModal"
2406
+ x-transition:enter="transition ease-out duration-300"
2407
+ x-transition:enter-start="opacity-0"
2408
+ x-transition:enter-end="opacity-100"
2409
+ x-transition:leave="transition ease-in duration-200"
2410
+ x-transition:leave-start="opacity-100"
2411
+ x-transition:leave-end="opacity-0"
2412
+ @click.self="showTmuxModal=false">
2413
+ <div class="modal-content"
2414
+ x-show="showTmuxModal"
2415
+ x-transition:enter="transition ease-out duration-300"
2416
+ x-transition:enter-start="opacity-0 scale-95"
2417
+ x-transition:enter-end="opacity-100 scale-100"
2418
+ x-transition:leave="transition ease-in duration-200"
2419
+ x-transition:leave-start="opacity-100 scale-100"
2420
+ x-transition:leave-end="opacity-0 scale-95"
2421
+ style="width: 99vw; height: 98vh; max-width: none; min-width: 1440px; min-height: 960px; display: grid; grid-template-columns: 1fr; grid-template-rows: auto 1fr; overflow: hidden;">
2422
+
2423
+ <!-- tmux Modal Header -->
2424
+ <div class="modal-header" style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%); border-bottom: 1px solid rgba(245, 158, 11, 0.2);">
2425
+ <h2 class="heading" style="margin: 0; font-size: 1.5rem; color: var(--text-primary); display: flex; align-items: center; gap: 0.75rem;">
2426
+ <span style="color: var(--accent-orange); font-size: 1.75rem;">🚀</span>
2427
+ <span>Launch tmux MCP Session</span>
2428
+ <span style="color: var(--text-muted); font-size: 0.9rem; font-weight: 400;">(Create a persistent development session with Claude and MCP tools)</span>
2429
+ </h2>
2430
+ <button class="btn btn-secondary" @click="showTmuxModal=false; claudePrompt=''"
2431
+ style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); color: var(--accent-orange);"
2432
+ @mouseenter="$el.style.background = 'rgba(245, 158, 11, 0.2)'"
2433
+ @mouseleave="$el.style.background = 'rgba(245, 158, 11, 0.1)'">
2434
+ ✕ Close
2435
+ </button>
2436
+ </div>
2437
+
2438
+ <!-- Combined Modal Content - Two Column Layout -->
2439
+ <div class="modal-main" style="display: flex; overflow: hidden;">
2440
+ <!-- Left Column: Launch Form -->
2441
+ <div style="flex: 1; padding: 2rem; border-right: 1px solid rgba(139, 69, 19, 0.2); background: rgba(0,0,0,0.02);">
2442
+ <h3 style="margin: 0 0 1rem 0; font-size: 1.1rem; font-weight: 600; color: var(--text-primary);">
2443
+ Use Bob's MCP to engineer the user's request below:
2444
+ </h3>
2445
+
2446
+ <textarea x-model="claudePrompt"
2447
+ placeholder="Enter your development task for Claude to execute with MCP tools...&#10;&#10;Examples:&#10;- Create a new React component with tests&#10;- Review and optimize my TypeScript codebase&#10;- Set up authentication with JWT tokens&#10;- Debug my API endpoints and fix issues&#10;- Create comprehensive documentation&#10;- Build a new feature from scratch"
2448
+ style="width: 100%; height: 140px; padding: 0.75rem; border: 1px solid rgba(139, 69, 19, 0.3); border-radius: 6px; background: rgba(0,0,0,0.15); color: var(--text-primary); font-family: var(--font-mono); resize: vertical; line-height: 1.4; margin-bottom: 1rem;"
2449
+ @focus="$el.style.borderColor = 'rgba(245, 158, 11, 0.5)'"
2450
+ @blur="$el.style.borderColor = 'rgba(139, 69, 19, 0.3)'"></textarea>
2451
+
2452
+ <div style="margin-bottom: 1rem; padding: 0.75rem; background: rgba(139, 69, 19, 0.1); border-left: 3px solid rgba(139, 69, 19, 0.4); border-radius: 0 4px 4px 0;">
2453
+ <p style="margin: 0; font-size: 0.85rem; color: var(--text-secondary); line-height: 1.4;">
2454
+ Claude will execute this prompt with full access to Bob's Workshop MCP tools in the persistent tmux session.
2455
+ </p>
2456
+ </div>
2457
+
2458
+ <!-- One-shot Launch Command -->
2459
+ <div style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(139, 69, 19, 0.08) 100%); border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
2460
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
2461
+ <h4 style="margin: 0; color: var(--accent-orange); font-size: 0.9rem; font-weight: 600;">⚡ One-Shot Launch Command:</h4>
2462
+ <button @click="copyOneShotCommand()"
2463
+ style="background: rgba(245, 158, 11, 0.2); border: 1px solid rgba(245, 158, 11, 0.4); color: var(--accent-orange); padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.7rem; cursor: pointer; font-weight: 500;"
2464
+ @mouseenter="$el.style.background = 'rgba(245, 158, 11, 0.3)'; $el.style.transform = 'translateY(-1px)'"
2465
+ @mouseleave="$el.style.background = 'rgba(245, 158, 11, 0.2)'; $el.style.transform = 'translateY(0)'">
2466
+ 📋 Copy
2467
+ </button>
2468
+ </div>
2469
+ <div style="background: rgba(0,0,0,0.4); border-radius: 6px; padding: 0.75rem; margin: 0.5rem 0;">
2470
+ <code style="color: #e5e7eb; font-family: 'Monaco', 'Menlo', monospace; font-size: 0.75rem; word-break: break-all; line-height: 1.3; display: block;"
2471
+ x-text="`tmux new-session -d -s mcp-workshop -c '/Users/pawanraviee/Documents/GitHub/bobs-workshop' 'claude -p \"${claudePrompt.replace(/\"/g, '\\"')}\" --dangerously-skip-permissions'; tmux attach -t mcp-workshop`"></code>
2472
+ </div>
2473
+ <p style="margin: 0; color: var(--text-secondary); font-size: 0.8rem;">
2474
+ Copy and run this command in your terminal for instant session creation + attachment
2475
+ </p>
2476
+ </div>
2477
+
2478
+ <!-- What happens next -->
2479
+ <div style="background: rgba(139, 69, 19, 0.08); border: 1px solid rgba(139, 69, 19, 0.2); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
2480
+ <h4 style="margin: 0 0 0.5rem 0; color: var(--text-primary); font-size: 0.9rem;">🚀 What happens next:</h4>
2481
+ <ul style="margin: 0; padding-left: 1.5rem; color: var(--text-secondary); font-size: 0.8rem; line-height: 1.5;">
2482
+ <li>Creates persistent tmux session: <code style="color: var(--accent-orange); background: rgba(0,0,0,0.3); padding: 0.1rem 0.3rem; border-radius: 3px;">mcp-workshop</code></li>
2483
+ <li>Launches Claude with full MCP tool access</li>
2484
+ <li>Runs your prompt in the development environment</li>
2485
+ <li>You can attach via: <code style="color: var(--accent-blue); background: rgba(0,0,0,0.3); padding: 0.1rem 0.3rem; border-radius: 3px;">tmux attach -t mcp-workshop</code></li>
2486
+ <li>Session persists across disconnections for continuous work</li>
2487
+ </ul>
2488
+ </div>
2489
+
2490
+ </div>
2491
+
2492
+ <!-- Right Column: Connect to tmux Sessions -->
2493
+ <div style="flex: 1; padding: 2rem; background: rgba(0,0,0,0.05); overflow-y: auto; max-height: 600px;">
2494
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1.5rem;">
2495
+ <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--text-primary);">
2496
+ 🔗 Manage tmux Sessions
2497
+ </h3>
2498
+ </div>
2499
+
2500
+ <!-- Session List - Table Style -->
2501
+ <div style="display: flex; flex-direction: column; gap: 1px; border: 1px solid rgba(139, 69, 19, 0.2); border-radius: 8px; overflow: hidden; background: rgba(139, 69, 19, 0.1);">
2502
+ <!-- Table Header -->
2503
+ <div style="display: grid; grid-template-columns: 2fr 1fr 1fr 2fr; gap: 1rem; padding: 0.75rem 1rem; background: rgba(139, 69, 19, 0.15); font-weight: 600; font-size: 0.85rem; color: var(--text-primary);">
2504
+ <div>Session Name</div>
2505
+ <div>Status</div>
2506
+ <div>Created</div>
2507
+ <div>Actions</div>
2508
+ </div>
2509
+
2510
+ <!-- Table Rows -->
2511
+ <template x-for="session in tmuxSessions?.sessions || []" :key="session.session_name">
2512
+ <div style="display: grid; grid-template-columns: 2fr 1fr 1fr 2fr; gap: 1rem; padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(139, 69, 19, 0.1); align-items: center;">
2513
+ <!-- Session Name -->
2514
+ <div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem; font-family: var(--font-mono);" x-text="session.session_name"></div>
2515
+
2516
+ <!-- Status -->
2517
+ <div style="display: flex; align-items: center; gap: 0.3rem;">
2518
+ <span x-text="getTmuxStatusIcon(session.status)" style="font-size: 0.8rem;"></span>
2519
+ <span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: capitalize;" x-text="session.status"></span>
2520
+ </div>
2521
+
2522
+ <!-- Created Date -->
2523
+ <div style="color: var(--text-muted); font-size: 0.8rem;" x-text="new Date(session.created_at).toLocaleDateString()"></div>
2524
+
2525
+ <!-- Actions -->
2526
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
2527
+ <button @click="copyTmuxAttachCommand(session.session_name)"
2528
+ style="background: rgba(34, 197, 94, 0.9); border: none; color: white; padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.75rem; cursor: pointer; font-weight: 500; font-family: var(--font-mono);"
2529
+ @mouseenter="$el.style.background = 'rgba(34, 197, 94, 1)'"
2530
+ @mouseleave="$el.style.background = 'rgba(34, 197, 94, 0.9)'">
2531
+ 📋 Copy Command
2532
+ </button>
2533
+ <button @click="copyTmuxKillCommand(session.session_name)"
2534
+ style="background: rgba(239, 68, 68, 0.9); border: none; color: white; padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.75rem; cursor: pointer; font-weight: 500; font-family: var(--font-mono);"
2535
+ @mouseenter="$el.style.background = 'rgba(239, 68, 68, 1)'"
2536
+ @mouseleave="$el.style.background = 'rgba(239, 68, 68, 0.9)'">
2537
+ 🗑 Kill Command
2538
+ </button>
2539
+ </div>
2540
+ </div>
2541
+ </template>
2542
+
2543
+ <!-- No sessions message -->
2544
+ <div x-show="!tmuxSessions?.sessions || tmuxSessions.sessions.length === 0"
2545
+ style="text-align: center; padding: 2rem; color: var(--text-secondary); background: rgba(255, 255, 255, 0.05);">
2546
+ <div style="font-size: 2rem; margin-bottom: 0.5rem;">🚀</div>
2547
+ <p style="margin: 0; font-size: 0.9rem;">No tmux sessions found</p>
2548
+ <p style="margin: 0.25rem 0 0 0; font-size: 0.8rem;">Launch your first MCP session to get started!</p>
2549
+ </div>
2550
+
2551
+ <!-- Session Summary -->
2552
+ <div x-show="tmuxSessions?.sessions && tmuxSessions.sessions.length > 0"
2553
+ style="text-align: center; padding: 1rem; margin-top: 1rem; background: rgba(139, 69, 19, 0.08); border-radius: 6px; border-top: 2px solid rgba(245, 158, 11, 0.3);">
2554
+ <p style="margin: 0; font-size: 0.8rem; color: var(--text-secondary);">
2555
+ <span x-text="tmuxSessions?.total_sessions || 0"></span> total sessions •
2556
+ <span x-text="tmuxSessions?.active_sessions || 0"></span> active •
2557
+ <span x-text="tmuxSessions?.sessions?.filter(s => s.status === 'detached').length || 0"></span> detached
2558
+ </p>
2559
+ </div>
2560
+ </div>
2561
+ </div>
2562
+ </div>
2563
+ </div>
2564
+ </div>
2565
+
2566
+
2567
+ <!-- Delete Confirmation Modal - Compact Top-Right -->
2568
+ <div x-show="confirmDelete"
2569
+ x-transition:enter="transition ease-out duration-300"
2570
+ x-transition:enter-start="opacity-0 translate-x-4"
2571
+ x-transition:enter-end="opacity-100 translate-x-0"
2572
+ x-transition:leave="transition ease-in duration-200"
2573
+ x-transition:leave-start="opacity-100 translate-x-0"
2574
+ x-transition:leave-end="opacity-0 translate-x-4"
2575
+ style="position: fixed; top: 1rem; right: 1rem; z-index: 1000; max-width: 400px;">
2576
+ <div class="glass"
2577
+ style="background: rgba(0, 0, 0, 0.95); border: 2px solid var(--accent-red); padding: 1.5rem; border-radius: 16px; backdrop-filter: blur(20px);">
2578
+ <div>
2579
+ <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
2580
+ <div style="font-size: 1.5rem; color: var(--accent-red);">⚠️</div>
2581
+ <h3 class="heading" style="color: var(--text-primary); margin: 0; font-size: 1rem;">Confirm Deletion</h3>
2582
+ </div>
2583
+ <p style="color: var(--text-secondary); margin-bottom: 1.5rem; font-size: 0.9rem; line-height: 1.4;">
2584
+ Delete "<span style="color: var(--text-primary); font-weight: 600;" x-text="confirmDelete?.title"></span>"?
2585
+ <br>
2586
+ <strong style="color: var(--accent-red); font-size: 0.85rem;">This cannot be undone.</strong>
2587
+ </p>
2588
+
2589
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
2590
+ <button class="btn btn-secondary"
2591
+ style="padding: 0.5rem 1rem; font-size: 0.85rem;"
2592
+ @click="cancelDelete()">
2593
+ Cancel
2594
+ </button>
2595
+ <button class="btn"
2596
+ style="padding: 0.5rem 1rem; font-size: 0.85rem; background: var(--accent-red); border: 1px solid var(--accent-red); color: white;"
2597
+ @click="deleteSpec()">
2598
+ 🗑 Delete
2599
+ </button>
2600
+ </div>
2601
+ </div>
2602
+ </div>
2603
+ </div>
2604
+
2605
+ <!-- Toast Notifications -->
2606
+ <div class="toast-container"
2607
+ style="position: fixed; top: 1rem; right: 1rem; z-index: 9998; max-width: 400px; padding-top: 0;"
2608
+ :style="confirmDelete ? 'padding-top: 200px;' : 'padding-top: 0;'">
2609
+ <template x-for="toast in toasts" :key="toast.id">
2610
+ <div x-show="toast.visible"
2611
+ x-transition:enter="transition ease-out duration-300 transform"
2612
+ x-transition:enter-start="translate-x-full opacity-0"
2613
+ x-transition:enter-end="translate-x-0 opacity-100"
2614
+ x-transition:leave="transition ease-in duration-200 transform"
2615
+ x-transition:leave-start="translate-x-0 opacity-100"
2616
+ x-transition:leave-end="translate-x-full opacity-0"
2617
+ class="toast-notification"
2618
+ :class="{
2619
+ 'toast-success': toast.type === 'success',
2620
+ 'toast-error': toast.type === 'error',
2621
+ 'toast-info': toast.type === 'info'
2622
+ }"
2623
+ style="margin-bottom: 0.75rem; padding: 1rem 1.5rem; border-radius: 8px; backdrop-filter: blur(10px); border: 1px solid; box-shadow: 0 4px 20px rgba(0,0,0,0.15); cursor: pointer; font-family: var(--font-body); font-size: 0.9rem; line-height: 1.4; display: flex; align-items: center; justify-content: space-between; min-width: 300px;"
2624
+ @click="removeToast(toast.id)">
2625
+ <span x-text="toast.message" style="flex: 1; margin-right: 0.75rem;"></span>
2626
+ <button @click.stop="removeToast(toast.id)"
2627
+ style="background: none; border: none; color: inherit; opacity: 0.7; font-size: 1.1em; cursor: pointer; padding: 0; margin: 0; line-height: 1;"
2628
+ @mouseover="$el.style.opacity = '1'"
2629
+ @mouseout="$el.style.opacity = '0.7'">×</button>
2630
+ </div>
2631
+ </template>
2632
+ </div>
2633
+
2634
+ <style>
2635
+ .toast-success {
2636
+ background: rgba(34, 197, 94, 0.15) !important;
2637
+ border-color: rgba(34, 197, 94, 0.3) !important;
2638
+ color: #22c55e !important;
2639
+ }
2640
+ .toast-error {
2641
+ background: rgba(239, 68, 68, 0.15) !important;
2642
+ border-color: rgba(239, 68, 68, 0.3) !important;
2643
+ color: #ef4444 !important;
2644
+ }
2645
+ .toast-info {
2646
+ background: rgba(59, 130, 246, 0.15) !important;
2647
+ border-color: rgba(59, 130, 246, 0.3) !important;
2648
+ color: #3b82f6 !important;
2649
+ }
2650
+ </style>
2651
+
2652
+ <!-- Toast Notification Container -->
2653
+ <div style="position: fixed; top: 1rem; right: 1rem; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem; max-width: 400px;">
2654
+ <template x-for="toast in toasts" :key="toast.id">
2655
+ <div x-show="true"
2656
+ x-transition:enter="transform transition ease-out duration-300"
2657
+ x-transition:enter-start="translate-x-full opacity-0"
2658
+ x-transition:enter-end="translate-x-0 opacity-100"
2659
+ x-transition:leave="transform transition ease-in duration-200"
2660
+ x-transition:leave-start="translate-x-0 opacity-100"
2661
+ x-transition:leave-end="translate-x-full opacity-0"
2662
+ :style="`background: ${getToastColor(toast.type)}; backdrop-filter: blur(10px); border-radius: 8px; padding: 1rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: 1px solid rgba(255,255,255,0.2); color: white; font-weight: 500; display: flex; align-items: center; gap: 0.75rem; cursor: pointer;`"
2663
+ @click="removeToast(toast.id)">
2664
+ <span x-text="getToastIcon(toast.type)" style="font-size: 1.1rem;"></span>
2665
+ <span x-text="toast.message" style="flex: 1; line-height: 1.4;"></span>
2666
+ <button @click.stop="removeToast(toast.id)"
2667
+ style="background: rgba(255,255,255,0.2); border: none; color: white; border-radius: 4px; padding: 0.25rem 0.5rem; font-size: 0.8rem; cursor: pointer; opacity: 0.8; transition: opacity 0.2s;"
2668
+ @mouseenter="$el.style.opacity = '1'"
2669
+ @mouseleave="$el.style.opacity = '0.8'">
2670
+
2671
+ </button>
2672
+ </div>
2673
+ </template>
2674
+ </div>
2675
+
2676
+ <!-- Global copy functionality and enhanced code block styles -->
2677
+ <script>
2678
+ // Global function for copying code blocks
2679
+ function copyToClipboard(text, buttonId) {
2680
+ navigator.clipboard.writeText(text).then(() => {
2681
+ const button = document.getElementById(buttonId);
2682
+ if (button) {
2683
+ const originalText = button.innerHTML;
2684
+ button.innerHTML = '✅ Copied!';
2685
+ button.style.background = 'rgba(16, 185, 129, 0.2)';
2686
+ button.style.borderColor = 'rgba(16, 185, 129, 0.4)';
2687
+
2688
+ setTimeout(() => {
2689
+ button.innerHTML = originalText;
2690
+ button.style.background = '';
2691
+ button.style.borderColor = '';
2692
+ }, 2000);
2693
+ }
2694
+
2695
+ // Also show toast notification if available
2696
+ try {
2697
+ window.Alpine.store('dashboard').addToast('📋 Command copied to clipboard!', 'success');
2698
+ } catch (e) {
2699
+ console.log('📋 Command copied to clipboard!');
2700
+ }
2701
+ }).catch(err => {
2702
+ console.error('Failed to copy:', err);
2703
+ try {
2704
+ window.Alpine.store('dashboard').addToast('❌ Failed to copy command', 'error');
2705
+ } catch (e) {
2706
+ console.error('Failed to copy command');
2707
+ }
2708
+ });
2709
+ }
2710
+ </script>
2711
+
2712
+ <style>
2713
+ /* Enhanced code block styles with copy buttons */
2714
+ .code-block-container {
2715
+ position: relative;
2716
+ margin: 1.5rem 0;
2717
+ background: rgba(0, 0, 0, 0.6);
2718
+ border-radius: 8px;
2719
+ border: 1px solid rgba(255, 255, 255, 0.15);
2720
+ overflow: hidden;
2721
+ }
2722
+
2723
+ .code-block-header {
2724
+ display: flex;
2725
+ justify-content: space-between;
2726
+ align-items: center;
2727
+ padding: 0.75rem 1rem;
2728
+ background: rgba(255, 255, 255, 0.05);
2729
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2730
+ }
2731
+
2732
+ .code-lang {
2733
+ font-size: 0.8rem;
2734
+ color: rgba(245, 158, 11, 0.9);
2735
+ font-weight: 500;
2736
+ text-transform: uppercase;
2737
+ letter-spacing: 0.5px;
2738
+ }
2739
+
2740
+ .copy-code-btn {
2741
+ background: rgba(255, 255, 255, 0.1);
2742
+ border: 1px solid rgba(255, 255, 255, 0.2);
2743
+ color: var(--text-primary);
2744
+ padding: 0.4rem 0.8rem;
2745
+ border-radius: 4px;
2746
+ font-size: 0.8rem;
2747
+ cursor: pointer;
2748
+ transition: all 0.2s ease;
2749
+ display: flex;
2750
+ align-items: center;
2751
+ gap: 0.3rem;
2752
+ }
2753
+
2754
+ .copy-code-btn:hover {
2755
+ background: rgba(255, 255, 255, 0.15);
2756
+ border-color: rgba(255, 255, 255, 0.3);
2757
+ transform: translateY(-1px);
2758
+ }
2759
+
2760
+ .code-block-container pre {
2761
+ margin: 0;
2762
+ padding: 1.2rem;
2763
+ background: transparent;
2764
+ border: none;
2765
+ overflow-x: auto;
2766
+ }
2767
+
2768
+ .code-block-container code {
2769
+ background: transparent;
2770
+ color: var(--text-primary);
2771
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
2772
+ font-size: 0.9rem;
2773
+ line-height: 1.5;
2774
+ }
2775
+
2776
+ .inline-code {
2777
+ background: rgba(255, 255, 255, 0.1) !important;
2778
+ padding: 0.2rem 0.4rem !important;
2779
+ border-radius: 3px !important;
2780
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important;
2781
+ font-size: 0.85em !important;
2782
+ color: rgba(245, 158, 11, 0.9) !important;
2783
+ border: 1px solid rgba(255, 255, 255, 0.15) !important;
2784
+ }
2785
+
2786
+ /* Quick Access section styling */
2787
+ .spec-content h3 {
2788
+ color: rgba(34, 197, 94, 0.9);
2789
+ border-bottom: 2px solid rgba(34, 197, 94, 0.3);
2790
+ padding-bottom: 0.5rem;
2791
+ margin-bottom: 1.5rem;
2792
+ }
2793
+
2794
+ .spec-content ul li {
2795
+ margin: 0.5rem 0;
2796
+ color: var(--text-secondary);
2797
+ }
2798
+
2799
+ .spec-content ul li strong {
2800
+ color: var(--text-primary);
2801
+ }
2802
+ </style>
2803
+
2804
+ </body>
2805
+ </html>