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.
Files changed (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +69 -27
  3. package/bin/claude-smart.js +296 -11
  4. package/package.json +11 -1
  5. package/plugin/.claude-plugin/plugin.json +17 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/LICENSE +202 -0
  8. package/plugin/README.md +37 -0
  9. package/plugin/bin/cs-cite +77 -0
  10. package/plugin/commands/clear-all.md +8 -0
  11. package/plugin/commands/dashboard.md +8 -0
  12. package/plugin/commands/learn.md +12 -0
  13. package/plugin/commands/restart.md +8 -0
  14. package/plugin/commands/show.md +8 -0
  15. package/plugin/dashboard/AGENTS.md +6 -0
  16. package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
  17. package/plugin/dashboard/app/api/config/route.ts +16 -0
  18. package/plugin/dashboard/app/api/health/route.ts +10 -0
  19. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
  20. package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
  21. package/plugin/dashboard/app/api/sessions/route.ts +14 -0
  22. package/plugin/dashboard/app/configure/env/page.tsx +318 -0
  23. package/plugin/dashboard/app/configure/layout.tsx +47 -0
  24. package/plugin/dashboard/app/configure/page.tsx +5 -0
  25. package/plugin/dashboard/app/configure/server/page.tsx +258 -0
  26. package/plugin/dashboard/app/dashboard/page.tsx +227 -0
  27. package/plugin/dashboard/app/globals.css +129 -0
  28. package/plugin/dashboard/app/icon.png +0 -0
  29. package/plugin/dashboard/app/layout.tsx +40 -0
  30. package/plugin/dashboard/app/page.tsx +5 -0
  31. package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
  32. package/plugin/dashboard/app/preferences/page.tsx +126 -0
  33. package/plugin/dashboard/app/providers.tsx +12 -0
  34. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
  35. package/plugin/dashboard/app/sessions/page.tsx +186 -0
  36. package/plugin/dashboard/app/skills/page.tsx +362 -0
  37. package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
  38. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
  39. package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
  40. package/plugin/dashboard/components/common/empty-state.tsx +34 -0
  41. package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
  42. package/plugin/dashboard/components/common/page-header.tsx +34 -0
  43. package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
  44. package/plugin/dashboard/components/common/stat-card.tsx +38 -0
  45. package/plugin/dashboard/components/layout/nav-items.ts +22 -0
  46. package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
  47. package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
  48. package/plugin/dashboard/components/stall-banner.tsx +53 -0
  49. package/plugin/dashboard/components/ui/badge.tsx +52 -0
  50. package/plugin/dashboard/components/ui/button.tsx +60 -0
  51. package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
  52. package/plugin/dashboard/components/ui/input.tsx +20 -0
  53. package/plugin/dashboard/components/ui/label.tsx +20 -0
  54. package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
  55. package/plugin/dashboard/components/ui/select.tsx +201 -0
  56. package/plugin/dashboard/components/ui/separator.tsx +25 -0
  57. package/plugin/dashboard/components/ui/sheet.tsx +135 -0
  58. package/plugin/dashboard/components/ui/switch.tsx +32 -0
  59. package/plugin/dashboard/components.json +25 -0
  60. package/plugin/dashboard/eslint.config.mjs +16 -0
  61. package/plugin/dashboard/hooks/use-settings.tsx +88 -0
  62. package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
  63. package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
  64. package/plugin/dashboard/lib/config-file.ts +131 -0
  65. package/plugin/dashboard/lib/format.ts +58 -0
  66. package/plugin/dashboard/lib/reflexio-client.ts +238 -0
  67. package/plugin/dashboard/lib/reflexio-url.ts +17 -0
  68. package/plugin/dashboard/lib/session-reader.ts +245 -0
  69. package/plugin/dashboard/lib/status.ts +24 -0
  70. package/plugin/dashboard/lib/types.ts +145 -0
  71. package/plugin/dashboard/lib/utils.ts +6 -0
  72. package/plugin/dashboard/next.config.ts +7 -0
  73. package/plugin/dashboard/package-lock.json +10275 -0
  74. package/plugin/dashboard/package.json +37 -0
  75. package/plugin/dashboard/postcss.config.mjs +7 -0
  76. package/plugin/dashboard/public/claude-smart-icon.png +0 -0
  77. package/plugin/dashboard/tsconfig.json +34 -0
  78. package/plugin/hooks/codex-hooks.json +67 -0
  79. package/plugin/hooks/hooks.json +111 -0
  80. package/plugin/pyproject.toml +49 -0
  81. package/plugin/scripts/_codex_env.sh +27 -0
  82. package/plugin/scripts/_lib.sh +325 -0
  83. package/plugin/scripts/backend-service.sh +208 -0
  84. package/plugin/scripts/cli.sh +40 -0
  85. package/plugin/scripts/dashboard-build.sh +139 -0
  86. package/plugin/scripts/dashboard-open.sh +107 -0
  87. package/plugin/scripts/dashboard-service.sh +195 -0
  88. package/plugin/scripts/ensure-plugin-root.sh +84 -0
  89. package/plugin/scripts/hook_entry.sh +70 -0
  90. package/plugin/scripts/smart-install.sh +411 -0
  91. package/plugin/src/claude_smart/__init__.py +3 -0
  92. package/plugin/src/claude_smart/cli.py +1273 -0
  93. package/plugin/src/claude_smart/context_format.py +277 -0
  94. package/plugin/src/claude_smart/context_inject.py +92 -0
  95. package/plugin/src/claude_smart/cs_cite.py +236 -0
  96. package/plugin/src/claude_smart/events/__init__.py +1 -0
  97. package/plugin/src/claude_smart/events/post_tool.py +148 -0
  98. package/plugin/src/claude_smart/events/pre_tool.py +52 -0
  99. package/plugin/src/claude_smart/events/session_end.py +20 -0
  100. package/plugin/src/claude_smart/events/session_start.py +119 -0
  101. package/plugin/src/claude_smart/events/stop.py +393 -0
  102. package/plugin/src/claude_smart/events/user_prompt.py +73 -0
  103. package/plugin/src/claude_smart/hook.py +114 -0
  104. package/plugin/src/claude_smart/ids.py +56 -0
  105. package/plugin/src/claude_smart/internal_call.py +89 -0
  106. package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
  107. package/plugin/src/claude_smart/publish.py +71 -0
  108. package/plugin/src/claude_smart/query_compose.py +51 -0
  109. package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
  110. package/plugin/src/claude_smart/runtime.py +52 -0
  111. package/plugin/src/claude_smart/stall_banner.py +61 -0
  112. package/plugin/src/claude_smart/state.py +276 -0
  113. 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
+ }