claude-smart 0.2.30 → 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.
- package/README.md +2 -2
- package/bin/claude-smart.js +172 -18
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/api/config/route.ts +11 -2
- package/plugin/dashboard/app/api/health/route.ts +45 -6
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
- package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
- package/plugin/dashboard/app/configure/env/page.tsx +36 -31
- package/plugin/dashboard/app/configure/layout.tsx +1 -1
- package/plugin/dashboard/app/configure/server/page.tsx +8 -14
- package/plugin/dashboard/app/dashboard/page.tsx +311 -115
- package/plugin/dashboard/app/globals.css +80 -66
- package/plugin/dashboard/app/layout.tsx +13 -10
- package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
- package/plugin/dashboard/app/preferences/page.tsx +154 -54
- package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
- package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
- package/plugin/dashboard/app/sessions/page.tsx +14 -10
- package/plugin/dashboard/app/skills/page.tsx +175 -56
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
- package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
- package/plugin/dashboard/components/common/empty-state.tsx +4 -2
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +5 -3
- package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
- package/plugin/dashboard/components/common/stat-card.tsx +9 -5
- package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
- package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
- package/plugin/dashboard/components/ui/input.tsx +1 -0
- package/plugin/dashboard/hooks/use-settings.tsx +30 -61
- package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
- package/plugin/dashboard/lib/config-file.ts +2 -0
- package/plugin/dashboard/lib/reflexio-client.ts +23 -48
- package/plugin/dashboard/lib/session-reader.ts +222 -6
- package/plugin/dashboard/lib/types.ts +20 -1
- package/plugin/dashboard/package-lock.json +70 -95
- package/plugin/dashboard/package.json +5 -2
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +126 -0
- package/plugin/scripts/backend-service.sh +32 -7
- package/plugin/scripts/cli.sh +4 -2
- package/plugin/scripts/codex-hook.js +100 -3
- package/plugin/scripts/dashboard-service.sh +98 -19
- package/plugin/scripts/hook_entry.sh +32 -11
- package/plugin/scripts/smart-install.sh +27 -44
- package/plugin/src/claude_smart/cli.py +204 -20
- package/plugin/src/claude_smart/context_format.py +244 -6
- package/plugin/src/claude_smart/context_inject.py +8 -1
- package/plugin/src/claude_smart/cs_cite.py +186 -34
- package/plugin/src/claude_smart/env_config.py +102 -0
- package/plugin/src/claude_smart/events/session_end.py +171 -6
- package/plugin/src/claude_smart/events/session_start.py +26 -2
- package/plugin/src/claude_smart/events/stop.py +48 -9
- package/plugin/src/claude_smart/hook.py +62 -4
- package/plugin/src/claude_smart/hook_log.py +301 -0
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/publish.py +5 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
- package/plugin/uv.lock +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { AlertTriangle, ArrowLeft } from "lucide-react";
|
|
4
|
+
import { PageHeader } from "@/components/common/page-header";
|
|
5
|
+
import { EmptyState } from "@/components/common/empty-state";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { resolveRuleLink } from "@/lib/session-reader";
|
|
8
|
+
|
|
9
|
+
export const dynamic = "force-dynamic";
|
|
10
|
+
|
|
11
|
+
export default async function RuleResolverPage({
|
|
12
|
+
params,
|
|
13
|
+
}: {
|
|
14
|
+
params: Promise<{ id: string }>;
|
|
15
|
+
}) {
|
|
16
|
+
const { id: rawId } = await params;
|
|
17
|
+
let id: string;
|
|
18
|
+
try {
|
|
19
|
+
id = decodeURIComponent(rawId);
|
|
20
|
+
} catch {
|
|
21
|
+
id = rawId;
|
|
22
|
+
}
|
|
23
|
+
const resolved = await resolveRuleLink(id);
|
|
24
|
+
if (resolved) {
|
|
25
|
+
redirect(resolved.href);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex-1 overflow-auto">
|
|
30
|
+
<PageHeader
|
|
31
|
+
title="Rule link not found"
|
|
32
|
+
description="This short claude-smart link could not be resolved from the local session registry."
|
|
33
|
+
/>
|
|
34
|
+
<div className="p-6 max-w-2xl mx-auto">
|
|
35
|
+
<EmptyState
|
|
36
|
+
icon={AlertTriangle}
|
|
37
|
+
title="Rule link not found"
|
|
38
|
+
description="The session may have been cleared, or the short link may come from a different machine."
|
|
39
|
+
action={
|
|
40
|
+
<Link href="/sessions">
|
|
41
|
+
<Button variant="outline" size="sm">
|
|
42
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
43
|
+
Back to sessions
|
|
44
|
+
</Button>
|
|
45
|
+
</Link>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -121,7 +121,7 @@ export default function InteractionDetailPage({
|
|
|
121
121
|
<article
|
|
122
122
|
key={idx}
|
|
123
123
|
className={cn(
|
|
124
|
-
"rounded-
|
|
124
|
+
"rounded-lg border px-4 py-3 bg-card",
|
|
125
125
|
flagged
|
|
126
126
|
? "border-destructive/30 bg-destructive/5"
|
|
127
127
|
: "border-border",
|
|
@@ -177,7 +177,7 @@ export default function InteractionDetailPage({
|
|
|
177
177
|
<div className="mt-1 space-y-1.5">
|
|
178
178
|
{hasInput && (
|
|
179
179
|
<div>
|
|
180
|
-
<div className="text-[10px] font-mono uppercase
|
|
180
|
+
<div className="text-[10px] font-mono uppercase text-muted-foreground/70 mb-0.5">
|
|
181
181
|
input
|
|
182
182
|
</div>
|
|
183
183
|
<pre className="whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2 py-1 text-[10px] font-mono text-muted-foreground">
|
|
@@ -187,7 +187,7 @@ export default function InteractionDetailPage({
|
|
|
187
187
|
)}
|
|
188
188
|
{hasOutput && (
|
|
189
189
|
<div>
|
|
190
|
-
<div className="text-[10px] font-mono uppercase
|
|
190
|
+
<div className="text-[10px] font-mono uppercase text-muted-foreground/70 mb-0.5">
|
|
191
191
|
output
|
|
192
192
|
</div>
|
|
193
193
|
<pre className="whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2 py-1 text-[10px] font-mono text-muted-foreground">
|
|
@@ -241,7 +241,7 @@ function CitedItemsRow({ items }: { items: CitedItem[] }) {
|
|
|
241
241
|
? item.source_kind === "agent_playbook"
|
|
242
242
|
? `/skills/shared/${encodeURIComponent(targetId)}`
|
|
243
243
|
: `/skills/project/${encodeURIComponent(targetId)}`
|
|
244
|
-
: `/preferences/${encodeURIComponent(targetId)}`;
|
|
244
|
+
: `/preferences/project/${encodeURIComponent(targetId)}`;
|
|
245
245
|
return (
|
|
246
246
|
<Link
|
|
247
247
|
key={item.id}
|
|
@@ -60,12 +60,12 @@ export default function SessionsPage() {
|
|
|
60
60
|
actions={
|
|
61
61
|
<div className="flex items-center gap-2">
|
|
62
62
|
<div className="relative">
|
|
63
|
-
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
63
|
+
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
64
64
|
<Input
|
|
65
65
|
value={filter}
|
|
66
66
|
onChange={(e) => setFilter(e.target.value)}
|
|
67
67
|
placeholder="Search sessions…"
|
|
68
|
-
className="h-
|
|
68
|
+
className="h-9 w-64 pl-8 text-xs bg-background/80"
|
|
69
69
|
/>
|
|
70
70
|
</div>
|
|
71
71
|
<DeleteAllButton
|
|
@@ -108,14 +108,14 @@ export default function SessionsPage() {
|
|
|
108
108
|
{grouped.map(([label, items]) => (
|
|
109
109
|
<section key={label}>
|
|
110
110
|
<div className="flex items-center gap-2 mb-2 px-1">
|
|
111
|
-
<h2 className="text-[11px] uppercase
|
|
111
|
+
<h2 className="text-[11px] uppercase font-semibold text-muted-foreground">
|
|
112
112
|
{label}
|
|
113
113
|
</h2>
|
|
114
114
|
<span className="text-[11px] text-muted-foreground/70">
|
|
115
115
|
· {items.length}
|
|
116
116
|
</span>
|
|
117
117
|
</div>
|
|
118
|
-
<div className="rounded-
|
|
118
|
+
<div className="rounded-lg border border-border divide-y divide-border bg-card/90 overflow-hidden shadow-sm">
|
|
119
119
|
{items.map((s) => (
|
|
120
120
|
<div
|
|
121
121
|
key={s.session_id}
|
|
@@ -134,11 +134,14 @@ export default function SessionsPage() {
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
}}
|
|
137
|
-
className="flex
|
|
137
|
+
className="flex cursor-pointer flex-col gap-2 px-4 py-3 transition-colors hover:bg-accent/45 focus:bg-accent/45 focus:outline-none sm:flex-row sm:items-center"
|
|
138
138
|
>
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
140
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
|
141
|
+
<MessageSquare className="h-4 w-4" />
|
|
142
|
+
</span>
|
|
143
|
+
<div className="flex-1 min-w-0">
|
|
144
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
142
145
|
<p className="text-sm truncate">
|
|
143
146
|
{s.preview ?? (
|
|
144
147
|
<span className="text-muted-foreground italic">
|
|
@@ -167,10 +170,11 @@ export default function SessionsPage() {
|
|
|
167
170
|
{s.published_up_to} published
|
|
168
171
|
</span>
|
|
169
172
|
</>
|
|
170
|
-
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
171
175
|
</div>
|
|
172
176
|
</div>
|
|
173
|
-
<div className="text-xs text-muted-foreground shrink-0
|
|
177
|
+
<div className="self-end text-xs text-muted-foreground tabular-nums sm:shrink-0">
|
|
174
178
|
{formatRelative(s.last_activity)}
|
|
175
179
|
</div>
|
|
176
180
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { BookOpen, Layers3 } from "lucide-react";
|
|
5
|
+
import { BookOpen, ChevronRight, Layers3 } from "lucide-react";
|
|
6
6
|
import { PageHeader } from "@/components/common/page-header";
|
|
7
7
|
import { EmptyState } from "@/components/common/empty-state";
|
|
8
8
|
import { DeleteAllButton } from "@/components/common/delete-all-button";
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
SelectValue,
|
|
18
18
|
} from "@/components/ui/select";
|
|
19
19
|
import { reflexio } from "@/lib/reflexio-client";
|
|
20
|
-
import { useSettings } from "@/hooks/use-settings";
|
|
21
20
|
import { formatRelative } from "@/lib/format";
|
|
22
21
|
import { cn } from "@/lib/utils";
|
|
23
22
|
import {
|
|
@@ -26,10 +25,17 @@ import {
|
|
|
26
25
|
type AgentPlaybookStatusLabel,
|
|
27
26
|
type StatusLabel,
|
|
28
27
|
} from "@/lib/status";
|
|
29
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
AgentPlaybook,
|
|
30
|
+
PlaybookApplicationStat,
|
|
31
|
+
UserPlaybook,
|
|
32
|
+
} from "@/lib/types";
|
|
30
33
|
|
|
31
34
|
type SkillKind = "project" | "shared";
|
|
32
35
|
type SkillStatus = StatusLabel | AgentPlaybookStatusLabel;
|
|
36
|
+
type SkillSort = "newest" | "applied";
|
|
37
|
+
|
|
38
|
+
const ALL_LIFECYCLE_STATUSES: (string | null)[] = [null, "pending", "archived"];
|
|
33
39
|
|
|
34
40
|
const SHARED_STATUS_META: Record<
|
|
35
41
|
AgentPlaybookStatusLabel,
|
|
@@ -86,27 +92,51 @@ function sharedSkill(p: AgentPlaybook): SkillCard {
|
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
function skillStatKey(skill: SkillCard): string {
|
|
96
|
+
const sourceKind =
|
|
97
|
+
skill.kind === "shared" ? "agent_playbook" : "user_playbook";
|
|
98
|
+
return `playbook:${sourceKind}:${skill.id}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
export default function SkillsPage() {
|
|
90
|
-
const { reflexioUrl } = useSettings();
|
|
91
102
|
const [projectSkills, setProjectSkills] = useState<UserPlaybook[] | null>(null);
|
|
92
103
|
const [sharedSkills, setSharedSkills] = useState<AgentPlaybook[] | null>(null);
|
|
104
|
+
const [appStats, setAppStats] = useState<PlaybookApplicationStat[] | null>(
|
|
105
|
+
null,
|
|
106
|
+
);
|
|
93
107
|
const [error, setError] = useState<string | null>(null);
|
|
94
108
|
const [activeKind, setActiveKind] = useState<SkillKind>("project");
|
|
95
109
|
const [agentVersion, setAgentVersion] = useState<string>("__all__");
|
|
96
110
|
const [statusFilter, setStatusFilter] = useState<string>("CURRENT");
|
|
111
|
+
const [sortBy, setSortBy] = useState<SkillSort>("newest");
|
|
97
112
|
const [search, setSearch] = useState("");
|
|
98
113
|
|
|
99
114
|
useEffect(() => {
|
|
100
115
|
let cancelled = false;
|
|
101
116
|
async function load() {
|
|
102
117
|
try {
|
|
103
|
-
const [projectRes, sharedRes] = await Promise.all([
|
|
104
|
-
reflexio.getUserPlaybooks({
|
|
105
|
-
|
|
118
|
+
const [projectRes, sharedRes, statsRes] = await Promise.all([
|
|
119
|
+
reflexio.getUserPlaybooks({
|
|
120
|
+
limit: 500,
|
|
121
|
+
statusFilter: ALL_LIFECYCLE_STATUSES,
|
|
122
|
+
}),
|
|
123
|
+
reflexio.getAgentPlaybooks({
|
|
124
|
+
limit: 500,
|
|
125
|
+
statusFilter: ALL_LIFECYCLE_STATUSES,
|
|
126
|
+
}),
|
|
127
|
+
fetch("/api/rules/applied?daysBack=30&limit=200", {
|
|
128
|
+
cache: "no-store",
|
|
129
|
+
})
|
|
130
|
+
.then((r) => r.json())
|
|
131
|
+
.catch(() => ({
|
|
132
|
+
success: false,
|
|
133
|
+
stats: [] as PlaybookApplicationStat[],
|
|
134
|
+
})),
|
|
106
135
|
]);
|
|
107
136
|
if (cancelled) return;
|
|
108
137
|
setProjectSkills(projectRes.user_playbooks ?? []);
|
|
109
138
|
setSharedSkills(sharedRes.agent_playbooks ?? []);
|
|
139
|
+
setAppStats(statsRes.stats ?? []);
|
|
110
140
|
setError(null);
|
|
111
141
|
} catch (e) {
|
|
112
142
|
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
@@ -116,7 +146,15 @@ export default function SkillsPage() {
|
|
|
116
146
|
return () => {
|
|
117
147
|
cancelled = true;
|
|
118
148
|
};
|
|
119
|
-
}, [
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
const statsByRule = useMemo(() => {
|
|
152
|
+
const map = new Map<string, PlaybookApplicationStat>();
|
|
153
|
+
for (const s of appStats ?? []) {
|
|
154
|
+
map.set(`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`, s);
|
|
155
|
+
}
|
|
156
|
+
return map;
|
|
157
|
+
}, [appStats]);
|
|
120
158
|
|
|
121
159
|
const activeSkills = useMemo(() => {
|
|
122
160
|
return activeKind === "project"
|
|
@@ -131,7 +169,7 @@ export default function SkillsPage() {
|
|
|
131
169
|
}, [activeSkills]);
|
|
132
170
|
|
|
133
171
|
const filtered = useMemo(() => {
|
|
134
|
-
|
|
172
|
+
const matches = activeSkills.filter((p) => {
|
|
135
173
|
if (agentVersion !== "__all__" && p.agentVersion !== agentVersion)
|
|
136
174
|
return false;
|
|
137
175
|
if (statusFilter !== "__all__" && p.status !== statusFilter) return false;
|
|
@@ -142,10 +180,24 @@ export default function SkillsPage() {
|
|
|
142
180
|
}
|
|
143
181
|
return true;
|
|
144
182
|
});
|
|
145
|
-
|
|
183
|
+
return matches.sort((a, b) => {
|
|
184
|
+
if (sortBy === "applied") {
|
|
185
|
+
const aStat = statsByRule.get(skillStatKey(a));
|
|
186
|
+
const bStat = statsByRule.get(skillStatKey(b));
|
|
187
|
+
const appliedDelta =
|
|
188
|
+
(bStat?.applied_count ?? 0) - (aStat?.applied_count ?? 0);
|
|
189
|
+
if (appliedDelta !== 0) return appliedDelta;
|
|
190
|
+
const recencyDelta =
|
|
191
|
+
(bStat?.last_applied_at ?? 0) - (aStat?.last_applied_at ?? 0);
|
|
192
|
+
if (recencyDelta !== 0) return recencyDelta;
|
|
193
|
+
}
|
|
194
|
+
return b.createdAt - a.createdAt;
|
|
195
|
+
});
|
|
196
|
+
}, [activeSkills, agentVersion, search, sortBy, statsByRule, statusFilter]);
|
|
146
197
|
|
|
147
198
|
const projectCount = projectSkills?.length ?? 0;
|
|
148
199
|
const sharedCount = sharedSkills?.length ?? 0;
|
|
200
|
+
const visibleActiveCount = filtered.length;
|
|
149
201
|
const activeCount = activeKind === "project" ? projectCount : sharedCount;
|
|
150
202
|
const loading = projectSkills === null || sharedSkills === null;
|
|
151
203
|
const hasNoSharedSkills = activeKind === "shared" && sharedCount === 0;
|
|
@@ -167,7 +219,7 @@ export default function SkillsPage() {
|
|
|
167
219
|
value={agentVersion}
|
|
168
220
|
onValueChange={(v) => setAgentVersion(v ?? "__all__")}
|
|
169
221
|
>
|
|
170
|
-
<SelectTrigger size="sm" className="w-40 text-xs">
|
|
222
|
+
<SelectTrigger size="sm" className="w-40 text-xs bg-background/80">
|
|
171
223
|
<SelectValue placeholder="Project" />
|
|
172
224
|
</SelectTrigger>
|
|
173
225
|
<SelectContent>
|
|
@@ -183,8 +235,10 @@ export default function SkillsPage() {
|
|
|
183
235
|
value={statusFilter}
|
|
184
236
|
onValueChange={(v) => setStatusFilter(v ?? "__all__")}
|
|
185
237
|
>
|
|
186
|
-
<SelectTrigger size="sm" className="w-36 text-xs">
|
|
187
|
-
<SelectValue placeholder="Status"
|
|
238
|
+
<SelectTrigger size="sm" className="w-36 text-xs bg-background/80">
|
|
239
|
+
<SelectValue placeholder="Status">
|
|
240
|
+
{statusFilterLabel(activeKind, statusFilter)}
|
|
241
|
+
</SelectValue>
|
|
188
242
|
</SelectTrigger>
|
|
189
243
|
<SelectContent>
|
|
190
244
|
<SelectItem value="__all__">All</SelectItem>
|
|
@@ -218,11 +272,25 @@ export default function SkillsPage() {
|
|
|
218
272
|
)}
|
|
219
273
|
</SelectContent>
|
|
220
274
|
</Select>
|
|
275
|
+
<Select
|
|
276
|
+
value={sortBy}
|
|
277
|
+
onValueChange={(v) => setSortBy((v as SkillSort) ?? "newest")}
|
|
278
|
+
>
|
|
279
|
+
<SelectTrigger size="sm" className="w-36 text-xs bg-background/80">
|
|
280
|
+
<SelectValue placeholder="Sort">
|
|
281
|
+
{sortBy === "applied" ? "Most applied" : "Newest"}
|
|
282
|
+
</SelectValue>
|
|
283
|
+
</SelectTrigger>
|
|
284
|
+
<SelectContent>
|
|
285
|
+
<SelectItem value="newest">Newest</SelectItem>
|
|
286
|
+
<SelectItem value="applied">Most applied</SelectItem>
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
221
289
|
<Input
|
|
222
290
|
value={search}
|
|
223
291
|
onChange={(e) => setSearch(e.target.value)}
|
|
224
292
|
placeholder="Search"
|
|
225
|
-
className="h-
|
|
293
|
+
className="h-9 w-48 text-xs bg-background/80"
|
|
226
294
|
/>
|
|
227
295
|
<DeleteAllButton
|
|
228
296
|
label={`Delete ${activeKind === "project" ? "project" : "shared"}${activeCount > 0 ? ` (${activeCount})` : ""}`}
|
|
@@ -230,10 +298,10 @@ export default function SkillsPage() {
|
|
|
230
298
|
disabled={activeCount === 0}
|
|
231
299
|
onConfirm={async () => {
|
|
232
300
|
if (activeKind === "project") {
|
|
233
|
-
await reflexio.deleteAllUserPlaybooks(
|
|
301
|
+
await reflexio.deleteAllUserPlaybooks();
|
|
234
302
|
setProjectSkills([]);
|
|
235
303
|
} else {
|
|
236
|
-
await reflexio.deleteAllAgentPlaybooks(
|
|
304
|
+
await reflexio.deleteAllAgentPlaybooks();
|
|
237
305
|
setSharedSkills([]);
|
|
238
306
|
}
|
|
239
307
|
}}
|
|
@@ -251,14 +319,14 @@ export default function SkillsPage() {
|
|
|
251
319
|
id: "project",
|
|
252
320
|
label: "Project-specific skills",
|
|
253
321
|
description: "Repo-local rules learned from direct corrections",
|
|
254
|
-
count: projectCount,
|
|
322
|
+
count: activeKind === "project" ? visibleActiveCount : projectCount,
|
|
255
323
|
icon: BookOpen,
|
|
256
324
|
},
|
|
257
325
|
{
|
|
258
326
|
id: "shared",
|
|
259
327
|
label: "Shared skills",
|
|
260
328
|
description: "Rollups available across projects",
|
|
261
|
-
count: sharedCount,
|
|
329
|
+
count: activeKind === "shared" ? visibleActiveCount : sharedCount,
|
|
262
330
|
icon: Layers3,
|
|
263
331
|
},
|
|
264
332
|
]}
|
|
@@ -266,7 +334,7 @@ export default function SkillsPage() {
|
|
|
266
334
|
|
|
267
335
|
{error && (
|
|
268
336
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm">
|
|
269
|
-
{error}. Is reflexio running on the
|
|
337
|
+
{error}. Is reflexio running on the configured backend URL?
|
|
270
338
|
</div>
|
|
271
339
|
)}
|
|
272
340
|
|
|
@@ -287,45 +355,60 @@ export default function SkillsPage() {
|
|
|
287
355
|
}
|
|
288
356
|
/>
|
|
289
357
|
) : (
|
|
290
|
-
<div className="
|
|
291
|
-
{filtered.map((p) =>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
299
|
-
<Badge variant="outline" className="h-5 font-mono text-[10px]">
|
|
300
|
-
{p.agentVersion}
|
|
301
|
-
</Badge>
|
|
302
|
-
<StatusBadge kind={p.kind} status={p.status} />
|
|
303
|
-
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
304
|
-
{p.kind === "project" ? "project-specific" : "shared"}
|
|
305
|
-
</Badge>
|
|
306
|
-
</div>
|
|
307
|
-
<span className="text-[11px] text-muted-foreground shrink-0">
|
|
308
|
-
{formatRelative(p.createdAt)}
|
|
309
|
-
</span>
|
|
310
|
-
</header>
|
|
311
|
-
<p
|
|
312
|
-
className={cn(
|
|
313
|
-
"text-sm leading-relaxed line-clamp-4",
|
|
314
|
-
!p.trigger && "text-muted-foreground italic",
|
|
315
|
-
)}
|
|
358
|
+
<div className="overflow-hidden rounded-lg border border-border bg-card/92 shadow-sm">
|
|
359
|
+
{filtered.map((p) => {
|
|
360
|
+
const stat = statsByRule.get(skillStatKey(p));
|
|
361
|
+
return (
|
|
362
|
+
<Link
|
|
363
|
+
key={`${p.kind}:${p.id}`}
|
|
364
|
+
href={`/skills/${p.kind}/${p.id}`}
|
|
365
|
+
className="group block border-b border-border px-4 py-3.5 transition-colors last:border-b-0 hover:bg-accent/35"
|
|
316
366
|
>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
367
|
+
<header className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
368
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
369
|
+
<Badge
|
|
370
|
+
variant="outline"
|
|
371
|
+
className="h-5 max-w-56 truncate font-mono text-[10px]"
|
|
372
|
+
>
|
|
373
|
+
{p.agentVersion}
|
|
374
|
+
</Badge>
|
|
375
|
+
<StatusBadge kind={p.kind} status={p.status} />
|
|
376
|
+
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
377
|
+
{p.kind === "project" ? "project-specific" : "shared"}
|
|
378
|
+
</Badge>
|
|
379
|
+
<ApplicationStatBadge stat={stat} />
|
|
380
|
+
</div>
|
|
381
|
+
<div className="flex shrink-0 items-center gap-1.5 pt-0.5">
|
|
382
|
+
<span className="text-[11px] text-muted-foreground">
|
|
383
|
+
{formatRelative(p.createdAt)}
|
|
384
|
+
</span>
|
|
385
|
+
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground/60 transition-colors group-hover:text-foreground" />
|
|
386
|
+
</div>
|
|
387
|
+
</header>
|
|
388
|
+
<p
|
|
389
|
+
className={cn(
|
|
390
|
+
"max-w-5xl text-sm leading-relaxed line-clamp-3",
|
|
391
|
+
!p.trigger && "text-muted-foreground italic",
|
|
392
|
+
)}
|
|
393
|
+
>
|
|
394
|
+
<span className="mr-2 align-baseline text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
395
|
+
Trigger
|
|
396
|
+
</span>
|
|
397
|
+
{p.trigger || "Always applies"}
|
|
325
398
|
</p>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
399
|
+
<p className="mt-2 max-w-5xl text-xs leading-relaxed text-muted-foreground line-clamp-2">
|
|
400
|
+
<span className="font-medium text-foreground/80">Rule:</span>{" "}
|
|
401
|
+
{p.content}
|
|
402
|
+
</p>
|
|
403
|
+
{p.rationale && (
|
|
404
|
+
<p className="mt-1 max-w-5xl text-xs leading-relaxed text-muted-foreground line-clamp-2">
|
|
405
|
+
<span className="font-medium text-foreground/80">Why:</span>{" "}
|
|
406
|
+
{p.rationale}
|
|
407
|
+
</p>
|
|
408
|
+
)}
|
|
409
|
+
</Link>
|
|
410
|
+
);
|
|
411
|
+
})}
|
|
329
412
|
</div>
|
|
330
413
|
)}
|
|
331
414
|
</div>
|
|
@@ -333,6 +416,42 @@ export default function SkillsPage() {
|
|
|
333
416
|
);
|
|
334
417
|
}
|
|
335
418
|
|
|
419
|
+
function ApplicationStatBadge({ stat }: { stat: PlaybookApplicationStat | undefined }) {
|
|
420
|
+
if (!stat || stat.applied_count === 0) {
|
|
421
|
+
return (
|
|
422
|
+
<Badge
|
|
423
|
+
variant="outline"
|
|
424
|
+
className="h-5 text-[10px] text-muted-foreground"
|
|
425
|
+
title="No citations recorded yet for this rule. It will count once an assistant reply cites it."
|
|
426
|
+
>
|
|
427
|
+
Never applied
|
|
428
|
+
</Badge>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const last = formatRelative(stat.last_applied_at);
|
|
432
|
+
return (
|
|
433
|
+
<Badge
|
|
434
|
+
variant="secondary"
|
|
435
|
+
className="h-5 text-[10px]"
|
|
436
|
+
title={`Last applied ${last}`}
|
|
437
|
+
>
|
|
438
|
+
Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
|
|
439
|
+
</Badge>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function statusFilterLabel(kind: SkillKind, status: string): string {
|
|
444
|
+
if (status === "__all__") return "All";
|
|
445
|
+
if (kind === "shared") {
|
|
446
|
+
const meta = SHARED_STATUS_META[status as AgentPlaybookStatusLabel];
|
|
447
|
+
if (meta) return meta.label;
|
|
448
|
+
}
|
|
449
|
+
if (status === "CURRENT") return "Current";
|
|
450
|
+
if (status === "PENDING") return "Pending";
|
|
451
|
+
if (status === "ARCHIVED") return "Archived";
|
|
452
|
+
return status;
|
|
453
|
+
}
|
|
454
|
+
|
|
336
455
|
function StatusBadge({
|
|
337
456
|
kind,
|
|
338
457
|
status,
|