selftune 0.2.2 → 0.2.6

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.
Files changed (53) hide show
  1. package/README.md +11 -0
  2. package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +1 -0
  3. package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +15 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-r2k_Ku_V.js +346 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/analytics.ts +354 -0
  7. package/cli/selftune/badge/badge.ts +2 -2
  8. package/cli/selftune/dashboard-server.ts +3 -3
  9. package/cli/selftune/evolution/evolve-body.ts +1 -1
  10. package/cli/selftune/evolution/evolve.ts +1 -1
  11. package/cli/selftune/index.ts +15 -1
  12. package/cli/selftune/init.ts +5 -1
  13. package/cli/selftune/observability.ts +63 -2
  14. package/cli/selftune/orchestrate.ts +1 -1
  15. package/cli/selftune/quickstart.ts +1 -1
  16. package/cli/selftune/status.ts +2 -2
  17. package/cli/selftune/types.ts +1 -0
  18. package/cli/selftune/utils/llm-call.ts +2 -1
  19. package/package.json +6 -4
  20. package/packages/ui/README.md +113 -0
  21. package/packages/ui/index.ts +10 -0
  22. package/packages/ui/package.json +62 -0
  23. package/packages/ui/src/components/ActivityTimeline.tsx +171 -0
  24. package/packages/ui/src/components/EvidenceViewer.tsx +718 -0
  25. package/packages/ui/src/components/EvolutionTimeline.tsx +252 -0
  26. package/packages/ui/src/components/InfoTip.tsx +19 -0
  27. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +164 -0
  28. package/packages/ui/src/components/index.ts +7 -0
  29. package/packages/ui/src/components/section-cards.tsx +155 -0
  30. package/packages/ui/src/components/skill-health-grid.tsx +686 -0
  31. package/packages/ui/src/lib/constants.tsx +43 -0
  32. package/packages/ui/src/lib/format.ts +37 -0
  33. package/packages/ui/src/lib/index.ts +3 -0
  34. package/packages/ui/src/lib/utils.ts +6 -0
  35. package/packages/ui/src/primitives/badge.tsx +52 -0
  36. package/packages/ui/src/primitives/button.tsx +58 -0
  37. package/packages/ui/src/primitives/card.tsx +103 -0
  38. package/packages/ui/src/primitives/checkbox.tsx +27 -0
  39. package/packages/ui/src/primitives/collapsible.tsx +7 -0
  40. package/packages/ui/src/primitives/dropdown-menu.tsx +266 -0
  41. package/packages/ui/src/primitives/index.ts +55 -0
  42. package/packages/ui/src/primitives/label.tsx +20 -0
  43. package/packages/ui/src/primitives/select.tsx +197 -0
  44. package/packages/ui/src/primitives/table.tsx +114 -0
  45. package/packages/ui/src/primitives/tabs.tsx +82 -0
  46. package/packages/ui/src/primitives/tooltip.tsx +64 -0
  47. package/packages/ui/src/types.ts +87 -0
  48. package/packages/ui/tsconfig.json +17 -0
  49. package/skill/SKILL.md +3 -0
  50. package/skill/Workflows/Telemetry.md +59 -0
  51. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +0 -15
  52. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +0 -1
  53. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +0 -346
