agentfit 0.1.0

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 (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
@@ -0,0 +1,294 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table'
14
+ import { Check, Circle, Terminal, Lightbulb, Sparkles, TrendingUp } from 'lucide-react'
15
+ import type { CommandAnalysis } from '@/lib/commands'
16
+
17
+ function CategoryBar({ name, used, total, invocations }: {
18
+ name: string
19
+ used: number
20
+ total: number
21
+ invocations: number
22
+ }) {
23
+ const pct = total > 0 ? Math.round((used / total) * 100) : 0
24
+ const getColor = (p: number) => {
25
+ if (p >= 75) return 'bg-chart-2'
26
+ if (p >= 50) return 'bg-chart-1'
27
+ if (p >= 25) return 'bg-chart-3'
28
+ return 'bg-chart-5'
29
+ }
30
+
31
+ return (
32
+ <div className="space-y-1.5">
33
+ <div className="flex items-center justify-between text-sm">
34
+ <span className="font-medium">{name}</span>
35
+ <span className="text-muted-foreground">
36
+ {used}/{total} commands ({invocations} uses)
37
+ </span>
38
+ </div>
39
+ <div className="h-2.5 w-full overflow-hidden rounded-full bg-muted">
40
+ <div
41
+ className={`h-full rounded-full transition-all duration-500 ${getColor(pct)}`}
42
+ style={{ width: `${Math.max(2, pct)}%` }}
43
+ />
44
+ </div>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export function CommandUsage() {
50
+ const [analysis, setAnalysis] = useState<CommandAnalysis | null>(null)
51
+ const [loading, setLoading] = useState(true)
52
+ const [filter, setFilter] = useState<'all' | 'used' | 'unused'>('all')
53
+
54
+ useEffect(() => {
55
+ async function load() {
56
+ try {
57
+ const res = await fetch('/api/commands')
58
+ const data = await res.json()
59
+ setAnalysis(data)
60
+ } catch (e) {
61
+ console.error('Failed to load command analysis:', e)
62
+ } finally {
63
+ setLoading(false)
64
+ }
65
+ }
66
+ load()
67
+ }, [])
68
+
69
+ if (loading) {
70
+ return (
71
+ <div className="flex items-center justify-center py-12 text-muted-foreground">
72
+ Loading command usage...
73
+ </div>
74
+ )
75
+ }
76
+
77
+ if (!analysis) {
78
+ return (
79
+ <div className="text-destructive py-12 text-center">
80
+ Failed to load command analysis
81
+ </div>
82
+ )
83
+ }
84
+
85
+ const filteredCommands = analysis.commands.filter(cmd => {
86
+ if (filter === 'used') return cmd.used
87
+ if (filter === 'unused') return !cmd.used
88
+ return true
89
+ })
90
+
91
+ // Pick top unused recommendations
92
+ const recommendations = analysis.commands
93
+ .filter(c => !c.used)
94
+ .filter(c => [
95
+ '/compact', '/diff', '/context', '/btw', '/branch', '/export',
96
+ '/plan', '/stats', '/insights', '/security-review', '/doctor',
97
+ '/cost', '/memory', '/hooks', '/keybindings',
98
+ ].includes(c.command))
99
+ .slice(0, 8)
100
+
101
+ return (
102
+ <div className="space-y-6">
103
+ {/* Overview cards */}
104
+ <div className="grid gap-4 sm:grid-cols-4">
105
+ <Card>
106
+ <CardContent className="pt-6">
107
+ <div className="flex items-center gap-2">
108
+ <Terminal className="h-4 w-4 text-muted-foreground" />
109
+ <span className="text-sm text-muted-foreground">Built-in Commands</span>
110
+ </div>
111
+ <div className="text-3xl font-bold mt-1">{analysis.totalCommands}</div>
112
+ </CardContent>
113
+ </Card>
114
+ <Card>
115
+ <CardContent className="pt-6">
116
+ <div className="flex items-center gap-2">
117
+ <Check className="h-4 w-4 text-chart-2" />
118
+ <span className="text-sm text-muted-foreground">Commands Used</span>
119
+ </div>
120
+ <div className="text-3xl font-bold mt-1">
121
+ {analysis.usedCommands}
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+ <Card>
126
+ <CardContent className="pt-6">
127
+ <div className="flex items-center gap-2">
128
+ <TrendingUp className="h-4 w-4 text-muted-foreground" />
129
+ <span className="text-sm text-muted-foreground">Usage Rate</span>
130
+ </div>
131
+ <div className="text-3xl font-bold mt-1">{analysis.usagePercentage}%</div>
132
+ </CardContent>
133
+ </Card>
134
+ <Card>
135
+ <CardContent className="pt-6">
136
+ <div className="flex items-center gap-2">
137
+ <Sparkles className="h-4 w-4 text-muted-foreground" />
138
+ <span className="text-sm text-muted-foreground">Total Invocations</span>
139
+ </div>
140
+ <div className="text-3xl font-bold mt-1">{analysis.totalInvocations}</div>
141
+ </CardContent>
142
+ </Card>
143
+ </div>
144
+
145
+ {/* Category breakdown */}
146
+ <Card>
147
+ <CardHeader>
148
+ <CardTitle>Coverage by Category</CardTitle>
149
+ <CardDescription>How many commands you've used in each category</CardDescription>
150
+ </CardHeader>
151
+ <CardContent className="space-y-4">
152
+ {Object.entries(analysis.categories)
153
+ .sort(([, a], [, b]) => b.invocations - a.invocations)
154
+ .map(([name, stats]) => (
155
+ <CategoryBar
156
+ key={name}
157
+ name={name}
158
+ used={stats.used}
159
+ total={stats.total}
160
+ invocations={stats.invocations}
161
+ />
162
+ ))}
163
+ </CardContent>
164
+ </Card>
165
+
166
+ {/* Recommendations */}
167
+ {recommendations.length > 0 && (
168
+ <Card>
169
+ <CardHeader>
170
+ <div className="flex items-center gap-2">
171
+ <Lightbulb className="h-5 w-5 text-chart-3" />
172
+ <div>
173
+ <CardTitle>Recommended Commands to Try</CardTitle>
174
+ <CardDescription>
175
+ Useful commands you haven't used yet
176
+ </CardDescription>
177
+ </div>
178
+ </div>
179
+ </CardHeader>
180
+ <CardContent>
181
+ <div className="grid gap-3 sm:grid-cols-2">
182
+ {recommendations.map(cmd => (
183
+ <div key={cmd.command} className="flex items-start gap-3 rounded-lg border p-3">
184
+ <code className="shrink-0 rounded bg-muted px-2 py-0.5 text-sm font-semibold text-primary">
185
+ {cmd.command}
186
+ </code>
187
+ <span className="text-sm text-muted-foreground">{cmd.description}</span>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </CardContent>
192
+ </Card>
193
+ )}
194
+
195
+ {/* Custom commands (skills/plugins) */}
196
+ {analysis.customCommands.length > 0 && (
197
+ <Card>
198
+ <CardHeader>
199
+ <CardTitle>Custom Commands (Skills & Plugins)</CardTitle>
200
+ <CardDescription>
201
+ Non-built-in commands you've used — these come from installed skills and plugins
202
+ </CardDescription>
203
+ </CardHeader>
204
+ <CardContent>
205
+ <div className="flex flex-wrap gap-2">
206
+ {analysis.customCommands.map(({ command, count }) => (
207
+ <Badge key={command} variant="secondary" className="gap-1.5 text-sm">
208
+ <code>{command}</code>
209
+ <span className="text-muted-foreground">{count}x</span>
210
+ </Badge>
211
+ ))}
212
+ </div>
213
+ </CardContent>
214
+ </Card>
215
+ )}
216
+
217
+ {/* Full command table */}
218
+ <Card>
219
+ <CardHeader>
220
+ <div className="flex items-center justify-between">
221
+ <div>
222
+ <CardTitle>All Commands</CardTitle>
223
+ <CardDescription>
224
+ Complete list of built-in Claude Code commands
225
+ </CardDescription>
226
+ </div>
227
+ <div className="flex gap-1.5">
228
+ {(['all', 'used', 'unused'] as const).map(f => (
229
+ <button
230
+ key={f}
231
+ onClick={() => setFilter(f)}
232
+ className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
233
+ filter === f
234
+ ? 'bg-primary text-primary-foreground'
235
+ : 'bg-muted text-muted-foreground hover:bg-muted/80'
236
+ }`}
237
+ >
238
+ {f === 'all' ? `All (${analysis.totalCommands})` :
239
+ f === 'used' ? `Used (${analysis.usedCommands})` :
240
+ `Unused (${analysis.unusedCommands})`}
241
+ </button>
242
+ ))}
243
+ </div>
244
+ </div>
245
+ </CardHeader>
246
+ <CardContent>
247
+ <Table>
248
+ <TableHeader>
249
+ <TableRow>
250
+ <TableHead className="w-8"></TableHead>
251
+ <TableHead>Command</TableHead>
252
+ <TableHead>Description</TableHead>
253
+ <TableHead>Category</TableHead>
254
+ <TableHead className="text-right">Uses</TableHead>
255
+ </TableRow>
256
+ </TableHeader>
257
+ <TableBody>
258
+ {filteredCommands.map(cmd => (
259
+ <TableRow key={cmd.command} className={cmd.used ? '' : 'opacity-60'}>
260
+ <TableCell>
261
+ {cmd.used ? (
262
+ <Check className="h-4 w-4 text-chart-2" />
263
+ ) : (
264
+ <Circle className="h-4 w-4 text-muted-foreground/40" />
265
+ )}
266
+ </TableCell>
267
+ <TableCell>
268
+ <div className="flex items-center gap-2">
269
+ <code className="text-sm font-semibold">{cmd.command}</code>
270
+ {cmd.aliases && cmd.aliases.length > 0 && (
271
+ <span className="text-xs text-muted-foreground">
272
+ ({cmd.aliases.join(', ')})
273
+ </span>
274
+ )}
275
+ </div>
276
+ </TableCell>
277
+ <TableCell className="text-sm text-muted-foreground">
278
+ {cmd.description}
279
+ </TableCell>
280
+ <TableCell>
281
+ <Badge variant="outline" className="text-xs">{cmd.category}</Badge>
282
+ </TableCell>
283
+ <TableCell className="text-right font-mono">
284
+ {cmd.count > 0 ? cmd.count : '–'}
285
+ </TableCell>
286
+ </TableRow>
287
+ ))}
288
+ </TableBody>
289
+ </Table>
290
+ </CardContent>
291
+ </Card>
292
+ </div>
293
+ )
294
+ }
@@ -0,0 +1,118 @@
1
+ 'use client'
2
+
3
+ import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
+ import {
6
+ ChartContainer,
7
+ ChartTooltip,
8
+ ChartTooltipContent,
9
+ type ChartConfig,
10
+ } from '@/components/ui/chart'
11
+ import type { DailyUsage } from '@/lib/parse-logs'
12
+ import { formatCost } from '@/lib/format'
13
+
14
+ const costConfig = {
15
+ cost: { label: 'Cost', color: 'var(--chart-5)' },
16
+ } satisfies ChartConfig
17
+
18
+ const sessionsConfig = {
19
+ sessions: { label: 'Sessions', color: 'var(--chart-2)' },
20
+ } satisfies ChartConfig
21
+
22
+ const messagesConfig = {
23
+ messages: { label: 'Messages', color: 'var(--chart-1)' },
24
+ } satisfies ChartConfig
25
+
26
+ export function DailyChart({ daily }: { daily: DailyUsage[] }) {
27
+ const data = daily.map((d) => ({
28
+ date: d.date.slice(5),
29
+ cost: Number(d.costUSD.toFixed(2)),
30
+ sessions: d.sessions,
31
+ messages: d.messages,
32
+ }))
33
+
34
+ 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>
66
+
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>
91
+
92
+ <Card>
93
+ <CardHeader>
94
+ <CardTitle>Daily Messages</CardTitle>
95
+ <CardDescription>Messages exchanged per day</CardDescription>
96
+ </CardHeader>
97
+ <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>
114
+ </CardContent>
115
+ </Card>
116
+ </>
117
+ )
118
+ }
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@/components/ui/table'
12
+ import { PaginationControls } from '@/components/pagination-controls'
13
+ import { usePagination } from '@/hooks/use-pagination'
14
+ import type { DailyUsage, SessionSummary } from '@/lib/parse-logs'
15
+ import { formatCost, formatTokens, formatNumber } from '@/lib/format'
16
+
17
+ interface DailyTableProps {
18
+ daily: DailyUsage[]
19
+ sessions: SessionSummary[]
20
+ }
21
+
22
+ export function DailyTable({ daily, sessions }: DailyTableProps) {
23
+ // Build models-per-day from sessions
24
+ const modelsByDate = new Map<string, Set<string>>()
25
+ for (const s of sessions) {
26
+ const date = s.startTime.slice(0, 10)
27
+ if (!modelsByDate.has(date)) modelsByDate.set(date, new Set())
28
+ modelsByDate.get(date)!.add(s.model)
29
+ }
30
+
31
+ // Totals
32
+ const totals = daily.reduce(
33
+ (acc, d) => ({
34
+ sessions: acc.sessions + d.sessions,
35
+ messages: acc.messages + d.messages,
36
+ inputTokens: acc.inputTokens + d.inputTokens,
37
+ outputTokens: acc.outputTokens + d.outputTokens,
38
+ cacheCreationTokens: acc.cacheCreationTokens + d.cacheCreationTokens,
39
+ cacheReadTokens: acc.cacheReadTokens + d.cacheReadTokens,
40
+ totalTokens: acc.totalTokens + d.totalTokens,
41
+ costUSD: acc.costUSD + d.costUSD,
42
+ }),
43
+ {
44
+ sessions: 0, messages: 0, inputTokens: 0, outputTokens: 0,
45
+ cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 0, costUSD: 0,
46
+ }
47
+ )
48
+
49
+ // Sort daily by date descending (most recent first)
50
+ const sorted = [...daily].sort((a, b) => b.date.localeCompare(a.date))
51
+ const pagination = usePagination(sorted, 20)
52
+
53
+ return (
54
+ <Card>
55
+ <CardHeader>
56
+ <CardTitle>Daily Breakdown</CardTitle>
57
+ <CardDescription>{daily.length} days of activity</CardDescription>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <Table>
61
+ <TableHeader>
62
+ <TableRow>
63
+ <TableHead>Date</TableHead>
64
+ <TableHead>Models</TableHead>
65
+ <TableHead className="text-right">Sessions</TableHead>
66
+ <TableHead className="text-right">Input</TableHead>
67
+ <TableHead className="text-right">Output</TableHead>
68
+ <TableHead className="text-right">Cache Create</TableHead>
69
+ <TableHead className="text-right">Cache Read</TableHead>
70
+ <TableHead className="text-right">Total Tokens</TableHead>
71
+ <TableHead className="text-right">Cost</TableHead>
72
+ </TableRow>
73
+ </TableHeader>
74
+ <TableBody>
75
+ {pagination.pageItems.map((d) => {
76
+ const models = modelsByDate.get(d.date)
77
+ const modelList = models ? Array.from(models).sort() : []
78
+ return (
79
+ <TableRow key={d.date}>
80
+ <TableCell className="font-medium whitespace-nowrap">{d.date}</TableCell>
81
+ <TableCell className="text-xs text-muted-foreground max-w-[180px]">
82
+ {modelList.map((m) => (
83
+ <div key={m}>{m}</div>
84
+ ))}
85
+ </TableCell>
86
+ <TableCell className="text-right">{d.sessions}</TableCell>
87
+ <TableCell className="text-right font-mono text-xs">{formatNumber(d.inputTokens)}</TableCell>
88
+ <TableCell className="text-right font-mono text-xs">{formatNumber(d.outputTokens)}</TableCell>
89
+ <TableCell className="text-right font-mono text-xs">{formatNumber(d.cacheCreationTokens)}</TableCell>
90
+ <TableCell className="text-right font-mono text-xs">{formatNumber(d.cacheReadTokens)}</TableCell>
91
+ <TableCell className="text-right font-mono text-xs">{formatNumber(d.totalTokens)}</TableCell>
92
+ <TableCell className="text-right font-medium">{formatCost(d.costUSD)}</TableCell>
93
+ </TableRow>
94
+ )
95
+ })}
96
+ </TableBody>
97
+ <TableBody>
98
+ <TableRow className="bg-muted/50 font-bold">
99
+ <TableCell>Total</TableCell>
100
+ <TableCell />
101
+ <TableCell className="text-right">{totals.sessions}</TableCell>
102
+ <TableCell className="text-right font-mono text-xs">{formatTokens(totals.inputTokens)}</TableCell>
103
+ <TableCell className="text-right font-mono text-xs">{formatTokens(totals.outputTokens)}</TableCell>
104
+ <TableCell className="text-right font-mono text-xs">{formatTokens(totals.cacheCreationTokens)}</TableCell>
105
+ <TableCell className="text-right font-mono text-xs">{formatTokens(totals.cacheReadTokens)}</TableCell>
106
+ <TableCell className="text-right font-mono text-xs">{formatTokens(totals.totalTokens)}</TableCell>
107
+ <TableCell className="text-right">{formatCost(totals.costUSD)}</TableCell>
108
+ </TableRow>
109
+ </TableBody>
110
+ </Table>
111
+ <PaginationControls pagination={pagination} noun="days" />
112
+ </CardContent>
113
+ </Card>
114
+ )
115
+ }
@@ -0,0 +1,149 @@
1
+ 'use client'
2
+
3
+ import { usePathname } from 'next/navigation'
4
+ import { RefreshCw } from 'lucide-react'
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
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '@/components/ui/select'
17
+ import { AppSidebar } from './app-sidebar'
18
+ import { useData, type TimeRange } from './data-provider'
19
+ import { getPlugin } from '@/lib/plugins'
20
+ import '@/plugins' // ensure plugins are registered
21
+ import type { ReactNode } from 'react'
22
+
23
+ const VIEW_TITLES: Record<string, string> = {
24
+ '/': 'Dashboard',
25
+ '/daily': 'Daily Usage',
26
+ '/tokens': 'Token Breakdown',
27
+ '/tools': 'Tool Usage',
28
+ '/projects': 'Projects',
29
+ '/sessions': 'Sessions',
30
+ '/personality': 'Personality Fit',
31
+ '/commands': 'Command Usage',
32
+ '/images': 'Images',
33
+ '/coach': 'Agent Coach',
34
+ }
35
+
36
+ function resolveTitle(pathname: string): string {
37
+ if (VIEW_TITLES[pathname]) return VIEW_TITLES[pathname]
38
+ if (pathname === '/community') return 'Community'
39
+ // Community plugin routes: /community/<slug>
40
+ const match = pathname.match(/^\/community\/([a-z0-9-]+)$/)
41
+ if (match) {
42
+ const plugin = getPlugin(match[1])
43
+ if (plugin) return plugin.manifest.name
44
+ }
45
+ return 'Dashboard'
46
+ }
47
+
48
+ const AGENT_LABELS: Record<string, string> = {
49
+ claude: 'Claude Code',
50
+ codex: 'Codex',
51
+ combined: 'Combined',
52
+ }
53
+
54
+ const TIME_RANGE_LABELS: Record<TimeRange, string> = {
55
+ '7d': 'Last 7 days',
56
+ '30d': 'Last 30 days',
57
+ '90d': 'Last 90 days',
58
+ 'all': 'All time',
59
+ }
60
+
61
+ // Pages where time range filter doesn't apply
62
+ const NO_TIME_FILTER = new Set(['/commands'])
63
+
64
+ export function DashboardShell({ children }: { children: ReactNode }) {
65
+ const pathname = usePathname()
66
+ const {
67
+ agent, setAgent, timeRange, setTimeRange,
68
+ syncing, lastSyncResult, lastSyncTime, handleSync, loading,
69
+ newSessionsAvailable,
70
+ } = useData()
71
+
72
+ const title = resolveTitle(pathname)
73
+ const communityMatch = pathname.match(/^\/community\/([a-z0-9-]+)$/)
74
+ const communityPlugin = communityMatch ? getPlugin(communityMatch[1]) : undefined
75
+ const showTimeFilter =
76
+ !NO_TIME_FILTER.has(pathname) && !communityPlugin?.manifest.customDataSource
77
+
78
+ if (loading) {
79
+ return (
80
+ <div className="flex min-h-screen items-center justify-center">
81
+ <div className="flex items-center gap-2 text-muted-foreground">
82
+ <RefreshCw className="h-4 w-4 animate-spin" />
83
+ Syncing logs...
84
+ </div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ return (
90
+ <SidebarProvider>
91
+ <AppSidebar />
92
+ <SidebarInset>
93
+ <header className="flex h-14 shrink-0 items-center justify-between border-b px-4">
94
+ <div className="flex items-center gap-2">
95
+ <SidebarTrigger className="-ml-1" />
96
+ <h1 className="ml-2 text-sm font-semibold">{title}</h1>
97
+ </div>
98
+ <div className="flex items-center gap-2">
99
+ {showTimeFilter && (
100
+ <div className="flex rounded-lg border p-0.5">
101
+ {(Object.entries(TIME_RANGE_LABELS) as [TimeRange, string][]).map(([key, label]) => (
102
+ <button
103
+ key={key}
104
+ onClick={() => setTimeRange(key)}
105
+ className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
106
+ timeRange === key
107
+ ? 'bg-primary text-primary-foreground'
108
+ : 'text-muted-foreground hover:text-foreground'
109
+ }`}
110
+ >
111
+ {key === 'all' ? 'All' : key}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ )}
116
+ <Select value={agent} onValueChange={(v) => v && setAgent(v as 'claude' | 'codex' | 'combined')}>
117
+ <SelectTrigger className="h-8 w-[130px] text-xs">
118
+ <SelectValue>{AGENT_LABELS[agent]}</SelectValue>
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ <SelectItem value="claude">Claude Code</SelectItem>
122
+ <SelectItem value="codex">Codex</SelectItem>
123
+ <SelectItem value="combined">Combined</SelectItem>
124
+ </SelectContent>
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
+ </div>
142
+ </header>
143
+ <main className="flex-1 space-y-6 p-6">
144
+ {children}
145
+ </main>
146
+ </SidebarInset>
147
+ </SidebarProvider>
148
+ )
149
+ }