claude-smart 0.2.31 → 0.2.32

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 (59) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +172 -18
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/api/config/route.ts +11 -2
  7. package/plugin/dashboard/app/api/health/route.ts +45 -6
  8. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
  9. package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
  10. package/plugin/dashboard/app/configure/env/page.tsx +36 -31
  11. package/plugin/dashboard/app/configure/layout.tsx +1 -1
  12. package/plugin/dashboard/app/configure/server/page.tsx +8 -14
  13. package/plugin/dashboard/app/dashboard/page.tsx +311 -115
  14. package/plugin/dashboard/app/globals.css +80 -66
  15. package/plugin/dashboard/app/layout.tsx +13 -10
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
  17. package/plugin/dashboard/app/preferences/page.tsx +154 -54
  18. package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
  19. package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
  20. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
  21. package/plugin/dashboard/app/sessions/page.tsx +14 -10
  22. package/plugin/dashboard/app/skills/page.tsx +175 -56
  23. package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
  24. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
  25. package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
  26. package/plugin/dashboard/components/common/empty-state.tsx +4 -2
  27. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  28. package/plugin/dashboard/components/common/page-header.tsx +5 -3
  29. package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
  30. package/plugin/dashboard/components/common/stat-card.tsx +9 -5
  31. package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
  32. package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
  33. package/plugin/dashboard/components/ui/input.tsx +1 -0
  34. package/plugin/dashboard/hooks/use-settings.tsx +30 -61
  35. package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
  36. package/plugin/dashboard/lib/config-file.ts +2 -0
  37. package/plugin/dashboard/lib/reflexio-client.ts +23 -48
  38. package/plugin/dashboard/lib/session-reader.ts +222 -6
  39. package/plugin/dashboard/lib/types.ts +20 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/pyproject.toml +1 -1
  43. package/plugin/scripts/_lib.sh +61 -0
  44. package/plugin/scripts/backend-service.sh +13 -2
  45. package/plugin/scripts/cli.sh +1 -0
  46. package/plugin/scripts/codex-hook.js +100 -3
  47. package/plugin/scripts/dashboard-service.sh +95 -19
  48. package/plugin/scripts/hook_entry.sh +13 -7
  49. package/plugin/scripts/smart-install.sh +18 -13
  50. package/plugin/src/claude_smart/cli.py +86 -20
  51. package/plugin/src/claude_smart/context_format.py +244 -6
  52. package/plugin/src/claude_smart/context_inject.py +8 -1
  53. package/plugin/src/claude_smart/cs_cite.py +186 -34
  54. package/plugin/src/claude_smart/env_config.py +102 -0
  55. package/plugin/src/claude_smart/events/stop.py +32 -6
  56. package/plugin/src/claude_smart/internal_call.py +30 -0
  57. package/plugin/src/claude_smart/publish.py +5 -0
  58. package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
  59. package/plugin/uv.lock +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import {
6
6
  BookOpen,
@@ -8,6 +8,7 @@ import {
8
8
  Sparkles,
9
9
  Activity,
10
10
  ExternalLink,
11
+ Users,
11
12
  } from "lucide-react";
12
13
  import { LearningsBadge } from "@/components/common/learnings-badge";
13
14
  import { PageHeader } from "@/components/common/page-header";
@@ -15,16 +16,35 @@ import { StatCard } from "@/components/common/stat-card";
15
16
  import { EmptyState } from "@/components/common/empty-state";
16
17
  import { Badge } from "@/components/ui/badge";
17
18
  import { reflexio } from "@/lib/reflexio-client";
18
- import { useSettings } from "@/hooks/use-settings";
19
- import { formatRelative, truncateId } from "@/lib/format";
19
+ import { formatRelative, truncate, truncateId } from "@/lib/format";
20
20
  import { agentPlaybookStatusLabel } from "@/lib/status";
21
- import type { AgentPlaybook, SessionSummary, UserPlaybook } from "@/lib/types";
21
+ import type {
22
+ AgentPlaybook,
23
+ PlaybookApplicationStat,
24
+ SessionSummary,
25
+ UserPlaybook,
26
+ UserProfile,
27
+ } from "@/lib/types";
28
+
29
+ type RecentLearningKind = "project-skill" | "shared-skill" | "preference";
30
+
31
+ interface RecentLearning {
32
+ id: string;
33
+ kind: RecentLearningKind;
34
+ href: string;
35
+ content: string;
36
+ createdAt: number;
37
+ statKey: string;
38
+ }
22
39
 
23
40
  export default function DashboardPage() {
24
- const { reflexioUrl } = useSettings();
25
41
  const [sessions, setSessions] = useState<SessionSummary[] | null>(null);
26
42
  const [projectSkills, setProjectSkills] = useState<UserPlaybook[] | null>(null);
27
43
  const [sharedSkills, setSharedSkills] = useState<AgentPlaybook[] | null>(null);
44
+ const [preferences, setPreferences] = useState<UserProfile[] | null>(null);
45
+ const [topApplied, setTopApplied] = useState<PlaybookApplicationStat[] | null>(
46
+ null,
47
+ );
28
48
  const [error, setError] = useState<string | null>(null);
29
49
 
30
50
  useEffect(() => {
@@ -32,19 +52,32 @@ export default function DashboardPage() {
32
52
  async function load() {
33
53
  setError(null);
34
54
  try {
35
- const [sRes, projectRes, sharedRes] = await Promise.all([
55
+ const [sRes, projectRes, sharedRes, prefRes, statsRes] = await Promise.all([
36
56
  fetch("/api/sessions", { cache: "no-store" }).then((r) => r.json()),
37
57
  reflexio
38
- .getUserPlaybooks({ reflexioUrl })
58
+ .getUserPlaybooks({})
39
59
  .catch(() => ({ user_playbooks: [] as UserPlaybook[] })),
40
60
  reflexio
41
- .getAgentPlaybooks({ reflexioUrl })
61
+ .getAgentPlaybooks({})
42
62
  .catch(() => ({ agent_playbooks: [] as AgentPlaybook[] })),
63
+ reflexio
64
+ .getAllProfiles({ limit: 100 })
65
+ .catch(() => ({ user_profiles: [] as UserProfile[] })),
66
+ fetch("/api/rules/applied?daysBack=30&limit=200", {
67
+ cache: "no-store",
68
+ })
69
+ .then((r) => r.json())
70
+ .catch(() => ({
71
+ success: false,
72
+ stats: [] as PlaybookApplicationStat[],
73
+ })),
43
74
  ]);
44
75
  if (cancelled) return;
45
76
  setSessions(sRes.sessions ?? []);
46
77
  setProjectSkills(projectRes.user_playbooks ?? []);
47
78
  setSharedSkills(sharedRes.agent_playbooks ?? []);
79
+ setPreferences(prefRes.user_profiles ?? []);
80
+ setTopApplied(statsRes.stats ?? []);
48
81
  } catch (e) {
49
82
  if (!cancelled)
50
83
  setError(e instanceof Error ? e.message : "failed to load");
@@ -54,7 +87,7 @@ export default function DashboardPage() {
54
87
  return () => {
55
88
  cancelled = true;
56
89
  };
57
- }, [reflexioUrl]);
90
+ }, []);
58
91
 
59
92
  // CURRENT project-specific skills arrive as `status: null` (response_model_exclude_none
60
93
  // strips the field). Anything else (e.g. "archived", "pending") is excluded.
@@ -62,6 +95,52 @@ export default function DashboardPage() {
62
95
  const approvedSharedSkills = (sharedSkills ?? []).filter(
63
96
  (p) => agentPlaybookStatusLabel(p) === "APPROVED",
64
97
  );
98
+ const currentPreferences = (preferences ?? []).filter((p) => p.status == null);
99
+ const statsByRule = useMemo(() => {
100
+ const map = new Map<string, PlaybookApplicationStat>();
101
+ for (const s of topApplied ?? []) {
102
+ map.set(`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`, s);
103
+ }
104
+ return map;
105
+ }, [topApplied]);
106
+ const recentLearnings = useMemo(() => {
107
+ const items: RecentLearning[] = [
108
+ ...currentProjectSkills.map((p) => ({
109
+ id: `project:${p.user_playbook_id}`,
110
+ kind: "project-skill" as const,
111
+ href: `/skills/project/${encodeURIComponent(p.user_playbook_id)}`,
112
+ content: p.content,
113
+ createdAt: p.created_at,
114
+ statKey: `playbook:user_playbook:${p.user_playbook_id}`,
115
+ })),
116
+ ...approvedSharedSkills.map((p) => ({
117
+ id: `shared:${p.agent_playbook_id}`,
118
+ kind: "shared-skill" as const,
119
+ href: `/skills/shared/${encodeURIComponent(p.agent_playbook_id)}`,
120
+ content: p.content,
121
+ createdAt: p.created_at,
122
+ statKey: `playbook:agent_playbook:${p.agent_playbook_id}`,
123
+ })),
124
+ ...currentPreferences.map((p) => ({
125
+ id: `preference:${p.profile_id}`,
126
+ kind: "preference" as const,
127
+ href: `/preferences/project/${encodeURIComponent(p.profile_id)}`,
128
+ content: p.content,
129
+ createdAt: p.last_modified_timestamp,
130
+ statKey: `profile:profile:${p.profile_id}`,
131
+ })),
132
+ ];
133
+ const seen = new Set<string>();
134
+ return items
135
+ .sort((a, b) => b.createdAt - a.createdAt)
136
+ .filter((item) => {
137
+ const key = `${item.kind}:${item.content.trim().toLowerCase()}`;
138
+ if (seen.has(key)) return false;
139
+ seen.add(key);
140
+ return true;
141
+ })
142
+ .slice(0, 4);
143
+ }, [approvedSharedSkills, currentPreferences, currentProjectSkills]);
65
144
  const learningInteractionTotal = (sessions ?? []).reduce(
66
145
  (acc, s) => acc + s.learning_interaction_count,
67
146
  0,
@@ -107,121 +186,238 @@ export default function DashboardPage() {
107
186
  </div>
108
187
  )}
109
188
 
110
- <section>
111
- <div className="flex items-center justify-between mb-3">
112
- <h2 className="text-sm font-semibold">Recent sessions</h2>
113
- <Link
114
- href="/sessions"
115
- className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
116
- >
117
- View all <ExternalLink className="h-3 w-3" />
118
- </Link>
119
- </div>
120
- {sessions && sessions.length > 0 ? (
121
- <div className="rounded-xl border border-border divide-y divide-border bg-card">
122
- {sessions.slice(0, 5).map((s) => (
123
- <Link
124
- key={s.session_id}
125
- href={`/sessions/${s.session_id}`}
126
- className="flex items-center justify-between px-4 py-3 hover:bg-accent/40 transition-colors"
127
- >
128
- <div className="min-w-0 flex items-center gap-3">
129
- <MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
130
- <code className="font-mono text-xs truncate">
131
- {truncateId(s.session_id, 10, 6)}
132
- </code>
133
- <LearningsBadge count={s.learning_interaction_count} />
134
- </div>
135
- <div className="flex items-center gap-4 text-xs text-muted-foreground shrink-0">
136
- <span>{s.turn_count} turns</span>
137
- <span>{formatRelative(s.last_activity)}</span>
138
- </div>
139
- </Link>
140
- ))}
189
+ <div className="grid min-w-0 gap-6 lg:grid-cols-2">
190
+ <section className="min-w-0">
191
+ <div className="flex items-center justify-between mb-3">
192
+ <div>
193
+ <h2 className="text-sm font-semibold">Recent sessions</h2>
194
+ <p className="text-xs text-muted-foreground">
195
+ Latest local buffers and cited learnings.
196
+ </p>
197
+ </div>
198
+ <Link
199
+ href="/sessions"
200
+ className="text-xs text-primary hover:text-foreground inline-flex items-center gap-1"
201
+ >
202
+ View all <ExternalLink className="h-3 w-3" />
203
+ </Link>
141
204
  </div>
142
- ) : (
143
- <EmptyState
144
- icon={MessageSquare}
145
- title="No sessions yet"
146
- description="Run Claude Code with claude-smart enabled — sessions will appear here."
147
- />
148
- )}
149
- </section>
150
-
151
- <section>
152
- <div className="flex items-center justify-between mb-3">
153
- <h2 className="text-sm font-semibold">Recent skills</h2>
154
- <Link
155
- href="/skills"
156
- className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
157
- >
158
- View all <ExternalLink className="h-3 w-3" />
159
- </Link>
160
- </div>
161
- {currentProjectSkills.length > 0 || approvedSharedSkills.length > 0 ? (
162
- <div className="grid gap-3 sm:grid-cols-2">
163
- {currentProjectSkills.slice(0, 2).map((p) => (
205
+ {sessions && sessions.length > 0 ? (
206
+ <div className="rounded-lg border border-border divide-y divide-border bg-card/92 shadow-sm overflow-hidden">
207
+ {sessions.slice(0, 5).map((s) => (
208
+ <Link
209
+ key={s.session_id}
210
+ href={`/sessions/${s.session_id}`}
211
+ className="flex flex-col gap-2 px-4 py-3 transition-colors hover:bg-accent/45 sm:flex-row sm:items-center sm:justify-between"
212
+ >
213
+ <div className="min-w-0 flex w-full items-center gap-3">
214
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
215
+ <MessageSquare className="h-4 w-4" />
216
+ </span>
217
+ <code className="font-mono text-xs truncate">
218
+ {truncateId(s.session_id, 10, 6)}
219
+ </code>
220
+ <LearningsBadge count={s.learning_interaction_count} />
221
+ </div>
222
+ <div className="flex w-full items-center justify-end gap-4 text-xs text-muted-foreground sm:w-auto sm:shrink-0">
223
+ <span>{s.turn_count} turns</span>
224
+ <span>{formatRelative(s.last_activity)}</span>
225
+ </div>
226
+ </Link>
227
+ ))}
228
+ </div>
229
+ ) : (
230
+ <EmptyState
231
+ icon={MessageSquare}
232
+ title="No sessions yet"
233
+ description="Run Claude Code with claude-smart enabled — sessions will appear here."
234
+ />
235
+ )}
236
+ </section>
237
+
238
+ <section className="min-w-0">
239
+ <div className="flex items-center justify-between mb-3">
240
+ <div>
241
+ <h2 className="text-sm font-semibold">Recent learnings</h2>
242
+ <p className="text-xs text-muted-foreground">
243
+ New skills and preferences extracted from recent patterns.
244
+ </p>
245
+ </div>
246
+ <div className="flex items-center gap-3">
164
247
  <Link
165
- key={`project:${p.user_playbook_id}`}
166
- href={`/skills/project/${p.user_playbook_id}`}
167
- className="block rounded-xl border border-border bg-card p-4 hover:bg-accent/40 transition-colors"
248
+ href="/skills"
249
+ className="text-xs text-primary hover:text-foreground inline-flex items-center gap-1"
168
250
  >
169
- <div className="flex items-center justify-between gap-2 mb-2">
170
- <Badge variant="outline" className="font-mono text-[10px]">
171
- {p.agent_version || "default"}
172
- </Badge>
173
- <Badge variant="secondary" className="text-[10px]">
174
- project-specific
175
- </Badge>
176
- <span className="text-[11px] text-muted-foreground">
177
- {formatRelative(p.created_at)}
178
- </span>
179
- </div>
180
- <p className="text-sm line-clamp-3">{p.content}</p>
181
- {p.trigger && (
182
- <p className="text-xs text-muted-foreground mt-2 line-clamp-1">
183
- <span className="font-medium">trigger:</span> {p.trigger}
184
- </p>
185
- )}
251
+ Skills <ExternalLink className="h-3 w-3" />
186
252
  </Link>
187
- ))}
188
- {approvedSharedSkills.slice(0, 2).map((p) => (
189
253
  <Link
190
- key={`shared:${p.agent_playbook_id}`}
191
- href={`/skills/shared/${p.agent_playbook_id}`}
192
- className="block rounded-xl border border-border bg-card p-4 hover:bg-accent/40 transition-colors"
254
+ href="/preferences"
255
+ className="text-xs text-primary hover:text-foreground inline-flex items-center gap-1"
193
256
  >
194
- <div className="flex items-center justify-between gap-2 mb-2">
195
- <div className="flex items-center gap-2">
196
- <Badge variant="outline" className="font-mono text-[10px]">
197
- {p.agent_version || "default"}
198
- </Badge>
199
- <Badge variant="secondary" className="text-[10px]">
200
- shared
201
- </Badge>
202
- </div>
203
- <span className="text-[11px] text-muted-foreground">
204
- {formatRelative(p.created_at)}
205
- </span>
206
- </div>
207
- <p className="text-sm line-clamp-3">{p.content}</p>
208
- {p.trigger && (
209
- <p className="text-xs text-muted-foreground mt-2 line-clamp-1">
210
- <span className="font-medium">trigger:</span> {p.trigger}
211
- </p>
212
- )}
257
+ Preferences <ExternalLink className="h-3 w-3" />
213
258
  </Link>
214
- ))}
259
+ </div>
215
260
  </div>
216
- ) : (
217
- <EmptyState
218
- icon={BookOpen}
219
- title="No skills yet"
220
- description="Keep using Claude with claude-smart enabled. Skills are extracted automatically when patterns emerge."
221
- />
222
- )}
223
- </section>
261
+ {recentLearnings.length > 0 ? (
262
+ <div className="rounded-lg border border-border divide-y divide-border bg-card/92 shadow-sm overflow-hidden">
263
+ {recentLearnings.map((item) => {
264
+ const stat = statsByRule.get(item.statKey);
265
+ return (
266
+ <Link
267
+ key={item.id}
268
+ href={item.href}
269
+ className="flex min-w-0 items-center justify-between gap-3 px-4 py-3 transition-colors hover:bg-accent/45"
270
+ >
271
+ <div className="min-w-0 flex flex-1 items-center gap-3">
272
+ {learningIcon(item.kind)}
273
+ <div className="min-w-0 flex-1">
274
+ <p className="text-sm truncate">{item.content}</p>
275
+ <div className="mt-1 flex flex-wrap items-center gap-2">
276
+ <Badge variant="outline" className="h-5 text-[10px]">
277
+ {learningKindLabel(item.kind)}
278
+ </Badge>
279
+ <span className="text-[11px] text-muted-foreground">
280
+ {learningScope(item.kind)}
281
+ </span>
282
+ <LearningApplicationStatBadge stat={stat} />
283
+ </div>
284
+ </div>
285
+ </div>
286
+ <div className="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
287
+ <span>{formatRelative(item.createdAt)}</span>
288
+ </div>
289
+ </Link>
290
+ );
291
+ })}
292
+ </div>
293
+ ) : (
294
+ <EmptyState
295
+ icon={BookOpen}
296
+ title="No learnings yet"
297
+ description="Keep using Claude with claude-smart enabled. Skills and preferences are extracted automatically when patterns emerge."
298
+ />
299
+ )}
300
+ </section>
301
+
302
+ <section className="min-w-0 lg:col-span-2">
303
+ <div className="flex items-center justify-between mb-3">
304
+ <div className="flex items-center gap-2">
305
+ <h2 className="text-sm font-semibold">
306
+ Most used claude-smart learnings
307
+ </h2>
308
+ <span className="text-[11px] text-muted-foreground">
309
+ last 30 days
310
+ </span>
311
+ </div>
312
+ <Link
313
+ href="/sessions"
314
+ className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
315
+ >
316
+ Review sessions <ExternalLink className="h-3 w-3" />
317
+ </Link>
318
+ </div>
319
+ {topApplied === null ? (
320
+ <div className="text-sm text-muted-foreground">Loading...</div>
321
+ ) : topApplied.length > 0 ? (
322
+ <div className="rounded-lg border border-border divide-y divide-border bg-card/92 shadow-sm">
323
+ {topApplied.slice(0, 5).map((s) => {
324
+ const href = s.href ?? null;
325
+ const label = appliedRuleLabel(s);
326
+ const rowBody = (
327
+ <>
328
+ <div className="min-w-0 flex flex-1 items-center gap-3">
329
+ <Sparkles className="h-4 w-4 text-muted-foreground shrink-0" />
330
+ <div className="min-w-0 flex-1">
331
+ <p className="text-sm truncate">
332
+ {s.title || (
333
+ <span className="text-muted-foreground italic">
334
+ (rule removed)
335
+ </span>
336
+ )}
337
+ </p>
338
+ <p className="text-[11px] text-muted-foreground">
339
+ {label} · {truncate(s.real_id, 12)}
340
+ </p>
341
+ </div>
342
+ </div>
343
+ <div className="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
344
+ <Badge variant="secondary" className="text-[10px]">
345
+ Used {s.applied_count}×
346
+ </Badge>
347
+ <span>{formatRelative(s.last_applied_at)}</span>
348
+ </div>
349
+ </>
350
+ );
351
+ return href ? (
352
+ <Link
353
+ key={`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`}
354
+ href={href}
355
+ className="flex min-w-0 items-center justify-between gap-3 px-4 py-3 hover:bg-accent/40 transition-colors"
356
+ >
357
+ {rowBody}
358
+ </Link>
359
+ ) : (
360
+ <div
361
+ key={`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`}
362
+ className="flex min-w-0 items-center justify-between gap-3 px-4 py-3"
363
+ >
364
+ {rowBody}
365
+ </div>
366
+ );
367
+ })}
368
+ </div>
369
+ ) : (
370
+ <EmptyState
371
+ icon={Sparkles}
372
+ title="No applied learnings yet"
373
+ description="When a claude-smart learning is cited in a local assistant response, it will appear here with usage and recency."
374
+ />
375
+ )}
376
+ </section>
377
+ </div>
224
378
  </div>
225
379
  </div>
226
380
  );
227
381
  }
382
+
383
+ function appliedRuleLabel(stat: PlaybookApplicationStat): string {
384
+ if (stat.kind === "profile") return "preference";
385
+ return stat.source_kind === "agent_playbook" ? "shared skill" : "project skill";
386
+ }
387
+
388
+ function learningScope(kind: RecentLearningKind): string {
389
+ if (kind === "shared-skill") return "shared";
390
+ return "project";
391
+ }
392
+
393
+ function learningIcon(kind: RecentLearningKind) {
394
+ const Icon = kind === "preference" ? Users : BookOpen;
395
+ return <Icon className="h-4 w-4 text-muted-foreground shrink-0" />;
396
+ }
397
+
398
+ function learningKindLabel(kind: RecentLearningKind): string {
399
+ return kind === "preference" ? "preference" : "skill";
400
+ }
401
+
402
+ function LearningApplicationStatBadge({
403
+ stat,
404
+ }: {
405
+ stat: PlaybookApplicationStat | undefined;
406
+ }) {
407
+ if (!stat || stat.applied_count === 0) {
408
+ return (
409
+ <Badge
410
+ variant="outline"
411
+ className="h-5 text-[10px] text-muted-foreground"
412
+ >
413
+ Never applied
414
+ </Badge>
415
+ );
416
+ }
417
+ const last = formatRelative(stat.last_applied_at);
418
+ return (
419
+ <Badge variant="secondary" className="h-5 text-[10px]">
420
+ Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
421
+ </Badge>
422
+ );
423
+ }