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
@@ -1,7 +1,7 @@
1
1
  export const CANONICAL_SCHEMA_VERSION = "2.0" as const;
2
2
  export type CanonicalSchemaVersion = typeof CANONICAL_SCHEMA_VERSION;
3
3
 
4
- export const CANONICAL_PLATFORMS = ["claude_code", "codex", "opencode", "openclaw"] as const;
4
+ export const CANONICAL_PLATFORMS = ["claude_code", "codex", "opencode", "openclaw", "pi"] as const;
5
5
  export type CanonicalPlatform = (typeof CANONICAL_PLATFORMS)[number];
6
6
 
7
7
  export const CANONICAL_CAPTURE_MODES = [
@@ -143,18 +143,7 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
143
143
  errors_encountered: number;
144
144
  input_tokens?: number;
145
145
  output_tokens?: number;
146
- cached_input_tokens?: number;
147
- reasoning_output_tokens?: number;
148
- cost_usd?: number;
149
146
  duration_ms?: number;
150
- files_changed?: number;
151
- lines_added?: number;
152
- lines_removed?: number;
153
- lines_modified?: number;
154
- /** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
155
- artifact_count?: number;
156
- /** Inferred session type based on tool distribution. */
157
- session_type?: "dev" | "research" | "content" | "mixed";
158
147
  completion_status?: CanonicalCompletionStatus;
159
148
  end_reason?: string;
160
149
  }
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test } from "bun:test";
2
-
3
2
  import { completePush } from "../fixtures/complete-push.js";
4
3
  import { evidenceOnlyPush } from "../fixtures/evidence-only-push.js";
5
4
  import { partialPushNoSessions } from "../fixtures/partial-push-no-sessions.js";
@@ -0,0 +1,16 @@
1
+ # @selftune/ui
2
+
3
+ Shared React component library used by both the cloud dashboard and the local OSS dashboard. Canonical copy lives here; synced to `oss/selftune/packages/ui` via `scripts/sync-embedded-shared.sh`.
4
+
5
+ | Directory | Contents |
6
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `src/primitives/` | Base UI: Badge, Button, Card, Checkbox, Collapsible, DropdownMenu, Label, Select, Table, Tabs, Tooltip |
8
+ | `src/components/` | Shared components: SkillHealthGrid, EvolutionTimeline, EvidenceViewer, ActivityTimeline, OrchestrateRunsPanel, SectionCards, InfoTip, SkillReportPanels, SkillReportGuide, InvocationsPanel, SkillsLibrary, AnalyticsCharts, OverviewPanels |
9
+ | `src/types.ts` | Shared types: SkillCard, SkillHealthStatus, EvalSnapshot, EvolutionEntry, TrustState, TrustFields, ExampleRow, AutonomyStatus, TrustWatchlistEntry, AttentionItem, AutonomousDecision |
10
+ | `src/lib/` | Utilities: format (formatRate, timeAgo), constants (STATUS_CONFIG), utils (deriveStatus, sortByPassRateAndChecks) |
11
+
12
+ **Exports:** `./primitives`, `./components`, `./types`, `./lib`
13
+
14
+ **Dependencies:** react, @base-ui/react, lucide-react, clsx, tailwind-merge
15
+
16
+ **Important:** Do NOT edit `oss/selftune/packages/ui/` directly. Edit here and run `scripts/sync-embedded-shared.sh`.
@@ -57,7 +57,7 @@ Presentational components for selftune dashboard views. No data fetching, no rou
57
57
  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
58
58
  | `SkillHealthGrid` | Sortable/filterable data table with drag-and-drop, pagination, and view tabs. Accepts `renderSkillName` prop for custom routing. |
59
59
  | `EvolutionTimeline` | Proposal lifecycle timeline grouped by proposal ID, with pass rate deltas. |
60
- | `ActivityPanel` | Tabbed activity feed (undeployed proposals, timeline events, unmatched queries). |
60
+ | `ActivityPanel` | Tabbed activity feed (pending proposals, timeline events, unmatched queries). |
61
61
  | `EvidenceViewer` | Full evidence trail for a proposal — side-by-side diffs, validation results, iteration rounds. |
