skill-viewer 0.1.2

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,2219 @@
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>Skills Viewer</title>
7
+ <script src="https://d3js.org/d3.v7.min.js"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ :root {
18
+ --bg-primary: #ffffff;
19
+ --bg-secondary: #f5f5f5;
20
+ --bg-tertiary: #e8e8e8;
21
+ --text-primary: #1a1a1a;
22
+ --text-secondary: #555555;
23
+ --text-muted: #888888;
24
+ --accent: #0066cc;
25
+ --accent-hover: #0052a3;
26
+ --border: #dddddd;
27
+ --success: #28a745;
28
+ --warning: #f0ad4e;
29
+ }
30
+
31
+ body {
32
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
33
+ background: var(--bg-primary);
34
+ color: var(--text-primary);
35
+ height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ }
39
+
40
+ header {
41
+ background: var(--bg-tertiary);
42
+ padding: 12px 20px;
43
+ border-bottom: 1px solid var(--border);
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 16px;
47
+ }
48
+
49
+ header h1 {
50
+ font-size: 18px;
51
+ font-weight: 600;
52
+ }
53
+
54
+ header .status {
55
+ font-size: 12px;
56
+ color: var(--text-muted);
57
+ }
58
+
59
+ .agent-dropdown {
60
+ position: relative;
61
+ }
62
+
63
+ .agent-dropdown-toggle {
64
+ background: var(--bg-secondary);
65
+ border: 1px solid var(--border);
66
+ border-radius: 6px;
67
+ padding: 4px 8px 4px 10px;
68
+ cursor: pointer;
69
+ font-size: 13px;
70
+ color: var(--text-secondary);
71
+ font-family: inherit;
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 4px;
75
+ }
76
+
77
+ .agent-dropdown-toggle:hover {
78
+ background: var(--bg-primary);
79
+ color: var(--text-primary);
80
+ }
81
+
82
+ .agent-dropdown.open .agent-dropdown-toggle {
83
+ background: var(--bg-primary);
84
+ border-color: var(--accent);
85
+ color: var(--text-primary);
86
+ }
87
+
88
+ .agent-dropdown-arrow {
89
+ font-size: 10px;
90
+ opacity: 0.6;
91
+ }
92
+
93
+ .agent-dropdown-menu {
94
+ display: none;
95
+ position: absolute;
96
+ top: 100%;
97
+ right: 0;
98
+ margin-top: 4px;
99
+ background: var(--bg-primary);
100
+ border: 1px solid var(--border);
101
+ border-radius: 6px;
102
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
103
+ min-width: 160px;
104
+ z-index: 1000;
105
+ overflow: hidden;
106
+ }
107
+
108
+ .agent-dropdown.open .agent-dropdown-menu {
109
+ display: block;
110
+ }
111
+
112
+ .agent-dropdown-item {
113
+ padding: 7px 12px;
114
+ font-size: 13px;
115
+ cursor: pointer;
116
+ color: var(--text-secondary);
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: space-between;
120
+ }
121
+
122
+ .agent-dropdown-item:hover {
123
+ background: var(--bg-secondary);
124
+ color: var(--text-primary);
125
+ }
126
+
127
+ .agent-dropdown-item.active {
128
+ color: var(--accent);
129
+ font-weight: 500;
130
+ }
131
+
132
+ .agent-dropdown-item.active::after {
133
+ content: '\2713';
134
+ font-size: 12px;
135
+ }
136
+
137
+ .container {
138
+ display: grid;
139
+ grid-template-columns: 220px 320px 1fr;
140
+ height: calc(100vh - 49px);
141
+ overflow: hidden;
142
+ }
143
+
144
+ .panel {
145
+ background: var(--bg-secondary);
146
+ border-right: 1px solid var(--border);
147
+ overflow-y: auto;
148
+ }
149
+
150
+ .panel::-webkit-scrollbar {
151
+ width: 8px;
152
+ }
153
+
154
+ .panel::-webkit-scrollbar-track {
155
+ background: var(--bg-tertiary);
156
+ }
157
+
158
+ .panel::-webkit-scrollbar-thumb {
159
+ background: var(--border);
160
+ border-radius: 4px;
161
+ }
162
+
163
+ /* Sources Panel */
164
+ .sources-panel .section {
165
+ padding: 4px 0;
166
+ }
167
+
168
+ .sources-panel .section-header {
169
+ padding: 6px 12px;
170
+ font-size: 11px;
171
+ font-weight: 600;
172
+ color: var(--text-muted);
173
+ text-transform: uppercase;
174
+ letter-spacing: 0.5px;
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 6px;
178
+ cursor: pointer;
179
+ }
180
+
181
+ .sources-panel .section-header:hover {
182
+ color: var(--text-secondary);
183
+ }
184
+
185
+ .sources-panel .section-header .toggle {
186
+ font-size: 10px;
187
+ }
188
+
189
+ .sources-panel .section-header .count {
190
+ background: var(--border);
191
+ padding: 1px 6px;
192
+ border-radius: 10px;
193
+ font-size: 10px;
194
+ }
195
+
196
+ /* Sidebar item — every clickable leaf in the sidebar */
197
+ .sb-item {
198
+ position: relative;
199
+ padding: 3px 12px 3px 20px;
200
+ cursor: pointer;
201
+ font-size: 13px;
202
+ color: var(--text-primary);
203
+ border-left: 3px solid transparent;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+
209
+ .sb-item:hover {
210
+ background: var(--bg-tertiary);
211
+ }
212
+
213
+ .sb-item.active {
214
+ background: var(--bg-tertiary);
215
+ border-left-color: var(--accent);
216
+ }
217
+
218
+ /* Source-type left border indicators */
219
+ .sb-item.sb-plugin {
220
+ border-left-color: #c8d6e5;
221
+ }
222
+
223
+ .sb-item.sb-user {
224
+ border-left-color: #d5c4f2;
225
+ }
226
+
227
+ .sb-item.sb-plugin.active {
228
+ border-left-color: var(--accent);
229
+ }
230
+
231
+ .sb-item.sb-user.active {
232
+ border-left-color: var(--accent);
233
+ }
234
+
235
+ /* Nested items under a group */
236
+ .sb-item.sb-nested {
237
+ padding-left: 32px;
238
+ font-size: 12px;
239
+ }
240
+
241
+ /* Group header — small muted uppercase label with toggle */
242
+ .sb-group {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 4px;
246
+ padding: 8px 12px 2px 14px;
247
+ font-size: 10px;
248
+ font-weight: 600;
249
+ color: var(--text-muted);
250
+ text-transform: uppercase;
251
+ letter-spacing: 0.4px;
252
+ cursor: pointer;
253
+ user-select: none;
254
+ }
255
+
256
+ .sb-group:hover {
257
+ color: var(--text-secondary);
258
+ }
259
+
260
+ .sb-group .sb-toggle {
261
+ font-size: 8px;
262
+ width: 8px;
263
+ }
264
+
265
+ .sb-group .sb-group-name {
266
+ flex: 1;
267
+ }
268
+
269
+ .sb-group .sb-group-count {
270
+ font-size: 10px;
271
+ font-weight: 400;
272
+ }
273
+
274
+ .sb-group-children {
275
+ display: none;
276
+ }
277
+
278
+ .sb-group-wrap.expanded > .sb-group-children {
279
+ display: block;
280
+ }
281
+
282
+ /* Meta text (e.g. item count on projects) */
283
+ .sb-meta {
284
+ float: right;
285
+ font-size: 10px;
286
+ color: var(--text-muted);
287
+ }
288
+
289
+ /* Project add form */
290
+ .project-add-form {
291
+ padding: 6px 12px 6px 20px;
292
+ }
293
+
294
+ .project-add-row {
295
+ display: flex;
296
+ gap: 6px;
297
+ }
298
+
299
+ .project-add-row input {
300
+ flex: 1;
301
+ min-width: 0;
302
+ padding: 4px 8px;
303
+ border: 1px solid var(--border);
304
+ border-radius: 4px;
305
+ background: var(--bg-primary);
306
+ color: var(--text-primary);
307
+ font-size: 12px;
308
+ font-family: inherit;
309
+ }
310
+
311
+ .project-add-row input:focus {
312
+ outline: none;
313
+ border-color: var(--accent);
314
+ }
315
+
316
+ .project-add-row button {
317
+ padding: 4px 10px;
318
+ border: none;
319
+ border-radius: 4px;
320
+ background: var(--accent);
321
+ color: #fff;
322
+ cursor: pointer;
323
+ font-size: 12px;
324
+ font-family: inherit;
325
+ white-space: nowrap;
326
+ flex-shrink: 0;
327
+ }
328
+
329
+ .project-add-row button:hover {
330
+ background: var(--accent-hover);
331
+ }
332
+
333
+ .project-error {
334
+ color: #cc3333;
335
+ font-size: 11px;
336
+ margin-top: 4px;
337
+ display: none;
338
+ }
339
+
340
+ .remove-project-btn {
341
+ position: absolute;
342
+ right: 6px;
343
+ top: 50%;
344
+ transform: translateY(-50%);
345
+ background: none;
346
+ border: none;
347
+ color: var(--text-muted);
348
+ cursor: pointer;
349
+ font-size: 12px;
350
+ padding: 2px 4px;
351
+ border-radius: 3px;
352
+ opacity: 0;
353
+ transition: opacity 0.1s;
354
+ }
355
+
356
+ .sb-item:hover .remove-project-btn {
357
+ opacity: 1;
358
+ }
359
+
360
+ .remove-project-btn:hover {
361
+ color: #cc3333;
362
+ }
363
+
364
+ /* Skills Panel */
365
+ .skills-panel {
366
+ display: flex;
367
+ flex-direction: column;
368
+ }
369
+
370
+ .search-box {
371
+ padding: 12px 16px;
372
+ background: var(--bg-tertiary);
373
+ border-bottom: 1px solid var(--border);
374
+ }
375
+
376
+ .search-box input {
377
+ width: 100%;
378
+ padding: 8px 12px;
379
+ background: var(--bg-secondary);
380
+ border: 1px solid var(--border);
381
+ border-radius: 6px;
382
+ color: var(--text-primary);
383
+ font-size: 14px;
384
+ outline: none;
385
+ }
386
+
387
+ .search-box input:focus {
388
+ border-color: var(--accent);
389
+ }
390
+
391
+ .search-box input::placeholder {
392
+ color: var(--text-muted);
393
+ }
394
+
395
+ .skills-list {
396
+ flex: 1;
397
+ overflow-y: auto;
398
+ }
399
+
400
+ .skills-list .skill-item {
401
+ border-bottom: 1px solid var(--border);
402
+ transition: background 0.15s ease;
403
+ }
404
+
405
+ .skills-list .skill-item .skill-header {
406
+ padding: 12px 16px;
407
+ cursor: pointer;
408
+ display: flex;
409
+ align-items: flex-start;
410
+ gap: 8px;
411
+ }
412
+
413
+ .skills-list .skill-item .skill-header:hover {
414
+ background: var(--bg-tertiary);
415
+ }
416
+
417
+ .skills-list .skill-item.active .skill-header {
418
+ background: var(--bg-tertiary);
419
+ border-left: 3px solid var(--accent);
420
+ }
421
+
422
+ .skills-list .skill-item .skill-toggle {
423
+ font-size: 10px;
424
+ color: var(--text-muted);
425
+ margin-top: 4px;
426
+ flex-shrink: 0;
427
+ width: 12px;
428
+ cursor: pointer;
429
+ user-select: none;
430
+ }
431
+
432
+ .skills-list .skill-item .skill-toggle:hover {
433
+ color: var(--accent);
434
+ }
435
+
436
+ .skills-list .skill-item .skill-info {
437
+ flex: 1;
438
+ min-width: 0;
439
+ }
440
+
441
+ .skills-list .skill-item .name {
442
+ font-size: 14px;
443
+ font-weight: 500;
444
+ margin-bottom: 4px;
445
+ }
446
+
447
+ .skills-list .skill-item .description {
448
+ font-size: 12px;
449
+ color: var(--text-secondary);
450
+ overflow: hidden;
451
+ text-overflow: ellipsis;
452
+ white-space: nowrap;
453
+ }
454
+
455
+ .skills-list .skill-item .filename {
456
+ font-size: 11px;
457
+ color: var(--text-muted);
458
+ margin-top: 4px;
459
+ }
460
+
461
+ .skills-list .skill-item .inline-file-tree {
462
+ display: none;
463
+ padding: 4px 16px 12px 36px;
464
+ border-top: 1px solid var(--border);
465
+ background: var(--bg-tertiary);
466
+ }
467
+
468
+ .skills-list .skill-item .inline-file-tree.expanded {
469
+ display: block;
470
+ }
471
+
472
+ .skills-list .skill-item .inline-file-tree .file-tree-item {
473
+ padding: 4px 6px;
474
+ font-size: 12px;
475
+ }
476
+
477
+ .skills-list .skill-item .inline-file-tree .file-tree-item .size {
478
+ font-size: 10px;
479
+ }
480
+
481
+ .empty-state {
482
+ padding: 40px 20px;
483
+ text-align: center;
484
+ color: var(--text-muted);
485
+ }
486
+
487
+ /* Detail Panel */
488
+ .detail-panel {
489
+ background: var(--bg-primary);
490
+ overflow-y: auto;
491
+ }
492
+
493
+ .detail-panel .empty {
494
+ display: flex;
495
+ align-items: center;
496
+ justify-content: center;
497
+ height: 100%;
498
+ color: var(--text-muted);
499
+ font-size: 14px;
500
+ }
501
+
502
+ .detail-content {
503
+ padding: 20px;
504
+ }
505
+
506
+ .detail-header {
507
+ margin-bottom: 20px;
508
+ }
509
+
510
+ .detail-header h2 {
511
+ font-size: 24px;
512
+ font-weight: 600;
513
+ margin-bottom: 8px;
514
+ }
515
+
516
+ .detail-header .description {
517
+ font-size: 14px;
518
+ color: var(--text-secondary);
519
+ }
520
+
521
+ .detail-header .path {
522
+ font-size: 12px;
523
+ color: var(--text-muted);
524
+ margin-top: 8px;
525
+ }
526
+
527
+ .section-title {
528
+ font-size: 12px;
529
+ font-weight: 600;
530
+ color: var(--text-muted);
531
+ text-transform: uppercase;
532
+ letter-spacing: 0.5px;
533
+ margin-bottom: 12px;
534
+ padding-bottom: 8px;
535
+ border-bottom: 1px solid var(--border);
536
+ }
537
+
538
+ .frontmatter-table {
539
+ display: grid;
540
+ gap: 8px;
541
+ margin-bottom: 24px;
542
+ }
543
+
544
+ .frontmatter-row {
545
+ display: grid;
546
+ grid-template-columns: 120px 1fr;
547
+ background: var(--bg-secondary);
548
+ padding: 8px 12px;
549
+ border-radius: 6px;
550
+ }
551
+
552
+ .frontmatter-row .key {
553
+ font-size: 12px;
554
+ font-weight: 500;
555
+ color: var(--accent);
556
+ }
557
+
558
+ .frontmatter-row .value {
559
+ font-size: 12px;
560
+ color: var(--text-primary);
561
+ }
562
+
563
+ .content-preview {
564
+ background: var(--bg-secondary);
565
+ padding: 16px;
566
+ border-radius: 8px;
567
+ margin-bottom: 24px;
568
+ overflow-x: auto;
569
+ }
570
+
571
+ .content-preview pre {
572
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
573
+ font-size: 13px;
574
+ line-height: 1.6;
575
+ white-space: pre-wrap;
576
+ word-wrap: break-word;
577
+ }
578
+
579
+ .content-preview pre code.hljs {
580
+ background: transparent;
581
+ padding: 0;
582
+ }
583
+
584
+ .references-tree {
585
+ margin-bottom: 24px;
586
+ }
587
+
588
+ .reference-group {
589
+ margin-bottom: 16px;
590
+ }
591
+
592
+ .reference-item {
593
+ display: flex;
594
+ align-items: center;
595
+ padding: 8px 12px;
596
+ background: var(--bg-secondary);
597
+ border-radius: 6px;
598
+ margin-bottom: 4px;
599
+ cursor: pointer;
600
+ }
601
+
602
+ .reference-item:hover {
603
+ background: var(--bg-tertiary);
604
+ }
605
+
606
+ .reference-item .icon {
607
+ margin-right: 8px;
608
+ font-size: 12px;
609
+ color: var(--text-muted);
610
+ }
611
+
612
+ .reference-item .name {
613
+ font-size: 13px;
614
+ }
615
+
616
+ .reference-dir {
617
+ margin-bottom: 8px;
618
+ }
619
+
620
+ .reference-dir-header {
621
+ display: flex;
622
+ align-items: center;
623
+ padding: 8px 12px;
624
+ background: var(--bg-tertiary);
625
+ border-radius: 6px;
626
+ cursor: pointer;
627
+ margin-bottom: 4px;
628
+ }
629
+
630
+ .reference-dir-header:hover {
631
+ background: var(--bg-secondary);
632
+ }
633
+
634
+ .reference-dir-header .toggle {
635
+ margin-right: 8px;
636
+ font-size: 10px;
637
+ }
638
+
639
+ .reference-dir-header .name {
640
+ font-size: 13px;
641
+ color: var(--text-secondary);
642
+ }
643
+
644
+ .reference-dir-children {
645
+ padding-left: 16px;
646
+ }
647
+
648
+ .file-tree {
649
+ margin-bottom: 24px;
650
+ }
651
+
652
+ .file-tree-item {
653
+ display: flex;
654
+ align-items: center;
655
+ padding: 6px 8px;
656
+ cursor: pointer;
657
+ border-radius: 4px;
658
+ transition: background 0.1s ease;
659
+ font-size: 13px;
660
+ }
661
+
662
+ .file-tree-item:hover {
663
+ background: var(--bg-secondary);
664
+ }
665
+
666
+ .file-tree-item .indent {
667
+ display: inline-block;
668
+ width: 16px;
669
+ flex-shrink: 0;
670
+ }
671
+
672
+ .file-tree-item .icon {
673
+ margin-right: 6px;
674
+ font-size: 12px;
675
+ flex-shrink: 0;
676
+ width: 16px;
677
+ text-align: center;
678
+ }
679
+
680
+ .file-tree-item .name {
681
+ flex: 1;
682
+ overflow: hidden;
683
+ text-overflow: ellipsis;
684
+ white-space: nowrap;
685
+ }
686
+
687
+ .file-tree-item .size {
688
+ font-size: 11px;
689
+ color: var(--text-muted);
690
+ margin-left: 8px;
691
+ flex-shrink: 0;
692
+ }
693
+
694
+ .file-tree-item.directory > .name {
695
+ font-weight: 500;
696
+ }
697
+
698
+ .file-tree-children {
699
+ display: none;
700
+ }
701
+
702
+ .file-tree-children.expanded {
703
+ display: block;
704
+ }
705
+
706
+ .modal {
707
+ display: none;
708
+ position: fixed;
709
+ top: 0;
710
+ left: 0;
711
+ width: 100%;
712
+ height: 100%;
713
+ background: rgba(0, 0, 0, 0.5);
714
+ z-index: 1000;
715
+ justify-content: center;
716
+ align-items: center;
717
+ }
718
+
719
+ .modal.active {
720
+ display: flex;
721
+ }
722
+
723
+ .modal-content {
724
+ background: var(--bg-primary);
725
+ width: 80%;
726
+ max-width: 800px;
727
+ max-height: 80vh;
728
+ border-radius: 12px;
729
+ overflow: hidden;
730
+ display: flex;
731
+ flex-direction: column;
732
+ }
733
+
734
+ .modal-header {
735
+ padding: 16px 20px;
736
+ background: var(--bg-tertiary);
737
+ border-bottom: 1px solid var(--border);
738
+ display: flex;
739
+ justify-content: space-between;
740
+ align-items: center;
741
+ }
742
+
743
+ .modal-header h3 {
744
+ font-size: 16px;
745
+ }
746
+
747
+ .modal-close {
748
+ cursor: pointer;
749
+ color: var(--text-muted);
750
+ font-size: 20px;
751
+ }
752
+
753
+ .modal-close:hover {
754
+ color: var(--text-primary);
755
+ }
756
+
757
+ .modal-body {
758
+ padding: 20px;
759
+ overflow-y: auto;
760
+ flex: 1;
761
+ }
762
+
763
+ .modal-body pre {
764
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
765
+ font-size: 13px;
766
+ line-height: 1.6;
767
+ white-space: pre-wrap;
768
+ word-wrap: break-word;
769
+ }
770
+
771
+ .loading {
772
+ display: flex;
773
+ align-items: center;
774
+ justify-content: center;
775
+ padding: 20px;
776
+ color: var(--text-muted);
777
+ }
778
+
779
+ .error {
780
+ padding: 20px;
781
+ background: rgba(220, 53, 69, 0.1);
782
+ border: 1px solid rgba(220, 53, 69, 0.3);
783
+ border-radius: 8px;
784
+ color: #dc3545;
785
+ }
786
+
787
+ .global-search {
788
+ position: relative;
789
+ flex: 1;
790
+ max-width: 400px;
791
+ }
792
+
793
+ .global-search input {
794
+ width: 100%;
795
+ padding: 6px 12px 6px 28px;
796
+ background: var(--bg-secondary);
797
+ border: 1px solid var(--border);
798
+ border-radius: 6px;
799
+ color: var(--text-primary);
800
+ font-size: 13px;
801
+ outline: none;
802
+ }
803
+
804
+ .global-search input:focus {
805
+ border-color: var(--accent);
806
+ }
807
+
808
+ .global-search .search-icon {
809
+ position: absolute;
810
+ left: 8px;
811
+ top: 50%;
812
+ transform: translateY(-50%);
813
+ font-size: 12px;
814
+ color: var(--text-muted);
815
+ }
816
+
817
+ .global-search-results {
818
+ display: none;
819
+ position: absolute;
820
+ top: 100%;
821
+ left: 0;
822
+ right: 0;
823
+ background: var(--bg-primary);
824
+ border: 1px solid var(--border);
825
+ border-radius: 0 0 8px 8px;
826
+ max-height: 400px;
827
+ overflow-y: auto;
828
+ z-index: 100;
829
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
830
+ }
831
+
832
+ .global-search-results.active {
833
+ display: block;
834
+ }
835
+
836
+ .search-result-item {
837
+ padding: 10px 12px;
838
+ cursor: pointer;
839
+ border-bottom: 1px solid var(--border);
840
+ }
841
+
842
+ .search-result-item:hover {
843
+ background: var(--bg-secondary);
844
+ }
845
+
846
+ .search-result-item:last-child {
847
+ border-bottom: none;
848
+ }
849
+
850
+ .search-result-item .result-name {
851
+ font-size: 13px;
852
+ font-weight: 500;
853
+ margin-bottom: 2px;
854
+ }
855
+
856
+ .search-result-item .result-source {
857
+ font-size: 11px;
858
+ color: var(--accent);
859
+ margin-bottom: 4px;
860
+ }
861
+
862
+ .search-result-item .result-snippet {
863
+ font-size: 12px;
864
+ color: var(--text-secondary);
865
+ line-height: 1.4;
866
+ overflow: hidden;
867
+ text-overflow: ellipsis;
868
+ white-space: nowrap;
869
+ }
870
+
871
+ .skill-item .badges {
872
+ display: flex;
873
+ gap: 6px;
874
+ margin-top: 6px;
875
+ flex-wrap: wrap;
876
+ }
877
+
878
+ .badge {
879
+ display: inline-flex;
880
+ align-items: center;
881
+ padding: 2px 6px;
882
+ border-radius: 4px;
883
+ font-size: 10px;
884
+ font-weight: 500;
885
+ gap: 3px;
886
+ }
887
+
888
+ .badge.recency-fresh {
889
+ background: rgba(40, 167, 69, 0.1);
890
+ color: #28a745;
891
+ }
892
+
893
+ .badge.recency-aging {
894
+ background: rgba(240, 173, 78, 0.1);
895
+ color: #c89100;
896
+ }
897
+
898
+ .badge.recency-stale {
899
+ background: rgba(220, 53, 69, 0.1);
900
+ color: #dc3545;
901
+ }
902
+
903
+ .badge.tag {
904
+ background: rgba(0, 102, 204, 0.08);
905
+ color: var(--accent);
906
+ }
907
+
908
+ .badge.gap {
909
+ background: rgba(220, 53, 69, 0.06);
910
+ color: #dc3545;
911
+ }
912
+
913
+ .tag-filters {
914
+ padding: 8px 16px;
915
+ background: var(--bg-tertiary);
916
+ border-bottom: 1px solid var(--border);
917
+ display: flex;
918
+ gap: 4px;
919
+ flex-wrap: wrap;
920
+ }
921
+
922
+ .tag-filter {
923
+ padding: 2px 8px;
924
+ border-radius: 12px;
925
+ font-size: 11px;
926
+ cursor: pointer;
927
+ background: var(--bg-secondary);
928
+ color: var(--text-secondary);
929
+ border: 1px solid transparent;
930
+ transition: all 0.1s ease;
931
+ }
932
+
933
+ .tag-filter:hover {
934
+ border-color: var(--accent);
935
+ }
936
+
937
+ .tag-filter.active {
938
+ background: var(--accent);
939
+ color: white;
940
+ }
941
+
942
+ .graph-overlay {
943
+ display: none;
944
+ position: fixed;
945
+ top: 49px;
946
+ left: 0;
947
+ right: 0;
948
+ bottom: 0;
949
+ background: rgba(255, 255, 255, 0.97);
950
+ z-index: 50;
951
+ }
952
+
953
+ .graph-overlay.active {
954
+ display: block;
955
+ }
956
+
957
+ .graph-overlay svg {
958
+ width: 100%;
959
+ height: 100%;
960
+ }
961
+
962
+ .graph-overlay .graph-controls {
963
+ position: absolute;
964
+ top: 12px;
965
+ right: 12px;
966
+ display: flex;
967
+ gap: 8px;
968
+ }
969
+
970
+ .graph-overlay .graph-controls button {
971
+ background: var(--bg-secondary);
972
+ border: 1px solid var(--border);
973
+ border-radius: 6px;
974
+ padding: 4px 10px;
975
+ cursor: pointer;
976
+ font-size: 12px;
977
+ }
978
+
979
+ .graph-node {
980
+ cursor: pointer;
981
+ }
982
+
983
+ .graph-node circle {
984
+ stroke: var(--border);
985
+ stroke-width: 1.5px;
986
+ }
987
+
988
+ .graph-node text {
989
+ font-size: 10px;
990
+ fill: var(--text-primary);
991
+ pointer-events: none;
992
+ }
993
+
994
+ .graph-link {
995
+ stroke: var(--border);
996
+ stroke-opacity: 0.6;
997
+ fill: none;
998
+ }
999
+
1000
+ .graph-link-arrow {
1001
+ fill: var(--border);
1002
+ opacity: 0.6;
1003
+ }
1004
+ </style>
1005
+ </head>
1006
+ <body>
1007
+ <header>
1008
+ <h1>Skills Viewer</h1>
1009
+ <div class="global-search">
1010
+ <span class="search-icon">&#x1F50D;</span>
1011
+ <input type="text" id="global-search-input" placeholder="Search all skills... (Cmd+K)">
1012
+ <div class="global-search-results" id="global-search-results"></div>
1013
+ </div>
1014
+ <span class="status" id="status">Loading...</span>
1015
+ <div style="margin-left: auto; display: flex; align-items: center; gap: 8px;">
1016
+ <div class="agent-dropdown" id="agent-dropdown">
1017
+ <button class="agent-dropdown-toggle" id="agent-dropdown-toggle">
1018
+ <span id="agent-dropdown-label">Claude Code</span>
1019
+ <span class="agent-dropdown-arrow">&#x25BE;</span>
1020
+ </button>
1021
+ <div class="agent-dropdown-menu" id="agent-dropdown-menu"></div>
1022
+ </div>
1023
+ <button id="graph-toggle" title="Dependency Graph" style="
1024
+ background: var(--bg-secondary);
1025
+ border: 1px solid var(--border);
1026
+ border-radius: 6px;
1027
+ padding: 4px 10px;
1028
+ cursor: pointer;
1029
+ font-size: 13px;
1030
+ color: var(--text-secondary);
1031
+ ">Graph</button>
1032
+ </div>
1033
+ </header>
1034
+
1035
+ <div class="container">
1036
+ <!-- Sources Panel -->
1037
+ <div class="panel sources-panel" id="sources-panel">
1038
+ <div class="section" id="skills-section">
1039
+ <div class="section-header">
1040
+ <span class="toggle">▼</span>
1041
+ Skills
1042
+ <span class="count" id="skills-count">0</span>
1043
+ </div>
1044
+ <div class="section-items" id="skills-items"></div>
1045
+ </div>
1046
+ <div class="section" id="commands-section">
1047
+ <div class="section-header">
1048
+ <span class="toggle">▼</span>
1049
+ Commands
1050
+ <span class="count" id="commands-count">0</span>
1051
+ </div>
1052
+ <div class="section-items" id="commands-items"></div>
1053
+ </div>
1054
+ <div class="section" id="projects-section">
1055
+ <div class="section-header">
1056
+ <span class="toggle">▼</span>
1057
+ Projects
1058
+ <span class="count" id="projects-count">0</span>
1059
+ </div>
1060
+ <div class="section-items">
1061
+ <div id="project-items"></div>
1062
+ <div class="project-add-form">
1063
+ <div class="project-add-row">
1064
+ <input type="text" id="project-dir-input" placeholder="/path/to/project">
1065
+ <button id="add-project-btn">Add</button>
1066
+ </div>
1067
+ <div class="project-error" id="project-error"></div>
1068
+ </div>
1069
+ </div>
1070
+ </div>
1071
+ </div>
1072
+
1073
+ <!-- Skills Panel -->
1074
+ <div class="panel skills-panel" id="skills-panel">
1075
+ <div class="search-box">
1076
+ <input type="text" id="search-input" placeholder="Search skills...">
1077
+ </div>
1078
+ <div class="tag-filters" id="tag-filters" style="display: none;"></div>
1079
+ <div class="skills-list" id="skills-list">
1080
+ <div class="empty-state">Select a source to view skills</div>
1081
+ </div>
1082
+ </div>
1083
+
1084
+ <!-- Detail Panel -->
1085
+ <div class="panel detail-panel" id="detail-panel">
1086
+ <div class="empty">Select a skill to view details</div>
1087
+ </div>
1088
+ </div>
1089
+
1090
+ <!-- Modal for reference files -->
1091
+ <div class="modal" id="modal">
1092
+ <div class="modal-content">
1093
+ <div class="modal-header">
1094
+ <h3 id="modal-title">Reference File</h3>
1095
+ <span class="modal-close" id="modal-close">×</span>
1096
+ </div>
1097
+ <div class="modal-body" id="modal-body">
1098
+ <pre id="modal-content"></pre>
1099
+ </div>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ <div class="graph-overlay" id="graph-overlay">
1104
+ <div class="graph-controls">
1105
+ <button id="graph-close">Close</button>
1106
+ </div>
1107
+ <svg id="graph-svg"></svg>
1108
+ </div>
1109
+
1110
+ <script>
1111
+ // State
1112
+ let sources = null;
1113
+ let currentSource = null;
1114
+ let currentSkills = [];
1115
+ let currentSkill = null;
1116
+
1117
+
1118
+ // DOM Elements
1119
+ const statusEl = document.getElementById('status');
1120
+ const sourcesPanel = document.getElementById('sources-panel');
1121
+ const skillsList = document.getElementById('skills-list');
1122
+ const detailPanel = document.getElementById('detail-panel');
1123
+ const searchInput = document.getElementById('search-input');
1124
+ const modal = document.getElementById('modal');
1125
+ const modalTitle = document.getElementById('modal-title');
1126
+ const modalContentEl = document.getElementById('modal-content');
1127
+ const modalClose = document.getElementById('modal-close');
1128
+ const globalSearchInput = document.getElementById('global-search-input');
1129
+ const globalSearchResults = document.getElementById('global-search-results');
1130
+ let searchDebounce = null;
1131
+ const graphToggle = document.getElementById('graph-toggle');
1132
+ const graphOverlay = document.getElementById('graph-overlay');
1133
+ const graphClose = document.getElementById('graph-close');
1134
+ const graphSvg = d3.select('#graph-svg');
1135
+ let graphData = null;
1136
+ const tagFiltersEl = document.getElementById('tag-filters');
1137
+ let activeTagFilters = new Set();
1138
+
1139
+ // API calls
1140
+ function encodePathForUrl(p) {
1141
+ return p.split('/').map(s => encodeURIComponent(s)).join('/');
1142
+ }
1143
+
1144
+ async function fetchSources() {
1145
+ const res = await fetch('/api/sources');
1146
+ return res.json();
1147
+ }
1148
+
1149
+ async function fetchSkills(sourcePath) {
1150
+ const res = await fetch(`/api/skills?source=${encodeURIComponent(sourcePath)}`);
1151
+ return res.json();
1152
+ }
1153
+
1154
+ function encodePathForUrl(p) { return p.split('/').map(s => encodeURIComponent(s)).join('/'); }
1155
+
1156
+ async function fetchSkill(skillPath) {
1157
+ const res = await fetch(`/api/skill${encodePathForUrl(skillPath)}`);
1158
+ return res.json();
1159
+ }
1160
+
1161
+ async function fetchReference(refPath) {
1162
+ const res = await fetch(`/api/reference${encodePathForUrl(refPath)}`);
1163
+ return res.json();
1164
+ }
1165
+
1166
+ // Render functions
1167
+ function clearActive() {
1168
+ document.querySelectorAll('.sb-item.active').forEach(el => el.classList.remove('active'));
1169
+ }
1170
+
1171
+ function renderSources(sources) {
1172
+ const skillsItems = document.getElementById('skills-items');
1173
+ const skillsCount = document.getElementById('skills-count');
1174
+ let html = '';
1175
+ let total = 0;
1176
+
1177
+ // Separate single-skill and multi-skill plugins
1178
+ const multiPlugins = [];
1179
+ const soloSkills = [];
1180
+ for (const p of sources.plugins) {
1181
+ const skills = p.skills || [];
1182
+ total += skills.length;
1183
+ if (skills.length > 1) {
1184
+ multiPlugins.push(p);
1185
+ } else if (skills.length === 1) {
1186
+ soloSkills.push({ ...skills[0], sourcePath: p.path });
1187
+ }
1188
+ }
1189
+
1190
+ // Plugins
1191
+ if (multiPlugins.length > 0 || soloSkills.length > 0) {
1192
+ html += `<div class="sb-group" style="pointer-events:none;"><span class="sb-group-name">Plugins</span></div>`;
1193
+ }
1194
+
1195
+ // Multi-skill plugins as groups
1196
+ for (const p of multiPlugins) {
1197
+ const children = p.skills.map(s =>
1198
+ `<div class="sb-item sb-nested sb-plugin" data-path="${escapeHtml(s.path)}" data-source="${escapeHtml(p.path)}" data-type="skill">${escapeHtml(s.name)}</div>`
1199
+ ).join('');
1200
+ html += `
1201
+ <div class="sb-group-wrap" data-path="${escapeHtml(p.path)}">
1202
+ <div class="sb-group">
1203
+ <span class="sb-toggle">▶</span>
1204
+ <span class="sb-group-name">${escapeHtml(p.name)}</span>
1205
+ <span class="sb-group-count">${p.skills.length}</span>
1206
+ </div>
1207
+ <div class="sb-group-children">${children}</div>
1208
+ </div>`;
1209
+ }
1210
+
1211
+ // Solo plugin skills as flat items
1212
+ for (const s of soloSkills) {
1213
+ html += `<div class="sb-item sb-plugin" data-path="${escapeHtml(s.path)}" data-source="${escapeHtml(s.sourcePath)}" data-type="skill">${escapeHtml(s.name)}</div>`;
1214
+ }
1215
+
1216
+ // User skills
1217
+ const customFiles = sources.custom.length > 0 ? (sources.custom[0].files || []) : [];
1218
+ if (customFiles.length > 0) {
1219
+ total += customFiles.length;
1220
+ html += `<div class="sb-group" style="pointer-events:none;"><span class="sb-group-name">User Skills</span></div>`;
1221
+ for (const f of customFiles) {
1222
+ html += `<div class="sb-item sb-nested sb-user" data-path="${escapeHtml(f.path)}" data-source="${escapeHtml(sources.custom[0].path)}" data-type="skill">${escapeHtml(f.name)}</div>`;
1223
+ }
1224
+ }
1225
+
1226
+ skillsCount.textContent = total;
1227
+ skillsItems.innerHTML = html;
1228
+
1229
+ // Commands
1230
+ const commandsItems = document.getElementById('commands-items');
1231
+ const commandsCount = document.getElementById('commands-count');
1232
+ let cmdHtml = '';
1233
+ let cmdTotal = 0;
1234
+ for (const c of sources.commands) {
1235
+ for (const f of (c.files || [])) {
1236
+ cmdTotal++;
1237
+ cmdHtml += `<div class="sb-item" data-path="${escapeHtml(f.path)}" data-source="${escapeHtml(c.path)}" data-type="command">${escapeHtml(f.name)}</div>`;
1238
+ }
1239
+ }
1240
+ commandsCount.textContent = cmdTotal;
1241
+ commandsItems.innerHTML = cmdHtml;
1242
+ document.getElementById('commands-section').style.display = cmdTotal === 0 ? 'none' : '';
1243
+
1244
+ // Projects
1245
+ const projectItems = document.getElementById('project-items');
1246
+ const projectsCount = document.getElementById('projects-count');
1247
+ const projectCount = sources.projects ? sources.projects.length : 0;
1248
+ projectsCount.textContent = projectCount;
1249
+ if (projectCount > 0) {
1250
+ projectItems.innerHTML = sources.projects.map(p => `
1251
+ <div class="sb-item" data-path="${escapeHtml(p.path)}" data-type="project">
1252
+ ${escapeHtml(p.name)}<span class="sb-meta">${p.commandCount + p.skillCount}</span>
1253
+ <button class="remove-project-btn" data-project-dir="${escapeHtml(p.projectDir)}" title="Remove">✕</button>
1254
+ </div>
1255
+ `).join('');
1256
+ } else {
1257
+ projectItems.innerHTML = '';
1258
+ }
1259
+
1260
+ // --- Event handlers ---
1261
+
1262
+ // Group expand/collapse + load middle panel
1263
+ document.querySelectorAll('.sb-group-wrap > .sb-group').forEach(header => {
1264
+ header.addEventListener('click', () => {
1265
+ const wrap = header.closest('.sb-group-wrap');
1266
+ const toggle = header.querySelector('.sb-toggle');
1267
+ const expanded = wrap.classList.toggle('expanded');
1268
+ toggle.textContent = expanded ? '▼' : '▶';
1269
+ selectSource(wrap.dataset.path, header);
1270
+ });
1271
+ });
1272
+
1273
+ // All clickable items
1274
+ document.querySelectorAll('.sb-item').forEach(el => {
1275
+ el.addEventListener('click', async (e) => {
1276
+ if (e.target.classList.contains('remove-project-btn')) return;
1277
+ clearActive();
1278
+ el.classList.add('active');
1279
+ if (el.dataset.type === 'skill' || el.dataset.type === 'command') {
1280
+ const sourcePath = el.dataset.source;
1281
+ if (sourcePath && sourcePath !== currentSource) {
1282
+ await selectSource(sourcePath, el);
1283
+ }
1284
+ selectSkill(el.dataset.path);
1285
+ scrollMiddleToSkill(el.dataset.path);
1286
+ } else {
1287
+ selectSource(el.dataset.path, el);
1288
+ }
1289
+ });
1290
+ });
1291
+
1292
+ // Remove project
1293
+ document.querySelectorAll('.remove-project-btn').forEach(btn => {
1294
+ btn.addEventListener('click', async (e) => {
1295
+ e.stopPropagation();
1296
+ const res = await fetch('/api/projects', {
1297
+ method: 'DELETE',
1298
+ headers: { 'Content-Type': 'application/json' },
1299
+ body: JSON.stringify({ path: btn.dataset.projectDir }),
1300
+ });
1301
+ const data = await res.json();
1302
+ if (data.ok) renderSources(data.sources);
1303
+ });
1304
+ });
1305
+
1306
+ // Section toggles
1307
+ document.querySelectorAll('.section-header').forEach(el => {
1308
+ el.addEventListener('click', () => {
1309
+ const items = el.nextElementSibling;
1310
+ const toggle = el.querySelector('.toggle');
1311
+ if (items.style.display === 'none') {
1312
+ items.style.display = 'block';
1313
+ toggle.textContent = '▼';
1314
+ } else {
1315
+ items.style.display = 'none';
1316
+ toggle.textContent = '▶';
1317
+ }
1318
+ });
1319
+ });
1320
+ }
1321
+
1322
+ function getRecencyBadge(health) {
1323
+ if (!health || !health.age_days) return '';
1324
+ const days = health.age_days;
1325
+ if (days < 14) return '<span class="badge recency-fresh">' + Math.round(days) + 'd ago</span>';
1326
+ if (days < 60) return '<span class="badge recency-aging">' + Math.round(days) + 'd ago</span>';
1327
+ return '<span class="badge recency-stale">' + Math.round(days) + 'd ago</span>';
1328
+ }
1329
+
1330
+ function getHealthBadges(skill) {
1331
+ if (!skill.health) return '';
1332
+ let badges = getRecencyBadge(skill.health);
1333
+
1334
+ if (skill.health.word_count && skill.health.word_count > 1000) {
1335
+ badges += `<span class="badge tag">${skill.health.word_count} words</span>`;
1336
+ }
1337
+
1338
+ if (skill.structural_tags) {
1339
+ skill.structural_tags.forEach(tag => {
1340
+ const label = tag.replace('has-', '').replace('-', ' ');
1341
+ badges += `<span class="badge tag">${escapeHtml(label)}</span>`;
1342
+ });
1343
+ }
1344
+
1345
+ if (skill.health.completeness_gaps) {
1346
+ skill.health.completeness_gaps.forEach(gap => {
1347
+ const label = gap.replace('-', ' ');
1348
+ badges += `<span class="badge gap">${escapeHtml(label)}</span>`;
1349
+ });
1350
+ }
1351
+
1352
+ return badges ? `<div class="badges">${badges}</div>` : '';
1353
+ }
1354
+
1355
+ function renderSkills(skills, filter = '') {
1356
+ const filtered = filter
1357
+ ? skills.filter(s =>
1358
+ s.name.toLowerCase().includes(filter.toLowerCase()) ||
1359
+ s.description.toLowerCase().includes(filter.toLowerCase())
1360
+ )
1361
+ : skills;
1362
+
1363
+ if (filtered.length === 0) {
1364
+ skillsList.innerHTML = `<div class="empty-state">No skills found</div>`;
1365
+ return;
1366
+ }
1367
+
1368
+ skillsList.innerHTML = filtered.map(s => {
1369
+ const treeDir = s.skill_dir || s.path.substring(0, s.path.lastIndexOf('/'));
1370
+ return `
1371
+ <div class="skill-item" data-path="${escapeHtml(s.path)}" data-tree-dir="${escapeHtml(treeDir)}">
1372
+ <div class="skill-header">
1373
+ <span class="skill-toggle">&#9654;</span>
1374
+ <div class="skill-info">
1375
+ <div class="name">${escapeHtml(s.name)}</div>
1376
+ <div class="description">${escapeHtml(s.description || 'No description')}</div>
1377
+ ${getHealthBadges(s)}
1378
+ </div>
1379
+ </div>
1380
+ <div class="inline-file-tree"></div>
1381
+ </div>
1382
+ `}).join('');
1383
+
1384
+ bindSkillItemHandlers();
1385
+ }
1386
+
1387
+ function extractAvailableTags(skills) {
1388
+ const tags = new Map(); // tag -> count
1389
+ skills.forEach(s => {
1390
+ if (s.tool_references) {
1391
+ s.tool_references.forEach(t => {
1392
+ tags.set(`tool:${t}`, (tags.get(`tool:${t}`) || 0) + 1);
1393
+ });
1394
+ }
1395
+ if (s.structural_tags) {
1396
+ s.structural_tags.forEach(t => {
1397
+ tags.set(t, (tags.get(t) || 0) + 1);
1398
+ });
1399
+ }
1400
+ });
1401
+ return [...tags.entries()]
1402
+ .filter(([, count]) => count >= 2)
1403
+ .sort((a, b) => b[1] - a[1])
1404
+ .slice(0, 15);
1405
+ }
1406
+
1407
+ function renderTagFilters(skills) {
1408
+ const tags = extractAvailableTags(skills);
1409
+ if (tags.length === 0) {
1410
+ tagFiltersEl.style.display = 'none';
1411
+ return;
1412
+ }
1413
+
1414
+ tagFiltersEl.style.display = 'flex';
1415
+ tagFiltersEl.innerHTML = tags.map(([tag, count]) => {
1416
+ const label = tag.replace('has-', '').replace('tool:', '');
1417
+ const isActive = activeTagFilters.has(tag);
1418
+ return `<span class="tag-filter ${isActive ? 'active' : ''}" data-tag="${escapeHtml(tag)}">${escapeHtml(label)} (${count})</span>`;
1419
+ }).join('');
1420
+ }
1421
+
1422
+ function bindSkillItemHandlers() {
1423
+ document.querySelectorAll('.skill-item').forEach(el => {
1424
+ // Clicking the toggle expands/collapses the inline file tree
1425
+ const toggle = el.querySelector('.skill-toggle');
1426
+ const treeContainer = el.querySelector('.inline-file-tree');
1427
+
1428
+ toggle.addEventListener('click', async (e) => {
1429
+ e.stopPropagation();
1430
+ const isExpanded = treeContainer.classList.contains('expanded');
1431
+
1432
+ if (isExpanded) {
1433
+ treeContainer.classList.remove('expanded');
1434
+ toggle.innerHTML = '&#9654;';
1435
+ } else {
1436
+ treeContainer.classList.add('expanded');
1437
+ toggle.innerHTML = '&#9660;';
1438
+
1439
+ // Load tree if not already loaded
1440
+ if (!treeContainer.dataset.loaded) {
1441
+ treeContainer.innerHTML = '<div class="loading" style="padding:8px;font-size:12px;">Loading...</div>';
1442
+ try {
1443
+ const dirPath = el.dataset.treeDir;
1444
+ const res = await fetch(`/api/skill-tree${encodePathForUrl(dirPath)}`);
1445
+ const data = await res.json();
1446
+ if (data.tree && data.tree.length > 0) {
1447
+ treeContainer.innerHTML = renderFileTree(data.tree);
1448
+ // Bind inline tree click handlers
1449
+ treeContainer.querySelectorAll('.file-tree-item.directory').forEach(dir => {
1450
+ dir.addEventListener('click', (ev) => {
1451
+ ev.stopPropagation();
1452
+ const childrenId = dir.dataset.childrenId;
1453
+ const childrenEl = document.getElementById(childrenId);
1454
+ if (childrenEl) {
1455
+ childrenEl.classList.toggle('expanded');
1456
+ dir.querySelector('.icon').textContent = childrenEl.classList.contains('expanded') ? '\u{1F4C2}' : '\u{1F4C1}';
1457
+ }
1458
+ });
1459
+ });
1460
+ treeContainer.querySelectorAll('.file-tree-item.file').forEach(file => {
1461
+ file.addEventListener('click', async (ev) => {
1462
+ ev.stopPropagation();
1463
+ const filePath = file.dataset.treeFilePath;
1464
+ const fileName = file.querySelector('.name').textContent;
1465
+ detailPanel.innerHTML = '<div class="loading">Loading file...</div>';
1466
+ try {
1467
+ const ref = await fetchReference(filePath);
1468
+ detailPanel.innerHTML = `
1469
+ <div class="detail-content">
1470
+ <div class="detail-header">
1471
+ <h2>${escapeHtml(ref.name)}</h2>
1472
+ <div class="path">${escapeHtml(ref.path)}</div>
1473
+ </div>
1474
+ <div class="content-preview">
1475
+ ${renderFileContent(ref.path, ref.content)}
1476
+ </div>
1477
+ </div>
1478
+ `;
1479
+ highlightDetailPanel();
1480
+ statusEl.textContent = `Viewing: ${ref.name}`;
1481
+ } catch (err) {
1482
+ detailPanel.innerHTML = `
1483
+ <div class="detail-content">
1484
+ <div class="detail-header">
1485
+ <h2>${escapeHtml(fileName)}</h2>
1486
+ </div>
1487
+ <div class="error">Unable to display file content</div>
1488
+ </div>
1489
+ `;
1490
+ }
1491
+ });
1492
+ });
1493
+ } else {
1494
+ treeContainer.innerHTML = '<div style="padding:8px;font-size:12px;color:var(--text-muted)">No files</div>';
1495
+ }
1496
+ treeContainer.dataset.loaded = 'true';
1497
+ } catch (err) {
1498
+ treeContainer.innerHTML = '<div style="padding:8px;font-size:12px;color:#dc3545">Error loading files</div>';
1499
+ }
1500
+ }
1501
+ }
1502
+ });
1503
+
1504
+ // Clicking the header (not toggle) selects the skill in detail panel
1505
+ el.querySelector('.skill-info').addEventListener('click', (e) => {
1506
+ e.stopPropagation();
1507
+ selectSkill(el.dataset.path, el);
1508
+ });
1509
+ });
1510
+ }
1511
+
1512
+ function applyFilters() {
1513
+ const textFilter = searchInput.value.toLowerCase();
1514
+ let filtered = currentSkills;
1515
+
1516
+ if (textFilter) {
1517
+ filtered = filtered.filter(s =>
1518
+ s.name.toLowerCase().includes(textFilter) ||
1519
+ (s.description || '').toLowerCase().includes(textFilter)
1520
+ );
1521
+ }
1522
+
1523
+ if (activeTagFilters.size > 0) {
1524
+ filtered = filtered.filter(s => {
1525
+ const skillTags = new Set([
1526
+ ...(s.tool_references || []).map(t => `tool:${t}`),
1527
+ ...(s.structural_tags || []),
1528
+ ]);
1529
+ return [...activeTagFilters].every(tag => skillTags.has(tag));
1530
+ });
1531
+ }
1532
+
1533
+ if (filtered.length === 0) {
1534
+ skillsList.innerHTML = '<div class="empty-state">No skills match filters</div>';
1535
+ } else {
1536
+ skillsList.innerHTML = filtered.map(s => {
1537
+ const treeDir = s.skill_dir || s.path.substring(0, s.path.lastIndexOf('/'));
1538
+ return `
1539
+ <div class="skill-item" data-path="${escapeHtml(s.path)}" data-tree-dir="${escapeHtml(treeDir)}">
1540
+ <div class="skill-header">
1541
+ <span class="skill-toggle">&#9654;</span>
1542
+ <div class="skill-info">
1543
+ <div class="name">${escapeHtml(s.name)}</div>
1544
+ <div class="description">${escapeHtml(s.description || 'No description')}</div>
1545
+ ${getHealthBadges(s)}
1546
+ </div>
1547
+ </div>
1548
+ <div class="inline-file-tree"></div>
1549
+ </div>
1550
+ `}).join('');
1551
+ bindSkillItemHandlers();
1552
+ }
1553
+
1554
+ renderTagFilters(currentSkills);
1555
+ }
1556
+
1557
+ tagFiltersEl.addEventListener('click', (e) => {
1558
+ const filter = e.target.closest('.tag-filter');
1559
+ if (!filter) return;
1560
+
1561
+ const tag = filter.dataset.tag;
1562
+ if (activeTagFilters.has(tag)) {
1563
+ activeTagFilters.delete(tag);
1564
+ } else {
1565
+ activeTagFilters.add(tag);
1566
+ }
1567
+
1568
+ applyFilters();
1569
+ });
1570
+
1571
+ // Top-level simple fields worth showing as key-value rows
1572
+ const METADATA_KEYS = ['name', 'description', 'argument-hint'];
1573
+
1574
+ function renderSkillDetail(skill) {
1575
+ // Metadata: clean key-value rows for simple top-level fields
1576
+ const metadataEntries = METADATA_KEYS
1577
+ .filter(k => skill.frontmatter[k] && skill.frontmatter[k].trim())
1578
+ .map(k => [k, skill.frontmatter[k]]);
1579
+
1580
+ const metadataHtml = metadataEntries.map(([k, v]) => `
1581
+ <div class="frontmatter-row">
1582
+ <div class="key">${escapeHtml(k)}</div>
1583
+ <div class="value">${escapeHtml(v)}</div>
1584
+ </div>
1585
+ `).join('');
1586
+
1587
+ // Raw YAML: show full frontmatter as syntax-highlighted YAML
1588
+ const rawYaml = skill.frontmatter_raw || '';
1589
+ const hasConfig = rawYaml && rawYaml.trim().split('\n').length > metadataEntries.length;
1590
+
1591
+ detailPanel.innerHTML = `
1592
+ <div class="detail-content">
1593
+ <div class="detail-header">
1594
+ <h2>${escapeHtml(skill.name)}</h2>
1595
+ <div class="description">${escapeHtml(skill.description || 'No description')}</div>
1596
+ <div class="path">${escapeHtml(skill.path)}</div>
1597
+ </div>
1598
+
1599
+ <div class="section-title">Metadata</div>
1600
+ <div class="frontmatter-table">
1601
+ ${metadataHtml || '<div class="empty-state">No metadata</div>'}
1602
+ </div>
1603
+
1604
+ ${hasConfig ? `
1605
+ <div class="section-title">Configuration</div>
1606
+ <div class="content-preview">
1607
+ <pre><code class="language-yaml">${escapeHtml(rawYaml)}</code></pre>
1608
+ </div>
1609
+ ` : ''}
1610
+
1611
+ <div class="section-title">Content</div>
1612
+ <div class="content-preview">
1613
+ <pre>${escapeHtml(skill.content)}</pre>
1614
+ </div>
1615
+ </div>
1616
+ `;
1617
+
1618
+ highlightDetailPanel();
1619
+ }
1620
+
1621
+ const FILE_ICONS = {
1622
+ '.md': '📄', '.py': '🐍', '.sh': '⚙️', '.json': '📋',
1623
+ '.html': '🌐', '.yaml': '📋', '.yml': '📋', '.txt': '📝',
1624
+ '.png': '🖼️', '.jpg': '🖼️', '.svg': '🖼️',
1625
+ };
1626
+
1627
+ function getFileIcon(entry) {
1628
+ if (entry.type === 'directory') return '📁';
1629
+ return FILE_ICONS[entry.extension] || '📄';
1630
+ }
1631
+
1632
+ function formatFileSize(bytes) {
1633
+ if (bytes < 1024) return `${bytes} B`;
1634
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1635
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1636
+ }
1637
+
1638
+ function renderFileTree(tree, depth = 0) {
1639
+ return tree.map(entry => {
1640
+ const icon = getFileIcon(entry);
1641
+ const indent = '<span class="indent"></span>'.repeat(depth);
1642
+ const sizeStr = entry.size != null ? `<span class="size">${formatFileSize(entry.size)}</span>` : '';
1643
+
1644
+ if (entry.type === 'directory') {
1645
+ const childrenId = `tree-${entry.path.replace(/[^a-zA-Z0-9]/g, '-')}`;
1646
+ return `
1647
+ <div class="file-tree-item directory" data-tree-path="${escapeHtml(entry.path)}" data-children-id="${childrenId}">
1648
+ ${indent}
1649
+ <span class="icon">${icon}</span>
1650
+ <span class="name">${escapeHtml(entry.name)}/</span>
1651
+ </div>
1652
+ <div class="file-tree-children" id="${childrenId}">
1653
+ ${entry.children ? renderFileTree(entry.children, depth + 1) : ''}
1654
+ </div>
1655
+ `;
1656
+ } else {
1657
+ return `
1658
+ <div class="file-tree-item file" data-tree-file-path="${escapeHtml(entry.path)}" data-ext="${escapeHtml(entry.extension || '')}">
1659
+ ${indent}
1660
+ <span class="icon">${icon}</span>
1661
+ <span class="name">${escapeHtml(entry.name)}</span>
1662
+ ${sizeStr}
1663
+ </div>
1664
+ `;
1665
+ }
1666
+ }).join('');
1667
+ }
1668
+
1669
+ function escapeHtml(text) {
1670
+ const div = document.createElement('div');
1671
+ div.textContent = text;
1672
+ return div.innerHTML;
1673
+ }
1674
+
1675
+ const EXT_TO_LANG = {
1676
+ '.py': 'python', '.js': 'javascript', '.ts': 'typescript',
1677
+ '.sh': 'bash', '.bash': 'bash', '.zsh': 'bash',
1678
+ '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
1679
+ '.html': 'xml', '.xml': 'xml', '.css': 'css',
1680
+ '.md': 'markdown', '.sql': 'sql', '.rb': 'ruby',
1681
+ '.go': 'go', '.rs': 'rust', '.java': 'java',
1682
+ '.c': 'c', '.cpp': 'cpp', '.h': 'c',
1683
+ '.toml': 'ini', '.ini': 'ini', '.cfg': 'ini',
1684
+ };
1685
+
1686
+ function getLangFromPath(filePath) {
1687
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
1688
+ return EXT_TO_LANG[ext] || null;
1689
+ }
1690
+
1691
+ function renderFileContent(filePath, content) {
1692
+ const lang = getLangFromPath(filePath);
1693
+ if (lang) {
1694
+ const escaped = escapeHtml(content);
1695
+ return `<pre><code class="language-${lang}">${escaped}</code></pre>`;
1696
+ }
1697
+ return `<pre>${escapeHtml(content)}</pre>`;
1698
+ }
1699
+
1700
+ function highlightDetailPanel() {
1701
+ detailPanel.querySelectorAll('pre code').forEach(block => {
1702
+ hljs.highlightElement(block);
1703
+ });
1704
+ }
1705
+
1706
+ // Event handlers
1707
+ function scrollMiddleToSkill(skillPath) {
1708
+ const item = document.querySelector(`.skill-item[data-path="${CSS.escape(skillPath)}"]`);
1709
+ if (item) {
1710
+ item.scrollIntoView({ behavior: 'smooth', block: 'center' });
1711
+ document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active'));
1712
+ item.classList.add('active');
1713
+ }
1714
+ }
1715
+
1716
+ async function selectSource(path, el) {
1717
+ // Update active state
1718
+ document.querySelectorAll('.source-item').forEach(e => e.classList.remove('active'));
1719
+ el.classList.add('active');
1720
+
1721
+ currentSource = path;
1722
+ currentSkill = null;
1723
+
1724
+ // Show loading
1725
+ skillsList.innerHTML = '<div class="loading">Loading skills...</div>';
1726
+ detailPanel.innerHTML = '<div class="empty">Select a skill to view details</div>';
1727
+
1728
+ // Fetch skills
1729
+ try {
1730
+ currentSkills = await fetchSkills(path);
1731
+ activeTagFilters.clear();
1732
+ renderSkills(currentSkills);
1733
+ renderTagFilters(currentSkills);
1734
+ statusEl.textContent = `${currentSkills.length} skills loaded`;
1735
+ } catch (err) {
1736
+ skillsList.innerHTML = `<div class="error">Error loading skills: ${escapeHtml(err.message)}</div>`;
1737
+ }
1738
+ }
1739
+
1740
+ async function selectSkill(path, el) {
1741
+ // Update active state
1742
+ document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active'));
1743
+ if (el) el.classList.add('active');
1744
+
1745
+ currentSkill = path;
1746
+
1747
+ // Show loading
1748
+ detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
1749
+
1750
+ // Fetch skill
1751
+ try {
1752
+ const skill = await fetchSkill(path);
1753
+ renderSkillDetail(skill);
1754
+ statusEl.textContent = `Viewing: ${skill.name}`;
1755
+ } catch (err) {
1756
+ detailPanel.innerHTML = `<div class="error">Error loading skill: ${escapeHtml(err.message)}</div>`;
1757
+ }
1758
+ }
1759
+
1760
+ // Search handler
1761
+ searchInput.addEventListener('input', () => applyFilters());
1762
+
1763
+ // Modal handlers
1764
+ modalClose.addEventListener('click', () => {
1765
+ modal.classList.remove('active');
1766
+ });
1767
+
1768
+ modal.addEventListener('click', (e) => {
1769
+ if (e.target === modal) {
1770
+ modal.classList.remove('active');
1771
+ }
1772
+ });
1773
+
1774
+ // Reference click handler (delegated)
1775
+ detailPanel.addEventListener('click', async (e) => {
1776
+ const treeDir = e.target.closest('.file-tree-item.directory');
1777
+ const treeFile = e.target.closest('.file-tree-item.file');
1778
+ const refItem = e.target.closest('.reference-item');
1779
+
1780
+ if (treeDir) {
1781
+ const childrenId = treeDir.dataset.childrenId;
1782
+ const childrenEl = document.getElementById(childrenId);
1783
+ if (childrenEl) {
1784
+ childrenEl.classList.toggle('expanded');
1785
+ const iconEl = treeDir.querySelector('.icon');
1786
+ iconEl.textContent = childrenEl.classList.contains('expanded') ? '📂' : '📁';
1787
+ }
1788
+ } else if (treeFile) {
1789
+ const filePath = treeFile.dataset.treeFilePath;
1790
+ const fileName = treeFile.querySelector('.name').textContent;
1791
+ detailPanel.innerHTML = '<div class="loading">Loading file...</div>';
1792
+ try {
1793
+ const ref = await fetchReference(filePath);
1794
+ detailPanel.innerHTML = `
1795
+ <div class="detail-content">
1796
+ <div class="detail-header">
1797
+ <h2>${escapeHtml(ref.name)}</h2>
1798
+ <div class="path">${escapeHtml(ref.path)}</div>
1799
+ </div>
1800
+ <div class="content-preview">
1801
+ ${renderFileContent(ref.path, ref.content)}
1802
+ </div>
1803
+ </div>
1804
+ `;
1805
+ highlightDetailPanel();
1806
+ statusEl.textContent = `Viewing: ${ref.name}`;
1807
+ } catch (err) {
1808
+ detailPanel.innerHTML = `
1809
+ <div class="detail-content">
1810
+ <div class="detail-header">
1811
+ <h2>${escapeHtml(fileName)}</h2>
1812
+ </div>
1813
+ <div class="error">Unable to display file content</div>
1814
+ </div>
1815
+ `;
1816
+ }
1817
+ } else if (refItem) {
1818
+ const path = refItem.dataset.path;
1819
+ try {
1820
+ const ref = await fetchReference(path);
1821
+ detailPanel.innerHTML = `
1822
+ <div class="detail-content">
1823
+ <div class="detail-header">
1824
+ <h2>${escapeHtml(ref.name)}</h2>
1825
+ <div class="path">${escapeHtml(ref.path)}</div>
1826
+ </div>
1827
+ <div class="content-preview">
1828
+ ${renderFileContent(ref.path, ref.content)}
1829
+ </div>
1830
+ </div>
1831
+ `;
1832
+ highlightDetailPanel();
1833
+ statusEl.textContent = `Viewing: ${ref.name}`;
1834
+ } catch (err) {
1835
+ detailPanel.innerHTML = `<div class="error">Error loading reference: ${escapeHtml(err.message)}</div>`;
1836
+ }
1837
+ }
1838
+ });
1839
+
1840
+ // Global search
1841
+ globalSearchInput.addEventListener('input', (e) => {
1842
+ clearTimeout(searchDebounce);
1843
+ const query = e.target.value.trim();
1844
+
1845
+ if (query.length < 2) {
1846
+ globalSearchResults.classList.remove('active');
1847
+ return;
1848
+ }
1849
+
1850
+ searchDebounce = setTimeout(async () => {
1851
+ try {
1852
+ const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
1853
+ const results = await res.json();
1854
+ renderGlobalSearchResults(results);
1855
+ } catch (err) {
1856
+ globalSearchResults.innerHTML = `<div class="search-result-item"><div class="result-name">Error: ${escapeHtml(err.message)}</div></div>`;
1857
+ globalSearchResults.classList.add('active');
1858
+ }
1859
+ }, 250);
1860
+ });
1861
+
1862
+ function renderGlobalSearchResults(results) {
1863
+ if (results.length === 0) {
1864
+ globalSearchResults.innerHTML = '<div class="search-result-item"><div class="result-name" style="color: var(--text-muted)">No results</div></div>';
1865
+ } else {
1866
+ globalSearchResults.innerHTML = results.slice(0, 20).map(r => `
1867
+ <div class="search-result-item" data-path="${escapeHtml(r.path)}">
1868
+ <div class="result-name">${escapeHtml(r.name)}</div>
1869
+ <div class="result-source">${escapeHtml(r.source_name)} &middot; ${r.source_type}</div>
1870
+ <div class="result-snippet">${escapeHtml(r.snippet)}</div>
1871
+ </div>
1872
+ `).join('');
1873
+ }
1874
+ globalSearchResults.classList.add('active');
1875
+ }
1876
+
1877
+ // Click on search result -> load that skill
1878
+ globalSearchResults.addEventListener('click', async (e) => {
1879
+ const item = e.target.closest('.search-result-item');
1880
+ if (!item || !item.dataset.path) return;
1881
+
1882
+ globalSearchResults.classList.remove('active');
1883
+ globalSearchInput.value = '';
1884
+
1885
+ detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
1886
+ try {
1887
+ const skill = await fetchSkill(item.dataset.path);
1888
+ renderSkillDetail(skill);
1889
+ currentSkill = item.dataset.path;
1890
+ statusEl.textContent = `Viewing: ${skill.name}`;
1891
+ } catch (err) {
1892
+ detailPanel.innerHTML = `<div class="error">Error: ${escapeHtml(err.message)}</div>`;
1893
+ }
1894
+ });
1895
+
1896
+ // Close search results when clicking outside
1897
+ document.addEventListener('click', (e) => {
1898
+ if (!e.target.closest('.global-search')) {
1899
+ globalSearchResults.classList.remove('active');
1900
+ }
1901
+ });
1902
+
1903
+ // Keyboard shortcut: Cmd+K to focus global search
1904
+ document.addEventListener('keydown', (e) => {
1905
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1906
+ e.preventDefault();
1907
+ globalSearchInput.focus();
1908
+ globalSearchInput.select();
1909
+ }
1910
+ });
1911
+
1912
+ // Graph overlay
1913
+ const SOURCE_COLORS = {
1914
+ plugin: '#0066cc',
1915
+ custom: '#28a745',
1916
+ command: '#f0ad4e',
1917
+ };
1918
+
1919
+ graphToggle.addEventListener('click', async () => {
1920
+ if (graphOverlay.classList.contains('active')) {
1921
+ graphOverlay.classList.remove('active');
1922
+ return;
1923
+ }
1924
+
1925
+ graphOverlay.classList.add('active');
1926
+
1927
+ if (!graphData) {
1928
+ try {
1929
+ const res = await fetch('/api/graph');
1930
+ graphData = await res.json();
1931
+ } catch (err) {
1932
+ graphOverlay.innerHTML = `<div class="error" style="margin:20px">Error loading graph: ${escapeHtml(err.message)}</div>`;
1933
+ return;
1934
+ }
1935
+ }
1936
+
1937
+ renderGraph(graphData);
1938
+ });
1939
+
1940
+ graphClose.addEventListener('click', () => {
1941
+ graphOverlay.classList.remove('active');
1942
+ });
1943
+
1944
+ function renderGraph(data) {
1945
+ graphSvg.selectAll('*').remove();
1946
+
1947
+ const width = graphOverlay.clientWidth;
1948
+ const height = graphOverlay.clientHeight;
1949
+
1950
+ const svg = graphSvg
1951
+ .attr('width', width)
1952
+ .attr('height', height);
1953
+
1954
+ // Create a container group for zoom/pan
1955
+ const container = svg.append('g');
1956
+
1957
+ // Arrow marker
1958
+ svg.append('defs').append('marker')
1959
+ .attr('id', 'arrowhead')
1960
+ .attr('viewBox', '0 -5 10 10')
1961
+ .attr('refX', 20)
1962
+ .attr('refY', 0)
1963
+ .attr('markerWidth', 6)
1964
+ .attr('markerHeight', 6)
1965
+ .attr('orient', 'auto')
1966
+ .append('path')
1967
+ .attr('d', 'M0,-5L10,0L0,5')
1968
+ .attr('class', 'graph-link-arrow');
1969
+
1970
+ // Deep clone data to avoid d3 mutation issues on re-render
1971
+ const nodes = data.nodes.map(n => ({...n}));
1972
+ const edges = data.edges.map(e => ({...e}));
1973
+
1974
+ const simulation = d3.forceSimulation(nodes)
1975
+ .force('link', d3.forceLink(edges).id(d => d.id).distance(120))
1976
+ .force('charge', d3.forceManyBody().strength(-300))
1977
+ .force('center', d3.forceCenter(width / 2, height / 2))
1978
+ .force('collision', d3.forceCollide().radius(40));
1979
+
1980
+ const link = container.append('g')
1981
+ .selectAll('line')
1982
+ .data(edges)
1983
+ .join('line')
1984
+ .attr('class', 'graph-link')
1985
+ .attr('marker-end', 'url(#arrowhead)');
1986
+
1987
+ const node = container.append('g')
1988
+ .selectAll('g')
1989
+ .data(nodes)
1990
+ .join('g')
1991
+ .attr('class', 'graph-node')
1992
+ .call(d3.drag()
1993
+ .on('start', (event, d) => {
1994
+ if (!event.active) simulation.alphaTarget(0.3).restart();
1995
+ d.fx = d.x;
1996
+ d.fy = d.y;
1997
+ })
1998
+ .on('drag', (event, d) => {
1999
+ d.fx = event.x;
2000
+ d.fy = event.y;
2001
+ })
2002
+ .on('end', (event, d) => {
2003
+ if (!event.active) simulation.alphaTarget(0);
2004
+ d.fx = null;
2005
+ d.fy = null;
2006
+ })
2007
+ );
2008
+
2009
+ // Size nodes by word count
2010
+ node.append('circle')
2011
+ .attr('r', d => Math.max(6, Math.min(18, Math.sqrt(d.word_count || 100) / 2)))
2012
+ .attr('fill', d => SOURCE_COLORS[d.source_type] || '#999');
2013
+
2014
+ node.append('text')
2015
+ .attr('dx', 14)
2016
+ .attr('dy', 4)
2017
+ .text(d => d.name);
2018
+
2019
+ // Click node to navigate to skill
2020
+ node.on('click', async (event, d) => {
2021
+ graphOverlay.classList.remove('active');
2022
+ detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
2023
+ try {
2024
+ const skill = await fetchSkill(d.id);
2025
+ renderSkillDetail(skill);
2026
+ currentSkill = d.id;
2027
+ statusEl.textContent = `Viewing: ${skill.name}`;
2028
+ } catch (err) {
2029
+ detailPanel.innerHTML = `<div class="error">Error: ${escapeHtml(err.message)}</div>`;
2030
+ }
2031
+ });
2032
+
2033
+ // Tooltip on hover
2034
+ node.append('title')
2035
+ .text(d => `${d.name}\n${d.source_name} (${d.source_type})\n${d.word_count || '?'} words`);
2036
+
2037
+ simulation.on('tick', () => {
2038
+ link
2039
+ .attr('x1', d => d.source.x)
2040
+ .attr('y1', d => d.source.y)
2041
+ .attr('x2', d => d.target.x)
2042
+ .attr('y2', d => d.target.y);
2043
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
2044
+ });
2045
+
2046
+ // Zoom - apply transform to container group only
2047
+ const zoom = d3.zoom()
2048
+ .scaleExtent([0.2, 4])
2049
+ .on('zoom', (event) => {
2050
+ container.attr('transform', event.transform);
2051
+ });
2052
+ svg.call(zoom);
2053
+ }
2054
+
2055
+ // Add project handler
2056
+ document.getElementById('add-project-btn').addEventListener('click', async () => {
2057
+ const input = document.getElementById('project-dir-input');
2058
+ const errorEl = document.getElementById('project-error');
2059
+ const dir = input.value.trim();
2060
+ if (!dir) return;
2061
+ errorEl.style.display = 'none';
2062
+ try {
2063
+ const res = await fetch('/api/projects', {
2064
+ method: 'POST',
2065
+ headers: { 'Content-Type': 'application/json' },
2066
+ body: JSON.stringify({ path: dir }),
2067
+ });
2068
+ const data = await res.json();
2069
+ if (!res.ok) {
2070
+ errorEl.textContent = data.error;
2071
+ errorEl.style.display = 'block';
2072
+ return;
2073
+ }
2074
+ input.value = '';
2075
+ renderSources(data.sources);
2076
+ } catch (err) {
2077
+ errorEl.textContent = err.message;
2078
+ errorEl.style.display = 'block';
2079
+ }
2080
+ });
2081
+ document.getElementById('project-dir-input').addEventListener('keydown', (e) => {
2082
+ if (e.key === 'Enter') document.getElementById('add-project-btn').click();
2083
+ });
2084
+
2085
+ // Agent switcher
2086
+ const agentDropdown = document.getElementById('agent-dropdown');
2087
+ const agentToggle = document.getElementById('agent-dropdown-toggle');
2088
+ const agentMenu = document.getElementById('agent-dropdown-menu');
2089
+ const agentLabel = document.getElementById('agent-dropdown-label');
2090
+ let activeAgentId = null;
2091
+
2092
+ agentToggle.addEventListener('click', (e) => {
2093
+ e.stopPropagation();
2094
+ agentDropdown.classList.toggle('open');
2095
+ });
2096
+
2097
+ document.addEventListener('click', () => {
2098
+ agentDropdown.classList.remove('open');
2099
+ });
2100
+
2101
+ async function loadAgents() {
2102
+ try {
2103
+ const res = await fetch('/api/agents');
2104
+ const data = await res.json();
2105
+ activeAgentId = data.active_id;
2106
+ const active = data.agents.find(a => a.id === data.active_id);
2107
+ if (active) agentLabel.textContent = active.name;
2108
+ agentMenu.innerHTML = data.agents.map(a =>
2109
+ `<div class="agent-dropdown-item ${a.id === data.active_id ? 'active' : ''}" data-id="${escapeHtml(a.id)}">${escapeHtml(a.name)}</div>`
2110
+ ).join('');
2111
+ agentMenu.querySelectorAll('.agent-dropdown-item').forEach(item => {
2112
+ item.addEventListener('click', () => switchAgent(item.dataset.id));
2113
+ });
2114
+ } catch (err) {
2115
+ console.error('Failed to load agents:', err);
2116
+ }
2117
+ }
2118
+
2119
+ async function switchAgent(agentId) {
2120
+ if (agentId === activeAgentId) {
2121
+ agentDropdown.classList.remove('open');
2122
+ return;
2123
+ }
2124
+ agentDropdown.classList.remove('open');
2125
+ statusEl.textContent = 'Switching agent...';
2126
+ currentSource = null;
2127
+ currentSkills = [];
2128
+ currentSkill = null;
2129
+ graphData = null;
2130
+
2131
+ try {
2132
+ const res = await fetch('/api/agent', {
2133
+ method: 'POST',
2134
+ headers: { 'Content-Type': 'application/json' },
2135
+ body: JSON.stringify({ id: agentId }),
2136
+ });
2137
+ const data = await res.json();
2138
+ if (data.ok) {
2139
+ activeAgentId = agentId;
2140
+ agentLabel.textContent = agentMenu.querySelector(`[data-id="${agentId}"]`).textContent;
2141
+ agentMenu.querySelectorAll('.agent-dropdown-item').forEach(el => {
2142
+ el.classList.toggle('active', el.dataset.id === agentId);
2143
+ });
2144
+ sources = data.sources;
2145
+ renderSources(sources);
2146
+ skillsList.innerHTML = '<div class="empty-state">Select a source</div>';
2147
+ detailPanel.innerHTML = '<div class="empty">Select a skill to view details</div>';
2148
+ statusEl.textContent = 'Ready';
2149
+ } else {
2150
+ statusEl.textContent = `Error: ${data.error}`;
2151
+ }
2152
+ } catch (err) {
2153
+ statusEl.textContent = `Error: ${err.message}`;
2154
+ }
2155
+ }
2156
+
2157
+ // Initialize
2158
+ async function init() {
2159
+ try {
2160
+ await loadAgents();
2161
+ sources = await fetchSources();
2162
+ renderSources(sources);
2163
+ statusEl.textContent = 'Ready';
2164
+ } catch (err) {
2165
+ statusEl.textContent = `Error: ${err.message}`;
2166
+ sourcesPanel.innerHTML = `<div class="error">Error loading sources: ${escapeHtml(err.message)}</div>`;
2167
+ }
2168
+ }
2169
+
2170
+ // WebSocket for live reload
2171
+ function connectWebSocket() {
2172
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
2173
+ const ws = new WebSocket(`${protocol}//${location.host}/ws`);
2174
+
2175
+ ws.onmessage = (event) => {
2176
+ const data = JSON.parse(event.data);
2177
+ if (data.type === 'skill_changed') {
2178
+ console.log('Skill changed:', data.path);
2179
+ statusEl.textContent = 'File changed — refreshing...';
2180
+
2181
+ // Invalidate graph cache
2182
+ graphData = null;
2183
+
2184
+ // Refresh current view
2185
+ if (currentSkill) {
2186
+ fetchSkill(currentSkill).then(skill => {
2187
+ renderSkillDetail(skill);
2188
+ statusEl.textContent = `Viewing: ${skill.name} (refreshed)`;
2189
+ }).catch(() => {});
2190
+ }
2191
+ if (currentSource) {
2192
+ fetchSkills(currentSource).then(skills => {
2193
+ currentSkills = skills;
2194
+ applyFilters();
2195
+ }).catch(() => {});
2196
+ }
2197
+
2198
+ // Refresh sources
2199
+ fetchSources().then(s => {
2200
+ sources = s;
2201
+ renderSources(s);
2202
+ }).catch(() => {});
2203
+ }
2204
+ };
2205
+
2206
+ ws.onclose = () => {
2207
+ setTimeout(connectWebSocket, 2000);
2208
+ };
2209
+
2210
+ ws.onerror = () => {
2211
+ ws.close();
2212
+ };
2213
+ }
2214
+
2215
+ connectWebSocket();
2216
+ init();
2217
+ </script>
2218
+ </body>
2219
+ </html>