claude-task-viewer 1.2.0 → 1.4.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.
package/public/index.html CHANGED
@@ -3,222 +3,1082 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Claude Task Viewer</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
6
+ <title>Claude Tasks</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">
8
10
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <script>
10
- tailwind.config = {
11
- darkMode: 'class',
12
- theme: {
13
- extend: {
14
- colors: {
15
- claude: {
16
- orange: '#E86F33',
17
- cream: '#F5F0E8'
18
- }
19
- }
20
- }
21
- }
22
- }
23
- </script>
24
- <style id="theme-styles">
25
- /* Light mode overrides */
26
- body.light { background: #f8fafc !important; color: #1e293b !important; }
27
- .light .bg-gray-900 { background: #ffffff !important; }
28
- .light .bg-gray-950 { background: #f8fafc !important; }
29
- .light .bg-gray-900\/50 { background: rgba(255,255,255,0.9) !important; }
30
- .light .bg-gray-800 { background: #f1f5f9 !important; }
31
- .light .bg-gray-800\/50 { background: rgba(241,245,249,0.7) !important; }
32
- .light .bg-gray-700 { background: #e2e8f0 !important; }
33
- .light .bg-gray-700\/50 { background: rgba(226,232,240,0.5) !important; }
34
- .light .border-gray-800 { border-color: #e2e8f0 !important; }
35
- .light .border-gray-700 { border-color: #cbd5e1 !important; }
36
- .light .text-gray-100 { color: #1e293b !important; }
37
- .light .text-gray-200 { color: #334155 !important; }
38
- .light .text-gray-300 { color: #475569 !important; }
39
- .light .text-gray-400 { color: #64748b !important; }
40
- .light .text-gray-500 { color: #64748b !important; }
41
- .light .text-gray-600 { color: #94a3b8 !important; }
42
- .light .hover\:text-gray-300:hover { color: #334155 !important; }
43
- .light .hover\:text-gray-400:hover { color: #475569 !important; }
44
- .light .hover\:bg-gray-800:hover { background: #f1f5f9 !important; }
45
- .light .hover\:bg-gray-800\/50:hover { background: rgba(241,245,249,0.5) !important; }
46
- .light .border-green-500\/30 { border-color: rgba(34,197,94,0.4) !important; }
47
- .light .bg-green-500\/10 { background: rgba(34,197,94,0.15) !important; }
48
- .light .bg-green-500\/20 { background: rgba(34,197,94,0.2) !important; }
49
- .light .border-claude-orange\/30 { border-color: rgba(232,111,51,0.4) !important; }
50
- .light .bg-claude-orange\/10 { background: rgba(232,111,51,0.12) !important; }
51
- .light .bg-claude-orange\/20 { background: rgba(232,111,51,0.2) !important; }
52
- .light .border-yellow-500\/20 { border-color: rgba(234,179,8,0.4) !important; }
53
- .light .bg-yellow-500\/10 { background: rgba(234,179,8,0.15) !important; }
54
- .light .bg-yellow-500\/20 { background: rgba(234,179,8,0.25) !important; }
55
- .light .border-blue-500\/20 { border-color: rgba(59,130,246,0.4) !important; }
56
- .light .bg-blue-500\/10 { background: rgba(59,130,246,0.15) !important; }
57
- .light .prose pre { background: #f1f5f9 !important; }
58
- .light .prose code { background: #e2e8f0 !important; }
59
- .light ::-webkit-scrollbar-thumb { background: #e2e8f0 !important; }
60
- .light ::-webkit-scrollbar-thumb:hover { background: #cbd5e1 !important; }
61
- body.light { scrollbar-color: #e2e8f0 transparent; }
62
- </style>
11
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
63
12
  <style>
64
- .prose pre { background: #1f2937; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
65
- .prose code { background: #374151; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
66
- .prose pre code { background: transparent; padding: 0; }
67
- .status-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
68
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
69
- .kanban-column { min-height: calc(100vh - 200px); }
70
-
71
- /* Custom scrollbar - dark mode default */
72
- ::-webkit-scrollbar { width: 8px; height: 8px; }
13
+ :root {
14
+ --bg-deep: #08090a;
15
+ --bg-surface: #0d0e10;
16
+ --bg-elevated: #131416;
17
+ --bg-hover: #1a1b1e;
18
+ --border: #1e2023;
19
+ --text-primary: #e8e8e8;
20
+ --text-secondary: #8b8d91;
21
+ --text-tertiary: #5a5c60;
22
+ --text-muted: #3d3f42;
23
+ --accent: #E86F33;
24
+ --accent-dim: rgba(232, 111, 51, 0.15);
25
+ --accent-glow: rgba(232, 111, 51, 0.4);
26
+ --success: #3ecf8e;
27
+ --success-dim: rgba(62, 207, 142, 0.12);
28
+ --warning: #f0b429;
29
+ --warning-dim: rgba(240, 180, 41, 0.12);
30
+ --mono: 'IBM Plex Mono', monospace;
31
+ --serif: 'Playfair Display', serif;
32
+ }
33
+
34
+ * { box-sizing: border-box; margin: 0; padding: 0; }
35
+
36
+ body {
37
+ font-family: var(--mono);
38
+ font-size: 13px;
39
+ background: var(--bg-deep);
40
+ color: var(--text-primary);
41
+ line-height: 1.5;
42
+ min-height: 100vh;
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+
46
+ /* Subtle scan-line texture */
47
+ body::before {
48
+ content: '';
49
+ position: fixed;
50
+ inset: 0;
51
+ background: repeating-linear-gradient(
52
+ 0deg,
53
+ transparent,
54
+ transparent 2px,
55
+ rgba(0,0,0,0.03) 2px,
56
+ rgba(0,0,0,0.03) 4px
57
+ );
58
+ pointer-events: none;
59
+ z-index: 9999;
60
+ }
61
+
62
+ /* Scrollbar */
63
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
73
64
  ::-webkit-scrollbar-track { background: transparent; }
74
- ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #374151); border-radius: 4px; }
75
- ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, #4b5563); }
76
- * { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb, #374151) transparent; }
77
- :root { --scrollbar-thumb: #374151; --scrollbar-thumb-hover: #4b5563; }
78
- body.light { --scrollbar-thumb: #d1d5db; --scrollbar-thumb-hover: #9ca3af; }
65
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
66
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
67
+
68
+ /* Layout */
69
+ .app { display: flex; height: 100vh; }
70
+
71
+ /* Sidebar */
72
+ .sidebar {
73
+ width: 280px;
74
+ background: var(--bg-surface);
75
+ border-right: 1px solid var(--border);
76
+ display: flex;
77
+ flex-direction: column;
78
+ flex-shrink: 0;
79
+ }
80
+
81
+ .sidebar-header {
82
+ padding: 20px;
83
+ border-bottom: 1px solid var(--border);
84
+ }
85
+
86
+ .logo {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 10px;
90
+ }
91
+
92
+ .logo-mark {
93
+ width: 24px;
94
+ height: 24px;
95
+ background: var(--accent);
96
+ border-radius: 4px;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ }
101
+
102
+ .logo-mark svg {
103
+ width: 14px;
104
+ height: 14px;
105
+ color: white;
106
+ }
107
+
108
+ .logo-text {
109
+ font-family: var(--serif);
110
+ font-size: 16px;
111
+ font-weight: 500;
112
+ letter-spacing: -0.02em;
113
+ }
114
+
115
+ .connection {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ margin-top: 12px;
120
+ font-size: 11px;
121
+ color: var(--text-tertiary);
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.05em;
124
+ }
125
+
126
+ .connection-dot {
127
+ width: 6px;
128
+ height: 6px;
129
+ border-radius: 50%;
130
+ background: var(--warning);
131
+ }
132
+
133
+ .connection-dot.live {
134
+ background: var(--success);
135
+ box-shadow: 0 0 8px var(--success);
136
+ }
137
+
138
+ .connection-dot.error {
139
+ background: #ef4444;
140
+ }
141
+
142
+ /* Sidebar sections */
143
+ .sidebar-section {
144
+ display: flex;
145
+ flex-direction: column;
146
+ border-bottom: 1px solid var(--border);
147
+ }
148
+
149
+ .sidebar-section.flex-1 {
150
+ flex: 1;
151
+ border-bottom: none;
152
+ overflow: hidden;
153
+ }
154
+
155
+ .section-header {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ padding: 12px 16px;
160
+ font-size: 10px;
161
+ font-weight: 600;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.1em;
164
+ color: var(--text-muted);
165
+ }
166
+
167
+ .filter-row {
168
+ display: flex;
169
+ gap: 8px;
170
+ padding: 0 12px 12px;
171
+ }
172
+
173
+ .filter-dropdown {
174
+ flex: 1;
175
+ appearance: none;
176
+ background: var(--bg-elevated);
177
+ border: 1px solid var(--border);
178
+ border-radius: 4px;
179
+ padding: 6px 24px 6px 10px;
180
+ font-family: var(--mono);
181
+ font-size: 11px;
182
+ color: var(--text-secondary);
183
+ cursor: pointer;
184
+ 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='%235a5c60' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
185
+ background-repeat: no-repeat;
186
+ background-position: right 8px center;
187
+ text-overflow: ellipsis;
188
+ min-width: 0;
189
+ }
190
+
191
+ .filter-dropdown option {
192
+ background: var(--bg-surface);
193
+ color: var(--text-primary);
194
+ }
195
+
196
+ .filter-dropdown:focus {
197
+ outline: none;
198
+ border-color: var(--accent);
199
+ }
200
+
201
+ /* Live Updates */
202
+ .live-updates {
203
+ padding: 0 12px 12px;
204
+ max-height: 180px;
205
+ overflow-y: auto;
206
+ }
207
+
208
+ .live-empty {
209
+ padding: 16px;
210
+ text-align: center;
211
+ font-size: 11px;
212
+ color: var(--text-muted);
213
+ }
214
+
215
+ .live-item {
216
+ display: flex;
217
+ align-items: flex-start;
218
+ gap: 10px;
219
+ padding: 10px 12px;
220
+ background: var(--bg-elevated);
221
+ border: 1px solid var(--border);
222
+ border-radius: 6px;
223
+ margin-bottom: 6px;
224
+ cursor: pointer;
225
+ transition: all 0.15s ease;
226
+ }
227
+
228
+ .live-item:hover {
229
+ border-color: var(--text-muted);
230
+ }
231
+
232
+ .live-item .pulse {
233
+ width: 8px;
234
+ height: 8px;
235
+ margin-top: 4px;
236
+ background: var(--accent);
237
+ border-radius: 50%;
238
+ flex-shrink: 0;
239
+ animation: pulse 2s ease-in-out infinite;
240
+ box-shadow: 0 0 12px var(--accent-glow);
241
+ }
242
+
243
+ .live-item-content {
244
+ flex: 1;
245
+ min-width: 0;
246
+ }
247
+
248
+ .live-item-action {
249
+ font-size: 12px;
250
+ color: var(--text-primary);
251
+ white-space: nowrap;
252
+ overflow: hidden;
253
+ text-overflow: ellipsis;
254
+ }
255
+
256
+ .live-item-session {
257
+ font-size: 10px;
258
+ color: var(--text-tertiary);
259
+ margin-top: 2px;
260
+ white-space: nowrap;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ }
264
+
265
+ /* Sessions */
266
+ .sessions-list {
267
+ flex: 1;
268
+ overflow-y: auto;
269
+ padding: 0 12px 12px;
270
+ }
271
+
272
+ .session-item {
273
+ display: block;
274
+ width: 100%;
275
+ padding: 12px;
276
+ margin-bottom: 4px;
277
+ background: transparent;
278
+ border: 1px solid transparent;
279
+ border-radius: 6px;
280
+ text-align: left;
281
+ cursor: pointer;
282
+ transition: all 0.15s ease;
283
+ }
284
+
285
+ .session-item:hover {
286
+ background: var(--bg-hover);
287
+ border-color: var(--border);
288
+ }
289
+
290
+ .session-item.active {
291
+ background: var(--bg-elevated);
292
+ border-color: var(--border);
293
+ }
294
+
295
+ .session-name {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: space-between;
299
+ gap: 8px;
300
+ }
301
+
302
+ .session-name span {
303
+ font-size: 13px;
304
+ color: var(--text-primary);
305
+ white-space: nowrap;
306
+ overflow: hidden;
307
+ text-overflow: ellipsis;
308
+ }
309
+
310
+ .session-name .pulse {
311
+ width: 8px;
312
+ height: 8px;
313
+ background: var(--accent);
314
+ border-radius: 50%;
315
+ animation: pulse 2s ease-in-out infinite;
316
+ box-shadow: 0 0 12px var(--accent-glow);
317
+ }
318
+
319
+ @keyframes pulse {
320
+ 0%, 100% { opacity: 1; transform: scale(1); }
321
+ 50% { opacity: 0.6; transform: scale(0.9); }
322
+ }
323
+
324
+ .session-secondary {
325
+ font-size: 11px;
326
+ color: var(--text-muted);
327
+ margin-top: 2px;
328
+ white-space: nowrap;
329
+ overflow: hidden;
330
+ text-overflow: ellipsis;
331
+ }
332
+
333
+ .session-progress {
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 8px;
337
+ margin-top: 8px;
338
+ }
339
+
340
+ .progress-bar {
341
+ flex: 1;
342
+ height: 2px;
343
+ background: var(--border);
344
+ border-radius: 1px;
345
+ overflow: hidden;
346
+ }
347
+
348
+ .progress-fill {
349
+ height: 100%;
350
+ background: var(--accent);
351
+ transition: width 0.3s ease;
352
+ }
353
+
354
+ .progress-text {
355
+ font-size: 10px;
356
+ color: var(--text-muted);
357
+ font-variant-numeric: tabular-nums;
358
+ }
359
+
360
+ .session-time {
361
+ font-size: 10px;
362
+ color: var(--text-muted);
363
+ margin-top: 6px;
364
+ }
365
+
366
+ /* Footer */
367
+ .sidebar-footer {
368
+ padding: 12px 20px;
369
+ border-top: 1px solid var(--border);
370
+ font-size: 10px;
371
+ color: var(--text-muted);
372
+ }
373
+
374
+ .sidebar-footer a {
375
+ color: var(--text-tertiary);
376
+ text-decoration: none;
377
+ transition: color 0.15s;
378
+ }
379
+
380
+ .sidebar-footer a:hover {
381
+ color: var(--text-secondary);
382
+ }
383
+
384
+ /* Main */
385
+ .main {
386
+ flex: 1;
387
+ display: flex;
388
+ flex-direction: column;
389
+ overflow: hidden;
390
+ background: var(--bg-deep);
391
+ }
392
+
393
+ /* Empty state */
394
+ .empty-state {
395
+ flex: 1;
396
+ display: flex;
397
+ align-items: center;
398
+ justify-content: center;
399
+ flex-direction: column;
400
+ gap: 16px;
401
+ color: var(--text-muted);
402
+ }
403
+
404
+ .empty-state svg {
405
+ width: 48px;
406
+ height: 48px;
407
+ opacity: 0.3;
408
+ }
409
+
410
+ .empty-state p {
411
+ font-size: 13px;
412
+ }
413
+
414
+ /* Session view */
415
+ .session-view {
416
+ flex: 1;
417
+ display: none;
418
+ flex-direction: column;
419
+ overflow: hidden;
420
+ }
421
+
422
+ .session-view.visible {
423
+ display: flex;
424
+ }
425
+
426
+ /* Header */
427
+ .view-header {
428
+ padding: 16px 24px;
429
+ border-bottom: 1px solid var(--border);
430
+ background: var(--bg-surface);
431
+ display: flex;
432
+ align-items: center;
433
+ justify-content: space-between;
434
+ }
435
+
436
+ .view-title {
437
+ font-family: var(--serif);
438
+ font-size: 20px;
439
+ font-weight: 400;
440
+ letter-spacing: -0.02em;
441
+ }
442
+
443
+ .view-meta {
444
+ font-size: 11px;
445
+ color: var(--text-tertiary);
446
+ margin-top: 4px;
447
+ }
448
+
449
+ .view-actions {
450
+ display: flex;
451
+ align-items: center;
452
+ gap: 16px;
453
+ }
454
+
455
+ .view-progress {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 10px;
459
+ }
460
+
461
+ .view-progress .progress-bar {
462
+ width: 120px;
463
+ height: 3px;
464
+ }
465
+
466
+ .view-progress .progress-text {
467
+ font-size: 13px;
468
+ font-weight: 500;
469
+ color: var(--accent);
470
+ }
471
+
472
+ .icon-btn {
473
+ width: 32px;
474
+ height: 32px;
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: center;
478
+ background: transparent;
479
+ border: 1px solid var(--border);
480
+ border-radius: 6px;
481
+ color: var(--text-tertiary);
482
+ cursor: pointer;
483
+ transition: all 0.15s ease;
484
+ }
485
+
486
+ .icon-btn:hover {
487
+ background: var(--bg-hover);
488
+ color: var(--text-primary);
489
+ border-color: var(--text-muted);
490
+ }
491
+
492
+ .icon-btn svg {
493
+ width: 16px;
494
+ height: 16px;
495
+ }
496
+
497
+ /* Kanban */
498
+ .kanban {
499
+ flex: 1;
500
+ display: flex;
501
+ gap: 24px;
502
+ padding: 24px;
503
+ overflow-x: auto;
504
+ }
505
+
506
+ .kanban-column {
507
+ width: 320px;
508
+ flex-shrink: 0;
509
+ display: flex;
510
+ flex-direction: column;
511
+ }
512
+
513
+ .column-header {
514
+ display: flex;
515
+ align-items: center;
516
+ gap: 10px;
517
+ padding-bottom: 16px;
518
+ border-bottom: 1px solid var(--border);
519
+ margin-bottom: 16px;
520
+ }
521
+
522
+ .column-dot {
523
+ width: 8px;
524
+ height: 8px;
525
+ border-radius: 50%;
526
+ }
527
+
528
+ .column-dot.pending { background: var(--text-muted); }
529
+ .column-dot.in-progress {
530
+ background: var(--accent);
531
+ box-shadow: 0 0 12px var(--accent-glow);
532
+ animation: pulse 2s ease-in-out infinite;
533
+ }
534
+ .column-dot.completed { background: var(--success); }
535
+
536
+ .column-title {
537
+ font-size: 12px;
538
+ font-weight: 500;
539
+ text-transform: uppercase;
540
+ letter-spacing: 0.05em;
541
+ }
542
+
543
+ .column-title.pending { color: var(--text-tertiary); }
544
+ .column-title.in-progress { color: var(--accent); }
545
+ .column-title.completed { color: var(--success); }
546
+
547
+ .column-count {
548
+ font-size: 10px;
549
+ padding: 2px 8px;
550
+ border-radius: 10px;
551
+ font-variant-numeric: tabular-nums;
552
+ }
553
+
554
+ .column-count.pending {
555
+ background: var(--bg-elevated);
556
+ color: var(--text-muted);
557
+ }
558
+
559
+ .column-count.in-progress {
560
+ background: var(--accent-dim);
561
+ color: var(--accent);
562
+ }
563
+
564
+ .column-count.completed {
565
+ background: var(--success-dim);
566
+ color: var(--success);
567
+ }
568
+
569
+ .column-tasks {
570
+ flex: 1;
571
+ overflow-y: auto;
572
+ display: flex;
573
+ flex-direction: column;
574
+ gap: 8px;
575
+ }
576
+
577
+ .column-empty {
578
+ text-align: center;
579
+ padding: 32px 16px;
580
+ color: var(--text-muted);
581
+ font-size: 12px;
582
+ }
583
+
584
+ /* Task card */
585
+ .task-card {
586
+ padding: 14px;
587
+ background: var(--bg-surface);
588
+ border: 1px solid var(--border);
589
+ border-radius: 8px;
590
+ cursor: pointer;
591
+ transition: all 0.15s ease;
592
+ }
593
+
594
+ .task-card:hover {
595
+ background: var(--bg-elevated);
596
+ border-color: var(--text-muted);
597
+ transform: translateY(-1px);
598
+ }
599
+
600
+ .task-card.in-progress {
601
+ border-color: var(--accent);
602
+ box-shadow: 0 0 20px var(--accent-dim), inset 0 0 20px var(--accent-dim);
603
+ }
604
+
605
+ .task-card.completed {
606
+ opacity: 0.6;
607
+ }
608
+
609
+ .task-card.blocked {
610
+ opacity: 0.5;
611
+ }
612
+
613
+ .task-id {
614
+ font-size: 10px;
615
+ color: var(--text-muted);
616
+ margin-bottom: 6px;
617
+ display: flex;
618
+ align-items: center;
619
+ gap: 8px;
620
+ }
621
+
622
+ .task-badge {
623
+ font-size: 9px;
624
+ padding: 2px 6px;
625
+ border-radius: 3px;
626
+ text-transform: uppercase;
627
+ letter-spacing: 0.03em;
628
+ }
629
+
630
+ .task-badge.blocked {
631
+ background: var(--warning-dim);
632
+ color: var(--warning);
633
+ }
634
+
635
+ .task-title {
636
+ font-size: 13px;
637
+ color: var(--text-primary);
638
+ line-height: 1.4;
639
+ }
640
+
641
+ .task-card.completed .task-title {
642
+ text-decoration: line-through;
643
+ color: var(--text-tertiary);
644
+ }
645
+
646
+ .task-session {
647
+ font-size: 11px;
648
+ color: var(--accent);
649
+ margin-top: 6px;
650
+ opacity: 0.8;
651
+ }
652
+
653
+ .task-active {
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 6px;
657
+ margin-top: 10px;
658
+ padding-top: 10px;
659
+ border-top: 1px solid var(--border);
660
+ font-size: 11px;
661
+ color: var(--accent);
662
+ }
663
+
664
+ .task-active::before {
665
+ content: '';
666
+ width: 6px;
667
+ height: 6px;
668
+ background: var(--accent);
669
+ border-radius: 50%;
670
+ animation: pulse 2s ease-in-out infinite;
671
+ }
672
+
673
+ .task-blocked {
674
+ font-size: 10px;
675
+ color: var(--text-muted);
676
+ margin-top: 8px;
677
+ }
678
+
679
+ .task-desc {
680
+ font-size: 11px;
681
+ color: var(--text-tertiary);
682
+ margin-top: 8px;
683
+ display: -webkit-box;
684
+ -webkit-line-clamp: 2;
685
+ -webkit-box-orient: vertical;
686
+ overflow: hidden;
687
+ }
688
+
689
+ /* Detail panel */
690
+ .detail-panel {
691
+ width: 400px;
692
+ background: var(--bg-surface);
693
+ border-left: 1px solid var(--border);
694
+ display: none;
695
+ flex-direction: column;
696
+ flex-shrink: 0;
697
+ overflow: hidden;
698
+ }
699
+
700
+ .detail-panel.visible {
701
+ display: flex;
702
+ }
703
+
704
+ .detail-header {
705
+ padding: 16px 20px;
706
+ border-bottom: 1px solid var(--border);
707
+ display: flex;
708
+ align-items: center;
709
+ justify-content: space-between;
710
+ }
711
+
712
+ .detail-header h3 {
713
+ font-family: var(--serif);
714
+ font-size: 14px;
715
+ font-weight: 500;
716
+ }
717
+
718
+ .detail-close {
719
+ width: 28px;
720
+ height: 28px;
721
+ display: flex;
722
+ align-items: center;
723
+ justify-content: center;
724
+ background: transparent;
725
+ border: none;
726
+ color: var(--text-muted);
727
+ cursor: pointer;
728
+ border-radius: 4px;
729
+ transition: all 0.15s;
730
+ }
731
+
732
+ .detail-close:hover {
733
+ background: var(--bg-hover);
734
+ color: var(--text-primary);
735
+ }
736
+
737
+ .detail-close svg {
738
+ width: 16px;
739
+ height: 16px;
740
+ }
741
+
742
+ .detail-content {
743
+ flex: 1;
744
+ overflow-y: auto;
745
+ padding: 20px;
746
+ }
747
+
748
+ .detail-section {
749
+ margin-bottom: 20px;
750
+ }
751
+
752
+ .detail-label {
753
+ font-size: 10px;
754
+ font-weight: 500;
755
+ text-transform: uppercase;
756
+ letter-spacing: 0.08em;
757
+ color: var(--text-muted);
758
+ margin-bottom: 8px;
759
+ }
760
+
761
+ .detail-title {
762
+ font-family: var(--serif);
763
+ font-size: 18px;
764
+ line-height: 1.4;
765
+ }
766
+
767
+ .detail-status {
768
+ display: inline-flex;
769
+ align-items: center;
770
+ gap: 6px;
771
+ font-size: 12px;
772
+ padding: 6px 10px;
773
+ border-radius: 4px;
774
+ }
775
+
776
+ .detail-status.pending {
777
+ background: var(--bg-elevated);
778
+ color: var(--text-tertiary);
779
+ }
780
+
781
+ .detail-status.in_progress {
782
+ background: var(--accent-dim);
783
+ color: var(--accent);
784
+ }
785
+
786
+ .detail-status.completed {
787
+ background: var(--success-dim);
788
+ color: var(--success);
789
+ }
790
+
791
+ .detail-status .dot {
792
+ width: 6px;
793
+ height: 6px;
794
+ border-radius: 50%;
795
+ background: currentColor;
796
+ }
797
+
798
+ .detail-status.in_progress .dot {
799
+ animation: pulse 2s ease-in-out infinite;
800
+ }
801
+
802
+ .detail-box {
803
+ padding: 12px;
804
+ border-radius: 6px;
805
+ font-size: 12px;
806
+ }
807
+
808
+ .detail-box.active {
809
+ background: var(--accent-dim);
810
+ border: 1px solid rgba(232, 111, 51, 0.2);
811
+ color: var(--accent);
812
+ }
813
+
814
+ .detail-box.blocked {
815
+ background: var(--warning-dim);
816
+ border: 1px solid rgba(240, 180, 41, 0.2);
817
+ color: var(--warning);
818
+ }
819
+
820
+ .detail-box.blocks {
821
+ background: rgba(96, 165, 250, 0.1);
822
+ border: 1px solid rgba(96, 165, 250, 0.2);
823
+ color: #60a5fa;
824
+ }
825
+
826
+ .detail-desc {
827
+ font-size: 13px;
828
+ line-height: 1.7;
829
+ color: var(--text-secondary);
830
+ }
831
+
832
+ .detail-desc pre {
833
+ background: var(--bg-elevated);
834
+ padding: 12px;
835
+ border-radius: 6px;
836
+ overflow-x: auto;
837
+ margin: 12px 0;
838
+ font-size: 12px;
839
+ }
840
+
841
+ .detail-desc code {
842
+ background: var(--bg-elevated);
843
+ padding: 2px 6px;
844
+ border-radius: 3px;
845
+ font-size: 0.9em;
846
+ }
847
+
848
+ .detail-desc pre code {
849
+ background: transparent;
850
+ padding: 0;
851
+ }
852
+
853
+ .detail-desc hr {
854
+ border: none;
855
+ border-top: 1px solid var(--border);
856
+ margin: 16px 0;
857
+ }
858
+
859
+ .detail-desc h4 {
860
+ font-size: 10px;
861
+ font-weight: 600;
862
+ text-transform: uppercase;
863
+ letter-spacing: 0.05em;
864
+ color: var(--accent);
865
+ margin: 0 0 8px 0;
866
+ }
867
+
868
+ .detail-desc p {
869
+ margin: 0 0 12px 0;
870
+ }
871
+
872
+ .detail-desc p:last-child {
873
+ margin-bottom: 0;
874
+ }
875
+
876
+ /* Note form */
877
+ .note-section {
878
+ margin-top: 24px;
879
+ padding-top: 20px;
880
+ border-top: 1px solid var(--border);
881
+ }
882
+
883
+ .note-form {
884
+ display: flex;
885
+ flex-direction: column;
886
+ gap: 10px;
887
+ }
888
+
889
+ .note-input {
890
+ width: 100%;
891
+ padding: 10px 12px;
892
+ background: var(--bg-elevated);
893
+ border: 1px solid var(--border);
894
+ border-radius: 6px;
895
+ color: var(--text-primary);
896
+ font-family: var(--mono);
897
+ font-size: 12px;
898
+ line-height: 1.5;
899
+ resize: vertical;
900
+ min-height: 60px;
901
+ }
902
+
903
+ .note-input:focus {
904
+ outline: none;
905
+ border-color: var(--accent);
906
+ box-shadow: 0 0 0 2px var(--accent-dim);
907
+ }
908
+
909
+ .note-input::placeholder {
910
+ color: var(--text-muted);
911
+ }
912
+
913
+ .note-submit {
914
+ align-self: flex-end;
915
+ padding: 8px 16px;
916
+ background: var(--accent);
917
+ border: none;
918
+ border-radius: 5px;
919
+ color: white;
920
+ font-family: var(--mono);
921
+ font-size: 11px;
922
+ font-weight: 500;
923
+ cursor: pointer;
924
+ transition: all 0.15s ease;
925
+ }
926
+
927
+ .note-submit:hover {
928
+ filter: brightness(1.1);
929
+ }
930
+
931
+ /* Light mode */
932
+ body.light {
933
+ --bg-deep: #fafafa;
934
+ --bg-surface: #ffffff;
935
+ --bg-elevated: #f5f5f5;
936
+ --bg-hover: #efefef;
937
+ --border: #e5e5e5;
938
+ --text-primary: #171717;
939
+ --text-secondary: #525252;
940
+ --text-tertiary: #737373;
941
+ --text-muted: #a3a3a3;
942
+ --accent-dim: rgba(232, 111, 51, 0.1);
943
+ --accent-glow: rgba(232, 111, 51, 0.3);
944
+ --success-dim: rgba(62, 207, 142, 0.1);
945
+ --warning-dim: rgba(240, 180, 41, 0.1);
946
+ }
947
+
948
+ body.light::before {
949
+ display: none;
950
+ }
79
951
  </style>
80
952
  </head>
81
- <body class="bg-gray-950 text-gray-100 min-h-screen">
82
- <div class="flex h-screen">
953
+ <body>
954
+ <div class="app">
83
955
  <!-- Sidebar -->
84
- <aside class="w-72 bg-gray-900 border-r border-gray-800 flex flex-col flex-shrink-0">
85
- <header class="p-4 border-b border-gray-800">
86
- <h1 class="text-lg font-semibold flex items-center gap-2">
87
- <svg class="w-6 h-6 text-claude-orange" viewBox="0 0 24 24" fill="currentColor">
88
- <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 2m-6 9l2 2 4-4"/>
89
- </svg>
90
- Claude Tasks
91
- </h1>
92
- <p class="text-xs text-gray-500 mt-1">~/.claude/tasks</p>
956
+ <aside class="sidebar">
957
+ <header class="sidebar-header">
958
+ <div class="logo">
959
+ <div class="logo-mark">
960
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
961
+ <path d="M5 13l4 4L19 7"/>
962
+ </svg>
963
+ </div>
964
+ <span class="logo-text">Claude Tasks</span>
965
+ </div>
966
+ <div id="connection-status" class="connection">
967
+ <span class="connection-dot"></span>
968
+ <span>Connecting</span>
969
+ </div>
93
970
  </header>
94
971
 
95
- <div class="p-3 border-b border-gray-800">
96
- <div id="connection-status" class="flex items-center gap-2 text-xs">
97
- <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
98
- <span class="text-gray-400">Connecting...</span>
972
+ <!-- Live Updates -->
973
+ <div class="sidebar-section">
974
+ <div class="section-header">
975
+ <span>Live Updates</span>
976
+ </div>
977
+ <div id="live-updates" class="live-updates">
978
+ <div class="live-empty">No active tasks</div>
99
979
  </div>
100
980
  </div>
101
981
 
102
- <!-- All Tasks button -->
103
- <div class="p-2 border-b border-gray-800">
104
- <button
105
- id="all-tasks-btn"
106
- onclick="showAllTasks()"
107
- class="w-full text-left p-3 rounded-lg transition-colors hover:bg-gray-800/50 flex items-center gap-2"
108
- >
109
- <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
111
- </svg>
112
- <span class="text-sm text-gray-300">All Tasks</span>
113
- </button>
114
- <label class="flex items-center gap-2 px-3 py-2 mt-1 text-xs text-gray-500 cursor-pointer hover:text-gray-400">
115
- <input type="checkbox" id="hide-inactive" onchange="toggleHideInactive()" class="rounded border-gray-600 bg-gray-800 text-claude-orange focus:ring-claude-orange focus:ring-offset-0">
116
- <span>Show active only</span>
117
- </label>
982
+ <!-- Tasks -->
983
+ <div class="sidebar-section flex-1">
984
+ <div class="section-header">
985
+ <span>Tasks</span>
986
+ </div>
987
+ <div class="filter-row">
988
+ <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)">
989
+ <option value="">All Projects</option>
990
+ </select>
991
+ <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)">
992
+ <option value="all">All Sessions</option>
993
+ <option value="active">Active Only</option>
994
+ </select>
995
+ </div>
996
+ <div id="sessions-list" class="sessions-list"></div>
118
997
  </div>
119
998
 
120
- <nav id="sessions-list" class="flex-1 overflow-y-auto p-2">
121
- <p class="text-gray-500 text-sm p-2">Loading sessions...</p>
122
- </nav>
123
-
124
- <footer class="p-3 border-t border-gray-800 text-xs text-gray-600">
125
- <a href="https://github.com/L1AD/claude-task-viewer" target="_blank" class="hover:text-gray-400">GitHub</a>
126
- <span class="mx-2">·</span>
127
- <a href="https://policylayer.com" target="_blank" class="hover:text-gray-400">PolicyLayer</a>
128
- <span class="mx-2">·</span>
129
- <span>v1.2.0</span>
999
+ <footer class="sidebar-footer">
1000
+ <a href="https://github.com/L1AD/claude-task-viewer" target="_blank">GitHub</a>
1001
+ <span style="margin: 0 6px;">·</span>
1002
+ <a href="https://policylayer.com" target="_blank">PolicyLayer</a>
130
1003
  </footer>
131
1004
  </aside>
132
1005
 
133
- <!-- Main content -->
134
- <main class="flex-1 flex flex-col overflow-hidden">
135
- <div id="no-session" class="flex-1 flex items-center justify-center text-gray-600">
136
- <div class="text-center">
137
- <svg class="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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"/>
139
- </svg>
140
- <p>Select a session to view tasks</p>
141
- </div>
1006
+ <!-- Main -->
1007
+ <main class="main">
1008
+ <div id="no-session" class="empty-state">
1009
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1010
+ <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"/>
1011
+ </svg>
1012
+ <p>Select a session to view tasks</p>
142
1013
  </div>
143
1014
 
144
- <div id="session-view" class="flex-1 flex flex-col overflow-hidden hidden">
145
- <!-- Session header -->
146
- <header class="p-4 border-b border-gray-800 bg-gray-900/50 flex-shrink-0">
147
- <div class="flex items-center justify-between">
148
- <div>
149
- <h2 id="session-title" class="text-sm font-mono text-gray-400">Session</h2>
150
- <p id="session-meta" class="text-xs text-gray-500 mt-0.5"></p>
151
- </div>
152
- <div class="flex items-center gap-4">
153
- <div class="flex items-center gap-2">
154
- <div class="w-32 h-2 bg-gray-800 rounded-full overflow-hidden">
155
- <div id="progress-bar" class="h-full bg-gradient-to-r from-claude-orange to-orange-400 transition-all duration-500" style="width: 0%"></div>
156
- </div>
157
- <span id="progress-percent" class="text-sm font-medium text-claude-orange">0%</span>
1015
+ <div id="session-view" class="session-view">
1016
+ <header class="view-header">
1017
+ <div>
1018
+ <h1 id="session-title" class="view-title">Session</h1>
1019
+ <p id="session-meta" class="view-meta"></p>
1020
+ </div>
1021
+ <div class="view-actions">
1022
+ <div class="view-progress">
1023
+ <div class="progress-bar">
1024
+ <div id="progress-bar" class="progress-fill" style="width: 0%"></div>
158
1025
  </div>
159
- <button id="theme-toggle" onclick="toggleTheme()" class="p-2 rounded-lg hover:bg-gray-800 text-gray-400 hover:text-gray-200 transition-colors" title="Toggle theme">
160
- <svg id="theme-icon-dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>
162
- </svg>
163
- <svg id="theme-icon-light" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
165
- </svg>
166
- </button>
1026
+ <span id="progress-percent" class="progress-text">0%</span>
167
1027
  </div>
1028
+ <button id="theme-toggle" class="icon-btn" onclick="toggleTheme()" title="Toggle theme">
1029
+ <svg id="theme-icon-dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1030
+ <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"/>
1031
+ </svg>
1032
+ <svg id="theme-icon-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
1033
+ <circle cx="12" cy="12" r="5"/>
1034
+ <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"/>
1035
+ </svg>
1036
+ </button>
168
1037
  </div>
169
1038
  </header>
170
1039
 
171
- <!-- Kanban board -->
172
- <div class="flex-1 overflow-x-auto p-4">
173
- <div class="flex gap-8 h-full min-w-max">
174
- <!-- Pending column -->
175
- <div class="w-80 flex flex-col">
176
- <div class="flex items-center gap-2 mb-3 px-1">
177
- <span class="w-3 h-3 rounded-full bg-gray-500"></span>
178
- <h3 class="font-medium text-gray-400">Pending</h3>
179
- <span id="pending-count" class="text-xs text-gray-600 bg-gray-800 px-2 py-0.5 rounded-full">0</span>
180
- </div>
181
- <div id="pending-tasks" class="flex-1 space-y-3 kanban-column overflow-y-auto pr-1">
182
- </div>
1040
+ <div class="kanban">
1041
+ <div class="kanban-column">
1042
+ <div class="column-header">
1043
+ <span class="column-dot pending"></span>
1044
+ <span class="column-title pending">Pending</span>
1045
+ <span id="pending-count" class="column-count pending">0</span>
183
1046
  </div>
1047
+ <div id="pending-tasks" class="column-tasks"></div>
1048
+ </div>
184
1049
 
185
- <!-- In Progress column -->
186
- <div class="w-80 flex flex-col">
187
- <div class="flex items-center gap-2 mb-3 px-1">
188
- <span class="w-3 h-3 rounded-full bg-claude-orange status-pulse"></span>
189
- <h3 class="font-medium text-claude-orange">In Progress</h3>
190
- <span id="in-progress-count" class="text-xs text-claude-orange/70 bg-claude-orange/20 px-2 py-0.5 rounded-full">0</span>
191
- </div>
192
- <div id="in-progress-tasks" class="flex-1 space-y-3 kanban-column overflow-y-auto pr-1">
193
- </div>
1050
+ <div class="kanban-column">
1051
+ <div class="column-header">
1052
+ <span class="column-dot in-progress"></span>
1053
+ <span class="column-title in-progress">In Progress</span>
1054
+ <span id="in-progress-count" class="column-count in-progress">0</span>
194
1055
  </div>
1056
+ <div id="in-progress-tasks" class="column-tasks"></div>
1057
+ </div>
195
1058
 
196
- <!-- Completed column -->
197
- <div class="w-80 flex flex-col">
198
- <div class="flex items-center gap-2 mb-3 px-1">
199
- <span class="w-3 h-3 rounded-full bg-green-500"></span>
200
- <h3 class="font-medium text-green-400">Completed</h3>
201
- <span id="completed-count" class="text-xs text-green-400/70 bg-green-500/20 px-2 py-0.5 rounded-full">0</span>
202
- </div>
203
- <div id="completed-tasks" class="flex-1 space-y-3 kanban-column overflow-y-auto pr-1">
204
- </div>
1059
+ <div class="kanban-column">
1060
+ <div class="column-header">
1061
+ <span class="column-dot completed"></span>
1062
+ <span class="column-title completed">Completed</span>
1063
+ <span id="completed-count" class="column-count completed">0</span>
205
1064
  </div>
1065
+ <div id="completed-tasks" class="column-tasks"></div>
206
1066
  </div>
207
1067
  </div>
208
1068
  </div>
209
1069
  </main>
210
1070
 
211
- <!-- Task detail panel -->
212
- <aside id="detail-panel" class="w-96 bg-gray-900 border-l border-gray-800 hidden overflow-y-auto flex-shrink-0">
213
- <header class="p-4 border-b border-gray-800 flex items-center justify-between sticky top-0 bg-gray-900 z-10">
214
- <h3 class="font-semibold">Task Details</h3>
215
- <button id="close-detail" class="text-gray-500 hover:text-gray-300">
216
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
217
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
1071
+ <!-- Detail panel -->
1072
+ <aside id="detail-panel" class="detail-panel">
1073
+ <header class="detail-header">
1074
+ <h3>Task Details</h3>
1075
+ <button id="close-detail" class="detail-close">
1076
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1077
+ <path d="M6 18L18 6M6 6l12 12"/>
218
1078
  </svg>
219
1079
  </button>
220
1080
  </header>
221
- <div id="detail-content" class="p-4"></div>
1081
+ <div id="detail-content" class="detail-content"></div>
222
1082
  </aside>
223
1083
  </div>
224
1084
 
@@ -227,10 +1087,11 @@
227
1087
  let sessions = [];
228
1088
  let currentSessionId = null;
229
1089
  let currentTasks = [];
230
- let viewMode = 'session'; // 'session' or 'all'
231
- let hideInactive = localStorage.getItem('hideInactive') === 'true';
1090
+ let viewMode = 'session';
1091
+ let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
1092
+ let filterProject = null; // null = all projects, or project path to filter
232
1093
 
233
- // DOM elements
1094
+ // DOM
234
1095
  const sessionsList = document.getElementById('sessions-list');
235
1096
  const noSession = document.getElementById('no-session');
236
1097
  const sessionView = document.getElementById('session-view');
@@ -248,18 +1109,55 @@
248
1109
  const detailContent = document.getElementById('detail-content');
249
1110
  const connectionStatus = document.getElementById('connection-status');
250
1111
 
251
- // Fetch sessions
252
1112
  async function fetchSessions() {
253
1113
  try {
254
1114
  const res = await fetch('/api/sessions');
255
1115
  sessions = await res.json();
256
1116
  renderSessions();
1117
+ fetchLiveUpdates();
257
1118
  } catch (error) {
258
1119
  console.error('Failed to fetch sessions:', error);
259
1120
  }
260
1121
  }
261
1122
 
262
- // Fetch tasks for a session
1123
+ async function fetchLiveUpdates() {
1124
+ try {
1125
+ const res = await fetch('/api/tasks/all');
1126
+ const allTasks = await res.json();
1127
+ let activeTasks = allTasks.filter(t => t.status === 'in_progress');
1128
+ if (filterProject) {
1129
+ activeTasks = activeTasks.filter(t => t.project === filterProject);
1130
+ }
1131
+ renderLiveUpdates(activeTasks);
1132
+ } catch (error) {
1133
+ console.error('Failed to fetch live updates:', error);
1134
+ }
1135
+ }
1136
+
1137
+ function renderLiveUpdates(activeTasks) {
1138
+ const container = document.getElementById('live-updates');
1139
+
1140
+ if (activeTasks.length === 0) {
1141
+ container.innerHTML = '<div class="live-empty">No active tasks</div>';
1142
+ return;
1143
+ }
1144
+
1145
+ container.innerHTML = activeTasks.map(task => `
1146
+ <div class="live-item" onclick="openLiveTask('${task.sessionId}', '${task.id}')">
1147
+ <span class="pulse"></span>
1148
+ <div class="live-item-content">
1149
+ <div class="live-item-action">${escapeHtml(task.activeForm || task.subject)}</div>
1150
+ <div class="live-item-session">${escapeHtml(task.sessionName || task.sessionId.slice(0, 8))}</div>
1151
+ </div>
1152
+ </div>
1153
+ `).join('');
1154
+ }
1155
+
1156
+ async function openLiveTask(sessionId, taskId) {
1157
+ await fetchTasks(sessionId);
1158
+ showTaskDetail(taskId, sessionId);
1159
+ }
1160
+
263
1161
  async function fetchTasks(sessionId) {
264
1162
  try {
265
1163
  viewMode = 'session';
@@ -272,13 +1170,16 @@
272
1170
  }
273
1171
  }
274
1172
 
275
- // Show all tasks across all sessions
276
1173
  async function showAllTasks() {
277
1174
  try {
278
1175
  viewMode = 'all';
279
1176
  currentSessionId = null;
280
1177
  const res = await fetch('/api/tasks/all');
281
- currentTasks = await res.json();
1178
+ let tasks = await res.json();
1179
+ if (filterProject) {
1180
+ tasks = tasks.filter(t => t.project === filterProject);
1181
+ }
1182
+ currentTasks = tasks;
282
1183
  renderAllTasks();
283
1184
  renderSessions();
284
1185
  } catch (error) {
@@ -286,44 +1187,54 @@
286
1187
  }
287
1188
  }
288
1189
 
289
- // Render all tasks view
290
1190
  function renderAllTasks() {
291
- noSession.classList.add('hidden');
292
- sessionView.classList.remove('hidden');
1191
+ noSession.style.display = 'none';
1192
+ sessionView.classList.add('visible');
293
1193
 
294
1194
  const totalTasks = currentTasks.length;
295
1195
  const completed = currentTasks.filter(t => t.status === 'completed').length;
296
1196
  const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
297
1197
 
298
- sessionTitle.textContent = 'All Tasks';
299
- sessionMeta.textContent = `${totalTasks} tasks across ${sessions.length} sessions`;
300
-
1198
+ const projectName = filterProject ? filterProject.split('/').pop() : null;
1199
+ sessionTitle.textContent = filterProject ? `Tasks: ${projectName}` : 'All Tasks';
1200
+ sessionMeta.textContent = filterProject
1201
+ ? `${totalTasks} tasks in this project`
1202
+ : `${totalTasks} tasks across ${sessions.length} sessions`;
301
1203
  progressPercent.textContent = `${percent}%`;
302
1204
  progressBar.style.width = `${percent}%`;
303
1205
 
304
1206
  renderKanban();
305
1207
  }
306
1208
 
307
- // Render sessions sidebar
308
1209
  function renderSessions() {
309
- // Update all tasks button state
310
- const allTasksBtn = document.getElementById('all-tasks-btn');
311
- if (allTasksBtn) {
312
- allTasksBtn.className = `w-full text-left p-3 rounded-lg transition-colors flex items-center gap-2 ${
313
- viewMode === 'all' ? 'bg-gray-800' : 'hover:bg-gray-800/50'
314
- }`;
315
- }
1210
+ // Update project dropdown
1211
+ updateProjectDropdown();
316
1212
 
317
- // Filter sessions based on hideInactive
318
- const filteredSessions = hideInactive
319
- ? sessions.filter(s => s.pending > 0 || s.inProgress > 0)
320
- : sessions;
1213
+ let filteredSessions = sessions;
1214
+ if (sessionFilter === 'active') {
1215
+ filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
1216
+ }
1217
+ if (filterProject) {
1218
+ filteredSessions = filteredSessions.filter(s => s.project === filterProject);
1219
+ }
321
1220
 
322
1221
  if (filteredSessions.length === 0) {
1222
+ let emptyMsg = 'No sessions found';
1223
+ let emptyHint = 'Tasks appear when you use Claude Code';
1224
+ if (filterProject && sessionFilter === 'active') {
1225
+ emptyMsg = 'No active sessions for this project';
1226
+ emptyHint = 'Try "All Sessions" or "All Projects"';
1227
+ } else if (filterProject) {
1228
+ emptyMsg = 'No sessions for this project';
1229
+ emptyHint = 'Select "All Projects" to see all';
1230
+ } else if (sessionFilter === 'active') {
1231
+ emptyMsg = 'No active sessions';
1232
+ emptyHint = 'Select "All Sessions" to see all';
1233
+ }
323
1234
  sessionsList.innerHTML = `
324
- <div class="text-gray-500 text-sm p-4 text-center">
325
- <p>${hideInactive ? 'No active sessions' : 'No sessions found'}</p>
326
- <p class="mt-2 text-xs">${hideInactive ? 'Uncheck "Show active only" to see all' : 'Tasks will appear here when you use Claude Code'}</p>
1235
+ <div style="padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 12px;">
1236
+ <p>${emptyMsg}</p>
1237
+ <p style="margin-top: 8px; font-size: 11px;">${emptyHint}</p>
327
1238
  </div>
328
1239
  `;
329
1240
  return;
@@ -334,35 +1245,31 @@
334
1245
  const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
335
1246
  const isActive = session.id === currentSessionId && viewMode === 'session';
336
1247
  const hasInProgress = session.inProgress > 0;
337
- const displayName = session.name || session.id.slice(0, 8) + '...';
1248
+ const sessionName = session.name || session.id.slice(0, 8) + '...';
338
1249
  const projectName = session.project ? session.project.split('/').pop() : null;
1250
+ const primaryName = projectName || sessionName;
1251
+ const secondaryName = projectName ? sessionName : null;
339
1252
 
340
1253
  return `
341
- <button
342
- onclick="fetchTasks('${session.id}')"
343
- class="w-full text-left p-3 rounded-lg transition-colors ${isActive ? 'bg-gray-800' : 'hover:bg-gray-800/50'}"
344
- >
345
- <div class="flex items-center justify-between gap-2">
346
- <span class="text-sm text-gray-200 truncate flex-1 ${session.name ? '' : 'font-mono text-xs text-gray-400'}">${escapeHtml(displayName)}</span>
347
- ${hasInProgress ? '<span class="w-2 h-2 rounded-full bg-claude-orange status-pulse flex-shrink-0"></span>' : ''}
1254
+ <button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}">
1255
+ <div class="session-name">
1256
+ <span>${escapeHtml(primaryName)}</span>
1257
+ ${hasInProgress ? '<span class="pulse"></span>' : ''}
348
1258
  </div>
349
- ${projectName ? `<p class="text-xs text-gray-500 mt-1 truncate">${escapeHtml(projectName)}</p>` : ''}
350
- <div class="flex items-center gap-2 mt-2">
351
- <div class="flex-1 h-1.5 bg-gray-700 rounded-full overflow-hidden">
352
- <div class="h-full bg-claude-orange transition-all" style="width: ${percent}%"></div>
353
- </div>
354
- <span class="text-xs text-gray-500">${session.completed}/${total}</span>
1259
+ ${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
1260
+ <div class="session-progress">
1261
+ <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
1262
+ <span class="progress-text">${session.completed}/${total}</span>
355
1263
  </div>
356
- <p class="text-xs text-gray-600 mt-1">Tasks updated ${formatDate(session.modifiedAt)}</p>
1264
+ <div class="session-time">${formatDate(session.modifiedAt)}</div>
357
1265
  </button>
358
1266
  `;
359
1267
  }).join('');
360
1268
  }
361
1269
 
362
- // Render current session
363
1270
  function renderSession() {
364
- noSession.classList.add('hidden');
365
- sessionView.classList.remove('hidden');
1271
+ noSession.style.display = 'none';
1272
+ sessionView.classList.add('visible');
366
1273
 
367
1274
  const session = sessions.find(s => s.id === currentSessionId);
368
1275
  if (!session) return;
@@ -370,7 +1277,7 @@
370
1277
  const displayName = session.name || currentSessionId;
371
1278
  sessionTitle.textContent = displayName;
372
1279
  const projectName = session.project ? session.project.split('/').pop() : null;
373
- sessionMeta.textContent = `${currentTasks.length} tasks${projectName ? ' · ' + projectName : ''} · Tasks updated ${formatDate(session.modifiedAt)}`;
1280
+ sessionMeta.textContent = `${currentTasks.length} tasks${projectName ? ' · ' + projectName : ''} · ${formatDate(session.modifiedAt)}`;
374
1281
 
375
1282
  const completed = currentTasks.filter(t => t.status === 'completed').length;
376
1283
  const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
@@ -382,41 +1289,27 @@
382
1289
  renderSessions();
383
1290
  }
384
1291
 
385
- // Render task card
386
1292
  function renderTaskCard(task) {
387
1293
  const isBlocked = task.blockedBy && task.blockedBy.length > 0;
388
- const statusStyles = {
389
- pending: 'border-gray-700 bg-gray-800/50',
390
- in_progress: 'border-claude-orange/30 bg-claude-orange/10',
391
- completed: 'border-green-500/30 bg-green-500/10'
392
- };
393
1294
  const taskId = viewMode === 'all' ? `${task.sessionId?.slice(0,4)}-${task.id}` : task.id;
394
1295
  const sessionLabel = viewMode === 'all' && task.sessionName ? task.sessionName : null;
1296
+ const statusClass = task.status.replace('_', '-');
395
1297
 
396
1298
  return `
397
- <div
398
- onclick="showTaskDetail('${task.id}', '${task.sessionId || ''}')"
399
- class="p-3 rounded-lg border ${statusStyles[task.status] || statusStyles.pending} cursor-pointer hover:brightness-110 transition-all ${isBlocked ? 'opacity-60' : ''}"
400
- >
401
- <div class="flex items-center gap-2 mb-2">
402
- <span class="text-xs font-mono text-gray-500">#${taskId}</span>
403
- ${isBlocked ? '<span class="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">blocked</span>' : ''}
1299
+ <div onclick="showTaskDetail('${task.id}', '${task.sessionId || ''}')" class="task-card ${statusClass} ${isBlocked ? 'blocked' : ''}">
1300
+ <div class="task-id">
1301
+ <span>#${taskId}</span>
1302
+ ${isBlocked ? '<span class="task-badge blocked">Blocked</span>' : ''}
404
1303
  </div>
405
- <h4 class="text-sm font-medium ${task.status === 'completed' ? 'line-through text-gray-500' : 'text-gray-200'}">${escapeHtml(task.subject)}</h4>
406
- ${sessionLabel ? `<p class="text-xs text-blue-400 mt-1">${escapeHtml(sessionLabel)}</p>` : ''}
407
- ${task.status === 'in_progress' && task.activeForm ? `
408
- <p class="text-xs text-claude-orange mt-2 flex items-center gap-1">
409
- <span class="status-pulse">●</span>
410
- ${escapeHtml(task.activeForm)}
411
- </p>
412
- ` : ''}
413
- ${isBlocked ? `<p class="text-xs text-gray-500 mt-2">Waiting on: ${task.blockedBy.map(id => '#' + id).join(', ')}</p>` : ''}
414
- ${task.description ? `<p class="text-xs text-gray-500 mt-2 line-clamp-2">${escapeHtml(task.description.split('\n')[0])}</p>` : ''}
1304
+ <div class="task-title">${escapeHtml(task.subject)}</div>
1305
+ ${sessionLabel ? `<div class="task-session">${escapeHtml(sessionLabel)}</div>` : ''}
1306
+ ${task.status === 'in_progress' && task.activeForm ? `<div class="task-active">${escapeHtml(task.activeForm)}</div>` : ''}
1307
+ ${isBlocked ? `<div class="task-blocked">Waiting on ${task.blockedBy.map(id => '#' + id).join(', ')}</div>` : ''}
1308
+ ${task.description ? `<div class="task-desc">${escapeHtml(task.description.split('\n')[0])}</div>` : ''}
415
1309
  </div>
416
1310
  `;
417
1311
  }
418
1312
 
419
- // Render kanban board
420
1313
  function renderKanban() {
421
1314
  const pending = currentTasks.filter(t => t.status === 'pending');
422
1315
  const inProgress = currentTasks.filter(t => t.status === 'in_progress');
@@ -428,112 +1321,160 @@
428
1321
 
429
1322
  pendingTasks.innerHTML = pending.length > 0
430
1323
  ? pending.map(renderTaskCard).join('')
431
- : '<p class="text-gray-600 text-sm text-center py-8">No pending tasks</p>';
1324
+ : '<div class="column-empty">No pending tasks</div>';
432
1325
 
433
1326
  inProgressTasks.innerHTML = inProgress.length > 0
434
1327
  ? inProgress.map(renderTaskCard).join('')
435
- : '<p class="text-gray-600 text-sm text-center py-8">No tasks in progress</p>';
1328
+ : '<div class="column-empty">No active tasks</div>';
436
1329
 
437
1330
  completedTasks.innerHTML = completed.length > 0
438
1331
  ? completed.map(renderTaskCard).join('')
439
- : '<p class="text-gray-600 text-sm text-center py-8">No completed tasks</p>';
1332
+ : '<div class="column-empty">No completed tasks</div>';
440
1333
  }
441
1334
 
442
- // Show task detail
443
1335
  function showTaskDetail(taskId, sessionId = null) {
444
- const task = currentTasks.find(t =>
445
- t.id === taskId && (!sessionId || t.sessionId === sessionId)
446
- );
1336
+ const task = currentTasks.find(t => t.id === taskId && (!sessionId || t.sessionId === sessionId));
447
1337
  if (!task) return;
448
1338
 
449
- detailPanel.classList.remove('hidden');
1339
+ detailPanel.classList.add('visible');
450
1340
 
451
1341
  const statusLabels = {
452
- completed: '<span class="inline-flex items-center gap-1 text-green-400"><span class="w-2 h-2 rounded-full bg-green-500"></span>Completed</span>',
453
- in_progress: '<span class="inline-flex items-center gap-1 text-claude-orange"><span class="w-2 h-2 rounded-full bg-claude-orange status-pulse"></span>In Progress</span>',
454
- pending: '<span class="inline-flex items-center gap-1 text-gray-400"><span class="w-2 h-2 rounded-full bg-gray-500"></span>Pending</span>'
1342
+ completed: '<span class="detail-status completed"><span class="dot"></span>Completed</span>',
1343
+ in_progress: '<span class="detail-status in_progress"><span class="dot"></span>In Progress</span>',
1344
+ pending: '<span class="detail-status pending"><span class="dot"></span>Pending</span>'
455
1345
  };
456
1346
 
457
1347
  detailContent.innerHTML = `
458
- <div class="space-y-4">
459
- <div>
460
- <span class="text-xs text-gray-500">Task #${task.id}</span>
461
- <h3 class="text-lg font-semibold mt-1">${escapeHtml(task.subject)}</h3>
462
- </div>
1348
+ <div class="detail-section">
1349
+ <div class="detail-label">Task #${task.id}</div>
1350
+ <h2 class="detail-title">${escapeHtml(task.subject)}</h2>
1351
+ </div>
463
1352
 
464
- <div class="text-sm">
465
- ${statusLabels[task.status] || statusLabels.pending}
466
- </div>
1353
+ <div class="detail-section">
1354
+ ${statusLabels[task.status] || statusLabels.pending}
1355
+ </div>
467
1356
 
468
- ${task.activeForm && task.status === 'in_progress' ? `
469
- <div class="text-sm bg-claude-orange/10 border border-claude-orange/20 rounded-lg p-3">
470
- <span class="text-gray-400">Currently:</span>
471
- <span class="text-claude-orange ml-1">${escapeHtml(task.activeForm)}</span>
1357
+ ${task.activeForm && task.status === 'in_progress' ? `
1358
+ <div class="detail-section">
1359
+ <div class="detail-box active">
1360
+ <strong>Currently:</strong> ${escapeHtml(task.activeForm)}
472
1361
  </div>
473
- ` : ''}
1362
+ </div>
1363
+ ` : ''}
474
1364
 
475
- ${task.blockedBy && task.blockedBy.length > 0 ? `
476
- <div class="text-sm bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-3">
477
- <span class="text-gray-400">Blocked by:</span>
478
- <span class="text-yellow-400 ml-1">${task.blockedBy.map(id => '#' + id).join(', ')}</span>
1365
+ ${task.blockedBy && task.blockedBy.length > 0 ? `
1366
+ <div class="detail-section">
1367
+ <div class="detail-box blocked">
1368
+ <strong>Blocked by:</strong> ${task.blockedBy.map(id => '#' + id).join(', ')}
479
1369
  </div>
480
- ` : ''}
1370
+ </div>
1371
+ ` : ''}
481
1372
 
482
- ${task.blocks && task.blocks.length > 0 ? `
483
- <div class="text-sm bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
484
- <span class="text-gray-400">Blocks:</span>
485
- <span class="text-blue-400 ml-1">${task.blocks.map(id => '#' + id).join(', ')}</span>
1373
+ ${task.blocks && task.blocks.length > 0 ? `
1374
+ <div class="detail-section">
1375
+ <div class="detail-box blocks">
1376
+ <strong>Blocks:</strong> ${task.blocks.map(id => '#' + id).join(', ')}
486
1377
  </div>
487
- ` : ''}
1378
+ </div>
1379
+ ` : ''}
488
1380
 
489
- ${task.description ? `
490
- <div class="border-t border-gray-800 pt-4 mt-4">
491
- <h4 class="text-sm font-medium text-gray-400 mb-3">Description</h4>
492
- <div class="prose prose-invert prose-sm max-w-none text-gray-300">
493
- ${marked.parse(task.description)}
494
- </div>
495
- </div>
496
- ` : ''}
1381
+ ${task.description ? `
1382
+ <div class="detail-section">
1383
+ <div class="detail-label">Description</div>
1384
+ <div class="detail-desc">${DOMPurify.sanitize(marked.parse(task.description))}</div>
1385
+ </div>
1386
+ ` : ''}
1387
+
1388
+ <div class="detail-section note-section">
1389
+ <div class="detail-label">Add Note</div>
1390
+ <form class="note-form" onsubmit="addNote(event, '${task.id}', '${task.sessionId || currentSessionId}')">
1391
+ <textarea id="note-input" class="note-input" placeholder="Add a note for Claude..." rows="3"></textarea>
1392
+ <button type="submit" class="note-submit">Add Note</button>
1393
+ </form>
497
1394
  </div>
498
1395
  `;
499
1396
  }
500
1397
 
501
- // Close detail panel
502
- document.getElementById('close-detail').onclick = () => {
503
- detailPanel.classList.add('hidden');
504
- };
1398
+ async function addNote(event, taskId, sessionId) {
1399
+ event.preventDefault();
1400
+ const input = document.getElementById('note-input');
1401
+ const note = input.value.trim();
1402
+ if (!note) return;
505
1403
 
506
- // Setup SSE for live updates
507
- function setupEventSource() {
508
- const eventSource = new EventSource('/api/events');
1404
+ try {
1405
+ const res = await fetch(`/api/tasks/${sessionId}/${taskId}/note`, {
1406
+ method: 'POST',
1407
+ headers: { 'Content-Type': 'application/json' },
1408
+ body: JSON.stringify({ note })
1409
+ });
1410
+
1411
+ if (res.ok) {
1412
+ input.value = '';
1413
+ // Refresh to show updated description
1414
+ if (viewMode === 'all') {
1415
+ const tasksRes = await fetch('/api/tasks/all');
1416
+ currentTasks = await tasksRes.json();
1417
+ } else {
1418
+ await fetchTasks(sessionId);
1419
+ }
1420
+ showTaskDetail(taskId, sessionId);
1421
+ }
1422
+ } catch (error) {
1423
+ console.error('Failed to add note:', error);
1424
+ }
1425
+ }
509
1426
 
510
- eventSource.onopen = () => {
511
- connectionStatus.innerHTML = `
512
- <span class="w-2 h-2 rounded-full bg-green-500"></span>
513
- <span class="text-gray-400">Live</span>
514
- `;
515
- };
1427
+ function closeDetailPanel() {
1428
+ detailPanel.classList.remove('visible');
1429
+ }
516
1430
 
517
- eventSource.onerror = () => {
518
- connectionStatus.innerHTML = `
519
- <span class="w-2 h-2 rounded-full bg-red-500"></span>
520
- <span class="text-gray-400">Disconnected</span>
521
- `;
522
- };
1431
+ document.getElementById('close-detail').onclick = closeDetailPanel;
523
1432
 
524
- eventSource.onmessage = (event) => {
525
- const data = JSON.parse(event.data);
1433
+ document.addEventListener('keydown', (e) => {
1434
+ if (e.key === 'Escape' && detailPanel.classList.contains('visible')) {
1435
+ closeDetailPanel();
1436
+ }
1437
+ });
526
1438
 
527
- if (data.type === 'update') {
528
- fetchSessions();
529
- if (data.sessionId === currentSessionId) {
530
- fetchTasks(currentSessionId);
1439
+ function setupEventSource() {
1440
+ let retryDelay = 1000;
1441
+ let eventSource;
1442
+
1443
+ function connect() {
1444
+ eventSource = new EventSource('/api/events');
1445
+
1446
+ eventSource.onopen = () => {
1447
+ retryDelay = 1000; // Reset on successful connection
1448
+ connectionStatus.innerHTML = `
1449
+ <span class="connection-dot live"></span>
1450
+ <span>Connected</span>
1451
+ `;
1452
+ };
1453
+
1454
+ eventSource.onerror = () => {
1455
+ eventSource.close();
1456
+ connectionStatus.innerHTML = `
1457
+ <span class="connection-dot error"></span>
1458
+ <span>Reconnecting...</span>
1459
+ `;
1460
+ setTimeout(connect, retryDelay);
1461
+ retryDelay = Math.min(retryDelay * 2, 30000); // Max 30s
1462
+ };
1463
+
1464
+ eventSource.onmessage = (event) => {
1465
+ const data = JSON.parse(event.data);
1466
+ if (data.type === 'update' || data.type === 'metadata-update') {
1467
+ fetchSessions();
1468
+ if (currentSessionId && data.sessionId === currentSessionId) {
1469
+ fetchTasks(currentSessionId);
1470
+ }
531
1471
  }
532
- }
533
- };
1472
+ };
1473
+ }
1474
+
1475
+ connect();
534
1476
  }
535
1477
 
536
- // Helpers
537
1478
  function formatDate(dateStr) {
538
1479
  const date = new Date(dateStr);
539
1480
  const now = new Date();
@@ -551,56 +1492,56 @@
551
1492
  return div.innerHTML;
552
1493
  }
553
1494
 
554
- // Toggle hide inactive sessions
555
- function toggleHideInactive() {
556
- hideInactive = document.getElementById('hide-inactive').checked;
557
- localStorage.setItem('hideInactive', hideInactive);
1495
+ function filterBySessions(value) {
1496
+ sessionFilter = value;
1497
+ localStorage.setItem('sessionFilter', sessionFilter);
1498
+ renderSessions();
1499
+ }
1500
+
1501
+ function filterByProject(project) {
1502
+ filterProject = project || null;
558
1503
  renderSessions();
1504
+ fetchLiveUpdates();
1505
+ showAllTasks();
1506
+ }
1507
+
1508
+ function updateProjectDropdown() {
1509
+ const dropdown = document.getElementById('project-filter');
1510
+ const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
1511
+
1512
+ dropdown.innerHTML = '<option value="">All Projects</option>' +
1513
+ projects.map(p => {
1514
+ const name = p.split('/').pop();
1515
+ const selected = p === filterProject ? ' selected' : '';
1516
+ return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
1517
+ }).join('');
559
1518
  }
560
1519
 
561
- // Theme toggle
562
1520
  function toggleTheme() {
563
- const body = document.body;
564
- const isLight = body.classList.contains('light');
565
-
566
- if (isLight) {
567
- body.classList.remove('light');
568
- localStorage.setItem('theme', 'dark');
569
- document.getElementById('theme-icon-dark').classList.remove('hidden');
570
- document.getElementById('theme-icon-light').classList.add('hidden');
571
- } else {
572
- body.classList.add('light');
573
- localStorage.setItem('theme', 'light');
574
- document.getElementById('theme-icon-dark').classList.add('hidden');
575
- document.getElementById('theme-icon-light').classList.remove('hidden');
576
- }
1521
+ const isLight = document.body.classList.toggle('light');
1522
+ localStorage.setItem('theme', isLight ? 'light' : 'dark');
1523
+ document.getElementById('theme-icon-dark').style.display = isLight ? 'none' : 'block';
1524
+ document.getElementById('theme-icon-light').style.display = isLight ? 'block' : 'none';
577
1525
  }
578
1526
 
579
- // Load saved theme
580
1527
  function loadTheme() {
581
- const saved = localStorage.getItem('theme');
582
- if (saved === 'light') {
1528
+ if (localStorage.getItem('theme') === 'light') {
583
1529
  document.body.classList.add('light');
584
- document.getElementById('theme-icon-dark').classList.add('hidden');
585
- document.getElementById('theme-icon-light').classList.remove('hidden');
1530
+ document.getElementById('theme-icon-dark').style.display = 'none';
1531
+ document.getElementById('theme-icon-light').style.display = 'block';
586
1532
  }
587
1533
  }
588
1534
 
589
- // Load saved preferences
590
1535
  function loadPreferences() {
591
- document.getElementById('hide-inactive').checked = hideInactive;
1536
+ document.getElementById('session-filter').value = sessionFilter;
592
1537
  }
593
1538
 
594
- // Initialize
1539
+ // Init
595
1540
  loadTheme();
596
1541
  loadPreferences();
597
1542
  fetchSessions();
598
1543
  setupEventSource();
599
-
600
- // Default to All Tasks view
601
- setTimeout(() => {
602
- showAllTasks();
603
- }, 500);
1544
+ setTimeout(showAllTasks, 500);
604
1545
  </script>
605
1546
  </body>
606
1547
  </html>