@@ -0,0 +1,252 @@
1
+ import { useState } from "react"
2
+ import { Badge } from "../primitives/badge"
3
+ import { cn } from "../lib/utils"
4
+ import type { EvalSnapshot, EvolutionEntry } from "../types"
5
+ import { timeAgo } from "../lib/format"
6
+ import {
7
+ CircleDotIcon,
8
+ RocketIcon,
9
+ ShieldCheckIcon,
10
+ XCircleIcon,
11
+ UndoIcon,
12
+ TrendingUpIcon,
13
+ TrendingDownIcon,
14
+ ChevronDownIcon,
15
+ ChevronRightIcon,
16
+ } from "lucide-react"
17
+
18
+ const ACTION_ICON: Record<string, React.ReactNode> = {
19
+ created: <CircleDotIcon className="size-3.5" />,
20
+ validated: <ShieldCheckIcon className="size-3.5" />,
21
+ deployed: <RocketIcon className="size-3.5" />,
22
+ rejected: <XCircleIcon className="size-3.5" />,
23
+ rolled_back: <UndoIcon className="size-3.5" />,
24
+ }
25
+
26
+ const ACTION_COLOR: Record<string, string> = {
27
+ created: "bg-blue-500",
28
+ validated: "bg-amber-500",
29
+ deployed: "bg-emerald-500",
30
+ rejected: "bg-red-500",
31
+ rolled_back: "bg-red-400",
32
+ }
33
+
34
+ const ACTION_RING: Record<string, string> = {
35
+ created: "ring-blue-500/30",
36
+ validated: "ring-amber-500/30",
37
+ deployed: "ring-emerald-500/30",
38
+ rejected: "ring-red-500/30",
39
+ rolled_back: "ring-red-400/30",
40
+ }
41
+
42
+ const ACTION_LINE: Record<string, string> = {
43
+ created: "bg-blue-500/30",
44
+ validated: "bg-amber-500/30",
45
+ deployed: "bg-emerald-500/30",
46
+ rejected: "bg-red-500/30",
47
+ rolled_back: "bg-red-400/30",
48
+ }
49
+
50
+ interface Props {
51
+ entries: EvolutionEntry[]
52
+ selectedProposalId: string | null
53
+ onSelect: (proposalId: string) => void
54
+ }
55
+
56
+ /** Group evolution entries by proposal_id, ordered newest-first. */
57
+ function groupByProposal(entries: EvolutionEntry[]) {
58
+ const map = new Map<string, EvolutionEntry[]>()
59
+ for (const e of entries) {
60
+ const group = map.get(e.proposal_id) ?? []
61
+ group.push(e)
62
+ map.set(e.proposal_id, group)
63
+ }
64
+ for (const group of map.values()) {
65
+ group.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
66
+ }
67
+ return Array.from(map.entries()).sort((a, b) => {
68
+ const aLast = a[1][a[1].length - 1]
69
+ const bLast = b[1][b[1].length - 1]
70
+ return new Date(bLast.timestamp).getTime() - new Date(aLast.timestamp).getTime()
71
+ })
72
+ }
73
+
74
+ function terminalAction(entries: EvolutionEntry[]): string {
75
+ return entries[entries.length - 1].action
76
+ }
77
+
78
+ /** Find the best eval_snapshot across all steps in a proposal group */
79
+ function findEvalSnapshot(steps: EvolutionEntry[]): EvalSnapshot | null {
80
+ for (let i = steps.length - 1; i >= 0; i--) {
81
+ if (steps[i].eval_snapshot) return steps[i].eval_snapshot!
82
+ }
83
+ return null
84
+ }
85
+
86
+ function PassRateDelta({ snapshot }: { snapshot: EvalSnapshot }) {
87
+ const net = snapshot.net_change
88
+ if (net === undefined || net === null) return null
89
+ const pct = Math.round(net * 100)
90
+ const isPositive = pct > 0
91
+ return (
92
+ <span className={cn(
93
+ "inline-flex items-center gap-0.5 text-[10px] font-mono font-medium",
94
+ isPositive ? "text-emerald-600 dark:text-emerald-400" : "text-red-500",
95
+ )}>
96
+ {isPositive ? <TrendingUpIcon className="size-2.5" /> : <TrendingDownIcon className="size-2.5" />}
97
+ {isPositive ? "+" : ""}{pct}%
98
+ </span>
99
+ )
100
+ }
101
+
102
+ const LIFECYCLE_STEPS = [
103
+ { action: "created", label: "Created", desc: "Proposal generated from session data" },
104
+ { action: "validated", label: "Validated", desc: "Eval tests run, awaiting deployment" },
105
+ { action: "deployed", label: "Deployed", desc: "Accepted and applied to skill file" },
106
+ { action: "rejected", label: "Rejected", desc: "Failed validation criteria" },
107
+ { action: "rolled_back", label: "Rolled Back", desc: "Reverted after deployment" },
108
+ ]
109
+
110
+ function LifecycleLegend() {
111
+ const [open, setOpen] = useState(false)
112
+
113
+ return (
114
+ <div className="px-2 pb-2">
115
+ <button
116
+ type="button"
117
+ onClick={() => setOpen(!open)}
118
+ className="flex items-center gap-1 text-[10px] text-muted-foreground/70 hover:text-muted-foreground transition-colors w-full"
119
+ >
120
+ {open ? <ChevronDownIcon className="size-3" /> : <ChevronRightIcon className="size-3" />}
121
+ Lifecycle stages
122
+ </button>
123
+ {open && (
124
+ <div className="mt-1.5 space-y-1.5 rounded-md border bg-muted/30 p-2">
125
+ {LIFECYCLE_STEPS.map((step) => (
126
+ <div key={step.action} className="flex items-start gap-2">
127
+ <div className={cn(
128
+ "size-2 rounded-full mt-1 shrink-0",
129
+ ACTION_COLOR[step.action],
130
+ )} />
131
+ <div className="min-w-0">
132
+ <span className="text-[10px] font-medium">{step.label}</span>
133
+ <p className="text-[10px] text-muted-foreground/70 leading-tight">{step.desc}</p>
134
+ </div>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ )}
139
+ </div>
140
+ )
141
+ }
142
+
143
+ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Props) {
144
+ const groups = groupByProposal(entries)
145
+
146
+ if (groups.length === 0) {
147
+ return (
148
+ <div className="flex items-center justify-center rounded-lg border border-dashed py-6 px-3">
149
+ <p className="text-xs text-muted-foreground">No evolution history yet</p>
150
+ </div>
151
+ )
152
+ }
153
+
154
+ return (
155
+ <div className="flex flex-col gap-0">
156
+ <h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2 pb-2 sticky top-0 z-10 bg-background">
157
+ Evolution
158
+ </h2>
159
+ <LifecycleLegend />
160
+ <nav className="flex flex-col">
161
+ {groups.map(([proposalId, steps], groupIdx) => {
162
+ const terminal = terminalAction(steps)
163
+ const isSelected = selectedProposalId === proposalId
164
+ const lastStep = steps[steps.length - 1]
165
+ const dotColor = ACTION_COLOR[terminal] ?? "bg-muted-foreground"
166
+ const ringColor = ACTION_RING[terminal] ?? "ring-muted-foreground/30"
167
+ const lineColor = ACTION_LINE[terminal] ?? "bg-border"
168
+ const isLast = groupIdx === groups.length - 1
169
+ const snapshot = findEvalSnapshot(steps)
170
+
171
+ return (
172
+ <div key={proposalId} className="relative flex gap-3">
173
+ {/* Vertical connector line */}
174
+ <div className="flex flex-col items-center">
175
+ <div className={cn(
176
+ "flex items-center justify-center size-7 rounded-full ring-2 text-white shrink-0 z-10",
177
+ dotColor,
178
+ ringColor,
179
+ )}>
180
+ {ACTION_ICON[terminal] ?? <CircleDotIcon className="size-3.5" />}
181
+ </div>
182
+ {!isLast && (
183
+ <div className={cn("w-0.5 flex-1 min-h-[16px]", lineColor)} />
184
+ )}
185
+ </div>
186
+
187
+ {/* Content */}
188
+ <button
189
+ type="button"
190
+ onClick={() => onSelect(proposalId)}
191
+ className={cn(
192
+ "flex-1 min-w-0 rounded-md px-2.5 py-2 text-left transition-all mb-1",
193
+ "hover:bg-accent/50",
194
+ isSelected
195
+ ? "bg-primary/5 ring-1 ring-primary/20"
196
+ : "",
197
+ )}
198
+ >
199
+ <div className="flex items-center gap-1.5">
200
+ <Badge
201
+ variant={terminal === "deployed" ? "default" : terminal === "rejected" || terminal === "rolled_back" ? "destructive" : "secondary"}
202
+ className="text-[10px] capitalize"
203
+ >
204
+ {terminal.replace("_", " ")}
205
+ </Badge>
206
+ <span className="text-[10px] text-muted-foreground">
207
+ {timeAgo(lastStep.timestamp)}
208
+ </span>
209
+ </div>
210
+ {/* Pass rate delta from eval snapshot */}
211
+ {snapshot && (
212
+ <div className="flex items-center gap-1.5 mt-1">
213
+ <PassRateDelta snapshot={snapshot} />
214
+ {snapshot.before_pass_rate !== undefined && snapshot.after_pass_rate !== undefined && (
215
+ <span className="text-[10px] text-muted-foreground/60 font-mono">
216
+ {Math.round(snapshot.before_pass_rate * 100)}&rarr;{Math.round(snapshot.after_pass_rate * 100)}%
217
+ </span>
218
+ )}
219
+ </div>
220
+ )}
221
+ <div className="flex items-center gap-1.5 mt-1">
222
+ <span className="text-[10px] font-mono text-muted-foreground/70">
223
+ #{proposalId.slice(0, 8)}
224
+ </span>
225
+ {/* Step dots */}
226
+ {steps.length > 1 && (
227
+ <div className="flex gap-0.5 ml-auto">
228
+ {steps.map((s, i) => (
229
+ <div
230
+ key={`${s.action}-${i}`}
231
+ className={cn(
232
+ "size-1.5 rounded-full",
233
+ ACTION_COLOR[s.action] ?? "bg-muted-foreground",
234
+ )}
235
+ />
236
+ ))}
237
+ </div>
238
+ )}
239
+ </div>
240
+ {lastStep.details && (
241
+ <p className="text-[11px] text-muted-foreground/80 line-clamp-2 mt-1 leading-snug">
242
+ {lastStep.details}
243
+ </p>
244
+ )}
245
+ </button>
246
+ </div>
247
+ )
248
+ })}
249
+ </nav>
250
+ </div>
251
+ )
252
+ }
@@ -0,0 +1,19 @@
1
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../primitives/tooltip"
2
+ import { InfoIcon } from "lucide-react"
3
+
4
+ /** Small info icon that shows a tooltip on hover. Used to explain metrics and concepts. */
5
+ export function InfoTip({ text }: { text: string }) {
6
+ return (
7
+ <Tooltip>
8
+ <TooltipTrigger
9
+ className="inline-flex items-center text-muted-foreground/50 hover:text-muted-foreground transition-colors cursor-help"
10
+ onClick={(e) => e.preventDefault()}
11
+ >
12
+ <InfoIcon className="size-3" />
13
+ </TooltipTrigger>
14
+ <TooltipContent side="top" className="max-w-[220px]">
15
+ {text}
16
+ </TooltipContent>
17
+ </Tooltip>
18
+ )
19
+ }
@@ -0,0 +1,164 @@
1
+ import { useState } from "react"
2
+ import { Badge } from "../primitives/badge"
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "../primitives/card"
10
+ import {
11
+ Collapsible,
12
+ CollapsibleContent,
13
+ CollapsibleTrigger,
14
+ } from "../primitives/collapsible"
15
+ import type { OrchestrateRunReport, OrchestrateRunSkillAction } from "../types"
16
+ import { timeAgo } from "../lib/format"
17
+ import {
18
+ BotIcon,
19
+ CheckCircleIcon,
20
+ ChevronRightIcon,
21
+ EyeIcon,
22
+ SkipForwardIcon,
23
+ ZapIcon,
24
+ } from "lucide-react"
25
+
26
+ const ACTION_ICON: Record<string, React.ReactNode> = {
27
+ evolve: <ZapIcon className="size-3 text-amber-500" />,
28
+ watch: <EyeIcon className="size-3 text-blue-500" />,
29
+ skip: <SkipForwardIcon className="size-3 text-muted-foreground" />,
30
+ }
31
+
32
+ function SkillActionRow({ action }: { action: OrchestrateRunSkillAction }) {
33
+ return (
34
+ <div className="flex items-start gap-2 py-1">
35
+ <div className="mt-0.5 shrink-0">{ACTION_ICON[action.action] ?? null}</div>
36
+ <div className="flex-1 min-w-0">
37
+ <div className="flex items-center gap-1.5">
38
+ <span className="text-xs font-medium truncate">{action.skill}</span>
39
+ <Badge
40
+ variant={
41
+ action.rolledBack ? "destructive"
42
+ : action.action === "evolve" && action.deployed ? "default"
43
+ : action.action === "evolve" ? "secondary"
44
+ : action.action === "watch" ? "outline"
45
+ : "secondary"
46
+ }
47
+ className="text-[10px] h-4 px-1.5 shrink-0"
48
+ >
49
+ {action.rolledBack ? "rolled back"
50
+ : action.action === "evolve" && action.deployed ? "deployed"
51
+ : action.action === "evolve" ? "evolved"
52
+ : action.action}
53
+ </Badge>
54
+ {action.alert && (
55
+ <Badge variant="destructive" className="text-[10px] h-4 px-1.5 shrink-0">
56
+ alert
57
+ </Badge>
58
+ )}
59
+ </div>
60
+ <p className="text-[11px] text-muted-foreground line-clamp-1">{action.reason}</p>
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ function RunCard({ run }: { run: OrchestrateRunReport }) {
67
+ const [open, setOpen] = useState(false)
68
+ const nonSkipActions = run.skill_actions.filter((a) => a.action !== "skip")
69
+ const skipActions = run.skill_actions.filter((a) => a.action === "skip")
70
+
71
+ return (
72
+ <Collapsible open={open} onOpenChange={setOpen}>
73
+ <CollapsibleTrigger className="w-full text-left">
74
+ <div className="flex items-start gap-3 py-2 hover:bg-muted/50 rounded-md px-2 -mx-2 transition-colors">
75
+ <div className={`mt-1.5 size-2 shrink-0 rounded-full ${
76
+ run.deployed > 0 ? "bg-emerald-500"
77
+ : run.evolved > 0 ? "bg-amber-400"
78
+ : "bg-muted-foreground/40"
79
+ }`} />
80
+ <div className="flex-1 min-w-0">
81
+ <div className="flex items-center gap-2">
82
+ <span className="text-xs font-mono text-muted-foreground">{timeAgo(run.timestamp)}</span>
83
+ {run.dry_run && (
84
+ <Badge variant="outline" className="text-[10px] h-4 px-1.5">dry-run</Badge>
85
+ )}
86
+ {run.approval_mode === "review" && (
87
+ <Badge variant="outline" className="text-[10px] h-4 px-1.5">review</Badge>
88
+ )}
89
+ </div>
90
+ <div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
91
+ {run.deployed > 0 && <span className="text-emerald-600 font-medium">{run.deployed} deployed</span>}
92
+ {run.evolved > 0 && <span>{run.evolved} evolved</span>}
93
+ {run.watched > 0 && <span>{run.watched} watched</span>}
94
+ {run.skipped > 0 && <span>{run.skipped} skipped</span>}
95
+ <span>{(run.elapsed_ms / 1000).toFixed(1)}s</span>
96
+ </div>
97
+ </div>
98
+ <ChevronRightIcon className={`size-4 text-muted-foreground shrink-0 mt-1 transition-transform duration-200 ${open ? "rotate-90" : ""}`} />
99
+ </div>
100
+ </CollapsibleTrigger>
101
+ <CollapsibleContent>
102
+ <div className="ml-5 pl-3 border-l border-border space-y-0.5 pb-2">
103
+ {nonSkipActions.map((action, i) => (
104
+ <SkillActionRow key={`${action.skill}-${i}`} action={action} />
105
+ ))}
106
+ {skipActions.length > 0 && (
107
+ <details className="group">
108
+ <summary className="text-[11px] text-muted-foreground cursor-pointer hover:text-foreground py-1">
109
+ {skipActions.length} skipped
110
+ </summary>
111
+ <div className="space-y-0.5">
112
+ {skipActions.map((action, i) => (
113
+ <SkillActionRow key={`${action.skill}-skip-${i}`} action={action} />
114
+ ))}
115
+ </div>
116
+ </details>
117
+ )}
118
+ </div>
119
+ </CollapsibleContent>
120
+ </Collapsible>
121
+ )
122
+ }
123
+
124
+ export function OrchestrateRunsPanel({ runs }: { runs: OrchestrateRunReport[] }) {
125
+ if (runs.length === 0) {
126
+ return (
127
+ <Card>
128
+ <CardHeader>
129
+ <CardTitle className="flex items-center gap-2 text-sm">
130
+ <BotIcon className="size-4" />
131
+ Orchestrate Runs
132
+ </CardTitle>
133
+ </CardHeader>
134
+ <CardContent>
135
+ <p className="text-sm text-muted-foreground text-center py-4">
136
+ No orchestrate runs yet. Run <code className="text-xs bg-muted px-1 py-0.5 rounded">selftune orchestrate</code> to start.
137
+ </p>
138
+ </CardContent>
139
+ </Card>
140
+ )
141
+ }
142
+
143
+ const totalDeployed = runs.reduce((sum, r) => sum + r.deployed, 0)
144
+
145
+ return (
146
+ <Card>
147
+ <CardHeader>
148
+ <CardTitle className="flex items-center gap-2 text-sm">
149
+ <BotIcon className="size-4" />
150
+ Orchestrate Runs
151
+ </CardTitle>
152
+ <CardDescription>
153
+ {runs.length} recent run{runs.length !== 1 ? "s" : ""}
154
+ {totalDeployed > 0 && <> &middot; {totalDeployed} total deployments</>}
155
+ </CardDescription>
156
+ </CardHeader>
157
+ <CardContent className="space-y-0">
158
+ {runs.slice(0, 10).map((run) => (
159
+ <RunCard key={run.run_id} run={run} />
160
+ ))}
161
+ </CardContent>
162
+ </Card>
163
+ )
164
+ }
@@ -0,0 +1,7 @@
1
+ export { ActivityPanel } from "./ActivityTimeline";
2
+ export { EvidenceViewer } from "./EvidenceViewer";
3
+ export { EvolutionTimeline } from "./EvolutionTimeline";
4
+ export { InfoTip } from "./InfoTip";
5
+ export { OrchestrateRunsPanel } from "./OrchestrateRunsPanel";
6
+ export { SectionCards } from "./section-cards";
7
+ export { SkillHealthGrid } from "./skill-health-grid";
@@ -0,0 +1,155 @@
1
+ import { Badge } from "../primitives/badge"
2
+ import {
3
+ Card,
4
+ CardAction,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "../primitives/card"
9
+ import { InfoTip } from "./InfoTip"
10
+ import {
11
+ TrendingUpIcon,
12
+ TrendingDownIcon,
13
+ AlertTriangleIcon,
14
+ ActivityIcon,
15
+ EyeIcon,
16
+ FlaskConicalIcon,
17
+ LayersIcon,
18
+ SearchXIcon,
19
+ } from "lucide-react"
20
+
21
+ interface SectionCardsProps {
22
+ skillsCount: number
23
+ avgPassRate: number | null
24
+ unmatchedCount: number
25
+ sessionsCount: number
26
+ pendingCount: number
27
+ evidenceCount: number
28
+ }
29
+
30
+ export function SectionCards({
31
+ skillsCount,
32
+ avgPassRate,
33
+ unmatchedCount,
34
+ sessionsCount,
35
+ pendingCount,
36
+ evidenceCount,
37
+ }: SectionCardsProps) {
38
+ const passRateStr = avgPassRate !== null ? `${Math.round(avgPassRate * 100)}%` : "--"
39
+ const passRateGood = avgPassRate !== null && avgPassRate >= 0.7
40
+
41
+ return (
42
+ <div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-3">
43
+ <Card className="@container/card">
44
+ <CardHeader>
45
+ <CardDescription className="flex items-center gap-1.5">
46
+ <LayersIcon className="size-3.5" />
47
+ Skills Monitored
48
+ <InfoTip text="Total number of skills detected and being tracked by selftune" />
49
+ </CardDescription>
50
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
51
+ {skillsCount}
52
+ </CardTitle>
53
+ <CardAction>
54
+ <Badge variant="outline">
55
+ <ActivityIcon className="size-3" />
56
+ live
57
+ </Badge>
58
+ </CardAction>
59
+ </CardHeader>
60
+ </Card>
61
+
62
+ <Card className="@container/card">
63
+ <CardHeader>
64
+ <CardDescription className="flex items-center gap-1.5">
65
+ <FlaskConicalIcon className="size-3.5" />
66
+ Avg Pass Rate
67
+ <InfoTip text="Average percentage of eval test cases that passed across all graded skills (5+ checks)" />
68
+ </CardDescription>
69
+ <CardTitle className={`text-2xl font-semibold tabular-nums @[250px]/card:text-3xl ${!passRateGood && avgPassRate !== null ? "text-red-600" : ""}`}>
70
+ {passRateStr}
71
+ </CardTitle>
72
+ {avgPassRate !== null && (
73
+ <CardAction>
74
+ <Badge variant={passRateGood ? "outline" : "destructive"}>
75
+ {passRateGood ? (
76
+ <TrendingUpIcon className="size-3" />
77
+ ) : (
78
+ <TrendingDownIcon className="size-3" />
79
+ )}
80
+ {passRateStr}
81
+ </Badge>
82
+ </CardAction>
83
+ )}
84
+ </CardHeader>
85
+ </Card>
86
+
87
+ <Card className="@container/card">
88
+ <CardHeader>
89
+ <CardDescription className="flex items-center gap-1.5">
90
+ <SearchXIcon className="size-3.5" />
91
+ Unmatched Queries
92
+ <InfoTip text="User prompts that didn't match any skill's trigger criteria — potential gaps in coverage" />
93
+ </CardDescription>
94
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
95
+ {unmatchedCount}
96
+ </CardTitle>
97
+ {unmatchedCount > 0 && (
98
+ <CardAction>
99
+ <Badge variant="destructive">
100
+ <AlertTriangleIcon className="size-3" />
101
+ needs attention
102
+ </Badge>
103
+ </CardAction>
104
+ )}
105
+ </CardHeader>
106
+ </Card>
107
+
108
+ <Card className="@container/card">
109
+ <CardHeader>
110
+ <CardDescription className="flex items-center gap-1.5">
111
+ <ActivityIcon className="size-3.5" />
112
+ Sessions
113
+ <InfoTip text="Total agent sessions that have been recorded and analyzed" />
114
+ </CardDescription>
115
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
116
+ {sessionsCount}
117
+ </CardTitle>
118
+ </CardHeader>
119
+ </Card>
120
+
121
+ <Card className="@container/card">
122
+ <CardHeader>
123
+ <CardDescription className="flex items-center gap-1.5">
124
+ <AlertTriangleIcon className="size-3.5" />
125
+ Pending Proposals
126
+ <InfoTip text="Evolution proposals that have been generated but not yet validated or deployed" />
127
+ </CardDescription>
128
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
129
+ {pendingCount}
130
+ </CardTitle>
131
+ {pendingCount > 0 && (
132
+ <CardAction>
133
+ <Badge variant="secondary">
134
+ awaiting review
135
+ </Badge>
136
+ </CardAction>
137
+ )}
138
+ </CardHeader>
139
+ </Card>
140
+
141
+ <Card className="@container/card">
142
+ <CardHeader>
143
+ <CardDescription className="flex items-center gap-1.5">
144
+ <EyeIcon className="size-3.5" />
145
+ Total Evidence
146
+ <InfoTip text="Number of evidence entries documenting skill changes with before/after validation results" />
147
+ </CardDescription>
148
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
149
+ {evidenceCount}
150
+ </CardTitle>
151
+ </CardHeader>
152
+ </Card>
153
+ </div>
154
+ )
155
+ }