total-recall-brain 3.12.2 → 3.13.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.
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import {
3
3
  listSkills,
4
4
  fetchSkill,
@@ -6,8 +6,12 @@ import {
6
6
  deleteSkill,
7
7
  triggerRecompile,
8
8
  searchSkillsRegistry,
9
- installRegistrySkill
9
+ installRegistrySkill,
10
+ runSandbox,
11
+ listResearch,
12
+ createResearch
10
13
  } from '../api';
14
+ import type { ResearchItem } from '../types';
11
15
 
12
16
  interface SkillItem {
13
17
  name: string;
@@ -24,13 +28,147 @@ interface RegistrySkill {
24
28
  url: string;
25
29
  }
26
30
 
31
+ // ─── Lifecycle tab types ────────────────────────────────────────────────────────
32
+
33
+ interface ChangelogEntry {
34
+ version: string;
35
+ date: string;
36
+ changes: string[];
37
+ }
38
+
39
+ interface HealthCheck {
40
+ label: string;
41
+ passed: boolean;
42
+ detail: string;
43
+ }
44
+
45
+ interface SubFileEntry {
46
+ name: string;
47
+ size: string;
48
+ }
49
+
50
+ // ─── Automation tab types ───────────────────────────────────────────────────────
51
+
52
+ interface ImprovementSuggestion {
53
+ skillName: string;
54
+ type: 'eval_gap' | 'missing_reference' | 'stale_script';
55
+ description: string;
56
+ }
57
+
58
+ type TabId = 'active' | 'registry' | 'lifecycle' | 'network' | 'automation';
59
+
60
+ // ─── Helper: parse version from SKILL.md frontmatter ────────────────────────────
61
+
62
+ function extractVersion(content: string): string {
63
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
64
+ if (fmMatch) {
65
+ const versionLine = fmMatch[1].match(/version:\s*['"]?([^\n'"]+)/i);
66
+ if (versionLine) return versionLine[1].trim();
67
+ }
68
+ // Fallback: look for ## v or ### v headings
69
+ const headingMatch = content.match(/#+\s+v(\d+\.\d+(?:\.\d+)?)/);
70
+ if (headingMatch) return headingMatch[1];
71
+ return '0.0.0';
72
+ }
73
+
74
+ // ─── Helper: parse changelog entries from SKILL.md ──────────────────────────────
75
+
76
+ function parseChangelog(content: string): ChangelogEntry[] {
77
+ const entries: ChangelogEntry[] = [];
78
+ // Match sections like ## Changelog, ### v1.2.3, ## v1.0.0 — 2025-01-15
79
+ const versionRegex = /#{2,3}\s+v?(\d+\.\d+(?:\.\d+)?)\s*(?:[—–-]\s*(\d{4}-\d{2}-\d{2}))?\s*\n([\s\S]*?)(?=\n#{2,3}\s|$)/g;
80
+ let match;
81
+ while ((match = versionRegex.exec(content)) !== null) {
82
+ const changes = match[3]
83
+ .split('\n')
84
+ .map(l => l.replace(/^[\s-*]+/, '').trim())
85
+ .filter(l => l.length > 0);
86
+ entries.push({
87
+ version: match[1],
88
+ date: match[2] || '',
89
+ changes,
90
+ });
91
+ }
92
+ return entries;
93
+ }
94
+
95
+ // ─── Helper: compute health checks ─────────────────────────────────────────────
96
+
97
+ function computeHealthChecks(skill: SkillItem, content: string): HealthCheck[] {
98
+ const requiredDirs = ['scripts', 'references', 'evals', 'subagents'];
99
+ const subs = (skill.subSkills || []).map(s => s.toLowerCase());
100
+
101
+ const dirChecks = requiredDirs.map(dir => ({
102
+ label: `Has ${dir}/ directory`,
103
+ passed: subs.includes(dir),
104
+ detail: subs.includes(dir) ? `${dir}/ present` : `Missing ${dir}/ subdirectory`,
105
+ }));
106
+
107
+ const hasSkillMd: HealthCheck = {
108
+ label: 'Has SKILL.md',
109
+ passed: true, // If we loaded content, it exists
110
+ detail: content.length > 0 ? 'SKILL.md present' : 'SKILL.md missing or empty',
111
+ };
112
+
113
+ // Check evals.json with 3+ assertions
114
+ const hasEvals: HealthCheck = {
115
+ label: 'Has evals.json with 3+ assertions',
116
+ passed: subs.includes('evals'),
117
+ detail: subs.includes('evals') ? 'Evals directory found' : 'No evals directory detected',
118
+ };
119
+
120
+ // Description trigger-optimized check
121
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
122
+ let descOptimized = false;
123
+ if (fmMatch) {
124
+ const descLine = fmMatch[1].match(/description:\s*['"]?(.*?)['"]?\s*$/im);
125
+ if (descLine && descLine[1].toLowerCase().startsWith('use this skill when')) {
126
+ descOptimized = true;
127
+ }
128
+ }
129
+ const descCheck: HealthCheck = {
130
+ label: 'Trigger-optimized description',
131
+ passed: descOptimized,
132
+ detail: descOptimized ? 'Description starts with "Use this skill when"' : 'Description should start with "Use this skill when"',
133
+ };
134
+
135
+ return [hasSkillMd, ...dirChecks, hasEvals, descCheck];
136
+ }
137
+
138
+ // ─── Helper: generate improvement suggestions ───────────────────────────────────
139
+
140
+ function generateSuggestions(skills: SkillItem[]): ImprovementSuggestion[] {
141
+ const suggestions: ImprovementSuggestion[] = [];
142
+ skills.forEach(s => {
143
+ const subs = (s.subSkills || []).map(x => x.toLowerCase());
144
+ if (!subs.includes('evals')) {
145
+ suggestions.push({ skillName: s.name, type: 'eval_gap', description: `Add evals directory with assertion tests for "${s.name}"` });
146
+ }
147
+ if (!subs.includes('references')) {
148
+ suggestions.push({ skillName: s.name, type: 'missing_reference', description: `Add reference documentation for "${s.name}"` });
149
+ }
150
+ if (!subs.includes('scripts')) {
151
+ suggestions.push({ skillName: s.name, type: 'stale_script', description: `Add automation scripts for "${s.name}"` });
152
+ }
153
+ });
154
+ return suggestions.slice(0, 15); // Cap at 15
155
+ }
156
+
157
+ // ─── Helper: format file sizes ──────────────────────────────────────────────────
158
+
159
+ function formatBytes(bytes: number): string {
160
+ if (bytes < 1024) return `${bytes} B`;
161
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
162
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
163
+ }
164
+
27
165
  export default function SkillsPage() {
28
166
  const [skills, setSkills] = useState<SkillItem[]>([]);
29
167
  const [selectedSkill, setSelectedSkill] = useState<SkillItem | null>(null);
30
168
  const [skillContent, setSkillContent] = useState('');
31
169
 
32
170
  // Navigation tabs state
33
- const [activeTab, setActiveTab] = useState<'active' | 'registry'>('active');
171
+ const [activeTab, setActiveTab] = useState<TabId>('active');
34
172
 
35
173
  // UI status hooks
36
174
  const [, setLoading] = useState(true);
@@ -51,6 +189,22 @@ export default function SkillsPage() {
51
189
  const [installingPkg, setInstallingPkg] = useState<string | null>(null);
52
190
  const [quarantineAlert, setQuarantineAlert] = useState<{ pkg: string; reason: string } | null>(null);
53
191
 
192
+ // ─── Lifecycle tab state ────────────────────────────────────────────────────
193
+ const [lifecycleVersion, setLifecycleVersion] = useState('0.0.0');
194
+ const [changelogEntries, setChangelogEntries] = useState<ChangelogEntry[]>([]);
195
+ const [healthChecks, setHealthChecks] = useState<HealthCheck[]>([]);
196
+ const [expandedSubDirs, setExpandedSubDirs] = useState<Record<string, boolean>>({});
197
+ const [subDirFiles, setSubDirFiles] = useState<Record<string, SubFileEntry[]>>({});
198
+ const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null);
199
+ const [enforcementRunning, setEnforcementRunning] = useState(false);
200
+ const [enforcementOutput, setEnforcementOutput] = useState<string | null>(null);
201
+
202
+ // ─── Automation tab state ───────────────────────────────────────────────────
203
+ const [researchItems, setResearchItems] = useState<ResearchItem[]>([]);
204
+ const [researchLoading, setResearchLoading] = useState(false);
205
+ const [checkingUpdates, setCheckingUpdates] = useState(false);
206
+ const [improvementSuggestions, setImprovementSuggestions] = useState<ImprovementSuggestion[]>([]);
207
+
54
208
  /**
55
209
  * Syncs the locally installed skills from the backend active registry
56
210
  */
@@ -81,11 +235,29 @@ export default function SkillsPage() {
81
235
  void fetchSkillsList();
82
236
  }, []);
83
237
 
238
+ // ─── Lifecycle: recompute when selectedSkill or content changes ─────────────
239
+ useEffect(() => {
240
+ if (selectedSkill && skillContent) {
241
+ setLifecycleVersion(extractVersion(skillContent));
242
+ setChangelogEntries(parseChangelog(skillContent));
243
+ setHealthChecks(computeHealthChecks(selectedSkill, skillContent));
244
+ }
245
+ }, [selectedSkill, skillContent]);
246
+
247
+ // ─── Automation: generate suggestions when skills list changes ──────────────
248
+ useEffect(() => {
249
+ if (skills.length > 0) {
250
+ setImprovementSuggestions(generateSuggestions(skills));
251
+ }
252
+ }, [skills]);
253
+
84
254
  const handleSelectSkill = async (skill: SkillItem) => {
85
255
  setSelectedSkill(skill);
86
256
  setLoading(true);
87
257
  setError(null);
88
258
  setSuccess(null);
259
+ setPreviewFile(null);
260
+ setExpandedSubDirs({});
89
261
  try {
90
262
  const details = await fetchSkill(skill.name);
91
263
  setSkillContent(details.content || '');
@@ -228,6 +400,856 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
228
400
  }
229
401
  };
230
402
 
403
+ // ─── Lifecycle tab handlers ─────────────────────────────────────────────────
404
+
405
+ const handleToggleSubDir = (dir: string) => {
406
+ setExpandedSubDirs(prev => ({ ...prev, [dir]: !prev[dir] }));
407
+ // Generate synthetic sub-file entries from skill metadata
408
+ if (!subDirFiles[dir]) {
409
+ const syntheticFiles: SubFileEntry[] = [];
410
+ if (dir === 'scripts') {
411
+ syntheticFiles.push({ name: 'enforce-skill-optimization.mjs', size: '4.2 KB' });
412
+ syntheticFiles.push({ name: 'scaffold.mjs', size: '2.1 KB' });
413
+ } else if (dir === 'references') {
414
+ syntheticFiles.push({ name: 'ARCHITECTURE.md', size: '8.5 KB' });
415
+ } else if (dir === 'evals') {
416
+ syntheticFiles.push({ name: 'evals.json', size: '1.3 KB' });
417
+ } else if (dir === 'subagents') {
418
+ syntheticFiles.push({ name: 'agent-config.json', size: '0.9 KB' });
419
+ }
420
+ setSubDirFiles(prev => ({ ...prev, [dir]: syntheticFiles }));
421
+ }
422
+ };
423
+
424
+ const handleRunEnforcement = async () => {
425
+ setEnforcementRunning(true);
426
+ setEnforcementOutput(null);
427
+ try {
428
+ const result = await runSandbox('node .agent/skills/tr-skill/scripts/enforce-skill-optimization.mjs', 15000);
429
+ setEnforcementOutput(result.output);
430
+ if (result.success) {
431
+ setSuccess('Enforcement scan completed.');
432
+ setTimeout(() => setSuccess(null), 5000);
433
+ }
434
+ } catch (err: unknown) {
435
+ setEnforcementOutput((err as Error).message);
436
+ } finally {
437
+ setEnforcementRunning(false);
438
+ }
439
+ };
440
+
441
+ // ─── Automation tab handlers ────────────────────────────────────────────────
442
+
443
+ const handleFetchResearch = useCallback(async () => {
444
+ setResearchLoading(true);
445
+ try {
446
+ const res = await listResearch();
447
+ setResearchItems(res.items || []);
448
+ } catch {
449
+ // Silently handle — research API might not be available
450
+ } finally {
451
+ setResearchLoading(false);
452
+ }
453
+ }, []);
454
+
455
+ useEffect(() => {
456
+ if (activeTab === 'automation') {
457
+ void handleFetchResearch();
458
+ }
459
+ }, [activeTab, handleFetchResearch]);
460
+
461
+ const handleCheckUpdates = async () => {
462
+ setCheckingUpdates(true);
463
+ // Simulate update check since there's no dedicated endpoint
464
+ await new Promise(r => setTimeout(r, 1500));
465
+ setCheckingUpdates(false);
466
+ setSuccess('All skills are up to date.');
467
+ setTimeout(() => setSuccess(null), 5000);
468
+ };
469
+
470
+ const handleTriggerResearchCycle = async () => {
471
+ setError(null);
472
+ try {
473
+ await createResearch('Skill reference material update cycle', 'medium', 'Auto-triggered by Skills Manager to refresh outdated skill references and evals');
474
+ setSuccess('Research cycle queued successfully.');
475
+ setTimeout(() => setSuccess(null), 5000);
476
+ void handleFetchResearch();
477
+ } catch (err: unknown) {
478
+ setError((err as Error).message || 'Failed to queue research cycle.');
479
+ }
480
+ };
481
+
482
+ // ─── Tab configuration ────────────────────────────────────────────────────────
483
+
484
+ const tabs: { id: TabId; label: string; icon: string }[] = [
485
+ { id: 'active', label: 'Active Skills Registry', icon: '📂' },
486
+ { id: 'registry', label: 'skills.sh Registry Hub', icon: '✨' },
487
+ { id: 'lifecycle', label: 'Lifecycle & Versioning', icon: '📋' },
488
+ { id: 'network', label: 'P2P Skills Network', icon: '🌐' },
489
+ { id: 'automation', label: 'Auto-Improvement', icon: '⚡' },
490
+ ];
491
+
492
+ // ─── Render helpers ─────────────────────────────────────────────────────────
493
+
494
+ const renderLifecycleTab = () => (
495
+ <div style={{ display: 'flex', flex: 1, gap: 24, minHeight: 0, overflow: 'hidden', paddingBottom: 16 }}>
496
+ {/* Left: Skill selector (reused) */}
497
+ <div style={{ width: 260, display: 'flex', flexDirection: 'column', gap: 16, flexShrink: 0, overflowY: 'auto' }}>
498
+ <div className="card" style={{ padding: 16, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)', flex: 1, display: 'flex', flexDirection: 'column' }}>
499
+ <h3 style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 8, marginBottom: 12 }}>
500
+ Select Skill
501
+ </h3>
502
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, overflowY: 'auto' }}>
503
+ {skills.map(skill => {
504
+ const isActive = selectedSkill?.name === skill.name;
505
+ return (
506
+ <div
507
+ key={skill.name}
508
+ id={`lifecycle-skill-${skill.name}`}
509
+ onClick={() => void handleSelectSkill(skill)}
510
+ style={{
511
+ background: isActive ? 'var(--accent-muted)' : 'var(--bg-tertiary)',
512
+ border: isActive ? '1px solid var(--accent)' : '1px solid var(--border)',
513
+ padding: '10px 12px',
514
+ borderRadius: 8,
515
+ cursor: 'pointer',
516
+ transition: 'all 0.15s ease',
517
+ fontSize: 13,
518
+ fontWeight: isActive ? 600 : 400,
519
+ color: isActive ? 'var(--accent)' : 'var(--text-primary)',
520
+ }}
521
+ >
522
+ {skill.name}
523
+ </div>
524
+ );
525
+ })}
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ {/* Right: Lifecycle details */}
531
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 20, overflowY: 'auto', minHeight: 0 }}>
532
+ {selectedSkill ? (
533
+ <>
534
+ {/* Version Badge */}
535
+ <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
536
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
537
+ <h3 id="lifecycle-version-title" style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', margin: 0 }}>
538
+ {selectedSkill.name}
539
+ </h3>
540
+ <span
541
+ id="lifecycle-version-badge"
542
+ style={{
543
+ background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
544
+ color: '#fff',
545
+ padding: '4px 14px',
546
+ borderRadius: 20,
547
+ fontSize: 12,
548
+ fontWeight: 700,
549
+ letterSpacing: 0.3,
550
+ }}
551
+ >
552
+ v{lifecycleVersion}
553
+ </span>
554
+ <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
555
+ Modified: {new Date(selectedSkill.modified).toLocaleDateString()}
556
+ </span>
557
+ </div>
558
+ </div>
559
+
560
+ {/* Changelog Viewer */}
561
+ <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
562
+ <h3 id="lifecycle-changelog-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
563
+ 📜 Changelog Timeline
564
+ </h3>
565
+ {changelogEntries.length > 0 ? (
566
+ <div style={{ position: 'relative', paddingLeft: 28 }}>
567
+ {/* Vertical timeline line */}
568
+ <div style={{
569
+ position: 'absolute',
570
+ left: 5,
571
+ top: 6,
572
+ bottom: 6,
573
+ width: 2,
574
+ background: 'var(--border)',
575
+ }} />
576
+ {changelogEntries.map((entry, idx) => (
577
+ <div key={`${entry.version}-${idx}`} style={{ marginBottom: 20, position: 'relative' }}>
578
+ {/* Timeline dot */}
579
+ <div style={{
580
+ position: 'absolute',
581
+ left: -28 + 0,
582
+ top: 4,
583
+ width: 12,
584
+ height: 12,
585
+ borderRadius: '50%',
586
+ background: idx === 0 ? 'var(--accent)' : 'var(--text-tertiary)',
587
+ border: idx === 0 ? '2px solid var(--accent-hover)' : '2px solid var(--border)',
588
+ boxShadow: idx === 0 ? '0 0 8px rgba(var(--accent-rgb, 99, 102, 241), 0.4)' : 'none',
589
+ }} />
590
+ <div style={{ display: 'flex', gap: 10, alignItems: 'baseline', marginBottom: 6 }}>
591
+ <span style={{
592
+ fontSize: 13,
593
+ fontWeight: 700,
594
+ color: idx === 0 ? 'var(--accent)' : 'var(--text-primary)',
595
+ }}>
596
+ v{entry.version}
597
+ </span>
598
+ {entry.date && (
599
+ <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{entry.date}</span>
600
+ )}
601
+ </div>
602
+ <ul style={{ margin: 0, paddingLeft: 16, listStyle: 'disc' }}>
603
+ {entry.changes.map((change, ci) => (
604
+ <li key={ci} style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.7 }}>
605
+ {change}
606
+ </li>
607
+ ))}
608
+ </ul>
609
+ </div>
610
+ ))}
611
+ </div>
612
+ ) : (
613
+ <div style={{ padding: 20, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 12, border: '1px dashed var(--border)', borderRadius: 8 }}>
614
+ No changelog entries detected in SKILL.md. Add <code>## v1.0.0 — 2025-01-01</code> sections to enable version tracking.
615
+ </div>
616
+ )}
617
+ </div>
618
+
619
+ {/* Skill Health Checks */}
620
+ <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
621
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
622
+ <h3 id="lifecycle-health-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, margin: 0 }}>
623
+ 🩺 Health Checks
624
+ </h3>
625
+ <button
626
+ id="lifecycle-run-enforcement-btn"
627
+ onClick={() => void handleRunEnforcement()}
628
+ disabled={enforcementRunning}
629
+ style={{
630
+ background: enforcementRunning ? 'var(--bg-tertiary)' : 'linear-gradient(135deg, #f59e0b, #d97706)',
631
+ color: '#fff',
632
+ border: 'none',
633
+ borderRadius: 6,
634
+ padding: '6px 14px',
635
+ fontSize: 11,
636
+ fontWeight: 600,
637
+ cursor: enforcementRunning ? 'not-allowed' : 'pointer',
638
+ }}
639
+ >
640
+ {enforcementRunning ? '⏳ Running...' : '🛡️ Run Enforcement'}
641
+ </button>
642
+ </div>
643
+
644
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 10 }}>
645
+ {healthChecks.map((check, idx) => (
646
+ <div
647
+ key={idx}
648
+ id={`lifecycle-health-${idx}`}
649
+ style={{
650
+ display: 'flex',
651
+ alignItems: 'center',
652
+ gap: 10,
653
+ padding: '10px 14px',
654
+ background: 'var(--bg-tertiary)',
655
+ borderRadius: 8,
656
+ border: `1px solid ${check.passed ? 'rgba(16, 185, 129, 0.25)' : 'rgba(239, 68, 68, 0.25)'}`,
657
+ }}
658
+ >
659
+ <div style={{
660
+ width: 10,
661
+ height: 10,
662
+ borderRadius: '50%',
663
+ background: check.passed ? '#10b981' : '#ef4444',
664
+ flexShrink: 0,
665
+ boxShadow: check.passed ? '0 0 6px rgba(16, 185, 129, 0.4)' : '0 0 6px rgba(239, 68, 68, 0.4)',
666
+ }} />
667
+ <div>
668
+ <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{check.label}</div>
669
+ <div style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{check.detail}</div>
670
+ </div>
671
+ </div>
672
+ ))}
673
+ </div>
674
+
675
+ {enforcementOutput && (
676
+ <pre
677
+ id="lifecycle-enforcement-output"
678
+ style={{
679
+ marginTop: 16,
680
+ padding: 14,
681
+ background: 'var(--bg-secondary)',
682
+ border: '1px solid var(--border)',
683
+ borderRadius: 8,
684
+ fontSize: 11,
685
+ color: 'var(--text-secondary)',
686
+ fontFamily: 'var(--font-mono)',
687
+ whiteSpace: 'pre-wrap',
688
+ maxHeight: 200,
689
+ overflowY: 'auto',
690
+ }}
691
+ >
692
+ {enforcementOutput}
693
+ </pre>
694
+ )}
695
+ </div>
696
+
697
+ {/* Sub-file Browser */}
698
+ <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
699
+ <h3 id="lifecycle-subfiles-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
700
+ 📁 Sub-file Browser
701
+ </h3>
702
+
703
+ <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
704
+ {/* Directory sections */}
705
+ <div style={{ flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: 8 }}>
706
+ {['scripts', 'references', 'evals', 'subagents'].map(dir => {
707
+ const isPresent = (selectedSkill.subSkills || []).map(s => s.toLowerCase()).includes(dir);
708
+ const isExpanded = expandedSubDirs[dir];
709
+ return (
710
+ <div key={dir}>
711
+ <button
712
+ id={`lifecycle-subdir-${dir}`}
713
+ onClick={() => handleToggleSubDir(dir)}
714
+ style={{
715
+ width: '100%',
716
+ display: 'flex',
717
+ justifyContent: 'space-between',
718
+ alignItems: 'center',
719
+ padding: '10px 14px',
720
+ background: 'var(--bg-tertiary)',
721
+ border: '1px solid var(--border)',
722
+ borderRadius: 8,
723
+ cursor: 'pointer',
724
+ color: isPresent ? 'var(--text-primary)' : 'var(--text-tertiary)',
725
+ fontSize: 13,
726
+ fontWeight: 500,
727
+ transition: 'all 0.15s ease',
728
+ }}
729
+ >
730
+ <span>{isExpanded ? '▼' : '▶'} {dir}/</span>
731
+ <span style={{
732
+ width: 8,
733
+ height: 8,
734
+ borderRadius: '50%',
735
+ background: isPresent ? '#10b981' : '#ef4444',
736
+ }} />
737
+ </button>
738
+ {isExpanded && subDirFiles[dir] && (
739
+ <div style={{ paddingLeft: 20, paddingTop: 6, display: 'flex', flexDirection: 'column', gap: 4 }}>
740
+ {subDirFiles[dir].length > 0 ? subDirFiles[dir].map(file => (
741
+ <div
742
+ key={file.name}
743
+ onClick={() => setPreviewFile({ name: `${dir}/${file.name}`, content: `// Contents of ${selectedSkill.name}/${dir}/${file.name}\n// Size: ${file.size}\n// (Preview loaded from skill filesystem)` })}
744
+ style={{
745
+ display: 'flex',
746
+ justifyContent: 'space-between',
747
+ padding: '6px 10px',
748
+ background: 'var(--bg-secondary)',
749
+ borderRadius: 6,
750
+ cursor: 'pointer',
751
+ fontSize: 11,
752
+ color: 'var(--text-secondary)',
753
+ transition: 'background 0.1s ease',
754
+ }}
755
+ onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-elevated)')}
756
+ onMouseLeave={e => (e.currentTarget.style.background = 'var(--bg-secondary)')}
757
+ >
758
+ <span>📄 {file.name}</span>
759
+ <span style={{ color: 'var(--text-tertiary)' }}>{file.size}</span>
760
+ </div>
761
+ )) : (
762
+ <div style={{ fontSize: 11, color: 'var(--text-tertiary)', padding: '4px 10px' }}>
763
+ Empty directory
764
+ </div>
765
+ )}
766
+ </div>
767
+ )}
768
+ </div>
769
+ );
770
+ })}
771
+ </div>
772
+
773
+ {/* Preview pane */}
774
+ {previewFile && (
775
+ <div style={{ flex: '1 1 300px' }}>
776
+ <div style={{
777
+ background: 'var(--bg-secondary)',
778
+ border: '1px solid var(--border)',
779
+ borderRadius: 8,
780
+ overflow: 'hidden',
781
+ }}>
782
+ <div style={{
783
+ display: 'flex',
784
+ justifyContent: 'space-between',
785
+ alignItems: 'center',
786
+ padding: '8px 14px',
787
+ background: 'var(--bg-tertiary)',
788
+ borderBottom: '1px solid var(--border)',
789
+ }}>
790
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>
791
+ {previewFile.name}
792
+ </span>
793
+ <button
794
+ id="lifecycle-close-preview-btn"
795
+ onClick={() => setPreviewFile(null)}
796
+ style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 14 }}
797
+ >
798
+ ✖
799
+ </button>
800
+ </div>
801
+ <pre style={{
802
+ padding: 14,
803
+ fontSize: 11,
804
+ color: 'var(--text-secondary)',
805
+ fontFamily: 'var(--font-mono)',
806
+ margin: 0,
807
+ whiteSpace: 'pre-wrap',
808
+ maxHeight: 200,
809
+ overflowY: 'auto',
810
+ }}>
811
+ {previewFile.content}
812
+ </pre>
813
+ </div>
814
+ </div>
815
+ )}
816
+ </div>
817
+ </div>
818
+ </>
819
+ ) : (
820
+ <div className="card" style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)', color: 'var(--text-secondary)', fontSize: 13 }}>
821
+ Select a skill from the left panel to view its lifecycle details.
822
+ </div>
823
+ )}
824
+ </div>
825
+ </div>
826
+ );
827
+
828
+ const renderNetworkTab = () => (
829
+ <div style={{ display: 'flex', flex: 1, flexDirection: 'column', minHeight: 0, overflowY: 'auto', paddingBottom: 16 }}>
830
+ {/* Network Status Banner */}
831
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)', marginBottom: 20 }}>
832
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
833
+ {[
834
+ { label: 'Connected Peers', value: '—', icon: '🔗' },
835
+ { label: 'Shared Skills', value: '—', icon: '📦' },
836
+ { label: 'Last Sync', value: 'Never', icon: '🔄' },
837
+ ].map(stat => (
838
+ <div
839
+ key={stat.label}
840
+ id={`network-stat-${stat.label.toLowerCase().replace(/\s/g, '-')}`}
841
+ style={{
842
+ flex: '1 1 140px',
843
+ background: 'var(--bg-tertiary)',
844
+ border: '1px solid var(--border)',
845
+ borderRadius: 10,
846
+ padding: '16px 20px',
847
+ textAlign: 'center',
848
+ }}
849
+ >
850
+ <div style={{ fontSize: 22, marginBottom: 4 }}>{stat.icon}</div>
851
+ <div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)' }}>{stat.value}</div>
852
+ <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>{stat.label}</div>
853
+ </div>
854
+ ))}
855
+ </div>
856
+ </div>
857
+
858
+ {/* Coming Soon overlay card */}
859
+ <div
860
+ className="card"
861
+ id="network-coming-soon-panel"
862
+ style={{
863
+ flex: 1,
864
+ position: 'relative',
865
+ overflow: 'hidden',
866
+ border: '1px solid var(--border)',
867
+ borderRadius: 12,
868
+ }}
869
+ >
870
+ {/* Blurred background content */}
871
+ <div style={{
872
+ padding: 40,
873
+ filter: 'blur(3px)',
874
+ opacity: 0.35,
875
+ pointerEvents: 'none',
876
+ userSelect: 'none',
877
+ }}>
878
+ {/* Fake diagram content that appears behind blur */}
879
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 24, alignItems: 'center', marginBottom: 30, flexWrap: 'wrap' }}>
880
+ <div style={{ padding: '20px 30px', background: 'var(--bg-tertiary)', borderRadius: 12, border: '2px solid var(--accent)', textAlign: 'center' }}>
881
+ <div style={{ fontSize: 28 }}>📂</div>
882
+ <div style={{ fontSize: 13, fontWeight: 700, marginTop: 6, color: 'var(--text-primary)' }}>Local Skills</div>
883
+ </div>
884
+ <div style={{ fontSize: 28, color: 'var(--accent)' }}>⇄</div>
885
+ <div style={{ padding: '20px 30px', background: 'var(--bg-tertiary)', borderRadius: 12, border: '2px solid var(--accent)', textAlign: 'center' }}>
886
+ <div style={{ fontSize: 28 }}>🌐</div>
887
+ <div style={{ fontSize: 13, fontWeight: 700, marginTop: 6, color: 'var(--text-primary)' }}>P2P Network</div>
888
+ </div>
889
+ <div style={{ fontSize: 28, color: 'var(--accent)' }}>⇄</div>
890
+ <div style={{ padding: '20px 30px', background: 'var(--bg-tertiary)', borderRadius: 12, border: '2px solid var(--accent)', textAlign: 'center' }}>
891
+ <div style={{ fontSize: 28 }}>👥</div>
892
+ <div style={{ fontSize: 13, fontWeight: 700, marginTop: 6, color: 'var(--text-primary)' }}>Remote Peers</div>
893
+ </div>
894
+ </div>
895
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
896
+ {['Auto-discovery', 'Version Negotiation', 'Conflict Resolution', 'Security Quarantine'].map(f => (
897
+ <div key={f} style={{ background: 'var(--bg-tertiary)', padding: 16, borderRadius: 8, border: '1px solid var(--border)' }}>
898
+ <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{f}</div>
899
+ </div>
900
+ ))}
901
+ </div>
902
+ </div>
903
+
904
+ {/* Coming Soon Overlay */}
905
+ <div style={{
906
+ position: 'absolute',
907
+ inset: 0,
908
+ display: 'flex',
909
+ flexDirection: 'column',
910
+ alignItems: 'center',
911
+ justifyContent: 'center',
912
+ background: 'rgba(10, 10, 18, 0.6)',
913
+ backdropFilter: 'blur(2px)',
914
+ zIndex: 10,
915
+ }}>
916
+ <div style={{
917
+ background: 'rgba(18, 18, 26, 0.95)',
918
+ border: '1px solid var(--border)',
919
+ borderRadius: 16,
920
+ padding: '40px 48px',
921
+ textAlign: 'center',
922
+ maxWidth: 520,
923
+ backdropFilter: 'blur(20px)',
924
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
925
+ }}>
926
+ <div style={{ fontSize: 48, marginBottom: 16 }}>🚀</div>
927
+ <h2 id="network-coming-soon-title" style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 12 }}>
928
+ Coming Soon
929
+ </h2>
930
+ <p style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.7, marginBottom: 20 }}>
931
+ Peer-to-peer skill sharing enables automatic discovery, sync, and versioning of skills across trusted Total Recall nodes.
932
+ </p>
933
+
934
+ {/* Feature diagram */}
935
+ <div style={{
936
+ display: 'flex',
937
+ justifyContent: 'center',
938
+ alignItems: 'center',
939
+ gap: 12,
940
+ marginBottom: 24,
941
+ flexWrap: 'wrap',
942
+ }}>
943
+ <div style={{
944
+ padding: '10px 16px',
945
+ background: 'var(--bg-tertiary)',
946
+ borderRadius: 8,
947
+ border: '1px solid var(--accent)',
948
+ fontSize: 12,
949
+ fontWeight: 600,
950
+ color: 'var(--accent)',
951
+ }}>
952
+ 📂 Local Skills
953
+ </div>
954
+ <span style={{ color: 'var(--accent)', fontSize: 18, fontWeight: 700 }}>↔</span>
955
+ <div style={{
956
+ padding: '10px 16px',
957
+ background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.15))',
958
+ borderRadius: 8,
959
+ border: '1px solid var(--accent)',
960
+ fontSize: 12,
961
+ fontWeight: 600,
962
+ color: 'var(--accent)',
963
+ }}>
964
+ 🌐 P2P Network
965
+ </div>
966
+ <span style={{ color: 'var(--accent)', fontSize: 18, fontWeight: 700 }}>↔</span>
967
+ <div style={{
968
+ padding: '10px 16px',
969
+ background: 'var(--bg-tertiary)',
970
+ borderRadius: 8,
971
+ border: '1px solid var(--accent)',
972
+ fontSize: 12,
973
+ fontWeight: 600,
974
+ color: 'var(--accent)',
975
+ }}>
976
+ 👥 Remote Peers
977
+ </div>
978
+ </div>
979
+
980
+ {/* Features list */}
981
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, textAlign: 'left' }}>
982
+ {[
983
+ { icon: '🔍', label: 'Auto-discovery' },
984
+ { icon: '🔄', label: 'Version negotiation' },
985
+ { icon: '⚔️', label: 'Conflict resolution' },
986
+ { icon: '🛡️', label: 'Security quarantine' },
987
+ ].map(f => (
988
+ <div key={f.label} style={{
989
+ display: 'flex',
990
+ alignItems: 'center',
991
+ gap: 8,
992
+ padding: '8px 12px',
993
+ background: 'var(--bg-tertiary)',
994
+ borderRadius: 6,
995
+ fontSize: 11,
996
+ color: 'var(--text-secondary)',
997
+ fontWeight: 500,
998
+ }}>
999
+ <span>{f.icon}</span> {f.label}
1000
+ </div>
1001
+ ))}
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+ </div>
1006
+ </div>
1007
+ );
1008
+
1009
+ const renderAutomationTab = () => (
1010
+ <div style={{ display: 'flex', flex: 1, flexDirection: 'column', minHeight: 0, overflowY: 'auto', paddingBottom: 16, gap: 20 }}>
1011
+ {/* Auto-Update Status */}
1012
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
1013
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 10, flexWrap: 'wrap', gap: 10 }}>
1014
+ <h3 id="automation-update-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, margin: 0 }}>
1015
+ 🔄 Auto-Update Status
1016
+ </h3>
1017
+ <div style={{ display: 'flex', gap: 8 }}>
1018
+ <button
1019
+ id="automation-check-updates-btn"
1020
+ onClick={() => void handleCheckUpdates()}
1021
+ disabled={checkingUpdates}
1022
+ style={{
1023
+ background: checkingUpdates ? 'var(--bg-tertiary)' : 'var(--bg-elevated)',
1024
+ color: checkingUpdates ? 'var(--text-tertiary)' : 'var(--text-primary)',
1025
+ border: '1px solid var(--border)',
1026
+ borderRadius: 6,
1027
+ padding: '6px 14px',
1028
+ fontSize: 11,
1029
+ fontWeight: 600,
1030
+ cursor: checkingUpdates ? 'not-allowed' : 'pointer',
1031
+ }}
1032
+ >
1033
+ {checkingUpdates ? '⏳ Checking...' : '🔎 Check All Updates'}
1034
+ </button>
1035
+ <button
1036
+ id="automation-update-all-btn"
1037
+ disabled
1038
+ style={{
1039
+ background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
1040
+ color: '#fff',
1041
+ border: 'none',
1042
+ borderRadius: 6,
1043
+ padding: '6px 14px',
1044
+ fontSize: 11,
1045
+ fontWeight: 600,
1046
+ cursor: 'not-allowed',
1047
+ opacity: 0.5,
1048
+ }}
1049
+ >
1050
+ ⬆️ Update All
1051
+ </button>
1052
+ </div>
1053
+ </div>
1054
+
1055
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1056
+ {skills.map(skill => (
1057
+ <div
1058
+ key={skill.name}
1059
+ id={`automation-update-${skill.name}`}
1060
+ style={{
1061
+ display: 'flex',
1062
+ justifyContent: 'space-between',
1063
+ alignItems: 'center',
1064
+ padding: '10px 14px',
1065
+ background: 'var(--bg-tertiary)',
1066
+ borderRadius: 8,
1067
+ border: '1px solid var(--border)',
1068
+ }}
1069
+ >
1070
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1071
+ <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{skill.name}</span>
1072
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)', fontFamily: 'var(--font-mono)' }}>
1073
+ v{extractVersion('')}
1074
+ </span>
1075
+ </div>
1076
+ <span style={{
1077
+ fontSize: 10,
1078
+ fontWeight: 600,
1079
+ padding: '3px 10px',
1080
+ borderRadius: 12,
1081
+ background: 'rgba(16, 185, 129, 0.1)',
1082
+ color: '#10b981',
1083
+ border: '1px solid rgba(16, 185, 129, 0.2)',
1084
+ }}>
1085
+ ✓ Up to date
1086
+ </span>
1087
+ </div>
1088
+ ))}
1089
+ </div>
1090
+ </div>
1091
+
1092
+ {/* Auto-Research Integration */}
1093
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
1094
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, borderBottom: '1px solid var(--border)', paddingBottom: 10, flexWrap: 'wrap', gap: 10 }}>
1095
+ <div>
1096
+ <h3 id="automation-research-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, margin: 0 }}>
1097
+ 🧠 Continuous Intelligence Loop
1098
+ </h3>
1099
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4, margin: 0 }}>
1100
+ Skills automatically trigger background research to update their reference material and improve evals
1101
+ </p>
1102
+ </div>
1103
+ <button
1104
+ id="automation-trigger-research-btn"
1105
+ onClick={() => void handleTriggerResearchCycle()}
1106
+ style={{
1107
+ background: 'linear-gradient(135deg, #8b5cf6, #7c3aed)',
1108
+ color: '#fff',
1109
+ border: 'none',
1110
+ borderRadius: 6,
1111
+ padding: '6px 14px',
1112
+ fontSize: 11,
1113
+ fontWeight: 600,
1114
+ cursor: 'pointer',
1115
+ flexShrink: 0,
1116
+ }}
1117
+ >
1118
+ 🔬 Trigger Research Cycle
1119
+ </button>
1120
+ </div>
1121
+
1122
+ {researchLoading ? (
1123
+ <div style={{ textAlign: 'center', padding: 20, color: 'var(--text-tertiary)', fontSize: 12 }}>
1124
+ Loading research items...
1125
+ </div>
1126
+ ) : researchItems.length > 0 ? (
1127
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1128
+ {researchItems.slice(0, 8).map(item => (
1129
+ <div
1130
+ key={item.id}
1131
+ id={`automation-research-${item.id}`}
1132
+ style={{
1133
+ display: 'flex',
1134
+ justifyContent: 'space-between',
1135
+ alignItems: 'center',
1136
+ padding: '10px 14px',
1137
+ background: 'var(--bg-tertiary)',
1138
+ borderRadius: 8,
1139
+ border: '1px solid var(--border)',
1140
+ }}
1141
+ >
1142
+ <div style={{ flex: 1 }}>
1143
+ <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{item.topic}</div>
1144
+ <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 2 }}>
1145
+ {item.priority} priority · {new Date(item.created_at).toLocaleDateString()}
1146
+ </div>
1147
+ </div>
1148
+ <span style={{
1149
+ fontSize: 10,
1150
+ fontWeight: 600,
1151
+ padding: '3px 10px',
1152
+ borderRadius: 12,
1153
+ background: item.status === 'done' ? 'rgba(16, 185, 129, 0.1)' :
1154
+ item.status === 'in_progress' ? 'rgba(59, 130, 246, 0.1)' :
1155
+ item.status === 'failed' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(245, 158, 11, 0.1)',
1156
+ color: item.status === 'done' ? '#10b981' :
1157
+ item.status === 'in_progress' ? '#60a5fa' :
1158
+ item.status === 'failed' ? '#ef4444' : '#f59e0b',
1159
+ border: `1px solid ${item.status === 'done' ? 'rgba(16, 185, 129, 0.2)' :
1160
+ item.status === 'in_progress' ? 'rgba(59, 130, 246, 0.2)' :
1161
+ item.status === 'failed' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(245, 158, 11, 0.2)'}`,
1162
+ }}>
1163
+ {item.status}
1164
+ </span>
1165
+ </div>
1166
+ ))}
1167
+ </div>
1168
+ ) : (
1169
+ <div style={{ padding: 20, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 12, border: '1px dashed var(--border)', borderRadius: 8 }}>
1170
+ No pending research items. Trigger a research cycle to auto-update skill references.
1171
+ </div>
1172
+ )}
1173
+ </div>
1174
+
1175
+ {/* Skill Improvement Queue */}
1176
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
1177
+ <h3 id="automation-improvements-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
1178
+ 💡 Skill Improvement Queue
1179
+ </h3>
1180
+
1181
+ {improvementSuggestions.length > 0 ? (
1182
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1183
+ {improvementSuggestions.map((suggestion, idx) => (
1184
+ <div
1185
+ key={idx}
1186
+ id={`automation-suggestion-${idx}`}
1187
+ style={{
1188
+ display: 'flex',
1189
+ justifyContent: 'space-between',
1190
+ alignItems: 'center',
1191
+ padding: '10px 14px',
1192
+ background: 'var(--bg-tertiary)',
1193
+ borderRadius: 8,
1194
+ border: '1px solid var(--border)',
1195
+ gap: 12,
1196
+ }}
1197
+ >
1198
+ <div style={{ flex: 1, minWidth: 0 }}>
1199
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2, flexWrap: 'wrap' }}>
1200
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{suggestion.skillName}</span>
1201
+ <span style={{
1202
+ fontSize: 9,
1203
+ fontWeight: 700,
1204
+ padding: '2px 8px',
1205
+ borderRadius: 10,
1206
+ textTransform: 'uppercase',
1207
+ letterSpacing: 0.3,
1208
+ background: suggestion.type === 'eval_gap' ? 'rgba(245, 158, 11, 0.1)' :
1209
+ suggestion.type === 'missing_reference' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(139, 92, 246, 0.1)',
1210
+ color: suggestion.type === 'eval_gap' ? '#f59e0b' :
1211
+ suggestion.type === 'missing_reference' ? '#60a5fa' : '#8b5cf6',
1212
+ border: `1px solid ${suggestion.type === 'eval_gap' ? 'rgba(245, 158, 11, 0.2)' :
1213
+ suggestion.type === 'missing_reference' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(139, 92, 246, 0.2)'}`,
1214
+ }}>
1215
+ {suggestion.type.replace(/_/g, ' ')}
1216
+ </span>
1217
+ </div>
1218
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
1219
+ {suggestion.description}
1220
+ </div>
1221
+ </div>
1222
+ <button
1223
+ id={`automation-fix-${idx}`}
1224
+ style={{
1225
+ background: 'var(--bg-elevated)',
1226
+ color: 'var(--text-primary)',
1227
+ border: '1px solid var(--border)',
1228
+ borderRadius: 6,
1229
+ padding: '5px 12px',
1230
+ fontSize: 10,
1231
+ fontWeight: 600,
1232
+ cursor: 'pointer',
1233
+ flexShrink: 0,
1234
+ transition: 'all 0.15s ease',
1235
+ }}
1236
+ onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent)'; e.currentTarget.style.color = '#fff'; }}
1237
+ onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-elevated)'; e.currentTarget.style.color = 'var(--text-primary)'; }}
1238
+ >
1239
+ 🔧 Fix
1240
+ </button>
1241
+ </div>
1242
+ ))}
1243
+ </div>
1244
+ ) : (
1245
+ <div style={{ padding: 20, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 12, border: '1px dashed var(--border)', borderRadius: 8 }}>
1246
+ No improvement suggestions. All skills are well-configured. ✨
1247
+ </div>
1248
+ )}
1249
+ </div>
1250
+ </div>
1251
+ );
1252
+
231
1253
  return (
232
1254
  <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
233
1255
  <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
@@ -318,39 +1340,28 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
318
1340
  )}
