selftune 0.2.21 → 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.
- package/README.md +15 -8
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/adapters/cline/hook.ts +167 -0
- package/cli/selftune/adapters/cline/install.ts +197 -0
- package/cli/selftune/adapters/codex/hook.ts +296 -0
- package/cli/selftune/adapters/codex/install.ts +289 -0
- package/cli/selftune/adapters/opencode/hook.ts +222 -0
- package/cli/selftune/adapters/opencode/install.ts +543 -0
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/dashboard-contract.ts +14 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +73 -20
- package/cli/selftune/evolution/validate-body.ts +78 -42
- package/cli/selftune/evolution/validate-routing.ts +45 -104
- package/cli/selftune/hooks/auto-activate.ts +43 -37
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/git-metadata.ts +149 -0
- package/cli/selftune/hooks-shared/hook-output.ts +105 -0
- package/cli/selftune/hooks-shared/normalize.ts +196 -0
- package/cli/selftune/hooks-shared/session-state.ts +76 -0
- package/cli/selftune/hooks-shared/skill-paths.ts +50 -0
- package/cli/selftune/hooks-shared/stdin-dispatch.ts +59 -0
- package/cli/selftune/hooks-shared/types.ts +91 -0
- package/cli/selftune/index.ts +76 -6
- package/cli/selftune/ingestors/pi-ingest.ts +726 -0
- package/cli/selftune/init.ts +11 -1
- package/cli/selftune/localdb/direct-write.ts +85 -0
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries.ts +126 -0
- package/cli/selftune/localdb/schema.ts +38 -0
- package/cli/selftune/observability.ts +8 -1
- package/cli/selftune/orchestrate.ts +43 -0
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +4 -1
- package/cli/selftune/status.ts +31 -0
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/types.ts +2 -1
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +99 -34
- package/cli/selftune/utils/skill-discovery.ts +22 -0
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
- package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +22 -4
- package/packages/telemetry-contract/src/types.ts +1 -12
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +652 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/types.ts +172 -4
- package/skill/SKILL.md +26 -2
- package/skill/Workflows/Ingest.md +60 -2
- package/skill/Workflows/Initialize.md +54 -9
- package/skill/Workflows/PlatformHooks.md +109 -0
- package/skill/Workflows/Registry.md +99 -0
- package/skill/Workflows/Sync.md +3 -1
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- 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
|
+
}
|