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
|
@@ -1,3 +1,8 @@
|
|
|
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";
|
|
1
6
|
import {
|
|
2
7
|
CircleDotIcon,
|
|
3
8
|
RocketIcon,
|
|
@@ -9,15 +14,8 @@ import {
|
|
|
9
14
|
ChevronDownIcon,
|
|
10
15
|
ChevronRightIcon,
|
|
11
16
|
} from "lucide-react";
|
|
12
|
-
import { useState } from "react";
|
|
13
|
-
import type { ReactNode } from "react";
|
|
14
|
-
|
|
15
|
-
import { timeAgo } from "../lib/format";
|
|
16
|
-
import { cn } from "../lib/utils";
|
|
17
|
-
import { Badge } from "../primitives/badge";
|
|
18
|
-
import type { EvalSnapshot, EvolutionEntry } from "../types";
|
|
19
17
|
|
|
20
|
-
const ACTION_ICON: Record<string, ReactNode> = {
|
|
18
|
+
const ACTION_ICON: Record<string, React.ReactNode> = {
|
|
21
19
|
created: <CircleDotIcon className="size-3.5" />,
|
|
22
20
|
validated: <ShieldCheckIcon className="size-3.5" />,
|
|
23
21
|
deployed: <RocketIcon className="size-3.5" />,
|
|
@@ -26,43 +24,27 @@ const ACTION_ICON: Record<string, ReactNode> = {
|
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
const ACTION_COLOR: Record<string, string> = {
|
|
29
|
-
created: "bg-
|
|
30
|
-
validated: "bg-
|
|
31
|
-
deployed: "bg-
|
|
32
|
-
rejected: "bg-
|
|
33
|
-
rolled_back: "bg-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const ACTION_ICON_COLOR: Record<string, string> = {
|
|
37
|
-
created: "text-primary/70",
|
|
38
|
-
validated: "text-primary/85",
|
|
39
|
-
deployed: "text-primary",
|
|
40
|
-
rejected: "text-destructive",
|
|
41
|
-
rolled_back: "text-destructive/70",
|
|
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",
|
|
42
32
|
};
|
|
43
33
|
|
|
44
34
|
const ACTION_RING: Record<string, string> = {
|
|
45
|
-
created: "ring-
|
|
46
|
-
validated: "ring-
|
|
47
|
-
deployed: "ring-
|
|
48
|
-
rejected: "ring-
|
|
49
|
-
rolled_back: "ring-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const ACTION_DOT: Record<string, string> = {
|
|
53
|
-
created: "bg-primary/40 ring-primary/30",
|
|
54
|
-
validated: "bg-primary/60 ring-primary/40",
|
|
55
|
-
deployed: "bg-primary ring-primary/50",
|
|
56
|
-
rejected: "bg-destructive/60 ring-destructive/40",
|
|
57
|
-
rolled_back: "bg-destructive/40 ring-destructive/30",
|
|
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",
|
|
58
40
|
};
|
|
59
41
|
|
|
60
42
|
const ACTION_LINE: Record<string, string> = {
|
|
61
|
-
created: "bg-
|
|
62
|
-
validated: "bg-
|
|
63
|
-
deployed: "bg-
|
|
64
|
-
rejected: "bg-
|
|
65
|
-
rolled_back: "bg-
|
|
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",
|
|
66
48
|
};
|
|
67
49
|
|
|
68
50
|
interface Props {
|
|
@@ -71,21 +53,6 @@ interface Props {
|
|
|
71
53
|
onSelect: (proposalId: string) => void;
|
|
72
54
|
}
|
|
73
55
|
|
|
74
|
-
function validationModeBadge(
|
|
75
|
-
mode?: string | null,
|
|
76
|
-
): { label: string; variant: "default" | "secondary" | "outline" } | null {
|
|
77
|
-
switch (mode) {
|
|
78
|
-
case "host_replay":
|
|
79
|
-
return { label: "replay", variant: "default" };
|
|
80
|
-
case "llm_judge":
|
|
81
|
-
return { label: "judge", variant: "secondary" };
|
|
82
|
-
case "structural_guard":
|
|
83
|
-
return { label: "structural", variant: "outline" };
|
|
84
|
-
default:
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
56
|
/** Group evolution entries by proposal_id, ordered newest-first. */
|
|
90
57
|
function groupByProposal(entries: EvolutionEntry[]) {
|
|
91
58
|
const map = new Map<string, EvolutionEntry[]>();
|
|
@@ -125,7 +92,7 @@ function PassRateDelta({ snapshot }: { snapshot: EvalSnapshot }) {
|
|
|
125
92
|
<span
|
|
126
93
|
className={cn(
|
|
127
94
|
"inline-flex items-center gap-0.5 text-[10px] font-mono font-medium",
|
|
128
|
-
isPositive ? "text-
|
|
95
|
+
isPositive ? "text-emerald-600 dark:text-emerald-400" : "text-red-500",
|
|
129
96
|
)}
|
|
130
97
|
>
|
|
131
98
|
{isPositive ? (
|
|
@@ -151,35 +118,23 @@ function LifecycleLegend() {
|
|
|
151
118
|
const [open, setOpen] = useState(false);
|
|
152
119
|
|
|
153
120
|
return (
|
|
154
|
-
<div className="px-2 pb-
|
|
121
|
+
<div className="px-2 pb-2">
|
|
155
122
|
<button
|
|
156
123
|
type="button"
|
|
157
124
|
onClick={() => setOpen(!open)}
|
|
158
|
-
|
|
159
|
-
aria-controls="evolution-lifecycle-stages"
|
|
160
|
-
className="flex w-full items-center gap-1 text-[10px] text-muted-foreground/70 transition-colors hover:text-muted-foreground"
|
|
125
|
+
className="flex items-center gap-1 text-[10px] text-muted-foreground/70 hover:text-muted-foreground transition-colors w-full"
|
|
161
126
|
>
|
|
162
127
|
{open ? <ChevronDownIcon className="size-3" /> : <ChevronRightIcon className="size-3" />}
|
|
163
128
|
Lifecycle stages
|
|
164
129
|
</button>
|
|
165
130
|
{open && (
|
|
166
|
-
<div
|
|
167
|
-
id="evolution-lifecycle-stages"
|
|
168
|
-
className="mt-1.5 space-y-2.5 rounded-md border bg-muted/30 p-2"
|
|
169
|
-
>
|
|
131
|
+
<div className="mt-1.5 space-y-1.5 rounded-md border bg-muted/30 p-2">
|
|
170
132
|
{LIFECYCLE_STEPS.map((step) => (
|
|
171
133
|
<div key={step.action} className="flex items-start gap-2">
|
|
172
|
-
<div
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)}
|
|
177
|
-
/>
|
|
178
|
-
<div className="min-w-0 flex flex-col gap-0.5">
|
|
179
|
-
<span className="text-[10px] font-medium leading-none">{step.label}</span>
|
|
180
|
-
<span className="text-[10px] text-muted-foreground/70 leading-tight">
|
|
181
|
-
{step.desc}
|
|
182
|
-
</span>
|
|
134
|
+
<div className={cn("size-2 rounded-full mt-1 shrink-0", ACTION_COLOR[step.action])} />
|
|
135
|
+
<div className="min-w-0">
|
|
136
|
+
<span className="text-[10px] font-medium">{step.label}</span>
|
|
137
|
+
<p className="text-[10px] text-muted-foreground/70 leading-tight">{step.desc}</p>
|
|
183
138
|
</div>
|
|
184
139
|
</div>
|
|
185
140
|
))}
|
|
@@ -202,7 +157,7 @@ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Pro
|
|
|
202
157
|
|
|
203
158
|
return (
|
|
204
159
|
<div className="flex flex-col gap-0">
|
|
205
|
-
<h2 className="
|
|
160
|
+
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2 pb-2 sticky top-0 z-10 bg-background">
|
|
206
161
|
Evolution
|
|
207
162
|
</h2>
|
|
208
163
|
<LifecycleLegend />
|
|
@@ -211,13 +166,11 @@ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Pro
|
|
|
211
166
|
const terminal = terminalAction(steps);
|
|
212
167
|
const isSelected = selectedProposalId === proposalId;
|
|
213
168
|
const lastStep = steps[steps.length - 1];
|
|
214
|
-
const dotColor = ACTION_COLOR[terminal] ?? "bg-muted-foreground
|
|
215
|
-
const iconColor = ACTION_ICON_COLOR[terminal] ?? "text-muted-foreground";
|
|
169
|
+
const dotColor = ACTION_COLOR[terminal] ?? "bg-muted-foreground";
|
|
216
170
|
const ringColor = ACTION_RING[terminal] ?? "ring-muted-foreground/30";
|
|
217
171
|
const lineColor = ACTION_LINE[terminal] ?? "bg-border";
|
|
218
172
|
const isLast = groupIdx === groups.length - 1;
|
|
219
173
|
const snapshot = findEvalSnapshot(steps);
|
|
220
|
-
const validationBadge = validationModeBadge(lastStep.validation_mode);
|
|
221
174
|
|
|
222
175
|
return (
|
|
223
176
|
<div key={proposalId} className="relative flex gap-3">
|
|
@@ -225,15 +178,14 @@ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Pro
|
|
|
225
178
|
<div className="flex flex-col items-center">
|
|
226
179
|
<div
|
|
227
180
|
className={cn(
|
|
228
|
-
"flex items-center justify-center size-7 rounded-full ring-2 shrink-0 z-10",
|
|
181
|
+
"flex items-center justify-center size-7 rounded-full ring-2 text-white shrink-0 z-10",
|
|
229
182
|
dotColor,
|
|
230
183
|
ringColor,
|
|
231
|
-
iconColor,
|
|
232
184
|
)}
|
|
233
185
|
>
|
|
234
186
|
{ACTION_ICON[terminal] ?? <CircleDotIcon className="size-3.5" />}
|
|
235
187
|
</div>
|
|
236
|
-
{!isLast && <div className={cn("w-0.5 flex-1 min-h-[
|
|
188
|
+
{!isLast && <div className={cn("w-0.5 flex-1 min-h-[16px]", lineColor)} />}
|
|
237
189
|
</div>
|
|
238
190
|
|
|
239
191
|
{/* Content */}
|
|
@@ -262,11 +214,6 @@ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Pro
|
|
|
262
214
|
<span className="text-[10px] text-muted-foreground">
|
|
263
215
|
{timeAgo(lastStep.timestamp)}
|
|
264
216
|
</span>
|
|
265
|
-
{validationBadge && (
|
|
266
|
-
<Badge variant={validationBadge.variant} className="text-[9px] uppercase">
|
|
267
|
-
{validationBadge.label}
|
|
268
|
-
</Badge>
|
|
269
|
-
)}
|
|
270
217
|
</div>
|
|
271
218
|
{/* Pass rate delta from eval snapshot */}
|
|
272
219
|
{snapshot && (
|
|
@@ -293,7 +240,7 @@ export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Pro
|
|
|
293
240
|
key={`${s.action}-${i}`}
|
|
294
241
|
className={cn(
|
|
295
242
|
"size-1.5 rounded-full",
|
|
296
|
-
|
|
243
|
+
ACTION_COLOR[s.action] ?? "bg-muted-foreground",
|
|
297
244
|
)}
|
|
298
245
|
/>
|
|
299
246
|
))}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { InfoIcon } from "lucide-react";
|
|
2
|
-
|
|
3
1
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../primitives/tooltip";
|
|
2
|
+
import { InfoIcon } from "lucide-react";
|
|
4
3
|
|
|
5
4
|
/** Small info icon that shows a tooltip on hover. Used to explain metrics and concepts. */
|
|
6
5
|
export function InfoTip({ text }: { text: string }) {
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { ChevronRightIcon, FilterIcon } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from "../primitives";
|
|
12
|
+
import { InfoTip } from "./InfoTip";
|
|
13
|
+
import { observationBadge, historicalContextBadge } from "./SkillReportPanels";
|
|
14
|
+
import { timeAgo } from "../lib/format";
|
|
15
|
+
|
|
16
|
+
import type { ObservationKind, HistoricalContext } from "../types";
|
|
17
|
+
|
|
18
|
+
/* ─── Public types ────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
export interface InvocationRow {
|
|
21
|
+
timestamp: string | null;
|
|
22
|
+
session_id: string | null;
|
|
23
|
+
triggered: boolean;
|
|
24
|
+
query: string;
|
|
25
|
+
invocation_mode: string | null;
|
|
26
|
+
confidence: number | null;
|
|
27
|
+
tool_name: string | null;
|
|
28
|
+
agent_type: string | null;
|
|
29
|
+
observation_kind?: ObservationKind | null;
|
|
30
|
+
historical_context?: HistoricalContext | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SessionMeta {
|
|
34
|
+
session_id: string;
|
|
35
|
+
started_at?: string | null;
|
|
36
|
+
model?: string | null;
|
|
37
|
+
workspace_path?: string | null;
|
|
38
|
+
platform?: string | null;
|
|
39
|
+
agent_cli?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type InvocationFilter = "all" | "misses" | "low_confidence";
|
|
43
|
+
|
|
44
|
+
/* ─── Session group ───────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
function SessionGroup({
|
|
47
|
+
sessionId,
|
|
48
|
+
meta,
|
|
49
|
+
invocations,
|
|
50
|
+
defaultExpanded,
|
|
51
|
+
}: {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
meta?: SessionMeta;
|
|
54
|
+
invocations: InvocationRow[];
|
|
55
|
+
defaultExpanded: boolean;
|
|
56
|
+
}) {
|
|
57
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
58
|
+
const ts = meta?.started_at ?? invocations[0]?.timestamp;
|
|
59
|
+
|
|
60
|
+
const modeBreakdown = invocations.reduce(
|
|
61
|
+
(acc, inv) => {
|
|
62
|
+
const mode = inv.invocation_mode ?? "unknown";
|
|
63
|
+
acc[mode] = (acc[mode] ?? 0) + 1;
|
|
64
|
+
return acc;
|
|
65
|
+
},
|
|
66
|
+
{} as Record<string, number>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const formatInvoker = (inv: InvocationRow): { label: string; hint: string } => {
|
|
70
|
+
const cli = meta?.agent_cli?.replace(/_/g, " ");
|
|
71
|
+
const platform = meta?.platform?.replace(/_/g, " ");
|
|
72
|
+
|
|
73
|
+
if (inv.agent_type && inv.agent_type !== "main") {
|
|
74
|
+
return {
|
|
75
|
+
label: inv.agent_type,
|
|
76
|
+
hint: cli ? `${cli} subagent` : "subagent invocation",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (cli) {
|
|
80
|
+
return {
|
|
81
|
+
label: cli,
|
|
82
|
+
hint:
|
|
83
|
+
inv.agent_type === "main"
|
|
84
|
+
? "main agent invocation"
|
|
85
|
+
: "session agent that invoked the skill",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (platform) {
|
|
89
|
+
return {
|
|
90
|
+
label: platform,
|
|
91
|
+
hint: inv.agent_type === "main" ? "main agent invocation" : "session platform",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (inv.agent_type) {
|
|
95
|
+
return { label: inv.agent_type, hint: "recorded subagent type" };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
label: "No data",
|
|
99
|
+
hint: "invoker was not captured in this record",
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="overflow-hidden rounded-lg border border-slate-200 transition-colors dark:border-slate-800">
|
|
105
|
+
{/* Session header */}
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-slate-50 active:bg-slate-100 dark:hover:bg-slate-800/40 dark:active:bg-slate-800/60"
|
|
109
|
+
onClick={() => setExpanded(!expanded)}
|
|
110
|
+
>
|
|
111
|
+
<ChevronRightIcon
|
|
112
|
+
className={`size-3.5 shrink-0 text-slate-400 transition-transform duration-150 dark:text-slate-500 ${expanded ? "rotate-90" : ""}`}
|
|
113
|
+
/>
|
|
114
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<span className="text-sm font-medium text-slate-900 dark:text-white">
|
|
117
|
+
{invocations.length} invocation
|
|
118
|
+
{invocations.length !== 1 ? "s" : ""}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
121
|
+
{ts ? timeAgo(ts) : ""}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
125
|
+
{meta?.model && (
|
|
126
|
+
<Badge variant="secondary" className="text-[10px] font-normal">
|
|
127
|
+
{meta.model}
|
|
128
|
+
</Badge>
|
|
129
|
+
)}
|
|
130
|
+
{meta?.workspace_path && (
|
|
131
|
+
<span
|
|
132
|
+
className="font-mono text-[11px] text-slate-500 dark:text-slate-400"
|
|
133
|
+
title={meta.workspace_path}
|
|
134
|
+
>
|
|
135
|
+
{meta.workspace_path.split("/").slice(-2).join("/")}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Compact mode summary when collapsed */}
|
|
142
|
+
{!expanded && (
|
|
143
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
144
|
+
{Object.entries(modeBreakdown).map(([mode, count]) => (
|
|
145
|
+
<Badge key={mode} variant="outline" className="gap-1 text-[10px] font-normal">
|
|
146
|
+
{mode} <span className="text-slate-400 dark:text-slate-500">{count}</span>
|
|
147
|
+
</Badge>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
<span className="shrink-0 font-mono text-[10px] text-slate-300 dark:text-slate-600">
|
|
152
|
+
{sessionId.substring(0, 8)}
|
|
153
|
+
</span>
|
|
154
|
+
</button>
|
|
155
|
+
|
|
156
|
+
{/* Invocation table */}
|
|
157
|
+
{expanded && (
|
|
158
|
+
<div className="overflow-x-auto border-t border-slate-200 dark:border-slate-800">
|
|
159
|
+
<Table>
|
|
160
|
+
<TableHeader>
|
|
161
|
+
<TableRow className="bg-slate-50 hover:bg-slate-50 dark:bg-slate-800/40 dark:hover:bg-slate-800/40">
|
|
162
|
+
<TableHead className="h-8 text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
163
|
+
Prompt <InfoTip text="The user prompt that led to this skill being invoked" />
|
|
164
|
+
</TableHead>
|
|
165
|
+
<TableHead className="h-8 w-[90px] text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
166
|
+
Mode{" "}
|
|
167
|
+
<InfoTip text="explicit = user typed /skillname; implicit = user mentioned skill by name; inferred = agent chose skill autonomously" />
|
|
168
|
+
</TableHead>
|
|
169
|
+
<TableHead className="h-8 w-[70px] text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
170
|
+
Confidence{" "}
|
|
171
|
+
<InfoTip text="Model's confidence score (0-100%) when routing this prompt to the skill" />
|
|
172
|
+
</TableHead>
|
|
173
|
+
<TableHead className="h-8 w-[110px] text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
174
|
+
Invoker{" "}
|
|
175
|
+
<InfoTip text="Who invoked the skill. Prefers subagent type when present, otherwise falls back to the session agent or platform." />
|
|
176
|
+
</TableHead>
|
|
177
|
+
<TableHead className="h-8 w-[120px] text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
178
|
+
Evidence
|
|
179
|
+
</TableHead>
|
|
180
|
+
<TableHead className="h-8 w-[70px] text-right text-[10px] font-semibold uppercase tracking-[0.15em]">
|
|
181
|
+
Time
|
|
182
|
+
</TableHead>
|
|
183
|
+
</TableRow>
|
|
184
|
+
</TableHeader>
|
|
185
|
+
<TableBody>
|
|
186
|
+
{invocations.map((inv, i) => (
|
|
187
|
+
<TableRow
|
|
188
|
+
key={i}
|
|
189
|
+
className={!inv.triggered ? "bg-red-50/50 dark:bg-red-950/10" : ""}
|
|
190
|
+
>
|
|
191
|
+
<TableCell
|
|
192
|
+
className="max-w-[500px] truncate py-2 text-sm"
|
|
193
|
+
title={inv.query || undefined}
|
|
194
|
+
>
|
|
195
|
+
{inv.query || (
|
|
196
|
+
<span className="italic text-slate-300 dark:text-slate-600">
|
|
197
|
+
No prompt recorded
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
{!inv.triggered && (
|
|
201
|
+
<Badge variant="destructive" className="ml-2 text-[10px] font-normal">
|
|
202
|
+
missed
|
|
203
|
+
</Badge>
|
|
204
|
+
)}
|
|
205
|
+
</TableCell>
|
|
206
|
+
<TableCell className="py-2">
|
|
207
|
+
{inv.invocation_mode ? (
|
|
208
|
+
<Badge variant="secondary" className="text-[10px] font-normal">
|
|
209
|
+
{inv.invocation_mode}
|
|
210
|
+
</Badge>
|
|
211
|
+
) : (
|
|
212
|
+
<span className="text-[11px] text-slate-400 dark:text-slate-500">
|
|
213
|
+
Unknown mode
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
</TableCell>
|
|
217
|
+
<TableCell className="py-2 font-mono text-xs tabular-nums text-slate-600 dark:text-slate-300">
|
|
218
|
+
{inv.confidence !== null
|
|
219
|
+
? `${Math.round(inv.confidence * 100)}%`
|
|
220
|
+
: "Not recorded"}
|
|
221
|
+
</TableCell>
|
|
222
|
+
<TableCell className="py-2">
|
|
223
|
+
{(() => {
|
|
224
|
+
const invoker = formatInvoker(inv);
|
|
225
|
+
return invoker.label === "No data" ? (
|
|
226
|
+
<span
|
|
227
|
+
className="text-[11px] text-slate-400 dark:text-slate-500"
|
|
228
|
+
title={invoker.hint}
|
|
229
|
+
>
|
|
230
|
+
{invoker.label}
|
|
231
|
+
</span>
|
|
232
|
+
) : (
|
|
233
|
+
<Badge
|
|
234
|
+
variant={
|
|
235
|
+
inv.agent_type && inv.agent_type !== "main" ? "outline" : "secondary"
|
|
236
|
+
}
|
|
237
|
+
className="text-[10px] font-normal capitalize"
|
|
238
|
+
title={invoker.hint}
|
|
239
|
+
>
|
|
240
|
+
{invoker.label}
|
|
241
|
+
</Badge>
|
|
242
|
+
);
|
|
243
|
+
})()}
|
|
244
|
+
</TableCell>
|
|
245
|
+
<TableCell className="py-2">
|
|
246
|
+
{(() => {
|
|
247
|
+
const observation = observationBadge(inv.observation_kind);
|
|
248
|
+
const historicalCtx = historicalContextBadge(inv.historical_context);
|
|
249
|
+
return observation ? (
|
|
250
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
251
|
+
<Badge variant={observation.variant} className="text-[10px] font-normal">
|
|
252
|
+
{observation.label}
|
|
253
|
+
</Badge>
|
|
254
|
+
{historicalCtx && (
|
|
255
|
+
<Badge
|
|
256
|
+
variant={historicalCtx.variant}
|
|
257
|
+
className="text-[10px] font-normal"
|
|
258
|
+
>
|
|
259
|
+
{historicalCtx.label}
|
|
260
|
+
</Badge>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
) : (
|
|
264
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
265
|
+
<span className="text-[11px] text-slate-400 dark:text-slate-500">
|
|
266
|
+
canonical
|
|
267
|
+
</span>
|
|
268
|
+
{historicalCtx && (
|
|
269
|
+
<Badge
|
|
270
|
+
variant={historicalCtx.variant}
|
|
271
|
+
className="text-[10px] font-normal"
|
|
272
|
+
>
|
|
273
|
+
{historicalCtx.label}
|
|
274
|
+
</Badge>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
})()}
|
|
279
|
+
</TableCell>
|
|
280
|
+
<TableCell className="whitespace-nowrap py-2 text-right font-mono text-[11px] text-slate-400 dark:text-slate-500">
|
|
281
|
+
{inv.timestamp ? timeAgo(inv.timestamp) : ""}
|
|
282
|
+
</TableCell>
|
|
283
|
+
</TableRow>
|
|
284
|
+
))}
|
|
285
|
+
</TableBody>
|
|
286
|
+
</Table>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ─── InvocationsPanel ────────────────────────────────── */
|
|
294
|
+
|
|
295
|
+
export function InvocationsPanel({
|
|
296
|
+
invocations,
|
|
297
|
+
sessionMetadata = [],
|
|
298
|
+
}: {
|
|
299
|
+
invocations: InvocationRow[];
|
|
300
|
+
sessionMetadata?: SessionMeta[];
|
|
301
|
+
}) {
|
|
302
|
+
const [filter, setFilter] = useState<InvocationFilter>("all");
|
|
303
|
+
|
|
304
|
+
const sessionMetaMap = useMemo(
|
|
305
|
+
() => new Map(sessionMetadata.map((s) => [s.session_id, s])),
|
|
306
|
+
[sessionMetadata],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const filtered = useMemo(() => {
|
|
310
|
+
switch (filter) {
|
|
311
|
+
case "misses":
|
|
312
|
+
return invocations.filter((i) => !i.triggered);
|
|
313
|
+
case "low_confidence":
|
|
314
|
+
return invocations.filter((i) => i.confidence !== null && i.confidence < 0.5);
|
|
315
|
+
default:
|
|
316
|
+
return invocations;
|
|
317
|
+
}
|
|
318
|
+
}, [invocations, filter]);
|
|
319
|
+
|
|
320
|
+
const groupedSessions = useMemo(() => {
|
|
321
|
+
const sessionMap = new Map<string, InvocationRow[]>();
|
|
322
|
+
for (const inv of filtered) {
|
|
323
|
+
const sid = inv.session_id ?? "unknown";
|
|
324
|
+
const arr = sessionMap.get(sid);
|
|
325
|
+
if (arr) arr.push(inv);
|
|
326
|
+
else sessionMap.set(sid, [inv]);
|
|
327
|
+
}
|
|
328
|
+
return [...sessionMap.entries()].sort(([, a], [, b]) =>
|
|
329
|
+
(b[0]?.timestamp ?? "").localeCompare(a[0]?.timestamp ?? ""),
|
|
330
|
+
);
|
|
331
|
+
}, [filtered]);
|
|
332
|
+
|
|
333
|
+
if (invocations.length === 0) {
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex items-center justify-center rounded-lg border border-dashed border-slate-300 py-12 dark:border-slate-700">
|
|
336
|
+
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
337
|
+
No invocation records yet. Invocations appear when skills are triggered during real
|
|
338
|
+
sessions.
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="space-y-3">
|
|
346
|
+
{/* Filters */}
|
|
347
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
348
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
349
|
+
<FilterIcon className="size-3.5 text-slate-400 dark:text-slate-500" />
|
|
350
|
+
{(
|
|
351
|
+
[
|
|
352
|
+
["all", "All"],
|
|
353
|
+
["misses", "Misses"],
|
|
354
|
+
["low_confidence", "Low confidence"],
|
|
355
|
+
] as const
|
|
356
|
+
).map(([key, label]) => (
|
|
357
|
+
<button key={key} type="button" onClick={() => setFilter(key)} className="inline-block">
|
|
358
|
+
<Badge
|
|
359
|
+
variant={filter === key ? "default" : "outline"}
|
|
360
|
+
className="cursor-pointer text-[10px]"
|
|
361
|
+
>
|
|
362
|
+
{label}
|
|
363
|
+
</Badge>
|
|
364
|
+
</button>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
368
|
+
{filtered.length} invocations across {groupedSessions.length} sessions
|
|
369
|
+
</span>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{/* Legend */}
|
|
373
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-slate-500 dark:text-slate-400">
|
|
374
|
+
<span className="inline-flex items-center gap-1.5">
|
|
375
|
+
<span className="size-1.5 rounded-full bg-slate-400" />
|
|
376
|
+
explicit = user typed /skill
|
|
377
|
+
</span>
|
|
378
|
+
<span className="inline-flex items-center gap-1.5">
|
|
379
|
+
<span className="size-1.5 rounded-full bg-slate-400" />
|
|
380
|
+
implicit = mentioned by name
|
|
381
|
+
</span>
|
|
382
|
+
<span className="inline-flex items-center gap-1.5">
|
|
383
|
+
<span className="size-1.5 rounded-full bg-slate-400" />
|
|
384
|
+
inferred = agent chose autonomously
|
|
385
|
+
</span>
|
|
386
|
+
<span className="inline-flex items-center gap-1.5">
|
|
387
|
+
<span className="size-1.5 rounded-full bg-red-400" />
|
|
388
|
+
missed = skill should have triggered
|
|
389
|
+
</span>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* Session groups */}
|
|
393
|
+
{groupedSessions.length === 0 ? (
|
|
394
|
+
<div className="flex items-center justify-center py-8 text-sm text-slate-500 dark:text-slate-400">
|
|
395
|
+
No invocations match this filter.
|
|
396
|
+
</div>
|
|
397
|
+
) : (
|
|
398
|
+
groupedSessions.map(([sessionId, sessionInvocations], idx) => {
|
|
399
|
+
const meta = sessionMetaMap.get(sessionId);
|
|
400
|
+
return (
|
|
401
|
+
<SessionGroup
|
|
402
|
+
key={sessionId}
|
|
403
|
+
sessionId={sessionId}
|
|
404
|
+
meta={meta}
|
|
405
|
+
invocations={sessionInvocations}
|
|
406
|
+
defaultExpanded={idx < 3}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
})
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
}
|