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
@@ -1,6 +1,20 @@
1
1
  'use client'
2
2
 
3
- import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts'
3
+ import { useMemo, useState, useEffect } from 'react'
4
+ import {
5
+ Line,
6
+ LineChart,
7
+ Bar,
8
+ BarChart,
9
+ Area,
10
+ AreaChart,
11
+ CartesianGrid,
12
+ XAxis,
13
+ YAxis,
14
+ Legend,
15
+ Cell,
16
+ Tooltip as RechartsTooltip,
17
+ } from 'recharts'
4
18
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
19
  import {
6
20
  ChartContainer,
@@ -8,111 +22,418 @@ import {
8
22
  ChartTooltipContent,
9
23
  type ChartConfig,
10
24
  } from '@/components/ui/chart'
11
- import type { DailyUsage } from '@/lib/parse-logs'
25
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
26
+ import type { DailyUsage, SessionSummary } from '@/lib/parse-logs'
12
27
  import { formatCost } from '@/lib/format'
13
28
 
29
+ // ─── 1. Daily Cost ──────────────────────────────────────────────────
30
+
14
31
  const costConfig = {
15
32
  cost: { label: 'Cost', color: 'var(--chart-5)' },
16
33
  } satisfies ChartConfig
17
34
 
18
- const sessionsConfig = {
19
- sessions: { label: 'Sessions', color: 'var(--chart-2)' },
35
+ export function DailyCostChart({ daily }: { daily: DailyUsage[] }) {
36
+ const data = daily.map((d) => ({
37
+ date: d.date.slice(5),
38
+ cost: Number(d.costUSD.toFixed(2)),
39
+ }))
40
+
41
+ return (
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle>Daily Cost</CardTitle>
45
+ <CardDescription>USD spent per day</CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <ChartContainer config={costConfig} className="min-h-[250px] w-full">
49
+ <LineChart data={data} accessibilityLayer>
50
+ <CartesianGrid vertical={false} />
51
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
52
+ <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `$${v}`} />
53
+ <ChartTooltip
54
+ content={<ChartTooltipContent formatter={(value) => formatCost(Number(value))} />}
55
+ />
56
+ <Line dataKey="cost" type="monotone" stroke="var(--color-cost)" strokeWidth={2} dot={{ r: 3, fill: 'var(--color-cost)' }} activeDot={{ r: 5 }} />
57
+ </LineChart>
58
+ </ChartContainer>
59
+ </CardContent>
60
+ </Card>
61
+ )
62
+ }
63
+
64
+ // ─── 2. Tool Mix Over Time ──────────────────────────────────────────
65
+
66
+ const TOP_TOOLS = ['Read', 'Edit', 'Write', 'Bash', 'Grep', 'Agent'] as const
67
+ const toolMixConfig = {
68
+ Read: { label: 'Read', color: 'var(--chart-1)' },
69
+ Edit: { label: 'Edit', color: 'var(--chart-2)' },
70
+ Write: { label: 'Write', color: 'var(--chart-3)' },
71
+ Bash: { label: 'Bash', color: 'var(--chart-4)' },
72
+ Grep: { label: 'Grep', color: 'var(--chart-6)' },
73
+ Agent: { label: 'Agent', color: 'var(--chart-7)' },
20
74
  } satisfies ChartConfig
21
75
 
22
- const messagesConfig = {
23
- messages: { label: 'Messages', color: 'var(--chart-1)' },
76
+ export function ToolMixChart({ daily }: { daily: DailyUsage[] }) {
77
+ const data = daily.map((d) => {
78
+ const row: Record<string, string | number> = { date: d.date.slice(5) }
79
+ for (const tool of TOP_TOOLS) {
80
+ row[tool] = d.toolCallsDetail[tool] || 0
81
+ }
82
+ return row
83
+ })
84
+
85
+ return (
86
+ <Card>
87
+ <CardHeader>
88
+ <CardTitle>Tool Mix Over Time</CardTitle>
89
+ <CardDescription>Daily tool calls — explore (Read/Grep) vs build (Edit/Write)</CardDescription>
90
+ </CardHeader>
91
+ <CardContent>
92
+ <ChartContainer config={toolMixConfig} className="min-h-[250px] w-full">
93
+ <AreaChart data={data} accessibilityLayer>
94
+ <CartesianGrid vertical={false} />
95
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
96
+ <YAxis tickLine={false} axisLine={false} />
97
+ <ChartTooltip content={<ChartTooltipContent />} />
98
+ {TOP_TOOLS.map((tool) => (
99
+ <Area
100
+ key={tool}
101
+ dataKey={tool}
102
+ type="monotone"
103
+ stackId="1"
104
+ stroke={`var(--color-${tool})`}
105
+ fill={`var(--color-${tool})`}
106
+ fillOpacity={0.4}
107
+ />
108
+ ))}
109
+ </AreaChart>
110
+ </ChartContainer>
111
+ </CardContent>
112
+ </Card>
113
+ )
114
+ }
115
+
116
+ // ─── 3. Interruption Rate Trend ─────────────────────────────────────
117
+
118
+ const interruptionConfig = {
119
+ rate: { label: 'Interruption Rate', color: 'var(--chart-5)' },
24
120
  } satisfies ChartConfig
25
121
 
26
- export function DailyChart({ daily }: { daily: DailyUsage[] }) {
122
+ export function InterruptionRateChart({ daily }: { daily: DailyUsage[] }) {
27
123
  const data = daily.map((d) => ({
28
124
  date: d.date.slice(5),
29
- cost: Number(d.costUSD.toFixed(2)),
30
- sessions: d.sessions,
31
- messages: d.messages,
125
+ rate: d.messages > 0 ? Number(((d.interruptions / d.messages) * 100).toFixed(1)) : 0,
126
+ interruptions: d.interruptions,
32
127
  }))
33
128
 
34
129
  return (
35
- <>
36
- <Card>
37
- <CardHeader>
38
- <CardTitle>Daily Cost</CardTitle>
39
- <CardDescription>USD spent per day</CardDescription>
40
- </CardHeader>
41
- <CardContent>
42
- <ChartContainer config={costConfig} className="min-h-[250px] w-full">
43
- <LineChart data={data} accessibilityLayer>
44
- <CartesianGrid vertical={false} />
45
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
46
- <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `$${v}`} />
47
- <ChartTooltip
48
- content={
49
- <ChartTooltipContent
50
- formatter={(value) => formatCost(Number(value))}
51
- />
52
- }
53
- />
54
- <Line
55
- dataKey="cost"
56
- type="monotone"
57
- stroke="var(--color-cost)"
58
- strokeWidth={2}
59
- dot={{ r: 3, fill: 'var(--color-cost)' }}
60
- activeDot={{ r: 5 }}
61
- />
62
- </LineChart>
63
- </ChartContainer>
64
- </CardContent>
65
- </Card>
130
+ <Card>
131
+ <CardHeader>
132
+ <CardTitle>Interruption Rate</CardTitle>
133
+ <CardDescription>Daily interruptions as % of messages — lower is better</CardDescription>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <ChartContainer config={interruptionConfig} className="min-h-[250px] w-full">
137
+ <LineChart data={data} accessibilityLayer>
138
+ <CartesianGrid vertical={false} />
139
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
140
+ <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
141
+ <ChartTooltip
142
+ content={
143
+ <ChartTooltipContent
144
+ formatter={(value, name) =>
145
+ name === 'rate' ? `${value}%` : String(value)
146
+ }
147
+ />
148
+ }
149
+ />
150
+ <Line dataKey="rate" type="monotone" stroke="var(--color-rate)" strokeWidth={2} dot={{ r: 3, fill: 'var(--color-rate)' }} activeDot={{ r: 5 }} />
151
+ </LineChart>
152
+ </ChartContainer>
153
+ </CardContent>
154
+ </Card>
155
+ )
156
+ }
66
157
 
67
- <Card>
68
- <CardHeader>
69
- <CardTitle>Daily Sessions</CardTitle>
70
- <CardDescription>Number of sessions per day</CardDescription>
71
- </CardHeader>
72
- <CardContent>
73
- <ChartContainer config={sessionsConfig} className="min-h-[250px] w-full">
74
- <LineChart data={data} accessibilityLayer>
75
- <CartesianGrid vertical={false} />
76
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
77
- <YAxis tickLine={false} axisLine={false} />
78
- <ChartTooltip content={<ChartTooltipContent />} />
79
- <Line
80
- dataKey="sessions"
81
- type="monotone"
82
- stroke="var(--color-sessions)"
83
- strokeWidth={2}
84
- dot={{ r: 3, fill: 'var(--color-sessions)' }}
85
- activeDot={{ r: 5 }}
86
- />
87
- </LineChart>
88
- </ChartContainer>
89
- </CardContent>
90
- </Card>
158
+ // ─── 4. Top Skills Used ─────────────────────────────────────────────
91
159
 
160
+ const commandConfig = {
161
+ count: { label: 'Uses', color: 'var(--chart-1)' },
162
+ } satisfies ChartConfig
163
+
164
+ export function TopCommandsChart() {
165
+ const [data, setData] = useState<{ name: string; count: number; fill: string }[]>([])
166
+
167
+ useEffect(() => {
168
+ fetch('/api/commands')
169
+ .then(r => r.json())
170
+ .then(analysis => {
171
+ const bi = analysis.commands
172
+ .filter((c: { used: boolean; count: number }) => c.used && c.count > 0)
173
+ .map((c: { command: string; count: number }) => ({ name: c.command, count: c.count, fill: 'var(--chart-1)' }))
174
+ .sort((a: { count: number }, b: { count: number }) => b.count - a.count)
175
+ .slice(0, 5)
176
+ const cu = analysis.customCommands
177
+ .sort((a: { count: number }, b: { count: number }) => b.count - a.count)
178
+ .slice(0, 5)
179
+ .map((c: { command: string; count: number }) => ({ name: c.command, count: c.count, fill: 'var(--chart-4)' }))
180
+ const combined = [...bi, ...cu].sort((a, b) => b.count - a.count)
181
+ setData(combined)
182
+ })
183
+ .catch(() => {})
184
+ }, [])
185
+
186
+ if (data.length === 0) {
187
+ return (
92
188
  <Card>
93
189
  <CardHeader>
94
- <CardTitle>Daily Messages</CardTitle>
95
- <CardDescription>Messages exchanged per day</CardDescription>
190
+ <CardTitle>Top Commands & Skills</CardTitle>
191
+ <CardDescription>Most used built-in commands and custom skills</CardDescription>
96
192
  </CardHeader>
97
193
  <CardContent>
98
- <ChartContainer config={messagesConfig} className="min-h-[250px] w-full">
99
- <LineChart data={data} accessibilityLayer>
100
- <CartesianGrid vertical={false} />
101
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
102
- <YAxis tickLine={false} axisLine={false} />
103
- <ChartTooltip content={<ChartTooltipContent />} />
104
- <Line
105
- dataKey="messages"
106
- type="monotone"
107
- stroke="var(--color-messages)"
108
- strokeWidth={2}
109
- dot={{ r: 3, fill: 'var(--color-messages)' }}
110
- activeDot={{ r: 5 }}
111
- />
112
- </LineChart>
113
- </ChartContainer>
194
+ <div className="flex min-h-[250px] items-center justify-center text-sm text-muted-foreground">
195
+ No commands used yet
196
+ </div>
114
197
  </CardContent>
115
198
  </Card>
199
+ )
200
+ }
201
+
202
+ return (
203
+ <Card>
204
+ <CardHeader>
205
+ <CardTitle>Top Commands & Skills</CardTitle>
206
+ <CardDescription>
207
+ <span className="inline-flex items-center gap-1.5"><span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ backgroundColor: 'var(--chart-1)' }} /> Built-in</span>
208
+ <span className="inline-flex items-center gap-1.5 ml-3"><span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ backgroundColor: 'var(--chart-4)' }} /> Custom skill</span>
209
+ </CardDescription>
210
+ </CardHeader>
211
+ <CardContent>
212
+ <ChartContainer config={commandConfig} className="w-full" style={{ minHeight: Math.max(200, data.length * 36) }}>
213
+ <BarChart data={data} layout="vertical" margin={{ left: 8 }} accessibilityLayer>
214
+ <XAxis type="number" tickLine={false} axisLine={false} />
215
+ <YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={140} />
216
+ <ChartTooltip content={<ChartTooltipContent />} />
217
+ <Bar dataKey="count" radius={[0, 4, 4, 0]}>
218
+ {data.map((entry, i) => (
219
+ <Cell key={i} fill={entry.fill} />
220
+ ))}
221
+ </Bar>
222
+ </BarChart>
223
+ </ChartContainer>
224
+ </CardContent>
225
+ </Card>
226
+ )
227
+ }
228
+
229
+ // ─── 5. User vs Assistant Messages ──────────────────────────────────
230
+
231
+ const msgRatioConfig = {
232
+ userMessages: { label: 'User', color: 'var(--chart-1)' },
233
+ assistantMessages: { label: 'Assistant', color: 'var(--chart-3)' },
234
+ } satisfies ChartConfig
235
+
236
+ export function UserVsAssistantChart({ daily }: { daily: DailyUsage[] }) {
237
+ const data = daily.map((d) => ({
238
+ date: d.date.slice(5),
239
+ userMessages: d.userMessages,
240
+ assistantMessages: d.assistantMessages,
241
+ }))
242
+
243
+ return (
244
+ <Card>
245
+ <CardHeader>
246
+ <CardTitle>User vs Assistant Messages</CardTitle>
247
+ <CardDescription>Low user ratio = Claude doing more autonomous turns</CardDescription>
248
+ </CardHeader>
249
+ <CardContent>
250
+ <ChartContainer config={msgRatioConfig} className="min-h-[250px] w-full">
251
+ <BarChart data={data} accessibilityLayer>
252
+ <CartesianGrid vertical={false} />
253
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
254
+ <YAxis tickLine={false} axisLine={false} />
255
+ <ChartTooltip content={<ChartTooltipContent />} />
256
+ <Bar dataKey="userMessages" stackId="1" fill="var(--color-userMessages)" radius={[0, 0, 0, 0]} />
257
+ <Bar dataKey="assistantMessages" stackId="1" fill="var(--color-assistantMessages)" radius={[4, 4, 0, 0]} />
258
+ </BarChart>
259
+ </ChartContainer>
260
+ </CardContent>
261
+ </Card>
262
+ )
263
+ }
264
+
265
+ // ─── 6. Token Usage Heatmap ─────────────────────────────────────────
266
+
267
+ const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
268
+
269
+ function formatTokensShort(n: number): string {
270
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
271
+ if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`
272
+ return String(n)
273
+ }
274
+
275
+ function formatDateLabel(dateStr: string): string {
276
+ const d = new Date(dateStr + 'T00:00:00')
277
+ const month = MONTH_NAMES[d.getMonth()]
278
+ const day = d.getDate()
279
+ const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th'
280
+ return `${month} ${day}${suffix}`
281
+ }
282
+
283
+ export function TokenUsageHeatmap({ daily }: { daily: DailyUsage[] }) {
284
+ const { tokensByDate, rateLimitByDate, maxTokens, totalTokens } = useMemo(() => {
285
+ const tMap = new Map<string, number>()
286
+ const rMap = new Map<string, number>()
287
+ let max = 0
288
+ let total = 0
289
+ for (const d of daily) {
290
+ tMap.set(d.date, d.totalTokens)
291
+ total += d.totalTokens
292
+ if (d.totalTokens > max) max = d.totalTokens
293
+ if (d.rateLimitErrors > 0) rMap.set(d.date, d.rateLimitErrors)
294
+ }
295
+ return { tokensByDate: tMap, rateLimitByDate: rMap, maxTokens: max, totalTokens: total }
296
+ }, [daily])
297
+
298
+ const { weeks, monthLabels } = useMemo(() => {
299
+ if (daily.length === 0) return { weeks: [], monthLabels: [] }
300
+ const lastDate = new Date(daily[daily.length - 1].date)
301
+ const result: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[][] = []
302
+ const startDate = new Date(lastDate)
303
+ startDate.setDate(startDate.getDate() - 83)
304
+ // Align to Sunday
305
+ startDate.setDate(startDate.getDate() - startDate.getDay())
306
+
307
+ let currentWeek: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[] = []
308
+ const d = new Date(startDate)
309
+ while (d <= lastDate) {
310
+ const dateStr = d.toLocaleDateString('en-CA')
311
+ currentWeek.push({
312
+ date: dateStr,
313
+ tokens: tokensByDate.get(dateStr) || 0,
314
+ rateLimits: rateLimitByDate.get(dateStr) || 0,
315
+ dayOfWeek: d.getDay(),
316
+ })
317
+ if (d.getDay() === 6) {
318
+ result.push(currentWeek)
319
+ currentWeek = []
320
+ }
321
+ d.setDate(d.getDate() + 1)
322
+ }
323
+ if (currentWeek.length > 0) result.push(currentWeek)
324
+
325
+ // Compute month labels with column positions
326
+ const labels: { label: string; col: number }[] = []
327
+ let lastMonth = -1
328
+ for (let wi = 0; wi < result.length; wi++) {
329
+ const firstDay = result[wi][0]
330
+ if (!firstDay) continue
331
+ const month = new Date(firstDay.date).getMonth()
332
+ if (month !== lastMonth) {
333
+ labels.push({ label: MONTH_NAMES[month], col: wi })
334
+ lastMonth = month
335
+ }
336
+ }
337
+
338
+ return { weeks: result, monthLabels: labels }
339
+ }, [daily, tokensByDate, rateLimitByDate])
340
+
341
+ const totalRateLimitDays = rateLimitByDate.size
342
+
343
+ function getIntensity(tokens: number): string {
344
+ if (tokens === 0) return 'bg-muted'
345
+ const ratio = tokens / maxTokens
346
+ if (ratio <= 0.25) return 'bg-blue-200 dark:bg-blue-900/50'
347
+ if (ratio <= 0.5) return 'bg-blue-300 dark:bg-blue-700/60'
348
+ if (ratio <= 0.75) return 'bg-blue-400 dark:bg-blue-600/70'
349
+ return 'bg-blue-500 dark:bg-blue-500'
350
+ }
351
+
352
+ return (
353
+ <Card>
354
+ <CardHeader>
355
+ <CardTitle>
356
+ {formatTokensShort(totalTokens)} tokens in the last {daily.length} days
357
+ </CardTitle>
358
+ <CardDescription>
359
+ Daily token volume
360
+ {totalRateLimitDays > 0 && (
361
+ <span className="ml-1">
362
+ · <span className="inline-block h-2.5 w-2.5 rounded-sm ring-2 ring-rose-400 ring-inset bg-blue-300 align-middle" /> = rate limited ({totalRateLimitDays}d)
363
+ </span>
364
+ )}
365
+ </CardDescription>
366
+ </CardHeader>
367
+ <CardContent>
368
+ <TooltipProvider>
369
+ <div className="overflow-x-auto">
370
+ <div className="flex w-full gap-[3px]">
371
+ {/* Day labels column — rendered as a matching grid so heights stay in sync */}
372
+ <div className="grid shrink-0 grid-rows-[16px_repeat(7,1fr)] gap-[3px] pr-1 text-[10px] text-muted-foreground">
373
+ <div />
374
+ <div className="flex items-center">Sun</div>
375
+ <div className="flex items-center">Mon</div>
376
+ <div className="flex items-center">Tue</div>
377
+ <div className="flex items-center">Wed</div>
378
+ <div className="flex items-center">Thu</div>
379
+ <div className="flex items-center">Fri</div>
380
+ <div className="flex items-center">Sat</div>
381
+ </div>
382
+ {/* Week columns */}
383
+ {weeks.map((week, wi) => {
384
+ const monthLabel = monthLabels.find((m) => m.col === wi)
385
+ return (
386
+ <div key={wi} className="grid flex-1 grid-rows-[16px_repeat(7,1fr)] gap-[3px]">
387
+ <div className="text-[10px] text-muted-foreground leading-4 truncate">
388
+ {monthLabel?.label ?? ''}
389
+ </div>
390
+ {Array.from({ length: 7 }, (_, dayIdx) => {
391
+ const cell = week.find((c) => c.dayOfWeek === dayIdx)
392
+ if (!cell) return <div key={dayIdx} className="aspect-square w-full" />
393
+ const hasRateLimit = cell.rateLimits > 0
394
+ const label = cell.tokens > 0
395
+ ? `${formatTokensShort(cell.tokens)} tokens on ${formatDateLabel(cell.date)}.${hasRateLimit ? ` Rate limited ${cell.rateLimits}×.` : ''}`
396
+ : `No tokens on ${formatDateLabel(cell.date)}.`
397
+ return (
398
+ <Tooltip key={dayIdx}>
399
+ <TooltipTrigger
400
+ render={<div />}
401
+ className={`aspect-square w-full rounded-sm ${getIntensity(cell.tokens)} ${hasRateLimit ? 'ring-2 ring-rose-400 ring-inset' : ''}`}
402
+ />
403
+ <TooltipContent>{label}</TooltipContent>
404
+ </Tooltip>
405
+ )
406
+ })}
407
+ </div>
408
+ )
409
+ })}
410
+ </div>
411
+ {/* Legend */}
412
+ <div className="mt-2 flex items-center justify-end gap-1 text-[10px] text-muted-foreground">
413
+ <span>Less</span>
414
+ <div className="h-[10px] w-[10px] rounded-sm bg-muted" />
415
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-200 dark:bg-blue-900/50" />
416
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-300 dark:bg-blue-700/60" />
417
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-400 dark:bg-blue-600/70" />
418
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-500 dark:bg-blue-500" />
419
+ <span>More</span>
420
+ </div>
421
+ </div>
422
+ </TooltipProvider>
423
+ </CardContent>
424
+ </Card>
425
+ )
426
+ }
427
+
428
+ // ─── Export ──────────────────────────────────────────────────────────
429
+
430
+ export function DailyChart({ daily, sessions }: { daily: DailyUsage[]; sessions: SessionSummary[] }) {
431
+ return (
432
+ <>
433
+ <DailyCostChart daily={daily} />
434
+ <TopCommandsChart />
435
+ <InterruptionRateChart daily={daily} />
436
+ <UserVsAssistantChart daily={daily} />
116
437
  </>
117
438
  )
118
439
  }
@@ -3,10 +3,6 @@
3
3
  import { usePathname } from 'next/navigation'
4
4
  import { RefreshCw } from 'lucide-react'
5
5
  import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
6
- import { Separator } from '@/components/ui/separator'
7
- import { Button } from '@/components/ui/button'
8
- import { Badge } from '@/components/ui/badge'
9
- import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
10
6
  import {
11
7
  Select,
12
8
  SelectContent,
@@ -20,7 +16,7 @@ import { getPlugin } from '@/lib/plugins'
20
16
  import '@/plugins' // ensure plugins are registered
21
17
  import type { ReactNode } from 'react'
22
18
 
23
- const VIEW_TITLES: Record<string, string> = {
19
+ const VIEW_TITLES: Record<string, ReactNode> = {
24
20
  '/': 'Dashboard',
25
21
  '/daily': 'Daily Usage',
26
22
  '/tokens': 'Token Breakdown',
@@ -30,10 +26,11 @@ const VIEW_TITLES: Record<string, string> = {
30
26
  '/personality': 'Personality Fit',
31
27
  '/commands': 'Command Usage',
32
28
  '/images': 'Images',
33
- '/coach': 'Agent Coach',
29
+ '/coach': 'CRAFT Coach',
30
+ '/flow': 'Session Flow',
34
31
  }
35
32
 
36
- function resolveTitle(pathname: string): string {
33
+ function resolveTitle(pathname: string): ReactNode {
37
34
  if (VIEW_TITLES[pathname]) return VIEW_TITLES[pathname]
38
35
  if (pathname === '/community') return 'Community'
39
36
  // Community plugin routes: /community/<slug>
@@ -42,6 +39,10 @@ function resolveTitle(pathname: string): string {
42
39
  const plugin = getPlugin(match[1])
43
40
  if (plugin) return plugin.manifest.name
44
41
  }
42
+ if (pathname === '/reports') return 'Reports'
43
+ if (pathname.startsWith('/reports/')) return 'Report Detail'
44
+ if (pathname === '/settings') return 'Settings'
45
+ if (pathname.startsWith('/sessions/')) return 'Session Detail'
45
46
  return 'Dashboard'
46
47
  }
47
48
 
@@ -65,8 +66,7 @@ export function DashboardShell({ children }: { children: ReactNode }) {
65
66
  const pathname = usePathname()
66
67
  const {
67
68
  agent, setAgent, timeRange, setTimeRange,
68
- syncing, lastSyncResult, lastSyncTime, handleSync, loading,
69
- newSessionsAvailable,
69
+ loading,
70
70
  } = useData()
71
71
 
72
72
  const title = resolveTitle(pathname)
@@ -123,21 +123,6 @@ export function DashboardShell({ children }: { children: ReactNode }) {
123
123
  <SelectItem value="combined">Combined</SelectItem>
124
124
  </SelectContent>
125
125
  </Select>
126
- {lastSyncTime && lastSyncResult && lastSyncResult.sessionsAdded > 0 && (
127
- <Badge variant="secondary" className="hidden text-xs sm:inline-flex">
128
- +{lastSyncResult.sessionsAdded} new
129
- </Badge>
130
- )}
131
- <Button
132
- variant="outline"
133
- size="sm"
134
- onClick={handleSync}
135
- disabled={syncing}
136
- className="gap-2"
137
- >
138
- <RefreshCw className={`h-3.5 w-3.5 ${syncing ? 'animate-spin' : ''}`} />
139
- {syncing ? 'Syncing...' : 'Sync'}
140
- </Button>
141
126
  </div>
142
127
  </header>
143
128
  <main className="flex-1 space-y-6 p-6">