claude-code-kanban 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3130 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CC Kanban</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ @font-face { font-display: swap; }
12
+ </style>
13
+ <script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ <script defer src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
15
+ <style>
16
+ :root {
17
+ --bg-deep: #101114;
18
+ --bg-surface: #16181c;
19
+ --bg-elevated: #1e2025;
20
+ --bg-hover: #282a30;
21
+ --border: #363840;
22
+ --text-primary: #f0f1f3;
23
+ --text-secondary: #c2c4c9;
24
+ --text-tertiary: #9a9da5;
25
+ --text-muted: #7d808a;
26
+ --accent: #E86F33;
27
+ --accent-dim: rgba(232, 111, 51, 0.22);
28
+ --accent-glow: rgba(232, 111, 51, 0.55);
29
+ --success: #3ecf8e;
30
+ --success-dim: rgba(62, 207, 142, 0.18);
31
+ --warning: #f0b429;
32
+ --warning-dim: rgba(240, 180, 41, 0.18);
33
+ --team: #60a5fa;
34
+ --team-dim: rgba(96, 165, 250, 0.18);
35
+ --mono: 'IBM Plex Mono', monospace;
36
+ --serif: 'Playfair Display', serif;
37
+ }
38
+
39
+ * { box-sizing: border-box; margin: 0; padding: 0; }
40
+
41
+ body {
42
+ font-family: var(--mono);
43
+ font-size: 14px;
44
+ background: var(--bg-deep);
45
+ color: var(--text-primary);
46
+ line-height: 1.5;
47
+ min-height: 100vh;
48
+ -webkit-font-smoothing: antialiased;
49
+ }
50
+
51
+ /* Subtle scan-line texture */
52
+ body::before {
53
+ display: none;
54
+ }
55
+
56
+ /* Scrollbar */
57
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
58
+ ::-webkit-scrollbar-track { background: transparent; }
59
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
60
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
61
+
62
+ /* Layout */
63
+ .app { display: flex; height: 100vh; }
64
+
65
+ /* Sidebar */
66
+ .sidebar {
67
+ width: 300px;
68
+ background: var(--bg-surface);
69
+ border-right: 1px solid var(--border);
70
+ box-shadow: 1px 0 12px rgba(0, 0, 0, 0.04);
71
+ display: flex;
72
+ flex-direction: column;
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ .sidebar-header {
77
+ padding: 20px 20px 16px;
78
+ border-bottom: none;
79
+ background-image: linear-gradient(to right, transparent, var(--border), transparent);
80
+ background-size: 100% 1px;
81
+ background-repeat: no-repeat;
82
+ background-position: bottom;
83
+ }
84
+
85
+ .logo {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 10px;
89
+ }
90
+
91
+ .logo-mark {
92
+ width: 24px;
93
+ height: 24px;
94
+ background: var(--accent);
95
+ border-radius: 6px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ }
100
+
101
+ .logo-mark svg {
102
+ width: 14px;
103
+ height: 14px;
104
+ color: white;
105
+ }
106
+
107
+ .logo-text {
108
+ font-family: var(--serif);
109
+ font-size: 17px;
110
+ font-weight: 500;
111
+ letter-spacing: -0.02em;
112
+ }
113
+
114
+ .connection {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 6px;
118
+ margin-top: 12px;
119
+ font-size: 12px;
120
+ color: var(--text-tertiary);
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.05em;
123
+ }
124
+
125
+ .connection-dot {
126
+ width: 6px;
127
+ height: 6px;
128
+ border-radius: 50%;
129
+ background: var(--warning);
130
+ }
131
+
132
+ .connection-dot.live {
133
+ background: var(--success);
134
+ box-shadow: 0 0 8px var(--success);
135
+ }
136
+
137
+ .connection-dot.error {
138
+ background: #ef4444;
139
+ }
140
+
141
+ /* Sidebar sections */
142
+ .sidebar-section {
143
+ display: flex;
144
+ flex-direction: column;
145
+ border-bottom: none;
146
+ background-image: linear-gradient(to right, transparent, var(--border), transparent);
147
+ background-size: 100% 1px;
148
+ background-repeat: no-repeat;
149
+ background-position: bottom;
150
+ }
151
+
152
+ .sidebar-section.flex-1 {
153
+ flex: 1;
154
+ border-bottom: none;
155
+ overflow: hidden;
156
+ }
157
+
158
+ .section-header {
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: space-between;
162
+ padding: 14px 20px 10px;
163
+ font-size: 11px;
164
+ font-weight: 500;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.12em;
167
+ color: var(--text-muted);
168
+ }
169
+
170
+ .filter-row {
171
+ display: flex;
172
+ gap: 6px;
173
+ padding: 0 16px 10px;
174
+ }
175
+
176
+ .filter-dropdown {
177
+ flex: 1;
178
+ appearance: none;
179
+ background: var(--bg-deep);
180
+ border: 1px solid transparent;
181
+ border-radius: 6px;
182
+ padding: 7px 26px 7px 10px;
183
+ font-family: var(--mono);
184
+ font-size: 12px;
185
+ color: var(--text-secondary);
186
+ cursor: pointer;
187
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%238b8d95' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
188
+ background-repeat: no-repeat;
189
+ background-position: right 8px center;
190
+ text-overflow: ellipsis;
191
+ min-width: 0;
192
+ transition: all 0.15s ease;
193
+ }
194
+
195
+ .filter-dropdown:hover {
196
+ border-color: var(--border);
197
+ }
198
+
199
+ .filter-dropdown option {
200
+ background: var(--bg-surface);
201
+ color: var(--text-primary);
202
+ }
203
+
204
+ .filter-dropdown:focus {
205
+ outline: none;
206
+ border-color: var(--accent);
207
+ box-shadow: 0 0 0 2px var(--accent-dim);
208
+ }
209
+
210
+ /* Live Updates */
211
+ .live-updates {
212
+ padding: 0 16px 12px;
213
+ max-height: 180px;
214
+ overflow-y: auto;
215
+ }
216
+
217
+ .live-empty {
218
+ padding: 16px;
219
+ text-align: center;
220
+ font-size: 11px;
221
+ color: var(--text-muted);
222
+ }
223
+
224
+ .live-item {
225
+ display: flex;
226
+ align-items: flex-start;
227
+ gap: 10px;
228
+ padding: 10px 12px;
229
+ background: var(--bg-deep);
230
+ border: 1px solid transparent;
231
+ border-radius: 8px;
232
+ margin-bottom: 4px;
233
+ cursor: pointer;
234
+ transition: all 0.15s ease;
235
+ }
236
+
237
+ .live-item:hover {
238
+ background: var(--bg-hover);
239
+ }
240
+
241
+ .live-item .pulse {
242
+ width: 8px;
243
+ height: 8px;
244
+ margin-top: 4px;
245
+ background: var(--accent);
246
+ border-radius: 50%;
247
+ flex-shrink: 0;
248
+ animation: pulse 2s ease-in-out infinite;
249
+ box-shadow: 0 0 12px var(--accent-glow);
250
+ }
251
+
252
+ .live-item-content {
253
+ flex: 1;
254
+ min-width: 0;
255
+ }
256
+
257
+ .live-item-action {
258
+ font-size: 13px;
259
+ color: var(--text-primary);
260
+ white-space: nowrap;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ }
264
+
265
+ .live-item-session {
266
+ font-size: 11px;
267
+ color: var(--text-tertiary);
268
+ margin-top: 2px;
269
+ white-space: nowrap;
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ }
273
+
274
+ /* Sessions */
275
+ .sessions-list {
276
+ flex: 1;
277
+ overflow-y: auto;
278
+ padding: 0 14px 12px;
279
+ }
280
+
281
+ .session-item {
282
+ display: block;
283
+ width: 100%;
284
+ padding: 12px 14px;
285
+ margin-bottom: 2px;
286
+ background: transparent;
287
+ border: 1px solid transparent;
288
+ border-radius: 8px;
289
+ text-align: left;
290
+ cursor: pointer;
291
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
292
+ }
293
+
294
+ .session-item:hover {
295
+ background: var(--bg-hover);
296
+ }
297
+
298
+ .session-item.active {
299
+ background: var(--bg-elevated);
300
+ border-color: var(--border);
301
+ }
302
+
303
+ .session-name {
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: space-between;
307
+ gap: 8px;
308
+ }
309
+
310
+ .session-name span {
311
+ font-size: 14px;
312
+ color: var(--text-primary);
313
+ white-space: nowrap;
314
+ overflow: hidden;
315
+ text-overflow: ellipsis;
316
+ }
317
+
318
+ .session-name .pulse {
319
+ width: 8px;
320
+ height: 8px;
321
+ background: var(--accent);
322
+ border-radius: 50%;
323
+ animation: pulse 2s ease-in-out infinite;
324
+ box-shadow: 0 0 12px var(--accent-glow);
325
+ }
326
+
327
+ @keyframes pulse {
328
+ 0%, 100% { opacity: 1; transform: scale(1); }
329
+ 50% { opacity: 0.7; transform: scale(0.9); }
330
+ }
331
+
332
+ .session-secondary {
333
+ font-size: 12px;
334
+ font-weight: 450;
335
+ color: var(--text-tertiary);
336
+ margin-top: 2px;
337
+ white-space: nowrap;
338
+ overflow: hidden;
339
+ }
340
+
341
+ .session-branch {
342
+ font-size: 10px;
343
+ color: var(--accent);
344
+ margin-top: 3px;
345
+ padding: 2px 6px;
346
+ background: var(--border);
347
+ border-radius: 3px;
348
+ display: inline-block;
349
+ font-family: var(--mono);
350
+ white-space: nowrap;
351
+ overflow: hidden;
352
+ text-overflow: ellipsis;
353
+ }
354
+
355
+ .session-branch {
356
+ font-size: 10px;
357
+ color: var(--accent);
358
+ margin-top: 3px;
359
+ padding: 2px 6px;
360
+ background: var(--border);
361
+ border-radius: 3px;
362
+ display: inline-block;
363
+ white-space: nowrap;
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ max-width: 100%;
367
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
368
+ }
369
+
370
+ .session-progress {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 8px;
374
+ margin-top: 8px;
375
+ }
376
+
377
+ .progress-bar {
378
+ flex: 1;
379
+ height: 2px;
380
+ background: var(--border);
381
+ border-radius: 1px;
382
+ overflow: hidden;
383
+ }
384
+
385
+ .progress-fill {
386
+ height: 100%;
387
+ background: var(--accent);
388
+ transition: width 0.5s ease-out;
389
+ }
390
+
391
+ .progress-text {
392
+ font-size: 11px;
393
+ font-weight: 500;
394
+ color: var(--text-tertiary);
395
+ font-variant-numeric: tabular-nums;
396
+ }
397
+
398
+ .session-time {
399
+ font-size: 11px;
400
+ font-weight: 450;
401
+ color: var(--text-tertiary);
402
+ margin-top: 6px;
403
+ }
404
+
405
+ /* Footer */
406
+ .sidebar-footer {
407
+ padding: 14px 20px;
408
+ border-top: none;
409
+ background-image: linear-gradient(to right, transparent, var(--border), transparent);
410
+ background-size: 100% 1px;
411
+ background-repeat: no-repeat;
412
+ background-position: top;
413
+ font-size: 10px;
414
+ color: var(--text-muted);
415
+ }
416
+
417
+ .sidebar-footer a {
418
+ color: var(--text-tertiary);
419
+ text-decoration: none;
420
+ transition: color 0.15s;
421
+ }
422
+
423
+ .sidebar-footer a:hover {
424
+ color: var(--text-secondary);
425
+ }
426
+
427
+ /* Main */
428
+ .main {
429
+ flex: 1;
430
+ display: flex;
431
+ flex-direction: column;
432
+ overflow: hidden;
433
+ background: var(--bg-deep);
434
+ }
435
+
436
+ /* Empty state */
437
+ .empty-state {
438
+ flex: 1;
439
+ display: flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ flex-direction: column;
443
+ gap: 16px;
444
+ color: var(--text-muted);
445
+ }
446
+
447
+ .empty-state svg {
448
+ width: 48px;
449
+ height: 48px;
450
+ opacity: 0.5;
451
+ }
452
+
453
+ .empty-state p {
454
+ font-size: 13px;
455
+ }
456
+
457
+ /* Session view */
458
+ .session-view {
459
+ flex: 1;
460
+ display: none;
461
+ flex-direction: column;
462
+ overflow: hidden;
463
+ }
464
+
465
+ .session-view.visible {
466
+ display: flex;
467
+ }
468
+
469
+ /* Header */
470
+ .view-header {
471
+ padding: 16px 24px;
472
+ border-bottom: 1px solid var(--border);
473
+ background: var(--bg-surface);
474
+ display: flex;
475
+ align-items: center;
476
+ justify-content: space-between;
477
+ }
478
+
479
+ .view-title {
480
+ font-family: var(--serif);
481
+ font-size: 26px;
482
+ font-weight: 400;
483
+ letter-spacing: -0.03em;
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 12px;
487
+ }
488
+
489
+ .view-meta {
490
+ font-size: 12px;
491
+ color: var(--text-tertiary);
492
+ margin-top: 4px;
493
+ }
494
+
495
+ .view-actions {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 16px;
499
+ }
500
+
501
+ .view-progress {
502
+ display: flex;
503
+ align-items: center;
504
+ gap: 10px;
505
+ }
506
+
507
+ .view-progress .progress-bar {
508
+ width: 120px;
509
+ height: 3px;
510
+ }
511
+
512
+ .view-progress .progress-text {
513
+ font-size: 14px;
514
+ font-weight: 500;
515
+ color: var(--accent);
516
+ }
517
+
518
+ .icon-btn {
519
+ width: 34px;
520
+ height: 34px;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ background: transparent;
525
+ border: 1px solid var(--border);
526
+ border-radius: 6px;
527
+ color: var(--text-tertiary);
528
+ cursor: pointer;
529
+ transition: all 0.15s ease;
530
+ }
531
+
532
+ .icon-btn:hover {
533
+ background: var(--bg-hover);
534
+ color: var(--text-primary);
535
+ border-color: var(--text-muted);
536
+ }
537
+
538
+ .icon-btn svg {
539
+ width: 16px;
540
+ height: 16px;
541
+ }
542
+
543
+ .icon-btn-danger {
544
+ color: #ef4444;
545
+ }
546
+
547
+ .icon-btn-danger:hover {
548
+ background: rgba(239, 68, 68, 0.1);
549
+ color: #dc2626;
550
+ border-color: #ef4444;
551
+ }
552
+
553
+ /* Kanban */
554
+ .kanban {
555
+ flex: 1;
556
+ display: flex;
557
+ gap: 24px;
558
+ padding: 24px;
559
+ overflow-x: auto;
560
+ }
561
+
562
+ .kanban-column {
563
+ flex: 1;
564
+ min-width: 280px;
565
+ max-width: 400px;
566
+ display: flex;
567
+ flex-direction: column;
568
+ }
569
+
570
+ .column-header {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 10px;
574
+ padding-bottom: 20px;
575
+ border-bottom: none;
576
+ margin-bottom: 16px;
577
+ background-image: linear-gradient(to bottom, transparent 80%, var(--border) 100%);
578
+ background-size: 100% 1px;
579
+ background-repeat: no-repeat;
580
+ background-position: bottom;
581
+ }
582
+
583
+ .column-dot {
584
+ width: 8px;
585
+ height: 8px;
586
+ border-radius: 50%;
587
+ }
588
+
589
+ .column-dot.pending { background: var(--text-tertiary); }
590
+ .column-dot.in-progress {
591
+ background: var(--accent);
592
+ box-shadow: 0 0 12px var(--accent-glow);
593
+ animation: pulse 2s ease-in-out infinite;
594
+ }
595
+ .column-dot.completed { background: var(--success); }
596
+
597
+ .column-title {
598
+ font-size: 14px;
599
+ font-weight: 500;
600
+ text-transform: uppercase;
601
+ letter-spacing: 0.06em;
602
+ }
603
+
604
+ .column-title.pending { color: var(--text-tertiary); }
605
+ .column-title.in-progress { color: var(--accent); }
606
+ .column-title.completed { color: var(--success); }
607
+
608
+ .column-count {
609
+ font-size: 11px;
610
+ padding: 2px 8px;
611
+ border-radius: 10px;
612
+ font-variant-numeric: tabular-nums;
613
+ }
614
+
615
+ .column-count.pending {
616
+ background: var(--bg-elevated);
617
+ color: var(--text-tertiary);
618
+ }
619
+
620
+ .column-count.in-progress {
621
+ background: var(--accent-dim);
622
+ color: var(--accent);
623
+ }
624
+
625
+ .column-count.completed {
626
+ background: var(--success-dim);
627
+ color: var(--success);
628
+ }
629
+
630
+ .column-tasks {
631
+ flex: 1;
632
+ overflow-y: auto;
633
+ display: flex;
634
+ flex-direction: column;
635
+ gap: 8px;
636
+ }
637
+
638
+ .column-empty {
639
+ text-align: center;
640
+ padding: 32px 16px;
641
+ color: var(--text-muted);
642
+ font-size: 12px;
643
+ border: 1px dashed var(--border);
644
+ border-radius: 8px;
645
+ margin-top: 4px;
646
+ }
647
+
648
+ .column-empty svg {
649
+ width: 24px;
650
+ height: 24px;
651
+ opacity: 0.5;
652
+ margin-bottom: 8px;
653
+ }
654
+
655
+ /* Task card */
656
+ .task-card {
657
+ padding: 16px;
658
+ padding-left: 18px;
659
+ background: var(--bg-surface);
660
+ border: 1px solid var(--border);
661
+ border-left: 2px solid var(--text-muted);
662
+ border-radius: 8px;
663
+ cursor: pointer;
664
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
665
+ }
666
+
667
+ .task-card:hover {
668
+ background: var(--bg-elevated);
669
+ border-color: var(--text-muted);
670
+ border-left-color: var(--text-muted);
671
+ }
672
+
673
+ .task-card:focus-visible {
674
+ outline: 2px solid var(--accent);
675
+ outline-offset: 2px;
676
+ }
677
+
678
+ .task-card.in-progress {
679
+ border-color: var(--accent);
680
+ border-left: 2px solid var(--accent);
681
+ }
682
+
683
+ .task-card.in-progress:hover {
684
+ border-left-color: var(--accent);
685
+ }
686
+
687
+ .task-card.completed {
688
+ opacity: 0.85;
689
+ border-left: 2px solid var(--success);
690
+ }
691
+
692
+ .task-card.completed:hover {
693
+ border-left-color: var(--success);
694
+ }
695
+
696
+ .task-card.blocked {
697
+ opacity: 0.7;
698
+ }
699
+
700
+ .task-id {
701
+ font-size: 11px;
702
+ color: var(--text-muted);
703
+ margin-bottom: 6px;
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 8px;
707
+ }
708
+
709
+ .task-badge {
710
+ font-size: 10px;
711
+ font-weight: 600;
712
+ padding: 3px 8px;
713
+ border-radius: 4px;
714
+ text-transform: uppercase;
715
+ letter-spacing: 0.04em;
716
+ }
717
+
718
+ .task-badge.blocked {
719
+ background: rgba(220, 80, 30, 0.15);
720
+ color: #dc4e1e;
721
+ }
722
+
723
+ .task-title {
724
+ font-size: 14px;
725
+ color: var(--text-primary);
726
+ line-height: 1.4;
727
+ }
728
+
729
+ .task-card.completed .task-title {
730
+ text-decoration: line-through;
731
+ color: var(--text-tertiary);
732
+ }
733
+
734
+ .task-session {
735
+ font-size: 12px;
736
+ color: var(--accent);
737
+ margin-top: 6px;
738
+ }
739
+
740
+ .task-active {
741
+ display: flex;
742
+ align-items: center;
743
+ gap: 6px;
744
+ margin-top: 10px;
745
+ padding-top: 10px;
746
+ border-top: 1px solid var(--border);
747
+ font-size: 11px;
748
+ font-weight: 500;
749
+ color: var(--accent);
750
+ }
751
+
752
+ .task-active::before {
753
+ content: '';
754
+ width: 6px;
755
+ height: 6px;
756
+ background: var(--accent);
757
+ border-radius: 50%;
758
+ animation: pulse 2s ease-in-out infinite;
759
+ }
760
+
761
+ .task-blocked {
762
+ font-size: 11px;
763
+ font-weight: 500;
764
+ color: var(--text-muted);
765
+ margin-top: 8px;
766
+ }
767
+
768
+ .task-desc {
769
+ font-size: 12px;
770
+ font-weight: 450;
771
+ color: var(--text-tertiary);
772
+ margin-top: 8px;
773
+ display: -webkit-box;
774
+ -webkit-line-clamp: 2;
775
+ -webkit-box-orient: vertical;
776
+ overflow: hidden;
777
+ }
778
+
779
+ /* Detail panel */
780
+ .detail-panel {
781
+ position: fixed;
782
+ top: 0;
783
+ right: 0;
784
+ width: 440px;
785
+ height: 100vh;
786
+ background: var(--bg-surface);
787
+ border-left: 1px solid var(--border);
788
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
789
+ display: flex;
790
+ flex-direction: column;
791
+ display: none;
792
+ z-index: 100;
793
+ overflow: hidden;
794
+ }
795
+
796
+ .detail-panel.visible {
797
+ display: flex;
798
+ }
799
+
800
+ .detail-header {
801
+ padding: 16px 20px;
802
+ border-bottom: none;
803
+ background-image: linear-gradient(to right, transparent, var(--border), transparent);
804
+ background-size: 100% 1px;
805
+ background-repeat: no-repeat;
806
+ background-position: bottom;
807
+ display: flex;
808
+ align-items: center;
809
+ justify-content: space-between;
810
+ }
811
+
812
+ .detail-header h3 {
813
+ font-family: var(--serif);
814
+ font-size: 14px;
815
+ font-weight: 500;
816
+ }
817
+
818
+ .detail-close {
819
+ width: 28px;
820
+ height: 28px;
821
+ display: flex;
822
+ align-items: center;
823
+ justify-content: center;
824
+ background: transparent;
825
+ border: none;
826
+ color: var(--text-muted);
827
+ cursor: pointer;
828
+ border-radius: 4px;
829
+ transition: all 0.15s;
830
+ }
831
+
832
+ .detail-close:hover {
833
+ background: var(--bg-hover);
834
+ color: var(--text-primary);
835
+ }
836
+
837
+ .detail-close svg {
838
+ width: 16px;
839
+ height: 16px;
840
+ }
841
+
842
+ .detail-content {
843
+ flex: 1;
844
+ overflow-y: auto;
845
+ padding: 20px;
846
+ }
847
+
848
+ .detail-section {
849
+ margin-bottom: 16px;
850
+ padding-bottom: 16px;
851
+ border-bottom: none;
852
+ background-image: linear-gradient(to right, transparent, var(--border), transparent);
853
+ background-size: 100% 1px;
854
+ background-repeat: no-repeat;
855
+ background-position: bottom;
856
+ }
857
+
858
+ .detail-section:last-child {
859
+ background-image: none;
860
+ }
861
+
862
+ .detail-label {
863
+ font-size: 11px;
864
+ font-weight: 500;
865
+ text-transform: uppercase;
866
+ letter-spacing: 0.08em;
867
+ color: var(--text-muted);
868
+ margin-bottom: 8px;
869
+ }
870
+
871
+ .detail-title {
872
+ font-family: var(--serif);
873
+ font-size: 22px;
874
+ line-height: 1.4;
875
+ }
876
+
877
+ .detail-status {
878
+ display: inline-flex;
879
+ align-items: center;
880
+ gap: 6px;
881
+ font-size: 11px;
882
+ font-weight: 600;
883
+ padding: 4px 10px;
884
+ border-radius: 20px;
885
+ letter-spacing: 0.03em;
886
+ text-transform: uppercase;
887
+ }
888
+
889
+ .detail-status.pending {
890
+ background: var(--bg-elevated);
891
+ color: var(--text-muted);
892
+ border: 1px solid var(--border);
893
+ }
894
+
895
+ .detail-status.in_progress {
896
+ background: var(--accent-dim);
897
+ color: var(--accent);
898
+ border: 1px solid var(--accent);
899
+ }
900
+
901
+ .detail-status.completed {
902
+ background: var(--success-dim);
903
+ color: var(--success);
904
+ border: 1px solid var(--success);
905
+ }
906
+
907
+ .detail-status .dot {
908
+ width: 8px;
909
+ height: 8px;
910
+ border-radius: 50%;
911
+ background: currentColor;
912
+ }
913
+
914
+ .detail-status.in_progress .dot {
915
+ animation: pulse 2s ease-in-out infinite;
916
+ }
917
+
918
+ .detail-box {
919
+ padding: 12px;
920
+ border-radius: 6px;
921
+ font-size: 12px;
922
+ }
923
+
924
+ .detail-box.active {
925
+ background: rgba(232, 111, 51, 0.08);
926
+ border: 1px solid rgba(232, 111, 51, 0.2);
927
+ color: var(--text-primary);
928
+ }
929
+
930
+ .detail-box.active strong {
931
+ color: var(--accent);
932
+ }
933
+
934
+ .detail-box.blocked {
935
+ background: var(--warning-dim);
936
+ border: 1px solid rgba(240, 180, 41, 0.35);
937
+ color: var(--warning);
938
+ }
939
+
940
+ .detail-box.blocks {
941
+ background: var(--team-dim);
942
+ border: 1px solid rgba(96, 165, 250, 0.35);
943
+ color: var(--team);
944
+ }
945
+
946
+ .detail-desc {
947
+ font-size: 14px;
948
+ line-height: 1.7;
949
+ color: var(--text-secondary);
950
+ }
951
+
952
+ .detail-desc pre {
953
+ background: var(--bg-elevated);
954
+ padding: 12px;
955
+ border-radius: 6px;
956
+ overflow-x: auto;
957
+ margin: 12px 0;
958
+ font-size: 12px;
959
+ }
960
+
961
+ .detail-desc code {
962
+ background: var(--bg-elevated);
963
+ padding: 2px 6px;
964
+ border-radius: 3px;
965
+ font-size: 0.9em;
966
+ }
967
+
968
+ .detail-desc pre code {
969
+ background: transparent;
970
+ padding: 0;
971
+ }
972
+
973
+ .detail-desc hr {
974
+ border: none;
975
+ border-top: 1px solid var(--border);
976
+ margin: 16px 0;
977
+ }
978
+
979
+ .detail-desc h4 {
980
+ font-size: 10px;
981
+ font-weight: 600;
982
+ text-transform: uppercase;
983
+ letter-spacing: 0.05em;
984
+ color: var(--accent);
985
+ margin: 0 0 8px 0;
986
+ }
987
+
988
+ .detail-desc p {
989
+ margin: 0 0 12px 0;
990
+ }
991
+
992
+ .detail-desc p:last-child {
993
+ margin-bottom: 0;
994
+ }
995
+
996
+ /* Note form */
997
+ .note-section {
998
+ margin-top: 24px;
999
+ padding-top: 20px;
1000
+ border-top: 1px solid var(--border);
1001
+ }
1002
+
1003
+ .note-form {
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ gap: 10px;
1007
+ }
1008
+
1009
+ .note-input {
1010
+ width: 100%;
1011
+ padding: 10px 12px;
1012
+ background: var(--bg-elevated);
1013
+ border: 1px solid var(--border);
1014
+ border-radius: 6px;
1015
+ color: var(--text-primary);
1016
+ font-family: var(--mono);
1017
+ font-size: 12px;
1018
+ line-height: 1.5;
1019
+ resize: vertical;
1020
+ min-height: 60px;
1021
+ }
1022
+
1023
+ .note-input:focus {
1024
+ outline: none;
1025
+ border-color: var(--accent);
1026
+ box-shadow: 0 0 0 2px var(--accent-dim);
1027
+ }
1028
+
1029
+ .note-input::placeholder {
1030
+ color: var(--text-muted);
1031
+ }
1032
+
1033
+ .note-submit {
1034
+ align-self: flex-end;
1035
+ padding: 8px 16px;
1036
+ background: var(--accent);
1037
+ border: none;
1038
+ border-radius: 5px;
1039
+ color: white;
1040
+ font-family: var(--mono);
1041
+ font-size: 11px;
1042
+ font-weight: 500;
1043
+ cursor: pointer;
1044
+ transition: all 0.15s ease;
1045
+ }
1046
+
1047
+ .note-submit:hover {
1048
+ filter: brightness(1.1);
1049
+ }
1050
+
1051
+ /* Team badge */
1052
+ .session-indicators {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ gap: 6px;
1056
+ flex-shrink: 0;
1057
+ }
1058
+
1059
+ .team-badge {
1060
+ display: inline-flex;
1061
+ align-items: center;
1062
+ gap: 3px;
1063
+ font-size: 11px;
1064
+ font-weight: 500;
1065
+ color: var(--text-muted);
1066
+ flex-shrink: 0;
1067
+ }
1068
+
1069
+ .team-badge .member-count {
1070
+ font-weight: 600;
1071
+ }
1072
+
1073
+ .team-info-btn {
1074
+ width: 24px;
1075
+ height: 24px;
1076
+ display: inline-flex;
1077
+ align-items: center;
1078
+ justify-content: center;
1079
+ font-size: 12px;
1080
+ background: var(--bg-deep);
1081
+ border: 1px solid transparent;
1082
+ border-radius: 4px;
1083
+ color: var(--team);
1084
+ cursor: pointer;
1085
+ font-size: 11px;
1086
+ flex-shrink: 0;
1087
+ transition: all 0.15s ease;
1088
+ }
1089
+
1090
+ .team-info-btn:hover {
1091
+ background: var(--team-dim);
1092
+ border-color: var(--team);
1093
+ }
1094
+
1095
+ /* Task owner badge */
1096
+ .task-owner-badge {
1097
+ display: inline-flex;
1098
+ align-items: center;
1099
+ gap: 4px;
1100
+ font-size: 10px;
1101
+ font-weight: 500;
1102
+ padding: 3px 8px;
1103
+ border-radius: 4px;
1104
+ text-transform: none;
1105
+ letter-spacing: 0;
1106
+ }
1107
+
1108
+ /* Team modal member card */
1109
+ .team-member-card {
1110
+ padding: 12px;
1111
+ background: var(--bg-elevated);
1112
+ border: 1px solid var(--border);
1113
+ border-radius: 8px;
1114
+ margin-bottom: 8px;
1115
+ }
1116
+
1117
+ .team-member-card .member-name {
1118
+ font-size: 13px;
1119
+ font-weight: 500;
1120
+ color: var(--text-primary);
1121
+ display: flex;
1122
+ align-items: center;
1123
+ gap: 6px;
1124
+ }
1125
+
1126
+ .team-member-card .member-detail {
1127
+ font-size: 11px;
1128
+ color: var(--text-tertiary);
1129
+ margin-top: 4px;
1130
+ }
1131
+
1132
+ .team-member-card .member-tasks {
1133
+ font-size: 11px;
1134
+ color: var(--accent);
1135
+ margin-top: 4px;
1136
+ }
1137
+
1138
+ .team-modal-desc {
1139
+ font-size: 12px;
1140
+ color: var(--text-secondary);
1141
+ font-style: italic;
1142
+ margin-bottom: 16px;
1143
+ }
1144
+
1145
+ .team-modal-meta {
1146
+ font-size: 11px;
1147
+ color: var(--text-muted);
1148
+ margin-top: 16px;
1149
+ padding-top: 12px;
1150
+ border-top: 1px solid var(--border);
1151
+ }
1152
+
1153
+ /* Owner filter — overlaid, zero layout impact */
1154
+ .kanban {
1155
+ position: relative;
1156
+ }
1157
+
1158
+ .owner-filter-bar {
1159
+ display: none;
1160
+ position: absolute;
1161
+ top: 24px;
1162
+ right: 24px;
1163
+ z-index: 2;
1164
+ }
1165
+
1166
+ .owner-filter-bar.visible {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ }
1170
+
1171
+ .owner-filter-bar .filter-dropdown {
1172
+ flex: none;
1173
+ width: auto;
1174
+ max-width: 180px;
1175
+ font-size: 13px;
1176
+ padding: 6px 10px;
1177
+ }
1178
+
1179
+ /* Light mode */
1180
+ body.light {
1181
+ --bg-deep: #e8e6e3;
1182
+ --bg-surface: #f4f3f1;
1183
+ --bg-elevated: #dddbd8;
1184
+ --bg-hover: #d2d0cc;
1185
+ --border: #a09b94;
1186
+ --text-primary: #0a0a0a;
1187
+ --text-secondary: #444444;
1188
+ --text-tertiary: #666666;
1189
+ --text-muted: #888888;
1190
+ --accent-dim: rgba(232, 111, 51, 0.18);
1191
+ --accent-glow: rgba(232, 111, 51, 0.5);
1192
+ --success: #1a8a5a;
1193
+ --success-dim: rgba(26, 138, 90, 0.15);
1194
+ --warning: #b07d0a;
1195
+ --warning-dim: rgba(176, 125, 10, 0.15);
1196
+ }
1197
+
1198
+ body.light::before {
1199
+ display: none;
1200
+ }
1201
+
1202
+ /* Interactive elements */
1203
+ .icon-btn.delete:hover {
1204
+ background: rgba(239, 68, 68, 0.1);
1205
+ border-color: #ef4444;
1206
+ }
1207
+
1208
+ .column-header {
1209
+ display: flex;
1210
+ align-items: center;
1211
+ gap: 10px;
1212
+ }
1213
+
1214
+ .column-header .icon-btn {
1215
+ width: 28px;
1216
+ height: 28px;
1217
+ }
1218
+
1219
+ .column-header .icon-btn svg {
1220
+ width: 14px;
1221
+ height: 14px;
1222
+ }
1223
+
1224
+ /* Search input */
1225
+ .search-container {
1226
+ position: relative;
1227
+ padding: 0 16px 8px;
1228
+ }
1229
+
1230
+ .search-input {
1231
+ width: 100%;
1232
+ padding: 9px 32px 9px 12px;
1233
+ background: var(--bg-deep);
1234
+ border: 1px solid transparent;
1235
+ border-radius: 6px;
1236
+ color: var(--text-primary);
1237
+ font-family: var(--mono);
1238
+ font-size: 13px;
1239
+ transition: all 0.15s ease;
1240
+ }
1241
+
1242
+ .search-input:hover {
1243
+ border-color: var(--border);
1244
+ }
1245
+
1246
+ .search-input:focus {
1247
+ outline: none;
1248
+ border-color: var(--accent);
1249
+ box-shadow: 0 0 0 2px var(--accent-dim);
1250
+ }
1251
+
1252
+ .search-input::placeholder {
1253
+ color: var(--text-muted);
1254
+ }
1255
+
1256
+ .search-clear {
1257
+ position: absolute;
1258
+ right: 18px;
1259
+ top: 8px;
1260
+ width: 20px;
1261
+ height: 20px;
1262
+ background: none;
1263
+ border: none;
1264
+ color: var(--text-tertiary);
1265
+ cursor: pointer;
1266
+ display: none;
1267
+ align-items: center;
1268
+ justify-content: center;
1269
+ padding: 0;
1270
+ border-radius: 3px;
1271
+ }
1272
+
1273
+ .search-clear:hover {
1274
+ background: var(--bg-hover);
1275
+ color: var(--text-primary);
1276
+ }
1277
+
1278
+ .search-clear.visible {
1279
+ display: flex;
1280
+ }
1281
+
1282
+ /* Modal */
1283
+ .modal-overlay {
1284
+ position: fixed;
1285
+ inset: 0;
1286
+ background: rgba(0, 0, 0, 0.5);
1287
+ display: none;
1288
+ align-items: center;
1289
+ justify-content: center;
1290
+ z-index: 10000;
1291
+ }
1292
+
1293
+ .modal-overlay.visible {
1294
+ display: flex;
1295
+ }
1296
+
1297
+ .modal {
1298
+ background: var(--bg-surface);
1299
+ border: 1px solid var(--border);
1300
+ border-radius: 12px;
1301
+ width: 90%;
1302
+ max-width: 500px;
1303
+ padding: 24px;
1304
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1305
+ }
1306
+
1307
+ .modal-header {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: space-between;
1311
+ margin-bottom: 20px;
1312
+ }
1313
+
1314
+ .modal-title {
1315
+ font-size: 18px;
1316
+ font-weight: 600;
1317
+ color: var(--text-primary);
1318
+ }
1319
+
1320
+ .modal-close {
1321
+ width: 32px;
1322
+ height: 32px;
1323
+ border: none;
1324
+ background: none;
1325
+ color: var(--text-secondary);
1326
+ cursor: pointer;
1327
+ display: flex;
1328
+ align-items: center;
1329
+ justify-content: center;
1330
+ border-radius: 6px;
1331
+ transition: all 0.15s ease;
1332
+ }
1333
+
1334
+ .modal-close:hover {
1335
+ background: var(--bg-hover);
1336
+ color: var(--text-primary);
1337
+ }
1338
+
1339
+ .modal-body {
1340
+ margin-bottom: 24px;
1341
+ }
1342
+
1343
+ .form-group {
1344
+ margin-bottom: 16px;
1345
+ }
1346
+
1347
+ .form-group:last-child {
1348
+ margin-bottom: 0;
1349
+ }
1350
+
1351
+ .form-label {
1352
+ display: block;
1353
+ font-size: 12px;
1354
+ font-weight: 500;
1355
+ color: var(--text-secondary);
1356
+ margin-bottom: 6px;
1357
+ text-transform: uppercase;
1358
+ letter-spacing: 0.5px;
1359
+ }
1360
+
1361
+ .form-input {
1362
+ width: 100%;
1363
+ padding: 10px 12px;
1364
+ background: var(--bg-elevated);
1365
+ border: 1px solid var(--border);
1366
+ border-radius: 6px;
1367
+ color: var(--text-primary);
1368
+ font-family: var(--mono);
1369
+ font-size: 14px;
1370
+ transition: all 0.15s ease;
1371
+ }
1372
+
1373
+ .form-input:focus {
1374
+ outline: none;
1375
+ border-color: var(--accent);
1376
+ box-shadow: 0 0 0 3px var(--accent-dim);
1377
+ }
1378
+
1379
+ .form-input::placeholder {
1380
+ color: var(--text-muted);
1381
+ }
1382
+
1383
+ textarea.form-input {
1384
+ resize: vertical;
1385
+ min-height: 80px;
1386
+ font-family: var(--mono);
1387
+ }
1388
+
1389
+ select.form-input[multiple] {
1390
+ padding: 4px;
1391
+ }
1392
+
1393
+ select.form-input optgroup {
1394
+ background: var(--bg-surface);
1395
+ color: var(--accent);
1396
+ font-weight: 600;
1397
+ font-size: 11px;
1398
+ text-transform: uppercase;
1399
+ letter-spacing: 0.05em;
1400
+ padding: 8px 8px 4px 8px;
1401
+ margin-top: 4px;
1402
+ font-style: normal;
1403
+ }
1404
+
1405
+ select.form-input optgroup:first-child {
1406
+ margin-top: 0;
1407
+ }
1408
+
1409
+ select.form-input option {
1410
+ padding: 8px 8px 8px 16px;
1411
+ background: var(--bg-elevated);
1412
+ color: var(--text-primary);
1413
+ font-weight: 400;
1414
+ font-size: 13px;
1415
+ border-radius: 4px;
1416
+ margin: 2px 4px;
1417
+ }
1418
+
1419
+ select.form-input option:checked {
1420
+ background: var(--accent);
1421
+ color: var(--bg-deep);
1422
+ font-weight: 500;
1423
+ }
1424
+
1425
+ .modal-footer {
1426
+ display: flex;
1427
+ gap: 12px;
1428
+ justify-content: flex-end;
1429
+ }
1430
+
1431
+ .btn {
1432
+ padding: 10px 20px;
1433
+ border: none;
1434
+ border-radius: 6px;
1435
+ font-family: var(--mono);
1436
+ font-size: 13px;
1437
+ font-weight: 500;
1438
+ cursor: pointer;
1439
+ transition: all 0.15s ease;
1440
+ }
1441
+
1442
+ .btn-primary {
1443
+ background: var(--accent);
1444
+ color: white;
1445
+ }
1446
+
1447
+ .btn-primary:hover {
1448
+ background: #d96329;
1449
+ transform: translateY(-1px);
1450
+ box-shadow: 0 4px 12px var(--accent-glow);
1451
+ }
1452
+
1453
+ .btn-secondary {
1454
+ background: var(--bg-elevated);
1455
+ color: var(--text-primary);
1456
+ border: 1px solid var(--border);
1457
+ }
1458
+
1459
+ .btn-secondary:hover {
1460
+ background: var(--bg-hover);
1461
+ border-color: var(--text-muted);
1462
+ }
1463
+
1464
+ /* Skip navigation */
1465
+ .skip-link {
1466
+ position: absolute;
1467
+ top: -100%;
1468
+ left: 16px;
1469
+ z-index: 100000;
1470
+ padding: 8px 16px;
1471
+ background: var(--accent);
1472
+ color: white;
1473
+ font-size: 13px;
1474
+ border-radius: 0 0 6px 6px;
1475
+ text-decoration: none;
1476
+ transition: top 0.2s;
1477
+ }
1478
+
1479
+ .skip-link:focus {
1480
+ top: 0;
1481
+ }
1482
+
1483
+ /* Visually hidden (a11y) */
1484
+ .sr-only {
1485
+ position: absolute;
1486
+ width: 1px;
1487
+ height: 1px;
1488
+ padding: 0;
1489
+ margin: -1px;
1490
+ overflow: hidden;
1491
+ clip: rect(0, 0, 0, 0);
1492
+ white-space: nowrap;
1493
+ border: 0;
1494
+ }
1495
+
1496
+ /* System theme detection */
1497
+ @media (prefers-color-scheme: light) {
1498
+ body:not(.dark-forced) {
1499
+ --bg-deep: #e8e6e3;
1500
+ --bg-surface: #f4f3f1;
1501
+ --bg-elevated: #dddbd8;
1502
+ --bg-hover: #d2d0cc;
1503
+ --border: #a09b94;
1504
+ --text-primary: #0a0a0a;
1505
+ --text-secondary: #444444;
1506
+ --text-tertiary: #666666;
1507
+ --text-muted: #888888;
1508
+ --accent-dim: rgba(232, 111, 51, 0.18);
1509
+ --accent-glow: rgba(232, 111, 51, 0.5);
1510
+ --success: #1a8a5a;
1511
+ --success-dim: rgba(26, 138, 90, 0.15);
1512
+ --warning: #b07d0a;
1513
+ --warning-dim: rgba(176, 125, 10, 0.15);
1514
+ }
1515
+
1516
+ body:not(.dark-forced)::before {
1517
+ display: none;
1518
+ }
1519
+ }
1520
+
1521
+ /* Card entrance animation */
1522
+ @keyframes fadeSlideIn {
1523
+ from { opacity: 0; transform: translateY(8px); }
1524
+ to { opacity: 1; transform: translateY(0); }
1525
+ }
1526
+
1527
+ .column-tasks .task-card {
1528
+ animation: fadeSlideIn 150ms ease-out both;
1529
+ }
1530
+
1531
+ .column-tasks .task-card:nth-child(1) { animation-delay: 0ms; }
1532
+ .column-tasks .task-card:nth-child(2) { animation-delay: 30ms; }
1533
+ .column-tasks .task-card:nth-child(3) { animation-delay: 60ms; }
1534
+ .column-tasks .task-card:nth-child(4) { animation-delay: 90ms; }
1535
+ .column-tasks .task-card:nth-child(5) { animation-delay: 120ms; }
1536
+ .column-tasks .task-card:nth-child(6) { animation-delay: 150ms; }
1537
+ .column-tasks .task-card:nth-child(7) { animation-delay: 180ms; }
1538
+ .column-tasks .task-card:nth-child(8) { animation-delay: 210ms; }
1539
+ .column-tasks .task-card:nth-child(9) { animation-delay: 240ms; }
1540
+ .column-tasks .task-card:nth-child(10) { animation-delay: 270ms; }
1541
+
1542
+ /* Connection status breathing */
1543
+ @keyframes breathe {
1544
+ 0%, 100% { opacity: 1; }
1545
+ 50% { opacity: 0.65; }
1546
+ }
1547
+
1548
+ .connection-dot.live {
1549
+ animation: breathe 3s ease-in-out infinite;
1550
+ }
1551
+
1552
+ /* Progress bar shimmer */
1553
+ @keyframes shimmer {
1554
+ 0% { background-position: -200% 0; }
1555
+ 100% { background-position: 200% 0; }
1556
+ }
1557
+
1558
+ .progress-fill.shimmer {
1559
+ background: linear-gradient(90deg, var(--accent) 0%, rgba(232,111,51,0.75) 50%, var(--accent) 100%);
1560
+ background-size: 200% 100%;
1561
+ animation: shimmer 2s linear infinite;
1562
+ }
1563
+
1564
+ /* Session list hover accent bar */
1565
+ .session-item {
1566
+ position: relative;
1567
+ }
1568
+
1569
+ .session-item::before {
1570
+ content: '';
1571
+ position: absolute;
1572
+ left: 0;
1573
+ top: 50%;
1574
+ transform: translateY(-50%);
1575
+ width: 0;
1576
+ height: 60%;
1577
+ background: var(--accent);
1578
+ border-radius: 0 2px 2px 0;
1579
+ transition: width 0.15s ease;
1580
+ }
1581
+
1582
+ .session-item:hover::before {
1583
+ width: 2px;
1584
+ }
1585
+ </style>
1586
+ </head>
1587
+ <body>
1588
+ <a href="#main-content" class="skip-link">Skip to main content</a>
1589
+ <div class="app">
1590
+ <!-- Sidebar -->
1591
+ <aside class="sidebar">
1592
+ <header class="sidebar-header">
1593
+ <div class="logo">
1594
+ <div class="logo-mark">
1595
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
1596
+ <path d="M5 13l4 4L19 7"/>
1597
+ </svg>
1598
+ </div>
1599
+ <span class="logo-text">Claude Tasks</span>
1600
+ </div>
1601
+ <div id="connection-status" class="connection">
1602
+ <span class="connection-dot"></span>
1603
+ <span>Connecting</span>
1604
+ </div>
1605
+ </header>
1606
+
1607
+ <!-- Live Updates -->
1608
+ <div class="sidebar-section">
1609
+ <div class="section-header">
1610
+ <span>Live Updates</span>
1611
+ </div>
1612
+ <div id="live-updates" class="live-updates">
1613
+ <div class="live-empty">No active tasks</div>
1614
+ </div>
1615
+ </div>
1616
+
1617
+ <!-- Tasks -->
1618
+ <div class="sidebar-section flex-1">
1619
+ <div class="section-header">
1620
+ <span>Sessions</span>
1621
+ <button onclick="showAllTasks()" style="background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; padding: 3px 8px; font-size: 10px; color: var(--text-secondary); cursor: pointer; font-family: var(--mono);">All Tasks</button>
1622
+ </div>
1623
+ <div class="search-container">
1624
+ <input
1625
+ id="search-input"
1626
+ type="text"
1627
+ class="search-input"
1628
+ placeholder="Search tasks, sessions, projects..."
1629
+ oninput="handleSearch(this.value)"
1630
+ />
1631
+ <button id="search-clear-btn" class="search-clear" onclick="clearSearch()" title="Clear search" aria-label="Clear search">
1632
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1633
+ <path d="M18 6L6 18M6 6l12 12"/>
1634
+ </svg>
1635
+ </button>
1636
+ </div>
1637
+ <div class="filter-row">
1638
+ <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)" aria-label="Filter by project">
1639
+ <option value="">All Projects</option>
1640
+ </select>
1641
+ <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)" aria-label="Filter by session status">
1642
+ <option value="all">All Sessions</option>
1643
+ <option value="active">Active Only</option>
1644
+ </select>
1645
+ </div>
1646
+ <div class="filter-row">
1647
+ <select id="session-limit" class="filter-dropdown" onchange="changeSessionLimit(this.value)" aria-label="Number of sessions to show">
1648
+ <option value="10">Show 10</option>
1649
+ <option value="20">Show 20</option>
1650
+ <option value="50">Show 50</option>
1651
+ <option value="all">Show All</option>
1652
+ </select>
1653
+ </div>
1654
+ <div id="sessions-list" class="sessions-list"></div>
1655
+ </div>
1656
+
1657
+ </aside>
1658
+
1659
+ <!-- Main -->
1660
+ <main class="main">
1661
+ <div id="no-session" class="empty-state">
1662
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1663
+ <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
1664
+ </svg>
1665
+ <p>Select a session to view tasks</p>
1666
+ </div>
1667
+
1668
+ <div id="session-view" class="session-view">
1669
+ <header class="view-header">
1670
+ <div>
1671
+ <h1 id="session-title" class="view-title">Session</h1>
1672
+ <p id="session-meta" class="view-meta"></p>
1673
+ </div>
1674
+ <div class="view-actions">
1675
+ <div class="view-progress">
1676
+ <div class="progress-bar">
1677
+ <div id="progress-bar" class="progress-fill" style="width: 0%"></div>
1678
+ </div>
1679
+ <span id="progress-percent" class="progress-text">0%</span>
1680
+ </div>
1681
+ <button id="theme-toggle" class="icon-btn" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
1682
+ <svg id="theme-icon-dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1683
+ <path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
1684
+ </svg>
1685
+ <svg id="theme-icon-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
1686
+ <circle cx="12" cy="12" r="5"/>
1687
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
1688
+ </svg>
1689
+ </button>
1690
+ <button class="icon-btn" onclick="showHelpModal()" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">
1691
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1692
+ <circle cx="12" cy="12" r="10"/>
1693
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
1694
+ <circle cx="12" cy="17" r="0.5" fill="currentColor"/>
1695
+ </svg>
1696
+ </button>
1697
+ <a href="https://github.com/NikiforovAll/claude-task-viewer" target="_blank" class="icon-btn" title="View on GitHub" aria-label="View on GitHub">
1698
+ <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
1699
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
1700
+ </svg>
1701
+ </a>
1702
+ </div>
1703
+ </header>
1704
+
1705
+ <div class="kanban" id="main-content">
1706
+ <div id="owner-filter-bar" class="owner-filter-bar">
1707
+ <select id="owner-filter" class="filter-dropdown" onchange="filterByOwner(this.value)" aria-label="Filter by team member">
1708
+ <option value="">All Members</option>
1709
+ </select>
1710
+ </div>
1711
+ <div class="kanban-column" aria-label="Pending tasks">
1712
+ <div class="column-header">
1713
+ <span class="column-dot pending"></span>
1714
+ <span class="column-title pending">Pending</span>
1715
+ <span id="pending-count" class="column-count pending">0</span>
1716
+ </div>
1717
+ <div id="pending-tasks" class="column-tasks" role="list"></div>
1718
+ </div>
1719
+
1720
+ <div class="kanban-column" aria-label="In progress tasks">
1721
+ <div class="column-header">
1722
+ <span class="column-dot in-progress"></span>
1723
+ <span class="column-title in-progress">In Progress</span>
1724
+ <span id="in-progress-count" class="column-count in-progress">0</span>
1725
+ </div>
1726
+ <div id="in-progress-tasks" class="column-tasks" role="list"></div>
1727
+ </div>
1728
+
1729
+ <div class="kanban-column" aria-label="Completed tasks">
1730
+ <div class="column-header">
1731
+ <span class="column-dot completed"></span>
1732
+ <span class="column-title completed">Completed</span>
1733
+ <span id="completed-count" class="column-count completed">0</span>
1734
+ </div>
1735
+ <div id="completed-tasks" class="column-tasks" role="list"></div>
1736
+ </div>
1737
+ </div>
1738
+ </div>
1739
+ </main>
1740
+
1741
+ <!-- Detail panel -->
1742
+ <aside id="detail-panel" class="detail-panel">
1743
+ <header class="detail-header">
1744
+ <h3>Task Details</h3>
1745
+ <button id="close-detail" class="detail-close" aria-label="Close detail panel">
1746
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1747
+ <path d="M6 18L18 6M6 6l12 12"/>
1748
+ </svg>
1749
+ </button>
1750
+ </header>
1751
+ <div id="detail-content" class="detail-content"></div>
1752
+ </aside>
1753
+ </div>
1754
+
1755
+ <script>
1756
+ // State
1757
+ let sessions = [];
1758
+ let currentSessionId = null;
1759
+ let currentTasks = [];
1760
+ let viewMode = 'session';
1761
+ let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
1762
+ let sessionLimit = localStorage.getItem('sessionLimit') || '20'; // '10', '20', '50', 'all'
1763
+ let filterProject = null; // null = all projects, or project path to filter
1764
+ let searchQuery = ''; // Search query for fuzzy search
1765
+ let allTasksCache = []; // Cache all tasks for search
1766
+ let bulkDeleteSessionId = null; // Track session for bulk delete
1767
+ let ownerFilter = '';
1768
+
1769
+ // DOM
1770
+ const sessionsList = document.getElementById('sessions-list');
1771
+ const noSession = document.getElementById('no-session');
1772
+ const sessionView = document.getElementById('session-view');
1773
+ const sessionTitle = document.getElementById('session-title');
1774
+ const sessionMeta = document.getElementById('session-meta');
1775
+ const progressPercent = document.getElementById('progress-percent');
1776
+ const progressBar = document.getElementById('progress-bar');
1777
+ const pendingTasks = document.getElementById('pending-tasks');
1778
+ const inProgressTasks = document.getElementById('in-progress-tasks');
1779
+ const completedTasks = document.getElementById('completed-tasks');
1780
+ const pendingCount = document.getElementById('pending-count');
1781
+ const inProgressCount = document.getElementById('in-progress-count');
1782
+ const completedCount = document.getElementById('completed-count');
1783
+ const detailPanel = document.getElementById('detail-panel');
1784
+ const detailContent = document.getElementById('detail-content');
1785
+ const connectionStatus = document.getElementById('connection-status');
1786
+
1787
+ let lastSessionsHash = '';
1788
+ let lastTasksHash = '';
1789
+
1790
+ async function fetchSessions() {
1791
+ console.log('[fetchSessions] Starting...');
1792
+ try {
1793
+ const res = await fetch(`/api/sessions?limit=${sessionLimit}`);
1794
+ const newSessions = await res.json();
1795
+ const tasksRes = await fetch('/api/tasks/all');
1796
+ const newTasks = await tasksRes.json();
1797
+
1798
+ const sessionsHash = JSON.stringify(newSessions);
1799
+ const tasksHash = JSON.stringify(newTasks);
1800
+ if (sessionsHash === lastSessionsHash && tasksHash === lastTasksHash) {
1801
+ console.log('[fetchSessions] No changes, skipping render');
1802
+ return;
1803
+ }
1804
+ lastSessionsHash = sessionsHash;
1805
+ lastTasksHash = tasksHash;
1806
+
1807
+ sessions = newSessions;
1808
+ allTasksCache = newTasks;
1809
+ console.log('[fetchSessions] Sessions loaded:', sessions.length);
1810
+ renderSessions();
1811
+ console.log('[fetchSessions] Render complete');
1812
+ fetchLiveUpdates();
1813
+ } catch (error) {
1814
+ console.error('Failed to fetch sessions:', error);
1815
+ }
1816
+ }
1817
+
1818
+ function handleSearch(query) {
1819
+ searchQuery = query.toLowerCase().trim();
1820
+
1821
+ // Show/hide clear button
1822
+ const clearBtn = document.getElementById('search-clear-btn');
1823
+ if (searchQuery) {
1824
+ clearBtn.classList.add('visible');
1825
+ } else {
1826
+ clearBtn.classList.remove('visible');
1827
+ }
1828
+
1829
+ renderSessions();
1830
+ }
1831
+
1832
+ function clearSearch() {
1833
+ const searchInput = document.getElementById('search-input');
1834
+ searchInput.value = '';
1835
+ searchQuery = '';
1836
+ document.getElementById('search-clear-btn').classList.remove('visible');
1837
+ renderSessions();
1838
+ }
1839
+
1840
+ function deleteAllSessionTasks(sessionId) {
1841
+ const session = sessions.find(s => s.id === sessionId);
1842
+ if (!session) return;
1843
+
1844
+ // When viewing a single session, currentTasks already contains only that session's tasks
1845
+ // When viewing "All Tasks", tasks have sessionId property, so we filter
1846
+ const sessionTasks = currentSessionId === sessionId
1847
+ ? currentTasks
1848
+ : currentTasks.filter(t => t.sessionId === sessionId);
1849
+
1850
+ if (sessionTasks.length === 0) {
1851
+ alert('No tasks to delete in this session');
1852
+ return;
1853
+ }
1854
+
1855
+ bulkDeleteSessionId = sessionId;
1856
+
1857
+ const displayName = session.name || sessionId;
1858
+ const message = `Delete all ${sessionTasks.length} task(s) from session "${displayName}"?`;
1859
+
1860
+ document.getElementById('delete-session-tasks-message').textContent = message;
1861
+
1862
+ const modal = document.getElementById('delete-session-tasks-modal');
1863
+ modal.classList.add('visible');
1864
+
1865
+ // Handle ESC key
1866
+ const keyHandler = (e) => {
1867
+ if (e.key === 'Escape') {
1868
+ e.preventDefault();
1869
+ closeDeleteSessionTasksModal();
1870
+ document.removeEventListener('keydown', keyHandler);
1871
+ }
1872
+ };
1873
+ document.addEventListener('keydown', keyHandler);
1874
+ }
1875
+
1876
+ function closeDeleteSessionTasksModal() {
1877
+ const modal = document.getElementById('delete-session-tasks-modal');
1878
+ modal.classList.remove('visible');
1879
+ bulkDeleteSessionId = null;
1880
+ }
1881
+
1882
+ async function confirmDeleteSessionTasks() {
1883
+ if (!bulkDeleteSessionId) return;
1884
+
1885
+ const sessionId = bulkDeleteSessionId;
1886
+ closeDeleteSessionTasksModal();
1887
+
1888
+ // Get tasks to delete
1889
+ const sessionTasks = currentSessionId === sessionId
1890
+ ? currentTasks
1891
+ : currentTasks.filter(t => t.sessionId === sessionId);
1892
+
1893
+ // Sort tasks by dependency order (blocked tasks first, then blockers)
1894
+ const sortedTasks = topologicalSort(sessionTasks);
1895
+
1896
+ let successCount = 0;
1897
+ let failedCount = 0;
1898
+ const failedTasks = [];
1899
+
1900
+ for (const task of sortedTasks) {
1901
+ try {
1902
+ const res = await fetch(`/api/tasks/${sessionId}/${task.id}`, {
1903
+ method: 'DELETE'
1904
+ });
1905
+
1906
+ if (res.ok) {
1907
+ successCount++;
1908
+ } else {
1909
+ failedCount++;
1910
+ const error = await res.json();
1911
+ failedTasks.push({ id: task.id, subject: task.subject, error: error.error });
1912
+ console.error(`Failed to delete task ${task.id}:`, error);
1913
+ }
1914
+ } catch (error) {
1915
+ failedCount++;
1916
+ failedTasks.push({ id: task.id, subject: task.subject, error: 'Network error' });
1917
+ console.error(`Error deleting task ${task.id}:`, error);
1918
+ }
1919
+ }
1920
+
1921
+ // Show result modal
1922
+ showDeleteResultModal(successCount, failedCount, failedTasks);
1923
+
1924
+ // Close detail panel if open
1925
+ closeDetailPanel();
1926
+
1927
+ // Refresh the view
1928
+ await refreshCurrentView();
1929
+ }
1930
+
1931
+ // Topological sort for task deletion order
1932
+ function topologicalSort(tasks) {
1933
+ const result = [];
1934
+ const visited = new Set();
1935
+ const visiting = new Set();
1936
+ const taskMap = new Map(tasks.map(t => [t.id, t]));
1937
+
1938
+ function visit(taskId) {
1939
+ if (visited.has(taskId)) return;
1940
+ if (visiting.has(taskId)) return; // Cycle - skip
1941
+
1942
+ visiting.add(taskId);
1943
+ const task = taskMap.get(taskId);
1944
+
1945
+ if (task && task.blocks && task.blocks.length > 0) {
1946
+ // Visit all tasks that this task blocks (dependencies first)
1947
+ for (const blockedId of task.blocks) {
1948
+ if (taskMap.has(blockedId)) {
1949
+ visit(blockedId);
1950
+ }
1951
+ }
1952
+ }
1953
+
1954
+ visiting.delete(taskId);
1955
+ visited.add(taskId);
1956
+ if (task) result.push(task);
1957
+ }
1958
+
1959
+ // Visit all tasks
1960
+ for (const task of tasks) {
1961
+ visit(task.id);
1962
+ }
1963
+
1964
+ return result;
1965
+ }
1966
+
1967
+ function showDeleteResultModal(successCount, failedCount, failedTasks) {
1968
+ const modal = document.getElementById('delete-result-modal');
1969
+ const messageEl = document.getElementById('delete-result-message');
1970
+ const detailsEl = document.getElementById('delete-result-details');
1971
+
1972
+ if (failedCount === 0) {
1973
+ messageEl.textContent = `Successfully deleted all ${successCount} task(s).`;
1974
+ detailsEl.style.display = 'none';
1975
+ } else {
1976
+ messageEl.textContent = `Deleted ${successCount} task(s). Failed to delete ${failedCount} task(s).`;
1977
+
1978
+ const failedList = failedTasks.map(t =>
1979
+ `<li><strong>${escapeHtml(t.subject)}</strong> (#${escapeHtml(t.id)}): ${escapeHtml(t.error)}</li>`
1980
+ ).join('');
1981
+ detailsEl.innerHTML = `<ul style="margin: 8px 0 0 0; padding-left: 20px;">${failedList}</ul>`;
1982
+ detailsEl.style.display = 'block';
1983
+ }
1984
+
1985
+ modal.classList.add('visible');
1986
+
1987
+ // Handle ESC key
1988
+ const keyHandler = (e) => {
1989
+ if (e.key === 'Escape') {
1990
+ e.preventDefault();
1991
+ closeDeleteResultModal();
1992
+ document.removeEventListener('keydown', keyHandler);
1993
+ }
1994
+ };
1995
+ document.addEventListener('keydown', keyHandler);
1996
+ }
1997
+
1998
+ function closeDeleteResultModal() {
1999
+ const modal = document.getElementById('delete-result-modal');
2000
+ modal.classList.remove('visible');
2001
+ }
2002
+
2003
+ function fuzzyMatch(text, query) {
2004
+ if (!query) return true;
2005
+ if (!text) return false;
2006
+
2007
+ text = text.toLowerCase();
2008
+ query = query.toLowerCase();
2009
+
2010
+ // Prioritize exact substring match
2011
+ if (text.includes(query)) return true;
2012
+
2013
+ // Split by common delimiters to search in individual words
2014
+ const words = text.split(/[\s\-_\/\.]+/);
2015
+
2016
+ // Check if query matches start of any word
2017
+ for (const word of words) {
2018
+ if (word.startsWith(query)) return true;
2019
+ }
2020
+
2021
+ // Check if any word contains the query
2022
+ for (const word of words) {
2023
+ if (word.includes(query)) return true;
2024
+ }
2025
+
2026
+ return false;
2027
+ }
2028
+
2029
+ async function fetchLiveUpdates() {
2030
+ try {
2031
+ const res = await fetch('/api/tasks/all');
2032
+ const allTasks = await res.json();
2033
+ let activeTasks = allTasks.filter(t => t.status === 'in_progress');
2034
+ if (filterProject) {
2035
+ activeTasks = activeTasks.filter(t => t.project === filterProject);
2036
+ }
2037
+ renderLiveUpdates(activeTasks);
2038
+ } catch (error) {
2039
+ console.error('Failed to fetch live updates:', error);
2040
+ }
2041
+ }
2042
+
2043
+ function renderLiveUpdates(activeTasks) {
2044
+ const container = document.getElementById('live-updates');
2045
+
2046
+ if (activeTasks.length === 0) {
2047
+ container.innerHTML = '<div class="live-empty">No active tasks</div>';
2048
+ return;
2049
+ }
2050
+
2051
+ container.innerHTML = activeTasks.map(task => `
2052
+ <div class="live-item" onclick="openLiveTask('${task.sessionId}', '${task.id}')">
2053
+ <span class="pulse"></span>
2054
+ <div class="live-item-content">
2055
+ <div class="live-item-action">${escapeHtml(task.activeForm || task.subject)}</div>
2056
+ <div class="live-item-session">${escapeHtml(task.sessionName || task.sessionId.slice(0, 8))}</div>
2057
+ </div>
2058
+ </div>
2059
+ `).join('');
2060
+ }
2061
+
2062
+ async function openLiveTask(sessionId, taskId) {
2063
+ await fetchTasks(sessionId);
2064
+ showTaskDetail(taskId, sessionId);
2065
+ }
2066
+
2067
+ let lastCurrentTasksHash = '';
2068
+
2069
+ async function fetchTasks(sessionId) {
2070
+ try {
2071
+ viewMode = 'session';
2072
+ const res = await fetch(`/api/sessions/${sessionId}`);
2073
+
2074
+ let newTasks;
2075
+ if (res.ok) {
2076
+ newTasks = await res.json();
2077
+ } else if (res.status === 404) {
2078
+ newTasks = [];
2079
+ } else {
2080
+ throw new Error(`Failed to fetch tasks: ${res.status}`);
2081
+ }
2082
+
2083
+ const hash = JSON.stringify(newTasks);
2084
+ if (sessionId === currentSessionId && hash === lastCurrentTasksHash) {
2085
+ console.log('[fetchTasks] No changes, skipping render');
2086
+ return;
2087
+ }
2088
+ lastCurrentTasksHash = hash;
2089
+
2090
+ currentTasks = newTasks;
2091
+ currentSessionId = sessionId;
2092
+ ownerFilter = '';
2093
+ renderSession();
2094
+ } catch (error) {
2095
+ console.error('Failed to fetch tasks:', error);
2096
+ currentTasks = [];
2097
+ currentSessionId = sessionId;
2098
+ lastCurrentTasksHash = '';
2099
+ renderSession();
2100
+ }
2101
+ }
2102
+
2103
+ async function showAllTasks() {
2104
+ try {
2105
+ viewMode = 'all';
2106
+ currentSessionId = null;
2107
+ ownerFilter = '';
2108
+ const res = await fetch('/api/tasks/all');
2109
+ let tasks = await res.json();
2110
+ if (filterProject) {
2111
+ tasks = tasks.filter(t => t.project === filterProject);
2112
+ }
2113
+ currentTasks = tasks;
2114
+ renderAllTasks();
2115
+ renderSessions();
2116
+ } catch (error) {
2117
+ console.error('Failed to fetch all tasks:', error);
2118
+ }
2119
+ }
2120
+
2121
+ function renderAllTasks() {
2122
+ noSession.style.display = 'none';
2123
+ sessionView.classList.add('visible');
2124
+ document.getElementById('owner-filter-bar').classList.remove('visible');
2125
+
2126
+ const totalTasks = currentTasks.length;
2127
+ const completed = currentTasks.filter(t => t.status === 'completed').length;
2128
+ const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
2129
+
2130
+ const projectName = filterProject ? filterProject.split('/').pop() : null;
2131
+ sessionTitle.textContent = filterProject ? `Tasks: ${projectName}` : 'All Tasks';
2132
+ sessionMeta.textContent = filterProject
2133
+ ? `${totalTasks} tasks in this project`
2134
+ : `${totalTasks} tasks across ${sessions.length} sessions`;
2135
+ progressPercent.textContent = `${percent}%`;
2136
+ progressBar.style.width = `${percent}%`;
2137
+
2138
+ renderKanban();
2139
+ }
2140
+
2141
+ function renderSessions() {
2142
+ // Update project dropdown
2143
+ updateProjectDropdown();
2144
+
2145
+ let filteredSessions = sessions;
2146
+ if (sessionFilter === 'active') {
2147
+ filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
2148
+ }
2149
+ if (filterProject) {
2150
+ filteredSessions = filteredSessions.filter(s => s.project === filterProject);
2151
+ }
2152
+
2153
+ // Apply search filter
2154
+ if (searchQuery) {
2155
+ filteredSessions = filteredSessions.filter(session => {
2156
+ // Search in session name and ID
2157
+ if (session.name && fuzzyMatch(session.name, searchQuery)) return true;
2158
+ if (session.id && fuzzyMatch(session.id, searchQuery)) return true;
2159
+
2160
+ // Search in project path
2161
+ if (session.project && fuzzyMatch(session.project, searchQuery)) return true;
2162
+
2163
+ // Search in description
2164
+ if (session.description && fuzzyMatch(session.description, searchQuery)) return true;
2165
+
2166
+ // Search in tasks for this session
2167
+ const sessionTasks = allTasksCache.filter(t => t.sessionId === session.id);
2168
+ return sessionTasks.some(task =>
2169
+ (task.subject && fuzzyMatch(task.subject, searchQuery)) ||
2170
+ (task.description && fuzzyMatch(task.description, searchQuery)) ||
2171
+ (task.activeForm && fuzzyMatch(task.activeForm, searchQuery))
2172
+ );
2173
+ });
2174
+ }
2175
+
2176
+ if (filteredSessions.length === 0) {
2177
+ let emptyMsg = 'No sessions found';
2178
+ let emptyHint = 'Tasks appear when you use Claude Code';
2179
+
2180
+ if (searchQuery) {
2181
+ emptyMsg = `No results for "${searchQuery}"`;
2182
+ emptyHint = 'Try a different search term or clear the search';
2183
+ } else if (filterProject && sessionFilter === 'active') {
2184
+ emptyMsg = 'No active sessions for this project';
2185
+ emptyHint = 'Try "All Sessions" or "All Projects"';
2186
+ } else if (filterProject) {
2187
+ emptyMsg = 'No sessions for this project';
2188
+ emptyHint = 'Select "All Projects" to see all';
2189
+ } else if (sessionFilter === 'active') {
2190
+ emptyMsg = 'No active sessions';
2191
+ emptyHint = 'Select "All Sessions" to see all';
2192
+ }
2193
+ sessionsList.innerHTML = `
2194
+ <div style="padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 12px;">
2195
+ <p>${emptyMsg}</p>
2196
+ <p style="margin-top: 8px; font-size: 11px;">${emptyHint}</p>
2197
+ </div>
2198
+ `;
2199
+ return;
2200
+ }
2201
+
2202
+ sessionsList.innerHTML = filteredSessions.map(session => {
2203
+ const total = session.taskCount;
2204
+ const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
2205
+ const isActive = session.id === currentSessionId && viewMode === 'session';
2206
+ const hasInProgress = session.inProgress > 0;
2207
+ const sessionName = session.name || session.id.slice(0, 8) + '...';
2208
+ const projectName = session.project ? session.project.split('/').pop() : null;
2209
+ const primaryName = projectName || sessionName;
2210
+ const secondaryName = projectName ? sessionName : null;
2211
+
2212
+ // Format git branch for display
2213
+ const gitBranch = session.gitBranch ? escapeHtml(session.gitBranch) : null;
2214
+
2215
+ // Format timestamps for display
2216
+ const createdDisplay = session.createdAt ? formatDate(session.createdAt) : '';
2217
+ const modifiedDisplay = formatDate(session.modifiedAt);
2218
+ const timeDisplay = session.createdAt && createdDisplay !== modifiedDisplay
2219
+ ? `Created ${createdDisplay} · Modified ${modifiedDisplay}`
2220
+ : modifiedDisplay;
2221
+
2222
+ // Build tooltip
2223
+ const tooltip = [timeDisplay, gitBranch ? `Branch: ${gitBranch}` : ''].filter(Boolean).join(' | ');
2224
+
2225
+ const isTeam = session.isTeam;
2226
+ const memberCount = session.memberCount || 0;
2227
+
2228
+ return `
2229
+ <button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
2230
+ <div class="session-name">
2231
+ <span>${escapeHtml(primaryName)}</span>
2232
+ <span class="session-indicators">
2233
+ ${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
2234
+ ${isTeam ? `<span class="team-info-btn" onclick="event.stopPropagation(); showTeamModalForSession('${session.id}')" title="View team info">ℹ</span>` : ''}
2235
+ ${hasInProgress ? '<span class="pulse"></span>' : ''}
2236
+ </span>
2237
+ </div>
2238
+ ${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
2239
+ ${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
2240
+ <div class="session-progress">
2241
+ <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
2242
+ <span class="progress-text">${session.completed}/${total}</span>
2243
+ </div>
2244
+ <div class="session-time">${formatDate(session.modifiedAt)}</div>
2245
+ </button>
2246
+ `;
2247
+ }).join('');
2248
+ }
2249
+
2250
+ function renderSession() {
2251
+ noSession.style.display = 'none';
2252
+ sessionView.classList.add('visible');
2253
+
2254
+ const session = sessions.find(s => s.id === currentSessionId);
2255
+ if (!session) return;
2256
+
2257
+ const displayName = session.name || currentSessionId;
2258
+
2259
+ // Create header with delete button
2260
+ sessionTitle.innerHTML = `
2261
+ <span style="flex: 1;">${escapeHtml(displayName)}</span>
2262
+ <button class="icon-btn icon-btn-danger" onclick="deleteAllSessionTasks('${session.id}')" title="Delete all tasks in this session">
2263
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2264
+ <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
2265
+ </svg>
2266
+ </button>
2267
+ `;
2268
+
2269
+ // Build meta text with project path, branch, and description
2270
+ const projectName = session.project ? session.project.split('/').pop() : null;
2271
+ const metaParts = [`${currentTasks.length} tasks`];
2272
+ if (projectName) {
2273
+ metaParts.push(projectName);
2274
+ }
2275
+ if (session.gitBranch) {
2276
+ metaParts.push(session.gitBranch);
2277
+ }
2278
+ if (session.description) {
2279
+ metaParts.push(session.description);
2280
+ }
2281
+ metaParts.push(formatDate(session.modifiedAt));
2282
+ sessionMeta.textContent = metaParts.join(' · ');
2283
+
2284
+ const completed = currentTasks.filter(t => t.status === 'completed').length;
2285
+ const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
2286
+
2287
+ progressPercent.textContent = `${percent}%`;
2288
+ progressBar.style.width = `${percent}%`;
2289
+ const hasInProgress = currentTasks.some(t => t.status === 'in_progress');
2290
+ progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
2291
+
2292
+ updateOwnerFilter();
2293
+ renderKanban();
2294
+ renderSessions();
2295
+ }
2296
+
2297
+ function renderTaskCard(task) {
2298
+ const isBlocked = task.blockedBy && task.blockedBy.length > 0;
2299
+ const taskId = viewMode === 'all' ? `${task.sessionId?.slice(0,4)}-${task.id}` : task.id;
2300
+ const sessionLabel = viewMode === 'all' && task.sessionName ? task.sessionName : null;
2301
+ const statusClass = task.status.replace('_', '-');
2302
+ const actualSessionId = task.sessionId || currentSessionId;
2303
+
2304
+ return `
2305
+ <div
2306
+ role="listitem"
2307
+ tabindex="0"
2308
+ onclick="showTaskDetail('${task.id}', '${actualSessionId}')"
2309
+ onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();showTaskDetail('${task.id}','${actualSessionId}')}"
2310
+ class="task-card ${statusClass} ${isBlocked ? 'blocked' : ''}"
2311
+ aria-label="${escapeHtml(task.subject)} — ${task.status.replace('_',' ')}">
2312
+ <div class="task-id">
2313
+ <span>#${taskId}</span>
2314
+ ${isBlocked ? '<span class="task-badge blocked">Blocked</span>' : ''}
2315
+ ${task.owner ? (() => { const c = getOwnerColor(task.owner); return `<span class="task-owner-badge" style="background:${c.bg};color:${c.color}">${escapeHtml(task.owner)}</span>`; })() : ''}
2316
+ </div>
2317
+ <div class="task-title">${escapeHtml(task.subject)}</div>
2318
+ ${sessionLabel ? `<div class="task-session">${escapeHtml(sessionLabel)}</div>` : ''}
2319
+ ${task.status === 'in_progress' && task.activeForm ? `<div class="task-active">${escapeHtml(task.activeForm)}</div>` : ''}
2320
+ ${isBlocked ? `<div class="task-blocked">Waiting on ${task.blockedBy.map(id => '#' + id).join(', ')}</div>` : ''}
2321
+ ${task.description ? `<div class="task-desc">${escapeHtml(task.description.split('\n')[0])}</div>` : ''}
2322
+ </div>
2323
+ `;
2324
+ }
2325
+
2326
+ function renderKanban() {
2327
+ let filtered = currentTasks;
2328
+ if (ownerFilter) {
2329
+ filtered = filtered.filter(t => t.owner === ownerFilter);
2330
+ }
2331
+ const pending = filtered.filter(t => t.status === 'pending');
2332
+ const inProgress = filtered.filter(t => t.status === 'in_progress');
2333
+ const completed = filtered.filter(t => t.status === 'completed');
2334
+
2335
+ pendingCount.textContent = pending.length;
2336
+ inProgressCount.textContent = inProgress.length;
2337
+ completedCount.textContent = completed.length;
2338
+
2339
+ const emptyIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>`;
2340
+
2341
+ pendingTasks.innerHTML = pending.length > 0
2342
+ ? pending.map(renderTaskCard).join('')
2343
+ : `<div class="column-empty">${emptyIcon}<div>No pending tasks</div></div>`;
2344
+
2345
+ inProgressTasks.innerHTML = inProgress.length > 0
2346
+ ? inProgress.map(renderTaskCard).join('')
2347
+ : `<div class="column-empty">${emptyIcon}<div>No active tasks</div></div>`;
2348
+
2349
+ completedTasks.innerHTML = completed.length > 0
2350
+ ? completed.map(renderTaskCard).join('')
2351
+ : `<div class="column-empty">${emptyIcon}<div>No completed tasks</div></div>`;
2352
+ }
2353
+
2354
+ function getAvailableTasksOptions(currentTaskId = null) {
2355
+ const pending = currentTasks.filter(t => t.status === 'pending' && t.id !== currentTaskId);
2356
+ const inProgress = currentTasks.filter(t => t.status === 'in_progress' && t.id !== currentTaskId);
2357
+ const completed = currentTasks.filter(t => t.status === 'completed' && t.id !== currentTaskId);
2358
+
2359
+ // Build options grouped by status
2360
+ let options = '';
2361
+
2362
+ if (pending.length > 0) {
2363
+ options += '<optgroup label="Pending">';
2364
+ pending.forEach((t, idx) => {
2365
+ options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
2366
+ });
2367
+ options += '</optgroup>';
2368
+ }
2369
+
2370
+ if (inProgress.length > 0) {
2371
+ options += '<optgroup label="In Progress">';
2372
+ inProgress.forEach((t, idx) => {
2373
+ options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
2374
+ });
2375
+ options += '</optgroup>';
2376
+ }
2377
+
2378
+ if (completed.length > 0) {
2379
+ options += '<optgroup label="Completed">';
2380
+ completed.forEach((t, idx) => {
2381
+ options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
2382
+ });
2383
+ options += '</optgroup>';
2384
+ }
2385
+
2386
+ return options;
2387
+ }
2388
+
2389
+ async function showTaskDetail(taskId, sessionId = null) {
2390
+ let task = currentTasks.find(t => t.id === taskId && (!sessionId || t.sessionId === sessionId));
2391
+
2392
+ // If task not found in currentTasks, fetch it from the session
2393
+ if (!task && sessionId && sessionId !== 'undefined') {
2394
+ try {
2395
+ const res = await fetch(`/api/sessions/${sessionId}`);
2396
+ const tasks = await res.json();
2397
+ task = tasks.find(t => t.id === taskId);
2398
+ if (!task) return;
2399
+ } catch (error) {
2400
+ console.error('Failed to fetch task:', error);
2401
+ return;
2402
+ }
2403
+ }
2404
+
2405
+ if (!task) return;
2406
+
2407
+ detailPanel.classList.add('visible');
2408
+
2409
+ const statusLabels = {
2410
+ completed: '<span class="detail-status completed"><span class="dot"></span>Completed</span>',
2411
+ in_progress: '<span class="detail-status in_progress"><span class="dot"></span>In Progress</span>',
2412
+ pending: '<span class="detail-status pending"><span class="dot"></span>Pending</span>'
2413
+ };
2414
+
2415
+ const isBlocked = task.blockedBy && task.blockedBy.length > 0;
2416
+ const actualSessionId = task.sessionId || sessionId || currentSessionId;
2417
+
2418
+ detailContent.innerHTML = `
2419
+ <div class="detail-section">
2420
+ <div style="display: flex; justify-content: space-between; align-items: start;">
2421
+ <div style="flex: 1;">
2422
+ <div class="detail-label">Task #${task.id}</div>
2423
+ <h2 class="detail-title">${escapeHtml(task.subject)}</h2>
2424
+ </div>
2425
+ <div style="display: flex; gap: 8px;">
2426
+ <button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="color: #ef4444; border-color: #ef4444;">
2427
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2428
+ <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
2429
+ </svg>
2430
+ </button>
2431
+ </div>
2432
+ </div>
2433
+ </div>
2434
+
2435
+ <div class="detail-section" style="display: flex; gap: 12px; align-items: center;">
2436
+ <div>${statusLabels[task.status] || ''}</div>
2437
+ ${task.owner ? `<div style="font-size: 13px; color: ${getOwnerColor(task.owner).color}; font-weight: 500;">${escapeHtml(task.owner)}</div>` : ''}
2438
+ ${isBlocked && task.status !== 'in_progress' ? '<div style="font-size: 10px; color: var(--warning);">Blocked</div>' : ''}
2439
+ </div>
2440
+
2441
+ <div class="detail-section">
2442
+ <div class="detail-label">Description</div>
2443
+ <div class="detail-desc">${task.description ? DOMPurify.sanitize(marked.parse(task.description)) : '<em style="color: var(--text-muted);">No description</em>'}</div>
2444
+ </div>
2445
+
2446
+ ${task.activeForm && task.status === 'in_progress' ? `
2447
+ <div class="detail-section">
2448
+ <div class="detail-box active">
2449
+ <strong>Currently:</strong> ${escapeHtml(task.activeForm)}
2450
+ </div>
2451
+ </div>
2452
+ ` : ''}
2453
+
2454
+ <div class="detail-section">
2455
+ <div class="detail-label">Blocked By</div>
2456
+ <div class="detail-desc">
2457
+ ${task.blockedBy && task.blockedBy.length > 0
2458
+ ? `<div class="detail-box blocked"><strong>Blocked by:</strong> ${task.blockedBy.map(id => '#' + id).join(', ')}</div>`
2459
+ : '<em style="color: var(--text-muted); font-size: 13px;">No dependencies</em>'}
2460
+ </div>
2461
+ </div>
2462
+
2463
+ <div class="detail-section">
2464
+ <div class="detail-label">Blocks</div>
2465
+ <div class="detail-desc">
2466
+ ${task.blocks && task.blocks.length > 0
2467
+ ? `<div class="detail-box blocks"><strong>Blocks:</strong> ${task.blocks.map(id => '#' + id).join(', ')}</div>`
2468
+ : '<em style="color: var(--text-muted); font-size: 13px;">No tasks blocked</em>'}
2469
+ </div>
2470
+ </div>
2471
+
2472
+ <div class="detail-section note-section">
2473
+ <label for="note-input" class="detail-label">Add Note</label>
2474
+ <form class="note-form" onsubmit="addNote(event, '${task.id}', '${actualSessionId}')">
2475
+ <textarea id="note-input" class="note-input" placeholder="Add a note for Claude..." rows="3"></textarea>
2476
+ <button type="submit" class="note-submit">Add Note</button>
2477
+ </form>
2478
+ </div>
2479
+ `;
2480
+
2481
+ // Setup button handlers
2482
+ document.getElementById('delete-task-btn').onclick = () => deleteTask(task.id, actualSessionId);
2483
+ }
2484
+
2485
+ async function addNote(event, taskId, sessionId) {
2486
+ event.preventDefault();
2487
+ const input = document.getElementById('note-input');
2488
+ const note = input.value.trim();
2489
+ if (!note) return;
2490
+
2491
+ try {
2492
+ const res = await fetch(`/api/tasks/${sessionId}/${taskId}/note`, {
2493
+ method: 'POST',
2494
+ headers: { 'Content-Type': 'application/json' },
2495
+ body: JSON.stringify({ note })
2496
+ });
2497
+
2498
+ if (res.ok) {
2499
+ input.value = '';
2500
+ // Refresh to show updated description
2501
+ if (viewMode === 'all') {
2502
+ const tasksRes = await fetch('/api/tasks/all');
2503
+ currentTasks = await tasksRes.json();
2504
+ } else {
2505
+ await fetchTasks(sessionId);
2506
+ }
2507
+ showTaskDetail(taskId, sessionId);
2508
+ }
2509
+ } catch (error) {
2510
+ console.error('Failed to add note:', error);
2511
+ }
2512
+ }
2513
+
2514
+ function closeDetailPanel() {
2515
+ detailPanel.classList.remove('visible');
2516
+ }
2517
+
2518
+ let deleteTaskId = null;
2519
+ let deleteSessionId = null;
2520
+
2521
+ function showBlockedTaskModal(task) {
2522
+ const messageDiv = document.getElementById('blocked-task-message');
2523
+
2524
+ const blockedByList = task.blockedBy.map(id => {
2525
+ const blockingTask = currentTasks.find(t => t.id === id);
2526
+ if (blockingTask) {
2527
+ return `<li><strong>#${blockingTask.id}</strong> - ${escapeHtml(blockingTask.subject)}</li>`;
2528
+ }
2529
+ return `<li><strong>#${id}</strong></li>`;
2530
+ }).join('');
2531
+
2532
+ messageDiv.innerHTML = `
2533
+ <p style="margin-bottom: 12px;">Task <strong>#${task.id}</strong> - ${escapeHtml(task.subject)} is currently blocked by:</p>
2534
+ <ul style="margin: 0 0 16px 20px; padding: 0;">${blockedByList}</ul>
2535
+ <p style="margin: 0; color: var(--text-secondary); font-size: 13px;">
2536
+ Please resolve these dependencies before moving this task to <strong>In Progress</strong>.
2537
+ </p>
2538
+ `;
2539
+
2540
+ const modal = document.getElementById('blocked-task-modal');
2541
+ modal.classList.add('visible');
2542
+
2543
+ // Handle ESC key
2544
+ const keyHandler = (e) => {
2545
+ if (e.key === 'Escape') {
2546
+ e.preventDefault();
2547
+ closeBlockedTaskModal();
2548
+ document.removeEventListener('keydown', keyHandler);
2549
+ }
2550
+ };
2551
+ document.addEventListener('keydown', keyHandler);
2552
+ }
2553
+
2554
+ function closeBlockedTaskModal() {
2555
+ const modal = document.getElementById('blocked-task-modal');
2556
+ modal.classList.remove('visible');
2557
+ }
2558
+
2559
+ function deleteTask(taskId, sessionId) {
2560
+ const task = currentTasks.find(t => t.id === taskId);
2561
+ if (!task) return;
2562
+
2563
+ deleteTaskId = taskId;
2564
+ deleteSessionId = sessionId;
2565
+
2566
+ const message = document.getElementById('delete-confirm-message');
2567
+ message.textContent = `Delete task "${task.subject}"? This cannot be undone.`;
2568
+
2569
+ const modal = document.getElementById('delete-confirm-modal');
2570
+ modal.classList.add('visible');
2571
+
2572
+ // Handle ESC key
2573
+ const keyHandler = (e) => {
2574
+ if (e.key === 'Escape') {
2575
+ e.preventDefault();
2576
+ closeDeleteConfirmModal();
2577
+ document.removeEventListener('keydown', keyHandler);
2578
+ }
2579
+ };
2580
+ document.addEventListener('keydown', keyHandler);
2581
+ }
2582
+
2583
+ function closeDeleteConfirmModal() {
2584
+ const modal = document.getElementById('delete-confirm-modal');
2585
+ modal.classList.remove('visible');
2586
+ deleteTaskId = null;
2587
+ deleteSessionId = null;
2588
+ }
2589
+
2590
+ async function confirmDelete() {
2591
+ if (!deleteTaskId || !deleteSessionId) return;
2592
+
2593
+ const taskId = deleteTaskId;
2594
+ const sessionId = deleteSessionId;
2595
+
2596
+ closeDeleteConfirmModal();
2597
+
2598
+ try {
2599
+ const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
2600
+ method: 'DELETE'
2601
+ });
2602
+
2603
+ if (res.ok) {
2604
+ closeDetailPanel();
2605
+ await refreshCurrentView();
2606
+ } else {
2607
+ const error = await res.json();
2608
+ alert('Failed to delete task: ' + (error.error || 'Unknown error'));
2609
+ }
2610
+ } catch (error) {
2611
+ console.error('Failed to delete task:', error);
2612
+ alert('Failed to delete task');
2613
+ }
2614
+ }
2615
+
2616
+ function showHelpModal() {
2617
+ const modal = document.getElementById('help-modal');
2618
+ modal.classList.add('visible');
2619
+
2620
+ // Handle keyboard shortcuts
2621
+ const keyHandler = (e) => {
2622
+ if (e.key === 'Escape' || e.key === '?') {
2623
+ e.preventDefault();
2624
+ closeHelpModal();
2625
+ document.removeEventListener('keydown', keyHandler);
2626
+ }
2627
+ };
2628
+ document.addEventListener('keydown', keyHandler);
2629
+ }
2630
+
2631
+ function closeHelpModal() {
2632
+ const modal = document.getElementById('help-modal');
2633
+ modal.classList.remove('visible');
2634
+ }
2635
+
2636
+ async function refreshCurrentView() {
2637
+ fetchLiveUpdates();
2638
+ if (viewMode === 'all') {
2639
+ await showAllTasks();
2640
+ } else if (currentSessionId) {
2641
+ await fetchTasks(currentSessionId);
2642
+ } else {
2643
+ await fetchSessions();
2644
+ }
2645
+ }
2646
+
2647
+ document.getElementById('close-detail').onclick = closeDetailPanel;
2648
+
2649
+ document.addEventListener('keydown', (e) => {
2650
+ // Ignore if typing in input/textarea
2651
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
2652
+ return;
2653
+ }
2654
+
2655
+ if (e.key === 'Escape' && detailPanel.classList.contains('visible')) {
2656
+ closeDetailPanel();
2657
+ }
2658
+
2659
+ if (detailPanel.classList.contains('visible')) {
2660
+ // Get task ID from detail panel
2661
+ const labelElement = document.querySelector('.detail-label');
2662
+ if (!labelElement) return;
2663
+
2664
+ const taskId = labelElement.textContent.match(/\d+/)?.[0];
2665
+ if (!taskId) return;
2666
+
2667
+ const task = currentTasks.find(t => t.id === taskId);
2668
+ if (!task) return;
2669
+
2670
+ const sessionId = task.sessionId || currentSessionId;
2671
+
2672
+ if (e.key === 'd' || e.key === 'D') {
2673
+ e.preventDefault();
2674
+ deleteTask(taskId, sessionId);
2675
+ }
2676
+ }
2677
+
2678
+ if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
2679
+ e.preventDefault();
2680
+ showHelpModal();
2681
+ }
2682
+ });
2683
+
2684
+ function setupEventSource() {
2685
+ let retryDelay = 1000;
2686
+ let eventSource;
2687
+
2688
+ function connect() {
2689
+ eventSource = new EventSource('/api/events');
2690
+
2691
+ eventSource.onopen = () => {
2692
+ retryDelay = 1000; // Reset on successful connection
2693
+ connectionStatus.innerHTML = `
2694
+ <span class="connection-dot live"></span>
2695
+ <span>Connected</span>
2696
+ `;
2697
+ };
2698
+
2699
+ eventSource.onerror = () => {
2700
+ eventSource.close();
2701
+ connectionStatus.innerHTML = `
2702
+ <span class="connection-dot error"></span>
2703
+ <span>Reconnecting...</span>
2704
+ `;
2705
+ setTimeout(connect, retryDelay);
2706
+ retryDelay = Math.min(retryDelay * 2, 30000); // Max 30s
2707
+ };
2708
+
2709
+ let refreshTimer = null;
2710
+ function debouncedRefresh(sessionId, isMetadata) {
2711
+ clearTimeout(refreshTimer);
2712
+ refreshTimer = setTimeout(() => {
2713
+ fetchSessions().catch(err => console.error('[SSE] fetchSessions failed:', err));
2714
+ if (currentSessionId && (isMetadata || sessionId === currentSessionId)) {
2715
+ fetchTasks(currentSessionId);
2716
+ }
2717
+ }, 500);
2718
+ }
2719
+
2720
+ eventSource.onmessage = (event) => {
2721
+ const data = JSON.parse(event.data);
2722
+ console.log('[SSE] Event received:', data);
2723
+ if (data.type === 'update' || data.type === 'metadata-update') {
2724
+ debouncedRefresh(data.sessionId, data.type === 'metadata-update');
2725
+ }
2726
+
2727
+ if (data.type === 'team-update') {
2728
+ console.log('[SSE] Team update:', data.teamName);
2729
+ debouncedRefresh(data.teamName, false);
2730
+ if (currentSessionId && data.teamName === currentSessionId) {
2731
+ fetchTasks(currentSessionId);
2732
+ }
2733
+ }
2734
+ };
2735
+ }
2736
+
2737
+ connect();
2738
+ }
2739
+
2740
+ function formatDate(dateStr) {
2741
+ const date = new Date(dateStr);
2742
+ const now = new Date();
2743
+ const diff = now - date;
2744
+
2745
+ if (diff < 60000) return 'just now';
2746
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
2747
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
2748
+ return date.toLocaleDateString();
2749
+ }
2750
+
2751
+ function escapeHtml(text) {
2752
+ const div = document.createElement('div');
2753
+ div.textContent = text;
2754
+ return div.innerHTML;
2755
+ }
2756
+
2757
+ const ownerColors = [
2758
+ { bg: 'rgba(37, 99, 235, 0.14)', color: '#1d5bbf' }, // blue
2759
+ { bg: 'rgba(168, 85, 247, 0.14)', color: '#7c3aed' }, // purple
2760
+ { bg: 'rgba(14, 165, 133, 0.14)', color: '#0d7d65' }, // teal
2761
+ { bg: 'rgba(220, 80, 30, 0.14)', color: '#c04a1a' }, // red-orange
2762
+ { bg: 'rgba(202, 138, 4, 0.14)', color: '#92700c' }, // amber
2763
+ { bg: 'rgba(219, 39, 119, 0.14)', color: '#b5246a' }, // pink
2764
+ { bg: 'rgba(22, 163, 74, 0.14)', color: '#15803d' }, // green
2765
+ { bg: 'rgba(99, 102, 241, 0.14)', color: '#4f46e5' }, // indigo
2766
+ ];
2767
+ const ownerColorCache = {};
2768
+ function getOwnerColor(name) {
2769
+ if (ownerColorCache[name]) return ownerColorCache[name];
2770
+ let hash = 5381;
2771
+ for (let i = 0; i < name.length; i++) {
2772
+ hash = ((hash * 33) ^ name.charCodeAt(i)) | 0;
2773
+ }
2774
+ const c = ownerColors[Math.abs(hash) % ownerColors.length];
2775
+ ownerColorCache[name] = c;
2776
+ return c;
2777
+ }
2778
+
2779
+ function filterBySessions(value) {
2780
+ sessionFilter = value;
2781
+ localStorage.setItem('sessionFilter', sessionFilter);
2782
+ renderSessions();
2783
+ }
2784
+
2785
+ function changeSessionLimit(value) {
2786
+ sessionLimit = value;
2787
+ localStorage.setItem('sessionLimit', sessionLimit);
2788
+ fetchSessions();
2789
+ }
2790
+
2791
+ function filterByProject(project) {
2792
+ filterProject = project || null;
2793
+ renderSessions();
2794
+ fetchLiveUpdates();
2795
+ showAllTasks();
2796
+ }
2797
+
2798
+ function updateProjectDropdown() {
2799
+ const dropdown = document.getElementById('project-filter');
2800
+ const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
2801
+
2802
+ dropdown.innerHTML = '<option value="">All Projects</option>' +
2803
+ projects.map(p => {
2804
+ const name = p.split('/').pop();
2805
+ const selected = p === filterProject ? ' selected' : '';
2806
+ return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
2807
+ }).join('');
2808
+ }
2809
+
2810
+ function toggleTheme() {
2811
+ const isCurrentlyLight = document.body.classList.contains('light');
2812
+ if (isCurrentlyLight) {
2813
+ document.body.classList.remove('light');
2814
+ document.body.classList.add('dark-forced');
2815
+ localStorage.setItem('theme', 'dark');
2816
+ } else {
2817
+ document.body.classList.add('light');
2818
+ document.body.classList.remove('dark-forced');
2819
+ localStorage.setItem('theme', 'light');
2820
+ }
2821
+ updateThemeIcon();
2822
+ }
2823
+
2824
+ function updateThemeIcon() {
2825
+ const saved = localStorage.getItem('theme');
2826
+ const isLight = document.body.classList.contains('light') ||
2827
+ (!saved && window.matchMedia('(prefers-color-scheme: light)').matches);
2828
+ document.getElementById('theme-icon-dark').style.display = isLight ? 'none' : 'block';
2829
+ document.getElementById('theme-icon-light').style.display = isLight ? 'block' : 'none';
2830
+ }
2831
+
2832
+ function loadTheme() {
2833
+ const saved = localStorage.getItem('theme');
2834
+ if (saved === 'light') {
2835
+ document.body.classList.add('light');
2836
+ document.body.classList.remove('dark-forced');
2837
+ } else if (saved === 'dark') {
2838
+ document.body.classList.remove('light');
2839
+ document.body.classList.add('dark-forced');
2840
+ }
2841
+ // If no saved preference, system prefers-color-scheme CSS handles it
2842
+ updateThemeIcon();
2843
+ }
2844
+
2845
+ function loadPreferences() {
2846
+ document.getElementById('session-filter').value = sessionFilter;
2847
+ document.getElementById('session-limit').value = sessionLimit;
2848
+ }
2849
+
2850
+ async function showTeamModalForSession(sessionId) {
2851
+ const session = sessions.find(s => s.id === sessionId);
2852
+ if (!session || !session.isTeam) return;
2853
+ try {
2854
+ const res = await fetch(`/api/teams/${sessionId}`);
2855
+ if (!res.ok) return;
2856
+ const teamConfig = await res.json();
2857
+ showTeamModal(teamConfig, currentSessionId === sessionId ? currentTasks : []);
2858
+ } catch (e) {
2859
+ console.error('Failed to fetch team config:', e);
2860
+ }
2861
+ }
2862
+
2863
+ function showTeamModal(teamConfig, tasks) {
2864
+ const modal = document.getElementById('team-modal');
2865
+ const titleEl = document.getElementById('team-modal-title');
2866
+ const bodyEl = document.getElementById('team-modal-body');
2867
+
2868
+ titleEl.textContent = `Team: ${teamConfig.team_name || teamConfig.name || 'Unknown'}`;
2869
+
2870
+ const ownerCounts = {};
2871
+ tasks.forEach(t => {
2872
+ if (t.owner) {
2873
+ ownerCounts[t.owner] = (ownerCounts[t.owner] || 0) + 1;
2874
+ }
2875
+ });
2876
+
2877
+ const members = teamConfig.members || [];
2878
+ const description = teamConfig.description || '';
2879
+ const lead = members.find(m => m.agentType === 'team-lead' || m.name === 'team-lead');
2880
+
2881
+ let html = '';
2882
+ if (description) {
2883
+ html += `<div class="team-modal-desc">"${escapeHtml(description)}"</div>`;
2884
+ }
2885
+
2886
+ html += `<div style="font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 10px;">Members (${members.length})</div>`;
2887
+
2888
+ members.forEach(member => {
2889
+ const taskCount = ownerCounts[member.name] || 0;
2890
+ html += `
2891
+ <div class="team-member-card">
2892
+ <div class="member-name">🟢 ${escapeHtml(member.name)}</div>
2893
+ <div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
2894
+ ${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
2895
+ <div class="member-tasks">Tasks: ${taskCount} assigned</div>
2896
+ </div>
2897
+ `;
2898
+ });
2899
+
2900
+ const metaParts = [];
2901
+ if (teamConfig.created_at) {
2902
+ metaParts.push(`Created: ${new Date(teamConfig.created_at).toLocaleString()}`);
2903
+ }
2904
+ if (lead) {
2905
+ metaParts.push(`Lead: ${lead.name}`);
2906
+ }
2907
+ if (teamConfig.working_dir) {
2908
+ metaParts.push(`Working dir: ${teamConfig.working_dir}`);
2909
+ }
2910
+ if (metaParts.length > 0) {
2911
+ html += `<div class="team-modal-meta">${metaParts.map(p => escapeHtml(p)).join('<br>')}</div>`;
2912
+ }
2913
+
2914
+ bodyEl.innerHTML = html;
2915
+ modal.classList.add('visible');
2916
+
2917
+ const keyHandler = (e) => {
2918
+ if (e.key === 'Escape') {
2919
+ e.preventDefault();
2920
+ closeTeamModal();
2921
+ document.removeEventListener('keydown', keyHandler);
2922
+ }
2923
+ };
2924
+ document.addEventListener('keydown', keyHandler);
2925
+ }
2926
+
2927
+ function closeTeamModal() {
2928
+ document.getElementById('team-modal').classList.remove('visible');
2929
+ }
2930
+
2931
+ function updateOwnerFilter() {
2932
+ const bar = document.getElementById('owner-filter-bar');
2933
+ const select = document.getElementById('owner-filter');
2934
+
2935
+ const session = sessions.find(s => s.id === currentSessionId);
2936
+ if (!session || !session.isTeam) {
2937
+ bar.classList.remove('visible');
2938
+ return;
2939
+ }
2940
+
2941
+ bar.classList.add('visible');
2942
+ const owners = [...new Set(currentTasks.map(t => t.owner).filter(Boolean))].sort();
2943
+ select.innerHTML = '<option value="">All Members</option>' +
2944
+ owners.map(o => {
2945
+ const c = getOwnerColor(o);
2946
+ return `<option value="${escapeHtml(o)}" style="color:${c.color};background:${c.bg}"${o === ownerFilter ? ' selected' : ''}>${escapeHtml(o)}</option>`;
2947
+ }).join('');
2948
+ const current = ownerFilter ? getOwnerColor(ownerFilter) : null;
2949
+ select.style.color = current ? current.color : '';
2950
+ select.style.backgroundColor = current ? current.bg : '';
2951
+ }
2952
+
2953
+ function filterByOwner(value) {
2954
+ ownerFilter = value;
2955
+ const select = document.getElementById('owner-filter');
2956
+ const c = value ? getOwnerColor(value) : null;
2957
+ select.style.color = c ? c.color : '';
2958
+ select.style.backgroundColor = c ? c.bg : '';
2959
+ renderKanban();
2960
+ }
2961
+
2962
+ // Init
2963
+ loadTheme();
2964
+ loadPreferences();
2965
+ setupEventSource();
2966
+
2967
+ // Fetch sessions and show newest one by default
2968
+ fetchSessions().then(() => {
2969
+ if (sessions.length > 0) {
2970
+ // Sessions are already sorted by newest first from API
2971
+ fetchTasks(sessions[0].id);
2972
+ } else {
2973
+ showAllTasks();
2974
+ }
2975
+ });
2976
+ </script>
2977
+
2978
+ <!-- Help Modal -->
2979
+ <div id="help-modal" class="modal-overlay" onclick="closeHelpModal()">
2980
+ <div class="modal" onclick="event.stopPropagation()">
2981
+ <div class="modal-header">
2982
+ <h3 class="modal-title">Keyboard Shortcuts</h3>
2983
+ <button class="modal-close" aria-label="Close dialog" onclick="closeHelpModal()">
2984
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2985
+ <path d="M18 6L6 18M6 6l12 12"/>
2986
+ </svg>
2987
+ </button>
2988
+ </div>
2989
+ <div class="modal-body">
2990
+ <div style="display: grid; gap: 16px;">
2991
+ <div>
2992
+ <h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Global</h4>
2993
+ <table style="width: 100%; font-size: 13px;">
2994
+ <tr>
2995
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">?</kbd></td>
2996
+ <td style="padding: 4px 0; color: var(--text-primary);">Show keyboard shortcuts</td>
2997
+ </tr>
2998
+ <tr>
2999
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Esc</kbd></td>
3000
+ <td style="padding: 4px 0; color: var(--text-primary);">Close panels or cancel</td>
3001
+ </tr>
3002
+ </table>
3003
+ </div>
3004
+ <div>
3005
+ <h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Task Actions</h4>
3006
+ <table style="width: 100%; font-size: 13px;">
3007
+ <tr>
3008
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">D</kbd></td>
3009
+ <td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
3010
+ </tr>
3011
+ </table>
3012
+ </div>
3013
+ </div>
3014
+ </div>
3015
+ <div class="modal-footer">
3016
+ <button class="btn btn-primary" onclick="closeHelpModal()">Got it</button>
3017
+ </div>
3018
+ </div>
3019
+ </div>
3020
+
3021
+ <!-- Delete Confirmation Modal -->
3022
+ <div id="delete-confirm-modal" class="modal-overlay" onclick="closeDeleteConfirmModal()">
3023
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 400px;">
3024
+ <div class="modal-header">
3025
+ <h3 class="modal-title">Delete Task</h3>
3026
+ <button class="modal-close" aria-label="Close dialog" onclick="closeDeleteConfirmModal()">
3027
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3028
+ <path d="M18 6L6 18M6 6l12 12"/>
3029
+ </svg>
3030
+ </button>
3031
+ </div>
3032
+ <div class="modal-body">
3033
+ <p id="delete-confirm-message" style="margin: 0; color: var(--text-primary);"></p>
3034
+ </div>
3035
+ <div class="modal-footer">
3036
+ <button class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
3037
+ <button class="btn btn-primary" onclick="confirmDelete()" style="background: #ef4444; border-color: #ef4444;">Delete</button>
3038
+ </div>
3039
+ </div>
3040
+ </div>
3041
+
3042
+ <!-- Delete All Session Tasks Confirmation Modal -->
3043
+ <div id="delete-session-tasks-modal" class="modal-overlay" onclick="closeDeleteSessionTasksModal()">
3044
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 500px;">
3045
+ <div class="modal-header">
3046
+ <h3 class="modal-title">Delete All Tasks</h3>
3047
+ <button class="modal-close" aria-label="Close dialog" onclick="closeDeleteSessionTasksModal()">
3048
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3049
+ <path d="M18 6L6 18M6 6l12 12"/>
3050
+ </svg>
3051
+ </button>
3052
+ </div>
3053
+ <div class="modal-body">
3054
+ <p id="delete-session-tasks-message" style="margin: 0 0 12px 0; color: var(--text-primary);"></p>
3055
+ <p style="margin: 0; font-size: 13px; color: var(--text-secondary);">This action cannot be undone.</p>
3056
+ </div>
3057
+ <div class="modal-footer">
3058
+ <button class="btn btn-secondary" onclick="closeDeleteSessionTasksModal()">Cancel</button>
3059
+ <button class="btn btn-primary" onclick="confirmDeleteSessionTasks()" style="background: #ef4444; border-color: #ef4444;">Delete All</button>
3060
+ </div>
3061
+ </div>
3062
+ </div>
3063
+
3064
+ <!-- Delete Result Modal -->
3065
+ <div id="delete-result-modal" class="modal-overlay" onclick="closeDeleteResultModal()">
3066
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 500px;">
3067
+ <div class="modal-header">
3068
+ <h3 class="modal-title">Deletion Result</h3>
3069
+ <button class="modal-close" aria-label="Close dialog" onclick="closeDeleteResultModal()">
3070
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3071
+ <path d="M18 6L6 18M6 6l12 12"/>
3072
+ </svg>
3073
+ </button>
3074
+ </div>
3075
+ <div class="modal-body">
3076
+ <p id="delete-result-message" style="margin: 0; color: var(--text-primary);"></p>
3077
+ <div id="delete-result-details" style="margin-top: 12px; font-size: 13px; color: var(--text-secondary);"></div>
3078
+ </div>
3079
+ <div class="modal-footer">
3080
+ <button class="btn btn-primary" onclick="closeDeleteResultModal()">Close</button>
3081
+ </div>
3082
+ </div>
3083
+ </div>
3084
+
3085
+ <!-- Team Info Modal -->
3086
+ <div id="team-modal" class="modal-overlay" onclick="closeTeamModal()">
3087
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 520px; max-height: 80vh; display: flex; flex-direction: column;">
3088
+ <div class="modal-header">
3089
+ <h3 id="team-modal-title" class="modal-title">Team</h3>
3090
+ <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
3091
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3092
+ <path d="M18 6L6 18M6 6l12 12"/>
3093
+ </svg>
3094
+ </button>
3095
+ </div>
3096
+ <div id="team-modal-body" class="modal-body" style="overflow-y: auto; flex: 1;"></div>
3097
+ <div class="modal-footer">
3098
+ <button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
3099
+ </div>
3100
+ </div>
3101
+ </div>
3102
+
3103
+ <!-- Blocked Task Warning Modal -->
3104
+ <div id="blocked-task-modal" class="modal-overlay" onclick="closeBlockedTaskModal()">
3105
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 450px;">
3106
+ <div class="modal-header">
3107
+ <h3 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
3108
+ <svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="width: 20px; height: 20px;">
3109
+ <circle cx="12" cy="12" r="10"/>
3110
+ <line x1="15" y1="9" x2="9" y2="15"/>
3111
+ <line x1="9" y1="9" x2="15" y2="15"/>
3112
+ </svg>
3113
+ Cannot Start Blocked Task
3114
+ </h3>
3115
+ <button class="modal-close" aria-label="Close dialog" onclick="closeBlockedTaskModal()">
3116
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3117
+ <path d="M18 6L6 18M6 6l12 12"/>
3118
+ </svg>
3119
+ </button>
3120
+ </div>
3121
+ <div class="modal-body">
3122
+ <div id="blocked-task-message" style="color: var(--text-primary); line-height: 1.6;"></div>
3123
+ </div>
3124
+ <div class="modal-footer">
3125
+ <button class="btn btn-primary" onclick="closeBlockedTaskModal()">OK</button>
3126
+ </div>
3127
+ </div>
3128
+ </div>
3129
+ </body>
3130
+ </html>