selftune 0.2.22 → 0.2.23

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 (94) hide show
  1. package/README.md +4 -2
  2. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
  3. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/adapters/pi/hook.ts +273 -0
  7. package/cli/selftune/adapters/pi/install.ts +207 -0
  8. package/cli/selftune/constants.ts +10 -1
  9. package/cli/selftune/dashboard-contract.ts +14 -0
  10. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  11. package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
  12. package/cli/selftune/evolution/evidence.ts +2 -6
  13. package/cli/selftune/evolution/evolve-body.ts +73 -20
  14. package/cli/selftune/evolution/validate-body.ts +78 -42
  15. package/cli/selftune/evolution/validate-routing.ts +45 -104
  16. package/cli/selftune/hooks/skill-eval.ts +2 -1
  17. package/cli/selftune/hooks-shared/types.ts +1 -0
  18. package/cli/selftune/index.ts +23 -5
  19. package/cli/selftune/ingestors/pi-ingest.ts +726 -0
  20. package/cli/selftune/init.ts +11 -1
  21. package/cli/selftune/localdb/direct-write.ts +85 -0
  22. package/cli/selftune/localdb/materialize.ts +6 -7
  23. package/cli/selftune/localdb/queries.ts +126 -0
  24. package/cli/selftune/localdb/schema.ts +38 -0
  25. package/cli/selftune/observability.ts +8 -1
  26. package/cli/selftune/orchestrate.ts +43 -0
  27. package/cli/selftune/registry/client.ts +74 -0
  28. package/cli/selftune/registry/history.ts +54 -0
  29. package/cli/selftune/registry/index.ts +90 -0
  30. package/cli/selftune/registry/install.ts +141 -0
  31. package/cli/selftune/registry/list.ts +44 -0
  32. package/cli/selftune/registry/push.ts +171 -0
  33. package/cli/selftune/registry/rollback.ts +49 -0
  34. package/cli/selftune/registry/status.ts +62 -0
  35. package/cli/selftune/registry/sync.ts +125 -0
  36. package/cli/selftune/repair/skill-usage.ts +4 -1
  37. package/cli/selftune/status.ts +31 -0
  38. package/cli/selftune/sync.ts +127 -23
  39. package/cli/selftune/types.ts +2 -1
  40. package/cli/selftune/utils/jsonl.ts +1 -30
  41. package/cli/selftune/utils/skill-discovery.ts +22 -0
  42. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  43. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  44. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  45. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  46. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  47. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
  48. package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
  49. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  50. package/package.json +1 -1
  51. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  52. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  53. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  54. package/packages/telemetry-contract/package.json +1 -1
  55. package/packages/telemetry-contract/src/index.ts +1 -0
  56. package/packages/telemetry-contract/src/schemas.ts +22 -4
  57. package/packages/telemetry-contract/src/types.ts +1 -12
  58. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  59. package/packages/ui/AGENTS.md +16 -0
  60. package/packages/ui/README.md +1 -1
  61. package/packages/ui/package.json +1 -1
  62. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  63. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  64. package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
  65. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  66. package/packages/ui/src/components/InfoTip.tsx +1 -2
  67. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  68. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  69. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  70. package/packages/ui/src/components/OverviewPanels.tsx +652 -0
  71. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  72. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  73. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  74. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  75. package/packages/ui/src/components/index.ts +56 -1
  76. package/packages/ui/src/components/section-cards.tsx +18 -35
  77. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  78. package/packages/ui/src/lib/constants.tsx +0 -1
  79. package/packages/ui/src/primitives/card.tsx +1 -1
  80. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  81. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  82. package/packages/ui/src/primitives/select.tsx +2 -2
  83. package/packages/ui/src/types.ts +172 -4
  84. package/skill/SKILL.md +18 -4
  85. package/skill/Workflows/Ingest.md +60 -2
  86. package/skill/Workflows/Initialize.md +8 -5
  87. package/skill/Workflows/PlatformHooks.md +19 -3
  88. package/skill/Workflows/Registry.md +99 -0
  89. package/skill/Workflows/Sync.md +3 -1
  90. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  91. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  92. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  93. package/cli/selftune/utils/html.ts +0 -27
  94. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
