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,597 @@
|
|
|
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 { reflexio } from "@/lib/reflexio-client";
|
|
28
|
+
import { useSettings } from "@/hooks/use-settings";
|
|
29
|
+
import { formatTimestamp, truncateId } from "@/lib/format";
|
|
30
|
+
import { cn } from "@/lib/utils";
|
|
31
|
+
import { statusLabel } from "@/lib/status";
|
|
32
|
+
import type { UserPlaybook } from "@/lib/types";
|
|
33
|
+
|
|
34
|
+
type FormState = { content: string; trigger: string; rationale: string };
|
|
35
|
+
|
|
36
|
+
function toForm(p: UserPlaybook): FormState {
|
|
37
|
+
return {
|
|
38
|
+
content: p.content,
|
|
39
|
+
trigger: p.trigger ?? "",
|
|
40
|
+
rationale: p.rationale ?? "",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function displayName(name: string | null | undefined): string | null {
|
|
45
|
+
if (!name) return null;
|
|
46
|
+
if (name === "default_playbook_extractor") return "project-specific skill";
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function ProjectSkillDetailPage({
|
|
51
|
+
params,
|
|
52
|
+
}: {
|
|
53
|
+
params: Promise<{ id: string }>;
|
|
54
|
+
}) {
|
|
55
|
+
const { id } = use(params);
|
|
56
|
+
const router = useRouter();
|
|
57
|
+
const { reflexioUrl } = useSettings();
|
|
58
|
+
|
|
59
|
+
const [playbook, setPlaybook] = useState<UserPlaybook | null>(null);
|
|
60
|
+
const [notFound, setNotFound] = useState(false);
|
|
61
|
+
const [error, setError] = useState<string | null>(null);
|
|
62
|
+
const [saving, setSaving] = useState(false);
|
|
63
|
+
const [deleting, setDeleting] = useState(false);
|
|
64
|
+
const [editing, setEditing] = useState(false);
|
|
65
|
+
const [form, setForm] = useState<FormState>({
|
|
66
|
+
content: "",
|
|
67
|
+
trigger: "",
|
|
68
|
+
rationale: "",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
reflexio
|
|
74
|
+
.getUserPlaybooks({ reflexioUrl })
|
|
75
|
+
.then((res) => {
|
|
76
|
+
if (cancelled) return;
|
|
77
|
+
const found = (res.user_playbooks ?? []).find(
|
|
78
|
+
(p) => String(p.user_playbook_id) === id,
|
|
79
|
+
);
|
|
80
|
+
if (!found) {
|
|
81
|
+
setNotFound(true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setPlaybook(found);
|
|
85
|
+
setForm(toForm(found));
|
|
86
|
+
})
|
|
87
|
+
.catch((e) => {
|
|
88
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
};
|
|
93
|
+
}, [id, reflexioUrl]);
|
|
94
|
+
|
|
95
|
+
const dirty = useMemo(() => {
|
|
96
|
+
if (!playbook) return false;
|
|
97
|
+
const orig = toForm(playbook);
|
|
98
|
+
return (
|
|
99
|
+
orig.content !== form.content ||
|
|
100
|
+
orig.trigger !== form.trigger ||
|
|
101
|
+
orig.rationale !== form.rationale
|
|
102
|
+
);
|
|
103
|
+
}, [playbook, form]);
|
|
104
|
+
|
|
105
|
+
const save = async () => {
|
|
106
|
+
if (!playbook || !dirty) return;
|
|
107
|
+
setSaving(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
try {
|
|
110
|
+
await reflexio.updateUserPlaybook(
|
|
111
|
+
{
|
|
112
|
+
user_playbook_id: playbook.user_playbook_id,
|
|
113
|
+
content: form.content,
|
|
114
|
+
trigger: form.trigger || null,
|
|
115
|
+
rationale: form.rationale || null,
|
|
116
|
+
},
|
|
117
|
+
reflexioUrl,
|
|
118
|
+
);
|
|
119
|
+
setPlaybook({
|
|
120
|
+
...playbook,
|
|
121
|
+
content: form.content,
|
|
122
|
+
trigger: form.trigger || null,
|
|
123
|
+
rationale: form.rationale || null,
|
|
124
|
+
});
|
|
125
|
+
setEditing(false);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
128
|
+
} finally {
|
|
129
|
+
setSaving(false);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const remove = async () => {
|
|
134
|
+
if (!playbook) return;
|
|
135
|
+
if (
|
|
136
|
+
!confirm(
|
|
137
|
+
`Delete project-specific skill #${playbook.user_playbook_id}? This cannot be undone.`,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return;
|
|
141
|
+
setDeleting(true);
|
|
142
|
+
try {
|
|
143
|
+
await reflexio.deleteUserPlaybook(playbook.user_playbook_id, reflexioUrl);
|
|
144
|
+
router.push("/skills");
|
|
145
|
+
} catch (e) {
|
|
146
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
147
|
+
setDeleting(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const cancelEdit = () => {
|
|
152
|
+
if (playbook) setForm(toForm(playbook));
|
|
153
|
+
setEditing(false);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (notFound) {
|
|
157
|
+
return (
|
|
158
|
+
<div className="flex-1 overflow-auto">
|
|
159
|
+
<PageHeader title="Project-specific skill not found" />
|
|
160
|
+
<div className="p-6 max-w-2xl mx-auto">
|
|
161
|
+
<EmptyState
|
|
162
|
+
icon={AlertTriangle}
|
|
163
|
+
title="Project-specific skill not found"
|
|
164
|
+
description="It may have been deleted, archived, or moved outside the first 100 results."
|
|
165
|
+
action={
|
|
166
|
+
<Link href="/skills">
|
|
167
|
+
<Button variant="outline" size="sm">
|
|
168
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
169
|
+
Back to skills
|
|
170
|
+
</Button>
|
|
171
|
+
</Link>
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const status = playbook ? statusLabel(playbook) : null;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex-1 overflow-auto">
|
|
183
|
+
<PageHeader
|
|
184
|
+
title={`Project-specific skill #${playbook?.user_playbook_id ?? id}`}
|
|
185
|
+
description="Project-specific skill learned by claude-smart."
|
|
186
|
+
actions={
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<Link href="/skills">
|
|
189
|
+
<Button variant="outline" size="sm">
|
|
190
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
191
|
+
Back
|
|
192
|
+
</Button>
|
|
193
|
+
</Link>
|
|
194
|
+
{!editing ? (
|
|
195
|
+
<Button
|
|
196
|
+
size="sm"
|
|
197
|
+
onClick={() => setEditing(true)}
|
|
198
|
+
disabled={!playbook}
|
|
199
|
+
>
|
|
200
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
201
|
+
Edit
|
|
202
|
+
</Button>
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
<Button
|
|
206
|
+
variant="outline"
|
|
207
|
+
size="sm"
|
|
208
|
+
onClick={cancelEdit}
|
|
209
|
+
disabled={saving}
|
|
210
|
+
>
|
|
211
|
+
<X className="h-3.5 w-3.5" />
|
|
212
|
+
Cancel
|
|
213
|
+
</Button>
|
|
214
|
+
<Button
|
|
215
|
+
size="sm"
|
|
216
|
+
onClick={save}
|
|
217
|
+
disabled={saving || !dirty}
|
|
218
|
+
>
|
|
219
|
+
<Save className="h-3.5 w-3.5" />
|
|
220
|
+
{saving ? "Saving…" : "Save"}
|
|
221
|
+
</Button>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
}
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
<div className="p-6">
|
|
229
|
+
<div className="mx-auto max-w-5xl grid gap-6 lg:grid-cols-[1fr_280px]">
|
|
230
|
+
<div className="space-y-6 min-w-0">
|
|
231
|
+
{error && (
|
|
232
|
+
<div className="rounded-xl border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm flex items-start gap-2">
|
|
233
|
+
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
|
234
|
+
<span>{error}</span>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{playbook && (
|
|
239
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
240
|
+
<Badge variant="outline" className="gap-1.5">
|
|
241
|
+
<FolderGit2 className="h-3 w-3" />
|
|
242
|
+
{playbook.agent_version || "default"}
|
|
243
|
+
</Badge>
|
|
244
|
+
<StatusBadge status={status!} />
|
|
245
|
+
{displayName(playbook.playbook_name) && (
|
|
246
|
+
<Badge variant="secondary" className="font-mono text-[10px]">
|
|
247
|
+
{displayName(playbook.playbook_name)}
|
|
248
|
+
</Badge>
|
|
249
|
+
)}
|
|
250
|
+
{dirty && (
|
|
251
|
+
<Badge variant="destructive" className="gap-1.5">
|
|
252
|
+
unsaved changes
|
|
253
|
+
</Badge>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
<Section
|
|
259
|
+
icon={AlertTriangle}
|
|
260
|
+
title="Trigger"
|
|
261
|
+
hint="When this rule should apply. Leave empty if it always applies."
|
|
262
|
+
>
|
|
263
|
+
{editing ? (
|
|
264
|
+
<AutoTextarea
|
|
265
|
+
value={form.trigger}
|
|
266
|
+
onChange={(v) => setForm((f) => ({ ...f, trigger: v }))}
|
|
267
|
+
rows={2}
|
|
268
|
+
placeholder="e.g. When writing or running async Python tests."
|
|
269
|
+
/>
|
|
270
|
+
) : (
|
|
271
|
+
<Prose text={playbook?.trigger ?? ""} muted={!playbook?.trigger} />
|
|
272
|
+
)}
|
|
273
|
+
</Section>
|
|
274
|
+
|
|
275
|
+
<Section
|
|
276
|
+
icon={BookMarked}
|
|
277
|
+
title="Rule"
|
|
278
|
+
hint="What Claude should do. Injected when relevant in future sessions."
|
|
279
|
+
>
|
|
280
|
+
{editing ? (
|
|
281
|
+
<AutoTextarea
|
|
282
|
+
value={form.content}
|
|
283
|
+
onChange={(v) => setForm((f) => ({ ...f, content: v }))}
|
|
284
|
+
rows={6}
|
|
285
|
+
placeholder="e.g. Use anyio with trio backend — never pytest-asyncio."
|
|
286
|
+
/>
|
|
287
|
+
) : (
|
|
288
|
+
<Prose text={playbook?.content ?? ""} />
|
|
289
|
+
)}
|
|
290
|
+
</Section>
|
|
291
|
+
|
|
292
|
+
<Section
|
|
293
|
+
icon={FileText}
|
|
294
|
+
title="Rationale"
|
|
295
|
+
hint="Why — the reason, constraint, or past incident behind this rule."
|
|
296
|
+
>
|
|
297
|
+
{editing ? (
|
|
298
|
+
<AutoTextarea
|
|
299
|
+
value={form.rationale}
|
|
300
|
+
onChange={(v) => setForm((f) => ({ ...f, rationale: v }))}
|
|
301
|
+
rows={3}
|
|
302
|
+
placeholder="e.g. pytest-asyncio deadlocked CI on project X — trio is the project standard."
|
|
303
|
+
/>
|
|
304
|
+
) : (
|
|
305
|
+
<Prose
|
|
306
|
+
text={playbook?.rationale ?? ""}
|
|
307
|
+
muted={!playbook?.rationale}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
</Section>
|
|
311
|
+
|
|
312
|
+
{!editing && playbook && (
|
|
313
|
+
<>
|
|
314
|
+
<Separator />
|
|
315
|
+
<DangerZone
|
|
316
|
+
onDelete={remove}
|
|
317
|
+
deleting={deleting}
|
|
318
|
+
disabled={saving}
|
|
319
|
+
/>
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{playbook && (
|
|
325
|
+
<aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
|
|
326
|
+
<div className="rounded-xl border border-border bg-card p-4">
|
|
327
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-3">
|
|
328
|
+
Metadata
|
|
329
|
+
</h3>
|
|
330
|
+
<dl className="space-y-2.5 text-sm">
|
|
331
|
+
<Meta
|
|
332
|
+
icon={Hash}
|
|
333
|
+
label="ID"
|
|
334
|
+
value={String(playbook.user_playbook_id)}
|
|
335
|
+
mono
|
|
336
|
+
/>
|
|
337
|
+
<Meta
|
|
338
|
+
icon={Clock}
|
|
339
|
+
label="Created"
|
|
340
|
+
value={formatTimestamp(playbook.created_at)}
|
|
341
|
+
/>
|
|
342
|
+
<Meta
|
|
343
|
+
label="Project"
|
|
344
|
+
value={playbook.agent_version || "default"}
|
|
345
|
+
mono
|
|
346
|
+
/>
|
|
347
|
+
{playbook.user_id && (
|
|
348
|
+
<CopyMeta
|
|
349
|
+
label="Project"
|
|
350
|
+
value={playbook.user_id}
|
|
351
|
+
display={truncateId(playbook.user_id, 32, 8)}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
354
|
+
{playbook.request_id && (
|
|
355
|
+
<CopyMeta
|
|
356
|
+
label="Request"
|
|
357
|
+
value={playbook.request_id}
|
|
358
|
+
display={truncateId(playbook.request_id, 8, 4)}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
{playbook.source && (
|
|
362
|
+
<Meta label="Source" value={playbook.source} mono />
|
|
363
|
+
)}
|
|
364
|
+
</dl>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
{playbook.source_interaction_ids?.length > 0 && (
|
|
368
|
+
<div className="rounded-xl border border-border bg-card p-4">
|
|
369
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-3">
|
|
370
|
+
Extracted from
|
|
371
|
+
</h3>
|
|
372
|
+
<p className="text-xs text-muted-foreground mb-2">
|
|
373
|
+
{playbook.source_interaction_ids.length} interaction
|
|
374
|
+
{playbook.source_interaction_ids.length === 1 ? "" : "s"}
|
|
375
|
+
</p>
|
|
376
|
+
<div className="flex flex-wrap gap-1">
|
|
377
|
+
{playbook.source_interaction_ids.slice(0, 24).map((iid) => (
|
|
378
|
+
<Badge
|
|
379
|
+
key={iid}
|
|
380
|
+
variant="outline"
|
|
381
|
+
className="font-mono text-[10px]"
|
|
382
|
+
>
|
|
383
|
+
#{iid}
|
|
384
|
+
</Badge>
|
|
385
|
+
))}
|
|
386
|
+
{playbook.source_interaction_ids.length > 24 && (
|
|
387
|
+
<Badge variant="ghost" className="text-[10px]">
|
|
388
|
+
+{playbook.source_interaction_ids.length - 24} more
|
|
389
|
+
</Badge>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</aside>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function Section({
|
|
403
|
+
icon: Icon,
|
|
404
|
+
title,
|
|
405
|
+
hint,
|
|
406
|
+
children,
|
|
407
|
+
}: {
|
|
408
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
409
|
+
title: string;
|
|
410
|
+
hint?: string;
|
|
411
|
+
children: React.ReactNode;
|
|
412
|
+
}) {
|
|
413
|
+
return (
|
|
414
|
+
<section className="space-y-2">
|
|
415
|
+
<div className="flex items-baseline gap-2">
|
|
416
|
+
<Label className="text-sm font-semibold flex items-center gap-1.5">
|
|
417
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
418
|
+
{title}
|
|
419
|
+
</Label>
|
|
420
|
+
{hint && (
|
|
421
|
+
<span className="text-xs text-muted-foreground">{hint}</span>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
{children}
|
|
425
|
+
</section>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function Prose({ text, muted = false }: { text: string; muted?: boolean }) {
|
|
430
|
+
if (!text) {
|
|
431
|
+
return (
|
|
432
|
+
<p className="text-sm text-muted-foreground italic">
|
|
433
|
+
{muted ? "Not set" : "—"}
|
|
434
|
+
</p>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return (
|
|
438
|
+
<div
|
|
439
|
+
className={cn(
|
|
440
|
+
"rounded-xl border border-border bg-card px-4 py-3",
|
|
441
|
+
muted && "bg-muted/30",
|
|
442
|
+
)}
|
|
443
|
+
>
|
|
444
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
445
|
+
{text}
|
|
446
|
+
</p>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function AutoTextarea({
|
|
452
|
+
value,
|
|
453
|
+
onChange,
|
|
454
|
+
rows = 3,
|
|
455
|
+
placeholder,
|
|
456
|
+
}: {
|
|
457
|
+
value: string;
|
|
458
|
+
onChange: (v: string) => void;
|
|
459
|
+
rows?: number;
|
|
460
|
+
placeholder?: string;
|
|
461
|
+
}) {
|
|
462
|
+
return (
|
|
463
|
+
<textarea
|
|
464
|
+
value={value}
|
|
465
|
+
onChange={(e) => onChange(e.target.value)}
|
|
466
|
+
rows={rows}
|
|
467
|
+
placeholder={placeholder}
|
|
468
|
+
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"
|
|
469
|
+
/>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function StatusBadge({
|
|
474
|
+
status,
|
|
475
|
+
}: {
|
|
476
|
+
status: "CURRENT" | "ARCHIVED" | "PENDING";
|
|
477
|
+
}) {
|
|
478
|
+
const variant =
|
|
479
|
+
status === "CURRENT"
|
|
480
|
+
? "secondary"
|
|
481
|
+
: status === "ARCHIVED"
|
|
482
|
+
? "outline"
|
|
483
|
+
: "default";
|
|
484
|
+
return (
|
|
485
|
+
<Badge variant={variant} className="gap-1.5">
|
|
486
|
+
<span
|
|
487
|
+
className={cn(
|
|
488
|
+
"h-1.5 w-1.5 rounded-full",
|
|
489
|
+
status === "CURRENT" && "bg-emerald-500",
|
|
490
|
+
status === "PENDING" && "bg-amber-500",
|
|
491
|
+
status === "ARCHIVED" && "bg-muted-foreground",
|
|
492
|
+
)}
|
|
493
|
+
/>
|
|
494
|
+
{status}
|
|
495
|
+
</Badge>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function Meta({
|
|
500
|
+
icon: Icon,
|
|
501
|
+
label,
|
|
502
|
+
value,
|
|
503
|
+
mono,
|
|
504
|
+
}: {
|
|
505
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
506
|
+
label: string;
|
|
507
|
+
value: string;
|
|
508
|
+
mono?: boolean;
|
|
509
|
+
}) {
|
|
510
|
+
return (
|
|
511
|
+
<div className="flex items-start justify-between gap-3">
|
|
512
|
+
<dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
|
|
513
|
+
{Icon && <Icon className="h-3 w-3" />}
|
|
514
|
+
{label}
|
|
515
|
+
</dt>
|
|
516
|
+
<dd
|
|
517
|
+
className={cn(
|
|
518
|
+
"text-xs text-right min-w-0 break-words",
|
|
519
|
+
mono && "font-mono",
|
|
520
|
+
)}
|
|
521
|
+
>
|
|
522
|
+
{value}
|
|
523
|
+
</dd>
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function CopyMeta({
|
|
529
|
+
label,
|
|
530
|
+
value,
|
|
531
|
+
display,
|
|
532
|
+
}: {
|
|
533
|
+
label: string;
|
|
534
|
+
value: string;
|
|
535
|
+
display: string;
|
|
536
|
+
}) {
|
|
537
|
+
const [copied, setCopied] = useState(false);
|
|
538
|
+
const copy = async () => {
|
|
539
|
+
try {
|
|
540
|
+
await navigator.clipboard.writeText(value);
|
|
541
|
+
setCopied(true);
|
|
542
|
+
setTimeout(() => setCopied(false), 1200);
|
|
543
|
+
} catch {
|
|
544
|
+
// ignore
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
return (
|
|
548
|
+
<div className="flex items-start justify-between gap-3">
|
|
549
|
+
<dt className="text-xs text-muted-foreground shrink-0">{label}</dt>
|
|
550
|
+
<dd className="text-xs min-w-0 flex items-center gap-1.5">
|
|
551
|
+
<code className="font-mono">{display}</code>
|
|
552
|
+
<button
|
|
553
|
+
onClick={copy}
|
|
554
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
555
|
+
title="Copy full id"
|
|
556
|
+
>
|
|
557
|
+
{copied ? (
|
|
558
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
559
|
+
) : (
|
|
560
|
+
<Copy className="h-3 w-3" />
|
|
561
|
+
)}
|
|
562
|
+
</button>
|
|
563
|
+
</dd>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function DangerZone({
|
|
569
|
+
onDelete,
|
|
570
|
+
deleting,
|
|
571
|
+
disabled,
|
|
572
|
+
}: {
|
|
573
|
+
onDelete: () => void;
|
|
574
|
+
deleting: boolean;
|
|
575
|
+
disabled: boolean;
|
|
576
|
+
}) {
|
|
577
|
+
return (
|
|
578
|
+
<section className="rounded-xl border border-destructive/30 bg-destructive/5 p-4 flex items-start justify-between gap-4">
|
|
579
|
+
<div className="min-w-0">
|
|
580
|
+
<h3 className="text-sm font-semibold text-destructive">Danger zone</h3>
|
|
581
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
582
|
+
Deleting removes this project-specific skill permanently. It will stop being
|
|
583
|
+
injected into future sessions.
|
|
584
|
+
</p>
|
|
585
|
+
</div>
|
|
586
|
+
<Button
|
|
587
|
+
variant="destructive"
|
|
588
|
+
size="sm"
|
|
589
|
+
onClick={onDelete}
|
|
590
|
+
disabled={deleting || disabled}
|
|
591
|
+
>
|
|
592
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
593
|
+
{deleting ? "Deleting…" : "Delete"}
|
|
594
|
+
</Button>
|
|
595
|
+
</section>
|
|
596
|
+
);
|
|
597
|
+
}
|