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,531 @@
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
+ Hash,
16
+ Clock,
17
+ CalendarClock,
18
+ FileText,
19
+ Sparkles,
20
+ Tags,
21
+ Braces,
22
+ FolderGit2,
23
+ } from "lucide-react";
24
+ import { PageHeader } from "@/components/common/page-header";
25
+ import { EmptyState } from "@/components/common/empty-state";
26
+ import { Button } from "@/components/ui/button";
27
+ import { Label } from "@/components/ui/label";
28
+ import { Badge } from "@/components/ui/badge";
29
+ import { Separator } from "@/components/ui/separator";
30
+ import { reflexio } from "@/lib/reflexio-client";
31
+ import { useSettings } from "@/hooks/use-settings";
32
+ import { formatTimestamp, truncateId } from "@/lib/format";
33
+ import { cn } from "@/lib/utils";
34
+ import { statusLabel as status } from "@/lib/status";
35
+ import type { UserProfile } from "@/lib/types";
36
+
37
+ export default function PreferenceDetailPage({
38
+ params,
39
+ }: {
40
+ params: Promise<{ id: string }>;
41
+ }) {
42
+ const { id: rawId } = use(params);
43
+ const id = decodeURIComponent(rawId);
44
+ const router = useRouter();
45
+ const { reflexioUrl } = useSettings();
46
+
47
+ const [profile, setProfile] = useState<UserProfile | null>(null);
48
+ const [notFound, setNotFound] = useState(false);
49
+ const [error, setError] = useState<string | null>(null);
50
+ const [saving, setSaving] = useState(false);
51
+ const [deleting, setDeleting] = useState(false);
52
+ const [editing, setEditing] = useState(false);
53
+ const [content, setContent] = useState("");
54
+
55
+ useEffect(() => {
56
+ let cancelled = false;
57
+ reflexio
58
+ .getAllProfiles({ reflexioUrl, limit: 500 })
59
+ .then((res) => {
60
+ if (cancelled) return;
61
+ const found = (res.user_profiles ?? []).find(
62
+ (p) => p.profile_id === id,
63
+ );
64
+ if (!found) {
65
+ setNotFound(true);
66
+ return;
67
+ }
68
+ setProfile(found);
69
+ setContent(found.content);
70
+ })
71
+ .catch((e) => {
72
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
73
+ });
74
+ return () => {
75
+ cancelled = true;
76
+ };
77
+ }, [id, reflexioUrl]);
78
+
79
+ const dirty = useMemo(
80
+ () => !!profile && profile.content !== content,
81
+ [profile, content],
82
+ );
83
+
84
+ const save = async () => {
85
+ if (!profile || !dirty) return;
86
+ setSaving(true);
87
+ setError(null);
88
+ try {
89
+ await reflexio.updateUserProfile(
90
+ {
91
+ user_id: profile.user_id,
92
+ profile_id: profile.profile_id,
93
+ content,
94
+ },
95
+ reflexioUrl,
96
+ );
97
+ setProfile({ ...profile, content });
98
+ setEditing(false);
99
+ } catch (e) {
100
+ setError(e instanceof Error ? e.message : String(e));
101
+ } finally {
102
+ setSaving(false);
103
+ }
104
+ };
105
+
106
+ const remove = async () => {
107
+ if (!profile) return;
108
+ if (!confirm("Delete this preference? This cannot be undone.")) return;
109
+ setDeleting(true);
110
+ try {
111
+ await reflexio.deleteUserProfile(
112
+ { user_id: profile.user_id, profile_id: profile.profile_id },
113
+ reflexioUrl,
114
+ );
115
+ router.push("/preferences");
116
+ } catch (e) {
117
+ setError(e instanceof Error ? e.message : String(e));
118
+ setDeleting(false);
119
+ }
120
+ };
121
+
122
+ const cancelEdit = () => {
123
+ if (profile) setContent(profile.content);
124
+ setEditing(false);
125
+ };
126
+
127
+ if (notFound) {
128
+ return (
129
+ <div className="flex-1 overflow-auto">
130
+ <PageHeader title="Preference not found" />
131
+ <div className="p-6 max-w-2xl mx-auto">
132
+ <EmptyState
133
+ icon={AlertTriangle}
134
+ title="Preference not found"
135
+ description="It may have been deleted, archived, or moved outside the retrieval window."
136
+ action={
137
+ <Link href="/preferences">
138
+ <Button variant="outline" size="sm">
139
+ <ArrowLeft className="h-3.5 w-3.5" />
140
+ Back to preferences
141
+ </Button>
142
+ </Link>
143
+ }
144
+ />
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ const customEntries = profile?.custom_features
151
+ ? Object.entries(profile.custom_features).filter(
152
+ ([, v]) => v !== null && v !== undefined && v !== "",
153
+ )
154
+ : [];
155
+
156
+ return (
157
+ <div className="flex-1 overflow-auto">
158
+ <PageHeader
159
+ title="Preference"
160
+ description="Project-scoped preference extracted by claude-smart."
161
+ actions={
162
+ <div className="flex items-center gap-2">
163
+ <Link href="/preferences">
164
+ <Button variant="outline" size="sm">
165
+ <ArrowLeft className="h-3.5 w-3.5" />
166
+ Back
167
+ </Button>
168
+ </Link>
169
+ {!editing ? (
170
+ <Button
171
+ size="sm"
172
+ onClick={() => setEditing(true)}
173
+ disabled={!profile}
174
+ >
175
+ <Pencil className="h-3.5 w-3.5" />
176
+ Edit
177
+ </Button>
178
+ ) : (
179
+ <>
180
+ <Button
181
+ variant="outline"
182
+ size="sm"
183
+ onClick={cancelEdit}
184
+ disabled={saving}
185
+ >
186
+ <X className="h-3.5 w-3.5" />
187
+ Cancel
188
+ </Button>
189
+ <Button size="sm" onClick={save} disabled={saving || !dirty}>
190
+ <Save className="h-3.5 w-3.5" />
191
+ {saving ? "Saving…" : "Save"}
192
+ </Button>
193
+ </>
194
+ )}
195
+ </div>
196
+ }
197
+ />
198
+
199
+ <div className="p-6">
200
+ <div className="mx-auto max-w-5xl grid gap-6 lg:grid-cols-[1fr_280px]">
201
+ <div className="space-y-6 min-w-0">
202
+ {error && (
203
+ <div className="rounded-xl border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm flex items-start gap-2">
204
+ <AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
205
+ <span>{error}</span>
206
+ </div>
207
+ )}
208
+
209
+ {profile && (
210
+ <div className="flex items-center gap-2 flex-wrap">
211
+ <StatusBadge status={status(profile)} />
212
+ <Badge variant="outline" className="font-mono gap-1.5">
213
+ <FolderGit2 className="h-3 w-3" />
214
+ {truncateId(profile.user_id, 32, 8)}
215
+ </Badge>
216
+ {profile.source && (
217
+ <Badge variant="secondary" className="font-mono text-[10px]">
218
+ {profile.source}
219
+ </Badge>
220
+ )}
221
+ {dirty && (
222
+ <Badge variant="destructive" className="gap-1.5">
223
+ unsaved changes
224
+ </Badge>
225
+ )}
226
+ </div>
227
+ )}
228
+
229
+ <Section
230
+ icon={Sparkles}
231
+ title="Preference"
232
+ hint="Project-scoped preference. Reinjected into future sessions in this project until its configured TTL expires."
233
+ >
234
+ {editing ? (
235
+ <textarea
236
+ value={content}
237
+ onChange={(e) => setContent(e.target.value)}
238
+ rows={6}
239
+ placeholder="e.g. Project bans pytest-asyncio; uses anyio with trio backend for async tests."
240
+ 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"
241
+ />
242
+ ) : (
243
+ <Prose text={profile?.content ?? ""} />
244
+ )}
245
+ </Section>
246
+
247
+ {profile?.extractor_names && profile.extractor_names.length > 0 && (
248
+ <Section
249
+ icon={Tags}
250
+ title="Extractors"
251
+ hint="Which reflexio extractor generated this preference."
252
+ >
253
+ <div className="flex flex-wrap gap-1.5">
254
+ {profile.extractor_names.map((name) => (
255
+ <Badge
256
+ key={name}
257
+ variant="outline"
258
+ className="font-mono text-[10px]"
259
+ >
260
+ {name}
261
+ </Badge>
262
+ ))}
263
+ </div>
264
+ </Section>
265
+ )}
266
+
267
+ {customEntries.length > 0 && (
268
+ <Section
269
+ icon={Braces}
270
+ title="Custom features"
271
+ hint="Structured metadata attached to this preference."
272
+ >
273
+ <div className="rounded-xl border border-border bg-card overflow-hidden">
274
+ <dl className="divide-y divide-border">
275
+ {customEntries.map(([k, v]) => (
276
+ <div
277
+ key={k}
278
+ className="flex items-start justify-between gap-4 px-4 py-2.5"
279
+ >
280
+ <dt className="text-xs font-medium text-muted-foreground font-mono shrink-0">
281
+ {k}
282
+ </dt>
283
+ <dd className="text-xs min-w-0 break-words text-right">
284
+ {typeof v === "string"
285
+ ? v
286
+ : JSON.stringify(v, null, 0)}
287
+ </dd>
288
+ </div>
289
+ ))}
290
+ </dl>
291
+ </div>
292
+ </Section>
293
+ )}
294
+
295
+ {!editing && profile && (
296
+ <>
297
+ <Separator />
298
+ <DangerZone
299
+ onDelete={remove}
300
+ deleting={deleting}
301
+ disabled={saving}
302
+ />
303
+ </>
304
+ )}
305
+ </div>
306
+
307
+ {profile && (
308
+ <aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
309
+ <div className="rounded-xl border border-border bg-card p-4">
310
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-3">
311
+ Metadata
312
+ </h3>
313
+ <dl className="space-y-2.5 text-sm">
314
+ <Meta
315
+ icon={Clock}
316
+ label="Modified"
317
+ value={formatTimestamp(profile.last_modified_timestamp)}
318
+ />
319
+ {profile.expiration_timestamp &&
320
+ profile.expiration_timestamp > 0 && (
321
+ <Meta
322
+ icon={CalendarClock}
323
+ label="Expires"
324
+ value={formatTimestamp(profile.expiration_timestamp)}
325
+ />
326
+ )}
327
+ {profile.profile_time_to_live && (
328
+ <Meta
329
+ label="TTL"
330
+ value={profile.profile_time_to_live}
331
+ mono
332
+ />
333
+ )}
334
+ <CopyMeta
335
+ icon={Hash}
336
+ label="ID"
337
+ value={profile.profile_id}
338
+ display={truncateId(profile.profile_id, 8, 4)}
339
+ />
340
+ <CopyMeta
341
+ icon={FolderGit2}
342
+ label="Project"
343
+ value={profile.user_id}
344
+ display={truncateId(profile.user_id, 32, 8)}
345
+ />
346
+ {profile.generated_from_request_id && (
347
+ <CopyMeta
348
+ icon={FileText}
349
+ label="Request"
350
+ value={profile.generated_from_request_id}
351
+ display={truncateId(profile.generated_from_request_id, 8, 4)}
352
+ />
353
+ )}
354
+ </dl>
355
+ </div>
356
+ </aside>
357
+ )}
358
+ </div>
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+
364
+ function Section({
365
+ icon: Icon,
366
+ title,
367
+ hint,
368
+ children,
369
+ }: {
370
+ icon: React.ComponentType<{ className?: string }>;
371
+ title: string;
372
+ hint?: string;
373
+ children: React.ReactNode;
374
+ }) {
375
+ return (
376
+ <section className="space-y-2">
377
+ <div className="flex items-baseline gap-2 flex-wrap">
378
+ <Label className="text-sm font-semibold flex items-center gap-1.5">
379
+ <Icon className="h-3.5 w-3.5 text-muted-foreground" />
380
+ {title}
381
+ </Label>
382
+ {hint && <span className="text-xs text-muted-foreground">{hint}</span>}
383
+ </div>
384
+ {children}
385
+ </section>
386
+ );
387
+ }
388
+
389
+ function Prose({ text }: { text: string }) {
390
+ if (!text) {
391
+ return <p className="text-sm text-muted-foreground italic">—</p>;
392
+ }
393
+ return (
394
+ <div className="rounded-xl border border-border bg-card px-4 py-3">
395
+ <p className="text-sm leading-relaxed whitespace-pre-wrap break-words">
396
+ {text}
397
+ </p>
398
+ </div>
399
+ );
400
+ }
401
+
402
+ function StatusBadge({
403
+ status,
404
+ }: {
405
+ status: "CURRENT" | "ARCHIVED" | "PENDING";
406
+ }) {
407
+ const variant =
408
+ status === "CURRENT"
409
+ ? "secondary"
410
+ : status === "ARCHIVED"
411
+ ? "outline"
412
+ : "default";
413
+ return (
414
+ <Badge variant={variant} className="gap-1.5">
415
+ <span
416
+ className={cn(
417
+ "h-1.5 w-1.5 rounded-full",
418
+ status === "CURRENT" && "bg-emerald-500",
419
+ status === "PENDING" && "bg-amber-500",
420
+ status === "ARCHIVED" && "bg-muted-foreground",
421
+ )}
422
+ />
423
+ {status}
424
+ </Badge>
425
+ );
426
+ }
427
+
428
+ function Meta({
429
+ icon: Icon,
430
+ label,
431
+ value,
432
+ mono,
433
+ }: {
434
+ icon?: React.ComponentType<{ className?: string }>;
435
+ label: string;
436
+ value: string;
437
+ mono?: boolean;
438
+ }) {
439
+ return (
440
+ <div className="flex items-start justify-between gap-3">
441
+ <dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
442
+ {Icon && <Icon className="h-3 w-3" />}
443
+ {label}
444
+ </dt>
445
+ <dd
446
+ className={cn(
447
+ "text-xs text-right min-w-0 break-words",
448
+ mono && "font-mono",
449
+ )}
450
+ >
451
+ {value}
452
+ </dd>
453
+ </div>
454
+ );
455
+ }
456
+
457
+ function CopyMeta({
458
+ icon: Icon,
459
+ label,
460
+ value,
461
+ display,
462
+ }: {
463
+ icon?: React.ComponentType<{ className?: string }>;
464
+ label: string;
465
+ value: string;
466
+ display: string;
467
+ }) {
468
+ const [copied, setCopied] = useState(false);
469
+ const copy = async () => {
470
+ try {
471
+ await navigator.clipboard.writeText(value);
472
+ setCopied(true);
473
+ setTimeout(() => setCopied(false), 1200);
474
+ } catch {
475
+ // ignore
476
+ }
477
+ };
478
+ return (
479
+ <div className="flex items-start justify-between gap-3">
480
+ <dt className="text-xs text-muted-foreground shrink-0 flex items-center gap-1.5">
481
+ {Icon && <Icon className="h-3 w-3" />}
482
+ {label}
483
+ </dt>
484
+ <dd className="text-xs min-w-0 flex items-center gap-1.5">
485
+ <code className="font-mono">{display}</code>
486
+ <button
487
+ onClick={copy}
488
+ className="text-muted-foreground hover:text-foreground transition-colors"
489
+ title="Copy full id"
490
+ >
491
+ {copied ? (
492
+ <Check className="h-3 w-3 text-emerald-500" />
493
+ ) : (
494
+ <Copy className="h-3 w-3" />
495
+ )}
496
+ </button>
497
+ </dd>
498
+ </div>
499
+ );
500
+ }
501
+
502
+ function DangerZone({
503
+ onDelete,
504
+ deleting,
505
+ disabled,
506
+ }: {
507
+ onDelete: () => void;
508
+ deleting: boolean;
509
+ disabled: boolean;
510
+ }) {
511
+ return (
512
+ <section className="rounded-xl border border-destructive/30 bg-destructive/5 p-4 flex items-start justify-between gap-4">
513
+ <div className="min-w-0">
514
+ <h3 className="text-sm font-semibold text-destructive">Danger zone</h3>
515
+ <p className="text-xs text-muted-foreground mt-0.5">
516
+ Deleting removes this preference permanently. Preferences regenerate from
517
+ fresh interactions, so this is safe but not reversible.
518
+ </p>
519
+ </div>
520
+ <Button
521
+ variant="destructive"
522
+ size="sm"
523
+ onClick={onDelete}
524
+ disabled={deleting || disabled}
525
+ >
526
+ <Trash2 className="h-3.5 w-3.5" />
527
+ {deleting ? "Deleting…" : "Delete"}
528
+ </Button>
529
+ </section>
530
+ );
531
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { Users, ChevronRight } from "lucide-react";
6
+ import { PageHeader } from "@/components/common/page-header";
7
+ import { EmptyState } from "@/components/common/empty-state";
8
+ import { DeleteAllButton } from "@/components/common/delete-all-button";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Input } from "@/components/ui/input";
11
+ import { reflexio } from "@/lib/reflexio-client";
12
+ import { useSettings } from "@/hooks/use-settings";
13
+ import { formatRelative, truncateId } from "@/lib/format";
14
+ import type { UserProfile } from "@/lib/types";
15
+
16
+ export default function PreferencesPage() {
17
+ const { reflexioUrl } = useSettings();
18
+ const [profiles, setProfiles] = useState<UserProfile[] | null>(null);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [filter, setFilter] = useState("");
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+ reflexio
25
+ .getAllProfiles({ reflexioUrl })
26
+ .then((res) => {
27
+ if (!cancelled) {
28
+ setProfiles(res.user_profiles ?? []);
29
+ setError(null);
30
+ }
31
+ })
32
+ .catch((e) => {
33
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
34
+ });
35
+ return () => {
36
+ cancelled = true;
37
+ };
38
+ }, [reflexioUrl]);
39
+
40
+ const filtered = (profiles ?? []).filter(
41
+ (p) =>
42
+ p.content.toLowerCase().includes(filter.toLowerCase()) ||
43
+ p.user_id.toLowerCase().includes(filter.toLowerCase()),
44
+ );
45
+
46
+ return (
47
+ <div className="flex-1 overflow-auto">
48
+ <PageHeader
49
+ title="Preferences"
50
+ description="Project-scoped preferences extracted from interactions."
51
+ actions={
52
+ <div className="flex items-center gap-2">
53
+ <Input
54
+ value={filter}
55
+ onChange={(e) => setFilter(e.target.value)}
56
+ placeholder="Filter"
57
+ className="h-8 w-56 text-xs"
58
+ />
59
+ <DeleteAllButton
60
+ label={`Delete all${profiles && profiles.length > 0 ? ` (${profiles.length})` : ""}`}
61
+ confirmMessage={`Delete ALL ${profiles?.length ?? 0} preferences? Preferences regenerate from fresh interactions, but this cannot be undone.`}
62
+ disabled={!profiles || profiles.length === 0}
63
+ onConfirm={async () => {
64
+ await reflexio.deleteAllProfiles(reflexioUrl);
65
+ setProfiles([]);
66
+ }}
67
+ />
68
+ </div>
69
+ }
70
+ />
71
+
72
+ <div className="p-6">
73
+ {error && (
74
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm mb-4">
75
+ {error}. Is reflexio running on the URL in the top bar?
76
+ </div>
77
+ )}
78
+
79
+ {profiles === null && !error ? (
80
+ <div className="text-sm text-muted-foreground">Loading…</div>
81
+ ) : filtered.length === 0 ? (
82
+ <EmptyState
83
+ icon={Users}
84
+ title="No preferences yet"
85
+ description="Keep using Claude with claude-smart enabled — preferences will appear here automatically as the extractor learns patterns from your interactions."
86
+ />
87
+ ) : (
88
+ <div className="grid gap-3 md:grid-cols-2">
89
+ {filtered.map((p) => (
90
+ <Link
91
+ key={p.profile_id}
92
+ href={`/preferences/${encodeURIComponent(p.profile_id)}`}
93
+ className="group block rounded-xl border border-border bg-card p-4 hover:bg-accent/40 transition-colors"
94
+ >
95
+ <header className="flex items-center justify-between gap-2 mb-2">
96
+ <div className="flex items-center gap-2 min-w-0">
97
+ <Badge variant="outline" className="h-5 font-mono text-[10px]">
98
+ {truncateId(p.user_id, 32, 8)}
99
+ </Badge>
100
+ {p.status && (
101
+ <Badge variant="secondary" className="h-5 text-[10px]">
102
+ {p.status}
103
+ </Badge>
104
+ )}
105
+ </div>
106
+ <div className="flex items-center gap-1.5 shrink-0">
107
+ <span className="text-[11px] text-muted-foreground">
108
+ {formatRelative(p.last_modified_timestamp)}
109
+ </span>
110
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/60 group-hover:text-foreground transition-colors" />
111
+ </div>
112
+ </header>
113
+ <p className="text-sm leading-relaxed line-clamp-4">{p.content}</p>
114
+ {p.source && (
115
+ <p className="text-[11px] text-muted-foreground mt-2 font-mono">
116
+ source: {p.source}
117
+ </p>
118
+ )}
119
+ </Link>
120
+ ))}
121
+ </div>
122
+ )}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { ThemeProvider } from "next-themes";
4
+ import { SettingsProvider } from "@/hooks/use-settings";
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ return (
8
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
9
+ <SettingsProvider>{children}</SettingsProvider>
10
+ </ThemeProvider>
11
+ );
12
+ }