@@ -0,0 +1,652 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { CheckCircleIcon, ChevronDownIcon } from "lucide-react";
5
+
6
+ import { Badge } from "../primitives/badge";
7
+ import {
8
+ Card,
9
+ CardAction,
10
+ CardContent,
11
+ CardDescription,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from "../primitives/card";
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
16
+ import { timeAgo } from "../lib/format";
17
+ import type {
18
+ AttentionItem,
19
+ AttentionSeverity,
20
+ AutonomousDecision,
21
+ AutonomyStatus,
22
+ AutonomyStatusLevel,
23
+ DecisionKind,
24
+ EvolutionEntry,
25
+ TrustBucket,
26
+ TrustWatchlistEntry,
27
+ } from "../types";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Shared constants
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const STATUS_DOT: Record<AutonomyStatusLevel, { color: string; glow: string }> = {
34
+ healthy: {
35
+ color: "bg-emerald-400",
36
+ glow: "shadow-[0_0_12px_rgba(52,211,153,0.6)]",
37
+ },
38
+ watching: {
39
+ color: "bg-cyan-400",
40
+ glow: "shadow-[0_0_12px_rgba(79,242,255,0.6)]",
41
+ },
42
+ needs_review: {
43
+ color: "bg-amber-400",
44
+ glow: "shadow-[0_0_12px_rgba(251,191,36,0.6)]",
45
+ },
46
+ blocked: {
47
+ color: "bg-red-400",
48
+ glow: "shadow-[0_0_12px_rgba(248,113,113,0.6)]",
49
+ },
50
+ };
51
+
52
+ const STATUS_LABELS: Record<AutonomyStatusLevel, string> = {
53
+ healthy: "Healthy",
54
+ watching: "Watching",
55
+ needs_review: "Needs Review",
56
+ blocked: "Blocked",
57
+ };
58
+
59
+ const SEVERITY: Record<AttentionSeverity, { dot: string; text: string; bg: string }> = {
60
+ critical: {
61
+ dot: "bg-red-400",
62
+ text: "text-red-400",
63
+ bg: "bg-red-500/10",
64
+ },
65
+ warning: {
66
+ dot: "bg-amber-400",
67
+ text: "text-amber-400",
68
+ bg: "bg-amber-500/10",
69
+ },
70
+ info: {
71
+ dot: "bg-cyan-400",
72
+ text: "text-primary",
73
+ bg: "bg-cyan-400/10",
74
+ },
75
+ };
76
+
77
+ const DECISION_MARKERS: Record<DecisionKind, string> = {
78
+ proposal_created: "bg-cyan-400",
79
+ proposal_rejected: "bg-red-400",
80
+ validation_failed: "bg-amber-400",
81
+ proposal_deployed: "bg-emerald-400",
82
+ rollback_triggered: "bg-red-400",
83
+ regression_found: "bg-amber-400",
84
+ };
85
+
86
+ const BUCKET_CFG: Record<TrustBucket, { label: string; accent: string; dot: string }> = {
87
+ at_risk: { label: "At Risk", accent: "text-red-400", dot: "bg-red-400" },
88
+ improving: { label: "Improving", accent: "text-primary", dot: "bg-cyan-400" },
89
+ uncertain: { label: "Uncertain", accent: "text-amber-400", dot: "bg-amber-400" },
90
+ stable: {
91
+ label: "Stable",
92
+ accent: "text-muted-foreground",
93
+ dot: "bg-muted-foreground/60",
94
+ },
95
+ };
96
+
97
+ // Ambient bar heights for hero background
98
+ const BARS = [35, 55, 40, 70, 45, 80, 30, 65, 50, 75, 38, 60, 42, 72];
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // AutonomyHeroCard
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export interface AutonomyHeroCardProps {
105
+ status: AutonomyStatus;
106
+ lastRun: string | null;
107
+ }
108
+
109
+ export function AutonomyHeroCard({ status, lastRun }: AutonomyHeroCardProps) {
110
+ const dot = STATUS_DOT[status.level];
111
+ const primaryStat =
112
+ status.attention_required > 0
113
+ ? { value: status.attention_required, label: "Attention Required" }
114
+ : { value: status.skills_observed, label: "Skills Observed" };
115
+
116
+ return (
117
+ <Card className="relative min-h-[332px] border-none bg-gradient-to-br from-muted via-muted to-primary/5 shadow-none py-0 ring-0">
118
+ {/* Ambient bars */}
119
+ <div className="absolute inset-0 flex items-end justify-around px-8 pb-24 pt-20 opacity-[0.08] pointer-events-none">
120
+ {BARS.map((h, i) => (
121
+ <div
122
+ key={i}
123
+ className="flex-1 rounded-t-sm min-w-[12px]"
124
+ style={{
125
+ height: `${h}%`,
126
+ backgroundColor: `rgba(79, 242, 255, ${0.15 + (h / 100) * 0.3})`,
127
+ }}
128
+ />
129
+ ))}
130
+ </div>
131
+
132
+ {/* Top: status + primary stat */}
133
+ <CardHeader className="relative z-10 px-8 pt-8 pb-0">
134
+ <div className="flex items-start gap-3">
135
+ <span
136
+ className={`mt-2 size-3.5 shrink-0 rounded-full animate-pulse ${dot.color} ${dot.glow}`}
137
+ />
138
+ <div>
139
+ <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
140
+ Autonomy Status
141
+ </p>
142
+ <CardTitle className="text-2xl font-extrabold tracking-tight text-foreground">
143
+ {STATUS_LABELS[status.level]}
144
+ </CardTitle>
145
+ <CardDescription className="mt-1.5 max-w-xl text-[13px] leading-relaxed text-muted-foreground">
146
+ {status.summary}
147
+ </CardDescription>
148
+ </div>
149
+ </div>
150
+ <CardAction>
151
+ <div className="text-right shrink-0">
152
+ <p
153
+ className="text-5xl font-extrabold text-primary leading-none"
154
+ style={{ filter: "drop-shadow(0 0 8px rgba(79,242,255,0.3))" }}
155
+ >
156
+ {primaryStat.value}
157
+ </p>
158
+ <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground mt-1.5">
159
+ {primaryStat.label}
160
+ </p>
161
+ </div>
162
+ </CardAction>
163
+ </CardHeader>
164
+
165
+ {/* Spacer */}
166
+ <div className="flex-1 min-h-6" />
167
+
168
+ {/* Bottom: compact stat chips */}
169
+ <CardContent className="relative z-10 flex flex-col gap-5 px-8 pb-8 pt-0">
170
+ <div className="flex flex-wrap items-center gap-2.5 text-xs">
171
+ <div className="rounded-full border border-border/40 bg-background/60 px-3 py-1.5 backdrop-blur-sm shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]">
172
+ <span className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground/70">
173
+ Last Run
174
+ </span>
175
+ <span className="ml-2 font-medium text-foreground">
176
+ {lastRun ? timeAgo(lastRun) : "Never"}
177
+ </span>
178
+ </div>
179
+ <div className="rounded-full border border-border/40 bg-background/60 px-3 py-1.5 backdrop-blur-sm shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]">
180
+ <span className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground/70">
181
+ Skills
182
+ </span>
183
+ <span className="ml-2 font-medium text-foreground">{status.skills_observed}</span>
184
+ </div>
185
+ <div className="rounded-full border border-primary/20 bg-primary/10 px-3 py-1.5 backdrop-blur-sm">
186
+ <span className="text-[10px] uppercase tracking-[0.18em] text-primary/80">Pending</span>
187
+ <span className="ml-2 font-semibold text-primary">{status.pending_reviews}</span>
188
+ </div>
189
+ </div>
190
+
191
+ {status.attention_required > 0 ? (
192
+ <a
193
+ href="#supervision-feed"
194
+ className="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/80 w-fit"
195
+ >
196
+ Review Attention Queue
197
+ </a>
198
+ ) : (
199
+ <span className="text-sm text-muted-foreground/70">No action needed</span>
200
+ )}
201
+ </CardContent>
202
+ </Card>
203
+ );
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // TrustWatchlistRail
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export interface TrustWatchlistRailProps {
211
+ entries: TrustWatchlistEntry[];
212
+ /** Optional render prop for skill name links */
213
+ renderSkillLink?: (skillName: string) => React.ReactNode;
214
+ }
215
+
216
+ export function TrustWatchlistRail({ entries, renderSkillLink }: TrustWatchlistRailProps) {
217
+ const buckets = useMemo(() => {
218
+ const order: TrustBucket[] = ["at_risk", "improving", "uncertain", "stable"];
219
+ const grouped: Record<TrustBucket, TrustWatchlistEntry[]> = {
220
+ at_risk: [],
221
+ improving: [],
222
+ uncertain: [],
223
+ stable: [],
224
+ };
225
+ for (const e of entries) grouped[e.bucket].push(e);
226
+ return order
227
+ .filter((b) => grouped[b].length > 0)
228
+ .map((b) => ({ bucket: b, items: grouped[b] }));
229
+ }, [entries]);
230
+
231
+ return (
232
+ <Card className="border-none bg-muted shadow-none py-0 max-h-[360px] ring-0">
233
+ <CardHeader className="px-5 pt-5 pb-0">
234
+ <div>
235
+ <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
236
+ Trust Watchlist
237
+ </p>
238
+ <CardDescription className="mt-1 text-[11px] text-muted-foreground/70">
239
+ Highest-risk skills worth checking next.
240
+ </CardDescription>
241
+ </div>
242
+ <CardAction>
243
+ <span className="text-[10px] text-muted-foreground/70 shrink-0">
244
+ {entries.length} skills
245
+ </span>
246
+ </CardAction>
247
+ </CardHeader>
248
+
249
+ {buckets.length === 0 ? (
250
+ <CardContent className="flex flex-1 items-center justify-center px-5 py-4">
251
+ <p className="text-xs text-muted-foreground/70">No skills tracked yet.</p>
252
+ </CardContent>
253
+ ) : (
254
+ <CardContent className="space-y-3 overflow-y-auto min-h-0 flex-1 px-5 py-4">
255
+ {buckets.map(({ bucket, items }) => (
256
+ <RailBucket
257
+ key={bucket}
258
+ bucket={bucket}
259
+ items={items}
260
+ renderSkillLink={renderSkillLink}
261
+ />
262
+ ))}
263
+ </CardContent>
264
+ )}
265
+ </Card>
266
+ );
267
+ }
268
+
269
+ function RailBucket({
270
+ bucket,
271
+ items,
272
+ renderSkillLink,
273
+ }: {
274
+ bucket: TrustBucket;
275
+ items: TrustWatchlistEntry[];
276
+ renderSkillLink?: (skillName: string) => React.ReactNode;
277
+ }) {
278
+ const cfg = BUCKET_CFG[bucket];
279
+ const [open, setOpen] = useState(false);
280
+ const MAX = 5;
281
+ const [showAll, setShowAll] = useState(false);
282
+ const visible = open ? (showAll ? items : items.slice(0, MAX)) : [];
283
+
284
+ return (
285
+ <div className="rounded-xl bg-background/40 px-3 py-2.5">
286
+ <button
287
+ type="button"
288
+ onClick={() => setOpen(!open)}
289
+ className="flex w-full items-center gap-1.5 text-left"
290
+ >
291
+ <span className={`size-1.5 shrink-0 rounded-full ${cfg.dot}`} />
292
+ <ChevronDownIcon
293
+ className={`size-3 text-muted-foreground transition-transform ${open ? "" : "-rotate-90"}`}
294
+ />
295
+ <span className={`text-xs font-medium ${cfg.accent}`}>{cfg.label}</span>
296
+ <span className="text-[10px] text-muted-foreground/70">({items.length})</span>
297
+ </button>
298
+ {open && (
299
+ <div className="mt-2 space-y-1">
300
+ {visible.map((e) => (
301
+ <div
302
+ key={e.skill_name}
303
+ className="rounded-lg px-2 py-1.5 transition-colors hover:bg-background/55"
304
+ >
305
+ <div className="flex items-baseline justify-between gap-2">
306
+ {renderSkillLink ? (
307
+ renderSkillLink(e.skill_name)
308
+ ) : (
309
+ <span className="text-[11px] font-medium text-foreground truncate">
310
+ {e.skill_name}
311
+ </span>
312
+ )}
313
+ {e.pass_rate != null && (
314
+ <span className="text-[10px] text-muted-foreground shrink-0">
315
+ {Math.round(e.pass_rate * 100)}%
316
+ </span>
317
+ )}
318
+ </div>
319
+ <p className="mt-0.5 line-clamp-1 text-[10px] text-muted-foreground/70">{e.reason}</p>
320
+ </div>
321
+ ))}
322
+ {items.length > MAX && !showAll && (
323
+ <button
324
+ type="button"
325
+ onClick={() => setShowAll(true)}
326
+ className="pl-2 text-[10px] text-primary hover:underline"
327
+ >
328
+ +{items.length - MAX} more
329
+ </button>
330
+ )}
331
+ </div>
332
+ )}
333
+ </div>
334
+ );
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // SupervisionFeed
339
+ // ---------------------------------------------------------------------------
340
+
341
+ export interface SupervisionFeedProps {
342
+ attention: AttentionItem[];
343
+ decisions: AutonomousDecision[];
344
+ /** Optional render prop for skill name links */
345
+ renderSkillLink?: (skillName: string) => React.ReactNode;
346
+ }
347
+
348
+ export function SupervisionFeed({ attention, decisions, renderSkillLink }: SupervisionFeedProps) {
349
+ return (
350
+ <Card
351
+ id="supervision-feed"
352
+ className="relative overflow-hidden border-none bg-muted shadow-none py-0 scroll-mt-6 ring-0"
353
+ >
354
+ <div className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-primary/5 via-transparent to-transparent" />
355
+ <Tabs defaultValue="attention" className="gap-0">
356
+ <CardHeader className="relative px-5 pt-4 pb-0">
357
+ <div>
358
+ <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
359
+ Supervision Feed
360
+ </p>
361
+ <CardDescription className="mt-1 text-[13px] text-muted-foreground/70">
362
+ What needs review and what selftune just decided.
363
+ </CardDescription>
364
+ </div>
365
+ <TabsList variant="line" className="mt-3">
366
+ <TabsTrigger value="attention" className="text-xs uppercase tracking-[0.15em]">
367
+ Attention Required
368
+ {attention.length > 0 && (
369
+ <Badge variant="secondary" className="ml-1.5 text-[10px] py-0 px-1.5">
370
+ {attention.length}
371
+ </Badge>
372
+ )}
373
+ </TabsTrigger>
374
+ <TabsTrigger value="decisions" className="text-xs uppercase tracking-[0.15em]">
375
+ Recent Decisions
376
+ {decisions.length > 0 && (
377
+ <span className="ml-1.5 text-[10px] text-muted-foreground">{decisions.length}</span>
378
+ )}
379
+ </TabsTrigger>
380
+ </TabsList>
381
+ </CardHeader>
382
+
383
+ <CardContent className="max-h-[440px] overflow-y-auto px-5 py-5">
384
+ <TabsContent value="attention">
385
+ <AttentionContent attention={attention} renderSkillLink={renderSkillLink} />
386
+ </TabsContent>
387
+ <TabsContent value="decisions">
388
+ <DecisionsContent decisions={decisions} renderSkillLink={renderSkillLink} />
389
+ </TabsContent>
390
+ </CardContent>
391
+ </Tabs>
392
+ </Card>
393
+ );
394
+ }
395
+
396
+ function AttentionContent({
397
+ attention,
398
+ renderSkillLink,
399
+ }: {
400
+ attention: AttentionItem[];
401
+ renderSkillLink?: (skillName: string) => React.ReactNode;
402
+ }) {
403
+ const [showAll, setShowAll] = useState(false);
404
+
405
+ if (attention.length === 0) {
406
+ return (
407
+ <div className="flex items-center gap-3 py-4">
408
+ <CheckCircleIcon className="size-5 text-emerald-400" />
409
+ <p className="text-sm text-muted-foreground">Nothing needs your attention</p>
410
+ </div>
411
+ );
412
+ }
413
+
414
+ const visible = showAll ? attention : attention.slice(0, 6);
415
+
416
+ return (
417
+ <div className="space-y-2">
418
+ {visible.map((item) => {
419
+ const sev = SEVERITY[item.severity];
420
+ return (
421
+ <div
422
+ key={`${item.skill_name}-${item.category}`}
423
+ className="flex items-start gap-3 rounded-xl bg-background/40 px-3 py-3 transition-colors hover:bg-background/55"
424
+ >
425
+ <span className={`mt-1.5 size-2 shrink-0 rounded-full ${sev.dot}`} />
426
+ <div className="flex-1 min-w-0">
427
+ <div className="flex items-center gap-2 flex-wrap">
428
+ {renderSkillLink ? (
429
+ renderSkillLink(item.skill_name)
430
+ ) : (
431
+ <span className="text-sm font-medium text-foreground">{item.skill_name}</span>
432
+ )}
433
+ <Badge
434
+ variant="outline"
435
+ className={`text-[10px] font-normal ${sev.text} ${sev.bg} border-transparent`}
436
+ >
437
+ {item.category.replace(/_/g, " ")}
438
+ </Badge>
439
+ </div>
440
+ <p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">{item.reason}</p>
441
+ <p className="mt-0.5 line-clamp-1 text-xs text-muted-foreground/70">
442
+ {item.recommended_action}
443
+ </p>
444
+ </div>
445
+ <span className="text-[10px] text-muted-foreground/70 shrink-0 mt-0.5">
446
+ {item.timestamp ? timeAgo(item.timestamp) : ""}
447
+ </span>
448
+ </div>
449
+ );
450
+ })}
451
+ {attention.length > 6 && !showAll && (
452
+ <div className="pt-3">
453
+ <button
454
+ type="button"
455
+ onClick={() => setShowAll(true)}
456
+ className="text-xs text-primary hover:underline"
457
+ >
458
+ Show all {attention.length} attention items
459
+ </button>
460
+ </div>
461
+ )}
462
+ </div>
463
+ );
464
+ }
465
+
466
+ function DecisionsContent({
467
+ decisions,
468
+ renderSkillLink,
469
+ }: {
470
+ decisions: AutonomousDecision[];
471
+ renderSkillLink?: (skillName: string) => React.ReactNode;
472
+ }) {
473
+ const [showAll, setShowAll] = useState(false);
474
+ const visible = showAll ? decisions : decisions.slice(0, 10);
475
+
476
+ if (decisions.length === 0) {
477
+ return <p className="text-xs text-muted-foreground/70 py-4">No autonomous decisions yet.</p>;
478
+ }
479
+
480
+ return (
481
+ <div className="space-y-1">
482
+ {visible.map((d, i) => {
483
+ const marker = DECISION_MARKERS[d.kind];
484
+ return (
485
+ <div
486
+ key={`${d.timestamp}-${d.skill_name}-${i}`}
487
+ className="flex items-start gap-2.5 rounded-xl bg-background/30 px-3 py-2 transition-colors hover:bg-background/45"
488
+ >
489
+ <span className={`mt-1.5 size-2 shrink-0 rounded-full ${marker}`} />
490
+ <div className="flex-1 min-w-0">
491
+ {renderSkillLink ? (
492
+ renderSkillLink(d.skill_name)
493
+ ) : (
494
+ <span className="text-xs font-medium text-foreground truncate block">
495
+ {d.skill_name}
496
+ </span>
497
+ )}
498
+ <p className="line-clamp-2 text-xs text-muted-foreground">{d.summary}</p>
499
+ </div>
500
+ <span className="text-[10px] text-muted-foreground/70 shrink-0 mt-0.5">
501
+ {timeAgo(d.timestamp)}
502
+ </span>
503
+ </div>
504
+ );
505
+ })}
506
+ {decisions.length > 10 && !showAll && (
507
+ <button
508
+ type="button"
509
+ onClick={() => setShowAll(true)}
510
+ className="text-xs text-primary hover:underline mt-1 pl-2"
511
+ >
512
+ Show all ({decisions.length})
513
+ </button>
514
+ )}
515
+ </div>
516
+ );
517
+ }
518
+
519
+ // ---------------------------------------------------------------------------
520
+ // SkillComparisonGrid
521
+ // ---------------------------------------------------------------------------
522
+
523
+ export interface SkillComparisonRow {
524
+ skillId?: string;
525
+ skillName: string;
526
+ platforms: string[];
527
+ triggerRate: number | null;
528
+ routingConfidence: number | null;
529
+ confidenceCoverage: number;
530
+ sessions: number;
531
+ lastEvolution: EvolutionEntry | null;
532
+ bucket: TrustBucket;
533
+ }
534
+
535
+ export interface SkillComparisonGridProps {
536
+ rows: SkillComparisonRow[];
537
+ /** Optional render prop for skill name links */
538
+ renderSkillLink?: (skillName: string) => React.ReactNode;
539
+ }
540
+
541
+ function formatEvolutionAction(action: string): string {
542
+ switch (action) {
543
+ case "created":
544
+ return "Proposal created";
545
+ case "validated":
546
+ return "Validated";
547
+ case "deployed":
548
+ return "Deployed";
549
+ case "rolled_back":
550
+ return "Rolled back";
551
+ case "watch":
552
+ return "Watching";
553
+ case "rejected":
554
+ return "Rejected";
555
+ default:
556
+ return action.replace(/_/g, " ");
557
+ }
558
+ }
559
+
560
+ export function SkillComparisonGrid({ rows, renderSkillLink }: SkillComparisonGridProps) {
561
+ if (rows.length === 0) return null;
562
+
563
+ return (
564
+ <Card className="border-none bg-muted shadow-none py-0 ring-0">
565
+ <CardHeader className="px-5 pt-5 pb-0">
566
+ <div>
567
+ <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
568
+ Skill Comparison
569
+ </p>
570
+ <CardDescription className="mt-1 text-[13px] text-muted-foreground/70">
571
+ Compare skill performance before drilling into the details.
572
+ </CardDescription>
573
+ </div>
574
+ </CardHeader>
575
+
576
+ <CardContent className="overflow-x-auto px-5 py-5">
577
+ <div className="min-w-[680px]">
578
+ <div className="grid grid-cols-[minmax(180px,2fr)_1fr_1fr_0.8fr_1.2fr_1fr] gap-3 px-3 pb-2 text-[10px] uppercase tracking-[0.18em] text-muted-foreground/70">
579
+ <span>Skill</span>
580
+ <span>Trigger Rate</span>
581
+ <span>Routing Conf.</span>
582
+ <span>Sessions</span>
583
+ <span>Last Evolution</span>
584
+ <span>Status</span>
585
+ </div>
586
+ <div className="space-y-1.5">
587
+ {rows.map((row) => {
588
+ const bucketCfg = BUCKET_CFG[row.bucket];
589
+ return (
590
+ <div
591
+ key={row.skillId ?? row.skillName}
592
+ className="grid grid-cols-[minmax(180px,2fr)_1fr_1fr_0.8fr_1.2fr_1fr] items-center gap-3 rounded-xl bg-background/35 px-3 py-3 text-sm transition-colors hover:bg-background/50"
593
+ >
594
+ <div className="min-w-0">
595
+ {renderSkillLink ? (
596
+ renderSkillLink(row.skillName)
597
+ ) : (
598
+ <p className="truncate font-medium text-foreground">{row.skillName}</p>
599
+ )}
600
+ </div>
601
+ <div className="font-medium text-foreground">
602
+ {row.triggerRate != null ? `${Math.round(row.triggerRate * 100)}%` : "--"}
603
+ </div>
604
+ <div className="min-w-0">
605
+ {row.routingConfidence != null && row.confidenceCoverage >= 0.5 ? (
606
+ <>
607
+ <p className="text-sm font-medium text-foreground">
608
+ {Math.round(row.routingConfidence * 100)}%
609
+ </p>
610
+ <p className="truncate text-xs text-muted-foreground/70">
611
+ {Math.round(row.confidenceCoverage * 100)}% coverage
612
+ </p>
613
+ </>
614
+ ) : (
615
+ <p className="text-sm font-medium text-muted-foreground/70">--</p>
616
+ )}
617
+ </div>
618
+ <div className="text-muted-foreground">{row.sessions}</div>
619
+ <div className="min-w-0">
620
+ {row.lastEvolution ? (
621
+ <>
622
+ <p className="truncate text-sm text-foreground">
623
+ {formatEvolutionAction(row.lastEvolution.action)}
624
+ </p>
625
+ <p className="text-xs text-muted-foreground/70">
626
+ {timeAgo(row.lastEvolution.timestamp)}
627
+ </p>
628
+ </>
629
+ ) : (
630
+ <span className="text-xs text-muted-foreground/70">No evolutions yet</span>
631
+ )}
632
+ </div>
633
+ <div>
634
+ <Badge
635
+ variant="outline"
636
+ className={`border-transparent ${bucketCfg.accent} bg-background/55`}
637
+ >
638
+ <span
639
+ className={`mr-1.5 inline-block size-1.5 rounded-full ${bucketCfg.dot}`}
640
+ />
641
+ {bucketCfg.label}
642
+ </Badge>
643
+ </div>
644
+ </div>
645
+ );
646
+ })}
647
+ </div>
648
+ </div>
649
+ </CardContent>
650
+ </Card>
651
+ );
652
+ }