claude-smart 0.2.23 → 0.2.24
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +69 -27
- package/bin/claude-smart.js +296 -11
- package/package.json +11 -1
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/.codex-plugin/plugin.json +35 -0
- package/plugin/LICENSE +202 -0
- package/plugin/README.md +37 -0
- package/plugin/bin/cs-cite +77 -0
- package/plugin/commands/clear-all.md +8 -0
- package/plugin/commands/dashboard.md +8 -0
- package/plugin/commands/learn.md +12 -0
- package/plugin/commands/restart.md +8 -0
- package/plugin/commands/show.md +8 -0
- package/plugin/dashboard/AGENTS.md +6 -0
- package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
- package/plugin/dashboard/app/api/config/route.ts +16 -0
- package/plugin/dashboard/app/api/health/route.ts +10 -0
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
- package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
- package/plugin/dashboard/app/api/sessions/route.ts +14 -0
- package/plugin/dashboard/app/configure/env/page.tsx +318 -0
- package/plugin/dashboard/app/configure/layout.tsx +47 -0
- package/plugin/dashboard/app/configure/page.tsx +5 -0
- package/plugin/dashboard/app/configure/server/page.tsx +258 -0
- package/plugin/dashboard/app/dashboard/page.tsx +227 -0
- package/plugin/dashboard/app/globals.css +129 -0
- package/plugin/dashboard/app/icon.png +0 -0
- package/plugin/dashboard/app/layout.tsx +40 -0
- package/plugin/dashboard/app/page.tsx +5 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
- package/plugin/dashboard/app/preferences/page.tsx +126 -0
- package/plugin/dashboard/app/providers.tsx +12 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
- package/plugin/dashboard/app/sessions/page.tsx +186 -0
- package/plugin/dashboard/app/skills/page.tsx +362 -0
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
- package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
- package/plugin/dashboard/components/common/empty-state.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
- package/plugin/dashboard/components/common/page-header.tsx +34 -0
- package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
- package/plugin/dashboard/components/common/stat-card.tsx +38 -0
- package/plugin/dashboard/components/layout/nav-items.ts +22 -0
- package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
- package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
- package/plugin/dashboard/components/stall-banner.tsx +53 -0
- package/plugin/dashboard/components/ui/badge.tsx +52 -0
- package/plugin/dashboard/components/ui/button.tsx +60 -0
- package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
- package/plugin/dashboard/components/ui/input.tsx +20 -0
- package/plugin/dashboard/components/ui/label.tsx +20 -0
- package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
- package/plugin/dashboard/components/ui/select.tsx +201 -0
- package/plugin/dashboard/components/ui/separator.tsx +25 -0
- package/plugin/dashboard/components/ui/sheet.tsx +135 -0
- package/plugin/dashboard/components/ui/switch.tsx +32 -0
- package/plugin/dashboard/components.json +25 -0
- package/plugin/dashboard/eslint.config.mjs +16 -0
- package/plugin/dashboard/hooks/use-settings.tsx +88 -0
- package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
- package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
- package/plugin/dashboard/lib/config-file.ts +131 -0
- package/plugin/dashboard/lib/format.ts +58 -0
- package/plugin/dashboard/lib/reflexio-client.ts +238 -0
- package/plugin/dashboard/lib/reflexio-url.ts +17 -0
- package/plugin/dashboard/lib/session-reader.ts +245 -0
- package/plugin/dashboard/lib/status.ts +24 -0
- package/plugin/dashboard/lib/types.ts +145 -0
- package/plugin/dashboard/lib/utils.ts +6 -0
- package/plugin/dashboard/next.config.ts +7 -0
- package/plugin/dashboard/package-lock.json +10275 -0
- package/plugin/dashboard/package.json +37 -0
- package/plugin/dashboard/postcss.config.mjs +7 -0
- package/plugin/dashboard/public/claude-smart-icon.png +0 -0
- package/plugin/dashboard/tsconfig.json +34 -0
- package/plugin/hooks/codex-hooks.json +67 -0
- package/plugin/hooks/hooks.json +111 -0
- package/plugin/pyproject.toml +49 -0
- package/plugin/scripts/_codex_env.sh +27 -0
- package/plugin/scripts/_lib.sh +325 -0
- package/plugin/scripts/backend-service.sh +208 -0
- package/plugin/scripts/cli.sh +40 -0
- package/plugin/scripts/dashboard-build.sh +139 -0
- package/plugin/scripts/dashboard-open.sh +107 -0
- package/plugin/scripts/dashboard-service.sh +195 -0
- package/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/scripts/hook_entry.sh +70 -0
- package/plugin/scripts/smart-install.sh +411 -0
- package/plugin/src/claude_smart/__init__.py +3 -0
- package/plugin/src/claude_smart/cli.py +1273 -0
- package/plugin/src/claude_smart/context_format.py +277 -0
- package/plugin/src/claude_smart/context_inject.py +92 -0
- package/plugin/src/claude_smart/cs_cite.py +236 -0
- package/plugin/src/claude_smart/events/__init__.py +1 -0
- package/plugin/src/claude_smart/events/post_tool.py +148 -0
- package/plugin/src/claude_smart/events/pre_tool.py +52 -0
- package/plugin/src/claude_smart/events/session_end.py +20 -0
- package/plugin/src/claude_smart/events/session_start.py +119 -0
- package/plugin/src/claude_smart/events/stop.py +393 -0
- package/plugin/src/claude_smart/events/user_prompt.py +73 -0
- package/plugin/src/claude_smart/hook.py +114 -0
- package/plugin/src/claude_smart/ids.py +56 -0
- package/plugin/src/claude_smart/internal_call.py +89 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
- package/plugin/src/claude_smart/publish.py +71 -0
- package/plugin/src/claude_smart/query_compose.py +51 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
- package/plugin/src/claude_smart/runtime.py +52 -0
- package/plugin/src/claude_smart/stall_banner.py +61 -0
- package/plugin/src/claude_smart/state.py +276 -0
- package/plugin/uv.lock +3720 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { use, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Trash2,
|
|
9
|
+
Save,
|
|
10
|
+
AlertTriangle,
|
|
11
|
+
Pencil,
|
|
12
|
+
X,
|
|
13
|
+
Copy,
|
|
14
|
+
Check,
|
|
15
|
+
BookMarked,
|
|
16
|
+
Hash,
|
|
17
|
+
FolderGit2,
|
|
18
|
+
Clock,
|
|
19
|
+
FileText,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import { PageHeader } from "@/components/common/page-header";
|
|
22
|
+
import { EmptyState } from "@/components/common/empty-state";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import { Label } from "@/components/ui/label";
|
|
25
|
+
import { Badge } from "@/components/ui/badge";
|
|
26
|
+
import { Separator } from "@/components/ui/separator";
|
|
27
|
+
import {
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectItem,
|
|
31
|
+
SelectTrigger,
|
|
32
|
+
SelectValue,
|
|
33
|
+
} from "@/components/ui/select";
|
|
34
|
+
import { reflexio } from "@/lib/reflexio-client";
|
|
35
|
+
import { useSettings } from "@/hooks/use-settings";
|
|
36
|
+
import { formatTimestamp, truncateId } from "@/lib/format";
|
|
37
|
+
import { cn } from "@/lib/utils";
|
|
38
|
+
import { agentPlaybookStatusLabel, statusLabel } from "@/lib/status";
|
|
39
|
+
import type { AgentPlaybook, AgentPlaybookStatus } from "@/lib/types";
|
|
40
|
+
|
|
41
|
+
type FormState = {
|
|
42
|
+
content: string;
|
|
43
|
+
trigger: string;
|
|
44
|
+
rationale: string;
|
|
45
|
+
playbookStatus: AgentPlaybookStatus;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function toForm(p: AgentPlaybook): FormState {
|
|
49
|
+
return {
|
|
50
|
+
content: p.content,
|
|
51
|
+
trigger: p.trigger ?? "",
|
|
52
|
+
rationale: p.rationale ?? "",
|
|
53
|
+
playbookStatus: p.playbook_status,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function displayName(name: string | null | undefined): string | null {
|
|
58
|
+
if (!name) return null;
|
|
59
|
+
if (name === "default_playbook_extractor") return "shared skill";
|
|
60
|
+
return name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const REVIEW_STATUS_META: Record<
|
|
64
|
+
AgentPlaybookStatus,
|
|
65
|
+
{ label: string; description: string }
|
|
66
|
+
> = {
|
|
67
|
+
pending: {
|
|
68
|
+
label: "Auto generated",
|
|
69
|
+
description: "Auto-generated shared skill. It may be updated automatically.",
|
|
70
|
+
},
|
|
71
|
+
approved: {
|
|
72
|
+
label: "Persisted",
|
|
73
|
+
description: "Persisted shared skill. It will not be auto updated.",
|
|
74
|
+
},
|
|
75
|
+
rejected: {
|
|
76
|
+
label: "Rejected",
|
|
77
|
+
description: "Rejected shared skill. It will not be used in claude-smart.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default function SharedSkillDetailPage({
|
|
82
|
+
params,
|
|
83
|
+
}: {
|
|
84
|
+
params: Promise<{ id: string }>;
|
|
85
|
+
}) {
|
|
86
|
+
const { id } = use(params);
|
|
87
|
+
const router = useRouter();
|
|
88
|
+
const { reflexioUrl } = useSettings();
|
|
89
|
+
|
|
90
|
+
const [playbook, setPlaybook] = useState<AgentPlaybook | null>(null);
|
|
91
|
+
const [notFound, setNotFound] = useState(false);
|
|
92
|
+
const [error, setError] = useState<string | null>(null);
|
|
93
|
+
const [saving, setSaving] = useState(false);
|
|
94
|
+
const [deleting, setDeleting] = useState(false);
|
|
95
|
+
const [reviewingStatus, setReviewingStatus] =
|
|
96
|
+
useState<AgentPlaybookStatus | null>(null);
|
|
97
|
+
const [editing, setEditing] = useState(false);
|
|
98
|
+
const [form, setForm] = useState<FormState>({
|
|
99
|
+
content: "",
|
|
100
|
+
trigger: "",
|
|
101
|
+
rationale: "",
|
|
102
|
+
playbookStatus: "pending",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
reflexio
|
|
108
|
+
.getAgentPlaybooks({ reflexioUrl })
|
|
109
|
+
.then((res) => {
|
|
110
|
+
if (cancelled) return;
|
|
111
|
+
const found = (res.agent_playbooks ?? []).find(
|
|
112
|
+
(p) => String(p.agent_playbook_id) === id,
|
|
113
|
+
);
|
|
114
|
+
if (!found) {
|
|
115
|
+
setNotFound(true);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setPlaybook(found);
|
|
119
|
+
setForm(toForm(found));
|
|
120
|
+
})
|
|
121
|
+
.catch((e) => {
|
|
122
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
123
|
+
});
|
|
124
|
+
return () => {
|
|
125
|
+
cancelled = true;
|
|
126
|
+
};
|
|
127
|
+
}, [id, reflexioUrl]);
|
|
128
|
+
|
|
129
|
+
const dirty = useMemo(() => {
|
|
130
|
+
if (!playbook) return false;
|
|
131
|
+
const orig = toForm(playbook);
|
|
132
|
+
return (
|
|
133
|
+
orig.content !== form.content ||
|
|
134
|
+
orig.trigger !== form.trigger ||
|
|
135
|
+
orig.rationale !== form.rationale ||
|
|
136
|
+
orig.playbookStatus !== form.playbookStatus
|
|
137
|
+
);
|
|
138
|
+
}, [playbook, form]);
|
|
139
|
+
|
|
140
|
+
const save = async () => {
|
|
141
|
+
if (!playbook || !dirty) return;
|
|
142
|
+
setSaving(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
try {
|
|
145
|
+
await reflexio.updateAgentPlaybook(
|
|
146
|
+
{
|
|
147
|
+
agent_playbook_id: playbook.agent_playbook_id,
|
|
148
|
+
content: form.content,
|
|
149
|
+
trigger: form.trigger || null,
|
|
150
|
+
rationale: form.rationale || null,
|
|
151
|
+
playbook_status: form.playbookStatus,
|
|
152
|
+
},
|
|
153
|
+
reflexioUrl,
|
|
154
|
+
);
|
|
155
|
+
setPlaybook({
|
|
156
|
+
...playbook,
|
|
157
|
+
content: form.content,
|
|
158
|
+
trigger: form.trigger || null,
|
|
159
|
+
rationale: form.rationale || null,
|
|
160
|
+
playbook_status: form.playbookStatus,
|
|
161
|
+
});
|
|
162
|
+
setEditing(false);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
165
|
+
} finally {
|
|
166
|
+
setSaving(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const setReviewStatus = async (nextStatus: AgentPlaybookStatus) => {
|
|
171
|
+
if (!playbook || playbook.playbook_status === nextStatus) return;
|
|
172
|
+
if (
|
|
173
|
+
nextStatus === "rejected" &&
|
|
174
|
+
!confirm(
|
|
175
|
+
`Reject shared skill #${playbook.agent_playbook_id}? Rejected shared skills will not be used in claude-smart.`,
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setReviewingStatus(nextStatus);
|
|
182
|
+
setError(null);
|
|
183
|
+
try {
|
|
184
|
+
await reflexio.updateAgentPlaybook(
|
|
185
|
+
{
|
|
186
|
+
agent_playbook_id: playbook.agent_playbook_id,
|
|
187
|
+
playbook_status: nextStatus,
|
|
188
|
+
},
|
|
189
|
+
reflexioUrl,
|
|
190
|
+
);
|
|
191
|
+
setPlaybook((current) =>
|
|
192
|
+
current ? { ...current, playbook_status: nextStatus } : current,
|
|
193
|
+
);
|
|
194
|
+
setForm((current) => ({ ...current, playbookStatus: nextStatus }));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
197
|
+
} finally {
|
|
198
|
+
setReviewingStatus(null);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const remove = async () => {
|
|
203
|
+
if (!playbook) return;
|
|
204
|
+
if (
|
|
205
|
+
!confirm(
|
|
206
|
+
`Delete shared skill #${playbook.agent_playbook_id}? This cannot be undone.`,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
return;
|
|
210
|
+
setDeleting(true);
|
|
211
|
+
try {
|
|
212
|
+
await reflexio.deleteAgentPlaybook(playbook.agent_playbook_id, reflexioUrl);
|
|
213
|
+
router.push("/skills");
|
|
214
|
+
} catch (e) {
|
|
215
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
216
|
+
setDeleting(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const cancelEdit = () => {
|
|
221
|
+
if (playbook) setForm(toForm(playbook));
|
|
222
|
+
setEditing(false);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (notFound) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="flex-1 overflow-auto">
|
|
228
|
+
<PageHeader title="Shared skill not found" />
|
|
229
|
+
<div className="p-6 max-w-2xl mx-auto">
|
|
230
|
+
<EmptyState
|
|
231
|
+
icon={AlertTriangle}
|
|
232
|
+
title="Shared skill not found"
|
|
233
|
+
description="It may have been deleted, archived, or moved outside the first 100 results."
|
|
234
|
+
action={
|
|
235
|
+
<Link href="/skills">
|
|
236
|
+
<Button variant="outline" size="sm">
|
|
237
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
238
|
+
Back to skills
|
|
239
|
+
</Button>
|
|
240
|
+
</Link>
|
|
241
|
+
}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lifecycleStatus = playbook ? statusLabel(playbook) : null;
|
|
249
|
+
const playbookStatus = playbook ? agentPlaybookStatusLabel(playbook) : null;
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="flex-1 overflow-auto">
|
|
253
|
+
<PageHeader
|
|
254
|
+
title={`Shared skill #${playbook?.agent_playbook_id ?? id}`}
|
|
255
|
+
description="Shared skill rolled up from project-specific skills."
|
|
256
|
+
actions={
|
|
257
|
+
<div className="flex items-center gap-2">
|
|
258
|
+
<Link href="/skills">
|
|
259
|
+
<Button variant="outline" size="sm">
|
|
260
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
261
|
+
Back
|
|
262
|
+
</Button>
|
|
263
|
+
</Link>
|
|
264
|
+
{!editing ? (
|
|
265
|
+
<Button
|
|
266
|
+
size="sm"
|
|
267
|
+
onClick={() => setEditing(true)}
|
|
268
|
+
disabled={!playbook || reviewingStatus !== null}
|
|
269
|
+
>
|
|
270
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
271
|
+
Edit
|
|
272
|
+
</Button>
|
|
273
|
+
) : (
|
|
274
|
+
<>
|
|
275
|
+
<Button
|
|
276
|
+
variant="outline"
|
|
277
|
+
size="sm"
|
|
278
|
+
onClick={cancelEdit}
|
|
279
|
+
disabled={saving}
|
|
280
|
+
>
|
|
281
|
+
<X className="h-3.5 w-3.5" />
|
|
282
|
+
Cancel
|
|
283
|
+
</Button>
|
|
284
|
+
<Button
|
|
285
|
+
size="sm"
|
|
286
|
+
onClick={save}
|
|
287
|
+
disabled={saving || !dirty}
|
|
288
|
+
>
|
|
289
|
+
<Save className="h-3.5 w-3.5" />
|
|
290
|
+
{saving ? "Saving…" : "Save"}
|
|
291
|
+
</Button>
|
|
292
|
+
</>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
}
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<div className="p-6">
|
|
299
|
+
<div className="mx-auto max-w-5xl grid gap-6 lg:grid-cols-[1fr_280px]">
|
|
300
|
+
<div className="space-y-6 min-w-0">
|
|
301
|
+
{error && (
|
|
302
|
+
<div className="rounded-xl border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm flex items-start gap-2">
|
|
303
|
+
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
|
304
|
+
<span>{error}</span>
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{playbook && (
|
|
309
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
310
|
+
<Badge variant="outline" className="gap-1.5">
|
|
311
|
+
<FolderGit2 className="h-3 w-3" />
|
|
312
|
+
{playbook.agent_version || "default"}
|
|
313
|
+
</Badge>
|
|
314
|
+
{editing ? (
|
|
315
|
+
<ReviewStatusBadge
|
|
316
|
+
status={playbook.playbook_status}
|
|
317
|
+
displayStatus={playbookStatus!}
|
|
318
|
+
/>
|
|
319
|
+
) : (
|
|
320
|
+
<ReviewStatusSelect
|
|
321
|
+
status={playbook.playbook_status}
|
|
322
|
+
displayStatus={playbookStatus!}
|
|
323
|
+
disabled={reviewingStatus !== null || deleting}
|
|
324
|
+
busy={reviewingStatus !== null}
|
|
325
|
+
onChange={setReviewStatus}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
{lifecycleStatus !== "CURRENT" && (
|
|
329
|
+
<StatusBadge status={lifecycleStatus!} />
|
|
330
|
+
)}
|
|
331
|
+
{displayName(playbook.playbook_name) && (
|
|
332
|
+
<Badge variant="secondary" className="font-mono text-[10px]">
|
|
333
|
+
{displayName(playbook.playbook_name)}
|
|
334
|
+
</Badge>
|
|
335
|
+
)}
|
|
336
|
+
{dirty && (
|
|
337
|
+
<Badge variant="destructive" className="gap-1.5">
|
|
338
|
+
unsaved changes
|
|
339
|
+
</Badge>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
<Section
|
|
345
|
+
icon={AlertTriangle}
|
|
346
|
+
title="Trigger"
|
|
347
|
+
hint="When this shared skill should apply. Leave empty if it always applies."
|
|
348
|
+
>
|
|
349
|
+
{editing ? (
|
|
350
|
+
<AutoTextarea
|
|
351
|
+
value={form.trigger}
|
|
352
|
+
onChange={(v) => setForm((f) => ({ ...f, trigger: v }))}
|
|
353
|
+
rows={2}
|
|
354
|
+
placeholder="e.g. When writing or running async Python tests."
|
|
355
|
+
/>
|
|
356
|
+
) : (
|
|
357
|
+
<Prose text={playbook?.trigger ?? ""} muted={!playbook?.trigger} />
|
|
358
|
+
)}
|
|
359
|
+
</Section>
|
|
360
|
+
|
|
361
|
+
<Section
|
|
362
|
+
icon={Check}
|
|
363
|
+
title="Review status"
|
|
364
|
+
hint="Auto generated, persisted, or rejected."
|
|
365
|
+
>
|
|
366
|
+
{editing ? (
|
|
367
|
+
<Select
|
|
368
|
+
value={form.playbookStatus}
|
|
369
|
+
onValueChange={(v) =>
|
|
370
|
+
setForm((f) => ({
|
|
371
|
+
...f,
|
|
372
|
+
playbookStatus: v as AgentPlaybookStatus,
|
|
373
|
+
}))
|
|
374
|
+
}
|
|
375
|
+
>
|
|
376
|
+
<SelectTrigger
|
|
377
|
+
className="w-48 text-xs"
|
|
378
|
+
title={REVIEW_STATUS_META[form.playbookStatus].description}
|
|
379
|
+
>
|
|
380
|
+
<SelectValue placeholder="Status" />
|
|
381
|
+
</SelectTrigger>
|
|
382
|
+
<SelectContent>
|
|
383
|
+
<SelectItem
|
|
384
|
+
value="pending"
|
|
385
|
+
title={REVIEW_STATUS_META.pending.description}
|
|
386
|
+
>
|
|
387
|
+
{REVIEW_STATUS_META.pending.label}
|
|
388
|
+
</SelectItem>
|
|
389
|
+
<SelectItem
|
|
390
|
+
value="approved"
|
|
391
|
+
title={REVIEW_STATUS_META.approved.description}
|
|
392
|
+
>
|
|
393
|
+
{REVIEW_STATUS_META.approved.label}
|
|
394
|
+
</SelectItem>
|
|
395
|
+
<SelectItem
|
|
396
|
+
value="rejected"
|
|
397
|
+
title={REVIEW_STATUS_META.rejected.description}
|
|
398
|
+
>
|
|
399
|
+
{REVIEW_STATUS_META.rejected.label}
|
|
400
|
+
</SelectItem>
|
|
401
|
+
</SelectContent>
|
|
402
|
+
</Select>
|
|
403
|
+
) : (
|
|
404
|
+
playbook && (
|
|
405
|
+
<ReviewStatusSelect
|
|
406
|
+
status={playbook.playbook_status}
|
|
407
|
+
displayStatus={playbookStatus ?? "PENDING"}
|
|
408
|
+
disabled={reviewingStatus !== null || deleting}
|
|
409
|
+
busy={reviewingStatus !== null}
|
|
410
|
+
onChange={setReviewStatus}
|
|
411
|
+
/>
|
|
412
|
+
)
|
|
413
|
+
)}
|
|
414
|
+
</Section>
|
|
415
|
+
|
|
416
|
+
<Section
|
|
417
|
+
icon={BookMarked}
|
|
418
|
+
title="Rule"
|
|
419
|
+
hint="What Claude should do. Injected when relevant in future sessions."
|
|
420
|
+
>
|
|
421
|
+
{editing ? (
|
|
422
|
+
<AutoTextarea
|
|
423
|
+
value={form.content}
|
|
424
|
+
onChange={(v) => setForm((f) => ({ ...f, content: v }))}
|
|
425
|
+
rows={6}
|
|
426
|
+
placeholder="e.g. Use anyio with trio backend — never pytest-asyncio."
|
|
427
|
+
/>
|
|
428
|
+
) : (
|
|
429
|
+
<Prose text={playbook?.content ?? ""} />
|
|
430
|
+
)}
|
|
431
|
+
</Section>
|
|
432
|
+
|
|
433
|
+
<Section
|
|
434
|
+
icon={FileText}
|
|
435
|
+
title="Rationale"
|
|
436
|
+
hint="Why — the reason, constraint, or past incident behind this rule."
|
|
437
|
+
>
|
|
438
|
+
{editing ? (
|
|
439
|
+
<AutoTextarea
|
|
440
|
+
value={form.rationale}
|
|
441
|
+
onChange={(v) => setForm((f) => ({ ...f, rationale: v }))}
|
|
442
|
+
rows={3}
|
|
443
|
+
placeholder="e.g. pytest-asyncio deadlocked CI on project X — trio is the project standard."
|
|
444
|
+
/>
|
|
445
|
+
) : (
|
|
446
|
+
<Prose
|
|
447
|
+
text={playbook?.rationale ?? ""}
|
|
448
|
+
muted={!playbook?.rationale}
|
|
449
|
+
/>
|
|
450
|
+
)}
|
|
451
|
+
</Section>
|
|
452
|
+
|
|
453
|
+
{!editing && playbook && (
|
|
454
|
+
<>
|
|
455
|
+
<Separator />
|
|
456
|
+
<DangerZone
|
|
457
|
+
onDelete={remove}
|
|
458
|
+
deleting={deleting}
|
|
459
|
+
disabled={saving}
|
|
460
|
+
/>
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
{playbook && (
|
|
466
|
+
<aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
|
|
467
|
+
<div className="rounded-xl border border-border bg-card p-4">
|
|
468
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-3">
|
|
469
|
+
Metadata
|
|
470
|
+
</h3>
|
|
471
|
+
<dl className="space-y-2.5 text-sm">
|
|
472
|
+
<Meta
|
|
473
|
+
icon={Hash}
|
|
474
|
+
label="ID"
|
|
475
|
+
value={String(playbook.agent_playbook_id)}
|
|
476
|
+
mono
|
|
477
|
+
/>
|
|
478
|
+
<Meta
|
|
479
|
+
icon={Clock}
|
|
480
|
+
label="Created"
|
|
481
|
+
value={formatTimestamp(playbook.created_at)}
|
|
482
|
+
/>
|
|
483
|
+
<Meta
|
|
484
|
+
label="Project"
|
|
485
|
+
value={playbook.agent_version || "default"}
|
|
486
|
+
mono
|
|
487
|
+
/>
|
|
488
|
+
<Meta
|
|
489
|
+
label="Review"
|
|
490
|
+
value={REVIEW_STATUS_META[playbook.playbook_status].label}
|
|
491
|
+
mono
|
|
492
|
+
/>
|
|
493
|
+
{playbook.playbook_metadata && (
|
|
494
|
+
<CopyMeta
|
|
495
|
+
label="Metadata"
|
|
496
|
+
value={playbook.playbook_metadata}
|
|
497
|
+
display={truncateId(playbook.playbook_metadata, 32, 8)}
|
|
498
|
+
/>
|
|
499
|
+
)}
|
|
500
|
+
</dl>
|
|
501
|
+
</div>
|
|
502
|
+
</aside>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function Section({
|
|
511
|
+
icon: Icon,
|
|
512
|
+
title,
|
|
513
|
+
hint,
|
|
514
|
+
children,
|
|
515
|
+
}: {
|
|
516
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
517
|
+
title: string;
|
|
518
|
+
hint?: string;
|
|
519
|
+
children: React.ReactNode;
|
|
520
|
+
}) {
|
|
521
|
+
return (
|
|
522
|
+
<section className="space-y-2">
|
|
523
|
+
<div className="flex items-baseline gap-2">
|
|
524
|
+
<Label className="text-sm font-semibold flex items-center gap-1.5">
|
|
525
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
526
|
+
{title}
|
|
527
|
+
</Label>
|
|
528
|
+
{hint && (
|
|
529
|
+
<span className="text-xs text-muted-foreground">{hint}</span>
|
|
530
|
+
)}
|
|
531
|
+
</div>
|
|
532
|
+
{children}
|
|
533
|
+
</section>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function Prose({ text, muted = false }: { text: string; muted?: boolean }) {
|
|
538
|
+
if (!text) {
|
|
539
|
+
return (
|
|
540
|
+
<p className="text-sm text-muted-foreground italic">
|
|
541
|
+
{muted ? "Not set" : "—"}
|
|
542
|
+
</p>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return (
|
|
546
|
+
<div
|
|
547
|
+
className={cn(
|
|
548
|
+
"rounded-xl border border-border bg-card px-4 py-3",
|
|
549
|
+
muted && "bg-muted/30",
|
|
550
|
+
)}
|
|
551
|
+
>
|
|
552
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
553
|
+
{text}
|
|
554
|
+
</p>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function AutoTextarea({
|
|
560
|
+
value,
|
|
561
|
+
onChange,
|
|
562
|
+
rows = 3,
|
|
563
|
+
placeholder,
|
|
564
|
+
}: {
|
|
565
|
+
value: string;
|
|
566
|
+
onChange: (v: string) => void;
|
|
567
|
+
rows?: number;
|
|
568
|
+
placeholder?: string;
|
|
569
|
+
}) {
|
|
570
|
+
return (
|
|
571
|
+
<textarea
|
|
572
|
+
value={value}
|
|
573
|
+
onChange={(e) => onChange(e.target.value)}
|
|
574
|
+
rows={rows}
|
|
575
|
+
placeholder={placeholder}
|
|
576
|
+
className="w-full rounded-xl border border-input bg-transparent px-4 py-3 text-sm leading-relaxed font-sans resize-y outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 placeholder:text-muted-foreground"
|
|
577
|
+
/>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function StatusBadge({
|
|
582
|
+
status,
|
|
583
|
+
}: {
|
|
584
|
+
status: "CURRENT" | "ARCHIVED" | "PENDING" | "APPROVED" | "REJECTED";
|
|
585
|
+
}) {
|
|
586
|
+
const variant =
|
|
587
|
+
status === "CURRENT" || status === "APPROVED"
|
|
588
|
+
? "secondary"
|
|
589
|
+
: status === "ARCHIVED" || status === "REJECTED"
|
|
590
|
+
? "outline"
|
|
591
|
+
: "default";
|
|
592
|
+
return (
|
|
593
|
+
<Badge variant={variant} className="gap-1.5">
|
|
594
|
+
<span
|
|
595
|
+
className={cn(
|
|
596
|
+
"h-1.5 w-1.5 rounded-full",
|
|
597
|
+
status === "CURRENT" && "bg-emerald-500",
|
|
598
|
+
status === "APPROVED" && "bg-emerald-500",
|
|
599
|
+
status === "PENDING" && "bg-amber-500",
|
|
600
|
+
status === "REJECTED" && "bg-destructive",
|
|
601
|
+
status === "ARCHIVED" && "bg-muted-foreground",
|
|
602
|
+
)}
|
|
603
|
+
/>
|
|
604
|
+
{status}
|
|
605
|
+
</Badge>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function ReviewStatusSelect({
|
|
610
|
+
status,
|
|
611
|
+
displayStatus,
|
|
612
|
+
disabled,
|
|
613
|
+
busy,
|
|
614
|
+
onChange,
|
|
615
|
+
}: {
|
|
616
|
+
status: AgentPlaybookStatus;
|
|
617
|
+
displayStatus: "PENDING" | "APPROVED" | "REJECTED";
|
|
618
|
+
disabled: boolean;
|
|
619
|
+
busy: boolean;
|
|
620
|
+
onChange: (status: AgentPlaybookStatus) => void;
|
|
621
|
+
}) {
|
|
622
|
+
const meta = REVIEW_STATUS_META[status];
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<Select
|
|
626
|
+
value={status}
|
|
627
|
+
onValueChange={(value) => onChange(value as AgentPlaybookStatus)}
|
|
628
|
+
disabled={disabled}
|
|
629
|
+
>
|
|
630
|
+
<SelectTrigger
|
|
631
|
+
size="sm"
|
|
632
|
+
aria-label="Review status"
|
|
633
|
+
title={meta.description}
|
|
634
|
+
className={cn(
|
|
635
|
+
"h-7 w-fit gap-1.5 rounded-lg border px-2.5 py-0 text-xs font-medium",
|
|
636
|
+
"bg-background hover:bg-muted focus-visible:ring-3",
|
|
637
|
+
displayStatus === "APPROVED" &&
|
|
638
|
+
"border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
|
639
|
+
displayStatus === "PENDING" &&
|
|
640
|
+
"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
641
|
+
displayStatus === "REJECTED" &&
|
|
642
|
+
"border-destructive/30 bg-destructive/10 text-destructive",
|
|
643
|
+
)}
|
|
644
|
+
>
|
|
645
|
+
<StatusPillContent
|
|
646
|
+
status={displayStatus}
|
|
647
|
+
label={busy ? "Updating" : meta.label}
|
|
648
|
+
/>
|
|
649
|
+
</SelectTrigger>
|
|
650
|
+
<SelectContent align="start" alignItemWithTrigger={false}>
|
|
651
|
+
<SelectItem
|
|
652
|
+
value="pending"
|
|
653
|
+
title={REVIEW_STATUS_META.pending.description}
|
|
654
|
+
>
|
|
655
|
+
<StatusPillContent
|
|
656
|
+
status="PENDING"
|
|
657
|
+
label={REVIEW_STATUS_META.pending.label}
|
|
658
|
+
/>
|
|
659
|
+
</SelectItem>
|
|
660
|
+
<SelectItem
|
|
661
|
+
value="approved"
|
|
662
|
+
title={REVIEW_STATUS_META.approved.description}
|
|
663
|
+
>
|
|
664
|
+
<StatusPillContent
|
|
665
|
+
status="APPROVED"
|
|
666
|
+
label={REVIEW_STATUS_META.approved.label}
|
|
667
|
+
/>
|
|
668
|
+
</SelectItem>
|
|
669
|
+
<SelectItem
|
|
670
|
+
value="rejected"
|
|
671
|
+
title={REVIEW_STATUS_META.rejected.description}
|
|
672
|
+
>
|
|
673
|
+
<StatusPillContent
|
|
674
|
+
status="REJECTED"
|
|
675
|
+
label={REVIEW_STATUS_META.rejected.label}
|
|
676
|
+
/>
|
|
677
|
+
</SelectItem>
|
|
678
|
+
</SelectContent>
|
|
679
|
+
</Select>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function ReviewStatusBadge({
|
|
684
|
+
status,
|
|
685
|
+
displayStatus,
|
|
686
|
+
}: {
|
|
687
|
+
status: AgentPlaybookStatus;
|
|
688
|
+
displayStatus: "PENDING" | "APPROVED" | "REJECTED";
|
|
689
|
+
}) {
|
|
690
|
+
const meta = REVIEW_STATUS_META[status];
|
|
691
|
+
return (
|
|
692
|
+
<Badge
|
|
693
|
+
variant={displayStatus === "REJECTED" ? "outline" : "secondary"}
|
|
694
|
+
className={cn(
|
|
695
|
+
"gap-1.5",
|
|
696
|
+
displayStatus === "APPROVED" &&
|
|
697
|
+
"border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
|
698
|
+
displayStatus === "PENDING" &&
|
|
699
|
+
"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
700
|
+
displayStatus === "REJECTED" &&
|
|
701
|
+
"border-destructive/30 bg-destructive/10 text-destructive",
|
|
702
|
+
)}
|
|
703
|
+
title={meta.description}
|
|
704
|
+
>
|
|
705
|
+
<StatusPillContent status={displayStatus} label={meta.label} />
|
|
706
|
+
</Badge>
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function StatusPillContent({
|
|
711
|
+
status,
|
|
712
|
+
label,
|
|
713
|
+
}: {
|
|
714
|
+
status: "PENDING" | "APPROVED" | "REJECTED";
|
|
715
|
+
label: string;
|
|
716
|
+
}) {
|
|
717
|
+
return (
|
|
718
|
+
<span className="inline-flex items-center gap-1.5">
|
|
719
|
+
<span
|
|
720
|
+
className={cn(
|
|
721
|
+
"h-1.5 w-1.5 rounded-full",
|
|
722
|
+
status === "APPROVED" && "bg-emerald-500",
|
|
723
|
+
status === "PENDING" && "bg-amber-500",
|
|
724
|
+
status === "REJECTED" && "bg-destructive",
|
|
725
|
+
)}
|
|
726
|
+
/>
|
|
727
|
+
{label}
|
|
728
|
+
</span>
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function Meta({
|
|
733
|
+
icon: Icon,
|
|
734
|
+
label,
|
|
735
|
+
value,
|
|
736
|
+
mono,
|
|
737
|
+
}: {
|
|
738
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
739
|
+
label: string;
|
|
740
|
+
value: string;
|
|
741
|
+
mono?: boolean;
|
|
742
|
+
}) {
|
|
743
|
+
return (
|
|
744
|
+
<div className="flex items-start justify-between gap-3">
|
|
745
|
+
<dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
|
|
746
|
+
{Icon && <Icon className="h-3 w-3" />}
|
|
747
|
+
{label}
|
|
748
|
+
</dt>
|
|
749
|
+
<dd
|
|
750
|
+
className={cn(
|
|
751
|
+
"text-xs text-right min-w-0 break-words",
|
|
752
|
+
mono && "font-mono",
|
|
753
|
+
)}
|
|
754
|
+
>
|
|
755
|
+
{value}
|
|
756
|
+
</dd>
|
|
757
|
+
</div>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function CopyMeta({
|
|
762
|
+
label,
|
|
763
|
+
value,
|
|
764
|
+
display,
|
|
765
|
+
}: {
|
|
766
|
+
label: string;
|
|
767
|
+
value: string;
|
|
768
|
+
display: string;
|
|
769
|
+
}) {
|
|
770
|
+
const [copied, setCopied] = useState(false);
|
|
771
|
+
const copy = async () => {
|
|
772
|
+
try {
|
|
773
|
+
await navigator.clipboard.writeText(value);
|
|
774
|
+
setCopied(true);
|
|
775
|
+
setTimeout(() => setCopied(false), 1200);
|
|
776
|
+
} catch {
|
|
777
|
+
// ignore
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
return (
|
|
781
|
+
<div className="flex items-start justify-between gap-3">
|
|
782
|
+
<dt className="text-xs text-muted-foreground shrink-0">{label}</dt>
|
|
783
|
+
<dd className="text-xs min-w-0 flex items-center gap-1.5">
|
|
784
|
+
<code className="font-mono">{display}</code>
|
|
785
|
+
<button
|
|
786
|
+
onClick={copy}
|
|
787
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
788
|
+
title="Copy full id"
|
|
789
|
+
>
|
|
790
|
+
{copied ? (
|
|
791
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
792
|
+
) : (
|
|
793
|
+
<Copy className="h-3 w-3" />
|
|
794
|
+
)}
|
|
795
|
+
</button>
|
|
796
|
+
</dd>
|
|
797
|
+
</div>
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function DangerZone({
|
|
802
|
+
onDelete,
|
|
803
|
+
deleting,
|
|
804
|
+
disabled,
|
|
805
|
+
}: {
|
|
806
|
+
onDelete: () => void;
|
|
807
|
+
deleting: boolean;
|
|
808
|
+
disabled: boolean;
|
|
809
|
+
}) {
|
|
810
|
+
return (
|
|
811
|
+
<section className="rounded-xl border border-destructive/30 bg-destructive/5 p-4 flex items-start justify-between gap-4">
|
|
812
|
+
<div className="min-w-0">
|
|
813
|
+
<h3 className="text-sm font-semibold text-destructive">Danger zone</h3>
|
|
814
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
815
|
+
Deleting removes this shared skill permanently. It will stop being
|
|
816
|
+
available in the dashboard.
|
|
817
|
+
</p>
|
|
818
|
+
</div>
|
|
819
|
+
<Button
|
|
820
|
+
variant="destructive"
|
|
821
|
+
size="sm"
|
|
822
|
+
onClick={onDelete}
|
|
823
|
+
disabled={deleting || disabled}
|
|
824
|
+
>
|
|
825
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
826
|
+
{deleting ? "Deleting…" : "Delete"}
|
|
827
|
+
</Button>
|
|
828
|
+
</section>
|
|
829
|
+
);
|
|
830
|
+
}
|