62
62
  | `SectionCards` | Dashboard metric stat cards (skills count, pass rate, unmatched, sessions, etc.). |
63
63
  | `OrchestrateRunsPanel` | Collapsible orchestrate run reports with per-skill action details. |
@@ -38,7 +38,7 @@
38
38
  "react": "^19.0.0",
39
39
  "react-dom": "^19.0.0",
40
40
  "react-markdown": "^10.0.0",
41
- "recharts": "^2.0.0"
41
+ "recharts": "^3.0.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@dnd-kit/core": {
@@ -1,11 +1,10 @@
1
- import { ClockIcon, GitPullRequestArrowIcon, SearchXIcon, ActivityIcon } from "lucide-react";
2
-
3
- import { timeAgo } from "../lib/format";
4
1
  import { Badge } from "../primitives/badge";
5
2
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
6
3
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
7
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/tooltip";
8
5
  import type { EvolutionEntry, PendingProposal, UnmatchedQuery } from "../types";
6
+ import { timeAgo } from "../lib/format";
7
+ import { ClockIcon, GitPullRequestArrowIcon, SearchXIcon, ActivityIcon } from "lucide-react";
9
8
 
10
9
  const ACTION_VARIANT: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
11
10
  created: "outline",
@@ -21,179 +20,15 @@ export function ActivityPanel({
21
20
  pendingProposals,
22
21
  unmatchedQueries,
23
22
  onSelectProposal,
24
- embedded = false,
25
23
  }: {
26
24
  evolution: EvolutionEntry[];
27
25
  pendingProposals: PendingProposal[];
28
26
  unmatchedQueries: UnmatchedQuery[];
29
27
  onSelectProposal?: (skillName: string, proposalId: string) => void;
30
- embedded?: boolean;
31
28
  }) {
32
29
  const hasActivity =
33
30
  evolution.length > 0 || pendingProposals.length > 0 || unmatchedQueries.length > 0;
34
31
 
35
- const content = hasActivity ? (
36
- <Tabs
37
- defaultValue={
38
- pendingProposals.length > 0 ? "pending" : evolution.length > 0 ? "timeline" : "unmatched"
39
- }
40
- >
41
- <TooltipProvider>
42
- <TabsList className="w-full">
43
- {pendingProposals.length > 0 && (
44
- <Tooltip>
45
- <TooltipTrigger
46
- render={
47
- <TabsTrigger
48
- value="pending"
49
- className="flex-1 gap-1.5"
50
- aria-label={`Pending proposals (${pendingProposals.length})`}
51
- />
52
- }
53
- >
54
- <GitPullRequestArrowIcon className="size-3.5" />
55
- <Badge variant="secondary" className="h-4 px-1 text-[10px]">
56
- {pendingProposals.length}
57
- </Badge>
58
- </TooltipTrigger>
59
- <TooltipContent>Undeployed proposals</TooltipContent>
60
- </Tooltip>
61
- )}
62
- <Tooltip>
63
- <TooltipTrigger
64
- render={<TabsTrigger value="timeline" className="flex-1" aria-label="Timeline" />}
65
- >
66
- <ClockIcon className="size-3.5" />
67
- </TooltipTrigger>
68
- <TooltipContent>Timeline</TooltipContent>
69
- </Tooltip>
70
- {unmatchedQueries.length > 0 && (
71
- <Tooltip>
72
- <TooltipTrigger
73
- render={
74
- <TabsTrigger
75
- value="unmatched"
76
- className="flex-1 gap-1.5"
77
- aria-label={`Unmatched queries (${unmatchedQueries.length})`}
78
- />
79
- }
80
- >
81
- <SearchXIcon className="size-3.5" />
82
- <Badge variant="destructive" className="h-4 px-1 text-[10px]">
83
- {unmatchedQueries.length}
84
- </Badge>
85
- </TooltipTrigger>
86
- <TooltipContent>Unmatched queries</TooltipContent>
87
- </Tooltip>
88
- )}
89
- </TabsList>
90
- </TooltipProvider>
91
-
92
- {pendingProposals.length > 0 && (
93
- <TabsContent value="pending" className="mt-4 space-y-3">
94
- {pendingProposals.slice(0, 10).map((p) => (
95
- <button
96
- key={p.proposal_id}
97
- type="button"
98
- onClick={() => {
99
- if (p.skill_name && onSelectProposal) onSelectProposal(p.skill_name, p.proposal_id);
100
- }}
101
- disabled={!p.skill_name || !onSelectProposal}
102
- className="flex w-full gap-3 rounded-md p-1.5 text-left transition-colors enabled:hover:bg-accent/40 disabled:cursor-default"
103
- >
104
- <div className="mt-1 size-2 shrink-0 rounded-full bg-primary-accent" />
105
- <div className="flex-1 min-w-0 space-y-1">
106
- <div className="flex items-center gap-2">
107
- <Badge variant={ACTION_VARIANT[p.action] ?? "secondary"} className="text-[10px]">
108
- {p.action}
109
- </Badge>
110
- <span className="text-[10px] text-slate-500 font-mono">
111
- {timeAgo(p.timestamp)}
112
- </span>
113
- </div>
114
- <p className="text-xs text-muted-foreground line-clamp-2">{p.details}</p>
115
- {p.skill_name && (
116
- <span className="text-[10px] text-muted-foreground/60 font-mono">
117
- {p.skill_name} · #{p.proposal_id.slice(0, 8)}
118
- </span>
119
- )}
120
- </div>
121
- </button>
122
- ))}
123
- </TabsContent>
124
- )}
125
-
126
- <TabsContent value="timeline" className="mt-4 space-y-3">
127
- {evolution.slice(0, 30).map((entry, i) => (
128
- <button
129
- key={`${entry.proposal_id}-${i}`}
130
- type="button"
131
- onClick={() => {
132
- if (entry.skill_name && onSelectProposal)
133
- onSelectProposal(entry.skill_name, entry.proposal_id);
134
- }}
135
- disabled={!entry.skill_name || !onSelectProposal}
136
- className="flex w-full gap-3 rounded-md p-1.5 text-left transition-colors enabled:hover:bg-accent/40 disabled:cursor-default"
137
- >
138
- <div
139
- className={`mt-1 size-2 shrink-0 rounded-full ${
140
- entry.action === "deployed"
141
- ? "bg-primary"
142
- : entry.action === "rejected" || entry.action === "rolled_back"
143
- ? "bg-destructive"
144
- : entry.action === "validated"
145
- ? "bg-primary-accent"
146
- : "bg-primary-accent"
147
- }`}
148
- />
149
- <div className="flex-1 min-w-0 space-y-1">
150
- <div className="flex items-center gap-2">
151
- <Badge
152
- variant={ACTION_VARIANT[entry.action] ?? "secondary"}
153
- className="text-[10px]"
154
- >
155
- {entry.action}
156
- </Badge>
157
- <span className="text-xs text-muted-foreground font-mono">
158
- {timeAgo(entry.timestamp)}
159
- </span>
160
- </div>
161
- <p className="text-xs text-muted-foreground line-clamp-2">{entry.details}</p>
162
- <span className="text-[10px] text-muted-foreground/60 font-mono">
163
- {entry.skill_name ? `${entry.skill_name} · ` : ""}#{entry.proposal_id.slice(0, 8)}
164
- </span>
165
- </div>
166
- </button>
167
- ))}
168
- {evolution.length === 0 && (
169
- <p className="text-sm text-muted-foreground py-4 text-center">No timeline events</p>
170
- )}
171
- </TabsContent>
172
-
173
- {unmatchedQueries.length > 0 && (
174
- <TabsContent value="unmatched" className="mt-4 space-y-2">
175
- {unmatchedQueries.slice(0, 15).map((q, i) => (
176
- <div key={`${q.session_id}-${i}`} className="flex gap-3">
177
- <div className="mt-1 size-2 shrink-0 rounded-full bg-muted-foreground/40" />
178
- <div className="flex-1 min-w-0 space-y-0.5">
179
- <span className="font-mono text-xs text-muted-foreground">
180
- {timeAgo(q.timestamp)}
181
- </span>
182
- <p className="line-clamp-2 font-mono text-xs text-foreground/80">{q.query}</p>
183
- </div>
184
- </div>
185
- ))}
186
- </TabsContent>
187
- )}
188
- </Tabs>
189
- ) : (
190
- <p className="py-6 text-center text-sm text-muted-foreground">No recent activity</p>
191
- );
192
-
193
- if (embedded) {
194
- return <div>{content}</div>;
195
- }
196
-
197
32
  if (!hasActivity) {
198
33
  return (
199
34
  <Card>
@@ -219,7 +54,156 @@ export function ActivityPanel({
219
54
  </CardTitle>
220
55
  <CardDescription>Recent evolution events and queries</CardDescription>
221
56
  </CardHeader>
222
- <CardContent>{content}</CardContent>
57
+ <CardContent>
58
+ <Tabs
59
+ defaultValue={
60
+ pendingProposals.length > 0
61
+ ? "pending"
62
+ : evolution.length > 0
63
+ ? "timeline"
64
+ : "unmatched"
65
+ }
66
+ >
67
+ <TooltipProvider>
68
+ <TabsList className="w-full">
69
+ {pendingProposals.length > 0 && (
70
+ <Tooltip>
71
+ <TooltipTrigger
72
+ render={<TabsTrigger value="pending" className="flex-1 gap-1.5" />}
73
+ >
74
+ <GitPullRequestArrowIcon className="size-3.5" />
75
+ <Badge variant="secondary" className="h-4 px-1 text-[10px]">
76
+ {pendingProposals.length}
77
+ </Badge>
78
+ </TooltipTrigger>
79
+ <TooltipContent>Pending proposals</TooltipContent>
80
+ </Tooltip>
81
+ )}
82
+ <Tooltip>
83
+ <TooltipTrigger render={<TabsTrigger value="timeline" className="flex-1" />}>
84
+ <ClockIcon className="size-3.5" />
85
+ </TooltipTrigger>
86
+ <TooltipContent>Timeline</TooltipContent>
87
+ </Tooltip>
88
+ {unmatchedQueries.length > 0 && (
89
+ <Tooltip>
90
+ <TooltipTrigger
91
+ render={<TabsTrigger value="unmatched" className="flex-1 gap-1.5" />}
92
+ >
93
+ <SearchXIcon className="size-3.5" />
94
+ <Badge variant="destructive" className="h-4 px-1 text-[10px]">
95
+ {unmatchedQueries.length}
96
+ </Badge>
97
+ </TooltipTrigger>
98
+ <TooltipContent>Unmatched queries</TooltipContent>
99
+ </Tooltip>
100
+ )}
101
+ </TabsList>
102
+ </TooltipProvider>
103
+
104
+ {pendingProposals.length > 0 && (
105
+ <TabsContent value="pending" className="mt-4 space-y-3">
106
+ {pendingProposals.slice(0, 10).map((p) => (
107
+ <button
108
+ key={p.proposal_id}
109
+ type="button"
110
+ onClick={() => {
111
+ if (p.skill_name && onSelectProposal)
112
+ onSelectProposal(p.skill_name, p.proposal_id);
113
+ }}
114
+ disabled={!p.skill_name || !onSelectProposal}
115
+ className="flex w-full gap-3 rounded-md p-1.5 text-left transition-colors enabled:hover:bg-accent/40 disabled:cursor-default"
116
+ >
117
+ <div className="mt-1 size-2 shrink-0 rounded-full bg-amber-400" />
118
+ <div className="flex-1 min-w-0 space-y-1">
119
+ <div className="flex items-center gap-2">
120
+ <Badge
121
+ variant={ACTION_VARIANT[p.action] ?? "secondary"}
122
+ className="text-[10px]"
123
+ >
124
+ {p.action}
125
+ </Badge>
126
+ <span className="text-xs text-muted-foreground font-mono">
127
+ {timeAgo(p.timestamp)}
128
+ </span>
129
+ </div>
130
+ <p className="text-xs text-muted-foreground line-clamp-2">{p.details}</p>
131
+ {p.skill_name && (
132
+ <span className="text-[10px] text-muted-foreground/60 font-mono">
133
+ {p.skill_name} · #{p.proposal_id.slice(0, 8)}
134
+ </span>
135
+ )}
136
+ </div>
137
+ </button>
138
+ ))}
139
+ </TabsContent>
140
+ )}
141
+
142
+ <TabsContent value="timeline" className="mt-4 space-y-3">
143
+ {evolution.slice(0, 30).map((entry, i) => (
144
+ <button
145
+ key={`${entry.proposal_id}-${i}`}
146
+ type="button"
147
+ onClick={() => {
148
+ if (entry.skill_name && onSelectProposal)
149
+ onSelectProposal(entry.skill_name, entry.proposal_id);
150
+ }}
151
+ disabled={!entry.skill_name || !onSelectProposal}
152
+ className="flex w-full gap-3 rounded-md p-1.5 text-left transition-colors enabled:hover:bg-accent/40 disabled:cursor-default"
153
+ >
154
+ <div
155
+ className={`mt-1 size-2 shrink-0 rounded-full ${
156
+ entry.action === "deployed"
157
+ ? "bg-emerald-500"
158
+ : entry.action === "rejected" || entry.action === "rolled_back"
159
+ ? "bg-red-500"
160
+ : entry.action === "validated"
161
+ ? "bg-amber-400"
162
+ : "bg-primary-accent"
163
+ }`}
164
+ />
165
+ <div className="flex-1 min-w-0 space-y-1">
166
+ <div className="flex items-center gap-2">
167
+ <Badge
168
+ variant={ACTION_VARIANT[entry.action] ?? "secondary"}
169
+ className="text-[10px]"
170
+ >
171
+ {entry.action}
172
+ </Badge>
173
+ <span className="text-xs text-muted-foreground font-mono">
174
+ {timeAgo(entry.timestamp)}
175
+ </span>
176
+ </div>
177
+ <p className="text-xs text-muted-foreground line-clamp-2">{entry.details}</p>
178
+ <span className="text-[10px] text-muted-foreground/60 font-mono">
179
+ {entry.skill_name ? `${entry.skill_name} · ` : ""}#
180
+ {entry.proposal_id.slice(0, 8)}
181
+ </span>
182
+ </div>
183
+ </button>
184
+ ))}
185
+ {evolution.length === 0 && (
186
+ <p className="text-sm text-muted-foreground text-center py-4">No timeline events</p>
187
+ )}
188
+ </TabsContent>
189
+
190
+ {unmatchedQueries.length > 0 && (
191
+ <TabsContent value="unmatched" className="mt-4 space-y-2">
192
+ {unmatchedQueries.slice(0, 15).map((q, i) => (
193
+ <div key={`${q.session_id}-${i}`} className="flex gap-3">
194
+ <div className="mt-1 size-2 shrink-0 rounded-full bg-muted-foreground/40" />
195
+ <div className="flex-1 min-w-0 space-y-0.5">
196
+ <span className="text-xs text-muted-foreground font-mono">
197
+ {timeAgo(q.timestamp)}
198
+ </span>
199
+ <p className="text-xs font-mono text-foreground/80 line-clamp-2">{q.query}</p>
200
+ </div>
201
+ </div>
202
+ ))}
203
+ </TabsContent>
204
+ )}
205
+ </Tabs>
206
+ </CardContent>
223
207
  </Card>
224
208
  );
225
209
  }