agentfit 0.1.0 → 0.1.1

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 (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
@@ -0,0 +1,307 @@
1
+ 'use client'
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import {
6
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
7
+ } from '@/components/ui/table'
8
+ import {
9
+ Trophy, AlertTriangle, Lightbulb, Brain, Flame, Wrench, Clock,
10
+ MessageSquare, FolderOpen, LayoutList, DollarSign, Terminal,
11
+ Zap, Search, Ban, Calendar,
12
+ } from 'lucide-react'
13
+ import { formatCost, formatDuration, formatNumber, formatTokens } from '@/lib/format'
14
+ import type { ReportContent } from '@/lib/report'
15
+ import type { CoachInsight, InsightSeverity, InsightCategory } from '@/lib/coach'
16
+
17
+ // ─── Score Ring ──────────────────────────────────────────────────────
18
+
19
+ function ScoreRing({ score, size = 140 }: { score: number; size?: number }) {
20
+ const radius = (size - 14) / 2
21
+ const circumference = 2 * Math.PI * radius
22
+ const offset = circumference - (score / 100) * circumference
23
+ const getColor = (s: number) => {
24
+ if (s >= 85) return 'var(--chart-2)'
25
+ if (s >= 70) return 'var(--chart-1)'
26
+ if (s >= 55) return 'var(--chart-3)'
27
+ return 'var(--chart-5)'
28
+ }
29
+ return (
30
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
31
+ <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="var(--muted)" strokeWidth="9" />
32
+ <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={getColor(score)} strokeWidth="9"
33
+ strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset}
34
+ transform={`rotate(-90 ${size / 2} ${size / 2})`} />
35
+ <text x={size / 2} y={size / 2 - 4} textAnchor="middle" dominantBaseline="middle" className="fill-foreground font-bold" style={{ fontSize: size * 0.22 }}>{score}</text>
36
+ <text x={size / 2} y={size / 2 + 16} textAnchor="middle" className="fill-muted-foreground" style={{ fontSize: size * 0.09 }}>/ 100</text>
37
+ </svg>
38
+ )
39
+ }
40
+
41
+ // ─── Insight Card ────────────────────────────────────────────────────
42
+
43
+ const SEVERITY_BORDER: Record<InsightSeverity, string> = {
44
+ achievement: 'border-l-chart-2',
45
+ warning: 'border-l-chart-5',
46
+ tip: 'border-l-chart-1',
47
+ }
48
+
49
+ const SEVERITY_ICON: Record<InsightSeverity, typeof Trophy> = {
50
+ achievement: Trophy,
51
+ warning: AlertTriangle,
52
+ tip: Lightbulb,
53
+ }
54
+
55
+ function InsightCard({ insight }: { insight: CoachInsight }) {
56
+ const Icon = SEVERITY_ICON[insight.severity]
57
+ return (
58
+ <div className={`rounded-lg border border-l-4 ${SEVERITY_BORDER[insight.severity]} p-4`}>
59
+ <div className="flex items-start gap-3">
60
+ <Icon className="h-4 w-4 shrink-0 mt-0.5 text-muted-foreground" />
61
+ <div className="space-y-1">
62
+ <div className="text-sm font-semibold">{insight.title}</div>
63
+ <p className="text-xs text-muted-foreground">{insight.description}</p>
64
+ {insight.recommendation && (
65
+ <div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{insight.recommendation}</div>
66
+ )}
67
+ </div>
68
+ {insight.metric && (
69
+ <span className="shrink-0 rounded-md bg-muted px-2 py-0.5 text-xs font-mono">{insight.metric}</span>
70
+ )}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ // ─── Main Report View ────────────────────────────────────────────────
77
+
78
+ export function ReportView({ content }: { content: ReportContent }) {
79
+ const g = content.atAGlance
80
+
81
+ return (
82
+ <div className="space-y-6 max-w-4xl">
83
+ {/* At a Glance */}
84
+ <Card>
85
+ <CardHeader>
86
+ <CardTitle>At a Glance</CardTitle>
87
+ <CardDescription>{g.dateRange.from} to {g.dateRange.to}</CardDescription>
88
+ </CardHeader>
89
+ <CardContent>
90
+ <div className="flex items-center gap-8">
91
+ <ScoreRing score={g.fitnessScore} />
92
+ <div className="flex-1">
93
+ <div className="text-lg font-semibold mb-1">Fitness: {g.scoreLabel}</div>
94
+ <div className="flex items-center gap-2 text-sm text-muted-foreground mb-3">
95
+ <Flame className="h-3.5 w-3.5" />
96
+ {g.currentStreak > 0 ? `${g.currentStreak}d streak` : 'No active streak'}
97
+ {g.longestStreak > 0 && <span>(best: {g.longestStreak}d)</span>}
98
+ </div>
99
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 text-sm">
100
+ <div><span className="text-muted-foreground">Sessions</span><div className="font-semibold">{formatNumber(g.totalSessions)}</div></div>
101
+ <div><span className="text-muted-foreground">Projects</span><div className="font-semibold">{formatNumber(g.totalProjects)}</div></div>
102
+ <div><span className="text-muted-foreground">Messages</span><div className="font-semibold">{formatNumber(g.totalMessages)}</div></div>
103
+ <div><span className="text-muted-foreground">Total Time</span><div className="font-semibold">{formatDuration(g.totalDurationMinutes)}</div></div>
104
+ <div><span className="text-muted-foreground">Tool Calls</span><div className="font-semibold">{formatNumber(g.totalToolCalls)}</div></div>
105
+ <div><span className="text-muted-foreground">Total Cost</span><div className="font-semibold">{formatCost(g.totalCostUSD)}</div></div>
106
+ <div><span className="text-muted-foreground">API Errors</span><div className="font-semibold">{formatNumber(g.totalApiErrors)}</div></div>
107
+ <div><span className="text-muted-foreground">Interruptions</span><div className="font-semibold">{formatNumber(g.totalUserInterruptions)}</div></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </CardContent>
112
+ </Card>
113
+
114
+ {/* Project Areas */}
115
+ {content.projectAreas.length > 0 && (
116
+ <Card>
117
+ <CardHeader>
118
+ <div className="flex items-center gap-2">
119
+ <FolderOpen className="h-5 w-5 text-muted-foreground" />
120
+ <div>
121
+ <CardTitle>What You Work On</CardTitle>
122
+ <CardDescription>Top projects by session count</CardDescription>
123
+ </div>
124
+ </div>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <Table>
128
+ <TableHeader>
129
+ <TableRow>
130
+ <TableHead>Project</TableHead>
131
+ <TableHead className="text-right">Sessions</TableHead>
132
+ <TableHead className="text-right">Messages</TableHead>
133
+ <TableHead className="text-right">Duration</TableHead>
134
+ <TableHead>Top Tools</TableHead>
135
+ </TableRow>
136
+ </TableHeader>
137
+ <TableBody>
138
+ {content.projectAreas.map(p => (
139
+ <TableRow key={p.name}>
140
+ <TableCell className="font-medium">{p.name}</TableCell>
141
+ <TableCell className="text-right">{p.sessions}</TableCell>
142
+ <TableCell className="text-right">{formatNumber(p.totalMessages)}</TableCell>
143
+ <TableCell className="text-right">{formatDuration(p.totalDurationMinutes)}</TableCell>
144
+ <TableCell className="text-xs text-muted-foreground">
145
+ {p.topTools.map(([name, count]) => `${name} (${count})`).join(', ')}
146
+ </TableCell>
147
+ </TableRow>
148
+ ))}
149
+ </TableBody>
150
+ </Table>
151
+ </CardContent>
152
+ </Card>
153
+ )}
154
+
155
+ {/* Interaction Style */}
156
+ <Card>
157
+ <CardHeader>
158
+ <div className="flex items-center gap-2">
159
+ <Brain className="h-5 w-5 text-muted-foreground" />
160
+ <div>
161
+ <CardTitle>How You Use Your Agent</CardTitle>
162
+ <CardDescription>Interaction patterns and coding style</CardDescription>
163
+ </div>
164
+ </div>
165
+ </CardHeader>
166
+ <CardContent>
167
+ <div className="flex items-start gap-6 mb-4">
168
+ <div>
169
+ <div className="text-3xl font-bold tracking-wider text-primary">{content.interactionStyle.mbtiType}</div>
170
+ <div className="text-xs text-muted-foreground mt-1">{content.interactionStyle.mbtiDescription}</div>
171
+ </div>
172
+ </div>
173
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 text-sm">
174
+ <div>
175
+ <span className="text-muted-foreground">Avg Messages/Session</span>
176
+ <div className="font-semibold">{Math.round(content.interactionStyle.avgMessagesPerSession)}</div>
177
+ </div>
178
+ <div>
179
+ <span className="text-muted-foreground">Avg Duration</span>
180
+ <div className="font-semibold">{formatDuration(content.interactionStyle.avgDurationMinutes)}</div>
181
+ </div>
182
+ <div>
183
+ <span className="text-muted-foreground">Read/Edit Ratio</span>
184
+ <div className="font-semibold">{content.interactionStyle.readEditRatio.toFixed(1)}x</div>
185
+ </div>
186
+ <div>
187
+ <span className="text-muted-foreground">Bash Usage</span>
188
+ <div className="font-semibold">{Math.round(content.interactionStyle.bashRatio * 100)}%</div>
189
+ </div>
190
+ <div>
191
+ <span className="text-muted-foreground">Subagent Calls</span>
192
+ <div className="font-semibold">{formatNumber(content.interactionStyle.agentCallsTotal)}</div>
193
+ </div>
194
+ <div>
195
+ <span className="text-muted-foreground">Peak Hour</span>
196
+ <div className="font-semibold">{content.interactionStyle.peakHour}:00</div>
197
+ </div>
198
+ <div>
199
+ <span className="text-muted-foreground">Most Active Day</span>
200
+ <div className="font-semibold">{content.interactionStyle.mostActiveDay}</div>
201
+ </div>
202
+ <div>
203
+ <span className="text-muted-foreground">Avg Cost/Session</span>
204
+ <div className="font-semibold">{formatCost(content.interactionStyle.avgCostPerSession)}</div>
205
+ </div>
206
+ </div>
207
+ </CardContent>
208
+ </Card>
209
+
210
+ {/* What Works */}
211
+ {content.whatWorks.length > 0 && (
212
+ <Card>
213
+ <CardHeader>
214
+ <div className="flex items-center gap-2">
215
+ <Trophy className="h-5 w-5 text-chart-2" />
216
+ <div>
217
+ <CardTitle>What's Working</CardTitle>
218
+ <CardDescription>Patterns and habits that are paying off</CardDescription>
219
+ </div>
220
+ </div>
221
+ </CardHeader>
222
+ <CardContent className="space-y-3">
223
+ {content.whatWorks.map(i => <InsightCard key={i.id} insight={i} />)}
224
+ </CardContent>
225
+ </Card>
226
+ )}
227
+
228
+ {/* Friction Analysis */}
229
+ {content.frictionAnalysis.length > 0 && (
230
+ <Card>
231
+ <CardHeader>
232
+ <div className="flex items-center gap-2">
233
+ <AlertTriangle className="h-5 w-5 text-chart-5" />
234
+ <div>
235
+ <CardTitle>Where Things Go Wrong</CardTitle>
236
+ <CardDescription>Friction points that slow you down</CardDescription>
237
+ </div>
238
+ </div>
239
+ </CardHeader>
240
+ <CardContent className="space-y-3">
241
+ {content.frictionAnalysis.map(i => <InsightCard key={i.id} insight={i} />)}
242
+ </CardContent>
243
+ </Card>
244
+ )}
245
+
246
+ {/* Suggestions */}
247
+ <Card>
248
+ <CardHeader>
249
+ <div className="flex items-center gap-2">
250
+ <Lightbulb className="h-5 w-5 text-chart-1" />
251
+ <div>
252
+ <CardTitle>Suggestions</CardTitle>
253
+ <CardDescription>Actionable ways to improve your workflow</CardDescription>
254
+ </div>
255
+ </div>
256
+ </CardHeader>
257
+ <CardContent className="space-y-6">
258
+ {/* CLAUDE.md rules */}
259
+ {content.suggestions.claudeMdRules.length > 0 && (
260
+ <div>
261
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
262
+ <Terminal className="h-4 w-4" /> Add to CLAUDE.md
263
+ </h4>
264
+ <div className="space-y-2">
265
+ {content.suggestions.claudeMdRules.map((rule, i) => (
266
+ <div key={i} className="rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap">{rule}</div>
267
+ ))}
268
+ </div>
269
+ </div>
270
+ )}
271
+
272
+ {/* Coach tips */}
273
+ {content.suggestions.coachTips.length > 0 && (
274
+ <div>
275
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
276
+ <Zap className="h-4 w-4" /> Workflow Tips
277
+ </h4>
278
+ <div className="space-y-3">
279
+ {content.suggestions.coachTips.map(i => <InsightCard key={i.id} insight={i} />)}
280
+ </div>
281
+ </div>
282
+ )}
283
+
284
+ {/* Command tips */}
285
+ {content.suggestions.commandTips.length > 0 && (
286
+ <div>
287
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
288
+ <Search className="h-4 w-4" /> Commands to Discover
289
+ </h4>
290
+ <div className="space-y-2">
291
+ {content.suggestions.commandTips.map(i => (
292
+ <div key={i.id} className="rounded-lg border p-3">
293
+ <div className="text-sm font-semibold">{i.title}</div>
294
+ <p className="text-xs text-muted-foreground mt-0.5">{i.description}</p>
295
+ {i.recommendation && (
296
+ <div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{i.recommendation}</div>
297
+ )}
298
+ </div>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ )}
303
+ </CardContent>
304
+ </Card>
305
+ </div>
306
+ )
307
+ }
@@ -12,6 +12,8 @@ import {
12
12
  } from '@/components/ui/chart'
13
13
  import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
14
14
  import { Badge } from '@/components/ui/badge'
15
+ import { PaginationControls } from '@/components/pagination-controls'
16
+ import { usePagination } from '@/hooks/use-pagination'
15
17
  import { formatCost, formatNumber } from '@/lib/format'
16
18
  import {
17
19
  Camera,
@@ -21,6 +23,14 @@ import {
21
23
  Image as ImageIcon,
22
24
  } from 'lucide-react'
23
25
 
26
+ interface ImageInfo {
27
+ filename: string
28
+ sessionId: string
29
+ timestamp: string
30
+ sizeBytes: number
31
+ project: string
32
+ }
33
+
24
34
  interface ImageAnalysis {
25
35
  overview: {
26
36
  totalImages: number
@@ -55,13 +65,7 @@ interface ImageAnalysis {
55
65
  cost: number
56
66
  date: string
57
67
  }[]
58
- recentImages: {
59
- filename: string
60
- sessionId: string
61
- timestamp: string
62
- sizeBytes: number
63
- project: string
64
- }[]
68
+ allImages: ImageInfo[]
65
69
  }
66
70
 
67
71
  const projectConfig = {
@@ -315,45 +319,46 @@ export function ScreenshotsAnalysis() {
315
319
  </CardContent>
316
320
  </Card>
317
321
 
318
- {/* Recent images gallery */}
319
- <Card>
320
- <CardHeader>
321
- <CardTitle>Recent Images</CardTitle>
322
- <CardDescription>Latest images from your sessions</CardDescription>
323
- </CardHeader>
324
- <CardContent>
325
- <div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
326
- {data.recentImages.map((img) => (
327
- <div
328
- key={img.filename}
329
- className="group relative mb-4 break-inside-avoid overflow-hidden rounded-xl border bg-card shadow-sm transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5"
330
- >
331
- <div className="relative">
332
- <img
333
- src={`/api/images/${img.filename}`}
334
- alt={`Image from ${img.project}`}
335
- className="w-full object-cover object-top"
336
- loading="lazy"
337
- />
338
- {/* Hover overlay */}
339
- <div className="absolute inset-0 bg-black/0 transition-colors duration-300 group-hover:bg-black/5" />
340
- {/* Project badge — always visible */}
341
- <div className="absolute top-2.5 left-2.5">
342
- <span className="inline-flex items-center rounded-md bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white backdrop-blur-sm">
343
- {img.project}
344
- </span>
345
- </div>
346
- </div>
347
- {/* Metadata footer */}
348
- <div className="flex items-center justify-between px-3 py-2.5 text-[11px] text-muted-foreground">
349
- <span>{new Date(img.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</span>
350
- <span>{Math.round(img.sizeBytes / 1024)} KB</span>
351
- </div>
352
- </div>
353
- ))}
354
- </div>
355
- </CardContent>
356
- </Card>
322
+ {/* All images grid with pagination */}
323
+ <ImageGallery images={data.allImages} />
357
324
  </div>
358
325
  )
359
326
  }
327
+
328
+ function ImageGallery({ images }: { images: ImageInfo[] }) {
329
+ const pagination = usePagination(images, 16)
330
+
331
+ if (images.length === 0) return null
332
+
333
+ return (
334
+ <Card>
335
+ <CardHeader>
336
+ <CardTitle>All Images</CardTitle>
337
+ <CardDescription>{images.length} images across all sessions</CardDescription>
338
+ </CardHeader>
339
+ <CardContent>
340
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
341
+ {pagination.pageItems.map((img) => (
342
+ <div
343
+ key={img.filename}
344
+ className="group relative overflow-hidden rounded-xl border bg-card shadow-sm transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5"
345
+ >
346
+ <img
347
+ src={`/api/images/${img.filename}`}
348
+ alt={`Image from ${img.project}`}
349
+ className="aspect-video w-full object-cover object-top"
350
+ loading="lazy"
351
+ />
352
+ <div className="flex items-center justify-between px-3 py-2 text-[11px] text-muted-foreground">
353
+ <span className="font-medium text-foreground">{img.project}</span>
354
+ <span>{new Date(img.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</span>
355
+ <span>{Math.round(img.sizeBytes / 1024)} KB</span>
356
+ </div>
357
+ </div>
358
+ ))}
359
+ </div>
360
+ <PaginationControls pagination={pagination} noun="images" />
361
+ </CardContent>
362
+ </Card>
363
+ )
364
+ }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { User, Brain, Wrench, CheckCircle, XCircle, Settings, ChevronDown, ChevronRight, Image } from 'lucide-react'
5
+ import type { ChatMessage } from '@/lib/session-detail'
6
+
7
+ const ROLE_CONFIG: Record<string, { color: string; bg: string; icon: typeof User; label: string }> = {
8
+ user: { color: '#3b82f6', bg: '#eff6ff', icon: User, label: 'User Input' },
9
+ assistant: { color: '#8b5cf6', bg: '#faf5ff', icon: Brain, label: 'Assistant' },
10
+ tool_result: { color: '#22c55e', bg: '#f0fdf4', icon: CheckCircle, label: 'Tool Result' },
11
+ system: { color: '#94a3b8', bg: '#f8fafc', icon: Settings, label: 'System' },
12
+ }
13
+
14
+ function ChatEntry({ message, index }: { message: ChatMessage; index: number }) {
15
+ const [expanded, setExpanded] = useState(false)
16
+ const config = ROLE_CONFIG[message.role] || ROLE_CONFIG.system
17
+ const Icon = message.toolName ? Wrench : (message.isThinking ? Brain : config.icon)
18
+ const isLong = message.content.length > 150
19
+
20
+ let label = config.label
21
+ if (message.toolName) label = message.toolName
22
+ else if (message.isThinking) label = 'Thinking'
23
+ else if (message.role === 'tool_result' && message.content.includes('error')) {
24
+ label = 'Error'
25
+ }
26
+
27
+ return (
28
+ <div
29
+ className="flex gap-3 py-2"
30
+ style={{ borderLeft: `3px solid ${config.color}`, paddingLeft: 12 }}
31
+ >
32
+ <div className="shrink-0 mt-0.5">
33
+ <div
34
+ className="flex h-6 w-6 items-center justify-center rounded-full"
35
+ style={{ backgroundColor: config.bg }}
36
+ >
37
+ <Icon className="h-3.5 w-3.5" style={{ color: config.color }} />
38
+ </div>
39
+ </div>
40
+ <div className="flex-1 min-w-0">
41
+ <div className="flex items-center gap-2 mb-0.5">
42
+ <span className="text-xs font-semibold" style={{ color: config.color }}>
43
+ #{message.stepIndex} {label}
44
+ </span>
45
+ {message.isSidechain && (
46
+ <span className="text-[10px] bg-muted px-1.5 py-0.5 rounded">sub-agent</span>
47
+ )}
48
+ {message.images && message.images > 0 && (
49
+ <span className="flex items-center gap-0.5 text-[10px] text-muted-foreground">
50
+ <Image className="h-3 w-3" /> {message.images}
51
+ </span>
52
+ )}
53
+ <span className="text-[10px] text-muted-foreground">
54
+ {message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : ''}
55
+ </span>
56
+ </div>
57
+ <div
58
+ className={`text-xs leading-relaxed text-foreground/80 ${
59
+ !expanded && isLong ? 'line-clamp-3' : ''
60
+ }`}
61
+ style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
62
+ >
63
+ {message.content}
64
+ </div>
65
+ {isLong && (
66
+ <button
67
+ onClick={() => setExpanded(!expanded)}
68
+ className="flex items-center gap-0.5 mt-1 text-[11px] text-primary hover:underline"
69
+ >
70
+ {expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
71
+ {expanded ? 'Show less' : 'Show more'}
72
+ </button>
73
+ )}
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export function SessionChatLog({ messages }: { messages: ChatMessage[] }) {
80
+ const [filter, setFilter] = useState<string>('all')
81
+
82
+ const filtered = filter === 'all'
83
+ ? messages
84
+ : messages.filter(m => m.role === filter || (filter === 'tool' && (m.toolName || m.role === 'tool_result')))
85
+
86
+ return (
87
+ <div className="space-y-2">
88
+ {/* Filter buttons */}
89
+ <div className="flex gap-1.5 pb-2 border-b">
90
+ {[
91
+ { key: 'all', label: 'All', count: messages.length },
92
+ { key: 'user', label: 'User', count: messages.filter(m => m.role === 'user').length },
93
+ { key: 'assistant', label: 'Assistant', count: messages.filter(m => m.role === 'assistant' && !m.toolName).length },
94
+ { key: 'tool', label: 'Tools', count: messages.filter(m => m.toolName || m.role === 'tool_result').length },
95
+ ].map(f => (
96
+ <button
97
+ key={f.key}
98
+ onClick={() => setFilter(f.key)}
99
+ className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
100
+ filter === f.key
101
+ ? 'bg-primary text-primary-foreground'
102
+ : 'bg-muted text-muted-foreground hover:text-foreground'
103
+ }`}
104
+ >
105
+ {f.label} ({f.count})
106
+ </button>
107
+ ))}
108
+ </div>
109
+
110
+ {/* Messages */}
111
+ <div className="space-y-0.5 max-h-[600px] overflow-y-auto pr-2">
112
+ {filtered.map((msg, i) => (
113
+ <ChatEntry key={msg.id} message={msg} index={i} />
114
+ ))}
115
+ </div>
116
+
117
+ {messages.length > 200 && (
118
+ <div className="text-xs text-muted-foreground text-center pt-2">
119
+ Showing {filtered.length} of {messages.length} messages
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }