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,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { use, useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Wrench,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
ChevronRight,
|
|
11
|
+
Trash2,
|
|
12
|
+
Clock,
|
|
13
|
+
FolderGit2,
|
|
14
|
+
Copy,
|
|
15
|
+
Check,
|
|
16
|
+
Sparkles,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { PageHeader } from "@/components/common/page-header";
|
|
19
|
+
import { EmptyState } from "@/components/common/empty-state";
|
|
20
|
+
import { Badge } from "@/components/ui/badge";
|
|
21
|
+
import { Button } from "@/components/ui/button";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
import { formatTimestamp, truncateId } from "@/lib/format";
|
|
24
|
+
import type { CitedItem, SessionDetail } from "@/lib/types";
|
|
25
|
+
|
|
26
|
+
export default function InteractionDetailPage({
|
|
27
|
+
params,
|
|
28
|
+
}: {
|
|
29
|
+
params: Promise<{ sessionId: string }>;
|
|
30
|
+
}) {
|
|
31
|
+
const { sessionId } = use(params);
|
|
32
|
+
const router = useRouter();
|
|
33
|
+
const [detail, setDetail] = useState<SessionDetail | null>(null);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const [deleting, setDeleting] = useState(false);
|
|
36
|
+
|
|
37
|
+
const remove = async () => {
|
|
38
|
+
if (!confirm(`Delete session ${sessionId}? This cannot be undone.`)) return;
|
|
39
|
+
setDeleting(true);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(
|
|
42
|
+
`/api/sessions/${encodeURIComponent(sessionId)}`,
|
|
43
|
+
{ method: "DELETE" },
|
|
44
|
+
);
|
|
45
|
+
if (!res.ok) throw new Error(`delete failed: ${res.status}`);
|
|
46
|
+
router.push("/sessions");
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
49
|
+
setDeleting(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let cancelled = false;
|
|
55
|
+
fetch(`/api/sessions/${encodeURIComponent(sessionId)}`, { cache: "no-store" })
|
|
56
|
+
.then(async (r) => {
|
|
57
|
+
if (!r.ok) throw new Error(`failed: ${r.status}`);
|
|
58
|
+
return r.json();
|
|
59
|
+
})
|
|
60
|
+
.then((data) => {
|
|
61
|
+
if (!cancelled) setDetail(data);
|
|
62
|
+
})
|
|
63
|
+
.catch((e) => {
|
|
64
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
65
|
+
});
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
};
|
|
69
|
+
}, [sessionId]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex-1 overflow-auto">
|
|
73
|
+
<PageHeader
|
|
74
|
+
title="Session transcript"
|
|
75
|
+
description={sessionId}
|
|
76
|
+
actions={
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<Link href="/sessions">
|
|
79
|
+
<Button variant="outline" size="sm">
|
|
80
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
81
|
+
Back
|
|
82
|
+
</Button>
|
|
83
|
+
</Link>
|
|
84
|
+
<Button
|
|
85
|
+
variant="destructive"
|
|
86
|
+
size="sm"
|
|
87
|
+
onClick={remove}
|
|
88
|
+
disabled={deleting}
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
91
|
+
{deleting ? "Deleting…" : "Delete"}
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<div className="p-6 max-w-3xl mx-auto">
|
|
98
|
+
{error && (
|
|
99
|
+
<EmptyState
|
|
100
|
+
icon={AlertTriangle}
|
|
101
|
+
title="Unable to load session"
|
|
102
|
+
description={error}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{!error && detail && detail.turns.length === 0 && (
|
|
107
|
+
<EmptyState
|
|
108
|
+
icon={AlertTriangle}
|
|
109
|
+
title="Empty session"
|
|
110
|
+
description="No turns recorded in this buffer yet."
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{detail && detail.turns.length > 0 && (
|
|
115
|
+
<div className="space-y-4">
|
|
116
|
+
{detail.turns.map((turn, idx) => {
|
|
117
|
+
const isUser = turn.role === "User";
|
|
118
|
+
const flagged =
|
|
119
|
+
turn.user_action && turn.user_action !== "NONE";
|
|
120
|
+
return (
|
|
121
|
+
<article
|
|
122
|
+
key={idx}
|
|
123
|
+
className={cn(
|
|
124
|
+
"rounded-xl border px-4 py-3 bg-card",
|
|
125
|
+
flagged
|
|
126
|
+
? "border-destructive/30 bg-destructive/5"
|
|
127
|
+
: "border-border",
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
<header className="flex items-center justify-between gap-2 mb-2">
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<Badge
|
|
133
|
+
variant={isUser ? "secondary" : "outline"}
|
|
134
|
+
className="h-5 font-mono text-[10px]"
|
|
135
|
+
>
|
|
136
|
+
{turn.role}
|
|
137
|
+
</Badge>
|
|
138
|
+
{flagged && (
|
|
139
|
+
<Badge variant="destructive" className="h-5">
|
|
140
|
+
{turn.user_action}
|
|
141
|
+
</Badge>
|
|
142
|
+
)}
|
|
143
|
+
{turn.tools_used && turn.tools_used.length > 0 && (
|
|
144
|
+
<div className="flex items-center gap-1 flex-wrap">
|
|
145
|
+
{turn.tools_used.map((t, ti) => {
|
|
146
|
+
const input = t.tool_data?.input;
|
|
147
|
+
const output = t.tool_data?.output;
|
|
148
|
+
const hasInput =
|
|
149
|
+
input && Object.keys(input).length > 0;
|
|
150
|
+
const hasOutput =
|
|
151
|
+
typeof output === "string" && output.length > 0;
|
|
152
|
+
if (!hasInput && !hasOutput) {
|
|
153
|
+
return (
|
|
154
|
+
<span key={ti}>
|
|
155
|
+
<Badge
|
|
156
|
+
variant="outline"
|
|
157
|
+
className="h-5 gap-1 text-[10px]"
|
|
158
|
+
>
|
|
159
|
+
<Wrench className="h-3 w-3" />
|
|
160
|
+
{t.tool_name}
|
|
161
|
+
</Badge>
|
|
162
|
+
</span>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return (
|
|
166
|
+
<details key={ti} className="group">
|
|
167
|
+
<summary className="cursor-pointer list-none">
|
|
168
|
+
<Badge
|
|
169
|
+
variant="outline"
|
|
170
|
+
className="h-5 gap-1 text-[10px]"
|
|
171
|
+
>
|
|
172
|
+
<Wrench className="h-3 w-3" />
|
|
173
|
+
{t.tool_name}
|
|
174
|
+
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
|
|
175
|
+
</Badge>
|
|
176
|
+
</summary>
|
|
177
|
+
<div className="mt-1 space-y-1.5">
|
|
178
|
+
{hasInput && (
|
|
179
|
+
<div>
|
|
180
|
+
<div className="text-[10px] font-mono uppercase tracking-wide text-muted-foreground/70 mb-0.5">
|
|
181
|
+
input
|
|
182
|
+
</div>
|
|
183
|
+
<pre className="whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2 py-1 text-[10px] font-mono text-muted-foreground">
|
|
184
|
+
{JSON.stringify(input, null, 2)}
|
|
185
|
+
</pre>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
{hasOutput && (
|
|
189
|
+
<div>
|
|
190
|
+
<div className="text-[10px] font-mono uppercase tracking-wide text-muted-foreground/70 mb-0.5">
|
|
191
|
+
output
|
|
192
|
+
</div>
|
|
193
|
+
<pre className="whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 px-2 py-1 text-[10px] font-mono text-muted-foreground">
|
|
194
|
+
{output}
|
|
195
|
+
</pre>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</details>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</header>
|
|
206
|
+
<pre className="whitespace-pre-wrap break-words text-sm font-sans leading-relaxed">
|
|
207
|
+
{turn.content}
|
|
208
|
+
</pre>
|
|
209
|
+
{turn.user_action_description && (
|
|
210
|
+
<p className="text-xs text-muted-foreground mt-2 italic">
|
|
211
|
+
{turn.user_action_description}
|
|
212
|
+
</p>
|
|
213
|
+
)}
|
|
214
|
+
{turn.cited_items && turn.cited_items.length > 0 && (
|
|
215
|
+
<CitedItemsRow items={turn.cited_items} />
|
|
216
|
+
)}
|
|
217
|
+
<TurnMeta ts={turn.ts} userId={turn.user_id} />
|
|
218
|
+
</article>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
<div className="text-xs text-muted-foreground text-center">
|
|
222
|
+
Published up to turn {detail.published_up_to}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function CitedItemsRow({ items }: { items: CitedItem[] }) {
|
|
232
|
+
return (
|
|
233
|
+
<div className="mt-3 flex items-start gap-2 text-[11px]">
|
|
234
|
+
<Sparkles className="h-3.5 w-3.5 mt-0.5 text-amber-500 shrink-0" />
|
|
235
|
+
<div className="flex items-center gap-1 flex-wrap">
|
|
236
|
+
<span className="text-muted-foreground">Used</span>
|
|
237
|
+
{items.map((item) => {
|
|
238
|
+
const targetId = item.real_id ?? item.id;
|
|
239
|
+
const href =
|
|
240
|
+
item.kind === "playbook"
|
|
241
|
+
? item.source_kind === "agent_playbook"
|
|
242
|
+
? `/skills/shared/${encodeURIComponent(targetId)}`
|
|
243
|
+
: `/skills/project/${encodeURIComponent(targetId)}`
|
|
244
|
+
: `/preferences/${encodeURIComponent(targetId)}`;
|
|
245
|
+
return (
|
|
246
|
+
<Link
|
|
247
|
+
key={item.id}
|
|
248
|
+
href={href}
|
|
249
|
+
target="_blank"
|
|
250
|
+
rel="noopener noreferrer"
|
|
251
|
+
title={`${item.kind === "playbook" ? "skill" : "preference"} • id=${targetId}`}
|
|
252
|
+
>
|
|
253
|
+
<Badge
|
|
254
|
+
variant="outline"
|
|
255
|
+
className="h-5 gap-1 text-[10px] border-amber-500/40 cursor-pointer hover:bg-amber-500/10 hover:border-amber-500/70 transition-colors"
|
|
256
|
+
>
|
|
257
|
+
{item.title || item.id}
|
|
258
|
+
</Badge>
|
|
259
|
+
</Link>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function TurnMeta({ ts, userId }: { ts?: number; userId?: string }) {
|
|
268
|
+
if (ts === undefined && !userId) return null;
|
|
269
|
+
return (
|
|
270
|
+
<dl className="mt-3 pt-2 border-t border-border/60 flex items-center justify-end gap-4 text-[11px]">
|
|
271
|
+
{ts !== undefined && (
|
|
272
|
+
<div className="flex items-center gap-1.5">
|
|
273
|
+
<dt className="text-muted-foreground flex items-center gap-1">
|
|
274
|
+
<Clock className="h-3 w-3" />
|
|
275
|
+
</dt>
|
|
276
|
+
<dd className="font-mono text-muted-foreground">
|
|
277
|
+
{formatTimestamp(ts)}
|
|
278
|
+
</dd>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{userId && (
|
|
282
|
+
<div className="flex items-center gap-1.5">
|
|
283
|
+
<dt className="text-muted-foreground flex items-center gap-1">
|
|
284
|
+
<FolderGit2 className="h-3 w-3" />
|
|
285
|
+
<span>Project</span>
|
|
286
|
+
</dt>
|
|
287
|
+
<dd className="flex items-center gap-1">
|
|
288
|
+
<code className="font-mono">{truncateId(userId, 32, 8)}</code>
|
|
289
|
+
<CopyButton value={userId} />
|
|
290
|
+
</dd>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</dl>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function CopyButton({ value }: { value: string }) {
|
|
298
|
+
const [copied, setCopied] = useState(false);
|
|
299
|
+
const copy = async () => {
|
|
300
|
+
try {
|
|
301
|
+
await navigator.clipboard.writeText(value);
|
|
302
|
+
setCopied(true);
|
|
303
|
+
setTimeout(() => setCopied(false), 1200);
|
|
304
|
+
} catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
return (
|
|
309
|
+
<button
|
|
310
|
+
onClick={copy}
|
|
311
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
312
|
+
title="Copy"
|
|
313
|
+
>
|
|
314
|
+
{copied ? (
|
|
315
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
316
|
+
) : (
|
|
317
|
+
<Copy className="h-3 w-3" />
|
|
318
|
+
)}
|
|
319
|
+
</button>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { MessageSquare, Search } 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 { LearningsBadge } from "@/components/common/learnings-badge";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { dayBucket, formatRelative, truncateId } from "@/lib/format";
|
|
12
|
+
import type { SessionSummary } from "@/lib/types";
|
|
13
|
+
|
|
14
|
+
export default function SessionsPage() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const [sessions, setSessions] = useState<SessionSummary[] | null>(null);
|
|
17
|
+
const [filter, setFilter] = useState("");
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
fetch("/api/sessions", { cache: "no-store" })
|
|
23
|
+
.then((r) => r.json())
|
|
24
|
+
.then((data) => {
|
|
25
|
+
if (!cancelled) setSessions(data.sessions ?? []);
|
|
26
|
+
})
|
|
27
|
+
.catch((e) => {
|
|
28
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
29
|
+
});
|
|
30
|
+
return () => {
|
|
31
|
+
cancelled = true;
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const filtered = useMemo(() => {
|
|
36
|
+
const q = filter.trim().toLowerCase();
|
|
37
|
+
if (!q) return sessions ?? [];
|
|
38
|
+
return (sessions ?? []).filter((s) => {
|
|
39
|
+
const hay = `${s.session_id} ${s.preview ?? ""}`.toLowerCase();
|
|
40
|
+
return hay.includes(q);
|
|
41
|
+
});
|
|
42
|
+
}, [sessions, filter]);
|
|
43
|
+
|
|
44
|
+
const grouped = useMemo(() => {
|
|
45
|
+
const buckets = new Map<string, SessionSummary[]>();
|
|
46
|
+
for (const s of filtered) {
|
|
47
|
+
const key = dayBucket(s.last_activity);
|
|
48
|
+
const arr = buckets.get(key);
|
|
49
|
+
if (arr) arr.push(s);
|
|
50
|
+
else buckets.set(key, [s]);
|
|
51
|
+
}
|
|
52
|
+
return Array.from(buckets.entries());
|
|
53
|
+
}, [filtered]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex-1 overflow-auto">
|
|
57
|
+
<PageHeader
|
|
58
|
+
title="Sessions"
|
|
59
|
+
description="Sessions buffered locally under ~/.claude-smart/sessions/. Each row is one session; click to see its interactions."
|
|
60
|
+
actions={
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
64
|
+
<Input
|
|
65
|
+
value={filter}
|
|
66
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
67
|
+
placeholder="Search sessions…"
|
|
68
|
+
className="h-8 w-64 pl-7 text-xs"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<DeleteAllButton
|
|
72
|
+
label={`Delete all${sessions && sessions.length > 0 ? ` (${sessions.length})` : ""}`}
|
|
73
|
+
confirmMessage={`Delete ALL ${sessions?.length ?? 0} local session buffers? This cannot be undone.`}
|
|
74
|
+
disabled={!sessions || sessions.length === 0}
|
|
75
|
+
onConfirm={async () => {
|
|
76
|
+
const res = await fetch("/api/sessions", { method: "DELETE" });
|
|
77
|
+
if (!res.ok) throw new Error(`delete failed: ${res.status}`);
|
|
78
|
+
setSessions([]);
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<div className="p-6">
|
|
86
|
+
{error && (
|
|
87
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm mb-4">
|
|
88
|
+
{error}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{sessions === null && !error ? (
|
|
93
|
+
<div className="text-sm text-muted-foreground">Loading…</div>
|
|
94
|
+
) : sessions && sessions.length > 0 && filtered.length === 0 ? (
|
|
95
|
+
<EmptyState
|
|
96
|
+
icon={Search}
|
|
97
|
+
title="No matches"
|
|
98
|
+
description={`No session matches "${filter}".`}
|
|
99
|
+
/>
|
|
100
|
+
) : filtered.length === 0 ? (
|
|
101
|
+
<EmptyState
|
|
102
|
+
icon={MessageSquare}
|
|
103
|
+
title="No sessions found"
|
|
104
|
+
description="Start Claude Code with claude-smart enabled — JSONL buffers will appear in ~/.claude-smart/sessions/."
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="space-y-6">
|
|
108
|
+
{grouped.map(([label, items]) => (
|
|
109
|
+
<section key={label}>
|
|
110
|
+
<div className="flex items-center gap-2 mb-2 px-1">
|
|
111
|
+
<h2 className="text-[11px] uppercase tracking-wider font-medium text-muted-foreground">
|
|
112
|
+
{label}
|
|
113
|
+
</h2>
|
|
114
|
+
<span className="text-[11px] text-muted-foreground/70">
|
|
115
|
+
· {items.length}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="rounded-xl border border-border divide-y divide-border bg-card overflow-hidden">
|
|
119
|
+
{items.map((s) => (
|
|
120
|
+
<div
|
|
121
|
+
key={s.session_id}
|
|
122
|
+
role="link"
|
|
123
|
+
tabIndex={0}
|
|
124
|
+
onClick={() =>
|
|
125
|
+
router.push(
|
|
126
|
+
`/sessions/${encodeURIComponent(s.session_id)}`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
onKeyDown={(e) => {
|
|
130
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
router.push(
|
|
133
|
+
`/sessions/${encodeURIComponent(s.session_id)}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
className="flex items-center gap-3 px-4 py-2.5 cursor-pointer hover:bg-accent/40 focus:bg-accent/40 focus:outline-none transition-colors"
|
|
138
|
+
>
|
|
139
|
+
<MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
140
|
+
<div className="flex-1 min-w-0">
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<p className="text-sm truncate">
|
|
143
|
+
{s.preview ?? (
|
|
144
|
+
<span className="text-muted-foreground italic">
|
|
145
|
+
(no user turns yet)
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</p>
|
|
149
|
+
<LearningsBadge
|
|
150
|
+
count={s.learning_interaction_count}
|
|
151
|
+
size="sm"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex items-center gap-3 text-[11px] text-muted-foreground mt-0.5">
|
|
155
|
+
<code className="font-mono">
|
|
156
|
+
{truncateId(s.session_id, 10, 6)}
|
|
157
|
+
</code>
|
|
158
|
+
<span>·</span>
|
|
159
|
+
<span className="tabular-nums">
|
|
160
|
+
{s.turn_count} turn{s.turn_count === 1 ? "" : "s"}
|
|
161
|
+
</span>
|
|
162
|
+
{s.published_up_to > 0 &&
|
|
163
|
+
s.published_up_to < s.turn_count && (
|
|
164
|
+
<>
|
|
165
|
+
<span>·</span>
|
|
166
|
+
<span className="tabular-nums">
|
|
167
|
+
{s.published_up_to} published
|
|
168
|
+
</span>
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="text-xs text-muted-foreground shrink-0 tabular-nums">
|
|
174
|
+
{formatRelative(s.last_activity)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|