claude-smart 0.2.23 → 0.2.25

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 +76 -28
  3. package/bin/claude-smart.js +355 -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 +1342 -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,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
+ }