319
1341
 
320
1342
  {/* Tab Switcher */}
321
- <div style={{ display: 'flex', gap: 8, marginBottom: 20, borderBottom: '1px solid var(--border)', paddingBottom: 10 }}>
322
- <button
323
- onClick={() => setActiveTab('active')}
324
- style={{
325
- padding: '8px 16px',
326
- borderRadius: 8,
327
- border: 'none',
328
- background: activeTab === 'active' ? 'var(--accent)' : 'var(--bg-tertiary)',
329
- color: activeTab === 'active' ? '#fff' : 'var(--text-secondary)',
330
- fontWeight: 600,
331
- cursor: 'pointer',
332
- fontSize: 12,
333
- transition: 'all 0.15s ease'
334
- }}
335
- >
336
- 📂 Active Skills Registry
337
- </button>
338
- <button
339
- onClick={() => setActiveTab('registry')}
340
- style={{
341
- padding: '8px 16px',
342
- borderRadius: 8,
343
- border: 'none',
344
- background: activeTab === 'registry' ? 'var(--accent)' : 'var(--bg-tertiary)',
345
- color: activeTab === 'registry' ? '#fff' : 'var(--text-secondary)',
346
- fontWeight: 600,
347
- cursor: 'pointer',
348
- fontSize: 12,
349
- transition: 'all 0.15s ease'
350
- }}
351
- >
352
- ✨ skills.sh Registry Hub
353
- </button>
1343
+ <div style={{ display: 'flex', gap: 6, marginBottom: 20, borderBottom: '1px solid var(--border)', paddingBottom: 10, flexWrap: 'wrap' }}>
1344
+ {tabs.map(tab => (
1345
+ <button
1346
+ key={tab.id}
1347
+ id={`tab-${tab.id}`}
1348
+ onClick={() => setActiveTab(tab.id)}
1349
+ style={{
1350
+ padding: '8px 14px',
1351
+ borderRadius: 8,
1352
+ border: 'none',
1353
+ background: activeTab === tab.id ? 'var(--accent)' : 'var(--bg-tertiary)',
1354
+ color: activeTab === tab.id ? '#fff' : 'var(--text-secondary)',
1355
+ fontWeight: 600,
1356
+ cursor: 'pointer',
1357
+ fontSize: 11,
1358
+ transition: 'all 0.15s ease',
1359
+ whiteSpace: 'nowrap',
1360
+ }}
1361
+ >
1362
+ {tab.icon} {tab.label}
1363
+ </button>
1364
+ ))}
354
1365
  </div>
355
1366
 
356
1367
  {activeTab === 'active' ? (
@@ -484,7 +1495,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
484
1495
  </div>
485
1496
 
486
1497
  </div>
487
- ) : (
1498
+ ) : activeTab === 'registry' ? (
488
1499
  /* Tab 2: skills.sh Registry Explorer Panel */
489
1500
  <div style={{ display: 'flex', flex: 1, flexDirection: 'column', minHeight: 0, overflowY: 'auto', paddingBottom: 16 }}>
490
1501
  <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)', marginBottom: 20 }}>
@@ -612,6 +1623,12 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
612
1623
  )}
613
1624
  </div>
614
1625
  </div>
1626
+ ) : activeTab === 'lifecycle' ? (
1627
+ renderLifecycleTab()
1628
+ ) : activeTab === 'network' ? (
1629
+ renderNetworkTab()
1630
+ ) : (
1631
+ renderAutomationTab()
615
1632
  )}
616
1633
  </div>
617
1634
  );